Morph relation : migration builder (#13173)
This PR will create the migration to be run for the morph relations - We created a dedicated util to generate the column name and refactored a little the code in order to have less dependencies and a clearer devX (updated the snapshot that changed because of this) - Moved the `createMigrationActions` to its own util as well - Created the `MorphRelationColumnActionFactory` based on the relation one --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -2,9 +2,10 @@ module.exports = {
|
||||
plugins: ['@stylistic'],
|
||||
extends: ['../../.eslintrc.global.cjs'],
|
||||
ignorePatterns: [
|
||||
'src/engine/workspace-manager/demo-objects-prefill-data/**',
|
||||
'src/engine/seeder/data-seeds/**',
|
||||
'src/engine/seeder/metadata-seeds/**',
|
||||
'src/engine/workspace-manager/dev-seeder/data/constants/**',
|
||||
'src/engine/workspace-manager/dev-seeder/data/seeds/**',
|
||||
'src/utils/email-providers.ts',
|
||||
'src/engine/core-modules/i18n/locales/generated/**',
|
||||
'src/engine/core-modules/serverless/drivers/constants/base-typescript-project/src/index.ts',
|
||||
],
|
||||
overrides: [
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import omit from 'lodash.omit';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@ -19,6 +19,8 @@ import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modul
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { computeMorphRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-join-column-name.util';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataMorphRelationService {
|
||||
@ -59,12 +61,26 @@ export class FieldMetadataMorphRelationService {
|
||||
const fieldsCreated: FieldMetadataEntity[] = [];
|
||||
|
||||
for (const relation of morphRelationsCreationPayload) {
|
||||
const targetObjectMetadata =
|
||||
objectMetadataMaps.byId[relation.targetObjectMetadataId];
|
||||
|
||||
if (!isDefined(targetObjectMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
'Target object metadata does not exist in the object metadata maps',
|
||||
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const relationFieldMetadataForCreate =
|
||||
await this.fieldMetadataRelationService.addCustomRelationFieldMetadataForCreation(
|
||||
{
|
||||
fieldMetadataInput: fieldMetadataForCreate,
|
||||
relationCreationPayload: relation,
|
||||
objectMetadata,
|
||||
joinColumnName: computeMorphRelationFieldJoinColumnName({
|
||||
name: fieldMetadataForCreate.name,
|
||||
targetObjectMetadataNameSingular:
|
||||
targetObjectMetadata.nameSingular,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -109,7 +125,9 @@ export class FieldMetadataMorphRelationService {
|
||||
? RelationType.MANY_TO_ONE
|
||||
: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
objectMetadata,
|
||||
joinColumnName: computeRelationFieldJoinColumnName({
|
||||
name: targetFieldMetadataToCreate.name,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { Injectable, ValidationError } from '@nestjs/common';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { IsEnum, IsString, IsUUID, validateOrReject } from 'class-validator';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@ -28,6 +28,7 @@ import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/v
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
|
||||
export class RelationCreationPayloadValidation {
|
||||
@IsUUID()
|
||||
@ -103,7 +104,9 @@ export class FieldMetadataRelationService {
|
||||
? RelationType.MANY_TO_ONE
|
||||
: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
objectMetadata,
|
||||
joinColumnName: computeRelationFieldJoinColumnName({
|
||||
name: targetFieldMetadataToCreate.name,
|
||||
}),
|
||||
});
|
||||
|
||||
// todo better type
|
||||
@ -285,11 +288,11 @@ export class FieldMetadataRelationService {
|
||||
addCustomRelationFieldMetadataForCreation({
|
||||
fieldMetadataInput,
|
||||
relationCreationPayload,
|
||||
objectMetadata,
|
||||
joinColumnName,
|
||||
}: {
|
||||
fieldMetadataInput: CreateFieldInput;
|
||||
relationCreationPayload: CreateFieldInput['relationCreationPayload'];
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
joinColumnName: string;
|
||||
}) {
|
||||
const isRelation =
|
||||
isFieldMetadataInterfaceOfType(
|
||||
@ -309,13 +312,6 @@ export class FieldMetadataRelationService {
|
||||
|
||||
const defaultIcon = 'IconRelationOneToMany';
|
||||
|
||||
const joinColumnName = isFieldMetadataInterfaceOfType(
|
||||
fieldMetadataInput,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
? `${fieldMetadataInput.name}${capitalize(objectMetadata.nameSingular)}Id`
|
||||
: `${fieldMetadataInput.name}Id`;
|
||||
|
||||
return {
|
||||
...fieldMetadataInput,
|
||||
icon: fieldMetadataInput.icon ?? defaultIcon,
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
computeColumnName,
|
||||
computeCompositeColumnName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { createMigrationActions } from 'src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util';
|
||||
import { generateRatingOptions } from 'src/engine/metadata-modules/field-metadata/utils/generate-rating-optionts.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util';
|
||||
@ -54,9 +55,9 @@ import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace
|
||||
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { ViewService } from 'src/modules/view/services/view.service';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntity> {
|
||||
@ -578,15 +579,14 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
createdFieldMetadatas.push(...createdFieldMetadataItems);
|
||||
|
||||
const fieldMigrationActions = await this.createMigrationActions({
|
||||
const fieldMigrationActions = await createMigrationActions({
|
||||
createdFieldMetadataItems,
|
||||
objectMetadataMap: objectMetadataMaps.byId,
|
||||
isRemoteCreation: fieldMetadataInput.isRemoteCreation ?? false,
|
||||
workspaceMigrationFactory: this.workspaceMigrationFactory,
|
||||
});
|
||||
|
||||
if (fieldMetadataInput.type !== FieldMetadataType.MORPH_RELATION) {
|
||||
migrationActions.push(...fieldMigrationActions);
|
||||
}
|
||||
migrationActions.push(...fieldMigrationActions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -675,7 +675,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
{
|
||||
fieldMetadataInput: fieldMetadataForCreate,
|
||||
relationCreationPayload: fieldMetadataInput.relationCreationPayload,
|
||||
objectMetadata,
|
||||
joinColumnName: computeRelationFieldJoinColumnName({
|
||||
name: fieldMetadataForCreate.name,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -707,56 +709,4 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async createMigrationActions({
|
||||
createdFieldMetadataItems,
|
||||
objectMetadataMap,
|
||||
isRemoteCreation,
|
||||
}: {
|
||||
createdFieldMetadataItems: FieldMetadataEntity[];
|
||||
objectMetadataMap: ObjectMetadataMaps['byId'];
|
||||
isRemoteCreation: boolean;
|
||||
}): Promise<WorkspaceMigrationTableAction[]> {
|
||||
if (isRemoteCreation) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const migrationActions: WorkspaceMigrationTableAction[] = [];
|
||||
|
||||
for (const createdFieldMetadata of createdFieldMetadataItems) {
|
||||
if (
|
||||
isFieldMetadataEntityOfType(
|
||||
createdFieldMetadata,
|
||||
FieldMetadataType.RELATION,
|
||||
)
|
||||
) {
|
||||
const relationType = createdFieldMetadata.settings?.relationType;
|
||||
|
||||
if (relationType === RelationType.ONE_TO_MANY) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
objectMetadataMap[createdFieldMetadata.objectMetadataId];
|
||||
|
||||
if (!isDefined(objectMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
'Object metadata does not exist',
|
||||
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
migrationActions.push({
|
||||
name: computeObjectTargetTable(objectMetadata),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
createdFieldMetadata,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return migrationActions;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
type ComputeMorphRelationFieldJoinColumnNameArgs = {
|
||||
name: string;
|
||||
targetObjectMetadataNameSingular: string;
|
||||
};
|
||||
|
||||
export const computeMorphRelationFieldJoinColumnName = ({
|
||||
name,
|
||||
targetObjectMetadataNameSingular,
|
||||
}: ComputeMorphRelationFieldJoinColumnNameArgs) => {
|
||||
return `${name}${capitalize(targetObjectMetadataNameSingular)}Id`;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
type ComputeRelationFieldJoinColumnNameArgs = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const computeRelationFieldJoinColumnName = ({
|
||||
name,
|
||||
}: ComputeRelationFieldJoinColumnNameArgs) => {
|
||||
return `${name}Id`;
|
||||
};
|
||||
@ -0,0 +1,77 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationTableAction,
|
||||
WorkspaceMigrationTableActionType,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
export const createMigrationActions = async ({
|
||||
createdFieldMetadataItems,
|
||||
objectMetadataMap,
|
||||
isRemoteCreation,
|
||||
workspaceMigrationFactory,
|
||||
}: {
|
||||
createdFieldMetadataItems: FieldMetadataEntity[];
|
||||
objectMetadataMap: ObjectMetadataMaps['byId'];
|
||||
isRemoteCreation: boolean;
|
||||
workspaceMigrationFactory: WorkspaceMigrationFactory;
|
||||
}): Promise<WorkspaceMigrationTableAction[]> => {
|
||||
if (isRemoteCreation) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const migrationActions: WorkspaceMigrationTableAction[] = [];
|
||||
|
||||
for (const createdFieldMetadata of createdFieldMetadataItems) {
|
||||
if (
|
||||
isFieldMetadataEntityOfType(
|
||||
createdFieldMetadata,
|
||||
FieldMetadataType.RELATION,
|
||||
) ||
|
||||
isFieldMetadataEntityOfType(
|
||||
createdFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
) {
|
||||
const relationType = createdFieldMetadata.settings?.relationType;
|
||||
|
||||
if (relationType === RelationType.ONE_TO_MANY) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
objectMetadataMap[createdFieldMetadata.objectMetadataId];
|
||||
|
||||
if (!isDefined(objectMetadata)) {
|
||||
throw new FieldMetadataException(
|
||||
'Object metadata does not exist',
|
||||
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
migrationActions.push({
|
||||
name: computeObjectTargetTable(objectMetadata),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
createdFieldMetadata,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return migrationActions;
|
||||
};
|
||||
@ -3,30 +3,93 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface';
|
||||
|
||||
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
|
||||
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationColumnAlter,
|
||||
WorkspaceMigrationColumnCreate,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import {
|
||||
WorkspaceMigrationException,
|
||||
WorkspaceMigrationExceptionCode,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
|
||||
|
||||
@Injectable()
|
||||
export class MorphRelationColumnActionFactory extends ColumnActionAbstractFactory<FieldMetadataType.MORPH_RELATION> {
|
||||
protected readonly logger = new Logger(MorphRelationColumnActionFactory.name);
|
||||
|
||||
protected handleCreateAction(
|
||||
_fieldMetadata: FieldMetadataInterface<FieldMetadataType.MORPH_RELATION>,
|
||||
fieldMetadata: FieldMetadataInterface<FieldMetadataType.MORPH_RELATION>,
|
||||
_options?: WorkspaceColumnActionOptions,
|
||||
): WorkspaceMigrationColumnCreate[] {
|
||||
return [];
|
||||
if (!fieldMetadata.settings || !fieldMetadata.settings.joinColumnName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const joinColumnName = fieldMetadata.settings.joinColumnName;
|
||||
|
||||
return [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.CREATE,
|
||||
columnName: joinColumnName,
|
||||
columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID),
|
||||
isArray: false,
|
||||
isNullable: fieldMetadata.isNullable ?? true,
|
||||
isUnique: false,
|
||||
defaultValue: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
protected handleAlterAction(
|
||||
_currentFieldMetadata: FieldMetadataInterface<FieldMetadataType.MORPH_RELATION>,
|
||||
_alteredFieldMetadata: FieldMetadataInterface<FieldMetadataType.MORPH_RELATION>,
|
||||
currentFieldMetadata: FieldMetadataInterface<FieldMetadataType.MORPH_RELATION>,
|
||||
alteredFieldMetadata: FieldMetadataInterface<FieldMetadataType.MORPH_RELATION>,
|
||||
_options?: WorkspaceColumnActionOptions,
|
||||
): WorkspaceMigrationColumnAlter[] {
|
||||
return [];
|
||||
if (!currentFieldMetadata.settings || !alteredFieldMetadata.settings) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
currentFieldMetadata.settings.relationType === RelationType.ONE_TO_MANY
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentJoinColumnName = currentFieldMetadata.settings.joinColumnName;
|
||||
const alteredJoinColumnName = alteredFieldMetadata.settings.joinColumnName;
|
||||
|
||||
if (!currentJoinColumnName || !alteredJoinColumnName) {
|
||||
throw new WorkspaceMigrationException(
|
||||
`Column name not found for current or altered field metadata`,
|
||||
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
action: WorkspaceMigrationColumnActionType.ALTER,
|
||||
currentColumnDefinition: {
|
||||
columnName: currentJoinColumnName,
|
||||
columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID),
|
||||
isArray: false,
|
||||
isNullable: currentFieldMetadata.isNullable ?? true,
|
||||
isUnique: false,
|
||||
defaultValue: null,
|
||||
},
|
||||
alteredColumnDefinition: {
|
||||
columnName: alteredJoinColumnName,
|
||||
columnType: fieldMetadataTypeToColumnType(FieldMetadataType.UUID),
|
||||
isArray: false,
|
||||
isNullable: alteredFieldMetadata.isNullable ?? true,
|
||||
isUnique: false,
|
||||
defaultValue: null,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Object metadata relation target not found for relation creation payload",
|
||||
"message": "Target object metadata does not exist in the object metadata maps",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@ -153,7 +153,7 @@ exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY
|
||||
"exceptionEventId": "mocked-exception-id",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Object metadata relation target not found for relation creation payload",
|
||||
"message": "Target object metadata does not exist in the object metadata maps",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
@ -149,7 +149,7 @@ describe('createOne FieldMetadataService morph relation fields', () => {
|
||||
|
||||
if (isManyToOne) {
|
||||
expect(createdField.settings?.joinColumnName).toBe(
|
||||
'ownerOpportunityForMorphRelationId',
|
||||
'ownerPersonForMorphRelationId',
|
||||
);
|
||||
} else {
|
||||
expect(createdField.settings?.joinColumnName).toBeUndefined();
|
||||
|
||||
Reference in New Issue
Block a user