diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1be94976bb..f12bb6f708 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,6 +42,11 @@ jobs: - name: Checking svelte sources... run: node common/scripts/install-run-rush.js svelte-check + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.3.0 + with: + mongodb-version: 4.4 + - name: Testing... uses: paambaati/codeclimate-action@v2.7.5 env: diff --git a/server/mongo/package.json b/server/mongo/package.json index 4c96533d64..0cc6e667e7 100644 --- a/server/mongo/package.json +++ b/server/mongo/package.json @@ -7,6 +7,7 @@ "scripts": { "build": "heft build", "build:watch": "tsc", + "test": "heft test", "lint:fix": "eslint --fix src", "lint": "eslint src", "format": "prettier --write src && eslint --fix src" diff --git a/server/mongo/src/__tests__/minmodel.ts b/server/mongo/src/__tests__/minmodel.ts new file mode 100644 index 0000000000..02609f4649 --- /dev/null +++ b/server/mongo/src/__tests__/minmodel.ts @@ -0,0 +1,196 @@ +// +// 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 { Account, Arr, Class, Data, Doc, Mixin, Obj, Ref, TxCreateDoc, TxCUD } from '@anticrm/core' +import core, { AttachedDoc, ClassifierKind, DOMAIN_MODEL, DOMAIN_TX, TxFactory } from '@anticrm/core' +import type { IntlString, Plugin } from '@anticrm/platform' +import { plugin } from '@anticrm/platform' + +export const txFactory = new TxFactory(core.account.System) + +export function createClass (_class: Ref>, attributes: Data>): TxCreateDoc { + return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class) +} + +/** + * @public + */ +export function createDoc ( + _class: Ref>, + attributes: Data, + id?: Ref, + modifiedBy?: Ref +): TxCreateDoc { + const result = txFactory.createTxCreateDoc(_class, core.space.Model, attributes, id) + if (modifiedBy !== undefined) { + result.modifiedBy = modifiedBy + } + return result +} + +/** + * @public + */ +export interface TestMixin extends Doc { + arr: Arr +} + +/** + * @public + */ +export interface AttachedComment extends AttachedDoc { + message: string +} + +/** + * @public + */ +export const test = plugin('test' as Plugin, { + mixin: { + TestMixin: '' as Ref> + }, + class: { + TestComment: '' as Ref> + } +}) + +/** + * @public + * Generate minimal model for testing purposes. + * @returns R + */ +export function genMinModel (): TxCUD[] { + const txes = [] + // Fill Tx'es with basic model classes. + txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS })) + txes.push( + createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS }) + ) + txes.push( + createClass(core.class.AttachedDoc, { + label: 'AttachedDoc' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.MIXIN + }) + ) + txes.push( + createClass(core.class.Class, { + label: 'Class' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + txes.push( + createClass(core.class.Space, { + label: 'Space' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + txes.push( + createClass(core.class.Account, { + label: 'Account' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_MODEL + }) + ) + + txes.push( + createClass(core.class.Tx, { + label: 'Tx' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCUD, { + label: 'TxCUD' as IntlString, + extends: core.class.Tx, + kind: ClassifierKind.CLASS, + domain: DOMAIN_TX + }) + ) + txes.push( + createClass(core.class.TxCreateDoc, { + label: 'TxCreateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxUpdateDoc, { + label: 'TxUpdateDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxRemoveDoc, { + label: 'TxRemoveDoc' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + txes.push( + createClass(core.class.TxCollectionCUD, { + label: 'TxCollectionCUD' as IntlString, + extends: core.class.TxCUD, + kind: ClassifierKind.CLASS + }) + ) + + txes.push( + createClass(test.mixin.TestMixin, { + label: 'TestMixin' as IntlString, + extends: core.class.Doc, + kind: ClassifierKind.MIXIN + }) + ) + + txes.push( + createClass(test.class.TestComment, { + label: 'TestComment' as IntlString, + extends: core.class.AttachedDoc, + kind: ClassifierKind.CLASS + }) + ) + + const u1 = 'User1' as Ref + const u2 = 'User2' as Ref + txes.push( + createDoc(core.class.Account, { email: 'user1@site.com' }, u1), + createDoc(core.class.Account, { email: 'user2@site.com' }, u2), + createDoc(core.class.Space, { + name: 'Sp1', + description: '', + private: false, + members: [u1, u2] + }) + ) + + txes.push( + createDoc(core.class.Space, { + name: 'Sp2', + description: '', + private: false, + members: [u1] + }) + ) + return txes +} diff --git a/server/mongo/src/__tests__/storage.test.ts b/server/mongo/src/__tests__/storage.test.ts new file mode 100644 index 0000000000..eeb87125c1 --- /dev/null +++ b/server/mongo/src/__tests__/storage.test.ts @@ -0,0 +1,288 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +// +// 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 core, { + Class, + Client, + createClient, + Doc, + DocumentQuery, + DOMAIN_MODEL, + DOMAIN_TX, + FindOptions, + FindResult, + generateId, + Hierarchy, ModelDb, + Ref, + SortingOrder, + Space, + Tx, + TxOperations, + TxResult +} from '@anticrm/core' +import { createServerStorage, DbAdapter, DbConfiguration, FullTextAdapter, IndexedDoc } from '@anticrm/server-core' +import { MongoClient } from 'mongodb' +import { createMongoAdapter, createMongoTxAdapter } from '..' +import { getMongoClient, shutdown } from '../utils' +import { genMinModel } from './minmodel' +import { createTaskModel, Task, TaskComment, taskPlugin } from './tasks' + +const txes = genMinModel() + +createTaskModel(txes) + +class NullDbAdapter implements DbAdapter { + async init (model: Tx[]): Promise {} + async findAll( + _class: Ref>, + query: DocumentQuery, + options?: FindOptions | undefined + ): Promise> { + return [] + } + + async tx (tx: Tx): Promise { + return {} + } +} + +async function createNullAdapter (hierarchy: Hierarchy, url: string, db: string, modelDb: ModelDb): Promise { + return new NullDbAdapter() +} + +class NullFullTextAdapter implements FullTextAdapter { + async index (doc: IndexedDoc): Promise { + console.log('noop full text indexer: ', doc) + return {} + } + + async update (id: Ref, update: Record): Promise { + return {} + } + + async search (query: any): Promise { + return [] + } +} + +async function createNullFullTextAdapter (): Promise { + return new NullFullTextAdapter() +} + +describe('mongo operations', () => { + const mongodbUri: string = process.env.MONGODB_URI ?? 'mongodb://localhost:27017' + let mongoClient!: MongoClient + let dbId: string = generateId() + let hierarchy: Hierarchy + let model: ModelDb + let client: Client + let operations: TxOperations + + beforeAll(async () => { + mongoClient = await getMongoClient(mongodbUri) + }) + + afterAll(async () => { + await shutdown() + }) + + beforeEach(async () => { + dbId = 'mongo-testdb-' + generateId() + }) + + afterEach(async () => { + try { + await mongoClient.db(dbId).dropDatabase() + } catch (eee) {} + }) + + async function initDb (): Promise { + // Remove all stuff from database. + hierarchy = new Hierarchy() + model = new ModelDb(hierarchy) + for (const t of txes) { + await hierarchy.tx(t) + } + for (const t of txes) { + await model.tx(t) + } + + const txStorage = await createMongoTxAdapter(hierarchy, mongodbUri, dbId, model) + + // Put all transactions to Tx + for (const t of txes) { + await txStorage.tx(t) + } + + const conf: DbConfiguration = { + domains: { + [DOMAIN_TX]: 'MongoTx', + [DOMAIN_MODEL]: 'Null' + }, + defaultAdapter: 'Mongo', + adapters: { + MongoTx: { + factory: createMongoTxAdapter, + url: mongodbUri + }, + Mongo: { + factory: createMongoAdapter, + url: mongodbUri + }, + Null: { + factory: createNullAdapter, + url: '' + } + }, + fulltextAdapter: { + factory: createNullFullTextAdapter, + url: '' + }, + workspace: dbId + } + const serverStorage = await createServerStorage(conf) + + client = await createClient(async (handler) => { + return await Promise.resolve(serverStorage) + }) + + operations = new TxOperations(client, core.account.System) + } + + beforeEach(async () => { + return await initDb() + }) + + it('check add', async () => { + for (let i = 0; i < 50; i++) { + await operations.createDoc(taskPlugin.class.Task, '' as Ref, { + name: `my-task-${i}`, + description: `${i * i}`, + rate: 20 + i + }) + } + + const r = await client.findAll(taskPlugin.class.Task, {}) + expect(r.length).toEqual(50) + + const r2 = await client.findAll(core.class.Tx, {}) + expect(r2.length).toBeGreaterThan(50) + }) + + it('check find by criteria', async () => { + for (let i = 0; i < 50; i++) { + await operations.createDoc(taskPlugin.class.Task, '' as Ref, { + name: `my-task-${i}`, + description: `${i * i}`, + rate: 20 + i + }) + } + + const r = await client.findAll(taskPlugin.class.Task, {}) + expect(r.length).toEqual(50) + + const first = await client.findAll(taskPlugin.class.Task, { name: 'my-task-0' }) + expect(first.length).toEqual(1) + + const second = await client.findAll(taskPlugin.class.Task, { name: { $like: '%0' } }) + expect(second.length).toEqual(5) + + const third = await client.findAll(taskPlugin.class.Task, { rate: { $in: [25, 26, 27, 28] } }) + expect(third.length).toEqual(4) + }) + + it('check update', async () => { + await operations.createDoc(taskPlugin.class.Task, '' as Ref, { + name: 'my-task', + description: 'some data ', + rate: 20 + }) + + const doc = (await client.findAll(taskPlugin.class.Task, {}))[0] + + await operations.updateDoc(doc._class, doc.space, doc._id, { rate: 30 }) + const tasks = await client.findAll(taskPlugin.class.Task, {}) + expect(tasks.length).toEqual(1) + expect(tasks[0].rate).toEqual(30) + }) + + it('check remove', async () => { + for (let i = 0; i < 10; i++) { + await operations.createDoc(taskPlugin.class.Task, '' as Ref, { + name: `my-task-${i}`, + description: `${i * i}`, + rate: 20 + i + }) + } + + let r = await client.findAll(taskPlugin.class.Task, {}) + expect(r.length).toEqual(10) + await operations.removeDoc(taskPlugin.class.Task, '' as Ref, r[0]._id) + r = await client.findAll(taskPlugin.class.Task, {}) + expect(r.length).toEqual(9) + }) + + it('limit and sorting', async () => { + for (let i = 0; i < 5; i++) { + await operations.createDoc(taskPlugin.class.Task, '' as Ref, { + name: `my-task-${i}`, + description: `${i * i}`, + rate: 20 + i + }) + } + + const without = await client.findAll(taskPlugin.class.Task, {}) + expect(without).toHaveLength(5) + + const limit = await client.findAll(taskPlugin.class.Task, {}, { limit: 1 }) + expect(limit).toHaveLength(1) + + const sortAsc = await client.findAll(taskPlugin.class.Task, {}, { sort: { name: SortingOrder.Ascending } }) + expect(sortAsc[0].name).toMatch('my-task-0') + + const sortDesc = await client.findAll(taskPlugin.class.Task, {}, { sort: { name: SortingOrder.Descending } }) + expect(sortDesc[0].name).toMatch('my-task-4') + }) + + it('check attached', async () => { + const docId = await operations.createDoc(taskPlugin.class.Task, '' as Ref, { + name: 'my-task', + description: 'Descr', + rate: 20 + }) + + await operations.addCollection(taskPlugin.class.TaskComment, '' as Ref, docId, taskPlugin.class.Task, 'tasks', { + message: 'my-msg', + date: new Date() + }) + + await operations.addCollection(taskPlugin.class.TaskComment, '' as Ref, docId, taskPlugin.class.Task, 'tasks', { + message: 'my-msg2', + date: new Date() + }) + + const r = await client.findAll(taskPlugin.class.TaskComment, {}) + expect(r.length).toEqual(2) + + const r2 = await client.findAll(taskPlugin.class.TaskComment, {}, { + lookup: { + attachedTo: taskPlugin.class.Task + } + }) + expect(r2.length).toEqual(2) + console.log(JSON.stringify(r2, undefined, 2)) + expect((r2[0].$lookup?.attachedTo as Task)?._id).toEqual(docId) + }) +}) diff --git a/server/mongo/src/__tests__/tasks.ts b/server/mongo/src/__tests__/tasks.ts new file mode 100644 index 0000000000..3808430dd1 --- /dev/null +++ b/server/mongo/src/__tests__/tasks.ts @@ -0,0 +1,100 @@ +import { Account, AttachedDoc, Class, ClassifierKind, Data, Doc, Domain, Ref, Space, Tx } from '@anticrm/core' +import { IntlString, plugin, Plugin } from '@anticrm/platform' +import { createClass } from './minmodel' + +export interface TaskComment extends AttachedDoc { + message: string + date: Date +} + +export enum TaskStatus { + Open, + Close, + Resolved = 100, + InProgress +} + +export enum TaskReproduce { + Always = 'always', + Rare = 'rare', + Sometimes = 'sometimes' +} + +export interface Task extends Doc { + name: string + description: string + rate?: number + status?: TaskStatus + reproduce?: TaskReproduce + eta?: TaskEstimate | null +} + +/** + * Define ROM and Estimated Time to arrival + */ +export interface TaskEstimate extends AttachedDoc { + rom: number // in hours + eta: number // in hours +} + +export interface TaskMixin extends Task { + textValue?: string +} + +export interface TaskWithSecond extends Task { + secondTask: string | null +} + +const taskIds = 'taskIds' as Plugin + +export const taskPlugin = plugin(taskIds, { + class: { + Task: '' as Ref>, + TaskEstimate: '' as Ref>, + TaskComment: '' as Ref> + } +}) + +/** + * Create a random task with name specified + * @param name + */ +export function createTask (name: string, rate: number, description: string): Data { + return { + name, + description, + rate + } +} + +export const doc1: Task = { + _id: 'd1' as Ref, + _class: taskPlugin.class.Task, + name: 'my-space', + description: 'some-value', + rate: 20, + modifiedBy: 'user' as Ref, + modifiedOn: 10, + // createOn: 10, + space: '' as Ref +} + +export function createTaskModel (txes: Tx[]): void { + txes.push( + createClass(taskPlugin.class.Task, { + kind: ClassifierKind.CLASS, + label: 'Task' as IntlString, + domain: 'test-task' as Domain + }), + createClass(taskPlugin.class.TaskEstimate, { + kind: ClassifierKind.CLASS, + label: 'Estimate' as IntlString, + domain: 'test-task' as Domain + }), + createClass(taskPlugin.class.TaskComment, { + kind: ClassifierKind.CLASS, + label: 'Comment' as IntlString, + domain: 'test-task' as Domain + }) + ) +} diff --git a/server/mongo/src/storage.ts b/server/mongo/src/storage.ts index 312def10f9..3598b5689e 100644 --- a/server/mongo/src/storage.ts +++ b/server/mongo/src/storage.ts @@ -13,29 +13,38 @@ // 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 { + Class, + Doc, + DocumentQuery, + FindOptions, + FindResult, + Ref, + Tx, + TxCreateDoc, + TxMixin, + TxPutBag, + TxRemoveDoc, + TxResult, + TxUpdateDoc +} from '@anticrm/core' +import core, { DOMAIN_MODEL, DOMAIN_TX, Hierarchy, isOperator, ModelDb, SortingOrder, TxProcessor } from '@anticrm/core' import type { DbAdapter, TxAdapter } from '@anticrm/server-core' - -import { MongoClient, Db, Filter, Document, Sort } from 'mongodb' +import { Db, Document, Filter, Sort } from 'mongodb' +import { getMongoClient } from './utils' 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 - ) { + constructor (protected readonly db: Db, protected readonly hierarchy: Hierarchy, protected readonly modelDb: ModelDb) { super() } async init (): Promise {} - private translateQuery (clazz: Ref>, query: DocumentQuery): Filter { + private translateQuery(clazz: Ref>, query: DocumentQuery): Filter { const translated: any = {} for (const key in query) { const value = (query as any)[key] @@ -62,7 +71,11 @@ abstract class MongoAdapterBase extends TxProcessor { return translated } - private async lookup (clazz: Ref>, query: DocumentQuery, options: FindOptions): Promise> { + 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 @@ -81,7 +94,7 @@ abstract class MongoAdapterBase extends TxProcessor { } const domain = this.hierarchy.getDomain(clazz) const cursor = this.db.collection(domain).aggregate(pipeline) - const result = await cursor.toArray() as FindResult + const result = (await cursor.toArray()) as FindResult for (const row of result) { const object = row as any object.$lookup = {} @@ -99,17 +112,22 @@ abstract class MongoAdapterBase extends TxProcessor { return result } - async findAll ( + 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) } + 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?.limit !== undefined) { + cursor = cursor.limit(options.limit) + } if (options !== null && options !== undefined) { if (options.sort !== undefined) { const sort: Sort = {} @@ -127,7 +145,6 @@ abstract class MongoAdapterBase extends TxProcessor { 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 {} } @@ -186,40 +203,56 @@ class MongoAdapter extends MongoAdapterBase { } } ] - 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' }) + 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 + 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' }) + 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 } }) + 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) } } @@ -246,7 +279,6 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter { } override async tx (tx: Tx): Promise { - console.log('mongotx', tx) await this.db.collection(DOMAIN_TX).insertOne(translateDoc(tx)) return {} } @@ -259,9 +291,13 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter { /** * @public */ -export async function createMongoAdapter (hierarchy: Hierarchy, url: string, dbName: string, modelDb: ModelDb): Promise { - const client = new MongoClient(url) - await client.connect() +export async function createMongoAdapter ( + hierarchy: Hierarchy, + url: string, + dbName: string, + modelDb: ModelDb +): Promise { + const client = await getMongoClient(url) const db = client.db(dbName) return new MongoAdapter(db, hierarchy, modelDb) } @@ -269,9 +305,13 @@ export async function createMongoAdapter (hierarchy: Hierarchy, url: string, dbN /** * @public */ -export async function createMongoTxAdapter (hierarchy: Hierarchy, url: string, dbName: string, modelDb: ModelDb): Promise { - const client = new MongoClient(url) - await client.connect() +export async function createMongoTxAdapter ( + hierarchy: Hierarchy, + url: string, + dbName: string, + modelDb: ModelDb +): Promise { + const client = await getMongoClient(url) const db = client.db(dbName) return new MongoTxAdapter(db, hierarchy, modelDb) } diff --git a/server/mongo/src/utils.ts b/server/mongo/src/utils.ts new file mode 100644 index 0000000000..067f575f33 --- /dev/null +++ b/server/mongo/src/utils.ts @@ -0,0 +1,46 @@ +// +// Copyright © 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 { MongoClient, MongoClientOptions } from 'mongodb' + +const connections = new Map>() + +// Register mongo close on process exit. +process.on('exit', () => { + shutdown().catch((err) => console.error(err)) +}) + +/** + * @public + */ +export async function shutdown (): Promise { + for (const c of connections.values()) { + await (await c).close() + } + connections.clear() +} +/** + * Initialize a workspace connection to DB + * @public + */ +export async function getMongoClient (uri: string, options?: MongoClientOptions): Promise { + let client = connections.get(uri) + if (client === undefined) { + client = MongoClient.connect(uri, { ...options }) + await client + connections.set(uri, client) + } + return await client +}