Solo transaction application synchronization service refactor (#17864)

# Introduction
Refactoring the application sync service to be making a single validate
build and run transaction instead of calling all the services n times
thanks to the universal workspace migration refactor that allow doing so

## What's next
Migrating all below entities to by syncableEntities so they can be
universalised too ( right now they're still calling the services n times
and won't be returned in the workspace migration )
-
[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)
This commit is contained in:
Paul Rastoin
2026-02-11 17:30:01 +01:00
committed by GitHub
parent 0ee63a0525
commit b2f7c745f8
27 changed files with 848 additions and 1127 deletions
@@ -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,
],
@@ -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];
@@ -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<WorkspaceCacheService['getOrRecompute']>
>['flatObjectMetadataMaps'];
refreshedFlatFieldMetadataMaps: Awaited<
ReturnType<WorkspaceCacheService['getOrRecompute']>
>['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,
},
});
}
}
}
@@ -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<ApplicationManifestMetadataName>
>;
export type ApplicationManifestAllFlatEntityMaps = Pick<
AllFlatEntityMaps,
MetadataToFlatEntityMapsKey<ApplicationManifestMetadataName>
>;
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;
};
@@ -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<FieldMetadataType>,
): fieldManifest is RelationFieldManifest =>
isMorphOrRelationFieldMetadataType(fieldManifest.type);
const getRelationTargetUniversalIdentifiers = (
fieldManifest: FieldManifest<FieldMetadataType>,
): {
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<FieldMetadataType> & {
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,
};
};
@@ -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,
};
};
@@ -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,
};
};
@@ -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,
};
};
@@ -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,
};
};
@@ -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;
};
@@ -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<typeof metadataName>
>({
applicationId,
flatEntityMaps: fromFlatEntityMaps,
}),
to: toFlatEntityMaps,
};
// @ts-expect-error Metadata flat entity maps cache key and metadataName colliding
fromToAllFlatEntityMaps[flatEntityMapsKey] = fromTo;
}
return fromToAllFlatEntityMaps;
};
@@ -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,
@@ -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,
@@ -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<T extends AllMetadataName> =
(typeof ALL_METADATA_RELATIONS)[T]['manyToOne'];
@@ -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);
@@ -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<AllFlatEntityMaps>;
additionalCacheDataMaps: WorkspaceMigrationBuilderAdditionalCacheDataMaps;
@@ -149,7 +149,7 @@ export class WorkspaceMigrationValidateBuildAndRunService {
applicationUniversalIdentifier,
});
const fromToAllFlatEntityMaps: FromToAllFlatEntityMaps = {};
const fromToAllFlatEntityMaps: FromToAllUniversalFlatEntityMaps = {};
const inferDeletionFromMissingEntities: InferDeletionFromMissingEntities =
{};
const idByUniversalIdentifierByMetadataName: IdByUniversalIdentifierByMetadataName =
@@ -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<AllUniversalFlatEntityMaps>;
additionalCacheDataMaps: WorkspaceMigrationBuilderAdditionalCacheDataMaps;
applicationUniversalIdentifier: string;
@@ -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 = {
@@ -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,
@@ -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<T>;
options?: FieldMetadataOptions<T>;
settings?: FieldMetadataSettings<T>;
universalSettings?: FieldMetadataUniversalSettings<T>;
isNullable?: boolean;
objectUniversalIdentifier: string;
};
export type RelationFieldManifest =
RegularFieldManifest<FieldMetadataType.RELATION> & {
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<T> & {
relationTargetFieldMetadataUniversalIdentifier: string;
relationTargetObjectMetadataUniversalIdentifier: string;
};
export type FieldManifest<
T extends FieldMetadataType = Exclude<
FieldMetadataType,
FieldMetadataType.RELATION
RelationAndMorphRelationFieldMetadataType
>,
> = RegularFieldManifest<T> | RelationFieldManifest;
@@ -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';
@@ -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[];
};
@@ -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<FieldMetadataSettings<T>>;
@@ -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:
@@ -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';