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

@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
import { isDefined } from 'src/utils/is-defined';
import { ArgsAliasFactory } from './args-alias.factory';
@ -13,10 +14,18 @@ export class ArgsStringFactory {
create(
initialArgs: Record<string, any> | undefined,
fieldMetadataCollection: FieldMetadataInterface[],
softDeletable?: boolean,
): string | null {
if (!initialArgs) {
return null;
}
if (softDeletable) {
initialArgs.filter = {
and: [initialArgs.filter, { deletedAt: { is: 'NULL' } }].filter(
isDefined,
),
};
}
let argsString = '';
const computedArgs = this.argsAliasFactory.create(
initialArgs,

View File

@ -22,19 +22,23 @@ export class FieldsStringFactory {
private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
) {}
create(
async create(
info: GraphQLResolveInfo,
fieldMetadataCollection: FieldMetadataInterface[],
objectMetadataCollection: ObjectMetadataInterface[],
withSoftDeleted?: boolean,
): Promise<string> {
const selectedFields: Partial<Record> = graphqlFields(info);
return this.createFieldsStringRecursive(
const res = await this.createFieldsStringRecursive(
info,
selectedFields,
fieldMetadataCollection,
objectMetadataCollection,
withSoftDeleted ?? false,
);
return res;
}
async createFieldsStringRecursive(
@ -42,6 +46,7 @@ export class FieldsStringFactory {
selectedFields: Partial<Record>,
fieldMetadataCollection: FieldMetadataInterface[],
objectMetadataCollection: ObjectMetadataInterface[],
withSoftDeleted: boolean,
accumulator = '',
): Promise<string> {
const fieldMetadataMap = new Map(
@ -65,6 +70,7 @@ export class FieldsStringFactory {
fieldMetadata,
objectMetadataCollection,
info,
withSoftDeleted,
);
fieldAlias = alias;
@ -91,6 +97,7 @@ export class FieldsStringFactory {
fieldValue,
fieldMetadataCollection,
objectMetadataCollection,
withSoftDeleted,
accumulator,
);
accumulator += `}\n`;

View File

@ -36,6 +36,7 @@ export class FindManyQueryFactory {
const argsString = this.argsStringFactory.create(
args,
options.fieldMetadataCollection,
!options.withSoftDeleted && !!options.objectMetadataItem.isSoftDeletable,
);
return `

View File

@ -26,10 +26,12 @@ export class FindOneQueryFactory {
options.info,
options.fieldMetadataCollection,
options.objectMetadataCollection,
options.withSoftDeleted,
);
const argsString = this.argsStringFactory.create(
args,
options.fieldMetadataCollection,
!options.withSoftDeleted && !!options.objectMetadataItem.isSoftDeletable,
);
return `

View File

@ -12,7 +12,6 @@ import {
RelationDirection,
} from 'src/engine/utils/deduce-relation-direction.util';
import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
@ -27,7 +26,6 @@ export class RelationFieldAliasFactory {
@Inject(forwardRef(() => FieldsStringFactory))
private readonly fieldsStringFactory: CircularDep<FieldsStringFactory>,
private readonly argsStringFactory: ArgsStringFactory,
private readonly objectMetadataService: ObjectMetadataService,
) {}
create(
@ -36,6 +34,7 @@ export class RelationFieldAliasFactory {
fieldMetadata: FieldMetadataInterface,
objectMetadataCollection: ObjectMetadataInterface[],
info: GraphQLResolveInfo,
withSoftDeleted?: boolean,
): Promise<string> {
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
throw new Error(`Field ${fieldMetadata.name} is not a relation field`);
@ -47,6 +46,7 @@ export class RelationFieldAliasFactory {
fieldMetadata,
objectMetadataCollection,
info,
withSoftDeleted,
);
}
@ -56,6 +56,7 @@ export class RelationFieldAliasFactory {
fieldMetadata: FieldMetadataInterface,
objectMetadataCollection: ObjectMetadataInterface[],
info: GraphQLResolveInfo,
withSoftDeleted?: boolean,
): Promise<string> {
const relationMetadata =
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
@ -98,9 +99,11 @@ export class RelationFieldAliasFactory {
relationDirection === RelationDirection.FROM
) {
const args = getFieldArgumentsByKey(info, fieldKey);
const argsString = this.argsStringFactory.create(
args,
referencedObjectMetadata.fields ?? [],
!withSoftDeleted && !!referencedObjectMetadata.isSoftDeletable,
);
const fieldsString =
await this.fieldsStringFactory.createFieldsStringRecursive(
@ -108,6 +111,7 @@ export class RelationFieldAliasFactory {
fieldValue,
referencedObjectMetadata.fields ?? [],
objectMetadataCollection,
withSoftDeleted ?? false,
);
return `
@ -137,6 +141,7 @@ export class RelationFieldAliasFactory {
fieldValue,
referencedObjectMetadata.fields ?? [],
objectMetadataCollection,
withSoftDeleted ?? false,
);
// Otherwise it means it's a relation destination is of kind ONE

View File

@ -3,6 +3,7 @@ export interface Record {
[key: string]: any;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export type RecordFilter = {

View File

@ -8,4 +8,5 @@ export interface WorkspaceQueryBuilderOptions {
info: GraphQLResolveInfo;
fieldMetadataCollection: FieldMetadataInterface[];
objectMetadataCollection: ObjectMetadataInterface[];
withSoftDeleted?: boolean;
}