backend/lib/seed/engine/utils/crud.utils.ts
2025-05-14 21:45:16 +02:00

722 lines
25 KiB
TypeScript

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<SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetQueryInput<SchemaDBInterface>,
): 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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetManyInput<SchemaDBInterface>,
): Promise<Schema[]> => {
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetCountInput<SchemaDBInterface>,
): Promise<number> => {
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetAllInput<SchemaDBInterface>,
): Promise<any> => {
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetOneInput<SchemaDBInterface>,
): Promise<Schema> => {
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: SaveOneInput<SchemaDBInterface>,
): Promise<Schema> => {
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: UpdateOneInput<SchemaDBInterface>,
): Promise<Schema> => {
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: UpdateOneCustomInput<SchemaDBInterface>,
): Promise<Schema> => {
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: DeleteOneInput<SchemaDBInterface>,
): Promise<Schema> => {
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: SaveManyInput<SchemaDBInterface>,
): Promise<Schema[]> => {
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: BulkInput<SchemaDBInterface, SchemaDB, Schema>,
): Promise<void> => {
const { inserts, updates, ctx } = input;
const bulkOps: BulkWriteOperation<SchemaDBInterface & IEngineSchema>[] = [];
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<SchemaDBInterface & IEngineSchema>[][] = [];
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 <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: UpdateOneSubField<SchemaDBInterface>,
): Promise<any> => {
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<this>[], ctx: ApolloContext | null): Promise<any[]> {
// // 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<this>, ctx: ApolloContext): Promise<FilterQuery<this>> {
// 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<SUB>(
// parentQ: { _id: string },
// subModelRefField: string,
// subModelFieldName: keyof this,
// subModel: SUB,
// subModelInput: any,
// ctx: ApolloContext | null,
// ): Promise<SUB> {
// 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<SUB extends BaseGraphModel>(
// subModel: SUB,
// subModelId: string,
// subModelInput: any,
// ctx: ApolloContext | null,
// ): Promise<SUB> {
// 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<SUB extends BaseGraphModel>(
// parentQ: FilterQuery<this>,
// subModelFieldName: keyof this,
// subModel: SUB,
// subModelId: string,
// ctx: ApolloContext | null,
// ): Promise<SUB> {
// 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<any> {
// 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<string> {
// 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;
// }
// }