import * as plugins from './smartdata.plugins.js'; import { SmartdataDb } from './smartdata.classes.db.js'; import { SmartdataDbCursor } from './smartdata.classes.cursor.js'; import { SmartDataDbDoc } from './smartdata.classes.doc.js'; import { SmartdataDbWatcher } from './smartdata.classes.watcher.js'; import { CollectionFactory } from './smartdata.classes.collectionfactory.js'; export interface IFindOptions { limit?: number; } /** * */ export interface IDocValidationFunc { (doc: T): boolean; } export type TDelayed = () => TDelayedArg; const collectionFactory = new CollectionFactory(); /** * This is a decorator that will tell the decorated class what dbTable to use * @param dbArg */ export function Collection(dbArg: SmartdataDb | TDelayed) { return function classDecorator(constructor: T) { return class extends constructor { public static get collection() { if (!(dbArg instanceof SmartdataDb)) { dbArg = dbArg(); } return collectionFactory.getCollection(constructor.name, dbArg); } public get collection() { if (!(dbArg instanceof SmartdataDb)) { dbArg = dbArg(); } return collectionFactory.getCollection(constructor.name, dbArg); } }; }; } export interface IManager { db: SmartdataDb; } export const setDefaultManagerForDoc = (managerArg: IManager, dbDocArg: T): T => { (dbDocArg as any).prototype.defaultManager = managerArg; return dbDocArg; }; /** * This is a decorator that will tell the decorated class what dbTable to use * @param dbArg */ export function Manager(managerArg?: TManager | TDelayed) { return function classDecorator(constructor: T) { return class extends constructor { public static get collection() { let dbArg: SmartdataDb; if (!managerArg) { dbArg = this.prototype.defaultManager.db; } else if (managerArg['db']) { dbArg = (managerArg as TManager).db; } else { dbArg = (managerArg as TDelayed)().db; } return collectionFactory.getCollection(constructor.name, dbArg); } public get collection() { let dbArg: SmartdataDb; if (!managerArg) { //console.log(this.defaultManager.db); //process.exit(0) dbArg = this.defaultManager.db; } else if (managerArg['db']) { dbArg = (managerArg as TManager).db; } else { dbArg = (managerArg as TDelayed)().db; } return collectionFactory.getCollection(constructor.name, dbArg); } public static get manager() { let manager: TManager; if (!managerArg) { manager = this.prototype.defaultManager; } else if (managerArg['db']) { manager = managerArg as TManager; } else { manager = (managerArg as TDelayed)(); } return manager; } public get manager() { let manager: TManager; if (!managerArg) { manager = this.defaultManager; } else if (managerArg['db']) { manager = managerArg as TManager; } else { manager = (managerArg as TDelayed)(); } return manager; } }; }; } // tslint:disable-next-line: max-classes-per-file export class SmartdataCollection { /** * the collection that is used */ public mongoDbCollection: plugins.mongodb.Collection; public objectValidation: IDocValidationFunc = null; public collectionName: string; public smartdataDb: SmartdataDb; public uniqueIndexes: string[] = []; constructor(classNameArg: string, smartDataDbArg: SmartdataDb) { // tell the collection where it belongs this.collectionName = classNameArg; this.smartdataDb = smartDataDbArg; // tell the db class about it (important since Db uses different systems under the hood) this.smartdataDb.addCollection(this); } /** * makes sure a collection exists within MongoDb that maps to the SmartdataCollection */ public async init() { if (!this.mongoDbCollection) { // connect this instance to a MongoDB collection const availableMongoDbCollections = await this.smartdataDb.mongoDb.collections(); const wantedCollection = availableMongoDbCollections.find((collection) => { return collection.collectionName === this.collectionName; }); if (!wantedCollection) { await this.smartdataDb.mongoDb.createCollection(this.collectionName); console.log(`Successfully initiated Collection ${this.collectionName}`); } this.mongoDbCollection = this.smartdataDb.mongoDb.collection(this.collectionName); } } /** * mark unique index */ public markUniqueIndexes(keyArrayArg: string[] = []) { for (const key of keyArrayArg) { if (!this.uniqueIndexes.includes(key)) { this.mongoDbCollection.createIndex(key, { unique: true, }); // make sure we only call this once and not for every doc we create this.uniqueIndexes.push(key); } } } /** * adds a validation function that all newly inserted and updated objects have to pass */ public addDocValidation(funcArg: IDocValidationFunc) { this.objectValidation = funcArg; } /** * finds an object in the DbCollection */ public async findOne(filterObject: any): Promise { await this.init(); const cursor = this.mongoDbCollection.find(filterObject); const result = await cursor.next(); cursor.close(); return result; } public async getCursor(filterObjectArg: any, dbDocArg: typeof SmartDataDbDoc): Promise> { await this.init(); const cursor = this.mongoDbCollection.find(filterObjectArg); return new SmartdataDbCursor(cursor, dbDocArg); } /** * finds an object in the DbCollection */ public async findAll(filterObject: any): Promise { await this.init(); const cursor = this.mongoDbCollection.find(filterObject); const result = await cursor.toArray(); cursor.close(); return result; } /** * watches the collection while applying a filter */ public async watch(filterObject: any, smartdataDbDocArg: typeof SmartDataDbDoc): Promise { await this.init(); const changeStream = this.mongoDbCollection.watch([ { $match: filterObject } ], { fullDocument: 'updateLookup' }); const smartdataWatcher = new SmartdataDbWatcher(changeStream, smartdataDbDocArg); await smartdataWatcher.readyDeferred.promise; return smartdataWatcher; } /** * create an object in the database */ public async insert(dbDocArg: T & SmartDataDbDoc): Promise { await this.init(); await this.checkDoc(dbDocArg); this.markUniqueIndexes(dbDocArg.uniqueIndexes); const saveableObject = await dbDocArg.createSavableObject(); const result = await this.mongoDbCollection.insertOne(saveableObject); return result; } /** * inserts object into the DbCollection */ public async update(dbDocArg: T & SmartDataDbDoc): Promise { await this.init(); await this.checkDoc(dbDocArg); const identifiableObject = await dbDocArg.createIdentifiableObject(); const saveableObject = await dbDocArg.createSavableObject(); const updateableObject: any = {}; for (const key of Object.keys(saveableObject)) { if (identifiableObject[key]) { continue; } updateableObject[key] = saveableObject[key]; } const result = await this.mongoDbCollection.updateOne( identifiableObject, { $set: updateableObject }, { upsert: true } ); return result; } public async delete(dbDocArg: T & SmartDataDbDoc): Promise { await this.init(); await this.checkDoc(dbDocArg); const identifiableObject = await dbDocArg.createIdentifiableObject(); await this.mongoDbCollection.deleteOne(identifiableObject); } /** * checks a Doc for constraints * if this.objectValidation is not set it passes. */ private checkDoc(docArg: T): Promise { const done = plugins.smartq.defer(); let validationResult = true; if (this.objectValidation) { validationResult = this.objectValidation(docArg); } if (validationResult) { done.resolve(); } else { done.reject('validation of object did not pass'); } return done.promise; } }