Migrate role and role target to v2 (#16009)

# Introduction
close https://github.com/twentyhq/core-team-issues/issues/1930
close https://github.com/twentyhq/core-team-issues/issues/1929
Migrating role and roleTarget entities to the v2 core engine, allowing
v2 caching leverage and allow migrating agent to v2 that needs role
target in prior
After agent we should be able to pass twenty standard app totally though
workspace migration

## Role target assignation
Please note that role target have 3 creation entrypoints:
- Agent
- User workspace
- ApiKey

Refactored all 3 of them to pass through a new role-target.service.ts
that consumes the v2 under the hood.

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Paul Rastoin
2025-11-28 18:06:11 +01:00
committed by GitHub
parent 23a7611aac
commit ea3c5d2d45
131 changed files with 6082 additions and 1319 deletions
@@ -23,6 +23,8 @@ export const useMetadataErrorHandler = () => {
cronTrigger: t`cron trigger`,
databaseEventTrigger: t`database trigger`,
routeTrigger: t`route trigger`,
role: t`role`,
roleTarget: t`role target`,
} as const satisfies Record<AllMetadataName, string>;
const handleMetadataError = useCallback(
@@ -0,0 +1,35 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class SyncableRoleTarget1763896975223 implements MigrationInterface {
name = 'SyncableRoleTarget1763896975223';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" ADD "universalIdentifier" uuid`,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" ADD "applicationId" uuid`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_3e571e80f99488686015f3d00c" ON "core"."roleTargets" ("workspaceId", "universalIdentifier") `,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "FK_d4fcfdc3cd562a3e81fa9f0dae5" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "FK_d4fcfdc3cd562a3e81fa9f0dae5"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_3e571e80f99488686015f3d00c"`,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" DROP COLUMN "applicationId"`,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" DROP COLUMN "universalIdentifier"`,
);
}
}
@@ -0,0 +1,37 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class UpdateRoleTargetsUniqueConstraint1764329720503
implements MigrationInterface
{
name = 'UpdateRoleTargetsUniqueConstraint1764329720503';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE"`,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_API_KEY" UNIQUE ("workspaceId", "apiKeyId")`,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_AGENT" UNIQUE ("workspaceId", "agentId")`,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_USER_WORKSPACE" UNIQUE ("workspaceId", "userWorkspaceId")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_USER_WORKSPACE"`,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_AGENT"`,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" DROP CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE_API_KEY"`,
);
await queryRunner.query(
`ALTER TABLE "core"."roleTargets" ADD CONSTRAINT "IDX_ROLE_TARGETS_UNIQUE" UNIQUE ("workspaceId", "userWorkspaceId", "agentId", "apiKeyId")`,
);
}
}
@@ -9,6 +9,7 @@ import {
ApiKeyExceptionCode,
} from 'src/engine/core-modules/api-key/api-key.exception';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
@@ -23,6 +24,7 @@ describe('ApiKeyRoleService', () => {
let mockApiKeyRepository: any;
let mockDataSource: any;
let mockWorkspacePermissionsCacheService: any;
let mockRoleTargetService: any;
const mockWorkspaceId = 'workspace-123';
const mockApiKeyId = 'api-key-456';
@@ -77,12 +79,6 @@ describe('ApiKeyRoleService', () => {
apiKey: mockApiKey,
} as RoleTargetsEntity;
const mockNewRoleTarget = {
...mockRoleTarget,
id: 'role-target-456',
roleId: mockNewRoleId,
} as RoleTargetsEntity;
beforeEach(async () => {
mockRoleTargetsRepository = {
save: jest.fn(),
@@ -112,6 +108,11 @@ describe('ApiKeyRoleService', () => {
getApiKeyRoleMapFromCache: jest.fn(),
};
mockRoleTargetService = {
create: jest.fn(),
delete: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
ApiKeyRoleService,
@@ -139,6 +140,10 @@ describe('ApiKeyRoleService', () => {
provide: WorkspacePermissionsCacheService,
useValue: mockWorkspacePermissionsCacheService,
},
{
provide: RoleTargetService,
useValue: mockRoleTargetService,
},
],
}).compile();
@@ -153,84 +158,12 @@ describe('ApiKeyRoleService', () => {
expect(service).toBeDefined();
});
describe('assignRoleToApiKeyWithManager', () => {
it('should assign role using provided transaction manager', async () => {
const mockManagerDelete = jest.fn().mockResolvedValue({ affected: 1 });
const mockManagerCreate = jest.fn().mockReturnValue(mockNewRoleTarget);
const mockManagerSave = jest.fn().mockResolvedValue(mockNewRoleTarget);
const mockManager = {
delete: mockManagerDelete,
create: mockManagerCreate,
save: mockManagerSave,
};
await service.assignRoleToApiKeyWithManager(mockManager as any, {
apiKeyId: mockApiKeyId,
roleId: mockNewRoleId,
workspaceId: mockWorkspaceId,
});
expect(mockManagerDelete).toHaveBeenCalledWith(RoleTargetsEntity, {
apiKeyId: mockApiKeyId,
workspaceId: mockWorkspaceId,
});
expect(mockManagerCreate).toHaveBeenCalledWith(RoleTargetsEntity, {
apiKeyId: mockApiKeyId,
roleId: mockNewRoleId,
workspaceId: mockWorkspaceId,
});
expect(mockManagerSave).toHaveBeenCalledWith(mockNewRoleTarget);
});
it('should handle manager operation failures', async () => {
const mockManagerDelete = jest
.fn()
.mockRejectedValue(new Error('Delete failed'));
const mockManagerCreate = jest.fn();
const mockManagerSave = jest.fn();
const mockManager = {
delete: mockManagerDelete,
create: mockManagerCreate,
save: mockManagerSave,
};
await expect(
service.assignRoleToApiKeyWithManager(mockManager as any, {
apiKeyId: mockApiKeyId,
roleId: mockNewRoleId,
workspaceId: mockWorkspaceId,
}),
).rejects.toThrow('Delete failed');
expect(mockManagerDelete).toHaveBeenCalled();
expect(mockManagerCreate).not.toHaveBeenCalled();
expect(mockManagerSave).not.toHaveBeenCalled();
});
});
describe('assignRoleToApiKey', () => {
it('should assign a new role to API key using transaction', async () => {
it('should assign a new role to API key using roleTargetService', async () => {
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
mockRoleRepository.findOne.mockResolvedValue(mockNewRole);
mockRoleTargetsRepository.findOne.mockResolvedValue(null);
const mockManagerDelete = jest.fn().mockResolvedValue({ affected: 1 });
const mockManagerCreate = jest.fn().mockReturnValue(mockNewRoleTarget);
const mockManagerSave = jest.fn().mockResolvedValue(mockNewRoleTarget);
mockDataSource.transaction.mockImplementation(
async (callback: (manager: any) => Promise<any>) => {
const mockManager = {
delete: mockManagerDelete,
create: mockManagerCreate,
save: mockManagerSave,
};
return await callback(mockManager);
},
);
mockRoleTargetService.create.mockResolvedValue(undefined);
await service.assignRoleToApiKey({
apiKeyId: mockApiKeyId,
@@ -238,20 +171,12 @@ describe('ApiKeyRoleService', () => {
workspaceId: mockWorkspaceId,
});
expect(mockDataSource.transaction).toHaveBeenCalled();
expect(mockManagerDelete).toHaveBeenCalledWith(RoleTargetsEntity, {
apiKeyId: mockApiKeyId,
workspaceId: mockWorkspaceId,
});
expect(mockManagerCreate).toHaveBeenCalledWith(RoleTargetsEntity, {
apiKeyId: mockApiKeyId,
roleId: mockNewRoleId,
workspaceId: mockWorkspaceId,
});
expect(mockManagerSave).toHaveBeenCalledWith(mockNewRoleTarget);
expect(
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache,
).toHaveBeenCalledWith({
expect(mockRoleTargetService.create).toHaveBeenCalledWith({
createRoleTargetInput: {
roleId: mockNewRoleId,
targetId: mockApiKeyId,
targetMetadataForeignKey: 'apiKeyId',
},
workspaceId: mockWorkspaceId,
});
});
@@ -267,10 +192,7 @@ describe('ApiKeyRoleService', () => {
workspaceId: mockWorkspaceId,
});
expect(mockDataSource.transaction).not.toHaveBeenCalled();
expect(
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache,
).not.toHaveBeenCalled();
expect(mockRoleTargetService.create).not.toHaveBeenCalled();
});
it('should throw exception if API key not found', async () => {
@@ -365,18 +287,6 @@ describe('ApiKeyRoleService', () => {
});
});
describe('recomputeCache', () => {
it('should trigger cache recomputation', async () => {
await service.recomputeCache(mockWorkspaceId);
expect(
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache,
).toHaveBeenCalledWith({
workspaceId: mockWorkspaceId,
});
});
});
describe('getRolesByApiKeys', () => {
it('should return empty map for empty API key IDs', async () => {
const result = await service.getRolesByApiKeys({
@@ -498,13 +408,13 @@ describe('ApiKeyRoleService', () => {
});
describe('error handling', () => {
it('should handle transaction failures gracefully', async () => {
it('should handle roleTargetService failures gracefully', async () => {
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
mockRoleRepository.findOne.mockResolvedValue(mockNewRole);
mockRoleTargetsRepository.findOne.mockResolvedValue(null);
mockDataSource.transaction.mockRejectedValue(
new Error('Transaction failed'),
mockRoleTargetService.create.mockRejectedValue(
new Error('Role target creation failed'),
);
await expect(
@@ -513,41 +423,36 @@ describe('ApiKeyRoleService', () => {
roleId: mockNewRoleId,
workspaceId: mockWorkspaceId,
}),
).rejects.toThrow('Transaction failed');
expect(
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache,
).not.toHaveBeenCalled();
).rejects.toThrow('Role target creation failed');
});
it('should handle cache service failures gracefully', async () => {
it('should throw exception if role cannot be assigned to API keys', async () => {
const roleNotForApiKeys = {
...mockNewRole,
canBeAssignedToApiKeys: false,
};
mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey);
mockRoleRepository.findOne.mockResolvedValue(mockNewRole);
mockRoleRepository.findOne.mockResolvedValue(roleNotForApiKeys);
mockRoleTargetsRepository.findOne.mockResolvedValue(null);
mockDataSource.transaction.mockImplementation(
async (callback: (manager: any) => Promise<any>) => {
const mockManager = {
delete: jest.fn().mockResolvedValue({ affected: 1 }),
create: jest.fn().mockReturnValue(mockNewRoleTarget),
save: jest.fn().mockResolvedValue(mockNewRoleTarget),
};
return await callback(mockManager);
},
);
mockWorkspacePermissionsCacheService.recomputeApiKeyRoleMapCache.mockRejectedValue(
new Error('Cache update failed'),
);
await expect(
service.assignRoleToApiKey({
apiKeyId: mockApiKeyId,
roleId: mockNewRoleId,
workspaceId: mockWorkspaceId,
}),
).rejects.toThrow('Cache update failed');
).rejects.toThrow(ApiKeyException);
await expect(
service.assignRoleToApiKey({
apiKeyId: mockApiKeyId,
roleId: mockNewRoleId,
workspaceId: mockWorkspaceId,
}),
).rejects.toMatchObject({
code: ApiKeyExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS,
});
});
});
});
@@ -1,20 +1,14 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import {
DataSource,
type EntityManager,
In,
IsNull,
Not,
Repository,
} from 'typeorm';
import { In, IsNull, Not, Repository } from 'typeorm';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import {
ApiKeyException,
ApiKeyExceptionCode,
} from 'src/engine/core-modules/api-key/api-key.exception';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -32,8 +26,7 @@ export class ApiKeyRoleService {
@InjectRepository(ApiKeyEntity)
private readonly apiKeyRepository: Repository<ApiKeyEntity>,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
@InjectDataSource()
private readonly dataSource: DataSource,
private readonly roleTargetService: RoleTargetService,
) {}
public async assignRoleToApiKey({
@@ -55,45 +48,16 @@ export class ApiKeyRoleService {
return;
}
await this.dataSource.transaction(async (manager) => {
await this.assignRoleToApiKeyWithManager(manager, {
apiKeyId,
await this.roleTargetService.create({
createRoleTargetInput: {
roleId,
workspaceId,
});
});
await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({
targetId: apiKeyId,
targetMetadataForeignKey: 'apiKeyId',
},
workspaceId,
});
}
public async assignRoleToApiKeyWithManager(
manager: EntityManager,
{
apiKeyId,
roleId,
workspaceId,
}: {
apiKeyId: string;
roleId: string;
workspaceId: string;
},
): Promise<void> {
await manager.delete(RoleTargetsEntity, {
apiKeyId,
workspaceId,
});
const roleTarget = manager.create(RoleTargetsEntity, {
apiKeyId,
roleId,
workspaceId,
});
await manager.save(roleTarget);
}
async getRoleIdForApiKey(
apiKeyId: string,
workspaceId: string,
@@ -115,12 +79,6 @@ export class ApiKeyRoleService {
return roleId;
}
async recomputeCache(workspaceId: string): Promise<void> {
await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({
workspaceId,
});
}
private async validateAssignRoleInput({
apiKeyId,
workspaceId,
@@ -10,6 +10,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@@ -26,11 +27,12 @@ import { ApiKeyController } from './controllers/api-key.controller';
WorkspaceEntity,
]),
JwtModule,
TokenModule,
WorkspacePermissionsCacheModule,
WorkspaceCacheStorageModule,
PermissionsModule,
FeatureFlagModule,
RoleTargetModule,
TokenModule,
PermissionsModule,
],
providers: [ApiKeyService, ApiKeyResolver, ApiKeyRoleService],
controllers: [ApiKeyController],
@@ -10,6 +10,7 @@ import {
} from 'src/engine/core-modules/api-key/api-key.exception';
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { ApiKeyEntity } from './api-key.entity';
@@ -21,6 +22,7 @@ describe('ApiKeyService', () => {
let mockRoleTargetsRepository: any;
let mockJwtWrapperService: any;
let mockApiKeyRoleService: any;
let mockRoleTargetService: any;
let mockDataSource: any;
const mockWorkspaceId = 'workspace-123';
@@ -75,6 +77,10 @@ describe('ApiKeyService', () => {
assignRoleToApiKeyWithManager: jest.fn(),
};
mockRoleTargetService = {
create: jest.fn(),
};
mockDataSource = {
transaction: jest.fn(),
};
@@ -90,6 +96,10 @@ describe('ApiKeyService', () => {
provide: JwtWrapperService,
useValue: mockJwtWrapperService,
},
{
provide: RoleTargetService,
useValue: mockRoleTargetService,
},
{
provide: getRepositoryToken(RoleTargetsEntity),
useValue: mockRoleTargetsRepository,
@@ -117,7 +127,7 @@ describe('ApiKeyService', () => {
});
describe('create', () => {
it('should create and save an API key using transaction', async () => {
it('should create and save an API key and assign role', async () => {
const apiKeyData = {
name: 'New API Key',
expiresAt: new Date('2025-12-31'),
@@ -131,50 +141,26 @@ describe('ApiKeyService', () => {
workspaceId: mockWorkspaceId,
};
mockApiKeyRoleService.assignRoleToApiKeyWithManager.mockResolvedValue(
undefined,
);
mockApiKeyRoleService.recomputeCache.mockResolvedValue(undefined);
const mockManagerCreate = jest.fn().mockReturnValue(mockApiKey);
const mockManagerSave = jest.fn().mockResolvedValue(mockApiKey);
mockDataSource.transaction.mockImplementation(
async (callback: (manager: any) => Promise<any>) => {
const mockManager = {
create: mockManagerCreate,
save: mockManagerSave,
};
return await callback(mockManager);
},
);
mockApiKeyRepository.save.mockResolvedValue(mockApiKey);
mockRoleTargetService.create.mockResolvedValue(undefined);
const result = await service.create(apiKeyData);
expect(mockDataSource.transaction).toHaveBeenCalled();
expect(mockManagerCreate).toHaveBeenCalledWith(
ApiKeyEntity,
expect(mockApiKeyRepository.save).toHaveBeenCalledWith(
expectedApiKeyFields,
);
expect(mockManagerSave).toHaveBeenCalledWith(mockApiKey);
expect(
mockApiKeyRoleService.assignRoleToApiKeyWithManager,
).toHaveBeenCalledWith(
expect.any(Object), // manager
{
apiKeyId: mockApiKey.id,
expect(mockRoleTargetService.create).toHaveBeenCalledWith({
createRoleTargetInput: {
roleId: 'mock-role-id',
workspaceId: mockWorkspaceId,
targetId: mockApiKey.id,
targetMetadataForeignKey: 'apiKeyId',
},
);
expect(mockApiKeyRoleService.recomputeCache).toHaveBeenCalledWith(
mockWorkspaceId,
);
workspaceId: mockWorkspaceId,
});
expect(result).toEqual(mockApiKey);
});
it('should handle role assignment failures within transaction', async () => {
it('should delete API key if role assignment fails', async () => {
const apiKeyData = {
name: 'New API Key',
expiresAt: new Date('2025-12-31'),
@@ -182,38 +168,22 @@ describe('ApiKeyService', () => {
roleId: 'mock-role-id',
};
const mockManagerCreate = jest.fn().mockReturnValue(mockApiKey);
const mockManagerSave = jest.fn().mockResolvedValue(mockApiKey);
mockApiKeyRoleService.assignRoleToApiKeyWithManager.mockRejectedValue(
mockApiKeyRepository.save.mockResolvedValue(mockApiKey);
mockApiKeyRepository.delete = jest.fn().mockResolvedValue(undefined);
mockRoleTargetService.create.mockRejectedValue(
new Error('Role assignment failed'),
);
mockDataSource.transaction.mockImplementation(
async (callback: (manager: any) => Promise<any>) => {
const mockManager = {
create: mockManagerCreate,
save: mockManagerSave,
};
return await callback(mockManager);
},
);
await expect(service.create(apiKeyData)).rejects.toThrow(
'Role assignment failed',
);
expect(mockDataSource.transaction).toHaveBeenCalled();
expect(mockManagerCreate).toHaveBeenCalled();
expect(mockManagerSave).toHaveBeenCalled();
expect(
mockApiKeyRoleService.assignRoleToApiKeyWithManager,
).toHaveBeenCalled();
expect(mockApiKeyRoleService.recomputeCache).not.toHaveBeenCalled();
expect(mockApiKeyRepository.save).toHaveBeenCalled();
expect(mockRoleTargetService.create).toHaveBeenCalled();
expect(mockApiKeyRepository.delete).toHaveBeenCalledWith(mockApiKey.id);
});
it('should handle transaction failures gracefully', async () => {
it('should handle save failures gracefully', async () => {
const apiKeyData = {
name: 'New API Key',
expiresAt: new Date('2025-12-31'),
@@ -221,19 +191,12 @@ describe('ApiKeyService', () => {
roleId: 'mock-role-id',
};
mockDataSource.transaction.mockRejectedValue(
new Error('Transaction failed'),
);
mockApiKeyRepository.save.mockRejectedValue(new Error('Save failed'));
await expect(service.create(apiKeyData)).rejects.toThrow(
'Transaction failed',
);
await expect(service.create(apiKeyData)).rejects.toThrow('Save failed');
expect(mockDataSource.transaction).toHaveBeenCalled();
expect(
mockApiKeyRoleService.assignRoleToApiKeyWithManager,
).not.toHaveBeenCalled();
expect(mockApiKeyRoleService.recomputeCache).not.toHaveBeenCalled();
expect(mockApiKeyRepository.save).toHaveBeenCalled();
expect(mockRoleTargetService.create).not.toHaveBeenCalled();
});
});
@@ -1,11 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { DataSource, IsNull, Repository } from 'typeorm';
import { IsNull, Repository } from 'typeorm';
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import {
ApiKeyException,
@@ -14,6 +13,7 @@ import {
import { type ApiKeyToken } from 'src/engine/core-modules/auth/dto/api-key-token.dto';
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
@Injectable()
export class ApiKeyService {
@@ -21,34 +21,30 @@ export class ApiKeyService {
@InjectRepository(ApiKeyEntity)
private readonly apiKeyRepository: Repository<ApiKeyEntity>,
private readonly jwtWrapperService: JwtWrapperService,
private readonly apiKeyRoleService: ApiKeyRoleService,
@InjectDataSource()
private readonly dataSource: DataSource,
private readonly roleTargetService: RoleTargetService,
) {}
async create(
apiKeyData: Partial<ApiKeyEntity> & { roleId: string },
): Promise<ApiKeyEntity> {
const { roleId, ...apiKeyFields } = apiKeyData;
const savedApiKey = await this.apiKeyRepository.save(apiKeyFields);
return await this.dataSource
.transaction(async (manager) => {
const apiKey = manager.create(ApiKeyEntity, apiKeyFields);
const savedApiKey = await manager.save(apiKey);
await this.apiKeyRoleService.assignRoleToApiKeyWithManager(manager, {
apiKeyId: savedApiKey.id,
try {
await this.roleTargetService.create({
createRoleTargetInput: {
roleId,
workspaceId: savedApiKey.workspaceId,
});
return savedApiKey;
})
.then(async (savedApiKey) => {
await this.apiKeyRoleService.recomputeCache(savedApiKey.workspaceId);
return savedApiKey;
targetId: savedApiKey.id,
targetMetadataForeignKey: 'apiKeyId',
},
workspaceId: savedApiKey.workspaceId,
});
} catch (error) {
await this.apiKeyRepository.delete(savedApiKey.id);
throw error;
}
return savedApiKey;
}
async findById(
@@ -1,8 +1,8 @@
import { Module } from '@nestjs/common';
import { ApplicationResolver } from 'src/engine/core-modules/application/application.resolver';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { ApplicationResolver } from 'src/engine/core-modules/application/application.resolver';
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/applicationVariable/application-variable.module';
import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron-trigger.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@@ -14,8 +14,8 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
import { RouteTriggerModule } from 'src/engine/metadata-modules/route-trigger/route-trigger.module';
import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
@Module({
imports: [
@@ -36,7 +36,7 @@ import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadat
providers: [
ApplicationResolver,
ApplicationSyncService,
ObjectMetadataGraphqlApiExceptionInterceptor,
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
],
exports: [ApplicationSyncService],
})
@@ -15,14 +15,14 @@ import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.APPLICATIONS),
)
@Resolver()
@UseInterceptors(ObjectMetadataGraphqlApiExceptionInterceptor)
@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor)
@UseFilters(ApplicationExceptionFilter)
export class ApplicationResolver {
constructor(
@@ -430,40 +430,31 @@ describe('UserWorkspaceService', () => {
workspace.id,
);
expect(service.create).toHaveBeenCalled();
expect(service.create).toHaveBeenCalledWith(
{
workspaceId: workspace.id,
userId: user.id,
isExistingUser: true,
},
undefined,
);
expect(service.create).toHaveBeenCalledWith({
workspaceId: workspace.id,
userId: user.id,
isExistingUser: true,
});
expect(service.createWorkspaceMember).toHaveBeenCalledWith(
workspace.id,
user,
);
expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith(
{
workspaceId: workspace.id,
userWorkspaceId: userWorkspace.id,
roleId: workspace.defaultRoleId,
},
undefined,
);
expect(userRoleService.assignRoleToUserWorkspace).toHaveBeenCalledWith({
workspaceId: workspace.id,
userWorkspaceId: userWorkspace.id,
roleId: workspace.defaultRoleId,
});
expect(
workspaceInvitationService.invalidateWorkspaceInvitation,
).toHaveBeenCalledWith(workspace.id, user.email, undefined);
).toHaveBeenCalledWith(workspace.id, user.email);
expect(
onboardingService.setOnboardingCreateProfilePending,
).toHaveBeenCalledWith(
{
userId: user.id,
workspaceId: workspace.id,
value: true,
},
undefined,
);
).toHaveBeenCalledWith({
userId: user.id,
workspaceId: workspace.id,
value: true,
});
});
it('should not add user to workspace if already in workspace', async () => {
@@ -133,7 +133,6 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
async addUserToWorkspaceIfUserNotInWorkspace(
user: UserEntity,
workspace: WorkspaceEntity,
queryRunner?: QueryRunner,
) {
let userWorkspace = await this.checkUserWorkspaceExists(
user.id,
@@ -141,14 +140,11 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
);
if (!userWorkspace) {
userWorkspace = await this.create(
{
userId: user.id,
workspaceId: workspace.id,
isExistingUser: true,
},
queryRunner,
);
userWorkspace = await this.create({
userId: user.id,
workspaceId: workspace.id,
isExistingUser: true,
});
await this.createWorkspaceMember(workspace.id, user);
@@ -161,29 +157,22 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
);
}
await this.userRoleService.assignRoleToUserWorkspace(
{
workspaceId: workspace.id,
userWorkspaceId: userWorkspace.id,
roleId: defaultRoleId,
},
queryRunner,
);
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId: workspace.id,
userWorkspaceId: userWorkspace.id,
roleId: defaultRoleId,
});
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
workspace.id,
user.email,
queryRunner,
);
await this.onboardingService.setOnboardingCreateProfilePending(
{
userId: user.id,
workspaceId: workspace.id,
value: true,
},
queryRunner,
);
await this.onboardingService.setOnboardingCreateProfilePending({
userId: user.id,
workspaceId: workspace.id,
value: true,
});
}
}
@@ -10,7 +10,7 @@ import ms from 'ms';
import { SendInviteLinkEmail } from 'twenty-emails';
import { AppPath } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { type QueryRunner, IsNull, Repository } from 'typeorm';
import { IsNull, Repository } from 'typeorm';
import {
AppTokenEntity,
@@ -203,24 +203,14 @@ export class WorkspaceInvitationService {
return 'success';
}
async invalidateWorkspaceInvitation(
workspaceId: string,
email: string,
queryRunner?: QueryRunner,
) {
async invalidateWorkspaceInvitation(workspaceId: string, email: string) {
const appToken = await this.getOneWorkspaceInvitation(workspaceId, email);
if (!isDefined(appToken)) {
return;
}
if (queryRunner) {
await queryRunner.manager
.getRepository(AppTokenEntity)
.delete(appToken.id);
} else {
await this.appTokenRepository.delete(appToken.id);
}
await this.appTokenRepository.delete(appToken.id);
}
async resendWorkspaceInvitation(
@@ -74,6 +74,7 @@ import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/cons
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { fromRoleEntityToRoleDto } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { getRequest } from 'src/utils/extract-request';
@@ -238,10 +239,14 @@ export class WorkspaceResolver {
return null;
}
return await this.roleService.getRoleById(
const defaultRoleEntity = await this.roleService.getRoleById(
workspace.defaultRoleId,
workspace.id,
);
return isDefined(defaultRoleEntity)
? fromRoleEntityToRoleDto(defaultRoleEntity)
: null;
}
@ResolveField(() => String, { nullable: true })
@@ -9,6 +9,8 @@ import {
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -19,6 +21,7 @@ describe('AiAgentRoleService', () => {
let agentRepository: Repository<AgentEntity>;
let roleRepository: Repository<RoleEntity>;
let roleTargetsRepository: Repository<RoleTargetsEntity>;
let roleTargetService: RoleTargetService;
const testWorkspaceId = 'test-workspace-id';
let testAgent: AgentEntity;
@@ -52,6 +55,13 @@ describe('AiAgentRoleService', () => {
find: jest.fn(),
},
},
{
provide: RoleTargetService,
useValue: {
create: jest.fn(),
delete: jest.fn(),
},
},
],
}).compile();
@@ -65,6 +75,7 @@ describe('AiAgentRoleService', () => {
roleTargetsRepository = module.get<Repository<RoleTargetsEntity>>(
getRepositoryToken(RoleTargetsEntity),
);
roleTargetService = module.get<RoleTargetService>(RoleTargetService);
// Setup test data
testAgent = {
@@ -122,24 +133,12 @@ describe('AiAgentRoleService', () => {
describe('assignRoleToAgent', () => {
it('should successfully assign a role to an agent', async () => {
// Arrange
const newRoleTarget = {
id: 'new-role-target-id',
roleId: testRole.id,
agentId: testAgent.id,
workspaceId: testWorkspaceId,
createdAt: new Date(),
updatedAt: new Date(),
} as RoleTargetsEntity;
jest.spyOn(agentRepository, 'findOne').mockResolvedValue(testAgent);
jest.spyOn(roleRepository, 'findOne').mockResolvedValue(testRole);
jest.spyOn(roleTargetsRepository, 'findOne').mockResolvedValue(null);
jest
.spyOn(roleTargetsRepository, 'save')
.mockResolvedValue(newRoleTarget);
jest
.spyOn(roleTargetsRepository, 'delete')
.mockResolvedValue({ affected: 0 } as any);
.spyOn(roleTargetService, 'create')
.mockResolvedValue({} as FlatRoleTarget);
// Act
await service.assignRoleToAgent({
@@ -162,38 +161,24 @@ describe('AiAgentRoleService', () => {
workspaceId: testWorkspaceId,
},
});
expect(roleTargetsRepository.save).toHaveBeenCalledWith({
roleId: testRole.id,
agentId: testAgent.id,
expect(roleTargetService.create).toHaveBeenCalledWith({
createRoleTargetInput: {
roleId: testRole.id,
targetId: testAgent.id,
targetMetadataForeignKey: 'agentId',
},
workspaceId: testWorkspaceId,
});
expect(roleTargetsRepository.delete).toHaveBeenCalledWith({
agentId: testAgent.id,
workspaceId: testWorkspaceId,
id: expect.any(Object), // Not(newRoleTarget.id)
});
});
it('should replace existing role when assigning a new role to an agent', async () => {
// Arrange
const newRoleTarget = {
id: 'new-role-target-id',
roleId: testRole2.id,
agentId: testAgent.id,
workspaceId: testWorkspaceId,
createdAt: new Date(),
updatedAt: new Date(),
} as RoleTargetsEntity;
jest.spyOn(agentRepository, 'findOne').mockResolvedValue(testAgent);
jest.spyOn(roleRepository, 'findOne').mockResolvedValue(testRole2);
jest.spyOn(roleTargetsRepository, 'findOne').mockResolvedValue(null);
jest
.spyOn(roleTargetsRepository, 'save')
.mockResolvedValue(newRoleTarget);
jest
.spyOn(roleTargetsRepository, 'delete')
.mockResolvedValue({ affected: 1 } as any);
.spyOn(roleTargetService, 'create')
.mockResolvedValue({} as FlatRoleTarget);
// Act
await service.assignRoleToAgent({
@@ -203,16 +188,14 @@ describe('AiAgentRoleService', () => {
});
// Assert
expect(roleTargetsRepository.save).toHaveBeenCalledWith({
roleId: testRole2.id,
agentId: testAgent.id,
expect(roleTargetService.create).toHaveBeenCalledWith({
createRoleTargetInput: {
roleId: testRole2.id,
targetId: testAgent.id,
targetMetadataForeignKey: 'agentId',
},
workspaceId: testWorkspaceId,
});
expect(roleTargetsRepository.delete).toHaveBeenCalledWith({
agentId: testAgent.id,
workspaceId: testWorkspaceId,
id: expect.any(Object), // Not(newRoleTarget.id)
});
});
it('should not create duplicate role target when assigning the same role', async () => {
@@ -240,8 +223,7 @@ describe('AiAgentRoleService', () => {
});
// Assert
expect(roleTargetsRepository.save).not.toHaveBeenCalled();
expect(roleTargetsRepository.delete).not.toHaveBeenCalled();
expect(roleTargetService.create).not.toHaveBeenCalled();
});
it('should throw AgentException when agent does not exist', async () => {
@@ -330,9 +312,19 @@ describe('AiAgentRoleService', () => {
describe('removeRoleFromAgent', () => {
it('should successfully remove role from agent', async () => {
// Arrange
const existingRoleTarget = {
id: 'existing-role-target-id',
roleId: testRole.id,
agentId: testAgent.id,
workspaceId: testWorkspaceId,
createdAt: new Date(),
updatedAt: new Date(),
} as RoleTargetsEntity;
jest
.spyOn(roleTargetsRepository, 'delete')
.mockResolvedValue({ affected: 1 } as any);
.spyOn(roleTargetsRepository, 'findOne')
.mockResolvedValue(existingRoleTarget);
jest.spyOn(roleTargetService, 'delete').mockResolvedValue(undefined);
// Act
await service.removeRoleFromAgent({
@@ -341,29 +333,38 @@ describe('AiAgentRoleService', () => {
});
// Assert
expect(roleTargetsRepository.delete).toHaveBeenCalledWith({
agentId: testAgent.id,
expect(roleTargetsRepository.findOne).toHaveBeenCalledWith({
where: {
agentId: testAgent.id,
workspaceId: testWorkspaceId,
},
});
expect(roleTargetService.delete).toHaveBeenCalledWith({
id: existingRoleTarget.id,
workspaceId: testWorkspaceId,
});
});
it('should not throw error when removing role from agent that has no role', async () => {
it('should throw error when removing role from agent that has no role', async () => {
// Arrange
jest
.spyOn(roleTargetsRepository, 'delete')
.mockResolvedValue({ affected: 0 } as any);
jest.spyOn(roleTargetsRepository, 'findOne').mockResolvedValue(null);
// Act & Assert - Should not throw
// Act & Assert
await expect(
service.removeRoleFromAgent({
workspaceId: testWorkspaceId,
agentId: testAgent.id,
}),
).resolves.not.toThrow();
).rejects.toThrow(AgentException);
expect(roleTargetsRepository.delete).toHaveBeenCalledWith({
agentId: testAgent.id,
workspaceId: testWorkspaceId,
await expect(
service.removeRoleFromAgent({
workspaceId: testWorkspaceId,
agentId: testAgent.id,
}),
).rejects.toMatchObject({
code: AgentExceptionCode.ROLE_NOT_FOUND,
message: `Role target not found for agent ${testAgent.id}`,
});
});
});
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -10,6 +11,7 @@ import { AiAgentRoleService } from './ai-agent-role.service';
@Module({
imports: [
TypeOrmModule.forFeature([AgentEntity, RoleEntity, RoleTargetsEntity]),
RoleTargetModule,
],
providers: [AiAgentRoleService],
exports: [AiAgentRoleService],
@@ -9,6 +9,7 @@ import {
AgentExceptionCode,
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -21,6 +22,7 @@ export class AiAgentRoleService {
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
private readonly roleTargetService: RoleTargetService,
) {}
public async assignRoleToAgent({
@@ -42,17 +44,14 @@ export class AiAgentRoleService {
return;
}
const newRoleTarget = await this.roleTargetsRepository.save({
roleId,
agentId,
await this.roleTargetService.create({
createRoleTargetInput: {
roleId,
targetId: agentId,
targetMetadataForeignKey: 'agentId',
},
workspaceId,
});
await this.roleTargetsRepository.delete({
agentId,
workspaceId,
id: Not(newRoleTarget.id),
});
}
public async assignStandardRoleToAgent({
@@ -68,7 +67,7 @@ export class AiAgentRoleService {
where: { standardId: standardRoleId, workspaceId },
});
if (!role) {
if (!isDefined(role)) {
throw new AgentException(
`Standard role with standard ID ${standardRoleId} not found in workspace`,
AgentExceptionCode.ROLE_NOT_FOUND,
@@ -89,8 +88,22 @@ export class AiAgentRoleService {
workspaceId: string;
agentId: string;
}): Promise<void> {
await this.roleTargetsRepository.delete({
agentId,
const existingRoleTarget = await this.roleTargetsRepository.findOne({
where: {
agentId,
workspaceId,
},
});
if (!isDefined(existingRoleTarget)) {
throw new AgentException(
`Role target not found for agent ${agentId}`,
AgentExceptionCode.ROLE_NOT_FOUND,
);
}
await this.roleTargetService.delete({
id: existingRoleTarget.id,
workspaceId,
});
}
@@ -1,4 +1,4 @@
import { Module, forwardRef } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
@@ -9,7 +9,6 @@ import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
import { AiChatRouterModule } from 'src/engine/metadata-modules/ai/ai-chat-router/ai-chat-router.module';
@@ -54,7 +53,6 @@ import { AgentToolGeneratorService } from './services/agent-tool-generator.servi
TokenModule,
WorkspaceDomainsModule,
WorkflowToolsModule,
forwardRef(() => UserModule),
UserWorkspaceModule,
UserRoleModule,
],
@@ -5,6 +5,8 @@ import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/meta
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant';
import { FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-object-metadata/constants/flat-object-metadata-editable-properties.constant';
import { FLAT_ROLE_TARGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role-target/constants/flat-role-target-editable-properties.constant';
import { FLAT_ROLE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant';
import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant';
import { FLAT_VIEW_FILTER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-editable-properties.constant';
import { FLAT_VIEW_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-group/constants/flat-view-group-editable-properties.constant';
@@ -87,6 +89,14 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
],
propertiesToStringify: ['value'],
},
role: {
propertiesToCompare: [...FLAT_ROLE_EDITABLE_PROPERTIES],
propertiesToStringify: [],
},
roleTarget: {
propertiesToCompare: [...FLAT_ROLE_TARGET_EDITABLE_PROPERTIES],
propertiesToStringify: [],
},
} as const satisfies {
[P in AllMetadataName]: OneFlatEntityConfiguration<P>;
};
@@ -101,4 +101,11 @@ export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = {
flatEntityForeignKeyAggregator: 'viewFilterIds',
},
},
role: {},
roleTarget: {
roleId: {
metadataName: 'role',
flatEntityForeignKeyAggregator: 'roleTargetIds',
},
},
} as const satisfies MetadataNameAndRelations;
@@ -10,4 +10,6 @@ export const ALL_METADATA_NAME = {
databaseEventTrigger: 'databaseEventTrigger',
routeTrigger: 'routeTrigger',
viewFilter: 'viewFilter',
role: 'role',
roleTarget: 'roleTarget',
} as const;
@@ -49,4 +49,8 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
fieldMetadata: true,
view: true,
},
role: {},
roleTarget: {
role: true,
},
} as const satisfies MetadataRequiredForValidation;
@@ -6,12 +6,16 @@ import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-meta
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@@ -44,6 +48,16 @@ import {
type DeleteObjectAction,
type UpdateObjectAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/types/workspace-migration-object-action-v2';
import {
type CreateRoleTargetAction,
type DeleteRoleTargetAction,
type UpdateRoleTargetAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type';
import {
type CreateRoleAction,
type DeleteRoleAction,
type UpdateRoleAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type';
import {
type CreateRouteTriggerAction,
type DeleteRouteTriggerAction,
@@ -175,4 +189,22 @@ export type AllFlatEntityTypesByMetadataName = {
flatEntity: FlatViewFilter;
entity: ViewFilterEntity;
};
role: {
actions: {
created: CreateRoleAction;
updated: UpdateRoleAction;
deleted: DeleteRoleAction;
};
flatEntity: FlatRole;
entity: RoleEntity;
};
roleTarget: {
actions: {
created: CreateRoleTargetAction;
updated: UpdateRoleTargetAction;
deleted: DeleteRoleTargetAction;
};
flatEntity: FlatRoleTarget;
entity: RoleTargetsEntity;
};
};
@@ -0,0 +1,7 @@
import { ROLE_TARGET_FOREIGN_KEY_PROPERTIES } from 'src/engine/metadata-modules/flat-role-target/constants/role-target-foreign-key-properties.constant';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
export const FLAT_ROLE_TARGET_EDITABLE_PROPERTIES = [
'roleId',
...ROLE_TARGET_FOREIGN_KEY_PROPERTIES,
] as const satisfies (keyof FlatRoleTarget)[];
@@ -0,0 +1,7 @@
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
export const ROLE_TARGET_FOREIGN_KEY_PROPERTIES = [
'userWorkspaceId',
'apiKeyId',
'agentId',
] as const satisfies (keyof FlatRoleTarget)[];
@@ -0,0 +1,55 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type';
import { fromRoleTargetsEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceFlatMapCache('flatRoleTargetMaps')
export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceFlatMapCacheService<FlatRoleTargetMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
) {
super(cacheStorageService);
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatRoleTargetMaps> {
const roleTargets = await this.roleTargetsRepository.find({
where: {
workspaceId,
},
withDeleted: true,
});
const flatRoleTargetMaps = createEmptyFlatEntityMaps();
for (const roleTargetEntity of roleTargets) {
const flatRoleTarget =
fromRoleTargetsEntityToFlatRoleTarget(roleTargetEntity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatRoleTarget,
flatEntityMapsToMutate: flatRoleTargetMaps,
});
}
return flatRoleTargetMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
export type FlatRoleTargetMaps = FlatEntityMaps<FlatRoleTarget>;
@@ -0,0 +1,12 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { type RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
export const roleTargetsEntityRelationProperties = ['role', 'apiKey'] as const;
export type RoleTargetsEntityRelationProperties =
(typeof roleTargetsEntityRelationProperties)[number];
export type FlatRoleTarget = FlatEntityFrom<
RoleTargetsEntity,
RoleTargetsEntityRelationProperties
>;
@@ -0,0 +1,4 @@
import { type ROLE_TARGET_FOREIGN_KEY_PROPERTIES } from 'src/engine/metadata-modules/flat-role-target/constants/role-target-foreign-key-properties.constant';
export type RoleTargetForeignKeyProperties =
(typeof ROLE_TARGET_FOREIGN_KEY_PROPERTIES)[number];
@@ -0,0 +1,23 @@
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type RoleTargetForeignKeyProperties } from 'src/engine/metadata-modules/flat-role-target/types/role-target-foreign-key-properties.type';
type FindAllFlatRoleTargetOfArgs = {
targetMetadataForeignKey: RoleTargetForeignKeyProperties;
targetId: string;
} & Pick<AllFlatEntityMaps, 'flatRoleTargetMaps'>;
export const findFlatRoleTargetFromForeignKey = ({
flatRoleTargetMaps,
targetMetadataForeignKey,
targetId,
}: FindAllFlatRoleTargetOfArgs): FlatRoleTarget | undefined => {
const allRoleTargets = Object.values(flatRoleTargetMaps.byId).filter(
isDefined,
);
return allRoleTargets.find(
(roleTarget) => roleTarget[targetMetadataForeignKey] === targetId,
);
};
@@ -0,0 +1,19 @@
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
export const fromRoleTargetsEntityToFlatRoleTarget = (
roleTarget: RoleTargetsEntity,
): FlatRoleTarget => {
return {
id: roleTarget.id,
workspaceId: roleTarget.workspaceId,
roleId: roleTarget.roleId,
userWorkspaceId: roleTarget.userWorkspaceId,
agentId: roleTarget.agentId,
apiKeyId: roleTarget.apiKeyId,
applicationId: roleTarget.applicationId,
universalIdentifier: roleTarget.universalIdentifier ?? roleTarget.id,
createdAt: roleTarget.createdAt,
updatedAt: roleTarget.updatedAt,
};
};
@@ -0,0 +1,16 @@
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
export const FLAT_ROLE_EDITABLE_PROPERTIES: (keyof FlatRole)[] = [
'label',
'description',
'icon',
'canUpdateAllSettings',
'canAccessAllTools',
'canReadAllObjectRecords',
'canUpdateAllObjectRecords',
'canSoftDeleteAllObjectRecords',
'canDestroyAllObjectRecords',
'canBeAssignedToUsers',
'canBeAssignedToAgents',
'canBeAssignedToApiKeys',
];
@@ -0,0 +1,11 @@
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
export const FLAT_ROLE_REQUIRED_PROPERTIES = [
'label',
'canUpdateAllSettings',
'canAccessAllTools',
'canReadAllObjectRecords',
'canUpdateAllObjectRecords',
'canSoftDeleteAllObjectRecords',
'canDestroyAllObjectRecords',
] as const satisfies (keyof FlatRole)[];
@@ -1,17 +1,28 @@
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { type FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { type ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { type PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { type RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
export const roleEntityRelationProperties = [
'roleTargets',
'objectPermissions',
'permissionFlags',
'fieldPermissions',
] as const;
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
export type RoleEntityRelationProperties =
(typeof roleEntityRelationProperties)[number];
ExtractRecordTypeOrmRelationProperties<
RoleEntity,
| RoleTargetsEntity
| ObjectPermissionEntity
| PermissionFlagEntity
| FieldPermissionEntity
| ApplicationEntity
>;
export type FlatRole = FlatEntityFrom<
RoleEntity,
RoleEntityRelationProperties | 'createdAt' | 'updatedAt'
>;
RoleEntityRelationProperties
> & {
roleTargetIds: string[];
objectPermissionIds: string[];
permissionFlagIds: string[];
fieldPermissionIds: string[];
};
@@ -0,0 +1,58 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { type CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto';
export const fromCreateRoleInputToFlatRoleToCreate = ({
createRoleInput,
workspaceId,
applicationId,
}: {
createRoleInput: CreateRoleInput;
workspaceId: string;
applicationId: string;
}): FlatRole => {
const now = new Date();
const {
label,
description,
icon,
id: inputId,
} = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
createRoleInput,
['description', 'icon', 'id', 'label'],
);
const id = inputId ?? v4();
return {
id,
standardId: null,
label,
description: description ?? null,
icon: icon ?? null,
canUpdateAllSettings: createRoleInput.canUpdateAllSettings ?? false,
canAccessAllTools: createRoleInput.canAccessAllTools ?? false,
canReadAllObjectRecords: createRoleInput.canReadAllObjectRecords ?? false,
canUpdateAllObjectRecords:
createRoleInput.canUpdateAllObjectRecords ?? false,
canSoftDeleteAllObjectRecords:
createRoleInput.canSoftDeleteAllObjectRecords ?? false,
canDestroyAllObjectRecords:
createRoleInput.canDestroyAllObjectRecords ?? false,
canBeAssignedToUsers: createRoleInput.canBeAssignedToUsers ?? true,
canBeAssignedToAgents: createRoleInput.canBeAssignedToAgents ?? true,
canBeAssignedToApiKeys: createRoleInput.canBeAssignedToApiKeys ?? true,
isEditable: true,
workspaceId,
createdAt: now,
updatedAt: now,
universalIdentifier: id,
applicationId,
roleTargetIds: [],
objectPermissionIds: [],
permissionFlagIds: [],
fieldPermissionIds: [],
};
};
@@ -0,0 +1,35 @@
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
export const fromDeleteRoleInputToFlatRoleOrThrow = ({
flatRoleMaps,
roleId,
}: {
flatRoleMaps: FlatEntityMaps<FlatRole>;
roleId: string;
}): FlatRole => {
const existingFlatRole = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: roleId,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(existingFlatRole)) {
throw new PermissionsException(
'Role not found',
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The role you are looking for could not be found. It may have been deleted or you may not have access to it.`,
},
);
}
return existingFlatRole;
};
@@ -19,7 +19,13 @@ export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => {
canBeAssignedToAgents: role.canBeAssignedToAgents,
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
workspaceId: role.workspaceId,
universalIdentifier: role.standardId || role.id,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
universalIdentifier: role.universalIdentifier ?? role.standardId ?? role.id,
applicationId: role.applicationId ?? null,
roleTargetIds: role.roleTargets.map((rt) => rt.id),
objectPermissionIds: role.objectPermissions.map((op) => op.id),
permissionFlagIds: role.permissionFlags.map((pf) => pf.id),
fieldPermissionIds: role.fieldPermissions.map((fp) => fp.id),
};
};
@@ -7,10 +7,18 @@ export const fromStandardRoleDefinitionToFlatRole = (
standardRoleDefinition: StandardRoleDefinition,
workspaceId: string,
): FlatRole => {
const createdAt = new Date();
return {
...standardRoleDefinition,
id: v4(),
workspaceId,
universalIdentifier: standardRoleDefinition.standardId || v4(),
createdAt,
updatedAt: createdAt,
permissionFlagIds: [],
fieldPermissionIds: [],
objectPermissionIds: [],
roleTargetIds: [],
};
};
@@ -0,0 +1,39 @@
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
export const fromUpdateRoleInputToFlatRoleToUpdateOrThrow = ({
flatRoleMaps,
updateRoleInput,
}: {
flatRoleMaps: FlatEntityMaps<FlatRole>;
updateRoleInput: UpdateRoleInput;
}): FlatRole => {
const existingFlatRole = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updateRoleInput.id,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(existingFlatRole)) {
throw new PermissionsException(
'Role not found',
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The role you are looking for could not be found. It may have been deleted or you may not have access to it.`,
},
);
}
return {
...existingFlatRole,
...updateRoleInput.update,
};
};
@@ -7,6 +7,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
@@ -24,8 +25,9 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
TypeOrmModule.forFeature([UserWorkspaceEntity]),
UserRoleModule,
WorkspacePermissionsCacheModule,
RoleTargetModule,
],
providers: [PermissionsService, ApiKeyRoleService],
providers: [ApiKeyRoleService, PermissionsService],
exports: [PermissionsService],
})
export class PermissionsModule {}
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
import { RoleTargetService } from './services/role-target.service';
@Module({
imports: [
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationV2Module,
ApplicationModule,
],
providers: [RoleTargetService],
exports: [RoleTargetService],
})
export class RoleTargetModule {}
@@ -0,0 +1,173 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { CreateRoleTargetInput } from 'src/engine/metadata-modules/role-target/types/create-role-target.input';
import { fromCreateRoleTargetInputToFlatRoleTargetToCreate } from 'src/engine/metadata-modules/role-target/utils/from-create-role-target-input-to-flat-role-target-to-create.util';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
export type DeleteRoleTargetInput = {
id: string;
workspaceId: string;
};
export type FindRoleTargetInput = {
id: string;
workspaceId: string;
};
@Injectable()
export class RoleTargetService {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
) {}
async create({
createRoleTargetInput,
workspaceId,
}: {
createRoleTargetInput: CreateRoleTargetInput;
workspaceId: string;
}): Promise<FlatRoleTarget> {
const { flatRoleTargetMaps, flatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleTargetMaps', 'flatRoleMaps'],
},
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const { flatRoleTargetToCreate, flatRoleTargetsToDelete } =
fromCreateRoleTargetInputToFlatRoleTargetToCreate({
createRoleTargetInput: {
...createRoleTargetInput,
applicationId:
createRoleTargetInput.applicationId ??
workspaceCustomFlatApplication.id,
},
flatRoleTargetMaps,
workspaceId,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatRoleTargetMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: flatRoleTargetMaps,
flatEntityToCreate: [flatRoleTargetToCreate],
flatEntityToDelete: flatRoleTargetsToDelete,
flatEntityToUpdate: [],
}),
},
dependencyAllFlatEntityMaps: {
flatRoleMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: {
roleTarget: true,
},
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating role target',
);
}
const { flatRoleTargetMaps: recomputedFlatRoleTargetMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleTargetMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatRoleTargetToCreate.id,
flatEntityMaps: recomputedFlatRoleTargetMaps,
});
}
async delete({ id, workspaceId }: DeleteRoleTargetInput): Promise<void> {
const { flatRoleTargetMaps: existingFlatRoleTargetMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleTargetMaps'],
},
);
const roleTargetToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: existingFlatRoleTargetMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatRoleTargetMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatRoleTargetMaps,
flatEntityToCreate: [],
flatEntityToDelete: [roleTargetToDelete],
flatEntityToUpdate: [],
}),
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: {
roleTarget: true,
},
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting role target',
);
}
}
async findOne({
findRoleTargetInput,
}: {
findRoleTargetInput: FindRoleTargetInput;
}): Promise<FlatRoleTarget | null> {
const { workspaceId, id } = findRoleTargetInput;
const { flatRoleTargetMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleTargetMaps'],
},
);
const roleTarget = flatRoleTargetMaps.byId[id];
return roleTarget ?? null;
}
}
@@ -0,0 +1,9 @@
import { type RoleTargetForeignKeyProperties } from 'src/engine/metadata-modules/flat-role-target/types/role-target-foreign-key-properties.type';
export type CreateRoleTargetInput = {
roleId: string;
applicationId?: string;
universalIdentifier?: string;
targetId: string;
targetMetadataForeignKey: RoleTargetForeignKeyProperties;
};
@@ -0,0 +1,50 @@
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { findFlatRoleTargetFromForeignKey } from 'src/engine/metadata-modules/flat-role-target/utils/find-flat-role-target-from-foreign-key.util';
import { type CreateRoleTargetInput } from 'src/engine/metadata-modules/role-target/types/create-role-target.input';
export const fromCreateRoleTargetInputToFlatRoleTargetToCreate = ({
createRoleTargetInput,
workspaceId,
flatRoleTargetMaps,
}: {
createRoleTargetInput: CreateRoleTargetInput & { applicationId: string };
workspaceId: string;
} & Pick<AllFlatEntityMaps, 'flatRoleTargetMaps'>): {
flatRoleTargetToCreate: FlatRoleTarget;
flatRoleTargetsToDelete: FlatRoleTarget[];
} => {
const now = new Date();
const { roleId, targetId, targetMetadataForeignKey, universalIdentifier } =
createRoleTargetInput;
const flatRoleTargetToCreate: FlatRoleTarget = {
id: v4(),
roleId,
userWorkspaceId: null,
agentId: null,
apiKeyId: null,
createdAt: now,
updatedAt: now,
universalIdentifier: universalIdentifier ?? v4(),
workspaceId,
applicationId: createRoleTargetInput.applicationId,
[targetMetadataForeignKey]: targetId,
};
const flatRoleTargetToDelete = findFlatRoleTargetFromForeignKey({
flatRoleTargetMaps,
targetMetadataForeignKey,
targetId,
});
return {
flatRoleTargetToCreate: flatRoleTargetToCreate,
flatRoleTargetsToDelete: isDefined(flatRoleTargetToDelete)
? [flatRoleTargetToDelete]
: [],
};
};
@@ -37,10 +37,10 @@ export class RoleDTO {
label: string;
@Field({ nullable: true })
description: string;
description?: string;
@Field({ nullable: true })
icon: string;
icon?: string;
@Field({ nullable: false })
isEditable: boolean;
@@ -55,7 +55,7 @@ export class RoleDTO {
canBeAssignedToApiKeys: boolean;
@HideField()
roleTargets: Relation<RoleTargetsEntity[]>;
roleTargets?: Relation<RoleTargetsEntity[]>;
@Field(() => [WorkspaceMemberDTO], { nullable: true })
workspaceMembers?: WorkspaceMemberDTO[];
@@ -0,0 +1,16 @@
import { CustomException } from 'src/utils/custom-exception';
export class RoleTargetException extends CustomException {
code: RoleTargetExceptionCode;
constructor(message: string, code: RoleTargetExceptionCode) {
super(message, code);
}
}
export enum RoleTargetExceptionCode {
ROLE_TARGET_NOT_FOUND = 'ROLE_TARGET_NOT_FOUND',
INVALID_ROLE_TARGET_DATA = 'INVALID_ROLE_TARGET_DATA',
ROLE_TARGET_MISSING_IDENTIFIER = 'ROLE_TARGET_MISSING_IDENTIFIER',
ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY = 'ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY',
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
}
@@ -12,16 +12,18 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@Entity('roleTargets')
@Unique('IDX_ROLE_TARGETS_UNIQUE', [
@Unique('IDX_ROLE_TARGETS_UNIQUE_USER_WORKSPACE', [
'workspaceId',
'userWorkspaceId',
'roleId',
'agentId',
'apiKeyId',
])
@Unique('IDX_ROLE_TARGETS_UNIQUE_AGENT', ['workspaceId', 'agentId'])
@Unique('IDX_ROLE_TARGETS_UNIQUE_API_KEY', ['workspaceId', 'apiKeyId'])
@Index('IDX_ROLE_TARGETS_WORKSPACE_ID', ['userWorkspaceId', 'workspaceId'])
@Index('IDX_ROLE_TARGETS_AGENT_ID', ['agentId'])
@Index('IDX_ROLE_TARGETS_API_KEY_ID', ['apiKeyId'])
@@ -29,7 +31,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
'CHK_role_targets_single_entity',
'("agentId" IS NOT NULL AND "userWorkspaceId" IS NULL AND "apiKeyId" IS NULL) OR ("agentId" IS NULL AND "userWorkspaceId" IS NOT NULL AND "apiKeyId" IS NULL) OR ("agentId" IS NULL AND "userWorkspaceId" IS NULL AND "apiKeyId" IS NOT NULL)',
)
export class RoleTargetsEntity {
export class RoleTargetsEntity extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -46,13 +48,13 @@ export class RoleTargetsEntity {
role: Relation<RoleEntity>;
@Column({ nullable: true, type: 'uuid' })
userWorkspaceId: string;
userWorkspaceId: string | null;
@Column({ nullable: true, type: 'uuid' })
agentId: string;
agentId: string | null;
@Column({ nullable: true, type: 'uuid' })
apiKeyId: string;
apiKeyId: string | null;
@ManyToOne(() => ApiKeyEntity, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'apiKeyId' })
@@ -18,12 +18,12 @@ import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets
@Entity('role')
@Unique('IDX_ROLE_LABEL_WORKSPACE_ID_UNIQUE', ['label', 'workspaceId'])
export class RoleEntity extends SyncableEntity {
export class RoleEntity extends SyncableEntity implements Required<RoleEntity> {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true, type: 'uuid' })
standardId?: string;
standardId: string | null;
@Column({ nullable: false })
label: string;
@@ -47,10 +47,10 @@ export class RoleEntity extends SyncableEntity {
canDestroyAllObjectRecords: boolean;
@Column({ nullable: true, type: 'text' })
description: string;
description: string | null;
@Column({ nullable: true })
icon: string;
@Column({ nullable: true, type: 'varchar' })
icon: string | null;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@@ -6,34 +6,61 @@ import { ApplicationModule } from 'src/engine/core-modules/application/applicati
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatRoleTargetMapCacheService } from 'src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { WorkspaceFlatRoleMapCacheService } from 'src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
@Module({
imports: [
TypeOrmModule.forFeature([RoleEntity, RoleTargetsEntity]),
TypeOrmModule.forFeature([UserWorkspaceEntity, WorkspaceEntity]),
TypeOrmModule.forFeature([
RoleEntity,
RoleTargetsEntity,
ObjectPermissionEntity,
PermissionFlagEntity,
FieldPermissionEntity,
UserWorkspaceEntity,
]),
UserRoleModule,
AiAgentRoleModule,
ApplicationModule,
ApiKeyModule,
PermissionsModule,
UserWorkspaceModule,
ObjectPermissionModule,
PermissionFlagModule,
WorkspacePermissionsCacheModule,
WorkspaceCacheStorageModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationV2Module,
UserWorkspaceModule,
FileModule,
ApplicationModule,
],
providers: [RoleService, RoleResolver],
exports: [RoleService],
providers: [
RoleService,
RoleResolver,
WorkspaceFlatRoleMapCacheService,
WorkspaceFlatRoleTargetMapCacheService,
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
],
exports: [
RoleService,
WorkspaceFlatRoleMapCacheService,
WorkspaceFlatRoleTargetMapCacheService,
],
})
export class RoleModule {}
@@ -1,4 +1,9 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import {
UseFilters,
UseGuards,
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import {
Args,
Mutation,
@@ -12,6 +17,7 @@ import { msg } from '@lingui/core/macro';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -49,7 +55,9 @@ import {
} from 'src/engine/metadata-modules/role/dtos/role.dto';
import { UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { fromRoleEntitiesToRoleDtos } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Resolver(() => RoleDTO)
@@ -62,6 +70,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
PermissionsGraphqlApiExceptionFilter,
PreventNestToAutoLogGraphqlErrorsFilter,
)
@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor)
export class RoleResolver {
constructor(
private readonly userRoleService: UserRoleService,
@@ -72,13 +81,16 @@ export class RoleResolver {
private readonly agentRoleService: AiAgentRoleService,
private readonly apiKeyRoleService: ApiKeyRoleService,
private readonly fieldPermissionService: FieldPermissionService,
private readonly applicationService: ApplicationService,
) {}
@Query(() => [RoleDTO])
async getRoles(
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<RoleDTO[]> {
return this.roleService.getWorkspaceRoles(workspace.id);
const roleEntities = await this.roleService.getWorkspaceRoles(workspace.id);
return fromRoleEntitiesToRoleDtos(roleEntities);
}
@Mutation(() => WorkspaceMemberDTO)
@@ -141,9 +153,18 @@ export class RoleResolver {
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('createRoleInput') createRoleInput: CreateRoleInput,
): Promise<RoleDTO> {
const { id: workspaceId } = workspace;
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
return await this.roleService.createRole({
workspaceId: workspace.id,
workspaceId,
input: createRoleInput,
applicationId: workspaceCustomFlatApplication.id,
});
}
@@ -165,12 +186,12 @@ export class RoleResolver {
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('roleId', { type: () => UUIDScalarType }) roleId: string,
): Promise<string> {
const deletedRoleId = await this.roleService.deleteRole(
const deletedRole = await this.roleService.deleteRole({
roleId,
workspace.id,
);
workspaceId: workspace.id,
});
return deletedRoleId;
return deletedRole.id;
}
@Mutation(() => [ObjectPermissionDTO])
@@ -1,12 +1,17 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { fromCreateRoleInputToFlatRoleToCreate } from 'src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util';
import { fromDeleteRoleInputToFlatRoleOrThrow } from 'src/engine/metadata-modules/flat-role/utils/from-delete-role-input-to-flat-role-or-throw.util';
import { fromUpdateRoleInputToFlatRoleToUpdateOrThrow } from 'src/engine/metadata-modules/flat-role/utils/from-update-role-input-to-flat-role-to-update-or-throw.util';
import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants';
import {
PermissionsException,
@@ -14,24 +19,24 @@ import {
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto';
import {
type UpdateRoleInput,
type UpdateRolePayload,
} from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { type UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { fromFlatRoleToRoleDto } from 'src/engine/metadata-modules/role/utils/fromFlatRoleToRoleDto.util';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class RoleService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
private readonly userRoleService: UserRoleService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly applicationService: ApplicationService,
) {}
public async getWorkspaceRoles(workspaceId: string): Promise<RoleEntity[]> {
@@ -69,46 +74,66 @@ export class RoleService {
public async createRole({
input,
workspaceId,
applicationId,
}: {
input: CreateRoleInput;
workspaceId: string;
}): Promise<RoleEntity> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
applicationId: string;
}): Promise<RoleDTO> {
const { flatRoleMaps: existingFlatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleMaps'],
},
);
const flatRoleToCreate = fromCreateRoleInputToFlatRoleToCreate({
createRoleInput: input,
workspaceId,
applicationId,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatRoleMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatRoleMaps,
flatEntityToCreate: [flatRoleToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
}),
},
dependencyAllFlatEntityMaps: {},
buildOptions: {
isSystemBuild: false,
},
workspaceId,
},
);
await this.validateRoleInputOrThrow({ input, workspaceId });
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating role',
);
}
const id = input.id ?? v4();
const role = await this.roleRepository.save({
id,
label: input.label,
description: input.description,
icon: input.icon,
canUpdateAllSettings: input.canUpdateAllSettings,
canAccessAllTools: input.canAccessAllTools,
canReadAllObjectRecords: input.canReadAllObjectRecords,
canUpdateAllObjectRecords: input.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: input.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: input.canDestroyAllObjectRecords,
canBeAssignedToUsers: input.canBeAssignedToUsers,
canBeAssignedToAgents: input.canBeAssignedToAgents,
canBeAssignedToApiKeys: input.canBeAssignedToApiKeys,
isEditable: true,
workspaceId,
applicationId: workspaceCustomFlatApplication.id,
universalIdentifier: id,
});
const { flatRoleMaps: recomputedFlatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleMaps'],
},
);
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
roleIds: [role.id],
});
return role;
return fromFlatRoleToRoleDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatRoleToCreate.id,
flatEntityMaps: recomputedFlatRoleMaps,
}),
);
}
public async updateRole({
@@ -117,57 +142,69 @@ export class RoleService {
}: {
input: UpdateRoleInput;
workspaceId: string;
}): Promise<RoleEntity> {
await this.validateRoleIsEditableOrThrow({
roleId: input.id,
workspaceId,
});
const existingRole = await this.roleRepository.findOne({
where: {
id: input.id,
workspaceId,
},
});
if (!isDefined(existingRole)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
}): Promise<RoleDTO> {
const { flatRoleMaps: existingFlatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
userFriendlyMessage: msg`The role you are looking for could not be found. It may have been deleted or you may not have access to it.`,
workspaceId,
flatMapsKeys: ['flatRoleMaps'],
},
);
const flatRoleToUpdate = fromUpdateRoleInputToFlatRoleToUpdateOrThrow({
flatRoleMaps: existingFlatRoleMaps,
updateRoleInput: input,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatRoleMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatRoleMaps,
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatRoleToUpdate],
}),
},
dependencyAllFlatEntityMaps: {},
buildOptions: {
isSystemBuild: false,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while updating role',
);
}
await this.validateRoleInputOrThrow({
input: input.update,
workspaceId,
roleId: input.id,
});
const { flatRoleMaps: recomputedFlatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleMaps'],
},
);
const updatedRole = await this.roleRepository.save({
id: input.id,
...input.update,
});
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
roleIds: [input.id],
});
return { ...existingRole, ...updatedRole };
return fromFlatRoleToRoleDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: input.id,
flatEntityMaps: recomputedFlatRoleMaps,
}),
);
}
public async deleteRole(
roleId: string,
workspaceId: string,
): Promise<string> {
await this.validateRoleIsEditableOrThrow({
roleId,
workspaceId,
});
public async deleteRole({
roleId,
workspaceId,
}: {
roleId: string;
workspaceId: string;
}): Promise<RoleDTO> {
const workspace = await this.workspaceRepository.findOne({
where: {
id: workspaceId,
@@ -186,10 +223,15 @@ export class RoleService {
);
}
await this.validateRoleIsNotDefaultRoleOrThrow({
roleId,
defaultRoleId,
});
if (defaultRoleId === roleId) {
throw new PermissionsException(
PermissionsExceptionMessage.DEFAULT_ROLE_CANNOT_BE_DELETED,
PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED,
{
userFriendlyMessage: msg`The default role cannot be deleted as it is required for the workspace to function properly.`,
},
);
}
await this.assignDefaultRoleToMembersWithRoleToDelete({
roleId,
@@ -197,16 +239,49 @@ export class RoleService {
defaultRoleId,
});
await this.roleRepository.delete({
id: roleId,
workspaceId,
const { flatRoleMaps: existingFlatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleMaps'],
},
);
const flatRoleToDelete = fromDeleteRoleInputToFlatRoleOrThrow({
flatRoleMaps: existingFlatRoleMaps,
roleId,
});
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatRoleMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatRoleMaps,
flatEntityToCreate: [],
flatEntityToDelete: [flatRoleToDelete],
flatEntityToUpdate: [],
}),
},
dependencyAllFlatEntityMaps: {},
buildOptions: {
inferDeletionFromMissingEntities: {
role: true,
},
isSystemBuild: false,
},
workspaceId,
},
);
return roleId;
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting role',
);
}
return fromFlatRoleToRoleDto(flatRoleToDelete);
}
public async createMemberRole({
@@ -215,165 +290,55 @@ export class RoleService {
}: {
applicationId: string;
workspaceId: string;
}): Promise<RoleEntity> {
const id = v4();
return this.roleRepository.save({
label: MEMBER_ROLE_LABEL,
description: 'Member role',
icon: 'IconUser',
canUpdateAllSettings: false,
canAccessAllTools: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
isEditable: true,
workspaceId,
}): Promise<RoleDTO> {
return this.createRole({
input: {
label: MEMBER_ROLE_LABEL,
description: 'Member role',
icon: 'IconUser',
canUpdateAllSettings: false,
canAccessAllTools: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
},
applicationId,
id,
universalIdentifier: id,
workspaceId,
});
}
// Only used for dev seeding and testing
public async createGuestRole({
workspaceId,
applicationId,
}: {
workspaceId: string;
applicationId: string;
}): Promise<RoleEntity> {
const id = v4();
return this.roleRepository.save({
label: 'Guest',
description: 'Guest role',
icon: 'IconUser',
canUpdateAllSettings: false,
canAccessAllTools: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
isEditable: false,
}): Promise<RoleDTO> {
return this.createRole({
input: {
label: 'Guest',
description: 'Guest role',
icon: 'IconUser',
canUpdateAllSettings: false,
canAccessAllTools: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
},
workspaceId,
applicationId,
id,
universalIdentifier: id,
});
}
private async validateRoleInputOrThrow({
input,
workspaceId,
roleId,
}: {
input: CreateRoleInput | UpdateRolePayload;
workspaceId: string;
roleId?: string;
}): Promise<void> {
const keysToValidate = [
'label',
'canUpdateAllSettings',
'canAccessAllTools',
'canReadAllObjectRecords',
'canUpdateAllObjectRecords',
'canSoftDeleteAllObjectRecords',
'canDestroyAllObjectRecords',
];
for (const key of keysToValidate) {
try {
isArgDefinedIfProvidedOrThrow({
input,
key,
// @ts-expect-error legacy noImplicitAny
value: input[key],
});
} catch (error) {
throw new PermissionsException(
error.message,
PermissionsExceptionCode.INVALID_ARG,
{
userFriendlyMessage: msg`Some of the information provided is invalid. Please check your input and try again.`,
},
);
}
}
const workspaceRoles = await this.getWorkspaceRoles(workspaceId);
if (isDefined(input.label)) {
let rolesForLabelComparison = workspaceRoles;
if (isDefined(roleId)) {
rolesForLabelComparison = workspaceRoles.filter(
(role) => role.id !== roleId,
);
}
if (rolesForLabelComparison.some((role) => role.label === input.label)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS,
PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS,
{ userFriendlyMessage: msg`A role with this label already exists.` },
);
}
}
const existingRole = workspaceRoles.find((role) => role.id === roleId);
await this.validateRoleReadAndWirtePermissionsConsistencyOrThrow({
input,
existingRole,
});
}
private async validateRoleReadAndWirtePermissionsConsistencyOrThrow({
input,
existingRole,
}: {
input: CreateRoleInput | UpdateRolePayload;
existingRole?: RoleEntity;
}) {
const hasReadingPermissionsAfterUpdate =
input.canReadAllObjectRecords ?? existingRole?.canReadAllObjectRecords;
const hasUpdatePermissionsAfterUpdate =
input.canUpdateAllObjectRecords ??
existingRole?.canUpdateAllObjectRecords;
const hasSoftDeletePermissionsAfterUpdate =
input.canSoftDeleteAllObjectRecords ??
existingRole?.canSoftDeleteAllObjectRecords;
const hasDestroyPermissionsAfterUpdate =
input.canDestroyAllObjectRecords ??
existingRole?.canDestroyAllObjectRecords;
if (
hasReadingPermissionsAfterUpdate === false &&
(hasUpdatePermissionsAfterUpdate ||
hasSoftDeletePermissionsAfterUpdate ||
hasDestroyPermissionsAfterUpdate)
) {
throw new PermissionsException(
PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
{
userFriendlyMessage: msg`You cannot grant edit permissions without also granting read permissions. Please enable read access first.`,
},
);
}
}
// TODO: Move to migration side effect / To address for rollback of role deletion
private async assignDefaultRoleToMembersWithRoleToDelete({
roleId,
workspaceId,
@@ -399,54 +364,4 @@ export class RoleService {
),
);
}
private async getRole(
roleId: string,
workspaceId: string,
): Promise<RoleEntity | null> {
return this.roleRepository.findOne({
where: {
id: roleId,
workspaceId,
},
});
}
private async validateRoleIsEditableOrThrow({
roleId,
workspaceId,
}: {
roleId: string;
workspaceId: string;
}) {
const role = await this.getRole(roleId, workspaceId);
if (!role?.isEditable) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
{
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
},
);
}
}
private async validateRoleIsNotDefaultRoleOrThrow({
roleId,
defaultRoleId,
}: {
roleId: string;
defaultRoleId: string;
}): Promise<void> {
if (defaultRoleId === roleId) {
throw new PermissionsException(
PermissionsExceptionMessage.DEFAULT_ROLE_CANNOT_BE_DELETED,
PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED,
{
userFriendlyMessage: msg`The default role cannot be deleted as it is required for the workspace to function properly.`,
},
);
}
}
}
@@ -0,0 +1,128 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { fromRoleEntityToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceFlatMapCache('flatRoleMaps')
export class WorkspaceFlatRoleMapCacheService extends WorkspaceFlatMapCacheService<
FlatEntityMaps<FlatRole>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
) {
super(cacheStorageService);
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatRole>> {
const [
roles,
roleTargets,
objectPermissions,
permissionFlags,
fieldPermissions,
] = await Promise.all([
this.roleRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.roleTargetsRepository.find({
where: { workspaceId },
select: ['id', 'roleId'],
withDeleted: true,
}),
this.objectPermissionRepository.find({
where: { workspaceId },
select: ['id', 'roleId'],
withDeleted: true,
}),
this.permissionFlagRepository.find({
where: { workspaceId },
select: ['id', 'roleId'],
withDeleted: true,
}),
this.fieldPermissionRepository.find({
where: { workspaceId },
select: ['id', 'roleId'],
withDeleted: true,
}),
]);
const [
roleTargetsByRoleId,
objectPermissionsByRoleId,
permissionFlagsByRoleId,
fieldPermissionsByRoleId,
] = (
[
{
entities: roleTargets,
foreignKey: 'roleId',
},
{
entities: objectPermissions,
foreignKey: 'roleId',
},
{
entities: permissionFlags,
foreignKey: 'roleId',
},
{
entities: fieldPermissions,
foreignKey: 'roleId',
},
] as const
).map(regroupEntitiesByRelatedEntityId);
const flatRoleMaps = createEmptyFlatEntityMaps();
for (const roleEntity of roles) {
const flatRole = fromRoleEntityToFlatRole({
...roleEntity,
roleTargets: roleTargetsByRoleId.get(roleEntity.id) || [],
objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [],
permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [],
fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [],
} as RoleEntity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatRole,
flatEntityMapsToMutate: flatRoleMaps,
});
}
return flatRoleMaps;
}
}
@@ -0,0 +1,38 @@
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
export const fromFlatRoleToRoleDto = ({
canAccessAllTools,
canBeAssignedToAgents,
canBeAssignedToApiKeys,
canBeAssignedToUsers,
canDestroyAllObjectRecords,
canReadAllObjectRecords,
canSoftDeleteAllObjectRecords,
canUpdateAllObjectRecords,
canUpdateAllSettings,
id,
isEditable,
label,
description,
icon,
standardId,
}: FlatRole): RoleDTO => {
return {
canAccessAllTools,
canBeAssignedToAgents,
canBeAssignedToApiKeys,
canBeAssignedToUsers,
canDestroyAllObjectRecords,
canReadAllObjectRecords,
canSoftDeleteAllObjectRecords,
canUpdateAllObjectRecords,
canUpdateAllSettings,
id,
isEditable,
label,
description: description ?? undefined,
icon: icon ?? undefined,
standardId: standardId ?? undefined,
};
};
@@ -4,12 +4,12 @@ import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => {
return {
id: role.id,
standardId: role.standardId,
standardId: role.standardId ?? undefined,
label: role.label,
canUpdateAllSettings: role.canUpdateAllSettings,
canAccessAllTools: role.canAccessAllTools,
description: role.description,
icon: role.icon,
description: role.description ?? undefined,
icon: role.icon ?? undefined,
isEditable: role.isEditable,
canReadAllObjectRecords: role.canReadAllObjectRecords,
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
@@ -19,6 +19,9 @@ export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => {
canBeAssignedToAgents: role.canBeAssignedToAgents,
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
roleTargets: role.roleTargets,
permissionFlags: role.permissionFlags,
objectPermissions: role.objectPermissions,
fieldPermissions: role.fieldPermissions,
};
};
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
@@ -12,6 +13,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
TypeOrmModule.forFeature([RoleEntity, RoleTargetsEntity]),
TypeOrmModule.forFeature([UserWorkspaceEntity]),
WorkspacePermissionsCacheModule,
RoleTargetModule,
],
providers: [UserRoleService],
exports: [UserRoleService],
@@ -2,7 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type QueryRunner, In, Not, Repository } from 'typeorm';
import { In, Repository } from 'typeorm';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import {
@@ -10,6 +10,7 @@ import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
@@ -19,28 +20,24 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
export class UserRoleService {
constructor(
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly roleTargetService: RoleTargetService,
) {}
public async assignRoleToUserWorkspace(
{
workspaceId,
userWorkspaceId,
roleId,
}: {
workspaceId: string;
userWorkspaceId: string;
roleId: string;
},
queryRunner?: QueryRunner,
): Promise<void> {
public async assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId,
roleId,
}: {
workspaceId: string;
userWorkspaceId: string;
roleId: string;
}): Promise<void> {
const validationResult = await this.validateAssignRoleInput({
userWorkspaceId,
workspaceId,
@@ -51,27 +48,14 @@ export class UserRoleService {
return;
}
const roleTargetsRepo = queryRunner
? queryRunner.manager.getRepository(RoleTargetsEntity)
: this.roleTargetsRepository;
const newRoleTarget = await roleTargetsRepo.save({
roleId,
userWorkspaceId,
workspaceId,
});
await roleTargetsRepo.delete({
userWorkspaceId,
workspaceId,
id: Not(newRoleTarget.id),
});
await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache(
{
workspaceId,
await this.roleTargetService.create({
createRoleTargetInput: {
roleId,
targetId: userWorkspaceId,
targetMetadataForeignKey: 'userWorkspaceId',
},
);
workspaceId,
});
}
public async getRoleIdForUserWorkspace({
@@ -248,32 +232,6 @@ export class UserRoleService {
);
}
const role = await this.roleRepository.findOne({
where: {
id: roleId,
},
});
if (!isDefined(role)) {
throw new PermissionsException(
'Role not found',
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The role you are trying to assign could not be found. It may have been deleted.`,
},
);
}
if (!role.canBeAssignedToUsers) {
throw new PermissionsException(
`Role "${role.label}" cannot be assigned to users`,
PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS,
{
userFriendlyMessage: msg`This role cannot be assigned to users. Please select a different role.`,
},
);
}
const roles = await this.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspace.id],
workspaceId,
@@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Record } from 'cloudflare/core';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import {
type ObjectsPermissions,
type ObjectsPermissionsByRoleId,
@@ -9,7 +10,6 @@ import {
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In, IsNull, Not, Repository } from 'typeorm';
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
@@ -290,9 +290,9 @@ export class WorkspacePermissionsCacheService {
canDestroyObjectRecords: canDestroy,
restrictedFields,
};
permissionsByRoleId[role.id] = objectRecordsPermissions;
}
permissionsByRoleId[role.id] = objectRecordsPermissions;
}
return permissionsByRoleId;
@@ -328,11 +328,15 @@ export class WorkspacePermissionsCacheService {
},
});
return roleTargetsMap.reduce((acc, roleTarget) => {
acc[roleTarget.userWorkspaceId] = roleTarget.roleId;
const initialAccumulator: UserWorkspaceRoleMap = {};
return acc;
}, {} as UserWorkspaceRoleMap);
return roleTargetsMap.reduce(
(acc, roleTarget) => ({
...acc,
[roleTarget.userWorkspaceId as string]: roleTarget.roleId,
}),
initialAccumulator,
);
}
private hasWorkflowsPermissions(role: RoleEntity): boolean {
@@ -9,6 +9,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
@@ -38,6 +39,7 @@ export class DevSeederPermissionsService {
private readonly roleRepository: Repository<RoleEntity>,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly fieldPermissionService: FieldPermissionService,
private readonly roleTargetService: RoleTargetService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {}
@@ -63,28 +65,15 @@ export class DevSeederPermissionsService {
}
try {
await this.coreDataSource
.createQueryBuilder()
.insert()
.into('core.roleTargets', ['roleId', 'apiKeyId', 'workspaceId'])
.orIgnore()
.values([
{
roleId: adminRole.id,
apiKeyId: API_KEY_DATA_SEED_IDS.ID_1,
workspaceId: workspaceId,
},
])
.execute();
await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({
await this.roleTargetService.create({
createRoleTargetInput: {
roleId: adminRole.id,
targetId: API_KEY_DATA_SEED_IDS.ID_1,
targetMetadataForeignKey: 'apiKeyId',
applicationId: twentyStandardApplication.id,
},
workspaceId,
});
await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache(
{
workspaceId,
},
);
} catch (error) {
this.logger.error(
`Could not assign role to test API key: ${error.message}`,
@@ -116,8 +105,10 @@ export class DevSeederPermissionsService {
roleId: guestRole.id,
});
const limitedRole =
await this.createLimitedRoleForSeedWorkspace(workspaceId);
const limitedRole = await this.createLimitedRoleForSeedWorkspace({
workspaceId,
applicationId: twentyStandardApplication.id,
});
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
@@ -164,8 +155,15 @@ export class DevSeederPermissionsService {
}
}
private async createLimitedRoleForSeedWorkspace(workspaceId: string) {
private async createLimitedRoleForSeedWorkspace({
applicationId,
workspaceId,
}: {
workspaceId: string;
applicationId: string;
}) {
const customRole = await this.roleService.createRole({
applicationId,
workspaceId,
input: {
label: 'Object-restricted',
@@ -13,6 +13,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@@ -34,6 +35,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
TypeORMModule,
DataSourceModule,
RoleModule,
RoleTargetModule,
UserRoleModule,
ApiKeyModule,
ApplicationModule,
@@ -28,6 +28,8 @@ export const fromWorkspaceMigrationBuilderExceptionToMetadataValidationResponseE
invalidRouteTrigger: 0,
invalidFieldMetadata: 0,
invalidViewGroup: 0,
invalidRole: 0,
invalidRoleTarget: 0,
totalErrors: 0,
},
errors: translatedReport,
@@ -0,0 +1,38 @@
import {
type CallHandler,
type ExecutionContext,
Injectable,
type NestInterceptor,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { type Observable, catchError } from 'rxjs';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
@Injectable()
export class WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor
implements NestInterceptor
{
constructor(private readonly i18nService: I18nService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const gqlContext = GqlExecutionContext.create(context);
const ctx = gqlContext.getContext();
const locale = ctx.req?.locale ?? SOURCE_LOCALE;
const i18n = this.i18nService.getI18nInstance(locale);
return next.handle().pipe(
catchError((error) => {
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
workspaceMigrationBuilderExceptionV2Formatter(error, i18n);
}
throw error;
}),
);
}
}
@@ -19,6 +19,8 @@ import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 's
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
import { WorkspaceMigrationV2IndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/workspace-migration-v2-object-actions-builder.service';
import { WorkspaceMigrationV2RoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service';
import { WorkspaceMigrationV2RoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service';
import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service';
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
@@ -39,7 +41,9 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationV2DatabaseEventTriggerActionsBuilderService: WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService,
private readonly workspaceMigrationV2CronTriggerActionsBuilderService: WorkspaceMigrationV2CronTriggerActionsBuilderService,
private readonly workspaceMigrationV2RouteTriggerActionsBuilderService: WorkspaceMigrationV2RouteTriggerActionsBuilderService,
private readonly workspaceMigrationV2RoleTargetActionsBuilderService: WorkspaceMigrationV2RoleTargetActionsBuilderService,
private readonly workspaceMigrationV2FieldActionsBuilderService: WorkspaceMigrationV2FieldActionsBuilderService,
private readonly workspaceMigrationV2RoleActionsBuilderService: WorkspaceMigrationV2RoleActionsBuilderService,
) {}
private setupOptimisticCache({
@@ -125,6 +129,8 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatFieldMetadataMaps,
flatViewFilterMaps,
flatViewGroupMaps,
flatRoleMaps,
flatRoleTargetMaps,
} = fromToAllFlatEntityMaps;
if (isDefined(flatObjectMetadataMaps)) {
@@ -513,6 +519,65 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatRoleMaps)) {
const { from: fromFlatRoleMaps, to: toFlatRoleMaps } = flatRoleMaps;
const roleResult =
this.workspaceMigrationV2RoleActionsBuilderService.validateAndBuild({
from: fromFlatRoleMaps,
to: toFlatRoleMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: undefined,
workspaceId,
});
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
{
allFlatEntityMaps: optimisticAllFlatEntityMaps,
flatEntityMapsAndRelatedFlatEntityMaps:
roleResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
},
);
if (roleResult.status === 'fail') {
orchestratorFailureReport.role.push(...roleResult.errors);
} else {
orchestratorActionsReport.role = roleResult.actions;
}
}
if (isDefined(flatRoleTargetMaps)) {
const { from: fromFlatRoleTargetMaps, to: toFlatRoleTargetMaps } =
flatRoleTargetMaps;
const roleTargetResult =
this.workspaceMigrationV2RoleTargetActionsBuilderService.validateAndBuild(
{
from: fromFlatRoleTargetMaps,
to: toFlatRoleTargetMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: {
flatRoleMaps: optimisticAllFlatEntityMaps.flatRoleMaps,
},
workspaceId,
},
);
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
{
allFlatEntityMaps: optimisticAllFlatEntityMaps,
flatEntityMapsAndRelatedFlatEntityMaps:
roleTargetResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
},
);
if (roleTargetResult.status === 'fail') {
orchestratorFailureReport.roleTarget.push(...roleTargetResult.errors);
} else {
orchestratorActionsReport.roleTarget = roleTargetResult.actions;
}
}
const allErrors = Object.values(orchestratorFailureReport);
if (allErrors.some((report) => report.length > 0)) {
@@ -585,9 +650,21 @@ export class WorkspaceMigrationBuildOrchestratorService {
///
// Route triggers
...orchestratorActionsReport.routeTrigger.deleted,
...orchestratorActionsReport.routeTrigger.created,
...orchestratorActionsReport.routeTrigger.updated,
...aggregatedOrchestratorActionsReport.routeTrigger.deleted,
...aggregatedOrchestratorActionsReport.routeTrigger.created,
...aggregatedOrchestratorActionsReport.routeTrigger.updated,
///
// Roles
...aggregatedOrchestratorActionsReport.role.deleted,
...aggregatedOrchestratorActionsReport.role.created,
...aggregatedOrchestratorActionsReport.role.updated,
///
// Role targets
...aggregatedOrchestratorActionsReport.roleTarget.deleted,
...aggregatedOrchestratorActionsReport.roleTarget.created,
...aggregatedOrchestratorActionsReport.roleTarget.updated,
///
],
workspaceId,
@@ -0,0 +1,26 @@
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
export type CreateRoleTargetAction = {
type: 'create_role_target';
roleTarget: FlatRoleTarget;
};
export type UpdateRoleTargetAction = {
type: 'update_role_target';
roleTargetId: string;
updates: FlatEntityPropertiesUpdates<'roleTarget'>;
};
export type DeleteRoleTargetAction = {
type: 'delete_role_target';
roleTargetId: string;
};
export type WorkspaceMigrationRoleTargetActionV2 =
| CreateRoleTargetAction
| UpdateRoleTargetAction
| DeleteRoleTargetAction;
export type WorkspaceMigrationRoleTargetActionTypeV2 =
WorkspaceMigrationRoleTargetActionV2['type'];
@@ -0,0 +1,105 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UpdateRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type';
import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type';
import { FlatRoleTargetValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service';
@Injectable()
export class WorkspaceMigrationV2RoleTargetActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
typeof ALL_METADATA_NAME.roleTarget
> {
constructor(
private readonly flatRoleTargetValidatorService: FlatRoleTargetValidatorService,
) {
super(ALL_METADATA_NAME.roleTarget);
}
protected validateFlatEntityCreation(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.roleTarget>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.roleTarget,
'created'
> {
const validationResult =
this.flatRoleTargetValidatorService.validateFlatRoleTargetCreation(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRoleTargetToValidate } = args;
return {
status: 'success',
action: {
type: 'create_role_target',
roleTarget: flatRoleTargetToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.roleTarget>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.roleTarget,
'deleted'
> {
const validationResult =
this.flatRoleTargetValidatorService.validateFlatRoleTargetDeletion(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRoleTargetToValidate } = args;
return {
status: 'success',
action: {
type: 'delete_role_target',
roleTargetId: flatRoleTargetToValidate.id,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<typeof ALL_METADATA_NAME.roleTarget>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.roleTarget,
'updated'
> {
const validationResult =
this.flatRoleTargetValidatorService.validateFlatRoleTargetUpdate(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityId, flatEntityUpdates } = args;
const updateRoleTargetAction: UpdateRoleTargetAction = {
type: 'update_role_target',
roleTargetId: flatEntityId,
updates: flatEntityUpdates,
};
return {
status: 'success',
action: updateRoleTargetAction,
};
}
}
@@ -0,0 +1,26 @@
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
export type CreateRoleAction = {
type: 'create_role';
role: FlatRole;
};
export type UpdateRoleAction = {
type: 'update_role';
roleId: string;
updates: FlatEntityPropertiesUpdates<'role'>;
};
export type DeleteRoleAction = {
type: 'delete_role';
roleId: string;
};
export type WorkspaceMigrationRoleActionV2 =
| CreateRoleAction
| UpdateRoleAction
| DeleteRoleAction;
export type WorkspaceMigrationRoleActionTypeV2 =
WorkspaceMigrationRoleActionV2['type'];
@@ -0,0 +1,96 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UpdateRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type';
import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type';
import { FlatRoleValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service';
@Injectable()
export class WorkspaceMigrationV2RoleActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
typeof ALL_METADATA_NAME.role
> {
constructor(
private readonly flatRoleValidatorService: FlatRoleValidatorService,
) {
super(ALL_METADATA_NAME.role);
}
protected validateFlatEntityCreation(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.role>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.role, 'created'> {
const validationResult =
this.flatRoleValidatorService.validateFlatRoleCreation(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRoleToValidate } = args;
return {
status: 'success',
action: {
type: 'create_role',
role: flatRoleToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.role>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.role, 'deleted'> {
const validationResult =
this.flatRoleValidatorService.validateFlatRoleDeletion(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRoleToValidate } = args;
return {
status: 'success',
action: {
type: 'delete_role',
roleId: flatRoleToValidate.id,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<typeof ALL_METADATA_NAME.role>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.role, 'updated'> {
const validationResult =
this.flatRoleValidatorService.validateFlatRoleUpdate(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityId, flatEntityUpdates } = args;
const updateRoleAction: UpdateRoleAction = {
type: 'update_role',
roleId: flatEntityId,
updates: flatEntityUpdates,
};
return {
status: 'success',
action: updateRoleAction,
};
}
}
@@ -0,0 +1,179 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { RoleTargetExceptionCode } from 'src/engine/metadata-modules/role/exceptions/role-target.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { validateFlatRoleTargetAssignationAvailability } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-assignation-availability.util';
import { validateFlatRoleTargetTargetsOnlyOneEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-targets-only-one-entity.util';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class FlatRoleTargetValidatorService {
validateFlatRoleTargetCreation({
flatEntityToValidate: flatRoleTargetToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleTargetMaps: optimisticFlatRoleTargetMaps,
flatRoleMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.roleTarget
>): FailedFlatEntityValidation<FlatRoleTarget> {
const validationResult: FailedFlatEntityValidation<FlatRoleTarget> = {
type: 'create_role_target',
errors: [],
flatEntityMinimalInformation: {
id: flatRoleTargetToValidate.id,
},
};
const existingRoleTarget = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatRoleTargetToValidate.id,
flatEntityMaps: optimisticFlatRoleTargetMaps,
});
if (isDefined(existingRoleTarget)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA,
message: t`Role target with this id already exists`,
userFriendlyMessage: msg`Role target with this id already exists`,
});
}
validationResult.errors.push(
...validateFlatRoleTargetTargetsOnlyOneEntity({
flatRoleTarget: flatRoleTargetToValidate,
}),
);
const referencedRole = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatRoleTargetToValidate.roleId,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else {
validationResult.errors.push(
...validateFlatRoleTargetAssignationAvailability({
flatRole: referencedRole,
flatRoleTarget: flatRoleTargetToValidate,
}),
);
}
return validationResult;
}
validateFlatRoleTargetDeletion({
flatEntityToValidate: flatRoleTargetToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleTargetMaps: optimisticFlatRoleTargetMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.roleTarget
>): FailedFlatEntityValidation<FlatRoleTarget> {
const validationResult: FailedFlatEntityValidation<FlatRoleTarget> = {
type: 'delete_role_target',
errors: [],
flatEntityMinimalInformation: {
id: flatRoleTargetToValidate.id,
},
};
const existingRoleTarget = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatRoleTargetToValidate.id,
flatEntityMaps: optimisticFlatRoleTargetMaps,
});
if (!isDefined(existingRoleTarget)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.ROLE_TARGET_NOT_FOUND,
message: t`Role target not found`,
userFriendlyMessage: msg`Role target not found`,
});
return validationResult;
}
return validationResult;
}
validateFlatRoleTargetUpdate({
flatEntityId,
flatEntityUpdates,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleTargetMaps: optimisticFlatRoleTargetMaps,
flatRoleMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.roleTarget
>): FailedFlatEntityValidation<FlatRoleTarget> {
const validationResult: FailedFlatEntityValidation<FlatRoleTarget> = {
type: 'update_role_target',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityId,
},
};
const existingRoleTarget = optimisticFlatRoleTargetMaps.byId[flatEntityId];
if (!isDefined(existingRoleTarget)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.ROLE_TARGET_NOT_FOUND,
message: t`Role target not found`,
userFriendlyMessage: msg`Role target not found`,
});
return validationResult;
}
const updatedFlatRoleTarget = {
...existingRoleTarget,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
};
validationResult.errors.push(
...validateFlatRoleTargetTargetsOnlyOneEntity({
flatRoleTarget: updatedFlatRoleTarget,
}),
);
const referencedRole = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updatedFlatRoleTarget.roleId,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
return validationResult;
}
validationResult.errors.push(
...validateFlatRoleTargetAssignationAvailability({
flatRole: referencedRole,
flatRoleTarget: updatedFlatRoleTarget,
}),
);
return validationResult;
}
}
@@ -0,0 +1,176 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { type ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/utils/find-flat-entity-property-update.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { validateRoleIsEditable } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-is-editable.util';
import { validateRoleLabelUniqueness } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-label-uniqueness.util';
import { validateRoleReadWritePermissionsConsistency } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-read-write-permissions-consistency.util';
import { validateRoleRequiredPropertiesAreDefined } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-required-properties-are-defined.util';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class FlatRoleValidatorService {
public validateFlatRoleCreation({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleMaps: optimisticFlatRoleMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.role
>): FailedFlatEntityValidation<FlatRole> {
const validationResult: FailedFlatEntityValidation<FlatRole> = {
type: 'create_role',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityToValidate.id,
label: flatEntityToValidate.label,
},
};
const existingRoles = Object.values(optimisticFlatRoleMaps.byId).filter(
isDefined,
);
validationResult.errors.push(
...validateRoleRequiredPropertiesAreDefined({
flatRole: flatEntityToValidate,
}),
);
validationResult.errors.push(
...validateRoleLabelUniqueness({
label: flatEntityToValidate.label,
existingFlatRoles: existingRoles,
}),
);
validationResult.errors.push(
...validateRoleReadWritePermissionsConsistency({
flatRole: flatEntityToValidate,
}),
);
return validationResult;
}
public validateFlatRoleDeletion({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleMaps: optimisticFlatRoleMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.role
>): FailedFlatEntityValidation<FlatRole> {
const validationResult: FailedFlatEntityValidation<FlatRole> = {
type: 'delete_role',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityToValidate.id,
label: flatEntityToValidate.label,
},
};
const existingRole = optimisticFlatRoleMaps.byId[flatEntityToValidate.id];
if (!isDefined(existingRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
return validationResult;
}
validationResult.errors.push(
...validateRoleIsEditable({
flatRole: existingRole,
}),
);
return validationResult;
}
public validateFlatRoleUpdate({
flatEntityId,
flatEntityUpdates,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleMaps: optimisticFlatRoleMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.role
>): FailedFlatEntityValidation<FlatRole> {
const validationResult: FailedFlatEntityValidation<FlatRole> = {
type: 'update_role',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityId,
},
};
const fromFlatRole = optimisticFlatRoleMaps.byId[flatEntityId];
if (!isDefined(fromFlatRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
return validationResult;
}
validationResult.errors.push(
...validateRoleIsEditable({
flatRole: fromFlatRole,
}),
);
const toFlatRole = {
...fromFlatRole,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
};
validationResult.errors.push(
...validateRoleRequiredPropertiesAreDefined({
flatRole: toFlatRole,
}),
);
const flatRoleLabelUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'label',
});
if (isDefined(flatRoleLabelUpdate)) {
const existingRoles = Object.values(optimisticFlatRoleMaps.byId).filter(
isDefined,
);
validationResult.errors.push(
...validateRoleLabelUniqueness({
label: flatRoleLabelUpdate.to,
existingFlatRoles: existingRoles,
}),
);
}
validationResult.errors.push(
...validateRoleReadWritePermissionsConsistency({
flatRole: toFlatRole,
}),
);
return validationResult;
}
}
@@ -0,0 +1,48 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'class-validator';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { RoleTargetExceptionCode } from 'src/engine/metadata-modules/role/exceptions/role-target.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
type ValidateFlatRoleTargetAssignationAvailabilityArgs = {
flatRole: FlatRole;
flatRoleTarget: FlatRoleTarget;
};
export const validateFlatRoleTargetAssignationAvailability = ({
flatRole,
flatRoleTarget,
}: ValidateFlatRoleTargetAssignationAvailabilityArgs): FlatEntityValidationError[] => {
const errors: FlatEntityValidationError[] = [];
const roleLabel = flatRole.label;
if (isDefined(flatRoleTarget.agentId)) {
if (!flatRole.canBeAssignedToAgents) {
errors.push({
code: RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY,
message: t`Role "${roleLabel}" cannot be assigned to agents`,
userFriendlyMessage: msg`Role "${roleLabel}" cannot be assigned to agents`,
});
}
} else if (isDefined(flatRoleTarget.userWorkspaceId)) {
if (!flatRole.canBeAssignedToUsers) {
errors.push({
code: RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY,
message: t`Role "${roleLabel}" cannot be assigned to users`,
userFriendlyMessage: msg`Role "${roleLabel}" cannot be assigned to users`,
});
}
} else if (isDefined(flatRoleTarget.apiKeyId)) {
if (!flatRole.canBeAssignedToApiKeys) {
errors.push({
code: RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY,
message: t`Role "${roleLabel}" cannot be assigned to API keys`,
userFriendlyMessage: msg`Role "${roleLabel}" cannot be assigned to API keys`,
});
}
}
return errors;
};
@@ -0,0 +1,30 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { RoleTargetExceptionCode } from 'src/engine/metadata-modules/role/exceptions/role-target.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateFlatRoleTargetTargetsOnlyOneEntity = ({
flatRoleTarget,
}: {
flatRoleTarget: FlatRoleTarget;
}) => {
const errors: FlatEntityValidationError[] = [];
const definedIdentifiersCount = [
isDefined(flatRoleTarget.apiKeyId),
isDefined(flatRoleTarget.userWorkspaceId),
isDefined(flatRoleTarget.agentId),
].filter(Boolean).length;
if (definedIdentifiersCount !== 1) {
errors.push({
code: RoleTargetExceptionCode.ROLE_TARGET_MISSING_IDENTIFIER,
message: t`Role target must have exactly one of: apiKeyId, userWorkspaceId, or agentId`,
userFriendlyMessage: msg`Role target must have exactly one of: apiKeyId, userWorkspaceId, or agentId`,
});
}
return errors;
};
@@ -0,0 +1,26 @@
import { msg } from '@lingui/core/macro';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateRoleIsEditable = ({
flatRole,
}: {
flatRole: FlatRole;
}): FlatEntityValidationError[] => {
const errors: FlatEntityValidationError[] = [];
if (!flatRole.isEditable) {
errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
return errors;
};
@@ -0,0 +1,28 @@
import { msg } from '@lingui/core/macro';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateRoleLabelUniqueness = ({
label,
existingFlatRoles,
}: {
label: string;
existingFlatRoles: FlatRole[];
}): FlatEntityValidationError[] => {
const errors: FlatEntityValidationError[] = [];
if (existingFlatRoles.some((role) => role.label === label)) {
errors.push({
code: PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS,
message: PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS,
userFriendlyMessage: msg`A role with this label already exists.`,
});
}
return errors;
};
@@ -0,0 +1,32 @@
import { msg } from '@lingui/core/macro';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateRoleReadWritePermissionsConsistency = ({
flatRole,
}: {
flatRole: FlatRole;
}): FlatEntityValidationError[] => {
const errors: FlatEntityValidationError[] = [];
if (
flatRole.canReadAllObjectRecords === false &&
(flatRole.canUpdateAllObjectRecords ||
flatRole.canSoftDeleteAllObjectRecords ||
flatRole.canDestroyAllObjectRecords)
) {
errors.push({
code: PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
message:
PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
userFriendlyMessage: msg`You cannot grant edit permissions without also granting read permissions. Please enable read access first.`,
});
}
return errors;
};
@@ -0,0 +1,28 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { FLAT_ROLE_REQUIRED_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-required-properties.constants';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateRoleRequiredPropertiesAreDefined = ({
flatRole,
}: {
flatRole: FlatRole;
}): FlatEntityValidationError[] =>
FLAT_ROLE_REQUIRED_PROPERTIES.flatMap<FlatEntityValidationError>(
(property) => {
if (isDefined(flatRole[property])) {
return [];
}
return [
{
code: PermissionsExceptionCode.INVALID_ARG,
message: t`Property ${property} is required for role`,
userFriendlyMessage: msg`Some of the information provided is invalid. Please check your input and try again.`,
},
];
},
);
@@ -7,6 +7,8 @@ import { FlatDatabaseEventTriggerValidatorService } from 'src/engine/workspace-m
import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service';
import { FlatIndexValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service';
import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service';
import { FlatRoleTargetValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service';
import { FlatRoleValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service';
import { FlatRouteTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-route-trigger-validator.service';
import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-serverless-function-validator.service';
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service';
@@ -29,6 +31,8 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatCronTriggerValidatorService,
FlatFieldMetadataTypeValidatorService,
FlatRouteTriggerValidatorService,
FlatRoleValidatorService,
FlatRoleTargetValidatorService,
],
exports: [
FlatViewValidatorService,
@@ -43,6 +47,8 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatCronTriggerValidatorService,
FlatFieldMetadataTypeValidatorService,
FlatRouteTriggerValidatorService,
FlatRoleValidatorService,
FlatRoleTargetValidatorService,
],
})
export class WorkspaceMigrationBuilderValidatorsModule {}
@@ -7,6 +7,8 @@ import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 's
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
import { WorkspaceMigrationV2IndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/workspace-migration-v2-object-actions-builder.service';
import { WorkspaceMigrationV2RoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service';
import { WorkspaceMigrationV2RoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service';
import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service';
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
@@ -30,6 +32,8 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2FieldActionsBuilderService,
WorkspaceMigrationV2CronTriggerActionsBuilderService,
WorkspaceMigrationV2RouteTriggerActionsBuilderService,
WorkspaceMigrationV2RoleActionsBuilderService,
WorkspaceMigrationV2RoleTargetActionsBuilderService,
],
exports: [
WorkspaceMigrationV2ViewActionsBuilderService,
@@ -43,6 +47,8 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService,
WorkspaceMigrationV2CronTriggerActionsBuilderService,
WorkspaceMigrationV2RouteTriggerActionsBuilderService,
WorkspaceMigrationV2RoleActionsBuilderService,
WorkspaceMigrationV2RoleTargetActionsBuilderService,
FlatFieldMetadataTypeValidatorService,
],
})
@@ -0,0 +1,55 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { CreateRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateRoleTargetActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_role_target',
) {
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<CreateRoleTargetAction>): Partial<AllFlatEntityMaps> {
const { flatRoleTargetMaps } = allFlatEntityMaps;
const { roleTarget } = action;
const updatedFlatRoleTargetMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: roleTarget,
flatEntityMaps: flatRoleTargetMaps,
});
return {
flatRoleTargetMaps: updatedFlatRoleTargetMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateRoleTargetAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { roleTarget } = action;
const roleTargetRepository =
queryRunner.manager.getRepository<RoleTargetsEntity>(RoleTargetsEntity);
await roleTargetRepository.insert({
...roleTarget,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateRoleTargetAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { DeleteRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteRoleTargetActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_role_target',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<DeleteRoleTargetAction>): Partial<AllFlatEntityMaps> {
const { flatRoleTargetMaps } = allFlatEntityMaps;
const { roleTargetId } = action;
const updatedFlatRoleTargetMaps = deleteFlatEntityFromFlatEntityMapsOrThrow(
{
entityToDeleteId: roleTargetId,
flatEntityMaps: flatRoleTargetMaps,
},
);
return {
flatRoleTargetMaps: updatedFlatRoleTargetMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteRoleTargetAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { roleTargetId } = action;
const roleTargetRepository =
queryRunner.manager.getRepository<RoleTargetsEntity>(RoleTargetsEntity);
await roleTargetRepository.delete({
id: roleTargetId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteRoleTargetAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,70 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { UpdateRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class UpdateRoleTargetActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_role_target',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<UpdateRoleTargetAction>): Partial<AllFlatEntityMaps> {
const { flatRoleTargetMaps } = allFlatEntityMaps;
const { roleTargetId } = action;
const existingRoleTarget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: roleTargetId,
flatEntityMaps: flatRoleTargetMaps,
});
const updatedRoleTarget = {
...existingRoleTarget,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action),
};
const updatedFlatRoleTargetMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: updatedRoleTarget,
flatEntityMaps: flatRoleTargetMaps,
});
return {
flatRoleTargetMaps: updatedFlatRoleTargetMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateRoleTargetAction>,
): Promise<void> {
const { action, queryRunner } = context;
const { roleTargetId } = action;
const roleTargetRepository =
queryRunner.manager.getRepository<RoleTargetsEntity>(RoleTargetsEntity);
const update = fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action);
await roleTargetRepository.update(roleTargetId, update);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateRoleTargetAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,59 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { CreateRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_role',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<CreateRoleAction>): Partial<AllFlatEntityMaps> {
const { flatRoleMaps } = allFlatEntityMaps;
const { role } = action;
const updatedFlatRoleMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: role,
flatEntityMaps: flatRoleMaps,
});
return {
flatRoleMaps: updatedFlatRoleMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateRoleAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { role } = action;
const roleRepository =
queryRunner.manager.getRepository<RoleEntity>(RoleEntity);
await roleRepository.insert({
...role,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateRoleAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { DeleteRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_role',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<DeleteRoleAction>): Partial<AllFlatEntityMaps> {
const { flatRoleMaps } = allFlatEntityMaps;
const { roleId } = action;
const updatedFlatRoleMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
entityToDeleteId: roleId,
flatEntityMaps: flatRoleMaps,
});
return {
flatRoleMaps: updatedFlatRoleMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteRoleAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { roleId } = action;
const roleRepository =
queryRunner.manager.getRepository<RoleEntity>(RoleEntity);
await roleRepository.delete({ id: roleId, workspaceId });
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteRoleAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,67 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UpdateRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class UpdateRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_role',
) {
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<UpdateRoleAction>): Partial<AllFlatEntityMaps> {
const { flatRoleMaps } = allFlatEntityMaps;
const { roleId } = action;
const existingRole = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: roleId,
flatEntityMaps: flatRoleMaps,
});
const updatedRole = {
...existingRole,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action),
};
const updatedFlatRoleMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: updatedRole,
flatEntityMaps: flatRoleMaps,
});
return {
flatRoleMaps: updatedFlatRoleMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateRoleAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { roleId } = action;
const roleRepository =
queryRunner.manager.getRepository<RoleEntity>(RoleEntity);
await roleRepository.update(
{ id: roleId, workspaceId },
fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action),
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateRoleAction>,
): Promise<void> {
return;
}
}
@@ -15,6 +15,12 @@ import { DeleteIndexActionHandlerService } from 'src/engine/workspace-manager/wo
import { CreateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service';
import { DeleteObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service';
import { UpdateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service';
import { CreateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/create-role-target-action-handler.service';
import { DeleteRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/delete-role-target-action-handler.service';
import { UpdateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/update-role-target-action-handler.service';
import { CreateRoleActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/create-role-action-handler.service';
import { DeleteRoleActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/delete-role-action-handler.service';
import { UpdateRoleActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/update-role-action-handler.service';
import { CreateRouteTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/route-trigger/services/create-route-trigger-action-handler.service';
import { DeleteRouteTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/route-trigger/services/delete-route-trigger-action-handler.service';
import { UpdateRouteTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/route-trigger/services/update-route-trigger-action-handler.service';
@@ -79,6 +85,14 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
CreateRouteTriggerActionHandlerService,
DeleteRouteTriggerActionHandlerService,
UpdateRouteTriggerActionHandlerService,
CreateRoleActionHandlerService,
UpdateRoleActionHandlerService,
DeleteRoleActionHandlerService,
CreateRoleTargetActionHandlerService,
DeleteRoleTargetActionHandlerService,
UpdateRoleTargetActionHandlerService,
],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
@@ -60,11 +60,6 @@ export class WorkspaceMigrationRunnerV2Service {
workspaceId,
),
);
asyncOperations.push(
this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
}),
);
}
const shouldInvalidFindCoreViewsGraphqlCacheOperation = actions.some(
@@ -103,6 +98,43 @@ export class WorkspaceMigrationRunnerV2Service {
);
}
const shouldInvalidateRoleMapCache = actions.some((action) => {
switch (action.type) {
case 'create_role':
case 'delete_role':
case 'update_role':
case 'create_role_target':
case 'delete_role_target':
case 'update_role_target': {
return true;
}
default: {
return false;
}
}
});
if (
shouldIncrementMetadataGraphqlSchemaVersion ||
shouldInvalidateRoleMapCache
) {
asyncOperations.push(
...[
this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({
workspaceId,
}),
this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
}),
this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache(
{
workspaceId,
},
),
],
);
}
return asyncOperations;
}
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { fromStandardRoleDefinitionToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface';
@@ -12,8 +13,8 @@ export class StandardRoleFactory {
roleDefinitions: StandardRoleDefinition[],
context: WorkspaceSyncContext,
existingRoles: RoleEntity[],
): Partial<RoleEntity>[] {
const computedRoles: Partial<RoleEntity>[] = [];
): FlatRole[] {
const computedRoles: FlatRole[] = [];
for (const roleDefinition of roleDefinitions) {
const existingRole = existingRoles.find(
@@ -30,6 +31,7 @@ export class StandardRoleFactory {
...flatRole,
id: existingRole.id,
universalIdentifier: roleDefinition.standardId || existingRole.id,
createdAt: existingRole.createdAt,
});
} else {
computedRoles.push({
@@ -38,7 +38,12 @@ export class WorkspaceSyncRoleService {
workspaceId: context.workspaceId,
standardId: Not(IsNull()),
},
relations: ['permissionFlags'],
relations: [
'permissionFlags',
'roleTargets',
'objectPermissions',
'fieldPermissions',
],
});
const targetStandardRoles = this.standardRoleFactory.create(
@@ -49,7 +54,7 @@ export class WorkspaceSyncRoleService {
const roleComparatorResults = this.workspaceRoleComparator.compare({
fromFlatRoles: existingStandardRoleEntities.map(fromRoleEntityToFlatRole),
toFlatRoles: targetStandardRoles.map(fromRoleEntityToFlatRole),
toFlatRoles: targetStandardRoles,
});
for (const roleComparatorResult of roleComparatorResults) {
@@ -3,7 +3,16 @@ import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions
export type StandardRoleDefinition = Omit<
FlatRole,
'id' | 'workspaceId' | 'universalIdentifier' | 'standardId'
| 'id'
| 'workspaceId'
| 'universalIdentifier'
| 'standardId'
| 'roleTargetIds'
| 'objectPermissionIds'
| 'permissionFlagIds'
| 'fieldPermissionIds'
| 'createdAt'
| 'updatedAt'
> & {
standardId: string;
permissionFlags?: PermissionFlagType[];
@@ -1,325 +0,0 @@
import gql from 'graphql-tag';
import { default as request } from 'supertest';
import { createRoleOperation } from 'test/integration/graphql/utils/create-custom-role-operation-factory.util';
import { deleteRole } from 'test/integration/graphql/utils/delete-one-role.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
const client = request(`http://localhost:${APP_PORT}`);
describe('Role Permissions Validation', () => {
describe('validateRoleDoesNotHaveWritingPermissionsWithoutReadingPermissionsOrThrow', () => {
describe('createRole - Valid Cases', () => {
it('should allow creating role with read=true and any write permissions', async () => {
const operation = createRoleOperation({
label: 'ValidRole',
description: 'Valid role with read and write permissions',
canUpdateAllSettings: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
});
const response = await makeGraphqlAPIRequest(operation);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.createOneRole).toBeDefined();
expect(response.body.data.createOneRole.label).toBe('ValidRole');
});
it('should allow creating role with read=false and all write permissions=false', async () => {
const operation = createRoleOperation({
label: 'ValidNoWriteRole',
description: 'Valid role with no write permissions',
canUpdateAllSettings: false,
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
});
const response = await makeGraphqlAPIRequest(operation);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.createOneRole).toBeDefined();
expect(response.body.data.createOneRole.label).toBe('ValidNoWriteRole');
});
});
describe('createRole - Invalid Cases', () => {
it('should throw error when creating role with read=false but canDestroyAllObjectRecords=true', async () => {
const operation = createRoleOperation({
label: 'InvalidDestroyRole',
description: 'Invalid role with destroy permission but no read',
canUpdateAllSettings: false,
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: true,
});
const response = await makeGraphqlAPIRequest(operation);
expect(response.body.data).toBeNull();
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
);
expect(response.body.errors[0].extensions.code).toBe(
ErrorCode.BAD_USER_INPUT,
);
});
});
describe('updateRole - Valid Cases', () => {
let baseRoleId: string;
beforeEach(async () => {
const operation = createRoleOperation({
label: 'BaseRole',
description: 'Base role for update tests',
canUpdateAllSettings: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
});
const response = await makeGraphqlAPIRequest(operation);
baseRoleId = response.body.data.createOneRole.id;
});
afterEach(async () => {
if (baseRoleId) {
await deleteRole(client, baseRoleId);
}
});
it('should allow updating role to have read=true and any write permissions', async () => {
const updateRoleOperation = {
query: gql`
mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) {
updateOneRole(updateRoleInput: $updateRoleInput) {
id
canReadAllObjectRecords
canUpdateAllObjectRecords
canSoftDeleteAllObjectRecords
canDestroyAllObjectRecords
}
}
`,
variables: {
updateRoleInput: {
id: baseRoleId,
update: {
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
},
},
},
};
const response = await makeGraphqlAPIRequest(updateRoleOperation);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.updateOneRole).toBeDefined();
expect(response.body.data.updateOneRole.canReadAllObjectRecords).toBe(
true,
);
expect(response.body.data.updateOneRole.canUpdateAllObjectRecords).toBe(
true,
);
expect(
response.body.data.updateOneRole.canSoftDeleteAllObjectRecords,
).toBe(true);
expect(
response.body.data.updateOneRole.canDestroyAllObjectRecords,
).toBe(true);
});
it('should allow updating role to have read=false and all write permissions=false', async () => {
const updateRoleOperation = {
query: gql`
mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) {
updateOneRole(updateRoleInput: $updateRoleInput) {
id
canReadAllObjectRecords
canUpdateAllObjectRecords
canSoftDeleteAllObjectRecords
canDestroyAllObjectRecords
}
}
`,
variables: {
updateRoleInput: {
id: baseRoleId,
update: {
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
},
},
},
};
const response = await makeGraphqlAPIRequest(updateRoleOperation);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.updateOneRole).toBeDefined();
expect(response.body.data.updateOneRole.canReadAllObjectRecords).toBe(
false,
);
expect(response.body.data.updateOneRole.canUpdateAllObjectRecords).toBe(
false,
);
expect(
response.body.data.updateOneRole.canSoftDeleteAllObjectRecords,
).toBe(false);
expect(
response.body.data.updateOneRole.canDestroyAllObjectRecords,
).toBe(false);
});
});
describe('updateRole - Invalid Cases', () => {
let roleWithWritePermissionsId: string;
beforeEach(async () => {
const operation = createRoleOperation({
label: 'RoleWithWritePermissions',
description: 'Role with write permissions for update tests',
canUpdateAllSettings: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
});
const response = await makeGraphqlAPIRequest(operation);
roleWithWritePermissionsId = response.body.data.createOneRole.id;
});
afterEach(async () => {
if (roleWithWritePermissionsId) {
await deleteRole(client, roleWithWritePermissionsId);
}
});
it('should throw error when updating role to read=false but keeping write permissions', async () => {
const updateRoleOperation = {
query: gql`
mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) {
updateOneRole(updateRoleInput: $updateRoleInput) {
id
}
}
`,
variables: {
updateRoleInput: {
id: roleWithWritePermissionsId,
update: {
canReadAllObjectRecords: false,
// Not explicitly setting write permissions, so they keep existing values (true)
},
},
},
};
const response = await makeGraphqlAPIRequest(updateRoleOperation);
expect(response.body.data).toBeNull();
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
);
expect(response.body.errors[0].extensions.code).toBe(
ErrorCode.BAD_USER_INPUT,
);
});
it('should allow updating role to read=false when explicitly setting all write permissions to false', async () => {
const updateRoleOperation = {
query: gql`
mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) {
updateOneRole(updateRoleInput: $updateRoleInput) {
id
canReadAllObjectRecords
canUpdateAllObjectRecords
canSoftDeleteAllObjectRecords
canDestroyAllObjectRecords
}
}
`,
variables: {
updateRoleInput: {
id: roleWithWritePermissionsId,
update: {
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
},
},
},
};
const response = await makeGraphqlAPIRequest(updateRoleOperation);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.updateOneRole).toBeDefined();
expect(response.body.data.updateOneRole.canReadAllObjectRecords).toBe(
false,
);
expect(response.body.data.updateOneRole.canUpdateAllObjectRecords).toBe(
false,
);
expect(
response.body.data.updateOneRole.canSoftDeleteAllObjectRecords,
).toBe(false);
expect(
response.body.data.updateOneRole.canDestroyAllObjectRecords,
).toBe(false);
});
it('should allow updating role to read=false when explicitly setting some write permissions to false', async () => {
const updateRoleOperation = {
query: gql`
mutation UpdateOneRole($updateRoleInput: UpdateRoleInput!) {
updateOneRole(updateRoleInput: $updateRoleInput) {
id
canReadAllObjectRecords
canUpdateAllObjectRecords
canSoftDeleteAllObjectRecords
canDestroyAllObjectRecords
}
}
`,
variables: {
updateRoleInput: {
id: roleWithWritePermissionsId,
update: {
canSoftDeleteAllObjectRecords: false,
// Keep other permissions as they are
},
},
},
};
const response = await makeGraphqlAPIRequest(updateRoleOperation);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.updateOneRole).toBeDefined();
expect(
response.body.data.updateOneRole.canSoftDeleteAllObjectRecords,
).toBe(false);
});
});
});
});
@@ -1,10 +1,13 @@
import request from 'supertest';
import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/delete-one-role-operation-factory.util';
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { findOneRoleByLabel } from 'test/integration/metadata/suites/role/utils/find-one-role-by-label.util';
import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util';
import { updateWorkspaceMemberRole } from 'test/integration/metadata/suites/role/utils/update-workspace-member-role.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
@@ -20,25 +23,9 @@ describe('Granular settings permissions', () => {
beforeAll(async () => {
// Get the original Member role ID for restoration later
const getRolesQuery = {
query: `
query GetRoles {
getRoles {
id
label
}
}
`,
};
const memberRole = await findOneRoleByLabel({ label: 'Member' });
const rolesResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(getRolesQuery);
originalMemberRoleId = rolesResponse.body.data.getRoles.find(
(role: any) => role.label === 'Member',
).id;
originalMemberRoleId = memberRole.id;
// Create a custom role with canUpdateAllSettings = false
const createRoleQuery = {
@@ -91,31 +78,23 @@ describe('Granular settings permissions', () => {
// Assign the custom role to JONY (who uses APPLE_JONY_MEMBER_ACCESS_TOKEN)
await updateWorkspaceMemberRole({
client,
roleId: customRoleId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
input: {
roleId: customRoleId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
},
expectToFail: false,
});
});
afterAll(async () => {
// Restore JONY's original Member role
const restoreMemberRoleQuery = {
query: `
mutation UpdateWorkspaceMemberRole {
updateWorkspaceMemberRole(
workspaceMemberId: "${WORKSPACE_MEMBER_DATA_SEED_IDS.JONY}"
roleId: "${originalMemberRoleId}"
) {
id
}
}
`,
};
await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(restoreMemberRoleQuery);
await updateWorkspaceMemberRole({
input: {
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
roleId: originalMemberRoleId,
},
expectToFail: false,
});
// Delete the custom role
const deleteRoleQuery = deleteOneRoleOperationFactory(customRoleId);
@@ -346,38 +325,32 @@ describe('Granular settings permissions', () => {
describe('Permission Inheritance', () => {
it('should verify that canUpdateAllSettings=false is properly overridden by specific setting permissions', async () => {
// Verify the role configuration
const getRoleQuery = {
query: `
query GetRole {
getRoles {
id
label
canUpdateAllSettings
permissionFlags {
flag
}
}
// Verify the role configuration using the new integration test utilities
const { data, errors } = await findRoles({
gqlFields: `
id
label
canUpdateAllSettings
permissionFlags {
flag
}
`,
};
expectToFail: false,
});
const response = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(getRoleQuery);
expect(errors).toBeUndefined();
expect(data).toBeDefined();
const customRole = response.body.data.getRoles.find(
(role: any) => role.id === customRoleId,
);
const customRole = data.getRoles.find((role) => role.id === customRoleId);
expect(customRole).toBeDefined();
jestExpectToBeDefined(customRole);
expect(customRole.canUpdateAllSettings).toBe(false);
expect(customRole.permissionFlags).toHaveLength(3);
expect(customRole.permissionFlags.map((p: any) => p.flag)).toContain(
jestExpectToBeDefined(customRole.permissionFlags);
expect(customRole.permissionFlags.map((p) => p.flag)).toContain(
PermissionFlagType.DATA_MODEL,
);
expect(customRole.permissionFlags.map((p: any) => p.flag)).toContain(
expect(customRole.permissionFlags.map((p) => p.flag)).toContain(
PermissionFlagType.WORKSPACE,
);
});
@@ -410,33 +383,28 @@ describe('Granular settings permissions', () => {
expect(response.body.errors).toBeUndefined();
expect(response.body.data.upsertPermissionFlags).toHaveLength(3);
// Verify the user now has access to security operations
// Note: This would require a specific security operation to test
// For now, we just verify the permission was added
const getRoleQuery = {
query: `
query GetRole {
getRoles {
id
permissionFlags {
flag
}
}
// Verify the permission was added using the new integration test utilities
const { data, errors } = await findRoles({
gqlFields: `
id
permissionFlags {
flag
}
`,
};
expectToFail: false,
});
const roleResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(getRoleQuery);
expect(errors).toBeUndefined();
expect(data).toBeDefined();
const updatedRole = roleResponse.body.data.getRoles.find(
(role: any) => role.id === customRoleId,
const updatedRole = data.getRoles.find(
(role) => role.id === customRoleId,
);
jestExpectToBeDefined(updatedRole);
expect(updatedRole.permissionFlags).toHaveLength(3);
expect(updatedRole.permissionFlags.map((p: any) => p.flag)).toContain(
jestExpectToBeDefined(updatedRole.permissionFlags);
expect(updatedRole.permissionFlags.map((p) => p.flag)).toContain(
PermissionFlagType.SECURITY,
);
});
@@ -467,31 +435,28 @@ describe('Granular settings permissions', () => {
expect(response.body.errors).toBeUndefined();
expect(response.body.data.upsertPermissionFlags).toHaveLength(2);
// Verify SECURITY permission was removed
const getRoleQuery = {
query: `
query GetRole {
getRoles {
id
permissionFlags {
flag
}
}
// Verify SECURITY permission was removed using the new integration test utilities
const { data, errors } = await findRoles({
gqlFields: `
id
permissionFlags {
flag
}
`,
};
expectToFail: false,
});
const roleResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(getRoleQuery);
expect(errors).toBeUndefined();
expect(data).toBeDefined();
const updatedRole = roleResponse.body.data.getRoles.find(
(role: any) => role.id === customRoleId,
const updatedRole = data.getRoles.find(
(role) => role.id === customRoleId,
);
jestExpectToBeDefined(updatedRole);
jestExpectToBeDefined(updatedRole.permissionFlags);
expect(updatedRole.permissionFlags).toHaveLength(2);
expect(updatedRole.permissionFlags.map((p: any) => p.flag)).not.toContain(
expect(updatedRole.permissionFlags.map((p) => p.flag)).not.toContain(
PermissionFlagType.SECURITY,
);
});
@@ -392,34 +392,6 @@ describe('roles permissions', () => {
await assertPermissionDeniedForMemberWithMemberRole({ query });
});
it('should throw an error when role is not editable', async () => {
const query = {
query: `
mutation UpdateOneRole {
updateOneRole(updateRoleInput: {id: "${adminRoleId}", update: {label: "new role label (2)"}}) {
id
}
}
`,
};
await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(query)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeNull();
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].message).toBe(
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
);
expect(res.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
});
it('should update a role when user has permission to update a role (admin role)', async () => {
const query = {
query: `
@@ -47,7 +47,7 @@ describe('Destroy core view', () => {
});
});
it.only('should destroy an existing view', async () => {
it('should destroy an existing view', async () => {
const {
data: { createCoreView: view },
} = await createOneCoreView({
@@ -10,6 +10,8 @@ exports[`View Field Resolver - Failing Create Many Operations - v2 should accumu
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -78,6 +80,8 @@ exports[`View Field Resolver - Failing Create Many Operations - v2 should accumu
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -10,6 +10,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 Should fail to crea
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -41,6 +43,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 Should fail to crea
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -66,6 +70,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 should fail to crea
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -97,6 +103,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 should fail to crea
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -122,6 +130,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 should fail to crea
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -153,6 +163,8 @@ exports[`View Field Resolver - Failing Create Operation - v2 should fail to crea
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -10,6 +10,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -41,6 +43,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -66,6 +70,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -97,6 +103,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -122,6 +130,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -151,6 +161,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -176,6 +188,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -207,6 +221,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -232,6 +248,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -263,6 +281,8 @@ exports[`View Field Resolver - Successful object metadata identifier update side
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -10,6 +10,8 @@ exports[`View Group Resolver - Failing Create Many Operations - v2 should accumu
"fieldMetadata": [],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -78,6 +80,8 @@ exports[`View Group Resolver - Failing Create Many Operations - v2 should accumu
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -28,6 +28,8 @@ exports[`createOne FieldMetadataService name/label sync should return an error w
],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -42,6 +44,8 @@ exports[`createOne FieldMetadataService name/label sync should return an error w
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -29,6 +29,8 @@ exports[`Failing create field metadata tests suite should fail to create NUMERIC
],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -43,6 +45,8 @@ exports[`Failing create field metadata tests suite should fail to create NUMERIC
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -88,6 +92,8 @@ exports[`Failing create field metadata tests suite should fail to create POSITIO
],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -102,6 +108,8 @@ exports[`Failing create field metadata tests suite should fail to create POSITIO
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -140,6 +148,8 @@ exports[`Failing create field metadata tests suite should fail to create TS_VECT
"type": "create_field",
},
],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -154,6 +164,8 @@ exports[`Failing create field metadata tests suite should fail to create TS_VECT
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -28,6 +28,8 @@ exports[`updateOne failing update should not update a field name if it is not sy
],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -42,6 +44,8 @@ exports[`updateOne failing update should not update a field name if it is not sy
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,
@@ -92,6 +96,8 @@ exports[`updateOne failing update should throw if the field name is not availabl
],
"index": [],
"objectMetadata": [],
"role": [],
"roleTarget": [],
"routeTrigger": [],
"serverlessFunction": [],
"view": [],
@@ -106,6 +112,8 @@ exports[`updateOne failing update should throw if the field name is not availabl
"invalidFieldMetadata": 0,
"invalidIndex": 0,
"invalidObjectMetadata": 0,
"invalidRole": 0,
"invalidRoleTarget": 0,
"invalidRouteTrigger": 0,
"invalidServerlessFunction": 0,
"invalidView": 0,

Some files were not shown because too many files have changed in this diff Show More