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.

295 lines (229 loc) 10.9 kB
import { EntityData, EntityMetadata, EntityProperty, IEntity, IEntityType, IPrimaryKey } from '../decorators'; import { MetadataStorage } from '../metadata'; import { Cascade, Collection, EntityIdentifier, ReferenceType } from '../entity'; import { ChangeSetComputer } from './ChangeSetComputer'; import { ChangeSetPersister } from './ChangeSetPersister'; import { ChangeSet, ChangeSetType } from './ChangeSet'; import { EntityManager } from '../EntityManager'; import { Utils } from '../utils'; import { FilterQuery } from '..'; export class UnitOfWork { /** map of references to managed entities */ private readonly identityMap = {} as Record<string, IEntity>; /** holds copy of identity map so we can compute changes when persisting managed entities */ private readonly originalEntityData = {} as Record<string, IEntity>; /** map of wrapped primary keys so we can compute change set without eager commit */ private readonly identifierMap = {} as Record<string, EntityIdentifier>; private readonly persistStack: IEntity[] = []; private readonly removeStack: IEntity[] = []; private readonly changeSets: ChangeSet<IEntity>[] = []; private readonly extraUpdates: [IEntityType<IEntity>, string & keyof IEntity, IEntityType<IEntity> | Collection<IEntity>][] = []; private readonly metadata = MetadataStorage.getMetadata(); private readonly platform = this.em.getDriver().getPlatform(); private readonly changeSetComputer = new ChangeSetComputer(this.em.getValidator(), this.originalEntityData, this.identifierMap); private readonly changeSetPersister = new ChangeSetPersister(this.em.getDriver(), this.identifierMap); constructor(private readonly em: EntityManager) { } merge<T extends IEntityType<T>>(entity: T, visited: IEntity[] = []): void { if (!entity.__primaryKey) { return; } this.identityMap[`${entity.constructor.name}-${entity.__serializedPrimaryKey}`] = entity; this.originalEntityData[entity.__uuid] = Utils.copy(entity); this.cascade(entity, Cascade.MERGE, visited); } getById<T extends IEntityType<T>>(entityName: string, id: IPrimaryKey): T { const token = `${entityName}-${this.platform.normalizePrimaryKey(id)}`; return this.identityMap[token] as T; } tryGetById<T extends IEntityType<T>>(entityName: string, where: FilterQuery<T> | IPrimaryKey): T | null { if (!Utils.isPrimaryKey(where)) { return null; } return this.getById<T>(entityName, where); } getIdentityMap(): Record<string, IEntity> { return this.identityMap; } persist<T extends IEntityType<T>>(entity: T, visited: IEntity[] = []): void { if (this.persistStack.includes(entity)) { return; } if (!entity.__primaryKey) { this.identifierMap[entity.__uuid] = new EntityIdentifier(); } this.persistStack.push(entity); this.cleanUpStack(this.removeStack, entity); this.cascade(entity, Cascade.PERSIST, visited); } remove(entity: IEntity, visited: IEntity[] = []): void { if (this.removeStack.includes(entity)) { return; } if (entity.__primaryKey) { this.removeStack.push(entity); } this.cleanUpStack(this.persistStack, entity); this.unsetIdentity(entity); this.cascade(entity, Cascade.REMOVE, visited); } async commit(): Promise<void> { this.computeChangeSets(); if (this.changeSets.length === 0) { return this.postCommitCleanup(); // nothing to do, do not start transaction } const driver = this.em.getDriver(); const runInTransaction = !driver.isInTransaction() && driver.getPlatform().supportsTransactions(); const promise = Utils.runSerial(this.changeSets, changeSet => this.commitChangeSet(changeSet)); if (runInTransaction) { await driver.transactional(() => promise); } else { await promise; } this.postCommitCleanup(); } clear(): void { Object.keys(this.identityMap).forEach(key => delete this.identityMap[key]); Object.keys(this.originalEntityData).forEach(key => delete this.originalEntityData[key]); this.postCommitCleanup(); } unsetIdentity(entity: IEntity): void { delete this.identityMap[`${entity.constructor.name}-${entity.__serializedPrimaryKey}`]; delete this.identifierMap[entity.__uuid]; delete this.originalEntityData[entity.__uuid]; } computeChangeSets(): void { this.changeSets.length = 0; while (this.persistStack.length) { this.findNewEntities(this.persistStack.shift()!); } while (this.extraUpdates.length) { const extraUpdate = this.extraUpdates.shift()!; extraUpdate[0][extraUpdate[1]] = extraUpdate[2]; const changeSet = this.changeSetComputer.computeChangeSet(extraUpdate[0])!; this.changeSets.push(changeSet); } for (const entity of Object.values(this.removeStack)) { const meta = this.metadata[entity.constructor.name]; this.changeSets.push({ entity, type: ChangeSetType.DELETE, name: meta.name, collection: meta.collection, payload: {} } as ChangeSet<IEntity>); } } private findNewEntities<T extends IEntityType<T>>(entity: T, visited: IEntity[] = []): void { visited.push(entity); const meta = this.metadata[entity.constructor.name] as EntityMetadata<T>; if (!entity.__primaryKey && !this.identifierMap[entity.__uuid]) { this.identifierMap[entity.__uuid] = new EntityIdentifier(); } for (const prop of Object.values(meta.properties)) { const reference = entity[prop.name as keyof T]; this.processReference(entity, prop, reference, visited); } const changeSet = this.changeSetComputer.computeChangeSet(entity); if (changeSet) { this.changeSets.push(changeSet); this.cleanUpStack(this.persistStack, entity); this.originalEntityData[entity.__uuid] = Utils.copy(entity); } } private processReference<T extends IEntityType<T>>(parent: T, prop: EntityProperty<T>, reference: any, visited: IEntity[]): void { const isToOne = prop.reference === ReferenceType.MANY_TO_ONE || prop.reference === ReferenceType.ONE_TO_ONE; if (isToOne && reference) { return this.processToOneReference(parent, prop, reference, visited); } if (Utils.isCollection(reference, prop, ReferenceType.MANY_TO_MANY) && reference.isDirty()) { this.processToManyReference(reference, visited, parent, prop); } } private processToOneReference<T extends IEntityType<T>>(parent: T, prop: EntityProperty<T>, reference: any, visited: IEntity[]): void { if (!this.hasIdentifier(reference) && visited.includes(reference)) { this.extraUpdates.push([parent, prop.name as keyof IEntity, reference]); delete parent[prop.name as keyof T]; } if (!this.originalEntityData[reference.__uuid]) { this.findNewEntities(reference, visited); } } private processToManyReference<T extends IEntityType<T>>(reference: Collection<IEntity>, visited: IEntity[], parent: T, prop: EntityProperty<T>): void { if (this.isCollectionSelfReferenced(reference, visited)) { this.extraUpdates.push([parent, prop.name as keyof IEntity, reference]); parent[prop.name] = new Collection<IEntity>(parent) as T[keyof T]; return; } reference.getItems() .filter(item => !this.originalEntityData[item.__uuid]) .forEach(item => this.findNewEntities(item, visited)); } private async commitChangeSet<T extends IEntityType<T>>(changeSet: ChangeSet<T>): Promise<void> { const type = changeSet.type.charAt(0).toUpperCase() + changeSet.type.slice(1); await this.runHooks(`before${type}`, changeSet.entity, changeSet.payload); await this.changeSetPersister.persistToDatabase(changeSet); if (changeSet.type !== ChangeSetType.DELETE) { this.em.merge(changeSet.entity); } await this.runHooks(`after${type}`, changeSet.entity); } private async runHooks<T extends IEntityType<T>>(type: string, entity: IEntityType<T>, payload?: EntityData<T>) { const hooks = this.metadata[entity.constructor.name].hooks; if (hooks && hooks[type] && hooks[type].length > 0) { const copy = Utils.copy(entity); await Utils.runSerial(hooks[type], hook => entity[hook as keyof T]()); if (payload) { Object.assign(payload, Utils.diffEntities(copy, entity)); } } } /** * clean up persist/remove stack from previous persist/remove calls for this entity done before flushing */ private cleanUpStack(stack: IEntity[], entity: IEntity): void { for (const index in stack) { if (stack[index] === entity) { stack.splice(+index, 1); } } } private postCommitCleanup(): void { Object.keys(this.identifierMap).forEach(key => delete this.identifierMap[key]); this.persistStack.length = 0; this.removeStack.length = 0; this.changeSets.length = 0; } private hasIdentifier<T extends IEntityType<T>>(entity: T): boolean { const pk = this.metadata[entity.constructor.name].primaryKey as keyof T; if (entity[pk]) { return true; } return this.identifierMap[entity.__uuid] && this.identifierMap[entity.__uuid].getValue(); } private cascade<T extends IEntityType<T>>(entity: T, type: Cascade, visited: IEntity[]): void { if (visited.includes(entity)) { return; } visited.push(entity); switch (type) { case Cascade.PERSIST: this.persist(entity, visited); break; case Cascade.MERGE: this.merge(entity, visited); break; case Cascade.REMOVE: this.remove(entity, visited); break; } const meta = this.metadata[entity.constructor.name]; for (const prop of Object.values(meta.properties)) { this.cascadeReference<T>(entity, prop, type, visited); } } private cascadeReference<T extends IEntityType<T>>(entity: T, prop: EntityProperty, type: Cascade, visited: IEntity[]): void { if (!prop.cascade || !(prop.cascade.includes(type) || prop.cascade.includes(Cascade.ALL))) { return; } if ((prop.reference === ReferenceType.MANY_TO_ONE || prop.reference === ReferenceType.ONE_TO_ONE) && entity[prop.name as keyof T]) { return this.cascade(entity[prop.name as keyof T], type, visited); } const collection = entity[prop.name as keyof T] as Collection<IEntity>; const requireFullyInitialized = type === Cascade.PERSIST; // only cascade persist needs fully initialized items if ([ReferenceType.ONE_TO_MANY, ReferenceType.MANY_TO_MANY].includes(prop.reference) && collection.isInitialized(requireFullyInitialized)) { collection.getItems().forEach(item => this.cascade(item, type, visited)); } } private isCollectionSelfReferenced(collection: Collection<IEntity>, visited: IEntity[]): boolean { const filtered = collection.getItems().filter(item => !this.originalEntityData[item.__uuid]); return filtered.some(items => visited.includes(items)); } }