import isEmpty from 'lodash.isempty'; import { ObjectRecordsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { DeleteResult, EntityManager, EntityTarget, FindManyOptions, FindOneOptions, FindOptionsWhere, In, InsertResult, ObjectId, ObjectLiteral, QueryRunner, RemoveOptions, Repository, SaveOptions, SelectQueryBuilder, TypeORMError, UpdateResult, } from 'typeorm'; import { DeepPartial } from 'typeorm/common/DeepPartial'; import { PickKeysByType } from 'typeorm/common/PickKeysByType'; import { EntityNotFoundError } from 'typeorm/error/EntityNotFoundError'; import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils'; import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer'; import { UpsertOptions } from 'typeorm/repository/UpsertOptions'; import { InstanceChecker } from 'typeorm/util/InstanceChecker'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { DeepPartialWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type'; import { QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception'; import { RelationNestedQueries } from 'src/engine/twenty-orm/relation-nested-queries/relation-nested-queries'; import { OperationType, validateOperationIsPermittedOrThrow, } from 'src/engine/twenty-orm/repository/permissions.utils'; import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; type PermissionOptions = { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; }; export class WorkspaceEntityManager extends EntityManager { private readonly internalContext: WorkspaceInternalContext; // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly repositories: Map>; declare connection: WorkspaceDataSource; constructor( internalContext: WorkspaceInternalContext, connection: WorkspaceDataSource, queryRunner?: QueryRunner, ) { super(connection, queryRunner); this.internalContext = internalContext; this.repositories = new Map(); } getFeatureFlagMap(): FeatureFlagMap { return this.connection.featureFlagMap; } override getRepository( target: EntityTarget, permissionOptions?: { shouldBypassPermissionChecks?: boolean; roleId?: string; }, authContext?: AuthContext, ): WorkspaceRepository { const dataSource = this.connection; let objectPermissions = {}; if (permissionOptions?.roleId) { const objectPermissionsByRoleId = dataSource.permissionsPerRoleId; if (!isDefined(objectPermissionsByRoleId?.[permissionOptions.roleId])) { throw new PermissionsException( `No permissions found for role in datasource (missing ${ !isDefined(objectPermissionsByRoleId) ? 'objectPermissionsByRoleId object' : `roleId in objectPermissionsByRoleId object (${permissionOptions.roleId})` })`, PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE, ); } else { objectPermissions = objectPermissionsByRoleId[permissionOptions.roleId]; } } const newRepository = new WorkspaceRepository( this.internalContext, target, this, dataSource.featureFlagMap, this.queryRunner, objectPermissions, permissionOptions?.shouldBypassPermissionChecks, authContext, ); return newRepository; } override createQueryBuilder( entityClassOrQueryRunner?: EntityTarget | QueryRunner, alias?: string, queryRunner?: QueryRunner, options: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; } = { shouldBypassPermissionChecks: false, objectRecordsPermissions: {}, }, ): WorkspaceSelectQueryBuilder { let queryBuilder: SelectQueryBuilder; if (alias) { queryBuilder = this.connection.createQueryBuilder( entityClassOrQueryRunner as EntityTarget, alias as string, queryRunner as QueryRunner | undefined, { calledByWorkspaceEntityManager: true, }, ); } else { queryBuilder = this.connection.createQueryBuilder( entityClassOrQueryRunner as QueryRunner, { calledByWorkspaceEntityManager: true, }, ); } return new WorkspaceSelectQueryBuilder( queryBuilder, options?.objectRecordsPermissions ?? {}, this.internalContext, options?.shouldBypassPermissionChecks ?? false, undefined, this.getFeatureFlagMap(), ); } override async insert( target: EntityTarget, entity: | QueryDeepPartialEntityWithNestedRelationFields | QueryDeepPartialEntityWithNestedRelationFields[], selectedColumns: string[] | '*' = '*', permissionOptions?: PermissionOptions, authContext?: AuthContext, ): Promise { const metadata = this.connection.getMetadata(target); return this.createQueryBuilder( target, metadata.name, undefined, permissionOptions, ) .insert() .setAuthContext(authContext ?? {}) .values(entity) .returning(selectedColumns) .execute(); } override upsert( target: EntityTarget, entityOrEntities: | QueryDeepPartialEntityWithNestedRelationFields | QueryDeepPartialEntityWithNestedRelationFields[], conflictPathsOrOptions: string[] | UpsertOptions, permissionOptions?: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; }, selectedColumns: string[] | '*' = '*', ): Promise { const metadata = this.connection.getMetadata(target); let options; if (Array.isArray(conflictPathsOrOptions)) { options = { conflictPaths: conflictPathsOrOptions, }; } else { options = conflictPathsOrOptions; } let entities: QueryDeepPartialEntityWithNestedRelationFields[]; if (!Array.isArray(entityOrEntities)) { entities = [entityOrEntities]; } else { entities = entityOrEntities; } const conflictColumns = metadata.mapPropertyPathsToColumns( Array.isArray(options.conflictPaths) ? options.conflictPaths : Object.keys(options.conflictPaths), ); const overwriteColumns = metadata.columns.filter( (col) => !conflictColumns.includes(col) && entities.some( (entity) => typeof col.getEntityValue(entity) !== 'undefined', ), ); return this.createQueryBuilder( undefined, undefined, undefined, permissionOptions, ) .insert() .into(target) .values(entities) .orUpdate( [...conflictColumns, ...overwriteColumns].map( (col) => col.databaseName, ), conflictColumns.map((col) => col.databaseName), { skipUpdateIfNoValuesChanged: options.skipUpdateIfNoValuesChanged, indexPredicate: options.indexPredicate, upsertType: options.upsertType || this.connection.driver.supportedUpsertTypes[0], }, ) .returning(selectedColumns) .execute(); } override update( target: EntityTarget, criteria: | string | string[] | number | number[] | Date | Date[] | ObjectId | ObjectId[] | unknown, partialEntity: QueryDeepPartialEntity, permissionOptions?: PermissionOptions, selectedColumns: string[] | '*' = '*', ): Promise { const metadata = this.connection.getMetadata(target); if ( criteria === undefined || criteria === null || criteria === '' || (Array.isArray(criteria) && criteria.length === 0) ) { return Promise.reject( new TypeORMError( `Empty criteria(s) are not allowed for the update method.`, ), ); } if ( typeof criteria === 'string' || typeof criteria === 'number' || criteria instanceof Date || Array.isArray(criteria) ) { return this.createQueryBuilder( target, metadata.name, undefined, permissionOptions, ) .update() .set(partialEntity) .whereInIds(criteria) .returning(selectedColumns) .execute(); } else { return this.createQueryBuilder( target, metadata.name, undefined, permissionOptions, ) .update() .set(partialEntity) .where(criteria) .returning(selectedColumns) .execute(); } } public updateMany( target: EntityTarget, inputs: { criteria: string; partialEntity: QueryDeepPartialEntity; }[], permissionOptions?: PermissionOptions, selectedColumns: string[] | '*' = '*', ): Promise { const metadata = this.connection.getMetadata(target); return this.createQueryBuilder( target, metadata.name, undefined, permissionOptions, ) .update() .setManyInputs(inputs) .returning(selectedColumns ?? []) .execute(); } override increment( target: EntityTarget, criteria: object, propertyPath: string, value: number | string, permissionOptions?: PermissionOptions, selectedColumns: string[] | '*' = '*', ): Promise { const metadata = this.connection.getMetadata(target); const column = metadata.findColumnWithPropertyPath(propertyPath); if (!column) throw new TypeORMError( `Column ${propertyPath} was not found in ${metadata.targetName} entity.`, ); if (isNaN(Number(value))) throw new TypeORMError(`Value "${value}" is not a number.`); // convert possible embeded path "social.likes" into object { social: { like: () => value } } // eslint-disable-next-line @typescript-eslint/no-explicit-any const values = propertyPath.split('.').reduceRight( (value, key) => ({ [key]: value }), () => this.connection.driver.escape(column.databaseName) + ' + ' + value, ); return this.update( target, criteria, values, permissionOptions, selectedColumns, ); } validatePermissions({ target, operationType, permissionOptions, selectedColumns, updatedColumns = [], }: { target: EntityTarget | Entity; operationType: OperationType; permissionOptions?: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; }; selectedColumns: string[]; updatedColumns?: string[]; }): void { if (permissionOptions?.shouldBypassPermissionChecks === true) { return; } const entityName = typeof target === 'function' || typeof target === 'string' ? this.extractTargetNameSingularFromEntityTarget(target) : this.extractTargetNameSingularFromEntity(target); validateOperationIsPermittedOrThrow({ entityName, operationType, objectRecordsPermissions: permissionOptions?.objectRecordsPermissions ?? {}, objectMetadataMaps: this.internalContext.objectMetadataMaps, selectedColumns, allFieldsSelected: false, updatedColumns, isFieldPermissionsEnabled: this.getFeatureFlagMap().IS_FIELDS_PERMISSIONS_ENABLED, }); } private extractTargetNameSingularFromEntityTarget( target: EntityTarget, ): string { return this.connection.getMetadata(target).name; } // eslint-disable-next-line @typescript-eslint/no-explicit-any private extractTargetNameSingularFromEntity(entity: any): string { return this.connection.getMetadata(entity.constructor).name; } override find( entityClass: EntityTarget, options?: FindManyOptions, permissionOptions?: PermissionOptions, ): Promise { const metadata = this.connection.getMetadata(entityClass); return this.createQueryBuilder( entityClass, FindOptionsUtils.extractFindManyOptionsAlias(options) || metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions(options || {}) .getMany(); } override findBy( entityClass: EntityTarget, where: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise { const metadata = this.connection.getMetadata(entityClass); return this.createQueryBuilder( entityClass, metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions({ where: where }) .getMany(); } override findOne( entityClass: EntityTarget, options: FindOneOptions, permissionOptions?: PermissionOptions, ): Promise { const metadata = this.connection.getMetadata(entityClass); // prepare alias for built query let alias = metadata.name; if (options && options.join) { alias = options.join.alias; } if (!options.where) { throw new Error( `You must provide selection conditions in order to find a single row.`, ); } // create query builder and apply find options return this.createQueryBuilder( entityClass, alias, this.queryRunner, permissionOptions, ) .setFindOptions({ ...options, take: 1, }) .getOne(); } override findOneBy( entityClass: EntityTarget, where: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise { const metadata = this.connection.getMetadata(entityClass); // create query builder and apply find options return this.createQueryBuilder( entityClass, metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions({ where, take: 1, }) .getOne(); } override findAndCount( entityClass: EntityTarget, options?: FindManyOptions, permissionOptions?: PermissionOptions, ): Promise<[Entity[], number]> { const metadata = this.connection.getMetadata(entityClass); return this.createQueryBuilder( entityClass, FindOptionsUtils.extractFindManyOptionsAlias(options) || metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions(options || {}) .getManyAndCount(); } override findAndCountBy( entityClass: EntityTarget, where: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise<[Entity[], number]> { const metadata = this.connection.getMetadata(entityClass); return this.createQueryBuilder( entityClass, metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions({ where }) .getManyAndCount(); } override findOneOrFail( entityClass: EntityTarget, options: FindOneOptions, permissionOptions?: PermissionOptions, ): Promise { return this.findOne(entityClass, options, permissionOptions).then( (value) => { if (value === null) { return Promise.reject(new EntityNotFoundError(entityClass, options)); } return Promise.resolve(value); }, ); } override findOneByOrFail( entityClass: EntityTarget, where: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise { return this.findOneBy(entityClass, where, permissionOptions).then( (value) => { if (value === null) { return Promise.reject(new EntityNotFoundError(entityClass, where)); } return Promise.resolve(value); }, ); } override delete( targetOrEntity: EntityTarget, criteria: unknown, permissionOptions?: PermissionOptions, selectedColumns: string[] | '*' = '*', ): Promise { if ( criteria === undefined || criteria === null || criteria === '' || (Array.isArray(criteria) && criteria.length === 0) ) { return Promise.reject( new TypeORMError( `Empty criteria(s) are not allowed for the delete method.`, ), ); } if ( typeof criteria === 'string' || typeof criteria === 'number' || criteria instanceof Date || Array.isArray(criteria) ) { return this.createQueryBuilder( undefined, undefined, undefined, permissionOptions, ) .delete() .from(targetOrEntity) .whereInIds(criteria) .returning(selectedColumns) .execute(); } else { return this.createQueryBuilder( undefined, undefined, undefined, permissionOptions, ) .delete() .from(targetOrEntity) .where(criteria) .returning(selectedColumns) .execute(); } } override softDelete( targetOrEntity: EntityTarget, criteria: unknown, permissionOptions?: PermissionOptions, selectedColumns: string[] | '*' = '*', ): Promise { // if user passed empty criteria or empty list of criterias, then throw an error if ( criteria === undefined || criteria === null || criteria === '' || (Array.isArray(criteria) && criteria.length === 0) ) { return Promise.reject( new TypeORMError( `Empty criteria(s) are not allowed for the softDelete method.`, ), ); } if ( typeof criteria === 'string' || typeof criteria === 'number' || criteria instanceof Date || Array.isArray(criteria) ) { return this.createQueryBuilder( undefined, undefined, this.queryRunner, permissionOptions, ) .softDelete() .from(targetOrEntity) .whereInIds(criteria) .returning(selectedColumns) .execute(); } else { return this.createQueryBuilder( undefined, undefined, this.queryRunner, permissionOptions, ) .softDelete() .from(targetOrEntity) .where(criteria) .returning(selectedColumns) .execute(); } } override restore( targetOrEntity: EntityTarget, criteria: unknown, permissionOptions?: PermissionOptions, selectedColumns: string[] | '*' = '*', ): Promise { // if user passed empty criteria or empty list of criterias, then throw an error if ( criteria === undefined || criteria === null || criteria === '' || (Array.isArray(criteria) && criteria.length === 0) ) { return Promise.reject( new TypeORMError( `Empty criteria(s) are not allowed for the restore method.`, ), ); } if ( typeof criteria === 'string' || typeof criteria === 'number' || criteria instanceof Date || Array.isArray(criteria) ) { return this.createQueryBuilder( undefined, undefined, this.queryRunner, permissionOptions, ) .restore() .from(targetOrEntity) .whereInIds(criteria) .returning(selectedColumns) .execute(); } else { return this.createQueryBuilder( undefined, undefined, this.queryRunner, permissionOptions, ) .restore() .from(targetOrEntity) .where(criteria) .returning(selectedColumns) .execute(); } } override exists( entityClass: EntityTarget, options?: FindManyOptions, permissionOptions?: PermissionOptions, ): Promise { const metadata = this.connection.getMetadata(entityClass); return this.createQueryBuilder( entityClass, FindOptionsUtils.extractFindManyOptionsAlias(options) || metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions(options || {}) .select('id') .limit(1) .getRawOne() .then((result) => isDefined(result)); } override existsBy( entityClass: EntityTarget, where: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise { const metadata = this.connection.getMetadata(entityClass); return this.createQueryBuilder( entityClass, metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions({ where }) .select('id') .limit(1) .getRawOne() .then((result) => isDefined(result)); } override count( entityClass: EntityTarget, options?: FindManyOptions, permissionOptions?: PermissionOptions, ): Promise { const metadata = this.connection.getMetadata(entityClass); return this.createQueryBuilder( entityClass, FindOptionsUtils.extractFindManyOptionsAlias(options) || metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions(options || {}) .getCount(); } override countBy( entityClass: EntityTarget, where: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise { const metadata = this.connection.getMetadata(entityClass); return this.createQueryBuilder( entityClass, metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions({ where }) .getCount(); } async callAggregateFunCustom( entityClass: EntityTarget, fnName: string, columnName: string, where = {}, permissionOptions?: PermissionOptions, ) { const metadata = this.connection.getMetadata(entityClass); const column = metadata.columns.find( (item) => item.propertyPath === columnName, ); if (!column) { throw new TypeORMError( `Column "${columnName}" was not found in table "${metadata.name}"`, ); } const result = await this.createQueryBuilder( entityClass, metadata.name, this.queryRunner, permissionOptions, ) .setFindOptions({ where }) .select( `${fnName}(${this.connection.driver.escape(column.databaseName)})`, fnName, ) .getRawOne(); return result[fnName] === null ? null : parseFloat(result[fnName]); } override sum( entityClass: EntityTarget, columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise { return this.callAggregateFunCustom( entityClass, 'SUM', columnName, where, permissionOptions, ); } override average( entityClass: EntityTarget, columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise { return this.callAggregateFunCustom( entityClass, 'AVG', columnName, where, permissionOptions, ); } override minimum( entityClass: EntityTarget, columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise { return this.callAggregateFunCustom( entityClass, 'MIN', columnName, where, permissionOptions, ); } override maximum( entityClass: EntityTarget, columnName: PickKeysByType, where?: FindOptionsWhere | FindOptionsWhere[], permissionOptions?: PermissionOptions, ): Promise { return this.callAggregateFunCustom( entityClass, 'MAX', columnName, where, permissionOptions, ); } override clear( entityClass: EntityTarget, permissionOptions?: PermissionOptions, ): Promise { this.validatePermissions({ target: entityClass, operationType: 'delete', permissionOptions, selectedColumns: [], // TODO }); return super.clear(entityClass); } override async preload( entityClass: EntityTarget, entityLike: DeepPartial, permissionOptions?: PermissionOptions, ): Promise { const objectMetadataItem = getObjectMetadataFromEntityTarget( entityClass, this.internalContext, ); const formattedEntityLike = formatData(entityLike, objectMetadataItem); const managerWithPermissionOptions = Object.assign( Object.create(Object.getPrototypeOf(this)), this, { findByIds: (entityClass: EntityTarget, ids: string[]) => { return this.findByIds(entityClass, ids, permissionOptions); }, }, ); const metadata = this.connection.getMetadata(entityClass); const plainObjectToDatabaseEntityTransformer = new PlainObjectToDatabaseEntityTransformer(managerWithPermissionOptions); const transformedEntity = await plainObjectToDatabaseEntityTransformer.transform( formattedEntityLike, metadata, ); if (transformedEntity) return this.merge( entityClass, transformedEntity, formattedEntityLike, ) as Entity; return undefined; } override decrement( target: EntityTarget, criteria: object, propertyPath: string, value: number | string, permissionOptions?: PermissionOptions, selectedColumns: string[] | '*' = '*', ): Promise { const metadata = this.connection.getMetadata(target); const column = metadata.findColumnWithPropertyPath(propertyPath); if (!column) throw new TypeORMError( `Column ${propertyPath} was not found in ${metadata.targetName} entity.`, ); if (isNaN(Number(value))) throw new TypeORMError(`Value "${value}" is not a number.`); // eslint-disable-next-line @typescript-eslint/no-explicit-any const values = propertyPath.split('.').reduceRight( (value, key) => ({ [key]: value }), () => this.connection.driver.escape(column.databaseName) + ' - ' + value, ); return this.update( target, criteria, values, permissionOptions, selectedColumns, ); } override async findByIds( entityClass: EntityTarget, ids: string[], permissionOptions?: PermissionOptions, ): Promise { if (!ids.length) return Promise.resolve([]); const metadata = this.connection.getMetadata(entityClass); return this.createQueryBuilder( entityClass, metadata.name, undefined, permissionOptions, ) .andWhereInIds(ids) .getMany(); } /** * Functions duplicated from EntityManager but with a queryRunner that will bypass permissions * because permissions cannot be passed on to the call to createQueryBuilder() done in SubjectExecutor called by EntityPersistExecutor * queryBuilder checks are replaced by validatePermissions() */ override save( entities: Entity[], options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override save( entity: Entity, options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override save>( targetOrEntity: EntityTarget, entities: T[], options: SaveOptions & { reload: false; }, permissionOptions?: PermissionOptions, ): Promise; override save>( targetOrEntity: EntityTarget, entities: T[], options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise<(T & Entity)[]>; override save>( targetOrEntity: EntityTarget, entity: T, options: SaveOptions & { reload: false; }, permissionOptions?: PermissionOptions, ): Promise; override save>( targetOrEntity: EntityTarget, entity: T, options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override async save< Entity extends ObjectLiteral, T extends DeepPartialWithNestedRelationFields, >( targetOrEntity: EntityTarget | Entity | Entity[], entityOrMaybeOptions: | T | T[] | SaveOptions | (SaveOptions & { reload: false }), maybeOptionsOrMaybePermissionOptions?: | PermissionOptions | SaveOptions | (SaveOptions & { reload: false }), permissionOptions?: PermissionOptions, ): Promise<(T & Entity) | (T & Entity)[] | Entity | Entity[]> { try { const permissionOptionsFromArgs = maybeOptionsOrMaybePermissionOptions && ('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions || 'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions) ? maybeOptionsOrMaybePermissionOptions : permissionOptions; let target = arguments.length > 1 && (typeof targetOrEntity === 'function' || InstanceChecker.isEntitySchema(targetOrEntity) || typeof targetOrEntity === 'string') ? targetOrEntity : undefined; const entity = target ? entityOrMaybeOptions : targetOrEntity; const options = target ? maybeOptionsOrMaybePermissionOptions : entityOrMaybeOptions; if (InstanceChecker.isEntitySchema(target)) target = target.options.name; if (Array.isArray(entity) && entity.length === 0) return Promise.resolve(entity as Entity[]); const queryRunnerForEntityPersistExecutor = this.connection.createQueryRunnerForEntityPersistExecutor(); const isEntityArray = Array.isArray(entity); const entityTarget = target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor); const entityArray = isEntityArray ? entity : [entity]; const relationNestedQueries = new RelationNestedQueries( this.internalContext, ); const relationNestedConfig = relationNestedQueries.prepareNestedRelationQueries( entityArray, entityTarget, ); const entityWithConnectedRelations = isDefined(relationNestedConfig) ? await relationNestedQueries.processRelationNestedQueries({ entities: entityArray, relationNestedConfig, queryBuilder: this.createQueryBuilder( undefined, undefined, undefined, permissionOptions, ), }) : entityArray; const entityIds = entityArray .map((entity) => (entity as { id: string }).id) .filter(isDefined); const beforeUpdate = await this.find( entityTarget, { where: { id: In(entityIds) }, }, { shouldBypassPermissionChecks: true }, // Bypass as this is for event emission ); const beforeUpdateMapById = beforeUpdate.reduce( (acc, e: ObjectLiteral) => { acc[e.id] = e; return acc; }, {} as Record, ); const objectMetadataItem = getObjectMetadataFromEntityTarget( entityTarget, this.internalContext, ); const formattedEntityOrEntities = formatData( entityWithConnectedRelations, objectMetadataItem, ); const updatedColumns = formattedEntityOrEntities .map((e) => Object.keys(e)) .flat(); this.validatePermissions({ target: targetOrEntity, operationType: 'update', permissionOptions: permissionOptionsFromArgs, selectedColumns: [], updatedColumns, }); const result = await new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'save', target, formattedEntityOrEntities as ObjectLiteral[], options as SaveOptions | (SaveOptions & { reload: false }), ) .execute() .then(() => formattedEntityOrEntities as Entity[]) .finally(() => queryRunnerForEntityPersistExecutor.release()); const resultArray = Array.isArray(result) ? result : [result]; let formattedResult = formatResult( resultArray, objectMetadataItem, this.internalContext.objectMetadataMaps, ); const updatedEntities = formattedResult.filter( (entity) => beforeUpdateMapById[entity.id], ); const createdEntities = formattedResult.filter( (entity) => !beforeUpdateMapById[entity.id], ); await this.internalContext.eventEmitterService.emitMutationEvent({ action: DatabaseEventAction.UPDATED, objectMetadataItem, workspaceId: this.internalContext.workspaceId, entities: updatedEntities, beforeEntities: updatedEntities.map( (entity) => beforeUpdateMapById[entity.id], ), }); await this.internalContext.eventEmitterService.emitMutationEvent({ action: DatabaseEventAction.CREATED, objectMetadataItem, workspaceId: this.internalContext.workspaceId, entities: createdEntities, }); const isFieldPermissionsEnabled = this.getFeatureFlagMap().IS_FIELDS_PERMISSIONS_ENABLED; const permissionCheckApplies = permissionOptionsFromArgs?.shouldBypassPermissionChecks !== true && objectMetadataItem.isSystem !== true; if (isFieldPermissionsEnabled && permissionCheckApplies) { formattedResult = this.getFormattedResultWithoutNonReadableFields({ formattedResult, objectMetadataItem, permissionOptionsFromArgs, }); } return isEntityArray ? formattedResult : formattedResult[0]; } catch (error) { throw computeTwentyORMException(error); } } private getFormattedResultWithoutNonReadableFields< Entity extends ObjectLiteral, >({ formattedResult, objectMetadataItem, permissionOptionsFromArgs, }: { formattedResult: Entity[]; objectMetadataItem: ObjectMetadataItemWithFieldMaps; permissionOptionsFromArgs: PermissionOptions | undefined; }): Entity[] { if (permissionOptionsFromArgs?.shouldBypassPermissionChecks === true) { return formattedResult; } const restrictedFields = permissionOptionsFromArgs?.objectRecordsPermissions?.[ objectMetadataItem.id ].restrictedFields; if (!restrictedFields) { throw new InternalServerError('Restricted fields not found'); } if (isEmpty(restrictedFields)) { return formattedResult; } const objectMetadataItemWithFieldMaps = this.internalContext.objectMetadataMaps.byId[objectMetadataItem.id]; const restrictedFieldNames = new Set( Object.entries(restrictedFields) .filter(([_, fieldPermissions]) => fieldPermissions.canRead === false) .map(([fieldMetadataId]) => { const fieldMetadata = objectMetadataItemWithFieldMaps?.fieldsById[fieldMetadataId]; if (!isDefined(fieldMetadata)) { throw new InternalServerError( `Field metadata not found for field ${fieldMetadataId}`, ); } return fieldMetadata.name; }), ); const filteredResult = formattedResult.map((individualFormattedResult) => { return Object.fromEntries( Object.entries(individualFormattedResult).filter( ([key]) => !restrictedFieldNames.has(key), ), ); }); return filteredResult as Entity[]; } override remove( entity: Entity, options?: RemoveOptions, permissionOptions?: PermissionOptions, ): Promise; override remove( targetOrEntity: EntityTarget, entity: Entity, options?: RemoveOptions, permissionOptions?: PermissionOptions, ): Promise; override remove( entity: Entity[], options?: RemoveOptions, permissionOptions?: PermissionOptions, ): Promise; override remove( targetOrEntity: EntityTarget, entity: Entity[], options?: RemoveOptions, permissionOptions?: PermissionOptions, ): Promise; override async remove( targetOrEntity: EntityTarget | Entity[] | Entity, entityOrMaybeOptions: Entity | Entity[] | RemoveOptions, maybeOptionsOrMaybePermissionOptions?: RemoveOptions | PermissionOptions, permissionOptions?: PermissionOptions, ): Promise { const permissionOptionsFromArgs = maybeOptionsOrMaybePermissionOptions && ('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions || 'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions) ? (maybeOptionsOrMaybePermissionOptions as PermissionOptions) : permissionOptions; this.validatePermissions({ target: targetOrEntity, operationType: 'delete', permissionOptions: permissionOptionsFromArgs, selectedColumns: [], // TODO }); const target = arguments.length > 1 && (typeof targetOrEntity === 'function' || InstanceChecker.isEntitySchema(targetOrEntity) || typeof targetOrEntity === 'string') ? targetOrEntity : undefined; const entity = target ? entityOrMaybeOptions : targetOrEntity; const options = target ? maybeOptionsOrMaybePermissionOptions : entityOrMaybeOptions; const isEntityArray = Array.isArray(entity); if (isEntityArray && entity.length === 0) return Promise.resolve(entity); const queryRunnerForEntityPersistExecutor = this.connection.createQueryRunnerForEntityPersistExecutor(); const entityTarget = target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor); const objectMetadataItem = getObjectMetadataFromEntityTarget( entityTarget, this.internalContext, ); const formattedEntity = formatData(entity, objectMetadataItem); const result = new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'remove', target as string | undefined, formattedEntity as ObjectLiteral, options as RemoveOptions, ) .execute() .then(() => formattedEntity as Entity | Entity[]) .finally(() => queryRunnerForEntityPersistExecutor.release()); const formattedResult = formatResult( result, objectMetadataItem, this.internalContext.objectMetadataMaps, ); await this.internalContext.eventEmitterService.emitMutationEvent({ action: DatabaseEventAction.DESTROYED, objectMetadataItem, workspaceId: this.internalContext.workspaceId, entities: formattedResult, }); return isEntityArray ? formattedResult : formattedResult[0]; } override softRemove( entities: Entity[], options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override softRemove( entities: Entity, options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override softRemove< Entity extends ObjectLiteral, T extends DeepPartial, >( targetOrEntity: EntityTarget, entities: T[], options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override softRemove< Entity extends ObjectLiteral, T extends DeepPartial, >( targetOrEntity: EntityTarget, entities: T, options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override async softRemove< Entity extends ObjectLiteral, T extends DeepPartial, >( targetOrEntityOrEntities: Entity | Entity[] | EntityTarget, entitiesOrMaybeOptions: T | T[] | SaveOptions, maybeOptionsOrMaybePermissionOptions?: SaveOptions | PermissionOptions, permissionOptions?: PermissionOptions, ): Promise { const permissionOptionsFromArgs = maybeOptionsOrMaybePermissionOptions && ('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions || 'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions) ? (maybeOptionsOrMaybePermissionOptions as PermissionOptions) : permissionOptions; this.validatePermissions({ target: targetOrEntityOrEntities, operationType: 'soft-delete', permissionOptions: permissionOptionsFromArgs, selectedColumns: [], // TODO }); let target = arguments.length > 1 && (typeof targetOrEntityOrEntities === 'function' || InstanceChecker.isEntitySchema(targetOrEntityOrEntities) || typeof targetOrEntityOrEntities === 'string') ? targetOrEntityOrEntities : undefined; const entity = target ? entitiesOrMaybeOptions : targetOrEntityOrEntities; const options = target ? maybeOptionsOrMaybePermissionOptions : entitiesOrMaybeOptions; if (InstanceChecker.isEntitySchema(target)) target = target.options.name; if (Array.isArray(entity) && entity.length === 0) return Promise.resolve(entity); const queryRunnerForEntityPersistExecutor = this.connection.createQueryRunnerForEntityPersistExecutor(); const isEntityArray = Array.isArray(entity); const entityTarget = target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor); const objectMetadataItem = getObjectMetadataFromEntityTarget( entityTarget, this.internalContext, ); const formattedEntity = formatData(entity, objectMetadataItem); const result = new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'soft-remove', target, formattedEntity as ObjectLiteral, options as SaveOptions, ) .execute() .then(() => formattedEntity as Entity) .finally(() => queryRunnerForEntityPersistExecutor.release()); const formattedResult = formatResult( result, objectMetadataItem, this.internalContext.objectMetadataMaps, ); await this.internalContext.eventEmitterService.emitMutationEvent({ action: DatabaseEventAction.DELETED, objectMetadataItem, workspaceId: this.internalContext.workspaceId, entities: formattedResult, }); return isEntityArray ? formattedResult : formattedResult[0]; } override recover( entities: Entity[], options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override recover( entity: Entity, options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override recover>( targetOrEntity: EntityTarget, entities: T[], options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override recover>( targetOrEntity: EntityTarget, entity: T, options?: SaveOptions, permissionOptions?: PermissionOptions, ): Promise; override async recover< Entity extends ObjectLiteral, T extends DeepPartial, >( targetOrEntityOrEntities: EntityTarget | Entity | Entity[], entityOrEntitiesOrMaybeOptions: T | T[] | SaveOptions, maybeOptionsOrMaybePermissionOptions?: SaveOptions | PermissionOptions, permissionOptions?: PermissionOptions, ): Promise { const permissionOptionsFromArgs = maybeOptionsOrMaybePermissionOptions && ('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions || 'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions) ? (maybeOptionsOrMaybePermissionOptions as PermissionOptions) : permissionOptions; this.validatePermissions({ target: targetOrEntityOrEntities, operationType: 'restore', permissionOptions: permissionOptionsFromArgs, selectedColumns: [], // TODO }); let target = arguments.length > 1 && (typeof targetOrEntityOrEntities === 'function' || InstanceChecker.isEntitySchema(targetOrEntityOrEntities) || typeof targetOrEntityOrEntities === 'string') ? targetOrEntityOrEntities : undefined; const entity = target ? entityOrEntitiesOrMaybeOptions : targetOrEntityOrEntities; const options = target ? maybeOptionsOrMaybePermissionOptions : entityOrEntitiesOrMaybeOptions; if (InstanceChecker.isEntitySchema(target)) target = target.options.name; const isEntityArray = Array.isArray(entity); if (isEntityArray && entity.length === 0) return Promise.resolve(entity); const queryRunnerForEntityPersistExecutor = this.connection.createQueryRunnerForEntityPersistExecutor(); const entityTarget = target ?? (isEntityArray ? entity[0]?.constructor : entity.constructor); const objectMetadataItem = getObjectMetadataFromEntityTarget( entityTarget, this.internalContext, ); const formattedEntity = formatData(entity, objectMetadataItem); const result = new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'recover', target, formattedEntity as ObjectLiteral, options as SaveOptions, ) .execute() .then(() => formattedEntity as Entity) .finally(() => queryRunnerForEntityPersistExecutor.release()); const formattedResult = formatResult( result, objectMetadataItem, this.internalContext.objectMetadataMaps, ); await this.internalContext.eventEmitterService.emitMutationEvent({ action: DatabaseEventAction.RESTORED, objectMetadataItem, workspaceId: this.internalContext.workspaceId, entities: formattedResult, }); return isEntityArray ? formattedResult : formattedResult[0]; } // Forbidden methods // eslint-disable-next-line @typescript-eslint/no-explicit-any override query(_query: string, _parameters?: any[]): Promise { throw new PermissionsException( 'Method not allowed.', PermissionsExceptionCode.RAW_SQL_NOT_ALLOWED, ); } }