Migrate object permission to syncable entity (#18609)
Closes [#2223](https://github.com/twentyhq/core-team-issues/issues/2223)
This commit is contained in:
@@ -256,6 +256,7 @@ export enum AllMetadataName {
|
||||
logicFunction = 'logicFunction',
|
||||
navigationMenuItem = 'navigationMenuItem',
|
||||
objectMetadata = 'objectMetadata',
|
||||
objectPermission = 'objectPermission',
|
||||
pageLayout = 'pageLayout',
|
||||
pageLayoutTab = 'pageLayoutTab',
|
||||
pageLayoutWidget = 'pageLayoutWidget',
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ export const useMetadataErrorHandler = () => {
|
||||
index: t`index`,
|
||||
logicFunction: t`logic function`,
|
||||
permissionFlag: t`permission flag`,
|
||||
objectPermission: t`object permission`,
|
||||
role: t`role`,
|
||||
roleTarget: t`role target`,
|
||||
agent: t`agent`,
|
||||
|
||||
@@ -2241,6 +2241,7 @@ enum AllMetadataName {
|
||||
commandMenuItem
|
||||
navigationMenuItem
|
||||
permissionFlag
|
||||
objectPermission
|
||||
frontComponent
|
||||
webhook
|
||||
}
|
||||
|
||||
@@ -1892,7 +1892,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' | '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' | 'frontComponent' | 'webhook'
|
||||
|
||||
export interface MinimalObjectMetadata {
|
||||
id: Scalars['UUID']
|
||||
@@ -8788,6 +8788,7 @@ export const enumAllMetadataName = {
|
||||
commandMenuItem: 'commandMenuItem' as const,
|
||||
navigationMenuItem: 'navigationMenuItem' as const,
|
||||
permissionFlag: 'permissionFlag' as const,
|
||||
objectPermission: 'objectPermission' as const,
|
||||
frontComponent: 'frontComponent' as const,
|
||||
webhook: 'webhook' as const
|
||||
}
|
||||
|
||||
+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 { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:identify-object-permission-metadata',
|
||||
description:
|
||||
'Identify object permission metadata (backfill universalIdentifier and applicationId)',
|
||||
})
|
||||
export class IdentifyObjectPermissionMetadataCommand 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 IdentifyObjectPermissionMetadataCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const repository = queryRunner.manager.getRepository(
|
||||
ObjectPermissionEntity,
|
||||
);
|
||||
const withNullApplicationId = await repository.find({
|
||||
where: { applicationId: IsNull() },
|
||||
relations: ['role'],
|
||||
});
|
||||
|
||||
const toUpdate = withNullApplicationId.filter((objectPermission) =>
|
||||
isDefined(objectPermission.role?.applicationId),
|
||||
);
|
||||
const toRemove = withNullApplicationId.filter(
|
||||
(objectPermission) => !isDefined(objectPermission.role?.applicationId),
|
||||
);
|
||||
|
||||
for (const objectPermission of toUpdate) {
|
||||
objectPermission.applicationId = objectPermission.role!.applicationId;
|
||||
objectPermission.universalIdentifier =
|
||||
objectPermission.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 objectPermission of withNullUniversalIdentifier) {
|
||||
objectPermission.universalIdentifier = v4();
|
||||
}
|
||||
|
||||
if (withNullUniversalIdentifier.length > 0) {
|
||||
await repository.save(withNullUniversalIdentifier);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run IdentifyObjectPermissionMetadataCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back IdentifyObjectPermissionMetadataCommand: ${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 { makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773317160558-make-object-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-object-permission-universal-identifier-and-application-id-not-nullable-migration',
|
||||
description:
|
||||
'Set NOT NULL on objectPermission universalIdentifier and applicationId, add unique index and FK (run identify-object-permission-metadata first)',
|
||||
})
|
||||
export class MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand 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 MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
'Successfully run MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
|
||||
);
|
||||
this.hasRunOnce = true;
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Rolling back MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -4,7 +4,9 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
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 { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.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 { 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 { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
|
||||
@@ -35,6 +37,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
providers: [
|
||||
IdentifyPermissionFlagMetadataCommand,
|
||||
MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
IdentifyObjectPermissionMetadataCommand,
|
||||
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillNavigationMenuItemTypeCommand,
|
||||
BackfillPageLayoutsCommand,
|
||||
@@ -44,6 +48,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
exports: [
|
||||
IdentifyPermissionFlagMetadataCommand,
|
||||
MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
IdentifyObjectPermissionMetadataCommand,
|
||||
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillNavigationMenuItemTypeCommand,
|
||||
BackfillPageLayoutsCommand,
|
||||
|
||||
+7
@@ -36,7 +36,9 @@ 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 { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.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 { 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 { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
|
||||
@@ -92,6 +94,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
// 1.20 Commands
|
||||
protected readonly identifyPermissionFlagMetadataCommand: IdentifyPermissionFlagMetadataCommand,
|
||||
protected readonly makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly identifyObjectPermissionMetadataCommand: IdentifyObjectPermissionMetadataCommand,
|
||||
protected readonly makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
protected readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand,
|
||||
protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand,
|
||||
protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand,
|
||||
@@ -147,6 +151,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.identifyPermissionFlagMetadataCommand,
|
||||
this
|
||||
.makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this.identifyObjectPermissionMetadataCommand,
|
||||
this
|
||||
.makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
this.backfillNavigationMenuItemTypeCommand,
|
||||
this.migrateRichTextToTextCommand,
|
||||
this.backfillCommandMenuItemsCommand,
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddUniversalIdentifierAndApplicationIdToObjectPermission1773317160558
|
||||
implements MigrationInterface
|
||||
{
|
||||
name =
|
||||
'AddUniversalIdentifierAndApplicationIdToObjectPermission1773317160558';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" ADD "universalIdentifier" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" ADD "applicationId" uuid`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" DROP COLUMN IF EXISTS "applicationId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" DROP COLUMN IF EXISTS "universalIdentifier"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
import { makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773317160558-make-object-permission-universal-identifier-and-application-id-not-null.util';
|
||||
|
||||
export class MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNull1773317160559
|
||||
implements MigrationInterface
|
||||
{
|
||||
name =
|
||||
'MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNull1773317160559';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const savepointName =
|
||||
'sp_make_object_permission_universal_identifier_and_application_id_not_null';
|
||||
|
||||
try {
|
||||
await queryRunner.query(`SAVEPOINT ${savepointName}`);
|
||||
|
||||
await makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries(
|
||||
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 MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNull1773317160559',
|
||||
rollbackError,
|
||||
);
|
||||
throw rollbackError;
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(
|
||||
'Swallowing MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNull1773317160559 error',
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" DROP CONSTRAINT IF EXISTS "FK_f2ecee1066fd43800dbc85f87e4"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "core"."IDX_c5ea53618b32558fe24e495f21"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" ALTER COLUMN "applicationId" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" ALTER COLUMN "universalIdentifier" DROP NOT NULL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
export const makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullQueries =
|
||||
async (queryRunner: QueryRunner): Promise<void> => {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" ALTER COLUMN "universalIdentifier" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" ALTER COLUMN "applicationId" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "core"."IDX_c5ea53618b32558fe24e495f21"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_c5ea53618b32558fe24e495f21" ON "core"."objectPermission" ("workspaceId", "universalIdentifier")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectPermission" ADD CONSTRAINT "FK_f2ecee1066fd43800dbc85f87e4" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
};
|
||||
+2
@@ -153,6 +153,7 @@ export const mockPersonFlatObjectMetadata = (
|
||||
workspaceId,
|
||||
universalIdentifier: objectMetadataId,
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldIds: mockFieldMetadatas.map((field) => field.id),
|
||||
viewIds: [],
|
||||
applicationId: 'test-application-id',
|
||||
@@ -168,6 +169,7 @@ export const mockPersonFlatObjectMetadata = (
|
||||
(field) => field.universalIdentifier,
|
||||
),
|
||||
viewUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
|
||||
+3
-2
@@ -11,10 +11,10 @@ import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { RunWorkspaceMigrationInput } from 'src/engine/core-modules/application/application-manifest/dtos/run-workspace-migration.input';
|
||||
import { UninstallApplicationInput } from 'src/engine/core-modules/application/application-manifest/dtos/uninstall-application.input';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
|
||||
|
||||
@@ -56,7 +57,7 @@ export class ApplicationManifestResolver {
|
||||
|
||||
await this.workspaceMigrationRunnerService.run({
|
||||
workspaceMigration: {
|
||||
actions,
|
||||
actions: actions as AllUniversalWorkspaceMigrationAction[],
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({
|
||||
isLabelSyncedWithName: false,
|
||||
fieldUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
objectManifest.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ export const fromRoleManifestToUniversalFlatRole = ({
|
||||
canBeAssignedToAgents: roleManifest.canBeAssignedToAgents ?? true,
|
||||
canBeAssignedToApiKeys: roleManifest.canBeAssignedToApiKeys ?? true,
|
||||
roleTargetUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateGroupUniversalIdentifiers: [],
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ const mockObjectMetadata: FlatObjectMetadata = {
|
||||
isAuditLogged: true,
|
||||
isSearchable: true,
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldIds: [],
|
||||
viewIds: [],
|
||||
applicationId: 'test-application-id',
|
||||
@@ -36,6 +37,7 @@ const mockObjectMetadata: FlatObjectMetadata = {
|
||||
duplicateCriteria: null,
|
||||
applicationUniversalIdentifier: 'test-application-id',
|
||||
fieldUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
|
||||
+11
@@ -136,6 +136,17 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"standardOverrides",
|
||||
],
|
||||
},
|
||||
"objectPermission": {
|
||||
"propertiesToCompare": [
|
||||
"roleUniversalIdentifier",
|
||||
"objectMetadataUniversalIdentifier",
|
||||
"canReadObjectRecords",
|
||||
"canUpdateObjectRecords",
|
||||
"canSoftDeleteObjectRecords",
|
||||
"canDestroyObjectRecords",
|
||||
],
|
||||
"propertiesToStringify": [],
|
||||
},
|
||||
"pageLayout": {
|
||||
"propertiesToCompare": [
|
||||
"name",
|
||||
|
||||
+42
@@ -1156,6 +1156,48 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
universalProperty: undefined,
|
||||
},
|
||||
},
|
||||
objectPermission: {
|
||||
roleId: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: 'roleUniversalIdentifier',
|
||||
},
|
||||
objectMetadataId: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: 'objectMetadataUniversalIdentifier',
|
||||
},
|
||||
canReadObjectRecords: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
canUpdateObjectRecords: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
canSoftDeleteObjectRecords: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
canDestroyObjectRecords: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
createdAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
updatedAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
},
|
||||
rowLevelPermissionPredicate: {
|
||||
fieldMetadataId: {
|
||||
toCompare: true,
|
||||
|
||||
+10
@@ -170,6 +170,16 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
|
||||
foreignKey: 'roleId',
|
||||
},
|
||||
},
|
||||
objectPermission: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
role: {
|
||||
foreignKey: 'roleId',
|
||||
},
|
||||
objectMetadata: {
|
||||
foreignKey: 'objectMetadataId',
|
||||
},
|
||||
},
|
||||
pageLayout: {
|
||||
workspace: null,
|
||||
objectMetadata: {
|
||||
|
||||
+18
@@ -291,6 +291,24 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
|
||||
universalForeignKey: 'roleUniversalIdentifier',
|
||||
},
|
||||
},
|
||||
objectPermission: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
role: {
|
||||
metadataName: 'role',
|
||||
foreignKey: 'roleId',
|
||||
inverseOneToManyProperty: 'objectPermissions',
|
||||
isNullable: false,
|
||||
universalForeignKey: 'roleUniversalIdentifier',
|
||||
},
|
||||
objectMetadata: {
|
||||
metadataName: 'objectMetadata',
|
||||
foreignKey: 'objectMetadataId',
|
||||
inverseOneToManyProperty: 'objectPermissions',
|
||||
isNullable: false,
|
||||
universalForeignKey: 'objectMetadataUniversalIdentifier',
|
||||
},
|
||||
},
|
||||
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 { 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';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
@@ -46,6 +47,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
|
||||
skill: SkillEntity,
|
||||
logicFunction: LogicFunctionEntity,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
objectPermission: ObjectPermissionEntity,
|
||||
role: RoleEntity,
|
||||
agent: AgentEntity,
|
||||
commandMenuItem: CommandMenuItemEntity,
|
||||
|
||||
+4
@@ -77,6 +77,10 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
|
||||
permissionFlag: {
|
||||
role: true,
|
||||
},
|
||||
objectPermission: {
|
||||
role: true,
|
||||
objectMetadata: true,
|
||||
},
|
||||
pageLayout: {
|
||||
objectMetadata: true,
|
||||
pageLayoutTab: true,
|
||||
|
||||
+1
@@ -34,6 +34,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
|
||||
role: {},
|
||||
roleTarget: {},
|
||||
permissionFlag: {},
|
||||
objectPermission: {},
|
||||
pageLayout: {},
|
||||
pageLayoutTab: {},
|
||||
pageLayoutWidget: {
|
||||
|
||||
+13
-2
@@ -85,7 +85,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
universalFlatEntityForeignKeyAggregator:
|
||||
'indexMetadataUniversalIdentifiers',
|
||||
},
|
||||
objectPermissions: null,
|
||||
objectPermissions: {
|
||||
metadataName: 'objectPermission',
|
||||
flatEntityForeignKeyAggregator: 'objectPermissionIds',
|
||||
universalFlatEntityForeignKeyAggregator:
|
||||
'objectPermissionUniversalIdentifiers',
|
||||
},
|
||||
fieldPermissions: null,
|
||||
views: {
|
||||
metadataName: 'view',
|
||||
@@ -144,7 +149,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
flatEntityForeignKeyAggregator: 'roleTargetIds',
|
||||
universalFlatEntityForeignKeyAggregator: 'roleTargetUniversalIdentifiers',
|
||||
},
|
||||
objectPermissions: null,
|
||||
objectPermissions: {
|
||||
metadataName: 'objectPermission',
|
||||
flatEntityForeignKeyAggregator: 'objectPermissionIds',
|
||||
universalFlatEntityForeignKeyAggregator:
|
||||
'objectPermissionUniversalIdentifiers',
|
||||
},
|
||||
permissionFlags: {
|
||||
metadataName: 'permissionFlag',
|
||||
flatEntityForeignKeyAggregator: 'permissionFlagIds',
|
||||
@@ -167,6 +177,7 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
},
|
||||
roleTarget: {},
|
||||
permissionFlag: {},
|
||||
objectPermission: {},
|
||||
pageLayout: {
|
||||
tabs: {
|
||||
metadataName: 'pageLayoutTab',
|
||||
|
||||
+9
-4
@@ -3,14 +3,15 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.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 { 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';
|
||||
import { WorkspaceFlatPageLayoutTabMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-tab/services/workspace-flat-page-layout-tab-map-cache.service';
|
||||
import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service';
|
||||
import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service';
|
||||
import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service';
|
||||
import { WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service';
|
||||
import { WorkspaceFlatRowLevelPermissionPredicateMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service';
|
||||
import { WorkspaceFlatViewFieldGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service';
|
||||
@@ -18,10 +19,13 @@ import { WorkspaceFlatViewFieldMapCacheService } from 'src/engine/metadata-modul
|
||||
import { WorkspaceFlatViewFilterGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service';
|
||||
import { WorkspaceFlatViewFilterMapCacheService } from 'src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service';
|
||||
import { WorkspaceFlatViewGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service';
|
||||
import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service';
|
||||
import { WorkspaceFlatViewMapCacheService } from 'src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
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 { 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';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
@@ -34,11 +38,9 @@ import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service';
|
||||
import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -55,6 +57,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module
|
||||
IndexFieldMetadataEntity,
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
ObjectPermissionEntity,
|
||||
PageLayoutEntity,
|
||||
PageLayoutTabEntity,
|
||||
PageLayoutWidgetEntity,
|
||||
@@ -77,6 +80,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module
|
||||
WorkspaceFlatIndexMapCacheService,
|
||||
WorkspaceFlatFieldMetadataMapCacheService,
|
||||
WorkspaceFlatViewGroupMapCacheService,
|
||||
WorkspaceFlatObjectPermissionMapCacheService,
|
||||
WorkspaceFlatPermissionFlagMapCacheService,
|
||||
WorkspaceFlatViewSortMapCacheService,
|
||||
WorkspaceFlatPageLayoutMapCacheService,
|
||||
@@ -96,6 +100,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module
|
||||
WorkspaceFlatIndexMapCacheService,
|
||||
WorkspaceFlatFieldMetadataMapCacheService,
|
||||
WorkspaceFlatViewGroupMapCacheService,
|
||||
WorkspaceFlatObjectPermissionMapCacheService,
|
||||
WorkspaceFlatPermissionFlagMapCacheService,
|
||||
WorkspaceFlatViewSortMapCacheService,
|
||||
WorkspaceFlatPageLayoutMapCacheService,
|
||||
|
||||
+59
-32
@@ -12,14 +12,16 @@ import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-m
|
||||
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatObjectPermissionMaps } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type';
|
||||
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
|
||||
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
|
||||
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
|
||||
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
|
||||
import { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type';
|
||||
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
|
||||
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
|
||||
@@ -55,22 +57,23 @@ import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/wo
|
||||
import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-logic-function.type';
|
||||
import { type UniversalFlatNavigationMenuItem } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-navigation-menu-item.type';
|
||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
import { type UniversalFlatPageLayout } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout.type';
|
||||
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
|
||||
import { type UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type';
|
||||
import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type';
|
||||
import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.type';
|
||||
import { type UniversalFlatRole } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type';
|
||||
import { type UniversalFlatPageLayout } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout.type';
|
||||
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
|
||||
import { type UniversalFlatRoleTarget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-target.type';
|
||||
import { type UniversalFlatRowLevelPermissionPredicate } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate.type';
|
||||
import { type UniversalFlatRole } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type';
|
||||
import { type UniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate-group.type';
|
||||
import { type UniversalFlatRowLevelPermissionPredicate } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate.type';
|
||||
import { type UniversalFlatSkill } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-skill.type';
|
||||
import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
import { type UniversalFlatViewFilterGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter-group.type';
|
||||
import { type UniversalFlatViewFilter } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter.type';
|
||||
import { type UniversalFlatViewGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-group.type';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
import { type UniversalFlatWebhook } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-webhook.type';
|
||||
import {
|
||||
type FlatCreateAgentAction,
|
||||
@@ -105,10 +108,10 @@ import {
|
||||
type UniversalUpdateFrontComponentAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
|
||||
import {
|
||||
type UniversalCreateIndexAction,
|
||||
type FlatCreateIndexAction,
|
||||
type FlatDeleteIndexAction,
|
||||
type FlatUpdateIndexAction,
|
||||
type UniversalCreateIndexAction,
|
||||
type UniversalDeleteIndexAction,
|
||||
type UniversalUpdateIndexAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/types/workspace-migration-index-action';
|
||||
@@ -128,6 +131,14 @@ import {
|
||||
type UniversalDeleteNavigationMenuItemAction,
|
||||
type UniversalUpdateNavigationMenuItemAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
|
||||
import {
|
||||
type FlatCreateObjectPermissionAction,
|
||||
type FlatDeleteObjectPermissionAction,
|
||||
type FlatUpdateObjectPermissionAction,
|
||||
type UniversalCreateObjectPermissionAction,
|
||||
type UniversalDeleteObjectPermissionAction,
|
||||
type UniversalUpdateObjectPermissionAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type';
|
||||
import {
|
||||
type FlatCreateObjectAction,
|
||||
type FlatDeleteObjectAction,
|
||||
@@ -136,14 +147,6 @@ import {
|
||||
type UniversalDeleteObjectAction,
|
||||
type UniversalUpdateObjectAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
|
||||
import {
|
||||
type FlatCreatePermissionFlagAction,
|
||||
type FlatDeletePermissionFlagAction,
|
||||
type FlatUpdatePermissionFlagAction,
|
||||
type UniversalCreatePermissionFlagAction,
|
||||
type UniversalDeletePermissionFlagAction,
|
||||
type UniversalUpdatePermissionFlagAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
|
||||
import {
|
||||
type FlatCreatePageLayoutTabAction,
|
||||
type FlatDeletePageLayoutTabAction,
|
||||
@@ -168,6 +171,14 @@ import {
|
||||
type UniversalDeletePageLayoutAction,
|
||||
type UniversalUpdatePageLayoutAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type';
|
||||
import {
|
||||
type FlatCreatePermissionFlagAction,
|
||||
type FlatDeletePermissionFlagAction,
|
||||
type FlatUpdatePermissionFlagAction,
|
||||
type UniversalCreatePermissionFlagAction,
|
||||
type UniversalDeletePermissionFlagAction,
|
||||
type UniversalUpdatePermissionFlagAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
|
||||
import {
|
||||
type FlatCreateRoleTargetAction,
|
||||
type FlatDeleteRoleTargetAction,
|
||||
@@ -208,14 +219,6 @@ import {
|
||||
type UniversalDeleteSkillAction,
|
||||
type UniversalUpdateSkillAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/types/workspace-migration-skill-action.type';
|
||||
import {
|
||||
type FlatCreateViewFieldAction,
|
||||
type FlatDeleteViewFieldAction,
|
||||
type FlatUpdateViewFieldAction,
|
||||
type UniversalCreateViewFieldAction,
|
||||
type UniversalDeleteViewFieldAction,
|
||||
type UniversalUpdateViewFieldAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/types/workspace-migration-view-field-action.type';
|
||||
import {
|
||||
type FlatCreateViewFieldGroupAction,
|
||||
type FlatDeleteViewFieldGroupAction,
|
||||
@@ -224,6 +227,14 @@ import {
|
||||
type UniversalDeleteViewFieldGroupAction,
|
||||
type UniversalUpdateViewFieldGroupAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type';
|
||||
import {
|
||||
type FlatCreateViewFieldAction,
|
||||
type FlatDeleteViewFieldAction,
|
||||
type FlatUpdateViewFieldAction,
|
||||
type UniversalCreateViewFieldAction,
|
||||
type UniversalDeleteViewFieldAction,
|
||||
type UniversalUpdateViewFieldAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/types/workspace-migration-view-field-action.type';
|
||||
import {
|
||||
type FlatCreateViewFilterGroupAction,
|
||||
type FlatDeleteViewFilterGroupAction,
|
||||
@@ -248,6 +259,14 @@ import {
|
||||
type UniversalDeleteViewGroupAction,
|
||||
type UniversalUpdateViewGroupAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/types/workspace-migration-view-group-action.type';
|
||||
import {
|
||||
type FlatCreateViewSortAction,
|
||||
type FlatDeleteViewSortAction,
|
||||
type FlatUpdateViewSortAction,
|
||||
type UniversalCreateViewSortAction,
|
||||
type UniversalDeleteViewSortAction,
|
||||
type UniversalUpdateViewSortAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type';
|
||||
import {
|
||||
type FlatCreateViewAction,
|
||||
type FlatDeleteViewAction,
|
||||
@@ -264,14 +283,6 @@ import {
|
||||
type UniversalDeleteWebhookAction,
|
||||
type UniversalUpdateWebhookAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type';
|
||||
import {
|
||||
type FlatCreateViewSortAction,
|
||||
type FlatDeleteViewSortAction,
|
||||
type FlatUpdateViewSortAction,
|
||||
type UniversalCreateViewSortAction,
|
||||
type UniversalDeleteViewSortAction,
|
||||
type UniversalUpdateViewSortAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type';
|
||||
|
||||
export type AllFlatEntityTypesByMetadataName = {
|
||||
fieldMetadata: {
|
||||
@@ -597,6 +608,22 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
universalFlatEntity: UniversalFlatPermissionFlag;
|
||||
entity: MetadataEntity<'permissionFlag'>;
|
||||
};
|
||||
objectPermission: {
|
||||
flatEntityMaps: FlatObjectPermissionMaps;
|
||||
universalActions: {
|
||||
create: UniversalCreateObjectPermissionAction;
|
||||
update: UniversalUpdateObjectPermissionAction;
|
||||
delete: UniversalDeleteObjectPermissionAction;
|
||||
};
|
||||
flatActions: {
|
||||
create: FlatCreateObjectPermissionAction;
|
||||
update: FlatUpdateObjectPermissionAction;
|
||||
delete: FlatDeleteObjectPermissionAction;
|
||||
};
|
||||
flatEntity: FlatObjectPermission;
|
||||
universalFlatEntity: UniversalFlatObjectPermission;
|
||||
entity: MetadataEntity<'objectPermission'>;
|
||||
};
|
||||
pageLayout: {
|
||||
flatEntityMaps: FlatPageLayoutMaps;
|
||||
universalActions: {
|
||||
|
||||
+9
@@ -42,10 +42,18 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
[
|
||||
"fieldMetadata",
|
||||
"index",
|
||||
"objectPermission",
|
||||
"view",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for objectPermission 1`] = `
|
||||
[
|
||||
"role",
|
||||
"objectMetadata",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for pageLayout 1`] = `
|
||||
[
|
||||
"objectMetadata",
|
||||
@@ -76,6 +84,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for role 1`] = `
|
||||
[
|
||||
"roleTarget",
|
||||
"objectPermission",
|
||||
"permissionFlag",
|
||||
"rowLevelPermissionPredicate",
|
||||
"rowLevelPermissionPredicateGroup",
|
||||
|
||||
+1
@@ -7,6 +7,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
|
||||
"viewField",
|
||||
"viewFilter",
|
||||
"commandMenuItem",
|
||||
"objectPermission",
|
||||
"pageLayoutWidget",
|
||||
"viewSort",
|
||||
"index",
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ export const getFlatObjectMetadataMock = (
|
||||
return {
|
||||
viewIds: [],
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldIds: [],
|
||||
description: 'default flat object metadata description',
|
||||
icon: 'icon',
|
||||
@@ -48,6 +49,7 @@ export const getFlatObjectMetadataMock = (
|
||||
duplicateCriteria: null,
|
||||
applicationUniversalIdentifier: applicationId,
|
||||
fieldUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
|
||||
+53
-28
@@ -13,6 +13,7 @@ import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-meta
|
||||
import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util';
|
||||
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 { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
|
||||
@@ -35,6 +36,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
@InjectRepository(ObjectPermissionEntity)
|
||||
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -42,35 +45,51 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<FlatEntityMaps<FlatObjectMetadata>> {
|
||||
const [objectMetadatas, applications, fields, indexMetadatas, views] =
|
||||
await Promise.all([
|
||||
this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.fieldMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier', 'objectMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.indexMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier', 'objectMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier', 'objectMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
const [
|
||||
objectMetadatas,
|
||||
applications,
|
||||
fields,
|
||||
indexMetadatas,
|
||||
views,
|
||||
objectPermissions,
|
||||
] = await Promise.all([
|
||||
this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.fieldMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier', 'objectMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.indexMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier', 'objectMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.viewRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier', 'objectMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.objectPermissionRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier', 'objectMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const [fieldsByObjectId, indexesByObjectId, viewsByObjectId] = (
|
||||
const [
|
||||
fieldsByObjectId,
|
||||
indexesByObjectId,
|
||||
viewsByObjectId,
|
||||
objectPermissionsByObjectId,
|
||||
] = (
|
||||
[
|
||||
{
|
||||
entities: fields,
|
||||
@@ -84,6 +103,10 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
entities: views,
|
||||
foreignKey: 'objectMetadataId',
|
||||
},
|
||||
{
|
||||
entities: objectPermissions,
|
||||
foreignKey: 'objectMetadataId',
|
||||
},
|
||||
] as const
|
||||
).map(regroupEntitiesByRelatedEntityId);
|
||||
|
||||
@@ -101,6 +124,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
fields: fieldsByObjectId.get(objectMetadataEntity.id) || [],
|
||||
indexMetadatas: indexesByObjectId.get(objectMetadataEntity.id) || [],
|
||||
views: viewsByObjectId.get(objectMetadataEntity.id) || [],
|
||||
objectPermissions:
|
||||
objectPermissionsByObjectId.get(objectMetadataEntity.id) || [],
|
||||
},
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
fieldMetadataIdToUniversalIdentifierMap,
|
||||
|
||||
+1
@@ -94,6 +94,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
targetTableName: 'DEPRECATED',
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
fieldUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
|
||||
+7
@@ -78,6 +78,9 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
|
||||
viewIds: objectMetadataEntity.views.map(({ id }) => id),
|
||||
indexMetadataIds: objectMetadataEntity.indexMetadatas.map(({ id }) => id),
|
||||
fieldIds: objectMetadataEntity.fields.map(({ id }) => id),
|
||||
objectPermissionIds: objectMetadataEntity.objectPermissions.map(
|
||||
({ id }) => id,
|
||||
),
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier,
|
||||
@@ -90,5 +93,9 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
|
||||
viewUniversalIdentifiers: objectMetadataEntity.views.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
objectPermissionUniversalIdentifiers:
|
||||
objectMetadataEntity.objectPermissions.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
|
||||
|
||||
export const FLAT_OBJECT_PERMISSION_EDITABLE_PROPERTIES = [
|
||||
'roleId',
|
||||
'objectMetadataId',
|
||||
'canReadObjectRecords',
|
||||
'canUpdateObjectRecords',
|
||||
'canSoftDeleteObjectRecords',
|
||||
'canDestroyObjectRecords',
|
||||
] as const satisfies MetadataEntityPropertyName<'objectPermission'>[];
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
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 { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type FlatObjectPermissionMaps } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type';
|
||||
import { fromObjectPermissionEntityToFlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/utils/from-object-permission-entity-to-flat-object-permission.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-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('flatObjectPermissionMaps')
|
||||
export class WorkspaceFlatObjectPermissionMapCacheService extends WorkspaceCacheProvider<FlatObjectPermissionMaps> {
|
||||
constructor(
|
||||
@InjectRepository(ObjectPermissionEntity)
|
||||
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectRepository(RoleEntity)
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<FlatObjectPermissionMaps> {
|
||||
const [objectPermissions, applications, roles, objectMetadatas] =
|
||||
await Promise.all([
|
||||
this.objectPermissionRepository.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,
|
||||
}),
|
||||
]);
|
||||
|
||||
const applicationIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(applications);
|
||||
const roleIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(roles);
|
||||
const objectMetadataIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(objectMetadatas);
|
||||
|
||||
const flatObjectPermissionMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
for (const objectPermissionEntity of objectPermissions) {
|
||||
const flatObjectPermission =
|
||||
fromObjectPermissionEntityToFlatObjectPermission({
|
||||
entity: objectPermissionEntity,
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
roleIdToUniversalIdentifierMap,
|
||||
objectMetadataIdToUniversalIdentifierMap,
|
||||
});
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: flatObjectPermission,
|
||||
flatEntityMapsToMutate: flatObjectPermissionMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatObjectPermissionMaps;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
|
||||
|
||||
export type FlatObjectPermissionMaps = FlatEntityMaps<FlatObjectPermission>;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
|
||||
export type FlatObjectPermission = FlatEntityFrom<ObjectPermissionEntity>;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
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 CreateObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/create-object-permission.input';
|
||||
import { type UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type';
|
||||
|
||||
export const fromCreateObjectPermissionInputToUniversalFlatObjectPermission = ({
|
||||
createObjectPermissionInput,
|
||||
flatApplication,
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}: {
|
||||
createObjectPermissionInput: CreateObjectPermissionInput;
|
||||
flatApplication: FlatApplication;
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatRoleMaps' | 'flatObjectMetadataMaps'
|
||||
>): UniversalFlatObjectPermission & { id: string } => {
|
||||
const {
|
||||
roleId,
|
||||
objectMetadataId,
|
||||
canReadObjectRecords,
|
||||
canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords,
|
||||
universalIdentifier,
|
||||
} = createObjectPermissionInput;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const { roleUniversalIdentifier, objectMetadataUniversalIdentifier } =
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'objectPermission',
|
||||
foreignKeyValues: { roleId, objectMetadataId },
|
||||
flatEntityMaps: {
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
});
|
||||
return {
|
||||
id: v4(),
|
||||
universalIdentifier: universalIdentifier ?? v4(),
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
canReadObjectRecords: canReadObjectRecords ?? undefined,
|
||||
canUpdateObjectRecords: canUpdateObjectRecords ?? undefined,
|
||||
canSoftDeleteObjectRecords: canSoftDeleteObjectRecords ?? undefined,
|
||||
canDestroyObjectRecords: canDestroyObjectRecords ?? undefined,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
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 FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
|
||||
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
|
||||
|
||||
export const fromObjectPermissionEntityToFlatObjectPermission = ({
|
||||
entity: objectPermissionEntity,
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
roleIdToUniversalIdentifierMap,
|
||||
objectMetadataIdToUniversalIdentifierMap,
|
||||
}: FromEntityToFlatEntityArgs<'objectPermission'>): FlatObjectPermission => {
|
||||
const objectPermissionEntityWithoutRelations = removePropertiesFromRecord(
|
||||
objectPermissionEntity,
|
||||
getMetadataEntityRelationProperties('objectPermission'),
|
||||
);
|
||||
|
||||
const applicationUniversalIdentifier =
|
||||
applicationIdToUniversalIdentifierMap.get(
|
||||
objectPermissionEntity.applicationId,
|
||||
);
|
||||
|
||||
if (!isDefined(applicationUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Application with id ${objectPermissionEntity.applicationId} not found for objectPermission ${objectPermissionEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
|
||||
objectPermissionEntity.roleId,
|
||||
);
|
||||
|
||||
if (!isDefined(roleUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Role with id ${objectPermissionEntity.roleId} not found for objectPermission ${objectPermissionEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataUniversalIdentifier =
|
||||
objectMetadataIdToUniversalIdentifierMap.get(
|
||||
objectPermissionEntity.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`ObjectMetadata with id ${objectPermissionEntity.objectMetadataId} not found for objectPermission ${objectPermissionEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...objectPermissionEntityWithoutRelations,
|
||||
createdAt: objectPermissionEntity.createdAt.toISOString(),
|
||||
updatedAt: objectPermissionEntity.updatedAt.toISOString(),
|
||||
universalIdentifier:
|
||||
objectPermissionEntityWithoutRelations.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
};
|
||||
};
|
||||
+1
@@ -55,6 +55,7 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
|
||||
roleTargetUniversalIdentifiers: [],
|
||||
objectPermissionIds: [],
|
||||
permissionFlagIds: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
fieldPermissionIds: [],
|
||||
rowLevelPermissionPredicateIds: [],
|
||||
|
||||
+3
@@ -68,6 +68,9 @@ export const fromRoleEntityToFlatRole = ({
|
||||
roleTargetUniversalIdentifiers: roleEntity.roleTargets.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
objectPermissionUniversalIdentifiers: roleEntity.objectPermissions.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
permissionFlagUniversalIdentifiers: roleEntity.permissionFlags.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
|
||||
+115
-134
@@ -1,58 +1,67 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { type UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
|
||||
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 { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const createMockFlatRoleMaps = (roleId: string, isEditable: boolean) => ({
|
||||
byUniversalIdentifier: {
|
||||
[roleId]: {
|
||||
id: roleId,
|
||||
universalIdentifier: roleId,
|
||||
isEditable,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
},
|
||||
},
|
||||
universalIdentifierById: { [roleId]: roleId },
|
||||
byId: { [roleId]: { id: roleId, universalIdentifier: roleId, isEditable } },
|
||||
});
|
||||
|
||||
const createMockFlatObjectMetadataMaps = (
|
||||
objectMetadataId: string,
|
||||
isSystem: boolean,
|
||||
) => ({
|
||||
byUniversalIdentifier: {
|
||||
[objectMetadataId]: {
|
||||
id: objectMetadataId,
|
||||
universalIdentifier: objectMetadataId,
|
||||
isSystem,
|
||||
},
|
||||
},
|
||||
universalIdentifierById: { [objectMetadataId]: objectMetadataId },
|
||||
byId: { [objectMetadataId]: { id: objectMetadataId, isSystem } },
|
||||
});
|
||||
|
||||
describe('ObjectPermissionService', () => {
|
||||
let service: ObjectPermissionService;
|
||||
let objectPermissionRepository: jest.Mocked<
|
||||
Repository<ObjectPermissionEntity>
|
||||
>;
|
||||
let roleRepository: jest.Mocked<Repository<RoleEntity>>;
|
||||
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
|
||||
let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
|
||||
let workspaceMigrationValidateBuildAndRunService: jest.Mocked<WorkspaceMigrationValidateBuildAndRunService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ObjectPermissionService,
|
||||
{
|
||||
provide: getRepositoryToken(ObjectPermissionEntity),
|
||||
provide: ApplicationService,
|
||||
useValue: {
|
||||
upsert: jest.fn(),
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(RoleEntity),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ObjectMetadataEntity),
|
||||
useValue: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceCacheService,
|
||||
useValue: {
|
||||
invalidate: jest.fn(),
|
||||
invalidateAndRecompute: jest.fn(),
|
||||
findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
workspaceCustomFlatApplication: {
|
||||
id: 'app-id',
|
||||
universalIdentifier: 'app-universal-id',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -61,18 +70,22 @@ describe('ObjectPermissionService', () => {
|
||||
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMigrationValidateBuildAndRunService,
|
||||
useValue: {
|
||||
validateBuildAndRunWorkspaceMigration: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ObjectPermissionService>(ObjectPermissionService);
|
||||
objectPermissionRepository = module.get(
|
||||
getRepositoryToken(ObjectPermissionEntity),
|
||||
);
|
||||
roleRepository = module.get(getRepositoryToken(RoleEntity));
|
||||
workspaceCacheService = module.get(WorkspaceCacheService);
|
||||
workspaceManyOrAllFlatEntityMapsCacheService = module.get(
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
);
|
||||
workspaceMigrationValidateBuildAndRunService = module.get(
|
||||
WorkspaceMigrationValidateBuildAndRunService,
|
||||
);
|
||||
});
|
||||
|
||||
describe('upsertObjectPermissions', () => {
|
||||
@@ -81,18 +94,7 @@ describe('ObjectPermissionService', () => {
|
||||
const systemObjectMetadataId = 'system-object-id';
|
||||
const customObjectMetadataId = 'custom-object-id';
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock role validation
|
||||
roleRepository.findOne.mockResolvedValue({
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
isEditable: true,
|
||||
objectPermissions: [],
|
||||
} as unknown as RoleEntity);
|
||||
});
|
||||
|
||||
it('should throw PermissionsException when trying to add object permission on system object', async () => {
|
||||
// Arrange
|
||||
const input: UpsertObjectPermissionsInput = {
|
||||
roleId,
|
||||
objectPermissions: [
|
||||
@@ -106,31 +108,21 @@ describe('ObjectPermissionService', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// Mock flat object metadata maps with a system object
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[systemObjectMetadataId]: {
|
||||
id: systemObjectMetadataId,
|
||||
isSystem: true,
|
||||
workspaceId,
|
||||
fieldIds: [],
|
||||
indexMetadataIds: [],
|
||||
viewIds: [],
|
||||
universalIdentifier: systemObjectMetadataId,
|
||||
applicationId: null,
|
||||
} as any,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[systemObjectMetadataId]: systemObjectMetadataId,
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
flatObjectPermissionMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
byId: {},
|
||||
},
|
||||
flatRoleMaps: createMockFlatRoleMaps(roleId, true),
|
||||
flatObjectMetadataMaps: createMockFlatObjectMetadataMaps(
|
||||
systemObjectMetadataId,
|
||||
true,
|
||||
),
|
||||
} as any,
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.upsertObjectPermissions({
|
||||
workspaceId,
|
||||
@@ -143,15 +135,12 @@ describe('ObjectPermissionService', () => {
|
||||
),
|
||||
);
|
||||
|
||||
// Verify that upsert was never called
|
||||
expect(objectPermissionRepository.upsert).not.toHaveBeenCalled();
|
||||
expect(
|
||||
workspaceCacheService.invalidateAndRecompute,
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should successfully create object permission for custom (non-system) object', async () => {
|
||||
// Arrange
|
||||
const input: UpsertObjectPermissionsInput = {
|
||||
roleId,
|
||||
objectPermissions: [
|
||||
@@ -165,82 +154,70 @@ describe('ObjectPermissionService', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// Mock flat object metadata maps with a custom object
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[customObjectMetadataId]: {
|
||||
id: customObjectMetadataId,
|
||||
isSystem: false,
|
||||
workspaceId,
|
||||
fieldIds: [],
|
||||
indexMetadataIds: [],
|
||||
viewIds: [],
|
||||
universalIdentifier: customObjectMetadataId,
|
||||
applicationId: null,
|
||||
} as any,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[customObjectMetadataId]: customObjectMetadataId,
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
},
|
||||
} as any,
|
||||
);
|
||||
|
||||
// Mock successful upsert
|
||||
const mockObjectPermission = {
|
||||
const permissionUniversalId = 'permission-universal-id';
|
||||
const freshFlatObjectPermission = {
|
||||
id: 'permission-id',
|
||||
universalIdentifier: permissionUniversalId,
|
||||
roleId,
|
||||
roleUniversalIdentifier: roleId,
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
workspaceId,
|
||||
objectMetadataUniversalIdentifier: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
} as ObjectPermissionEntity;
|
||||
};
|
||||
|
||||
objectPermissionRepository.upsert.mockResolvedValue({
|
||||
generatedMaps: [{ id: 'permission-id' }],
|
||||
identifiers: [],
|
||||
raw: [],
|
||||
});
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps
|
||||
.mockResolvedValueOnce({
|
||||
flatObjectPermissionMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
byId: {},
|
||||
},
|
||||
flatRoleMaps: createMockFlatRoleMaps(roleId, true),
|
||||
flatObjectMetadataMaps: createMockFlatObjectMetadataMaps(
|
||||
customObjectMetadataId,
|
||||
false,
|
||||
),
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
flatObjectPermissionMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[permissionUniversalId]: freshFlatObjectPermission,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[freshFlatObjectPermission.id]: permissionUniversalId,
|
||||
},
|
||||
byId: {
|
||||
[freshFlatObjectPermission.id]: freshFlatObjectPermission,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
objectPermissionRepository.find.mockResolvedValue([mockObjectPermission]);
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
|
||||
{ status: 'success' } as any,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await service.upsertObjectPermissions({
|
||||
workspaceId,
|
||||
input,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual([mockObjectPermission]);
|
||||
expect(objectPermissionRepository.upsert).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
roleId,
|
||||
workspaceId,
|
||||
},
|
||||
],
|
||||
{
|
||||
conflictPaths: ['objectMetadataId', 'roleId'],
|
||||
},
|
||||
);
|
||||
expect(workspaceCacheService.invalidateAndRecompute).toHaveBeenCalledWith(
|
||||
workspaceId,
|
||||
['rolesPermissions'],
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
});
|
||||
expect(
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw PermissionsException when object metadata is not found', async () => {
|
||||
// Arrange
|
||||
const input: UpsertObjectPermissionsInput = {
|
||||
roleId,
|
||||
objectPermissions: [
|
||||
@@ -254,18 +231,22 @@ describe('ObjectPermissionService', () => {
|
||||
],
|
||||
};
|
||||
|
||||
// Mock empty flat object metadata maps
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatObjectPermissionMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
byId: {},
|
||||
},
|
||||
flatRoleMaps: createMockFlatRoleMaps(roleId, true),
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
byId: {},
|
||||
},
|
||||
} as any,
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.upsertObjectPermissions({
|
||||
workspaceId,
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export type CreateObjectPermissionInput = {
|
||||
roleId: string;
|
||||
objectMetadataId: string;
|
||||
canReadObjectRecords?: boolean;
|
||||
canUpdateObjectRecords?: boolean;
|
||||
canSoftDeleteObjectRecords?: boolean;
|
||||
canDestroyObjectRecords?: boolean;
|
||||
universalIdentifier?: string;
|
||||
};
|
||||
+2
-2
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
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('objectPermission')
|
||||
@Unique('IDX_OBJECT_PERMISSION_OBJECT_METADATA_ID_ROLE_ID_UNIQUE', [
|
||||
@@ -21,7 +21,7 @@ import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/works
|
||||
'roleId',
|
||||
])
|
||||
@Index('IDX_OBJECT_PERMISSION_WORKSPACE_ID_ROLE_ID', ['workspaceId', 'roleId'])
|
||||
export class ObjectPermissionEntity extends WorkspaceRelatedEntity {
|
||||
export class ObjectPermissionEntity extends SyncableEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -10,6 +11,7 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi
|
||||
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -20,8 +22,10 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
FieldPermissionEntity,
|
||||
FieldMetadataEntity,
|
||||
]),
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
WorkspaceCacheModule,
|
||||
ApplicationModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [ObjectPermissionService, FieldPermissionService],
|
||||
exports: [ObjectPermissionService, FieldPermissionService],
|
||||
|
||||
+248
-205
@@ -1,35 +1,33 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
|
||||
import { fromCreateObjectPermissionInputToUniversalFlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/utils/from-create-object-permission-input-to-universal-flat-object-permission.util';
|
||||
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
|
||||
import {
|
||||
type ObjectPermissionInput,
|
||||
type UpsertObjectPermissionsInput,
|
||||
} from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-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 UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectPermissionService {
|
||||
constructor(
|
||||
@InjectRepository(ObjectPermissionEntity)
|
||||
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
|
||||
@InjectRepository(RoleEntity)
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
public async upsertObjectPermissions({
|
||||
@@ -38,144 +36,269 @@ export class ObjectPermissionService {
|
||||
}: {
|
||||
workspaceId: string;
|
||||
input: UpsertObjectPermissionsInput;
|
||||
}): Promise<ObjectPermissionEntity[]> {
|
||||
try {
|
||||
const role = await this.getRoleOrThrow({
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
}): Promise<FlatObjectPermission[]> {
|
||||
const { flatObjectPermissionMaps, flatRoleMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatObjectPermissionMaps',
|
||||
'flatRoleMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const roleUniversalIdentifier =
|
||||
flatRoleMaps.universalIdentifierById[input.roleId];
|
||||
const flatRole = isDefined(roleUniversalIdentifier)
|
||||
? flatRoleMaps.byUniversalIdentifier[roleUniversalIdentifier]
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(flatRole)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.ROLE_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`The role you are trying to modify could not be found.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const currentObjectPermissionsForRole = Object.values(
|
||||
flatObjectPermissionMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(op): op is FlatObjectPermission =>
|
||||
isDefined(op) && op.roleUniversalIdentifier === roleUniversalIdentifier,
|
||||
);
|
||||
|
||||
this.validateObjectPermissionsReadAndWriteConsistencyOrThrow({
|
||||
objectPermissions: input.objectPermissions,
|
||||
flatRole,
|
||||
currentObjectPermissionsForRole,
|
||||
});
|
||||
|
||||
const flatApplication =
|
||||
await this.getFlatApplicationForWorkspace(workspaceId);
|
||||
|
||||
const desiredByObjectMetadataId = new Map(
|
||||
input.objectPermissions.map((op) => [op.objectMetadataId, op]),
|
||||
);
|
||||
|
||||
for (const desired of input.objectPermissions) {
|
||||
const objectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: desired.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
await this.validateRoleIsEditableOrThrow({
|
||||
role,
|
||||
});
|
||||
|
||||
await this.validateObjectPermissionsReadAndWriteConsistencyOrThrow({
|
||||
objectPermissions: input.objectPermissions,
|
||||
roleWithObjectPermissions: role,
|
||||
});
|
||||
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
if (!isDefined(objectMetadata)) {
|
||||
throw new PermissionsException(
|
||||
'Object metadata id not found',
|
||||
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`,
|
||||
},
|
||||
);
|
||||
|
||||
input.objectPermissions.forEach((objectPermission) => {
|
||||
const objectMetadataForObjectPermission =
|
||||
findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: objectPermission.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(objectMetadataForObjectPermission)) {
|
||||
throw new PermissionsException(
|
||||
'Object metadata id not found',
|
||||
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (objectMetadataForObjectPermission.isSystem === true) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
{
|
||||
userFriendlyMessage: msg`You cannot set permissions on system objects as they are managed by the platform.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const objectPermissions = input.objectPermissions.map(
|
||||
(objectPermission) => ({
|
||||
...objectPermission,
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await this.objectPermissionRepository.upsert(
|
||||
objectPermissions,
|
||||
{
|
||||
conflictPaths: ['objectMetadataId', 'roleId'],
|
||||
},
|
||||
);
|
||||
|
||||
const objectPermissionId = result.generatedMaps?.[0]?.id;
|
||||
|
||||
if (!isDefined(objectPermissionId)) {
|
||||
throw new Error('Failed to upsert object permission');
|
||||
}
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'rolesPermissions',
|
||||
]);
|
||||
|
||||
return this.objectPermissionRepository.find({
|
||||
where: {
|
||||
roleId: input.roleId,
|
||||
objectMetadataId: In(
|
||||
input.objectPermissions.map(
|
||||
(objectPermission) => objectPermission.objectMetadataId,
|
||||
),
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await this.handleForeignKeyError({
|
||||
error,
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
objectMetadataIds: input.objectPermissions.map(
|
||||
(objectPermission) => objectPermission.objectMetadataId,
|
||||
),
|
||||
});
|
||||
|
||||
throw error;
|
||||
if (objectMetadata.isSystem === true) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
|
||||
{
|
||||
userFriendlyMessage: msg`You cannot set permissions on system objects as they are managed by the platform.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const flatEntityToCreate: (UniversalFlatObjectPermission & {
|
||||
id: string;
|
||||
})[] = [];
|
||||
const flatEntityToUpdate: UniversalFlatObjectPermission[] = [];
|
||||
const flatEntityToDelete: UniversalFlatObjectPermission[] = [];
|
||||
|
||||
const currentByObjectMetadataId = new Map(
|
||||
currentObjectPermissionsForRole.map((op) => [op.objectMetadataId, op]),
|
||||
);
|
||||
|
||||
for (const desired of input.objectPermissions) {
|
||||
const current = currentByObjectMetadataId.get(desired.objectMetadataId);
|
||||
|
||||
if (!isDefined(current)) {
|
||||
flatEntityToCreate.push(
|
||||
fromCreateObjectPermissionInputToUniversalFlatObjectPermission({
|
||||
createObjectPermissionInput: {
|
||||
roleId: input.roleId,
|
||||
objectMetadataId: desired.objectMetadataId,
|
||||
canReadObjectRecords: desired.canReadObjectRecords,
|
||||
canUpdateObjectRecords: desired.canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords: desired.canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords: desired.canDestroyObjectRecords,
|
||||
},
|
||||
flatApplication,
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const effectiveCanRead =
|
||||
desired.canReadObjectRecords ?? current.canReadObjectRecords;
|
||||
const effectiveCanUpdate =
|
||||
desired.canUpdateObjectRecords ?? current.canUpdateObjectRecords;
|
||||
const effectiveCanSoftDelete =
|
||||
desired.canSoftDeleteObjectRecords ??
|
||||
current.canSoftDeleteObjectRecords;
|
||||
const effectiveCanDestroy =
|
||||
desired.canDestroyObjectRecords ?? current.canDestroyObjectRecords;
|
||||
|
||||
const canChanged =
|
||||
effectiveCanRead !== current.canReadObjectRecords ||
|
||||
effectiveCanUpdate !== current.canUpdateObjectRecords ||
|
||||
effectiveCanSoftDelete !== current.canSoftDeleteObjectRecords ||
|
||||
effectiveCanDestroy !== current.canDestroyObjectRecords;
|
||||
|
||||
if (canChanged) {
|
||||
const now = new Date().toISOString();
|
||||
flatEntityToUpdate.push({
|
||||
universalIdentifier: current.universalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
current.applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier: current.roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
current.objectMetadataUniversalIdentifier,
|
||||
canReadObjectRecords: effectiveCanRead,
|
||||
canUpdateObjectRecords: effectiveCanUpdate,
|
||||
canSoftDeleteObjectRecords: effectiveCanSoftDelete,
|
||||
canDestroyObjectRecords: effectiveCanDestroy,
|
||||
createdAt: current.createdAt,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const current of currentObjectPermissionsForRole) {
|
||||
if (!desiredByObjectMetadataId.has(current.objectMetadataId)) {
|
||||
flatEntityToDelete.push({
|
||||
universalIdentifier: current.universalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
current.applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier: current.roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
current.objectMetadataUniversalIdentifier,
|
||||
canReadObjectRecords: current.canReadObjectRecords,
|
||||
canUpdateObjectRecords: current.canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords: current.canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords: current.canDestroyObjectRecords,
|
||||
createdAt: current.createdAt,
|
||||
updatedAt: current.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
flatEntityToCreate.length === 0 &&
|
||||
flatEntityToUpdate.length === 0 &&
|
||||
flatEntityToDelete.length === 0
|
||||
) {
|
||||
const unchanged = currentObjectPermissionsForRole.filter((op) =>
|
||||
desiredByObjectMetadataId.has(op.objectMetadataId),
|
||||
);
|
||||
return unchanged;
|
||||
}
|
||||
|
||||
const buildAndRunResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
objectPermission: {
|
||||
flatEntityToCreate,
|
||||
flatEntityToUpdate,
|
||||
flatEntityToDelete,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (buildAndRunResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
buildAndRunResult,
|
||||
'Validation errors occurred while upserting object permissions',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatObjectPermissionMaps: freshFlatObjectPermissionMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectPermissionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const resultObjectPermissions = Object.values(
|
||||
freshFlatObjectPermissionMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(op): op is FlatObjectPermission =>
|
||||
isDefined(op) && op.roleUniversalIdentifier === roleUniversalIdentifier,
|
||||
);
|
||||
|
||||
const desiredObjectMetadataIds = new Set(
|
||||
input.objectPermissions.map((op) => op.objectMetadataId),
|
||||
);
|
||||
const filtered = resultObjectPermissions.filter((op) =>
|
||||
desiredObjectMetadataIds.has(op.objectMetadataId),
|
||||
);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private async validateObjectPermissionsReadAndWriteConsistencyOrThrow({
|
||||
private validateObjectPermissionsReadAndWriteConsistencyOrThrow({
|
||||
objectPermissions: newObjectPermissions,
|
||||
roleWithObjectPermissions,
|
||||
flatRole,
|
||||
currentObjectPermissionsForRole,
|
||||
}: {
|
||||
objectPermissions: ObjectPermissionInput[];
|
||||
roleWithObjectPermissions: RoleEntity;
|
||||
}) {
|
||||
const existingObjectPermissions =
|
||||
roleWithObjectPermissions.objectPermissions;
|
||||
|
||||
flatRole: Pick<
|
||||
FlatRole,
|
||||
| 'canReadAllObjectRecords'
|
||||
| 'canUpdateAllObjectRecords'
|
||||
| 'canSoftDeleteAllObjectRecords'
|
||||
| 'canDestroyAllObjectRecords'
|
||||
>;
|
||||
currentObjectPermissionsForRole: FlatObjectPermission[];
|
||||
}): void {
|
||||
for (const newObjectPermission of newObjectPermissions) {
|
||||
const existingObjectRecordPermission = existingObjectPermissions.find(
|
||||
(objectPermission) =>
|
||||
objectPermission.objectMetadataId ===
|
||||
newObjectPermission.objectMetadataId,
|
||||
);
|
||||
const existingObjectRecordPermission =
|
||||
currentObjectPermissionsForRole.find(
|
||||
(objectPermission) =>
|
||||
objectPermission.objectMetadataId ===
|
||||
newObjectPermission.objectMetadataId,
|
||||
);
|
||||
|
||||
const hasReadPermissionAfterUpdate =
|
||||
newObjectPermission.canReadObjectRecords ??
|
||||
existingObjectRecordPermission?.canReadObjectRecords ??
|
||||
roleWithObjectPermissions.canReadAllObjectRecords;
|
||||
flatRole.canReadAllObjectRecords;
|
||||
|
||||
if (hasReadPermissionAfterUpdate === false) {
|
||||
const hasUpdatePermissionAfterUpdate =
|
||||
newObjectPermission.canUpdateObjectRecords ??
|
||||
existingObjectRecordPermission?.canUpdateObjectRecords ??
|
||||
roleWithObjectPermissions.canUpdateAllObjectRecords;
|
||||
flatRole.canUpdateAllObjectRecords;
|
||||
|
||||
const hasSoftDeletePermissionAfterUpdate =
|
||||
newObjectPermission.canSoftDeleteObjectRecords ??
|
||||
existingObjectRecordPermission?.canSoftDeleteObjectRecords ??
|
||||
roleWithObjectPermissions.canSoftDeleteAllObjectRecords;
|
||||
flatRole.canSoftDeleteAllObjectRecords;
|
||||
|
||||
const hasDestroyPermissionAfterUpdate =
|
||||
newObjectPermission.canDestroyObjectRecords ??
|
||||
existingObjectRecordPermission?.canDestroyObjectRecords ??
|
||||
roleWithObjectPermissions.canDestroyAllObjectRecords;
|
||||
flatRole.canDestroyAllObjectRecords;
|
||||
|
||||
if (
|
||||
hasUpdatePermissionAfterUpdate ||
|
||||
@@ -194,91 +317,11 @@ export class ObjectPermissionService {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleForeignKeyError({
|
||||
error,
|
||||
roleId,
|
||||
workspaceId,
|
||||
objectMetadataIds,
|
||||
}: {
|
||||
error: Error;
|
||||
roleId: string;
|
||||
workspaceId: string;
|
||||
objectMetadataIds: string[];
|
||||
}) {
|
||||
if (error.message.includes('violates foreign key constraint')) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadata = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
id: In(objectMetadataIds),
|
||||
},
|
||||
});
|
||||
|
||||
if (objectMetadata.length !== objectMetadataIds.length) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND,
|
||||
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`One or more objects you are trying to set permissions for could not be found. They may have been deleted.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getRoleOrThrow({
|
||||
roleId,
|
||||
workspaceId,
|
||||
}: {
|
||||
roleId: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['objectPermissions'],
|
||||
});
|
||||
|
||||
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.`,
|
||||
},
|
||||
private async getFlatApplicationForWorkspace(workspaceId: string) {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
}
|
||||
|
||||
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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
return workspaceCustomFlatApplication;
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
|
||||
import { type ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
|
||||
export const fromFlatObjectPermissionToObjectPermissionDto = (
|
||||
flatObjectPermission: FlatObjectPermission,
|
||||
): ObjectPermissionDTO => ({
|
||||
objectMetadataId: flatObjectPermission.objectMetadataId,
|
||||
canReadObjectRecords: flatObjectPermission.canReadObjectRecords,
|
||||
canUpdateObjectRecords: flatObjectPermission.canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords: flatObjectPermission.canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords: flatObjectPermission.canDestroyObjectRecords,
|
||||
});
|
||||
@@ -39,6 +39,7 @@ 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 { 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';
|
||||
@@ -211,10 +212,14 @@ export class RoleResolver {
|
||||
@Args('upsertObjectPermissionsInput')
|
||||
upsertObjectPermissionsInput: UpsertObjectPermissionsInput,
|
||||
): Promise<ObjectPermissionDTO[]> {
|
||||
return this.objectPermissionService.upsertObjectPermissions({
|
||||
workspaceId: workspace.id,
|
||||
input: upsertObjectPermissionsInput,
|
||||
});
|
||||
const flatObjectPermissions =
|
||||
await this.objectPermissionService.upsertObjectPermissions({
|
||||
workspaceId: workspace.id,
|
||||
input: upsertObjectPermissionsInput,
|
||||
});
|
||||
return flatObjectPermissions.map(
|
||||
fromFlatObjectPermissionToObjectPermissionDto,
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => [PermissionFlagDTO])
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
|
||||
}),
|
||||
this.objectPermissionRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'roleId'],
|
||||
select: ['id', 'universalIdentifier', 'roleId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.permissionFlagRepository.find({
|
||||
|
||||
+2
@@ -110,6 +110,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
targetTableName: 'test_entity',
|
||||
fieldIds: ['field-id'],
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
viewIds: [],
|
||||
universalIdentifier: 'test-entity-id',
|
||||
description: null,
|
||||
@@ -125,6 +126,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
applicationUniversalIdentifier: 'test-application-id',
|
||||
fieldUniversalIdentifiers: ['field-id'],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
|
||||
+8
@@ -224,6 +224,7 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
isAuditLogged: true,
|
||||
isSearchable: true,
|
||||
universalIdentifier: partial.id,
|
||||
objectPermissionIds: [],
|
||||
viewIds: [],
|
||||
applicationId: null,
|
||||
isLabelSyncedWithName: false,
|
||||
@@ -236,6 +237,13 @@ describe('computeRelationConnectQueryConfigs', () => {
|
||||
labelIdentifierFieldMetadataId: null,
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
duplicateCriteria: null,
|
||||
applicationUniversalIdentifier: '',
|
||||
fieldUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
...partial,
|
||||
}) as FlatObjectMetadata;
|
||||
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => {
|
||||
universalIdentifier: objectMetadataId,
|
||||
fieldIds: ['name-id'],
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
viewIds: [],
|
||||
applicationId: null,
|
||||
} as unknown as FlatObjectMetadata;
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
|
||||
workspaceId: 'test-workspace-id',
|
||||
universalIdentifier: 'test-object-id',
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldIds,
|
||||
viewIds: [],
|
||||
applicationId: 'test-application-id',
|
||||
@@ -41,6 +42,7 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
|
||||
duplicateCriteria: null,
|
||||
applicationUniversalIdentifier: 'test-application-id',
|
||||
fieldUniversalIdentifiers: fieldIds,
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
|
||||
workspaceId: 'test-workspace-id',
|
||||
universalIdentifier: 'test-object-id',
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldIds,
|
||||
viewIds: [],
|
||||
applicationId: 'test-application-id',
|
||||
@@ -41,6 +42,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
|
||||
duplicateCriteria: null,
|
||||
applicationUniversalIdentifier: 'test-application-id',
|
||||
fieldUniversalIdentifiers: fieldIds,
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => {
|
||||
workspaceId: 'test-workspace-id',
|
||||
universalIdentifier: 'test-object-id',
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldIds,
|
||||
viewIds: [],
|
||||
applicationId: 'test-application-id',
|
||||
@@ -41,6 +42,7 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => {
|
||||
duplicateCriteria: null,
|
||||
applicationUniversalIdentifier: 'test-application-id',
|
||||
fieldUniversalIdentifiers: fieldIds,
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
type ObjectsPermissionsByRoleId,
|
||||
type FeatureFlagKey,
|
||||
type ObjectsPermissionsByRoleId,
|
||||
} from 'twenty-shared/types';
|
||||
import { type EntityMetadata } from 'typeorm';
|
||||
|
||||
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
|
||||
import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/application/application-variable/types/application-variable-cache-maps.type';
|
||||
import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type';
|
||||
import { type FlatWorkspaceMemberMaps } from 'src/engine/core-modules/user/types/flat-workspace-member-maps.type';
|
||||
import { type FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
@@ -33,6 +33,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
|
||||
flatRoleMaps: 'flat-maps:role',
|
||||
flatRoleTargetMaps: 'flat-maps:role-target',
|
||||
flatPermissionFlagMaps: 'flat-maps:permission-flag',
|
||||
flatObjectPermissionMaps: 'flat-maps:object-permission',
|
||||
ORMEntityMetadatas: 'orm:entity-metadatas',
|
||||
flatAgentMaps: 'flat-maps:agent',
|
||||
flatSkillMaps: 'flat-maps:skill',
|
||||
|
||||
+2
@@ -95,12 +95,14 @@ export const createStandardObjectFlatMetadata = <
|
||||
targetTableName: 'DEPRECATED',
|
||||
fieldIds: [],
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
viewIds: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
id: standardObjectMetadataRelatedEntityIds[nameSingular].id,
|
||||
applicationUniversalIdentifier: twentyStandardApplicationId,
|
||||
fieldUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
|
||||
+2
-1
@@ -1,8 +1,8 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
import { type AllStandardRoleName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-role-name.type';
|
||||
import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-standard-application/types/metadata-standard-buillder-args.type';
|
||||
|
||||
@@ -74,6 +74,7 @@ export const createStandardRoleFlatMetadata = ({
|
||||
updatedAt: now,
|
||||
permissionFlagIds: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionIds: [],
|
||||
objectPermissionIds: [],
|
||||
roleTargetIds: [],
|
||||
|
||||
+1
@@ -168,6 +168,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
|
||||
case 'frontComponent':
|
||||
case 'viewSort':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'webhook': {
|
||||
if (!isDefined(idByUniversalIdentifier)) {
|
||||
return action;
|
||||
|
||||
+37
@@ -20,6 +20,7 @@ import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engin
|
||||
import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service';
|
||||
import { WorkspaceMigrationLogicFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/workspace-migration-logic-function-actions-builder.service';
|
||||
import { WorkspaceMigrationNavigationMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/workspace-migration-navigation-menu-item-actions-builder.service';
|
||||
import { WorkspaceMigrationObjectPermissionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/workspace-migration-object-permission-actions-builder.service';
|
||||
import { WorkspaceMigrationObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/workspace-migration-object-actions-builder.service';
|
||||
import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-tab/workspace-migration-page-layout-tab-actions-builder.service';
|
||||
import { WorkspaceMigrationPageLayoutWidgetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/workspace-migration-page-layout-widget-actions-builder.service';
|
||||
@@ -51,6 +52,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
private readonly workspaceMigrationViewGroupActionsBuilderService: WorkspaceMigrationViewGroupActionsBuilderService,
|
||||
private readonly workspaceMigrationViewFieldGroupActionsBuilderService: WorkspaceMigrationViewFieldGroupActionsBuilderService,
|
||||
private readonly workspaceMigrationViewSortActionsBuilderService: WorkspaceMigrationViewSortActionsBuilderService,
|
||||
private readonly workspaceMigrationObjectPermissionActionsBuilderService: WorkspaceMigrationObjectPermissionActionsBuilderService,
|
||||
private readonly workspaceMigrationPermissionFlagActionsBuilderService: WorkspaceMigrationPermissionFlagActionsBuilderService,
|
||||
private readonly workspaceMigrationLogicFunctionActionsBuilderService: WorkspaceMigrationLogicFunctionActionsBuilderService,
|
||||
private readonly workspaceMigrationRoleTargetActionsBuilderService: WorkspaceMigrationRoleTargetActionsBuilderService,
|
||||
@@ -142,6 +144,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
flatRowLevelPermissionPredicateMaps,
|
||||
flatRowLevelPermissionPredicateGroupMaps,
|
||||
flatRoleMaps,
|
||||
flatObjectPermissionMaps,
|
||||
flatPermissionFlagMaps,
|
||||
flatRoleTargetMaps,
|
||||
flatAgentMaps,
|
||||
@@ -484,6 +487,34 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatObjectPermissionMaps)) {
|
||||
const {
|
||||
from: fromFlatObjectPermissionMaps,
|
||||
to: toFlatObjectPermissionMaps,
|
||||
} = flatObjectPermissionMaps;
|
||||
|
||||
const objectPermissionResult =
|
||||
await this.workspaceMigrationObjectPermissionActionsBuilderService.validateAndBuild(
|
||||
{
|
||||
additionalCacheDataMaps,
|
||||
from: fromFlatObjectPermissionMaps,
|
||||
to: toFlatObjectPermissionMaps,
|
||||
buildOptions,
|
||||
dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (objectPermissionResult.status === 'fail') {
|
||||
orchestratorFailureReport.objectPermission.push(
|
||||
...objectPermissionResult.errors,
|
||||
);
|
||||
} else {
|
||||
orchestratorActionsReport.objectPermission =
|
||||
objectPermissionResult.actions;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatPermissionFlagMaps)) {
|
||||
const { from: fromFlatPermissionFlagMaps, to: toFlatPermissionFlagMaps } =
|
||||
flatPermissionFlagMaps;
|
||||
@@ -839,6 +870,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
...aggregatedOrchestratorActionsReport.roleTarget.update,
|
||||
///
|
||||
|
||||
// Object permissions
|
||||
...aggregatedOrchestratorActionsReport.objectPermission.delete,
|
||||
...aggregatedOrchestratorActionsReport.objectPermission.create,
|
||||
...aggregatedOrchestratorActionsReport.objectPermission.update,
|
||||
///
|
||||
|
||||
// Permission flags
|
||||
...aggregatedOrchestratorActionsReport.permissionFlag.delete,
|
||||
...aggregatedOrchestratorActionsReport.permissionFlag.create,
|
||||
|
||||
+3
@@ -19,8 +19,10 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc
|
||||
"objectMetadata": [
|
||||
"fieldUniversalIdentifiers",
|
||||
"indexMetadataUniversalIdentifiers",
|
||||
"objectPermissionUniversalIdentifiers",
|
||||
"viewUniversalIdentifiers",
|
||||
],
|
||||
"objectPermission": [],
|
||||
"pageLayout": [
|
||||
"tabUniversalIdentifiers",
|
||||
],
|
||||
@@ -31,6 +33,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc
|
||||
"permissionFlag": [],
|
||||
"role": [
|
||||
"roleTargetUniversalIdentifiers",
|
||||
"objectPermissionUniversalIdentifiers",
|
||||
"permissionFlagUniversalIdentifiers",
|
||||
"rowLevelPermissionPredicateUniversalIdentifiers",
|
||||
"rowLevelPermissionPredicateGroupUniversalIdentifiers",
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
|
||||
commandMenuItem: {},
|
||||
navigationMenuItem: {},
|
||||
permissionFlag: {},
|
||||
objectPermission: {},
|
||||
frontComponent: {},
|
||||
} as const satisfies {
|
||||
[P in AllMetadataName]: Partial<{
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
|
||||
|
||||
export type UniversalFlatObjectPermission = UniversalFlatEntityFrom<
|
||||
ObjectPermissionEntity,
|
||||
'objectPermission'
|
||||
>;
|
||||
+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 FlatCreateObjectPermissionAction =
|
||||
BaseFlatCreateWorkspaceMigrationAction<'objectPermission'>;
|
||||
|
||||
export type FlatUpdateObjectPermissionAction =
|
||||
BaseFlatUpdateWorkspaceMigrationAction<'objectPermission'>;
|
||||
|
||||
export type FlatDeleteObjectPermissionAction =
|
||||
BaseFlatDeleteWorkspaceMigrationAction<'objectPermission'>;
|
||||
|
||||
export type UniversalCreateObjectPermissionAction =
|
||||
BaseUniversalCreateWorkspaceMigrationAction<'objectPermission'>;
|
||||
|
||||
export type UniversalUpdateObjectPermissionAction =
|
||||
BaseUniversalUpdateWorkspaceMigrationAction<'objectPermission'>;
|
||||
|
||||
export type UniversalDeleteObjectPermissionAction =
|
||||
BaseUniversalDeleteWorkspaceMigrationAction<'objectPermission'>;
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
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 { UniversalUpdateObjectPermissionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type';
|
||||
import { FlatObjectPermissionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-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 WorkspaceMigrationObjectPermissionActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
|
||||
typeof ALL_METADATA_NAME.objectPermission
|
||||
> {
|
||||
constructor(
|
||||
private readonly flatObjectPermissionValidatorService: FlatObjectPermissionValidatorService,
|
||||
) {
|
||||
super(ALL_METADATA_NAME.objectPermission);
|
||||
}
|
||||
|
||||
protected validateFlatEntityCreation(
|
||||
args: UniversalFlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.objectPermission
|
||||
>,
|
||||
): UniversalFlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.objectPermission,
|
||||
'create'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatObjectPermissionValidatorService.validateFlatObjectPermissionCreation(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatObjectPermissionToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'create',
|
||||
metadataName: 'objectPermission',
|
||||
flatEntity: flatObjectPermissionToValidate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityDeletion(
|
||||
args: UniversalFlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.objectPermission
|
||||
>,
|
||||
): UniversalFlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.objectPermission,
|
||||
'delete'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatObjectPermissionValidatorService.validateFlatObjectPermissionDeletion(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatObjectPermissionToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'delete',
|
||||
metadataName: 'objectPermission',
|
||||
universalIdentifier: flatObjectPermissionToValidate.universalIdentifier,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityUpdate(
|
||||
args: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.objectPermission
|
||||
>,
|
||||
): UniversalFlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.objectPermission,
|
||||
'update'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatObjectPermissionValidatorService.validateFlatObjectPermissionUpdate(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { universalIdentifier, flatEntityUpdate } = args;
|
||||
|
||||
const updateObjectPermissionAction: UniversalUpdateObjectPermissionAction =
|
||||
{
|
||||
type: 'update',
|
||||
metadataName: 'objectPermission',
|
||||
universalIdentifier,
|
||||
update: flatEntityUpdate,
|
||||
};
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: updateObjectPermissionAction,
|
||||
};
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
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 FlatObjectPermissionValidatorService {
|
||||
validateFlatObjectPermissionCreation({
|
||||
flatEntityToValidate: flatObjectPermissionToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatObjectPermissionMaps: optimisticFlatObjectPermissionMaps,
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
}: UniversalFlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.objectPermission
|
||||
>): FailedFlatEntityValidation<'objectPermission', 'create'> {
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier: flatObjectPermissionToValidate.universalIdentifier,
|
||||
roleUniversalIdentifier:
|
||||
flatObjectPermissionToValidate.roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
flatObjectPermissionToValidate.objectMetadataUniversalIdentifier,
|
||||
},
|
||||
metadataName: 'objectPermission',
|
||||
type: 'create',
|
||||
});
|
||||
|
||||
const existingByUniversalId = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier: flatObjectPermissionToValidate.universalIdentifier,
|
||||
flatEntityMaps: optimisticFlatObjectPermissionMaps,
|
||||
});
|
||||
|
||||
if (isDefined(existingByUniversalId)) {
|
||||
validationResult.errors.push({
|
||||
code: PermissionsExceptionCode.INVALID_SETTING,
|
||||
message: t`Object permission with universal identifier ${flatObjectPermissionToValidate.universalIdentifier} already exists`,
|
||||
userFriendlyMessage: msg`Object permission already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
const referencedRole = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier:
|
||||
flatObjectPermissionToValidate.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:
|
||||
flatObjectPermissionToValidate.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 duplicateForSameRoleAndObject = Object.values(
|
||||
optimisticFlatObjectPermissionMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(op) =>
|
||||
isDefined(op) &&
|
||||
op.roleUniversalIdentifier ===
|
||||
flatObjectPermissionToValidate.roleUniversalIdentifier &&
|
||||
op.objectMetadataUniversalIdentifier ===
|
||||
flatObjectPermissionToValidate.objectMetadataUniversalIdentifier &&
|
||||
op.universalIdentifier !==
|
||||
flatObjectPermissionToValidate.universalIdentifier,
|
||||
);
|
||||
|
||||
if (duplicateForSameRoleAndObject.length > 0) {
|
||||
validationResult.errors.push({
|
||||
code: PermissionsExceptionCode.INVALID_SETTING,
|
||||
message: t`Object permission for this role and object already exists`,
|
||||
userFriendlyMessage: msg`This object permission is already set for the role`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
validateFlatObjectPermissionUpdate({
|
||||
universalIdentifier,
|
||||
flatEntityUpdate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatObjectPermissionMaps: optimisticFlatObjectPermissionMaps,
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
}: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.objectPermission
|
||||
>): FailedFlatEntityValidation<'objectPermission', 'update'> {
|
||||
const existingFlatObjectPermission = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier,
|
||||
flatEntityMaps: optimisticFlatObjectPermissionMaps,
|
||||
});
|
||||
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier,
|
||||
},
|
||||
metadataName: 'objectPermission',
|
||||
type: 'update',
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatObjectPermission)) {
|
||||
validationResult.errors.push({
|
||||
code: PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND,
|
||||
message: t`Object permission to update not found`,
|
||||
userFriendlyMessage: msg`Object permission not found`,
|
||||
});
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const updatedFlatObjectPermission = {
|
||||
...existingFlatObjectPermission,
|
||||
...flatEntityUpdate,
|
||||
};
|
||||
|
||||
const referencedRole = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier: updatedFlatObjectPermission.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:
|
||||
updatedFlatObjectPermission.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 duplicateForSameRoleAndObject = Object.values(
|
||||
optimisticFlatObjectPermissionMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(op) =>
|
||||
isDefined(op) &&
|
||||
op.roleUniversalIdentifier ===
|
||||
updatedFlatObjectPermission.roleUniversalIdentifier &&
|
||||
op.objectMetadataUniversalIdentifier ===
|
||||
updatedFlatObjectPermission.objectMetadataUniversalIdentifier &&
|
||||
op.universalIdentifier !== universalIdentifier,
|
||||
);
|
||||
|
||||
if (duplicateForSameRoleAndObject.length > 0) {
|
||||
validationResult.errors.push({
|
||||
code: PermissionsExceptionCode.INVALID_SETTING,
|
||||
message: t`Object permission for this role and object already exists`,
|
||||
userFriendlyMessage: msg`This object permission is already set for the role`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
validateFlatObjectPermissionDeletion({
|
||||
flatEntityToValidate: { universalIdentifier },
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatObjectPermissionMaps: optimisticFlatObjectPermissionMaps,
|
||||
flatRoleMaps,
|
||||
},
|
||||
}: UniversalFlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.objectPermission
|
||||
>): FailedFlatEntityValidation<'objectPermission', 'delete'> {
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier,
|
||||
},
|
||||
metadataName: 'objectPermission',
|
||||
type: 'delete',
|
||||
});
|
||||
|
||||
const existingFlatObjectPermission = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier,
|
||||
flatEntityMaps: optimisticFlatObjectPermissionMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingFlatObjectPermission)) {
|
||||
validationResult.errors.push({
|
||||
code: PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND,
|
||||
message: t`Object permission to delete not found`,
|
||||
userFriendlyMessage: msg`Object permission not found`,
|
||||
});
|
||||
} else {
|
||||
const referencedRole = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier:
|
||||
existingFlatObjectPermission.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
@@ -11,6 +11,7 @@ import { FlatIndexValidatorService } from 'src/engine/workspace-manager/workspac
|
||||
import { FlatLogicFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-logic-function-validator.service';
|
||||
import { FlatNavigationMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service';
|
||||
import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-metadata-validator.service';
|
||||
import { FlatObjectPermissionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-object-permission-validator.service';
|
||||
import { FlatPageLayoutTabValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-tab-validator.service';
|
||||
import { FlatPageLayoutValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-validator.service';
|
||||
import { FlatPageLayoutWidgetValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service';
|
||||
@@ -39,6 +40,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp
|
||||
FlatViewGroupValidatorService,
|
||||
FlatViewFieldGroupValidatorService,
|
||||
FlatViewSortValidatorService,
|
||||
FlatObjectPermissionValidatorService,
|
||||
FlatPermissionFlagValidatorService,
|
||||
FlatIndexValidatorService,
|
||||
FlatFieldMetadataValidatorService,
|
||||
@@ -68,6 +70,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp
|
||||
FlatViewGroupValidatorService,
|
||||
FlatViewFieldGroupValidatorService,
|
||||
FlatViewSortValidatorService,
|
||||
FlatObjectPermissionValidatorService,
|
||||
FlatPermissionFlagValidatorService,
|
||||
FlatIndexValidatorService,
|
||||
FlatFieldMetadataValidatorService,
|
||||
|
||||
+3
@@ -9,6 +9,7 @@ import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engin
|
||||
import { WorkspaceMigrationIndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/workspace-migration-index-actions-builder.service';
|
||||
import { WorkspaceMigrationLogicFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/workspace-migration-logic-function-actions-builder.service';
|
||||
import { WorkspaceMigrationNavigationMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/workspace-migration-navigation-menu-item-actions-builder.service';
|
||||
import { WorkspaceMigrationObjectPermissionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/workspace-migration-object-permission-actions-builder.service';
|
||||
import { WorkspaceMigrationObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/workspace-migration-object-actions-builder.service';
|
||||
import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-tab/workspace-migration-page-layout-tab-actions-builder.service';
|
||||
import { WorkspaceMigrationPageLayoutWidgetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/workspace-migration-page-layout-widget-actions-builder.service';
|
||||
@@ -41,6 +42,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationViewGroupActionsBuilderService,
|
||||
WorkspaceMigrationViewFieldGroupActionsBuilderService,
|
||||
WorkspaceMigrationViewSortActionsBuilderService,
|
||||
WorkspaceMigrationObjectPermissionActionsBuilderService,
|
||||
WorkspaceMigrationPermissionFlagActionsBuilderService,
|
||||
WorkspaceMigrationIndexActionsBuilderService,
|
||||
WorkspaceMigrationLogicFunctionActionsBuilderService,
|
||||
@@ -69,6 +71,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationViewGroupActionsBuilderService,
|
||||
WorkspaceMigrationViewFieldGroupActionsBuilderService,
|
||||
WorkspaceMigrationViewSortActionsBuilderService,
|
||||
WorkspaceMigrationObjectPermissionActionsBuilderService,
|
||||
WorkspaceMigrationPermissionFlagActionsBuilderService,
|
||||
WorkspaceMigrationFieldActionsBuilderService,
|
||||
WorkspaceMigrationLogicFunctionActionsBuilderService,
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
FlatCreateObjectPermissionAction,
|
||||
UniversalCreateObjectPermissionAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-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 CreateObjectPermissionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'create',
|
||||
'objectPermission',
|
||||
) {
|
||||
override async transpileUniversalActionToFlatAction({
|
||||
action,
|
||||
allFlatEntityMaps,
|
||||
flatApplication,
|
||||
workspaceId,
|
||||
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateObjectPermissionAction>): Promise<FlatCreateObjectPermissionAction> {
|
||||
const { roleId, objectMetadataId } =
|
||||
resolveUniversalRelationIdentifiersToIds({
|
||||
flatEntityMaps: allFlatEntityMaps,
|
||||
metadataName: action.metadataName,
|
||||
universalForeignKeyValues: action.flatEntity,
|
||||
});
|
||||
|
||||
return {
|
||||
...action,
|
||||
flatEntity: {
|
||||
...action.flatEntity,
|
||||
roleId,
|
||||
objectMetadataId,
|
||||
applicationId: flatApplication.id,
|
||||
id: action.id ?? v4(),
|
||||
workspaceId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerContext<FlatCreateObjectPermissionAction>,
|
||||
): Promise<void> {
|
||||
const { flatAction, queryRunner } = context;
|
||||
const { flatEntity } = flatAction;
|
||||
|
||||
await this.insertFlatEntitiesInRepository({
|
||||
queryRunner,
|
||||
flatEntities: [flatEntity],
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerContext<FlatCreateObjectPermissionAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import {
|
||||
FlatDeleteObjectPermissionAction,
|
||||
UniversalDeleteObjectPermissionAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-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 DeleteObjectPermissionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'delete',
|
||||
'objectPermission',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override async transpileUniversalActionToFlatAction(
|
||||
context: WorkspaceMigrationActionRunnerArgs<UniversalDeleteObjectPermissionAction>,
|
||||
): Promise<FlatDeleteObjectPermissionAction> {
|
||||
return this.transpileUniversalDeleteActionToFlatDeleteAction(context);
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerContext<FlatDeleteObjectPermissionAction>,
|
||||
): Promise<void> {
|
||||
const { flatAction, queryRunner, workspaceId } = context;
|
||||
|
||||
const objectPermissionRepository =
|
||||
queryRunner.manager.getRepository<ObjectPermissionEntity>(
|
||||
ObjectPermissionEntity,
|
||||
);
|
||||
|
||||
await objectPermissionRepository.delete({
|
||||
id: flatAction.entityId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerContext<FlatDeleteObjectPermissionAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
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 { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import {
|
||||
FlatUpdateObjectPermissionAction,
|
||||
UniversalUpdateObjectPermissionAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-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 UpdateObjectPermissionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'update',
|
||||
'objectPermission',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override async transpileUniversalActionToFlatAction(
|
||||
context: WorkspaceMigrationActionRunnerArgs<UniversalUpdateObjectPermissionAction>,
|
||||
): Promise<FlatUpdateObjectPermissionAction> {
|
||||
const { action, allFlatEntityMaps } = context;
|
||||
|
||||
const flatObjectPermission = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: allFlatEntityMaps.flatObjectPermissionMaps,
|
||||
universalIdentifier: action.universalIdentifier,
|
||||
});
|
||||
|
||||
const update = resolveUniversalUpdateRelationIdentifiersToIds({
|
||||
metadataName: 'objectPermission',
|
||||
universalUpdate: action.update,
|
||||
allFlatEntityMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'update',
|
||||
metadataName: 'objectPermission',
|
||||
entityId: flatObjectPermission.id,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerContext<FlatUpdateObjectPermissionAction>,
|
||||
): Promise<void> {
|
||||
const { flatAction, queryRunner, workspaceId } = context;
|
||||
const { entityId, update } = flatAction;
|
||||
|
||||
const objectPermissionRepository =
|
||||
queryRunner.manager.getRepository<ObjectPermissionEntity>(
|
||||
ObjectPermissionEntity,
|
||||
);
|
||||
|
||||
await objectPermissionRepository.update(
|
||||
{ id: entityId, workspaceId },
|
||||
update,
|
||||
);
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerContext<FlatUpdateObjectPermissionAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+1
@@ -122,6 +122,7 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({
|
||||
fieldIds: [],
|
||||
viewIds: [],
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
...emptyUniversalForeignKeyAggregators,
|
||||
};
|
||||
};
|
||||
|
||||
+7
@@ -24,6 +24,9 @@ import { UpdateLogicFunctionActionHandlerService } from 'src/engine/workspace-ma
|
||||
import { CreateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/create-navigation-menu-item-action-handler.service';
|
||||
import { DeleteNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/delete-navigation-menu-item-action-handler.service';
|
||||
import { UpdateNavigationMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/navigation-menu-item/services/update-navigation-menu-item-action-handler.service';
|
||||
import { CreateObjectPermissionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/create-object-permission-action-handler.service';
|
||||
import { DeleteObjectPermissionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/delete-object-permission-action-handler.service';
|
||||
import { UpdateObjectPermissionActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object-permission/services/update-object-permission-action-handler.service';
|
||||
import { CreateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/create-object-action-handler.service';
|
||||
import { DeleteObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/delete-object-action-handler.service';
|
||||
import { UpdateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/update-object-action-handler.service';
|
||||
@@ -141,6 +144,10 @@ import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/
|
||||
UpdatePermissionFlagActionHandlerService,
|
||||
DeletePermissionFlagActionHandlerService,
|
||||
|
||||
CreateObjectPermissionActionHandlerService,
|
||||
UpdateObjectPermissionActionHandlerService,
|
||||
DeleteObjectPermissionActionHandlerService,
|
||||
|
||||
CreateAgentActionHandlerService,
|
||||
UpdateAgentActionHandlerService,
|
||||
DeleteAgentActionHandlerService,
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ export const METADATA_EVENTS_TO_EMIT = {
|
||||
commandMenuItem: true,
|
||||
navigationMenuItem: true,
|
||||
permissionFlag: true,
|
||||
objectPermission: true,
|
||||
rowLevelPermissionPredicate: true,
|
||||
rowLevelPermissionPredicateGroup: true,
|
||||
index: true,
|
||||
|
||||
+1
@@ -90,6 +90,7 @@ const deriveAllMetadataEventsFromCreateAction = (
|
||||
case 'frontComponent':
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook': {
|
||||
return [
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ const deriveAllMetadataEventsFromDeleteAction = ({
|
||||
case 'frontComponent':
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook': {
|
||||
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
|
||||
|
||||
+1
@@ -132,6 +132,7 @@ const deriveAllMetadataEventsFromUpdateAction = ({
|
||||
case 'frontComponent':
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook': {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
|
||||
|
||||
+1
@@ -69,6 +69,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({
|
||||
case 'frontComponent':
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'webhook': {
|
||||
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: flatAction.flatEntity,
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({
|
||||
case 'frontComponent':
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'webhook': {
|
||||
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
|
||||
MetadataFlatEntity<typeof flatAction.metadataName>
|
||||
|
||||
+2
-1
@@ -6,8 +6,8 @@ import { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } f
|
||||
import { deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { type AllFlatWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
import { replaceFlatEntityInFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/replace-flat-entity-in-flat-entity-maps-through-mutation-or-throw.util';
|
||||
import { type AllFlatWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
|
||||
|
||||
export type OptimisticallyApplyUpdateActionOnAllFlatEntityMapsArgs = {
|
||||
flatAction: AllFlatWorkspaceMigrationAction<'update'>;
|
||||
@@ -62,6 +62,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({
|
||||
case 'frontComponent':
|
||||
case 'navigationMenuItem':
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'webhook': {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
|
||||
flatAction.metadataName,
|
||||
|
||||
+11
-4
@@ -493,12 +493,19 @@ describe('roles permissions', () => {
|
||||
.expect((res) => {
|
||||
expect(res.body.data).toBeNull();
|
||||
expect(res.body.errors).toBeDefined();
|
||||
expect(res.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
|
||||
);
|
||||
expect(res.body.errors[0].extensions.code).toBe(
|
||||
ErrorCode.FORBIDDEN,
|
||||
ErrorCode.METADATA_VALIDATION_FAILED,
|
||||
);
|
||||
const objectPermissionErrors =
|
||||
res.body.errors[0].extensions.errors?.objectPermission ?? [];
|
||||
const hasRoleNotEditable = objectPermissionErrors.some(
|
||||
(failure: { errors?: Array<{ code?: string }> }) =>
|
||||
failure.errors?.some(
|
||||
(err) =>
|
||||
err.code === PermissionsExceptionCode.ROLE_NOT_EDITABLE,
|
||||
),
|
||||
);
|
||||
expect(hasRoleNotEditable).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`Object permission upsert should fail when object is system object 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "FORBIDDEN",
|
||||
"subCode": "CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT",
|
||||
"userFriendlyMessage": "You cannot set permissions on system objects as they are managed by the platform.",
|
||||
},
|
||||
"message": "Cannot add object permission on system object",
|
||||
"name": "ForbiddenError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Object 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 id not found",
|
||||
"name": "NotFoundError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Object permission upsert should fail when read=false but canUpdateObjectRecords=true (read/write consistency) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT",
|
||||
"userFriendlyMessage": "You cannot grant edit permissions without also granting read permissions. Please enable read access first.",
|
||||
},
|
||||
"message": "Cannot give update permission to non-readable object",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Object permission upsert should fail when role is not editable (system role) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"objectPermission": [
|
||||
{
|
||||
"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": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"roleUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectPermission",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 objectPermission",
|
||||
"summary": {
|
||||
"objectPermission": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Metadata validation failed",
|
||||
},
|
||||
"message": "Validation errors occurred while upserting object permissions",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Object permission upsert should fail when roleId does not exist 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "NOT_FOUND",
|
||||
"subCode": "ROLE_NOT_FOUND",
|
||||
"userFriendlyMessage": "The role you are trying to modify could not be found.",
|
||||
},
|
||||
"message": "Role not found",
|
||||
"name": "NotFoundError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Object 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",
|
||||
}
|
||||
`;
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.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 UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
|
||||
|
||||
type GlobalTestContext = {
|
||||
editableRoleId: string;
|
||||
nonEditableRoleId: string;
|
||||
systemObjectMetadataId: string;
|
||||
nonSystemObjectMetadataId: string;
|
||||
editableRoleWithNoReadId: string;
|
||||
};
|
||||
|
||||
type TestContext = {
|
||||
input: (globalContext: GlobalTestContext) => UpsertObjectPermissionsInput;
|
||||
};
|
||||
|
||||
const failingObjectPermissionUpsertTestCases: EachTestingContext<TestContext>[] =
|
||||
[
|
||||
{
|
||||
title: 'when roleId is not a valid UUID',
|
||||
context: {
|
||||
input: () => ({
|
||||
roleId: 'invalid-uuid',
|
||||
objectPermissions: [
|
||||
{
|
||||
objectMetadataId: v4(),
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when roleId does not exist',
|
||||
context: {
|
||||
input: () => ({
|
||||
roleId: v4(),
|
||||
objectPermissions: [
|
||||
{
|
||||
objectMetadataId: v4(),
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when role is not editable (system role)',
|
||||
context: {
|
||||
input: (globalContext) => ({
|
||||
roleId: globalContext.nonEditableRoleId,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectMetadataId: globalContext.nonSystemObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when objectMetadataId does not exist',
|
||||
context: {
|
||||
input: (globalContext) => ({
|
||||
roleId: globalContext.editableRoleId,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectMetadataId: v4(),
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when object is system object',
|
||||
context: {
|
||||
input: (globalContext) => ({
|
||||
roleId: globalContext.editableRoleId,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectMetadataId: globalContext.systemObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when read=false but canUpdateObjectRecords=true (read/write consistency)',
|
||||
context: {
|
||||
input: (globalContext) => ({
|
||||
roleId: globalContext.editableRoleWithNoReadId,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectMetadataId: globalContext.nonSystemObjectMetadataId,
|
||||
canReadObjectRecords: false,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('Object permission upsert should fail', () => {
|
||||
let editableRoleId: string;
|
||||
let nonEditableRoleId: string;
|
||||
let systemObjectMetadataId: string;
|
||||
let nonSystemObjectMetadataId: string;
|
||||
let editableRoleWithNoReadId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data: roleData } = await createOneRole({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
label: 'Test Role For Object Permission Failing',
|
||||
description: 'Role for object 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: roleNoReadData } = await createOneRole({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
label: 'Test Role No Read For Object Permission',
|
||||
description: 'Role with no read for consistency test',
|
||||
icon: 'IconSettings',
|
||||
canUpdateAllSettings: false,
|
||||
canAccessAllTools: true,
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
},
|
||||
});
|
||||
|
||||
editableRoleWithNoReadId = roleNoReadData?.createOneRole?.id;
|
||||
jestExpectToBeDefined(editableRoleWithNoReadId);
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (isDefined(editableRoleId)) {
|
||||
await deleteOneRole({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: editableRoleId },
|
||||
});
|
||||
}
|
||||
if (isDefined(editableRoleWithNoReadId)) {
|
||||
await deleteOneRole({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: editableRoleWithNoReadId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it.each(eachTestingContextFilter(failingObjectPermissionUpsertTestCases))(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const globalContext: GlobalTestContext = {
|
||||
editableRoleId: editableRoleId ?? '',
|
||||
nonEditableRoleId: nonEditableRoleId ?? '',
|
||||
systemObjectMetadataId: systemObjectMetadataId ?? '',
|
||||
nonSystemObjectMetadataId: nonSystemObjectMetadataId ?? '',
|
||||
editableRoleWithNoReadId: editableRoleWithNoReadId ?? '',
|
||||
};
|
||||
const input = context.input(globalContext);
|
||||
|
||||
const { errors } = await upsertObjectPermissions({
|
||||
expectToFail: true,
|
||||
input,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { 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 { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
describe('Object permission upsert should succeed', () => {
|
||||
let createdRoleId: string;
|
||||
let customObjectMetadataId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data: roleData } = await createOneRole({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
label: 'Test Role For Object Permission Success',
|
||||
description: 'Role for object permission successful tests',
|
||||
icon: 'IconSettings',
|
||||
canUpdateAllSettings: false,
|
||||
canAccessAllTools: true,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canBeAssignedToUsers: true,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
},
|
||||
});
|
||||
|
||||
createdRoleId = roleData?.createOneRole?.id;
|
||||
jestExpectToBeDefined(createdRoleId);
|
||||
|
||||
const {
|
||||
data: { createOneObject },
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'testObjectPermissionSuccess',
|
||||
namePlural: 'testObjectPermissionSuccesses',
|
||||
labelSingular: 'Test Object Permission Success',
|
||||
labelPlural: 'Test Object Permission Successes',
|
||||
icon: 'IconSettings',
|
||||
},
|
||||
});
|
||||
|
||||
customObjectMetadataId = createOneObject.id;
|
||||
jestExpectToBeDefined(customObjectMetadataId);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (isDefined(createdRoleId)) {
|
||||
await deleteOneRole({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdRoleId },
|
||||
});
|
||||
}
|
||||
if (isDefined(customObjectMetadataId)) {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: customObjectMetadataId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should upsert one object permission (create)', async () => {
|
||||
const { data } = await upsertObjectPermissions({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
roleId: createdRoleId,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(data?.upsertObjectPermissions).toHaveLength(1);
|
||||
expect(data?.upsertObjectPermissions?.[0]).toMatchObject({
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should upsert to update existing object permission flags', async () => {
|
||||
const { data } = await upsertObjectPermissions({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
roleId: createdRoleId,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(data?.upsertObjectPermissions).toHaveLength(1);
|
||||
expect(data?.upsertObjectPermissions?.[0]).toMatchObject({
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should upsert with read and all write permissions true', async () => {
|
||||
const { data } = await upsertObjectPermissions({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
roleId: createdRoleId,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(data?.upsertObjectPermissions).toHaveLength(1);
|
||||
expect(data?.upsertObjectPermissions?.[0]).toMatchObject({
|
||||
objectMetadataId: customObjectMetadataId,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
|
||||
import { type UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
|
||||
|
||||
export type UpsertObjectPermissionsFactoryInput = UpsertObjectPermissionsInput;
|
||||
|
||||
const DEFAULT_OBJECT_PERMISSION_GQL_FIELDS = `
|
||||
objectMetadataId
|
||||
canReadObjectRecords
|
||||
canUpdateObjectRecords
|
||||
canSoftDeleteObjectRecords
|
||||
canDestroyObjectRecords
|
||||
`;
|
||||
|
||||
export const upsertObjectPermissionsQueryFactory = ({
|
||||
input,
|
||||
gqlFields = DEFAULT_OBJECT_PERMISSION_GQL_FIELDS,
|
||||
}: PerformMetadataQueryParams<UpsertObjectPermissionsFactoryInput>) => ({
|
||||
query: gql`
|
||||
mutation UpsertObjectPermissions(
|
||||
$upsertObjectPermissionsInput: UpsertObjectPermissionsInput!
|
||||
) {
|
||||
upsertObjectPermissions(
|
||||
upsertObjectPermissionsInput: $upsertObjectPermissionsInput
|
||||
) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
upsertObjectPermissionsInput: input,
|
||||
},
|
||||
});
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
type UpsertObjectPermissionsFactoryInput,
|
||||
upsertObjectPermissionsQueryFactory,
|
||||
} from 'test/integration/metadata/suites/object-permission/utils/upsert-object-permissions-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
|
||||
export const upsertObjectPermissions = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
token,
|
||||
}: PerformMetadataQueryParams<UpsertObjectPermissionsFactoryInput>): CommonResponseBody<{
|
||||
upsertObjectPermissions: ObjectPermissionDTO[];
|
||||
}> => {
|
||||
const graphqlOperation = upsertObjectPermissionsQueryFactory({
|
||||
input,
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation, token);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage:
|
||||
'Object permissions upsert should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'Object permissions upsert has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -22,6 +22,7 @@ export const ALL_METADATA_NAME = {
|
||||
commandMenuItem: 'commandMenuItem',
|
||||
navigationMenuItem: 'navigationMenuItem',
|
||||
permissionFlag: 'permissionFlag',
|
||||
objectPermission: 'objectPermission',
|
||||
frontComponent: 'frontComponent',
|
||||
webhook: 'webhook',
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user