[OBJECT_MANIFEST_BREAKING_CHANGE] Sync returns workspace migration (#17918)

# Introduction
In this PR we start returning a workspace migration post sync so it can
committed and provided within the tarball

## Universal aggregators utils
Created two utils
### deleteUniversalFlatEntityForeignKeyAggregators
Used when building a universal create action, a newly created actions
should not contain any aggregated foreign key so they won't be codegen
in the workspace migration but also they are overriden at uninversal to
flat transpilation anw

### resetUniversalFlatEntityForeignKeyAggregators
Used before validating a new flat entity creation, some validator will
consume the fk aggregator in order to validate integrity, but of
optimstically provided it can result to errors. To avoid caller
responsability we override them here

## create-field-action refactor
Refactored the universal and flat field create action to be following
the base actions in order to ease typing
Also it was tailored to handle unlimited amount of flat field metadata
in the same actions whereas in the reality we were always only sending
at max 2 ( for relation fields )

Note: relation field has to be provided at the same as if not optimistic
would fail to retrieve circular universal identifiers

## ObjectManifest
Now always expect a `labelIdentifierFieldMetadataUniversalIdentifier`

## Integration test
Created an integration test that creates an app, sync a first manifest
and a second implying update workspace migration action generation
This commit is contained in:
Paul Rastoin
2026-02-16 15:00:24 +01:00
committed by GitHub
parent 4b3c58e013
commit 2b7b05de2e
107 changed files with 1662 additions and 566 deletions
@@ -2269,7 +2269,7 @@ export type Mutation = {
startChannelSync: ChannelSyncSuccess;
switchBillingPlan: BillingUpdateOutput;
switchSubscriptionInterval: BillingUpdateOutput;
syncApplication: Scalars['Boolean'];
syncApplication: WorkspaceMigrationDto;
trackAnalytics: Analytics;
uninstallApplication: Scalars['Boolean'];
updateApiKey?: Maybe<ApiKey>;
@@ -5174,6 +5174,12 @@ export enum WorkspaceMigrationActionType {
update = 'update'
}
export type WorkspaceMigrationDto = {
__typename?: 'WorkspaceMigrationDTO';
actions: Scalars['JSON'];
applicationUniversalIdentifier: Scalars['String'];
};
export type WorkspaceMigrationDeleteActionInput = {
metadataName: AllMetadataName;
type: WorkspaceMigrationActionType;
@@ -10,6 +10,8 @@ export default defineObject({
labelPlural: 'First objects',
description: 'First object with duplicate ID',
icon: 'IconBox',
labelIdentifierFieldMetadataUniversalIdentifier:
'first-field-0000-0000-000000000001',
fields: [
{
universalIdentifier: 'first-field-0000-0000-000000000001',
@@ -10,6 +10,8 @@ export default defineObject({
labelPlural: 'Second objects',
description: 'Second object with duplicate ID',
icon: 'IconBox',
labelIdentifierFieldMetadataUniversalIdentifier:
'second-field-0000-0000-000000000001',
fields: [
{
universalIdentifier: 'second-field-0000-0000-000000000001',
@@ -123,6 +123,8 @@ export const EXPECTED_MANIFEST: Manifest = {
},
],
icon: 'IconNote',
labelIdentifierFieldMetadataUniversalIdentifier:
'b0b1b2b3-b4b5-4000-8000-000000000002',
labelPlural: 'Root notes',
labelSingular: 'Root note',
namePlural: 'rootNotes',
@@ -199,6 +201,8 @@ export const EXPECTED_MANIFEST: Manifest = {
},
],
icon: 'IconMail',
labelIdentifierFieldMetadataUniversalIdentifier:
'58a0a314-d7ea-4865-9850-7fb84e72f30b',
labelPlural: 'Post cards',
labelSingular: 'Post card',
namePlural: 'postCards',
@@ -20,6 +20,8 @@ export default defineObject({
labelPlural: 'Post cards',
description: 'A post card object',
icon: 'IconMail',
labelIdentifierFieldMetadataUniversalIdentifier:
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
@@ -8,6 +8,8 @@ export default defineObject({
labelPlural: 'Root notes',
description: 'A simple root-level object',
icon: 'IconNote',
labelIdentifierFieldMetadataUniversalIdentifier:
'b0b1b2b3-b4b5-4000-8000-000000000002',
fields: [
{
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000002',
@@ -22,6 +22,8 @@ export const EXPECTED_MANIFEST: Manifest = {
labelPlural: 'My notes',
description: 'A simple root-level object',
icon: 'IconNote',
labelIdentifierFieldMetadataUniversalIdentifier:
'e1e2e3e4-e5e6-4000-8000-000000000031',
fields: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000031',
@@ -8,6 +8,8 @@ export default defineObject({
labelPlural: 'My notes',
description: 'A simple root-level object',
icon: 'IconNote',
labelIdentifierFieldMetadataUniversalIdentifier:
'e1e2e3e4-e5e6-4000-8000-000000000031',
fields: [
{
universalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000031',
@@ -157,6 +157,7 @@ describe('manifestValidate', () => {
namePlural: 'myObjects',
labelSingular: 'My Object',
labelPlural: 'My Objects',
labelIdentifierFieldMetadataUniversalIdentifier: sharedId,
fields: [
{
universalIdentifier: sharedId,
@@ -10,6 +10,7 @@ import {
type FrontComponentConfig,
type LogicFunctionConfig,
} from '@/sdk';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { glob } from 'fast-glob';
import { readFile } from 'fs-extra';
import { basename, extname, relative } from 'path';
@@ -24,8 +25,8 @@ import {
type ObjectManifest,
type RoleManifest,
} from 'twenty-shared/application';
import { assertUnreachable } from 'twenty-shared/utils';
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
import { assertUnreachable } from 'twenty-shared/utils';
const loadSources = async (appPath: string): Promise<string[]> => {
return await glob(['**/*.ts', '**/*.tsx'], {
@@ -98,11 +99,23 @@ export const buildManifest = async (
break;
}
case ManifestEntityKey.Objects: {
const extract = await extractManifestFromFile<ObjectManifest>({
const extract = await extractManifestFromFile<ObjectConfig>({
appPath,
filePath,
});
objects.push(extract.config);
const { labelIdentifierFieldMetadataUniversalIdentifier, ...rest } =
extract.config;
const objectManifest: ObjectManifest = {
...rest,
labelIdentifierFieldMetadataUniversalIdentifier:
// TODO replace by system id universal identifier once we've refactored it
labelIdentifierFieldMetadataUniversalIdentifier ?? '',
};
objects.push(objectManifest);
errors.push(...extract.errors);
objectsFilePaths.push(relativePath);
break;
@@ -1,9 +1,9 @@
import { type ApplicationConfig } from '@/sdk/application/application-config';
import { type FrontComponentConfig } from '@/sdk/front-component-config';
import { type LogicFunctionConfig } from '@/sdk/logic-functions/logic-function-config';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import {
type FieldManifest,
type ObjectManifest,
type RoleManifest,
} from 'twenty-shared/application';
@@ -15,14 +15,12 @@ export type ValidationResult<T> = {
export type DefinableEntity =
| ApplicationConfig
| ObjectManifest
| ObjectConfig
| FieldManifest
| FrontComponentConfig
| LogicFunctionConfig
| RoleManifest;
export type DefineEntity<C extends DefinableEntity = DefinableEntity> = <
T extends C,
>(
export type DefineEntity<T extends DefinableEntity = DefinableEntity> = (
config: T,
) => ValidationResult<T>;
@@ -34,7 +34,7 @@ describe('defineLogicFunction', () => {
const result = defineLogicFunction(config as any);
expect(result.config.cronTriggerSettings.pattern).toBeDefined();
expect(result.config.cronTriggerSettings?.pattern).toBeDefined();
});
it('should accept databaseEventTriggerSettings', () => {
@@ -49,7 +49,7 @@ describe('defineLogicFunction', () => {
const result = defineLogicFunction(config as any);
expect(result.config.databaseEventTriggerSettings.eventName).toBeDefined();
expect(result.config.databaseEventTriggerSettings?.eventName).toBeDefined();
});
it('should pass through optional fields', () => {
@@ -1,6 +1,6 @@
import { defineObject } from '@/sdk';
import { FieldMetadataType } from 'twenty-shared/types';
import { type ObjectManifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
describe('defineObject', () => {
const validConfig: ObjectManifest = {
@@ -10,6 +10,8 @@ describe('defineObject', () => {
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
icon: 'IconMail',
labelIdentifierFieldMetadataUniversalIdentifier:
'58a0a314-d7ea-4865-9850-7fb84e72f30b',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
@@ -111,17 +113,6 @@ describe('defineObject', () => {
expect(result.config.fields).toEqual([]);
});
it('should accept missing fields', () => {
const config = {
...validConfig,
fields: undefined,
};
const result = defineObject(config as any);
expect(result.config.fields).toBeUndefined();
});
it('should return error when field is missing label', () => {
const config = {
...validConfig,
@@ -239,4 +230,19 @@ describe('defineObject', () => {
expect(result.config.fields[0].options).toHaveLength(2);
});
it('should return error when labelIdentifierFieldMetadataUniversalIdentifier references non-existent field', () => {
const config: ObjectManifest = {
...validConfig,
labelIdentifierFieldMetadataUniversalIdentifier:
'non-existent-field-uuid',
};
const result = defineObject(config);
expect(result.success).toBe(false);
expect(result.errors).toContain(
'labelIdentifierFieldMetadataUniversalIdentifier must reference a field defined in the fields array',
);
});
});
@@ -1,10 +1,10 @@
import { type ObjectManifest } from 'twenty-shared/application';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import { type DefineEntity } from '@/sdk/common/types/define-entity.type';
import { createValidationResult } from '@/sdk/common/utils/create-validation-result';
import { validateFields } from '@/sdk/fields/validate-fields';
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { isDefined } from 'twenty-shared/utils';
export const defineObject: DefineEntity<ObjectManifest> = (config) => {
export const defineObject: DefineEntity<ObjectConfig> = (config) => {
const errors = [];
if (!config.universalIdentifier) {
@@ -31,6 +31,19 @@ export const defineObject: DefineEntity<ObjectManifest> = (config) => {
errors.push(...fieldErrors);
if (
isDefined(config.labelIdentifierFieldMetadataUniversalIdentifier) &&
!config.fields.some(
(field) =>
field.universalIdentifier ===
config.labelIdentifierFieldMetadataUniversalIdentifier,
)
) {
errors.push(
'labelIdentifierFieldMetadataUniversalIdentifier must reference a field defined in the fields array',
);
}
return createValidationResult({
config,
errors,
@@ -0,0 +1,8 @@
import { type ObjectManifest } from 'twenty-shared/application';
export type ObjectConfig = Omit<
ObjectManifest,
'labelIdentifierFieldMetadataUniversalIdentifier'
> & {
labelIdentifierFieldMetadataUniversalIdentifier?: string;
};
@@ -753,7 +753,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new Error(
`Failed to create navigation menu items: ${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
@@ -0,0 +1,12 @@
import { Field, ObjectType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
@ObjectType()
export class WorkspaceMigrationDTO {
@Field(() => String)
applicationUniversalIdentifier: string;
@Field(() => GraphQLJSON)
actions: unknown[];
}
@@ -23,6 +23,7 @@ import { ApplicationInput } from 'src/engine/core-modules/application/dtos/appli
import { CreateApplicationInput } from 'src/engine/core-modules/application/dtos/create-application.input';
import { GenerateApplicationTokenInput } from 'src/engine/core-modules/application/dtos/generate-application-token.input';
import { UploadApplicationFileInput } from 'src/engine/core-modules/application/dtos/uploadApplicationFileInput';
import { WorkspaceMigrationDTO } from 'src/engine/core-modules/application/dtos/workspace-migration.dto';
import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { AuthToken } from 'src/engine/core-modules/auth/dto/auth-token.dto';
@@ -69,18 +70,23 @@ export class ApplicationDevelopmentResolver {
});
}
@Mutation(() => Boolean)
@Mutation(() => WorkspaceMigrationDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async syncApplication(
@Args() { manifest }: ApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
await this.applicationSyncService.synchronizeFromManifest({
workspaceId,
manifest,
});
): Promise<WorkspaceMigrationDTO> {
const workspaceMigration =
await this.applicationSyncService.synchronizeFromManifest({
workspaceId,
manifest,
});
return true;
return {
applicationUniversalIdentifier:
workspaceMigration.applicationUniversalIdentifier,
actions: workspaceMigration.actions,
};
}
@Mutation(() => ApplicationDTO)
@@ -144,10 +144,12 @@ export class ApplicationResolver {
);
await this.workspaceMigrationRunnerService.run({
actions,
workspaceMigration: {
actions,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
});
return true;
@@ -20,6 +20,7 @@ import { PermissionFlagService } from 'src/engine/metadata-modules/permission-fl
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 { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type';
@Injectable()
export class ApplicationManifestMigrationService {
@@ -44,7 +45,7 @@ export class ApplicationManifestMigrationService {
manifest: Manifest;
workspaceId: string;
ownerFlatApplication: FlatApplication;
}) {
}): Promise<WorkspaceMigration> {
const now = new Date().toISOString();
const toAllUniversalFlatEntityMaps =
@@ -88,7 +89,7 @@ export class ApplicationManifestMigrationService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while syncing application manifest metadata',
@@ -104,6 +105,8 @@ export class ApplicationManifestMigrationService {
workspaceId,
ownerFlatApplication,
});
return validateAndBuildResult.workspaceMigration;
}
/**
@@ -23,6 +23,7 @@ import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-e
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 { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@Injectable()
@@ -43,7 +44,7 @@ export class ApplicationSyncService {
manifest,
}: ApplicationInput & {
workspaceId: string;
}) {
}): Promise<WorkspaceMigration> {
const application = await this.syncApplication({
workspaceId,
manifest,
@@ -51,13 +52,16 @@ export class ApplicationSyncService {
const ownerFlatApplication: FlatApplication = application;
await this.applicationManifestMigrationService.syncMetadataFromManifest({
manifest,
workspaceId,
ownerFlatApplication,
});
const workspaceMigration =
await this.applicationManifestMigrationService.syncMetadataFromManifest({
manifest,
workspaceId,
ownerFlatApplication,
});
this.logger.log('✅ Application sync from manifest completed');
return workspaceMigration;
}
private async syncApplication({
@@ -129,7 +133,7 @@ export class ApplicationSyncService {
}: {
workspaceId: string;
applicationUniversalIdentifier: string;
}) {
}): Promise<WorkspaceMigration> {
const application = await this.applicationService.findByUniversalIdentifier(
{ universalIdentifier: applicationUniversalIdentifier, workspaceId },
);
@@ -180,7 +184,7 @@ export class ApplicationSyncService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while uninstalling application',
@@ -191,5 +195,7 @@ export class ApplicationSyncService {
applicationUniversalIdentifier,
workspaceId,
);
return validateAndBuildResult.workspaceMigration;
}
}
@@ -35,7 +35,8 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({
fieldUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier: null,
labelIdentifierFieldMetadataUniversalIdentifier:
objectManifest.labelIdentifierFieldMetadataUniversalIdentifier,
imageIdentifierFieldMetadataUniversalIdentifier: null,
createdAt: now,
updatedAt: now,
@@ -158,7 +158,7 @@ export class AgentService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating agent',
@@ -242,7 +242,7 @@ export class AgentService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating agent',
@@ -357,7 +357,7 @@ export class AgentService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
`Multiple validation errors occurred while deleting agent${ids.length > 1 ? 's' : ''}`,
@@ -127,7 +127,7 @@ export class CommandMenuItemService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating command menu item',
@@ -194,7 +194,7 @@ export class CommandMenuItemService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating command menu item',
@@ -254,7 +254,7 @@ export class CommandMenuItemService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting command menu item',
@@ -141,7 +141,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting field',
@@ -286,7 +286,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating field',
@@ -394,7 +394,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating fields',
@@ -1,14 +1,10 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Equal, type Expect } from 'twenty-shared/testing';
import { type Expect } from 'twenty-shared/testing';
import { type AllFlatEntityTypesByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type WorkspaceMigrationActionType } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type UniversalSyncableFlatEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type';
import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type';
type ExpectedGenericFlatEntityInformation = {
universalActions: {
@@ -44,16 +40,4 @@ type Assertions = [
? true
: false
>,
Expect<
Equal<
BaseUniversalCreateWorkspaceMigrationAction<'objectMetadata'>['flatEntity'],
UniversalFlatObjectMetadata
>
>,
Expect<
Equal<
BaseFlatCreateWorkspaceMigrationAction<'view'>['flatEntity'],
FlatView
>
>,
];
@@ -114,7 +114,7 @@ export class FrontComponentService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating front component',
@@ -185,7 +185,7 @@ export class FrontComponentService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating front component',
@@ -262,7 +262,7 @@ export class FrontComponentService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying front component',
@@ -118,7 +118,7 @@ export class LogicFunctionMetadataService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating logic function',
@@ -178,7 +178,7 @@ export class LogicFunctionMetadataService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating logic function',
@@ -267,7 +267,7 @@ export class LogicFunctionMetadataService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying logic function',
@@ -198,7 +198,7 @@ export class NavigationMenuItemService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating navigation menu item',
@@ -287,7 +287,7 @@ export class NavigationMenuItemService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating navigation menu item',
@@ -367,7 +367,7 @@ export class NavigationMenuItemService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting navigation menu item',
@@ -74,7 +74,7 @@ export class NavigationMenuItemDeletionService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting navigation menu items for deleted records',
@@ -158,7 +158,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating object',
@@ -363,7 +363,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
`Multiple validation errors occurred while deleting object${deleteObjectInputs.length > 1 ? 's' : ''}`,
@@ -560,7 +560,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating object',
@@ -173,7 +173,7 @@ export class PageLayoutTabService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating page layout tab',
@@ -252,7 +252,7 @@ export class PageLayoutTabService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating page layout tab',
@@ -324,7 +324,7 @@ export class PageLayoutTabService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying page layout tab',
@@ -90,7 +90,7 @@ export class PageLayoutWidgetService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
errorMessage,
@@ -143,7 +143,7 @@ export class PageLayoutDuplicationService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while duplicating page layout',
@@ -163,7 +163,7 @@ export class PageLayoutUpdateService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating page layout with tabs',
@@ -223,7 +223,7 @@ export class PageLayoutService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating page layout',
@@ -300,7 +300,7 @@ export class PageLayoutService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating page layout',
@@ -376,7 +376,7 @@ export class PageLayoutService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying page layout',
@@ -444,7 +444,7 @@ export class PageLayoutService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying page layouts',
@@ -113,7 +113,7 @@ export class RoleTargetService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating role targets',
@@ -174,7 +174,7 @@ export class RoleTargetService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting role target',
@@ -131,7 +131,7 @@ export class RoleService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating role',
@@ -201,7 +201,7 @@ export class RoleService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating role',
@@ -338,7 +338,7 @@ export class RoleService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
`Multiple validation errors occurred while deleting role${ids.length > 1 ? 's' : ''}`,
@@ -540,7 +540,7 @@ export class RowLevelPermissionPredicateService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while upserting row level permission predicates',
@@ -97,7 +97,7 @@ export class SkillService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating skill',
@@ -159,7 +159,7 @@ export class SkillService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating skill',
@@ -218,7 +218,7 @@ export class SkillService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting skill',
@@ -308,7 +308,7 @@ export class SkillService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while activating skill',
@@ -373,7 +373,7 @@ export class SkillService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deactivating skill',
@@ -110,7 +110,7 @@ export class ViewFieldGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating view field groups',
@@ -176,7 +176,7 @@ export class ViewFieldGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating view field group',
@@ -247,7 +247,7 @@ export class ViewFieldGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view field group',
@@ -322,7 +322,7 @@ export class ViewFieldGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying view field group',
@@ -116,7 +116,7 @@ export class ViewFieldService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating view fields',
@@ -186,7 +186,7 @@ export class ViewFieldService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating view field',
@@ -254,7 +254,7 @@ export class ViewFieldService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view field',
@@ -328,7 +328,7 @@ export class ViewFieldService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view field',
@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, type Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
@@ -82,7 +81,7 @@ export class ViewFilterGroupService {
},
);
if (isDefined(buildAndRunResult)) {
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Multiple validation errors occurred while creating view filter group',
@@ -154,7 +153,7 @@ export class ViewFilterGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating view filter group',
@@ -227,7 +226,7 @@ export class ViewFilterGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view filter group',
@@ -305,7 +304,7 @@ export class ViewFilterGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying view filter group',
@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
@@ -84,7 +83,7 @@ export class ViewFilterService {
},
);
if (isDefined(buildAndRunResult)) {
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Multiple validation errors occurred while creating view filter',
@@ -161,7 +160,7 @@ export class ViewFilterService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating view filter',
@@ -232,7 +231,7 @@ export class ViewFilterService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view filter',
@@ -306,7 +305,7 @@ export class ViewFilterService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying view filter',
@@ -124,7 +124,7 @@ export class ViewGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating view groups',
@@ -190,7 +190,7 @@ export class ViewGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating view group',
@@ -261,7 +261,7 @@ export class ViewGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view group',
@@ -335,7 +335,7 @@ export class ViewGroupService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying view group',
@@ -96,7 +96,7 @@ export class ViewService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating view',
@@ -181,7 +181,7 @@ export class ViewService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating view',
@@ -249,7 +249,7 @@ export class ViewService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting view',
@@ -321,7 +321,7 @@ export class ViewService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying view',
@@ -130,7 +130,7 @@ export class WebhookService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating webhook',
@@ -202,7 +202,7 @@ export class WebhookService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating webhook',
@@ -260,7 +260,7 @@ export class WebhookService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting webhook',
@@ -125,7 +125,7 @@ export class TwentyStandardApplicationService {
},
);
if (isDefined(validateAndBuildResult)) {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while synchronizing twenty-standard application',
@@ -1,20 +1,19 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import { type UniversalCreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
import { type UniversalCreatePageLayoutAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type';
import { type WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration';
import { type WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type';
export type IdByUniversalIdentifierByMetadataName = {
[P in AllMetadataName]?: Record<string, string>;
};
const buildFieldIdByUniversalIdentifier = ({
const buildFieldIdByUniversalIdentifierForObjectAction = ({
action,
fieldMetadataIdByUniversalIdentifier,
}: {
action: UniversalCreateObjectAction | UniversalCreateFieldAction;
action: UniversalCreateObjectAction;
fieldMetadataIdByUniversalIdentifier: Record<string, string>;
}): Record<string, string> | undefined => {
const fieldIdByUniversalIdentifier = {
@@ -96,7 +95,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
const fieldIdByUniversalIdentifier = isDefined(
fieldMetadataIdByUniversalIdentifier,
)
? buildFieldIdByUniversalIdentifier({
? buildFieldIdByUniversalIdentifierForObjectAction({
action,
fieldMetadataIdByUniversalIdentifier,
})
@@ -113,12 +112,20 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
return action;
}
const relatedFieldId = isDefined(
action.relatedUniversalFlatFieldMetadata,
)
? fieldMetadataIdByUniversalIdentifier[
action.relatedUniversalFlatFieldMetadata.universalIdentifier
]
: undefined;
return {
...action,
fieldIdByUniversalIdentifier: buildFieldIdByUniversalIdentifier({
action,
fieldMetadataIdByUniversalIdentifier,
}),
id: fieldMetadataIdByUniversalIdentifier[
action.flatEntity.universalIdentifier
],
relatedFieldId,
};
}
case 'pageLayout': {
@@ -30,8 +30,8 @@ import { WorkspaceMigrationRoleActionsBuilderService } from 'src/engine/workspac
import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate-group/workspace-migration-row-level-permission-predicate-group-actions-builder.service';
import { WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate/workspace-migration-row-level-permission-predicate-actions-builder.service';
import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service';
import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service';
import { WorkspaceMigrationViewFieldGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/workspace-migration-view-field-group-actions-builder.service';
import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service';
import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service';
import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service';
import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service';
@@ -1138,7 +1138,6 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.webhook.update,
///
],
workspaceId,
},
};
}
@@ -27,6 +27,7 @@ import {
FromToAllUniversalFlatEntityMaps,
WorkspaceMigrationOrchestratorBuildArgs,
WorkspaceMigrationOrchestratorFailedResult,
WorkspaceMigrationOrchestratorSuccessfulResult,
} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { computeUniversalFlatEntityMapsFromTo } from 'src/engine/workspace-manager/workspace-migration/utils/compute-universal-flat-entity-maps-from-to.util';
import { InferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/infer-deletion-from-missing-entities.type';
@@ -217,7 +218,10 @@ export class WorkspaceMigrationValidateBuildAndRunService {
args: WorkspaceMigrationOrchestratorBuildArgs & {
idByUniversalIdentifierByMetadataName?: IdByUniversalIdentifierByMetadataName;
},
) {
): Promise<
| WorkspaceMigrationOrchestratorFailedResult
| WorkspaceMigrationOrchestratorSuccessfulResult
> {
const { idByUniversalIdentifierByMetadataName, ...buildArgs } = args;
const validateAndBuildResult =
@@ -239,10 +243,6 @@ export class WorkspaceMigrationValidateBuildAndRunService {
return validateAndBuildResult;
}
if (validateAndBuildResult.workspaceMigration.actions.length === 0) {
return undefined;
}
const workspaceMigration = isDefined(idByUniversalIdentifierByMetadataName)
? enrichCreateWorkspaceMigrationActionsWithIds({
idByUniversalIdentifierByMetadataName,
@@ -250,13 +250,24 @@ export class WorkspaceMigrationValidateBuildAndRunService {
})
: validateAndBuildResult.workspaceMigration;
const { metadataEvents } =
await this.workspaceMigrationRunnerService.run(workspaceMigration);
if (workspaceMigration.actions.length > 0) {
const { metadataEvents } = await this.workspaceMigrationRunnerService.run(
{
workspaceId: args.workspaceId,
workspaceMigration,
},
);
this.metadataEventEmitter.emitMetadataEvents({
metadataEvents,
workspaceId: args.workspaceId,
});
this.metadataEventEmitter.emitMetadataEvents({
metadataEvents,
workspaceId: args.workspaceId,
});
}
return {
status: 'success',
workspaceMigration,
};
}
public async validateBuildAndRunWorkspaceMigration({
@@ -265,7 +276,8 @@ export class WorkspaceMigrationValidateBuildAndRunService {
isSystemBuild = false,
applicationUniversalIdentifier,
}: ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs): Promise<
WorkspaceMigrationOrchestratorFailedResult | undefined
| WorkspaceMigrationOrchestratorFailedResult
| WorkspaceMigrationOrchestratorSuccessfulResult
> {
const {
fromToAllFlatEntityMaps,
@@ -8,8 +8,8 @@ import {
import { type WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-builder-additional-cache-data-maps.type';
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
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';
import { type WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type';
export type FromToAllUniversalFlatEntityMaps = {
[P in keyof AllUniversalFlatEntityMaps]?: FromTo<
@@ -0,0 +1,58 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should match snapshot 1`] = `
{
"agent": [],
"commandMenuItem": [],
"fieldMetadata": [
"calendarViewUniversalIdentifiers",
"kanbanAggregateOperationViewUniversalIdentifiers",
"mainGroupByFieldMetadataViewUniversalIdentifiers",
"viewFieldUniversalIdentifiers",
"viewFilterUniversalIdentifiers",
],
"frontComponent": [],
"index": [],
"logicFunction": [],
"navigationMenuItem": [],
"objectMetadata": [
"fieldUniversalIdentifiers",
"viewUniversalIdentifiers",
"indexMetadataUniversalIdentifiers",
],
"pageLayout": [
"tabUniversalIdentifiers",
],
"pageLayoutTab": [
"widgetUniversalIdentifiers",
],
"pageLayoutWidget": [],
"role": [
"roleTargetUniversalIdentifiers",
],
"roleTarget": [],
"rowLevelPermissionPredicate": [],
"rowLevelPermissionPredicateGroup": [
"childRowLevelPermissionPredicateGroupUniversalIdentifiers",
],
"skill": [],
"view": [
"viewFieldUniversalIdentifiers",
"viewFieldGroupUniversalIdentifiers",
"viewFilterUniversalIdentifiers",
"viewGroupUniversalIdentifiers",
"viewFilterGroupUniversalIdentifiers",
],
"viewField": [],
"viewFieldGroup": [
"viewFieldUniversalIdentifiers",
],
"viewFilter": [],
"viewFilterGroup": [
"viewFilterUniversalIdentifiers",
"childViewFilterGroupUniversalIdentifiers",
],
"viewGroup": [],
"webhook": [],
}
`;
@@ -0,0 +1,14 @@
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
describe('ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES', () => {
it('should match snapshot', () => {
expect(
Object.keys(ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES),
).toMatchObject(Object.values(ALL_METADATA_NAME));
expect(
ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES,
).toMatchSnapshot();
});
});
@@ -0,0 +1,71 @@
import {
ALL_METADATA_NAME,
type AllMetadataName,
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ALL_UNIVERSAL_METADATA_RELATIONS } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-metadata-relations.constant';
type ExtractForeignKeyAggregatorFromManyToOneRelations<
ManyToOneRelations,
TargetMetadataName extends AllMetadataName,
> = {
[K in keyof ManyToOneRelations]: ManyToOneRelations[K] extends {
metadataName: TargetMetadataName;
universalFlatEntityForeignKeyAggregator: infer Agg;
}
? Agg extends string
? Agg
: never
: never;
}[keyof ManyToOneRelations];
export type ExtractUniversalForeignKeyAggregatorForMetadataName<
T extends AllMetadataName,
> = {
[M in AllMetadataName]: ExtractForeignKeyAggregatorFromManyToOneRelations<
(typeof ALL_UNIVERSAL_METADATA_RELATIONS)[M]['manyToOne'],
T
>;
}[AllMetadataName];
type UniversalFlatEntityForeignKeyAggregatorProperties = {
[P in AllMetadataName]: ExtractUniversalForeignKeyAggregatorForMetadataName<P>[];
};
const computeForeignKeyAggregatorProperties = <T extends AllMetadataName>(
metadataName: T,
): ExtractUniversalForeignKeyAggregatorForMetadataName<T>[] => {
const aggregatorProperties: ExtractUniversalForeignKeyAggregatorForMetadataName<T>[] =
[];
for (const relationsEntry of Object.values(
ALL_UNIVERSAL_METADATA_RELATIONS,
)) {
for (const relation of Object.values(relationsEntry.manyToOne)) {
if (!isDefined(relation)) {
continue;
}
if (
relation.metadataName === metadataName &&
isDefined(relation.universalFlatEntityForeignKeyAggregator)
) {
aggregatorProperties.push(
relation.universalFlatEntityForeignKeyAggregator as ExtractUniversalForeignKeyAggregatorForMetadataName<T>,
);
}
}
}
return aggregatorProperties;
};
export const ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES =
Object.values(ALL_METADATA_NAME).reduce(
(acc, metadataName) => ({
...acc,
[metadataName]: computeForeignKeyAggregatorProperties(metadataName),
}),
{} as UniversalFlatEntityForeignKeyAggregatorProperties,
);
@@ -0,0 +1,36 @@
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { deleteUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util';
describe('deleteUniversalFlatEntityForeignKeyAggregators', () => {
it('should delete all aggregator keys from the entity', () => {
const objectMetadata = getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
fieldUniversalIdentifiers: ['field-1', 'field-2'],
viewUniversalIdentifiers: ['view-1'],
indexMetadataUniversalIdentifiers: ['index-1'],
});
const result = deleteUniversalFlatEntityForeignKeyAggregators({
universalFlatEntity: objectMetadata,
metadataName: 'objectMetadata',
});
expect(result).not.toHaveProperty('fieldUniversalIdentifiers');
expect(result).not.toHaveProperty('viewUniversalIdentifiers');
expect(result).not.toHaveProperty('indexMetadataUniversalIdentifiers');
expect(result.universalIdentifier).toBe('object-1');
});
it('should return the same reference for metadata with no aggregators', () => {
const entity = getFlatObjectMetadataMock({
universalIdentifier: 'logic-1',
});
const result = deleteUniversalFlatEntityForeignKeyAggregators({
universalFlatEntity: entity,
metadataName: 'logicFunction' as any,
});
expect(result).toBe(entity);
});
});
@@ -0,0 +1,50 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { resetUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
describe('resetUniversalFlatEntityForeignKeyAggregators', () => {
it('should reset aggregator properties to empty arrays for objectMetadata', () => {
const objectMetadata = getFlatObjectMetadataMock({
universalIdentifier: 'object-1',
fieldUniversalIdentifiers: ['field-1', 'field-2'],
viewUniversalIdentifiers: ['view-1'],
indexMetadataUniversalIdentifiers: ['index-1'],
});
const result = resetUniversalFlatEntityForeignKeyAggregators({
universalFlatEntity: objectMetadata,
metadataName: 'objectMetadata',
});
expect(result).toMatchObject({
fieldUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
});
});
it('should reset aggregator properties to empty arrays for fieldMetadata', () => {
const fieldMetadata = getFlatFieldMetadataMock({
universalIdentifier: 'field-1',
objectMetadataId: 'object-1',
type: FieldMetadataType.TEXT,
viewFieldUniversalIdentifiers: ['vf-1'],
viewFilterUniversalIdentifiers: ['filter-1'],
});
const result = resetUniversalFlatEntityForeignKeyAggregators({
universalFlatEntity: fieldMetadata,
metadataName: 'fieldMetadata',
});
expect(result).toMatchObject({
viewFieldUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
});
});
});
@@ -0,0 +1,29 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import { ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
export const deleteUniversalFlatEntityForeignKeyAggregators = <
T extends AllMetadataName,
>({
universalFlatEntity,
metadataName,
}: {
universalFlatEntity: MetadataUniversalFlatEntity<T>;
metadataName: T;
}): MetadataUniversalFlatEntity<T> => {
const aggregatorProperties =
ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES[metadataName];
if (aggregatorProperties.length === 0) {
return universalFlatEntity;
}
const result = { ...universalFlatEntity };
for (const aggregatorProperty of aggregatorProperties) {
delete (result as Record<string, unknown>)[aggregatorProperty];
}
return result;
};
@@ -0,0 +1,48 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import {
ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES,
type ExtractUniversalForeignKeyAggregatorForMetadataName,
} from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
export const getUniversalFlatEntityEmptyForeignKeyAggregators = <
T extends AllMetadataName,
>({
metadataName,
}: {
metadataName: T;
}) => {
const aggregatorProperties =
ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES[metadataName];
const emptyAggregatorsRecord = {} as Record<
ExtractUniversalForeignKeyAggregatorForMetadataName<T>,
string[]
>;
for (const aggregatorProperty of aggregatorProperties) {
emptyAggregatorsRecord[aggregatorProperty] = [];
}
return emptyAggregatorsRecord;
};
export const resetUniversalFlatEntityForeignKeyAggregators = <
T extends AllMetadataName,
>({
universalFlatEntity,
metadataName,
}: {
universalFlatEntity: MetadataUniversalFlatEntity<T>;
metadataName: T;
}): MetadataUniversalFlatEntity<T> => {
const overrides = getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName,
});
return {
...universalFlatEntity,
...overrides,
};
};
@@ -9,10 +9,18 @@ import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/wo
import { type UniversalCreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
describe('aggregateNonRelationFieldsIntoObjectActions', () => {
it('should merge non-relation fields into matching create-object actions', () => {
it('should merge non-relation field into matching create-object action', () => {
const objectUniversalId = 'object-1';
const fieldUniversalId = 'field-1';
const flatFieldMetadata = getFlatFieldMetadataMock({
universalIdentifier: fieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.TEXT,
name: 'testField',
});
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
@@ -36,18 +44,8 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => {
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: fieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.TEXT,
name: 'testField',
}),
],
fieldIdByUniversalIdentifier: {
[fieldUniversalId]: 'generated-field-id',
},
flatEntity: flatFieldMetadata,
id: 'generated-field-id',
} satisfies UniversalCreateFieldAction,
],
update: [],
@@ -75,11 +73,21 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => {
expect(result.fieldMetadata.create).toHaveLength(0);
});
it('should keep relation fields in separate create-field actions', () => {
it('should keep relation field in separate create-field action', () => {
const objectUniversalId = 'object-1';
const relationFieldUniversalId = 'relation-field-1';
const targetFieldUniversalId = 'target-field-1';
const flatRelationFieldMetadata = getFlatFieldMetadataMock({
universalIdentifier: relationFieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.RELATION,
name: 'relationField',
relationTargetFieldMetadataUniversalIdentifier: targetFieldUniversalId,
relationTargetObjectMetadataUniversalIdentifier: 'other-object',
});
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
@@ -103,21 +111,8 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => {
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: relationFieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.RELATION,
name: 'relationField',
relationTargetFieldMetadataUniversalIdentifier:
targetFieldUniversalId,
relationTargetObjectMetadataUniversalIdentifier: 'other-object',
}),
],
fieldIdByUniversalIdentifier: {
[relationFieldUniversalId]: 'generated-relation-field-id',
},
flatEntity: flatRelationFieldMetadata,
id: 'generated-relation-field-id',
} satisfies UniversalCreateFieldAction,
],
update: [],
@@ -137,17 +132,23 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => {
// Relation field should remain in field actions
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: relationFieldUniversalId },
],
flatEntity: { universalIdentifier: relationFieldUniversalId },
},
]);
});
it('should keep fields for existing objects in separate create-field actions', () => {
it('should keep field for existing object in separate create-field action', () => {
const existingObjectUniversalId = 'existing-object';
const fieldUniversalId = 'field-for-existing-object';
const flatFieldMetadata = getFlatFieldMetadataMock({
universalIdentifier: fieldUniversalId,
objectMetadataId: 'existing-object-metadata-id',
objectMetadataUniversalIdentifier: existingObjectUniversalId,
type: FieldMetadataType.TEXT,
name: 'fieldForExistingObject',
});
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
@@ -160,18 +161,8 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => {
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: fieldUniversalId,
objectMetadataId: 'existing-object-metadata-id',
objectMetadataUniversalIdentifier: existingObjectUniversalId,
type: FieldMetadataType.TEXT,
name: 'fieldForExistingObject',
}),
],
fieldIdByUniversalIdentifier: {
[fieldUniversalId]: 'generated-field-id',
},
flatEntity: flatFieldMetadata,
id: 'generated-field-id',
} satisfies UniversalCreateFieldAction,
],
update: [],
@@ -189,18 +180,34 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => {
// Field should remain in field actions (no matching object to merge into)
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: fieldUniversalId },
],
flatEntity: { universalIdentifier: fieldUniversalId },
},
]);
});
it('should handle mixed relation and non-relation fields', () => {
it('should handle mixed relation and non-relation fields as separate actions', () => {
const objectUniversalId = 'object-1';
const textFieldUniversalId = 'text-field';
const relationFieldUniversalId = 'relation-field';
const flatTextFieldMetadata = getFlatFieldMetadataMock({
universalIdentifier: textFieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.TEXT,
name: 'textField',
});
const flatRelationFieldMetadata = getFlatFieldMetadataMock({
universalIdentifier: relationFieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.RELATION,
name: 'relationField',
relationTargetFieldMetadataUniversalIdentifier: 'target-field',
relationTargetObjectMetadataUniversalIdentifier: 'other-object',
});
const input: OrchestratorActionsReport = {
...createEmptyOrchestratorActionsReport(),
objectMetadata: {
@@ -224,28 +231,14 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => {
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: textFieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.TEXT,
name: 'textField',
}),
getFlatFieldMetadataMock({
universalIdentifier: relationFieldUniversalId,
objectMetadataId: 'object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.RELATION,
name: 'relationField',
relationTargetFieldMetadataUniversalIdentifier: 'target-field',
relationTargetObjectMetadataUniversalIdentifier: 'other-object',
}),
],
fieldIdByUniversalIdentifier: {
[textFieldUniversalId]: 'text-field-id',
[relationFieldUniversalId]: 'relation-field-id',
},
flatEntity: flatTextFieldMetadata,
id: 'text-field-id',
} satisfies UniversalCreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
flatEntity: flatRelationFieldMetadata,
id: 'relation-field-id',
} satisfies UniversalCreateFieldAction,
],
update: [],
@@ -269,9 +262,7 @@ describe('aggregateNonRelationFieldsIntoObjectActions', () => {
// Relation field should remain in field actions
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: relationFieldUniversalId },
],
flatEntity: { universalIdentifier: relationFieldUniversalId },
},
]);
});
@@ -7,7 +7,7 @@ import { aggregateRelationFieldPairs } from 'src/engine/workspace-manager/worksp
import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
describe('aggregateRelationFieldPairs', () => {
it('should bundle relation field pairs into single action with merged fieldIdByUniversalIdentifier', () => {
it('should bundle relation field pairs into single action with relatedFieldId', () => {
const attachmentObjectId = 'attachment-object';
const taskObjectId = 'task-object';
const targetTaskFieldId = 'target-task-field';
@@ -20,41 +20,32 @@ describe('aggregateRelationFieldPairs', () => {
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: targetTaskFieldId,
objectMetadataId: 'attachment-object-metadata-id',
objectMetadataUniversalIdentifier: attachmentObjectId,
type: FieldMetadataType.RELATION,
name: 'targetTask',
relationTargetFieldMetadataUniversalIdentifier:
attachmentsFieldId,
relationTargetObjectMetadataUniversalIdentifier: taskObjectId,
}),
],
fieldIdByUniversalIdentifier: {
[targetTaskFieldId]: 'target-task-generated-id',
},
flatEntity: getFlatFieldMetadataMock({
universalIdentifier: targetTaskFieldId,
objectMetadataId: 'attachment-object-metadata-id',
objectMetadataUniversalIdentifier: attachmentObjectId,
type: FieldMetadataType.RELATION,
name: 'targetTask',
relationTargetFieldMetadataUniversalIdentifier:
attachmentsFieldId,
relationTargetObjectMetadataUniversalIdentifier: taskObjectId,
}),
id: 'target-task-generated-id',
} satisfies UniversalCreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: attachmentsFieldId,
objectMetadataId: 'task-object-metadata-id',
objectMetadataUniversalIdentifier: taskObjectId,
type: FieldMetadataType.RELATION,
name: 'attachments',
relationTargetFieldMetadataUniversalIdentifier:
targetTaskFieldId,
relationTargetObjectMetadataUniversalIdentifier:
attachmentObjectId,
}),
],
fieldIdByUniversalIdentifier: {
[attachmentsFieldId]: 'attachments-generated-id',
},
flatEntity: getFlatFieldMetadataMock({
universalIdentifier: attachmentsFieldId,
objectMetadataId: 'task-object-metadata-id',
objectMetadataUniversalIdentifier: taskObjectId,
type: FieldMetadataType.RELATION,
name: 'attachments',
relationTargetFieldMetadataUniversalIdentifier: targetTaskFieldId,
relationTargetObjectMetadataUniversalIdentifier:
attachmentObjectId,
}),
id: 'attachments-generated-id',
} satisfies UniversalCreateFieldAction,
],
update: [],
@@ -66,17 +57,15 @@ describe('aggregateRelationFieldPairs', () => {
orchestratorActionsReport: input,
});
// Should be bundled into a single action with both fields and merged ID map
// Should be bundled into a single action with flatEntity and relatedUniversalFlatFieldMetadata
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: targetTaskFieldId },
{ universalIdentifier: attachmentsFieldId },
],
fieldIdByUniversalIdentifier: {
[targetTaskFieldId]: 'target-task-generated-id',
[attachmentsFieldId]: 'attachments-generated-id',
flatEntity: { universalIdentifier: targetTaskFieldId },
id: 'target-task-generated-id',
relatedUniversalFlatFieldMetadata: {
universalIdentifier: attachmentsFieldId,
},
relatedFieldId: 'attachments-generated-id',
},
]);
});
@@ -92,18 +81,14 @@ describe('aggregateRelationFieldPairs', () => {
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: fieldUniversalId,
objectMetadataId: 'some-object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.TEXT,
name: 'standaloneField',
}),
],
fieldIdByUniversalIdentifier: {
[fieldUniversalId]: 'standalone-field-id',
},
flatEntity: getFlatFieldMetadataMock({
universalIdentifier: fieldUniversalId,
objectMetadataId: 'some-object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.TEXT,
name: 'standaloneField',
}),
id: 'standalone-field-id',
} satisfies UniversalCreateFieldAction,
],
update: [],
@@ -115,14 +100,16 @@ describe('aggregateRelationFieldPairs', () => {
orchestratorActionsReport: input,
});
// Should remain as single action
// Should remain as single action without relatedUniversalFlatFieldMetadata
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: fieldUniversalId },
],
flatEntity: { universalIdentifier: fieldUniversalId },
},
]);
expect(
result.fieldMetadata.create[0].relatedUniversalFlatFieldMetadata,
).toBeUndefined();
expect(result.fieldMetadata.create[0].relatedFieldId).toBeUndefined();
});
it('should handle relation field with target not being created (existing field)', () => {
@@ -137,21 +124,17 @@ describe('aggregateRelationFieldPairs', () => {
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: relationFieldId,
objectMetadataId: 'some-object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.RELATION,
name: 'relationToExisting',
relationTargetFieldMetadataUniversalIdentifier:
existingTargetFieldId,
relationTargetObjectMetadataUniversalIdentifier: 'other-object',
}),
],
fieldIdByUniversalIdentifier: {
[relationFieldId]: 'new-relation-field-id',
},
flatEntity: getFlatFieldMetadataMock({
universalIdentifier: relationFieldId,
objectMetadataId: 'some-object-metadata-id',
objectMetadataUniversalIdentifier: objectUniversalId,
type: FieldMetadataType.RELATION,
name: 'relationToExisting',
relationTargetFieldMetadataUniversalIdentifier:
existingTargetFieldId,
relationTargetObjectMetadataUniversalIdentifier: 'other-object',
}),
id: 'new-relation-field-id',
} satisfies UniversalCreateFieldAction,
],
update: [],
@@ -163,12 +146,16 @@ describe('aggregateRelationFieldPairs', () => {
orchestratorActionsReport: input,
});
// Should remain as single action (target not being created)
// Should remain as single action without relatedUniversalFlatFieldMetadata (target not being created)
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [{ universalIdentifier: relationFieldId }],
flatEntity: { universalIdentifier: relationFieldId },
},
]);
expect(
result.fieldMetadata.create[0].relatedUniversalFlatFieldMetadata,
).toBeUndefined();
expect(result.fieldMetadata.create[0].relatedFieldId).toBeUndefined();
});
it('should handle multiple independent relation pairs', () => {
@@ -184,66 +171,58 @@ describe('aggregateRelationFieldPairs', () => {
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: pair1FieldA,
objectMetadataId: 'object-1-metadata-id',
objectMetadataUniversalIdentifier: 'object-1',
type: FieldMetadataType.RELATION,
name: 'pair1FieldA',
relationTargetFieldMetadataUniversalIdentifier: pair1FieldB,
relationTargetObjectMetadataUniversalIdentifier: 'object-2',
}),
],
fieldIdByUniversalIdentifier: { [pair1FieldA]: 'id-1a' },
flatEntity: getFlatFieldMetadataMock({
universalIdentifier: pair1FieldA,
objectMetadataId: 'object-1-metadata-id',
objectMetadataUniversalIdentifier: 'object-1',
type: FieldMetadataType.RELATION,
name: 'pair1FieldA',
relationTargetFieldMetadataUniversalIdentifier: pair1FieldB,
relationTargetObjectMetadataUniversalIdentifier: 'object-2',
}),
id: 'id-1a',
} satisfies UniversalCreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: pair1FieldB,
objectMetadataId: 'object-2-metadata-id',
objectMetadataUniversalIdentifier: 'object-2',
type: FieldMetadataType.RELATION,
name: 'pair1FieldB',
relationTargetFieldMetadataUniversalIdentifier: pair1FieldA,
relationTargetObjectMetadataUniversalIdentifier: 'object-1',
}),
],
fieldIdByUniversalIdentifier: { [pair1FieldB]: 'id-1b' },
flatEntity: getFlatFieldMetadataMock({
universalIdentifier: pair1FieldB,
objectMetadataId: 'object-2-metadata-id',
objectMetadataUniversalIdentifier: 'object-2',
type: FieldMetadataType.RELATION,
name: 'pair1FieldB',
relationTargetFieldMetadataUniversalIdentifier: pair1FieldA,
relationTargetObjectMetadataUniversalIdentifier: 'object-1',
}),
id: 'id-1b',
} satisfies UniversalCreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: pair2FieldA,
objectMetadataId: 'object-3-metadata-id',
objectMetadataUniversalIdentifier: 'object-3',
type: FieldMetadataType.RELATION,
name: 'pair2FieldA',
relationTargetFieldMetadataUniversalIdentifier: pair2FieldB,
relationTargetObjectMetadataUniversalIdentifier: 'object-4',
}),
],
fieldIdByUniversalIdentifier: { [pair2FieldA]: 'id-2a' },
flatEntity: getFlatFieldMetadataMock({
universalIdentifier: pair2FieldA,
objectMetadataId: 'object-3-metadata-id',
objectMetadataUniversalIdentifier: 'object-3',
type: FieldMetadataType.RELATION,
name: 'pair2FieldA',
relationTargetFieldMetadataUniversalIdentifier: pair2FieldB,
relationTargetObjectMetadataUniversalIdentifier: 'object-4',
}),
id: 'id-2a',
} satisfies UniversalCreateFieldAction,
{
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [
getFlatFieldMetadataMock({
universalIdentifier: pair2FieldB,
objectMetadataId: 'object-4-metadata-id',
objectMetadataUniversalIdentifier: 'object-4',
type: FieldMetadataType.RELATION,
name: 'pair2FieldB',
relationTargetFieldMetadataUniversalIdentifier: pair2FieldA,
relationTargetObjectMetadataUniversalIdentifier: 'object-3',
}),
],
fieldIdByUniversalIdentifier: { [pair2FieldB]: 'id-2b' },
flatEntity: getFlatFieldMetadataMock({
universalIdentifier: pair2FieldB,
objectMetadataId: 'object-4-metadata-id',
objectMetadataUniversalIdentifier: 'object-4',
type: FieldMetadataType.RELATION,
name: 'pair2FieldB',
relationTargetFieldMetadataUniversalIdentifier: pair2FieldA,
relationTargetObjectMetadataUniversalIdentifier: 'object-3',
}),
id: 'id-2b',
} satisfies UniversalCreateFieldAction,
],
update: [],
@@ -255,27 +234,23 @@ describe('aggregateRelationFieldPairs', () => {
orchestratorActionsReport: input,
});
// Should result in 2 bundled actions (one per pair), each with merged fieldIdByUniversalIdentifier
// Should result in 2 bundled actions (one per pair)
expect(result.fieldMetadata.create).toMatchObject([
{
universalFlatFieldMetadatas: [
{ universalIdentifier: pair1FieldA },
{ universalIdentifier: pair1FieldB },
],
fieldIdByUniversalIdentifier: {
[pair1FieldA]: 'id-1a',
[pair1FieldB]: 'id-1b',
flatEntity: { universalIdentifier: pair1FieldA },
id: 'id-1a',
relatedUniversalFlatFieldMetadata: {
universalIdentifier: pair1FieldB,
},
relatedFieldId: 'id-1b',
},
{
universalFlatFieldMetadatas: [
{ universalIdentifier: pair2FieldA },
{ universalIdentifier: pair2FieldB },
],
fieldIdByUniversalIdentifier: {
[pair2FieldA]: 'id-2a',
[pair2FieldB]: 'id-2b',
flatEntity: { universalIdentifier: pair2FieldA },
id: 'id-2a',
relatedUniversalFlatFieldMetadata: {
universalIdentifier: pair2FieldB,
},
relatedFieldId: 'id-2b',
},
]);
});
@@ -1,10 +1,8 @@
import { isDefined } from 'twenty-shared/utils';
import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { mergeFieldIdByUniversalIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util';
import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import { type UniversalCreateObjectAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
@@ -29,64 +27,29 @@ export const aggregateNonRelationFieldsIntoObjectActions = ({
const remainingCreateFieldActions: UniversalCreateFieldAction[] = [];
for (const createFieldAction of createFieldActions) {
const nonRelationFields: UniversalFlatFieldMetadata[] = [];
const relationFields: UniversalFlatFieldMetadata[] = [];
const flatFieldMetadata = createFieldAction.flatEntity;
for (const field of createFieldAction.universalFlatFieldMetadatas) {
if (isMorphOrRelationUniversalFlatFieldMetadata(field)) {
relationFields.push(field);
} else {
nonRelationFields.push(field);
}
if (isMorphOrRelationFieldMetadataType(flatFieldMetadata.type)) {
remainingCreateFieldActions.push(createFieldAction);
continue;
}
const nonRelationFieldsByObjectUniversalIdentifier = new Map<
string,
UniversalFlatFieldMetadata[]
>();
const objectUniversalId =
flatFieldMetadata.objectMetadataUniversalIdentifier;
const matchingObjectAction =
createObjectActionByObjectUniversalIdentifier.get(objectUniversalId);
for (const field of nonRelationFields) {
const objectUniversalId = field.objectMetadataUniversalIdentifier;
const existing =
nonRelationFieldsByObjectUniversalIdentifier.get(objectUniversalId);
if (isDefined(matchingObjectAction)) {
matchingObjectAction.universalFlatFieldMetadatas.push(flatFieldMetadata);
if (isDefined(existing)) {
existing.push(field);
} else {
nonRelationFieldsByObjectUniversalIdentifier.set(objectUniversalId, [
field,
]);
if (isDefined(createFieldAction.id)) {
matchingObjectAction.fieldIdByUniversalIdentifier = {
...matchingObjectAction.fieldIdByUniversalIdentifier,
[flatFieldMetadata.universalIdentifier]: createFieldAction.id,
};
}
}
const unmergedNonRelationFields: UniversalFlatFieldMetadata[] = [];
for (const [
objectUniversalId,
fields,
] of nonRelationFieldsByObjectUniversalIdentifier) {
const matchingObjectAction =
createObjectActionByObjectUniversalIdentifier.get(objectUniversalId);
if (isDefined(matchingObjectAction)) {
matchingObjectAction.universalFlatFieldMetadatas.push(...fields);
matchingObjectAction.fieldIdByUniversalIdentifier =
mergeFieldIdByUniversalIdentifier(
matchingObjectAction.fieldIdByUniversalIdentifier,
createFieldAction.fieldIdByUniversalIdentifier,
);
} else {
unmergedNonRelationFields.push(...fields);
}
}
const remainingFields = [...unmergedNonRelationFields, ...relationFields];
if (remainingFields.length > 0) {
remainingCreateFieldActions.push({
...createFieldAction,
universalFlatFieldMetadatas: remainingFields,
});
} else {
remainingCreateFieldActions.push(createFieldAction);
}
}
@@ -2,29 +2,23 @@ import { isDefined } from 'twenty-shared/utils';
import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-aggregate-orchestrator-actions-report-args.type';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { mergeFieldIdByUniversalIdentifier } from 'src/engine/workspace-manager/workspace-migration/utils/merge-field-id-by-universal-identifier.util';
import { type UniversalCreateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
type FieldWithActionContext = {
field: UniversalFlatFieldMetadata;
fieldIdByUniversalIdentifier: Record<string, string> | undefined;
};
export const aggregateRelationFieldPairs = ({
orchestratorActionsReport,
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
const createFieldActions = orchestratorActionsReport.fieldMetadata.create;
const fieldByUniversalIdentifier = new Map<string, FieldWithActionContext>();
const actionByFieldUniversalIdentifier = new Map<
string,
UniversalCreateFieldAction
>();
for (const action of createFieldActions) {
for (const field of action.universalFlatFieldMetadatas) {
fieldByUniversalIdentifier.set(field.universalIdentifier, {
field,
fieldIdByUniversalIdentifier: action.fieldIdByUniversalIdentifier,
});
}
actionByFieldUniversalIdentifier.set(
action.flatEntity.universalIdentifier,
action,
);
}
const processedFieldUniversalIdentifiers = new Set<string>();
@@ -32,45 +26,43 @@ export const aggregateRelationFieldPairs = ({
for (const [
universalIdentifier,
fieldContext,
] of fieldByUniversalIdentifier) {
action,
] of actionByFieldUniversalIdentifier) {
if (processedFieldUniversalIdentifiers.has(universalIdentifier)) {
continue;
}
const { field, fieldIdByUniversalIdentifier } = fieldContext;
processedFieldUniversalIdentifiers.add(universalIdentifier);
const fieldsToBundle: UniversalFlatFieldMetadata[] = [field];
let mergedFieldIdMap = fieldIdByUniversalIdentifier;
const targetUniversalIdentifier =
field.relationTargetFieldMetadataUniversalIdentifier;
action.flatEntity.relationTargetFieldMetadataUniversalIdentifier;
if (isDefined(targetUniversalIdentifier)) {
const targetFieldContext = fieldByUniversalIdentifier.get(
targetUniversalIdentifier,
);
if (
isDefined(targetFieldContext) &&
!processedFieldUniversalIdentifiers.has(targetUniversalIdentifier)
) {
fieldsToBundle.push(targetFieldContext.field);
processedFieldUniversalIdentifiers.add(targetUniversalIdentifier);
mergedFieldIdMap = mergeFieldIdByUniversalIdentifier(
mergedFieldIdMap,
targetFieldContext.fieldIdByUniversalIdentifier,
);
}
if (!isDefined(targetUniversalIdentifier)) {
aggregatedCreateFieldActions.push(action);
continue;
}
const targetAction = actionByFieldUniversalIdentifier.get(
targetUniversalIdentifier,
);
if (
!isDefined(targetAction) ||
processedFieldUniversalIdentifiers.has(targetUniversalIdentifier)
) {
aggregatedCreateFieldActions.push(action);
continue;
}
processedFieldUniversalIdentifiers.add(targetUniversalIdentifier);
aggregatedCreateFieldActions.push({
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: fieldsToBundle,
fieldIdByUniversalIdentifier: mergedFieldIdMap,
flatEntity: action.flatEntity,
id: action.id,
relatedUniversalFlatFieldMetadata: targetAction.flatEntity,
relatedFieldId: targetAction.id,
});
}
@@ -1,15 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
export const mergeFieldIdByUniversalIdentifier = (
existing: Record<string, string> | undefined,
incoming: Record<string, string> | undefined,
): Record<string, string> | undefined => {
if (!isDefined(existing) && !isDefined(incoming)) {
return undefined;
}
return {
...existing,
...incoming,
};
};
@@ -1,4 +1,5 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type';
import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type';
@@ -8,14 +9,16 @@ import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/wor
import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type';
// Universal action types (always use universal identifiers)
export type UniversalCreateFieldAction = Omit<
BaseUniversalCreateWorkspaceMigrationAction<'fieldMetadata'>,
'flatEntity'
> & {
universalFlatFieldMetadatas: UniversalFlatFieldMetadata[];
// Optional map to provide specific IDs for field creation (for API metadata).
fieldIdByUniversalIdentifier?: Record<string, string>;
};
export type UniversalCreateFieldAction =
BaseUniversalCreateWorkspaceMigrationAction<'fieldMetadata'> & {
// For relation fields, the related field on the other side of the relation.
relatedUniversalFlatFieldMetadata?: Omit<
UniversalFlatFieldMetadata,
ExtractUniversalForeignKeyAggregatorForMetadataName<'fieldMetadata'>
>;
// Optional ID for the related field (for API metadata).
relatedFieldId?: string;
};
export type UniversalUpdateFieldAction =
BaseUniversalUpdateWorkspaceMigrationAction<'fieldMetadata'>;
@@ -24,12 +27,10 @@ export type UniversalDeleteFieldAction =
BaseUniversalDeleteWorkspaceMigrationAction<'fieldMetadata'>;
// Flat action types (always use entity IDs)
export type FlatCreateFieldAction = Omit<
BaseFlatCreateWorkspaceMigrationAction<'fieldMetadata'>,
'flatEntity'
> & {
flatFieldMetadatas: FlatFieldMetadata[];
};
export type FlatCreateFieldAction =
BaseFlatCreateWorkspaceMigrationAction<'fieldMetadata'> & {
relatedFlatFieldMetadata?: FlatFieldMetadata;
};
export type FlatUpdateFieldAction =
BaseFlatUpdateWorkspaceMigrationAction<'fieldMetadata'>;
@@ -44,7 +44,7 @@ export class WorkspaceMigrationFieldActionsBuilderService extends WorkspaceEntit
action: {
type: 'create',
metadataName: 'fieldMetadata',
universalFlatFieldMetadatas: [flatFieldMetadataToValidate],
flatEntity: flatFieldMetadataToValidate,
},
};
}
@@ -1,4 +1,5 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type';
import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type';
@@ -10,7 +11,10 @@ import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/wor
// Universal action types (always use universal identifiers)
export type UniversalCreateObjectAction =
BaseUniversalCreateWorkspaceMigrationAction<'objectMetadata'> & {
universalFlatFieldMetadatas: UniversalFlatFieldMetadata[];
universalFlatFieldMetadatas: Omit<
UniversalFlatFieldMetadata,
ExtractUniversalForeignKeyAggregatorForMetadataName<'fieldMetadata'>
>[];
// Optional map to provide specific IDs for field creation (for API metadata).
fieldIdByUniversalIdentifier?: Record<string, string>;
};
@@ -10,6 +10,7 @@ import {
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { MetadataUniversalFlatEntityAndRelatedFlatEntityMapsForValidation } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-and-related-flat-entity-maps-for-validation.type';
import { MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { MetadataValidationRelatedUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-related-types.type';
import { MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
@@ -18,9 +19,11 @@ import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-e
import { WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-builder-additional-cache-data-maps.type';
import { MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-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';
import { deleteUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util';
import { deleteUniversalFlatEntityFromUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-from-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
import { deleteUniversalFlatEntityFromUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-from-universal-flat-entity-maps-through-mutation-or-throw.util';
import { replaceUniversalFlatEntityInUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/replace-universal-flat-entity-in-universal-flat-entity-maps-through-mutation-or-throw.util';
import { resetUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { flatEntityDeletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/universal-flat-entity-deleted-created-updated-matrix-dispatcher.util';
import { getMetadataEmptyWorkspaceMigrationActionRecord } from 'src/engine/workspace-manager/workspace-migration/utils/get-metadata-empty-workspace-migration-action-record.util';
import { shouldInferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/utils/should-infer-deletion-from-missing-entities.util';
@@ -29,6 +32,7 @@ import { SuccessfulFlatEntityValidateAndBuild } from 'src/engine/workspace-manag
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
export type ValidateAndBuildArgs<T extends AllMetadataName> = {
@@ -111,23 +115,31 @@ export abstract class WorkspaceEntityMigrationBuilderService<
'creation validation',
);
for (const flatEntityToCreateUniversalIdentifier in createdFlatEntityMaps.byUniversalIdentifier) {
const flatEntityToCreate = findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier: flatEntityToCreateUniversalIdentifier,
flatEntityMaps: createdFlatEntityMaps,
});
const rawUniversalflatEntityToCreate =
findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier: flatEntityToCreateUniversalIdentifier,
flatEntityMaps: createdFlatEntityMaps,
});
const flatEntityToCreateId = flatEntityToCreate.universalIdentifier;
const universalFlatEntityToCreate =
resetUniversalFlatEntityForeignKeyAggregators({
metadataName: this.metadataName,
universalFlatEntity: rawUniversalflatEntityToCreate,
});
const universalIdentifierToDelete =
universalFlatEntityToCreate.universalIdentifier;
deleteUniversalFlatEntityFromUniversalFlatEntityMapsThroughMutationOrThrow(
{
universalIdentifierToDelete: flatEntityToCreateId,
universalIdentifierToDelete,
universalFlatEntityMapsToMutate: remainingFlatEntityMapsToCreate,
},
);
const validationResult = await this.validateFlatEntityCreation({
additionalCacheDataMaps,
flatEntityToValidate: flatEntityToCreate,
flatEntityToValidate: universalFlatEntityToCreate,
workspaceId,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
remainingFlatEntityMapsToValidate: remainingFlatEntityMapsToCreate,
@@ -141,18 +153,26 @@ export abstract class WorkspaceEntityMigrationBuilderService<
addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow(
{
universalFlatEntity: flatEntityToCreate,
universalFlatEntity: universalFlatEntityToCreate,
universalFlatEntityAndRelatedMapsToMutate:
optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
metadataName: this.metadataName,
},
);
actionsResult.create.push(
...(Array.isArray(validationResult.action)
? validationResult.action
: [validationResult.action]),
);
const formattedNewCreateAction: AllUniversalWorkspaceMigrationAction<
'create',
typeof this.metadataName
> = {
...validationResult.action,
flatEntity: deleteUniversalFlatEntityForeignKeyAggregators({
metadataName: this.metadataName,
universalFlatEntity: validationResult.action
.flatEntity as MetadataFlatEntity<T>,
}),
};
actionsResult.create.push(formattedNewCreateAction);
}
this.logger.timeEnd(
@@ -1,12 +1,16 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
import { type WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant';
export type BaseUniversalCreateWorkspaceMigrationAction<
T extends AllMetadataName,
> = {
flatEntity: MetadataUniversalFlatEntity<T>;
flatEntity: Omit<
MetadataUniversalFlatEntity<T>,
ExtractUniversalForeignKeyAggregatorForMetadataName<T>
>;
type: typeof WORKSPACE_MIGRATION_ACTION_TYPE.create;
metadataName: T;
// Optional ID to use when creating the entity (for API metadata).
@@ -5,6 +5,5 @@ export type WorkspaceMigration<
AllUniversalWorkspaceMigrationAction = AllUniversalWorkspaceMigrationAction,
> = {
actions: TActions[];
workspaceId: string;
applicationUniversalIdentifier: string;
};
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import {
FlatCreateAgentAction,
UniversalCreateAgentAction,
@@ -27,6 +28,11 @@ export class CreateAgentActionHandlerService extends WorkspaceMigrationRunnerAct
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateAgentAction>): Promise<FlatCreateAgentAction> {
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'agent',
});
return {
...action,
flatEntity: {
@@ -34,6 +40,7 @@ export class CreateAgentActionHandlerService extends WorkspaceMigrationRunnerAct
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateCommandMenuItemAction,
@@ -36,6 +37,11 @@ export class CreateCommandMenuItemActionHandlerService extends WorkspaceMigratio
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'commandMenuItem',
});
return {
...action,
flatEntity: {
@@ -45,6 +51,7 @@ export class CreateCommandMenuItemActionHandlerService extends WorkspaceMigratio
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -47,40 +47,44 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
context: WorkspaceMigrationActionRunnerArgs<UniversalCreateFieldAction>,
): Promise<FlatCreateFieldAction> {
const { action, allFlatEntityMaps } = context;
const { universalFlatFieldMetadatas, fieldIdByUniversalIdentifier } =
action;
const allFieldIdToBeCreatedInActionByUniversalIdentifierMap = new Map<
string,
string
>();
for (const universalFlatFieldMetadata of universalFlatFieldMetadatas) {
const providedId =
fieldIdByUniversalIdentifier?.[
universalFlatFieldMetadata.universalIdentifier
];
allFieldIdToBeCreatedInActionByUniversalIdentifierMap.set(
action.flatEntity.universalIdentifier,
action.id ?? v4(),
);
if (isDefined(action.relatedUniversalFlatFieldMetadata)) {
allFieldIdToBeCreatedInActionByUniversalIdentifierMap.set(
universalFlatFieldMetadata.universalIdentifier,
providedId ?? v4(),
action.relatedUniversalFlatFieldMetadata.universalIdentifier,
action.relatedFieldId ?? v4(),
);
}
const universalFlatFieldMetadatas = isDefined(
action.relatedUniversalFlatFieldMetadata,
)
? [action.flatEntity, action.relatedUniversalFlatFieldMetadata]
: [action.flatEntity];
const flatFieldMetadatas = universalFlatFieldMetadatas.map(
(universalFlatFieldMetadata) =>
const [flatFieldMetadata, relatedFlatFieldMetadata] =
universalFlatFieldMetadatas.map((universalFlatFieldMetadata) =>
fromUniversalFlatFieldMetadataToFlatFieldMetadata({
universalFlatFieldMetadata,
allFieldIdToBeCreatedInActionByUniversalIdentifierMap,
allFlatEntityMaps,
context,
}),
);
);
return {
type: action.type,
metadataName: action.metadataName,
flatFieldMetadatas,
flatEntity: flatFieldMetadata,
relatedFlatFieldMetadata,
};
}
@@ -88,11 +92,11 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
context: WorkspaceMigrationActionRunnerContext<FlatCreateFieldAction>,
): Promise<void> {
const { queryRunner, flatAction } = context;
const { flatFieldMetadatas } = flatAction;
const { flatEntity, relatedFlatFieldMetadata } = flatAction;
await this.insertFlatEntitiesInRepository({
queryRunner,
flatEntities: flatFieldMetadatas,
flatEntities: [flatEntity, relatedFlatFieldMetadata].filter(isDefined),
});
}
@@ -105,14 +109,14 @@ export class CreateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
allFlatEntityMaps: { flatObjectMetadataMaps },
workspaceId,
} = context;
const { flatFieldMetadatas } = flatAction;
const { flatEntity, relatedFlatFieldMetadata } = flatAction;
const fieldsByObjectMetadataId = new Map<
string,
typeof flatFieldMetadatas
>();
const fieldsByObjectMetadataId = new Map<string, FlatFieldMetadata[]>();
for (const flatFieldMetadata of flatFieldMetadatas) {
for (const flatFieldMetadata of [
flatEntity,
relatedFlatFieldMetadata,
].filter(isDefined)) {
const existingFields = fieldsByObjectMetadataId.get(
flatFieldMetadata.objectMetadataId,
);
@@ -2,7 +2,9 @@ import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { type AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
import { findFieldMetadataIdInCreateFieldContext } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/find-field-metadata-id-in-create-field-context.util';
import { fromUniversalSettingsToFlatFieldMetadataSettings } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/utils/from-universal-settings-to-flat-field-metadata-settings.util';
@@ -10,7 +12,10 @@ import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-ma
export type FromUniversalFlatFieldMetadataToFlatFieldMetadataArgs = {
allFieldIdToBeCreatedInActionByUniversalIdentifierMap: Map<string, string>;
universalFlatFieldMetadata: UniversalFlatFieldMetadata;
universalFlatFieldMetadata: Omit<
UniversalFlatFieldMetadata,
ExtractUniversalForeignKeyAggregatorForMetadataName<'fieldMetadata'>
>;
allFlatEntityMaps: AllFlatEntityMaps;
context: Pick<
WorkspaceMigrationActionRunnerArgs<AllUniversalWorkspaceMigrationAction>,
@@ -35,13 +40,6 @@ export const fromUniversalFlatFieldMetadataToFlatFieldMetadata = ({
objectMetadataUniversalIdentifier,
relationTargetFieldMetadataUniversalIdentifier,
relationTargetObjectMetadataUniversalIdentifier,
viewFilterUniversalIdentifiers: _viewFilterUniversalIdentifiers,
viewFieldUniversalIdentifiers: _viewFieldUniversalIdentifiers,
kanbanAggregateOperationViewUniversalIdentifiers:
_kanbanAggregateOperationViewUniversalIdentifiers,
calendarViewUniversalIdentifiers: _calendarViewUniversalIdentifiers,
mainGroupByFieldMetadataViewUniversalIdentifiers:
_mainGroupByFieldMetadataViewUniversalIdentifiers,
universalSettings,
...restProperties
} = universalFlatFieldMetadata;
@@ -107,6 +105,11 @@ export const fromUniversalFlatFieldMetadataToFlatFieldMetadata = ({
flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'fieldMetadata',
});
return {
...restProperties,
settings,
@@ -130,10 +133,6 @@ export const fromUniversalFlatFieldMetadataToFlatFieldMetadata = ({
calendarViewIds: [],
mainGroupByFieldMetadataViewIds: [],
kanbanAggregateOperationViewIds: [],
viewFieldUniversalIdentifiers: [],
viewFilterUniversalIdentifiers: [],
calendarViewUniversalIdentifiers: [],
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
kanbanAggregateOperationViewUniversalIdentifiers: [],
...emptyUniversalForeignKeyAggregators,
};
};
@@ -11,6 +11,7 @@ import {
FrontComponentException,
FrontComponentExceptionCode,
} from 'src/engine/metadata-modules/front-component/front-component.exception';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import {
FlatCreateFrontComponentAction,
UniversalCreateFrontComponentAction,
@@ -34,6 +35,11 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateFrontComponentAction>): Promise<FlatCreateFrontComponentAction> {
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'frontComponent',
});
return {
...action,
flatEntity: {
@@ -41,6 +47,7 @@ export class CreateFrontComponentActionHandlerService extends WorkspaceMigration
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import {
FlatCreateLogicFunctionAction,
UniversalCreateLogicFunctionAction,
@@ -23,6 +24,11 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateLogicFunctionAction>): Promise<FlatCreateLogicFunctionAction> {
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'logicFunction',
});
return {
...action,
flatEntity: {
@@ -30,6 +36,7 @@ export class CreateLogicFunctionActionHandlerService extends WorkspaceMigrationR
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateNavigationMenuItemAction,
@@ -36,6 +37,11 @@ export class CreateNavigationMenuItemActionHandlerService extends WorkspaceMigra
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'navigationMenuItem',
});
return {
...action,
flatEntity: {
@@ -46,6 +52,7 @@ export class CreateNavigationMenuItemActionHandlerService extends WorkspaceMigra
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -2,12 +2,17 @@ import { isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type ExtractUniversalForeignKeyAggregatorForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-flat-entity-foreign-key-aggregator-properties.constant';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { type AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
export type FromUniversalFlatObjectMetadataToFlatObjectMetadataArgs = {
universalFlatObjectMetadata: UniversalFlatObjectMetadata;
universalFlatObjectMetadata: Omit<
UniversalFlatObjectMetadata,
ExtractUniversalForeignKeyAggregatorForMetadataName<'objectMetadata'>
>;
generatedId: string;
allFlatEntityMaps: AllFlatEntityMaps;
allFieldIdToBeCreatedInActionByUniversalIdentifierMap: Map<string, string>;
@@ -60,9 +65,6 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
imageIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifiers: _viewUniversalIdentifiers,
indexMetadataUniversalIdentifiers: _indexMetadataUniversalIdentifiers,
fieldUniversalIdentifiers: _fieldUniversalIdentifiers,
...restProperties
} = universalFlatObjectMetadata;
@@ -98,6 +100,11 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({
}
}
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'objectMetadata',
});
return {
...restProperties,
dataSourceId,
@@ -115,8 +122,6 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({
fieldIds: [],
viewIds: [],
indexMetadataIds: [],
fieldUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
...emptyUniversalForeignKeyAggregators,
};
};
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreatePageLayoutTabAction,
@@ -35,6 +36,11 @@ export class CreatePageLayoutTabActionHandlerService extends WorkspaceMigrationR
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'pageLayoutTab',
});
return {
...action,
flatEntity: {
@@ -44,6 +50,7 @@ export class CreatePageLayoutTabActionHandlerService extends WorkspaceMigrationR
id: action.id ?? v4(),
workspaceId,
widgetIds: [],
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreatePageLayoutWidgetAction,
@@ -43,6 +44,11 @@ export class CreatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati
flatFieldMetadataMaps: allFlatEntityMaps.flatFieldMetadataMaps,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'pageLayoutWidget',
});
return {
...action,
flatEntity: {
@@ -53,6 +59,7 @@ export class CreatePageLayoutWidgetActionHandlerService extends WorkspaceMigrati
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -9,6 +9,7 @@ import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreatePageLayoutAction,
@@ -70,6 +71,11 @@ export class CreatePageLayoutActionHandlerService extends WorkspaceMigrationRunn
}
}
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'pageLayout',
});
return {
...action,
flatEntity: {
@@ -80,6 +86,7 @@ export class CreatePageLayoutActionHandlerService extends WorkspaceMigrationRunn
id: action.id ?? v4(),
workspaceId,
tabIds: [],
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateRoleTargetAction,
@@ -31,6 +32,11 @@ export class CreateRoleTargetActionHandlerService extends WorkspaceMigrationRunn
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'roleTarget',
});
return {
...action,
flatEntity: {
@@ -39,6 +45,7 @@ export class CreateRoleTargetActionHandlerService extends WorkspaceMigrationRunn
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import {
FlatCreateRoleAction,
UniversalCreateRoleAction,
@@ -27,6 +28,11 @@ export class CreateRoleActionHandlerService extends WorkspaceMigrationRunnerActi
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateRoleAction>): Promise<FlatCreateRoleAction> {
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'role',
});
return {
...action,
flatEntity: {
@@ -40,6 +46,7 @@ export class CreateRoleActionHandlerService extends WorkspaceMigrationRunnerActi
objectPermissionIds: [],
permissionFlagIds: [],
fieldPermissionIds: [],
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -6,6 +6,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateRowLevelPermissionPredicateGroupAction,
@@ -37,6 +38,11 @@ export class CreateRowLevelPermissionPredicateGroupActionHandlerService extends
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'rowLevelPermissionPredicateGroup',
});
return {
...action,
flatEntity: {
@@ -49,6 +55,7 @@ export class CreateRowLevelPermissionPredicateGroupActionHandlerService extends
workspaceId,
rowLevelPermissionPredicateIds: [],
childRowLevelPermissionPredicateGroupIds: [],
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -6,6 +6,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateRowLevelPermissionPredicateAction,
@@ -39,6 +40,11 @@ export class CreateRowLevelPermissionPredicateActionHandlerService extends Works
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'rowLevelPermissionPredicate',
});
return {
...action,
flatEntity: {
@@ -51,6 +57,7 @@ export class CreateRowLevelPermissionPredicateActionHandlerService extends Works
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import {
FlatCreateSkillAction,
UniversalCreateSkillAction,
@@ -27,6 +28,11 @@ export class CreateSkillActionHandlerService extends WorkspaceMigrationRunnerAct
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateSkillAction>): Promise<FlatCreateSkillAction> {
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'skill',
});
return {
...action,
flatEntity: {
@@ -34,6 +40,7 @@ export class CreateSkillActionHandlerService extends WorkspaceMigrationRunnerAct
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -5,6 +5,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateViewFieldGroupAction,
@@ -32,16 +33,21 @@ export class CreateViewFieldGroupActionHandlerService extends WorkspaceMigration
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'viewFieldGroup',
});
return {
...action,
flatEntity: {
...action.flatEntity,
viewId,
viewFieldIds: [],
viewFieldUniversalIdentifiers: [],
id: action.id ?? v4(),
applicationId: flatApplication.id,
workspaceId,
viewFieldIds: [],
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateViewFieldAction,
@@ -36,6 +37,11 @@ export class CreateViewFieldActionHandlerService extends WorkspaceMigrationRunne
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'viewField',
});
return {
...action,
flatEntity: {
@@ -46,6 +52,7 @@ export class CreateViewFieldActionHandlerService extends WorkspaceMigrationRunne
id: action.id ?? v4(),
applicationId: flatApplication.id,
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
type FlatCreateViewFilterGroupAction,
@@ -32,6 +33,11 @@ export class CreateViewFilterGroupActionHandlerService extends WorkspaceMigratio
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'viewFilterGroup',
});
return {
...action,
flatEntity: {
@@ -43,6 +49,7 @@ export class CreateViewFilterGroupActionHandlerService extends WorkspaceMigratio
workspaceId,
childViewFilterGroupIds: [],
viewFilterIds: [],
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateViewFilterAction,
@@ -32,6 +33,11 @@ export class CreateViewFilterActionHandlerService extends WorkspaceMigrationRunn
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'viewFilter',
});
return {
...action,
flatEntity: {
@@ -42,6 +48,7 @@ export class CreateViewFilterActionHandlerService extends WorkspaceMigrationRunn
id: action.id ?? v4(),
applicationId: flatApplication.id,
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateViewGroupAction,
@@ -31,6 +32,11 @@ export class CreateViewGroupActionHandlerService extends WorkspaceMigrationRunne
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'viewGroup',
});
return {
...action,
flatEntity: {
@@ -39,6 +45,7 @@ export class CreateViewGroupActionHandlerService extends WorkspaceMigrationRunne
id: action.id ?? v4(),
applicationId: flatApplication.id,
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateViewAction,
@@ -40,6 +41,11 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'view',
});
return {
...action,
flatEntity: {
@@ -56,6 +62,7 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi
viewGroupIds: [],
viewFilterIds: [],
viewFilterGroupIds: [],
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import {
FlatCreateWebhookAction,
UniversalCreateWebhookAction,
@@ -27,6 +28,11 @@ export class CreateWebhookActionHandlerService extends WorkspaceMigrationRunnerA
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateWebhookAction>): Promise<FlatCreateWebhookAction> {
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'webhook',
});
return {
...action,
flatEntity: {
@@ -34,6 +40,7 @@ export class CreateWebhookActionHandlerService extends WorkspaceMigrationRunnerA
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -15,7 +15,7 @@ import { FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION } from 'src/engine/metadata-modul
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration';
import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.type';
import {
WorkspaceMigrationRunnerException,
WorkspaceMigrationRunnerExceptionCode,
@@ -150,10 +150,12 @@ export class WorkspaceMigrationRunnerService {
}
run = async ({
actions,
applicationUniversalIdentifier,
workspaceMigration: { actions, applicationUniversalIdentifier },
workspaceId,
}: WorkspaceMigration): Promise<{
}: {
workspaceMigration: WorkspaceMigration;
workspaceId: string;
}): Promise<{
allFlatEntityMaps: AllFlatEntityMaps;
metadataEvents: MetadataEvent[];
}> => {
@@ -1,4 +1,4 @@
import { assertUnreachable } from 'twenty-shared/utils';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { type AllFlatWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
import {
@@ -12,7 +12,12 @@ export const deriveMetadataEventsFromCreateAction = (
): MetadataEvent[] => {
switch (flatAction.metadataName) {
case 'fieldMetadata': {
return flatAction.flatFieldMetadatas.map(
const flatFieldMetadatas = [
flatAction.flatEntity,
flatAction.relatedFlatFieldMetadata,
].filter(isDefined);
return flatFieldMetadatas.map(
(flatFieldMetadata): CreateMetadataEvent<'fieldMetadata'> => ({
type: 'created',
recordId: flatFieldMetadata.id,
@@ -1,4 +1,4 @@
import { assertUnreachable } from 'twenty-shared/utils';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
@@ -15,7 +15,12 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({
}: OptimisticallyApplyCreateActionOnAllFlatEntityMapsArgs): AllFlatEntityMaps => {
switch (flatAction.metadataName) {
case 'fieldMetadata': {
flatAction.flatFieldMetadatas.forEach((flatEntity) =>
const flatFieldMetadatas = [
flatAction.flatEntity,
flatAction.relatedFlatFieldMetadata,
].filter(isDefined);
flatFieldMetadatas.forEach((flatEntity) =>
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
flatEntity,
flatEntityAndRelatedMapsToMutate: allFlatEntityMaps,
@@ -0,0 +1,138 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`syncApplication should return workspace migration actions on initial sync then on second sync with field rename and new role 1`] = `
{
"syncApplication": {
"actions": [
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"description": "A support ticket",
"duplicateCriteria": null,
"icon": "IconTicket",
"imageIdentifierFieldMetadataUniversalIdentifier": null,
"isActive": true,
"isAuditLogged": true,
"isCustom": true,
"isLabelSyncedWithName": true,
"isRemote": false,
"isSearchable": false,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataUniversalIdentifier": Any<String>,
"labelPlural": "Tickets",
"labelSingular": "Ticket",
"namePlural": "tickets",
"nameSingular": "ticket",
"shortcut": null,
"standardOverrides": null,
"targetTableName": "DEPRECATED",
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "objectMetadata",
"type": "create",
"universalFlatFieldMetadatas": [
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": "Ticket description",
"icon": "IconFileDescription",
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Description",
"morphId": null,
"name": "description",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
],
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"canAccessAllTools": false,
"canBeAssignedToAgents": true,
"canBeAssignedToApiKeys": true,
"canBeAssignedToUsers": true,
"canDestroyAllObjectRecords": false,
"canReadAllObjectRecords": false,
"canSoftDeleteAllObjectRecords": false,
"canUpdateAllObjectRecords": false,
"canUpdateAllSettings": false,
"createdAt": Any<String>,
"description": "A test role",
"icon": null,
"isEditable": true,
"label": "Test Role",
"rowLevelPermissionPredicateGroupUniversalIdentifiers": [],
"rowLevelPermissionPredicateUniversalIdentifiers": [],
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "role",
"type": "create",
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
exports[`syncApplication should return workspace migration actions on initial sync then on second sync with field rename and new role 2`] = `
{
"syncApplication": {
"actions": [
{
"metadataName": "fieldMetadata",
"type": "update",
"universalIdentifier": Any<String>,
"update": {
"label": "Body",
"name": "body",
},
},
{
"flatEntity": {
"applicationUniversalIdentifier": Any<String>,
"canAccessAllTools": false,
"canBeAssignedToAgents": true,
"canBeAssignedToApiKeys": true,
"canBeAssignedToUsers": true,
"canDestroyAllObjectRecords": false,
"canReadAllObjectRecords": false,
"canSoftDeleteAllObjectRecords": false,
"canUpdateAllObjectRecords": false,
"canUpdateAllSettings": false,
"createdAt": Any<String>,
"description": "A read-only role",
"icon": null,
"isEditable": true,
"label": "Viewer Role",
"rowLevelPermissionPredicateGroupUniversalIdentifiers": [],
"rowLevelPermissionPredicateUniversalIdentifiers": [],
"universalIdentifier": Any<String>,
"updatedAt": Any<String>,
},
"metadataName": "role",
"type": "create",
},
],
"applicationUniversalIdentifier": Any<String>,
},
}
`;
@@ -0,0 +1,155 @@
import { type Manifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
import { createOneApplication } from 'test/integration/metadata/suites/application/utils/create-one-application.util';
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const TEST_SECOND_ROLE_ID = uuidv4();
const TEST_OBJECT_ID = uuidv4();
const TEST_FIELD_ID = uuidv4();
describe('syncApplication', () => {
let appCreated = false;
beforeAll(async () => {
await createOneApplication({
universalIdentifier: TEST_APP_ID,
name: 'Test Application',
description: 'A test application',
version: '1.0.0',
sourcePath: 'test-sync',
expectToFail: false,
});
appCreated = true;
// File upload uses multipart which requires real timers
jest.useRealTimers();
const packageJson = JSON.stringify({
name: 'test-application',
version: '1.0.0',
});
await uploadApplicationFile({
applicationUniversalIdentifier: TEST_APP_ID,
fileFolder: 'Dependencies',
filePath: 'package.json',
fileBuffer: Buffer.from(packageJson),
filename: 'package.json',
expectToFail: false,
});
jest.useFakeTimers();
}, 60000);
afterAll(async () => {
if (!appCreated) {
return;
}
await uninstallApplication({
universalIdentifier: TEST_APP_ID,
expectToFail: false,
});
});
it('should return workspace migration actions on initial sync then on second sync with field rename and new role', async () => {
const initialManifest: Manifest = {
application: {
universalIdentifier: TEST_APP_ID,
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
displayName: 'Test Application',
description: 'A test application for workspace migration',
icon: 'IconTestPipe',
applicationVariables: {},
packageJsonChecksum: null,
yarnLockChecksum: null,
},
roles: [
{
universalIdentifier: TEST_ROLE_ID,
label: 'Test Role',
description: 'A test role',
},
],
objects: [
{
labelIdentifierFieldMetadataUniversalIdentifier: TEST_FIELD_ID,
universalIdentifier: TEST_OBJECT_ID,
nameSingular: 'ticket',
namePlural: 'tickets',
labelSingular: 'Ticket',
labelPlural: 'Tickets',
description: 'A support ticket',
icon: 'IconTicket',
fields: [],
},
],
fields: [
{
universalIdentifier: TEST_FIELD_ID,
type: FieldMetadataType.TEXT,
name: 'description',
label: 'Description',
description: 'Ticket description',
icon: 'IconFileDescription',
objectUniversalIdentifier: TEST_OBJECT_ID,
},
],
logicFunctions: [],
frontComponents: [],
publicAssets: [],
};
const { data: firstSyncData } = await syncApplication({
manifest: initialManifest,
expectToFail: false,
});
expect(firstSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(firstSyncData),
);
const updatedManifest: Manifest = {
...initialManifest,
roles: [
{
universalIdentifier: TEST_ROLE_ID,
label: 'Test Role',
description: 'A test role',
},
{
universalIdentifier: TEST_SECOND_ROLE_ID,
label: 'Viewer Role',
description: 'A read-only role',
},
],
fields: [
{
universalIdentifier: TEST_FIELD_ID,
type: FieldMetadataType.TEXT,
name: 'body',
label: 'Body',
description: 'Ticket description',
icon: 'IconFileDescription',
objectUniversalIdentifier: TEST_OBJECT_ID,
},
],
};
const { data: secondSyncData } = await syncApplication({
manifest: updatedManifest,
expectToFail: false,
});
expect(secondSyncData).toMatchSnapshot(
extractRecordIdsAndDatesAsExpectAny(secondSyncData),
);
}, 60000);
});
@@ -0,0 +1,34 @@
import gql from 'graphql-tag';
export const createOneApplicationQueryFactory = ({
universalIdentifier,
name,
description,
version,
sourcePath,
}: {
universalIdentifier: string;
name: string;
description?: string;
version: string;
sourcePath: string;
}) => ({
query: gql`
mutation CreateOneApplication($input: CreateApplicationInput!) {
createOneApplication(input: $input) {
id
universalIdentifier
name
}
}
`,
variables: {
input: {
universalIdentifier,
name,
description,
version,
sourcePath,
},
},
});
@@ -0,0 +1,57 @@
import { createOneApplicationQueryFactory } from 'test/integration/metadata/suites/application/utils/create-one-application-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
type CreatedApplication = {
id: string;
universalIdentifier: string;
name: string;
};
export const createOneApplication = async ({
universalIdentifier,
name,
description,
version,
sourcePath,
expectToFail = false,
token,
}: {
universalIdentifier: string;
name: string;
description?: string;
version: string;
sourcePath: string;
expectToFail?: boolean;
token?: string;
}): CommonResponseBody<{
createOneApplication: CreatedApplication;
}> => {
const graphqlOperation = createOneApplicationQueryFactory({
universalIdentifier,
name,
description,
version,
sourcePath,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Create one application should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Create one application has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};

Some files were not shown because too many files have changed in this diff Show More