UNPKG

ionic-orm-2

Version:

Data-mapper ORM for Ionic WebSQL and SQLite

388 lines 22.5 kB
import { EntityMetadata } from "../metadata/EntityMetadata"; import { ColumnMetadata } from "../metadata/ColumnMetadata"; import { ForeignKeyMetadata } from "../metadata/ForeignKeyMetadata"; import { EntityMetadataValidator } from "./EntityMetadataValidator"; import { IndexMetadata } from "../metadata/IndexMetadata"; import { JoinColumnMetadata } from "../metadata/JoinColumnMetadata"; import { TableMetadata } from "../metadata/TableMetadata"; import { getMetadataArgsStorage, getFromContainer } from "../../index"; import { RelationMetadata } from "../metadata/RelationMetadata"; import { JoinTableMetadata } from "../metadata/JoinTableMetadata"; import { JunctionEntityMetadataBuilder } from "./JunctionEntityMetadataBuilder"; import { ClosureJunctionEntityMetadataBuilder } from "./ClosureJunctionEntityMetadataBuilder"; import { EmbeddedMetadata } from "../metadata/EmbeddedMetadata"; import { MetadataArgsStorage } from "../metadata-args/MetadataArgsStorage"; /** * Aggregates all metadata: table, column, relation into one collection grouped by tables for a given set of classes. */ export class EntityMetadataBuilder { // todo: type in function validation, inverse side function validation // todo: check on build for duplicate names, since naming checking was removed from MetadataStorage // todo: duplicate name checking for: table, relation, column, index, naming strategy, join tables/columns? // todo: check if multiple tree parent metadatas in validator // todo: tree decorators can be used only on closure table (validation) // todo: throw error if parent tree metadata was not specified in a closure table // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- buildFromSchemas(driver, lazyRelationsWrapper, namingStrategy, schemas) { const metadataArgsStorage = new MetadataArgsStorage(); // extract into separate class? schemas.forEach(schema => { // add table metadata args from the schema const tableSchema = schema.table || {}; const table = { target: schema.target || schema.name, name: tableSchema.name, type: tableSchema.type || "regular", // targetId: schema.name, orderBy: tableSchema.orderBy }; metadataArgsStorage.tables.add(table); // add columns metadata args from the schema Object.keys(schema.columns).forEach(columnName => { const columnSchema = schema.columns[columnName]; let mode = "regular"; if (columnSchema.createDate) mode = "createDate"; if (columnSchema.updateDate) mode = "updateDate"; if (columnSchema.version) mode = "version"; if (columnSchema.treeChildrenCount) mode = "treeChildrenCount"; if (columnSchema.treeLevel) mode = "treeLevel"; const column = { target: schema.target || schema.name, mode: mode, propertyName: columnName, // todo: what to do with it?: propertyType: options: { type: columnSchema.type, name: columnSchema.name, length: columnSchema.length, primary: columnSchema.primary, generated: columnSchema.generated, unique: columnSchema.unique, nullable: columnSchema.nullable, comment: columnSchema.comment, default: columnSchema.default, precision: columnSchema.precision, scale: columnSchema.scale } }; metadataArgsStorage.columns.add(column); }); // add relation metadata args from the schema if (schema.relations) { Object.keys(schema.relations).forEach(relationName => { const relationSchema = schema.relations[relationName]; const relation = { target: schema.target || schema.name, propertyName: relationName, relationType: relationSchema.type, isLazy: relationSchema.isLazy || false, type: relationSchema.target, inverseSideProperty: relationSchema.inverseSide, isTreeParent: relationSchema.isTreeParent, isTreeChildren: relationSchema.isTreeChildren, options: { cascadeAll: relationSchema.cascadeAll, cascadeInsert: relationSchema.cascadeInsert, cascadeUpdate: relationSchema.cascadeUpdate, cascadeRemove: relationSchema.cascadeRemove, nullable: relationSchema.nullable, onDelete: relationSchema.onDelete } }; metadataArgsStorage.relations.add(relation); // add join column if (relationSchema.joinColumn) { if (typeof relationSchema.joinColumn === "boolean") { const joinColumn = { target: schema.target || schema.name, propertyName: relationName }; metadataArgsStorage.joinColumns.push(joinColumn); } else { const joinColumn = { target: schema.target || schema.name, propertyName: relationName, name: relationSchema.joinColumn.name, referencedColumnName: relationSchema.joinColumn.referencedColumnName }; metadataArgsStorage.joinColumns.push(joinColumn); } } // add join table if (relationSchema.joinTable) { if (typeof relationSchema.joinTable === "boolean") { const joinTable = { target: schema.target || schema.name, propertyName: relationName }; metadataArgsStorage.joinTables.push(joinTable); } else { const joinTable = { target: schema.target || schema.name, propertyName: relationName, name: relationSchema.joinTable.name, joinColumn: relationSchema.joinTable.joinColumn, inverseJoinColumn: relationSchema.joinTable.inverseJoinColumn }; metadataArgsStorage.joinTables.push(joinTable); } } }); } }); return this.build(driver, lazyRelationsWrapper, metadataArgsStorage, namingStrategy); } /** * Builds a complete metadata aggregations for the given entity classes. */ buildFromMetadataArgsStorage(driver, lazyRelationsWrapper, namingStrategy, entityClasses) { return this.build(driver, lazyRelationsWrapper, getMetadataArgsStorage(), namingStrategy, entityClasses); } // ------------------------------------------------------------------------- // Private Methods // ------------------------------------------------------------------------- build(driver, lazyRelationsWrapper, metadataArgsStorage, namingStrategy, entityClasses) { const embeddableMergedArgs = metadataArgsStorage.getMergedEmbeddableTableMetadatas(entityClasses); const entityMetadatas = []; const allMergedArgs = metadataArgsStorage.getMergedTableMetadatas(entityClasses); allMergedArgs.forEach(mergedArgs => { const tables = [mergedArgs.table].concat(mergedArgs.children); tables.forEach(tableArgs => { // find embeddable tables for embeddeds registered in this table and create EmbeddedMetadatas from them const embeddeds = []; mergedArgs.embeddeds.forEach(embedded => { const embeddableTable = embeddableMergedArgs.find(embeddedMergedArgs => embeddedMergedArgs.table.target === embedded.type()); if (embeddableTable) { const table = new TableMetadata(embeddableTable.table); const columns = embeddableTable.columns.map(args => new ColumnMetadata(args)); embeddeds.push(new EmbeddedMetadata(embedded.type(), embedded.propertyName, table, columns)); } }); // create metadatas from args const argsForTable = mergedArgs.inheritance && mergedArgs.inheritance.type === "single-table" ? mergedArgs.table : tableArgs; const table = new TableMetadata(argsForTable); const columns = mergedArgs.columns.map(args => { // if column's target is a child table then this column should have all nullable columns if (mergedArgs.inheritance && mergedArgs.inheritance.type === "single-table" && args.target !== mergedArgs.table.target && !!mergedArgs.children.find(childTable => childTable.target === args.target)) { args.options.nullable = true; } return new ColumnMetadata(args); }); const relations = mergedArgs.relations.map(args => new RelationMetadata(args)); const indices = mergedArgs.indices.map(args => new IndexMetadata(args)); const discriminatorValueArgs = mergedArgs.discriminatorValues.find(discriminatorValueArgs => { return discriminatorValueArgs.target === tableArgs.target; }); // create a new entity metadata const entityMetadata = new EntityMetadata({ target: tableArgs.target, tablesPrefix: driver.options.tablesPrefix, namingStrategy: namingStrategy, tableMetadata: table, columnMetadatas: columns, relationMetadatas: relations, indexMetadatas: indices, embeddedMetadatas: embeddeds, inheritanceType: mergedArgs.inheritance ? mergedArgs.inheritance.type : undefined, discriminatorValue: discriminatorValueArgs ? discriminatorValueArgs.value : tableArgs.target.name // todo: pass this to naming strategy to generate a name }, lazyRelationsWrapper); entityMetadatas.push(entityMetadata); // create entity's relations join tables entityMetadata.manyToManyRelations.forEach(relation => { const joinTableMetadata = mergedArgs.joinTables.findByProperty(relation.propertyName); if (joinTableMetadata) { const joinTable = new JoinTableMetadata(joinTableMetadata); relation.joinTable = joinTable; joinTable.relation = relation; } }); // create entity's relations join columns entityMetadata.oneToOneRelations .concat(entityMetadata.manyToOneRelations) .forEach(relation => { // since for many-to-one relations having JoinColumn is not required on decorators level, we need to go // throw all of them which don't have JoinColumn decorators and create it for them let joinColumnMetadata = mergedArgs.joinColumns.findByProperty(relation.propertyName); if (!joinColumnMetadata && relation.isManyToOne) { joinColumnMetadata = { target: relation.entityMetadata.target, propertyName: relation.propertyName }; } if (joinColumnMetadata) { const joinColumn = new JoinColumnMetadata(joinColumnMetadata); relation.joinColumn = joinColumn; joinColumn.relation = relation; } }); // save relation id-s data entityMetadata.relations.forEach(relation => { const relationIdMetadata = mergedArgs.relationIds.find(relationId => { if (relationId.relation instanceof Function) return relation.propertyName === relationId.relation(entityMetadata.createPropertiesMap()); return relation.propertyName === relationId.relation; }); if (relationIdMetadata) { if (relation.isOneToOneNotOwner || relation.isOneToMany) throw new Error(`RelationId cannot be used for the one-to-one without join column or one-to-many relations.`); relation.idField = relationIdMetadata.propertyName; } }); // save relation counter-s data entityMetadata.relations.forEach(relation => { const relationCountMetadata = mergedArgs.relationCounts.find(relationCount => { if (relationCount.relation instanceof Function) return relation.propertyName === relationCount.relation(entityMetadata.createPropertiesMap()); return relation.propertyName === relationCount.relation; }); if (relationCountMetadata) relation.countField = relationCountMetadata.propertyName; }); // add lazy initializer for entity relations if (entityMetadata.target instanceof Function) { entityMetadata.relations .filter(relation => relation.isLazy) .forEach(relation => { lazyRelationsWrapper.wrap(entityMetadata.target.prototype, relation); }); } }); }); // after all metadatas created we set inverse side (related) entity metadatas for all relation metadatas entityMetadatas.forEach(entityMetadata => { entityMetadata.relations.forEach(relation => { const inverseEntityMetadata = entityMetadatas.find(m => m.target === relation.type || (typeof relation.type === "string" && m.targetName === relation.type)); if (!inverseEntityMetadata) throw new Error("Entity metadata for " + entityMetadata.name + "#" + relation.propertyName + " was not found."); relation.inverseEntityMetadata = inverseEntityMetadata; }); }); // after all metadatas created we set parent entity metadata for class-table inheritance entityMetadatas.forEach(entityMetadata => { const mergedArgs = allMergedArgs.find(mergedArgs => { return mergedArgs.table.target === entityMetadata.target; }); if (mergedArgs && mergedArgs.parent) { const parentEntityMetadata = entityMetadatas.find(entityMetadata => entityMetadata.table.target === mergedArgs.parent.target); // todo: weird compiler error here, thats why type casing is used if (parentEntityMetadata) entityMetadata.parentEntityMetadata = parentEntityMetadata; } }); // check for errors in a built metadata schema (we need to check after relationEntityMetadata is set) getFromContainer(EntityMetadataValidator).validateMany(entityMetadatas); // generate columns and foreign keys for tables with relations entityMetadatas.forEach(metadata => { metadata.relationsWithJoinColumns.forEach(relation => { // find relational column and if it does not exist - add it const inverseSideColumn = relation.joinColumn.referencedColumn; let relationalColumn = metadata.columns.find(column => column.name === relation.name); if (!relationalColumn) { relationalColumn = new ColumnMetadata({ target: metadata.target, propertyName: relation.name, propertyType: inverseSideColumn.propertyType, mode: "virtual", options: { type: inverseSideColumn.type, nullable: relation.isNullable, primary: relation.isPrimary } }); metadata.addColumn(relationalColumn); } // create and add foreign key const foreignKey = new ForeignKeyMetadata([relationalColumn], relation.inverseEntityMetadata.table, [inverseSideColumn], relation.onDelete); foreignKey.entityMetadata = metadata; metadata.foreignKeys.push(foreignKey); }); }); // generate junction tables for all closure tables entityMetadatas.forEach(metadata => { if (!metadata.table.isClosure) return; if (metadata.primaryColumns.length > 1) throw new Error(`Cannot use given entity ${metadata.name} as a closure table, because it have multiple primary keys. Entities with multiple primary keys are not supported in closure tables.`); const closureJunctionEntityMetadata = getFromContainer(ClosureJunctionEntityMetadataBuilder).build(driver, lazyRelationsWrapper, { namingStrategy: namingStrategy, table: metadata.table, primaryColumn: metadata.firstPrimaryColumn, hasTreeLevelColumn: metadata.hasTreeLevelColumn }); metadata.closureJunctionTable = closureJunctionEntityMetadata; entityMetadatas.push(closureJunctionEntityMetadata); }); // generate junction tables for all many-to-many tables entityMetadatas.forEach(metadata => { metadata.ownerManyToManyRelations.forEach(relation => { const junctionEntityMetadata = getFromContainer(JunctionEntityMetadataBuilder).build(driver, lazyRelationsWrapper, { namingStrategy: namingStrategy, firstTable: metadata.table, secondTable: relation.inverseEntityMetadata.table, joinTable: relation.joinTable }); relation.junctionEntityMetadata = junctionEntityMetadata; if (relation.hasInverseSide) relation.inverseRelation.junctionEntityMetadata = junctionEntityMetadata; entityMetadatas.push(junctionEntityMetadata); }); }); // generate keys for tables with single-table inheritance entityMetadatas .filter(metadata => metadata.inheritanceType === "single-table" && metadata.hasDiscriminatorColumn) .forEach(metadata => { const indexForKey = new IndexMetadata({ target: metadata.target, columns: [metadata.discriminatorColumn.name], unique: false }); indexForKey.entityMetadata = metadata; metadata.indices.push(indexForKey); const indexForKeyWithPrimary = new IndexMetadata({ target: metadata.target, columns: [metadata.firstPrimaryColumn.propertyName, metadata.discriminatorColumn.propertyName], unique: false }); indexForKeyWithPrimary.entityMetadata = metadata; metadata.indices.push(indexForKeyWithPrimary); }); // generate virtual column with foreign key for class-table inheritance entityMetadatas .filter(metadata => !!metadata.parentEntityMetadata) .forEach(metadata => { const parentEntityMetadataPrimaryColumn = metadata.parentEntityMetadata.firstPrimaryColumn; // todo: make sure to create columns for all its primary columns const columnName = namingStrategy.classTableInheritanceParentColumnName(metadata.parentEntityMetadata.table.name, parentEntityMetadataPrimaryColumn.propertyName); const parentRelationColumn = new ColumnMetadata({ target: metadata.parentEntityMetadata.table.target, propertyName: parentEntityMetadataPrimaryColumn.propertyName, propertyType: parentEntityMetadataPrimaryColumn.propertyType, mode: "parentId", options: { name: columnName, type: parentEntityMetadataPrimaryColumn.type, nullable: false, primary: false } }); // add column metadata.addColumn(parentRelationColumn); // add foreign key const foreignKey = new ForeignKeyMetadata([parentRelationColumn], metadata.parentEntityMetadata.table, [parentEntityMetadataPrimaryColumn], "CASCADE"); foreignKey.entityMetadata = metadata; metadata.foreignKeys.push(foreignKey); }); return entityMetadatas; } } //# sourceMappingURL=EntityMetadataBuilder.js.map