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
14 changed files with 341 additions and 118 deletions

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>(