Remove direct execution feature flag - WIP (#19254)

Bug fixes exposed by always-on direct execution
1. GraphQL spec compliance — data[field] = null on resolver error
direct-execution.service.ts — Changed from Promise.allSettled (which
lost the responseKey on rejection) to Promise.all with per-field
try/catch; errors now set data[responseKey] = null per spec
2. Empty object arguments skipped (extractArgumentsFromAst)
extract-arguments-from-ast.util.ts — Removed isEmptyObject check;
filter: {}, data: {} now correctly passed to resolvers instead of
silently dropped (which caused permissions to never be checked)
3. orderBy: {} factory default treated as "no ordering"
direct-execution.service.ts — Before calling the resolver, strips
orderBy: {} and orderByForRecords: {} (empty-object factory defaults
that mean "no ordering")
assert-find-many-args.util.ts / assert-group-by-args.util.ts — Accept {}
for orderBy without throwing
4. orderBy: { field: '...' } object auto-coerced to [{ field: '...' }]
array
direct-execution.service.ts — Applies GraphQL list coercion: a
non-array, non-empty orderBy object is wrapped in an array before
assertion and resolver call
5. totalCount and aggregate fields returned as strings from PostgreSQL
graphql-format-result-from-selected-fields.util.ts — Added
coerceAggregateValue that parses numeric strings to numbers for
totalCount, sum*, avg*, min*, max*, count*, percentageOf* fields
Test updates
nested-relation-queries.integration-spec.ts — Updated expected error
message from Yoga schema-validation message to direct execution resolver
message
~30 snapshot files — Updated to reflect direct execution's error
messages (different from Yoga schema-validation messages for input type
errors)
This commit is contained in:
Etienne
2026-04-03 17:23:01 +02:00
committed by GitHub
parent 90597e47ca
commit d562a384c2
102 changed files with 647 additions and 629 deletions
@@ -49,8 +49,17 @@ export const fieldMetadataConfigByFieldName: Record<
{ value: 'OPTION_2' },
] as FieldMetadataDefaultOption[],
},
manyToOneRelationField: {
name: 'manyToOneRelationField',
type: FieldMetadataType.RELATION,
isNullable: true,
settings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'manyToOneRelationFieldId',
},
},
manyToOneRelationFieldId: {
name: 'manyToOneRelationFieldId',
name: 'manyToOneRelationField',
type: FieldMetadataType.RELATION,
isNullable: true,
settings: {
@@ -32,7 +32,7 @@ describe('FilterArgProcessorService', () => {
byUniversalIdentifier[universalId] = {
id: fieldId,
name: fieldName,
name: config.name,
type: config.type ?? FieldMetadataType.TEXT,
isNullable: config.isNullable ?? true,
objectMetadataId: 'object-id',
@@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
import {
compositeTypeDefinitions,
FieldMetadataType,
RelationType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -16,11 +17,13 @@ import {
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/composite-field-metadata-type.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@Injectable()
@@ -109,7 +112,9 @@ export class FilterArgProcessorService {
fieldIdByName: Record<string, string>,
fieldIdByJoinColumnName: Record<string, string>,
): Record<string, unknown> {
const fieldMetadataId = fieldIdByName[key] || fieldIdByJoinColumnName[key];
const resolvedByName = fieldIdByName[key];
const resolvedByJoinColumn = fieldIdByJoinColumnName[key];
const fieldMetadataId = resolvedByName ?? resolvedByJoinColumn;
if (!isDefined(fieldMetadataId)) {
const nameSingular = flatObjectMetadata.nameSingular;
@@ -138,6 +143,38 @@ export class FilterArgProcessorService {
);
}
if (
isDefined(resolvedByName) &&
!isDefined(resolvedByJoinColumn) &&
(isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION) ||
isFlatFieldMetadataOfType(
fieldMetadata,
FieldMetadataType.MORPH_RELATION,
))
) {
if (fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) {
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
name: key,
});
throw new CommonQueryRunnerException(
`Cannot filter by relation field "${key}": use "${joinColumnName}" instead`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER,
{
userFriendlyMessage: msg`Invalid filter: use "${joinColumnName}" to filter by this relation field`,
},
);
}
throw new CommonQueryRunnerException(
`Cannot filter by relation field "${key}"`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER,
{
userFriendlyMessage: msg`Invalid filter: filtering by relation field "${key}" is not supported`,
},
);
}
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
return this.validateAndTransformCompositeFieldFilter(
fieldMetadata,
@@ -6,6 +6,7 @@ import { validateAndTransformValueByFieldType } from './validate-and-transform-v
import { validateArrayOperatorValueOrThrow } from './validate-array-operator-value-or-throw.util';
import { validateIsEmptyArrayOperatorValueOrThrow } from './validate-is-empty-array-operator-value-or-throw.util';
import { validateIsOperatorFilterValueOrThrow } from './validate-is-operator-filter-value-or-throw.util';
import { validateStringOperatorValueOrThrow } from './validate-string-operator-value-or-throw.util';
export const validateAndTransformValueOrThrow = (
operator: FilterOperator,
@@ -50,6 +51,15 @@ export const validateAndTransformValueOrThrow = (
fieldName,
);
case 'like':
case 'ilike':
case 'startsWith':
case 'endsWith':
case 'containsIlike':
validateStringOperatorValueOrThrow(value, operator, fieldName);
return value;
default:
return value;
}
@@ -0,0 +1,24 @@
import { msg } from '@lingui/core/macro';
import { isString } from '@sniptt/guards';
import { type FilterOperator } from 'src/engine/api/common/common-args-processors/filter-arg-processor/types/filter-operator.type';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
export const validateStringOperatorValueOrThrow = (
value: unknown,
operator: FilterOperator,
fieldName: string,
): void => {
if (!isString(value)) {
throw new CommonQueryRunnerException(
`Filter operator "${operator}" requires a string value for field "${fieldName}", got ${typeof value}`,
CommonQueryRunnerExceptionCode.INVALID_ARGS_FILTER,
{
userFriendlyMessage: msg`Invalid filter: "${operator}" operator requires a String`,
},
);
}
};
@@ -25,7 +25,10 @@ import {
CommonQueryArgs,
CommonQueryNames,
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonQueryResult } from 'src/engine/api/common/types/common-query-result.type';
import {
CommonQueryExecutionResult,
CommonQueryResult,
} from 'src/engine/api/common/types/common-query-result.type';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
@@ -98,7 +101,7 @@ export abstract class CommonBaseQueryRunnerService<
public async execute(
args: CommonInput<Args>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<Output> {
): Promise<CommonQueryExecutionResult<Output, Args>> {
const {
authContext,
flatObjectMetadata,
@@ -127,26 +130,32 @@ export abstract class CommonBaseQueryRunnerService<
args.selectedFields,
);
this.validateQueryComplexity(
selectedFieldsResult,
args,
queryRunnerContext,
);
const processedArgs = {
...(await this.processArgs(args, queryRunnerContext, this.operationName)),
selectedFieldsResult,
} as CommonExtendedInput<Args>;
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () =>
this.executeQueryAndEnrichResults(
processedArgs,
queryRunnerContext,
commonQueryParser,
),
authContext,
this.validateQueryComplexity(
selectedFieldsResult,
processedArgs,
queryRunnerContext,
);
const results =
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () =>
this.executeQueryAndEnrichResults(
processedArgs,
queryRunnerContext,
commonQueryParser,
),
authContext,
);
return {
results,
args: processedArgs,
};
}
protected abstract run(
@@ -174,7 +183,7 @@ export abstract class CommonBaseQueryRunnerService<
protected computeQueryComplexity(
selectedFieldsResult: CommonSelectedFieldsResult,
_args: CommonInput<Args>,
_args: CommonExtendedInput<Args>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): number {
const simpleFieldsComplexity = 1;
@@ -387,7 +396,7 @@ export abstract class CommonBaseQueryRunnerService<
private validateQueryComplexity(
selectedFieldsResult: CommonSelectedFieldsResult,
args: CommonInput<Args>,
args: CommonExtendedInput<Args>,
queryRunnerContext: CommonBaseQueryRunnerContext,
) {
const maximumComplexity = this.twentyConfigService.get(
@@ -318,7 +318,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
protected override computeQueryComplexity(
selectedFieldsResult: CommonSelectedFieldsResult,
args: CommonInput<FindManyQueryArgs>,
args: CommonExtendedInput<FindManyQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): number {
const baseComplexity = super.computeQueryComplexity(
@@ -3,6 +3,10 @@ import { type ObjectRecord } from 'twenty-shared/types';
import { type CommonFindDuplicatesOutputItem } from 'src/engine/api/common/types/common-find-duplicates-output-item.type';
import { type CommonFindManyOutput } from 'src/engine/api/common/types/common-find-many-output.type';
import { type CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
import {
CommonExtendedInput,
CommonQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
export type CommonQueryResult =
| ObjectRecord[]
@@ -10,3 +14,11 @@ export type CommonQueryResult =
| CommonGroupByOutputItem[]
| CommonFindManyOutput
| CommonFindDuplicatesOutputItem[];
export type CommonQueryExecutionResult<
Output extends CommonQueryResult,
Args extends CommonQueryArgs,
> = {
results: Output;
args: CommonExtendedInput<Args>;
};
@@ -203,74 +203,73 @@ export class DirectExecutionService {
const variables = req.body.variables ?? {};
const data: Record<string, unknown> = {};
const { graphQLResolverNameMap } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'graphQLResolverNameMap',
]);
const {
graphQLResolverNameMap,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
} = await this.loadWorkspaceMetadata(workspaceId);
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'graphQLResolverNameMap',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
]);
const results = await Promise.allSettled(
const { idByNameSingular: objectIdByNameSingular } =
buildObjectIdByNameMaps(flatObjectMetadataMaps);
const errors: GraphQLFormattedError[] = [];
await Promise.all(
topLevelFields.map(async (field) => {
const entry = graphQLResolverNameMap[field.name.value];
const responseKey = field.alias?.value ?? field.name.value;
const args = extractArgumentsFromAst(field.arguments, variables);
try {
const args = extractArgumentsFromAst(field.arguments, variables);
const graphqlPartialResolveInfo = graphQLBuildPartialResolveInfo(
field,
fragmentMap,
);
const workspaceSchemaBuilderContext =
buildWorkspaceSchemaBuilderContext(
entry,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
const graphqlPartialResolveInfo = graphQLBuildPartialResolveInfo(
field,
fragmentMap,
);
const result = (await this.executeField({
entry,
args,
graphqlPartialResolveInfo,
workspaceSchemaBuilderContext,
})) as ResolverOutput;
const workspaceSchemaBuilderContext =
buildWorkspaceSchemaBuilderContext(
entry,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
);
const formattedResult = graphQLFormatResultFromSelectedFields(
result,
graphqlFields(
graphqlPartialResolveInfo as GraphQLResolveInfo,
{},
{ excludedFields: [] },
),
workspaceSchemaBuilderContext.flatObjectMetadata.nameSingular,
{
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
method: entry.method,
},
);
const result = (await this.executeField({
entry,
args,
graphqlPartialResolveInfo,
workspaceSchemaBuilderContext,
})) as ResolverOutput;
return { responseKey, result: formattedResult };
const formattedResult = graphQLFormatResultFromSelectedFields(
result,
graphqlFields(
graphqlPartialResolveInfo as GraphQLResolveInfo,
{},
{ excludedFields: [] },
),
workspaceSchemaBuilderContext.flatObjectMetadata.nameSingular,
{
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
method: entry.method,
},
);
data[responseKey] = formattedResult;
} catch (error) {
data[responseKey] = null;
errors.push(this.formatError(error, req));
}
}),
);
const errors: GraphQLFormattedError[] = [];
for (const settled of results) {
if (settled.status === 'fulfilled') {
data[settled.value.responseKey] = settled.value.result;
} else {
errors.push(this.formatError(settled.reason, req));
}
}
if (errors.length > 0) {
return { data, errors };
}
@@ -440,24 +439,4 @@ export class DirectExecutionService {
seen.add(name);
}
}
private async loadWorkspaceMetadata(workspaceId: string) {
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
await this.workspaceFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
},
);
const { idByNameSingular } = buildObjectIdByNameMaps(
flatObjectMetadataMaps,
);
return {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular: idByNameSingular,
};
}
}
@@ -1,7 +1,6 @@
import { type Request } from 'express';
import { DocumentNode, parse } from 'graphql';
import { type Plugin } from 'graphql-yoga';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isNull } from '@sniptt/guards';
import { type DirectExecutionService } from 'src/engine/api/graphql/direct-execution/direct-execution.service';
@@ -26,16 +25,6 @@ export function useDirectExecution(
return;
}
const isDirectExecutionEnabled =
await config.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_DIRECT_GRAPHQL_EXECUTION_ENABLED,
req.workspace.id,
);
if (!isDirectExecutionEnabled) {
return;
}
const queryString = req.body.query as string;
const operationName = req.body.operationName as string | undefined;
@@ -1,10 +1,10 @@
import { isObject } from 'class-validator';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import {
GraphqlDirectExecutionException,
GraphqlDirectExecutionExceptionCode,
} from 'src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { type DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
export function assertDeleteManyArgs(
@@ -1,10 +1,10 @@
import { isObject } from 'class-validator';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import {
GraphqlDirectExecutionException,
GraphqlDirectExecutionExceptionCode,
} from 'src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { type DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
export function assertDestroyManyArgs(
@@ -1,6 +1,6 @@
import { isArray, isNumber, isObject, isString } from 'class-validator';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isEmptyObject } from 'twenty-shared/utils';
import {
GraphqlDirectExecutionException,
@@ -66,7 +66,13 @@ export function assertFindManyArgs(
);
}
if ('orderBy' in args && isDefined(args.orderBy) && !isArray(args.orderBy)) {
if (
'orderBy' in args &&
isDefined(args.orderBy) &&
!isEmptyObject(args.orderBy) &&
!isArray(args.orderBy) &&
!isObject(args.orderBy)
) {
throw new GraphqlDirectExecutionException(
'Invalid argument: "orderBy" must be an array',
GraphqlDirectExecutionExceptionCode.INVALID_QUERY_INPUT,
@@ -2,11 +2,11 @@ import { isObject } from 'class-validator';
import { isDefined } from 'twenty-shared/utils';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import {
GraphqlDirectExecutionException,
GraphqlDirectExecutionExceptionCode,
} from 'src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { type FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
export function assertFindOneArgs(
@@ -6,7 +6,7 @@ import {
isString,
} from 'class-validator';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isEmptyObject } from 'twenty-shared/utils';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import {
@@ -68,7 +68,13 @@ export function assertGroupByArgs(
);
}
if ('orderBy' in args && isDefined(args.orderBy) && !isArray(args.orderBy)) {
if (
'orderBy' in args &&
isDefined(args.orderBy) &&
!isEmptyObject(args.orderBy) &&
!isArray(args.orderBy) &&
!isObject(args.orderBy)
) {
throw new GraphqlDirectExecutionException(
'Invalid argument: "orderBy" must be an array',
GraphqlDirectExecutionExceptionCode.INVALID_QUERY_INPUT,
@@ -79,7 +85,9 @@ export function assertGroupByArgs(
if (
'orderByForRecords' in args &&
isDefined(args.orderByForRecords) &&
!isArray(args.orderByForRecords)
!isEmptyObject(args.orderByForRecords) &&
!isArray(args.orderByForRecords) &&
!isObject(args.orderByForRecords)
) {
throw new GraphqlDirectExecutionException(
'Invalid argument: "orderByForRecords" must be an array',
@@ -1,10 +1,10 @@
import { isObject } from 'class-validator';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import {
GraphqlDirectExecutionException,
GraphqlDirectExecutionExceptionCode,
} from 'src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { type RestoreManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
export function assertRestoreManyArgs(
@@ -1,10 +1,10 @@
import { isObject } from 'class-validator';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import {
GraphqlDirectExecutionException,
GraphqlDirectExecutionExceptionCode,
} from 'src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { type UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
export function assertUpdateManyArgs(
@@ -1,5 +1,5 @@
import { type ArgumentNode, valueFromASTUntyped } from 'graphql';
import { isDefined, isEmptyObject } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
// Converts GraphQL AST argument nodes into a plain JS object,
// resolving variable references from the variables map.
@@ -15,8 +15,9 @@ export const extractArgumentsFromAst = (
for (const arg of argumentNodes) {
const value = valueFromASTUntyped(arg.value, variables);
if (!isDefined(value) || isEmptyObject(value)) continue;
result[arg.name.value] = valueFromASTUntyped(arg.value, variables);
if (!isDefined(value)) continue;
result[arg.name.value] = value;
}
return result;
@@ -1,5 +1,9 @@
import { isNull } from '@sniptt/guards';
import { ObjectRecord, RelationType } from 'twenty-shared/types';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import {
FieldMetadataType,
ObjectRecord,
RelationType,
} from 'twenty-shared/types';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import {
@@ -181,6 +185,16 @@ const backfillNullValuesAndComputeTypeNameForObjectRecord = (
continue;
}
if (
isDefined(fieldMetadata) &&
fieldMetadata.type === FieldMetadataType.NUMBER &&
isNonEmptyString(value) &&
isFinite(Number(value))
) {
formatted[key] = Number(value);
continue;
}
formatted[key] = value;
}
@@ -275,9 +289,13 @@ const backfillNullValuesAndComputeTypeNameForConnection = (
continue;
}
//aggregate fields
formatted[key] =
const rawAggregateValue =
(connection as unknown as Record<string, unknown>)[key] ?? null;
formatted[key] =
isNonEmptyString(rawAggregateValue) && isFinite(Number(rawAggregateValue))
? Number(rawAggregateValue)
: rawAggregateValue;
}
return formatted;
@@ -1,4 +1,4 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { type GqlOptionsFactory } from '@nestjs/graphql';
@@ -31,6 +31,7 @@ import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hook
import { useValidateGraphqlQueryComplexity } from 'src/engine/core-modules/graphql/hooks/use-validate-graphql-query-complexity.hook';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
@@ -46,6 +47,8 @@ export interface GraphQLContext extends YogaDriverServerContext<'express'> {
export class GraphQLConfigService
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
{
private readonly logger = new Logger(GraphQLConfigService.name);
constructor(
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly twentyConfigService: TwentyConfigService,
@@ -102,6 +105,10 @@ export class GraphQLConfigService
return new GraphQLSchema({});
}
this.logger.log(
`Creating schema for workspace ${workspace.id} for request ${context?.req?.body?.operationName}`,
);
return await this.createSchema(context, workspace, application?.id);
} catch (error) {
if (error instanceof UnauthorizedException) {
@@ -188,6 +195,11 @@ export class GraphQLConfigService
},
);
await this.metricsService.incrementCounter({
key: MetricsKeys.GraphqlSchemaBuild,
shouldStoreInCache: false,
});
return await workspaceFactory.createGraphQLSchema(workspace, applicationId);
}
}
@@ -8,6 +8,7 @@ import {
type ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type GroupByField } from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser';
import { GraphqlQueryOrderGroupByParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order-group-by.parser';
import {
@@ -19,7 +20,6 @@ import {
GraphqlQuerySelectedFieldsParser,
type GraphqlQuerySelectedFieldsResult,
} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { type GroupByField } from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -39,10 +39,11 @@ export class CreateManyResolverFactory
});
try {
const records = await this.commonCreateManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: records } =
await this.commonCreateManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -38,10 +38,11 @@ export class CreateOneResolverFactory
});
try {
const record = await this.commonCreateOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: record } =
await this.commonCreateOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -39,10 +39,11 @@ export class DeleteManyResolverFactory
});
try {
const records = await this.commonDeleteManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: records } =
await this.commonDeleteManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -38,10 +38,11 @@ export class DeleteOneResolverFactory
});
try {
const record = await this.commonDeleteOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: record } =
await this.commonDeleteOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -39,10 +39,11 @@ export class DestroyManyResolverFactory
});
try {
const records = await this.commonDestroyManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: records } =
await this.commonDestroyManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -38,10 +38,11 @@ export class DestroyOneResolverFactory
});
try {
const record = await this.commonDestroyOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: record } =
await this.commonDestroyOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -39,7 +39,7 @@ export class FindDuplicatesResolverFactory
});
try {
const paginatedDuplicates =
const { results: paginatedDuplicates } =
await this.commonFindDuplicatesQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
@@ -40,11 +40,14 @@ export class FindManyResolverFactory
try {
const {
records,
aggregatedValues,
totalCount,
pageInfo,
selectedFieldsResult,
results: {
records,
aggregatedValues,
totalCount,
pageInfo,
selectedFieldsResult,
},
args: processedArgs,
} = await this.commonFindManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
@@ -64,7 +67,7 @@ export class FindManyResolverFactory
objectName: resolverContext.flatObjectMetadata.nameSingular,
take: args.first ?? args.last ?? QUERY_MAX_RECORDS,
totalCount,
order: args.orderBy,
order: processedArgs.orderBy,
hasNextPage: pageInfo.hasNextPage,
hasPreviousPage: pageInfo.hasPreviousPage,
});
@@ -38,10 +38,11 @@ export class FindOneResolverFactory
workspaceSchemaBuilderContext: internalContext,
});
const record = await this.commonFindOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: record } =
await this.commonFindOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -50,7 +50,7 @@ export class GroupByResolverFactory
resolverContext.objectIdByNameSingular,
);
const results = await this.commonGroupByQueryRunnerService.execute(
const { results } = await this.commonGroupByQueryRunnerService.execute(
{ ...args, selectedFields, includeRecords: shouldIncludeRecords },
resolverContext,
);
@@ -38,10 +38,11 @@ export class MergeManyResolverFactory
});
try {
const record = await this.commonMergeManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: record } =
await this.commonMergeManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -39,10 +39,11 @@ export class RestoreManyResolverFactory
});
try {
const records = await this.commonRestoreManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: records } =
await this.commonRestoreManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -38,10 +38,11 @@ export class RestoreOneResolverFactory
});
try {
const record = await this.commonRestoreOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: record } =
await this.commonRestoreOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -39,10 +39,11 @@ export class UpdateManyResolverFactory
});
try {
const records = await this.commonUpdateManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: records } =
await this.commonUpdateManyQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -38,10 +38,11 @@ export class UpdateOneResolverFactory
});
try {
const record = await this.commonUpdateOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const { results: record } =
await this.commonUpdateOneQueryRunnerService.execute(
{ ...args, selectedFields },
resolverContext,
);
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(
@@ -9,8 +9,6 @@ import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-
import { WorkspaceGraphqlSchemaSDLService } from 'src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service';
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
@@ -20,7 +18,6 @@ export class WorkspaceSchemaFactory {
private readonly scalarsExplorerService: ScalarsExplorerService,
private readonly workspaceResolverFactory: WorkspaceResolverFactory,
private readonly workspaceGraphqlSchemaSDLService: WorkspaceGraphqlSchemaSDLService,
private readonly metricsService: MetricsService,
) {}
async createGraphQLSchema(
@@ -67,11 +64,6 @@ export class WorkspaceSchemaFactory {
},
});
await this.metricsService.incrementCounter({
key: MetricsKeys.GraphqlSchemaBuild,
shouldStoreInCache: false,
});
return executableSchema;
}
}
@@ -37,16 +37,17 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
authContext,
});
const records = await this.commonCreateManyQueryRunnerService.execute(
{ data, selectedFields, upsert },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: records } =
await this.commonCreateManyQueryRunnerService.execute(
{ data, selectedFields, upsert },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(records, flatObjectMetadata.namePlural);
} catch (error) {
@@ -38,16 +38,17 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
authContext,
});
const record = await this.commonCreateOneQueryRunnerService.execute(
{ data, selectedFields, upsert },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: record } =
await this.commonCreateOneQueryRunnerService.execute(
{ data, selectedFields, upsert },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(record, flatObjectMetadata.nameSingular);
} catch (error) {
@@ -40,16 +40,17 @@ export class RestApiDeleteManyHandler extends RestApiBaseHandler {
} = await this.buildCommonOptions(request);
try {
const records = await this.commonDeleteManyQueryRunnerService.execute(
{ filter, selectedFields: { id: true } },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: records } =
await this.commonDeleteManyQueryRunnerService.execute(
{ filter, selectedFields: { id: true } },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(records, flatObjectMetadata.namePlural);
} catch (error) {
@@ -29,16 +29,17 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler {
objectIdByNameSingular,
} = await this.buildCommonOptions(request);
const record = await this.commonDeleteOneQueryRunnerService.execute(
{ id, selectedFields: { id: true } },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: record } =
await this.commonDeleteOneQueryRunnerService.execute(
{ id, selectedFields: { id: true } },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(record, flatObjectMetadata.nameSingular);
} catch (error) {
@@ -40,16 +40,17 @@ export class RestApiDestroyManyHandler extends RestApiBaseHandler {
} = await this.buildCommonOptions(request);
try {
const records = await this.commonDestroyManyQueryRunnerService.execute(
{ filter, selectedFields: { id: true } },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: records } =
await this.commonDestroyManyQueryRunnerService.execute(
{ filter, selectedFields: { id: true } },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(records, flatObjectMetadata.namePlural);
} catch (error) {
@@ -29,16 +29,17 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler {
objectIdByNameSingular,
} = await this.buildCommonOptions(request);
const record = await this.commonDestroyOneQueryRunnerService.execute(
{ id, selectedFields: { id: true } },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: record } =
await this.commonDestroyOneQueryRunnerService.execute(
{ id, selectedFields: { id: true } },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(record, flatObjectMetadata.nameSingular);
} catch (error) {
@@ -36,7 +36,7 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
authContext,
});
const duplicateConnections =
const { results: duplicateConnections } =
await this.commonFindDuplicatesQueryRunnerService.execute(
{ data, ids, selectedFields },
{
@@ -43,20 +43,21 @@ export class RestApiFindManyHandler extends RestApiBaseHandler {
authContext,
});
const { records, aggregatedValues, pageInfo } =
await this.commonFindManyQueryRunnerService.execute(
{
...parsedArgs,
selectedFields: { ...selectedFields, totalCount: true },
},
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const {
results: { records, aggregatedValues, pageInfo },
} = await this.commonFindManyQueryRunnerService.execute(
{
...parsedArgs,
selectedFields: { ...selectedFields, totalCount: true },
},
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(
records,
@@ -36,16 +36,17 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
authContext,
});
const record = await this.commonFindOneQueryRunnerService.execute(
{ filter, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: record } =
await this.commonFindOneQueryRunnerService.execute(
{ filter, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(record, flatObjectMetadata.nameSingular);
} catch (error) {
@@ -41,7 +41,7 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
limit,
} = await this.parseRequestArgs(request);
return await this.commonGroupByQueryRunnerService.execute(
const { results } = await this.commonGroupByQueryRunnerService.execute(
{
filter,
orderBy,
@@ -60,6 +60,8 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
objectIdByNameSingular,
},
);
return results;
} catch (error) {
return workspaceQueryRunnerRestApiExceptionHandler(error);
}
@@ -36,16 +36,17 @@ export class RestApiMergeManyHandler extends RestApiBaseHandler {
authContext,
});
const record = await this.commonMergeManyQueryRunnerService.execute(
{ ...restArgs, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: record } =
await this.commonMergeManyQueryRunnerService.execute(
{ ...restArgs, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(record, flatObjectMetadata.nameSingular);
} catch (error) {
@@ -42,16 +42,17 @@ export class RestApiRestoreManyHandler extends RestApiBaseHandler {
authContext,
});
const records = await this.commonRestoreManyQueryRunnerService.execute(
{ filter, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: records } =
await this.commonRestoreManyQueryRunnerService.execute(
{ filter, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(records, flatObjectMetadata.namePlural);
} catch (error) {
@@ -38,16 +38,17 @@ export class RestApiRestoreOneHandler extends RestApiBaseHandler {
authContext,
});
const record = await this.commonRestoreOneQueryRunnerService.execute(
{ id, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: record } =
await this.commonRestoreOneQueryRunnerService.execute(
{ id, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(record, flatObjectMetadata.nameSingular);
} catch (error) {
@@ -38,16 +38,17 @@ export class RestApiUpdateManyHandler extends RestApiBaseHandler {
authContext,
});
const records = await this.commonUpdateManyQueryRunnerService.execute(
{ data, filter, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: records } =
await this.commonUpdateManyQueryRunnerService.execute(
{ data, filter, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(records, flatObjectMetadata.namePlural);
} catch (error) {
@@ -38,16 +38,17 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
authContext,
});
const record = await this.commonUpdateOneQueryRunnerService.execute(
{ id, data, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
const { results: record } =
await this.commonUpdateOneQueryRunnerService.execute(
{ id, data, selectedFields },
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return this.formatRestResponse(record, flatObjectMetadata.nameSingular);
} catch (error) {