reimplemented naming strategies and added few more tests for connection

This commit is contained in:
Umed Khudoiberdiev 2016-06-07 02:03:42 +05:00
parent 87c47e50da
commit 2fd70e00b7
17 changed files with 222 additions and 142 deletions

View File

@ -11,9 +11,9 @@ const options: CreateConnectionOptions = {
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true,
namingStrategy: "custom_strategy"
autoSchemaCreate: true
},
usedNamingStrategy: "custom_strategy",
entities: [Post],
namingStrategies: [CustomNamingStrategy]
};

View File

@ -6,16 +6,12 @@ import {DefaultNamingStrategy} from "../../../src/naming-strategy/DefaultNamingS
@NamingStrategy("custom_strategy")
export class CustomNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface {
tableName(className: string): string {
return _.snakeCase(className);
tableName(className: string, customName: string): string {
return customName ? customName : _.snakeCase(className);
}
tableNameCustomized(customName: string): string {
return customName;
}
columnName(propertyName: string): string {
return _.snakeCase(propertyName);
columnName(propertyName: string, customName: string): string {
return customName ? customName : _.snakeCase(propertyName);
}
columnNameCustomized(customName: string): string {

View File

@ -53,6 +53,12 @@ export class ConnectionManager {
if (options.namingStrategies)
connection.importNamingStrategies(options.namingStrategies);
if (options.usedNamingStrategy && typeof options.usedNamingStrategy === "string")
connection.useNamingStrategy(options.usedNamingStrategy);
if (options.usedNamingStrategy && options.usedNamingStrategy instanceof Function)
connection.useNamingStrategy(options.usedNamingStrategy);
return connection;
}

View File

@ -21,6 +21,11 @@ export interface CreateConnectionOptions {
*/
connectionName?: string;
/**
* Name of the naming strategy or target class of the naming strategy to be used on this connection.
*/
usedNamingStrategy?: string|Function;
/**
* Entities to be loaded for the new connection.
*/

View File

@ -12,7 +12,7 @@ import {getMetadataArgsStorage, getFromContainer} from "../index";
import {EntityMetadataBuilder} from "../metadata-builder/EntityMetadataBuilder";
import {DefaultNamingStrategy} from "../naming-strategy/DefaultNamingStrategy";
import {EntityMetadataCollection} from "../metadata-args/collection/EntityMetadataCollection";
import {NamingStrategyMetadata} from "../metadata/NamingStrategyMetadata";
// import {NamingStrategyMetadata} from "../metadata/NamingStrategyMetadata";
import {NoConnectionForRepositoryError} from "./error/NoConnectionForRepositoryError";
import {CannotImportAlreadyConnectedError} from "./error/CannotImportAlreadyConnectedError";
import {CannotCloseNotConnectedError} from "./error/CannotCloseNotConnectedError";
@ -30,6 +30,7 @@ import {ReactiveRepositoryNotFoundError} from "./error/ReactiveRepositoryNotFoun
import {RepositoryNotTreeError} from "./error/RepositoryNotTreeError";
import {EntitySchema} from "../metadata/entity-schema/EntitySchema";
import {CannotSyncNotConnectedError} from "./error/CannotSyncNotConnectedError";
import {CannotUseNamingStrategyNotConnectedError} from "./error/CannotUseNamingStrategyNotConnectedError";
/**
* A single connection instance to the database. Each connection has its own repositories, subscribers and metadatas.
@ -101,7 +102,7 @@ export class Connection {
/**
* All naming strategy metadatas that are registered for this connection.
*/
private readonly namingStrategyMetadatas: NamingStrategyMetadata[] = [];
// private readonly namingStrategyMetadatas: NamingStrategyMetadata[] = [];
/**
* Registered entity classes to be used for this connection.
@ -123,6 +124,11 @@ export class Connection {
*/
private readonly namingStrategyClasses: Function[] = [];
/**
* Naming strategy to be used in this connection.
*/
private usedNamingStrategy: Function|string;
/**
* Indicates if connection has been done or not.
*/
@ -282,6 +288,34 @@ export class Connection {
return this;
}
/**
* Sets given naming strategy to be used. Naming strategy must be set to be used before connection is established.
*/
useNamingStrategy(name: string): this;
/**
* Sets given naming strategy to be used. Naming strategy must be set to be used before connection is established.
*/
useNamingStrategy(strategy: Function): this;
/**
* Sets given naming strategy to be used. Naming strategy must be set to be used before connection is established.
*/
useNamingStrategy(strategyClassOrName: string|Function): this {
if (this.isConnected)
throw new CannotUseNamingStrategyNotConnectedError(this.name);
this.usedNamingStrategy = strategyClassOrName;
return this;
}
/**
* Gets the entity metadata of the given entity target.
*/
getMetadata(entity: Function) {
return this.entityMetadatas.findByTarget(entity);
}
/**
* Gets repository for the given entity class.
*/
@ -303,6 +337,9 @@ export class Connection {
if (!this.isConnected)
throw new NoConnectionForRepositoryError(this.name);
if (!this.entityMetadatas.hasTarget(entityClassOrName))
throw new RepositoryNotFoundError(this.name, entityClassOrName);
const metadata = this.entityMetadatas.findByTarget(entityClassOrName);
const repository = this.repositories.find(repository => Repository.ownsMetadata(repository, metadata));
if (!repository)
@ -379,14 +416,8 @@ export class Connection {
* Builds all registered metadatas.
*/
private buildMetadatas() {
// take imported naming strategy metadatas
if (this.namingStrategyClasses && this.namingStrategyClasses.length) {
getMetadataArgsStorage()
.namingStrategies
.filterByTargets(this.namingStrategyClasses)
.forEach(metadata => this.namingStrategyMetadatas.push(new NamingStrategyMetadata(metadata)));
}
const namingStrategy = this.createNamingStrategy();
// take imported event subscribers
if (this.subscriberClasses && this.subscriberClasses.length) {
@ -408,7 +439,7 @@ export class Connection {
// build entity metadatas from metadata args storage (collected from decorators)
if (this.entityClasses && this.entityClasses.length) {
getFromContainer(EntityMetadataBuilder)
.buildFromMetadataArgsStorage(this.createNamingStrategy(), this.entityClasses)
.buildFromMetadataArgsStorage(namingStrategy, this.entityClasses)
.forEach(metadata => {
this.entityMetadatas.push(metadata);
this.createRepository(metadata);
@ -418,27 +449,39 @@ export class Connection {
// build entity metadatas from given entity schemas
if (this.entitySchemas && this.entitySchemas.length) {
getFromContainer(EntityMetadataBuilder)
.buildFromSchemas(this.createNamingStrategy(), this.entitySchemas)
.buildFromSchemas(namingStrategy, this.entitySchemas)
.forEach(metadata => {
this.entityMetadatas.push(metadata);
this.createRepository(metadata);
});
}
}
/**
* Creates a naming strategy to be used for this connection.
*/
private createNamingStrategy(): NamingStrategyInterface {
if (!this.options.namingStrategy)
return getFromContainer(DefaultNamingStrategy);
if (this.namingStrategyClasses && this.namingStrategyClasses.length && this.usedNamingStrategy) {
const metadatas = getMetadataArgsStorage()
.namingStrategies
.filterByTargets(this.namingStrategyClasses);
const namingMetadata = this.namingStrategyMetadatas.find(strategy => strategy.name === this.options.namingStrategy);
if (!namingMetadata)
throw new NamingStrategyNotFoundError(this.options.namingStrategy, this.name);
if (typeof this.usedNamingStrategy === "string") {
const namingMetadata = metadatas.find(strategy => strategy.name === this.usedNamingStrategy);
if (!namingMetadata)
throw new NamingStrategyNotFoundError(this.usedNamingStrategy, this.name);
return getFromContainer<NamingStrategyInterface>(namingMetadata.target);
return getFromContainer<NamingStrategyInterface>(namingMetadata.target);
} else {
const namingMetadata = metadatas.find(strategy => strategy.target === this.usedNamingStrategy);
if (!namingMetadata)
throw new NamingStrategyNotFoundError(this.usedNamingStrategy, this.name);
return getFromContainer<NamingStrategyInterface>(namingMetadata.target);
}
}
return getFromContainer(DefaultNamingStrategy);
}
/**

View File

@ -38,11 +38,6 @@ export interface ConnectionOptions {
*/
autoSchemaCreate?: boolean;
/**
* Name of the naming strategy to be used on this connection.
*/
namingStrategy?: string;
/**
* Logging options.
*/

View File

@ -0,0 +1,12 @@
/**
* @internal
*/
export class CannotUseNamingStrategyNotConnectedError extends Error {
name = "CannotUseNamingStrategyNotConnectedError";
constructor(connectionName: string) {
super();
this.message = `Cannot use a given naming strategy for "${connectionName}" connection because connection to the database already established.`;
}
}

View File

@ -4,9 +4,10 @@
export class NamingStrategyNotFoundError extends Error {
name = "NamingStrategyNotFoundError";
constructor(strategyName: string, connectionName: string) {
constructor(strategyName: string|Function, connectionName: string) {
super();
this.message = `Naming strategy named "${strategyName}" was not found. Looks like this naming strategy does not ` +
const name = strategyName instanceof Function ? (strategyName as any).name : strategyName;
this.message = `Naming strategy "${name}" was not found. Looks like this naming strategy does not ` +
`exist or it was not registered in current "${connectionName}" connection?`;
}

View File

@ -161,20 +161,12 @@ export class ColumnMetadata extends PropertyMetadata {
get name(): string {
// if this column is embedded's column then apply different entity
if (this.embeddedMetadata) {
if (this._name)
return this.embeddedMetadata.entityMetadata.namingStrategy.embeddedColumnNameCustomized(this.embeddedMetadata.propertyName, this._name);
if (this.embeddedMetadata)
return this.embeddedMetadata.entityMetadata.namingStrategy.embeddedColumnName(this.embeddedMetadata.propertyName, this.propertyName, this._name);
return this.embeddedMetadata.entityMetadata.namingStrategy.embeddedColumnName(this.embeddedMetadata.propertyName, this.propertyName);
}
if (this.entityMetadata) {
if (this._name)
return this.entityMetadata.namingStrategy.columnNameCustomized(this._name);
// if there is a naming strategy then use it to normalize propertyName as column name
return this.entityMetadata.namingStrategy.columnName(this.propertyName);
}
// if there is a naming strategy then use it to normalize propertyName as column name
if (this.entityMetadata)
return this.entityMetadata.namingStrategy.columnName(this.propertyName, this._name);
throw new Error(`Column${this._name ? this._name + " " : ""} is not attached to any entity or embedded.`);
}

View File

@ -1,32 +0,0 @@
import {TargetMetadata} from "./TargetMetadata";
import {NamingStrategyMetadataArgs} from "../metadata-args/NamingStrategyMetadataArgs";
/**
* This metadata interface contains all information about naming strategy.
*/
export class NamingStrategyMetadata {
// ---------------------------------------------------------------------
// Readonly Properties
// ---------------------------------------------------------------------
/**
* Target class to which metadata is applied.
*/
readonly target: Function;
/**
* Naming strategy name.
*/
readonly name: string;
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(args: NamingStrategyMetadataArgs) {
this.target = args.target;
this.name = args.name;
}
}

View File

@ -68,19 +68,10 @@ export class TableMetadata extends TargetMetadata {
if (this.isClosureJunction && this._name)
return this.entityMetadata.namingStrategy.closureJunctionTableName(this._name);
// if custom name is given then use it
if (this._name)
return this.entityMetadata.namingStrategy.tableNameCustomized(this._name);
// otherwise use target's table name
if (this.target) {
const name = typeof this.target === "string" ? this.target : (this.target as any).name;
return this.entityMetadata.namingStrategy.tableName(name);
}
// in the case if error
throw new Error("Table does not have neither table name neither target specified.");
// otherwise use target's table name
const name = this.target instanceof Function ? (this.target as any).name : this.target;
return this.entityMetadata.namingStrategy.tableName(name, this._name);
}
/**

View File

@ -6,38 +6,22 @@ import * as _ from "lodash";
*/
export class DefaultNamingStrategy implements NamingStrategyInterface {
tableName(className: string): string {
return _.snakeCase(className);
tableName(className: string, customName: string): string {
return customName ? customName : _.snakeCase(className);
}
tableNameCustomized(customName: string): string {
return customName;
columnName(propertyName: string, customName: string): string {
return customName ? customName : propertyName;
}
columnName(propertyName: string): string {
return propertyName;
}
columnNameCustomized(customName: string): string {
return customName;
}
embeddedColumnName(embeddedPropertyName: string, columnPropertyName: string): string {
return embeddedPropertyName + "_" + columnPropertyName;
}
embeddedColumnNameCustomized(embeddedPropertyName: string, columnCustomName: string): string {
return embeddedPropertyName + "_" + columnCustomName;
embeddedColumnName(embeddedPropertyName: string, columnPropertyName: string, columnCustomName?: string): string {
return embeddedPropertyName + "_" + (columnCustomName ? columnCustomName : columnPropertyName);
}
relationName(propertyName: string): string {
return propertyName;
}
relationNameCustomized(customName: string): string {
return customName;
}
indexName(name: string|undefined, columns: string[]): string {
if (name)
return name;

View File

@ -5,45 +5,30 @@
export interface NamingStrategyInterface {
/**
* Gets the table name from the given class name.
* Naming strategy name.
*/
tableName(className: string): string;
name?: string;
/**
* Gets the table name from the given custom table name.
* Gets the table name from the given class name.
*/
tableNameCustomized(customName: string): string;
tableName(className: string, customName?: string): string;
/**
* Gets the table's column name from the given property name.
*/
columnName(propertyName: string): string;
/**
* Gets the column name from the given custom column name.
*/
columnNameCustomized(customName: string): string;
columnName(propertyName: string, customName?: string): string;
/**
* Gets the embedded's column name from the given property name.
*/
embeddedColumnName(embeddedPropertyName: string, columnPropertyName: string): string;
/**
* Gets the embedded's column name from the given custom column name.
*/
embeddedColumnNameCustomized(embeddedPropertyName: string, columnCustomName: string): string;
embeddedColumnName(embeddedPropertyName: string, columnPropertyName: string, columnCustomName?: string): string;
/**
* Gets the table's relation name from the given property name.
*/
relationName(propertyName: string): string;
/**
* Gets the relation name from the given custom relation name.
*/
relationNameCustomized(customName: string): string;
/**
* Gets the name of the index - simple and compose index.
*/

View File

@ -14,6 +14,11 @@ import {ReactiveRepository} from "../../../src/repository/ReactiveRepository";
import {ReactiveTreeRepository} from "../../../src/repository/ReactiveTreeRepository";
import {getConnectionManager} from "../../../src/index";
import {CreateConnectionOptions} from "../../../src/connection-manager/CreateConnectionOptions";
import {CannotSyncNotConnectedError} from "../../../src/connection/error/CannotSyncNotConnectedError";
import {NoConnectionForRepositoryError} from "../../../src/connection/error/NoConnectionForRepositoryError";
import {RepositoryNotFoundError} from "../../../src/connection/error/RepositoryNotFoundError";
import {FirstCustomNamingStrategy} from "./naming-strategy/FirstCustomNamingStrategy";
import {SecondCustomNamingStrategy} from "./naming-strategy/SecondCustomNamingStrategy";
chai.should();
chai.use(require("sinon-chai"));
@ -32,12 +37,18 @@ describe("Connection", () => {
};
connection = await getConnectionManager().create(options);
});
after(() => {
if (connection.isConnected)
return connection.close();
return Promise.resolve();
});
it("connection.isConnected should be false", () => {
connection.isConnected.should.be.false;
});
it("import entities, entity schemas, subscribers and naming strategies should work", () => () => {
it("import entities, entity schemas, subscribers and naming strategies should work", () => {
connection.importEntities([Post]).should.be.fulfilled;
connection.importSchemas([]).should.be.fulfilled;
connection.importSubscribers([]).should.be.fulfilled;
@ -48,12 +59,24 @@ describe("Connection", () => {
connection.importNamingStrategiesFromDirectories([]).should.be.fulfilled;
});
it("should not be able to connect", () => () => {
connection.connect().should.be.fulfilled;
it("should not be able to close", () => {
connection.close().should.be.rejectedWith(CannotCloseNotConnectedError);
});
it("should be able to close a connection", () => () => {
connection.close().should.be.fulfilled;
it("should not be able to sync a schema", () => {
connection.syncSchema().should.be.rejectedWith(CannotSyncNotConnectedError);
});
it("should not be able to use repositories", () => {
expect(() => connection.getRepository(Post)).to.throw(NoConnectionForRepositoryError);
expect(() => connection.getTreeRepository(Category)).to.throw(NoConnectionForRepositoryError);
expect(() => connection.getReactiveRepository(Post)).to.throw(NoConnectionForRepositoryError);
expect(() => connection.getReactiveTreeRepository(Category)).to.throw(NoConnectionForRepositoryError);
});
it("should be able to connect", () => {
// connection.connect().should.eventually.
return connection.connect().should.be.fulfilled;
});
});
@ -158,5 +181,60 @@ describe("Connection", () => {
}));
});
describe("import entities / entity schemas / subscribers / naming strategies", function() {
let firstConnection: Connection, secondConnection: Connection;
beforeEach(async () => {
const firstOptions: CreateConnectionOptions = {
driver: "mysql",
connection: createTestingConnectionOptions("mysql")
};
const secondOptions: CreateConnectionOptions = {
driver: "mysql",
connection: createTestingConnectionOptions("mysql")
};
firstConnection = await getConnectionManager().create(firstOptions);
secondConnection = await getConnectionManager().create(secondOptions);
});
it("should import first connection's entities only", async () => {
firstConnection.importEntities([Post]);
await firstConnection.connect();
firstConnection.getRepository(Post).should.be.instanceOf(Repository);
firstConnection.getRepository(Post).target.should.be.equal(Post);
expect(() => firstConnection.getRepository(Category)).to.throw(RepositoryNotFoundError);
firstConnection.close();
});
it("should import second connection's entities only", async () => {
secondConnection.importEntities([Category]);
await secondConnection.connect();
secondConnection.getRepository(Category).should.be.instanceOf(Repository);
secondConnection.getRepository(Category).target.should.be.equal(Category);
expect(() => secondConnection.getRepository(Post)).to.throw(RepositoryNotFoundError);
secondConnection.close();
});
it("should import first connection's naming strategies only", async () => {
firstConnection.importEntities([Post]);
firstConnection.importNamingStrategies([FirstCustomNamingStrategy]);
firstConnection.useNamingStrategy(FirstCustomNamingStrategy);
await firstConnection.connect();
firstConnection.getMetadata(Post).table.name.should.be.equal("POST");
firstConnection.close();
});
it("should import second connection's entities only", async () => {
secondConnection.importEntities([Category]);
secondConnection.importNamingStrategies([SecondCustomNamingStrategy]);
secondConnection.useNamingStrategy("secondCustomNamingStrategy");
await secondConnection.connect();
secondConnection.getMetadata(Category).table.name.should.be.equal("category");
secondConnection.close();
});
});
});

View File

@ -5,7 +5,7 @@ import {TreeParent} from "../../../../src/decorator/tree/TreeParent";
import {TreeChildren} from "../../../../src/decorator/tree/TreeChildren";
import {TreeLevelColumn} from "../../../../src/decorator/tree/TreeLevelColumn";
@ClosureTable("category")
@ClosureTable("CaTeGoRy")
export class Category {
@PrimaryColumn("int", { generated: true })

View File

@ -0,0 +1,12 @@
import {DefaultNamingStrategy} from "../../../../src/naming-strategy/DefaultNamingStrategy";
import {NamingStrategy} from "../../../../src/decorator/NamingStrategy";
import {NamingStrategyInterface} from "../../../../src/naming-strategy/NamingStrategyInterface";
@NamingStrategy("firstCustomNamingStrategy")
export class FirstCustomNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface {
tableName(className: string, customName: string): string {
return customName ? customName.toUpperCase() : className.toUpperCase();
}
}

View File

@ -0,0 +1,12 @@
import {DefaultNamingStrategy} from "../../../../src/naming-strategy/DefaultNamingStrategy";
import {NamingStrategy} from "../../../../src/decorator/NamingStrategy";
import {NamingStrategyInterface} from "../../../../src/naming-strategy/NamingStrategyInterface";
@NamingStrategy("secondCustomNamingStrategy")
export class SecondCustomNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface {
tableName(className: string, customName: string): string {
return customName ? customName.toLowerCase() : className.toLowerCase();
}
}