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,
@@ -36,10 +36,10 @@ import { assertUpdateOneArgs } from 'src/engine/api/graphql/direct-execution/uti
import { type ResolverNameMapEntry } from 'src/engine/api/graphql/direct-execution/utils/build-resolver-name-map.util';
import { buildWorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/direct-execution/utils/build-workspace-schema-builder-context.util';
import { extractArgumentsFromAst } from 'src/engine/api/graphql/direct-execution/utils/extract-arguments-from-ast.util';
import { graphQLBackfillNullsFromSelectedFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-backfill-nulls-from-selected-fields.util';
import { graphQLBuildFragmentMap } from 'src/engine/api/graphql/direct-execution/utils/graphql-build-fragment-map.util';
import { graphQLBuildPartialResolveInfo } from 'src/engine/api/graphql/direct-execution/utils/graphql-build-partial-resolve-info.util';
import { graphQLExtractTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-extract-top-level-fields.util';
import { graphQLFormatResultFromSelectedFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-format-result-from-selected-fields.util';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
import { CreateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory';
@@ -67,7 +67,7 @@ import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
type DirectExecutionResult = {
data: Record<string, unknown> | null;
data?: Record<string, unknown>;
errors?: GraphQLFormattedError[];
};
@@ -214,12 +214,23 @@ export class DirectExecutionService {
workspaceSchemaBuilderContext,
});
graphQLBackfillNullsFromSelectedFields(
const formattedResult = graphQLFormatResultFromSelectedFields(
result,
graphqlFields(graphqlPartialResolveInfo as GraphQLResolveInfo),
graphqlFields(
graphqlPartialResolveInfo as GraphQLResolveInfo,
{},
{ excludedFields: [] },
),
workspaceSchemaBuilderContext.flatObjectMetadata.nameSingular,
{
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
method: entry.method,
},
);
return { responseKey, result };
return { responseKey, result: formattedResult };
}),
);
@@ -239,7 +250,7 @@ export class DirectExecutionService {
return { data };
} catch (error) {
return { data: null, errors: [this.formatError(error, req)] };
return { errors: [this.formatError(error, req)] };
}
}
@@ -8,11 +8,11 @@ import {
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 GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
export function assertGroupByArgs(
@@ -49,9 +49,12 @@ export function assertGroupByArgs(
}
}
if (!('groupBy' in args) || !Array.isArray(args.groupBy)) {
if (
!('groupBy' in args) ||
(!Array.isArray(args.groupBy) && !isObject(args.groupBy))
) {
throw new GraphqlDirectExecutionException(
'Missing required argument: "groupBy" (array)',
'Missing required argument: "groupBy" must be an array.',
GraphqlDirectExecutionExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
@@ -1,52 +0,0 @@
// GraphQL schema execution automatically returns null for nullable fields
// that are missing from the resolved object. Since direct execution bypasses
// schema resolution, we need to explicitly set requested-but-missing fields
// to null so the response shape matches what GraphQL would produce.
export const graphQLBackfillNullsFromSelectedFields = (
result: unknown,
selectedFields: Record<string, object>,
): unknown => {
if (result === null || result === undefined || typeof result !== 'object') {
return result;
}
if (Array.isArray(result)) {
return result.map((item) =>
graphQLBackfillNullsFromSelectedFields(item, selectedFields),
);
}
const record = result as Record<string, unknown>;
for (const [key, subFields] of Object.entries(selectedFields)) {
if (!(key in record)) {
record[key] = null;
continue;
}
const hasNestedFields =
subFields &&
typeof subFields === 'object' &&
Object.keys(subFields).length > 0;
if (!hasNestedFields || record[key] === null || record[key] === undefined) {
continue;
}
if (Array.isArray(record[key])) {
record[key] = (record[key] as unknown[]).map((item) =>
graphQLBackfillNullsFromSelectedFields(
item,
subFields as Record<string, object>,
),
);
} else if (typeof record[key] === 'object') {
graphQLBackfillNullsFromSelectedFields(
record[key],
subFields as Record<string, object>,
);
}
}
return result;
};
@@ -0,0 +1,320 @@
import { isNull, isObject } from '@sniptt/guards';
import { RelationType } from 'twenty-shared/types';
import { isDefined, isEmptyObject, pascalCase } from 'twenty-shared/utils';
import { CONNECTION_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/connection-method-names';
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { type 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 { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
type GraphQLFormatInput = {
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
objectIdByNameSingular: Record<string, string>;
method: string;
};
type GraphQLFormatContext = GraphQLFormatInput & {
fieldMetadataByNameCache: Map<string, Map<string, FlatFieldMetadata>>;
};
type GraphQLObjectTypeKind =
| 'connection'
| 'groupByConnection'
| 'edge'
| 'node'
| 'pageInfo';
export const graphQLFormatResultFromSelectedFields = (
result: unknown,
selectedFields: Record<string, object>,
objectNameSingular: string,
input: GraphQLFormatInput,
): unknown => {
const context: GraphQLFormatContext = {
...input,
fieldMetadataByNameCache: new Map(),
};
const objectTypeKind = inferObjectTypeKind(context.method);
return format(
result,
selectedFields,
objectNameSingular,
objectTypeKind,
context,
);
};
const format = (
value: unknown,
selectedFields: Record<string, object>,
objectNameSingular: string,
objectTypeKind: GraphQLObjectTypeKind,
context: GraphQLFormatContext,
): unknown => {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map((item) =>
format(item, selectedFields, objectNameSingular, objectTypeKind, context),
);
}
if (isObject(value)) {
return backfillNullValuesAndComputeTypeName(
value as Record<string, unknown>,
selectedFields,
objectNameSingular,
objectTypeKind,
context,
);
}
return value;
};
const backfillNullValuesAndComputeTypeName = (
record: Record<string, unknown>,
selectedFields: Record<string, object>,
objectNameSingular: string,
objectTypeKind: GraphQLObjectTypeKind,
context: GraphQLFormatContext,
): Record<string, unknown> => {
const formatted: Record<string, unknown> = {};
for (const [key, subFields] of Object.entries(selectedFields)) {
if (key === '__typename') {
formatted.__typename = deriveTypeName(objectNameSingular, objectTypeKind);
continue;
}
if (!isDefined(record[key])) {
formatted[key] = null;
continue;
}
const value = record[key];
const hasNestedFields = isDefined(subFields) && !isEmptyObject(subFields);
if (!hasNestedFields || isNull(value)) {
formatted[key] = value;
continue;
}
const childObjectTypeKind = CONNECTION_FIELD_TO_OBJECT_TYPE_KIND[key];
if (isDefined(childObjectTypeKind)) {
formatted[key] = format(
value,
subFields as Record<string, object>,
objectNameSingular,
childObjectTypeKind,
context,
);
continue;
}
const relationInfo = findRelationInfo(objectNameSingular, key, context);
if (isDefined(relationInfo)) {
formatted[key] = format(
value,
subFields as Record<string, object>,
relationInfo.targetObjectNameSingular,
relationInfo.objectTypeKind,
context,
);
continue;
}
const fieldMetadata = findFieldMetadataByName(
objectNameSingular,
key,
context,
);
if (
isDefined(fieldMetadata) &&
isCompositeFieldMetadataType(fieldMetadata.type)
) {
formatted[key] = backfillNullValuesAndComputeTypeNameForCompositeField(
value as Record<string, unknown>,
subFields as Record<string, object>,
fieldMetadata.type,
);
continue;
}
formatted[key] = value;
}
return formatted;
};
const backfillNullValuesAndComputeTypeNameForCompositeField = (
record: Record<string, unknown>,
selectedFields: Record<string, object>,
fieldMetadataType: string,
): Record<string, unknown> => {
const formatted: Record<string, unknown> = {};
for (const key of Object.keys(selectedFields)) {
if (key === '__typename') {
formatted.__typename = pascalCase(fieldMetadataType);
continue;
}
formatted[key] = record[key] ?? null;
}
return formatted;
};
const deriveTypeName = (
objectNameSingular: string,
objectTypeKind: GraphQLObjectTypeKind,
): string => {
const pascal = pascalCase(objectNameSingular);
switch (objectTypeKind) {
case 'connection':
return `${pascal}Connection`;
case 'groupByConnection':
return `${pascal}GroupByConnection`;
case 'edge':
return `${pascal}Edge`;
case 'node':
return pascal;
case 'pageInfo':
return 'PageInfo';
}
};
const inferObjectTypeKind = (method: string): GraphQLObjectTypeKind => {
if (method === RESOLVER_METHOD_NAMES.GROUP_BY) {
return 'groupByConnection';
}
if (CONNECTION_METHOD_NAMES.has(method)) {
return 'connection';
}
return 'node';
};
const CONNECTION_FIELD_TO_OBJECT_TYPE_KIND: Record<
string,
GraphQLObjectTypeKind
> = {
edges: 'edge',
node: 'node',
pageInfo: 'pageInfo',
};
const getOrBuildFieldMetadataByNameMap = (
objectNameSingular: string,
context: GraphQLFormatContext,
): Map<string, FlatFieldMetadata> => {
const cached = context.fieldMetadataByNameCache.get(objectNameSingular);
if (isDefined(cached)) {
return cached;
}
const map = new Map<string, FlatFieldMetadata>();
const objectId = context.objectIdByNameSingular[objectNameSingular];
if (!isDefined(objectId)) {
context.fieldMetadataByNameCache.set(objectNameSingular, map);
return map;
}
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: objectId,
flatEntityMaps: context.flatObjectMetadataMaps,
});
if (!isDefined(flatObjectMetadata)) {
context.fieldMetadataByNameCache.set(objectNameSingular, map);
return map;
}
for (const fieldId of flatObjectMetadata.fieldIds) {
const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldId,
flatEntityMaps: context.flatFieldMetadataMaps,
});
if (isDefined(fieldMetadata)) {
map.set(fieldMetadata.name, fieldMetadata);
}
}
context.fieldMetadataByNameCache.set(objectNameSingular, map);
return map;
};
const findFieldMetadataByName = (
objectNameSingular: string,
fieldName: string,
context: GraphQLFormatContext,
): FlatFieldMetadata | undefined => {
return getOrBuildFieldMetadataByNameMap(objectNameSingular, context).get(
fieldName,
);
};
type RelationInfo = {
targetObjectNameSingular: string;
objectTypeKind: GraphQLObjectTypeKind;
};
const findRelationInfo = (
objectNameSingular: string,
fieldName: string,
context: GraphQLFormatContext,
): RelationInfo | undefined => {
const fieldMetadata = findFieldMetadataByName(
objectNameSingular,
fieldName,
context,
);
if (
!isDefined(fieldMetadata) ||
!isMorphOrRelationFlatFieldMetadata(fieldMetadata)
) {
return undefined;
}
const targetObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldMetadata.relationTargetObjectMetadataId,
flatEntityMaps: context.flatObjectMetadataMaps,
});
if (!isDefined(targetObjectMetadata)) {
return undefined;
}
const objectTypeKind: GraphQLObjectTypeKind =
fieldMetadata.settings.relationType === RelationType.ONE_TO_MANY
? 'connection'
: 'node';
return {
targetObjectNameSingular: targetObjectMetadata.nameSingular,
objectTypeKind,
};
};
@@ -0,0 +1,11 @@
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
export const CONNECTION_METHOD_NAMES = new Set<string>([
RESOLVER_METHOD_NAMES.FIND_MANY,
RESOLVER_METHOD_NAMES.FIND_DUPLICATES,
RESOLVER_METHOD_NAMES.CREATE_MANY,
RESOLVER_METHOD_NAMES.UPDATE_MANY,
RESOLVER_METHOD_NAMES.DELETE_MANY,
RESOLVER_METHOD_NAMES.DESTROY_MANY,
RESOLVER_METHOD_NAMES.RESTORE_MANY,
]);
@@ -0,0 +1,135 @@
import { randomUUID } from 'crypto';
import request from 'supertest';
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { FeatureFlagKey } from 'twenty-shared/types';
const FIND_MANY_COMPANIES_WITH_TYPENAME = `
query Companies($filter: CompanyFilterInput, $orderBy: [CompanyOrderByInput]) {
companies(filter: $filter, orderBy: $orderBy) {
__typename
edges {
__typename
node {
__typename
id
name
domainName {
__typename
primaryLinkLabel
primaryLinkUrl
}
}
cursor
}
pageInfo {
__typename
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`;
describe('direct execution __typename filling (integration)', () => {
const testCompanyId1 = randomUUID();
const testCompanyId2 = randomUUID();
beforeAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_DIRECT_GRAPHQL_EXECUTION_ENABLED,
value: false,
expectToFail: false,
});
const gqlFields = 'id name';
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields,
data: {
id: testCompanyId1,
name: 'TypeName Test Company A',
},
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields,
data: {
id: testCompanyId2,
name: 'TypeName Test Company B',
},
}),
);
});
afterAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_DIRECT_GRAPHQL_EXECUTION_ENABLED,
value: false,
expectToFail: false,
});
for (const id of [testCompanyId1, testCompanyId2]) {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: 'id',
recordId: id,
}),
);
}
});
it('should produce identical __typename values with and without direct execution', async () => {
const client = request(`http://localhost:${APP_PORT}`);
const variables = {
filter: {
id: { in: [testCompanyId1, testCompanyId2] },
},
orderBy: [{ name: 'AscNullsLast' }],
};
// Run through standard GraphQL Yoga schema execution
const yogaResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({ query: FIND_MANY_COMPANIES_WITH_TYPENAME, variables })
.expect(200);
expect(yogaResponse.body.errors).toBeUndefined();
expect(yogaResponse.body.data).toBeDefined();
const yogaResult = yogaResponse.body.data.companies;
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_DIRECT_GRAPHQL_EXECUTION_ENABLED,
value: true,
expectToFail: false,
});
// Run through direct execution path
const directResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({ query: FIND_MANY_COMPANIES_WITH_TYPENAME, variables })
.expect(200);
expect(directResponse.body.errors).toBeUndefined();
expect(directResponse.body.data).toBeDefined();
const directResult = directResponse.body.data.companies;
expect(directResult).toStrictEqual(yogaResult);
});
});
@@ -414,6 +414,7 @@ export const successfulCreateInputByFieldMetadataType: {
},
{
input: {
name: 'position',
position: undefined,
},
validateInput: (record: Record<string, any>) => {