import { injectObservability } from "../mobx_internal"; import { Model } from "../model"; import { SerialMeta } from "./base"; import { BaseRelationField, BaseRelationMeta, BaseRelationOptions } from "./base_relation"; export interface RefOptions extends BaseRelationOptions { // If true, type must not be 'local' parent?: boolean, } export class ReferenceField, R> extends BaseRelationField<{} & BaseRelationMeta, RefOptions, S> { constructor(ctx: ClassMemberDecoratorContext, options?: RefOptions) { super(ctx, Object.assign({ parent: false, type: 'virtual', resolve_reference: null, } as RefOptions, options)); } get isPrimaryRelation() { return this.options.parent } protected getValue(inst: S): any { return this.access.get.call(inst); } protected setValue(inst: S, v) { this.access.set.call(inst, v); } offerObjectFromInverse(inst: S, object: any, inverseField: BaseRelationField) { const current = this.getValue(inst); if (current == object) return; if (current && current != object) { throw "Already linked!" } this.setValue(inst, object); } removeObjectFromInverse(inst: S, object: any, inverseField: BaseRelationField) { if (this.getValue(inst) == object) this.setValue(inst, null); } private access: ClassAccessorDecoratorTarget; decorate(value: any, ctx: ClassMemberDecoratorContext) { const field = this; if (ctx.kind == 'accessor') { return injectObservability(this.options.observable, (value, ctx) => { this.access = value; return { get(this: S) { return value.get.call(this); }, set(this: S, newRef) { value.set.call(this, newRef); const attRef = field.shouldAutoAttach(this) ? newRef : null; if (attRef || field.shouldAutoDetach(this)) { field.updateInverseReference(this, attRef); } }, } })(value, ctx); } } serialize(inst: S, object, meta: SerialMeta) { if (this.referenceType == 'virtual') { } else if (this.referenceType == 'reference') { object[this.serializedName] = { id: this.getValue(inst).id, }; } else if (this.referenceType == 'local') { const value = this.getValue(inst); object[this.serializedName] = value ? value.serialize() : null; } } deserialize(inst: S, object, meta: SerialMeta) { const value = object[this.serializedName]; this.setValue(inst, this.resolveReference(inst, value, { raise: true })); } attach(inst: S) { this.updateInverseReference(inst, this.getValue(inst)); // // This snippet may potentially be used to allow for computed references, but MobX waits until // // the end of the transaction to trigger our snippet that recomputes inverse references. // const instMeta = getInstanceOpts(this); // if (instMeta.observerDisposer) instMeta.observerDisposer(); // if (inverseKey) { // const disposer = observe(this, propName as any, (change) => { // if (change.oldValue == change.newValue) return; // updateInverseReference(this, change.newValue); // }, true); // instMeta.observerDisposer = disposer; // } else { // instMeta.observerDisposer = null; // } } detach(inst: S) { const instMeta = this.getInstanceMeta(inst); if (instMeta.observerDisposer) { instMeta.observerDisposer(); instMeta.observerDisposer = null; } this.updateInverseReference(inst, null); } protected updateInverseReference(inst: S, newInvInst) { const fieldInstMeta = this.getInstanceMeta(inst); const curInvInst = fieldInstMeta.inverseInstance; if ((curInvInst != newInvInst) && this.inversePropKey) { if (curInvInst) { fieldInstMeta.inverseInstance = null; this.detachFromInverse(inst, curInvInst); } if (newInvInst) { fieldInstMeta.inverseInstance = newInvInst; this.attachToInverse(inst, newInvInst); } fieldInstMeta.attached = !!newInvInst; } } } export type AnyReferenceField = ReferenceField; /** * Mark a property as a Collection reference. * Automatically handles adding/removing Model instances from the collection when this property is changed. * Will not add the Model instance to the collection until it is saved (has an `id`). * @param collection_field The field_name on the Collection object */ export const reference = >(options: RefOptions) => (value: any, ctx: ClassAccessorDecoratorContext): any => { const field = new ReferenceField(ctx, options); return field.decorate(value, ctx); } /** * Mark a property as an ID for a collection. * Requires a resolver function that, given an ID, returns the collections instance. * * Accepts an optional `name` parameter as the first parameter. This should be the same as the `@collection` property name. * If not provided, defaults to the `@collection_id` property name, minus the last underscore-separated segment. * Eg `some_collection_id` -> `some_collection` */ export function reference_id(retrieveReference: (id: number, self: T) => C) export function reference_id(reference_field: string, retrieveReference: (id: number, self: T) => C) export function reference_id(a, b?) { const getFunction: (id: number, self: T) => C = b || a; return (value: any, ctx: ClassAccessorDecoratorContext): any => { const field_name = (b ? a : null) || (ctx.name as string).split('_').slice(0, -1).join('_'); return { get(this: T) { return this[field_name] && this[field_name].id }, set(this: T, grp: number) { this[field_name] = getFunction.call(this, grp, this); }, } } }