// // Copyright © 2020, 2021 Anticrm Platform Contributors. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // import type { KeysByType } from 'simplytyped' import type { Account, Arr, AttachedDoc, Class, Data, Doc, Domain, Mixin, PropertyType, Ref, Space } from './classes' import core from './component' import { _getOperator } from './operator' import { _toDoc } from './proxy' import type { DocumentQuery, TxResult } from './storage' import { generateId } from './utils' /** * @public */ export interface Tx extends Doc { objectSpace: Ref // space where transaction will operate } /** * Event to be send by server during model upgrade procedure. * @public */ export interface TxModelUpgrade extends Tx {} /** * @public */ export interface TxCUD extends Tx { objectId: Ref objectClass: Ref> } /** * @public */ export interface TxCreateDoc extends TxCUD { attributes: Data } /** * * Will perform create/update/delete of attached documents. * * @public */ export interface TxCollectionCUD extends TxCUD { collection: string tx: TxCUD

} /** * @public */ export interface DocumentClassQuery { _class: Ref> query: DocumentQuery } /** * @public * Apply set of transactions in sequential manner with verification of set of queries. */ export interface TxApplyIf extends Tx { // only one operation per scope is allowed at one time. scope: string // All matches should be true with at least one document. match: DocumentClassQuery[] // If all matched execute following transactions. txes: TxCUD[] } /** * @public */ export type MixinData = Omit & PushOptions> & IncOptions> /** * @public */ export type MixinUpdate = Partial> & PushOptions> & IncOptions> /** * Define Create/Update for mixin attributes. * @public */ export interface TxMixin extends TxCUD { mixin: Ref> attributes: MixinUpdate } /** * @public */ export type ArrayAsElement = { [P in keyof T]: T[P] extends Arr ? Partial | PullArray : never } /** * @public */ export interface Position { $each: X[] $position: number } /** * @public */ export interface QueryUpdate { $query: Partial $update: Partial } /** * @public */ export interface PullArray { $in: X[] } /** * @public */ export interface MoveDescriptor { $value: X $position: number } /** * @public */ export type ArrayAsElementPosition = { [P in keyof T]-?: T[P] extends Arr ? X | Position : never } /** * @public */ export type ArrayAsElementUpdate = { [P in keyof T]-?: T[P] extends Arr ? X | QueryUpdate : never } /** * @public */ export type ArrayMoveDescriptor = { [P in keyof T]: T[P] extends Arr ? MoveDescriptor : never } /** * @public */ export type NumberProperties = { [P in keyof T]: T[P] extends number | undefined ? T[P] : never } /** * @public */ export type OmitNever = Omit> /** * @public */ export interface PushOptions { $push?: Partial>>> $pull?: Partial>>> $move?: Partial>>> } /** * @public */ export interface SetEmbeddedOptions { $update?: Partial>>> } /** * @public */ export interface PushMixinOptions { $pushMixin?: { $mixin: Ref> values: Partial>> } } /** * @public */ export interface IncOptions { $inc?: Partial>> } /** * @public */ export interface SpaceUpdate { space?: Ref } /** * @public */ export type DocumentUpdate = Partial> & PushOptions & SetEmbeddedOptions & PushMixinOptions & IncOptions & SpaceUpdate /** * @public */ export interface TxUpdateDoc extends TxCUD { operations: DocumentUpdate retrieve?: boolean } /** * @public */ export interface TxRemoveDoc extends TxCUD {} /** * @public */ export const DOMAIN_TX = 'tx' as Domain /** * @public */ export interface WithTx { tx: (tx: Tx) => Promise } /** * @public */ export abstract class TxProcessor implements WithTx { async tx (tx: Tx): Promise { switch (tx._class) { case core.class.TxCreateDoc: return await this.txCreateDoc(tx as TxCreateDoc) case core.class.TxCollectionCUD: return await this.txCollectionCUD(tx as TxCollectionCUD) case core.class.TxUpdateDoc: return await this.txUpdateDoc(tx as TxUpdateDoc) case core.class.TxRemoveDoc: return await this.txRemoveDoc(tx as TxRemoveDoc) case core.class.TxMixin: return await this.txMixin(tx as TxMixin) case core.class.TxApplyIf: // Apply if processed on server return await Promise.resolve({}) } throw new Error('TxProcessor: unhandled transaction class: ' + tx._class) } static createDoc2Doc(tx: TxCreateDoc): T { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { ...tx.attributes, _id: tx.objectId, _class: tx.objectClass, space: tx.objectSpace, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } as T } static updateDoc2Doc(rawDoc: T, tx: TxUpdateDoc): T { const doc = _toDoc(rawDoc) const ops = tx.operations as any for (const key in ops) { if (key.startsWith('$')) { const operator = _getOperator(key) operator(doc, ops[key]) } else { ;(doc as any)[key] = ops[key] } } doc.modifiedBy = tx.modifiedBy doc.modifiedOn = tx.modifiedOn return rawDoc } static updateMixin4Doc(rawDoc: D, tx: TxMixin): D { const ops = tx.attributes as any const doc = _toDoc(rawDoc) const mixin = (doc as any)[tx.mixin] ?? {} for (const key in ops) { if (key.startsWith('$')) { const operator = _getOperator(key) operator(mixin, ops[key]) } else { mixin[key] = ops[key] } } rawDoc.modifiedBy = tx.modifiedBy rawDoc.modifiedOn = tx.modifiedOn ;(doc as any)[tx.mixin] = mixin return rawDoc } static buildDoc2Doc(txes: Tx[]): D | undefined { let doc: Doc let createTx = txes.find((tx) => tx._class === core.class.TxCreateDoc) const collectionTxes = false if (createTx === undefined) { const collectionTxes = txes.filter((tx) => tx._class === core.class.TxCollectionCUD) as Array< TxCollectionCUD > createTx = collectionTxes.find((p) => p.tx._class === core.class.TxCreateDoc) } if (createTx === undefined) return doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc) for (let tx of txes) { if (collectionTxes) { tx = TxProcessor.extractTx(tx) } if (tx._class === core.class.TxUpdateDoc) { doc = TxProcessor.updateDoc2Doc(doc, tx as TxUpdateDoc) } else if (tx._class === core.class.TxMixin) { const mixinTx = tx as TxMixin doc = TxProcessor.updateMixin4Doc(doc, mixinTx) } } return doc as D } static extractTx (tx: Tx): Tx { if (tx._class === core.class.TxCollectionCUD) { const ctx = tx as TxCollectionCUD if (ctx.tx._class === core.class.TxCreateDoc) { const create = ctx.tx as TxCreateDoc create.attributes.attachedTo = ctx.objectId create.attributes.attachedToClass = ctx.objectClass create.attributes.collection = ctx.collection return create } return ctx.tx } return tx } protected abstract txCreateDoc (tx: TxCreateDoc): Promise protected abstract txUpdateDoc (tx: TxUpdateDoc): Promise protected abstract txRemoveDoc (tx: TxRemoveDoc): Promise protected abstract txMixin (tx: TxMixin): Promise protected txCollectionCUD (tx: TxCollectionCUD): Promise { // We need update only create transactions to contain attached, attachedToClass. if (tx.tx._class === core.class.TxCreateDoc) { const createTx = tx.tx as TxCreateDoc const d: TxCreateDoc = { ...createTx, attributes: { ...createTx.attributes, attachedTo: tx.objectId, attachedToClass: tx.objectClass, collection: tx.collection } } return this.txCreateDoc(d) } return this.tx(tx.tx) } } /** * @public */ export class TxFactory { constructor (readonly account: Ref) {} createTxCreateDoc( _class: Ref>, space: Ref, attributes: Data, objectId?: Ref ): TxCreateDoc { return { _id: generateId(), _class: core.class.TxCreateDoc, space: core.space.Tx, objectId: objectId ?? generateId(), objectClass: _class, objectSpace: space, modifiedOn: Date.now(), modifiedBy: this.account, attributes } } createTxCollectionCUD( _class: Ref>, objectId: Ref, space: Ref, collection: string, tx: TxCUD

): TxCollectionCUD { return { _id: generateId(), _class: core.class.TxCollectionCUD, space: core.space.Tx, objectId, objectClass: _class, objectSpace: space, modifiedOn: Date.now(), modifiedBy: this.account, collection, tx } } createTxUpdateDoc( _class: Ref>, space: Ref, objectId: Ref, operations: DocumentUpdate, retrieve?: boolean ): TxUpdateDoc { return { _id: generateId(), _class: core.class.TxUpdateDoc, space: core.space.Tx, modifiedBy: this.account, modifiedOn: Date.now(), objectId, objectClass: _class, objectSpace: space, operations, retrieve } } createTxRemoveDoc(_class: Ref>, space: Ref, objectId: Ref): TxRemoveDoc { return { _id: generateId(), _class: core.class.TxRemoveDoc, space: core.space.Tx, modifiedBy: this.account, modifiedOn: Date.now(), objectId, objectClass: _class, objectSpace: space } } createTxMixin( objectId: Ref, objectClass: Ref>, objectSpace: Ref, mixin: Ref>, attributes: MixinUpdate ): TxMixin { return { _id: generateId(), _class: core.class.TxMixin, space: core.space.Tx, modifiedBy: this.account, modifiedOn: Date.now(), objectId, objectClass, objectSpace, mixin, attributes } } createTxApplyIf (space: Ref, scope: string, match: DocumentClassQuery[], txes: TxCUD[]): TxApplyIf { return { _id: generateId(), _class: core.class.TxApplyIf, space: core.space.Tx, modifiedBy: this.account, modifiedOn: Date.now(), objectSpace: space, scope, match, txes } } }