import { Entity } from '@microsoft/microsoft-graph-types'; import { ObjectRecordsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { DeleteResult, EntityManager, EntityTarget, FindManyOptions, FindOneOptions, FindOptionsWhere, 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 { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { QueryDeepPartialEntityWithRelationConnect } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-relation-connect.type'; import { RelationConnectQueryConfig } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type'; import { TwentyORMException, TwentyORMExceptionCode, } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; 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 { computeRelationConnectQueryConfigs } from 'src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util'; import { createSqlWhereTupleInClause } from 'src/engine/twenty-orm/utils/create-sql-where-tuple-in-clause.utils'; import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; import { getRecordToConnectFields } from 'src/engine/twenty-orm/utils/get-record-to-connect-fields.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; }, ): WorkspaceRepository { const dataSource = this.connection; const repositoryKey = this.getRepositoryKey({ target, dataSource, roleId: permissionOptions?.roleId, shouldBypassPermissionChecks: permissionOptions?.shouldBypassPermissionChecks ?? false, }); const repoFromMap = this.repositories.get(repositoryKey); if (repoFromMap) { return repoFromMap as WorkspaceRepository; } 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, ); this.repositories.set(repositoryKey, newRepository); return newRepository; } override createQueryBuilder( entityClassOrQueryRunner?: EntityTarget | QueryRunner, alias?: string, queryRunner?: QueryRunner, options: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; } = { shouldBypassPermissionChecks: false, objectRecordsPermissions: {}, }, ): SelectQueryBuilder | 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, ); } override async insert( target: EntityTarget, entity: | QueryDeepPartialEntityWithRelationConnect | QueryDeepPartialEntityWithRelationConnect[], permissionOptions?: PermissionOptions, ): Promise { const entityArray = Array.isArray(entity) ? entity : [entity]; const connectedEntities = await this.processRelationConnect( entityArray, target, permissionOptions, ); return this.createQueryBuilder( undefined, undefined, undefined, permissionOptions, ) .insert() .into(target) .values(connectedEntities) .execute(); } override upsert( target: EntityTarget, entityOrEntities: | QueryDeepPartialEntity | QueryDeepPartialEntity[], conflictPathsOrOptions: string[] | UpsertOptions, permissionOptions?: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; }, ): Promise { const metadata = this.connection.getMetadata(target); let options; if (Array.isArray(conflictPathsOrOptions)) { options = { conflictPaths: conflictPathsOrOptions, }; } else { options = conflictPathsOrOptions; } let entities: QueryDeepPartialEntity[]; 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], }, ) .execute(); } override update( target: EntityTarget, criteria: | string | string[] | number | number[] | Date | Date[] | ObjectId | ObjectId[] | unknown, partialEntity: QueryDeepPartialEntity, permissionOptions?: PermissionOptions, ): 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 update method.`, ), ); } if ( typeof criteria === 'string' || typeof criteria === 'number' || criteria instanceof Date || Array.isArray(criteria) ) { return this.createQueryBuilder( undefined, undefined, undefined, permissionOptions, ) .update(target) .set(partialEntity) .whereInIds(criteria) .execute(); } else { return this.createQueryBuilder( undefined, undefined, undefined, permissionOptions, ) .update(target) .set(partialEntity) .where(criteria) .execute(); } } override increment( target: EntityTarget, criteria: object, propertyPath: string, value: number | string, permissionOptions?: PermissionOptions, ): 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.createQueryBuilder( target, 'entity', undefined, permissionOptions, ) .update(target as QueryDeepPartialEntity) .set(values) .where(criteria) .execute(); } private getRepositoryKey({ target, dataSource, roleId, shouldBypassPermissionChecks, }: { // eslint-disable-next-line @typescript-eslint/no-explicit-any target: EntityTarget; dataSource: WorkspaceDataSource; shouldBypassPermissionChecks: boolean; roleId?: string; }) { const repositoryPrefix = dataSource.getMetadata(target).name; const roleIdSuffix = roleId ? `_${roleId}` : ''; const rolesPermissionsVersionSuffix = dataSource.rolesPermissionsVersion ? `_${dataSource.rolesPermissionsVersion}` : ''; const featureFlagMapVersionSuffix = dataSource.featureFlagMapVersion ? `_${dataSource.featureFlagMapVersion}` : ''; return shouldBypassPermissionChecks ? `${repositoryPrefix}_bypass${featureFlagMapVersionSuffix}` : `${repositoryPrefix}${roleIdSuffix}${rolesPermissionsVersionSuffix}${featureFlagMapVersionSuffix}`; } validatePermissions( target: EntityTarget | Entity, operationType: OperationType, permissionOptions?: { shouldBypassPermissionChecks?: boolean; objectRecordsPermissions?: ObjectRecordsPermissions; }, ): 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, }); } 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, ): 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) .execute(); } else { return this.createQueryBuilder( undefined, undefined, undefined, permissionOptions, ) .delete() .from(targetOrEntity) .where(criteria) .execute(); } } override softDelete( targetOrEntity: EntityTarget, criteria: unknown, permissionOptions?: PermissionOptions, ): 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) .execute(); } else { return this.createQueryBuilder( undefined, undefined, this.queryRunner, permissionOptions, ) .softDelete() .from(targetOrEntity) .where(criteria) .execute(); } } override restore( targetOrEntity: EntityTarget, criteria: unknown, permissionOptions?: PermissionOptions, ): 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) .execute(); } else { return this.createQueryBuilder( undefined, undefined, this.queryRunner, permissionOptions, ) .restore() .from(targetOrEntity) .where(criteria) .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('1') .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('1') .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(entityClass, 'delete', permissionOptions); return super.clear(entityClass); } override async preload( entityClass: EntityTarget, entityLike: DeepPartial, permissionOptions?: PermissionOptions, ): Promise { 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( entityLike, metadata, ); if (transformedEntity) return this.merge(entityClass, transformedEntity, entityLike) as Entity; return undefined; } override decrement( target: EntityTarget, criteria: object, propertyPath: string, value: number | string, permissionOptions?: PermissionOptions, ): 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.createQueryBuilder( target, 'entity', undefined, permissionOptions, ) .update(target as QueryDeepPartialEntity) .set(values) .where(criteria) .execute(); } 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 DeepPartial, >( 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[]> { const permissionOptionsFromArgs = maybeOptionsOrMaybePermissionOptions && ('shouldBypassPermissionChecks' in maybeOptionsOrMaybePermissionOptions || 'objectRecordsPermissions' in maybeOptionsOrMaybePermissionOptions) ? maybeOptionsOrMaybePermissionOptions : permissionOptions; this.validatePermissions( targetOrEntity, 'update', permissionOptionsFromArgs, ); 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(); return new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'save', target, entity as ObjectLiteral, options as SaveOptions | (SaveOptions & { reload: false }), ) .execute() .then(() => entity as Entity) .finally(() => queryRunnerForEntityPersistExecutor.release()); } 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( targetOrEntity, 'delete', permissionOptionsFromArgs, ); 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; if (Array.isArray(entity) && entity.length === 0) return Promise.resolve(entity); const queryRunnerForEntityPersistExecutor = this.connection.createQueryRunnerForEntityPersistExecutor(); return new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'remove', target as string | undefined, entity as ObjectLiteral, options as RemoveOptions, ) .execute() .then(() => entity as Entity | Entity[]) .finally(() => queryRunnerForEntityPersistExecutor.release()); } 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( targetOrEntityOrEntities, 'soft-delete', permissionOptionsFromArgs, ); 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(); return new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'soft-remove', target, entity as ObjectLiteral, options as SaveOptions, ) .execute() .then(() => entity as Entity) .finally(() => queryRunnerForEntityPersistExecutor.release()); } 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( targetOrEntityOrEntities, 'restore', permissionOptionsFromArgs, ); 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; if (Array.isArray(entity) && entity.length === 0) return Promise.resolve(entity); const queryRunnerForEntityPersistExecutor = this.connection.createQueryRunnerForEntityPersistExecutor(); return new EntityPersistExecutor( this.connection, queryRunnerForEntityPersistExecutor, 'recover', target, entity as ObjectLiteral, options as SaveOptions, ) .execute() .then(() => entity as Entity) .finally(() => queryRunnerForEntityPersistExecutor.release()); } // 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, ); } private async processRelationConnect( entities: QueryDeepPartialEntityWithRelationConnect[], target: EntityTarget, permissionOptions?: PermissionOptions, ): Promise[]> { const objectMetadata = getObjectMetadataFromEntityTarget( target, this.internalContext, ); const objectMetadataMap = this.internalContext.objectMetadataMaps; const relationConnectQueryConfigs = computeRelationConnectQueryConfigs( entities, objectMetadata, objectMetadataMap, ); if (!isDefined(relationConnectQueryConfigs)) return entities; const recordsToConnectWithConfig = await this.executeConnectQueries( relationConnectQueryConfigs, permissionOptions, ); const updatedEntities = this.updateEntitiesWithRecordToConnectId( entities, recordsToConnectWithConfig, ); return updatedEntities; } private async executeConnectQueries( relationConnectQueryConfigs: Record, permissionOptions?: PermissionOptions, ): Promise<[RelationConnectQueryConfig, Record[]][]> { const AllRecordsToConnectWithConfig: [ RelationConnectQueryConfig, Record[], ][] = []; for (const connectQueryConfig of Object.values( relationConnectQueryConfigs, )) { const { clause, parameters } = createSqlWhereTupleInClause( connectQueryConfig.recordToConnectConditions, connectQueryConfig.targetObjectName, ); const recordsToConnect = await this.createQueryBuilder( connectQueryConfig.targetObjectName, connectQueryConfig.targetObjectName, undefined, permissionOptions, ) .select(getRecordToConnectFields(connectQueryConfig)) .where(clause, parameters) .getRawMany(); AllRecordsToConnectWithConfig.push([ connectQueryConfig, recordsToConnect, ]); } return AllRecordsToConnectWithConfig; } private updateEntitiesWithRecordToConnectId( entities: QueryDeepPartialEntityWithRelationConnect[], recordsToConnectWithConfig: [ RelationConnectQueryConfig, Record[], ][], ): QueryDeepPartialEntity[] { return entities.map((entity, index) => { for (const [ connectQueryConfig, recordsToConnect, ] of recordsToConnectWithConfig) { if ( isDefined( connectQueryConfig.recordToConnectConditionByEntityIndex[index], ) ) { const recordToConnect = recordsToConnect.filter((record) => connectQueryConfig.recordToConnectConditionByEntityIndex[ index ].every(([field, value]) => record[field] === value), ); if (recordToConnect.length !== 1) { const recordToConnectTotal = recordToConnect.length; const connectFieldName = connectQueryConfig.connectFieldName; throw new TwentyORMException( `Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal}.`, TwentyORMExceptionCode.CONNECT_RECORD_NOT_FOUND, ); } entity = { ...entity, [connectQueryConfig.relationFieldName]: recordToConnect[0]['id'], [connectQueryConfig.connectFieldName]: null, }; } } return entity; }); } }