import { currentlyInstantiatingEntity , deTagIds , ensureNotDeleted , fail , getEm , IdOf , LoadedReference , setField , } from "../" ; import { oneToOneDataLoader } from "../dataloaders/oneToOneDataLoader" ; import { Entity , EntityMetadata , getMetadata } from "../EntityManager" ; import { AbstractRelationImpl } from "./AbstractRelationImpl" ; import { ManyToOneReference } from "./ManyToOneReference" ; import { Reference , ReferenceN } from "./Reference" ; import { RelationT , RelationU } from "./Relation" ; const OneToOne = Symbol ( ) ; /** The lazy-loaded/lookup side of a one-to-one, i.e. the side w/o the unique foreign key column. */ export interface OneToOneReference < T extends Entity , U extends Entity > extends Reference < T , U , undefined > { [ OneToOne ] : T ; } /** Adds a known-safe `get` accessor. */ export interface LoadedOneToOneReference < T extends Entity , U extends Entity > extends LoadedReference < T , U , undefined > { get : U | undefined ; getWithDeleted : U | undefined ; /** Returns the id of the current assigned entity or a runtime error if it's either a) unset or b) set to a new entity that doesn't have an `id` yet. */ idOrFail : IdOf < U > ; idUntagged : string | undefined ; idUntaggedOrFail : string ; /** Returns `true` if this relation is currently set (i.e. regardless of whether it's loaded, or if it is set but the assigned entity doesn't have an id saved. */ readonly isSet : boolean ; } /** Type guard utility for determining if an entity field is a Reference. */ export function isOneToOneReference ( maybeReference ) { return maybeReference instanceof OneToOneReferenceImpl ; } /** Type guard utility for determining if an entity field is a loaded Reference. */ export function isLoadedOneToOneReference ( maybeReference ) { return isOneToOneReference ( maybeReference ) && maybeReference . isLoaded ; } /** An alias for creating `OneToOneReference`s. */ export function hasOneToOne < T extends Entity , U extends Entity > ( otherMeta , fieldName , otherFieldName , otherColumnName ) { const entity = currentlyInstantiatingEntity as T ; return new OneToOneReferenceImpl < T , U > ( entity , otherMeta , fieldName , otherFieldName , otherColumnName ) ; } /**
 * Represents the "many" side of a one-to-one relationship.
 *
 * I.e. in a one-to-many from Book -> Reviews, there is a review.book_id that can have many books.
 *
 * This class is for when that `review.book_id` column is itself unique, i.e. like `image.book_id`, and
 * so instead of `Book.images: OneToManyCollection` we have a `Book.image: OneToOneReference`.
 *
 * This class implements `Reference` because it is essentially like "one entity pointing/refereing to another",
 * however because we require a `.load` call to lazily know the value of other side (unlike ManyToOneReference
 * which has it's `book_id` column immediately available in the entity `data` hash), there is some wonkiness
 * around methods like `Reference.id` that are usually callable without `load`/`populate`, that for this
 * class can actually only be called post `load`/`populate`.
 *
 * Currently we enforce this with a runtime check, which is not great, but the trade-off of implementing
 * `Reference` seemed worth the downside of a un-type-safe `.id` property.
 */ export class OneToOneReferenceImpl < T extends Entity , U extends Entity > extends AbstractRelationImpl < U > implements OneToOneReference < T , U > { private loaded : U | undefined ; private _isLoaded : boolean = false ; private isCascadeDelete : boolean ; constructor ( public entity , public otherMeta , public fieldName , public otherFieldName , public otherColumnName ) { super ( ) ; this . isCascadeDelete = getMetadata ( entity ) . config . __data . cascadeDeleteFields . includes ( fieldName as any ) ; } get id ( ) : IdOf < U > | undefined { if ( this . _isLoaded ) { return this . loaded ?. id as IdOf < U > | undefined ; } throw new Error ( `${ this . entity }.${ this . fieldName } was not loaded` ) ; } get idOrFail ( ) : IdOf < U > { return this . id || fail ( `${ this . entity }.${ this . fieldName } has no id yet` ) ; } get idUntagged ( ) : string | undefined { return this . id && deTagIds ( this . otherMeta , [ this . id ] ) [ 0 ] ; } get idUntaggedOrFail ( ) : string { return this . idUntagged || fail ( "Reference is unset or assigned to a new entity" ) ; } get isSet ( ) : boolean { if ( this . _isLoaded ) { return this . loaded !== undefined ; } throw new Error ( `${ this . entity }.${ this . fieldName } was not loaded` ) ; } async load ( opts ? ) { ensureNotDeleted ( this . entity , { ignore : "pending" } ) ; if ( ! this . _isLoaded ) { if ( ! this . entity . isNewEntity ) { this . loaded = await oneToOneDataLoader ( getEm ( this . entity ) , this ) . load ( this . entity . idOrFail ) ; } this . _isLoaded = true ; } return this . filterDeleted ( this . loaded , opts ) ; } set ( other ) { ensureNotDeleted ( this . entity , { ignore : "pending" } ) ; if ( other === this . loaded ) { return ; } if ( this . _isLoaded ) { if ( this . loaded ) { this . getOtherRelation ( this . loaded ) . set ( undefined ) ; } } this . loaded = other ; this . _isLoaded = true ; if ( other ) { this . getOtherRelation ( other ) . set ( this . entity ) ; } } get isLoaded ( ) : boolean { return this . _isLoaded ; } get getWithDeleted ( ) : U | undefined { return this . filterDeleted ( this . doGet ( ) , { withDeleted : true } ) ; } get get ( ) : U | undefined { return this . filterDeleted ( this . doGet ( ) , { withDeleted : false } ) ; } private doGet ( ) { ensureNotDeleted ( this . entity , { ignore : "pending" } ) ; if ( ! this . _isLoaded ) { throw new Error ( "get was called when not preloaded" ) ; } return this . loaded ; } setFromOpts ( other ) { this . set ( other ) ; } initializeForNewEntity ( ) { this . _isLoaded = true ; } async refreshIfLoaded ( ) { if ( this . _isLoaded ) { this . _isLoaded = false ; await this . load ( ) ; } } maybeCascadeDelete ( ) { if ( this . isCascadeDelete && this . loaded ) { getEm ( this . entity ) . delete ( this . loaded ) ; } } async cleanupOnEntityDeleted ( ) { const current = await this . load ( { withDeleted : true } ) ; if ( current !== undefined ) { this . getOtherRelation ( current ) . set ( undefined as any ) ; setField ( current , this . otherFieldName , undefined ) ; } this . loaded = undefined as any ; this . _isLoaded = true ; } public toString ( ) { return `OneToOneReference(entity: ${ this . entity }, fieldName: ${ this . fieldName }, otherType: ${ this . otherMeta . type }, otherFieldName: ${ this . otherFieldName })` ; } private filterDeleted ( entity , opts ? ) { return opts ?. withDeleted === true || entity === undefined || ! entity . isDeletedEntity ? entity : undefined ; } /** Returns the other relation that points back at us, i.e. we're `Author.image` and this is `Image.author_id`. */ private getOtherRelation ( other ) { return ( other as U ) [ this . otherFieldName ] as any ; } [ RelationT ] = null ! ; [ RelationU ] = null ! ; [ ReferenceN ] = null ! ; [ OneToOne ] = null ! ; }