diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts new file mode 100644 index 000000000..fe165ee77 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -0,0 +1,255 @@ +import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { EntityManager } from 'typeorm'; + +import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { validateOperationIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils'; + +import { WorkspaceEntityManager } from './workspace-entity-manager'; + +jest.mock('src/engine/twenty-orm/repository/permissions.utils', () => ({ + validateOperationIsPermittedOrThrow: jest.fn(), +})); + +jest.mock('../repository/workspace-select-query-builder', () => ({ + WorkspaceSelectQueryBuilder: jest.fn().mockImplementation(() => ({ + where: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue([]), + getOne: jest.fn().mockResolvedValue(null), + getManyAndCount: jest.fn().mockResolvedValue([[], 0]), + execute: jest + .fn() + .mockResolvedValue({ affected: 1, raw: [], generatedMaps: [] }), + setFindOptions: jest.fn().mockReturnThis(), + })), +})); + +describe('WorkspaceEntityManager', () => { + let entityManager: WorkspaceEntityManager; + let mockInternalContext: WorkspaceInternalContext; + let mockDataSource: WorkspaceDataSource; + let mockPermissionOptions: { + shouldBypassPermissionChecks: boolean; + objectRecordsPermissions?: ObjectRecordsPermissions; + }; + + beforeEach(() => { + mockInternalContext = { + workspaceId: 'test-workspace-id', + objectMetadataMaps: { + idByNameSingular: {}, + }, + featureFlagsMap: { + [FeatureFlagKey.IsPermissionsV2Enabled]: true, + }, + } as WorkspaceInternalContext; + + mockDataSource = { + featureFlagMap: { + [FeatureFlagKey.IsPermissionsV2Enabled]: true, + }, + permissionsPerRoleId: {}, + } as WorkspaceDataSource; + + mockPermissionOptions = { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: { + 'test-entity': { + canRead: true, + canUpdate: false, + canSoftDelete: false, + canDestroy: false, + }, + }, + }; + + // Mock TypeORM connection methods + const mockWorkspaceDataSource = { + getMetadata: jest.fn().mockReturnValue({ + name: 'test-entity', + columns: [], + relations: [], + findInheritanceMetadata: jest.fn(), + findColumnWithPropertyPath: jest.fn(), + }), + createQueryBuilder: jest.fn().mockReturnValue({ + delete: jest.fn().mockReturnThis(), + from: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + execute: jest + .fn() + .mockResolvedValue({ affected: 1, raw: [], generatedMaps: [] }), + getMany: jest.fn().mockResolvedValue([]), + getOne: jest.fn().mockResolvedValue(null), + getManyAndCount: jest.fn().mockResolvedValue([[], 0]), + update: jest.fn().mockReturnThis(), + softDelete: jest.fn().mockReturnThis(), + restore: jest.fn().mockReturnThis(), + }), + createQueryRunner: jest.fn().mockReturnValue({ + connect: jest.fn(), + startTransaction: jest.fn(), + commitTransaction: jest.fn(), + rollbackTransaction: jest.fn(), + release: jest.fn(), + clearTable: jest.fn(), + }), + }; + + entityManager = new WorkspaceEntityManager( + mockInternalContext, + mockDataSource, + ); + + Object.defineProperty(entityManager, 'connection', { + get: () => mockWorkspaceDataSource, + }); + + jest.spyOn(entityManager as any, 'validatePermissions'); + jest.spyOn(entityManager as any, 'createQueryBuilder'); + + jest + .spyOn(entityManager as any, 'extractTargetNameSingularFromEntityTarget') + .mockImplementation((entityName: string) => { + return entityName; + }); + + // Mock getFeatureFlagMap + jest.spyOn(entityManager as any, 'getFeatureFlagMap').mockReturnValue({ + [FeatureFlagKey.IsPermissionsV2Enabled]: true, + }); + + // Mock typeORM's EntityManager methods + jest + .spyOn(EntityManager.prototype, 'save') + .mockImplementation(() => Promise.resolve({})); + jest + .spyOn(EntityManager.prototype, 'update') + .mockImplementation(() => + Promise.resolve({ affected: 1, raw: [], generatedMaps: [] }), + ); + jest + .spyOn(EntityManager.prototype, 'restore') + .mockImplementation(() => + Promise.resolve({ affected: 1, raw: [], generatedMaps: [] }), + ); + jest + .spyOn(EntityManager.prototype, 'clear') + .mockImplementation(() => Promise.resolve()); + jest + .spyOn(EntityManager.prototype, 'preload') + .mockImplementation(() => Promise.resolve({})); + + // Mock metadata methods + const mockMetadata = { + hasAllPrimaryKeys: jest.fn().mockReturnValue(true), + columns: [], + relations: [], + findInheritanceMetadata: jest.fn(), + findColumnWithPropertyPath: jest.fn(), + }; + + // Update mockWorkspaceDataSource to include metadata + mockWorkspaceDataSource.getMetadata = jest + .fn() + .mockReturnValue(mockMetadata); + + // Reset the mock before each test + jest.clearAllMocks(); + }); + + describe('Query Method', () => { + it('should call validatePermissions and validateOperationIsPermittedOrThrow for find', async () => { + await entityManager.find('test-entity', {}, mockPermissionOptions); + + expect(entityManager.createQueryBuilder).toHaveBeenCalledWith( + 'test-entity', + undefined, + undefined, + mockPermissionOptions, + ); + }); + it('should throw error for query', async () => { + expect(() => entityManager.query('SELECT * FROM test')).toThrow( + 'Method not allowed.', + ); + }); + }); + + describe('Save Methods', () => { + it('should call validatePermissions and validateOperationIsPermittedOrThrow for save', async () => { + await entityManager.save( + 'test-entity', + {}, + { reload: false }, + mockPermissionOptions, + ); + expect(entityManager['validatePermissions']).toHaveBeenCalledWith( + 'test-entity', + 'update', + mockPermissionOptions, + ); + expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({ + entityName: 'test-entity', + operationType: 'update', + objectMetadataMaps: mockInternalContext.objectMetadataMaps, + objectRecordsPermissions: + mockPermissionOptions.objectRecordsPermissions, + }); + }); + }); + + describe('Update Methods', () => { + it('should call validatePermissions and validateOperationIsPermittedOrThrow for update', async () => { + await entityManager.update('test-entity', {}, {}, mockPermissionOptions); + expect(entityManager['validatePermissions']).toHaveBeenCalledWith( + 'test-entity', + 'update', + mockPermissionOptions, + ); + expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({ + entityName: 'test-entity', + operationType: 'update', + objectMetadataMaps: mockInternalContext.objectMetadataMaps, + objectRecordsPermissions: + mockPermissionOptions.objectRecordsPermissions, + }); + }); + }); + + describe('Other Methods', () => { + it('should call validatePermissions and validateOperationIsPermittedOrThrow for clear', async () => { + await entityManager.clear('test-entity', mockPermissionOptions); + expect(entityManager['validatePermissions']).toHaveBeenCalledWith( + 'test-entity', + 'delete', + mockPermissionOptions, + ); + expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({ + entityName: 'test-entity', + operationType: 'delete', + objectMetadataMaps: mockInternalContext.objectMetadataMaps, + objectRecordsPermissions: + mockPermissionOptions.objectRecordsPermissions, + }); + }); + + it('should call validatePermissions and validateOperationIsPermittedOrThrow for preload', async () => { + await entityManager.preload('test-entity', {}, mockPermissionOptions); + expect(entityManager['validatePermissions']).toHaveBeenCalledWith( + 'test-entity', + 'select', + mockPermissionOptions, + ); + expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({ + entityName: 'test-entity', + operationType: 'select', + objectMetadataMaps: mockInternalContext.objectMetadataMaps, + objectRecordsPermissions: + mockPermissionOptions.objectRecordsPermissions, + }); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index 6581c220e..09ad6b4b2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -1,15 +1,28 @@ +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 { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { UpsertOptions } from 'typeorm/repository/UpsertOptions'; @@ -29,6 +42,11 @@ import { import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +type PermissionOptions = { + shouldBypassPermissionChecks?: boolean; + objectRecordsPermissions?: ObjectRecordsPermissions; +}; + export class WorkspaceEntityManager extends EntityManager { private readonly internalContext: WorkspaceInternalContext; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -117,10 +135,11 @@ export class WorkspaceEntityManager extends EntityManager { alias?: string, queryRunner?: QueryRunner, options: { - shouldBypassPermissionChecks: boolean; - roleId?: string; + shouldBypassPermissionChecks?: boolean; + objectRecordsPermissions?: ObjectRecordsPermissions; } = { shouldBypassPermissionChecks: false, + objectRecordsPermissions: {}, }, ): SelectQueryBuilder | WorkspaceSelectQueryBuilder { let queryBuilder: SelectQueryBuilder; @@ -145,50 +164,23 @@ export class WorkspaceEntityManager extends EntityManager { if (!isPermissionsV2Enabled) { return queryBuilder; } else { - let objectPermissions = {}; - - if (options?.roleId) { - const dataSource = this.connection as WorkspaceDataSource; - const objectPermissionsByRoleId = dataSource.permissionsPerRoleId; - - objectPermissions = objectPermissionsByRoleId?.[options.roleId] ?? {}; - } - return new WorkspaceSelectQueryBuilder( queryBuilder, - objectPermissions, + options?.objectRecordsPermissions ?? {}, this.internalContext, options?.shouldBypassPermissionChecks ?? false, ); } } - override find( - target: EntityTarget, - options?: FindManyOptions, - permissionOptions?: { - shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectRecordsPermissions; - }, - ): Promise { - this.validatePermissions(target, 'select', permissionOptions); - - return super.find(target, options); - } - override insert( target: EntityTarget, - entityOrEntities: - | QueryDeepPartialEntity - | QueryDeepPartialEntity[], - permissionOptions?: { - shouldBypassPermissionChecks?: boolean; - objectRecordsPermissions?: ObjectRecordsPermissions; - }, + entity: QueryDeepPartialEntity | QueryDeepPartialEntity[], + permissionOptions?: PermissionOptions, ): Promise { this.validatePermissions(target, 'insert', permissionOptions); - return super.insert(target, entityOrEntities); + return super.insert(target, entity); } override upsert( @@ -207,6 +199,141 @@ export class WorkspaceEntityManager extends EntityManager { return super.upsert(target, entityOrEntities, conflictPathsOrOptions); } + override update( + target: EntityTarget, + criteria: + | string + | string[] + | number + | number[] + | Date + | Date[] + | ObjectId + | ObjectId[] + | unknown, + partialEntity: QueryDeepPartialEntity, + permissionOptions?: PermissionOptions, + ): Promise { + this.validatePermissions(target, 'update', permissionOptions); + + return super.update(target, criteria, partialEntity); + } + + 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 save>( + 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, + ); + + const target = + arguments.length > 1 && + (typeof targetOrEntity === 'function' || + typeof targetOrEntity === 'string') + ? (targetOrEntity as EntityTarget) + : undefined; + + const entityOrEntities = target + ? (entityOrMaybeOptions as T | T[]) + : (targetOrEntity as Entity | Entity[]); + + const options = target + ? (maybeOptionsOrMaybePermissionOptions as SaveOptions | undefined) + : (entityOrMaybeOptions as SaveOptions | undefined); + + if (isDefined(target)) { + let entity: T | undefined; + let entities: T[] | undefined; + + if (Array.isArray(entityOrEntities)) { + entities = entityOrEntities as T[]; + + return super.save(target, entities, options); + } else { + entity = entityOrEntities as T; + + return super.save(target, entity, options); + } + } else { + return super.save(entityOrEntities as Entity | Entity[], options); + } + } + + override increment( + target: EntityTarget, + criteria: unknown, + propertyPath: string, + value: number | string, + permissionOptions?: PermissionOptions, + ): Promise { + this.validatePermissions(target, 'update', permissionOptions); + + return super.increment(target, criteria, propertyPath, value); + } + private getRepositoryKey({ target, dataSource, @@ -214,7 +341,7 @@ export class WorkspaceEntityManager extends EntityManager { shouldBypassPermissionChecks, }: { // eslint-disable-next-line @typescript-eslint/no-explicit-any - target: EntityTarget; + target: EntityTarget; dataSource: WorkspaceDataSource; shouldBypassPermissionChecks: boolean; roleId?: string; @@ -233,8 +360,8 @@ export class WorkspaceEntityManager extends EntityManager { : `${repositoryPrefix}${roleIdSuffix}${rolesPermissionsVersionSuffix}${featureFlagMapVersionSuffix}`; } - private validatePermissions( - target: EntityTarget, + validatePermissions( + target: EntityTarget | Entity, operationType: OperationType, permissionOptions?: { shouldBypassPermissionChecks?: boolean; @@ -254,8 +381,13 @@ export class WorkspaceEntityManager extends EntityManager { return; } + const entityName = + typeof target === 'function' || typeof target === 'string' + ? this.extractTargetNameSingularFromEntityTarget(target) + : this.extractTargetNameSingularFromEntity(target); + validateOperationIsPermittedOrThrow({ - entityName: this.extractTargetNameSingularFromEntityTarget(target), + entityName, operationType, objectRecordsPermissions: permissionOptions?.objectRecordsPermissions ?? {}, @@ -264,9 +396,706 @@ export class WorkspaceEntityManager extends EntityManager { } private extractTargetNameSingularFromEntityTarget( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - target: EntityTarget, + 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; + } + + // Forbidden methods + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + override query(_query: string, _parameters?: any[]): Promise { + throw new Error('Method not allowed.'); + } + + // Not in use methods - duplicated from TypeORM's EntityManager to use our createQueryBuilder + + 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 { + this.validatePermissions(targetOrEntity, 'delete', permissionOptions); + + return super.delete(targetOrEntity, criteria); + } + + 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 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 = + typeof targetOrEntity === 'function' || typeof targetOrEntity === 'string' + ? (targetOrEntity as EntityTarget) + : undefined; + + const entityOrEntities = target + ? (entityOrMaybeOptions as Entity | Entity[]) + : (targetOrEntity as Entity | Entity[]); + + const options = target + ? (maybeOptionsOrMaybePermissionOptions as RemoveOptions | undefined) + : (entityOrMaybeOptions as RemoveOptions); + + if (isDefined(target)) { + if (Array.isArray(entityOrEntities)) { + return super.remove(target, entityOrEntities as Entity[], options); + } else { + return super.remove(target, entityOrEntities as Entity, options); + } + } else { + return super.remove(entityOrEntities as Entity | Entity[], options); + } + } + + 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 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, + ); + + const target = + typeof targetOrEntityOrEntities === 'function' || + typeof targetOrEntityOrEntities === 'string' + ? (targetOrEntityOrEntities as EntityTarget) + : undefined; + + const entityOrEntities = target + ? (entitiesOrMaybeOptions as T | T[]) + : (targetOrEntityOrEntities as Entity | Entity[]); + + const options = target + ? (maybeOptionsOrMaybePermissionOptions as SaveOptions | undefined) + : (entitiesOrMaybeOptions as SaveOptions); + + if (isDefined(target)) { + if (Array.isArray(entityOrEntities)) { + return super.softRemove(target, entityOrEntities as T[], options); + } else { + return super.softRemove(target, entityOrEntities as T, options); + } + } else { + if (Array.isArray(entityOrEntities)) { + return super.softRemove(entityOrEntities as Entity | Entity[], options); + } else { + return super.softRemove(entityOrEntities as Entity, options); + } + } + } + + 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 recover>( + 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, + ); + + const target = + typeof targetOrEntityOrEntities === 'function' || + typeof targetOrEntityOrEntities === 'string' + ? (targetOrEntityOrEntities as EntityTarget) + : undefined; + + const options = target + ? (maybeOptionsOrMaybePermissionOptions as SaveOptions | undefined) + : (entityOrEntitiesOrMaybeOptions as SaveOptions); + + if (target) { + if (Array.isArray(entityOrEntitiesOrMaybeOptions)) { + return super.recover( + target, + entityOrEntitiesOrMaybeOptions as T[], + options, + ); + } else { + return super.recover( + target, + entityOrEntitiesOrMaybeOptions as T, + options, + ); + } + } else { + if (Array.isArray(entityOrEntitiesOrMaybeOptions)) { + return super.recover( + entityOrEntitiesOrMaybeOptions as Entity | Entity[], + options, + ); + } else { + return super.recover(entityOrEntitiesOrMaybeOptions as Entity, options); + } + } + } + + 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 || {}) + .getExists(); + } + + 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 }) + .getExists(); + } + + 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 preload( + entityClass: EntityTarget, + entityLike: DeepPartial, + permissionOptions?: PermissionOptions, + ): Promise { + this.validatePermissions(entityClass, 'select', permissionOptions); + + return super.preload(entityClass, entityLike); + } + + override decrement( + target: EntityTarget, + criteria: unknown, + propertyPath: string, + value: number | string, + permissionOptions?: PermissionOptions, + ): Promise { + this.validatePermissions(target, 'update', permissionOptions); + + return super.decrement(target, criteria, propertyPath, value); + } } diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.spec.ts new file mode 100644 index 000000000..14f984a30 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.spec.ts @@ -0,0 +1,425 @@ +import { ObjectRecordsPermissions } from 'twenty-shared/types'; +import { + DeepPartial, + FindManyOptions, + FindOneOptions, + FindOptionsWhere, + ObjectLiteral, + QueryRunner, +} from 'typeorm'; + +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 { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; + +describe('WorkspaceRepository', () => { + let repository: WorkspaceRepository; + let mockEntityManager: jest.Mocked; + let mockInternalContext: WorkspaceInternalContext; + let mockFeatureFlagMap: FeatureFlagMap; + let mockObjectRecordsPermissions: ObjectRecordsPermissions; + let mockQueryRunner: QueryRunner; + + beforeEach(() => { + mockEntityManager = { + find: jest.fn(), + findBy: jest.fn(), + findAndCount: jest.fn(), + findAndCountBy: jest.fn(), + findOne: jest.fn(), + findOneBy: jest.fn(), + findOneOrFail: jest.fn(), + findOneByOrFail: jest.fn(), + save: jest.fn(), + remove: jest.fn(), + delete: jest.fn(), + softRemove: jest.fn(), + softDelete: jest.fn(), + recover: jest.fn(), + restore: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + upsert: jest.fn(), + exists: jest.fn(), + existsBy: jest.fn(), + count: jest.fn(), + countBy: jest.fn(), + sum: jest.fn(), + average: jest.fn(), + minimum: jest.fn(), + maximum: jest.fn(), + increment: jest.fn(), + decrement: jest.fn(), + preload: jest.fn(), + clear: jest.fn(), + } as unknown as jest.Mocked; + + mockInternalContext = { + workspaceId: 'test-workspace-id', + objectMetadataMaps: { + idByNameSingular: {}, + }, + featureFlagsMap: {}, + } as WorkspaceInternalContext; + + mockFeatureFlagMap = Object.values(FeatureFlagKey).reduce( + (acc, key) => ({ ...acc, [key]: false }), + {} as FeatureFlagMap, + ); + mockObjectRecordsPermissions = { + 'test-entity': { + canRead: true, + canUpdate: false, + canSoftDelete: false, + canDestroy: false, + }, + }; + mockQueryRunner = {} as QueryRunner; + + repository = new WorkspaceRepository( + mockInternalContext, + 'test-entity', + mockEntityManager, + mockFeatureFlagMap, + mockQueryRunner, + mockObjectRecordsPermissions, + false, + ); + + // Mock the private methods + jest + .spyOn(repository as any, 'getObjectMetadataFromTarget') + .mockResolvedValue({ + id: 'test-metadata-id', + nameSingular: 'test-entity', + namePlural: 'test-entities', + fields: [], + }); + + jest.spyOn(repository as any, 'formatData').mockImplementation((data) => { + if (Array.isArray(data)) { + return data.map((item) => Object.assign({}, item)); + } + + return Object.assign({}, data); + }); + + jest.spyOn(repository as any, 'formatResult').mockImplementation((data) => { + if (Array.isArray(data)) { + return data.map((item) => Object.assign({}, item)); + } + + return Object.assign({}, data); + }); + }); + + describe('Find Methods', () => { + it('should delegate to workspaceEntityManager find', async () => { + const options: FindManyOptions = { + where: { id: 'test-id' }, + }; + + mockEntityManager.find.mockResolvedValue([{ id: 'test-id' }]); + + await repository.find(options); + + expect(mockEntityManager.find).toHaveBeenCalledWith( + 'test-entity', + { where: { id: 'test-id' } }, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + + it('should delegate to workspaceEntityManager findBy', async () => { + const where: FindOptionsWhere = { id: 'test-id' }; + + mockEntityManager.findBy.mockResolvedValue([{ id: 'test-id' }]); + + await repository.findBy(where); + + expect(mockEntityManager.findBy).toHaveBeenCalledWith( + 'test-entity', + { id: 'test-id' }, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + + it('should delegate to workspaceEntityManager findAndCount', async () => { + const options: FindManyOptions = { + where: { id: 'test-id' }, + }; + + mockEntityManager.findAndCount.mockResolvedValue([ + [{ id: 'test-id' }], + 1, + ]); + + await repository.findAndCount(options); + + expect(mockEntityManager.findAndCount).toHaveBeenCalledWith( + 'test-entity', + { where: { id: 'test-id' } }, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + + it('should delegate to workspaceEntityManager findOne', async () => { + const options: FindOneOptions = { + where: { id: 'test-id' }, + }; + + mockEntityManager.findOne.mockResolvedValue({ id: 'test-id' }); + + await repository.findOne(options); + + expect(mockEntityManager.findOne).toHaveBeenCalledWith( + 'test-entity', + { where: { id: 'test-id' } }, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + }); + + describe('Save Methods', () => { + it('should delegate to workspaceEntityManager save', async () => { + const entity: DeepPartial = { id: 'test-id' }; + + mockEntityManager.save.mockResolvedValue({ id: 'test-id' }); + + await repository.save(entity); + + expect(mockEntityManager.save).toHaveBeenCalledWith( + 'test-entity', + { id: 'test-id' }, + undefined, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + }); + + describe('Remove Methods', () => { + it('should delegate to workspaceEntityManager remove', async () => { + const entity: ObjectLiteral = { id: 'test-id' }; + const expectedResult = [{ id: 'test-id' }]; + + mockEntityManager.remove.mockResolvedValue(expectedResult); + + const result = await repository.remove(entity); + + expect(mockEntityManager.remove).toHaveBeenCalledWith( + 'test-entity', + { id: 'test-id' }, + undefined, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + expect(result).toEqual(expectedResult); + }); + + it('should delegate to workspaceEntityManager delete', async () => { + const criteria: FindOptionsWhere = { id: 'test-id' }; + const expectedResult = { affected: 1, raw: [] }; + + mockEntityManager.delete.mockResolvedValue(expectedResult); + + const result = await repository.delete(criteria); + + expect(mockEntityManager.delete).toHaveBeenCalledWith( + 'test-entity', + { id: 'test-id' }, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + expect(result).toEqual(expectedResult); + }); + }); + + describe('Insert Methods', () => { + it('should delegate to workspaceEntityManager insert', async () => { + const entity: DeepPartial = { id: 'test-id' }; + + mockEntityManager.insert.mockResolvedValue({ + identifiers: [{ id: 'test-id' }], + generatedMaps: [{ id: 'test-id' }], + raw: [], + }); + + await repository.insert(entity); + + expect(mockEntityManager.insert).toHaveBeenCalledWith( + 'test-entity', + { id: 'test-id' }, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + + it('should delegate to workspaceEntityManager upsert', async () => { + const entity: DeepPartial = { id: 'test-id' }; + + mockEntityManager.upsert.mockResolvedValue({ + identifiers: [{ id: 'test-id' }], + generatedMaps: [{ id: 'test-id' }], + raw: [], + }); + + await repository.upsert(entity, ['id']); + + expect(mockEntityManager.upsert).toHaveBeenCalledWith( + 'test-entity', + { id: 'test-id' }, + ['id'], + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + }); + + describe('Update Methods', () => { + it('should delegate to workspaceEntityManager update', async () => { + const criteria: FindOptionsWhere = { id: 'test-id' }; + const partialEntity: DeepPartial = { name: 'test' }; + + mockEntityManager.update.mockResolvedValue({ + affected: 1, + raw: [], + generatedMaps: [], + }); + + await repository.update(criteria, partialEntity); + + expect(mockEntityManager.update).toHaveBeenCalledWith( + 'test-entity', + { id: 'test-id' }, + { name: 'test' }, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + }); + + describe('Math Methods', () => { + it('should delegate to workspaceEntityManager sum', async () => { + const where: FindOptionsWhere = { id: 'test-id' }; + + mockEntityManager.sum.mockResolvedValue(100); + + await repository.sum('testColumn', where); + + expect(mockEntityManager.sum).toHaveBeenCalledWith( + 'test-entity', + 'testColumn', + { id: 'test-id' }, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + + it('should delegate to workspaceEntityManager increment', async () => { + const conditions: FindOptionsWhere = { id: 'test-id' }; + + mockEntityManager.increment.mockResolvedValue({ + affected: 1, + raw: [], + generatedMaps: [], + }); + + await repository.increment(conditions, 'testColumn', 1); + + expect(mockEntityManager.increment).toHaveBeenCalledWith( + 'test-entity', + { id: 'test-id' }, + 'testColumn', + 1, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + }); + + describe('Preload and Clear Methods', () => { + it('should delegate to workspaceEntityManager preload', async () => { + const entityLike: DeepPartial = { id: 'test-id' }; + + mockEntityManager.preload.mockResolvedValue({ id: 'test-id' }); + + await repository.preload(entityLike); + + expect(mockEntityManager.preload).toHaveBeenCalledWith( + 'test-entity', + { id: 'test-id' }, + { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }, + ); + }); + + it('should delegate to workspaceEntityManager clear', async () => { + mockEntityManager.clear.mockResolvedValue(undefined); + + await repository.clear(); + + expect(mockEntityManager.clear).toHaveBeenCalledWith('test-entity', { + shouldBypassPermissionChecks: false, + objectRecordsPermissions: mockObjectRecordsPermissions, + }); + }); + }); + + describe('Restricted Methods', () => { + it('should throw error for query', async () => { + await expect(repository.query()).rejects.toThrow('Method not allowed.'); + }); + + it('should throw error for findByIds', async () => { + await expect(repository.findByIds()).rejects.toThrow( + 'findByIds is deprecated. Please use findBy with In operator instead.', + ); + }); + + it('should throw error for findOneById', async () => { + await expect(repository.findOneById()).rejects.toThrow( + 'findOneById is deprecated. Please use findOneBy with id condition instead.', + ); + }); + + it('should throw error for exist', async () => { + await expect(repository.exist()).rejects.toThrow( + 'exist is deprecated. Please use exists method instead.', + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index e60a05889..ad4c84460 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -94,7 +94,15 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; const computedOptions = await this.transformOptions(options); - const result = await manager.find(this.target, computedOptions); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + const result = await manager.find( + this.target, + computedOptions, + permissionOptions, + ); const formattedResult = await this.formatResult(result); return formattedResult; @@ -106,7 +114,15 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); - const result = await manager.findBy(this.target, computedOptions.where); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + const result = await manager.findBy( + this.target, + computedOptions.where, + permissionOptions, + ); const formattedResult = await this.formatResult(result); return formattedResult; @@ -118,7 +134,15 @@ export class WorkspaceRepository< ): Promise<[T[], number]> { const manager = entityManager || this.manager; const computedOptions = await this.transformOptions(options); - const result = await manager.findAndCount(this.target, computedOptions); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + const result = await manager.findAndCount( + this.target, + computedOptions, + permissionOptions, + ); const formattedResult = await this.formatResult(result); return formattedResult; @@ -130,9 +154,14 @@ export class WorkspaceRepository< ): Promise<[T[], number]> { const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; const result = await manager.findAndCountBy( this.target, computedOptions.where, + permissionOptions, ); const formattedResult = await this.formatResult(result); @@ -145,7 +174,15 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; const computedOptions = await this.transformOptions(options); - const result = await manager.findOne(this.target, computedOptions); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + const result = await manager.findOne( + this.target, + computedOptions, + permissionOptions, + ); const formattedResult = await this.formatResult(result); return formattedResult; @@ -157,7 +194,15 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); - const result = await manager.findOneBy(this.target, computedOptions.where); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + const result = await manager.findOneBy( + this.target, + computedOptions.where, + permissionOptions, + ); const formattedResult = await this.formatResult(result); return formattedResult; @@ -169,7 +214,15 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; const computedOptions = await this.transformOptions(options); - const result = await manager.findOneOrFail(this.target, computedOptions); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + const result = await manager.findOneOrFail( + this.target, + computedOptions, + permissionOptions, + ); const formattedResult = await this.formatResult(result); return formattedResult; @@ -181,9 +234,14 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; const result = await manager.findOneByOrFail( this.target, computedOptions.where, + permissionOptions, ); const formattedResult = await this.formatResult(result); @@ -219,25 +277,32 @@ export class WorkspaceRepository< override async save>( entityOrEntities: U | U[], - options?: SaveOptions, + options?: SaveOptions | (SaveOptions & { reload: false }), entityManager?: WorkspaceEntityManager, ): Promise { const manager = entityManager || this.manager; const formattedEntityOrEntities = await this.formatData(entityOrEntities); let result: U | U[]; + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + // Needed because save method has multiple signature, otherwise we will need to do a type assertion if (Array.isArray(formattedEntityOrEntities)) { result = await manager.save( this.target, formattedEntityOrEntities, options, + permissionOptions, ); } else { result = await manager.save( this.target, formattedEntityOrEntities, options, + permissionOptions, ); } @@ -268,10 +333,15 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; const formattedEntityOrEntities = await this.formatData(entityOrEntities); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; const result = await manager.remove( this.target, formattedEntityOrEntities, options, + permissionOptions, ); const formattedResult = await this.formatResult(result); @@ -298,7 +368,12 @@ export class WorkspaceRepository< criteria = await this.transformOptions(criteria); } - return manager.delete(this.target, criteria); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.delete(this.target, criteria, permissionOptions); } override softRemove>( @@ -332,20 +407,26 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; const formattedEntityOrEntities = await this.formatData(entityOrEntities); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; let result: U | U[]; - // Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion + // Needed because save method has multiple signature, otherwise we will need to do a type assertion if (Array.isArray(formattedEntityOrEntities)) { result = await manager.softRemove( this.target, formattedEntityOrEntities, options, + permissionOptions, ); } else { result = await manager.softRemove( this.target, formattedEntityOrEntities, options, + permissionOptions, ); } @@ -373,7 +454,12 @@ export class WorkspaceRepository< criteria = await this.transformOptions(criteria); } - return manager.softDelete(this.target, criteria); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.softDelete(this.target, criteria, permissionOptions); } /** @@ -410,20 +496,26 @@ export class WorkspaceRepository< ): Promise { const manager = entityManager || this.manager; const formattedEntityOrEntities = await this.formatData(entityOrEntities); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; let result: U | U[]; - // Needed becasuse save method has multiple signature, otherwise we will need to do a type assertion + // Needed because save method has multiple signature, otherwise we will need to do a type assertion if (Array.isArray(formattedEntityOrEntities)) { result = await manager.recover( this.target, formattedEntityOrEntities, options, + permissionOptions, ); } else { result = await manager.recover( this.target, formattedEntityOrEntities, options, + permissionOptions, ); } @@ -451,7 +543,12 @@ export class WorkspaceRepository< criteria = await this.transformOptions(criteria); } - return manager.restore(this.target, criteria); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.restore(this.target, criteria, permissionOptions); } /** @@ -464,10 +561,15 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; const formattedEntity = await this.formatData(entity); - const result = await manager.insert(this.target, formattedEntity, { + const permissionOptions = { shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, objectRecordsPermissions: this.objectRecordsPermissions, - }); + }; + const result = await manager.insert( + this.target, + formattedEntity, + permissionOptions, + ); const formattedResult = await this.formatResult(result.generatedMaps); return { @@ -500,7 +602,17 @@ export class WorkspaceRepository< criteria = await this.transformOptions(criteria); } - return manager.update(this.target, criteria, partialEntity); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.update( + this.target, + criteria, + partialEntity, + permissionOptions, + ); } override async upsert( @@ -512,14 +624,16 @@ export class WorkspaceRepository< const formattedEntityOrEntities = await this.formatData(entityOrEntities); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + const result = await manager.upsert( this.target, formattedEntityOrEntities, conflictPathsOrOptions, - { - shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, - objectRecordsPermissions: this.objectRecordsPermissions, - }, + permissionOptions, ); const formattedResult = await this.formatResult(result.generatedMaps); @@ -541,7 +655,12 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; const computedOptions = await this.transformOptions(options); - return manager.exists(this.target, computedOptions); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.exists(this.target, computedOptions, permissionOptions); } override async existsBy( @@ -551,7 +670,16 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); - return manager.existsBy(this.target, computedOptions.where); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.existsBy( + this.target, + computedOptions.where, + permissionOptions, + ); } /** @@ -564,7 +692,12 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; const computedOptions = await this.transformOptions(options); - return manager.count(this.target, computedOptions); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.count(this.target, computedOptions, permissionOptions); } override async countBy( @@ -574,7 +707,16 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); - return manager.countBy(this.target, computedOptions.where); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.countBy( + this.target, + computedOptions.where, + permissionOptions, + ); } /** @@ -588,7 +730,17 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); - return manager.sum(this.target, columnName, computedOptions.where); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.sum( + this.target, + columnName, + computedOptions.where, + permissionOptions, + ); } override async average( @@ -599,7 +751,17 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); - return manager.average(this.target, columnName, computedOptions.where); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.average( + this.target, + columnName, + computedOptions.where, + permissionOptions, + ); } override async minimum( @@ -610,7 +772,17 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); - return manager.minimum(this.target, columnName, computedOptions.where); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.minimum( + this.target, + columnName, + computedOptions.where, + permissionOptions, + ); } override async maximum( @@ -621,7 +793,17 @@ export class WorkspaceRepository< const manager = entityManager || this.manager; const computedOptions = await this.transformOptions({ where }); - return manager.maximum(this.target, columnName, computedOptions.where); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.maximum( + this.target, + columnName, + computedOptions.where, + permissionOptions, + ); } override async increment( @@ -635,11 +817,17 @@ export class WorkspaceRepository< where: conditions, }); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + return manager.increment( this.target, computedConditions.where, propertyPath, value, + permissionOptions, ); } @@ -654,14 +842,73 @@ export class WorkspaceRepository< where: conditions, }); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + return manager.decrement( this.target, computedConditions.where, propertyPath, value, + permissionOptions, ); } + /** + * PRELOAD METHOD + */ + override async preload>( + entityLike: U, + entityManager?: WorkspaceEntityManager, + ): Promise { + const manager = entityManager || this.manager; + const formattedEntityLike = await this.formatData(entityLike); + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.preload(this.target, formattedEntityLike, permissionOptions); + } + + /** + * CLEAR METHOD + */ + override async clear(entityManager?: WorkspaceEntityManager): Promise { + const manager = entityManager || this.manager; + const permissionOptions = { + shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, + objectRecordsPermissions: this.objectRecordsPermissions, + }; + + return manager.clear(this.target, permissionOptions); + } + + /** + * DEPRECATED AND RESTRICTED METHODS + */ + override async query(): Promise { + throw new Error('Method not allowed.'); + } + + override async findByIds(): Promise { + throw new Error( + 'findByIds is deprecated. Please use findBy with In operator instead.', + ); + } + + override async findOneById(): Promise { + throw new Error( + 'findOneById is deprecated. Please use findOneBy with id condition instead.', + ); + } + + override async exist(): Promise { + throw new Error('exist is deprecated. Please use exists method instead.'); + } + /** * PRIVATE METHODS */