Gql direct execution - Improvements (#18972)

#### Direct Execution  __typename & null backfill
##### __typename filling
The direct execution path now correctly derives __typename at every
level of the GraphQL response:
Connection types — CompanyConnection, CompanyEdge, Company, PageInfo
GroupBy types — TaskGroupByConnection (was incorrectly producing
TaskConnection)
Composite fields — Links, FullName, Currency, etc. handled by a
dedicated formatter (was inheriting the parent object's typename)
Previously, __typename was derived from the object's universal
identifier (a UUID), producing broken values like
20202020B3744779A56180086Cb2E17FConnection.
##### Null backfill
Selected fields missing from the resolver result are backfilled with
null, matching the standard Yoga schema behavior.
##### Integration test
A new test runs the same findMany query (with __typename at all
structural levels) through both paths — standard Yoga schema and direct
execution — and asserts identical output via toStrictEqual.
This commit is contained in:
Etienne
2026-03-30 17:20:25 +02:00
committed by GitHub
parent 2a5aab0c44
commit 3d1c53ec9d
15 changed files with 611 additions and 63 deletions
@@ -1,9 +1,15 @@
import { DataArgProcessorService } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service';
import { FilterArgProcessorService } from 'src/engine/api/common/common-args-processors/filter-arg-processor/filter-arg-processor.service';
import { GroupByArgProcessorService } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/group-by-arg-processor.service';
import { OrderByArgProcessorService } from 'src/engine/api/common/common-args-processors/order-by-arg-processor/order-by-arg-processor.service';
import { OrderByWithGroupByArgProcessorService } from 'src/engine/api/common/common-args-processors/order-by-with-group-by-arg-processor/order-by-with-group-by-arg-processor.service';
import { QueryRunnerArgsFactory } from 'src/engine/api/common/common-args-processors/query-runner-args.factory';
export const CommonArgsProcessors = [
DataArgProcessorService,
FilterArgProcessorService,
GroupByArgProcessorService,
OrderByArgProcessorService,
OrderByWithGroupByArgProcessorService,
QueryRunnerArgsFactory,
]; // TODO: Refacto-common Remove QueryRunnerArgsFactory
@@ -1,9 +1,10 @@
import { isString } from '@sniptt/guards';
import { isNullEquivalentArrayFieldValue } from 'src/engine/api/common/common-args-processors/data-arg-processor/utils/is-null-equivalent-array-field-value.util';
export const transformArrayField = (
value: string | string[] | null,
): string[] | null => {
if (typeof value === 'string') return [value];
if (isString(value)) return [value];
return isNullEquivalentArrayFieldValue(value) ? null : value;
};
@@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import {
ObjectRecordGroupByForAtomicField,
ObjectRecordGroupByForCompositeField,
ObjectRecordGroupByForDateField,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
@Injectable()
export class GroupByArgProcessorService {
process({
groupBy,
}: {
groupBy:
| ObjectRecordGroupByForAtomicField
| ObjectRecordGroupByForCompositeField
| ObjectRecordGroupByForDateField
| Array<
| ObjectRecordGroupByForAtomicField
| ObjectRecordGroupByForCompositeField
| ObjectRecordGroupByForDateField
>;
}): Array<
| ObjectRecordGroupByForAtomicField
| ObjectRecordGroupByForCompositeField
| ObjectRecordGroupByForDateField
> {
if (Array.isArray(groupBy)) {
return groupBy;
}
return [groupBy];
}
}
@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import {
ObjectRecordOrderByForCompositeField,
ObjectRecordOrderByForScalarField,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@Injectable()
export class OrderByArgProcessorService {
process({
orderBy,
}: {
orderBy:
| undefined
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderBy;
}): ObjectRecordOrderBy | undefined {
if (Array.isArray(orderBy) || !isDefined(orderBy)) {
return orderBy;
}
return [orderBy];
}
}
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import {
OrderByWithGroupBy,
type AggregateOrderByWithGroupByField,
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForRelationField,
type ObjectRecordOrderByForScalarField,
type ObjectRecordOrderByWithGroupByDateField,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@Injectable()
export class OrderByWithGroupByArgProcessorService {
process({
orderBy,
}: {
orderBy:
| undefined
| ObjectRecordOrderByForScalarField
| ObjectRecordOrderByForCompositeField
| ObjectRecordOrderByWithGroupByDateField
| ObjectRecordOrderByForRelationField
| AggregateOrderByWithGroupByField
| OrderByWithGroupBy;
}): OrderByWithGroupBy | undefined {
if (Array.isArray(orderBy) || !isDefined(orderBy)) {
return orderBy;
}
return [orderBy];
}
}
@@ -6,6 +6,9 @@ import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-ru
import { DataArgProcessorService } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service';
import { FilterArgProcessorService } from 'src/engine/api/common/common-args-processors/filter-arg-processor/filter-arg-processor.service';
import { GroupByArgProcessorService } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/group-by-arg-processor.service';
import { OrderByArgProcessorService } from 'src/engine/api/common/common-args-processors/order-by-arg-processor/order-by-arg-processor.service';
import { OrderByWithGroupByArgProcessorService } from 'src/engine/api/common/common-args-processors/order-by-with-group-by-arg-processor/order-by-with-group-by-arg-processor.service';
import { QueryRunnerArgsFactory } from 'src/engine/api/common/common-args-processors/query-runner-args.factory';
import { ProcessNestedRelationsHelper } from 'src/engine/api/common/common-nested-relations-processor/process-nested-relations.helper';
import {
@@ -64,6 +67,12 @@ export abstract class CommonBaseQueryRunnerService<
@Inject()
protected readonly filterArgProcessor: FilterArgProcessorService;
@Inject()
protected readonly groupByArgProcessor: GroupByArgProcessorService;
@Inject()
protected readonly orderByArgProcessor: OrderByArgProcessorService;
@Inject()
protected readonly orderByWithGroupByArgProcessor: OrderByWithGroupByArgProcessorService;
@Inject()
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
@Inject()
protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper;
@@ -13,7 +13,6 @@ import {
ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
@@ -39,6 +38,7 @@ import {
countRelationFieldsInOrderBy,
hasRelationFieldInOrderBy,
} from 'src/engine/api/utils/validate-and-get-order-by.utils';
import { WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
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';
@@ -235,6 +235,9 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
return {
...args,
orderBy: this.orderByArgProcessor.process({
orderBy: args.orderBy,
}),
filter: this.filterArgProcessor.process({
filter: args.filter,
flatObjectMetadata,
@@ -405,6 +405,15 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
return {
...args,
groupBy: this.groupByArgProcessor.process({
groupBy: args.groupBy,
}),
orderBy: this.orderByWithGroupByArgProcessor.process({
orderBy: args.orderBy,
}),
orderByForRecords: this.orderByArgProcessor.process({
orderBy: args.orderByForRecords,
}),
filter: this.filterArgProcessor.process({
filter: args.filter,
flatObjectMetadata,