feat: soft delete (#6576)

Implement soft delete on standards and custom objects.
This is a temporary solution, when we drop `pg_graphql` we should rely
on the `softDelete` functions of TypeORM.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Jérémy M
2024-08-16 21:20:02 +02:00
committed by GitHub
parent 20d84755bb
commit db54469c8a
118 changed files with 1675 additions and 492 deletions

View File

@ -1,6 +1,7 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsPimaryField } from 'src/engine/twenty-orm/decorators/workspace-is-primary-field.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsPrimaryField } from 'src/engine/twenty-orm/decorators/workspace-is-primary-field.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
@ -13,7 +14,7 @@ export abstract class BaseWorkspaceEntity {
defaultValue: 'uuid',
icon: 'Icon123',
})
@WorkspaceIsPimaryField()
@WorkspaceIsPrimaryField()
@WorkspaceIsSystem()
id: string;
@ -25,7 +26,7 @@ export abstract class BaseWorkspaceEntity {
icon: 'IconCalendar',
defaultValue: 'now',
})
createdAt: Date;
createdAt: string;
@WorkspaceField({
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
@ -35,5 +36,15 @@ export abstract class BaseWorkspaceEntity {
icon: 'IconCalendarClock',
defaultValue: 'now',
})
updatedAt: Date;
updatedAt: string;
@WorkspaceField({
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
type: FieldMetadataType.DATE_TIME,
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
})
@WorkspaceIsNullable()
deletedAt?: string | null;
}

View File

@ -8,7 +8,7 @@ import {
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceCustomObject } from 'src/engine/twenty-orm/decorators/workspace-custom-object.decorator';
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@ -21,7 +21,9 @@ import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/not
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
@WorkspaceCustomObject()
@WorkspaceCustomEntity({
softDelete: true,
})
export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,

View File

@ -1,7 +1,13 @@
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';
export function WorkspaceCustomObject(): ClassDecorator {
interface WorkspaceCustomEntityOptions {
softDelete?: boolean;
}
export function WorkspaceCustomEntity(
options: WorkspaceCustomEntityOptions = {},
): ClassDecorator {
return (target) => {
const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args',
@ -11,6 +17,7 @@ export function WorkspaceCustomObject(): ClassDecorator {
metadataArgsStorage.addExtendedEntities({
target,
gate,
softDelete: options.softDelete,
});
};
}

View File

@ -12,6 +12,7 @@ interface WorkspaceEntityOptions {
icon?: string;
labelIdentifierStandardId?: string;
imageIdentifierStandardId?: string;
softDelete?: boolean;
}
export function WorkspaceEntity(
@ -47,6 +48,7 @@ export function WorkspaceEntity(
isAuditLogged,
isSystem,
gate,
softDelete: options.softDelete,
});
};
}

View File

@ -1,6 +1,6 @@
import { TypedReflect } from 'src/utils/typed-reflect';
export function WorkspaceIsPimaryField(): PropertyDecorator {
export function WorkspaceIsPrimaryField(): PropertyDecorator {
return (object, propertyKey) => {
TypedReflect.defineMetadata(
'workspace:is-primary-field-metadata-args',

View File

@ -22,12 +22,18 @@ type EntitySchemaColumnMap = {
export class EntitySchemaColumnFactory {
create(
fieldMetadataCollection: FieldMetadataEntity[],
softDelete: boolean,
): EntitySchemaColumnMap {
let entitySchemaColumnMap: EntitySchemaColumnMap = {};
for (const fieldMetadata of fieldMetadataCollection) {
const key = fieldMetadata.name;
// Skip deletedAt column if soft delete is not enabled
if (!softDelete && key === 'deletedAt') {
continue;
}
if (isRelationFieldMetadataType(fieldMetadata.type)) {
const relationMetadata =
fieldMetadata.fromRelationMetadata ??

View File

@ -21,6 +21,7 @@ export class EntitySchemaFactory {
): Promise<EntitySchema> {
const columns = this.entitySchemaColumnFactory.create(
objectMetadata.fields,
objectMetadata.isSoftDeletable ?? false,
);
const relations = await this.entitySchemaRelationFactory.create(

View File

@ -54,12 +54,15 @@ export interface WorkspaceEntityMetadataArgs {
/**
* Label identifier.
*/
readonly labelIdentifierStandardId: string | null;
/**
* Image identifier.
*/
readonly imageIdentifierStandardId: string | null;
/**
* Enable soft delete.
*/
readonly softDelete?: boolean;
}

View File

@ -13,4 +13,9 @@ export interface WorkspaceExtendedEntityMetadataArgs {
* Entity gate.
*/
readonly gate?: Gate;
/**
* Enable soft delete.
*/
readonly softDelete?: boolean;
}