import { parsePaginationOptions } from '@lib/seed/helpers/Request'; import { AccountTypeEnum } from '@src/accounts/account.components'; import { validateOrReject } from 'class-validator'; import { EngineAccessService } from '../EngineAccessService'; import { EngineModel } from '../EngineModel'; import { IEngineSchema } from '../EngineSchema'; import { v4 as uuid } from 'uuid'; import { BulkInput, DeleteOneInput, GetAllInput, GetCountInput, GetManyInput, GetOneInput, GetQueryInput, SaveManyInput, SaveOneInput, UpdateOneCustomInput, UpdateOneInput, UpdateOneSubField, } from './__interface'; import DB from '@lib/seed/services/database/DBService'; import { createChangeStream } from './streams/stream.service'; import { StreamOperationType } from './streams/schemas/stream.components'; import { BulkWriteOperation, ObjectId } from 'mongodb'; import { newError } from '@seed/helpers/Error'; import { clog } from '@seed/helpers/Utils'; export function getQuery( model: EngineModel, input: GetQueryInput, ): any { let finalQuery: any; const { query, ctx } = input; if (ctx) { const account = ctx.ctx.user; const organisationId = ctx.ctx.organisationId; // We add the permission (on r field) to make sure that kind of user has access to the ressource if (!ctx.ctx.noPermissionCheck) finalQuery = EngineAccessService.addPermissionToQuery(account, 'get', { ...query }); // We check if it comes from an organisation // Check of the user's correct authorisation on that organisation comes from the middleware if (!ctx.ctx.noOrganisationCheck) { if (organisationId) { if (model.collectionName == 'accounts') finalQuery['organisationIds'] = organisationId; else finalQuery['organisationId'] = { $eq: organisationId }; } else { // If admin & public query -> do not add model condition and let search every records) if (account && account.types && !account.types.includes(AccountTypeEnum.public) && !account.types.includes(AccountTypeEnum.admin)) { finalQuery['organisationId'] = { $exists: false }; } } } } else { finalQuery = { ...query }; } return finalQuery; } export const getMany = async ( model: EngineModel, input: GetManyInput, ): Promise => { let { pagination } = input; const finalQuery = getQuery(model, input); if (!pagination) pagination = { limit: 100, skip: 0 }; if (!pagination.sort) pagination.sort = model.defaultSort; const paginationOption = parsePaginationOptions(pagination); try { console.log('search request', JSON.stringify(finalQuery, null, 2)); const dbResults = await (await model.db()).find(finalQuery, paginationOption).toArray(); return model.plainToClass(dbResults) as any; } catch (error) { return []; } }; export const getCount = async ( model: EngineModel, input: GetCountInput, ): Promise => { const finalQuery = getQuery(model, input); try { const result = (await (await model.db()).countDocuments(finalQuery)) as any; return result; } catch (error) { return 0; } }; export const getAll = async ( model: EngineModel, input: GetAllInput, ): Promise => { const finalQuery = getQuery(model, input); try { clog(finalQuery); const res = await (await model.db()).find(finalQuery).toArray(); return model.plainToClass(res); } catch (error) { return []; } }; export const getOne = async ( model: EngineModel, input: GetOneInput, ): Promise => { const { query, ctx } = input; let dataToReturn; if (query._id) { const _id = query._id as any; if (_id.match(/^[0-9a-fA-F]{24}$/)) { query._id = new ObjectId(_id) as any; } } if (ctx) { const account = ctx.ctx.user; const organisationId = ctx.ctx.organisationId; const doc = await (await model.db()).findOne(query); if (!doc) throw await newError(4041, { id: query._id, model: model.collectionName }); dataToReturn = model.plainToClass(doc) as any; model.set(dataToReturn); if (!ctx.ctx.noOrganisationCheck) { if (organisationId) { if (model.collectionName != 'accounts') EngineAccessService.checkOrganisationPermissions(model, organisationId); } } if (!ctx.ctx.noPermissionCheck) EngineAccessService.checkPermissions(dataToReturn, account, 'r'); } else { const doc = await (await model.db()).findOne(query); if (!doc) throw await newError(4041, { id: query._id, model: model.collectionName }); dataToReturn = model.plainToClass(doc) as any; model.set(dataToReturn); } return model.get(); }; export const saveOne = async ( model: EngineModel, input: SaveOneInput, ): Promise => { const { newData, additionnalData, upsert } = input; let ctx = input.ctx; if (newData) model.set(newData); if (additionnalData) model.set(additionnalData); // Set the path model.set({ paths: model.getPaths() }); if (ctx) { const account = ctx.ctx.user; const organisationId = ctx.ctx.organisationId; EngineAccessService.addPermissions(model, ['r', 'w', 'd'], [account._id]); // TODO : Check create permissions if (organisationId) { model.set({ organisationId }); } if (model.by) model.by.createdBy = account._id; else { model.by = { createdBy: account._id, }; } } // if (!ctx) ctx = await createApolloContext(); try { if (model.beforeCreate) await model.beforeCreate(ctx); await validateOrReject(model.dbData as any); } catch (error) { throw error; } const dataToSave = model.get() as any; if (model.searchText) dataToSave.searchT = model.searchText(); if (!model._id) model._id = uuid(); delete dataToSave._id; try { const result = await (await model.db()).findOneAndUpdate( { _id: model._id } as any, { $set: dataToSave, $setOnInsert: { _id: model._id, }, } as any, { upsert: upsert ? upsert : true, returnOriginal: false }, ); if (result.ok == 0) throw await newError(1000, result); await createChangeStream(model, StreamOperationType.insert); return model.get(); } catch (error) { throw await newError(1000, error); } }; export const updateOne = async ( model: EngineModel, input: UpdateOneInput, ): Promise => { try { const { query, newData, ctx } = input; if (!model._id) await model.getOne(input); if (newData) model.set(newData); model.set({ paths: model.getPaths() }); if (ctx) { const account = ctx.ctx.user; // Check if doc exists & correct permissions if (model.by) model.by.updatedBy = account._id; else { model.by = { updatedBy: account._id, }; } EngineAccessService.checkPermissions(model, account, 'w'); } if (model.beforeUpdate) await model.beforeUpdate(ctx); const dataToSave = model.get() as any; if (model.searchText) dataToSave.searchT = model.searchText(); const result = await (await model.db()).findOneAndUpdate(query, { $set: dataToSave }, { upsert: false, returnOriginal: false }); if (result.ok != 1) throw await newError(1000, result); model.set(result.value); await createChangeStream(model, StreamOperationType.update); return model.get(); } catch (error) { throw error; } }; export const updateOneCustom = async ( model: EngineModel, input: UpdateOneCustomInput, ): Promise => { try { const { query, updateRequest, ctx } = input; if (!model._id) await model.getOne(input); if (ctx) { const account = ctx.ctx.user; // Check if doc exists // dataBeforeChange = model.get(); EngineAccessService.checkPermissions(model, account, 'w'); if (model.by) model.by.updatedBy = account._id; else { model.by = { updatedBy: account._id, }; } // Validate new data that will go into the DB // const validation = await model.validate(); // if (!validation.success) return validation; } // Prepare data model.updatedAt = new Date(); const result = await (await model.db()).findOneAndUpdate(query, updateRequest, { upsert: false, returnOriginal: false }); if (result.ok != 1) throw await newError(1000, result); if (result.value) model.set(result.value); await createChangeStream(model, StreamOperationType.update); return model.get(); } catch (error) { throw error; } }; export const deleteOne = async ( model: EngineModel, input: DeleteOneInput, ): Promise => { const { query, ctx } = input; try { if (!model._id) await model.getOne(input); if (model.beforeDelete) await model.beforeDelete(ctx); if (ctx) { const account = ctx.ctx.user; // Check if doc exists & correct permissions EngineAccessService.checkPermissions(model, account, 'd'); if (model.by) model.by.deletedBy = account._id; else { model.by = { deletedBy: account._id, }; } } const result = await (await model.db()).findOneAndDelete(query); if (result.ok != 1) throw await newError(1001); const deletedModel = '_deleted_' + model.collectionName; await (await DB.getInstance()).db.collection(deletedModel).insertOne(model.get()); await createChangeStream(model, StreamOperationType.delete); return model.get(); } catch (error) { throw error; } }; export const saveMany = async ( model: EngineModel, input: SaveManyInput, ): Promise => { const ctx = input.ctx; const models = input.models; const dataToSaves: any[] = []; for (let index = 0; index < models.length; index++) { const element = models[index]; // Set the path element.set({ paths: element.getPaths() }); if (ctx) { const account = ctx.ctx.user; const organisationId = ctx.ctx.organisationId; EngineAccessService.addPermissions(element, ['r', 'w', 'd'], [account._id]); // TODO : Check create permissions if (organisationId) { element.set({ organisationId }); } if (element.by) element.by.createdBy = account._id; else { element.by = { createdBy: account._id, }; } try { if (element.beforeCreate) await element.beforeCreate(ctx); await validateOrReject(element.dbData); } catch (error) { throw error; } } const dataToSave = element.get() as any; if (element.searchText) dataToSave.searchT = element.searchText(); if (!element._id) dataToSave._id = uuid(); dataToSaves.push(dataToSave); } try { const result = await (await model.db()).insertMany(dataToSaves, { ordered: true }); if (result.result.ok == 0) throw await newError(1000, result); // await createChangeStream(model, StreamOperationType.insert); return dataToSaves; } catch (error) { throw await newError(1000, error); } }; export const bulk = async ( model: EngineModel, input: BulkInput, ): Promise => { const { inserts, updates, ctx } = input; const bulkOps: BulkWriteOperation[] = []; if (inserts) { inserts.forEach((element) => { bulkOps.push({ insertOne: { document: element.get() as any, }, }); }); } if (updates) { updates.forEach((element) => { if (element.newData) bulkOps.push({ updateOne: { filter: element.query, update: { $set: element.newData }, }, }); else if (element.updateRequest) bulkOps.push({ updateOne: { filter: element.query, update: element.updateRequest, }, }); }); } const bOpsArray: BulkWriteOperation[][] = []; if (bulkOps.length > 0) { const chunk = 1000; let i, j; for (i = 0, j = bulkOps.length; i < j; i += chunk) { bOpsArray.push(bulkOps.slice(i, i + chunk)); } } else bOpsArray.push(bulkOps); try { for (let index = 0; index < bOpsArray.length; index++) { const element = bOpsArray[index]; const res = await (await model.db()).bulkWrite(element); console.log(res); } } catch (error) { throw await newError(1000, error); } return; }; export const updateOneSubField = async ( model: EngineModel, input: UpdateOneSubField, ): Promise => { const { fieldName, fieldValue, subPath, query, ctx } = input; let updatePath = fieldName as string; if (subPath) updatePath += subPath; try { const filterQ = { ...query, }; return await model.updateOneCustom({ query: filterQ, updateRequest: { $set: { [`${updatePath}`]: fieldValue, } as any, }, ctx, }); } catch (error) { throw error; } }; // public async saveMany(newDataArr: Partial[], ctx: ApolloContext | null): Promise { // // Initialize the change stream // const changeStreams: any[] = []; // const afterCreatePromises: any[] = []; // const prehooksPromises = newDataArr.map(async (newData) => { // const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this); // if (ctx) { // const account = ctx.ctx.user; // const organisationId = ctx.ctx.organisationId; // addPermissions(clone, ['r', 'w', 'd'], [account._id]); // if (organisationId) { // newData.organisationId = organisationId; // } // newData.createdBy = account._id; // } // clone.set(newData); // if (!ctx) ctx = await createApolloContext(); // if (clone.prehook) await clone.prehook(); // if (clone.beforeCreate) await clone.beforeCreate(ctx); // const { _id, ...dataToSave } = clone.get(); // const savedId = _id ? _id : uuid(); // clone.set({ _id: savedId }); // const changeStreamId = uuid(); // const changeStreamData = { // _id: changeStreamId, // operation: StreamOperationType.insert, // collection: clone.collectionName, // documentKey: clone._id, // insertedValues: clone.get(), // hookStatus: PostHookStatus.new, // createdAt: new Date(), // updateAt: new Date(), // }; // changeStreams.push(changeStreamData); // // eslint-disable-next-line @typescript-eslint/no-this-alias // afterCreatePromises.push(async () => { // if (clone.afterCreate) { // const res = await asyncHook(changeStreamData); // if (res) { // await clone.afterCreate(changeStreamData); // await (await DB.getInstance()).db // .collection('stream.changes') // .updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } }); // } // } // }); // return clone.get(); // }); // try { // const dataArr = await Promise.all(prehooksPromises); // await (await this.db()).insertMany(dataArr, { ordered: false }); // await (await DB.getInstance()).db.collection('stream.changes').insertMany(changeStreams, { ordered: false }); // await promiseAll(afterCreatePromises); // return dataArr; // } catch (error) { // console.error(error); // throw await newError('db.error', error); // } // } // ██████╗ ███████╗██████╗ ███╗ ███╗██╗███████╗███████╗██╗ ██████╗ ███╗ ██╗███████╗ // ██╔══██╗██╔════╝██╔══██╗████╗ ████║██║██╔════╝██╔════╝██║██╔═══██╗████╗ ██║██╔════╝ // ██████╔╝█████╗ ██████╔╝██╔████╔██║██║███████╗███████╗██║██║ ██║██╔██╗ ██║███████╗ // ██╔═══╝ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║╚════██║╚════██║██║██║ ██║██║╚██╗██║╚════██║ // ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║███████║███████║██║╚██████╔╝██║ ╚████║███████║ // ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ // public async updateQueryWithPermission(query: FilterQuery, ctx: ApolloContext): Promise> { // const account = ctx.ctx.user; // const organisationId = ctx.ctx.organisationId; // // We add the permission (on r field) to make sure that kind of user has access to the ressource // const finalQuery = addPermissionToQuery(account, 'get', { ...query }); // // We check if it comes from an organisation // // Check of the user's correct authorisation on that organisation comes from the middleware // if (organisationId) { // finalQuery['organisationId'] = { $eq: organisationId }; // } else { // finalQuery['organisationId'] = { $exists: false }; // } // return finalQuery; // } // ███████╗██╗ ██╗██████╗ ██████╗ ███████╗███████╗███████╗ // ██╔════╝██║ ██║██╔══██╗ ██╔══██╗██╔════╝██╔════╝██╔════╝ // ███████╗██║ ██║██████╔╝█████╗██████╔╝█████╗ ███████╗███████╗ // ╚════██║██║ ██║██╔══██╗╚════╝██╔══██╗██╔══╝ ╚════██║╚════██║ // ███████║╚██████╔╝██████╔╝ ██║ ██║███████╗███████║███████║ // ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ // public async addOneSubModel( // parentQ: { _id: string }, // subModelRefField: string, // subModelFieldName: keyof this, // subModel: SUB, // subModelInput: any, // ctx: ApolloContext | null, // ): Promise { // try { // subModelInput[subModelRefField] = parentQ._id; // const subM = await subModel.saveOne(subModelInput, ctx); // await this.updateOneCustom(parentQ as any, { $push: { [`${subModelFieldName}`]: subM._id } } as any, null); // return subM; // } catch (error) { // throw error; // } // } // public async editOneSubModel( // subModel: SUB, // subModelId: string, // subModelInput: any, // ctx: ApolloContext | null, // ): Promise { // try { // // No need to check if belongs to the correct organisation, the middleware does that // const subM = await subModel.updateOne({ _id: subModelId } as any, subModelInput, ctx); // return subM; // } catch (error) { // throw error; // } // } // public async deleteOneSubModel( // parentQ: FilterQuery, // subModelFieldName: keyof this, // subModel: SUB, // subModelId: string, // ctx: ApolloContext | null, // ): Promise { // try { // const subM = await subModel.deleteOne({ _id: subModelId } as any, ctx); // await this.updateOneCustom(parentQ, { $pull: { [`${subModelFieldName}`]: subM._id } } as any, null); // return subM; // } catch (error) { // throw error; // } // } // public getOneSubRessource(subRessoureFieldName: keyof this | string, subRessoureId: string): any | undefined { // try { // const subress = _.get(this, subRessoureFieldName); // if (subress) { // const ind = _.findIndex(subress, { _id: subRessoureId }); // if (ind !== -1) return subress[ind]; // } // return; // } catch (error) { // throw error; // } // } // public getDefaultSubRessource(subRessoureFieldName: keyof this | string): any | undefined { // try { // const subress = _.get(this, subRessoureFieldName); // if (subress) { // const ind = _.findIndex(subress, { default: true }); // if (ind !== -1) return subress[ind]; // return subress[0]; // } // return; // } catch (error) { // throw error; // } // } // public async addOneSubRessource( // parentQ: { _id: string }, // subRessoureFieldName: keyof this | string, // subRessoure: any, // ctx: ApolloContext | null, // ): Promise { // try { // if (subRessoure.default && _.get(this, subRessoureFieldName)) { // // Remove all default from others // await this.updateOneCustom( // { _id: this._id } as any, // { // $set: { [`${subRessoureFieldName}.$[].default`]: false } as any, // }, // ctx, // ); // } else subRessoure.default = false; // // Generate ID if not in the model // if (!subRessoure._id) subRessoure._id = uuid(); // await this.updateOneCustom(parentQ as any, { $push: { [`${subRessoureFieldName}`]: subRessoure } } as any, ctx); // return subRessoure; // } catch (error) { // throw error; // } // } // public async deleteOneSubRessource( // parentQ: { _id: string }, // subRessoureFieldName: keyof this | string, // subRessoureId: string, // ctx: ApolloContext | null, // ): Promise { // try { // if (!this.getOneSubRessource(subRessoureFieldName, subRessoureId)) throw await newError('notFound', '400'); // const filterQ = { // ...parentQ, // [`${subRessoureFieldName}._id`]: subRessoureId, // }; // await this.updateOneCustom(filterQ as any, { $pull: { [`${subRessoureFieldName}`]: { _id: subRessoureId } } } as any, ctx); // return subRessoureId; // } catch (error) { // throw error; // } // }