Polish permission flag definition foundation

This commit is contained in:
ehconitin
2026-05-08 03:12:30 +05:30
parent ef61f247f8
commit ae889bfa99
24 changed files with 267 additions and 140 deletions
@@ -2847,7 +2847,7 @@ type PermissionFlagDefinition {
label: String!
description: String
iconKey: String
category: String!
permissionType: String!
isRelevantForAgents: Boolean!
isRelevantForUsers: Boolean!
isRelevantForApiKeys: Boolean!
@@ -4232,7 +4232,7 @@ input CreatePermissionFlagDefinitionInput {
label: String!
description: String
iconKey: String
category: String!
permissionType: String!
isRelevantForAgents: Boolean
isRelevantForUsers: Boolean
isRelevantForApiKeys: Boolean
@@ -4250,7 +4250,7 @@ input UpdatePermissionFlagDefinitionInputUpdates {
label: String
description: String
iconKey: String
category: String
permissionType: String
isRelevantForAgents: Boolean
isRelevantForUsers: Boolean
isRelevantForApiKeys: Boolean
@@ -2497,7 +2497,7 @@ export interface PermissionFlagDefinition {
label: Scalars['String']
description?: Scalars['String']
iconKey?: Scalars['String']
category: Scalars['String']
permissionType: Scalars['String']
isRelevantForAgents: Scalars['Boolean']
isRelevantForUsers: Scalars['Boolean']
isRelevantForApiKeys: Scalars['Boolean']
@@ -5532,7 +5532,7 @@ export interface PermissionFlagDefinitionGenqlSelection{
label?: boolean | number
description?: boolean | number
iconKey?: boolean | number
category?: boolean | number
permissionType?: boolean | number
isRelevantForAgents?: boolean | number
isRelevantForUsers?: boolean | number
isRelevantForApiKeys?: boolean | number
@@ -6263,7 +6263,7 @@ update: UpdateWebhookInputUpdates}
export interface UpdateWebhookInputUpdates {targetUrl?: (Scalars['String'] | null),operations?: (Scalars['String'][] | null),description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
export interface CreatePermissionFlagDefinitionInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),key: Scalars['String'],label: Scalars['String'],description?: (Scalars['String'] | null),iconKey?: (Scalars['String'] | null),category: Scalars['String'],isRelevantForAgents?: (Scalars['Boolean'] | null),isRelevantForUsers?: (Scalars['Boolean'] | null),isRelevantForApiKeys?: (Scalars['Boolean'] | null)}
export interface CreatePermissionFlagDefinitionInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),key: Scalars['String'],label: Scalars['String'],description?: (Scalars['String'] | null),iconKey?: (Scalars['String'] | null),permissionType: Scalars['String'],isRelevantForAgents?: (Scalars['Boolean'] | null),isRelevantForUsers?: (Scalars['Boolean'] | null),isRelevantForApiKeys?: (Scalars['Boolean'] | null)}
export interface UpdatePermissionFlagDefinitionInput {
/** The id of the permission flag definition to update */
@@ -6271,7 +6271,7 @@ id: Scalars['UUID'],
/** The fields to update */
update: UpdatePermissionFlagDefinitionInputUpdates}
export interface UpdatePermissionFlagDefinitionInputUpdates {label?: (Scalars['String'] | null),description?: (Scalars['String'] | null),iconKey?: (Scalars['String'] | null),category?: (Scalars['String'] | null),isRelevantForAgents?: (Scalars['Boolean'] | null),isRelevantForUsers?: (Scalars['Boolean'] | null),isRelevantForApiKeys?: (Scalars['Boolean'] | null)}
export interface UpdatePermissionFlagDefinitionInputUpdates {label?: (Scalars['String'] | null),description?: (Scalars['String'] | null),iconKey?: (Scalars['String'] | null),permissionType?: (Scalars['String'] | null),isRelevantForAgents?: (Scalars['Boolean'] | null),isRelevantForUsers?: (Scalars['Boolean'] | null),isRelevantForApiKeys?: (Scalars['Boolean'] | null)}
export interface CreateSkillInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content: Scalars['String']}
@@ -5659,7 +5659,7 @@ export default {
"iconKey": [
1
],
"category": [
"permissionType": [
1
],
"isRelevantForAgents": [
@@ -10751,7 +10751,7 @@ export default {
"iconKey": [
1
],
"category": [
"permissionType": [
1
],
"isRelevantForAgents": [
@@ -10788,7 +10788,7 @@ export default {
"iconKey": [
1
],
"category": [
"permissionType": [
1
],
"isRelevantForAgents": [
@@ -1150,7 +1150,6 @@ export type CreatePageLayoutWidgetInput = {
};
export type CreatePermissionFlagDefinitionInput = {
category: Scalars['String'];
description?: InputMaybe<Scalars['String']>;
iconKey?: InputMaybe<Scalars['String']>;
id?: InputMaybe<Scalars['UUID']>;
@@ -1159,6 +1158,7 @@ export type CreatePermissionFlagDefinitionInput = {
isRelevantForUsers?: InputMaybe<Scalars['Boolean']>;
key: Scalars['String'];
label: Scalars['String'];
permissionType: Scalars['String'];
universalIdentifier?: InputMaybe<Scalars['UUID']>;
};
@@ -3938,7 +3938,6 @@ export type PermissionFlag = {
export type PermissionFlagDefinition = {
__typename?: 'PermissionFlagDefinition';
applicationId: Scalars['UUID'];
category: Scalars['String'];
createdAt: Scalars['DateTime'];
description?: Maybe<Scalars['String']>;
iconKey?: Maybe<Scalars['String']>;
@@ -3949,6 +3948,7 @@ export type PermissionFlagDefinition = {
isRelevantForUsers: Scalars['Boolean'];
key: Scalars['String'];
label: Scalars['String'];
permissionType: Scalars['String'];
universalIdentifier: Scalars['UUID'];
updatedAt: Scalars['DateTime'];
};
@@ -5227,13 +5227,13 @@ export type UpdatePermissionFlagDefinitionInput = {
};
export type UpdatePermissionFlagDefinitionInputUpdates = {
category?: InputMaybe<Scalars['String']>;
description?: InputMaybe<Scalars['String']>;
iconKey?: InputMaybe<Scalars['String']>;
isRelevantForAgents?: InputMaybe<Scalars['Boolean']>;
isRelevantForApiKeys?: InputMaybe<Scalars['Boolean']>;
isRelevantForUsers?: InputMaybe<Scalars['Boolean']>;
label?: InputMaybe<Scalars['String']>;
permissionType?: InputMaybe<Scalars['String']>;
};
export type UpdateRoleInput = {
@@ -130,7 +130,9 @@ export abstract class WorkspaceCommandRunner<
});
if (report.fail.length > 0) {
throw new Error(`Command failed for ${report.fail.length} workspace(s)`);
throw new Error(
`Command failed for ${report.fail.length} workspace(s)`,
);
}
this.logger.log(chalk.blue('Command completed!'));
@@ -18,7 +18,7 @@ export class PermissionFlagDefinitionSyncableEntityFastInstanceCommand
"label" varchar NOT NULL,
"description" text,
"iconKey" varchar,
"category" varchar NOT NULL,
"permissionType" varchar NOT NULL,
"isRelevantForAgents" boolean NOT NULL DEFAULT false,
"isRelevantForUsers" boolean NOT NULL DEFAULT false,
"isRelevantForApiKeys" boolean NOT NULL DEFAULT false,
@@ -55,17 +55,19 @@ export class PermissionFlagDefinitionSyncableEntityFastInstanceCommand
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."permissionFlagDefinition" DROP CONSTRAINT "FK_PERMISSION_FLAG_DEFINITION_APPLICATION"`,
`ALTER TABLE "core"."permissionFlagDefinition" DROP CONSTRAINT IF EXISTS "FK_PERMISSION_FLAG_DEFINITION_APPLICATION"`,
);
await queryRunner.query(
`ALTER TABLE "core"."permissionFlagDefinition" DROP CONSTRAINT "FK_PERMISSION_FLAG_DEFINITION_WORKSPACE"`,
`ALTER TABLE "core"."permissionFlagDefinition" DROP CONSTRAINT IF EXISTS "FK_PERMISSION_FLAG_DEFINITION_WORKSPACE"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_PERMISSION_FLAG_DEFINITION_WORKSPACE_UNIVERSAL_IDENTIFIER"`,
`DROP INDEX IF EXISTS "core"."IDX_PERMISSION_FLAG_DEFINITION_WORKSPACE_UNIVERSAL_IDENTIFIER"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_PERMISSION_FLAG_DEFINITION_APPLICATION_ID"`,
`DROP INDEX IF EXISTS "core"."IDX_PERMISSION_FLAG_DEFINITION_APPLICATION_ID"`,
);
await queryRunner.query(
`DROP TABLE IF EXISTS "core"."permissionFlagDefinition"`,
);
await queryRunner.query(`DROP TABLE "core"."permissionFlagDefinition"`);
}
}
@@ -1224,7 +1224,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
toStringify: false,
universalProperty: undefined,
},
category: {
permissionType: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
@@ -4,7 +4,7 @@ export const FLAT_PERMISSION_FLAG_DEFINITION_EDITABLE_PROPERTIES = [
'label',
'description',
'iconKey',
'category',
'permissionType',
'isRelevantForAgents',
'isRelevantForUsers',
'isRelevantForApiKeys',
@@ -34,7 +34,7 @@ export const fromCreatePermissionFlagDefinitionInputToFlatPermissionFlagDefiniti
label,
description: description ?? null,
iconKey: iconKey ?? null,
category: createPermissionFlagDefinitionInput.category,
permissionType: createPermissionFlagDefinitionInput.permissionType,
isRelevantForAgents:
createPermissionFlagDefinitionInput.isRelevantForAgents ?? false,
isRelevantForUsers:
@@ -10,7 +10,7 @@ export const fromFlatPermissionFlagDefinitionToPermissionFlagDefinitionDto = (
label: flatDefinition.label,
description: flatDefinition.description,
iconKey: flatDefinition.iconKey,
category: flatDefinition.category,
permissionType: flatDefinition.permissionType,
isRelevantForAgents: flatDefinition.isRelevantForAgents,
isRelevantForUsers: flatDefinition.isRelevantForUsers,
isRelevantForApiKeys: flatDefinition.isRelevantForApiKeys,
@@ -0,0 +1,7 @@
export const PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES = [
'settings',
'tool',
] as const;
export type PermissionFlagDefinitionPermissionType =
(typeof PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES)[number];
@@ -10,6 +10,10 @@ import {
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES,
type PermissionFlagDefinitionPermissionType,
} from 'src/engine/metadata-modules/permission-flag-definition/constants/permission-flag-definition-permission-type.constant';
@InputType()
export class CreatePermissionFlagDefinitionInput {
@@ -43,9 +47,9 @@ export class CreatePermissionFlagDefinitionInput {
@Field({ nullable: true })
iconKey?: string;
@IsIn(['settings', 'tool'])
@Field()
category: 'settings' | 'tool';
@IsIn(PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES)
@Field(() => String)
permissionType: PermissionFlagDefinitionPermissionType;
@IsBoolean()
@IsOptional()
@@ -11,6 +11,10 @@ import {
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES,
type PermissionFlagDefinitionPermissionType,
} from 'src/engine/metadata-modules/permission-flag-definition/constants/permission-flag-definition-permission-type.constant';
@ObjectType('PermissionFlagDefinition')
export class PermissionFlagDefinitionDTO {
@@ -44,9 +48,9 @@ export class PermissionFlagDefinitionDTO {
@Field(() => String, { nullable: true })
iconKey: string | null;
@IsIn(['settings', 'tool'])
@Field()
category: 'settings' | 'tool';
@IsIn(PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES)
@Field(() => String)
permissionType: PermissionFlagDefinitionPermissionType;
@IsBoolean()
@Field()
@@ -12,6 +12,10 @@ import {
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES,
type PermissionFlagDefinitionPermissionType,
} from 'src/engine/metadata-modules/permission-flag-definition/constants/permission-flag-definition-permission-type.constant';
@InputType()
export class UpdatePermissionFlagDefinitionInputUpdates {
@@ -31,9 +35,9 @@ export class UpdatePermissionFlagDefinitionInputUpdates {
iconKey?: string;
@IsOptional()
@IsIn(['settings', 'tool'])
@Field({ nullable: true })
category?: 'settings' | 'tool';
@IsIn(PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES)
@Field(() => String, { nullable: true })
permissionType?: PermissionFlagDefinitionPermissionType;
@IsOptional()
@IsBoolean()
@@ -8,6 +8,7 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { type PermissionFlagDefinitionPermissionType } from 'src/engine/metadata-modules/permission-flag-definition/constants/permission-flag-definition-permission-type.constant';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity('permissionFlagDefinition')
@@ -33,7 +34,7 @@ export class PermissionFlagDefinitionEntity extends SyncableEntity {
iconKey: string | null;
@Column({ nullable: false, type: 'varchar' })
category: 'settings' | 'tool';
permissionType: PermissionFlagDefinitionPermissionType;
@Column({ nullable: false, type: 'boolean', default: false })
isRelevantForAgents: boolean;
@@ -9,7 +9,7 @@ export enum PermissionFlagDefinitionExceptionCode {
PERMISSION_FLAG_DEFINITION_ALREADY_EXISTS = 'PERMISSION_FLAG_DEFINITION_ALREADY_EXISTS',
INVALID_PERMISSION_FLAG_DEFINITION_INPUT = 'INVALID_PERMISSION_FLAG_DEFINITION_INPUT',
INVALID_PERMISSION_FLAG_DEFINITION_KEY = 'INVALID_PERMISSION_FLAG_DEFINITION_KEY',
INVALID_PERMISSION_FLAG_DEFINITION_CATEGORY = 'INVALID_PERMISSION_FLAG_DEFINITION_CATEGORY',
INVALID_PERMISSION_FLAG_DEFINITION_PERMISSION_TYPE = 'INVALID_PERMISSION_FLAG_DEFINITION_PERMISSION_TYPE',
PERMISSION_FLAG_DEFINITION_KEY_IMMUTABLE = 'PERMISSION_FLAG_DEFINITION_KEY_IMMUTABLE',
PERMISSION_FLAG_DEFINITION_APPLICATION_IMMUTABLE = 'PERMISSION_FLAG_DEFINITION_APPLICATION_IMMUTABLE',
PERMISSION_FLAG_DEFINITION_IS_STANDARD = 'PERMISSION_FLAG_DEFINITION_IS_STANDARD',
@@ -28,8 +28,8 @@ const getPermissionFlagDefinitionExceptionUserFriendlyMessage = (
return msg`Invalid permission flag definition input.`;
case PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_KEY:
return msg`Invalid permission flag definition key.`;
case PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_CATEGORY:
return msg`Invalid permission flag definition category.`;
case PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_PERMISSION_TYPE:
return msg`Invalid permission flag definition permission type.`;
case PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_KEY_IMMUTABLE:
return msg`Permission flag definition key cannot be changed.`;
case PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_APPLICATION_IMMUTABLE:
@@ -1,11 +1,8 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
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 { FlatPermissionFlagDefinitionModule } from 'src/engine/metadata-modules/flat-permission-flag-definition/flat-permission-flag-definition.module';
import { PermissionFlagDefinitionEntity } from 'src/engine/metadata-modules/permission-flag-definition/entities/permission-flag-definition.entity';
import { PermissionFlagDefinitionResolver } from 'src/engine/metadata-modules/permission-flag-definition/permission-flag-definition.resolver';
import { PermissionFlagDefinitionService } from 'src/engine/metadata-modules/permission-flag-definition/permission-flag-definition.service';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@@ -14,10 +11,6 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
@Module({
imports: [
TypeOrmModule.forFeature([
ApplicationEntity,
PermissionFlagDefinitionEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
WorkspaceCacheStorageModule,
@@ -1,60 +1,41 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
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 { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { fromCreatePermissionFlagDefinitionInputToFlatPermissionFlagDefinitionToCreate } from 'src/engine/metadata-modules/flat-permission-flag-definition/utils/from-create-permission-flag-definition-input-to-flat-permission-flag-definition-to-create.util';
import { fromDeletePermissionFlagDefinitionInputToFlatPermissionFlagDefinitionOrThrow } from 'src/engine/metadata-modules/flat-permission-flag-definition/utils/from-delete-permission-flag-definition-input-to-flat-permission-flag-definition-or-throw.util';
import { fromFlatPermissionFlagDefinitionToPermissionFlagDefinitionDto } from 'src/engine/metadata-modules/flat-permission-flag-definition/utils/from-flat-permission-flag-definition-to-permission-flag-definition-dto.util';
import { fromPermissionFlagDefinitionEntityToFlatPermissionFlagDefinition } from 'src/engine/metadata-modules/flat-permission-flag-definition/utils/from-permission-flag-definition-entity-to-flat-permission-flag-definition.util';
import { fromUpdatePermissionFlagDefinitionInputToFlatPermissionFlagDefinitionToUpdateOrThrow } from 'src/engine/metadata-modules/flat-permission-flag-definition/utils/from-update-permission-flag-definition-input-to-flat-permission-flag-definition-to-update-or-throw.util';
import { type CreatePermissionFlagDefinitionInput } from 'src/engine/metadata-modules/permission-flag-definition/dtos/create-permission-flag-definition.input';
import { type PermissionFlagDefinitionDTO } from 'src/engine/metadata-modules/permission-flag-definition/dtos/permission-flag-definition.dto';
import { type UpdatePermissionFlagDefinitionInput } from 'src/engine/metadata-modules/permission-flag-definition/dtos/update-permission-flag-definition.input';
import { PermissionFlagDefinitionEntity } from 'src/engine/metadata-modules/permission-flag-definition/entities/permission-flag-definition.entity';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class PermissionFlagDefinitionService {
constructor(
@InjectRepository(PermissionFlagDefinitionEntity)
private readonly permissionFlagDefinitionRepository: Repository<PermissionFlagDefinitionEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
) {}
async findAll(workspaceId: string): Promise<PermissionFlagDefinitionDTO[]> {
const [definitions, applications] = await Promise.all([
this.permissionFlagDefinitionRepository.find({
where: { workspaceId },
order: { createdAt: 'ASC' },
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
}),
]);
const { flatPermissionFlagDefinitionMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagDefinitionMaps'],
},
);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
return definitions
.map((entity) =>
fromPermissionFlagDefinitionEntityToFlatPermissionFlagDefinition({
entity,
applicationIdToUniversalIdentifierMap,
}),
)
return Object.values(flatPermissionFlagDefinitionMaps.byUniversalIdentifier)
.filter(isDefined)
.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
.map(fromFlatPermissionFlagDefinitionToPermissionFlagDefinitionDto);
}
@@ -62,28 +43,25 @@ export class PermissionFlagDefinitionService {
id: string,
workspaceId: string,
): Promise<PermissionFlagDefinitionDTO | null> {
const [definition, applications] = await Promise.all([
this.permissionFlagDefinitionRepository.findOne({
where: { id, workspaceId },
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
}),
]);
const { flatPermissionFlagDefinitionMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagDefinitionMaps'],
},
);
if (!isDefined(definition)) {
const flatPermissionFlagDefinition = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatPermissionFlagDefinitionMaps,
});
if (!isDefined(flatPermissionFlagDefinition)) {
return null;
}
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
return fromFlatPermissionFlagDefinitionToPermissionFlagDefinitionDto(
fromPermissionFlagDefinitionEntityToFlatPermissionFlagDefinition({
entity: definition,
applicationIdToUniversalIdentifierMap,
}),
flatPermissionFlagDefinition,
);
}
@@ -24,7 +24,7 @@ export const permissionFlagDefinitionGraphqlApiExceptionHandler = (
throw new ForbiddenError(error);
case PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_INPUT:
case PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_KEY:
case PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_CATEGORY:
case PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_PERMISSION_TYPE:
case PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_KEY_IMMUTABLE:
case PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_APPLICATION_IMMUTABLE:
case PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_IN_USE:
@@ -13,10 +13,7 @@ import { permissionFlagDefinitionGraphqlApiExceptionHandler } from 'src/engine/m
export class PermissionFlagDefinitionGraphqlApiExceptionFilter
implements ExceptionFilter
{
catch(
exception: PermissionFlagDefinitionException,
_host: ExecutionContext,
) {
catch(exception: PermissionFlagDefinitionException, _host: ExecutionContext) {
return permissionFlagDefinitionGraphqlApiExceptionHandler(exception);
}
}
@@ -1,5 +1,7 @@
import { type PermissionFlagType } from 'twenty-shared/constants';
import { type PermissionFlagDefinitionPermissionType } from 'src/engine/metadata-modules/permission-flag-definition/constants/permission-flag-definition-permission-type.constant';
// Each entry maps a PermissionFlagType enum key to a stable universalIdentifier
// plus the metadata used to render the flag in the Roles UI. Adding a new
// built-in flag requires:
@@ -14,7 +16,7 @@ export type StandardPermissionFlagDefinitionMetadata = {
label: string;
description: string | null;
iconKey: string | null;
category: 'settings' | 'tool';
permissionType: PermissionFlagDefinitionPermissionType;
isRelevantForAgents: boolean;
isRelevantForUsers: boolean;
isRelevantForApiKeys: boolean;
@@ -26,7 +28,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'API Keys and Webhooks',
description: 'Manage API keys and webhooks',
iconKey: 'IconApi',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -36,7 +38,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Workspace',
description: 'Manage workspace settings',
iconKey: 'IconBuildingSkyscraper',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -46,7 +48,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Workspace Members',
description: 'Manage workspace members',
iconKey: 'IconUsers',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -56,7 +58,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Roles',
description: 'Manage roles and permissions',
iconKey: 'IconShieldLock',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -66,7 +68,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Data Model',
description: 'Manage objects and fields',
iconKey: 'IconHierarchy2',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -76,7 +78,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Security',
description: 'Manage security settings',
iconKey: 'IconLock',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -86,7 +88,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Workflows',
description: 'Manage workflows and logic functions',
iconKey: 'IconSettingsAutomation',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -96,7 +98,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Impersonate',
description: 'Impersonate other workspace members',
iconKey: 'IconUser',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -106,7 +108,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'SSO Bypass',
description: 'Bypass single sign-on requirements',
iconKey: 'IconKey',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -116,7 +118,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Applications',
description: 'Manage installed applications',
iconKey: 'IconApps',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -126,7 +128,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Marketplace Apps',
description: 'Browse and install marketplace apps',
iconKey: 'IconShoppingBag',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -136,7 +138,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Layouts',
description: 'Manage page layouts',
iconKey: 'IconLayout',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -146,7 +148,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Billing',
description: 'Manage billing and subscriptions',
iconKey: 'IconCreditCard',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -156,7 +158,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'AI Settings',
description: 'Manage AI provider settings',
iconKey: 'IconBrain',
category: 'settings',
permissionType: 'settings',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -166,7 +168,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'AI',
description: 'Use AI features',
iconKey: 'IconSparkles',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: true,
isRelevantForApiKeys: true,
@@ -176,7 +178,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Views',
description: 'Read and write views',
iconKey: 'IconLayoutGrid',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: true,
isRelevantForApiKeys: true,
@@ -186,7 +188,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Upload File',
description: 'Upload files to the workspace',
iconKey: 'IconUpload',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: true,
isRelevantForApiKeys: true,
@@ -196,7 +198,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Download File',
description: 'Download files from the workspace',
iconKey: 'IconDownload',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: true,
isRelevantForApiKeys: true,
@@ -206,7 +208,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Send Email',
description: 'Send emails on behalf of users',
iconKey: 'IconAt',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: false,
isRelevantForApiKeys: false,
@@ -216,7 +218,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'HTTP Request',
description: 'Make outbound HTTP requests',
iconKey: 'IconApi',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: false,
isRelevantForApiKeys: false,
@@ -226,7 +228,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Code Interpreter',
description: 'Run code to analyze files and data',
iconKey: 'IconCode',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -236,7 +238,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Import CSV',
description: 'Import records from CSV files',
iconKey: 'IconFileImport',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -246,7 +248,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Export CSV',
description: 'Export records to CSV files',
iconKey: 'IconFileExport',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: false,
isRelevantForUsers: true,
isRelevantForApiKeys: false,
@@ -256,7 +258,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Connected Accounts',
description: 'Read connected email and calendar accounts',
iconKey: 'IconMail',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: false,
isRelevantForApiKeys: false,
@@ -266,7 +268,7 @@ export const STANDARD_PERMISSION_FLAG_DEFINITION = {
label: 'Profile Information',
description: 'Read profile information',
iconKey: 'IconUser',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: false,
isRelevantForApiKeys: false,
@@ -31,7 +31,7 @@ export const createStandardPermissionFlagDefinitionFlatMetadata = ({
label: metadata.label,
description: metadata.description,
iconKey: metadata.iconKey,
category: metadata.category,
permissionType: metadata.permissionType,
isRelevantForAgents: metadata.isRelevantForAgents,
isRelevantForUsers: metadata.isRelevantForUsers,
isRelevantForApiKeys: metadata.isRelevantForApiKeys,
@@ -7,6 +7,7 @@ import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-enti
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type FlatPermissionFlagDefinition } from 'src/engine/metadata-modules/flat-permission-flag-definition/types/flat-permission-flag-definition.type';
import { type PermissionFlagDefinitionPermissionType } from 'src/engine/metadata-modules/permission-flag-definition/constants/permission-flag-definition-permission-type.constant';
import { PermissionFlagDefinitionExceptionCode } from 'src/engine/metadata-modules/permission-flag-definition/permission-flag-definition.exception';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import { FlatPermissionFlagDefinitionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-definition-validator.service';
@@ -21,7 +22,7 @@ const buildFlatDefinition = (
label: 'Test Flag',
description: 'A flag for tests',
iconKey: 'IconTest',
category: 'tool',
permissionType: 'tool',
isRelevantForAgents: true,
isRelevantForUsers: false,
isRelevantForApiKeys: false,
@@ -100,17 +101,17 @@ describe('FlatPermissionFlagDefinitionValidatorService', () => {
);
});
it('rejects an unknown category', () => {
it('rejects an unknown permission type', () => {
const result = service.validateFlatPermissionFlagDefinitionCreation(
buildArgs(
buildFlatDefinition({
category: 'invalid' as 'settings' | 'tool',
permissionType: 'invalid' as PermissionFlagDefinitionPermissionType,
}),
),
);
expect(result.errors.map((error) => error.code)).toContain(
PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_CATEGORY,
PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_PERMISSION_TYPE,
);
});
@@ -131,9 +132,50 @@ describe('FlatPermissionFlagDefinitionValidatorService', () => {
PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_ALREADY_EXISTS,
);
});
it('rejects a duplicate universal identifier', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagDefinitionCreation(
buildArgs(
buildFlatDefinition({
id: '00000000-0000-0000-0000-000000000002',
key: 'ANOTHER_TEST_FLAG',
}),
optimisticMaps,
),
);
expect(result.errors.map((error) => error.code)).toContain(
PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_ALREADY_EXISTS,
);
});
});
describe('validateFlatPermissionFlagDefinitionUpdate', () => {
it('passes for a valid update', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagDefinitionUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: { label: 'Updated Label' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagDefinitionMaps: optimisticMaps,
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagDefinitionValidatorService['validateFlatPermissionFlagDefinitionUpdate']
>[0]);
expect(result.errors).toHaveLength(0);
});
it('rejects updating the immutable key field', () => {
const existing = buildFlatDefinition({ key: 'ORIGINAL_KEY' });
const optimisticMaps = buildEmptyMaps();
@@ -172,9 +214,101 @@ describe('FlatPermissionFlagDefinitionValidatorService', () => {
PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_NOT_FOUND,
);
});
it('rejects updating a standard definition from a custom application', () => {
const existing = buildFlatDefinition({
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION.universalIdentifier,
});
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagDefinitionUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: { label: 'Updated Label' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagDefinitionMaps: optimisticMaps,
},
buildOptions: {
isSystemBuild: false,
applicationUniversalIdentifier:
'00000000-0000-0000-0000-000000000aaa',
},
} as unknown as Parameters<
FlatPermissionFlagDefinitionValidatorService['validateFlatPermissionFlagDefinitionUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toContain(
PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_IS_STANDARD,
);
});
it('rejects updating to an unknown permission type', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagDefinitionUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: {
permissionType: 'invalid' as PermissionFlagDefinitionPermissionType,
},
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagDefinitionMaps: optimisticMaps,
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagDefinitionValidatorService['validateFlatPermissionFlagDefinitionUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toContain(
PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_PERMISSION_TYPE,
);
});
});
describe('validateFlatPermissionFlagDefinitionDeletion', () => {
it('passes for a valid deletion', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagDefinitionDeletion({
flatEntityToValidate: existing,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagDefinitionMaps: optimisticMaps,
flatPermissionFlagMaps: buildEmptyPermissionFlagMaps(),
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagDefinitionValidatorService['validateFlatPermissionFlagDefinitionDeletion']
>[0]);
expect(result.errors).toHaveLength(0);
});
it('returns not-found if no existing definition matches the universalIdentifier', () => {
const result = service.validateFlatPermissionFlagDefinitionDeletion({
flatEntityToValidate: buildFlatDefinition({
universalIdentifier: '00000000-0000-0000-0000-deadbeefdead',
}),
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagDefinitionMaps: buildEmptyMaps(),
flatPermissionFlagMaps: buildEmptyPermissionFlagMaps(),
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagDefinitionValidatorService['validateFlatPermissionFlagDefinitionDeletion']
>[0]);
expect(result.errors.map((error) => error.code)).toContain(
PermissionFlagDefinitionExceptionCode.PERMISSION_FLAG_DEFINITION_NOT_FOUND,
);
});
it('rejects deleting a standard definition from a custom application', () => {
const existing = buildFlatDefinition({
applicationUniversalIdentifier:
@@ -6,6 +6,7 @@ import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES } from 'src/engine/metadata-modules/permission-flag-definition/constants/permission-flag-definition-permission-type.constant';
import { PermissionFlagDefinitionExceptionCode } from 'src/engine/metadata-modules/permission-flag-definition/permission-flag-definition.exception';
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
@@ -14,8 +15,6 @@ import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
const VALID_CATEGORIES = ['settings', 'tool'] as const;
@Injectable()
export class FlatPermissionFlagDefinitionValidatorService {
validateFlatPermissionFlagDefinitionCreation({
@@ -78,14 +77,14 @@ export class FlatPermissionFlagDefinitionValidatorService {
}
if (
!VALID_CATEGORIES.includes(
flatPermissionFlagDefinitionToValidate.category as (typeof VALID_CATEGORIES)[number],
!PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES.includes(
flatPermissionFlagDefinitionToValidate.permissionType,
)
) {
validationResult.errors.push({
code: PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_CATEGORY,
message: t`Permission flag definition category must be 'settings' or 'tool'`,
userFriendlyMessage: msg`Invalid category`,
code: PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_PERMISSION_TYPE,
message: t`Permission flag definition permission type must be 'settings' or 'tool'`,
userFriendlyMessage: msg`Invalid permission type`,
});
}
@@ -152,15 +151,15 @@ export class FlatPermissionFlagDefinitionValidatorService {
}
if (
isDefined(flatEntityUpdate.category) &&
!VALID_CATEGORIES.includes(
flatEntityUpdate.category as (typeof VALID_CATEGORIES)[number],
isDefined(flatEntityUpdate.permissionType) &&
!PERMISSION_FLAG_DEFINITION_PERMISSION_TYPES.includes(
flatEntityUpdate.permissionType,
)
) {
validationResult.errors.push({
code: PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_CATEGORY,
message: t`Permission flag definition category must be 'settings' or 'tool'`,
userFriendlyMessage: msg`Invalid category`,
code: PermissionFlagDefinitionExceptionCode.INVALID_PERMISSION_FLAG_DEFINITION_PERMISSION_TYPE,
message: t`Permission flag definition permission type must be 'settings' or 'tool'`,
userFriendlyMessage: msg`Invalid permission type`,
});
}