import { deepEqual } from 'fast-equals' import { DocumentUpdate, Hierarchy, MixinData, MixinUpdate, ModelDb, toFindResult } from '.' import type { Account, AnyAttribute, AttachedData, AttachedDoc, Class, Data, Doc, Mixin, Ref, Space, Timestamp } from './classes' import { Client } from './client' import core from './component' import type { DocumentQuery, FindOptions, FindResult, TxResult, WithLookup } from './storage' import { DocumentClassQuery, Tx, TxCUD, TxFactory, TxProcessor } from './tx' /** * @public * * High Level operations with client, will create low level transactions. * * `notify` is not supported by TxOperations. */ export class TxOperations implements Omit { readonly txFactory: TxFactory constructor (readonly client: Client, readonly user: Ref, readonly isDerived: boolean = false) { this.txFactory = new TxFactory(user, isDerived) } getHierarchy (): Hierarchy { return this.client.getHierarchy() } getModel (): ModelDb { return this.client.getModel() } async close (): Promise { return await this.client.close() } findAll( _class: Ref>, query: DocumentQuery, options?: FindOptions | undefined ): Promise> { return this.client.findAll(_class, query, options) } findOne( _class: Ref>, query: DocumentQuery, options?: FindOptions | undefined ): Promise | undefined> { return this.client.findOne(_class, query, options) } tx (tx: Tx): Promise { return this.client.tx(tx) } async createDoc( _class: Ref>, space: Ref, attributes: Data, id?: Ref, modifiedOn?: Timestamp, modifiedBy?: Ref ): Promise> { const hierarchy = this.client.getHierarchy() if (hierarchy.isDerived(_class, core.class.AttachedDoc)) { throw new Error('createDoc cannot be used for objects inherited from AttachedDoc') } const tx = this.txFactory.createTxCreateDoc(_class, space, attributes, id, modifiedOn, modifiedBy) await this.client.tx(tx) return tx.objectId } async addCollection( _class: Ref>, space: Ref, attachedTo: Ref, attachedToClass: Ref>, collection: string, attributes: AttachedData

, id?: Ref

, modifiedOn?: Timestamp, modifiedBy?: Ref ): Promise> { const tx = this.txFactory.createTxCollectionCUD( attachedToClass, attachedTo, space, collection, this.txFactory.createTxCreateDoc

(_class, space, attributes as unknown as Data

, id, modifiedOn, modifiedBy), modifiedOn, modifiedBy ) await this.client.tx(tx) return tx.tx.objectId as unknown as Ref

} async updateCollection( _class: Ref>, space: Ref, objectId: Ref

, attachedTo: Ref, attachedToClass: Ref>, collection: string, operations: DocumentUpdate

, retrieve?: boolean, modifiedOn?: Timestamp, modifiedBy?: Ref ): Promise> { const tx = this.txFactory.createTxCollectionCUD( attachedToClass, attachedTo, space, collection, this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve, modifiedOn, modifiedBy), modifiedOn, modifiedBy ) await this.client.tx(tx) return tx.objectId } async removeCollection( _class: Ref>, space: Ref, objectId: Ref

, attachedTo: Ref, attachedToClass: Ref>, collection: string, modifiedOn?: Timestamp, modifiedBy?: Ref ): Promise> { const tx = this.txFactory.createTxCollectionCUD( attachedToClass, attachedTo, space, collection, this.txFactory.createTxRemoveDoc(_class, space, objectId, modifiedOn, modifiedBy), modifiedOn, modifiedBy ) await this.client.tx(tx) return tx.objectId } updateDoc( _class: Ref>, space: Ref, objectId: Ref, operations: DocumentUpdate, retrieve?: boolean, modifiedOn?: Timestamp, modifiedBy?: Ref ): Promise { const tx = this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve, modifiedOn, modifiedBy) return this.client.tx(tx) } removeDoc( _class: Ref>, space: Ref, objectId: Ref, modifiedOn?: Timestamp, modifiedBy?: Ref ): Promise { const tx = this.txFactory.createTxRemoveDoc(_class, space, objectId, modifiedOn, modifiedBy) return this.client.tx(tx) } createMixin( objectId: Ref, objectClass: Ref>, objectSpace: Ref, mixin: Ref>, attributes: MixinData, modifiedOn?: Timestamp, modifiedBy?: Ref ): Promise { const tx = this.txFactory.createTxMixin( objectId, objectClass, objectSpace, mixin, attributes, modifiedOn, modifiedBy ) return this.client.tx(tx) } updateMixin( objectId: Ref, objectClass: Ref>, objectSpace: Ref, mixin: Ref>, attributes: MixinUpdate, modifiedOn?: Timestamp, modifiedBy?: Ref ): Promise { const tx = this.txFactory.createTxMixin( objectId, objectClass, objectSpace, mixin, attributes, modifiedOn, modifiedBy ) return this.client.tx(tx) } async update( doc: T, update: DocumentUpdate, retrieve?: boolean, modifiedOn?: Timestamp, modifiedBy?: Ref ): Promise { const hierarchy = this.client.getHierarchy() const mixClass = Hierarchy.mixinOrClass(doc) if (hierarchy.isMixin(mixClass)) { const baseClass = hierarchy.getBaseClass(doc._class) const byClass = this.splitMixinUpdate(update, mixClass, baseClass) const ops = this.apply(doc._id) for (const it of byClass) { if (hierarchy.isMixin(it[0])) { await ops.updateMixin(doc._id, baseClass, doc.space, it[0], it[1], modifiedOn, modifiedBy) } else { if (hierarchy.isDerived(it[0], core.class.AttachedDoc)) { const adoc = doc as unknown as AttachedDoc return await this.updateCollection( it[0], doc.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection, it[1], retrieve, modifiedOn, modifiedBy ) } await ops.updateDoc(it[0], doc.space, doc._id, it[1], retrieve, modifiedOn, modifiedBy) } } return await ops.commit() } if (hierarchy.isDerived(doc._class, core.class.AttachedDoc)) { const adoc = doc as unknown as AttachedDoc return await this.updateCollection( doc._class, doc.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection, update, retrieve, modifiedOn, modifiedBy ) } return await this.updateDoc(doc._class, doc.space, doc._id, update, retrieve, modifiedOn, modifiedBy) } remove(doc: T, modifiedOn?: Timestamp, modifiedBy?: Ref): Promise { if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc)) { const adoc = doc as unknown as AttachedDoc return this.removeCollection( doc._class, doc.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection, modifiedOn, modifiedBy ) } return this.removeDoc(doc._class, doc.space, doc._id) } apply (scope: string): ApplyOperations { return new ApplyOperations(this, scope) } async diffUpdate( doc: T, update: T | Data | DocumentUpdate, date?: Timestamp, account?: Ref ): Promise { // We need to update fields if they are different. const documentUpdate: DocumentUpdate = {} for (const [k, v] of Object.entries(update)) { if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { continue } const dv = (doc as any)[k] if (!deepEqual(dv, v) && v !== undefined) { ;(documentUpdate as any)[k] = v } } if (Object.keys(documentUpdate).length > 0) { await this.update(doc, documentUpdate, false, date ?? Date.now(), account) TxProcessor.applyUpdate(doc, documentUpdate) } return doc } async mixinDiffUpdate ( doc: Doc, raw: Doc | Data, mixin: Ref>>, modifiedBy: Ref, modifiedOn: Timestamp ): Promise { // We need to update fields if they are different. if (!this.getHierarchy().hasMixin(doc, mixin)) { await this.createMixin(doc._id, doc._class, doc.space, mixin, raw as MixinData, modifiedOn, modifiedBy) TxProcessor.applyUpdate(this.getHierarchy().as(doc, mixin), raw) return doc } const documentUpdate: MixinUpdate = {} for (const [k, v] of Object.entries(raw)) { if (['_class', '_id', 'modifiedBy', 'modifiedOn', 'space', 'attachedTo', 'attachedToClass'].includes(k)) { continue } const dv = (doc as any)[k] if (!deepEqual(dv, v) && v != null) { ;(documentUpdate as any)[k] = v } } if (Object.keys(documentUpdate).length > 0) { await this.updateMixin(doc._id, doc._class, doc.space, mixin, documentUpdate, modifiedOn, modifiedBy) TxProcessor.applyUpdate(this.getHierarchy().as(doc, mixin), documentUpdate) } return doc } private splitMixinUpdate( update: DocumentUpdate, mixClass: Ref>, baseClass: Ref> ): Map>, DocumentUpdate> { const hierarchy = this.getHierarchy() const attributes = hierarchy.getAllAttributes(mixClass) const updateAttrs = Object.fromEntries( Object.entries(update).filter((it) => !it[0].startsWith('$')) ) as DocumentUpdate const updateOps = Object.fromEntries( Object.entries(update).filter((it) => it[0].startsWith('$')) ) as DocumentUpdate const result: Map>, DocumentUpdate> = this.splitObjectAttributes( updateAttrs, baseClass, attributes ) for (const [key, value] of Object.entries(updateOps)) { const updates = this.splitObjectAttributes(value as object, baseClass, attributes) for (const [opsClass, opsUpdate] of updates) { const upd: DocumentUpdate = result.get(opsClass) ?? {} result.set(opsClass, { ...upd, [key]: opsUpdate }) } } return result } private splitObjectAttributes( obj: T, objClass: Ref>, attributes: Map ): Map>, object> { const hierarchy = this.getHierarchy() const result: Map>, any> = new Map() for (const [key, value] of Object.entries(obj)) { const attributeOf = attributes.get(key)?.attributeOf const clazz = attributeOf !== undefined && hierarchy.isMixin(attributeOf) ? attributeOf : objClass result.set(clazz, { ...(result.get(clazz) ?? {}), [key]: value }) } return result } } /** * @public * * Builder for ApplyOperation, with same syntax as TxOperations. * * Will send real command on commit and will return boolean of operation success. */ export class ApplyOperations extends TxOperations { txes: TxCUD[] = [] matches: DocumentClassQuery[] = [] notMatches: DocumentClassQuery[] = [] constructor (readonly ops: TxOperations, readonly scope: string) { const txClient: Client = { getHierarchy: () => ops.client.getHierarchy(), getModel: () => ops.client.getModel(), close: () => ops.client.close(), findOne: (_class, query, options?) => ops.client.findOne(_class, query, options), findAll: (_class, query, options?) => ops.client.findAll(_class, query, options), tx: async (tx): Promise => { if (ops.getHierarchy().isDerived(tx._class, core.class.TxCUD)) { this.txes.push(tx as TxCUD) } return {} } } super(txClient, ops.user) } match(_class: Ref>, query: DocumentQuery): ApplyOperations { this.matches.push({ _class, query }) return this } notMatch(_class: Ref>, query: DocumentQuery): ApplyOperations { this.notMatches.push({ _class, query }) return this } async commit (notify: boolean = true, extraNotify: Ref>[] = []): Promise { if (this.txes.length > 0) { return await ((await this.ops.tx( this.ops.txFactory.createTxApplyIf( core.space.Tx, this.scope, this.matches, this.notMatches, this.txes, notify, extraNotify ) )) as Promise) } return true } } /** * @public * * Builder for TxOperations. */ export class TxBuilder extends TxOperations { txes: TxCUD[] = [] matches: DocumentClassQuery[] = [] constructor (readonly hierarchy: Hierarchy, readonly modelDb: ModelDb, user: Ref) { const txClient: Client = { getHierarchy: () => this.hierarchy, getModel: () => this.modelDb, close: async () => {}, findOne: async (_class, query, options?) => undefined, findAll: async (_class, query, options?) => toFindResult([]), tx: async (tx): Promise => { if (this.hierarchy.isDerived(tx._class, core.class.TxCUD)) { this.txes.push(tx as TxCUD) } return {} } } super(txClient, user) } } /** * @public */ export async function updateAttribute ( client: TxOperations, object: Doc, _class: Ref>, attribute: { key: string, attr: AnyAttribute }, value: any, modifyBy?: Ref ): Promise { const doc = object const attributeKey = attribute.key if ((doc as any)[attributeKey] === value) return const attr = attribute.attr if (client.getHierarchy().isMixin(attr.attributeOf)) { await client.updateMixin( doc._id, _class, doc.space, attr.attributeOf, { [attributeKey]: value }, Date.now(), modifyBy ) } else { if (client.getHierarchy().isDerived(attribute.attr.type._class, core.class.ArrOf)) { const oldValue: any[] = (object as any)[attributeKey] ?? [] const val: any[] = value const toPull = oldValue.filter((it: any) => !val.includes(it)) const toPush = val.filter((it) => !oldValue.includes(it)) if (toPull.length > 0) { await client.update(object, { $pull: { [attributeKey]: { $in: toPull } } }, false, Date.now(), modifyBy) } if (toPush.length > 0) { await client.update( object, { $push: { [attributeKey]: { $each: toPush, $position: 0 } } }, false, Date.now(), modifyBy ) } } else { await client.update(object, { [attributeKey]: value }, false, Date.now(), modifyBy) } } }