Cascade delete Task targets when tasks deleted - logic + migration command (#17019)
Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
+85
-8
@@ -3,13 +3,76 @@ import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadataTypeValidationArgs } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { validateMorphOrRelationFlatFieldJoinColumName } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-join-column-name.util';
|
||||
import { validateMorphOrRelationFlatFieldOnDelete } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-on-delete.util';
|
||||
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/types/property-update.type';
|
||||
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/utils/find-flat-entity-property-update.util';
|
||||
|
||||
type ValidateMorphOrRelationFlatFieldMetadataUpdatesArgs = Omit<
|
||||
FlatFieldMetadataTypeValidationArgs<MorphOrRelationFieldMetadataType>,
|
||||
'updates'
|
||||
> & {
|
||||
updates: FlatEntityPropertiesUpdates<'fieldMetadata'>;
|
||||
};
|
||||
|
||||
export const validateMorphOrRelationFlatFieldMetadataUpdates = ({
|
||||
flatEntityToValidate: flatFieldMetadataToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
updates,
|
||||
buildOptions,
|
||||
}: ValidateMorphOrRelationFlatFieldMetadataUpdatesArgs): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
const settingsUpdate = findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates: updates,
|
||||
property: 'settings',
|
||||
}) as
|
||||
| PropertyUpdate<
|
||||
FlatFieldMetadata<MorphOrRelationFieldMetadataType>,
|
||||
'settings'
|
||||
>
|
||||
| undefined;
|
||||
|
||||
const toSettings = settingsUpdate?.to;
|
||||
const fromSettings = settingsUpdate?.from;
|
||||
|
||||
const isJoinColumnNameUpdated =
|
||||
isDefined(settingsUpdate) &&
|
||||
isDefined(toSettings?.joinColumnName) &&
|
||||
isDefined(fromSettings?.joinColumnName) &&
|
||||
toSettings.joinColumnName !== fromSettings.joinColumnName;
|
||||
|
||||
if (isJoinColumnNameUpdated) {
|
||||
errors.push(
|
||||
...validateMorphOrRelationFlatFieldJoinColumName({
|
||||
buildOptions,
|
||||
flatFieldMetadata: flatFieldMetadataToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
errors.push(
|
||||
...validateMorphOrRelationFlatFieldOnDelete({
|
||||
flatFieldMetadata: flatFieldMetadataToValidate,
|
||||
}),
|
||||
);
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const validateMorphOrRelationFlatFieldMetadata = ({
|
||||
flatEntityToValidate: flatFieldMetadataToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
@@ -19,6 +82,8 @@ export const validateMorphOrRelationFlatFieldMetadata = ({
|
||||
updates,
|
||||
remainingFlatEntityMapsToValidate,
|
||||
buildOptions,
|
||||
workspaceId,
|
||||
additionalCacheDataMaps,
|
||||
}: FlatFieldMetadataTypeValidationArgs<MorphOrRelationFieldMetadataType>): FlatFieldMetadataValidationError[] => {
|
||||
const { relationTargetFieldMetadataId, relationTargetObjectMetadataId } =
|
||||
flatFieldMetadataToValidate;
|
||||
@@ -120,15 +185,23 @@ export const validateMorphOrRelationFlatFieldMetadata = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!isDefined(updates) ||
|
||||
isDefined(
|
||||
findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates: updates,
|
||||
property: 'settings',
|
||||
// TODO prastoin refactor FlatFieldMetadataTypeValidator to implement two code flow: create and update https://github.com/twentyhq/core-team-issues/issues/2044
|
||||
if (isDefined(updates)) {
|
||||
errors.push(
|
||||
...validateMorphOrRelationFlatFieldMetadataUpdates({
|
||||
flatEntityToValidate: flatFieldMetadataToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
remainingFlatEntityMapsToValidate,
|
||||
workspaceId,
|
||||
updates,
|
||||
buildOptions,
|
||||
additionalCacheDataMaps,
|
||||
}),
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
errors.push(
|
||||
...validateMorphOrRelationFlatFieldJoinColumName({
|
||||
buildOptions,
|
||||
@@ -138,7 +211,11 @@ export const validateMorphOrRelationFlatFieldMetadata = ({
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
}),
|
||||
...validateMorphOrRelationFlatFieldOnDelete({
|
||||
flatFieldMetadata: flatFieldMetadataToValidate,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
export const validateMorphOrRelationFlatFieldOnDelete = ({
|
||||
flatFieldMetadata,
|
||||
}: {
|
||||
flatFieldMetadata: FlatFieldMetadata<MorphOrRelationFieldMetadataType>;
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
if (
|
||||
isDefined(flatFieldMetadata.settings.onDelete) &&
|
||||
flatFieldMetadata.settings.relationType !== RelationType.MANY_TO_ONE
|
||||
) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: 'On delete action is only supported for many to one relations',
|
||||
userFriendlyMessage: msg`On delete action is only supported for many to one relations`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
+37
@@ -89,4 +89,41 @@ export class WorkspaceSchemaForeignKeyManagerService {
|
||||
|
||||
await queryRunner.query(sql);
|
||||
}
|
||||
|
||||
async getForeignKeyName({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnName,
|
||||
}: {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
}): Promise<string | undefined> {
|
||||
const safeSchemaName = removeSqlDDLInjection(schemaName);
|
||||
const safeTableName = removeSqlDDLInjection(tableName);
|
||||
const safeColumnName = removeSqlDDLInjection(columnName);
|
||||
|
||||
const foreignKeys = await queryRunner.query(
|
||||
`
|
||||
SELECT
|
||||
tc.constraint_name AS constraint_name
|
||||
FROM
|
||||
information_schema.table_constraints AS tc
|
||||
JOIN
|
||||
information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
WHERE
|
||||
tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = $1
|
||||
AND tc.table_name = $2
|
||||
AND kcu.column_name = $3
|
||||
`,
|
||||
[safeSchemaName, safeTableName, safeColumnName],
|
||||
);
|
||||
|
||||
return foreignKeys[0]?.constraint_name;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ export const buildTaskTargetStandardFlatFieldMetadatas = ({
|
||||
targetFieldName: 'taskTargets',
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'taskId',
|
||||
},
|
||||
},
|
||||
|
||||
+65
-1
@@ -18,11 +18,13 @@ import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
|
||||
import { PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/types/property-update.type';
|
||||
import { convertOnDeleteActionToOnDelete } from 'src/engine/workspace-manager/workspace-migration/utils/convert-on-delete-action-to-on-delete.util';
|
||||
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/utils/find-flat-entity-property-update.util';
|
||||
import { isPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/utils/is-property-update.util';
|
||||
import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
|
||||
import { UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
|
||||
import { serializeDefaultValue } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/serialize-default-value.util';
|
||||
import {
|
||||
WorkspaceMigrationRunnerException,
|
||||
@@ -221,6 +223,68 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct
|
||||
settings: update.to,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
isMorphOrRelationFlatFieldMetadata(optimisticFlatFieldMetadata) &&
|
||||
isDefined(optimisticFlatFieldMetadata.settings.joinColumnName) &&
|
||||
isPropertyUpdate(update, 'settings') &&
|
||||
isDefined(update.from?.onDelete) &&
|
||||
isDefined(update.to?.onDelete) &&
|
||||
update.to.onDelete !== update.from.onDelete
|
||||
) {
|
||||
const foreignKeyName =
|
||||
await this.workspaceSchemaManagerService.foreignKeyManager.getForeignKeyName(
|
||||
{
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnName: optimisticFlatFieldMetadata.settings.joinColumnName,
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(foreignKeyName)) {
|
||||
throw new WorkspaceMigrationRunnerException(
|
||||
'Foreign key not found',
|
||||
WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceSchemaManagerService.foreignKeyManager.dropForeignKey(
|
||||
{
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
foreignKeyName,
|
||||
},
|
||||
);
|
||||
|
||||
const targetFlatObjectMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId:
|
||||
optimisticFlatFieldMetadata.relationTargetObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const referencedTableName = computeObjectTargetTable(
|
||||
targetFlatObjectMetadata,
|
||||
);
|
||||
|
||||
await this.workspaceSchemaManagerService.foreignKeyManager.createForeignKey(
|
||||
{
|
||||
queryRunner,
|
||||
schemaName,
|
||||
foreignKey: {
|
||||
tableName,
|
||||
columnName: update.to.joinColumnName,
|
||||
referencedTableName,
|
||||
referencedColumnName: 'id',
|
||||
onDelete:
|
||||
convertOnDeleteActionToOnDelete(update.to.onDelete) ??
|
||||
'CASCADE',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user