import isEmpty from 'lodash.isempty'; import { type ObjectsPermissions, type ObjectsPermissionsByRoleId, } from 'twenty-shared/types'; import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; import { type DeleteResult, EntityManager, type EntityTarget, type FindManyOptions, type FindOneOptions, type FindOptionsWhere, In, type InsertResult, type ObjectId, type ObjectLiteral, type QueryRunner, type RemoveOptions, type Repository, type SaveOptions, type SelectQueryBuilder, TypeORMError, type UpdateResult, } from 'typeorm'; import { type DeepPartial } from 'typeorm/common/DeepPartial'; import { type 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 { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer'; import { type UpdateOptions } from 'typeorm/repository/UpdateOptions'; import { type UpsertOptions } from 'typeorm/repository/UpsertOptions'; import { InstanceChecker } from 'typeorm/util/InstanceChecker'; import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { type 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 { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type'; import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { type BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { type DeepPartialWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type'; import { type QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { getEntityTarget } from 'src/engine/twenty-orm/entity-manager/utils/get-entity-target'; import { computeTwentyORMException } from 'src/engine/twenty-orm/error-handling/compute-twenty-orm-exception'; import { FilesFieldSync } from 'src/engine/twenty-orm/field-operations/files-field-sync/files-field-sync'; import { RelationNestedQueries } from 'src/engine/twenty-orm/field-operations/relation-nested-queries/relation-nested-queries'; import { type GlobalWorkspaceDataSource } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource'; import { type 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 { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; import { type WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; type PermissionOptions = { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectsPermissions; }; export class WorkspaceEntityManager extends EntityManager { // oxlint-disable-next-line typescript/no-explicit-any readonly repositories: Map>; declare connection: GlobalWorkspaceDataSource; constructor( connection: GlobalWorkspaceDataSource, queryRunner?: QueryRunner, ) { super(connection, queryRunner); this.repositories = new Map(); } private get eventEmitterService(): WorkspaceEventEmitter { return this.connection.eventEmitterService; } get authContext(): WorkspaceAuthContext { const context = getWorkspaceContext(); return context.authContext; } get internalContext(): WorkspaceInternalContext { const context = getWorkspaceContext(); return { workspaceId: context.authContext.workspace.id, flatObjectMetadataMaps: context.flatObjectMetadataMaps, flatFieldMetadataMaps: context.flatFieldMetadataMaps, flatIndexMaps: context.flatIndexMaps, flatRowLevelPermissionPredicateMaps: context.flatRowLevelPermissionPredicateMaps, flatRowLevelPermissionPredicateGroupMaps: context.flatRowLevelPermissionPredicateGroupMaps, objectIdByNameSingular: context.objectIdByNameSingular, featureFlagsMap: context.featureFlagsMap, userWorkspaceRoleMap: context.userWorkspaceRoleMap, apiKeyRoleMap: context.apiKeyRoleMap, eventEmitterService: this.eventEmitterService, coreDataSource: this.connection.coreDataSource, }; } getFeatureFlagMap(): FeatureFlagMap { return this.connection.featureFlagMap; } private getPermissionsForRole( roleId: string, permissionsPerRoleId: ObjectsPermissionsByRoleId, ): ObjectsPermissions { if (!isDefined(permissionsPerRoleId?.[roleId])) { throw new PermissionsException( `No permissions found for role in datasource (roleId: ${roleId})`, PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE, ); } return permissionsPerRoleId[roleId]; } override getRepository( target: EntityTarget, rolePermissionConfig?: RolePermissionConfig, authContext?: WorkspaceAuthContext, ): WorkspaceRepository { const dataSource = this.connection; let objectPermissions = {}; let shouldBypassPermissionChecks = false; const objectPermissionsByRoleId = dataSource.permissionsPerRoleId; if ( rolePermissionConfig && 'shouldBypassPermissionChecks' in rolePermissionConfig ) { shouldBypassPermissionChecks = rolePermissionConfig.shouldBypassPermissionChecks; } if (rolePermissionConfig && 'unionOf' in rolePermissionConfig) { if (rolePermissionConfig.unionOf.length === 1) { objectPermissions = this.getPermissionsForRole( rolePermissionConfig.unionOf[0], objectPermissionsByRoleId, ); } else { // TODO: Implement union logic for combining permissions across multiple roles throw new Error( 'Union permission logic for multiple roles not yet implemented', ); } } if (rolePermissionConfig && 'intersectionOf' in rolePermissionConfig) { const allRolePermissions = rolePermissionConfig.intersectionOf.map( (roleId: string) => objectPermissionsByRoleId?.[roleId], ); // defaultRoleId has no foreign key and can dangle. A bound that cannot // be resolved denies rather than letting the rest decide alone. objectPermissions = allRolePermissions.every(isDefined) ? computePermissionIntersection(allRolePermissions) : {}; } const newRepository = new WorkspaceRepository( target, this, dataSource.featureFlagMap, this.queryRunner, objectPermissions, shouldBypassPermissionChecks, authContext, ); return newRepository; } override createQueryBuilder( entityClassOrQueryRunner?: EntityTarget | QueryRunner, alias?: string, queryRunner?: QueryRunner, options: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectsPermissions; } = { 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, this.authContext, this.getFeatureFlagMap(), ); } override async insert( target: EntityTarget, entity: | QueryDeepPartialEntityWithNestedRelationFields | QueryDeepPartialEntityWithNestedRelationFields[], selectedColumns: string[] | '*' = '*', permissionOptions?: PermissionOptions, authContext?: WorkspaceAuthContext, ): Promise { const metadata = this.connection.getMetadata(target); return this.createQueryBuilder( target, metadata.name, undefined, permissionOptions, ) .insert() .setWorkspaceAuthContext(authContext ?? ({} as WorkspaceAuthContext)) .values(entity) .returning(selectedColumns) .execute(); } override upsert( target: EntityTarget, entityOrEntities: | QueryDeepPartialEntityWithNestedRelationFields | QueryDeepPartialEntityWithNestedRelationFields[], conflictPathsOrOptions: string[] | UpsertOptions, permissionOptions?: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectsPermissions; }, 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', ), ); const overwrites = [...conflictColumns, ...overwriteColumns].map( (col) => col.databaseName, ); const conflictTargets = conflictColumns.map((col) => col.databaseName); const upsertOptions = { skipUpdateIfNoValuesChanged: options.skipUpdateIfNoValuesChanged, indexPredicate: options.indexPredicate, upsertType: options.upsertType || this.connection.driver.supportedUpsertTypes[0], }; const queryBuilder = this.createQueryBuilder( undefined, undefined, undefined, permissionOptions, ) .insert() .into(target) .values(entities) .orUpdate(overwrites, conflictTargets, upsertOptions) .returning(options.returning ?? selectedColumns); return queryBuilder.execute(); } override update( target: EntityTarget, criteria: | string | string[] | number | number[] | Date | Date[] | ObjectId | ObjectId[] | unknown, partialEntity: QueryDeepPartialEntity, options?: UpdateOptions, 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(options?.returning ?? selectedColumns) .execute(); } else { return this.createQueryBuilder( target, metadata.name, undefined, permissionOptions, ) .update() .set(partialEntity) .where(criteria) .returning(options?.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 } } // oxlint-disable-next-line typescript/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, undefined, permissionOptions, selectedColumns, ); } validatePermissions({ target, operationType, permissionOptions, selectedColumns, updatedColumns = [], }: { target: EntityTarget | Entity; operationType: OperationType; permissionOptions?: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectsPermissions; }; 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, objectsPermissions: permissionOptions?.objectRecordsPermissions ?? {}, flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, objectIdByNameSingular: this.internalContext.objectIdByNameSingular, selectedColumns, allFieldsSelected: false, updatedColumns, }); } private extractTargetNameSingularFromEntityTarget( target: EntityTarget, ): string { return this.connection.getMetadata(target).name; } // oxlint-disable-next-line typescript/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, this.internalContext.flatFieldMetadataMaps, ); 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.`); // oxlint-disable-next-line typescript/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, undefined, 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) }, withDeleted: true, }, { 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 filesFieldSync = new FilesFieldSync(this.internalContext); let filesFieldDiffByEntityIndex = null; let filesFieldFileIds = null; filesFieldDiffByEntityIndex = filesFieldSync.computeFilesFieldDiffBeforeUpsert( entityWithConnectedRelations, entityTarget, beforeUpdateMapById, ); if (isDefined(filesFieldDiffByEntityIndex)) { const result = await filesFieldSync.enrichFilesFields({ entities: entityWithConnectedRelations, filesFieldDiffByEntityIndex, workspaceId: this.internalContext.workspaceId, target: entityTarget, }); filesFieldFileIds = result.fileIds; entityWithConnectedRelations.splice( 0, entityWithConnectedRelations.length, ...result.entities, ); } const objectMetadataItem = getObjectMetadataFromEntityTarget( entityTarget, this.internalContext, ); const formattedEntityOrEntities = formatData( entityWithConnectedRelations, objectMetadataItem, this.internalContext.flatFieldMetadataMaps, ); 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[]) // oxlint-disable-next-line typescript/no-misused-promises .finally(() => queryRunnerForEntityPersistExecutor.release()); if (isDefined(filesFieldFileIds)) { await filesFieldSync.updateFileEntityRecords(filesFieldFileIds); } const resultArray = Array.isArray(result) ? result : [result]; let formattedResult = formatResult( resultArray, objectMetadataItem, this.internalContext.flatObjectMetadataMaps, this.internalContext.flatFieldMetadataMaps, ); const createdEntities = formattedResult.filter( (entity) => !beforeUpdateMapById[entity.id], ); const updatedEntityIds = formattedResult .map((entity) => entity.id) .filter((entityId) => isDefined(beforeUpdateMapById[entityId])); const updatedEntities = isNonEmptyArray(updatedEntityIds) ? await this.find( entityTarget, { where: { id: In(updatedEntityIds) }, withDeleted: true, }, { shouldBypassPermissionChecks: true }, // Bypass as this is for event emission ) : []; this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, objectMetadataItem, flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, recordsAfter: updatedEntities, recordsBefore: beforeUpdate, }), ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.CREATED, objectMetadataItem, flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, recordsAfter: createdEntities, }), ); const permissionCheckApplies = permissionOptionsFromArgs?.shouldBypassPermissionChecks !== true && objectMetadataItem.isSystem !== true; if (permissionCheckApplies) { formattedResult = this.getFormattedResultWithoutNonReadableFields({ formattedResult, objectMetadataItem, permissionOptionsFromArgs, }); } return isEntityArray ? formattedResult : formattedResult[0]; } catch (error) { const objectMetadataItem = getObjectMetadataFromEntityTarget( getEntityTarget(targetOrEntity, entityOrMaybeOptions), this.internalContext, ); throw await computeTwentyORMException( error, objectMetadataItem, this, this.internalContext, ); } } private getFormattedResultWithoutNonReadableFields< Entity extends ObjectLiteral, >({ formattedResult, objectMetadataItem, permissionOptionsFromArgs, }: { formattedResult: Entity[]; objectMetadataItem: FlatObjectMetadata; 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 restrictedFieldNames = new Set( Object.entries(restrictedFields) .filter(([_, fieldPermissions]) => fieldPermissions.canRead === false) .map(([fieldMetadataId]) => { const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({ flatEntityId: fieldMetadataId, flatEntityMaps: this.internalContext.flatFieldMetadataMaps, }); 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, this.internalContext.flatFieldMetadataMaps, ); const result = new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'remove', target as string | undefined, formattedEntity as ObjectLiteral, options as RemoveOptions, ) .execute() .then(() => formattedEntity as Entity | Entity[]) // oxlint-disable-next-line typescript/no-misused-promises .finally(() => queryRunnerForEntityPersistExecutor.release()); const formattedResult = formatResult( result, objectMetadataItem, this.internalContext.flatObjectMetadataMaps, this.internalContext.flatFieldMetadataMaps, ); const recordsBefore = Array.isArray(formattedResult) ? formattedResult : [formattedResult]; this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DESTROYED, objectMetadataItem, flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, recordsBefore, }), ); 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 entityArray = isEntityArray ? entity : [entity]; const entityIds = entityArray .map((entity) => (entity as { id: string }).id) .filter(isDefined); const recordsBeforeFindResult = await this.find( entityTarget, { where: { id: In(entityIds) }, }, { shouldBypassPermissionChecks: true }, // Bypass as this is for event emission ); const beforeUpdateMapById = recordsBeforeFindResult.reduce( (acc, e: BaseWorkspaceEntity) => { acc[e.id] = e; return acc; }, {} as Record, ); const objectMetadataItem = getObjectMetadataFromEntityTarget( entityTarget, this.internalContext, ); const formattedEntity = formatData( entity, objectMetadataItem, this.internalContext.flatFieldMetadataMaps, ); const result = new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'soft-remove', target, formattedEntity as ObjectLiteral, options as SaveOptions, ) .execute() .then(() => formattedEntity as Entity) // oxlint-disable-next-line typescript/no-misused-promises .finally(() => queryRunnerForEntityPersistExecutor.release()); const formattedResult = formatResult( result, objectMetadataItem, this.internalContext.flatObjectMetadataMaps, this.internalContext.flatFieldMetadataMaps, ); const recordsAfter = Array.isArray(formattedResult) ? formattedResult : [formattedResult]; const recordsBefore = recordsAfter.map( (record) => beforeUpdateMapById[record.id] as unknown as Entity, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DELETED, objectMetadataItem, flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, recordsAfter, recordsBefore, }), ); 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 entityArray = isEntityArray ? entity : [entity]; const entityIds = entityArray .map((entity) => (entity as { id: string }).id) .filter(isDefined); const recordsBeforeFindResult = await this.find( entityTarget, { where: { id: In(entityIds) }, }, { shouldBypassPermissionChecks: true }, // Bypass as this is for event emission ); const beforeUpdateMapById = recordsBeforeFindResult.reduce( (acc, e: BaseWorkspaceEntity) => { acc[e.id] = e; return acc; }, {} as Record, ); const objectMetadataItem = getObjectMetadataFromEntityTarget( entityTarget, this.internalContext, ); const formattedEntity = formatData( entity, objectMetadataItem, this.internalContext.flatFieldMetadataMaps, ); const result = new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'recover', target, formattedEntity as ObjectLiteral, options as SaveOptions, ) .execute() .then(() => formattedEntity as Entity) // oxlint-disable-next-line typescript/no-misused-promises .finally(() => queryRunnerForEntityPersistExecutor.release()); const formattedResult = formatResult( result, objectMetadataItem, this.internalContext.flatObjectMetadataMaps, this.internalContext.flatFieldMetadataMaps, ); const recordsAfter = Array.isArray(formattedResult) ? formattedResult : [formattedResult]; const recordsBefore = recordsAfter.map( (record) => beforeUpdateMapById[record.id] as unknown as Entity, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.RESTORED, objectMetadataItem, flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, recordsAfter, recordsBefore, }), ); return isEntityArray ? formattedResult : formattedResult[0]; } // Forbidden methods // oxlint-disable-next-line typescript/no-explicit-any override query(_query: string, _parameters?: any[]): Promise { throw new PermissionsException( 'Method not allowed.', PermissionsExceptionCode.RAW_SQL_NOT_ALLOWED, ); } }