mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-01-16 23:00:51 +00:00
1581 lines
46 KiB
TypeScript
1581 lines
46 KiB
TypeScript
import { EncryptionSecret, WorkflowHostname } from "../EnvironmentConfig";
|
|
import PostgresAppInstance from "../Infrastructure/PostgresDatabase";
|
|
import ClusterKeyAuthorization from "../Middleware/ClusterKeyAuthorization";
|
|
import CountBy from "../Types/Database/CountBy";
|
|
import CreateBy from "../Types/Database/CreateBy";
|
|
import DeleteBy from "../Types/Database/DeleteBy";
|
|
import DeleteById from "../Types/Database/DeleteById";
|
|
import DeleteOneBy from "../Types/Database/DeleteOneBy";
|
|
import FindBy from "../Types/Database/FindBy";
|
|
import FindOneBy from "../Types/Database/FindOneBy";
|
|
import FindOneByID from "../Types/Database/FindOneByID";
|
|
import {
|
|
DatabaseTriggerType,
|
|
OnCreate,
|
|
OnDelete,
|
|
OnFind,
|
|
OnUpdate,
|
|
} from "../Types/Database/Hooks";
|
|
import ModelPermission from "../Types/Database/Permissions/Index";
|
|
import { CheckReadPermissionType } from "../Types/Database/Permissions/ReadPermission";
|
|
import Query from "../Types/Database/Query";
|
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
import RelationSelect from "../Types/Database/RelationSelect";
|
|
import SearchBy from "../Types/Database/SearchBy";
|
|
import SearchResult from "../Types/Database/SearchResult";
|
|
import Select from "../Types/Database/Select";
|
|
import UpdateBy from "../Types/Database/UpdateBy";
|
|
import UpdateByID from "../Types/Database/UpdateByID";
|
|
import UpdateByIDAndFetch from "../Types/Database/UpdateByIDAndFetch";
|
|
import UpdateOneBy from "../Types/Database/UpdateOneBy";
|
|
import Encryption from "../Utils/Encryption";
|
|
import logger from "../Utils/Logger";
|
|
import BaseService from "./BaseService";
|
|
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
import { WorkflowRoute } from "../../ServiceRoute";
|
|
import Protocol from "../../Types/API/Protocol";
|
|
import Route from "../../Types/API/Route";
|
|
import URL from "../../Types/API/URL";
|
|
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
import { getMaxLengthFromTableColumnType } from "../../Types/Database/ColumnLength";
|
|
import Columns from "../../Types/Database/Columns";
|
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
import PartialEntity from "../../Types/Database/PartialEntity";
|
|
import { TableColumnMetadata } from "../../Types/Database/TableColumn";
|
|
import TableColumnType from "../../Types/Database/TableColumnType";
|
|
import { getUniqueColumnsBy } from "../../Types/Database/UniqueColumnBy";
|
|
import Dictionary from "../../Types/Dictionary";
|
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
import DatabaseNotConnectedException from "../../Types/Exception/DatabaseNotConnectedException";
|
|
import Exception from "../../Types/Exception/Exception";
|
|
import HashedString from "../../Types/HashedString";
|
|
import { JSONObject, JSONValue } from "../../Types/JSON";
|
|
import JSONFunctions from "../../Types/JSONFunctions";
|
|
import ObjectID from "../../Types/ObjectID";
|
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
import Text from "../../Types/Text";
|
|
import Typeof from "../../Types/Typeof";
|
|
import API from "../../Utils/API";
|
|
import Slug from "../../Utils/Slug";
|
|
import { DataSource, Repository, SelectQueryBuilder } from "typeorm";
|
|
import { FindWhere } from "../../Types/BaseDatabase/Query";
|
|
import Realtime from "../Utils/Realtime";
|
|
import ModelEventType from "../../Types/Realtime/ModelEventType";
|
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
|
|
class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
|
|
public modelType!: { new (): TBaseModel };
|
|
private model!: TBaseModel;
|
|
private modelName!: string;
|
|
|
|
private _hardDeleteItemByColumnName: string = "";
|
|
public get hardDeleteItemByColumnName(): string {
|
|
return this._hardDeleteItemByColumnName;
|
|
}
|
|
public set hardDeleteItemByColumnName(v: string) {
|
|
this._hardDeleteItemByColumnName = v;
|
|
}
|
|
|
|
private _hardDeleteItemsOlderThanDays: number = 0;
|
|
public get hardDeleteItemsOlderThanDays(): number {
|
|
return this._hardDeleteItemsOlderThanDays;
|
|
}
|
|
public set hardDeleteItemsOlderThanDays(v: number) {
|
|
this._hardDeleteItemsOlderThanDays = v;
|
|
}
|
|
|
|
public doNotAllowDelete: boolean = false;
|
|
|
|
public constructor(modelType: { new (): TBaseModel }) {
|
|
super();
|
|
this.modelType = modelType;
|
|
this.model = new modelType();
|
|
this.modelName = modelType.name;
|
|
}
|
|
|
|
public setDoNotAllowDelete(doNotAllowDelete: boolean): void {
|
|
this.doNotAllowDelete = doNotAllowDelete;
|
|
}
|
|
|
|
public hardDeleteItemsOlderThanInDays(
|
|
columnName: string,
|
|
olderThan: number,
|
|
): void {
|
|
this.hardDeleteItemByColumnName = columnName;
|
|
this.hardDeleteItemsOlderThanDays = olderThan;
|
|
}
|
|
|
|
public getModel(): TBaseModel {
|
|
return this.model;
|
|
}
|
|
|
|
public getQueryBuilder(modelName: string): SelectQueryBuilder<TBaseModel> {
|
|
return this.getRepository().createQueryBuilder(modelName);
|
|
}
|
|
|
|
public getRepository(): Repository<TBaseModel> {
|
|
if (!PostgresAppInstance.isConnected()) {
|
|
throw new DatabaseNotConnectedException();
|
|
}
|
|
|
|
const dataSource: DataSource | null = PostgresAppInstance.getDataSource();
|
|
|
|
if (dataSource) {
|
|
return dataSource.getRepository<TBaseModel>(this.modelType.name);
|
|
}
|
|
|
|
throw new DatabaseNotConnectedException();
|
|
}
|
|
|
|
protected isValid(data: TBaseModel): boolean {
|
|
if (!data) {
|
|
throw new BadDataException("Data cannot be null");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected generateDefaultValues(data: TBaseModel): TBaseModel {
|
|
const tableColumns: Array<string> = data.getTableColumns().columns;
|
|
|
|
for (const column of tableColumns) {
|
|
const metadata: TableColumnMetadata = data.getTableColumnMetadata(column);
|
|
if (metadata.forceGetDefaultValueOnCreate) {
|
|
(data as any)[column] = metadata.forceGetDefaultValueOnCreate();
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
protected async checkForUniqueValues(data: TBaseModel): Promise<TBaseModel> {
|
|
const tableColumns: Array<string> = data.getTableColumns().columns;
|
|
|
|
for (const columnName of tableColumns) {
|
|
const metadata: TableColumnMetadata =
|
|
data.getTableColumnMetadata(columnName);
|
|
if (metadata.unique && data.getColumnValue(columnName)) {
|
|
// check for unique values.
|
|
const count: PositiveNumber = await this.countBy({
|
|
query: {
|
|
[columnName]: data.getColumnValue(columnName),
|
|
} as any,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (count.toNumber() > 0) {
|
|
throw new BadDataException(
|
|
`${metadata.title} ${data
|
|
.getColumnValue(columnName)
|
|
?.toString()} already exists. Please choose a different ${
|
|
metadata.title
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
protected checkRequiredFields(data: TBaseModel): TBaseModel {
|
|
// Check required fields.
|
|
|
|
const relationalColumns: Dictionary<string> = {};
|
|
|
|
const tableColumns: Array<string> = data.getTableColumns().columns;
|
|
|
|
for (const column of tableColumns) {
|
|
const metadata: TableColumnMetadata = data.getTableColumnMetadata(column);
|
|
if (metadata.manyToOneRelationColumn) {
|
|
relationalColumns[metadata.manyToOneRelationColumn] = column;
|
|
}
|
|
}
|
|
|
|
for (const requiredField of data.getRequiredColumns().columns) {
|
|
if (typeof (data as any)[requiredField] === Typeof.Boolean) {
|
|
if (
|
|
!(data as any)[requiredField] &&
|
|
(data as any)[requiredField] !== false &&
|
|
!data.isDefaultValueColumn(requiredField)
|
|
) {
|
|
throw new BadDataException(`${requiredField} is required`);
|
|
}
|
|
} else if (
|
|
!(data as any)[requiredField] &&
|
|
!data.isDefaultValueColumn(requiredField)
|
|
) {
|
|
const metadata: TableColumnMetadata =
|
|
data.getTableColumnMetadata(requiredField);
|
|
|
|
if (
|
|
metadata &&
|
|
metadata.manyToOneRelationColumn &&
|
|
metadata.type === TableColumnType.Entity &&
|
|
data.getColumnValue(metadata.manyToOneRelationColumn)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
relationalColumns[requiredField] &&
|
|
data.getColumnValue(relationalColumns[requiredField] as string)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
throw new BadDataException(`${requiredField} is required`);
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
protected async onBeforeCreate(
|
|
createBy: CreateBy<TBaseModel>,
|
|
): Promise<OnCreate<TBaseModel>> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve({
|
|
createBy: createBy as CreateBy<TBaseModel>,
|
|
carryForward: undefined,
|
|
});
|
|
}
|
|
|
|
private async _onBeforeCreate(
|
|
createBy: CreateBy<TBaseModel>,
|
|
): Promise<OnCreate<TBaseModel>> {
|
|
// Private method that runs before create.
|
|
const projectIdColumn: string | null = this.model.getTenantColumn();
|
|
|
|
if (projectIdColumn && createBy.props.tenantId) {
|
|
(createBy.data as any)[projectIdColumn] = createBy.props.tenantId;
|
|
}
|
|
|
|
return await this.onBeforeCreate(createBy);
|
|
}
|
|
|
|
protected async encrypt(
|
|
data: TBaseModel | PartialEntity<TBaseModel>,
|
|
): Promise<TBaseModel | PartialEntity<TBaseModel>> {
|
|
for (const key of this.model.getEncryptedColumns().columns) {
|
|
if (!(data as any)[key]) {
|
|
continue;
|
|
}
|
|
|
|
// If data is an object.
|
|
if (typeof (data as any)[key] === Typeof.Object) {
|
|
const dataObj: JSONObject = (data as any)[key] as JSONObject;
|
|
|
|
for (const key in dataObj) {
|
|
dataObj[key] = await Encryption.encrypt(dataObj[key] as string);
|
|
}
|
|
|
|
(data as any)[key] = dataObj;
|
|
} else {
|
|
//If its string or other type.
|
|
(data as any)[key] = await Encryption.encrypt(
|
|
(data as any)[key] as string,
|
|
);
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
protected async hash(data: TBaseModel): Promise<TBaseModel> {
|
|
const columns: Columns = data.getHashedColumns();
|
|
|
|
for (const key of columns.columns) {
|
|
if (
|
|
data.hasValue(key) &&
|
|
!(data.getValue(key) as HashedString).isValueHashed()
|
|
) {
|
|
await ((data as any)[key] as HashedString).hashValue(EncryptionSecret);
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
protected async decrypt(data: TBaseModel): Promise<TBaseModel> {
|
|
for (const key of data.getEncryptedColumns().columns) {
|
|
if (!data.hasValue(key)) {
|
|
continue;
|
|
}
|
|
|
|
// If data is an object.
|
|
if (typeof data.getValue(key) === Typeof.Object) {
|
|
const dataObj: JSONObject = data.getValue(key) as JSONObject;
|
|
|
|
for (const key in dataObj) {
|
|
dataObj[key] = await Encryption.decrypt(dataObj[key] as string);
|
|
}
|
|
|
|
data.setValue(key, dataObj);
|
|
} else {
|
|
//If its string or other type.
|
|
data.setValue(key, await Encryption.decrypt((data as any)[key]));
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
protected async onBeforeDelete(
|
|
deleteBy: DeleteBy<TBaseModel>,
|
|
): Promise<OnDelete<TBaseModel>> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve({ deleteBy, carryForward: null });
|
|
}
|
|
|
|
protected async onBeforeUpdate(
|
|
updateBy: UpdateBy<TBaseModel>,
|
|
): Promise<OnUpdate<TBaseModel>> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve({ updateBy, carryForward: null });
|
|
}
|
|
|
|
protected async onBeforeFind(
|
|
findBy: FindBy<TBaseModel>,
|
|
): Promise<OnFind<TBaseModel>> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve({ findBy, carryForward: null });
|
|
}
|
|
|
|
protected async onCreateSuccess(
|
|
_onCreate: OnCreate<TBaseModel>,
|
|
createdItem: TBaseModel,
|
|
): Promise<TBaseModel> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve(createdItem);
|
|
}
|
|
|
|
protected async onCreateError(error: Exception): Promise<Exception> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve(error);
|
|
}
|
|
|
|
protected async onUpdateSuccess(
|
|
onUpdate: OnUpdate<TBaseModel>,
|
|
_updatedItemIds: Array<ObjectID>,
|
|
): Promise<OnUpdate<TBaseModel>> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve(onUpdate);
|
|
}
|
|
|
|
protected async onUpdateError(error: Exception): Promise<Exception> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve(error);
|
|
}
|
|
|
|
protected async onDeleteSuccess(
|
|
onDelete: OnDelete<TBaseModel>,
|
|
_itemIdsBeforeDelete: Array<ObjectID>,
|
|
): Promise<OnDelete<TBaseModel>> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve(onDelete);
|
|
}
|
|
|
|
protected async onDeleteError(error: Exception): Promise<Exception> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve(error);
|
|
}
|
|
|
|
protected async onFindSuccess(
|
|
onFind: OnFind<TBaseModel>,
|
|
items: Array<TBaseModel>,
|
|
): Promise<OnFind<TBaseModel>> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve({ ...onFind, carryForward: items });
|
|
}
|
|
|
|
protected async onFindError(error: Exception): Promise<Exception> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve(error);
|
|
}
|
|
|
|
protected async onCountSuccess(
|
|
count: PositiveNumber,
|
|
): Promise<PositiveNumber> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve(count);
|
|
}
|
|
|
|
protected async onCountError(error: Exception): Promise<Exception> {
|
|
// A place holder method used for overriding.
|
|
return Promise.resolve(error);
|
|
}
|
|
|
|
protected async getException(error: Exception): Promise<void> {
|
|
throw error;
|
|
}
|
|
|
|
private generateSlug(createBy: CreateBy<TBaseModel>): CreateBy<TBaseModel> {
|
|
if (createBy.data.getSlugifyColumn()) {
|
|
(createBy.data as any)[createBy.data.getSaveSlugToColumn() as string] =
|
|
Slug.getSlug(
|
|
(createBy.data as any)[createBy.data.getSlugifyColumn() as string]
|
|
? ((createBy.data as any)[
|
|
createBy.data.getSlugifyColumn() as string
|
|
] as string)
|
|
: null,
|
|
);
|
|
}
|
|
|
|
return createBy;
|
|
}
|
|
|
|
private async sanitizeCreateOrUpdate(
|
|
data: TBaseModel | PartialEntity<TBaseModel>,
|
|
props: DatabaseCommonInteractionProps,
|
|
isUpdate: boolean = false,
|
|
): Promise<TBaseModel | PartialEntity<TBaseModel>> {
|
|
data = this.checkMaxLengthOfFields(data as TBaseModel);
|
|
|
|
const columns: Columns = this.model.getTableColumns();
|
|
|
|
for (const columnName of columns.columns) {
|
|
const tableColumnMetadata: TableColumnMetadata =
|
|
this.model.getTableColumnMetadata(columnName);
|
|
|
|
if (this.model.isEntityColumn(columnName)) {
|
|
const columnValue: JSONValue = (data as any)[columnName];
|
|
|
|
if (
|
|
data &&
|
|
columnName &&
|
|
tableColumnMetadata.modelType &&
|
|
columnValue &&
|
|
tableColumnMetadata.type === TableColumnType.Entity &&
|
|
(typeof columnValue === "string" || columnValue instanceof ObjectID)
|
|
) {
|
|
const relatedType: BaseModel = new tableColumnMetadata.modelType();
|
|
relatedType._id = columnValue.toString();
|
|
(data as any)[columnName] = relatedType;
|
|
}
|
|
|
|
if (
|
|
data &&
|
|
Array.isArray(columnValue) &&
|
|
columnValue.length > 0 &&
|
|
tableColumnMetadata.modelType &&
|
|
columnValue &&
|
|
tableColumnMetadata.type === TableColumnType.EntityArray
|
|
) {
|
|
const itemsArray: Array<BaseModel> = [];
|
|
for (const item of columnValue) {
|
|
if (typeof item === "string" || item instanceof ObjectID) {
|
|
const basemodelItem: BaseModel =
|
|
new tableColumnMetadata.modelType();
|
|
basemodelItem._id = item.toString();
|
|
itemsArray.push(basemodelItem);
|
|
} else if (
|
|
item &&
|
|
typeof item === Typeof.Object &&
|
|
(item as JSONObject)["_id"] &&
|
|
typeof (item as JSONObject)["_id"] === Typeof.String
|
|
) {
|
|
const basemodelItem: BaseModel =
|
|
new tableColumnMetadata.modelType();
|
|
basemodelItem._id = (
|
|
(item as JSONObject)["_id"] as string
|
|
).toString();
|
|
itemsArray.push(basemodelItem);
|
|
} else if (item instanceof BaseModel) {
|
|
itemsArray.push(item);
|
|
}
|
|
}
|
|
(data as any)[columnName] = itemsArray;
|
|
}
|
|
}
|
|
|
|
if (this.model.isHashedStringColumn(columnName)) {
|
|
const columnValue: JSONValue = (data as any)[columnName];
|
|
|
|
if (
|
|
data &&
|
|
columnName &&
|
|
columnValue &&
|
|
columnValue instanceof HashedString
|
|
) {
|
|
if (!columnValue.isValueHashed()) {
|
|
await columnValue.hashValue(EncryptionSecret);
|
|
}
|
|
|
|
(data as any)[columnName] = columnValue.toString();
|
|
}
|
|
}
|
|
|
|
// if its a Date column and if date is null then set it to null.
|
|
if (
|
|
(data as any)[columnName] === "" &&
|
|
tableColumnMetadata.type === TableColumnType.Date
|
|
) {
|
|
(data as any)[columnName] = null;
|
|
}
|
|
}
|
|
|
|
// check createByUserId.
|
|
|
|
if (!isUpdate && props.userId) {
|
|
(data as any)["createdByUserId"] = props.userId;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async onTriggerRealtime(
|
|
modelId: ObjectID,
|
|
projectId: ObjectID,
|
|
modelEventType: ModelEventType,
|
|
): Promise<void> {
|
|
logger.debug("Realtime Events Enabled");
|
|
logger.debug(this.model.enableRealtimeEventsOn);
|
|
|
|
if (Realtime.isInitialized() && this.model.enableRealtimeEventsOn) {
|
|
logger.debug("Emitting realtime event");
|
|
let shouldEmitEvent: boolean = false;
|
|
|
|
if (
|
|
this.model.enableRealtimeEventsOn.create &&
|
|
modelEventType === ModelEventType.Create
|
|
) {
|
|
shouldEmitEvent = true;
|
|
}
|
|
|
|
if (
|
|
this.model.enableRealtimeEventsOn.update &&
|
|
modelEventType === ModelEventType.Update
|
|
) {
|
|
shouldEmitEvent = true;
|
|
}
|
|
|
|
if (
|
|
this.model.enableRealtimeEventsOn.delete &&
|
|
modelEventType === ModelEventType.Delete
|
|
) {
|
|
shouldEmitEvent = true;
|
|
}
|
|
|
|
if (!shouldEmitEvent) {
|
|
logger.debug("Realtime event not enabled for this event type");
|
|
return;
|
|
}
|
|
|
|
logger.debug("Emitting realtime event");
|
|
Realtime.emitModelEvent({
|
|
tenantId: projectId,
|
|
eventType: modelEventType,
|
|
modelId: modelId,
|
|
modelType: this.modelType,
|
|
}).catch((err: Error) => {
|
|
logger.error("Cannot emit realtime event");
|
|
logger.error(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async onTriggerWorkflow(
|
|
id: ObjectID,
|
|
projectId: ObjectID,
|
|
triggerType: DatabaseTriggerType,
|
|
miscData?: JSONObject | undefined, // miscData is used for passing data to workflow.
|
|
): Promise<void> {
|
|
if (this.getModel().enableWorkflowOn) {
|
|
API.post(
|
|
new URL(
|
|
Protocol.HTTP,
|
|
WorkflowHostname,
|
|
new Route(
|
|
`/${WorkflowRoute.toString()}/model/${projectId.toString()}/${Text.pascalCaseToDashes(
|
|
this.getModel().tableName!,
|
|
)}/${triggerType}`,
|
|
),
|
|
),
|
|
{
|
|
data: {
|
|
_id: id.toString(),
|
|
miscData: miscData,
|
|
},
|
|
},
|
|
{
|
|
...ClusterKeyAuthorization.getClusterKeyHeaders(),
|
|
},
|
|
).catch((error: Error) => {
|
|
logger.error(error);
|
|
});
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async create(createBy: CreateBy<TBaseModel>): Promise<TBaseModel> {
|
|
const onCreate: OnCreate<TBaseModel> = createBy.props.ignoreHooks
|
|
? { createBy, carryForward: [] }
|
|
: await this._onBeforeCreate(createBy);
|
|
|
|
let _createdBy: CreateBy<TBaseModel> = onCreate.createBy;
|
|
|
|
const carryForward: any = onCreate.carryForward;
|
|
|
|
_createdBy = this.generateSlug(_createdBy);
|
|
|
|
let data: TBaseModel = _createdBy.data;
|
|
|
|
// add tenantId if present.
|
|
const tenantColumnName: string | null = data.getTenantColumn();
|
|
|
|
if (tenantColumnName && _createdBy.props.tenantId) {
|
|
data.setColumnValue(tenantColumnName, _createdBy.props.tenantId);
|
|
}
|
|
|
|
data = this.generateDefaultValues(data);
|
|
|
|
data = this.checkRequiredFields(data);
|
|
|
|
await this.checkForUniqueValues(data);
|
|
|
|
if (!this.isValid(data)) {
|
|
throw new BadDataException("Data is not valid");
|
|
}
|
|
|
|
// check total items by.
|
|
|
|
await this.checkTotalItemsBy(_createdBy);
|
|
|
|
// Encrypt data
|
|
data = (await this.encrypt(data)) as TBaseModel;
|
|
|
|
// hash data
|
|
data = await this.hash(data);
|
|
|
|
ModelPermission.checkCreatePermissions(
|
|
this.modelType,
|
|
data,
|
|
_createdBy.props,
|
|
);
|
|
|
|
createBy.data = data;
|
|
|
|
// check uniqueColumns by:
|
|
createBy = await this.checkUniqueColumnBy(createBy);
|
|
|
|
// serialize.
|
|
createBy.data = (await this.sanitizeCreateOrUpdate(
|
|
createBy.data,
|
|
createBy.props,
|
|
)) as TBaseModel;
|
|
|
|
try {
|
|
createBy.data = await this.getRepository().save(createBy.data);
|
|
|
|
if (!createBy.props.ignoreHooks) {
|
|
createBy.data = await this.onCreateSuccess(
|
|
{
|
|
createBy,
|
|
carryForward,
|
|
},
|
|
createBy.data,
|
|
);
|
|
}
|
|
|
|
let tenantId: ObjectID | undefined = createBy.props.tenantId;
|
|
|
|
if (!tenantId && this.getModel().getTenantColumn()) {
|
|
tenantId = createBy.data.getValue<ObjectID>(
|
|
this.getModel().getTenantColumn()!,
|
|
);
|
|
}
|
|
|
|
// hit workflow.;
|
|
if (this.getModel().enableWorkflowOn?.create && tenantId) {
|
|
await this.onTriggerWorkflow(createBy.data.id!, tenantId, "on-create");
|
|
}
|
|
|
|
if (tenantId) {
|
|
await this.onTriggerRealtime(
|
|
createBy.data.id!,
|
|
tenantId,
|
|
ModelEventType.Create,
|
|
);
|
|
}
|
|
|
|
return createBy.data;
|
|
} catch (error) {
|
|
await this.onCreateError(error as Exception);
|
|
throw this.getException(error as Exception);
|
|
}
|
|
}
|
|
|
|
private checkMaxLengthOfFields<TBaseModel extends BaseModel>(
|
|
data: TBaseModel,
|
|
): TBaseModel {
|
|
// Check required fields.
|
|
|
|
const tableColumns: Array<string> = this.model.getTableColumns().columns;
|
|
|
|
for (const column of tableColumns) {
|
|
const metadata: TableColumnMetadata =
|
|
this.model.getTableColumnMetadata(column);
|
|
if (
|
|
(data as any)[column] &&
|
|
metadata.type &&
|
|
getMaxLengthFromTableColumnType(metadata.type)
|
|
) {
|
|
if (
|
|
(data as any)[column].toString().length >
|
|
getMaxLengthFromTableColumnType(metadata.type)!
|
|
) {
|
|
throw new BadDataException(
|
|
`${column} length cannot be more than ${getMaxLengthFromTableColumnType(
|
|
metadata.type,
|
|
)} characters`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
private async checkTotalItemsBy(
|
|
createdBy: CreateBy<TBaseModel>,
|
|
): Promise<void> {
|
|
const totalItemsColumnName: string | null =
|
|
this.model.getTotalItemsByColumnName();
|
|
const totalItemsNumber: number | null = this.model.getTotalItemsNumber();
|
|
const errorMessage: string | null =
|
|
this.model.getTotalItemsByErrorMessage();
|
|
|
|
if (
|
|
totalItemsColumnName &&
|
|
totalItemsNumber &&
|
|
errorMessage &&
|
|
createdBy.data.getColumnValue(totalItemsColumnName)
|
|
) {
|
|
const count: PositiveNumber = await this.countBy({
|
|
query: {
|
|
[totalItemsColumnName]:
|
|
createdBy.data.getColumnValue(totalItemsColumnName),
|
|
} as FindWhere<TBaseModel>,
|
|
skip: 0,
|
|
limit: LIMIT_MAX,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (count.positiveNumber > totalItemsNumber - 1) {
|
|
throw new BadDataException(errorMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async checkUniqueColumnBy(
|
|
createBy: CreateBy<TBaseModel>,
|
|
): Promise<CreateBy<TBaseModel>> {
|
|
let existingItemsWithSameNameCount: number = 0;
|
|
|
|
const uniqueColumnsBy: Dictionary<string | Array<string>> =
|
|
getUniqueColumnsBy(createBy.data);
|
|
|
|
for (const key in uniqueColumnsBy) {
|
|
if (!uniqueColumnsBy[key]) {
|
|
continue;
|
|
}
|
|
|
|
if (typeof uniqueColumnsBy[key] === Typeof.String) {
|
|
uniqueColumnsBy[key] = [uniqueColumnsBy[key] as string];
|
|
}
|
|
|
|
const query: Query<TBaseModel> = {};
|
|
|
|
for (const uniqueByColumnName of uniqueColumnsBy[key] as Array<string>) {
|
|
const columnValue: JSONValue = (createBy.data as any)[
|
|
uniqueByColumnName as string
|
|
];
|
|
if (columnValue === null || columnValue === undefined) {
|
|
(query as any)[uniqueByColumnName] = QueryHelper.isNull();
|
|
} else {
|
|
(query as any)[uniqueByColumnName] = columnValue;
|
|
}
|
|
}
|
|
|
|
existingItemsWithSameNameCount = (
|
|
await this.countBy({
|
|
query: {
|
|
[key]: QueryHelper.findWithSameText(
|
|
(createBy.data as any)[key]
|
|
? ((createBy.data as any)[key]! as string)
|
|
: "",
|
|
),
|
|
...query,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
})
|
|
).toNumber();
|
|
|
|
if (existingItemsWithSameNameCount > 0) {
|
|
throw new BadDataException(
|
|
`${this.model.singularName} with the same ${key} already exists.`,
|
|
);
|
|
}
|
|
|
|
existingItemsWithSameNameCount = 0;
|
|
}
|
|
|
|
return Promise.resolve(createBy);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async countBy({
|
|
query,
|
|
skip,
|
|
limit,
|
|
props,
|
|
groupBy,
|
|
distinctOn,
|
|
}: CountBy<TBaseModel>): Promise<PositiveNumber> {
|
|
try {
|
|
if (groupBy && Object.keys(groupBy).length > 0) {
|
|
throw new BadDataException("Group By is not supported for countBy");
|
|
}
|
|
|
|
if (!skip) {
|
|
skip = new PositiveNumber(0);
|
|
}
|
|
|
|
if (!limit) {
|
|
limit = new PositiveNumber(Infinity);
|
|
}
|
|
|
|
if (!(skip instanceof PositiveNumber)) {
|
|
skip = new PositiveNumber(skip);
|
|
}
|
|
|
|
if (!(limit instanceof PositiveNumber)) {
|
|
limit = new PositiveNumber(limit);
|
|
}
|
|
|
|
const findBy: FindBy<TBaseModel> = {
|
|
query,
|
|
skip,
|
|
limit,
|
|
props,
|
|
};
|
|
|
|
const checkReadPermissionType: CheckReadPermissionType<TBaseModel> =
|
|
await ModelPermission.checkReadQueryPermission(
|
|
this.modelType,
|
|
query,
|
|
null,
|
|
props,
|
|
);
|
|
|
|
findBy.query = checkReadPermissionType.query;
|
|
let count: number = 0;
|
|
|
|
if (distinctOn) {
|
|
const queryBuilder: SelectQueryBuilder<TBaseModel> =
|
|
this.getQueryBuilder(this.modelName)
|
|
.where(findBy.query)
|
|
.skip(skip.toNumber())
|
|
.take(limit.toNumber());
|
|
|
|
if (distinctOn) {
|
|
queryBuilder.groupBy(`${this.modelName}.${distinctOn}`);
|
|
}
|
|
|
|
count = await queryBuilder.getCount();
|
|
} else {
|
|
count = await this.getRepository().count({
|
|
where: findBy.query as any,
|
|
skip: (findBy.skip as PositiveNumber).toNumber(),
|
|
take: (findBy.limit as PositiveNumber).toNumber(),
|
|
});
|
|
}
|
|
|
|
let countPositive: PositiveNumber = new PositiveNumber(count);
|
|
countPositive = await this.onCountSuccess(countPositive);
|
|
return countPositive;
|
|
} catch (error) {
|
|
await this.onCountError(error as Exception);
|
|
throw this.getException(error as Exception);
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async deleteOneById(deleteById: DeleteById): Promise<number> {
|
|
await ModelPermission.checkDeletePermissionByModel({
|
|
modelType: this.modelType,
|
|
fetchModelWithAccessControlIds: async () => {
|
|
const selectModel: Select<TBaseModel> = {};
|
|
const accessControlColumn: string | null =
|
|
this.getModel().getAccessControlColumn();
|
|
|
|
if (accessControlColumn) {
|
|
(selectModel as any)[accessControlColumn] = {
|
|
_id: true,
|
|
name: true,
|
|
};
|
|
}
|
|
|
|
return await this.findOneById({
|
|
id: deleteById.id,
|
|
select: selectModel,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
},
|
|
props: deleteById.props,
|
|
});
|
|
|
|
return await this.deleteOneBy({
|
|
query: {
|
|
_id: deleteById.id.toString(),
|
|
} as any,
|
|
deletedByUser: deleteById.deletedByUser,
|
|
props: deleteById.props,
|
|
});
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async deleteOneBy(
|
|
deleteOneBy: DeleteOneBy<TBaseModel>,
|
|
): Promise<number> {
|
|
return await this._deleteBy({ ...deleteOneBy, limit: 1, skip: 0 });
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async deleteBy(deleteBy: DeleteBy<TBaseModel>): Promise<number> {
|
|
return await this._deleteBy(deleteBy);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async hardDeleteBy(deleteBy: DeleteBy<TBaseModel>): Promise<number> {
|
|
try {
|
|
const onDelete: OnDelete<TBaseModel> = deleteBy.props.ignoreHooks
|
|
? { deleteBy, carryForward: [] }
|
|
: await this.onBeforeDelete(deleteBy);
|
|
const beforeDeleteBy: DeleteBy<TBaseModel> = onDelete.deleteBy;
|
|
|
|
beforeDeleteBy.query = await ModelPermission.checkDeleteQueryPermission(
|
|
this.modelType,
|
|
beforeDeleteBy.query,
|
|
deleteBy.props,
|
|
);
|
|
|
|
if (!(beforeDeleteBy.skip instanceof PositiveNumber)) {
|
|
beforeDeleteBy.skip = new PositiveNumber(beforeDeleteBy.skip);
|
|
}
|
|
|
|
if (!(beforeDeleteBy.limit instanceof PositiveNumber)) {
|
|
beforeDeleteBy.limit = new PositiveNumber(beforeDeleteBy.limit);
|
|
}
|
|
|
|
const items: Array<TBaseModel> = await this._findBy(
|
|
{
|
|
query: beforeDeleteBy.query,
|
|
skip: beforeDeleteBy.skip.toNumber(),
|
|
limit: beforeDeleteBy.limit.toNumber(),
|
|
select: {},
|
|
props: { ...beforeDeleteBy.props, ignoreHooks: true },
|
|
},
|
|
true,
|
|
);
|
|
|
|
let numberOfDocsAffected: number = 0;
|
|
|
|
if (items.length > 0) {
|
|
beforeDeleteBy.query = {
|
|
...beforeDeleteBy.query,
|
|
_id: QueryHelper.any(
|
|
items.map((i: TBaseModel) => {
|
|
return i.id!;
|
|
}),
|
|
),
|
|
};
|
|
|
|
numberOfDocsAffected =
|
|
(await this.getRepository().delete(beforeDeleteBy.query as any))
|
|
.affected || 0;
|
|
}
|
|
|
|
return numberOfDocsAffected;
|
|
} catch (error) {
|
|
await this.onDeleteError(error as Exception);
|
|
throw this.getException(error as Exception);
|
|
}
|
|
}
|
|
|
|
private async _deleteBy(deleteBy: DeleteBy<TBaseModel>): Promise<number> {
|
|
try {
|
|
if (this.doNotAllowDelete && !deleteBy.props.isRoot) {
|
|
throw new BadDataException("Delete not allowed");
|
|
}
|
|
|
|
const onDelete: OnDelete<TBaseModel> = deleteBy.props.ignoreHooks
|
|
? { deleteBy, carryForward: [] }
|
|
: await this.onBeforeDelete(deleteBy);
|
|
|
|
const beforeDeleteBy: DeleteBy<TBaseModel> = onDelete.deleteBy;
|
|
|
|
const carryForward: any = onDelete.carryForward;
|
|
|
|
beforeDeleteBy.query = await ModelPermission.checkDeleteQueryPermission(
|
|
this.modelType,
|
|
beforeDeleteBy.query,
|
|
deleteBy.props,
|
|
);
|
|
|
|
if (!(beforeDeleteBy.skip instanceof PositiveNumber)) {
|
|
beforeDeleteBy.skip = new PositiveNumber(beforeDeleteBy.skip);
|
|
}
|
|
|
|
if (!(beforeDeleteBy.limit instanceof PositiveNumber)) {
|
|
beforeDeleteBy.limit = new PositiveNumber(beforeDeleteBy.limit);
|
|
}
|
|
|
|
const select: Select<TBaseModel> = {};
|
|
|
|
if (this.getModel().getTenantColumn()) {
|
|
(select as any)[this.getModel().getTenantColumn() as string] = true;
|
|
}
|
|
|
|
const items: Array<TBaseModel> = await this._findBy({
|
|
query: beforeDeleteBy.query,
|
|
skip: beforeDeleteBy.skip.toNumber(),
|
|
limit: beforeDeleteBy.limit.toNumber(),
|
|
select: select,
|
|
props: {
|
|
isRoot: true, // isRoot because query has already been checked for permissions.
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
|
|
// We are hard deleting anyway. So, this does not make sense. Please uncomment if
|
|
// we change the code to soft-delete.
|
|
|
|
// await this._updateBy({
|
|
// query: deleteBy.query,
|
|
// data: {
|
|
// deletedByUserId: deleteBy.props.userId,
|
|
// } as any,
|
|
// limit: deleteBy.limit,
|
|
// skip: deleteBy.skip,
|
|
// props: {
|
|
// isRoot: true,
|
|
// ignoreHooks: true,
|
|
// },
|
|
// });
|
|
|
|
let numberOfDocsAffected: number = 0;
|
|
|
|
if (items.length > 0) {
|
|
const query: Query<TBaseModel> = {
|
|
_id: QueryHelper.any(
|
|
items.map((i: TBaseModel) => {
|
|
return i.id!;
|
|
}),
|
|
),
|
|
};
|
|
|
|
numberOfDocsAffected =
|
|
(await this.getRepository().delete(query as any)).affected || 0;
|
|
}
|
|
|
|
// hit workflow.
|
|
if (
|
|
this.getModel().enableWorkflowOn?.delete &&
|
|
(deleteBy.props.tenantId || this.getModel().getTenantColumn())
|
|
) {
|
|
for (const item of items) {
|
|
if (this.getModel().enableWorkflowOn?.create) {
|
|
let tenantId: ObjectID | undefined = deleteBy.props.tenantId;
|
|
|
|
if (!tenantId && this.getModel().getTenantColumn()) {
|
|
tenantId = item.getValue<ObjectID>(
|
|
this.getModel().getTenantColumn()!,
|
|
);
|
|
}
|
|
|
|
if (tenantId) {
|
|
await this.onTriggerWorkflow(item.id!, tenantId, "on-delete");
|
|
await this.onTriggerRealtime(
|
|
item.id!,
|
|
tenantId,
|
|
ModelEventType.Delete,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!deleteBy.props.ignoreHooks) {
|
|
await this.onDeleteSuccess(
|
|
{ deleteBy, carryForward },
|
|
items.map((i: TBaseModel) => {
|
|
return new ObjectID(i._id!);
|
|
}),
|
|
);
|
|
}
|
|
|
|
return numberOfDocsAffected;
|
|
} catch (error) {
|
|
await this.onDeleteError(error as Exception);
|
|
throw this.getException(error as Exception);
|
|
}
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async findBy(findBy: FindBy<TBaseModel>): Promise<Array<TBaseModel>> {
|
|
return await this._findBy(findBy);
|
|
}
|
|
|
|
private async _findBy(
|
|
findBy: FindBy<TBaseModel>,
|
|
withDeleted?: boolean | undefined,
|
|
): Promise<Array<TBaseModel>> {
|
|
try {
|
|
let automaticallyAddedCreatedAtInSelect: boolean = false;
|
|
|
|
if (!findBy.sort || Object.keys(findBy.sort).length === 0) {
|
|
findBy.sort = {
|
|
createdAt: SortOrder.Descending,
|
|
};
|
|
|
|
if (!findBy.select) {
|
|
findBy.select = {} as any;
|
|
}
|
|
|
|
if (!(findBy.select as any)["createdAt"]) {
|
|
(findBy.select as any)["createdAt"] = true;
|
|
automaticallyAddedCreatedAtInSelect = true;
|
|
}
|
|
}
|
|
|
|
const onFind: OnFind<TBaseModel> = findBy.props.ignoreHooks
|
|
? { findBy, carryForward: [] }
|
|
: await this.onBeforeFind(findBy);
|
|
const onBeforeFind: FindBy<TBaseModel> = { ...onFind.findBy };
|
|
const carryForward: any = onFind.carryForward;
|
|
|
|
if (
|
|
!onBeforeFind.select ||
|
|
Object.keys(onBeforeFind.select).length === 0
|
|
) {
|
|
onBeforeFind.select = {} as any;
|
|
}
|
|
|
|
if (!(onBeforeFind.select as any)["_id"]) {
|
|
(onBeforeFind.select as any)["_id"] = true;
|
|
}
|
|
|
|
const result: {
|
|
query: Query<TBaseModel>;
|
|
select: Select<TBaseModel> | null;
|
|
relationSelect: RelationSelect<TBaseModel> | null;
|
|
} = await ModelPermission.checkReadQueryPermission(
|
|
this.modelType,
|
|
onBeforeFind.query,
|
|
onBeforeFind.select || null,
|
|
onBeforeFind.props,
|
|
);
|
|
|
|
onBeforeFind.query = result.query;
|
|
onBeforeFind.select = result.select || undefined;
|
|
|
|
if (!(onBeforeFind.skip instanceof PositiveNumber)) {
|
|
onBeforeFind.skip = new PositiveNumber(onBeforeFind.skip);
|
|
}
|
|
|
|
if (!(onBeforeFind.limit instanceof PositiveNumber)) {
|
|
onBeforeFind.limit = new PositiveNumber(onBeforeFind.limit);
|
|
}
|
|
|
|
if (
|
|
onBeforeFind.groupBy &&
|
|
Object.keys(onBeforeFind.groupBy).length > 0
|
|
) {
|
|
throw new BadDataException("GroupBy is currently not supported");
|
|
}
|
|
|
|
const items: Array<TBaseModel> = await this.getRepository().find({
|
|
skip: onBeforeFind.skip.toNumber(),
|
|
take: onBeforeFind.limit.toNumber(),
|
|
where: onBeforeFind.query as any,
|
|
order: onBeforeFind.sort as any,
|
|
relations: result.relationSelect as any,
|
|
select: onBeforeFind.select as any,
|
|
withDeleted: withDeleted || false,
|
|
});
|
|
|
|
let decryptedItems: Array<TBaseModel> = [];
|
|
|
|
for (const item of items) {
|
|
decryptedItems.push(await this.decrypt(item));
|
|
}
|
|
|
|
decryptedItems = this.sanitizeFindByItems(decryptedItems, onBeforeFind);
|
|
|
|
for (const item of decryptedItems) {
|
|
if (automaticallyAddedCreatedAtInSelect) {
|
|
delete (item as any).createdAt;
|
|
}
|
|
}
|
|
|
|
if (!findBy.props.ignoreHooks) {
|
|
decryptedItems = await (
|
|
await this.onFindSuccess({ findBy, carryForward }, decryptedItems)
|
|
).carryForward;
|
|
}
|
|
|
|
return decryptedItems;
|
|
} catch (error) {
|
|
await this.onFindError(error as Exception);
|
|
throw this.getException(error as Exception);
|
|
}
|
|
}
|
|
|
|
private sanitizeFindByItems(
|
|
items: Array<TBaseModel>,
|
|
findBy: FindBy<TBaseModel>,
|
|
): Array<TBaseModel> {
|
|
// if there's no select then there's nothing to do.
|
|
if (!findBy.select) {
|
|
return items;
|
|
}
|
|
|
|
for (const key in findBy.select) {
|
|
// for each key in select check if there's nested properties, this indicates there's a relation.
|
|
if (typeof findBy.select[key] === Typeof.Object) {
|
|
// get meta data to check if this column is an entity array.
|
|
const tableColumnMetadata: TableColumnMetadata =
|
|
this.model.getTableColumnMetadata(key);
|
|
|
|
if (!tableColumnMetadata.modelType) {
|
|
throw new BadDataException(
|
|
"Select not supported on " +
|
|
key +
|
|
" of " +
|
|
this.model.singularName +
|
|
" because this column modelType is not found.",
|
|
);
|
|
}
|
|
|
|
const relatedModel: BaseModel = new tableColumnMetadata.modelType();
|
|
if (tableColumnMetadata.type === TableColumnType.EntityArray) {
|
|
const tableColumns: Array<string> =
|
|
relatedModel.getTableColumns().columns;
|
|
const columnsToKeep: Array<string> = Object.keys(
|
|
(findBy.select as any)[key],
|
|
);
|
|
|
|
for (const item of items) {
|
|
if (item[key] && Array.isArray(item[key])) {
|
|
const relatedArray: Array<BaseModel> = item[key] as any;
|
|
const newArray: Array<BaseModel> = [];
|
|
// now we need to sanitize data.
|
|
|
|
for (const relatedArrayItem of relatedArray) {
|
|
for (const column of tableColumns) {
|
|
if (!columnsToKeep.includes(column)) {
|
|
(relatedArrayItem as any)[column] = undefined;
|
|
}
|
|
}
|
|
newArray.push(relatedArrayItem);
|
|
}
|
|
|
|
(item[key] as any) = newArray;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async findOneBy(
|
|
findOneBy: FindOneBy<TBaseModel>,
|
|
): Promise<TBaseModel | null> {
|
|
const findBy: FindBy<TBaseModel> = findOneBy as FindBy<TBaseModel>;
|
|
findBy.limit = new PositiveNumber(1);
|
|
findBy.skip = new PositiveNumber(0);
|
|
|
|
const documents: Array<TBaseModel> = await this._findBy(findBy);
|
|
|
|
if (documents && documents[0]) {
|
|
return documents[0];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async findOneById(
|
|
findOneById: FindOneByID<TBaseModel>,
|
|
): Promise<TBaseModel | null> {
|
|
if (!findOneById.id) {
|
|
throw new BadDataException("findOneById.id is required");
|
|
}
|
|
|
|
return await this.findOneBy({
|
|
query: {
|
|
_id: findOneById.id.toString() as any,
|
|
},
|
|
select: findOneById.select || {},
|
|
props: findOneById.props,
|
|
});
|
|
}
|
|
|
|
private async _updateBy(updateBy: UpdateBy<TBaseModel>): Promise<number> {
|
|
try {
|
|
const onUpdate: OnUpdate<TBaseModel> = updateBy.props.ignoreHooks
|
|
? { updateBy, carryForward: [] }
|
|
: await this.onBeforeUpdate(updateBy);
|
|
|
|
// Encrypt data
|
|
updateBy.data = (await this.encrypt(
|
|
updateBy.data,
|
|
)) as PartialEntity<TBaseModel>;
|
|
|
|
const beforeUpdateBy: UpdateBy<TBaseModel> = onUpdate.updateBy;
|
|
const carryForward: any = onUpdate.carryForward;
|
|
|
|
beforeUpdateBy.query = await ModelPermission.checkUpdateQueryPermissions(
|
|
this.modelType,
|
|
beforeUpdateBy.query,
|
|
beforeUpdateBy.data,
|
|
beforeUpdateBy.props,
|
|
);
|
|
|
|
const data: PartialEntity<TBaseModel> =
|
|
(await this.sanitizeCreateOrUpdate(
|
|
beforeUpdateBy.data,
|
|
updateBy.props,
|
|
true,
|
|
)) as PartialEntity<TBaseModel>;
|
|
|
|
if (!(updateBy.skip instanceof PositiveNumber)) {
|
|
updateBy.skip = new PositiveNumber(updateBy.skip);
|
|
}
|
|
|
|
if (!(updateBy.limit instanceof PositiveNumber)) {
|
|
updateBy.limit = new PositiveNumber(updateBy.limit);
|
|
}
|
|
|
|
const dataColumns: [string, boolean][] = [];
|
|
const dataKeys: string[] = Object.keys(data);
|
|
for (const key of dataKeys) {
|
|
dataColumns.push([key, true]);
|
|
}
|
|
// Select the `_id` column and the columns in `data`.
|
|
// `_id` is used for locating database records for updates, and `data`
|
|
// columns are used for checking if the update causes a change in values.
|
|
const selectColumns: Select<TBaseModel> = {
|
|
_id: true,
|
|
...Object.fromEntries(dataColumns),
|
|
};
|
|
|
|
if (this.getModel().getTenantColumn()) {
|
|
(selectColumns as any)[this.getModel().getTenantColumn()!.toString()] =
|
|
true;
|
|
}
|
|
|
|
const items: Array<TBaseModel> = await this._findBy({
|
|
query: beforeUpdateBy.query,
|
|
skip: updateBy.skip.toNumber(),
|
|
limit: updateBy.limit.toNumber(),
|
|
select: selectColumns,
|
|
props: { isRoot: true, ignoreHooks: true },
|
|
});
|
|
|
|
for (const item of items) {
|
|
const updatedItem: any = {
|
|
_id: item._id!,
|
|
...data,
|
|
} as any;
|
|
|
|
logger.debug("Updated Item");
|
|
logger.debug(JSON.stringify(updatedItem, null, 2));
|
|
|
|
await this.getRepository().save(updatedItem);
|
|
|
|
// hit workflow.
|
|
if (
|
|
this.getModel().enableWorkflowOn?.update &&
|
|
// Only trigger workflow if there's a change in values
|
|
!this.hasSameValues({ item, updatedItem })
|
|
) {
|
|
let tenantId: ObjectID | undefined = updateBy.props.tenantId;
|
|
|
|
if (!tenantId && this.getModel().getTenantColumn()) {
|
|
tenantId = item.getValue<ObjectID>(
|
|
this.getModel().getTenantColumn()!,
|
|
);
|
|
}
|
|
|
|
if (tenantId) {
|
|
await this.onTriggerWorkflow(item.id!, tenantId, "on-update", {
|
|
updatedFields: JSONFunctions.serialize(data as JSONObject),
|
|
});
|
|
|
|
await this.onTriggerRealtime(
|
|
item.id!,
|
|
tenantId,
|
|
ModelEventType.Update,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cant Update relations.
|
|
// https://github.com/typeorm/typeorm/issues/2821
|
|
|
|
// const numberOfDocsAffected: number =
|
|
// (
|
|
// await this.getRepository().update(
|
|
// query as any,
|
|
// data
|
|
// )
|
|
// ).affected || 0;
|
|
|
|
if (!updateBy.props.ignoreHooks) {
|
|
await this.onUpdateSuccess(
|
|
{ updateBy, carryForward },
|
|
items.map((i: TBaseModel) => {
|
|
return new ObjectID(i._id!);
|
|
}),
|
|
);
|
|
}
|
|
|
|
return items.length;
|
|
} catch (error) {
|
|
await this.onUpdateError(error as Exception);
|
|
throw this.getException(error as Exception);
|
|
}
|
|
}
|
|
|
|
private hasSameValues(data: { item: TBaseModel; updatedItem: any }): boolean {
|
|
const { item, updatedItem } = data;
|
|
const columns: string[] = Object.keys(updatedItem);
|
|
for (const column of columns) {
|
|
if (
|
|
// `toString()` is necessary so we can compare wrapped values
|
|
// (e.g. `ObjectID`) with raw values (e.g. `string`)
|
|
item.getColumnValue(column)?.toString() !==
|
|
updatedItem[column]?.toString()
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async updateOneBy(
|
|
updateOneBy: UpdateOneBy<TBaseModel>,
|
|
): Promise<number> {
|
|
return await this._updateBy({ ...updateOneBy, limit: 1, skip: 0 });
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async updateBy(updateBy: UpdateBy<TBaseModel>): Promise<number> {
|
|
return await this._updateBy(updateBy);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async updateOneById(
|
|
updateById: UpdateByID<TBaseModel>,
|
|
): Promise<void> {
|
|
if (!updateById.id) {
|
|
throw new BadDataException("updateById.id is required");
|
|
}
|
|
|
|
await ModelPermission.checkUpdatePermissionByModel({
|
|
modelType: this.modelType,
|
|
fetchModelWithAccessControlIds: async () => {
|
|
const selectModel: Select<TBaseModel> = {};
|
|
const accessControlColumn: string | null =
|
|
this.getModel().getAccessControlColumn();
|
|
|
|
if (accessControlColumn) {
|
|
(selectModel as any)[accessControlColumn] = {
|
|
_id: true,
|
|
name: true,
|
|
};
|
|
}
|
|
|
|
return await this.findOneById({
|
|
id: updateById.id,
|
|
select: selectModel,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
},
|
|
props: updateById.props,
|
|
});
|
|
|
|
await this.updateOneBy({
|
|
query: {
|
|
_id: updateById.id.toString() as any,
|
|
},
|
|
data: updateById.data as any,
|
|
props: updateById.props,
|
|
});
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async updateOneByIdAndFetch(
|
|
updateById: UpdateByIDAndFetch<TBaseModel>,
|
|
): Promise<TBaseModel | null> {
|
|
await this.updateOneById(updateById);
|
|
return this.findOneById({
|
|
id: updateById.id,
|
|
select: updateById.select,
|
|
props: updateById.props,
|
|
});
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async searchBy({
|
|
skip,
|
|
limit,
|
|
select,
|
|
props,
|
|
}: SearchBy<TBaseModel>): Promise<SearchResult<TBaseModel>> {
|
|
const query: Query<TBaseModel> = {};
|
|
|
|
// query[column] = RegExp(`^${text}`, 'i');
|
|
|
|
const [items, count]: [Array<TBaseModel>, PositiveNumber] =
|
|
await Promise.all([
|
|
this.findBy({
|
|
query,
|
|
skip,
|
|
limit,
|
|
select,
|
|
props: props,
|
|
}),
|
|
this.countBy({
|
|
query,
|
|
skip: new PositiveNumber(0),
|
|
limit: new PositiveNumber(Infinity),
|
|
props: props,
|
|
}),
|
|
]);
|
|
|
|
return { items, count };
|
|
}
|
|
}
|
|
|
|
export default DatabaseService;
|