// // 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 justClone from 'just-clone' import type { KeysByType } from 'simplytyped' import type { Account, Arr, AttachedDoc, Class, Data, Doc, Domain, Mixin, PropertyType, Ref, Space, Timestamp } from './classes' import core from './component' import { setObjectValue } from './objvalue' 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 } /** * @public */ export enum WorkspaceEvent { UpgradeScheduled, Upgrade, IndexingUpdate, SecurityChange, MaintenanceNotification, BulkUpdate } /** * Event to be send by server during model upgrade procedure. * @public */ export interface TxWorkspaceEvent extends Tx { event: WorkspaceEvent params: T } /** * @public */ export interface IndexingUpdateEvent { _class: Ref>[] } /** * @public */ export interface BulkUpdateEvent { _class: Ref>[] } /** * @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[] // All matches should be false for all documents. notMatch: DocumentClassQuery[] // If all matched execute following transactions. txes: TxCUD[] notify?: boolean // If false will not send notifications. // If passed, will send WorkspaceEvent.BulkUpdate event with list of classes to update extraNotify?: Ref>[] } export interface TxApplyResult { success: boolean derived: Tx[] // Some derived transactions to handle. } /** * @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 | X : 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 type UnsetProperties = Record /** * @public */ export interface UnsetOptions { $unset?: UnsetProperties } /** * @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: (...txs: Tx[]) => Promise } /** * @public */ export abstract class TxProcessor implements WithTx { async tx (...txes: Tx[]): Promise { const result: TxResult[] = [] for (const tx of txes) { switch (tx._class) { case core.class.TxCreateDoc: result.push(await this.txCreateDoc(tx as TxCreateDoc)) break case core.class.TxCollectionCUD: result.push(await this.txCollectionCUD(tx as TxCollectionCUD)) break case core.class.TxUpdateDoc: result.push(await this.txUpdateDoc(tx as TxUpdateDoc)) break case core.class.TxRemoveDoc: result.push(await this.txRemoveDoc(tx as TxRemoveDoc)) break case core.class.TxMixin: result.push(await this.txMixin(tx as TxMixin)) break case core.class.TxApplyIf: // Apply if processed on server return await Promise.resolve([]) } } return result } static createDoc2Doc(tx: TxCreateDoc): T { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { ...justClone(tx.attributes), _id: tx.objectId, _class: tx.objectClass, space: tx.objectSpace, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn, createdBy: tx.createdBy ?? tx.modifiedBy, createdOn: tx.createdOn ?? tx.modifiedOn } as T } static updateDoc2Doc(rawDoc: T, tx: TxUpdateDoc): T { const doc = _toDoc(rawDoc) TxProcessor.applyUpdate(doc, tx.operations as any) doc.modifiedBy = tx.modifiedBy doc.modifiedOn = tx.modifiedOn return rawDoc } static applyUpdate(doc: T, ops: any): void { for (const key in ops) { if (key.startsWith('$')) { const operator = _getOperator(key) operator(doc, ops[key]) } else { setObjectValue(key, doc, ops[key]) } } } 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 { setObjectValue(key, mixin, 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) if (createTx === undefined) { const collectionTxes = txes.filter((tx) => tx._class === core.class.TxCollectionCUD) as Array< TxCollectionCUD > const collectionCreateTx = collectionTxes.find((p) => p.tx._class === core.class.TxCreateDoc) if (collectionCreateTx === undefined) return createTx = TxProcessor.extractTx(collectionCreateTx) } if (createTx === undefined) return const objectId = (createTx as TxCreateDoc).objectId doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc) for (let tx of txes) { if ((tx as TxCUD).objectId !== objectId && tx._class === core.class.TxCollectionCUD) { 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 { private readonly txSpace: Ref constructor ( readonly account: Ref, readonly isDerived: boolean = false ) { this.txSpace = isDerived ? core.space.DerivedTx : core.space.Tx } createTxCreateDoc( _class: Ref>, space: Ref, attributes: Data, objectId?: Ref, modifiedOn?: Timestamp, modifiedBy?: Ref ): TxCreateDoc { return { _id: generateId(), _class: core.class.TxCreateDoc, space: this.txSpace, objectId: objectId ?? generateId(), objectClass: _class, objectSpace: space, modifiedOn: modifiedOn ?? Date.now(), modifiedBy: modifiedBy ?? this.account, createdBy: modifiedBy ?? this.account, attributes } } createTxCollectionCUD( _class: Ref>, objectId: Ref, space: Ref, collection: string, tx: TxCUD

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