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 16:30:01 +00: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;
};