Refactor graphql query runner and add mutation resolvers (#7418)
Fixes https://github.com/twentyhq/twenty/issues/6859 This PR adds all the remaining resolvers for - updateOne/updateMany - createOne/createMany - deleteOne/deleteMany - destroyOne - restoreMany Also - refactored the graphql-query-runner to be able to add other resolvers without too much boilerplate. - add missing events that were not sent anymore as well as webhooks - make resolver injectable so they can inject other services as well - use objectMetadataMap from cache instead of computing it multiple time - various fixes (mutation not correctly parsing JSON, relationHelper fetching data with empty ids set, ...) Next steps: - Wrapping query builder to handle DB events properly - Move webhook emitters to db event listener - Add pagination where it's missing (findDuplicates, nested relations, etc...)
This commit is contained in:
@ -1,51 +1,53 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { In, InsertResult } from 'typeorm';
|
||||
|
||||
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 { CreateManyResolverArgs } 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 { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
|
||||
import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
|
||||
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||
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 { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
export class GraphqlQueryCreateManyResolverService {
|
||||
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
@Injectable()
|
||||
export class GraphqlQueryCreateManyResolverService
|
||||
implements ResolverService<CreateManyResolverArgs, IRecord[]>
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
|
||||
this.twentyORMGlobalManager = twentyORMGlobalManager;
|
||||
}
|
||||
|
||||
async createMany<ObjectRecord extends IRecord = IRecord>(
|
||||
async resolve<ObjectRecord extends IRecord = IRecord>(
|
||||
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord[] | undefined> {
|
||||
const { authContext, objectMetadataItem, objectMetadataCollection, info } =
|
||||
): Promise<ObjectRecord[]> {
|
||||
const { authContext, info, objectMetadataMap, objectMetadataMapItem } =
|
||||
options;
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
const repository = dataSource.getRepository(
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
const objectMetadataMap = generateObjectMetadataMap(
|
||||
objectMetadataCollection,
|
||||
);
|
||||
const objectMetadata = getObjectMetadataOrThrow(
|
||||
objectMetadataMap,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadata.fields,
|
||||
objectMetadataMapItem.fields,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
const { select, relations } = graphqlQueryParser.parseSelectedFields(
|
||||
objectMetadataItem,
|
||||
const { relations } = graphqlQueryParser.parseSelectedFields(
|
||||
objectMetadataMapItem,
|
||||
selectedFields,
|
||||
);
|
||||
|
||||
@ -56,24 +58,59 @@ export class GraphqlQueryCreateManyResolverService {
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
});
|
||||
|
||||
const upsertedRecords = await repository.find({
|
||||
where: {
|
||||
const queryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
const nonFormattedUpsertedRecords = (await queryBuilder
|
||||
.where({
|
||||
id: In(objectRecords.generatedMaps.map((record) => record.id)),
|
||||
},
|
||||
select,
|
||||
relations,
|
||||
});
|
||||
})
|
||||
.take(QUERY_MAX_RECORDS)
|
||||
.getMany()) as ObjectRecord[];
|
||||
|
||||
const upsertedRecords = formatResult(
|
||||
nonFormattedUpsertedRecords,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
|
||||
|
||||
if (relations) {
|
||||
await processNestedRelationsHelper.processNestedRelations(
|
||||
objectMetadataMap,
|
||||
objectMetadataMapItem,
|
||||
upsertedRecords,
|
||||
relations,
|
||||
QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource,
|
||||
);
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
|
||||
|
||||
return upsertedRecords.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord(
|
||||
record,
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async validate<ObjectRecord extends IRecord>(
|
||||
args: CreateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataItem);
|
||||
args.data.forEach((record) => {
|
||||
if (record?.id) {
|
||||
assertIsValidUuid(record.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,33 +1,68 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
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 {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
export class GraphqlQueryDestroyOneResolverService {
|
||||
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
@Injectable()
|
||||
export class GraphqlQueryDestroyOneResolverService
|
||||
implements ResolverService<DestroyOneResolverArgs, IRecord>
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
|
||||
this.twentyORMGlobalManager = twentyORMGlobalManager;
|
||||
}
|
||||
|
||||
async destroyOne<ObjectRecord extends IRecord = IRecord>(
|
||||
async resolve<ObjectRecord extends IRecord = IRecord>(
|
||||
args: DestroyOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord> {
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
const { authContext, objectMetadataMapItem, objectMetadataMap } = options;
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
const record = await repository.findOne({
|
||||
const nonFormattedRecordBeforeDeletion = await repository.findOne({
|
||||
where: { id: args.id },
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (!nonFormattedRecordBeforeDeletion) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Record not found',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const recordBeforeDeletion = formatResult(
|
||||
[nonFormattedRecordBeforeDeletion],
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMap,
|
||||
)[0];
|
||||
|
||||
await repository.delete(args.id);
|
||||
|
||||
return record as ObjectRecord;
|
||||
return recordBeforeDeletion as ObjectRecord;
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: DestroyOneResolverArgs,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
if (!args.id) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Missing id',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,214 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
|
||||
import {
|
||||
Record as IRecord,
|
||||
OrderByDirection,
|
||||
RecordFilter,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
import { FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
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 { settings } from 'src/engine/constants/settings';
|
||||
import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants';
|
||||
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryFindDuplicatesResolverService
|
||||
implements
|
||||
ResolverService<FindDuplicatesResolverArgs, IConnection<IRecord>[]>
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
async resolve<ObjectRecord extends IRecord = IRecord>(
|
||||
args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<ObjectRecord>[]> {
|
||||
const { authContext, objectMetadataMapItem, objectMetadataMap } = options;
|
||||
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||
authContext.workspace.id,
|
||||
);
|
||||
const repository = dataSource.getRepository(
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
const existingRecordsQueryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
const duplicateRecordsQueryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadataMap[objectMetadataMapItem.nameSingular].fields,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
|
||||
|
||||
let objectRecords: Partial<ObjectRecord>[] = [];
|
||||
|
||||
if (args.ids) {
|
||||
const nonFormattedObjectRecords = (await existingRecordsQueryBuilder
|
||||
.where({ id: In(args.ids) })
|
||||
.getMany()) as ObjectRecord[];
|
||||
|
||||
objectRecords = formatResult(
|
||||
nonFormattedObjectRecords,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMap,
|
||||
);
|
||||
} else if (args.data && !isEmpty(args.data)) {
|
||||
objectRecords = formatData(args.data, objectMetadataMapItem);
|
||||
}
|
||||
|
||||
const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all(
|
||||
objectRecords.map(async (record) => {
|
||||
const duplicateConditions = this.buildDuplicateConditions(
|
||||
objectMetadataMapItem,
|
||||
[record],
|
||||
record.id,
|
||||
);
|
||||
|
||||
if (isEmpty(duplicateConditions)) {
|
||||
return typeORMObjectRecordsParser.createConnection(
|
||||
[],
|
||||
objectMetadataMapItem.nameSingular,
|
||||
0,
|
||||
0,
|
||||
[{ id: OrderByDirection.AscNullsFirst }],
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||
duplicateRecordsQueryBuilder,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
duplicateConditions,
|
||||
);
|
||||
|
||||
const nonFormattedDuplicates =
|
||||
(await withFilterQueryBuilder.getMany()) as ObjectRecord[];
|
||||
|
||||
const duplicates = formatResult(
|
||||
nonFormattedDuplicates,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
return typeORMObjectRecordsParser.createConnection(
|
||||
duplicates,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
duplicates.length,
|
||||
duplicates.length,
|
||||
[{ id: OrderByDirection.AscNullsFirst }],
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return duplicateConnections;
|
||||
}
|
||||
|
||||
private buildDuplicateConditions(
|
||||
objectMetadataMapItem: ObjectMetadataMapItem,
|
||||
records?: Partial<IRecord>[] | undefined,
|
||||
filteringByExistingRecordId?: string,
|
||||
): Partial<RecordFilter> {
|
||||
if (!records || records.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const criteriaCollection = this.getApplicableDuplicateCriteriaCollection(
|
||||
objectMetadataMapItem,
|
||||
);
|
||||
|
||||
const conditions = records.flatMap((record) => {
|
||||
const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) =>
|
||||
criteria.columnNames.every((columnName) => {
|
||||
const value = record[columnName] as string | undefined;
|
||||
|
||||
return (
|
||||
value && value.length >= settings.minLengthOfStringForDuplicateCheck
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return criteriaWithMatchingArgs.map((criteria) => {
|
||||
const condition = {};
|
||||
|
||||
criteria.columnNames.forEach((columnName) => {
|
||||
condition[columnName] = { eq: record[columnName] };
|
||||
});
|
||||
|
||||
return condition;
|
||||
});
|
||||
});
|
||||
|
||||
const filter: Partial<RecordFilter> = {};
|
||||
|
||||
if (conditions && !isEmpty(conditions)) {
|
||||
filter.or = conditions;
|
||||
|
||||
if (filteringByExistingRecordId) {
|
||||
filter.id = { neq: filteringByExistingRecordId };
|
||||
}
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
private getApplicableDuplicateCriteriaCollection(
|
||||
objectMetadataMapItem: ObjectMetadataMapItem,
|
||||
) {
|
||||
return DUPLICATE_CRITERIA_COLLECTION.filter(
|
||||
(duplicateCriteria) =>
|
||||
duplicateCriteria.objectName === objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
}
|
||||
|
||||
async validate(
|
||||
args: FindDuplicatesResolverArgs,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
if (!args.data && !args.ids) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'You have to provide either "data" or "ids" argument',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (args.data && args.ids) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'You cannot provide both "data" and "ids" arguments',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (!args.ids && isEmpty(args.data)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'The "data" condition can not be empty when "ids" input not provided',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
|
||||
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
|
||||
import {
|
||||
Record as IRecord,
|
||||
OrderByDirection,
|
||||
@ -17,26 +20,25 @@ import {
|
||||
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 { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
|
||||
import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter';
|
||||
import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||
import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
|
||||
import {
|
||||
ObjectMetadataMapItem,
|
||||
generateObjectMetadataMap,
|
||||
} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||
getCursor,
|
||||
getPaginationInfo,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
export class GraphqlQueryFindManyResolverService {
|
||||
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
@Injectable()
|
||||
export class GraphqlQueryFindManyResolverService
|
||||
implements ResolverService<FindManyResolverArgs, IConnection<IRecord>>
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
|
||||
this.twentyORMGlobalManager = twentyORMGlobalManager;
|
||||
}
|
||||
|
||||
async findMany<
|
||||
async resolve<
|
||||
ObjectRecord extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||
@ -44,51 +46,41 @@ export class GraphqlQueryFindManyResolverService {
|
||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<ObjectRecord>> {
|
||||
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
|
||||
const { authContext, objectMetadataMapItem, info, objectMetadataMap } =
|
||||
options;
|
||||
|
||||
this.validateArgsOrThrow(args);
|
||||
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
const repository = dataSource.getRepository(
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
const queryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
const countQueryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
const objectMetadataMap = generateObjectMetadataMap(
|
||||
objectMetadataCollection,
|
||||
);
|
||||
|
||||
const objectMetadata = getObjectMetadataOrThrow(
|
||||
objectMetadataMap,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadata.fields,
|
||||
objectMetadataMapItem.fields,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||
countQueryBuilder,
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
args.filter ?? ({} as Filter),
|
||||
);
|
||||
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
const { relations } = graphqlQueryParser.parseSelectedFields(
|
||||
objectMetadataItem,
|
||||
objectMetadataMapItem,
|
||||
selectedFields,
|
||||
);
|
||||
const isForwardPagination = !isDefined(args.before);
|
||||
@ -105,7 +97,7 @@ export class GraphqlQueryFindManyResolverService {
|
||||
? await withDeletedCountQueryBuilder.getCount()
|
||||
: 0;
|
||||
|
||||
const cursor = this.getCursor(args);
|
||||
const cursor = getCursor(args);
|
||||
|
||||
let appliedFilters = args.filter ?? ({} as Filter);
|
||||
|
||||
@ -118,7 +110,7 @@ export class GraphqlQueryFindManyResolverService {
|
||||
const cursorArgFilter = computeCursorArgFilter(
|
||||
cursor,
|
||||
orderByWithIdCondition,
|
||||
objectMetadata.fields,
|
||||
objectMetadataMapItem.fields,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
@ -131,14 +123,14 @@ export class GraphqlQueryFindManyResolverService {
|
||||
|
||||
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
|
||||
withFilterQueryBuilder,
|
||||
orderByWithIdCondition,
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
isForwardPagination,
|
||||
);
|
||||
|
||||
@ -153,11 +145,11 @@ export class GraphqlQueryFindManyResolverService {
|
||||
|
||||
const objectRecords = formatResult(
|
||||
nonFormattedObjectRecords,
|
||||
objectMetadata,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
const { hasNextPage, hasPreviousPage } = this.getPaginationInfo(
|
||||
const { hasNextPage, hasPreviousPage } = getPaginationInfo(
|
||||
objectRecords,
|
||||
limit,
|
||||
isForwardPagination,
|
||||
@ -167,14 +159,12 @@ export class GraphqlQueryFindManyResolverService {
|
||||
objectRecords.pop();
|
||||
}
|
||||
|
||||
const processNestedRelationsHelper = new ProcessNestedRelationsHelper(
|
||||
this.twentyORMGlobalManager,
|
||||
);
|
||||
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
|
||||
|
||||
if (relations) {
|
||||
await processNestedRelationsHelper.processNestedRelations(
|
||||
objectMetadataMap,
|
||||
objectMetadata,
|
||||
objectMetadataMapItem,
|
||||
objectRecords,
|
||||
relations,
|
||||
limit,
|
||||
@ -184,20 +174,25 @@ export class GraphqlQueryFindManyResolverService {
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
|
||||
|
||||
return typeORMObjectRecordsParser.createConnection(
|
||||
const result = typeORMObjectRecordsParser.createConnection(
|
||||
objectRecords,
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
limit,
|
||||
totalCount,
|
||||
orderByWithIdCondition,
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private validateArgsOrThrow(args: FindManyResolverArgs<any, any>) {
|
||||
async validate<Filter extends RecordFilter>(
|
||||
args: FindManyResolverArgs<Filter>,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
if (args.first && args.last) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Cannot provide both first and last',
|
||||
@ -235,49 +230,4 @@ export class GraphqlQueryFindManyResolverService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private getCursor(
|
||||
args: FindManyResolverArgs<any, any>,
|
||||
): Record<string, any> | undefined {
|
||||
if (args.after) return decodeCursor(args.after);
|
||||
if (args.before) return decodeCursor(args.before);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private addOrderByColumnsToSelect(
|
||||
order: Record<string, any>,
|
||||
select: Record<string, boolean>,
|
||||
) {
|
||||
for (const column of Object.keys(order || {})) {
|
||||
if (!select[column]) {
|
||||
select[column] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addForeingKeyColumnsToSelect(
|
||||
relations: Record<string, any>,
|
||||
select: Record<string, boolean>,
|
||||
objectMetadata: ObjectMetadataMapItem,
|
||||
) {
|
||||
for (const column of Object.keys(relations || {})) {
|
||||
if (!select[`${column}Id`] && objectMetadata.fields[`${column}Id`]) {
|
||||
select[`${column}Id`] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPaginationInfo(
|
||||
objectRecords: any[],
|
||||
limit: number,
|
||||
isForwardPagination: boolean,
|
||||
) {
|
||||
const hasMoreRecords = objectRecords.length > limit;
|
||||
|
||||
return {
|
||||
hasNextPage: isForwardPagination && hasMoreRecords,
|
||||
hasPreviousPage: !isForwardPagination && hasMoreRecords,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
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,
|
||||
RecordFilter,
|
||||
@ -13,28 +16,31 @@ import {
|
||||
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 { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
|
||||
import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
|
||||
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||
import {
|
||||
WorkspaceQueryRunnerException,
|
||||
WorkspaceQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
export class GraphqlQueryFindOneResolverService {
|
||||
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
@Injectable()
|
||||
export class GraphqlQueryFindOneResolverService
|
||||
implements ResolverService<FindOneResolverArgs, IRecord>
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
|
||||
this.twentyORMGlobalManager = twentyORMGlobalManager;
|
||||
}
|
||||
|
||||
async findOne<
|
||||
async resolve<
|
||||
ObjectRecord extends IRecord = IRecord,
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
>(
|
||||
args: FindOneResolverArgs<Filter>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<ObjectRecord | undefined> {
|
||||
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
|
||||
): Promise<ObjectRecord> {
|
||||
const { authContext, objectMetadataMapItem, info, objectMetadataMap } =
|
||||
options;
|
||||
|
||||
const dataSource =
|
||||
@ -43,37 +49,28 @@ export class GraphqlQueryFindOneResolverService {
|
||||
);
|
||||
|
||||
const repository = dataSource.getRepository(
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
const queryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
const objectMetadataMap = generateObjectMetadataMap(
|
||||
objectMetadataCollection,
|
||||
);
|
||||
|
||||
const objectMetadata = getObjectMetadataOrThrow(
|
||||
objectMetadataMap,
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
);
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadata.fields,
|
||||
objectMetadataMapItem.fields,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
const selectedFields = graphqlFields(info);
|
||||
|
||||
const { relations } = graphqlQueryParser.parseSelectedFields(
|
||||
objectMetadataItem,
|
||||
objectMetadataMapItem,
|
||||
selectedFields,
|
||||
);
|
||||
|
||||
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
args.filter ?? ({} as Filter),
|
||||
);
|
||||
|
||||
@ -86,12 +83,10 @@ export class GraphqlQueryFindOneResolverService {
|
||||
|
||||
const objectRecord = formatResult(
|
||||
nonFormattedObjectRecord,
|
||||
objectMetadata,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
const limit = QUERY_MAX_RECORDS;
|
||||
|
||||
if (!objectRecord) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Record not found',
|
||||
@ -99,32 +94,42 @@ export class GraphqlQueryFindOneResolverService {
|
||||
);
|
||||
}
|
||||
|
||||
const processNestedRelationsHelper = new ProcessNestedRelationsHelper(
|
||||
this.twentyORMGlobalManager,
|
||||
);
|
||||
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
|
||||
|
||||
const objectRecords = [objectRecord];
|
||||
|
||||
if (relations) {
|
||||
await processNestedRelationsHelper.processNestedRelations(
|
||||
objectMetadataMap,
|
||||
objectMetadata,
|
||||
objectMetadataMapItem,
|
||||
objectRecords,
|
||||
relations,
|
||||
limit,
|
||||
QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource,
|
||||
);
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord(
|
||||
objectRecords[0],
|
||||
objectMetadataItem.nameSingular,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
1,
|
||||
1,
|
||||
) as ObjectRecord;
|
||||
}
|
||||
|
||||
async validate<Filter extends RecordFilter>(
|
||||
args: FindOneResolverArgs<Filter>,
|
||||
_options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
if (!args.filter || Object.keys(args.filter).length === 0) {
|
||||
throw new WorkspaceQueryRunnerException(
|
||||
'Missing filter argument',
|
||||
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface';
|
||||
import {
|
||||
Record as IRecord,
|
||||
OrderByDirection,
|
||||
@ -11,47 +14,25 @@ import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
|
||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
export class GraphqlQuerySearchResolverService {
|
||||
private twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
private featureFlagService: FeatureFlagService;
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQuerySearchResolverService
|
||||
implements ResolverService<SearchResolverArgs, IConnection<IRecord>>
|
||||
{
|
||||
constructor(
|
||||
twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
this.twentyORMGlobalManager = twentyORMGlobalManager;
|
||||
this.featureFlagService = featureFlagService;
|
||||
}
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
async search<ObjectRecord extends IRecord = IRecord>(
|
||||
async resolve<ObjectRecord extends IRecord = IRecord>(
|
||||
args: SearchResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<ObjectRecord>> {
|
||||
const { authContext, objectMetadataItem, objectMetadataCollection } =
|
||||
options;
|
||||
|
||||
const featureFlagsForWorkspace =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlags(
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
const isQueryRunnerTwentyORMEnabled =
|
||||
featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED;
|
||||
|
||||
const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED;
|
||||
|
||||
if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'This endpoint is not available yet, please use findMany instead.',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
const { authContext, objectMetadataItem, objectMetadataMap } = options;
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
@ -59,21 +40,8 @@ export class GraphqlQuerySearchResolverService {
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
const objectMetadataMap = generateObjectMetadataMap(
|
||||
objectMetadataCollection,
|
||||
);
|
||||
|
||||
const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular];
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Object metadata not found for ${objectMetadataItem.nameSingular}`,
|
||||
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
|
||||
|
||||
if (!args.searchInput) {
|
||||
return typeORMObjectRecordsParser.createConnection(
|
||||
@ -100,7 +68,7 @@ export class GraphqlQuerySearchResolverService {
|
||||
'DESC',
|
||||
)
|
||||
.setParameter('searchTerms', searchTerms)
|
||||
.limit(limit)
|
||||
.take(limit)
|
||||
.getMany()) as ObjectRecord[];
|
||||
|
||||
const objectRecords = await repository.formatResult(resultsWithTsVector);
|
||||
@ -129,4 +97,26 @@ export class GraphqlQuerySearchResolverService {
|
||||
|
||||
return formattedWords.join(' | ');
|
||||
}
|
||||
|
||||
async validate(
|
||||
_args: SearchResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
const featureFlagsForWorkspace =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlags(
|
||||
options.authContext.workspace.id,
|
||||
);
|
||||
|
||||
const isQueryRunnerTwentyORMEnabled =
|
||||
featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED;
|
||||
|
||||
const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED;
|
||||
|
||||
if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'This endpoint is not available yet, please use findMany instead.',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,116 @@
|
||||
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 { UpdateManyResolverArgs } 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 { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryUpdateManyResolverService
|
||||
implements ResolverService<UpdateManyResolverArgs, IRecord[]>
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
async resolve<ObjectRecord extends IRecord = IRecord>(
|
||||
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
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 data = formatData(args.data, objectMetadataMapItem);
|
||||
|
||||
const result = await withFilterQueryBuilder
|
||||
.update()
|
||||
.set(data)
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
const nonFormattedUpdatedObjectRecords = result.raw;
|
||||
|
||||
const updatedRecords = formatResult(
|
||||
nonFormattedUpdatedObjectRecords,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
|
||||
|
||||
if (relations) {
|
||||
await processNestedRelationsHelper.processNestedRelations(
|
||||
objectMetadataMap,
|
||||
objectMetadataMapItem,
|
||||
updatedRecords,
|
||||
relations,
|
||||
QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource,
|
||||
);
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
|
||||
|
||||
return updatedRecords.map((record: ObjectRecord) =>
|
||||
typeORMObjectRecordsParser.processRecord(
|
||||
record,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async validate<ObjectRecord extends IRecord = IRecord>(
|
||||
args: UpdateManyResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataMapItem);
|
||||
args.filter?.id?.in?.forEach((id: string) => assertIsValidUuid(id));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
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 { UpdateOneResolverArgs } 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 { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
@Injectable()
|
||||
export class GraphqlQueryUpdateOneResolverService
|
||||
implements ResolverService<UpdateOneResolverArgs, IRecord>
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
async resolve<ObjectRecord extends IRecord = IRecord>(
|
||||
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
|
||||
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 = queryBuilder.where({ id: args.id });
|
||||
|
||||
const data = formatData(args.data, objectMetadataMapItem);
|
||||
|
||||
const result = await withFilterQueryBuilder
|
||||
.update()
|
||||
.set(data)
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
const nonFormattedUpdatedObjectRecords = result.raw;
|
||||
|
||||
const updatedRecords = formatResult(
|
||||
nonFormattedUpdatedObjectRecords,
|
||||
objectMetadataMapItem,
|
||||
objectMetadataMap,
|
||||
);
|
||||
|
||||
if (updatedRecords.length === 0) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Record not found',
|
||||
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedRecord = updatedRecords[0] as ObjectRecord;
|
||||
|
||||
const processNestedRelationsHelper = new ProcessNestedRelationsHelper();
|
||||
|
||||
if (relations) {
|
||||
await processNestedRelationsHelper.processNestedRelations(
|
||||
objectMetadataMap,
|
||||
objectMetadataMapItem,
|
||||
[updatedRecord],
|
||||
relations,
|
||||
QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource,
|
||||
);
|
||||
}
|
||||
|
||||
const typeORMObjectRecordsParser =
|
||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
|
||||
|
||||
return typeORMObjectRecordsParser.processRecord<ObjectRecord>(
|
||||
updatedRecord,
|
||||
objectMetadataMapItem.nameSingular,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
async validate<ObjectRecord extends IRecord = IRecord>(
|
||||
args: UpdateOneResolverArgs<Partial<ObjectRecord>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<void> {
|
||||
assertMutationNotOnRemoteObject(options.objectMetadataMapItem);
|
||||
assertIsValidUuid(args.id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user