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:
@@ -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']>;
|
||||
|
||||
+1
@@ -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;
|
||||
|
||||
+111
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -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
@@ -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,
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+25
@@ -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"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+58
@@ -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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
@@ -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`,
|
||||
);
|
||||
};
|
||||
+2
@@ -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,
|
||||
|
||||
+1
@@ -94,6 +94,7 @@ export const fromFieldManifestToUniversalFlatFieldMetadata = ({
|
||||
relationTargetObjectMetadataUniversalIdentifier,
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({
|
||||
fieldUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
objectManifest.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ export const fromRoleManifestToUniversalFlatRole = ({
|
||||
canBeAssignedToApiKeys: roleManifest.canBeAssignedToApiKeys ?? true,
|
||||
roleTargetUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateGroupUniversalIdentifiers: [],
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+1
@@ -146,6 +146,7 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@FilterableField(() => UUIDScalarType)
|
||||
objectMetadataId: string;
|
||||
|
||||
@IsBoolean()
|
||||
|
||||
+10
@@ -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",
|
||||
|
||||
+37
@@ -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,
|
||||
|
||||
+13
@@ -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: {
|
||||
|
||||
+25
@@ -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: {
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
|
||||
roleTarget: {},
|
||||
permissionFlag: {},
|
||||
objectPermission: {},
|
||||
fieldPermission: {},
|
||||
pageLayout: {},
|
||||
pageLayoutTab: {},
|
||||
pageLayoutWidget: {
|
||||
|
||||
+19
-3
@@ -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
@@ -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,
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ type Assertions = [
|
||||
Expect<
|
||||
Equal<
|
||||
FieldMetadataOneToManySyncableRelations,
|
||||
| 'fieldPermissions'
|
||||
| 'viewFields'
|
||||
| 'viewFilters'
|
||||
| 'kanbanAggregateOperationViews'
|
||||
|
||||
+27
@@ -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: {
|
||||
|
||||
+11
@@ -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",
|
||||
]
|
||||
|
||||
+1
@@ -3,6 +3,7 @@
|
||||
exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = `
|
||||
[
|
||||
"rowLevelPermissionPredicate",
|
||||
"fieldPermission",
|
||||
"navigationMenuItem",
|
||||
"viewField",
|
||||
"viewFilter",
|
||||
|
||||
+2
@@ -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: [],
|
||||
|
||||
+2
@@ -37,6 +37,8 @@ export const getRelationTargetFlatFieldMetadataMock = ({
|
||||
viewFilterIds: [],
|
||||
viewFieldIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
description: 'default flat field metadata description',
|
||||
|
||||
+4
@@ -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,
|
||||
|
||||
+6
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+1
@@ -53,6 +53,7 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
|
||||
+9
@@ -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'>[];
|
||||
+100
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
@@ -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>;
|
||||
+4
@@ -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>;
|
||||
+56
@@ -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,
|
||||
};
|
||||
};
|
||||
+81
@@ -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,
|
||||
};
|
||||
};
|
||||
+2
@@ -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:
|
||||
|
||||
+1
@@ -95,6 +95,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
fieldUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
|
||||
+6
@@ -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,
|
||||
) ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateIds: [],
|
||||
rowLevelPermissionPredicateUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateGroupIds: [],
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+8
@@ -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>;
|
||||
|
||||
|
||||
+1
@@ -151,6 +151,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
viewSortUniversalIdentifiers: [],
|
||||
};
|
||||
|
||||
+272
-90
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
|
||||
+302
-294
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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,
|
||||
});
|
||||
+4
@@ -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',
|
||||
|
||||
+1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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({
|
||||
|
||||
+4
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+1
@@ -225,6 +225,7 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
isSearchable: true,
|
||||
universalIdentifier: partial.id,
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
viewIds: [],
|
||||
applicationId: null,
|
||||
isLabelSyncedWithName: false,
|
||||
|
||||
+1
@@ -74,6 +74,7 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => {
|
||||
fieldIds: ['name-id'],
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
viewIds: [],
|
||||
applicationId: null,
|
||||
} as unknown as FlatObjectMetadata;
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+2
@@ -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',
|
||||
|
||||
+2
@@ -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: [],
|
||||
|
||||
+2
@@ -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: [],
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+1
@@ -76,6 +76,7 @@ export const createStandardRoleFlatMetadata = ({
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
objectPermissionIds: [],
|
||||
roleTargetIds: [],
|
||||
roleTargetUniversalIdentifiers: [],
|
||||
|
||||
+1
@@ -169,6 +169,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
|
||||
case 'viewSort':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'webhook': {
|
||||
if (!isDefined(idByUniversalIdentifier)) {
|
||||
return action;
|
||||
|
||||
+37
@@ -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,
|
||||
|
||||
+5
-1
@@ -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",
|
||||
],
|
||||
|
||||
+1
@@ -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<{
|
||||
|
||||
+7
@@ -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'
|
||||
>;
|
||||
+8
@@ -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,
|
||||
|
||||
+24
@@ -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'>;
|
||||
+120
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+290
@@ -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;
|
||||
}
|
||||
}
|
||||
+3
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+65
@@ -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;
|
||||
}
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+67
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
@@ -130,6 +130,7 @@ export const fromUniversalFlatFieldMetadataToFlatFieldMetadata = ({
|
||||
// Empty aggregator arrays for newly created entities
|
||||
viewFieldIds: [],
|
||||
viewFilterIds: [],
|
||||
fieldPermissionIds: [],
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
|
||||
+1
@@ -123,6 +123,7 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({
|
||||
viewIds: [],
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
...emptyUniversalForeignKeyAggregators,
|
||||
};
|
||||
};
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ export const METADATA_EVENTS_TO_EMIT = {
|
||||
navigationMenuItem: true,
|
||||
permissionFlag: true,
|
||||
objectPermission: true,
|
||||
fieldPermission: true,
|
||||
rowLevelPermissionPredicate: true,
|
||||
rowLevelPermissionPredicateGroup: true,
|
||||
index: true,
|
||||
|
||||
+1
@@ -91,6 +91,7 @@ const deriveAllMetadataEventsFromCreateAction = (
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook': {
|
||||
return [
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ const deriveAllMetadataEventsFromDeleteAction = ({
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook': {
|
||||
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
|
||||
|
||||
+1
@@ -133,6 +133,7 @@ const deriveAllMetadataEventsFromUpdateAction = ({
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook': {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
|
||||
|
||||
+1
@@ -70,6 +70,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'webhook': {
|
||||
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: flatAction.flatEntity,
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'webhook': {
|
||||
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
|
||||
MetadataFlatEntity<typeof flatAction.metadataName>
|
||||
|
||||
+1
@@ -63,6 +63,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'webhook': {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
|
||||
flatAction.metadataName,
|
||||
|
||||
+27
-9
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+48
-13
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
-39
@@ -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,
|
||||
},
|
||||
});
|
||||
+8
-21
@@ -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,
|
||||
|
||||
+126
@@ -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",
|
||||
}
|
||||
`;
|
||||
+308
@@ -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
Reference in New Issue
Block a user