import * as plugins from './smartdata.plugins.js'; import { SmartdataDb } from './smartdata.classes.db.js'; import { SmartdataDbCursor } from './smartdata.classes.cursor.js'; import { type IManager, SmartdataCollection } from './smartdata.classes.collection.js'; import { SmartdataDbWatcher } from './smartdata.classes.watcher.js'; export type TDocCreation = 'db' | 'new' | 'mixed'; /** * saveable - saveable decorator to be used on class properties */ export function svDb() { return (target: SmartDataDbDoc, key: string) => { console.log(`called svDb() on >${target.constructor.name}.${key}<`); if (!target.saveableProperties) { target.saveableProperties = []; } target.saveableProperties.push(key); }; } /** * unique index - decorator to mark a unique index */ export function unI() { return (target: SmartDataDbDoc, key: string) => { console.log(`called unI on >>${target.constructor.name}.${key}<<`); // mark the index as unique if (!target.uniqueIndexes) { target.uniqueIndexes = []; } target.uniqueIndexes.push(key); // and also save it if (!target.saveableProperties) { target.saveableProperties = []; } target.saveableProperties.push(key); }; } export const convertFilterForMongoDb = (filterArg: { [key: string]: any }) => { const convertedFilter: { [key: string]: any } = {}; const convertFilterArgument = (keyPathArg2: string, filterArg2: any) => { if (typeof filterArg2 === 'object') { for (const key of Object.keys(filterArg2)) { if (key.startsWith('$')) { convertedFilter[keyPathArg2] = filterArg2; return; } else if (key.includes('.')) { throw new Error('keys cannot contain dots'); } } for (const key of Object.keys(filterArg2)) { convertFilterArgument(`${keyPathArg2}.${key}`, filterArg2[key]); } } else { convertedFilter[keyPathArg2] = filterArg2; } }; for (const key of Object.keys(filterArg)) { convertFilterArgument(key, filterArg[key]); } return convertedFilter; }; export class SmartDataDbDoc { /** * the collection object an Doc belongs to */ public static collection: SmartdataCollection; public collection: SmartdataCollection; public static defaultManager; public static manager; public manager: TManager; // STATIC public static createInstanceFromMongoDbNativeDoc( this: plugins.tsclass.typeFest.Class, mongoDbNativeDocArg: any ): T { const newInstance = new this(); (newInstance as any).creationStatus = 'db'; for (const key of Object.keys(mongoDbNativeDocArg)) { newInstance[key] = mongoDbNativeDocArg[key]; } return newInstance; } /** * gets all instances as array * @param this * @param filterArg * @returns */ public static async getInstances( this: plugins.tsclass.typeFest.Class, filterArg: plugins.tsclass.typeFest.PartialDeep ): Promise { const foundDocs = await (this as any).collection.findAll(convertFilterForMongoDb(filterArg)); const returnArray = []; for (const foundDoc of foundDocs) { const newInstance: T = (this as any).createInstanceFromMongoDbNativeDoc(foundDoc); returnArray.push(newInstance); } return returnArray; } /** * gets the first matching instance * @param this * @param filterArg * @returns */ public static async getInstance( this: plugins.tsclass.typeFest.Class, filterArg: plugins.tsclass.typeFest.PartialDeep ): Promise { const foundDoc = await (this as any).collection.findOne(convertFilterForMongoDb(filterArg)); if (foundDoc) { const newInstance: T = (this as any).createInstanceFromMongoDbNativeDoc(foundDoc); return newInstance; } else { return null; } } /** * get a unique id prefixed with the class name */ public static async getNewId(this: plugins.tsclass.typeFest.Class, lengthArg: number = 20) { return `${(this as any).className}:${plugins.smartunique.shortId(lengthArg)}`; } /** * get cursor * @returns */ public static async getCursor( this: plugins.tsclass.typeFest.Class, filterArg: plugins.tsclass.typeFest.PartialDeep ) { const collection: SmartdataCollection = (this as any).collection; const cursor: SmartdataDbCursor = await collection.getCursor( convertFilterForMongoDb(filterArg), this as any as typeof SmartDataDbDoc ); return cursor; } /** * watch the collection * @param this * @param filterArg * @param forEachFunction */ public static async watch( this: plugins.tsclass.typeFest.Class, filterArg: plugins.tsclass.typeFest.PartialDeep ) { const collection: SmartdataCollection = (this as any).collection; const watcher: SmartdataDbWatcher = await collection.watch( convertFilterForMongoDb(filterArg), this as any ); return watcher; } /** * run a function for all instances * @returns */ public static async forEach( this: plugins.tsclass.typeFest.Class, filterArg: plugins.tsclass.typeFest.PartialDeep, forEachFunction: (itemArg: T) => Promise ) { const cursor: SmartdataDbCursor = await (this as any).getCursor(filterArg); await cursor.forEach(forEachFunction); } // INSTANCE /** * how the Doc in memory was created, may prove useful later. */ public creationStatus: TDocCreation = 'new'; /** * updated from db in any case where doc comes from db */ @svDb() _createdAt: number = Date.now(); /** * will be updated everytime the doc is saved */ @svDb() _updatedAt: number = Date.now(); /** * unique indexes */ public uniqueIndexes: string[]; /** * an array of saveable properties of a doc */ public saveableProperties: string[]; /** * name */ public name: string; /** * primary id in the database */ public dbDocUniqueId: string; /** * class constructor */ constructor() {} /** * saves this instance but not any connected items * may lead to data inconsistencies, but is faster */ public async save() { // tslint:disable-next-line: no-this-assignment const self: any = this; let dbResult: any; this._updatedAt = Date.now(); switch (this.creationStatus) { case 'db': dbResult = await this.collection.update(self); break; case 'new': dbResult = await this.collection.insert(self); this.creationStatus = 'db'; break; default: console.error('neither new nor in db?'); } return dbResult; } /** * deletes a document from the database */ public async delete() { await this.collection.delete(this); } /** * also store any referenced objects to DB * better for data consistency */ public saveDeep(savedMapArg: plugins.lik.ObjectMap> = null) { if (!savedMapArg) { savedMapArg = new plugins.lik.ObjectMap>(); } savedMapArg.add(this); this.save(); for (const propertyKey of Object.keys(this)) { const property: any = this[propertyKey]; if (property instanceof SmartDataDbDoc && !savedMapArg.checkForObject(property)) { property.saveDeep(savedMapArg); } } } /** * updates an object from db */ public async updateFromDb() { const mongoDbNativeDoc = await this.collection.findOne(await this.createIdentifiableObject()); for (const key of Object.keys(mongoDbNativeDoc)) { this[key] = mongoDbNativeDoc[key]; } } /** * creates a saveable object so the instance can be persisted as json in the database */ public async createSavableObject(): Promise { const saveableObject: unknown = {}; // is not exposed to outside, so any is ok here for (const propertyNameString of this.saveableProperties) { saveableObject[propertyNameString] = this[propertyNameString]; } return saveableObject as TImplements; } /** * creates an identifiable object for operations that require filtering */ public async createIdentifiableObject() { const identifiableObject: any = {}; // is not exposed to outside, so any is ok here for (const propertyNameString of this.uniqueIndexes) { identifiableObject[propertyNameString] = this[propertyNameString]; } return identifiableObject; } }