diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts index 1d9f0fc8f1..1e26795748 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts @@ -4,20 +4,14 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { ApplicationResolver } from 'src/engine/core-modules/application/resolvers/application.resolver'; import { MarketplaceResolver } from 'src/engine/core-modules/application/resolvers/marketplace.resolver'; +import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/services/application-manifest-migration.service'; import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service'; import { ApplicationVariableEntityModule } from 'src/engine/core-modules/applicationVariable/application-variable.module'; import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; -import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; -import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module'; -import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module'; import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; -import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module'; @@ -30,14 +24,8 @@ import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workf TypeOrmModule.forFeature([FileEntity]), ApplicationModule, ApplicationVariableEntityModule, - WorkspaceManyOrAllFlatEntityMapsCacheModule, - ObjectMetadataModule, - FieldMetadataModule, - DataSourceModule, - LogicFunctionModule, WorkspaceMigrationModule, PermissionsModule, - RoleModule, ObjectPermissionModule, PermissionFlagModule, WorkflowCommonModule, @@ -45,11 +33,11 @@ import { CodeStepBuildModule } from 'src/modules/workflow/workflow-builder/workf FileStorageModule, WorkspaceCacheModule, WorkspaceMigrationRunnerModule, - FrontComponentModule, ], providers: [ ApplicationResolver, MarketplaceResolver, + ApplicationManifestMigrationService, ApplicationSyncService, WorkspaceMigrationGraphqlApiExceptionInterceptor, ], diff --git a/packages/twenty-server/src/engine/core-modules/application/constants/application-manifest-metadata-names.constant.ts b/packages/twenty-server/src/engine/core-modules/application/constants/application-manifest-metadata-names.constant.ts new file mode 100644 index 0000000000..715608dcce --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/constants/application-manifest-metadata-names.constant.ts @@ -0,0 +1,12 @@ +import { type AllMetadataName } from 'twenty-shared/metadata'; + +export const APPLICATION_MANIFEST_METADATA_NAMES = [ + 'objectMetadata', + 'fieldMetadata', + 'logicFunction', + 'frontComponent', + 'role', +] as const satisfies AllMetadataName[]; + +export type ApplicationManifestMetadataName = + (typeof APPLICATION_MANIFEST_METADATA_NAMES)[number]; diff --git a/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts new file mode 100644 index 0000000000..a6ce72286a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/services/application-manifest-migration.service.ts @@ -0,0 +1,293 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { type Manifest, type RoleManifest } from 'twenty-shared/application'; +import { isDefined } from 'twenty-shared/utils'; + +import { + ApplicationException, + ApplicationExceptionCode, +} from 'src/engine/core-modules/application/application.exception'; +import { APPLICATION_MANIFEST_METADATA_NAMES } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; +import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { computeApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util'; +import { getSubApplicationFromToAllFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util'; +import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +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 { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service'; +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'; + +@Injectable() +export class ApplicationManifestMigrationService { + private readonly logger = new Logger( + ApplicationManifestMigrationService.name, + ); + + constructor( + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly applicationService: ApplicationService, + private readonly objectPermissionService: ObjectPermissionService, + private readonly fieldPermissionService: FieldPermissionService, + private readonly permissionFlagService: PermissionFlagService, + ) {} + + async syncMetadataFromManifest({ + manifest, + workspaceId, + ownerFlatApplication, + }: { + manifest: Manifest; + workspaceId: string; + ownerFlatApplication: FlatApplication; + }) { + const now = new Date().toISOString(); + + const toAllUniversalFlatEntityMaps = + computeApplicationManifestAllUniversalFlatEntityMaps({ + manifest, + applicationUniversalIdentifier: + ownerFlatApplication.universalIdentifier, + now, + }); + + const cacheResult = await this.workspaceCacheService.getOrRecompute( + workspaceId, + [ + ...APPLICATION_MANIFEST_METADATA_NAMES.map( + getMetadataFlatEntityMapsKey, + ), + 'featureFlagsMap', + ], + ); + + const { featureFlagsMap, ...fromAllFlatEntityMaps } = cacheResult; + + const fromToAllFlatEntityMaps = getSubApplicationFromToAllFlatEntityMaps({ + applicationId: ownerFlatApplication.id, + fromAllFlatEntityMaps, + toAllUniversalFlatEntityMaps, + }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo( + { + buildOptions: { + isSystemBuild: true, + inferDeletionFromMissingEntities: true, + }, + fromToAllFlatEntityMaps, + workspaceId, + additionalCacheDataMaps: { featureFlagsMap }, + applicationUniversalIdentifier: + ownerFlatApplication.universalIdentifier, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Validation errors occurred while syncing application manifest metadata', + ); + } + + this.logger.log( + `Metadata migration completed for application ${ownerFlatApplication.universalIdentifier}`, + ); + + await this.syncRolePermissionsAndDefaultRole({ + manifest, + workspaceId, + ownerFlatApplication, + }); + } + + /** + * @deprecated should be remove once below issues are resolved: + * - [objectPermission](https://github.com/twentyhq/core-team-issues/issues/2223) + * - [fieldPermission](https://github.com/twentyhq/core-team-issues/issues/2224) + * - [permissionFlag](https://github.com/twentyhq/core-team-issues/issues/2225) + */ + private async syncRolePermissionsAndDefaultRole({ + manifest, + workspaceId, + ownerFlatApplication, + }: { + manifest: Manifest; + workspaceId: string; + ownerFlatApplication: FlatApplication; + }) { + const { + flatRoleMaps: refreshedFlatRoleMaps, + flatObjectMetadataMaps: refreshedFlatObjectMetadataMaps, + flatFieldMetadataMaps: refreshedFlatFieldMetadataMaps, + } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatRoleMaps', + 'flatObjectMetadataMaps', + 'flatFieldMetadataMaps', + ]); + + let defaultRoleId: string | null = null; + + for (const role of manifest.roles) { + const flatRole = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: refreshedFlatRoleMaps, + universalIdentifier: role.universalIdentifier, + }); + + if (!isDefined(flatRole)) { + throw new ApplicationException( + `Failed to resolve role for universalIdentifier ${role.universalIdentifier}`, + ApplicationExceptionCode.ENTITY_NOT_FOUND, + ); + } + + await this.syncApplicationRolePermissions({ + role, + workspaceId, + roleId: flatRole.id, + refreshedFlatObjectMetadataMaps, + refreshedFlatFieldMetadataMaps, + }); + + if ( + role.universalIdentifier === + manifest.application.defaultRoleUniversalIdentifier + ) { + defaultRoleId = flatRole.id; + } + } + + if (isDefined(defaultRoleId)) { + await this.applicationService.update(ownerFlatApplication.id, { + defaultRoleId, + }); + } + } + + private async syncApplicationRolePermissions({ + role, + workspaceId, + roleId, + refreshedFlatObjectMetadataMaps, + refreshedFlatFieldMetadataMaps, + }: { + role: RoleManifest; + workspaceId: string; + roleId: string; + refreshedFlatObjectMetadataMaps: Awaited< + ReturnType + >['flatObjectMetadataMaps']; + refreshedFlatFieldMetadataMaps: Awaited< + ReturnType + >['flatFieldMetadataMaps']; + }) { + if ( + (role.objectPermissions ?? []).length > 0 || + (role.fieldPermissions ?? []).length > 0 + ) { + const formattedObjectPermissions = role.objectPermissions + ?.map((permission) => { + const flatObjectMetadata = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: refreshedFlatObjectMetadataMaps, + universalIdentifier: permission.objectUniversalIdentifier, + }); + + if (!isDefined(flatObjectMetadata)) { + throw new ApplicationException( + `Failed to find object with universalIdentifier ${permission.objectUniversalIdentifier}`, + ApplicationExceptionCode.OBJECT_NOT_FOUND, + ); + } + + return { + ...permission, + objectMetadataId: flatObjectMetadata.id, + }; + }) + .filter( + ( + permission, + ): permission is typeof permission & { objectMetadataId: string } => + isDefined(permission.objectMetadataId), + ); + + if (isDefined(formattedObjectPermissions)) { + await this.objectPermissionService.upsertObjectPermissions({ + workspaceId, + input: { + roleId, + objectPermissions: formattedObjectPermissions, + }, + }); + } + + const formattedFieldPermissions = role.fieldPermissions + ?.map((permission) => { + const flatObjectMetadata = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: refreshedFlatObjectMetadataMaps, + universalIdentifier: permission.objectUniversalIdentifier, + }); + + if (!isDefined(flatObjectMetadata)) { + throw new ApplicationException( + `Failed to find object with universalIdentifier ${permission.objectUniversalIdentifier}`, + ApplicationExceptionCode.OBJECT_NOT_FOUND, + ); + } + + const flatFieldMetadata = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: refreshedFlatFieldMetadataMaps, + universalIdentifier: permission.fieldUniversalIdentifier, + }); + + if (!isDefined(flatFieldMetadata)) { + throw new ApplicationException( + `Failed to find field with universalIdentifier ${permission.fieldUniversalIdentifier}`, + ApplicationExceptionCode.FIELD_NOT_FOUND, + ); + } + + return { + ...permission, + objectMetadataId: flatObjectMetadata.id, + fieldMetadataId: flatFieldMetadata.id, + }; + }) + .filter( + ( + permission, + ): permission is typeof permission & { + objectMetadataId: string; + fieldMetadataId: string; + } => + isDefined(permission.objectMetadataId) && + isDefined(permission.fieldMetadataId), + ); + + if (isDefined(formattedFieldPermissions)) { + await this.fieldPermissionService.upsertFieldPermissions({ + workspaceId, + input: { + roleId, + fieldPermissions: formattedFieldPermissions, + }, + }); + } + } + + if (isDefined(role.permissionFlags) && role.permissionFlags.length > 0) { + await this.permissionFlagService.upsertPermissionFlags({ + workspaceId, + input: { + roleId, + permissionFlagKeys: role.permissionFlags, + }, + }); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts index 2cccc78734..6627f9ced9 100644 --- a/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts @@ -1,56 +1,27 @@ import { Injectable, Logger } from '@nestjs/common'; -import { parse } from 'path'; - -import { - CronTriggerSettings, - DatabaseEventTriggerSettings, - FieldManifest, - HttpRouteTriggerSettings, - FrontComponentManifest, - LogicFunctionManifest, - Manifest, - ObjectFieldManifest, - ObjectManifest, - RelationFieldManifest, - RoleManifest, -} from 'twenty-shared/application'; -import { FieldMetadataType, FileFolder } from 'twenty-shared/types'; +import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { PackageJson } from 'type-fest'; -import { v4 } from 'uuid'; +import { getDefaultApplicationPackageFields } from 'src/engine/core-modules/application-layer/utils/get-default-application-package-fields.util'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationException, ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; +import { APPLICATION_MANIFEST_METADATA_NAMES } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input'; +import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/services/application-manifest-migration.service'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; -import { getDefaultApplicationPackageFields } from 'src/engine/core-modules/application-layer/utils/get-default-application-package-fields.util'; +import { getEmptyApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util'; +import { getSubApplicationFromToAllFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util'; import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import type { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; -import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { findFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entities-by-application-id.util'; -import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; -import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service'; -import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service'; -import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -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 { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service'; -import { RoleService } from 'src/engine/metadata-modules/role/role.service'; -import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +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 { streamToBuffer } from 'src/utils/stream-to-buffer'; @@ -61,18 +32,10 @@ export class ApplicationSyncService { constructor( private readonly applicationService: ApplicationService, private readonly applicationVariableService: ApplicationVariableEntityService, - private readonly objectMetadataService: ObjectMetadataService, - private readonly fieldMetadataService: FieldMetadataService, - private readonly logicFunctionMetadataService: LogicFunctionMetadataService, - private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - private readonly dataSourceService: DataSourceService, + private readonly applicationManifestMigrationService: ApplicationManifestMigrationService, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, - private readonly roleService: RoleService, - private readonly objectPermissionService: ObjectPermissionService, - private readonly fieldPermissionService: FieldPermissionService, - private readonly permissionService: PermissionFlagService, + private readonly workspaceCacheService: WorkspaceCacheService, private readonly fileStorageService: FileStorageService, - private readonly frontComponentService: FrontComponentService, ) {} public async synchronizeFromManifest({ @@ -88,43 +51,7 @@ export class ApplicationSyncService { const ownerFlatApplication: FlatApplication = application; - await this.syncObjects({ - objectsToSync: manifest.objects, - workspaceId, - ownerFlatApplication, - }); - - await this.syncObjectRelations({ - objectsToSync: manifest.objects, - workspaceId, - ownerFlatApplication, - }); - - if (manifest.fields.length > 0) { - await this.syncFieldsOrThrow({ - fieldsToSync: manifest.fields, - workspaceId, - ownerFlatApplication, - }); - } - - if (manifest.logicFunctions.length > 0) { - await this.syncLogicFunctions({ - logicFunctionsToSync: manifest.logicFunctions, - workspaceId, - ownerFlatApplication, - }); - } - - if ((manifest.frontComponents ?? []).length > 0) { - await this.syncFrontComponents({ - frontComponentsToSync: manifest.frontComponents, - workspaceId, - ownerFlatApplication, - }); - } - - await this.syncRoles({ + await this.applicationManifestMigrationService.syncMetadataFromManifest({ manifest, workspaceId, ownerFlatApplication, @@ -196,904 +123,6 @@ export class ApplicationSyncService { }); } - private async syncRoles({ - manifest, - workspaceId, - ownerFlatApplication, - }: { - manifest: Manifest; - workspaceId: string; - ownerFlatApplication: FlatApplication; - }) { - let defaultRoleId: string | null = null; - - for (const role of manifest.roles) { - let existingRole = await this.roleService.getRoleByUniversalIdentifier({ - universalIdentifier: role.universalIdentifier, - workspaceId, - }); - - if (existingRole) { - await this.roleService.updateRole({ - input: { - id: existingRole.id, - update: role, - }, - workspaceId, - ownerFlatApplication, - }); - } else { - existingRole = await this.roleService.createRole({ - input: role, - workspaceId, - ownerFlatApplication, - }); - } - - await this.syncApplicationRolePermissions({ - role, - workspaceId, - roleId: existingRole.id, - }); - - if ( - existingRole.universalIdentifier === - manifest.application.defaultRoleUniversalIdentifier - ) { - defaultRoleId = existingRole.id; - } - } - - if (isDefined(defaultRoleId)) { - await this.applicationService.update(ownerFlatApplication.id, { - defaultRoleId: defaultRoleId, - }); - } - } - - private async syncApplicationRolePermissions({ - role, - workspaceId, - roleId, - }: { - role: RoleManifest; - workspaceId: string; - roleId: string; - }) { - if ( - (role.objectPermissions ?? []).length > 0 || - (role.fieldPermissions ?? []).length > 0 - ) { - const { flatObjectMetadataMaps, flatFieldMetadataMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], - }, - ); - - const formattedObjectPermissions = role.objectPermissions - ?.map((perm) => { - const flatObjectMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: perm.objectUniversalIdentifier, - }); - - if (!isDefined(flatObjectMetadata)) { - throw new ApplicationException( - `Failed to find object with universalIdentifier ${perm.objectUniversalIdentifier}`, - ApplicationExceptionCode.OBJECT_NOT_FOUND, - ); - } - - return { - ...perm, - objectMetadataId: flatObjectMetadata.id, - }; - }) - .filter((perm): perm is typeof perm & { objectMetadataId: string } => - isDefined(perm.objectMetadataId), - ); - - if (isDefined(formattedObjectPermissions)) { - await this.objectPermissionService.upsertObjectPermissions({ - workspaceId, - input: { - roleId, - objectPermissions: formattedObjectPermissions, - }, - }); - } - - const formattedFieldPermissions = role?.fieldPermissions - ?.map((perm) => { - const flatObjectMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: perm.objectUniversalIdentifier, - }); - - if (!isDefined(flatObjectMetadata)) { - throw new ApplicationException( - `Failed to find object with universalIdentifier ${perm.objectUniversalIdentifier}`, - ApplicationExceptionCode.OBJECT_NOT_FOUND, - ); - } - - const flatFieldMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatFieldMetadataMaps, - universalIdentifier: perm.fieldUniversalIdentifier, - }); - - if (!isDefined(flatFieldMetadata)) { - throw new ApplicationException( - `Failed to find field with universalIdentifier ${perm.fieldUniversalIdentifier}`, - ApplicationExceptionCode.FIELD_NOT_FOUND, - ); - } - - return { - ...perm, - objectMetadataId: flatObjectMetadata.id, - fieldMetadataId: flatFieldMetadata.id, - }; - }) - .filter( - ( - perm, - ): perm is typeof perm & { - objectMetadataId: string; - fieldMetadataId: string; - } => - isDefined(perm.objectMetadataId) && isDefined(perm.fieldMetadataId), - ); - - if (isDefined(formattedFieldPermissions)) { - await this.fieldPermissionService.upsertFieldPermissions({ - workspaceId, - input: { - roleId, - fieldPermissions: formattedFieldPermissions, - }, - }); - } - } - - if (isDefined(role?.permissionFlags) && role.permissionFlags.length > 0) { - await this.permissionService.upsertPermissionFlags({ - workspaceId, - input: { - roleId, - permissionFlagKeys: role.permissionFlags, - }, - }); - } - } - - private isRelationFieldManifest( - field: ObjectFieldManifest, - ): field is RelationFieldManifest { - return this.isFieldTypeRelation(field.type); - } - - private isFieldTypeRelation(type: FieldMetadataType): boolean { - return type === FieldMetadataType.RELATION; - } - - private async syncObjectFieldsWithoutRelations({ - objectId, - fieldsToSync, - workspaceId, - ownerFlatApplication, - }: { - objectId: string; - workspaceId: string; - ownerFlatApplication: FlatApplication; - fieldsToSync: ObjectFieldManifest[]; - }) { - const fieldsWithoutRelation = fieldsToSync.filter( - (field): field is FieldManifest => !this.isRelationFieldManifest(field), - ); - - if (fieldsWithoutRelation.length === 0) { - return; - } - - const { flatFieldMetadataMaps: existingFlatFieldMetadataMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatFieldMetadataMaps'], - }, - ); - - const existingFields = Object.values( - existingFlatFieldMetadataMaps.byUniversalIdentifier, - ).filter( - (field) => - isDefined(field) && - field.objectMetadataId === objectId && - !this.isFieldTypeRelation(field.type), - ) as FlatFieldMetadata[]; - - const fieldsToSyncUniversalIds = fieldsWithoutRelation.map( - (field) => field.universalIdentifier, - ); - - const existingFieldsStandardIds = existingFields.map( - (field) => field.universalIdentifier, - ); - - const fieldsToDelete = existingFields.filter( - (field) => - isDefined(field.universalIdentifier) && - !fieldsToSyncUniversalIds.includes(field.universalIdentifier) && - field.isCustom === true && - field.isSystem === false, - ); - - const fieldsToUpdate = existingFields.filter( - (field) => - isDefined(field.universalIdentifier) && - fieldsToSyncUniversalIds.includes(field.universalIdentifier), - ); - - const fieldsToCreate = fieldsWithoutRelation.filter( - (fieldToSync) => - !existingFieldsStandardIds.includes(fieldToSync.universalIdentifier), - ); - - for (const fieldToDelete of fieldsToDelete) { - await this.fieldMetadataService.updateOneField({ - updateFieldInput: { - id: fieldToDelete.id, - isActive: false, - }, - workspaceId, - ownerFlatApplication, - }); - await this.fieldMetadataService.deleteOneField({ - deleteOneFieldInput: { id: fieldToDelete.id }, - workspaceId, - ownerFlatApplication, - }); - } - - for (const fieldToUpdate of fieldsToUpdate) { - const fieldToSync = fieldsWithoutRelation.find( - (field) => - field.universalIdentifier === fieldToUpdate.universalIdentifier, - ); - - if (!fieldToSync) { - throw new ApplicationException( - `Failed to find field to sync with universalIdentifier ${fieldToUpdate.universalIdentifier}`, - ApplicationExceptionCode.FIELD_NOT_FOUND, - ); - } - - const updateFieldInput = { - id: fieldToUpdate.id, - label: fieldToSync.label, - description: fieldToSync.description ?? undefined, - icon: fieldToSync.icon ?? undefined, - defaultValue: fieldToSync.defaultValue ?? undefined, - options: fieldToSync.options ?? undefined, - settings: fieldToSync.settings ?? undefined, - isNullable: fieldToSync.isNullable ?? true, - }; - - await this.fieldMetadataService.updateOneField({ - updateFieldInput, - workspaceId, - ownerFlatApplication, - }); - } - - for (const fieldToCreate of fieldsToCreate) { - const createFieldInput: CreateFieldInput = { - name: computeMetadataNameFromLabelOrThrow(fieldToCreate.label), - type: fieldToCreate.type, - label: fieldToCreate.label, - description: fieldToCreate.description ?? undefined, - icon: fieldToCreate.icon ?? undefined, - defaultValue: fieldToCreate.defaultValue ?? undefined, - options: fieldToCreate.options ?? undefined, - settings: fieldToCreate.settings ?? undefined, - isNullable: fieldToCreate.isNullable ?? true, - objectMetadataId: objectId, - universalIdentifier: fieldToCreate.universalIdentifier, - applicationId: ownerFlatApplication.id, - isCustom: true, - workspaceId, - }; - - await this.fieldMetadataService.createOneField({ - createFieldInput, - workspaceId, - ownerFlatApplication, - }); - } - } - - private async syncObjectFieldsRelationOnly({ - objectId, - fieldsToSync, - workspaceId, - ownerFlatApplication, - flatObjectMetadataMaps, - }: { - objectId: string; - fieldsToSync: ObjectFieldManifest[]; - workspaceId: string; - ownerFlatApplication: FlatApplication; - flatObjectMetadataMaps: FlatEntityMaps; - }) { - const relationFields = fieldsToSync.filter((field) => - this.isRelationFieldManifest(field), - ); - - const { flatFieldMetadataMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatFieldMetadataMaps'], - }, - ); - - for (const relation of relationFields) { - const existingRelationField = Object.values( - flatFieldMetadataMaps.byUniversalIdentifier, - ).find( - (field) => - isDefined(field) && - field.universalIdentifier === relation.universalIdentifier, - ); - - if (isDefined(existingRelationField)) { - continue; - } - - const targetObjectMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: relation.targetObjectUniversalIdentifier, - }); - - if (!isDefined(targetObjectMetadata)) { - throw new ApplicationException( - `Failed to find target object with universalIdentifier ${relation.targetObjectUniversalIdentifier}`, - ApplicationExceptionCode.OBJECT_NOT_FOUND, - ); - } - - const createFieldInput: CreateFieldInput = { - name: computeMetadataNameFromLabelOrThrow(relation.label), - type: FieldMetadataType.RELATION, - label: relation.label, - description: relation.description ?? undefined, - icon: relation.icon ?? undefined, - objectMetadataId: objectId, - universalIdentifier: relation.universalIdentifier, - applicationId: ownerFlatApplication.id, - isCustom: true, - workspaceId, - relationCreationPayload: { - type: relation.relationType, - targetObjectMetadataId: targetObjectMetadata.id, - targetFieldLabel: relation.targetFieldLabel, - targetFieldIcon: relation.targetFieldIcon ?? 'IconRelationOneToMany', - }, - }; - - await this.fieldMetadataService.createOneField({ - createFieldInput, - workspaceId, - ownerFlatApplication, - }); - } - } - - private async syncObjects({ - objectsToSync, - workspaceId, - ownerFlatApplication, - }: { - objectsToSync: ObjectManifest[]; - workspaceId: string; - ownerFlatApplication: FlatApplication; - }) { - const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps'], - }, - ); - - const applicationObjects = Object.values( - existingFlatObjectMetadataMaps.byUniversalIdentifier, - ).filter( - (obj) => isDefined(obj) && obj.applicationId === ownerFlatApplication.id, - // TODO handle when migrating to trinite usage - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) as any[]; - - const objectsToSyncUniversalIds = objectsToSync.map( - (obj) => obj.universalIdentifier, - ); - - const applicationObjectsStandardIds = applicationObjects.map( - (obj) => obj.universalIdentifier, - ); - - const objectsToDelete = applicationObjects.filter( - (obj) => - isDefined(obj.universalIdentifier) && - !objectsToSyncUniversalIds.includes(obj.universalIdentifier), - ); - - const objectsToUpdate = applicationObjects.filter( - (obj) => - isDefined(obj.universalIdentifier) && - objectsToSyncUniversalIds.includes(obj.universalIdentifier), - ); - - const objectsToCreate = objectsToSync.filter( - (objectToSync) => - !applicationObjectsStandardIds.includes( - objectToSync.universalIdentifier, - ), - ); - - for (const objectToDelete of objectsToDelete) { - await this.objectMetadataService.deleteOneObject({ - deleteObjectInput: { id: objectToDelete.id }, - workspaceId, - isSystemBuild: true, - ownerFlatApplication, - }); - } - - for (const objectToUpdate of objectsToUpdate) { - const objectToSync = objectsToSync.find( - (obj) => obj.universalIdentifier === objectToUpdate.universalIdentifier, - ); - - if (!objectToSync) { - throw new ApplicationException( - `Failed to find object to sync with universalIdentifier ${objectToUpdate.universalIdentifier}`, - ApplicationExceptionCode.OBJECT_NOT_FOUND, - ); - } - - const updateObjectInput = { - id: objectToUpdate.id, - update: { - nameSingular: objectToSync.nameSingular, - namePlural: objectToSync.namePlural, - labelSingular: objectToSync.labelSingular, - labelPlural: objectToSync.labelPlural, - icon: objectToSync.icon || undefined, - description: objectToSync.description || undefined, - }, - }; - - await this.objectMetadataService.updateOneObject({ - updateObjectInput, - workspaceId, - ownerFlatApplication, - }); - - await this.syncObjectFieldsWithoutRelations({ - fieldsToSync: objectToSync.fields, - objectId: objectToUpdate.id, - workspaceId, - ownerFlatApplication, - }); - } - - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - workspaceId, - ); - - for (const objectToCreate of objectsToCreate) { - const createObjectInput = { - nameSingular: objectToCreate.nameSingular, - namePlural: objectToCreate.namePlural, - labelSingular: objectToCreate.labelSingular, - labelPlural: objectToCreate.labelPlural, - icon: objectToCreate.icon || undefined, - description: objectToCreate.description || undefined, - dataSourceId: dataSourceMetadata.id, - universalIdentifier: objectToCreate.universalIdentifier, - applicationId: ownerFlatApplication.id, - }; - - const createdObject = await this.objectMetadataService.createOneObject({ - createObjectInput, - ownerFlatApplication, - workspaceId, - }); - - await this.syncObjectFieldsWithoutRelations({ - fieldsToSync: objectToCreate.fields, - objectId: createdObject.id, - workspaceId, - ownerFlatApplication, - }); - } - } - - private async syncObjectRelations({ - objectsToSync, - workspaceId, - ownerFlatApplication, - }: { - objectsToSync: ObjectManifest[]; - workspaceId: string; - ownerFlatApplication: FlatApplication; - }) { - const { flatObjectMetadataMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps'], - }, - ); - - for (const objectToSync of objectsToSync) { - const sourceObjectMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: objectToSync.universalIdentifier, - }); - - if (!isDefined(sourceObjectMetadata)) { - throw new ApplicationException( - `Failed to find source object with universalIdentifier ${objectToSync.universalIdentifier}`, - ApplicationExceptionCode.OBJECT_NOT_FOUND, - ); - } - - await this.syncObjectFieldsRelationOnly({ - objectId: sourceObjectMetadata.id, - fieldsToSync: objectToSync.fields, - workspaceId, - ownerFlatApplication, - flatObjectMetadataMaps, - }); - } - } - - private async syncFieldsOrThrow({ - fieldsToSync, - workspaceId, - ownerFlatApplication, - }: { - fieldsToSync: FieldManifest[]; - workspaceId: string; - ownerFlatApplication: FlatApplication; - }) { - const { flatObjectMetadataMaps, flatFieldMetadataMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], - }, - ); - - for (const fieldToSync of fieldsToSync) { - const targetObjectMetadata = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatObjectMetadataMaps, - universalIdentifier: fieldToSync.objectUniversalIdentifier, - }); - - if (!isDefined(targetObjectMetadata)) { - throw new ApplicationException( - 'Object extension must specify either nameSingular or universalIdentifier in targetObject', - ApplicationExceptionCode.INVALID_INPUT, - ); - } - - if (!this.isFieldTypeRelation(fieldToSync.type)) { - const existingField = findFlatEntityByUniversalIdentifier({ - flatEntityMaps: flatFieldMetadataMaps, - universalIdentifier: fieldToSync.universalIdentifier, - }); - - if (isDefined(existingField)) { - await this.fieldMetadataService.updateOneField({ - updateFieldInput: { - id: existingField.id, - label: fieldToSync.label, - description: fieldToSync.description ?? undefined, - icon: fieldToSync.icon ?? undefined, - defaultValue: fieldToSync.defaultValue ?? undefined, - options: fieldToSync.options ?? undefined, - settings: fieldToSync.settings ?? undefined, - isNullable: fieldToSync.isNullable ?? true, - }, - workspaceId, - ownerFlatApplication, - }); - } else { - const createFieldInput: CreateFieldInput = { - name: computeMetadataNameFromLabelOrThrow(fieldToSync.label), - type: fieldToSync.type, - label: fieldToSync.label, - description: fieldToSync.description ?? undefined, - icon: fieldToSync.icon ?? undefined, - defaultValue: fieldToSync.defaultValue ?? undefined, - options: fieldToSync.options ?? undefined, - settings: fieldToSync.settings ?? undefined, - isNullable: fieldToSync.isNullable ?? true, - objectMetadataId: targetObjectMetadata.id, - universalIdentifier: fieldToSync.universalIdentifier, - applicationId: ownerFlatApplication.id, - isCustom: true, - workspaceId, - }; - - await this.fieldMetadataService.createOneField({ - createFieldInput, - workspaceId, - ownerFlatApplication, - }); - } - } - - await this.syncObjectFieldsRelationOnly({ - objectId: targetObjectMetadata.id, - fieldsToSync: [fieldToSync], - workspaceId, - ownerFlatApplication, - flatObjectMetadataMaps, - }); - } - } - - private async syncLogicFunctions({ - logicFunctionsToSync, - workspaceId, - ownerFlatApplication, - }: { - logicFunctionsToSync: LogicFunctionManifest[]; - workspaceId: string; - ownerFlatApplication: FlatApplication; - }) { - const { flatLogicFunctionMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatLogicFunctionMaps'], - }, - ); - - const applicationLogicFunctions = Object.values( - flatLogicFunctionMaps.byUniversalIdentifier, - ).filter( - (logicFunction) => - isDefined(logicFunction) && - logicFunction.applicationId === ownerFlatApplication.id, - ) as FlatLogicFunction[]; - - const logicFunctionsToSyncUniversalIdentifiers = logicFunctionsToSync.map( - (logicFunction) => logicFunction.universalIdentifier, - ); - - const applicationLogicFunctionsUniversalIdentifiers = - applicationLogicFunctions.map( - (logicFunction) => logicFunction.universalIdentifier, - ); - - const logicFunctionsToDelete = applicationLogicFunctions.filter( - (logicFunction) => - isDefined(logicFunction.universalIdentifier) && - !logicFunctionsToSyncUniversalIdentifiers.includes( - logicFunction.universalIdentifier, - ), - ); - - const logicFunctionsToUpdate = applicationLogicFunctions.filter( - (logicFunction) => - isDefined(logicFunction.universalIdentifier) && - logicFunctionsToSyncUniversalIdentifiers.includes( - logicFunction.universalIdentifier, - ), - ); - - const logicFunctionsToCreate = logicFunctionsToSync.filter( - (logicFunctionToSync) => - !applicationLogicFunctionsUniversalIdentifiers.includes( - logicFunctionToSync.universalIdentifier, - ), - ); - - for (const logicFunctionToDelete of logicFunctionsToDelete) { - await this.logicFunctionMetadataService.destroyOne({ - id: logicFunctionToDelete.id, - workspaceId, - isSystemBuild: true, - ownerFlatApplication, - }); - } - - for (const logicFunctionToUpdate of logicFunctionsToUpdate) { - const logicFunctionToSync = logicFunctionsToSync.find( - (logicFunction) => - logicFunction.universalIdentifier === - logicFunctionToUpdate.universalIdentifier, - ); - - if (!logicFunctionToSync) { - throw new ApplicationException( - `Failed to find logicFunction to sync with universalIdentifier ${logicFunctionToUpdate.universalIdentifier}`, - ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND, - ); - } - - const name = - logicFunctionToSync.name ?? parse(logicFunctionToSync.handlerName).name; - - await this.logicFunctionMetadataService.updateOne({ - id: logicFunctionToUpdate.id, - update: { - name, - timeoutSeconds: logicFunctionToSync.timeoutSeconds, - sourceHandlerPath: logicFunctionToSync.sourceHandlerPath, - builtHandlerPath: logicFunctionToSync.builtHandlerPath, - handlerName: logicFunctionToSync.handlerName, - toolInputSchema: logicFunctionToSync.toolInputSchema, - isTool: logicFunctionToSync.isTool, - checksum: logicFunctionToSync.builtHandlerChecksum, - cronTriggerSettings: logicFunctionToSync.cronTriggerSettings as - | JsonbProperty - | undefined, - databaseEventTriggerSettings: - logicFunctionToSync.databaseEventTriggerSettings as - | JsonbProperty - | undefined, - httpRouteTriggerSettings: - logicFunctionToSync.httpRouteTriggerSettings as - | JsonbProperty - | undefined, - }, - workspaceId, - ownerFlatApplication, - }); - } - - for (const logicFunctionToCreate of logicFunctionsToCreate) { - const name = - logicFunctionToCreate.name ?? - parse(logicFunctionToCreate.handlerName).name; - - await this.logicFunctionMetadataService.createOne({ - input: { - name, - universalIdentifier: logicFunctionToCreate.universalIdentifier, - timeoutSeconds: logicFunctionToCreate.timeoutSeconds, - sourceHandlerPath: logicFunctionToCreate.sourceHandlerPath, - handlerName: logicFunctionToCreate.handlerName, - builtHandlerPath: logicFunctionToCreate.builtHandlerPath, - toolInputSchema: logicFunctionToCreate.toolInputSchema, - isTool: logicFunctionToCreate.isTool, - checksum: logicFunctionToCreate.builtHandlerChecksum, - id: v4(), - cronTriggerSettings: logicFunctionToCreate.cronTriggerSettings as - | JsonbProperty - | undefined, - databaseEventTriggerSettings: - logicFunctionToCreate.databaseEventTriggerSettings as - | JsonbProperty - | undefined, - httpRouteTriggerSettings: - logicFunctionToCreate.httpRouteTriggerSettings as - | JsonbProperty - | undefined, - }, - workspaceId, - ownerFlatApplication, - }); - } - } - - private async syncFrontComponents({ - frontComponentsToSync, - workspaceId, - ownerFlatApplication, - }: { - frontComponentsToSync: FrontComponentManifest[]; - workspaceId: string; - ownerFlatApplication: FlatApplication; - }) { - const { flatFrontComponentMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatFrontComponentMaps'], - }, - ); - - const applicationFrontComponents = Object.values( - flatFrontComponentMaps.byUniversalIdentifier, - ).filter( - (frontComponent) => - isDefined(frontComponent) && - frontComponent.applicationId === ownerFlatApplication.id, - ) as FlatFrontComponent[]; - - const frontComponentsToSyncUniversalIdentifiers = frontComponentsToSync.map( - (frontComponent) => frontComponent.universalIdentifier, - ); - - const applicationFrontComponentsUniversalIdentifiers = - applicationFrontComponents.map( - (frontComponent) => frontComponent.universalIdentifier, - ); - - const frontComponentsToDelete = applicationFrontComponents.filter( - (frontComponent) => - isDefined(frontComponent.universalIdentifier) && - !frontComponentsToSyncUniversalIdentifiers.includes( - frontComponent.universalIdentifier, - ), - ); - - const frontComponentsToUpdate = applicationFrontComponents.filter( - (frontComponent) => - isDefined(frontComponent.universalIdentifier) && - frontComponentsToSyncUniversalIdentifiers.includes( - frontComponent.universalIdentifier, - ), - ); - - const frontComponentsToCreate = frontComponentsToSync.filter( - (frontComponentToSync) => - !applicationFrontComponentsUniversalIdentifiers.includes( - frontComponentToSync.universalIdentifier, - ), - ); - - for (const frontComponentToDelete of frontComponentsToDelete) { - await this.frontComponentService.destroyOne({ - id: frontComponentToDelete.id, - workspaceId, - isSystemBuild: true, - ownerFlatApplication, - }); - } - - for (const frontComponentToUpdate of frontComponentsToUpdate) { - const frontComponentToSync = frontComponentsToSync.find( - (frontComponent) => - frontComponent.universalIdentifier === - frontComponentToUpdate.universalIdentifier, - ); - - if (!frontComponentToSync) { - throw new ApplicationException( - `Failed to find frontComponent to sync with universalIdentifier ${frontComponentToUpdate.universalIdentifier}`, - ApplicationExceptionCode.FRONT_COMPONENT_NOT_FOUND, - ); - } - - await this.frontComponentService.updateOne({ - id: frontComponentToUpdate.id, - update: frontComponentToSync, - workspaceId, - ownerFlatApplication, - }); - } - - for (const frontComponentToCreate of frontComponentsToCreate) { - await this.frontComponentService.createOne({ - input: frontComponentToCreate, - workspaceId, - ownerFlatApplication, - }); - } - } - public async uninstallApplication({ workspaceId, applicationUniversalIdentifier, @@ -1101,27 +130,6 @@ export class ApplicationSyncService { workspaceId: string; applicationUniversalIdentifier: string; }) { - const { - flatObjectMetadataMaps: existingFlatObjectMetadataMaps, - flatIndexMaps: existingFlatIndexMetadataMaps, - flatFieldMetadataMaps: existingFlatFieldMetadataMaps, - flatFrontComponentMaps: existingFlatFrontComponentMaps, - flatLogicFunctionMaps: existingFlatLogicFunctionMaps, - flatRoleMaps: existingFlatRoleMaps, - } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: [ - 'flatObjectMetadataMaps', - 'flatIndexMaps', - 'flatFieldMetadataMaps', - 'flatFrontComponentMaps', - 'flatLogicFunctionMaps', - 'flatRoleMaps', - ], - }, - ); - const application = await this.applicationService.findByUniversalIdentifier( { universalIdentifier: applicationUniversalIdentifier, workspaceId }, ); @@ -1140,80 +148,44 @@ export class ApplicationSyncService { ); } - const flatObjectMetadataMapsByApplicationId = - findFlatEntitiesByApplicationId({ - flatEntityMaps: existingFlatObjectMetadataMaps, - applicationId: application.id, - }); + const flatEntityMapsCacheKeys = APPLICATION_MANIFEST_METADATA_NAMES.map( + getMetadataFlatEntityMapsKey, + ); - const flatIndexMetadataMapsByApplicationId = - findFlatEntitiesByApplicationId({ - flatEntityMaps: existingFlatIndexMetadataMaps, - applicationId: application.id, - }); + const cacheResult = await this.workspaceCacheService.getOrRecompute( + workspaceId, + [...flatEntityMapsCacheKeys, 'featureFlagsMap'], + ); - const flatFieldMetadataMapsByApplicationId = - findFlatEntitiesByApplicationId({ - flatEntityMaps: existingFlatFieldMetadataMaps, - applicationId: application.id, - }); + const { featureFlagsMap, ...fromAllFlatEntityMaps } = cacheResult; - const flatFrontComponentMapsByApplicationId = - findFlatEntitiesByApplicationId({ - flatEntityMaps: existingFlatFrontComponentMaps, - applicationId: application.id, - }); - - const flatLogicFunctionMapsByApplicationId = - findFlatEntitiesByApplicationId({ - flatEntityMaps: existingFlatLogicFunctionMaps, - applicationId: application.id, - }); - - const flatRoleMapsByApplicationId = findFlatEntitiesByApplicationId({ - flatEntityMaps: existingFlatRoleMaps, + const fromToAllFlatEntityMaps = getSubApplicationFromToAllFlatEntityMaps({ applicationId: application.id, + fromAllFlatEntityMaps, + toAllUniversalFlatEntityMaps: + getEmptyApplicationManifestAllUniversalFlatEntityMaps(), }); - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - allFlatEntityOperationByMetadataName: { - objectMetadata: { - flatEntityToCreate: [], - flatEntityToDelete: flatObjectMetadataMapsByApplicationId, - flatEntityToUpdate: [], - }, - index: { - flatEntityToCreate: [], - flatEntityToDelete: flatIndexMetadataMapsByApplicationId, - flatEntityToUpdate: [], - }, - fieldMetadata: { - flatEntityToCreate: [], - flatEntityToDelete: flatFieldMetadataMapsByApplicationId, - flatEntityToUpdate: [], - }, - frontComponent: { - flatEntityToCreate: [], - flatEntityToDelete: flatFrontComponentMapsByApplicationId, - flatEntityToUpdate: [], - }, - logicFunction: { - flatEntityToCreate: [], - flatEntityToDelete: flatLogicFunctionMapsByApplicationId, - flatEntityToUpdate: [], - }, - role: { - flatEntityToCreate: [], - flatEntityToDelete: flatRoleMapsByApplicationId, - flatEntityToUpdate: [], + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo( + { + buildOptions: { + isSystemBuild: true, + inferDeletionFromMissingEntities: true, }, + fromToAllFlatEntityMaps, + workspaceId, + additionalCacheDataMaps: { featureFlagsMap }, + applicationUniversalIdentifier, }, - workspaceId, - isSystemBuild: true, - applicationUniversalIdentifier, - }, - ); + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Validation errors occurred while uninstalling application', + ); + } await this.applicationService.delete( applicationUniversalIdentifier, diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts new file mode 100644 index 0000000000..3a6fb74f7e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util.ts @@ -0,0 +1,127 @@ +import { type Manifest } from 'twenty-shared/application'; + +import { type ApplicationManifestMetadataName } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; +import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util'; +import { fromFrontComponentManifestToUniversalFlatFrontComponent } from 'src/engine/core-modules/application/utils/from-front-component-manifest-to-universal-flat-front-component.util'; +import { fromLogicFunctionManifestToUniversalFlatLogicFunction } from 'src/engine/core-modules/application/utils/from-logic-function-manifest-to-universal-flat-logic-function.util'; +import { fromObjectManifestToUniversalFlatObjectMetadata } from 'src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util'; +import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/utils/from-role-manifest-to-universal-flat-role.util'; +import { getEmptyApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key'; +import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type'; +import { addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util'; + +export type ApplicationManifestAllUniversalFlatEntityMaps = Pick< + AllUniversalFlatEntityMaps, + MetadataToFlatEntityMapsKey +>; + +export type ApplicationManifestAllFlatEntityMaps = Pick< + AllFlatEntityMaps, + MetadataToFlatEntityMapsKey +>; + +export const computeApplicationManifestAllUniversalFlatEntityMaps = ({ + manifest, + applicationUniversalIdentifier, + now, +}: { + manifest: Manifest; + applicationUniversalIdentifier: string; + now: string; +}): ApplicationManifestAllUniversalFlatEntityMaps => { + const allUniversalFlatEntityMaps = + getEmptyApplicationManifestAllUniversalFlatEntityMaps(); + + for (const objectManifest of manifest.objects) { + addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( + { + metadataName: 'objectMetadata', + universalFlatEntity: fromObjectManifestToUniversalFlatObjectMetadata({ + objectManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, + }, + ); + + for (const fieldManifest of objectManifest.fields) { + addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( + { + metadataName: 'fieldMetadata', + universalFlatEntity: fromFieldManifestToUniversalFlatFieldMetadata({ + fieldManifest: { + ...fieldManifest, + objectUniversalIdentifier: objectManifest.universalIdentifier, + }, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, + }, + ); + } + } + + for (const fieldManifest of manifest.fields) { + addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( + { + metadataName: 'fieldMetadata', + universalFlatEntity: fromFieldManifestToUniversalFlatFieldMetadata({ + fieldManifest: fieldManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, + }, + ); + } + + for (const logicFunctionManifest of manifest.logicFunctions) { + addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( + { + metadataName: 'logicFunction', + universalFlatEntity: + fromLogicFunctionManifestToUniversalFlatLogicFunction({ + logicFunctionManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, + }, + ); + } + + for (const frontComponentManifest of manifest.frontComponents) { + addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( + { + metadataName: 'frontComponent', + universalFlatEntity: + fromFrontComponentManifestToUniversalFlatFrontComponent({ + frontComponentManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, + }, + ); + } + + for (const roleManifest of manifest.roles) { + addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow( + { + metadataName: 'role', + universalFlatEntity: fromRoleManifestToUniversalFlatRole({ + roleManifest, + applicationUniversalIdentifier, + now, + }), + universalFlatEntityAndRelatedMapsToMutate: allUniversalFlatEntityMaps, + }, + ); + } + + return allUniversalFlatEntityMaps; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts new file mode 100644 index 0000000000..6342e5f3eb --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-field-manifest-to-universal-flat-field-metadata.util.ts @@ -0,0 +1,99 @@ +import { + type FieldManifest, + type RelationFieldManifest, +} from 'twenty-shared/application'; +import { type FieldMetadataType } from 'twenty-shared/types'; + +import { + ApplicationException, + ApplicationExceptionCode, +} from 'src/engine/core-modules/application/application.exception'; +import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value'; +import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; +import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; + +const isRelationFieldManifest = ( + fieldManifest: FieldManifest, +): fieldManifest is RelationFieldManifest => + isMorphOrRelationFieldMetadataType(fieldManifest.type); + +const getRelationTargetUniversalIdentifiers = ( + fieldManifest: FieldManifest, +): { + relationTargetFieldMetadataUniversalIdentifier: string | null; + relationTargetObjectMetadataUniversalIdentifier: string | null; +} => { + if (!isRelationFieldManifest(fieldManifest)) { + return { + relationTargetFieldMetadataUniversalIdentifier: null, + relationTargetObjectMetadataUniversalIdentifier: null, + }; + } + + if ( + !fieldManifest.relationTargetFieldMetadataUniversalIdentifier || + !fieldManifest.relationTargetObjectMetadataUniversalIdentifier + ) { + throw new ApplicationException( + `Field "${fieldManifest.name}" is of type ${fieldManifest.type} but is missing relationTargetFieldMetadataUniversalIdentifier or relationTargetObjectMetadataUniversalIdentifier`, + ApplicationExceptionCode.INVALID_INPUT, + ); + } + + return { + relationTargetFieldMetadataUniversalIdentifier: + fieldManifest.relationTargetFieldMetadataUniversalIdentifier, + relationTargetObjectMetadataUniversalIdentifier: + fieldManifest.relationTargetObjectMetadataUniversalIdentifier, + }; +}; + +export const fromFieldManifestToUniversalFlatFieldMetadata = ({ + fieldManifest, + applicationUniversalIdentifier, + now, +}: { + fieldManifest: FieldManifest & { + objectUniversalIdentifier: string; + }; + applicationUniversalIdentifier: string; + now: string; +}): UniversalFlatFieldMetadata => { + const { + relationTargetFieldMetadataUniversalIdentifier, + relationTargetObjectMetadataUniversalIdentifier, + } = getRelationTargetUniversalIdentifiers(fieldManifest); + + return { + universalIdentifier: fieldManifest.universalIdentifier, + applicationUniversalIdentifier, + type: fieldManifest.type, + name: fieldManifest.name, + label: fieldManifest.label, + description: fieldManifest.description ?? null, + icon: fieldManifest.icon ?? null, + standardOverrides: null, + options: fieldManifest.options ?? null, + defaultValue: + fieldManifest.defaultValue ?? generateDefaultValue(fieldManifest.type), + universalSettings: fieldManifest.universalSettings ?? null, + isCustom: true, + isActive: true, + isSystem: false, + isUIReadOnly: false, + isNullable: fieldManifest.isNullable ?? true, + isUnique: false, + isLabelSyncedWithName: false, + morphId: null, + objectMetadataUniversalIdentifier: fieldManifest.objectUniversalIdentifier, + relationTargetFieldMetadataUniversalIdentifier, + relationTargetObjectMetadataUniversalIdentifier, + viewFieldUniversalIdentifiers: [], + viewFilterUniversalIdentifiers: [], + kanbanAggregateOperationViewUniversalIdentifiers: [], + calendarViewUniversalIdentifiers: [], + mainGroupByFieldMetadataViewUniversalIdentifiers: [], + createdAt: now, + updatedAt: now, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-front-component-manifest-to-universal-flat-front-component.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-front-component-manifest-to-universal-flat-front-component.util.ts new file mode 100644 index 0000000000..10057415dd --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-front-component-manifest-to-universal-flat-front-component.util.ts @@ -0,0 +1,26 @@ +import { type FrontComponentManifest } from 'twenty-shared/application'; + +import { type UniversalFlatFrontComponent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-front-component.type'; + +export const fromFrontComponentManifestToUniversalFlatFrontComponent = ({ + frontComponentManifest, + applicationUniversalIdentifier, + now, +}: { + frontComponentManifest: FrontComponentManifest; + applicationUniversalIdentifier: string; + now: string; +}): UniversalFlatFrontComponent => { + return { + universalIdentifier: frontComponentManifest.universalIdentifier, + applicationUniversalIdentifier, + name: frontComponentManifest.name ?? frontComponentManifest.componentName, + description: frontComponentManifest.description ?? null, + sourceComponentPath: frontComponentManifest.sourceComponentPath, + builtComponentPath: frontComponentManifest.builtComponentPath, + componentName: frontComponentManifest.componentName, + builtComponentChecksum: frontComponentManifest.builtComponentChecksum, + createdAt: now, + updatedAt: now, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-logic-function-manifest-to-universal-flat-logic-function.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-logic-function-manifest-to-universal-flat-logic-function.util.ts new file mode 100644 index 0000000000..1309246477 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-logic-function-manifest-to-universal-flat-logic-function.util.ts @@ -0,0 +1,42 @@ +import { parse } from 'path'; + +import { type LogicFunctionManifest } from 'twenty-shared/application'; + +import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; +import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-logic-function.type'; + +export const fromLogicFunctionManifestToUniversalFlatLogicFunction = ({ + logicFunctionManifest, + applicationUniversalIdentifier, + now, +}: { + logicFunctionManifest: LogicFunctionManifest; + applicationUniversalIdentifier: string; + now: string; +}): UniversalFlatLogicFunction => { + const name = + logicFunctionManifest.name ?? parse(logicFunctionManifest.handlerName).name; + + return { + universalIdentifier: logicFunctionManifest.universalIdentifier, + applicationUniversalIdentifier, + name, + description: logicFunctionManifest.description ?? null, + runtime: LogicFunctionRuntime.NODE22, + timeoutSeconds: logicFunctionManifest.timeoutSeconds ?? 300, + sourceHandlerPath: logicFunctionManifest.sourceHandlerPath, + builtHandlerPath: logicFunctionManifest.builtHandlerPath, + handlerName: logicFunctionManifest.handlerName, + checksum: logicFunctionManifest.builtHandlerChecksum, + toolInputSchema: logicFunctionManifest.toolInputSchema ?? null, + isTool: logicFunctionManifest.isTool ?? false, + cronTriggerSettings: logicFunctionManifest.cronTriggerSettings ?? null, + databaseEventTriggerSettings: + logicFunctionManifest.databaseEventTriggerSettings ?? null, + httpRouteTriggerSettings: + logicFunctionManifest.httpRouteTriggerSettings ?? null, + createdAt: now, + updatedAt: now, + deletedAt: null, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts new file mode 100644 index 0000000000..ee1dde3f4d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-object-manifest-to-universal-flat-object-metadata.util.ts @@ -0,0 +1,43 @@ +import { type ObjectManifest } from 'twenty-shared/application'; + +import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; + +export const fromObjectManifestToUniversalFlatObjectMetadata = ({ + objectManifest, + applicationUniversalIdentifier, + now, +}: { + objectManifest: ObjectManifest; + applicationUniversalIdentifier: string; + now: string; +}): UniversalFlatObjectMetadata => { + return { + universalIdentifier: objectManifest.universalIdentifier, + applicationUniversalIdentifier, + nameSingular: objectManifest.nameSingular, + namePlural: objectManifest.namePlural, + labelSingular: objectManifest.labelSingular, + labelPlural: objectManifest.labelPlural, + description: objectManifest.description ?? null, + icon: objectManifest.icon ?? null, + standardOverrides: null, + targetTableName: 'DEPRECATED', + isCustom: true, + isRemote: false, + isActive: true, + isSystem: false, + isUIReadOnly: false, + isAuditLogged: true, + isSearchable: false, + duplicateCriteria: null, + shortcut: null, + isLabelSyncedWithName: true, + fieldUniversalIdentifiers: [], + indexMetadataUniversalIdentifiers: [], + viewUniversalIdentifiers: [], + labelIdentifierFieldMetadataUniversalIdentifier: null, + imageIdentifierFieldMetadataUniversalIdentifier: null, + createdAt: now, + updatedAt: now, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/from-role-manifest-to-universal-flat-role.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/from-role-manifest-to-universal-flat-role.util.ts new file mode 100644 index 0000000000..f1105b8a7e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/from-role-manifest-to-universal-flat-role.util.ts @@ -0,0 +1,38 @@ +import { type RoleManifest } from 'twenty-shared/application'; + +import { type UniversalFlatRole } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type'; + +export const fromRoleManifestToUniversalFlatRole = ({ + roleManifest, + applicationUniversalIdentifier, + now, +}: { + roleManifest: RoleManifest; + applicationUniversalIdentifier: string; + now: string; +}): UniversalFlatRole => { + return { + universalIdentifier: roleManifest.universalIdentifier, + applicationUniversalIdentifier, + label: roleManifest.label, + description: roleManifest.description ?? null, + icon: roleManifest.icon ?? null, + canUpdateAllSettings: roleManifest.canUpdateAllSettings ?? false, + canAccessAllTools: roleManifest.canAccessAllTools ?? false, + canReadAllObjectRecords: roleManifest.canReadAllObjectRecords ?? false, + canUpdateAllObjectRecords: roleManifest.canUpdateAllObjectRecords ?? false, + canSoftDeleteAllObjectRecords: + roleManifest.canSoftDeleteAllObjectRecords ?? false, + canDestroyAllObjectRecords: + roleManifest.canDestroyAllObjectRecords ?? false, + isEditable: true, + canBeAssignedToUsers: roleManifest.canBeAssignedToUsers ?? true, + canBeAssignedToAgents: roleManifest.canBeAssignedToAgents ?? true, + canBeAssignedToApiKeys: roleManifest.canBeAssignedToApiKeys ?? true, + roleTargetUniversalIdentifiers: [], + rowLevelPermissionPredicateUniversalIdentifiers: [], + rowLevelPermissionPredicateGroupUniversalIdentifiers: [], + createdAt: now, + updatedAt: now, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util.ts new file mode 100644 index 0000000000..12dde95c7d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/get-empty-application-manifest-all-universal-flat-entity-maps.util.ts @@ -0,0 +1,16 @@ +import { APPLICATION_MANIFEST_METADATA_NAMES } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; +import { type ApplicationManifestAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; + +export const getEmptyApplicationManifestAllUniversalFlatEntityMaps = + (): ApplicationManifestAllUniversalFlatEntityMaps => { + const result = {} as ApplicationManifestAllUniversalFlatEntityMaps; + + for (const metadataName of APPLICATION_MANIFEST_METADATA_NAMES) { + const key = getMetadataFlatEntityMapsKey(metadataName); + + result[key] = { byUniversalIdentifier: {} }; + } + + return result; + }; diff --git a/packages/twenty-server/src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util.ts b/packages/twenty-server/src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util.ts new file mode 100644 index 0000000000..5eeb479293 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/utils/get-sub-application-from-to-all-flat-entity-maps.util.ts @@ -0,0 +1,51 @@ +import { type FromTo } from 'twenty-shared/types'; + +import { APPLICATION_MANIFEST_METADATA_NAMES } from 'src/engine/core-modules/application/constants/application-manifest-metadata-names.constant'; +import { + type ApplicationManifestAllFlatEntityMaps, + type ApplicationManifestAllUniversalFlatEntityMaps, +} from 'src/engine/core-modules/application/utils/compute-application-manifest-all-universal-flat-entity-maps.util'; +import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +import { getSubFlatEntityMapsByApplicationIdOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-by-application-id-or-throw.util'; +import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type'; + +export type FromToApplicationManifestAllUniversalFlatEntityMaps = { + [P in keyof ApplicationManifestAllFlatEntityMaps]?: FromTo< + AllUniversalFlatEntityMaps[P] + >; +}; + +export const getSubApplicationFromToAllFlatEntityMaps = ({ + applicationId, + fromAllFlatEntityMaps, + toAllUniversalFlatEntityMaps, +}: { + applicationId: string; + fromAllFlatEntityMaps: ApplicationManifestAllFlatEntityMaps; + toAllUniversalFlatEntityMaps: ApplicationManifestAllUniversalFlatEntityMaps; +}): FromToApplicationManifestAllUniversalFlatEntityMaps => { + const fromToAllFlatEntityMaps: FromToApplicationManifestAllUniversalFlatEntityMaps = + {}; + + for (const metadataName of APPLICATION_MANIFEST_METADATA_NAMES) { + const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName); + const fromFlatEntityMaps = fromAllFlatEntityMaps[flatEntityMapsKey]; + const toFlatEntityMaps = toAllUniversalFlatEntityMaps[flatEntityMapsKey]; + + const fromTo = { + from: getSubFlatEntityMapsByApplicationIdOrThrow< + MetadataFlatEntity + >({ + applicationId, + flatEntityMaps: fromFlatEntityMaps, + }), + to: toFlatEntityMaps, + }; + + // @ts-expect-error Metadata flat entity maps cache key and metadataName colliding + fromToAllFlatEntityMaps[flatEntityMapsKey] = fromTo; + } + + return fromToAllFlatEntityMaps; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type.ts index 3da8d53c33..28e1ebf7d7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type.ts @@ -1,7 +1,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; +import { type RemoveSuffix } from 'twenty-shared/types'; import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type'; -import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; export type AddSuffixToEntityManyToOneProperties< TEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type.ts index 1e1046551f..c33cfd44bb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type.ts @@ -1,6 +1,7 @@ +import { type RemoveSuffix } from 'twenty-shared/types'; + import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type'; import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; -import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; export type AddSuffixToEntityOneToManyProperties< TEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts index e567b3ec88..3dcaf7eaf2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts @@ -1,6 +1,7 @@ import { t } from '@lingui/core/macro'; import { type AllMetadataName } from 'twenty-shared/metadata'; import { isDefined } from 'twenty-shared/utils'; +import { type RemoveSuffix } from 'twenty-shared/types'; import { ALL_METADATA_RELATIONS, @@ -16,7 +17,6 @@ import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/typ import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type'; import { type MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key'; import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; type ManyToOneConfig = (typeof ALL_METADATA_RELATIONS)[T]['manyToOne']; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service.ts index 9ca3c8d616..98a61c123c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service.ts @@ -17,7 +17,7 @@ import { TWENTY_STANDARD_ALL_METADATA_NAME } from 'src/engine/workspace-manager/ import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant'; 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 { FromToAllFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; +import { FromToAllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; @Injectable() @@ -87,7 +87,7 @@ export class TwentyStandardApplicationService { shouldIncludeRecordPageLayouts, }); - const fromToAllFlatEntityMaps: FromToAllFlatEntityMaps = {}; + const fromToAllFlatEntityMaps: FromToAllUniversalFlatEntityMaps = {}; for (const metadataName of TWENTY_STANDARD_ALL_METADATA_NAME) { const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts index f115b1cf49..a278e91166 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts @@ -23,7 +23,7 @@ import { import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service'; import { WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-builder-additional-cache-data-maps.type'; import { - FromToAllFlatEntityMaps, + FromToAllUniversalFlatEntityMaps, WorkspaceMigrationOrchestratorBuildArgs, WorkspaceMigrationOrchestratorFailedResult, } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type'; @@ -133,7 +133,7 @@ export class WorkspaceMigrationValidateBuildAndRunService { workspaceId, applicationUniversalIdentifier, }: ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs): Promise<{ - fromToAllFlatEntityMaps: FromToAllFlatEntityMaps; + fromToAllFlatEntityMaps: FromToAllUniversalFlatEntityMaps; inferDeletionFromMissingEntities: InferDeletionFromMissingEntities; dependencyAllFlatEntityMaps: Partial; additionalCacheDataMaps: WorkspaceMigrationBuilderAdditionalCacheDataMaps; @@ -149,7 +149,7 @@ export class WorkspaceMigrationValidateBuildAndRunService { applicationUniversalIdentifier, }); - const fromToAllFlatEntityMaps: FromToAllFlatEntityMaps = {}; + const fromToAllFlatEntityMaps: FromToAllUniversalFlatEntityMaps = {}; const inferDeletionFromMissingEntities: InferDeletionFromMissingEntities = {}; const idByUniversalIdentifierByMetadataName: IdByUniversalIdentifierByMetadataName = diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type.ts index e05ed03714..8aacc5c4d3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type.ts @@ -11,7 +11,7 @@ import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/wo import { type WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration'; import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type'; -export type FromToAllFlatEntityMaps = { +export type FromToAllUniversalFlatEntityMaps = { [P in keyof AllUniversalFlatEntityMaps]?: FromTo< AllUniversalFlatEntityMaps[P] >; @@ -20,7 +20,7 @@ export type FromToAllFlatEntityMaps = { export type WorkspaceMigrationOrchestratorBuildArgs = { workspaceId: string; buildOptions: WorkspaceMigrationBuilderOptions; - fromToAllFlatEntityMaps: FromToAllFlatEntityMaps; + fromToAllFlatEntityMaps: FromToAllUniversalFlatEntityMaps; dependencyAllFlatEntityMaps?: Partial; additionalCacheDataMaps: WorkspaceMigrationBuilderAdditionalCacheDataMaps; applicationUniversalIdentifier: string; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-record-serialized-relation-properties.test-type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-record-serialized-relation-properties.test-type.ts index 9635458d7d..210452b58b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-record-serialized-relation-properties.test-type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-record-serialized-relation-properties.test-type.ts @@ -1,7 +1,9 @@ import { type Equal, type Expect } from 'twenty-shared/testing'; -import { type SerializedRelation } from 'twenty-shared/types'; +import { + type SerializedRelation, + type FormatRecordSerializedRelationProperties, +} from 'twenty-shared/types'; -import { type FormatRecordSerializedRelationProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type'; import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; type ObjectWithRelation = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts index 0566e36df5..d193d2fab3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts @@ -1,4 +1,5 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; +import { type FormatRecordSerializedRelationProperties } from 'twenty-shared/types'; import { type ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant'; import { type AddSuffixToEntityManyToOneProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type'; @@ -8,7 +9,6 @@ import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modul import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type'; import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; import { type AllJsonbPropertiesWithSerializedPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant'; -import { type FormatRecordSerializedRelationProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type'; export type UniversalSyncableFlatEntity = Omit< SyncableEntity, diff --git a/packages/twenty-shared/src/application/fieldManifestType.ts b/packages/twenty-shared/src/application/fieldManifestType.ts index 896fa36751..63e544ae3d 100644 --- a/packages/twenty-shared/src/application/fieldManifestType.ts +++ b/packages/twenty-shared/src/application/fieldManifestType.ts @@ -1,17 +1,16 @@ -import { - type FieldMetadataType, - type FieldMetadataSettings, - type FieldMetadataOptions, - type FieldMetadataDefaultValue, -} from '@/types'; import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; -import { type RelationOnDeleteAction } from '@/types/RelationOnDeleteAction.type'; -import { type RelationType } from '@/types/RelationType'; +import { + type FieldMetadataDefaultValue, + type FieldMetadataOptions, + type FieldMetadataType, + type FieldMetadataUniversalSettings, + type RelationAndMorphRelationFieldMetadataType, +} from '@/types'; export type RegularFieldManifest< T extends FieldMetadataType = Exclude< FieldMetadataType, - FieldMetadataType.RELATION + RelationAndMorphRelationFieldMetadataType >, > = SyncableEntityOptions & { type: T; @@ -21,23 +20,24 @@ export type RegularFieldManifest< icon?: string; defaultValue?: FieldMetadataDefaultValue; options?: FieldMetadataOptions; - settings?: FieldMetadataSettings; + universalSettings?: FieldMetadataUniversalSettings; isNullable?: boolean; objectUniversalIdentifier: string; }; -export type RelationFieldManifest = - RegularFieldManifest & { - relationType: RelationType; - targetObjectUniversalIdentifier: string; - targetFieldLabel: string; - targetFieldIcon?: string; - onDelete?: RelationOnDeleteAction; - }; +// Both sides of a relation must be declared explicitly in the manifest. +// Relation-specific universal settings (relationType, onDelete, joinColumnName) +// are provided through the `universalSettings` field. +export type RelationFieldManifest< + T extends RelationAndMorphRelationFieldMetadataType = RelationAndMorphRelationFieldMetadataType, +> = RegularFieldManifest & { + relationTargetFieldMetadataUniversalIdentifier: string; + relationTargetObjectMetadataUniversalIdentifier: string; +}; export type FieldManifest< T extends FieldMetadataType = Exclude< FieldMetadataType, - FieldMetadataType.RELATION + RelationAndMorphRelationFieldMetadataType >, > = RegularFieldManifest | RelationFieldManifest; diff --git a/packages/twenty-shared/src/application/index.ts b/packages/twenty-shared/src/application/index.ts index 5e41cdb3da..df772e694f 100644 --- a/packages/twenty-shared/src/application/index.ts +++ b/packages/twenty-shared/src/application/index.ts @@ -36,5 +36,9 @@ export type { export type { Manifest } from './manifestType'; export type { ObjectFieldManifest } from './objectFieldManifest.type'; export type { ObjectManifest } from './objectManifestType'; -export type { RoleManifest } from './roleManifestType'; +export type { + ObjectPermissionManifest, + FieldPermissionManifest, + RoleManifest, +} from './roleManifestType'; export type { SyncableEntityOptions } from './syncableEntityOptionsType'; diff --git a/packages/twenty-shared/src/application/roleManifestType.ts b/packages/twenty-shared/src/application/roleManifestType.ts index ac16863ca5..8c7737216a 100644 --- a/packages/twenty-shared/src/application/roleManifestType.ts +++ b/packages/twenty-shared/src/application/roleManifestType.ts @@ -1,7 +1,7 @@ import { type PermissionFlagType } from '@/constants'; import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType'; -type ObjectPermission = { +export type ObjectPermissionManifest = { objectUniversalIdentifier: string; canReadObjectRecords?: boolean; canUpdateObjectRecords?: boolean; @@ -9,7 +9,7 @@ type ObjectPermission = { canDestroyObjectRecords?: boolean; }; -type FieldPermission = { +export type FieldPermissionManifest = { objectUniversalIdentifier: string; fieldUniversalIdentifier: string; canReadFieldValue?: boolean; @@ -29,7 +29,7 @@ export type RoleManifest = SyncableEntityOptions & { canBeAssignedToUsers?: boolean; canBeAssignedToAgents?: boolean; canBeAssignedToApiKeys?: boolean; - objectPermissions?: ObjectPermission[]; - fieldPermissions?: FieldPermission[]; + objectPermissions?: ObjectPermissionManifest[]; + fieldPermissions?: FieldPermissionManifest[]; permissionFlags?: PermissionFlagType[]; }; diff --git a/packages/twenty-shared/src/types/FieldMetadataUniversalSettings.ts b/packages/twenty-shared/src/types/FieldMetadataUniversalSettings.ts new file mode 100644 index 0000000000..a3246e3cd6 --- /dev/null +++ b/packages/twenty-shared/src/types/FieldMetadataUniversalSettings.ts @@ -0,0 +1,7 @@ +import { type FieldMetadataType } from '@/types/FieldMetadataType'; +import { type FieldMetadataSettings } from '@/types/FieldMetadataSettings'; +import { type FormatRecordSerializedRelationProperties } from '@/types/FormatRecordSerializedRelationProperties.type'; + +export type FieldMetadataUniversalSettings< + T extends FieldMetadataType = FieldMetadataType, +> = FormatRecordSerializedRelationProperties>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type.ts b/packages/twenty-shared/src/types/FormatRecordSerializedRelationProperties.type.ts similarity index 84% rename from packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type.ts rename to packages/twenty-shared/src/types/FormatRecordSerializedRelationProperties.type.ts index 223885c695..6afb0b1420 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type.ts +++ b/packages/twenty-shared/src/types/FormatRecordSerializedRelationProperties.type.ts @@ -1,9 +1,6 @@ -import { - type ExtractSerializedRelationProperties, - type IsSerializedRelation, -} from 'twenty-shared/types'; - -import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; +import { type ExtractSerializedRelationProperties } from '@/types/ExtractSerializedRelationProperties.type'; +import { type IsSerializedRelation } from '@/types/IsSerializedRelation.type'; +import { type RemoveSuffix } from '@/types/RemoveSuffix.type'; // Determines if a property should be transformed to a universal identifier // A property is transformed only if: diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type.ts b/packages/twenty-shared/src/types/RemoveSuffix.type.ts similarity index 100% rename from packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type.ts rename to packages/twenty-shared/src/types/RemoveSuffix.type.ts diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index 9e737ef0c8..fecfb95ea6 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -94,6 +94,7 @@ export type { } from './FieldMetadataSettings'; export { NumberDataType, DateDisplayFormat } from './FieldMetadataSettings'; export { FieldMetadataType } from './FieldMetadataType'; +export type { FieldMetadataUniversalSettings } from './FieldMetadataUniversalSettings'; export type { FieldRatingValue } from './FieldRatingValue'; export type { FileCategory } from './FileCategory'; export { FILE_CATEGORIES } from './FileCategory'; @@ -104,6 +105,7 @@ export type { } from './FilterableFieldType'; export { FILTERABLE_FIELD_TYPES } from './FilterableFieldType'; export { FirstDayOfTheWeek } from './FirstDayOfTheWeek'; +export type { FormatRecordSerializedRelationProperties } from './FormatRecordSerializedRelationProperties.type'; export type { FromTo } from './FromToType'; export { HTTPMethod } from './HttpMethod'; export type { IsEmptyObject } from './IsEmptyObject.type'; @@ -190,6 +192,7 @@ export type { RelationCreationPayload } from './RelationCreationPayload'; export { RelationOnDeleteAction } from './RelationOnDeleteAction.type'; export { RelationType } from './RelationType'; export type { RelationUpdatePayload } from './RelationUpdatePayload'; +export type { RemoveSuffix } from './RemoveSuffix.type'; export type { RestrictedFieldPermissions } from './RestrictedFieldPermissions'; export type { RestrictedFieldsPermissions } from './RestrictedFieldsPermissions'; export type { RowLevelPermissionPredicate } from './RowLevelPermissionPredicate';