added build from entity schemas - first step

This commit is contained in:
Umed Khudoiberdiev 2016-06-02 23:36:53 +05:00
parent fa252fa68b
commit 0c20445d83
15 changed files with 423 additions and 62 deletions

View File

@ -29,6 +29,12 @@ export class ConnectionManager {
const driver = this.createDriver(options.driver);
const connection = this.createConnection(options.connectionName || "default", driver, options.connection);
if (options.entitySchemaDirectories && options.entitySchemaDirectories.length > 0)
connection.importEntitySchemaFromDirectories(options.entitySchemaDirectories);
if (options.entitySchemas)
connection.importEntities(options.entitySchemas);
if (options.entityDirectories && options.entityDirectories.length > 0)
connection.importEntitiesFromDirectories(options.entityDirectories);

View File

@ -28,6 +28,7 @@ import {RepositoryFactory} from "../repository/RepositoryFactory";
import {SchemaCreatorFactory} from "../schema-creator/SchemaCreatorFactory";
import {ReactiveRepositoryNotFoundError} from "./error/ReactiveRepositoryNotFoundError";
import {RepositoryNotTreeError} from "./error/RepositoryNotTreeError";
import {EntitySchema} from "../metadata/entity-schema/EntitySchema";
/**
* A single connection instance to the database. Each connection has its own repositories, subscribers and metadatas.
@ -106,6 +107,11 @@ export class Connection {
*/
private readonly entityClasses: Function[] = [];
/**
* Registered entity schemas to be used for this connection.
*/
private readonly entitySchemas: EntitySchema[] = [];
/**
* Registered subscriber classes to be used for this connection.
*/
@ -202,6 +208,14 @@ export class Connection {
return this;
}
/**
* Imports entity schemas from the given paths (directories) for the current connection.
*/
importEntitySchemaFromDirectories(paths: string[]): this {
this.importSchemas(importClassesFromDirectories(paths) as any[]); // todo: check it.
return this;
}
/**
* Imports subscribers from the given paths (directories) for the current connection.
*/
@ -229,6 +243,17 @@ export class Connection {
return this;
}
/**
* Imports schemas for the current connection.
*/
importSchemas(schemas: EntitySchema[]): this {
if (this.isConnected)
throw new CannotImportAlreadyConnectedError("schemas", this.name);
schemas.forEach(schema => this.entitySchemas.push(schema));
return this;
}
/**
* Imports entities for the given connection. If connection name is not given then default connection is used.
*/
@ -365,13 +390,26 @@ export class Connection {
.filterByClasses(this.entityClasses)
.forEach(metadata => this.entityListeners.push(new EntityListenerMetadata(metadata)));
// build entity metadatas for the current connection
getFromContainer(EntityMetadataBuilder)
.build(this.createNamingStrategy(), this.entityClasses)
.forEach(layout => {
this.entityMetadatas.push(layout);
this.createRepository(layout);
});
// build entity metadatas from metadata args storage (collected from decorators)
if (this.entityClasses) {
getFromContainer(EntityMetadataBuilder)
.buildFromMetadataArgsStorage(this.createNamingStrategy(), this.entityClasses)
.forEach(metadata => {
this.entityMetadatas.push(metadata);
this.createRepository(metadata);
});
}
// build entity metadatas from given entity schemas
if (this.entitySchemas) {
getFromContainer(EntityMetadataBuilder)
.buildFromSchemas(this.createNamingStrategy(), this.entitySchemas)
.forEach(metadata => {
this.entityMetadatas.push(metadata);
this.createRepository(metadata);
});
}
}
/**
@ -394,13 +432,13 @@ export class Connection {
private createRepository(entityLayout: EntityMetadata) {
const repositoryFactory = getFromContainer(RepositoryFactory);
if (entityLayout.table.isClosure) {
const repository = repositoryFactory.createRepository(this, this.entityMetadatas, entityLayout);
const reactiveRepository = repositoryFactory.createReactiveRepository(repository);
const repository = repositoryFactory.createTreeRepository(this, this.entityMetadatas, entityLayout);
const reactiveRepository = repositoryFactory.createReactiveTreeRepository(repository);
this.repositories.push(repository);
this.reactiveRepositories.push(reactiveRepository);
} else {
const repository = repositoryFactory.createTreeRepository(this, this.entityMetadatas, entityLayout);
const reactiveRepository = repositoryFactory.createReactiveTreeRepository(repository);
const repository = repositoryFactory.createRepository(this, this.entityMetadatas, entityLayout);
const reactiveRepository = repositoryFactory.createReactiveRepository(repository);
this.repositories.push(repository);
this.reactiveRepositories.push(reactiveRepository);
}

View File

@ -69,10 +69,5 @@ export interface ColumnOptions {
* Column collation. Note that not all databases support it.
*/
readonly collation?: string; // todo: looks like this is not used
/**
* Indicates if this column is order id column.
*/
readonly isOrderId?: boolean; // todo: looks like this is not implemented yet
}

View File

@ -49,7 +49,7 @@ export class MysqlDriver extends BaseDriver implements Driver {
}
/**
* Access to the native implementation of the database.
* Access to the native connection to the database.
*/
get nativeConnection(): any {
return this.mysqlConnection;
@ -80,8 +80,9 @@ export class MysqlDriver extends BaseDriver implements Driver {
if (!mysql && require) {
try {
mysql = require("mysql");
} catch (e) {
throw new Error("Mysql package was not found installed. Try to install it: npm install mysql --save");
throw new Error("Mysql package has not been found installed. Try to install it: npm install mysql --save");
}
} else {
throw new Error("Cannot load mysql driver dependencies. Try to install all required dependencies.");

View File

@ -7,7 +7,7 @@ import {ReactiveRepository} from "../repository/ReactiveRepository";
import {TreeRepository} from "../repository/TreeRepository";
/**
* Entity manager supposed to work with any entity, automatically find its repository and call its method, whatever
* Entity manager supposed to work with any entity, automatically find its repository and call its methods, whatever
* entity type are you passing.
*/
export class EntityManager {

View File

@ -1,10 +1,10 @@
import * as Rx from "rxjs/Rx";
import {Connection} from "../connection/Connection";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {FindOptions} from "../repository/FindOptions";
import {Repository} from "../repository/Repository";
import {ConstructorFunction} from "../common/ConstructorFunction";
import {ReactiveRepository} from "../repository/ReactiveRepository";
import * as Rx from "rxjs/Rx";
import {ReactiveTreeRepository} from "../repository/ReactiveTreeRepository";
/**
@ -303,6 +303,7 @@ export class ReactiveEntityManager {
/**
* Roots are entities that have no ancestors. Finds them all.
* Used on the tree-type (e.g. closure table) entities.
*/
findRoots<Entity>(entityClass: ConstructorFunction<Entity>|Function): Rx.Observable<Entity[]> {
return this.getReactiveTreeRepository(entityClass).findRoots();
@ -310,6 +311,7 @@ export class ReactiveEntityManager {
/**
* Creates a query builder used to get descendants of the entities in a tree.
* Used on the tree-type (e.g. closure table) entities.
*/
createDescendantsQueryBuilder<Entity>(entityClass: ConstructorFunction<Entity>|Function, alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
return this.getReactiveTreeRepository(entityClass).createDescendantsQueryBuilder(alias, closureTableAlias, entity);
@ -317,6 +319,7 @@ export class ReactiveEntityManager {
/**
* Gets all children (descendants) of the given entity. Returns them all in a flat array.
* Used on the tree-type (e.g. closure table) entities.
*/
findDescendants<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Rx.Observable<Entity[]> {
return this.getReactiveTreeRepository(entityClass).findDescendants(entity);
@ -324,6 +327,7 @@ export class ReactiveEntityManager {
/**
* Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
* Used on the tree-type (e.g. closure table) entities.
*/
findDescendantsTree<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Rx.Observable<Entity> {
return this.getReactiveTreeRepository(entityClass).findDescendantsTree(entity);
@ -331,6 +335,7 @@ export class ReactiveEntityManager {
/**
* Gets number of descendants of the entity.
* Used on the tree-type (e.g. closure table) entities.
*/
countDescendants<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Rx.Observable<number> {
return this.getReactiveTreeRepository(entityClass).countDescendants(entity);
@ -338,6 +343,7 @@ export class ReactiveEntityManager {
/**
* Creates a query builder used to get ancestors of the entities in the tree.
* Used on the tree-type (e.g. closure table) entities.
*/
createAncestorsQueryBuilder<Entity>(entityClass: ConstructorFunction<Entity>|Function, alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
return this.getReactiveTreeRepository(entityClass).createAncestorsQueryBuilder(alias, closureTableAlias, entity);
@ -345,20 +351,23 @@ export class ReactiveEntityManager {
/**
* Gets all parents (ancestors) of the given entity. Returns them all in a flat array.
* Used on the tree-type (e.g. closure table) entities.
*/
findAncestors<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Rx.Observable<Entity[]> {
return this.getReactiveTreeRepository(entityClass).findAncestors(entity);
}
/**
* Gets all parents (ancestors) of the given entity. Returns them in a tree - nested into each other.
* Gets all parents (ancestors) of the given entity. \Returns them in a tree - nested into each other.
* Used on the tree-type (e.g. closure table) entities.
*/
findAncestorsTree<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Rx.Observable<Entity> {
return this.getReactiveTreeRepository(entityClass).findAncestorsTree(entity);
}
/**
* Gets number of ancestors of the entity.
* Gets number of ancestors of the entity.
* Used on the tree-type (e.g. closure table) entities.
*/
countAncestors<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Rx.Observable<number> {
return this.getReactiveTreeRepository(entityClass).countAncestors(entity);

View File

@ -11,6 +11,12 @@ export interface ColumnMetadataArgs {
*/
readonly target?: Function;
/**
* In the case if this column is without a target, targetId must be specified.
* This is used for entity schemas without classes.
*/
readonly targetId?: string;
/**
* Class's property name to which column is applied.
*/
@ -18,11 +24,15 @@ export interface ColumnMetadataArgs {
/**
* Class's property type (reflected) to which column is applied.
*
* todo: check when this is not set, because for the entity schemas we don't set it.
*/
readonly propertyType: string;
readonly propertyType?: string;
/**
* Column mode in which column will work.
*
* todo: find name better then "mode".
*/
readonly mode: ColumnMode;

View File

@ -10,7 +10,13 @@ export interface RelationMetadataArgs {
/**
* Class to which this relation is applied.
*/
readonly target: Function;
readonly target?: Function;
/**
* In the case if this relation is without a target, targetId must be specified.
* This is used for entity schemas without classes.
*/
readonly targetId?: string;
/**
* Class's property name to which this relation is applied.
@ -19,8 +25,10 @@ export interface RelationMetadataArgs {
/**
* Original (reflected) class's property type.
*
* todo: this can be empty for relations from entity schemas.
*/
readonly propertyType: any;
readonly propertyType?: any;
/**
* Type of relation. Can be one of the value of the RelationTypes class.

View File

@ -10,6 +10,12 @@ export interface TableMetadataArgs {
*/
readonly target?: Function;
/**
* In the case if this table is without a target, targetId must be specified.
* This is used for entity schemas without classes.
*/
readonly targetId?: string;
/**
* Table name.
*/

View File

@ -13,6 +13,11 @@ import {JoinTableMetadata} from "../metadata/JoinTableMetadata";
import {JunctionEntityMetadataBuilder} from "./JunctionEntityMetadataBuilder";
import {ClosureJunctionEntityMetadataBuilder} from "./ClosureJunctionEntityMetadataBuilder";
import {EmbeddedMetadata} from "../metadata/EmbeddedMetadata";
import {EntitySchema} from "../metadata/entity-schema/EntitySchema";
import {MetadataArgsStorage} from "../metadata-args/MetadataArgsStorage";
import {TableMetadataArgs} from "../metadata-args/TableMetadataArgs";
import {ColumnMetadataArgs} from "../metadata-args/ColumnMetadataArgs";
import {RelationMetadataArgs} from "../metadata-args/RelationMetadataArgs";
/**
* Aggregates all metadata: table, column, relation into one collection grouped by tables for a given set of classes.
@ -31,14 +36,93 @@ export class EntityMetadataBuilder {
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
buildFromSchemas(namingStrategy: NamingStrategyInterface, schemas: EntitySchema[]): EntityMetadata[] {
const metadataArgsStorage = new MetadataArgsStorage();
schemas.forEach(schema => {
// add table metadata args from the schema
const tableSchema = schema.table || {} as any;
const table: TableMetadataArgs = {
target: schema.target,
name: tableSchema.name,
type: tableSchema.type || "regular",
targetId: schema.name,
orderBy: tableSchema.orderBy,
primaryKeys: tableSchema.primaryKeys
};
metadataArgsStorage.tables.add(table);
// add columns metadata args from the schema
Object.keys(schema.columns).forEach(columnName => {
const columnSchema = schema.columns[columnName];
const column: ColumnMetadataArgs = {
target: schema.target,
targetId: schema.name,
mode: columnSchema.mode,
propertyName: columnName,
// todo: what to do with it?: propertyType:
options: {
type: columnSchema.type,
name: columnSchema.name,
length: columnSchema.length,
generated: columnSchema.generated,
unique: columnSchema.unique,
nullable: columnSchema.nullable,
columnDefinition: columnSchema.columnDefinition,
comment: columnSchema.comment,
oldColumnName: columnSchema.oldColumnName,
precision: columnSchema.precision,
scale: columnSchema.scale,
collation: columnSchema.collation
}
};
metadataArgsStorage.columns.add(column);
});
// add relation metadata args from the schema
Object.keys(schema.relations).forEach(relationName => {
const relationSchema = schema.relations[relationName];
const relation: RelationMetadataArgs = {
target: schema.target,
targetId: schema.name,
propertyName: relationName,
// todo: what to do with it?: propertyType:
relationType: relationSchema.relationType,
type: relationSchema.type,
inverseSideProperty: relationSchema.inverseSide,
isTreeParent: relationSchema.isTreeParent,
isTreeChildren: relationSchema.isTreeChildren,
options: {
cascadeAll: relationSchema.cascadeAll,
cascadeInsert: relationSchema.cascadeInsert,
cascadeUpdate: relationSchema.cascadeUpdate,
cascadeRemove: relationSchema.cascadeRemove,
oldColumnName: relationSchema.oldColumnName,
nullable: relationSchema.nullable,
onDelete: relationSchema.onDelete
}
};
metadataArgsStorage.relations.add(relation);
});
});
return this.buildFromAnyMetadataArgsStorage(metadataArgsStorage, namingStrategy, []);
}
/**
* Builds a complete metadata aggregations for the given entity classes.
*/
build(namingStrategy: NamingStrategyInterface, entityClasses: Function[]): EntityMetadata[] {
const embeddableMergedArgs = getMetadataArgsStorage().getMergedEmbeddableTableMetadatas(entityClasses);
const entityMetadatas = getMetadataArgsStorage().getMergedTableMetadatas(entityClasses).map(mergedArgs => {
buildFromMetadataArgsStorage(namingStrategy: NamingStrategyInterface, entityClasses: Function[]): EntityMetadata[] {
return this.buildFromAnyMetadataArgsStorage(getMetadataArgsStorage(), namingStrategy, entityClasses);
}
buildFromAnyMetadataArgsStorage(metadataArgsStorage: MetadataArgsStorage, namingStrategy: NamingStrategyInterface, entityClasses: Function[]): EntityMetadata[] {
const embeddableMergedArgs = metadataArgsStorage.getMergedEmbeddableTableMetadatas(entityClasses);
const entityMetadatas = metadataArgsStorage.getMergedTableMetadatas(entityClasses).map(mergedArgs => {
// find embeddable tables for embeddeds registered in this table and create EmbeddedMetadatas from them
const embeddeds: EmbeddedMetadata[] = [];

View File

@ -6,7 +6,9 @@ import {EmbeddedMetadata} from "./EmbeddedMetadata";
/**
* Kinda type of the column. Not a type in the database, but locally used type to determine what kind of column
* we are working with.
* we are working with.
* For example, "primary" means that it will be a primary column, or "createDate" means that it will create a create
* date column.
*/
export type ColumnMode = "regular"|"virtual"|"primary"|"createDate"|"updateDate"|"version"|"treeChildrenCount"|"treeLevel";

View File

@ -9,7 +9,7 @@ import {RelationMetadataArgs} from "../metadata-args/RelationMetadataArgs";
/**
* Function that returns a type of the field. Returned value must be a class used on the relation.
*/
export type RelationTypeInFunction = ((type?: any) => Function);
export type RelationTypeInFunction = ((type?: any) => Function)|string; // todo: |string ?
/**
@ -198,8 +198,8 @@ export class RelationMetadata extends PropertyMetadata {
/**
* Gets the property's type to which this relation is applied.
*/
get type(): Function {
return this._type();
get type(): Function|string {
return this._type instanceof Function ? this._type() : this._type;
}
/**

View File

@ -0,0 +1,226 @@
import {ColumnMode} from "../ColumnMetadata";
import {ColumnType} from "../types/ColumnTypes";
import {TableType} from "../TableMetadata";
import {RelationType} from "../types/RelationTypes";
import {OnDeleteType} from "../ForeignKeyMetadata";
export interface EntitySchema {
/**
* Name of the schema it extends.
*/
extends?: string;
/**
* Target bind to this entity schema. Optional.
*/
target?: Function;
/**
* Entity name.
*/
name: string;
/**
* Entity table's options.
*/
table: {
/**
* Table name.
*/
name?: string;
/**
* Table type.
*/
type: TableType;
/**
* Specifies array of properties that will be used in a composite primary key of the table.
*/
primaryKeys?: string|((object: any) => string|any)[];
/**
* Specifies a property name by which queries will perform ordering by default when fetching rows.
*/
orderBy?: string|((object: any) => string|any);
};
/**
* Entity column's options.
*/
columns: {
[columnName: string]: {
/**
* Column mode in which column will work.
* For example, "primary" means that it will be a primary column, or "createDate" means that it will create a create
* date column.
*/
mode: ColumnMode;
/**
* Column type. Must be one of the value from the ColumnTypes class.
*/
type: ColumnType;
/**
* Column name in the database.
*/
name?: string;
/**
* Column type's length. For example type = "string" and length = 100 means that ORM will create a column with
* type varchar(100).
*/
length?: string;
/**
* Specifies if this column will use AUTO_INCREMENT or not (e.g. generated number).
*/
generated?: boolean;
/**
* Specifies if column's value must be unique or not.
*/
unique?: boolean;
/**
* Indicates if column's value can be set to NULL.
*/
nullable?: boolean;
/**
* Extra column definition. Should be used only in emergency situations. Note that if you'll use this property
* auto schema generation will not work properly anymore. Avoid using it.
*/
columnDefinition?: string;
/**
* Column comment.
*/
comment?: string;
/**
* Column name used previously for this column. Used to make safe schema updates. Experimental and most probably
* will be removed in the future. Avoid using it.
*/
oldColumnName?: string;
/**
* The precision for a decimal (exact numeric) column (applies only for decimal column), which is the maximum
* number of digits that are stored for the values.
*/
precision?: number;
/**
* The scale for a decimal (exact numeric) column (applies only for decimal column), which represents the number
* of digits to the right of the decimal point and must not be greater than precision.
*/
scale?: number;
/**
* Column collation. Note that not all databases support it.
*/
collation?: string; // todo: looks like this is not used
};
};
/**
* Entity relation's options.
*/
relations: {
[relationName: string]: {
/**
* Type of relation. Can be one of the value of the RelationTypes class.
*/
relationType: RelationType;
/**
* Type of the relation. This type is in function because of language specifics and problems with recursive
* referenced classes.
*/
type: string;
/**
* Inverse side of the relation.
*/
inverseSide?: string;
/**
* Join table options of this column. If set to true then it simply means that it has a join table.
*/
joinTable?: boolean|{
};
/**
* Join column options of this column. If set to true then it simply means that it has a join column.
*/
joinColumn?: boolean|{
/**
* Name of the column.
*/
name?: string;
/**
* Name of the column in the entity to which this column is referenced.
*/
referencedColumnName?: string;
};
/**
* Indicates if this is a parent (can be only many-to-one relation) relation in the tree tables.
*/
isTreeParent?: boolean;
/**
* Indicates if this is a children (can be only one-to-many relation) relation in the tree tables.
*/
isTreeChildren?: boolean;
/**
* If set to true then it means that related object can be allowed to be inserted / updated / removed to the db.
* This is option a shortcut if you would like to set cascadeInsert, cascadeUpdate and cascadeRemove to true.
*/
cascadeAll?: boolean;
/**
* If set to true then it means that related object can be allowed to be inserted to the db.
*/
cascadeInsert?: boolean;
/**
* If set to true then it means that related object can be allowed to be updated in the db.
*/
cascadeUpdate?: boolean;
/**
* If set to true then it means that related object can be allowed to be remove from the db.
*/
cascadeRemove?: boolean;
/**
* Column name used previously for this column. Used to make safe schema updates. Experimental and most probably
* will be removed in the future. Avoid using it.
*/
oldColumnName?: string;
/**
* Indicates if relation column value can be nullable or not.
*/
nullable?: boolean;
/**
* Database cascade action on delete.
*/
onDelete?: OnDeleteType;
};
};
}

View File

@ -8,6 +8,7 @@ import {Category} from "./entity/Category";
import {CreateConnectionOptions} from "../../../../src/connection-manager/CreateConnectionOptions";
import {createConnection} from "../../../../src/index";
import {CategoryMetadata} from "./entity/CategoryMetadata";
import {setupConnection} from "../../../utils/utils";
chai.should();
chai.use(require("sinon-chai"));
@ -19,35 +20,10 @@ describe("persistence > custom-column-names", function() {
// Configuration
// -------------------------------------------------------------------------
const parameters: CreateConnectionOptions = {
driver: "mysql",
connection: {
host: "192.168.99.100",
port: 3306,
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true,
logging: {
logFailedQueryError: true
}
},
entities: [Post, Category, CategoryMetadata]
};
// connect to db
let connection: Connection;
before(function() {
return createConnection(parameters)
.then(con => connection = con)
.catch(e => {
console.log("Error during connection to db: " + e);
throw e;
});
});
after(function() {
connection.close();
});
before(setupConnection(con => connection = con, [Post, Category, CategoryMetadata]));
after(() => connection.close());
// clean up database before each test
function reloadDatabase() {

View File

@ -2,7 +2,7 @@ import {CreateConnectionOptions} from "../../src/connection-manager/CreateConnec
import {createConnection} from "../../src/index";
import {Connection} from "../../src/connection/Connection";
export function setupConnection(entities: Function[], callback?: (connection: Connection) => any) {
export function setupConnection(callback: (connection: Connection) => any, entities: Function[]) {
const parameters: CreateConnectionOptions = {
driver: "mysql",