Add DestroyMany to graphql query runner (#7507)

## Context
destroyMany was not implemented, this PR adds it
This commit is contained in:
Weiko 2024-10-08 17:40:48 +02:00 committed by GitHub
parent e662f6ccb3
commit d5bd320b8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 341 additions and 118 deletions

View File

@ -8,6 +8,7 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service';
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
@ -37,6 +38,8 @@ export class GraphqlQueryResolverFactory {
return this.moduleRef.get(GraphqlQueryCreateManyResolverService);
case 'destroyOne':
return this.moduleRef.get(GraphqlQueryDestroyOneResolverService);
case 'destroyMany':
return this.moduleRef.get(GraphqlQueryDestroyManyResolverService);
case 'updateOne':
case 'deleteOne':
return this.moduleRef.get(GraphqlQueryUpdateOneResolverService);

View File

@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service';
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
@ -16,14 +17,15 @@ import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-que
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
const graphqlQueryResolvers = [
GraphqlQueryFindOneResolverService,
GraphqlQueryFindManyResolverService,
GraphqlQueryFindDuplicatesResolverService,
GraphqlQueryCreateManyResolverService,
GraphqlQueryDestroyManyResolverService,
GraphqlQueryDestroyOneResolverService,
GraphqlQueryUpdateOneResolverService,
GraphqlQueryUpdateManyResolverService,
GraphqlQueryFindDuplicatesResolverService,
GraphqlQueryFindManyResolverService,
GraphqlQueryFindOneResolverService,
GraphqlQuerySearchResolverService,
GraphqlQueryUpdateManyResolverService,
GraphqlQueryUpdateOneResolverService,
];
@Module({

View File

@ -13,6 +13,7 @@ import {
CreateOneResolverArgs,
DeleteManyResolverArgs,
DeleteOneResolverArgs,
DestroyManyResolverArgs,
DestroyOneResolverArgs,
FindDuplicatesResolverArgs,
FindManyResolverArgs,
@ -285,6 +286,25 @@ export class GraphqlQueryRunnerService {
return result;
}
@LogExecutionTime()
async destroyMany<ObjectRecord extends IRecord>(
args: DestroyManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
const result = await this.executeQuery<
DestroyManyResolverArgs,
ObjectRecord[]
>('destroyMany', args, options);
this.apiEventEmitterService.emitDestroyEvents(
result,
options.authContext,
options.objectMetadataItem,
);
return result;
}
@LogExecutionTime()
public async restoreMany<ObjectRecord extends IRecord>(
args: RestoreManyResolverArgs,

View File

@ -27,25 +27,34 @@ export class ObjectRecordsToGraphqlConnectionHelper {
this.objectMetadataMap = objectMetadataMap;
}
public createConnection<ObjectRecord extends IRecord = IRecord>(
objectRecords: ObjectRecord[],
objectName: string,
take: number,
totalCount: number,
order: RecordOrderBy | undefined,
hasNextPage: boolean,
hasPreviousPage: boolean,
public createConnection<ObjectRecord extends IRecord = IRecord>({
objectRecords,
objectName,
take,
totalCount,
order,
hasNextPage,
hasPreviousPage,
depth = 0,
): IConnection<ObjectRecord> {
}: {
objectRecords: ObjectRecord[];
objectName: string;
take: number;
totalCount: number;
order?: RecordOrderBy;
hasNextPage: boolean;
hasPreviousPage: boolean;
depth?: number;
}): IConnection<ObjectRecord> {
const edges = (objectRecords ?? []).map((objectRecord) => ({
node: this.processRecord(
node: this.processRecord({
objectRecord,
objectName,
take,
totalCount,
order,
depth,
),
}),
cursor: encodeCursor(objectRecord, order),
}));
@ -61,14 +70,21 @@ export class ObjectRecordsToGraphqlConnectionHelper {
};
}
public processRecord<T extends Record<string, any>>(
objectRecord: T,
objectName: string,
take: number,
totalCount: number,
order?: RecordOrderBy,
public processRecord<T extends Record<string, any>>({
objectRecord,
objectName,
take,
totalCount,
order,
depth = 0,
): T {
}: {
objectRecord: T;
objectName: string;
take: number;
totalCount: number;
order?: RecordOrderBy;
depth?: number;
}): T {
if (depth >= CONNECTION_MAX_DEPTH) {
throw new GraphqlQueryRunnerException(
`Maximum depth of ${CONNECTION_MAX_DEPTH} reached`,
@ -97,27 +113,31 @@ export class ObjectRecordsToGraphqlConnectionHelper {
if (isRelationFieldMetadataType(fieldMetadata.type)) {
if (Array.isArray(value)) {
processedObjectRecord[key] = this.createConnection(
value,
getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
.nameSingular,
processedObjectRecord[key] = this.createConnection({
objectRecords: value,
objectName: getRelationObjectMetadata(
fieldMetadata,
this.objectMetadataMap,
).nameSingular,
take,
value.length,
totalCount: value.length,
order,
false,
false,
depth + 1,
);
hasNextPage: false,
hasPreviousPage: false,
depth: depth + 1,
});
} else if (isPlainObject(value)) {
processedObjectRecord[key] = this.processRecord(
value,
getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
.nameSingular,
processedObjectRecord[key] = this.processRecord({
objectRecord: value,
objectName: getRelationObjectMetadata(
fieldMetadata,
this.objectMetadataMap,
).nameSingular,
take,
totalCount,
order,
depth + 1,
);
depth: depth + 1,
});
}
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
processedObjectRecord[key] = this.processCompositeField(

View File

@ -93,12 +93,12 @@ export class GraphqlQueryCreateManyResolverService
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return upsertedRecords.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord(
record,
objectMetadataMapItem.nameSingular,
1,
1,
),
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataMapItem.nameSingular,
take: 1,
totalCount: 1,
}),
);
}

View File

@ -0,0 +1,108 @@
import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@Injectable()
export class GraphqlQueryDestroyManyResolverService
implements ResolverService<DestroyManyResolverArgs, IRecord[]>
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async resolve<ObjectRecord extends IRecord = IRecord>(
args: DestroyManyResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord[]> {
const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
);
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
);
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataMapItem.nameSingular,
args.filter,
);
const nonFormattedDeletedObjectRecords = await withFilterQueryBuilder
.delete()
.returning('*')
.execute();
const deletedRecords = formatResult(
nonFormattedDeletedObjectRecords.raw,
objectMetadataMapItem,
objectMetadataMap,
);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
deletedRecords,
relations,
QUERY_MAX_RECORDS,
authContext,
dataSource,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return deletedRecords.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataMapItem.nameSingular,
take: 1,
totalCount: 1,
}),
);
}
async validate(
args: DestroyManyResolverArgs,
_options: WorkspaceQueryRunnerOptions,
): Promise<void> {
if (!args.filter) {
throw new Error('Filter is required');
}
}
}

View File

@ -1,14 +1,20 @@
import { Injectable } from '@nestjs/common';
import graphqlFields from 'graphql-fields';
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
@ -24,19 +30,43 @@ export class GraphqlQueryDestroyOneResolverService
args: DestroyOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord> {
const { authContext, objectMetadataMapItem, objectMetadataMap } = options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
const { authContext, objectMetadataMapItem, objectMetadataMap, info } =
options;
const dataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
authContext.workspace.id,
objectMetadataMapItem.nameSingular,
);
const nonFormattedRecordBeforeDeletion = await repository.findOne({
where: { id: args.id },
withDeleted: true,
});
const repository = dataSource.getRepository(
objectMetadataMapItem.nameSingular,
);
if (!nonFormattedRecordBeforeDeletion) {
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadataMapItem.fields,
objectMetadataMap,
);
const selectedFields = graphqlFields(info);
const { relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataMapItem,
selectedFields,
);
const queryBuilder = repository.createQueryBuilder(
objectMetadataMapItem.nameSingular,
);
const nonFormattedDeletedObjectRecords = await queryBuilder
.where({
id: args.id,
})
.take(1)
.delete()
.returning('*')
.execute();
if (!nonFormattedDeletedObjectRecords.affected) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
@ -44,14 +74,34 @@ export class GraphqlQueryDestroyOneResolverService
}
const recordBeforeDeletion = formatResult(
[nonFormattedRecordBeforeDeletion],
nonFormattedDeletedObjectRecords.raw,
objectMetadataMapItem,
objectMetadataMap,
)[0];
await repository.delete(args.id);
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
return recordBeforeDeletion as ObjectRecord;
if (relations) {
await processNestedRelationsHelper.processNestedRelations(
objectMetadataMap,
objectMetadataMapItem,
[recordBeforeDeletion],
relations,
QUERY_MAX_RECORDS,
authContext,
dataSource,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return typeORMObjectRecordsParser.processRecord({
objectRecord: recordBeforeDeletion,
objectName: objectMetadataMapItem.nameSingular,
take: 1,
totalCount: 1,
});
}
async validate(

View File

@ -88,15 +88,15 @@ export class GraphqlQueryFindDuplicatesResolverService
);
if (isEmpty(duplicateConditions)) {
return typeORMObjectRecordsParser.createConnection(
[],
objectMetadataMapItem.nameSingular,
0,
0,
[{ id: OrderByDirection.AscNullsFirst }],
false,
false,
);
return typeORMObjectRecordsParser.createConnection({
objectRecords: [],
objectName: objectMetadataMapItem.nameSingular,
take: 0,
totalCount: 0,
order: [{ id: OrderByDirection.AscNullsFirst }],
hasNextPage: false,
hasPreviousPage: false,
});
}
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
@ -114,15 +114,15 @@ export class GraphqlQueryFindDuplicatesResolverService
objectMetadataMap,
);
return typeORMObjectRecordsParser.createConnection(
duplicates,
objectMetadataMapItem.nameSingular,
duplicates.length,
duplicates.length,
[{ id: OrderByDirection.AscNullsFirst }],
false,
false,
);
return typeORMObjectRecordsParser.createConnection({
objectRecords: duplicates,
objectName: objectMetadataMapItem.nameSingular,
take: duplicates.length,
totalCount: duplicates.length,
order: [{ id: OrderByDirection.AscNullsFirst }],
hasNextPage: false,
hasPreviousPage: false,
});
}),
);

View File

@ -176,15 +176,15 @@ export class GraphqlQueryFindManyResolverService
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
const result = typeORMObjectRecordsParser.createConnection(
const result = typeORMObjectRecordsParser.createConnection({
objectRecords,
objectMetadataMapItem.nameSingular,
limit,
objectName: objectMetadataMapItem.nameSingular,
take: limit,
totalCount,
orderByWithIdCondition,
order: orderByWithIdCondition,
hasNextPage,
hasPreviousPage,
);
});
return result;
}

View File

@ -113,12 +113,12 @@ export class GraphqlQueryFindOneResolverService
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return typeORMObjectRecordsParser.processRecord(
objectRecords[0],
objectMetadataMapItem.nameSingular,
1,
1,
) as ObjectRecord;
return typeORMObjectRecordsParser.processRecord({
objectRecord: objectRecords[0],
objectName: objectMetadataMapItem.nameSingular,
take: 1,
totalCount: 1,
}) as ObjectRecord;
}
async validate<Filter extends RecordFilter>(

View File

@ -44,15 +44,15 @@ export class GraphqlQuerySearchResolverService
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
if (!args.searchInput) {
return typeORMObjectRecordsParser.createConnection(
[],
objectMetadataItem.nameSingular,
0,
0,
[{ id: OrderByDirection.AscNullsFirst }],
false,
false,
);
return typeORMObjectRecordsParser.createConnection({
objectRecords: [],
objectName: objectMetadataItem.nameSingular,
take: 0,
totalCount: 0,
order: [{ id: OrderByDirection.AscNullsFirst }],
hasNextPage: false,
hasPreviousPage: false,
});
}
const searchTerms = this.formatSearchTerms(args.searchInput);
@ -76,15 +76,15 @@ export class GraphqlQuerySearchResolverService
const totalCount = await repository.count();
const order = undefined;
return typeORMObjectRecordsParser.createConnection(
objectRecords ?? [],
objectMetadataItem.nameSingular,
limit,
return typeORMObjectRecordsParser.createConnection({
objectRecords: objectRecords ?? [],
objectName: objectMetadataItem.nameSingular,
take: limit,
totalCount,
order,
false,
false,
);
hasNextPage: false,
hasPreviousPage: false,
});
}
private formatSearchTerms(searchTerm: string) {

View File

@ -65,15 +65,13 @@ export class GraphqlQueryUpdateManyResolverService
const data = formatData(args.data, objectMetadataMapItem);
const result = await withFilterQueryBuilder
const nonFormattedUpdatedObjectRecords = await withFilterQueryBuilder
.update(data)
.returning('*')
.execute();
const nonFormattedUpdatedObjectRecords = result.raw;
const updatedRecords = formatResult(
nonFormattedUpdatedObjectRecords,
nonFormattedUpdatedObjectRecords.raw,
objectMetadataMapItem,
objectMetadataMap,
);
@ -96,12 +94,12 @@ export class GraphqlQueryUpdateManyResolverService
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return updatedRecords.map((record: ObjectRecord) =>
typeORMObjectRecordsParser.processRecord(
record,
objectMetadataMapItem.nameSingular,
1,
1,
),
typeORMObjectRecordsParser.processRecord({
objectRecord: record,
objectName: objectMetadataMapItem.nameSingular,
take: 1,
totalCount: 1,
}),
);
}
@ -110,6 +108,10 @@ export class GraphqlQueryUpdateManyResolverService
options: WorkspaceQueryRunnerOptions,
): Promise<void> {
assertMutationNotOnRemoteObject(options.objectMetadataMapItem);
args.filter?.id?.in?.forEach((id: string) => assertIsValidUuid(id));
if (!args.filter) {
throw new Error('Filter is required');
}
args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id));
}
}

View File

@ -103,12 +103,12 @@ export class GraphqlQueryUpdateOneResolverService
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
return typeORMObjectRecordsParser.processRecord<ObjectRecord>(
updatedRecord,
objectMetadataMapItem.nameSingular,
1,
1,
);
return typeORMObjectRecordsParser.processRecord<ObjectRecord>({
objectRecord: updatedRecord,
objectName: objectMetadataMapItem.nameSingular,
take: 1,
totalCount: 1,
});
}
async validate<ObjectRecord extends IRecord = IRecord>(

View File

@ -8,8 +8,11 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class DestroyManyResolverFactory
@ -19,6 +22,8 @@ export class DestroyManyResolverFactory
constructor(
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
create(
@ -38,6 +43,19 @@ export class DestroyManyResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
const isQueryRunnerTwentyORMEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
internalContext.authContext.workspace.id,
);
if (isQueryRunnerTwentyORMEnabled) {
return await this.graphqlQueryRunnerService.destroyMany(
args,
options,
);
}
return await this.workspaceQueryRunnerService.destroyMany(
args,
options,