UNPKG

quaerateum

Version:

Simple typescript ORM for node.js based on data-mapper, unit-of-work and identity-map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JS.

152 lines (122 loc) 6.38 kB
import { EntityProperty, IEntity, IEntityType } from '../decorators'; import { MetadataStorage } from '../metadata'; import { EntityManager } from '../EntityManager'; import { ReferenceType } from './enums'; import { Utils } from '../utils'; import { Collection } from './Collection'; import { QueryOrder } from '../query'; export class EntityLoader { private readonly metadata = MetadataStorage.getMetadata(); private readonly driver = this.em.getDriver(); constructor(private readonly em: EntityManager) { } async populate<T extends IEntityType<T>>(entityName: string, entities: IEntityType<T>[], populate: string[], validate = true): Promise<void> { if (entities.length === 0) { return; } for (const field of populate) { if (validate && !this.em.canPopulate(entityName, field)) { throw new Error(`Entity '${entityName}' does not have property '${field}'`); } await this.populateField(entityName, entities, field); } } /** * preload everything in one call (this will update already existing references in IM) */ private async populateMany<T extends IEntityType<T>>(entityName: string, entities: T[], field: keyof T): Promise<IEntity[]> { // set populate flag entities.forEach(entity => { if (Utils.isEntity(entity[field]) || entity[field] as object instanceof Collection) { (entity[field] as IEntity).populated(); } }); const prop = this.metadata[entityName].properties[field as string]; const filtered = this.filterCollections<T>(entities, field); if (prop.reference === ReferenceType.MANY_TO_MANY && this.driver.getPlatform().usesPivotTable()) { return this.findChildrenFromPivotTable<T>(filtered, prop, field); } const data = await this.findChildren<T>(entities, prop); this.initializeCollections<T>(filtered, prop, field, data); return data; } private initializeCollections<T extends IEntityType<T>>(filtered: T[], prop: EntityProperty, field: keyof T, children: IEntity[]): void { if (prop.reference === ReferenceType.ONE_TO_MANY) { this.initializeOneToMany<T>(filtered, children, prop, field); } if (prop.reference === ReferenceType.MANY_TO_MANY && !prop.owner && !this.driver.getPlatform().usesPivotTable()) { this.initializeManyToMany<T>(filtered, children, prop, field); } } private initializeOneToMany<T extends IEntityType<T>>(filtered: T[], children: IEntity[], prop: EntityProperty, field: keyof T): void { for (const entity of filtered) { const items = children.filter(child => child[(prop.mappedBy as keyof IEntity)] as object === entity); (entity[field] as Collection<IEntity>).set(items, true); } } private initializeManyToMany<T extends IEntityType<T>>(filtered: T[], children: IEntity[], prop: EntityProperty, field: keyof T): void { for (const entity of filtered) { const items = children.filter(child => (child[prop.mappedBy as keyof IEntity] as object as Collection<IEntity>).contains(entity)); (entity[field] as Collection<IEntity>).set(items, true); } } private async findChildren<T extends IEntityType<T>>(entities: T[], prop: EntityProperty): Promise<IEntityType<any>[]> { const children = this.getChildReferences<T>(entities, prop); let fk = this.metadata[prop.type].primaryKey; if (prop.reference === ReferenceType.ONE_TO_MANY || (prop.reference === ReferenceType.MANY_TO_MANY && !prop.owner)) { fk = this.metadata[prop.type].properties[prop.mappedBy].fieldName; } if (children.length === 0) { return []; } const ids = Utils.unique(children.map(e => e.__primaryKey)); const orderBy = prop.orderBy || { [fk]: QueryOrder.ASC }; return this.em.find<IEntity>(prop.type, { [fk]: { $in: ids } }, [], orderBy); } private async populateField<T extends IEntityType<T>>(entityName: string, entities: IEntityType<T>[], field: string): Promise<void> { // nested populate if (field.includes('.')) { const [f, ...parts] = field.split('.'); await this.populateMany<T>(entityName, entities, f as keyof T); const children: IEntity[] = []; entities.forEach(entity => { if (Utils.isEntity(entity[f as keyof T])) { children.push(entity[f as keyof T]); } else if (entity[f as keyof T] as object instanceof Collection) { children.push(...entity[f as keyof T].getItems()); } }); const filtered = Utils.unique(children); const prop = this.metadata[entityName].properties[f]; await this.populate(prop.type, filtered, [parts.join('.')], false); } else { await this.populateMany<T>(entityName, entities, field as keyof T); } } private async findChildrenFromPivotTable<T extends IEntityType<T>>(filtered: T[], prop: EntityProperty, field: keyof T): Promise<IEntity[]> { const map = await this.driver.loadFromPivotTable(prop, filtered.map(e => e.__primaryKey)); const children: IEntity[] = []; for (const entity of filtered) { const items = map[entity.__primaryKey as number].map(item => this.em.merge(prop.type, item)); (entity[field] as Collection<IEntity>).set(items, true); children.push(...items); } return children; } private getChildReferences<T extends IEntityType<T>>(entities: T[], prop: EntityProperty<T>): IEntity[] { const filtered = this.filterCollections(entities, prop.name); const children: IEntity[] = []; if (prop.reference === ReferenceType.ONE_TO_MANY) { children.push(...filtered.map(e => e[prop.name].owner)); } else if (prop.reference === ReferenceType.MANY_TO_MANY && prop.owner) { children.push(...filtered.reduce((a, b) => [...a, ...(b[prop.name] as Collection<IEntity>).getItems()], [] as IEntity[])); } else if (prop.reference === ReferenceType.MANY_TO_MANY) { // inversed side children.push(...filtered); } else { // MANY_TO_ONE or ONE_TO_ONE children.push(...entities.filter(e => Utils.isEntity(e[prop.name]) && !(e[prop.name] as IEntity).isInitialized()).map(e => e[prop.name])); } return children; } private filterCollections<T extends IEntityType<T>>(entities: T[], field: keyof T): T[] { return entities.filter(e => e[field] as object instanceof Collection && !(e[field] as Collection<IEntity>).isInitialized(true)); } }