// // Copyright © 2020 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 { PlatformError, Severity, Status } from '@anticrm/platform' import type { Class, Doc, Ref } from './classes' import type { Tx, TxCreateDoc, TxMixin, TxPutBag, TxRemoveDoc, TxUpdateDoc } from './tx' import core from './component' import type { Hierarchy } from './hierarchy' import { _getOperator } from './operator' import { findProperty, resultSort } from './query' import type { DocumentQuery, FindOptions, FindResult, Storage, WithLookup, LookupData, Refs } from './storage' import { TxProcessor } from './tx' /** * @public */ export abstract class MemDb extends TxProcessor { protected readonly hierarchy: Hierarchy private readonly objectsByClass = new Map>, Doc[]>() private readonly objectById = new Map, Doc>() constructor (hierarchy: Hierarchy) { super() this.hierarchy = hierarchy } private getObjectsByClass (_class: Ref>): Doc[] { const result = this.objectsByClass.get(_class) if (result === undefined) { const result: Doc[] = [] this.objectsByClass.set(_class, result) return result } return result } private cleanObjectByClass (_class: Ref>, _id: Ref): void { let result = this.objectsByClass.get(_class) if (result !== undefined) { result = result.filter((cl) => cl._id !== _id) this.objectsByClass.set(_class, result) } } private getByIdQuery(query: DocumentQuery, _class: Ref>): T[] { const result: T[] = [] if (typeof query._id === 'string') { const obj = this.objectById.get(query._id) as T if (obj !== undefined) result.push(obj) } else if (query._id?.$in !== undefined) { const ids = query._id.$in for (const id of ids) { const obj = this.objectById.get(id) as T if (obj !== undefined) result.push(obj) } } return result } getObject(_id: Ref): T { const doc = this.objectById.get(_id) if (doc === undefined) { console.log(_id) throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id })) } return doc as T } private lookup(docs: T[], lookup: Refs): WithLookup[] { const withLookup: WithLookup[] = [] for (const doc of docs) { const result: LookupData = {} for (const key in lookup) { const id = (doc as any)[key] as Ref if (id !== undefined) { (result as any)[key] = this.getObject(id) } } withLookup.push(Object.assign({}, doc, { $lookup: result })) } return withLookup } async findAll( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { let result: Doc[] if ( Object.prototype.hasOwnProperty.call(query, '_id') && (typeof query._id === 'string' || query._id?.$in !== undefined || query._id === undefined || query._id === null) ) { result = this.getByIdQuery(query, _class) } else { result = this.getObjectsByClass(_class) } for (const key in query) { if (key === '_id' && ((query._id as any)?.$like === undefined || query._id === undefined)) continue const value = (query as any)[key] result = findProperty(result, key, value) } if (options?.sort !== undefined) resultSort(result, options?.sort) if (options?.lookup !== undefined) result = this.lookup(result as T[], options.lookup) result = result.slice(0, options?.limit) return result as T[] } addDoc (doc: Doc): void { this.hierarchy.getAncestors(doc._class).forEach((_class) => { this.getObjectsByClass(_class).push(doc) }) this.objectById.set(doc._id, doc) } delDoc (_id: Ref): void { const doc = this.objectById.get(_id) if (doc === undefined) { throw new PlatformError(new Status(Severity.ERROR, core.status.ObjectNotFound, { _id })) } this.objectById.delete(_id) this.hierarchy.getAncestors(doc._class).forEach((_class) => { this.cleanObjectByClass(_class, _id) }) } } /** * Hold transactions * * @public */ export class TxDb extends MemDb implements Storage { protected txCreateDoc (tx: TxCreateDoc): Promise { throw new Error('Method not implemented.') } protected txPutBag (tx: TxPutBag): Promise { throw new Error('Method not implemented.') } protected txUpdateDoc (tx: TxUpdateDoc): Promise { throw new Error('Method not implemented.') } protected txRemoveDoc (tx: TxRemoveDoc): Promise { throw new Error('Method not implemented.') } protected txMixin (tx: TxMixin): Promise { throw new Error('Method not implemented.') } async tx (tx: Tx): Promise { this.addDoc(tx) } } /** * Hold model objects and classes * * @public */ export class ModelDb extends MemDb implements Storage { protected override async txPutBag (tx: TxPutBag): Promise { const doc = this.getObject(tx.objectId) as any let bag = doc[tx.bag] if (bag === undefined) { doc[tx.bag] = bag = {} } bag[tx.key] = tx.value doc.modifiedBy = tx.modifiedBy doc.modifiedOn = tx.modifiedOn } protected override async txCreateDoc (tx: TxCreateDoc): Promise { this.addDoc(TxProcessor.createDoc2Doc(tx)) } protected async txUpdateDoc (tx: TxUpdateDoc): Promise { const doc = this.getObject(tx.objectId) as any const ops = tx.operations as any for (const key in ops) { if (key.startsWith('$')) { const operator = _getOperator(key) operator(doc, ops[key]) } else { doc[key] = ops[key] } } doc.modifiedBy = tx.modifiedBy doc.modifiedOn = tx.modifiedOn } protected async txRemoveDoc (tx: TxRemoveDoc): Promise { this.delDoc(tx.objectId) } // TODO: process ancessor mixins protected async txMixin (tx: TxMixin): Promise { const obj = this.getObject(tx.objectId) as any obj[tx.mixin] = tx.attributes } }