import { Collection, Db, MongoClient, Sort, UpdateOptions } from 'mongodb'; import { DatabaseDriver, DatabaseOption, Encrypted, Index, Records, SortOrder } from '../typings'; import * as dbutils from './utils'; type _Document = { value: Encrypted; expiresAt?: Date; modifiedAt: string; namespace: string; indexes: string[]; }; class Mongo implements DatabaseDriver { private options: DatabaseOption; private client!: MongoClient; private collection!: Collection; private db!: Db; constructor(options: DatabaseOption) { this.options = options; } async init(): Promise { const dbUrl = this.options.url as string; try { this.client = new MongoClient(dbUrl); await this.client.connect(); } catch (err) { console.error(`error connecting to engine: ${this.options.engine}, db: ${err}`); throw err; } this.db = this.client.db(); this.collection = this.db.collection('jacksonStore'); await this.collection.createIndex({ indexes: 1 }); await this.collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 1 }); await this.collection.createIndex({ namespace: 1 }); // eslint-disable-next-line no-constant-condition while (true) { try { if (!this.options.manualMigration) { await this.indexNamespace(); } break; } catch (err) { console.error( `error in index namespace execution for db engine: ${this.options.engine}, err: ${err}` ); await dbutils.sleep(1000); continue; } } return this; } async indexNamespace() { const docs = await this.collection.find({ namespace: { $exists: false } }).toArray(); const searchTerm = ':'; for (const doc of docs || []) { const tokens2 = doc._id.toString().split(searchTerm).slice(0, 2); const namespace = tokens2.join(searchTerm); await this.collection.updateOne({ _id: doc._id }, { $set: { namespace } }); } } async get(namespace: string, key: string): Promise { const res = await this.collection.findOne({ _id: dbutils.key(namespace, key) as any, }); if (res && res.value) { return res.value; } return null; } // eslint-disable-next-line @typescript-eslint/no-unused-vars async getAll( namespace: string, pageOffset?: number, pageLimit?: number, _?: string, sortOrder?: SortOrder ): Promise { const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({ pageOffset, pageLimit, maxLimit: this.options.pageLimit!, }); const docs = await this.collection .find( { namespace: namespace }, { sort: { createdAt: sortOrder === 'ASC' ? 1 : -1 }, skip, limit, } ) .toArray(); if (docs) { return { data: docs.map(({ value }) => value) }; } return { data: [] }; } async getByIndex( namespace: string, idx: Index, pageOffset?: number, pageLimit?: number, // eslint-disable-next-line @typescript-eslint/no-unused-vars _?: string, sortOrder?: SortOrder ): Promise { const sort: Sort = { createdAt: sortOrder === 'ASC' ? 'asc' : 'desc' }; const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({ pageOffset, pageLimit, maxLimit: this.options.pageLimit!, }); const docs = await this.collection .find( { indexes: dbutils.keyForIndex(namespace, idx), }, { sort, skip, limit, } ) .toArray(); const ret: string[] = []; for (const doc of docs || []) { ret.push(doc.value); } return { data: ret }; } async getCount(namespace: string, idx?: Index): Promise { const count = idx !== undefined ? await this.collection.countDocuments( { indexes: dbutils.keyForIndex(namespace, idx) }, { hint: 'indexes_1' } ) : await this.collection.countDocuments({ namespace }, { hint: 'namespace_1' }); return count; } async put(namespace: string, key: string, val: Encrypted, ttl = 0, ...indexes: any[]): Promise { const doc = <_Document>{ value: val, }; if (ttl) { doc.expiresAt = new Date(Date.now() + ttl * 1000); } doc.namespace = namespace; // no ttl support for secondary indexes for (const idx of indexes || []) { const idxKey = dbutils.keyForIndex(namespace, idx); if (!doc.indexes) { doc.indexes = []; } doc.indexes.push(idxKey); } doc.modifiedAt = new Date().toISOString(); await this.collection.updateOne( { _id: dbutils.key(namespace, key) as any }, { $set: doc, $setOnInsert: { createdAt: new Date().toISOString(), }, }, { upsert: true } as UpdateOptions ); } async delete(namespace: string, key: string): Promise { return await this.collection.deleteOne({ _id: dbutils.key(namespace, key) as any, }); } async deleteMany(namespace: string, keys: string[]): Promise { if (keys.length === 0) { return; } const dbKeys = keys.map((key) => dbutils.key(namespace, key)) as any[]; await this.collection.deleteMany({ _id: { $in: dbKeys }, }); } async close(): Promise { await this.client.close(); } } export default { new: async (options: DatabaseOption): Promise => { return await new Mongo(options).init(); }, };