Migrate field permission to syncable entity (#18751)

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Abdul Rahman
2026-03-27 19:17:27 +05:30
committed by GitHub
parent c2b058a6a7
commit 5efe69f8d3
105 changed files with 2956 additions and 490 deletions
@@ -343,6 +343,7 @@ type Field {
defaultValue: JSON
options: JSON
settings: JSON
objectMetadataId: UUID!
isLabelSyncedWithName: Boolean
morphId: UUID
createdAt: DateTime!
@@ -540,6 +541,7 @@ input FieldFilter {
isActive: BooleanFieldComparison
isSystem: BooleanFieldComparison
isUIReadOnly: BooleanFieldComparison
objectMetadataId: UUIDFilterComparison
}
input IndexFilter {
@@ -3090,6 +3092,7 @@ enum AllMetadataName {
navigationMenuItem
permissionFlag
objectPermission
fieldPermission
frontComponent
webhook
}
@@ -4273,8 +4276,8 @@ input CreateFieldInput {
defaultValue: JSON
options: JSON
settings: JSON
isLabelSyncedWithName: Boolean
objectMetadataId: UUID!
isLabelSyncedWithName: Boolean
isRemoteCreation: Boolean
relationCreationPayload: JSON
morphRelationsCreationPayload: [JSON!]
@@ -4302,6 +4305,7 @@ input UpdateFieldInput {
defaultValue: JSON
options: JSON
settings: JSON
objectMetadataId: UUID
isLabelSyncedWithName: Boolean
morphRelationsUpdatePayload: [JSON!]
}
@@ -296,6 +296,7 @@ export interface Field {
defaultValue?: Scalars['JSON']
options?: Scalars['JSON']
settings?: Scalars['JSON']
objectMetadataId: Scalars['UUID']
isLabelSyncedWithName?: Scalars['Boolean']
morphId?: Scalars['UUID']
createdAt: Scalars['DateTime']
@@ -2676,7 +2677,7 @@ export interface CollectionHash {
__typename: 'CollectionHash'
}
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'frontComponent' | 'webhook'
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook'
export interface MinimalObjectMetadata {
id: Scalars['UUID']
@@ -3361,6 +3362,7 @@ export interface FieldGenqlSelection{
defaultValue?: boolean | number
options?: boolean | number
settings?: boolean | number
objectMetadataId?: boolean | number
isLabelSyncedWithName?: boolean | number
morphId?: boolean | number
createdAt?: boolean | number
@@ -3477,7 +3479,7 @@ export interface ObjectGenqlSelection{
__scalar?: boolean | number
}
export interface FieldFilter {and?: (FieldFilter[] | null),or?: (FieldFilter[] | null),id?: (UUIDFilterComparison | null),isCustom?: (BooleanFieldComparison | null),isActive?: (BooleanFieldComparison | null),isSystem?: (BooleanFieldComparison | null),isUIReadOnly?: (BooleanFieldComparison | null)}
export interface FieldFilter {and?: (FieldFilter[] | null),or?: (FieldFilter[] | null),id?: (UUIDFilterComparison | null),isCustom?: (BooleanFieldComparison | null),isActive?: (BooleanFieldComparison | null),isSystem?: (BooleanFieldComparison | null),isUIReadOnly?: (BooleanFieldComparison | null),objectMetadataId?: (UUIDFilterComparison | null)}
export interface IndexFilter {and?: (IndexFilter[] | null),or?: (IndexFilter[] | null),id?: (UUIDFilterComparison | null),isCustom?: (BooleanFieldComparison | null)}
@@ -6529,7 +6531,7 @@ export interface CreateOneFieldMetadataInput {
/** The record to create */
field: CreateFieldInput}
export interface CreateFieldInput {type: FieldMetadataType,name: Scalars['String'],label: Scalars['String'],description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),isCustom?: (Scalars['Boolean'] | null),isActive?: (Scalars['Boolean'] | null),isSystem?: (Scalars['Boolean'] | null),isUIReadOnly?: (Scalars['Boolean'] | null),isNullable?: (Scalars['Boolean'] | null),isUnique?: (Scalars['Boolean'] | null),defaultValue?: (Scalars['JSON'] | null),options?: (Scalars['JSON'] | null),settings?: (Scalars['JSON'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),objectMetadataId: Scalars['UUID'],isRemoteCreation?: (Scalars['Boolean'] | null),relationCreationPayload?: (Scalars['JSON'] | null),morphRelationsCreationPayload?: (Scalars['JSON'][] | null)}
export interface CreateFieldInput {type: FieldMetadataType,name: Scalars['String'],label: Scalars['String'],description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),isCustom?: (Scalars['Boolean'] | null),isActive?: (Scalars['Boolean'] | null),isSystem?: (Scalars['Boolean'] | null),isUIReadOnly?: (Scalars['Boolean'] | null),isNullable?: (Scalars['Boolean'] | null),isUnique?: (Scalars['Boolean'] | null),defaultValue?: (Scalars['JSON'] | null),options?: (Scalars['JSON'] | null),settings?: (Scalars['JSON'] | null),objectMetadataId: Scalars['UUID'],isLabelSyncedWithName?: (Scalars['Boolean'] | null),isRemoteCreation?: (Scalars['Boolean'] | null),relationCreationPayload?: (Scalars['JSON'] | null),morphRelationsCreationPayload?: (Scalars['JSON'][] | null)}
export interface UpdateOneFieldMetadataInput {
/** The id of the record to update */
@@ -6537,7 +6539,7 @@ id: Scalars['UUID'],
/** The record to update */
update: UpdateFieldInput}
export interface UpdateFieldInput {universalIdentifier?: (Scalars['String'] | null),name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),isSystem?: (Scalars['Boolean'] | null),isUIReadOnly?: (Scalars['Boolean'] | null),isNullable?: (Scalars['Boolean'] | null),isUnique?: (Scalars['Boolean'] | null),defaultValue?: (Scalars['JSON'] | null),options?: (Scalars['JSON'] | null),settings?: (Scalars['JSON'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),morphRelationsUpdatePayload?: (Scalars['JSON'][] | null)}
export interface UpdateFieldInput {universalIdentifier?: (Scalars['String'] | null),name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),isSystem?: (Scalars['Boolean'] | null),isUIReadOnly?: (Scalars['Boolean'] | null),isNullable?: (Scalars['Boolean'] | null),isUnique?: (Scalars['Boolean'] | null),defaultValue?: (Scalars['JSON'] | null),options?: (Scalars['JSON'] | null),settings?: (Scalars['JSON'] | null),objectMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),morphRelationsUpdatePayload?: (Scalars['JSON'][] | null)}
export interface DeleteOneFieldInput {
/** The id of the field to delete. */
@@ -9427,6 +9429,7 @@ export const enumAllMetadataName = {
navigationMenuItem: 'navigationMenuItem' as const,
permissionFlag: 'permissionFlag' as const,
objectPermission: 'objectPermission' as const,
fieldPermission: 'fieldPermission' as const,
frontComponent: 'frontComponent' as const,
webhook: 'webhook' as const
}
@@ -777,6 +777,9 @@ export default {
"settings": [
15
],
"objectMetadataId": [
3
],
"isLabelSyncedWithName": [
6
],
@@ -1164,6 +1167,9 @@ export default {
"isUIReadOnly": [
43
],
"objectMetadataId": [
42
],
"__typename": [
1
]
@@ -10754,12 +10760,12 @@ export default {
"settings": [
15
],
"isLabelSyncedWithName": [
6
],
"objectMetadataId": [
3
],
"isLabelSyncedWithName": [
6
],
"isRemoteCreation": [
6
],
@@ -10824,6 +10830,9 @@ export default {
"settings": [
15
],
"objectMetadataId": [
3
],
"isLabelSyncedWithName": [
6
],
@@ -266,6 +266,7 @@ export enum AllMetadataName {
agent = 'agent',
commandMenuItem = 'commandMenuItem',
fieldMetadata = 'fieldMetadata',
fieldPermission = 'fieldPermission',
frontComponent = 'frontComponent',
index = 'index',
logicFunction = 'logicFunction',
@@ -1748,6 +1749,7 @@ export type Field = {
morphRelations?: Maybe<Array<Relation>>;
name: Scalars['String'];
object?: Maybe<Object>;
objectMetadataId: Scalars['UUID'];
options?: Maybe<Scalars['JSON']>;
relation?: Maybe<Relation>;
settings?: Maybe<Scalars['JSON']>;
@@ -1794,6 +1796,7 @@ export type FieldFilter = {
isCustom?: InputMaybe<BooleanFieldComparison>;
isSystem?: InputMaybe<BooleanFieldComparison>;
isUIReadOnly?: InputMaybe<BooleanFieldComparison>;
objectMetadataId?: InputMaybe<UuidFilterComparison>;
or?: InputMaybe<Array<FieldFilter>>;
};
@@ -5245,6 +5248,7 @@ export type UpdateFieldInput = {
label?: InputMaybe<Scalars['String']>;
morphRelationsUpdatePayload?: InputMaybe<Array<Scalars['JSON']>>;
name?: InputMaybe<Scalars['String']>;
objectMetadataId?: InputMaybe<Scalars['UUID']>;
options?: InputMaybe<Scalars['JSON']>;
settings?: InputMaybe<Scalars['JSON']>;
universalIdentifier?: InputMaybe<Scalars['String']>;
@@ -32,6 +32,7 @@ export const useMetadataErrorHandler = () => {
logicFunction: t`logic function`,
permissionFlag: t`permission flag`,
objectPermission: t`object permission`,
fieldPermission: t`field permission`,
role: t`role`,
roleTarget: t`role target`,
agent: t`agent`,
@@ -20,12 +20,14 @@ export type FieldMetadataItem = Omit<
| '__typename'
| 'applicationId'
| 'defaultValue'
| 'objectMetadataId'
| 'options'
| 'relation'
| 'morphRelations'
> & {
__typename?: string;
applicationId?: string;
objectMetadataId?: string;
defaultValue?: any;
options?: FieldMetadataItemOption[] | null;
relation?: FieldMetadataItemRelation | null;
@@ -0,0 +1,111 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, IsNull, type Repository } from 'typeorm';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@Command({
name: 'upgrade:1-20:identify-field-permission-metadata',
description:
'Identify field permission metadata (backfill universalIdentifier and applicationId)',
})
export class IdentifyFieldPermissionMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
private hasRunOnce = false;
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
options,
}: RunOnWorkspaceArgs): Promise<void> {
if (this.hasRunOnce) {
this.logger.log(
'Skipping has already been run once IdentifyFieldPermissionMetadataCommand',
);
return;
}
if (options.dryRun) {
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const repository = queryRunner.manager.getRepository(
FieldPermissionEntity,
);
const withNullApplicationId = await repository.find({
where: { applicationId: IsNull() },
relations: ['role'],
});
const toUpdate = withNullApplicationId.filter((fieldPermission) =>
isDefined(fieldPermission.role?.applicationId),
);
const toRemove = withNullApplicationId.filter(
(fieldPermission) => !isDefined(fieldPermission.role?.applicationId),
);
for (const fieldPermission of toUpdate) {
fieldPermission.applicationId = fieldPermission.role!.applicationId;
fieldPermission.universalIdentifier =
fieldPermission.universalIdentifier ?? v4();
}
if (toUpdate.length > 0) {
await repository.save(toUpdate);
}
if (toRemove.length > 0) {
await repository.remove(toRemove);
}
const withNullUniversalIdentifier = await repository.find({
where: { universalIdentifier: IsNull() },
});
for (const fieldPermission of withNullUniversalIdentifier) {
fieldPermission.universalIdentifier = v4();
}
if (withNullUniversalIdentifier.length > 0) {
await repository.save(withNullUniversalIdentifier);
}
await queryRunner.commitTransaction();
this.logger.log(
'Successfully run IdentifyFieldPermissionMetadataCommand',
);
this.hasRunOnce = true;
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Rolling back IdentifyFieldPermissionMetadataCommand: ${error.message}`,
);
throw error;
} finally {
await queryRunner.release();
}
}
}
@@ -0,0 +1,72 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, type Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773400000000-make-field-permission-universal-identifier-and-application-id-not-null.util';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@Command({
name: 'upgrade:1-20:make-field-permission-universal-identifier-and-application-id-not-nullable-migration',
description:
'Set NOT NULL on fieldPermission universalIdentifier and applicationId, add unique index and FK (run identify-field-permission-metadata first)',
})
export class MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
private hasRunOnce = false;
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
options,
}: RunOnWorkspaceArgs): Promise<void> {
if (this.hasRunOnce) {
this.logger.log(
'Skipping has already been run once MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
);
return;
}
if (options.dryRun) {
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries(
queryRunner,
);
await queryRunner.commitTransaction();
this.logger.log(
'Successfully run MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
);
this.hasRunOnce = true;
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Rolling back MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
);
throw error;
} finally {
await queryRunner.release();
}
}
}
@@ -6,8 +6,10 @@ import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upg
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
import { IdentifyFieldPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command';
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
import { MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-field-permission-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command';
@@ -60,6 +62,8 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyObjectPermissionMetadataCommand,
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyFieldPermissionMetadataCommand,
MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
BackfillCommandMenuItemsCommand,
BackfillNavigationMenuItemTypeCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
@@ -76,6 +80,8 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyObjectPermissionMetadataCommand,
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyFieldPermissionMetadataCommand,
MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
BackfillCommandMenuItemsCommand,
BackfillNavigationMenuItemTypeCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
@@ -36,10 +36,12 @@ import { SeedServerIdCommand } from 'src/database/commands/upgrade-version-comma
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
import { IdentifyFieldPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.command';
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
import { MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-field-permission-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command';
@@ -101,6 +103,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly identifyObjectPermissionMetadataCommand: IdentifyObjectPermissionMetadataCommand,
protected readonly makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly identifyFieldPermissionMetadataCommand: IdentifyFieldPermissionMetadataCommand,
protected readonly makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand,
protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand,
protected readonly deleteOrphanNavigationMenuItemsCommand: DeleteOrphanNavigationMenuItemsCommand,
@@ -164,6 +168,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.identifyObjectPermissionMetadataCommand,
this
.makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this.identifyFieldPermissionMetadataCommand,
this
.makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this.backfillNavigationMenuItemTypeCommand,
this.migrateRichTextToTextCommand,
this.deleteOrphanNavigationMenuItemsCommand,
@@ -0,0 +1,25 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddUniversalIdentifierAndApplicationIdToFieldPermission1773400000000
implements MigrationInterface
{
name = 'AddUniversalIdentifierAndApplicationIdToFieldPermission1773400000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ADD "universalIdentifier" uuid`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ADD "applicationId" uuid`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" DROP COLUMN IF EXISTS "applicationId"`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" DROP COLUMN IF EXISTS "universalIdentifier"`,
);
}
}
@@ -0,0 +1,58 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
import { makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773400000000-make-field-permission-universal-identifier-and-application-id-not-null.util';
export class MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNull1773400000001
implements MigrationInterface
{
name =
'MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNull1773400000001';
public async up(queryRunner: QueryRunner): Promise<void> {
const savepointName =
'sp_make_field_permission_universal_identifier_and_application_id_not_null';
try {
await queryRunner.query(`SAVEPOINT ${savepointName}`);
await makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries(
queryRunner,
);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (e) {
try {
await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (rollbackError) {
// oxlint-disable-next-line no-console
console.error(
'Failed to rollback to savepoint in MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNull1773400000001',
rollbackError,
);
throw rollbackError;
}
// oxlint-disable-next-line no-console
console.error(
'Swallowing MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNull1773400000001 error',
e,
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" DROP CONSTRAINT IF EXISTS "FK_71cc60c4a1c9f8a7c434d91d38c"`,
);
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."IDX_0dedb90c717e179ef653c512b9"`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ALTER COLUMN "applicationId" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ALTER COLUMN "universalIdentifier" DROP NOT NULL`,
);
}
}
@@ -0,0 +1,20 @@
import { type QueryRunner } from 'typeorm';
export const makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries =
async (queryRunner: QueryRunner): Promise<void> => {
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ALTER COLUMN "universalIdentifier" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ALTER COLUMN "applicationId" SET NOT NULL`,
);
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."IDX_0dedb90c717e179ef653c512b9"`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_0dedb90c717e179ef653c512b9" ON "core"."fieldPermission" ("workspaceId", "universalIdentifier")`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ADD CONSTRAINT "FK_71cc60c4a1c9f8a7c434d91d38c" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
};
@@ -154,6 +154,7 @@ export const mockPersonFlatObjectMetadata = (
universalIdentifier: objectMetadataId,
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
fieldIds: mockFieldMetadatas.map((field) => field.id),
viewIds: [],
applicationId: 'test-application-id',
@@ -170,6 +171,7 @@ export const mockPersonFlatObjectMetadata = (
),
viewUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
imageIdentifierFieldMetadataUniversalIdentifier: null,
@@ -94,6 +94,7 @@ export const fromFieldManifestToUniversalFlatFieldMetadata = ({
relationTargetObjectMetadataUniversalIdentifier,
viewFieldUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
@@ -36,6 +36,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({
fieldUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier:
objectManifest.labelIdentifierFieldMetadataUniversalIdentifier,
@@ -31,6 +31,7 @@ export const fromRoleManifestToUniversalFlatRole = ({
canBeAssignedToApiKeys: roleManifest.canBeAssignedToApiKeys ?? true,
roleTargetUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
permissionFlagUniversalIdentifiers: [],
rowLevelPermissionPredicateUniversalIdentifiers: [],
rowLevelPermissionPredicateGroupUniversalIdentifiers: [],
@@ -24,6 +24,7 @@ const mockObjectMetadata: FlatObjectMetadata = {
isSearchable: true,
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
fieldIds: [],
viewIds: [],
applicationId: 'test-application-id',
@@ -40,6 +41,7 @@ const mockObjectMetadata: FlatObjectMetadata = {
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
@@ -146,6 +146,7 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
@HideField()
workspaceId: string;
@FilterableField(() => UUIDScalarType)
objectMetadataId: string;
@IsBoolean()
@@ -58,6 +58,16 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"universalSettings",
],
},
"fieldPermission": {
"propertiesToCompare": [
"roleUniversalIdentifier",
"objectMetadataUniversalIdentifier",
"fieldMetadataUniversalIdentifier",
"canReadFieldValue",
"canUpdateFieldValue",
],
"propertiesToStringify": [],
},
"frontComponent": {
"propertiesToCompare": [
"name",
@@ -1156,6 +1156,43 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: undefined,
},
},
fieldPermission: {
roleId: {
toCompare: true,
toStringify: false,
universalProperty: 'roleUniversalIdentifier',
},
objectMetadataId: {
toCompare: true,
toStringify: false,
universalProperty: 'objectMetadataUniversalIdentifier',
},
fieldMetadataId: {
toCompare: true,
toStringify: false,
universalProperty: 'fieldMetadataUniversalIdentifier',
},
canReadFieldValue: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
canUpdateFieldValue: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
updatedAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
},
objectPermission: {
roleId: {
toCompare: true,
@@ -180,6 +180,19 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
foreignKey: 'objectMetadataId',
},
},
fieldPermission: {
workspace: null,
application: null,
role: {
foreignKey: 'roleId',
},
objectMetadata: {
foreignKey: 'objectMetadataId',
},
fieldMetadata: {
foreignKey: 'fieldMetadataId',
},
},
pageLayout: {
workspace: null,
objectMetadata: {
@@ -309,6 +309,31 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
},
fieldPermission: {
workspace: null,
application: null,
role: {
metadataName: 'role',
foreignKey: 'roleId',
inverseOneToManyProperty: 'fieldPermissions',
isNullable: false,
universalForeignKey: 'roleUniversalIdentifier',
},
objectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'objectMetadataId',
inverseOneToManyProperty: 'fieldPermissions',
isNullable: false,
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
fieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'fieldMetadataId',
inverseOneToManyProperty: 'fieldPermissions',
isNullable: false,
universalForeignKey: 'fieldMetadataUniversalIdentifier',
},
},
pageLayout: {
workspace: null,
objectMetadata: {
@@ -9,6 +9,7 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
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 { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
@@ -48,6 +49,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
logicFunction: LogicFunctionEntity,
objectMetadata: ObjectMetadataEntity,
objectPermission: ObjectPermissionEntity,
fieldPermission: FieldPermissionEntity,
role: RoleEntity,
agent: AgentEntity,
commandMenuItem: CommandMenuItemEntity,
@@ -81,6 +81,11 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
role: true,
objectMetadata: true,
},
fieldPermission: {
role: true,
objectMetadata: true,
fieldMetadata: true,
},
pageLayout: {
objectMetadata: true,
pageLayoutTab: true,
@@ -35,6 +35,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
roleTarget: {},
permissionFlag: {},
objectPermission: {},
fieldPermission: {},
pageLayout: {},
pageLayoutTab: {},
pageLayoutWidget: {
@@ -42,7 +42,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
metadataName: 'viewSort',
universalFlatEntityForeignKeyAggregator: 'viewSortUniversalIdentifiers',
},
fieldPermissions: null,
fieldPermissions: {
metadataName: 'fieldPermission',
flatEntityForeignKeyAggregator: 'fieldPermissionIds',
universalFlatEntityForeignKeyAggregator:
'fieldPermissionUniversalIdentifiers',
},
indexFieldMetadatas: null,
viewFields: {
metadataName: 'viewField',
@@ -91,7 +96,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
universalFlatEntityForeignKeyAggregator:
'objectPermissionUniversalIdentifiers',
},
fieldPermissions: null,
fieldPermissions: {
metadataName: 'fieldPermission',
flatEntityForeignKeyAggregator: 'fieldPermissionIds',
universalFlatEntityForeignKeyAggregator:
'fieldPermissionUniversalIdentifiers',
},
views: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewIds',
@@ -161,7 +171,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
universalFlatEntityForeignKeyAggregator:
'permissionFlagUniversalIdentifiers',
},
fieldPermissions: null,
fieldPermissions: {
metadataName: 'fieldPermission',
flatEntityForeignKeyAggregator: 'fieldPermissionIds',
universalFlatEntityForeignKeyAggregator:
'fieldPermissionUniversalIdentifiers',
},
rowLevelPermissionPredicates: {
metadataName: 'rowLevelPermissionPredicate',
flatEntityForeignKeyAggregator: 'rowLevelPermissionPredicateIds',
@@ -178,6 +193,7 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
roleTarget: {},
permissionFlag: {},
objectPermission: {},
fieldPermission: {},
pageLayout: {
tabs: {
metadataName: 'pageLayoutTab',
@@ -5,6 +5,7 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { WorkspaceFlatFieldMetadataMapCacheService } from 'src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service';
import { WorkspaceFlatFieldPermissionMapCacheService } from 'src/engine/metadata-modules/flat-field-permission/services/workspace-flat-field-permission-map-cache.service';
import { WorkspaceFlatIndexMapCacheService } from 'src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service';
import { WorkspaceFlatObjectMetadataMapCacheService } from 'src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service';
import { WorkspaceFlatObjectPermissionMapCacheService } from 'src/engine/metadata-modules/flat-object-permission/services/workspace-flat-object-permission-map-cache.service';
@@ -25,6 +26,7 @@ import { FrontComponentEntity } from 'src/engine/metadata-modules/front-componen
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
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 { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
@@ -58,6 +60,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
FieldMetadataEntity,
ObjectMetadataEntity,
ObjectPermissionEntity,
FieldPermissionEntity,
PageLayoutEntity,
PageLayoutTabEntity,
PageLayoutWidgetEntity,
@@ -81,6 +84,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatFieldMetadataMapCacheService,
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatFieldPermissionMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -101,6 +105,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatFieldMetadataMapCacheService,
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatFieldPermissionMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -36,6 +36,7 @@ type Assertions = [
Expect<
Equal<
FieldMetadataOneToManySyncableRelations,
| 'fieldPermissions'
| 'viewFields'
| 'viewFilters'
| 'kanbanAggregateOperationViews'
@@ -6,6 +6,8 @@ import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-comma
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatFieldPermissionMaps } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission-maps.type';
import { type FlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission.type';
import { type FlatFrontComponentMaps } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component-maps.type';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
@@ -52,6 +54,7 @@ import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-module
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
import { type UniversalFlatCommandMenuItem } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-command-menu-item.type';
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
import { type UniversalFlatFieldPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-permission.type';
import { type UniversalFlatFrontComponent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-front-component.type';
import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type';
import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-logic-function.type';
@@ -91,6 +94,14 @@ import {
type UniversalDeleteCommandMenuItemAction,
type UniversalUpdateCommandMenuItemAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/types/workspace-migration-command-menu-item-action.type';
import {
type FlatCreateFieldPermissionAction,
type FlatDeleteFieldPermissionAction,
type FlatUpdateFieldPermissionAction,
type UniversalCreateFieldPermissionAction,
type UniversalDeleteFieldPermissionAction,
type UniversalUpdateFieldPermissionAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/types/workspace-migration-field-permission-action.type';
import {
type FlatCreateFieldAction,
type FlatDeleteFieldAction,
@@ -624,6 +635,22 @@ export type AllFlatEntityTypesByMetadataName = {
universalFlatEntity: UniversalFlatObjectPermission;
entity: MetadataEntity<'objectPermission'>;
};
fieldPermission: {
flatEntityMaps: FlatFieldPermissionMaps;
universalActions: {
create: UniversalCreateFieldPermissionAction;
update: UniversalUpdateFieldPermissionAction;
delete: UniversalDeleteFieldPermissionAction;
};
flatActions: {
create: FlatCreateFieldPermissionAction;
update: FlatUpdateFieldPermissionAction;
delete: FlatDeleteFieldPermissionAction;
};
flatEntity: FlatFieldPermission;
universalFlatEntity: UniversalFlatFieldPermission;
entity: MetadataEntity<'fieldPermission'>;
};
pageLayout: {
flatEntityMaps: FlatPageLayoutMaps;
universalActions: {
@@ -14,12 +14,21 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
"objectMetadata",
"fieldMetadata",
"viewSort",
"fieldPermission",
"viewField",
"viewFilter",
"view",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for fieldPermission 1`] = `
[
"role",
"objectMetadata",
"fieldMetadata",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for frontComponent 1`] = `[]`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for index 1`] = `
@@ -43,6 +52,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
"fieldMetadata",
"index",
"objectPermission",
"fieldPermission",
"view",
]
`;
@@ -86,6 +96,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
"roleTarget",
"objectPermission",
"permissionFlag",
"fieldPermission",
"rowLevelPermissionPredicate",
"rowLevelPermissionPredicateGroup",
]
@@ -3,6 +3,7 @@
exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = `
[
"rowLevelPermissionPredicate",
"fieldPermission",
"navigationMenuItem",
"viewField",
"viewFilter",
@@ -20,6 +20,7 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
viewFilterIds: [],
kanbanAggregateOperationViewIds: [],
viewFieldIds: [],
fieldPermissionIds: [],
createdAt,
mainGroupByFieldMetadataViewIds: [],
updatedAt: createdAt,
@@ -50,6 +51,7 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
@@ -37,6 +37,8 @@ export const getRelationTargetFlatFieldMetadataMock = ({
viewFilterIds: [],
viewFieldIds: [],
kanbanAggregateOperationViewIds: [],
fieldPermissionIds: [],
fieldPermissionUniversalIdentifiers: [],
createdAt,
updatedAt: createdAt,
description: 'default flat field metadata description',
@@ -121,6 +121,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"createdAt": Any<String>,
"defaultValue": null,
"description": "new field description",
"fieldPermissionUniversalIdentifiers": [],
"icon": "IconRelationOneToMany",
"isActive": true,
"isCustom": true,
@@ -155,6 +156,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"createdAt": Any<String>,
"defaultValue": null,
"description": "new field description",
"fieldPermissionUniversalIdentifiers": [],
"icon": "IconPet",
"isActive": true,
"isCustom": true,
@@ -191,6 +193,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"createdAt": Any<String>,
"defaultValue": null,
"description": "new field description",
"fieldPermissionUniversalIdentifiers": [],
"icon": "IconRelationOneToMany",
"isActive": true,
"isCustom": true,
@@ -225,6 +228,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"createdAt": Any<String>,
"defaultValue": null,
"description": "new field description",
"fieldPermissionUniversalIdentifiers": [],
"icon": "IconBuilding",
"isActive": true,
"isCustom": true,
@@ -106,6 +106,8 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = ({
[],
viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id),
viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id),
fieldPermissionIds:
fieldMetadataEntity.fieldPermissions?.map(({ id }) => id) ?? [],
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier,
@@ -132,6 +134,10 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = ({
fieldMetadataEntity.viewSorts?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
fieldPermissionUniversalIdentifiers:
fieldMetadataEntity.fieldPermissions?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
universalSettings: settingsWithUniversalIdentifiers,
};
};
@@ -53,6 +53,7 @@ export const getDefaultFlatFieldMetadata = ({
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
@@ -0,0 +1,9 @@
import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
export const FLAT_FIELD_PERMISSION_EDITABLE_PROPERTIES = [
'roleId',
'objectMetadataId',
'fieldMetadataId',
'canReadFieldValue',
'canUpdateFieldValue',
] as const satisfies MetadataEntityPropertyName<'fieldPermission'>[];
@@ -0,0 +1,100 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatFieldPermissionMaps } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission-maps.type';
import { fromFieldPermissionEntityToFlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/utils/from-field-permission-entity-to-flat-field-permission.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatFieldPermissionMaps')
export class WorkspaceFlatFieldPermissionMapCacheService extends WorkspaceCacheProvider<FlatFieldPermissionMaps> {
constructor(
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatFieldPermissionMaps> {
const [
fieldPermissions,
applications,
roles,
objectMetadatas,
fieldMetadatas,
] = await Promise.all([
this.fieldPermissionRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const fieldMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(fieldMetadatas);
const flatFieldPermissionMaps = createEmptyFlatEntityMaps();
for (const fieldPermissionEntity of fieldPermissions) {
const flatFieldPermission =
fromFieldPermissionEntityToFlatFieldPermission({
entity: fieldPermissionEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatFieldPermission,
flatEntityMapsToMutate: flatFieldPermissionMaps,
});
}
return flatFieldPermissionMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission.type';
export type FlatFieldPermissionMaps = FlatEntityMaps<FlatFieldPermission>;
@@ -0,0 +1,4 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
export type FlatFieldPermission = FlatEntityFrom<FieldPermissionEntity>;
@@ -0,0 +1,56 @@
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
import { type FieldPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
import { type UniversalFlatFieldPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-permission.type';
export const fromCreateFieldPermissionInputToUniversalFlatFieldPermission = ({
fieldPermissionInput,
roleId,
flatApplication,
flatRoleMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
}: {
fieldPermissionInput: FieldPermissionInput;
roleId: string;
flatApplication: FlatApplication;
} & Pick<
AllFlatEntityMaps,
'flatRoleMaps' | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps'
>): UniversalFlatFieldPermission & { id: string } => {
const now = new Date().toISOString();
const {
roleUniversalIdentifier,
objectMetadataUniversalIdentifier,
fieldMetadataUniversalIdentifier,
} = resolveEntityRelationUniversalIdentifiers({
metadataName: 'fieldPermission',
foreignKeyValues: {
roleId,
objectMetadataId: fieldPermissionInput.objectMetadataId,
fieldMetadataId: fieldPermissionInput.fieldMetadataId,
},
flatEntityMaps: {
flatRoleMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
});
return {
id: v4(),
universalIdentifier: v4(),
applicationUniversalIdentifier: flatApplication.universalIdentifier,
roleUniversalIdentifier,
objectMetadataUniversalIdentifier,
fieldMetadataUniversalIdentifier,
canReadFieldValue: fieldPermissionInput.canReadFieldValue ?? undefined,
canUpdateFieldValue: fieldPermissionInput.canUpdateFieldValue ?? undefined,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,81 @@
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromFieldPermissionEntityToFlatFieldPermission = ({
entity: fieldPermissionEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'fieldPermission'>): FlatFieldPermission => {
const fieldPermissionEntityWithoutRelations = removePropertiesFromRecord(
fieldPermissionEntity,
getMetadataEntityRelationProperties('fieldPermission'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
fieldPermissionEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${fieldPermissionEntity.applicationId} not found for fieldPermission ${fieldPermissionEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
fieldPermissionEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${fieldPermissionEntity.roleId} not found for fieldPermission ${fieldPermissionEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const objectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
fieldPermissionEntity.objectMetadataId,
);
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${fieldPermissionEntity.objectMetadataId} not found for fieldPermission ${fieldPermissionEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const fieldMetadataUniversalIdentifier =
fieldMetadataIdToUniversalIdentifierMap.get(
fieldPermissionEntity.fieldMetadataId,
);
if (!isDefined(fieldMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`FieldMetadata with id ${fieldPermissionEntity.fieldMetadataId} not found for fieldPermission ${fieldPermissionEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...fieldPermissionEntityWithoutRelations,
createdAt: fieldPermissionEntity.createdAt.toISOString(),
updatedAt: fieldPermissionEntity.updatedAt.toISOString(),
universalIdentifier:
fieldPermissionEntityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
objectMetadataUniversalIdentifier,
fieldMetadataUniversalIdentifier,
};
};
@@ -20,6 +20,7 @@ export const getFlatObjectMetadataMock = (
viewIds: [],
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
fieldIds: [],
description: 'default flat object metadata description',
icon: 'icon',
@@ -50,6 +51,7 @@ export const getFlatObjectMetadataMock = (
applicationUniversalIdentifier: applicationId,
fieldUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier:
@@ -95,6 +95,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
applicationUniversalIdentifier: flatApplication.universalIdentifier,
fieldUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier,
@@ -81,6 +81,8 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
objectPermissionIds: objectMetadataEntity.objectPermissions.map(
({ id }) => id,
),
fieldPermissionIds:
objectMetadataEntity.fieldPermissions?.map(({ id }) => id) ?? [],
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
imageIdentifierFieldMetadataUniversalIdentifier,
@@ -97,5 +99,9 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
objectMetadataEntity.objectPermissions.map(
({ universalIdentifier }) => universalIdentifier,
),
fieldPermissionUniversalIdentifiers:
objectMetadataEntity.fieldPermissions?.map(
({ universalIdentifier }) => universalIdentifier,
) ?? [],
};
};
@@ -58,6 +58,7 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
objectPermissionUniversalIdentifiers: [],
permissionFlagUniversalIdentifiers: [],
fieldPermissionIds: [],
fieldPermissionUniversalIdentifiers: [],
rowLevelPermissionPredicateIds: [],
rowLevelPermissionPredicateUniversalIdentifiers: [],
rowLevelPermissionPredicateGroupIds: [],
@@ -74,6 +74,9 @@ export const fromRoleEntityToFlatRole = ({
permissionFlagUniversalIdentifiers: roleEntity.permissionFlags.map(
({ universalIdentifier }) => universalIdentifier,
),
fieldPermissionUniversalIdentifiers: roleEntity.fieldPermissions.map(
({ universalIdentifier }) => universalIdentifier,
),
rowLevelPermissionPredicateUniversalIdentifiers:
roleEntity.rowLevelPermissionPredicates.map(
({ universalIdentifier }) => universalIdentifier,
@@ -38,6 +38,7 @@ const PARTIAL_ID_FIELD = {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.UUID>;
@@ -66,6 +67,7 @@ const PARTIAL_CREATED_AT_FIELD = {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
@@ -94,6 +96,7 @@ const PARTIAL_UPDATED_AT_FIELD = {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
@@ -122,6 +125,7 @@ const PARTIAL_DELETED_AT_FIELD = {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
@@ -150,6 +154,7 @@ const PARTIAL_CREATED_BY_FIELD = {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.ACTOR>;
@@ -178,6 +183,7 @@ const PARTIAL_UPDATED_BY_FIELD = {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.ACTOR>;
@@ -206,6 +212,7 @@ const PARTIAL_POSITION_FIELD = {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.POSITION>;
@@ -236,6 +243,7 @@ const PARTIAL_SEARCH_VECTOR_FIELD = {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortUniversalIdentifiers: [],
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.TS_VECTOR>;
@@ -151,6 +151,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
universalSettings: null,
viewSortUniversalIdentifiers: [],
};
@@ -2,13 +2,14 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
import { In, type Repository } from 'typeorm';
import { type Repository } from 'typeorm';
import {
fieldRelationMock,
fieldTextMock,
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
@@ -21,8 +22,18 @@ import {
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
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';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
const emptyFlatFieldPermissionMaps = {
byUniversalIdentifier: {},
universalIdentifierById: {},
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
};
describe('FieldPermissionService', () => {
let service: FieldPermissionService;
let fieldPermissionsRepository: jest.Mocked<
@@ -32,6 +43,17 @@ describe('FieldPermissionService', () => {
let fieldMetadataRepository: jest.Mocked<Repository<FieldMetadataEntity>>;
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
let workspaceMigrationValidateBuildAndRunService: jest.Mocked<WorkspaceMigrationValidateBuildAndRunService>;
let flatRoleMaps: {
byUniversalIdentifier: Record<
string,
{ id: string; isEditable: boolean; universalIdentifier: string }
>;
universalIdentifierById: Record<string, string>;
byId: Record<string, unknown>;
idByUniversalIdentifier: Record<string, string>;
universalIdentifiersByApplicationId: Record<string, string[]>;
};
const testWorkspaceId = '20202020-0000-0000-0000-000000000000';
const testRoleId = '20202020-0000-0000-0000-000000000001';
@@ -114,10 +136,32 @@ describe('FieldPermissionService', () => {
find: jest.fn(),
},
},
{
provide: ApplicationService,
useValue: {
findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest
.fn()
.mockResolvedValue({
workspaceCustomFlatApplication: {
id: 'app-id',
universalIdentifier: 'app-universal-id',
},
}),
},
},
{
provide: WorkspaceMigrationValidateBuildAndRunService,
useValue: {
validateBuildAndRunWorkspaceMigration: jest.fn(),
},
},
],
}).compile();
service = module.get<FieldPermissionService>(FieldPermissionService);
workspaceMigrationValidateBuildAndRunService = module.get(
WorkspaceMigrationValidateBuildAndRunService,
);
fieldPermissionsRepository = module.get(
getRepositoryToken(FieldPermissionEntity),
);
@@ -136,9 +180,19 @@ describe('FieldPermissionService', () => {
fieldTextMock,
fieldRelationMock,
]);
(workspaceCacheService.getOrRecompute as jest.Mock).mockResolvedValue({
rolesPermissions: mockRolesPermissions,
} as any);
(workspaceCacheService.getOrRecompute as jest.Mock).mockImplementation(
(_workspaceId: string, keys: string[]) => {
if (keys?.includes('flatFieldPermissionMaps')) {
return Promise.resolve({
flatFieldPermissionMaps: emptyFlatFieldPermissionMaps,
});
}
if (keys?.includes('rolesPermissions')) {
return Promise.resolve({ rolesPermissions: mockRolesPermissions });
}
return Promise.resolve({});
},
);
const testFieldMetadata = getMockFieldMetadataEntity({
...fieldTextMock,
label: 'Test Field',
@@ -147,8 +201,22 @@ describe('FieldPermissionService', () => {
id: testFieldMetadataId,
});
flatRoleMaps = {
byUniversalIdentifier: {
[testRoleId]: {
id: testRoleId,
isEditable: true,
universalIdentifier: testRoleId,
},
},
universalIdentifierById: { [testRoleId]: testRoleId },
byId: {},
idByUniversalIdentifier: {},
universalIdentifiersByApplicationId: {},
};
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatRoleMaps,
flatObjectMetadataMaps: {
byUniversalIdentifier: {
[testObjectMetadataId]: {
@@ -169,11 +237,23 @@ describe('FieldPermissionService', () => {
universalIdentifier: fieldRelationMock.objectMetadataId,
applicationId: null,
} as any,
[fieldRelationMock.relationTargetObjectMetadataId!]: {
...objectMetadataItemMock,
id: fieldRelationMock.relationTargetObjectMetadataId,
fieldIds: [fieldRelationMock.relationTargetFieldMetadataId!],
indexMetadataIds: [],
viewIds: [],
universalIdentifier:
fieldRelationMock.relationTargetObjectMetadataId,
applicationId: null,
} as any,
},
universalIdentifierById: {
[testObjectMetadataId]: testObjectMetadataId,
[fieldRelationMock.objectMetadataId]:
fieldRelationMock.objectMetadataId,
[fieldRelationMock.relationTargetObjectMetadataId!]:
fieldRelationMock.relationTargetObjectMetadataId!,
},
universalIdentifiersByApplicationId: {},
},
@@ -181,10 +261,18 @@ describe('FieldPermissionService', () => {
byUniversalIdentifier: {
[testFieldMetadata.universalIdentifier]: testFieldMetadata as any,
[fieldRelationMock.universalIdentifier]: fieldRelationMock as any,
[fieldRelationMock.relationTargetFieldMetadataId!]: {
...fieldRelationMock,
id: fieldRelationMock.relationTargetFieldMetadataId,
universalIdentifier:
fieldRelationMock.relationTargetFieldMetadataId,
} as any,
},
universalIdentifierById: {
[testFieldMetadataId]: testFieldMetadata.universalIdentifier,
[fieldRelationMock.id]: fieldRelationMock.universalIdentifier,
[fieldRelationMock.relationTargetFieldMetadataId!]:
fieldRelationMock.relationTargetFieldMetadataId!,
},
universalIdentifiersByApplicationId: {},
},
@@ -218,6 +306,10 @@ describe('FieldPermissionService', () => {
describe('successful cases', () => {
it('should successfully upsert field permissions', async () => {
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
{ status: 'success' } as any,
);
const input = createUpsertInput([
{
canReadFieldValue: false,
@@ -225,51 +317,81 @@ describe('FieldPermissionService', () => {
},
]);
await service.upsertFieldPermissions({
const result = await service.upsertFieldPermissions({
workspaceId: testWorkspaceId,
input,
});
expect(fieldPermissionsRepository.upsert).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
roleId: testRoleId,
workspaceId: testWorkspaceId,
objectMetadataId: testObjectMetadataId,
fieldMetadataId: testFieldMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: false,
expect(
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId: testWorkspaceId,
isSystemBuild: false,
allFlatEntityOperationByMetadataName: expect.objectContaining({
fieldPermission: expect.objectContaining({
flatEntityToCreate: expect.any(Array),
flatEntityToUpdate: expect.any(Array),
flatEntityToDelete: expect.any(Array),
}),
}),
]),
{
conflictPaths: ['fieldMetadataId', 'roleId'],
},
}),
);
expect(result).toEqual(expect.any(Array));
expect(
workspaceCacheService.invalidateAndRecompute,
).toHaveBeenCalledWith(testWorkspaceId, ['rolesPermissions']);
});
it('should delete field permissions when both canReadFieldValue and canUpdateFieldValue are null', async () => {
const existingFieldPermission: FieldPermissionEntity = {
id: 'existing-field-permission-id',
roleId: testRoleId,
objectMetadataId: testObjectMetadataId,
fieldMetadataId: testFieldMetadataId,
canReadFieldValue: null,
canUpdateFieldValue: false,
workspaceId: testWorkspaceId,
createdAt: new Date(),
updatedAt: new Date(),
} as unknown as FieldPermissionEntity;
fieldPermissionsRepository.find.mockResolvedValue([
existingFieldPermission,
]);
const existingUniversalId = 'existing-fp-universal-id';
const mapsWithOneCurrentPermission = {
...emptyFlatFieldPermissionMaps,
byUniversalIdentifier: {
[existingUniversalId]: {
id: 'existing-fp-id',
universalIdentifier: existingUniversalId,
roleUniversalIdentifier: testRoleId,
objectMetadataId: testObjectMetadataId,
fieldMetadataId: testFieldMetadataId,
canReadFieldValue: undefined,
canUpdateFieldValue: false,
applicationUniversalIdentifier: 'app-ui',
objectMetadataUniversalIdentifier: testObjectMetadataId,
fieldMetadataUniversalIdentifier: testFieldMetadataId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
},
universalIdentifierById: { 'existing-fp-id': existingUniversalId },
};
let flatFieldPermissionMapsCallCount = 0;
(workspaceCacheService.getOrRecompute as jest.Mock).mockImplementation(
(_w: string, keys: string[]) => {
if (keys?.includes('flatFieldPermissionMaps')) {
flatFieldPermissionMapsCallCount += 1;
return Promise.resolve({
flatFieldPermissionMaps:
flatFieldPermissionMapsCallCount === 1
? mapsWithOneCurrentPermission
: emptyFlatFieldPermissionMaps,
});
}
if (keys?.includes('rolesPermissions')) {
return Promise.resolve({
rolesPermissions: mockRolesPermissions,
});
}
return Promise.resolve({});
},
);
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
{ status: 'success' } as any,
);
const input = createUpsertInput([
{
canReadFieldValue: null,
canUpdateFieldValue: null,
},
]);
@@ -279,27 +401,68 @@ describe('FieldPermissionService', () => {
input,
});
expect(fieldPermissionsRepository.delete).toHaveBeenCalledWith({
id: In(['existing-field-permission-id']),
});
expect(
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
).toHaveBeenCalledWith(
expect.objectContaining({
allFlatEntityOperationByMetadataName: expect.objectContaining({
fieldPermission: expect.objectContaining({
flatEntityToDelete: expect.arrayContaining([
expect.objectContaining({
universalIdentifier: existingUniversalId,
}),
]),
}),
}),
}),
);
});
it('should not delete field permissions when one value is null and the other is false', async () => {
const existingFieldPermission: FieldPermissionEntity = {
id: 'existing-field-permission-id',
roleId: testRoleId,
objectMetadataId: testObjectMetadataId,
fieldMetadataId: testFieldMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: null,
workspaceId: testWorkspaceId,
createdAt: new Date(),
updatedAt: new Date(),
} as FieldPermissionEntity;
fieldPermissionsRepository.find.mockResolvedValue([
existingFieldPermission,
]);
const existingUniversalId = 'existing-fp-ui-2';
const mapsWithOneCurrent = {
...emptyFlatFieldPermissionMaps,
byUniversalIdentifier: {
[existingUniversalId]: {
id: 'existing-fp-id-2',
universalIdentifier: existingUniversalId,
roleUniversalIdentifier: testRoleId,
objectMetadataId: testObjectMetadataId,
fieldMetadataId: testFieldMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: undefined,
applicationUniversalIdentifier: 'app-ui',
objectMetadataUniversalIdentifier: testObjectMetadataId,
fieldMetadataUniversalIdentifier: testFieldMetadataId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
},
universalIdentifierById: { 'existing-fp-id-2': existingUniversalId },
};
let flatMapsCallCount = 0;
(workspaceCacheService.getOrRecompute as jest.Mock).mockImplementation(
(_w: string, keys: string[]) => {
if (keys?.includes('flatFieldPermissionMaps')) {
flatMapsCallCount += 1;
return Promise.resolve({
flatFieldPermissionMaps:
flatMapsCallCount === 1
? mapsWithOneCurrent
: emptyFlatFieldPermissionMaps,
});
}
if (keys?.includes('rolesPermissions')) {
return Promise.resolve({
rolesPermissions: mockRolesPermissions,
});
}
return Promise.resolve({});
},
);
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
{ status: 'success' } as any,
);
const input = createUpsertInput([
{
@@ -313,12 +476,26 @@ describe('FieldPermissionService', () => {
input,
});
expect(fieldPermissionsRepository.delete).not.toHaveBeenCalled();
const callArg = workspaceMigrationValidateBuildAndRunService
.validateBuildAndRunWorkspaceMigration.mock
.calls[0][0] as unknown as {
allFlatEntityOperationByMetadataName: {
fieldPermission: { flatEntityToDelete: unknown[] };
};
};
expect(
callArg.allFlatEntityOperationByMetadataName.fieldPermission
.flatEntityToDelete,
).toHaveLength(0);
});
});
describe('relation cases', () => {
it('should create two field permissions when a relation field permission is created', async () => {
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
{ status: 'success' } as any,
);
const input = createUpsertInput([
{
canReadFieldValue: false,
@@ -333,28 +510,17 @@ describe('FieldPermissionService', () => {
input,
});
expect(fieldPermissionsRepository.upsert).toHaveBeenCalledWith(
[
{
fieldMetadataId: fieldRelationMock.id,
objectMetadataId: fieldRelationMock.objectMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: false,
roleId: testRoleId,
workspaceId: testWorkspaceId,
},
{
fieldMetadataId: fieldRelationMock.relationTargetFieldMetadataId,
objectMetadataId:
fieldRelationMock.relationTargetObjectMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: false,
roleId: testRoleId,
workspaceId: testWorkspaceId,
},
],
{ conflictPaths: ['fieldMetadataId', 'roleId'] },
);
const callArg = workspaceMigrationValidateBuildAndRunService
.validateBuildAndRunWorkspaceMigration.mock
.calls[0][0] as unknown as {
allFlatEntityOperationByMetadataName: {
fieldPermission: { flatEntityToCreate: unknown[] };
};
};
expect(
callArg.allFlatEntityOperationByMetadataName.fieldPermission
.flatEntityToCreate,
).toHaveLength(2);
});
});
@@ -404,6 +570,7 @@ describe('FieldPermissionService', () => {
it('should throw error when object metadata is not found', async () => {
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatRoleMaps,
flatObjectMetadataMaps: {
byUniversalIdentifier: {},
universalIdentifierById: {},
@@ -446,6 +613,7 @@ describe('FieldPermissionService', () => {
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatRoleMaps,
flatObjectMetadataMaps: {
byUniversalIdentifier: {
[testObjectMetadataId]: {
@@ -494,6 +662,7 @@ describe('FieldPermissionService', () => {
it('should throw error when field metadata is not found', async () => {
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatRoleMaps,
flatObjectMetadataMaps: {
byUniversalIdentifier: {
[testObjectMetadataId]: {
@@ -541,9 +710,19 @@ describe('FieldPermissionService', () => {
});
it('should throw error when object permission is not found', async () => {
(workspaceCacheService.getOrRecompute as jest.Mock).mockResolvedValue({
rolesPermissions: {},
} as any);
(workspaceCacheService.getOrRecompute as jest.Mock).mockImplementation(
(_w: string, keys: string[]) => {
if (keys?.includes('flatFieldPermissionMaps')) {
return Promise.resolve({
flatFieldPermissionMaps: emptyFlatFieldPermissionMaps,
});
}
if (keys?.includes('rolesPermissions')) {
return Promise.resolve({ rolesPermissions: {} });
}
return Promise.resolve({});
},
);
const input = createUpsertInput([
{
@@ -568,12 +747,20 @@ describe('FieldPermissionService', () => {
describe('role validation errors', () => {
it('should throw error when role is not editable', async () => {
const nonEditableRole: RoleEntity = {
...mockRole,
isEditable: false,
};
roleRepository.findOne.mockResolvedValue(nonEditableRole);
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
{
status: 'fail',
report: {
fieldPermission: [
{
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: 'Role is not editable',
userFriendlyMessage: 'This role cannot be modified.',
},
],
},
} as any,
);
const input = createUpsertInput([
{
@@ -587,12 +774,7 @@ describe('FieldPermissionService', () => {
workspaceId: testWorkspaceId,
input,
}),
).rejects.toThrow(
new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
),
);
).rejects.toThrow(WorkspaceMigrationBuilderException);
});
});
});
@@ -15,7 +15,7 @@ import {
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity('fieldPermission')
@Unique('IDX_FIELD_PERMISSION_FIELD_METADATA_ID_ROLE_ID_UNIQUE', [
@@ -23,7 +23,7 @@ import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/works
'roleId',
])
@Index('IDX_FIELD_PERMISSION_WORKSPACE_ID_ROLE_ID', ['workspaceId', 'roleId'])
export class FieldPermissionEntity extends WorkspaceRelatedEntity {
export class FieldPermissionEntity extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -1,45 +1,49 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import {
InternalServerError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
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 FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatFieldPermissionMaps } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission-maps.type';
import { type FlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission.type';
import { fromCreateFieldPermissionInputToUniversalFlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/utils/from-create-field-permission-input-to-universal-flat-field-permission.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
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';
import { type UniversalFlatFieldPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-permission.type';
type DesiredFieldPermission = {
objectMetadataId: string;
fieldMetadataId: string;
canReadFieldValue?: boolean | null;
canUpdateFieldValue?: boolean | null;
};
const keyFrom = (objectMetadataId: string, fieldMetadataId: string) =>
`${objectMetadataId}:${fieldMetadataId}`;
@Injectable()
export class FieldPermissionService {
constructor(
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionsRepository: Repository<FieldPermissionEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationService: ApplicationService,
) {}
public async upsertFieldPermissions({
@@ -48,135 +52,249 @@ export class FieldPermissionService {
}: {
workspaceId: string;
input: UpsertFieldPermissionsInput;
}): Promise<FieldPermissionEntity[]> {
const role = await this.getRoleOrThrow({
roleId: input.roleId,
workspaceId,
});
}): Promise<FlatFieldPermission[]> {
const [flatMapsForRoleObjectField, flatFieldPermissionMapsResult] =
await Promise.all([
this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatRoleMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
],
},
),
this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatFieldPermissionMaps',
] as unknown as Parameters<WorkspaceCacheService['getOrRecompute']>[1]),
]);
const flatRoleMaps = flatMapsForRoleObjectField.flatRoleMaps;
const flatObjectMetadataMaps =
flatMapsForRoleObjectField.flatObjectMetadataMaps;
const flatFieldMetadataMaps =
flatMapsForRoleObjectField.flatFieldMetadataMaps;
const flatFieldPermissionMapsResolved: FlatFieldPermissionMaps = (
flatFieldPermissionMapsResult as unknown as {
flatFieldPermissionMaps: FlatFieldPermissionMaps;
}
).flatFieldPermissionMaps;
const { rolesPermissions } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'rolesPermissions',
]);
await this.validateRoleIsEditableOrThrow({
role,
});
const roleUniversalIdentifier =
flatRoleMaps.universalIdentifierById[input.roleId];
const flatRole: FlatRole | undefined = isDefined(roleUniversalIdentifier)
? (flatRoleMaps.byUniversalIdentifier[roleUniversalIdentifier] as
| FlatRole
| undefined)
: undefined;
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
},
);
const existingFieldPermissions = await this.fieldPermissionsRepository.find(
{
where: {
roleId: input.roleId,
workspaceId,
},
},
const currentFieldPermissionsForRole = Object.values(
flatFieldPermissionMapsResolved.byUniversalIdentifier,
).filter(
(fp): fp is FlatFieldPermission =>
isDefined(fp) && fp.roleUniversalIdentifier === roleUniversalIdentifier,
);
const fieldPermissionsToDeleteIds: string[] = [];
const desiredMap = new Map<string, DesiredFieldPermission>();
input.fieldPermissions.forEach((fieldPermission) => {
for (const fieldPermission of input.fieldPermissions) {
this.validateFieldPermission({
allFieldPermissions: input.fieldPermissions,
fieldPermission,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
rolesPermissions,
role,
flatRole,
});
if (
fieldPermission.canReadFieldValue === null ||
fieldPermission.canUpdateFieldValue === null
) {
this.checkIfFieldPermissionShouldBeDeleted({
fieldPermission,
existingFieldPermissions,
fieldPermissionsToDeleteIds,
});
const bothNull =
(fieldPermission.canReadFieldValue === null ||
fieldPermission.canReadFieldValue === undefined) &&
(fieldPermission.canUpdateFieldValue === null ||
fieldPermission.canUpdateFieldValue === undefined);
if (bothNull) {
continue;
}
desiredMap.set(
keyFrom(
fieldPermission.objectMetadataId,
fieldPermission.fieldMetadataId,
),
{
objectMetadataId: fieldPermission.objectMetadataId,
fieldMetadataId: fieldPermission.fieldMetadataId,
canReadFieldValue: fieldPermission.canReadFieldValue ?? undefined,
canUpdateFieldValue: fieldPermission.canUpdateFieldValue ?? undefined,
},
);
}
this.addRelatedFieldPermissionsToDesired({
desiredMap,
inputFieldPermissions: input.fieldPermissions,
flatFieldMetadataMaps,
});
const fieldPermissions = input.fieldPermissions.map((fieldPermission) => ({
...fieldPermission,
roleId: input.roleId,
workspaceId,
}));
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const existingFieldPermissionsToDelete = existingFieldPermissions.filter(
(existingFieldPermissionToFilter) =>
fieldPermissionsToDeleteIds.includes(
existingFieldPermissionToFilter.id,
),
const flatEntityToCreate: (UniversalFlatFieldPermission & {
id: string;
})[] = [];
const flatEntityToUpdate: UniversalFlatFieldPermission[] = [];
const flatEntityToDelete: UniversalFlatFieldPermission[] = [];
const currentByKey = new Map(
currentFieldPermissionsForRole.map((fp) => [
keyFrom(fp.objectMetadataId, fp.fieldMetadataId),
fp,
]),
);
const fieldPermissionsToUpsert = fieldPermissions.filter(
(fieldPermissionToUpsert) =>
!existingFieldPermissionsToDelete.some(
(existingFieldPermissionToDelete) =>
existingFieldPermissionToDelete.fieldMetadataId ===
fieldPermissionToUpsert.fieldMetadataId,
),
);
for (const [, desired] of desiredMap) {
const current = currentByKey.get(
keyFrom(desired.objectMetadataId, desired.fieldMetadataId),
);
const fieldMetadatasForFieldPermissions =
await this.fieldMetadataRepository.find({
where: {
id: In(fieldPermissions.map((fp) => fp.fieldMetadataId)),
},
});
if (!isDefined(current)) {
flatEntityToCreate.push(
fromCreateFieldPermissionInputToUniversalFlatFieldPermission({
fieldPermissionInput: {
objectMetadataId: desired.objectMetadataId,
fieldMetadataId: desired.fieldMetadataId,
canReadFieldValue: desired.canReadFieldValue ?? null,
canUpdateFieldValue: desired.canUpdateFieldValue ?? null,
},
roleId: input.roleId,
flatApplication: workspaceCustomFlatApplication,
flatRoleMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
}),
);
} else {
const effectiveCanRead =
desired.canReadFieldValue ?? current.canReadFieldValue;
const effectiveCanUpdate =
desired.canUpdateFieldValue ?? current.canUpdateFieldValue;
const changed =
effectiveCanRead !== current.canReadFieldValue ||
effectiveCanUpdate !== current.canUpdateFieldValue;
const relatedFieldPermissionsToUpsert =
this.computeFieldPermissionForRelationTargetFieldMetadata({
fieldPermissions: fieldPermissionsToUpsert,
fieldMetadatasForFieldPermissions,
});
if (changed) {
const now = new Date().toISOString();
flatEntityToUpdate.push({
universalIdentifier: current.universalIdentifier,
applicationUniversalIdentifier:
current.applicationUniversalIdentifier,
roleUniversalIdentifier: current.roleUniversalIdentifier,
objectMetadataUniversalIdentifier:
current.objectMetadataUniversalIdentifier,
fieldMetadataUniversalIdentifier:
current.fieldMetadataUniversalIdentifier,
canReadFieldValue: effectiveCanRead ?? undefined,
canUpdateFieldValue: effectiveCanUpdate ?? undefined,
createdAt: current.createdAt,
updatedAt: now,
});
}
}
}
await this.fieldPermissionsRepository.upsert(
[...fieldPermissionsToUpsert, ...relatedFieldPermissionsToUpsert],
{
conflictPaths: ['fieldMetadataId', 'roleId'],
},
);
if (fieldPermissionsToDeleteIds.length > 0) {
const relatedFieldPermissionToDeleteIds =
this.getRelatedFieldPermissionsToDeleteIds({
allFieldPermissions: existingFieldPermissions,
fieldPermissionsToDelete: existingFieldPermissionsToDelete,
fieldMetadatas: fieldMetadatasForFieldPermissions,
for (const current of currentFieldPermissionsForRole) {
const key = keyFrom(current.objectMetadataId, current.fieldMetadataId);
if (!desiredMap.has(key)) {
flatEntityToDelete.push({
universalIdentifier: current.universalIdentifier,
applicationUniversalIdentifier:
current.applicationUniversalIdentifier,
roleUniversalIdentifier: current.roleUniversalIdentifier,
objectMetadataUniversalIdentifier:
current.objectMetadataUniversalIdentifier,
fieldMetadataUniversalIdentifier:
current.fieldMetadataUniversalIdentifier,
canReadFieldValue: current.canReadFieldValue ?? undefined,
canUpdateFieldValue: current.canUpdateFieldValue ?? undefined,
createdAt: current.createdAt,
updatedAt: current.updatedAt,
});
}
}
await this.fieldPermissionsRepository.delete({
id: In([
...fieldPermissionsToDeleteIds,
...relatedFieldPermissionToDeleteIds,
]),
});
if (
flatEntityToCreate.length === 0 &&
flatEntityToUpdate.length === 0 &&
flatEntityToDelete.length === 0
) {
const desiredObjectMetadataIds = new Set(
input.fieldPermissions.map((fp) => fp.objectMetadataId),
);
return currentFieldPermissionsForRole.filter((fp) =>
desiredObjectMetadataIds.has(fp.objectMetadataId),
);
}
const buildAndRunResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
fieldPermission: {
flatEntityToCreate,
flatEntityToUpdate,
flatEntityToDelete,
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
} as Parameters<
WorkspaceMigrationValidateBuildAndRunService['validateBuildAndRunWorkspaceMigration']
>[0],
);
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Validation errors occurred while upserting field permissions',
);
}
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'rolesPermissions',
]);
return this.fieldPermissionsRepository.find({
where: {
roleId: input.roleId,
objectMetadataId: In(
input.fieldPermissions.map(
(fieldPermission) => fieldPermission.objectMetadataId,
),
),
workspaceId,
},
});
const freshFlatFieldPermissionMaps: FlatFieldPermissionMaps = (
(await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatFieldPermissionMaps',
] as unknown as Parameters<
WorkspaceCacheService['getOrRecompute']
>[1])) as unknown as { flatFieldPermissionMaps: FlatFieldPermissionMaps }
).flatFieldPermissionMaps;
const resultFieldPermissions = Object.values(
freshFlatFieldPermissionMaps.byUniversalIdentifier,
).filter(
(fp): fp is FlatFieldPermission =>
isDefined(fp) && fp.roleUniversalIdentifier === roleUniversalIdentifier,
);
const desiredObjectMetadataIds = new Set(
input.fieldPermissions.map((fp) => fp.objectMetadataId),
);
const filtered = resultFieldPermissions.filter((fp) =>
desiredObjectMetadataIds.has(fp.objectMetadataId),
);
return filtered;
}
private validateFieldPermission({
@@ -185,14 +303,14 @@ export class FieldPermissionService {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
rolesPermissions,
role,
flatRole,
}: {
allFieldPermissions: UpsertFieldPermissionsInput['fieldPermissions'];
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
rolesPermissions: ObjectsPermissionsByRoleId;
role: RoleEntity;
rolesPermissions: Record<string, Record<string, unknown>> | undefined;
flatRole: { id: string } | undefined;
}) {
const duplicateFieldPermissions = allFieldPermissions.filter(
(permission) =>
@@ -204,12 +322,11 @@ export class FieldPermissionService {
`Cannot accept more than one fieldPermission for field ${fieldPermission.fieldMetadataId} in input.`,
);
}
if (
('canUpdateFieldValue' in fieldPermission &&
fieldPermission.canUpdateFieldValue !== null &&
(fieldPermission.canUpdateFieldValue !== null &&
fieldPermission.canUpdateFieldValue !== false) ||
('canReadFieldValue' in fieldPermission &&
fieldPermission.canReadFieldValue !== null &&
(fieldPermission.canReadFieldValue !== null &&
fieldPermission.canReadFieldValue !== false)
) {
throw new PermissionsException(
@@ -261,10 +378,11 @@ export class FieldPermissionService {
);
}
const rolePermissionOnObject =
rolesPermissions?.[role.id]?.[fieldPermission.objectMetadataId];
const rolePermissionOnObject = isDefined(flatRole)
? rolesPermissions?.[flatRole.id]?.[fieldPermission.objectMetadataId]
: undefined;
if (!isDefined(rolePermissionOnObject)) {
if (isDefined(flatRole) && !isDefined(rolePermissionOnObject)) {
throw new PermissionsException(
PermissionsExceptionMessage.OBJECT_PERMISSION_NOT_FOUND,
PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND,
@@ -275,194 +393,84 @@ export class FieldPermissionService {
}
}
private async getRoleOrThrow({
roleId,
workspaceId,
private addRelatedFieldPermissionsToDesired({
desiredMap,
inputFieldPermissions,
flatFieldMetadataMaps,
}: {
roleId: string;
workspaceId: string;
desiredMap: Map<string, DesiredFieldPermission>;
inputFieldPermissions: UpsertFieldPermissionsInput['fieldPermissions'];
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}) {
const role = await this.roleRepository.findOne({
where: {
id: roleId,
workspaceId,
},
relations: ['objectPermissions', 'fieldPermissions'],
});
if (!isDefined(role)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.`,
},
);
}
return role;
}
private async validateRoleIsEditableOrThrow({ role }: { role: RoleEntity }) {
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 checkIfFieldPermissionShouldBeDeleted({
fieldPermission,
existingFieldPermissions,
fieldPermissionsToDeleteIds,
}: {
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
existingFieldPermissions: FieldPermissionEntity[];
fieldPermissionsToDeleteIds: string[];
}) {
const existingFieldPermission = existingFieldPermissions.find(
(existingFieldPermission) =>
existingFieldPermission.fieldMetadataId ===
fieldPermission.fieldMetadataId,
const inputKeys = new Set(
inputFieldPermissions.map((fp) =>
keyFrom(fp.objectMetadataId, fp.fieldMetadataId),
),
);
if (existingFieldPermission) {
const finalCanReadFieldValue =
'canReadFieldValue' in fieldPermission
? fieldPermission.canReadFieldValue
: existingFieldPermission.canReadFieldValue;
const finalCanUpdateFieldValue =
'canUpdateFieldValue' in fieldPermission
? fieldPermission.canUpdateFieldValue
: existingFieldPermission.canUpdateFieldValue;
for (const fieldPermission of inputFieldPermissions) {
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldPermission.fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (
finalCanReadFieldValue === null &&
finalCanUpdateFieldValue === null
!isDefined(flatFieldMetadata) ||
flatFieldMetadata.type !== FieldMetadataType.RELATION
) {
fieldPermissionsToDeleteIds.push(existingFieldPermission.id);
continue;
}
}
}
private getRelatedFieldPermissionsToDeleteIds({
allFieldPermissions,
fieldPermissionsToDelete,
fieldMetadatas,
}: {
allFieldPermissions: FieldPermissionEntity[];
fieldPermissionsToDelete: FieldPermissionEntity[];
fieldMetadatas: FieldMetadataEntity[];
}) {
const fieldMetadatasForFieldPermissionsToDelete = fieldMetadatas.filter(
(fieldMetadata) =>
fieldPermissionsToDelete.some(
(existingFieldPermissionToDelete) =>
existingFieldPermissionToDelete.fieldMetadataId ===
fieldMetadata.id,
),
);
const relationTargetFieldMetadataIds: string[] = [];
for (const fieldMetadataForFieldPermissionToDelete of fieldMetadatasForFieldPermissionsToDelete) {
const relationType = (
flatFieldMetadata.settings as { relationType?: string } | undefined
)?.relationType;
if (
isFieldMetadataTypeRelation(fieldMetadataForFieldPermissionToDelete)
relationType !== RelationType.ONE_TO_MANY &&
relationType !== RelationType.MANY_TO_ONE
) {
if (
fieldMetadataForFieldPermissionToDelete.settings?.relationType ===
RelationType.ONE_TO_MANY ||
fieldMetadataForFieldPermissionToDelete.settings?.relationType ===
RelationType.MANY_TO_ONE
) {
relationTargetFieldMetadataIds.push(
fieldMetadataForFieldPermissionToDelete.relationTargetFieldMetadataId,
);
}
continue;
}
}
const fieldPermissionsForRelationTargetFieldMetadataIds =
allFieldPermissions
.filter((fieldPermission) =>
relationTargetFieldMetadataIds.includes(
fieldPermission.fieldMetadataId,
),
)
.map((fieldPermission) => fieldPermission.id);
const targetObjectId =
flatFieldMetadata.relationTargetObjectMetadataId ?? undefined;
const targetFieldId =
flatFieldMetadata.relationTargetFieldMetadataId ?? undefined;
return fieldPermissionsForRelationTargetFieldMetadataIds;
}
if (!targetObjectId || !targetFieldId) {
continue;
}
private computeFieldPermissionForRelationTargetFieldMetadata({
fieldPermissions,
fieldMetadatasForFieldPermissions,
}: {
fieldPermissions: UpsertFieldPermissionsInput['fieldPermissions'];
fieldMetadatasForFieldPermissions: FieldMetadataEntity[];
}) {
return fieldPermissions
.map((fieldPermission) => {
const fieldMetadata = fieldMetadatasForFieldPermissions.find(
(fm) => fm.id === fieldPermission.fieldMetadataId,
const targetKey = keyFrom(targetObjectId, targetFieldId);
if (inputKeys.has(targetKey)) {
const targetInInput = inputFieldPermissions.find(
(fp) =>
fp.objectMetadataId === targetObjectId &&
fp.fieldMetadataId === targetFieldId,
);
if (!isDefined(fieldMetadata)) {
throw new InternalServerError(
'Field metadata not found for field permission',
);
}
if (isFieldMetadataTypeRelation(fieldMetadata)) {
if (
fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY ||
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE
) {
const fieldPermissionsOnRelationTargetField =
fieldPermissions.filter(
(fieldPermissionInput) =>
fieldPermissionInput.fieldMetadataId ===
fieldMetadata.relationTargetFieldMetadataId,
);
if (fieldPermissionsOnRelationTargetField.length > 0) {
const firstFieldPermission =
fieldPermissionsOnRelationTargetField[0]; // validation rules guarantee there can only be one
const hasConflictingPermissions =
fieldPermission.canReadFieldValue !==
firstFieldPermission.canReadFieldValue ||
fieldPermission.canUpdateFieldValue !==
firstFieldPermission.canUpdateFieldValue;
if (hasConflictingPermissions) {
const fieldName = fieldMetadata.name;
throw new UserInputError(
'Conflicting field permissions found for relation target field',
{
userFriendlyMessage: msg`Contradicting field permissions have been detected on a relation field (${fieldName}).`,
},
);
}
return;
}
return {
...fieldPermission,
objectMetadataId: fieldMetadata.relationTargetObjectMetadataId,
fieldMetadataId: fieldMetadata.relationTargetFieldMetadataId,
};
if (isDefined(targetInInput)) {
const hasConflict =
fieldPermission.canReadFieldValue !==
targetInInput.canReadFieldValue ||
fieldPermission.canUpdateFieldValue !==
targetInInput.canUpdateFieldValue;
if (hasConflict) {
throw new UserInputError(
'Conflicting field permissions found for relation target field',
{
userFriendlyMessage: msg`Contradicting field permissions have been detected on a relation field.`,
},
);
}
}
continue;
}
return null;
})
.filter(isDefined);
desiredMap.set(targetKey, {
objectMetadataId: targetObjectId,
fieldMetadataId: targetFieldId,
canReadFieldValue: fieldPermission.canReadFieldValue ?? undefined,
canUpdateFieldValue: fieldPermission.canUpdateFieldValue ?? undefined,
});
}
}
}
@@ -0,0 +1,13 @@
import { type FlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission.type';
import { type FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
export const fromFlatFieldPermissionToFieldPermissionDto = (
flatFieldPermission: FlatFieldPermission,
): FieldPermissionDTO => ({
id: flatFieldPermission.id,
objectMetadataId: flatFieldPermission.objectMetadataId,
fieldMetadataId: flatFieldPermission.fieldMetadataId,
roleId: flatFieldPermission.roleId,
canReadFieldValue: flatFieldPermission.canReadFieldValue ?? null,
canUpdateFieldValue: flatFieldPermission.canUpdateFieldValue ?? null,
});
@@ -42,6 +42,7 @@ export enum PermissionsExceptionCode {
UPSERT_FIELD_PERMISSION_FAILED = 'UPSERT_FIELD_PERMISSION_FAILED',
PERMISSION_NOT_FOUND = 'PERMISSION_NOT_FOUND',
OBJECT_PERMISSION_NOT_FOUND = 'OBJECT_PERMISSION_NOT_FOUND',
FIELD_PERMISSION_NOT_FOUND = 'FIELD_PERMISSION_NOT_FOUND',
EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'EMPTY_FIELD_PERMISSION_NOT_ALLOWED',
JOIN_COLUMN_NAME_REQUIRED = 'JOIN_COLUMN_NAME_REQUIRED',
COMPOSITE_TYPE_NOT_FOUND = 'COMPOSITE_TYPE_NOT_FOUND',
@@ -128,6 +129,8 @@ const getPermissionsExceptionUserFriendlyMessage = (
return msg`Permission not found.`;
case PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND:
return msg`Object permission not found.`;
case PermissionsExceptionCode.FIELD_PERMISSION_NOT_FOUND:
return msg`Field permission not found.`;
case PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED:
return msg`Empty field permissions are not allowed.`;
case PermissionsExceptionCode.JOIN_COLUMN_NAME_REQUIRED:
@@ -184,6 +187,7 @@ export enum PermissionsExceptionMessage {
FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT = 'Field restriction only makes sense on readable object',
FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT = 'Field restriction on update only makes sense on updatable object',
OBJECT_PERMISSION_NOT_FOUND = 'Object permission not found',
FIELD_PERMISSION_NOT_FOUND = 'Field permission not found',
EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'Empty field permission not allowed',
ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET = 'Role must be assignable to at least one target type',
ROLE_CANNOT_BE_ASSIGNED_TO_USERS = 'Role cannot be assigned to users',
@@ -48,6 +48,7 @@ export const permissionGraphqlApiExceptionHandler = (
case PermissionsExceptionCode.ROLE_NOT_FOUND:
case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND:
case PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND:
case PermissionsExceptionCode.FIELD_PERMISSION_NOT_FOUND:
case PermissionsExceptionCode.PERMISSION_NOT_FOUND:
throw new NotFoundError(error);
case PermissionsExceptionCode.UPSERT_FIELD_PERMISSION_FAILED:
@@ -8,9 +8,10 @@ import { Args, Mutation, Parent, Query, ResolveField } from '@nestjs/graphql';
import { msg } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
@@ -20,7 +21,6 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
import { WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
@@ -39,11 +39,12 @@ import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-
import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
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 { fromFlatFieldPermissionToFieldPermissionDto } from 'src/engine/metadata-modules/object-permission/utils/from-flat-field-permission-to-field-permission-dto.util';
import { fromFlatObjectPermissionToObjectPermissionDto } from 'src/engine/metadata-modules/object-permission/utils/from-flat-object-permission-to-object-permission-dto.util';
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util';
import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input';
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util';
import {
PermissionsException,
PermissionsExceptionCode,
@@ -242,10 +243,14 @@ export class RoleResolver {
@Args('upsertFieldPermissionsInput')
upsertFieldPermissionsInput: UpsertFieldPermissionsInput,
): Promise<FieldPermissionDTO[]> {
return this.fieldPermissionService.upsertFieldPermissions({
workspaceId: workspace.id,
input: upsertFieldPermissionsInput,
});
const flatFieldPermissions =
await this.fieldPermissionService.upsertFieldPermissions({
workspaceId: workspace.id,
input: upsertFieldPermissionsInput,
});
return flatFieldPermissions.map(
fromFlatFieldPermissionToFieldPermissionDto,
);
}
@Mutation(() => UpsertRowLevelPermissionPredicatesResultDTO)
@@ -87,7 +87,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
}),
this.fieldPermissionRepository.find({
where: { workspaceId },
select: ['id', 'roleId'],
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
}),
this.rowLevelPermissionPredicateRepository.find({
@@ -111,6 +111,7 @@ describe('WorkspaceEntityManager', () => {
fieldIds: ['field-id'],
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
viewIds: [],
universalIdentifier: 'test-entity-id',
description: null,
@@ -127,6 +128,7 @@ describe('WorkspaceEntityManager', () => {
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: ['field-id'],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
@@ -162,6 +164,7 @@ describe('WorkspaceEntityManager', () => {
workspaceId: 'test-workspace-id',
viewFieldIds: [],
viewFilterIds: [],
fieldPermissionIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
mainGroupByFieldMetadataViewIds: [],
@@ -178,6 +181,7 @@ describe('WorkspaceEntityManager', () => {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortIds: [],
viewSortUniversalIdentifiers: [],
universalSettings: null,
@@ -93,6 +93,7 @@ describe('WorkspaceRepository', () => {
relationTargetObjectMetadataId: null,
calendarViewIds: [],
viewFilterIds: [],
fieldPermissionIds: [],
kanbanAggregateOperationViewIds: [],
viewFieldIds: [],
mainGroupByFieldMetadataViewIds: [],
@@ -105,6 +106,7 @@ describe('WorkspaceRepository', () => {
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewSortIds: [],
viewSortUniversalIdentifiers: [],
universalSettings: null,
@@ -225,6 +225,7 @@ describe('computeRelationConnectQueryConfigs', () => {
isSearchable: true,
universalIdentifier: partial.id,
objectPermissionIds: [],
fieldPermissionIds: [],
viewIds: [],
applicationId: null,
isLabelSyncedWithName: false,
@@ -74,6 +74,7 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => {
fieldIds: ['name-id'],
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
viewIds: [],
applicationId: null,
} as unknown as FlatObjectMetadata;
@@ -27,6 +27,7 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
fieldIds,
viewIds: [],
applicationId: 'test-application-id',
@@ -43,6 +44,7 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: fieldIds,
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
@@ -27,6 +27,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
fieldIds,
viewIds: [],
applicationId: 'test-application-id',
@@ -43,6 +44,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: fieldIds,
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
@@ -27,6 +27,7 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => {
universalIdentifier: 'test-object-id',
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
fieldIds,
viewIds: [],
applicationId: 'test-application-id',
@@ -43,6 +44,7 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => {
applicationUniversalIdentifier: 'test-application-id',
fieldUniversalIdentifiers: fieldIds,
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
@@ -35,6 +35,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
flatRoleTargetMaps: 'flat-maps:role-target',
flatPermissionFlagMaps: 'flat-maps:permission-flag',
flatObjectPermissionMaps: 'flat-maps:object-permission',
flatFieldPermissionMaps: 'flat-maps:field-permission',
ORMEntityMetadatas: 'orm:entity-metadatas',
flatAgentMaps: 'flat-maps:agent',
flatSkillMaps: 'flat-maps:skill',
@@ -95,6 +95,7 @@ export const createStandardFieldFlatMetadata = <
morphId: null,
viewFieldIds: [],
viewFilterIds: [],
fieldPermissionIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
mainGroupByFieldMetadataViewIds: [],
@@ -107,6 +108,7 @@ export const createStandardFieldFlatMetadata = <
relationTargetFieldMetadataUniversalIdentifier: null,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
@@ -113,6 +113,7 @@ export const createStandardRelationFieldFlatMetadata = <
morphId,
viewFieldIds: [],
viewFilterIds: [],
fieldPermissionIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
mainGroupByFieldMetadataViewIds: [],
@@ -127,6 +128,7 @@ export const createStandardRelationFieldFlatMetadata = <
targetFieldDefinition.universalIdentifier,
viewFilterUniversalIdentifiers: [],
viewFieldUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
@@ -96,6 +96,7 @@ export const createStandardObjectFlatMetadata = <
fieldIds: [],
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
viewIds: [],
createdAt: now,
updatedAt: now,
@@ -103,6 +104,7 @@ export const createStandardObjectFlatMetadata = <
applicationUniversalIdentifier: twentyStandardApplicationId,
fieldUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier,
@@ -76,6 +76,7 @@ export const createStandardRoleFlatMetadata = ({
permissionFlagUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionIds: [],
fieldPermissionUniversalIdentifiers: [],
objectPermissionIds: [],
roleTargetIds: [],
roleTargetUniversalIdentifiers: [],
@@ -169,6 +169,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
case 'viewSort':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'webhook': {
if (!isDefined(idByUniversalIdentifier)) {
return action;
@@ -15,6 +15,7 @@ import { aggregateOrchestratorActionsReport } from 'src/engine/workspace-manager
import { crossEntityTransversalValidation } from 'src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util';
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
import { WorkspaceMigrationFieldPermissionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/workspace-migration-field-permission-actions-builder.service';
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service';
import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service';
@@ -52,6 +53,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationViewGroupActionsBuilderService: WorkspaceMigrationViewGroupActionsBuilderService,
private readonly workspaceMigrationViewFieldGroupActionsBuilderService: WorkspaceMigrationViewFieldGroupActionsBuilderService,
private readonly workspaceMigrationViewSortActionsBuilderService: WorkspaceMigrationViewSortActionsBuilderService,
private readonly workspaceMigrationFieldPermissionActionsBuilderService: WorkspaceMigrationFieldPermissionActionsBuilderService,
private readonly workspaceMigrationObjectPermissionActionsBuilderService: WorkspaceMigrationObjectPermissionActionsBuilderService,
private readonly workspaceMigrationPermissionFlagActionsBuilderService: WorkspaceMigrationPermissionFlagActionsBuilderService,
private readonly workspaceMigrationLogicFunctionActionsBuilderService: WorkspaceMigrationLogicFunctionActionsBuilderService,
@@ -145,6 +147,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatRowLevelPermissionPredicateGroupMaps,
flatRoleMaps,
flatObjectPermissionMaps,
flatFieldPermissionMaps,
flatPermissionFlagMaps,
flatRoleTargetMaps,
flatAgentMaps,
@@ -515,6 +518,34 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatFieldPermissionMaps)) {
const {
from: fromFlatFieldPermissionMaps,
to: toFlatFieldPermissionMaps,
} = flatFieldPermissionMaps;
const fieldPermissionResult =
await this.workspaceMigrationFieldPermissionActionsBuilderService.validateAndBuild(
{
additionalCacheDataMaps,
from: fromFlatFieldPermissionMaps,
to: toFlatFieldPermissionMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps,
workspaceId,
},
);
if (fieldPermissionResult.status === 'fail') {
orchestratorFailureReport.fieldPermission.push(
...fieldPermissionResult.errors,
);
} else {
orchestratorActionsReport.fieldPermission =
fieldPermissionResult.actions;
}
}
if (isDefined(flatPermissionFlagMaps)) {
const { from: fromFlatPermissionFlagMaps, to: toFlatPermissionFlagMaps } =
flatPermissionFlagMaps;
@@ -876,6 +907,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.objectPermission.update,
///
// Field permissions
...aggregatedOrchestratorActionsReport.fieldPermission.delete,
...aggregatedOrchestratorActionsReport.fieldPermission.create,
...aggregatedOrchestratorActionsReport.fieldPermission.update,
///
// Permission flags
...aggregatedOrchestratorActionsReport.permissionFlag.delete,
...aggregatedOrchestratorActionsReport.permissionFlag.create,
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should match snapshot 1`] = `
{
@@ -6,12 +6,14 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc
"commandMenuItem": [],
"fieldMetadata": [
"viewSortUniversalIdentifiers",
"fieldPermissionUniversalIdentifiers",
"viewFieldUniversalIdentifiers",
"viewFilterUniversalIdentifiers",
"kanbanAggregateOperationViewUniversalIdentifiers",
"calendarViewUniversalIdentifiers",
"mainGroupByFieldMetadataViewUniversalIdentifiers",
],
"fieldPermission": [],
"frontComponent": [],
"index": [],
"logicFunction": [],
@@ -20,6 +22,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc
"fieldUniversalIdentifiers",
"indexMetadataUniversalIdentifiers",
"objectPermissionUniversalIdentifiers",
"fieldPermissionUniversalIdentifiers",
"viewUniversalIdentifiers",
],
"objectPermission": [],
@@ -35,6 +38,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc
"roleTargetUniversalIdentifiers",
"objectPermissionUniversalIdentifiers",
"permissionFlagUniversalIdentifiers",
"fieldPermissionUniversalIdentifiers",
"rowLevelPermissionPredicateUniversalIdentifiers",
"rowLevelPermissionPredicateGroupUniversalIdentifiers",
],
@@ -39,6 +39,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
navigationMenuItem: {},
permissionFlag: {},
objectPermission: {},
fieldPermission: {},
frontComponent: {},
} as const satisfies {
[P in AllMetadataName]: Partial<{
@@ -0,0 +1,7 @@
import { type FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
export type UniversalFlatFieldPermission = UniversalFlatEntityFrom<
FieldPermissionEntity,
'fieldPermission'
>;
@@ -12,6 +12,8 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a crea
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
"fieldPermissionIds": [],
"fieldPermissionUniversalIdentifiers": [],
"icon": "icon",
"id": "field-id-1",
"isActive": true,
@@ -75,6 +77,8 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a dele
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
"fieldPermissionIds": [],
"fieldPermissionUniversalIdentifiers": [],
"icon": "icon",
"id": "field-id-1",
"isActive": true,
@@ -152,6 +156,8 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
"fieldPermissionIds": [],
"fieldPermissionUniversalIdentifiers": [],
"icon": "icon",
"id": "field-id-3",
"isActive": true,
@@ -201,6 +207,8 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
"fieldPermissionIds": [],
"fieldPermissionUniversalIdentifiers": [],
"icon": "icon",
"id": "field-id-2",
"isActive": true,
@@ -0,0 +1,24 @@
import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type';
import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type';
import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type';
import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type';
import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type';
import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type';
export type FlatCreateFieldPermissionAction =
BaseFlatCreateWorkspaceMigrationAction<'fieldPermission'>;
export type FlatUpdateFieldPermissionAction =
BaseFlatUpdateWorkspaceMigrationAction<'fieldPermission'>;
export type FlatDeleteFieldPermissionAction =
BaseFlatDeleteWorkspaceMigrationAction<'fieldPermission'>;
export type UniversalCreateFieldPermissionAction =
BaseUniversalCreateWorkspaceMigrationAction<'fieldPermission'>;
export type UniversalUpdateFieldPermissionAction =
BaseUniversalUpdateWorkspaceMigrationAction<'fieldPermission'>;
export type UniversalDeleteFieldPermissionAction =
BaseUniversalDeleteWorkspaceMigrationAction<'fieldPermission'>;
@@ -0,0 +1,120 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
import { UniversalUpdateFieldPermissionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/types/workspace-migration-field-permission-action.type';
import { FlatFieldPermissionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-field-permission-validator.service';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
@Injectable()
export class WorkspaceMigrationFieldPermissionActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
typeof ALL_METADATA_NAME.fieldPermission
> {
constructor(
private readonly flatFieldPermissionValidatorService: FlatFieldPermissionValidatorService,
) {
super(ALL_METADATA_NAME.fieldPermission);
}
protected validateFlatEntityCreation(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.fieldPermission
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.fieldPermission,
'create'
> {
const validationResult =
this.flatFieldPermissionValidatorService.validateFlatFieldPermissionCreation(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatFieldPermissionToValidate } = args;
return {
status: 'success',
action: {
type: 'create',
metadataName: 'fieldPermission',
flatEntity: flatFieldPermissionToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.fieldPermission
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.fieldPermission,
'delete'
> {
const validationResult =
this.flatFieldPermissionValidatorService.validateFlatFieldPermissionDeletion(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatFieldPermissionToValidate } = args;
return {
status: 'success',
action: {
type: 'delete',
metadataName: 'fieldPermission',
universalIdentifier: flatFieldPermissionToValidate.universalIdentifier,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.fieldPermission
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.fieldPermission,
'update'
> {
const validationResult =
this.flatFieldPermissionValidatorService.validateFlatFieldPermissionUpdate(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { universalIdentifier, flatEntityUpdate } = args;
const updateFieldPermissionAction: UniversalUpdateFieldPermissionAction = {
type: 'update',
metadataName: 'fieldPermission',
universalIdentifier,
update: flatEntityUpdate,
};
return {
status: 'success',
action: updateFieldPermissionAction,
};
}
}
@@ -0,0 +1,290 @@
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 { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
@Injectable()
export class FlatFieldPermissionValidatorService {
validateFlatFieldPermissionCreation({
flatEntityToValidate: flatFieldPermissionToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatFieldPermissionMaps: optimisticFlatFieldPermissionMaps,
flatRoleMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.fieldPermission
>): FailedFlatEntityValidation<'fieldPermission', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier: flatFieldPermissionToValidate.universalIdentifier,
roleUniversalIdentifier:
flatFieldPermissionToValidate.roleUniversalIdentifier,
objectMetadataUniversalIdentifier:
flatFieldPermissionToValidate.objectMetadataUniversalIdentifier,
fieldMetadataUniversalIdentifier:
flatFieldPermissionToValidate.fieldMetadataUniversalIdentifier,
},
metadataName: 'fieldPermission',
type: 'create',
});
const existingByUniversalId = findFlatEntityByUniversalIdentifier({
universalIdentifier: flatFieldPermissionToValidate.universalIdentifier,
flatEntityMaps: optimisticFlatFieldPermissionMaps,
});
if (isDefined(existingByUniversalId)) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Field permission with universal identifier ${flatFieldPermissionToValidate.universalIdentifier} already exists`,
userFriendlyMessage: msg`Field permission already exists`,
});
}
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatFieldPermissionToValidate.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
const referencedObjectMetadata = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatFieldPermissionToValidate.objectMetadataUniversalIdentifier,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(referencedObjectMetadata)) {
validationResult.errors.push({
code: PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
message: t`Object metadata not found`,
userFriendlyMessage: msg`Object metadata not found`,
});
}
const referencedFieldMetadata = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatFieldPermissionToValidate.fieldMetadataUniversalIdentifier,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(referencedFieldMetadata)) {
validationResult.errors.push({
code: PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND,
message: t`Field metadata not found`,
userFriendlyMessage: msg`Field metadata not found`,
});
}
const duplicateForSameRoleObjectField = Object.values(
optimisticFlatFieldPermissionMaps.byUniversalIdentifier,
).filter(
(fp) =>
isDefined(fp) &&
fp.roleUniversalIdentifier ===
flatFieldPermissionToValidate.roleUniversalIdentifier &&
fp.objectMetadataUniversalIdentifier ===
flatFieldPermissionToValidate.objectMetadataUniversalIdentifier &&
fp.fieldMetadataUniversalIdentifier ===
flatFieldPermissionToValidate.fieldMetadataUniversalIdentifier &&
fp.universalIdentifier !==
flatFieldPermissionToValidate.universalIdentifier,
);
if (duplicateForSameRoleObjectField.length > 0) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Field permission for this role, object and field already exists`,
userFriendlyMessage: msg`This field permission is already set for the role`,
});
}
return validationResult;
}
validateFlatFieldPermissionUpdate({
universalIdentifier,
flatEntityUpdate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatFieldPermissionMaps: optimisticFlatFieldPermissionMaps,
flatRoleMaps,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.fieldPermission
>): FailedFlatEntityValidation<'fieldPermission', 'update'> {
const existingFlatFieldPermission = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatFieldPermissionMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
},
metadataName: 'fieldPermission',
type: 'update',
});
if (!isDefined(existingFlatFieldPermission)) {
validationResult.errors.push({
code: PermissionsExceptionCode.FIELD_PERMISSION_NOT_FOUND,
message: t`Field permission to update not found`,
userFriendlyMessage: msg`Field permission not found`,
});
return validationResult;
}
const updatedFlatFieldPermission = {
...existingFlatFieldPermission,
...flatEntityUpdate,
};
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier: updatedFlatFieldPermission.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
if (isDefined(flatEntityUpdate.objectMetadataUniversalIdentifier)) {
const referencedObjectMetadata = findFlatEntityByUniversalIdentifier({
universalIdentifier:
updatedFlatFieldPermission.objectMetadataUniversalIdentifier,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(referencedObjectMetadata)) {
validationResult.errors.push({
code: PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
message: t`Object metadata not found`,
userFriendlyMessage: msg`Object metadata not found`,
});
}
}
if (isDefined(flatEntityUpdate.fieldMetadataUniversalIdentifier)) {
const referencedFieldMetadata = findFlatEntityByUniversalIdentifier({
universalIdentifier:
updatedFlatFieldPermission.fieldMetadataUniversalIdentifier,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(referencedFieldMetadata)) {
validationResult.errors.push({
code: PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND,
message: t`Field metadata not found`,
userFriendlyMessage: msg`Field metadata not found`,
});
}
}
const duplicateForSameRoleObjectField = Object.values(
optimisticFlatFieldPermissionMaps.byUniversalIdentifier,
).filter(
(fp) =>
isDefined(fp) &&
fp.roleUniversalIdentifier ===
updatedFlatFieldPermission.roleUniversalIdentifier &&
fp.objectMetadataUniversalIdentifier ===
updatedFlatFieldPermission.objectMetadataUniversalIdentifier &&
fp.fieldMetadataUniversalIdentifier ===
updatedFlatFieldPermission.fieldMetadataUniversalIdentifier &&
fp.universalIdentifier !== universalIdentifier,
);
if (duplicateForSameRoleObjectField.length > 0) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Field permission for this role, object and field already exists`,
userFriendlyMessage: msg`This field permission is already set for the role`,
});
}
return validationResult;
}
validateFlatFieldPermissionDeletion({
flatEntityToValidate: { universalIdentifier },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatFieldPermissionMaps: optimisticFlatFieldPermissionMaps,
flatRoleMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.fieldPermission
>): FailedFlatEntityValidation<'fieldPermission', 'delete'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
},
metadataName: 'fieldPermission',
type: 'delete',
});
const existingFlatFieldPermission = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatFieldPermissionMaps,
});
if (!isDefined(existingFlatFieldPermission)) {
validationResult.errors.push({
code: PermissionsExceptionCode.FIELD_PERMISSION_NOT_FOUND,
message: t`Field permission to delete not found`,
userFriendlyMessage: msg`Field permission not found`,
});
} else {
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier:
existingFlatFieldPermission.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (isDefined(referencedRole) && !referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
return validationResult;
}
}
@@ -6,6 +6,7 @@ import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-mo
import { FlatAgentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-agent-validator.service';
import { FlatCommandMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-command-menu-item-validator.service';
import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-field-metadata-validator.service';
import { FlatFieldPermissionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-field-permission-validator.service';
import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service';
import { FlatIndexValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-index-metadata-validator.service';
import { FlatLogicFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-logic-function-validator.service';
@@ -40,6 +41,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp
FlatViewGroupValidatorService,
FlatViewFieldGroupValidatorService,
FlatViewSortValidatorService,
FlatFieldPermissionValidatorService,
FlatObjectPermissionValidatorService,
FlatPermissionFlagValidatorService,
FlatIndexValidatorService,
@@ -70,6 +72,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp
FlatViewGroupValidatorService,
FlatViewFieldGroupValidatorService,
FlatViewSortValidatorService,
FlatFieldPermissionValidatorService,
FlatObjectPermissionValidatorService,
FlatPermissionFlagValidatorService,
FlatIndexValidatorService,
@@ -4,6 +4,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
import { WorkspaceMigrationFieldPermissionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/workspace-migration-field-permission-actions-builder.service';
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service';
import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service';
@@ -42,6 +43,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationViewGroupActionsBuilderService,
WorkspaceMigrationViewFieldGroupActionsBuilderService,
WorkspaceMigrationViewSortActionsBuilderService,
WorkspaceMigrationFieldPermissionActionsBuilderService,
WorkspaceMigrationObjectPermissionActionsBuilderService,
WorkspaceMigrationPermissionFlagActionsBuilderService,
WorkspaceMigrationIndexActionsBuilderService,
@@ -71,6 +73,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationViewGroupActionsBuilderService,
WorkspaceMigrationViewFieldGroupActionsBuilderService,
WorkspaceMigrationViewSortActionsBuilderService,
WorkspaceMigrationFieldPermissionActionsBuilderService,
WorkspaceMigrationObjectPermissionActionsBuilderService,
WorkspaceMigrationPermissionFlagActionsBuilderService,
WorkspaceMigrationFieldActionsBuilderService,
@@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import {
FlatCreateFieldPermissionAction,
UniversalCreateFieldPermissionAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/types/workspace-migration-field-permission-action.type';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
@Injectable()
export class CreateFieldPermissionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'fieldPermission',
) {
override async transpileUniversalActionToFlatAction({
action,
allFlatEntityMaps,
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateFieldPermissionAction>): Promise<FlatCreateFieldPermissionAction> {
const { roleId, objectMetadataId, fieldMetadataId } =
resolveUniversalRelationIdentifiersToIds({
flatEntityMaps: allFlatEntityMaps,
metadataName: action.metadataName,
universalForeignKeyValues: action.flatEntity,
});
return {
...action,
flatEntity: {
...action.flatEntity,
roleId,
objectMetadataId,
fieldMetadataId,
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
},
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatCreateFieldPermissionAction>,
): Promise<void> {
const { flatAction, queryRunner } = context;
const { flatEntity } = flatAction;
await this.insertFlatEntitiesInRepository({
queryRunner,
flatEntities: [flatEntity],
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatCreateFieldPermissionAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import {
FlatDeleteFieldPermissionAction,
UniversalDeleteFieldPermissionAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/types/workspace-migration-field-permission-action.type';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteFieldPermissionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete',
'fieldPermission',
) {
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalDeleteFieldPermissionAction>,
): Promise<FlatDeleteFieldPermissionAction> {
return this.transpileUniversalDeleteActionToFlatDeleteAction(context);
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatDeleteFieldPermissionAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const fieldPermissionRepository =
queryRunner.manager.getRepository<FieldPermissionEntity>(
FieldPermissionEntity,
);
await fieldPermissionRepository.delete({
id: flatAction.entityId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatDeleteFieldPermissionAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,67 @@
import { Injectable } from '@nestjs/common';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import {
FlatUpdateFieldPermissionAction,
UniversalUpdateFieldPermissionAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/types/workspace-migration-field-permission-action.type';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
@Injectable()
export class UpdateFieldPermissionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update',
'fieldPermission',
) {
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalUpdateFieldPermissionAction>,
): Promise<FlatUpdateFieldPermissionAction> {
const { action, allFlatEntityMaps } = context;
const flatFieldPermission = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatFieldPermissionMaps,
universalIdentifier: action.universalIdentifier,
});
const update = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'fieldPermission',
universalUpdate: action.update,
allFlatEntityMaps,
});
return {
type: 'update',
metadataName: 'fieldPermission',
entityId: flatFieldPermission.id,
update,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatUpdateFieldPermissionAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const { entityId, update } = flatAction;
const fieldPermissionRepository =
queryRunner.manager.getRepository<FieldPermissionEntity>(
FieldPermissionEntity,
);
await fieldPermissionRepository.update(
{ id: entityId, workspaceId },
update,
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatUpdateFieldPermissionAction>,
): Promise<void> {
return;
}
}
@@ -130,6 +130,7 @@ export const fromUniversalFlatFieldMetadataToFlatFieldMetadata = ({
// Empty aggregator arrays for newly created entities
viewFieldIds: [],
viewFilterIds: [],
fieldPermissionIds: [],
calendarViewIds: [],
mainGroupByFieldMetadataViewIds: [],
kanbanAggregateOperationViewIds: [],
@@ -123,6 +123,7 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({
viewIds: [],
indexMetadataIds: [],
objectPermissionIds: [],
fieldPermissionIds: [],
...emptyUniversalForeignKeyAggregators,
};
};
@@ -9,6 +9,9 @@ import { UpdateAgentActionHandlerService } from 'src/engine/workspace-manager/wo
import { CreateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/create-command-menu-item-action-handler.service';
import { DeleteCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/delete-command-menu-item-action-handler.service';
import { UpdateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/update-command-menu-item-action-handler.service';
import { CreateFieldPermissionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field-permission/services/create-field-permission-action-handler.service';
import { DeleteFieldPermissionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field-permission/services/delete-field-permission-action-handler.service';
import { UpdateFieldPermissionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field-permission/services/update-field-permission-action-handler.service';
import { CreateFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/create-field-action-handler.service';
import { DeleteFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/delete-field-action-handler.service';
import { UpdateFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service';
@@ -144,6 +147,10 @@ import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/
UpdatePermissionFlagActionHandlerService,
DeletePermissionFlagActionHandlerService,
CreateFieldPermissionActionHandlerService,
UpdateFieldPermissionActionHandlerService,
DeleteFieldPermissionActionHandlerService,
CreateObjectPermissionActionHandlerService,
UpdateObjectPermissionActionHandlerService,
DeleteObjectPermissionActionHandlerService,
@@ -21,6 +21,7 @@ export const METADATA_EVENTS_TO_EMIT = {
navigationMenuItem: true,
permissionFlag: true,
objectPermission: true,
fieldPermission: true,
rowLevelPermissionPredicate: true,
rowLevelPermissionPredicateGroup: true,
index: true,
@@ -91,6 +91,7 @@ const deriveAllMetadataEventsFromCreateAction = (
case 'navigationMenuItem':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'viewSort':
case 'webhook': {
return [
@@ -55,6 +55,7 @@ const deriveAllMetadataEventsFromDeleteAction = ({
case 'navigationMenuItem':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'viewSort':
case 'webhook': {
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
@@ -133,6 +133,7 @@ const deriveAllMetadataEventsFromUpdateAction = ({
case 'navigationMenuItem':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'viewSort':
case 'webhook': {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
@@ -70,6 +70,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({
case 'navigationMenuItem':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'webhook': {
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
flatEntity: flatAction.flatEntity,
@@ -42,6 +42,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({
case 'navigationMenuItem':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'webhook': {
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
MetadataFlatEntity<typeof flatAction.metadataName>
@@ -63,6 +63,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({
case 'navigationMenuItem':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'webhook': {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
flatAction.metadataName,
@@ -82,7 +82,14 @@ const COMPANY_GQL_FIELDS_WITH_PEOPLE_CITY_AGGREGATE = `
}
`;
const expectNoGraphQLErrors = (response: any) => {
expect(response.body.errors).toBeUndefined();
expect(response.body.data).toBeDefined();
};
const expectPermissionDeniedError = (response: any) => {
expect(response.body.errors).toBeDefined();
expect(response.body.errors.length).toBeGreaterThan(0);
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
@@ -272,7 +279,7 @@ describe('Field permissions restrictions', () => {
}
});
describe('should throw an error if requesting a restricted field', () => {
describe('should allow restricted fields to be read', () => {
beforeEach(async () => {
await restrictAccessToCompanyEmployee(
customRoleId,
@@ -290,7 +297,10 @@ describe('Field permissions restrictions', () => {
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expectPermissionDeniedError(response);
expectNoGraphQLErrors(response);
expect(
response.body.data.companies.edges[0].node.employees,
).toBeDefined();
});
it('2. findOne', async () => {
@@ -302,7 +312,8 @@ describe('Field permissions restrictions', () => {
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expectPermissionDeniedError(response);
expectNoGraphQLErrors(response);
expect(response.body.data.company.employees).toBeDefined();
});
it('3. updateMany', async () => {
@@ -388,7 +399,7 @@ describe('Field permissions restrictions', () => {
});
});
it('2. should throw an error if requesting a restricted field of a related object', async () => {
it('2. should allow a restricted field of a related object to be read', async () => {
await restrictAccessToPersonCity(
customRoleId,
personObjectId,
@@ -402,7 +413,10 @@ describe('Field permissions restrictions', () => {
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expectPermissionDeniedError(response);
expectNoGraphQLErrors(response);
expect(
response.body.data.companies.edges[0].node.people.edges[0].node.city,
).toBeDefined();
});
it('3. should succeed if restricted fields exist but are not requested', async () => {
@@ -432,7 +446,7 @@ describe('Field permissions restrictions', () => {
});
describe('Aggregate operations', () => {
it('1. should throw an error if requesting a restricted field through aggregates', async () => {
it('1. should allow aggregate over a restricted field', async () => {
await restrictAccessToCompanyEmployee(
customRoleId,
companyObjectId,
@@ -452,10 +466,11 @@ describe('Field permissions restrictions', () => {
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expectPermissionDeniedError(response);
expectNoGraphQLErrors(response);
expect(response.body.data.companies.countEmptyEmployees).toBeDefined();
});
it('2. should throw an error if requesting a restricted field on related object through aggregates', async () => {
it('2. should allow aggregate over a restricted field on a related object', async () => {
await restrictAccessToPersonCity(
customRoleId,
personObjectId,
@@ -471,7 +486,10 @@ describe('Field permissions restrictions', () => {
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expectPermissionDeniedError(response);
expectNoGraphQLErrors(response);
expect(
response.body.data.companies.edges[0].node.people.percentageEmptyCity,
).toBeDefined();
});
});
});
@@ -14,8 +14,6 @@ import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update
import { upsertFieldPermissions } from 'test/integration/graphql/utils/upsert-field-permissions.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-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';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
const client = request(`http://localhost:${APP_PORT}`);
@@ -30,11 +28,33 @@ const COMPANY_GQL_FIELDS_WITHOUT_EMPLOYEES = `
id
name
`;
const expectPermissionDeniedError = (response: any) => {
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
const expectEmployeesIsAccessible = ({
response,
operationName,
expectedEmployees,
}: {
response: any;
operationName:
| 'createCompanies'
| 'createCompany'
| 'updateCompanies'
| 'updateCompany';
expectedEmployees?: number;
}) => {
expect(response.body.errors).toBeUndefined();
expect(response.body.data).toBeDefined();
const result =
operationName === 'createCompany' || operationName === 'updateCompany'
? response.body.data[operationName]
: response.body.data[operationName]?.[0];
expect(result).toBeDefined();
if (typeof expectedEmployees === 'number') {
expect(result.employees).toBe(expectedEmployees);
} else {
expect(typeof result.employees).toBe('number');
}
};
describe('Field update permissions restrictions', () => {
@@ -294,7 +314,7 @@ describe('Field update permissions restrictions', () => {
// });
// });
describe('should throw an error if creating with restricted fields', () => {
describe('should allow employees field when creating', () => {
beforeEach(async () => {
await restrictUpdateAccessToCompanyEmployee(
customRoleId,
@@ -317,7 +337,11 @@ describe('Field update permissions restrictions', () => {
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expectPermissionDeniedError(response);
expectEmployeesIsAccessible({
response,
operationName: 'createCompanies',
expectedEmployees: 15,
});
});
it('2. createOne with restricted field', async () => {
@@ -330,10 +354,14 @@ describe('Field update permissions restrictions', () => {
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expectPermissionDeniedError(response);
expectEmployeesIsAccessible({
response,
operationName: 'createCompany',
expectedEmployees: 25,
});
});
});
describe('should throw an error if reading restricted fields in update operations', () => {
describe('should allow employees field in update operation responses', () => {
beforeEach(async () => {
await restrictReadAccessToCompanyEmployee(
customRoleId,
@@ -348,12 +376,16 @@ describe('Field update permissions restrictions', () => {
objectMetadataPluralName: 'companies',
gqlFields: COMPANY_GQL_FIELDS_WITH_EMPLOYEES,
data: { name: 'UpdatedCompany' },
filter: { id: { eq: companyId } },
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expectPermissionDeniedError(response);
expectEmployeesIsAccessible({
response,
operationName: 'updateCompanies',
});
});
it('2. updateOne requesting restricted field in response', async () => {
@@ -367,7 +399,10 @@ describe('Field update permissions restrictions', () => {
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expectPermissionDeniedError(response);
expectEmployeesIsAccessible({
response,
operationName: 'updateCompany',
});
});
});
@@ -1,39 +0,0 @@
import gql from 'graphql-tag';
export const createUpsertFieldPermissionsOperation = (
roleId: string,
fieldPermissions: Array<{
objectMetadataId: string;
fieldMetadataId: string;
canReadFieldValue?: boolean | null;
canUpdateFieldValue?: boolean | null;
}>,
selectedFields: string[] = [
'id',
'roleId',
'objectMetadataId',
'fieldMetadataId',
'canReadFieldValue',
'canUpdateFieldValue',
],
) => ({
query: gql`
mutation UpsertFieldPermissions(
$roleId: UUID!
$fieldPermissions: [FieldPermissionInput!]!
) {
upsertFieldPermissions(
upsertFieldPermissionsInput: {
roleId: $roleId
fieldPermissions: $fieldPermissions
}
) {
${selectedFields.join('\n')}
}
}
`,
variables: {
roleId,
fieldPermissions,
},
});
@@ -1,27 +1,14 @@
import { createUpsertFieldPermissionsOperation } from 'test/integration/graphql/utils/upsert-field-permissions-operation-factory.util';
import { upsertFieldPermissionsQueryFactory } from 'test/integration/metadata/suites/field-permission/utils/upsert-field-permissions-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
export const upsertFieldPermissions = async ({
roleId,
fieldPermissions,
selectedFields,
}: {
roleId: string;
fieldPermissions: Array<{
objectMetadataId: string;
fieldMetadataId: string;
canReadFieldValue?: boolean | null;
canUpdateFieldValue?: boolean | null;
}>;
selectedFields?: string[];
}) => {
const operation = createUpsertFieldPermissionsOperation(
roleId,
fieldPermissions,
selectedFields,
);
import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
const response = await makeMetadataAPIRequest(operation);
export const upsertFieldPermissions = async (
input: UpsertFieldPermissionsInput,
) => {
const graphqlOperation = upsertFieldPermissionsQueryFactory({ input });
const response = await makeMetadataAPIRequest(graphqlOperation);
return {
data: response.body.data,
@@ -0,0 +1,126 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Field permission upsert should fail when canReadFieldValue is true (only restriction allowed) 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "ONLY_FIELD_RESTRICTION_ALLOWED",
"userFriendlyMessage": "Field permissions can only be used to restrict access, not to grant additional permissions.",
},
"message": "Field permission can only introduce a restriction",
"name": "UserInputError",
}
`;
exports[`Field permission upsert should fail when canUpdateFieldValue is true (only restriction allowed) 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"subCode": "ONLY_FIELD_RESTRICTION_ALLOWED",
"userFriendlyMessage": "Field permissions can only be used to restrict access, not to grant additional permissions.",
},
"message": "Field permission can only introduce a restriction",
"name": "UserInputError",
}
`;
exports[`Field permission upsert should fail when fieldMetadataId does not exist 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "FIELD_METADATA_NOT_FOUND",
"userFriendlyMessage": "The field you are trying to set permissions for could not be found. It may have been deleted.",
},
"message": "Field metadata not found",
"name": "NotFoundError",
}
`;
exports[`Field permission upsert should fail when object is system object 1`] = `
{
"extensions": {
"code": "FORBIDDEN",
"subCode": "CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT",
"userFriendlyMessage": "You cannot set field permissions on system objects as they are managed by the platform.",
},
"message": "Cannot add field permission on system object",
"name": "ForbiddenError",
}
`;
exports[`Field permission upsert should fail when objectMetadataId does not exist 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "OBJECT_METADATA_NOT_FOUND_PERMISSIONS",
"userFriendlyMessage": "The object you are trying to set permissions for could not be found. It may have been deleted.",
},
"message": "Object metadata not found",
"name": "NotFoundError",
}
`;
exports[`Field permission upsert should fail when role is not editable (system role) 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"fieldPermission": [
{
"errors": [
{
"code": "ROLE_NOT_EDITABLE",
"message": "Role is not editable",
"userFriendlyMessage": "This role cannot be modified because it is a system role. Only custom roles can be edited.",
},
],
"flatEntityMinimalInformation": {
"fieldMetadataUniversalIdentifier": Any<String>,
"objectMetadataUniversalIdentifier": Any<String>,
"roleUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "fieldPermission",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 1 fieldPermission",
"summary": {
"fieldPermission": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while upserting field permissions",
"name": "GraphQLError",
}
`;
exports[`Field permission upsert should fail when roleId does not exist 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "OBJECT_METADATA_NOT_FOUND_PERMISSIONS",
"userFriendlyMessage": "The object you are trying to set permissions for could not be found. It may have been deleted.",
},
"message": "Object metadata not found",
"name": "NotFoundError",
}
`;
exports[`Field permission upsert should fail when roleId is not a valid UUID 1`] = `
{
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
"value": "invalid-uuid",
},
"message": "Invalid UUID: 'invalid-uuid'",
"name": "ValidationError",
}
`;
@@ -0,0 +1,308 @@
import gql from 'graphql-tag';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { findManyFieldsMetadata } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata.util';
import { upsertFieldPermissions } from 'test/integration/metadata/suites/field-permission/utils/upsert-field-permissions.util';
import { upsertObjectPermissions } from 'test/integration/metadata/suites/object-permission/utils/upsert-object-permissions.util';
import { createOneRole } from 'test/integration/metadata/suites/role/utils/create-one-role.util';
import { deleteOneRole } from 'test/integration/metadata/suites/role/utils/delete-one-role.util';
import { findRoles } from 'test/integration/metadata/suites/role/utils/find-roles.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
type GlobalTestContext = {
editableRoleId: string;
nonEditableRoleId: string;
systemObjectMetadataId: string;
nonSystemObjectMetadataId: string;
oneFieldMetadataId: string;
};
type TestContext = {
input: (globalContext: GlobalTestContext) => UpsertFieldPermissionsInput;
};
const failingFieldPermissionUpsertTestCases: EachTestingContext<TestContext>[] =
[
{
title: 'when roleId is not a valid UUID',
context: {
input: () => ({
roleId: 'invalid-uuid',
fieldPermissions: [
{
objectMetadataId: v4(),
fieldMetadataId: v4(),
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
}),
},
},
{
title: 'when roleId does not exist',
context: {
input: () => ({
roleId: v4(),
fieldPermissions: [
{
objectMetadataId: v4(),
fieldMetadataId: v4(),
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
}),
},
},
{
title: 'when role is not editable (system role)',
context: {
input: (globalContext: GlobalTestContext) => ({
roleId: globalContext.nonEditableRoleId,
fieldPermissions: [
{
objectMetadataId: globalContext.nonSystemObjectMetadataId,
fieldMetadataId: globalContext.oneFieldMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
}),
},
},
{
title: 'when objectMetadataId does not exist',
context: {
input: (globalContext: GlobalTestContext) => ({
roleId: globalContext.editableRoleId,
fieldPermissions: [
{
objectMetadataId: v4(),
fieldMetadataId: globalContext.oneFieldMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
}),
},
},
{
title: 'when fieldMetadataId does not exist',
context: {
input: (globalContext: GlobalTestContext) => ({
roleId: globalContext.editableRoleId,
fieldPermissions: [
{
objectMetadataId: globalContext.nonSystemObjectMetadataId,
fieldMetadataId: v4(),
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
}),
},
},
{
title: 'when object is system object',
context: {
input: (globalContext: GlobalTestContext) => ({
roleId: globalContext.editableRoleId,
fieldPermissions: [
{
objectMetadataId: globalContext.systemObjectMetadataId,
fieldMetadataId: globalContext.oneFieldMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
}),
},
},
{
title: 'when canReadFieldValue is true (only restriction allowed)',
context: {
input: (globalContext: GlobalTestContext) => ({
roleId: globalContext.editableRoleId,
fieldPermissions: [
{
objectMetadataId: globalContext.nonSystemObjectMetadataId,
fieldMetadataId: globalContext.oneFieldMetadataId,
canReadFieldValue: true,
canUpdateFieldValue: false,
},
],
}),
},
},
{
title: 'when canUpdateFieldValue is true (only restriction allowed)',
context: {
input: (globalContext: GlobalTestContext) => ({
roleId: globalContext.editableRoleId,
fieldPermissions: [
{
objectMetadataId: globalContext.nonSystemObjectMetadataId,
fieldMetadataId: globalContext.oneFieldMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: true,
},
],
}),
},
},
];
describe('Field permission upsert should fail', () => {
let editableRoleId: string;
let nonEditableRoleId: string;
let systemObjectMetadataId: string;
let nonSystemObjectMetadataId: string;
let oneFieldMetadataId: string;
beforeAll(async () => {
const { data: roleData } = await createOneRole({
expectToFail: false,
input: {
label: 'Test Role For Field Permission Failing',
description: 'Role for field permission failing tests',
icon: 'IconSettings',
canUpdateAllSettings: false,
canAccessAllTools: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
},
});
editableRoleId = roleData?.createOneRole?.id;
jestExpectToBeDefined(editableRoleId);
const { data: rolesData } = await findRoles({
expectToFail: false,
gqlFields: `
id
label
isEditable
`,
});
jestExpectToBeDefined(rolesData?.getRoles);
const adminRole = rolesData.getRoles.find(
(role: { label: string; isEditable: boolean }) =>
role.label === 'Admin' && role.isEditable === false,
);
jestExpectToBeDefined(adminRole);
nonEditableRoleId = adminRole.id;
const getObjectMetadataOperation = {
query: gql`
query {
objects(paging: { first: 1000 }) {
edges {
node {
id
nameSingular
isSystem
}
}
}
}
`,
};
const objectMetadataResponse = await makeMetadataAPIRequest(
getObjectMetadataOperation,
);
const edges = objectMetadataResponse.body.data?.objects?.edges ?? [];
const systemObjectNode = edges.find(
(edge: { node: { isSystem: boolean | string } }) =>
edge.node.isSystem === true || String(edge.node.isSystem) === 'true',
)?.node;
jestExpectToBeDefined(systemObjectNode);
systemObjectMetadataId = systemObjectNode.id;
const nonSystemObjectNode = edges.find(
(edge: { node: { isSystem: boolean | string } }) =>
edge.node.isSystem === false || String(edge.node.isSystem) === 'false',
)?.node;
jestExpectToBeDefined(nonSystemObjectNode);
nonSystemObjectMetadataId = nonSystemObjectNode.id;
const { fields } = await findManyFieldsMetadata({
expectToFail: false,
input: {
filter: { objectMetadataId: { eq: nonSystemObjectMetadataId } },
paging: { first: 50 },
},
gqlFields: 'id type',
});
jestExpectToBeDefined(fields);
expect(fields?.length).toBeGreaterThan(0);
const nonRelationField = fields.find(
(field: { node: { type: string } }) => field.node.type !== 'RELATION',
);
jestExpectToBeDefined(nonRelationField);
oneFieldMetadataId = nonRelationField.node.id;
await upsertObjectPermissions({
expectToFail: false,
input: {
roleId: editableRoleId,
objectPermissions: [
{
objectMetadataId: nonSystemObjectMetadataId,
canReadObjectRecords: true,
canUpdateObjectRecords: false,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
},
});
});
afterAll(async () => {
if (isDefined(editableRoleId)) {
await deleteOneRole({
expectToFail: false,
input: { idToDelete: editableRoleId },
});
}
});
it.each(eachTestingContextFilter(failingFieldPermissionUpsertTestCases))(
'$title',
async ({ context }) => {
const globalContext: GlobalTestContext = {
editableRoleId: editableRoleId ?? '',
nonEditableRoleId: nonEditableRoleId ?? '',
systemObjectMetadataId: systemObjectMetadataId ?? '',
nonSystemObjectMetadataId: nonSystemObjectMetadataId ?? '',
oneFieldMetadataId: oneFieldMetadataId ?? '',
};
const input = context.input(globalContext);
const { errors } = await upsertFieldPermissions({
expectToFail: true,
input,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
},
);
});

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