import { injectObservability } from "../mobx_internal"; import { Model } from "../model"; import { SerialMeta } from "./base"; import { BaseRelationField, BaseRelationMeta, BaseRelationOptions } from "./base_relation"; import { ArrayInterface, BaseCollectionInterface, CollectionInterface, MapInterface, ObjectInterface, SetInterface } from './collection_interfaces'; export interface CollectionOptions extends BaseRelationOptions { indexBy?: keyof R; backingType?: 'Object' | 'Array' | 'Map' | 'Set' | typeof Object | typeof Array | typeof Map | typeof Set | typeof BaseCollectionInterface; // sliced?: ((start, end) => SliceResponse) | AllowedNames SliceResponse>; // lazy?: (self: T) => any[]; } function asSet(v) { if (v instanceof Set) return new Set(v); let vals; if (v instanceof Map) { vals = v.values(); } else { vals = Object.values(v); } return new Set(vals); } function subSet(a: Set, b: Set): Set { return new Set([...a].filter(x => !b.has(x))); } export class CollectionField, R> extends BaseRelationField<{ collectionInitialized: boolean; collectionInterface: BaseCollectionInterface; } & BaseRelationMeta, CollectionOptions, S> { constructor(ctx: ClassMemberDecoratorContext, options?: CollectionOptions) { super(ctx, Object.assign({ parent: false, type: 'virtual', resolve_reference: null, } as CollectionOptions, options)); } protected inInverseAction = false; protected getInterface(inst: S) { const fmeta = this.getInstanceMeta(inst); return fmeta.collectionInterface; } protected getCollection(inst: S) { return this.access.get.call(inst); } protected assertInitialized(inst: S) { const coll = this.getCollection(inst); if (coll == null) throw new Error("Collection was not initialized correctly") return coll; } protected *iterateCollectionItems(inst: S): IterableIterator { const iface = this.getInterface(inst); if (iface) yield* iface.items(); } protected getCollectionInterfaceType(inst: S, given): CollectionInterface { let cinfType: typeof BaseCollectionInterface; if (this.options.backingType && (this.options.backingType as any).prototype instanceof BaseCollectionInterface) { return this.options.backingType as CollectionInterface; } else { const type = this.options.backingType || given; if (type == 'Array' || Array.isArray(type) || type === Array) { return ArrayInterface; } else if (type == 'Set' || type instanceof Set || type === Set) { return SetInterface; } else if (type == 'Map' || type instanceof Map || type === Map) { return MapInterface; } else if (type == 'Object' || typeof type == 'object' || type === Object) { return ObjectInterface; } } } protected buildCollectionInterface(inst: S, given) { return new (this.getCollectionInterfaceType(inst, given))(this, inst); } protected setCollection(inst: S, coll) { const curSet = this.getInterface(inst).asSet(); const nextSet = asSet(coll); const addedItems = subSet(nextSet, curSet); const removedItems = subSet(curSet, nextSet); for (let o of removedItems) { this.unstoreObject(inst, o); if (this.shouldAutoDetach(inst)) { this.detachFromInverse(inst, o); } } for (let o of addedItems) { this.storeObject(inst, o); if (this.shouldAutoAttach(inst)) { this.attachToInverse(inst, o); } } } protected storeObject(inst: S, object: any, { } = {}) { const iface = this.getInterface(inst); iface.storeItem(object); } protected unstoreObject(inst: S, object: any) { this.assertInitialized(inst); this.getInterface(inst).unstoreItem(object); } offerObjectFromInverse(inst: S, object: any, inverseField: BaseRelationField) { this.inInverseAction = true; try { this.storeObject(inst, object); } finally { this.inInverseAction = false; } } removeObjectFromInverse(inst: S, object: any, inverseField: BaseRelationField) { this.inInverseAction = true; try { this.unstoreObject(inst, object); } finally { this.inInverseAction = false; } } access: ClassAccessorDecoratorTarget; decorate(target: any, context: ClassMemberDecoratorContext) { const field = this; if (context.kind == 'accessor') { return injectObservability(this.options.observable, (value, ctx) => { this.access = value; return { init(this: S, value) { const fmeta = field.getInstanceMeta(this); // TODO Where possible, observe mutations of the backing object, however, w/o Proxies, it is difficult to guarantee that // collection mutations will cause appropriate updates of inverse references. // TODO Provide a single, unified Collection class that is iterable and tracks additions/deletions. fmeta.collectionInterface = field.buildCollectionInterface(this, value); return fmeta.collectionInterface.instanciate(value || []); }, get(this: S) { return field.assertInitialized(this); }, set(this: S, newRef) { field.setCollection(this, newRef) }, } })(target, context); } } serialize(inst: S, object: any, meta: SerialMeta) { if (this.referenceType == 'virtual') return; const serValues = []; if (this.referenceType == 'reference') { for (let v of this.iterateCollectionItems(inst)) { // TODO Make the format here less opinionated. Maybe add a `serializeReference` method to the model. // Or add a `serialize` option to the `@collection` decorator, possibly replacing `type`. serValues.push({ id: (v as any).id }); } } else if (this.referenceType == 'local') { for (let v of this.iterateCollectionItems(inst)) { if (v instanceof Model) { serValues.push(v.serialize()); } else { serValues.push(v); } } } object[this.serializedName] = serValues; } deserialize(inst: S, object: any, meta: SerialMeta) { const values = object[this.serializedName] || []; const desValues = []; for (let value of values) { if (!value) continue; desValues.push(this.resolveReference(inst, value, { raise: true })); } this.setCollection(inst, desValues); } attach(inst: S) { for (let i of this.iterateCollectionItems(inst)) { this.attachToInverse(inst, i); } } detach(inst: S) { for (let i of this.iterateCollectionItems(inst)) { this.detachFromInverse(inst, i); } } } export type AnyCollectionField = CollectionField; export const collection = >(options: CollectionOptions) => (value: any, context: ClassAccessorDecoratorContext): any => { const field = new CollectionField(context, options); return field.decorate(value, context); }