import { AsyncLocalStorage } from "async_hooks" ; import DataLoader from "dataloader" ; import { Knex } from "knex" ; import { createOrUpdatePartial } from "./createOrUpdatePartial" ; import { findDataLoader } from "./dataloaders/findDataLoader" ; import { loadDataLoader } from "./dataloaders/loadDataLoader" ; import { Driver } from "./drivers/driver" ; import { assertIdsAreTagged , ConfigApi , DeepPartialOrNull , EntityHook , FieldSerde , GenericError , getConstructorFromTaggedId , getEm , getRelations , keyToString , maybeResolveReferenceToId , OneToManyCollection , PartialOrNull , PolymorphicKeySerde , Reference , setField , setOpts , tagIfNeeded , ValidationError , ValidationErrors , ValidationRuleResult , } from "./index" ; import { Loaded , LoadHint , NestedLoadHint , New , RelationsIn } from "./loaded" ; import { ManyToOneReferenceImpl , OneToOneReferenceImpl } from "./relations" ; import { JoinRow } from "./relations/ManyToManyCollection" ; import { combineJoinRows , createTodos , getTodo , Todo } from "./Todo" ; import { fail , toArray } from "./utils" ; export interface EntityConstructor < T > { new ( em , opts ) : T ; } /** Return the `FooOpts` type a given `Foo` entity constructor. */ export type OptsOf < T > = T extends { __orm : { optsType : infer O ; } ; } ? O : never ; export type OptIdsOf < T > = T extends { __orm : { optIdsType : infer O ; } ; } ? O : never ; /** Return the `Foo` type for a given `Foo` entity constructor. */ export type EntityOf < C > = C extends new ( em , opts ) => infer T ? T : never ; /** Pulls the entity query type out of a given entity type T. */ export type FilterOf < T > = T extends { __orm : { filterType : infer Q ; } ; } ? Q : never ; /** Pulls the entity GraphQL query type out of a given entity type T. */ export type GraphQLFilterOf < T > = T extends { __orm : { gqlFilterType : infer Q ; } ; } ? Q : never ; /** Pulls the entity order type out of a given entity type T. */ export type OrderOf < T > = T extends { __orm : { orderType : infer Q ; } ; } ? Q : never ; /**
 * Returns the opts of the entity's `newEntity` factory method, as exists in the actual file.
 *
 * This is because `FactoryOpts` is a set of defaults, but the user can customize it if they want.
 */ export type ActualFactoryOpts < T > = T extends { __orm : { factoryOptsType : infer Q ; } ; } ? Q : never ; /** Pulls the entity's id type out of a given entity type T. */ export type IdOf < T > = T extends { id : infer I | undefined ; } ? I : never ; /** The `__orm` metadata field we track on each instance. */ export interface EntityOrmField { /** A point to our entity type's metadata. */ metadata : EntityMetadata < Entity > ; /** A bag for our primitives/fk column values. */ data : Record < any , any > ; /** A bag to keep the original values, lazily populated. */ originalData : Record < any , any > ; /** Whether our entity has been deleted or not. */ deleted ? : "pending" | "deleted" ; /** All entities must be associated to an `EntityManager` to handle lazy loading/etc. */ em : EntityManager ; /** Whether our entity is new or not. */ isNew : boolean ; /** Whether our entity should flush regardless of any other changes. */ isTouched : boolean ; } export let currentlyInstantiatingEntity ; /** A marker/base interface for all of our entity types. */ export interface Entity { id : string | undefined ; idOrFail : string ; __orm : EntityOrmField ; readonly isNewEntity : boolean ; readonly isDeletedEntity : boolean ; readonly isDirtyEntity : boolean ; readonly isPendingFlush : boolean ; readonly isPendingDelete : boolean ; set ( opts ) : void ; setPartial ( values ) : void ; } type MaybePromise < T > = T | PromiseLike < T > ; export type EntityManagerHook = "beforeTransaction" | "afterTransaction" ; type HookFn = ( em : EntityManager , knex : Knex . Transaction ) => MaybePromise < any > ; /**
 * A marker to prevent setter calls during `flush` calls.
 *
 * The `flush` process does a dirty check + SQL flush and generally doesn't want
 * entities to re-dirtied after it's done the initial dirty check. So we'd like
 * to prevent all setter calls while `flush` is running.
 *
 * That said, lifecycle code like hooks actually can make setter calls b/c `flush`
 * invokes them at a specific point in its process.
 *
 * We solve this by using node's `AsyncLocalStorage` to mark certain callbacks (promise
 * handlers) as blessed / invoked-from-`flush`-itself, and they are allowed to call setters,
 * but any external callers (i.e. application code) will be rejected.
 */ export const currentFlushSecret = new AsyncLocalStorage < { flushSecret : number ; } > ( ) ; export type LoaderCache = Record < string , DataLoader < any , any > > ; export interface FlushOptions { /** Skip all validations, including reactive validations, when flushing */ skipValidation ? : boolean ; } export class EntityManager < C = { } > { public readonly ctx : C ; public driver : Driver ; public currentTxnKnex : Knex | undefined ; private _entities : Entity [ ] = [ ] ; private _entityIndex : Map < string , Entity > = new Map ( ) ; private flushSecret : number = 0 ; private _isFlushing : boolean = false ; public loadLoaders : LoaderCache = { } ; public findLoaders : LoaderCache = { } ; __data = { joinRows : { } as Record < string , JoinRow [ ] > , } ; private hooks : Record < EntityManagerHook , HookFn [ ] > = { beforeTransaction : [ ] , afterTransaction : [ ] , } ; constructor ( em ) ; constructor ( ctx , driver ) ; constructor ( emOrCtx , driver ? ) { if ( emOrCtx instanceof EntityManager ) { const em = emOrCtx ; this . driver = em . driver ; this . hooks = { beforeTransaction : [ ... em . hooks . beforeTransaction ] , afterTransaction : [ ... em . hooks . afterTransaction ] , } ; this . ctx = em . ctx ! ; } else { this . ctx = emOrCtx ! ; this . driver = driver ! ; } } get entities ( ) : ReadonlyArray < Entity > { return [ ... this . _entities ] ; } public async find < T extends Entity > ( type , where ) ; public async find < T extends Entity , H extends LoadHint < T > > ( type , where , options ? ) ; async find < T extends Entity > ( type , where , options ? ) { const rows = await findDataLoader ( this , type ) . load ( { where , ... options } ) ; const result = rows . map ( ( row ) => this . hydrate ( type , row , { overwriteExisting : false } ) ) ; if ( options ?. populate ) { await this . populate ( result , options . populate ) ; } return result ; } /**
     * Works exactly like `find` but accepts "less than greatly typed" GraphQL filters.
     *
     * I.e. filtering by `null` on fields that are non-`nullable`.
     */ public async findGql < T extends Entity > ( type , where ) ; public async findGql < T extends Entity , H extends LoadHint < T > > ( type , where , options ? ) ; async findGql < T extends Entity > ( type , where , options ? ) { const rows = await findDataLoader ( this , type ) . load ( { where , ... options } ) ; const result = rows . map ( ( row ) => this . hydrate ( type , row , { overwriteExisting : false } ) ) ; if ( options ?. populate ) { await this . populate ( result , options . populate ) ; } return result ; } public async findOne < T extends Entity > ( type , where ) ; public async findOne < T extends Entity , H extends LoadHint < T > > ( type , where , options ? ) ; async findOne < T extends Entity > ( type , where , options ? ) { const list = await this . find ( type , where , options ) ; if ( list . length === 0 ) { return undefined ; } else if ( list . length === 1 ) { return list [ 0 ] ; } else { throw new TooManyError ( `Found more than one: ${ list . map ( ( e ) => e . toString ( ) ) . join ( ", " ) }` ) ; } } /** Executes a given query filter and returns exactly one result, otherwise throws `NotFoundError` or `TooManyError`. */ public async findOneOrFail < T extends Entity > ( type , where ) ; public async findOneOrFail < T extends Entity , H extends LoadHint < T > > ( type , where , options ) ; async findOneOrFail < T extends Entity > ( type , where , options ? ) { const list = await this . find ( type , where , options ) ; if ( list . length === 0 ) { throw new NotFoundError ( `Did not find ${ type . name } for given query` ) ; } else if ( list . length > 1 ) { throw new TooManyError ( `Found more than one: ${ list . map ( ( e ) => e . toString ( ) ) . join ( ", " ) }` ) ; } return list [ 0 ] ; } param type param where param ifNew param upsert async findOrCreate < T extends Entity , F extends Partial < OptsOf < T > > , U extends Partial < OptsOf < T > > | { } , O extends Omit < OptsOf < T > , keyof F | keyof U > > ( type , where , ifNew , upsert ? ) ; async findOrCreate < T extends Entity , F extends Partial < OptsOf < T > > , U extends Partial < OptsOf < T > > | { } , O extends Omit < OptsOf < T > , keyof F | keyof U > , H extends LoadHint < T > > ( type , where , ifNew , upsert ? , populate ? ) ; async findOrCreate < T extends Entity , F extends Partial < OptsOf < T > > , U extends Partial < OptsOf < T > > | { } , O extends Omit < OptsOf < T > , keyof F | keyof U > , H extends LoadHint < T > > ( type , where , ifNew , upsert ? , populate ? ) { const entities = await this . find ( type , where as FilterOf < T > ) ; let entity ; if ( entities . length > 1 ) { throw new TooManyError ( ) ; } else if ( entities . length === 1 ) { entity = entities [ 0 ] ; } else { entity = this . create ( type , { ... where , ... ifNew } as OptsOf < T > ) ; } if ( upsert ) { entity . set ( upsert ) ; } if ( populate ) { await this . populate ( entity , populate ) ; } return entity ; } /** Creates a new `type` and marks it as loaded, i.e. we know its collections are all safe to access in memory. */ public create < T extends Entity , O extends OptsOf < T > > ( type , opts ) { return new type ( this , opts ) as New < T , O > ; } /** Creates a new `type` but with `opts` that are nullable, to accept partial-update-style input. */ public createPartial < T extends Entity > ( type , opts ) { const entity = new type ( this , undefined ! ) ; setOpts ( entity , opts as OptsOf < T > , { partial : true , calledFromConstructor : true } ) ; return entity ; } /** Creates a new `type` but with `opts` that are nullable, to accept partial-update-style input. */ public createOrUpdatePartial < T extends Entity > ( type , opts ) { return createOrUpdatePartial ( this , type , opts ) ; } param entity param hint example example exmaple public async clone < T extends Entity , H extends LoadHint < T > > ( entity , hint ? ) { const meta = getMetadata ( entity ) ; const { id , ... data } = entity . __orm . data ; const clone = this . create ( meta . cstr , { } as OptsOf < T > ) ; clone . __orm . data = data ; if ( hint ) { if ( ( typeof hint as any ) === "string" ) { hint = { [ hint as any ] : { } } as any ; } else if ( Array . isArray ( hint ) ) { hint = Object . fromEntries ( hint . map ( ( relation ) => [ relation , { } ] ) ) ; } await Promise . all ( ( Object . entries ( hint as NestedLoadHint < T > ) as [ keyof RelationsIn < T > , LoadHint < any > ] [ ] ) . map ( async ( [ relationName , nested ] ) => { const relation = entity [ relationName ] as any as OneToManyCollection < T , any > | OneToOneReferenceImpl < T , any > | ManyToOneReferenceImpl < T , any , undefined | never > ; if ( relation instanceof OneToManyCollection ) { const relatedEntities = await relation . load ( ) ; await Promise . all ( relatedEntities . map ( async ( related ) => { const clonedRelated = await this . clone ( related , nested ) ; clonedRelated . __orm . data [ relation . otherFieldName ] = undefined ; const relationToClone = clonedRelated [ relation . otherFieldName ] as Reference < Entity , T , undefined > ; relationToClone . set ( clone ) ; } ) ) ; } else if ( relation instanceof OneToOneReferenceImpl ) { const related = await relation . load ( ) ; if ( related ) { const clonedRelated = await this . clone ( related , nested ) ; clonedRelated . __orm . data [ relation . otherFieldName ] = undefined ; const relationToClone = clone [ relation . fieldName ] as any ; relationToClone . set ( clonedRelated ) ; } } else if ( relation instanceof ManyToOneReferenceImpl ) { const related = await relation . load ( ) ; if ( related ) { const clonedRelated = await this . clone ( related , nested ) ; clone . __orm . data [ relationName ] = undefined ; const relationToClone = clone [ relationName ] as any ; relationToClone . set ( clonedRelated ) ; } } else { fail ( `Uncloneable relation: ${ relationName }` ) ; } } ) ) ; } return clone as Loaded < T , H > ; } /** Returns an instance of `type` for the given `id`, resolving to an existing instance if in our Unit of Work. */ public async load ( id ) ; public async load < T extends Entity > ( type , id ) ; public async load < T extends Entity , H extends LoadHint < T > > ( type , id , populate ) ; public async load < T extends Entity , H extends LoadHint < T > > ( type , id , populate ) ; async load < T extends Entity > ( typeOrId , id ? , hint ? ) { let type ; if ( typeof typeOrId === "string" ) { type = getConstructorFromTaggedId ( typeOrId ) ; id = typeOrId ; } else { type = typeOrId ; id = id || fail ( ) ; } if ( typeof ( id as any ) !== "string" ) { throw new Error ( `Expected ${ id } to be a string` ) ; } const meta = getMetadata ( type ) ; const tagged = tagIfNeeded ( meta , id ) ; const entity = this . findExistingInstance < T > ( tagged ) || ( await loadDataLoader ( this , meta ) . load ( tagged ) ) ; if ( ! entity ) { throw new Error ( `${ tagged } was not found` ) ; } if ( hint ) { await this . populate ( entity , hint ) ; } return entity ; } /** Returns instances of `type` for the given `ids`, resolving to an existing instance if in our Unit of Work. */ public async loadAll < T extends Entity > ( type , ids ) ; public async loadAll < T extends Entity , H extends LoadHint < T > > ( type , ids , populate ) ; async loadAll < T extends Entity > ( type , _ids , hint ? ) { const meta = getMetadata ( type ) ; const ids = _ids . map ( ( id ) => tagIfNeeded ( meta , id ) ) ; const entities = await Promise . all ( ids . map ( ( id ) => { return this . findExistingInstance ( id ) || loadDataLoader ( this , meta ) . load ( id ) ; } ) ) ; const idsNotFound = ids . filter ( ( id , i ) => entities [ i ] === undefined ) ; if ( idsNotFound . length > 0 ) { throw new Error ( `${ idsNotFound . join ( "," ) } were not found` ) ; } if ( hint ) { await this . populate ( entities as T [ ] , hint ) ; } return entities as T [ ] ; } /**
     * Returns instances of `type` for the given `ids`, resolving to an existing instance if in our Unit of Work. Ignores
     * IDs that are not found.
     */ public async loadAllIfExists < T extends Entity > ( type , ids ) ; public async loadAllIfExists < T extends Entity , H extends LoadHint < T > > ( type , ids , populate ) ; async loadAllIfExists < T extends Entity > ( type , _ids , hint ? ) { const meta = getMetadata ( type ) ; const ids = _ids . map ( ( id ) => tagIfNeeded ( meta , id ) ) ; const entities = ( await Promise . all ( ids . map ( ( id ) => { return this . findExistingInstance ( id ) || loadDataLoader ( this , meta ) . load ( id ) ; } ) ) ) . filter ( Boolean ) ; if ( hint ) { await this . populate ( entities as T [ ] , hint ) ; } return entities as T [ ] ; } /** Loads entities from a knex QueryBuilder. */ public async loadFromQuery < T extends Entity > ( type , query ) ; public async loadFromQuery < T extends Entity , H extends LoadHint < T > > ( type , query , populate ) ; public async loadFromQuery < T extends Entity > ( type , query , populate ? ) { const rows = await query ; const entities = rows . map ( ( row ) => this . hydrate ( type , row , { overwriteExisting : false } ) ) ; if ( populate ) { await this . populate ( entities , populate ) ; } return entities ; } /** Given a hint `H` (a field, array of fields, or nested hash), pre-load that data into `entity` for sync access. */ public async populate < T extends Entity , H extends LoadHint < T > > ( entity , hint ) ; public async populate < T extends Entity , H extends LoadHint < T > > ( entities , hint ) ; async populate < T extends Entity , H extends LoadHint < T > > ( entityOrList , hint ) { const list = toArray ( entityOrList ) ; const promises = list . filter ( ( e ) => e !== undefined && ( e . isPendingDelete || ! e . isDeletedEntity ) ) . flatMap ( ( entity ) => { if ( typeof hint === "string" ) { return ( entity as any ) [ hint ] . load ( ) ; } else if ( Array . isArray ( hint ) ) { return ( hint as string [ ] ) . map ( ( key ) => ( entity as any ) [ key ] . load ( ) ) ; } else if ( typeof hint === "object" ) { return Object . entries ( hint as object ) . map ( async ( [ key , nestedHint ] ) => { const relation = ( entity as any ) [ key ] ; const result = await relation . load ( ) ; return this . populate ( result , nestedHint ) ; } ) ; } else { throw new Error ( `Unexpected hint ${ hint }` ) ; } } ) ; await Promise . all ( promises ) ; return entityOrList as any ; } /**
     * Executes `fn` with a transaction, and automatically calls `flush`/`commit` at the end.
     *
     * This ensures both any `.find` as well as `.flush` operations happen within the same
     * transaction, which is useful for enforcing cross-table/application-level invariants that
     * cannot be enforced with database-level constraints.
     */ public async transaction < T > ( fn ) { return this . driver . transaction ( this , async ( knex ) => { const result = await fn ( knex ) ; await this . flush ( ) ; return result ; } , "serializable" ) ; } /** Registers a newly-instantiated entity with our EntityManager; only called by entity constructors. */ register ( meta , entity ) { if ( entity . id && this . findExistingInstance ( entity . id ) !== undefined ) { throw new Error ( `Entity ${ entity } has a duplicate instance already loaded` ) ; } entity . __orm . data [ "createdAt" ] = new Date ( ) ; entity . __orm . data [ "updatedAt" ] = new Date ( ) ; this . _entities . push ( entity ) ; if ( entity . id ) { assertIdsAreTagged ( [ entity . id ] ) ; this . _entityIndex . set ( entity . id , entity ) ; } if ( this . _entities . length >= entityLimit ) { throw new Error ( `More than ${ entityLimit } entities have been instantiated` ) ; } currentlyInstantiatingEntity = entity ; } /**
     * Marks an instance to be deleted.
     *
     * Any loaded collections that are currently "pointing to" this entity will be updated to
     * no longer include this entity, i.e. if you `em.delete(b1)`, then `author.books` will have
     * `b1` removed (if needed).
     *
     * This is done for all currently-loaded collections; i.e. technically unloaded collections
     * may still point to this entity. We defer unsetting these not-currently-loaded references
     * until `EntityManager.flush`, when we can make the async calls to load-and-unset them.
     */ delete ( deletedEntity ) { if ( deletedEntity . __orm . deleted ) { return ; } deletedEntity . __orm . deleted = "pending" ; getRelations ( deletedEntity ) . forEach ( ( relation ) => relation . maybeCascadeDelete ( ) ) ; } /**
     * Flushes the SQL for any changed entities to the database.
     *
     * If this is run outside of an existing transaction, it will `BEGIN` and `COMMIT`
     * a new transaction on every `.flush()` call so that all of the `INSERT`s/etc.
     * happen atomically.
     *
     * If this is run within an existing transaction, i.e. `EntityManager.transaction`,
     * then it will only issue `INSERT`s/etc. and defer to the caller to `COMMIT`
     * the transaction.
     *
     * It returns entities that have changed (an entity is considered changed if it has been deleted, inserted, or updated)
     */ async flush ( flushOptions = { } ) { const { skipValidation = false } = flushOptions ; if ( this . isFlushing ) { throw new Error ( "Cannot flush while another flush is already in progress" ) ; } this . _isFlushing = true ; const entitiesToFlush = [ ] ; let pendingEntities = this . entities . filter ( ( e ) => e . isPendingFlush ) ; try { while ( pendingEntities . length > 0 ) { await currentFlushSecret . run ( { flushSecret : this . flushSecret } , async ( ) => { const todos = createTodos ( pendingEntities ) ; await addReactiveAsyncDerivedValues ( todos ) ; await addReactiveValidations ( todos ) ; await beforeDelete ( this . ctx , todos ) ; await cleanupDeletedRelations ( todos ) ; await beforeFlush ( this . ctx , todos ) ; await beforeCreate ( this . ctx , todos ) ; await beforeUpdate ( this . ctx , todos ) ; recalcDerivedFields ( todos ) ; await recalcAsyncDerivedFields ( this , todos ) ; if ( ! skipValidation ) { await validate ( todos ) ; await afterValidation ( this . ctx , todos ) ; } entitiesToFlush . push ( ... pendingEntities ) ; pendingEntities = this . entities . filter ( ( e ) => e . isPendingFlush && ! entitiesToFlush . includes ( e ) ) ; this . flushSecret += 1 ; } ) ; } const entityTodos = createTodos ( entitiesToFlush ) ; const joinRowTodos = combineJoinRows ( this . __data . joinRows ) ; if ( Object . keys ( entityTodos ) . length > 0 || Object . keys ( joinRowTodos ) . length > 0 ) { await this . driver . transaction ( this , async ( ) => { await this . driver . flushEntities ( this , entityTodos ) ; await this . driver . flushJoinTables ( this , joinRowTodos ) ; } ) ; await afterCommit ( this . ctx , entityTodos ) ; Object . values ( entityTodos ) . forEach ( ( todo ) => { todo . inserts . forEach ( ( e ) => { this . _entityIndex . set ( e . id ! , e ) ; e . __orm . isNew = false ; } ) ; [ todo . inserts , todo . updates , todo . deletes ] . flat ( ) . forEach ( ( e ) => { e . __orm . originalData = { } ; e . __orm . isTouched = false ; } ) ; } ) ; this . loadLoaders = { } ; this . findLoaders = { } ; } return entitiesToFlush ; } finally { this . _isFlushing = false ; } } get isFlushing ( ) : boolean { return this . _isFlushing ; } /**
     * A very simple toJSON.
     *
     * This is not really meant to be useful, it's to prevent huge/circular output if
     * an EntityManager accidentally ends up getting logged to something like pino that
     * over-zealous toJSONs anything it touches.
     */ public toJSON ( ) { return `<EntityManager ${ this . entities . length }>` ; } /**
     * For all entities in the current `EntityManager`, load their latest data from the database.
     *
     * This is primarily useful in tests, i.e. having 1 `EntityManager` with some test data, running business
     * logic in a dedicated `EntityManager`, and then `refresh`-ing the test data `EntityManager` to assert
     * against the latest values.
     *
     * This works with primitive fields as well as references and collections.
     *
     * TODO Newly-found collection entries will not have prior load hints applied to this.
     */ async refresh ( ) ; async refresh ( entity ) ; async refresh ( entities ) ; async refresh ( entityOrListOrUndefined ? ) { this . loadLoaders = { } ; this . findLoaders = { } ; const list = entityOrListOrUndefined === undefined ? this . _entities : Array . isArray ( entityOrListOrUndefined ) ? entityOrListOrUndefined : [ entityOrListOrUndefined ] ; await Promise . all ( list . map ( async ( entity ) => { if ( entity . id ) { await loadDataLoader ( this , getMetadata ( entity ) ) . load ( entity . id ) ; if ( entity . __orm . deleted === undefined ) { await Promise . all ( getRelations ( entity ) . map ( ( r ) => r . refreshIfLoaded ( ) ) ) ; } } } ) ) ; } public get numberOfEntities ( ) : number { return this . entities . length ; } public findExistingInstance < T > ( id ) { assertIdsAreTagged ( [ id ] ) ; return this . _entityIndex . get ( id ) as T | undefined ; } /**
     * Takes a result `row` from a custom query and maps the db values into a new-or-existing domain object for that row.
     *
     * The `overwriteExisting` controls whether `row`'s values should overwrite the existing fields on
     * an entity. By default this is true, as we assume the user calling this means they know the DB has
     * updated values that should be put into the entities. A few internal callers set this to false,
     * i.e. when we're loading collections and have db results that are potentially stale compared to
     * the WIP entity state.
     */ public hydrate < T extends Entity > ( type , row , options ? ) { const meta = getMetadata ( type ) ; const id = keyToString ( meta , row [ "id" ] ) || fail ( "No id column was available" ) ; let entity = this . findExistingInstance ( id ) as T ; if ( ! entity ) { entity = new type ( this , id ) ; Object . values ( meta . fields ) . forEach ( ( f ) => f . serde ?. setOnEntity ( entity ! . __orm . data , row ) ) ; } else if ( options ?. overwriteExisting !== false ) { Object . values ( meta . fields ) . forEach ( ( f ) => f . serde ?. setOnEntity ( entity ! . __orm . data , row ) ) ; } return entity ; } /**
     * Mark an entity as needing to be flushed regardless of its state
     */ public touch ( entity ) { entity . __orm . isTouched = true ; } public beforeTransaction ( fn ) { this . hooks . beforeTransaction . push ( fn ) ; } public afterTransaction ( fn ) { this . hooks . afterTransaction . push ( fn ) ; } public toString ( ) { return "EntityManager" ; } } export let entityLimit = 10000 ; export function setEntityLimit ( limit ) { entityLimit = limit ; } export function setDefaultEntityLimit ( ) { entityLimit = 10000 ; } export interface EntityMetadata < T extends Entity > { cstr : EntityConstructor < T > ; type : string ; idType : "int" | "uuid" ; tableName : string ; tagName : string ; fields : Record < string , Field > ; config : ConfigApi < T , any > ; factory : ( em : EntityManager , opts ? : any ) => New < T > ; } export type Field = PrimaryKeyField | PrimitiveField | EnumField | OneToManyField | ManyToOneField | ManyToManyField | OneToOneField | PolymorphicField ; export type SerdeField = PrimaryKeyField | PrimitiveField | EnumField | ManyToOneField | PolymorphicField ; export type PrimaryKeyField = { kind : "primaryKey" ; fieldName : string ; fieldIdName : undefined ; required : true ; serde : FieldSerde ; } ; export type PrimitiveField = { kind : "primitive" ; fieldName : string ; fieldIdName : undefined ; required : boolean ; derived : "orm" | "sync" | "async" | false ; protected : boolean ; type : string | Function ; serde : FieldSerde ; } ; export type EnumField = { kind : "enum" ; fieldName : string ; fieldIdName : undefined ; required : boolean ; enumDetailType : { getValues ( ) : ReadonlyArray < unknown > ; } ; serde : FieldSerde ; } ; export type OneToManyField = { kind : "o2m" ; fieldName : string ; fieldIdName : string ; required : boolean ; otherMetadata : ( ) => EntityMetadata < any > ; otherFieldName : string ; serde : undefined ; } ; export type ManyToOneField = { kind : "m2o" ; fieldName : string ; fieldIdName : string ; required : boolean ; otherMetadata : ( ) => EntityMetadata < any > ; otherFieldName : string ; serde : FieldSerde ; } ; export type ManyToManyField = { kind : "m2m" ; fieldName : string ; fieldIdName : string ; required : boolean ; otherMetadata : ( ) => EntityMetadata < any > ; otherFieldName : string ; serde : undefined ; } ; export type OneToOneField = { kind : "o2o" ; fieldName : string ; fieldIdName : string ; required : boolean ; otherMetadata : ( ) => EntityMetadata < any > ; otherFieldName : string ; serde : undefined ; } ; export type PolymorphicField = { kind : "poly" ; fieldName : string ; fieldIdName : string ; required : boolean ; components : PolymorphicFieldComponent [ ] ; serde : PolymorphicKeySerde ; } ; export type PolymorphicFieldComponent = { otherMetadata : ( ) => EntityMetadata < any > ; otherFieldName : string ; columnName : string ; } ; export function isEntity ( maybeEntity ) { return maybeEntity && typeof maybeEntity === "object" && "id" in maybeEntity && "__orm" in maybeEntity ; } export function isKey ( k ) { return typeof k === "string" ; } /** Compares `a` to `b`, where `b` might be an id. B/c ids can overlap, we need to know `b`'s metadata type. */ export function sameEntity ( a , bMeta , bCurrent ) { if ( a === undefined || bCurrent === undefined ) { return false ; } return ( a === bCurrent || ( getMetadata ( a ) === bMeta && maybeResolveReferenceToId ( a ) === maybeResolveReferenceToId ( bCurrent ) ) ) ; } export function getMetadata < T extends Entity > ( entity ) ; export function getMetadata < T extends Entity > ( type ) ; export function getMetadata < T extends Entity > ( entityOrType ) { return ( typeof entityOrType === "function" ? ( entityOrType as any ) . metadata : entityOrType . __orm . metadata ) as EntityMetadata < T > ; } /** Thrown by `findOneOrFail` if an entity is not found. */ export class NotFoundError extends Error { } /** Thrown by `findOne` and `findOneOrFail` if more than one entity is found. */ export class TooManyError extends Error { } /**
 * For the entities currently in `todos`, find any reactive validation rules that point
 * from the currently-changed entities back to each rule's originally-defined-in entity,
 * and ensure those entities are added to `todos`.
 *
 * Note that we don't check whether `entitiesToFlush` already the entities we're adding,
 * because rules should be side effect free, so invoking them twice, if that does happen
 * to occur, should be fine (and desirable given something about the entity has changed).
 */ async function addReactiveValidations ( todos ) { const p = Object . values ( todos ) . flatMap ( ( todo ) => { const entities = [ ... todo . inserts , ... todo . updates , ... todo . deletes ] ; return todo . metadata . config . __data . reactiveRules . map ( async ( reverseHint ) => { ( await followReverseHint ( entities , reverseHint ) ) . forEach ( ( entity ) => { const todo = getTodo ( todos , entity ) ; if ( ! todo . inserts . includes ( entity ) && ! todo . updates . includes ( entity ) && ! entity . isDeletedEntity ) { todo . validates . push ( entity ) ; } } ) ; } ) ; } ) ; await Promise . all ( p ) ; } /**
 * Given the current changed entities in `todos`, use the static metadata of `reactiveDerivedValues`
 * to find any potentially-unloaded entities we should now re-calc, and add them to `todos`.
 */ async function addReactiveAsyncDerivedValues ( todos ) { const p = Object . values ( todos ) . flatMap ( ( todo ) => { const entities = [ ... todo . inserts , ... todo . updates ] ; return todo . metadata . config . __data . reactiveDerivedValues . map ( async ( reverseHint ) => { ( await followReverseHint ( entities , reverseHint ) ) . forEach ( ( entity ) => { const todo = getTodo ( todos , entity ) ; if ( ! todo . inserts . includes ( entity ) && ! todo . updates . includes ( entity ) && ! entity . isDeletedEntity ) { todo . updates . push ( entity ) ; } } ) ; } ) ; } ) ; await Promise . all ( p ) ; } /** Find all deleted entities and ensure their references all know about their deleted-ness. */ async function cleanupDeletedRelations ( todos ) { const entities = Object . values ( todos ) . flatMap ( ( todo ) => todo . deletes ) ; await Promise . all ( entities . flatMap ( getRelations ) . map ( ( relation ) => relation . cleanupOnEntityDeleted ( ) ) ) ; } async function validate ( todos ) { const p = Object . values ( todos ) . flatMap ( ( todo ) => { const rules = todo . metadata . config . __data . rules ; return [ ... todo . inserts , ... todo . updates , ... todo . validates ] . filter ( ( e ) => ! e . isDeletedEntity ) . flatMap ( ( entity ) => { return rules . flatMap ( async ( rule ) => coerceError ( entity , await rule ( entity ) ) ) ; } ) ; } ) ; const errors = ( await Promise . all ( p ) ) . flat ( ) ; if ( errors . length > 0 ) { throw new ValidationErrors ( errors ) ; } } export function beforeTransaction ( em , knex ) { return Promise . all ( em [ "hooks" ] . beforeTransaction . map ( ( fn ) => fn ( em , knex ) ) ) ; } export function afterTransaction ( em , knex ) { return Promise . all ( em [ "hooks" ] . afterTransaction . map ( ( fn ) => fn ( em , knex ) ) ) ; } async function runHook ( ctx , hook , todos , keys ) { const p = Object . values ( todos ) . flatMap ( ( todo ) => { const hookFns = todo . metadata . config . __data . hooks [ hook ] ; return keys . flatMap ( ( k ) => todo [ k ] . filter ( ( e ) => k === "deletes" || ! e . isDeletedEntity ) ) . flatMap ( ( entity ) => { return hookFns . map ( async ( fn ) => fn ( entity , ctx as any ) ) ; } ) ; } ) ; const rejects = ( await Promise . allSettled ( p ) ) . filter ( ( r ) => r . status === "rejected" ) ; if ( rejects . length > 0 && rejects [ 0 ] . status === "rejected" ) { throw rejects [ 0 ] . reason ; } } function beforeDelete ( ctx , todos ) { return runHook ( ctx , "beforeDelete" , todos , [ "deletes" ] ) ; } function beforeFlush ( ctx , todos ) { return runHook ( ctx , "beforeFlush" , todos , [ "inserts" , "updates" ] ) ; } function beforeCreate ( ctx , todos ) { return runHook ( ctx , "beforeCreate" , todos , [ "inserts" ] ) ; } function beforeUpdate ( ctx , todos ) { return runHook ( ctx , "beforeUpdate" , todos , [ "updates" ] ) ; } function afterValidation ( ctx , todos ) { return runHook ( ctx , "afterValidation" , todos , [ "inserts" , "updates" ] ) ; } function afterCommit ( ctx , todos ) { return runHook ( ctx , "afterCommit" , todos , [ "inserts" , "updates" ] ) ; } function coerceError ( entity , maybeError ) { if ( maybeError === undefined ) { return [ ] ; } else if ( typeof maybeError === "string" ) { return [ { entity , message : maybeError } ] ; } else if ( Array . isArray ( maybeError ) ) { return ( maybeError as GenericError [ ] ) . map ( ( ve ) => ( { entity , ... ve } ) ) ; } else { return [ { entity , ... maybeError } ] ; } } type Narrowable = string | number | boolean | symbol | object | undefined | void | null | { } | [ ] ; type Const < N > = N | { [ K in keyof N ] : N [ K ] extends Narrowable ? N [ K ] | Const < N [ K ] > : never ; } ; /**
 * Evaluates each derived field to see if it's value has changed.
 *
 * This is a) not at all reactive, b) only works for primitives, c) doesn't work
 * with async/promise-based logic, and d) doesn't support passing an app-specific
 * context, but it's a start.
 */ function recalcDerivedFields ( todos ) { const entities = Object . values ( todos ) . flatMap ( ( todo ) => [ ... todo . inserts , ... todo . updates ] ) . filter ( ( e ) => ! e . isDeletedEntity ) ; const derivedFieldsByMeta = new Map ( [ ... new Set ( entities . map ( getMetadata ) ) ] . map ( ( m ) => { return [ m , Object . values ( m . fields ) . filter ( ( f ) => f . kind === "primitive" && f . derived === "sync" ) . map ( ( f ) => f . fieldName ) , ] ; } ) ) ; for ( const entity of entities ) { const derivedFields = derivedFieldsByMeta . get ( entity . __orm . metadata ) ; derivedFields ?. forEach ( ( fieldName ) => { setField ( entity , fieldName as any , ( entity as any ) [ fieldName ] ) ; } ) ; } } /**
 * Calcs async derived fields for inserts and updates.
 *
 * We assume that `addReactiveAsyncDerivedValues` has already found any "reactive"
 * entities that need fields re-calced, and has already added them to `todos`.
 */ async function recalcAsyncDerivedFields ( em , todos ) { const p = Object . values ( todos ) . map ( async ( todo ) => { const { asyncDerivedFields } = todo . metadata . config . __data ; const changed = [ ... todo . inserts , ... todo . updates ] ; const p = Object . entries ( asyncDerivedFields ) . map ( async ( [ key , entry ] ) => { if ( entry ) { const [ hint , fn ] = entry ; await em . populate ( changed , hint ) ; await Promise . all ( changed . map ( ( entity ) => setField ( entity , key as any , fn ( entity ) ) ) ) ; } } ) ; await Promise . all ( p ) ; } ) ; await Promise . all ( p ) ; } /**
 * Walks `reverseHint` for every entity in `entities`.
 *
 * I.e. given `[book1, book2]` and `["author", 'publisher"]`, will return all of the books' authors' publishers.
 */ async function followReverseHint ( entities , reverseHint ) { let current = [ ... entities ] ; const paths = [ ... reverseHint ] ; while ( paths . length ) { const fieldName = paths . shift ( ) ! ; const entitiesOrLists = await Promise . all ( current . flatMap ( ( c ) => { const currentValuePromise = ( c as any ) [ fieldName ] . load ( ) ; const isReference = getMetadata ( c ) . fields [ fieldName ] ?. kind === "m2o" ; const hasChanged = isReference && ( c as any ) . changes [ fieldName ] . hasChanged ; const originalValue = ( c as any ) . changes [ fieldName ] . originalValue ; if ( hasChanged && originalValue ) { const originalEntityMaybePromise = isEntity ( originalValue ) ? originalValue : getEm ( c ) . load ( ( c as any ) [ fieldName ] . otherMeta . cstr , originalValue ) ; return [ currentValuePromise , originalEntityMaybePromise ] ; } return [ currentValuePromise ] ; } ) ) ; const entities = entitiesOrLists . flat ( ) . filter ( ( e ) => e !== undefined ) ; current = entities as Entity [ ] ; } return current ; }