/** Collection is like a flat redux store slice. It's an array of models that can be */ import { cloneDeep } from '../utils/object.js' import { SDKModel, SDKModelSubclass } from './model.js' /** Subscribed to. On every change it returns a fresh array to observer */ export class SDKCollection< TClass extends SDKModelSubclass = SDKModelSubclass, TInstance extends InstanceType = InstanceType, TInterface = TInstance extends SDKModel ? TInterface : never, TMinimal = TInstance extends SDKModel ? TInterface : never, TID = TInstance['idStruct'] > extends Array { observers: ((collection: TInstance[]) => any)[] = [] constructor(...args: any) { super(...args) this.defineProperties() } defineProperties() { Object.defineProperty(this, 'snapshotted', { enumerable: false, writable: true }) Object.defineProperty(this, 'observers', { enumerable: false, value: [] }) } get first() { return this[0] } onChange() { this.onCollectionChange() this.observers.forEach((observer) => observer.call(this, this), this) } onCollectionChange() { this.model.onCollectionChange(this as unknown as SDKCollection) } observe(observer: (collection: TInstance[]) => any) { this.observers.push(observer) } unobserve(observer: (collection: TInstance[]) => any) { const index = this.observers.indexOf(observer) if (index > -1) this.observers.splice(index, 1) } getId(object: TInstance) { return object.getId() } model: TClass newInstance() {} /** Construct instance of a collection model */ construct(object?: any, isNew = false): TInstance { const clone = new this.model() as TInstance /** Define properties that model inherits from collection */ clone.sdk.utils.defineImplicitAccessors(clone, this.getImplicitAttributes, clone.getHiddenProps()) /** Copy hidden properties that may need copying from given object */ if (object) clone.copyHiddenProps(object, false) clone.set(object) /** Set New flag */ if (isNew) { clone.setDefaults() clone.isNew = true } /** Run transformation hook */ clone.set(this.transformAttributes(clone)) return clone } transformAttributes(object?: any) { if (this.model) return this.model.transformAttributes(object) return object } getImplicitAttributes(): Partial { return {} } /** Instantiate model with default values */ new(object: TMinimal) { return this.construct(object, true) } /** Instantiate model and add it to collection */ add(object: TMinimal) { return this.addItem(this.new(object)) } /** Instantiate model and send create request without adding to collection INSERT is alias */ post(object: TMinimal) { return this.new(object).post() } insert(object: TMinimal) { return this.post(object) } /** PUT item (with all attributes), UPDATE as alias */ put(data: TMinimal) { return this.construct(data).put() } update(data: TMinimal) { return this.put(data) } save(data: TMinimal) { return this.construct(data, true).save() } withId(target: string | number | TID | { id: string; status: string; revision: number }): TInstance { return this.construct(typeof target == 'object' ? target : { id: target }) } /** DELETE item (or by id) */ delete(target: string | number | TID) { return this.withId(target).delete() } /** GET item (or by id) */ get(target: string | number | TID | { id: string; status: string; revision: number }) { return this.withId(target).get() } /** GET ALL items, SELECT as alias */ fetch(options?: any) { return this.construct() .fetch(options) .then((items) => this.setItems(items)) } select(options?: any) { return this.fetch(options) } /** GET ALL items matching criteria */ search(options?: any) { return this.construct().search(options) } /** Replace all items in collections with items in given array */ setItems(objects: (TInstance | TInterface | TMinimal)[]) { this.splice(0, this.length, ...objects.map((v) => this.construct(v), this)) this.snapshot() this.onChange() return this } /** Updates or adds item to collection */ upsertItem(object: TInstance | TInterface | TMinimal) { const model = this.construct(object) const index = this.findIndex((other) => this.getId(other) === this.getId(model)) if (index == -1) { this.push(model) } else { this.splice(index, 1, model) } this.onChange() return model } /** Adds new item into a collection */ addItem(object: TInstance | TInterface | TMinimal) { const model = this.construct(object) const index = this.findIndex((other) => this.getId(other) === this.getId(model)) if (index != -1) { throw new Error( 'Trying to add object that is in collection already. Use upsertItem if you want to allow updating object.' ) } this.push(model) this.onChange() return model } /** Patches the item in collection with matching id */ updateItem(changes: Partial & TID) { const index = this.indexOf(this.getItem(changes)) if (index == -1) throw new Error('Trying to update object that is not in collection') this.splice(index, 1, this.construct(this[index].clone(changes))) this.onChange() return this[index] } /** Swaps item for another within collection */ replaceItem(changes: Partial & TID) { const index = this.indexOf(this.getItem(changes)) if (index == -1) throw new Error('Trying to replace object that is not in collection') this.splice(index, 1, this.construct(changes)) this.onChange() return this[index] } /** Removes item from collection */ removeItem(target: string | number | TID) { const index = this.indexOf(this.getItem(target)) if (index === -1) throw new Error('Trying to delete object that is not in collection') var removed = this[index] this.splice(index, 1) this.onChange() return removed } /** Gets item in collection by id or by id in object */ getItem(target: string | number | TID) { var id = typeof target == 'object' ? this.getId(this.construct(target)) : target return this.find((object) => this.getId(object) === id) } /** Return plain array with models exported as plain objects */ export(): TInterface[] { return this.map((model) => model.export() as TInterface) } /** Return instance of collection with new identity, but fully refering to current one */ proxy() { return new Proxy(this, {}) as this } /** * Define a collection of given models. During construction of models, the attributes returned by second callback will * be used as implicitly provided values */ static construct< M extends SDKModelSubclass, C extends (model?: InstanceType) => any, TInterface = InstanceType extends SDKModel ? TInterface : never, TMinimal = InstanceType extends SDKModel ? TMinimal : never >(model: M, getImplicitAttributes: C) { const collection = new this, TInterface, Omit>>() Object.defineProperty(collection, 'model', { value: model, enumerable: false }) Object.defineProperty(collection, 'getImplicitAttributes', { value: getImplicitAttributes, enumerable: false }) return collection } /** Save collection as raw values for future reference */ snapshotted?: TInterface[] snapshot() { this.snapshotted = this.export() return this } prependItem(object: TInstance | TInterface | TMinimal) { const model = this.construct(object) const index = this.findIndex((other) => this.getId(other) === this.getId(model)) if (index != -1) { throw new Error( 'Trying to add object that is in collection already. Use upsertItem if you want to allow updating object.' ) } this.unshift(model) this.onChange() return model } } /** An observable collection that does not have a class assigned to it. It simply calls a function on each item */ export class SDKFunctionalCollection extends SDKCollection< any, TInstance, TInstance, TMinimal > { constructor(transformAttributes: (...args: any) => TInstance, getId: (object: TInstance) => string) { super() this.transformAttributes = transformAttributes this.getId = getId } construct(object?: any, isNew = false) { return this.transformAttributes(object) } export() { return cloneDeep(this) } onCollectionChange() { return this } }