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`,
},
);
}
};