// // 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 clone from 'just-clone' import { Lookup, ReverseLookups } from '.' import type { Class, Doc, Ref } from './classes' import core from './component' import { Hierarchy } from './hierarchy' import { matchQuery, resultSort } from './query' import type { DocumentQuery, FindOptions, FindResult, LookupData, Storage, TxResult, WithLookup } from './storage' import type { Tx, TxCreateDoc, TxMixin, TxPutBag, TxRemoveDoc, TxUpdateDoc } from './tx' import { TxProcessor } from './tx' /** * @public */ export abstract class MemDb extends TxProcessor { private readonly objectsByClass = new Map>, Doc[]>() private readonly objectById = new Map, Doc>() constructor (protected readonly hierarchy: Hierarchy) { super() } 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 async getLookupValue (doc: T, lookup: Lookup, result: LookupData): Promise { for (const key in lookup) { if (key === '_id') { await this.getReverseLookupValue(doc, lookup, result) continue } const value = (lookup as any)[key] if (Array.isArray(value)) { const [_class, nested] = value const objects = await this.findAll(_class, { _id: (doc as any)[key] }) ;(result as any)[key] = objects[0] const nestedResult = {} const parent = (result as any)[key] await this.getLookupValue(parent, nested, nestedResult) Object.assign(parent, { $lookup: nestedResult }) } else { const objects = await this.findAll(value, { _id: (doc as any)[key] }) ;(result as any)[key] = objects[0] } } } private async getReverseLookupValue (doc: T, lookup: ReverseLookups, result: LookupData): Promise { for (const key in lookup._id) { const value = lookup._id[key] const objects = await this.findAll(value, { attachedTo: doc._id }) ;(result as any)[key] = objects } } private async lookup(docs: T[], lookup: Lookup): Promise[]> { const withLookup: WithLookup[] = [] for (const doc of docs) { const result: LookupData = {} await this.getLookupValue(doc, lookup, result) withLookup.push(Object.assign({}, doc, { $lookup: result })) } return withLookup } async findAll( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { let result: Doc[] const baseClass = this.hierarchy.getBaseClass(_class) 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, baseClass) } else { result = this.getObjectsByClass(baseClass) } result = matchQuery(result, query) if (baseClass !== _class) { // We need to filter instances without mixin was set result = result.filter(r => (r as any)[_class] !== undefined) } if (options?.lookup !== undefined) result = await this.lookup(result as T[], options.lookup) if (options?.sort !== undefined) resultSort(result, options?.sort) result = result.slice(0, options?.limit) return clone(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) return {} } } /** * 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 return {} } protected override async txCreateDoc (tx: TxCreateDoc): Promise { this.addDoc(TxProcessor.createDoc2Doc(tx)) return {} } protected async txUpdateDoc (tx: TxUpdateDoc): Promise { const doc = this.getObject(tx.objectId) as any TxProcessor.updateDoc2Doc(doc, tx) return tx.retrieve === true ? { object: doc } : {} } protected async txRemoveDoc (tx: TxRemoveDoc): Promise { this.delDoc(tx.objectId) return {} } // TODO: process ancessor mixins protected async txMixin (tx: TxMixin): Promise { const obj = this.getObject(tx.objectId) as any TxProcessor.updateMixin4Doc(obj, tx.mixin, tx.attributes) return {} } }