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:
+10
-1
@@ -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: {
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
+38
-1
@@ -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,
|
||||
|
||||
+10
@@ -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;
|
||||
}
|
||||
|
||||
+24
@@ -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`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user