import { Entity , IdOf } from "../EntityManager" ; import { ensureNotDeleted , fail , Reference , unsafeDeTagIds } from "../index" ; import { AbstractRelationImpl } from "./AbstractRelationImpl" ; import { ReferenceN } from "./Reference" ; import { RelationT , RelationU } from "./Relation" ; export type CustomReferenceOpts < T extends Entity , U extends Entity , N extends never | undefined > = { load : ( entity : T ) => Promise < void > ; get : ( entity : T ) => U | N ; set ? : ( entity : T , other : U ) => void ; } ; /**
 * Allows user-defined references that will work in `populate` / preload hints.
 *
 * This works because Joist's `populate`/preloading is not based on creating giant SQL joins,
 * but instead by just walking "1 dataloader per level". Given this, we don't really care
 * what happens at each "level" of resolution, as long as: a) it's a promise, and b) the
 * caller uses dataloader to be batch friendly.
 *
 * This `CustomReference` API is fairly low-level; users should more likely prefer higher-level
 * abstractions like `hasOneThrough`, which are built on `CustomReference.
 */ export class CustomReference < T extends Entity , U extends Entity , N extends never | undefined > extends AbstractRelationImpl < U > implements Reference < T , U , N > { private loadPromise : Promise < void > | undefined ; private _isLoaded = false ; constructor ( public entity , private opts ) { super ( ) ; } get get ( ) : U | N { return this . doGet ( { withDeleted : false } ) ; } get getWithDeleted ( ) : U | N { return this . doGet ( { withDeleted : true } ) ; } get isLoaded ( ) : boolean { return this . _isLoaded ; } async load ( opts ? ) { ensureNotDeleted ( this . entity , { ignore : "pending" } ) ; if ( ! this . isLoaded ) { if ( this . loadPromise === undefined ) { this . loadPromise = this . opts . load ( this . entity ) ; await this . loadPromise ; this . loadPromise = undefined ; this . _isLoaded = true ; } else { await this . loadPromise ; } } return this . doGet ( opts ) ; } initializeForNewEntity ( ) { } get id ( ) : IdOf < U > | undefined { return fail ( `CustomReference cannot resolve 'id'` ) ; } get idOrFail ( ) : IdOf < U > { return fail ( `CustomReference cannot resolve 'idOrFail'` ) ; } get idUntagged ( ) : string | undefined { return this . id && unsafeDeTagIds ( [ this . id ] ) [ 0 ] ; } get idUntaggedOrFail ( ) : string { return this . idUntagged || fail ( "Reference is unset or assigned to a new entity" ) ; } get isSet ( ) : boolean { ensureNewOrLoaded ( this ) ; const { get } = this . opts ; return get ( this . entity ) !== undefined ; } set ( value ) { ensureNewOrLoaded ( this ) ; const { set } = this . opts ; if ( set === undefined ) { throw new Error ( `'set' not implemented on ${ this }` ) ; } set ( this . entity , value ) ; } setFromOpts ( value ) { this . set ( value ) ; } async cleanupOnEntityDeleted ( ) { } maybeCascadeDelete ( ) { } async refreshIfLoaded ( ) { } /** Finds this CustomReferences field name by looking in the entity for the key that we're assigned to. */ get fieldName ( ) : string { return Object . entries ( this . entity ) . filter ( ( e ) => e [ 1 ] === this ) [ 0 ] [ 0 ] ; } toString ( ) { return `CustomReference(entity: ${ this . entity }, fieldName: ${ this . fieldName })` ; } private doGet ( opts ? ) { ensureNotDeleted ( this . entity , { ignore : "pending" } ) ; ensureNewOrLoaded ( this ) ; return this . filterDeleted ( this . opts . get ( this . entity ) , opts ) ; } private filterDeleted ( entity , opts ? ) { return opts ?. withDeleted === true || entity === undefined || ! entity . isDeletedEntity ? entity : ( undefined as N ) ; } [ RelationT ] : T = null ! ; [ RelationU ] : U = null ! ; [ ReferenceN ] : N = null ! ; } function ensureNewOrLoaded ( reference ) { if ( ! ( reference . isLoaded || reference . entity . isNewEntity ) ) { throw new Error ( `${ reference . entity }.${ reference . fieldName } was not loaded` ) ; } }