import { Observability } from "../mobx_internal"; import { Model } from "../model"; import { BaseField } from "./base"; /** * Reference Types: * - `local` - When serializing the referencer, the referencee is serialized and embedded * - `reference` - When serializing the referencer, the referencee is serialized as a reference * - `virtual` - The field is not serialized at all. Ideal for inverses * * `local` Cycles must be avoided - If instance `A` has a `local` reference to `B`, `B` must not of a `local` reference to `A` * A tree structure where model instances are `local` referenced exactly once is ideal. */ export type ReferenceType = 'reference' | 'local' | 'virtual'; export interface BaseRelationOptions { // How this reference should be serialized type?: ReferenceType; resolve_reference?: (ref, self: T) => R; model?: typeof Model | (() => typeof Model); inverseOf?: keyof R; autoLink?: 'always' | 'never' | 'model' | ((inst) => boolean); observable?: Observability; } export interface BaseRelationMeta { inverseInstance: any; observerDisposer?: () => void; attached?: boolean; } export abstract class BaseRelationField, S extends Model> extends BaseField { constructor(ctx: ClassMemberDecoratorContext, options?: O) { super(ctx, options); const fld = this; ctx.addInitializer(function (this: S) { fld.possiblyAttach(this); }); } get referenceType() { return this.options.type } get inversePropKey() { if (!this.options.inverseOf) return null; return String(this.options.inverseOf) } get referenceResolver() { return this.options.resolve_reference } get refModelType(): typeof Model { let model = this.options.model; // @ts-ignore if (model.prototype instanceof Model) return model; // @ts-ignore return model(); } abstract offerObjectFromInverse(inst: S, object: any, inverseField: BaseRelationField); abstract removeObjectFromInverse(inst: S, object: any, inverseField: BaseRelationField); resolveReference(inst: S, ref, { raise = false } = {}) { if (!ref) return ref; if (ref instanceof Model) return ref; if (this.referenceResolver) return this.referenceResolver(ref, inst); if (this.refModelType && this.referenceType != 'reference') { return new this.refModelType(ref); } if (raise && ref == null) { throw new Error(`Could not resolve reference "${ref}"`); } return ref; } protected shouldAutoAttach(inst: S) { let lpolicy = this.options.autoLink; if (lpolicy == null) { lpolicy = this.referenceType == 'local' ? 'always' : 'model'; } if (lpolicy == 'always') { return true; } else if (lpolicy == 'model') { return inst.allowCollectionAttachment(this); } else if (lpolicy == 'never') { return false; } else if (typeof lpolicy == 'function') { return lpolicy(inst); } return false; } protected shouldAutoDetach(inst: S) { return true; } protected detachFromInverse(inst: S, inverseInst) { const inverseKey = this.inversePropKey; if (!inverseKey) return; const detachName = `_remove_${inverseKey}`; if (inverseKey) { if (inverseInst[detachName]) inverseInst[detachName](inst); else { const inverseField = (inverseInst as Model)._model.getField(inverseKey) as BaseRelationField; if (!inverseField) throw new Error(`Field "${inverseKey}" does not exist on ${inverseInst}`); inverseField.removeObjectFromInverse(inverseInst, inst, this); } } } protected attachToInverse(inst: S, inverseInst) { const inverseKey = this.inversePropKey; if (!inverseKey) return; const attachName = `_add_${inverseKey}`; if (inverseInst[attachName]) inverseInst[attachName](inst); else { const inverseField = (inverseInst as Model)._model.getField(inverseKey) as BaseRelationField; if (!inverseField) throw new Error(`Field "${inverseKey}" does not exist on ${inverseInst}`); inverseField.offerObjectFromInverse(inverseInst, inst, this); } } abstract attach(inst: S); abstract detach(inst: S); possiblyAttach(inst: S) { if (this.shouldAutoAttach(inst)) { this.attach(inst); } } }