// // 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 type { Tx, Ref, Doc, Class, DocumentQuery, FindResult, FindOptions, TxCreateDoc, TxUpdateDoc, TxMixin, TxPutBag, TxRemoveDoc, TxResult } from '@anticrm/core' import core, { DOMAIN_TX, DOMAIN_MODEL, SortingOrder, TxProcessor, Hierarchy, isOperator, ModelDb } from '@anticrm/core' import type { DbAdapter, TxAdapter } from '@anticrm/server-core' import { MongoClient, Db, Filter, Document, Sort } from 'mongodb' function translateDoc (doc: Doc): Document { return doc as Document } abstract class MongoAdapterBase extends TxProcessor { constructor ( protected readonly db: Db, protected readonly hierarchy: Hierarchy, protected readonly modelDb: ModelDb ) { super() } async init (): Promise {} private translateQuery (clazz: Ref>, query: DocumentQuery): Filter { const translated: any = {} for (const key in query) { const value = (query as any)[key] if (value !== null && typeof value === 'object') { const keys = Object.keys(value) if (keys[0] === '$like') { const pattern = value.$like as string translated[key] = { $regex: `^${pattern.split('%').join('.*')}$`, $options: 'i' } continue } } translated[key] = value } const classes = this.hierarchy.getDescendants(clazz) // Only replace if not specified if (translated._class?.$in === undefined) { translated._class = { $in: classes } } // return Object.assign({}, query, { _class: { $in: classes } }) return translated } private async lookup (clazz: Ref>, query: DocumentQuery, options: FindOptions): Promise> { const pipeline = [] pipeline.push({ $match: this.translateQuery(clazz, query) }) const lookups = options.lookup as any for (const key in lookups) { const clazz = lookups[key] const domain = this.hierarchy.getDomain(clazz) if (domain !== DOMAIN_MODEL) { const step = { from: domain, localField: key, foreignField: '_id', as: key + '_lookup' } pipeline.push({ $lookup: step }) } } const domain = this.hierarchy.getDomain(clazz) const cursor = this.db.collection(domain).aggregate(pipeline) const result = await cursor.toArray() as FindResult for (const row of result) { const object = row as any object.$lookup = {} for (const key in lookups) { const clazz = lookups[key] const domain = this.hierarchy.getDomain(clazz) if (domain !== DOMAIN_MODEL) { const arr = object[key + '_lookup'] object.$lookup[key] = arr[0] } else { object.$lookup[key] = this.modelDb.getObject(object[key]) } } } return result } async findAll ( _class: Ref>, query: DocumentQuery, options?: FindOptions ): Promise> { // TODO: rework this if (options !== null && options !== undefined) { if (options.lookup !== undefined) { return await this.lookup(_class, query, options) } } const domain = this.hierarchy.getDomain(_class) let cursor = this.db.collection(domain).find(this.translateQuery(_class, query)) if (options !== null && options !== undefined) { if (options.sort !== undefined) { const sort: Sort = {} for (const key in options.sort) { const order = options.sort[key] === SortingOrder.Ascending ? 1 : -1 sort[key] = order } cursor = cursor.sort(sort) } } return await cursor.toArray() } } class MongoAdapter extends MongoAdapterBase { protected override async txPutBag (tx: TxPutBag): Promise { const domain = this.hierarchy.getDomain(tx.objectClass) console.log('mongo', { $set: { [tx.bag + '.' + tx.key]: tx.value } }) await this.db.collection(domain).updateOne({ _id: tx.objectId }, { $set: { [tx.bag + '.' + tx.key]: tx.value } }) return {} } protected override async txRemoveDoc (tx: TxRemoveDoc): Promise { const domain = this.hierarchy.getDomain(tx.objectClass) await this.db.collection(domain).deleteOne({ _id: tx.objectId }) return {} } protected txMixin (tx: TxMixin): Promise { throw new Error('Method not implemented.') } protected override async txCreateDoc (tx: TxCreateDoc): Promise { const doc = TxProcessor.createDoc2Doc(tx) const domain = this.hierarchy.getDomain(doc._class) await this.db.collection(domain).insertOne(translateDoc(doc)) return {} } protected override async txUpdateDoc (tx: TxUpdateDoc): Promise { const domain = this.hierarchy.getDomain(tx.objectClass) if (isOperator(tx.operations)) { const operator = Object.keys(tx.operations)[0] if (operator === '$move') { const keyval = (tx.operations as any).$move const arr = Object.keys(keyval)[0] const desc = keyval[arr] const ops = [ { updateOne: { filter: { _id: tx.objectId }, update: { $pull: { [arr]: desc.$value } } } }, { updateOne: { filter: { _id: tx.objectId }, update: { $set: { modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn }, $push: { [arr]: { $each: [desc.$value], $position: desc.$position } } } } } ] console.log('ops', ops) return await this.db.collection(domain).bulkWrite(ops as any) } else { if (tx.retrieve === true) { const result = await this.db.collection(domain).findOneAndUpdate({ _id: tx.objectId }, { ...tx.operations, $set: { modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } }, { returnDocument: 'after' }) return { object: result.value } } else { return await this.db.collection(domain).updateOne({ _id: tx.objectId }, { ...tx.operations, $set: { modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } }) } } } else { if (tx.retrieve === true) { const result = await this.db.collection(domain).findOneAndUpdate({ _id: tx.objectId }, { $set: { ...tx.operations, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } }, { returnDocument: 'after' }) return { object: result.value } } else { return await this.db.collection(domain).updateOne({ _id: tx.objectId }, { $set: { ...tx.operations, modifiedBy: tx.modifiedBy, modifiedOn: tx.modifiedOn } }) } } } override tx (tx: Tx): Promise { console.log('mongo', tx) return super.tx(tx) } } class MongoTxAdapter extends MongoAdapterBase implements TxAdapter { 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.') } override async tx (tx: Tx): Promise { console.log('mongotx', tx) await this.db.collection(DOMAIN_TX).insertOne(translateDoc(tx)) return {} } async getModel (): Promise { return await this.db.collection(DOMAIN_TX).find({ objectSpace: core.space.Model }).sort({ _id: 1 }).toArray() } } /** * @public */ export async function createMongoAdapter (hierarchy: Hierarchy, url: string, dbName: string, modelDb: ModelDb): Promise { const client = new MongoClient(url) await client.connect() const db = client.db(dbName) return new MongoAdapter(db, hierarchy, modelDb) } /** * @public */ export async function createMongoTxAdapter (hierarchy: Hierarchy, url: string, dbName: string, modelDb: ModelDb): Promise { const client = new MongoClient(url) await client.connect() const db = client.db(dbName) return new MongoTxAdapter(db, hierarchy, modelDb) }