Skip to content

Commit

Permalink
fix(federation): avoid extra calls if keys are already fetched
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Oct 24, 2024
1 parent cee240f commit 180f3f0
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .changeset/breezy-buckets-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-tools/federation': patch
'@graphql-tools/delegate': patch
'@graphql-tools/stitch': patch
---

Avoid extra calls if the keys are already resolved
9 changes: 7 additions & 2 deletions packages/delegate/src/mergeFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,15 @@ export function handleResolverResult(
object[responseKey] = existingPropValue.map((existingElement, index) =>
sourcePropValue instanceof Error
? existingElement
: mergeDeep([existingElement, sourcePropValue[index]]),
: mergeDeep([existingElement, sourcePropValue[index]], undefined, true, true),
);
} else if (!(sourcePropValue instanceof Error)) {
object[responseKey] = mergeDeep([existingPropValue, sourcePropValue]);
object[responseKey] = mergeDeep(
[existingPropValue, sourcePropValue],
undefined,
true,
true,
);
}
} else {
object[responseKey] = sourcePropValue;
Expand Down
67 changes: 67 additions & 0 deletions packages/federation/test/getStitchedSchemaFromLocalSchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ExecutionResult, GraphQLSchema } from 'graphql';
import { kebabCase } from 'lodash';
import { createDefaultExecutor } from '@graphql-tools/delegate';
import { ExecutionRequest, isPromise } from '@graphql-tools/utils';
import { getStitchedSchemaFromSupergraphSdl } from '../src/supergraph';

export interface LocalSchemaItem {
name: string;
schema: GraphQLSchema;
}

export async function getStitchedSchemaFromLocalSchemas(
localSchemas: Record<string, GraphQLSchema>,
onSubgraphExecute?: (
subgraph: string,
executionRequest: ExecutionRequest,
result: ExecutionResult | AsyncIterable<ExecutionResult>,
) => void,
): Promise<GraphQLSchema> {
const { IntrospectAndCompose, LocalGraphQLDataSource } = await import('@apollo/gateway');
const introspectAndCompose = await new IntrospectAndCompose({
subgraphs: Object.keys(localSchemas).map(name => ({ name, url: 'http://localhost/' + name })),
}).initialize({
healthCheck() {
return Promise.resolve();
},
update() {},
getDataSource({ name }) {
const [, localSchema] =
Object.entries(localSchemas).find(([key]) => kebabCase(key) === kebabCase(name)) || [];
if (localSchema) {
return new LocalGraphQLDataSource(localSchema);
}
throw new Error(`Unknown subgraph ${name}`);
},
});
function createTracedExecutor(name: string, schema: GraphQLSchema) {
const executor = createDefaultExecutor(schema);
return function tracedExecutor(request: ExecutionRequest) {
const result = executor(request);
if (onSubgraphExecute) {
if (isPromise(result)) {
return result.then(result => {
onSubgraphExecute(name, request, result);
return result;
});
}
onSubgraphExecute(name, request, result);
}
return result;
};
}
return getStitchedSchemaFromSupergraphSdl({
supergraphSdl: introspectAndCompose.supergraphSdl,
onSubschemaConfig(subschemaConfig) {
const [name, localSchema] =
Object.entries(localSchemas).find(
([key]) => kebabCase(key) === kebabCase(subschemaConfig.name),
) || [];
if (name && localSchema) {
subschemaConfig.executor = createTracedExecutor(name, localSchema);
} else {
throw new Error(`Unknown subgraph ${subschemaConfig.name}`);
}
},
});
}
140 changes: 140 additions & 0 deletions packages/federation/test/optimizations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Dschema,
Eschema,
} from './fixtures/optimizations/awareness-of-other-fields';
import { getStitchedSchemaFromLocalSchemas } from './getStitchedSchemaFromLocalSchemas';

describe('Optimizations', () => {
let serviceCallCnt: Record<string, number>;
Expand Down Expand Up @@ -547,3 +548,142 @@ it('prevents recursively depending fields in case of multiple keys', async () =>
},
});
});

it('nested recursive requirements', async () => {
const inventory = buildSubgraphSchema({
typeDefs: parse(/* GraphQL */ `
type Query {
colos: [Colo]
}
type Colo @key(fields: "id") {
id: ID!
cages: Cages
}
type Cages @key(fields: "id") @key(fields: "spatialId") {
id: ID!
spatialId: String
number: Int!
cabinets: [Cabinet]
}
type Cabinet @key(fields: "id") @key(fields: "spatialId") {
id: ID!
spatialId: String
number: Int!
}
`),
resolvers: {
Query: {
colos() {
return [
{
id: '1',
},
];
},
},
Colo: {
cages() {
return {
id: '1',
number: 1,
cabinets: [
{
id: '1',
spatialId: '1',
number: 1,
},
],
};
},
},
},
});

const spatial = buildSubgraphSchema({
typeDefs: parse(/* GraphQL */ `
type Cabinet @key(fields: "spatialId") {
id: ID!
spatialId: String
spatialCabinet: SpatialCabinet
}
type SpatialCabinet {
spatialId: String
}
`),
resolvers: {
Cabinet: {
spatialCabinet() {
return {
spatialId: '1',
};
},
},
},
});

const subgraphCalls: Record<string, number> = {};

const schema = await getStitchedSchemaFromLocalSchemas(
{
inventory,
spatial,
},
subgraph => {
subgraphCalls[subgraph] = (subgraphCalls[subgraph] || 0) + 1;
},
);

expect(
await normalizedExecutor({
schema,
document: parse(/* GraphQL */ `
query {
colos {
cages {
id
number
cabinets {
id
number
spatialCabinet {
spatialId
}
}
}
}
}
`),
}),
).toMatchInlineSnapshot(`
{
"data": {
"colos": [
{
"cages": {
"cabinets": [
{
"id": "1",
"number": 1,
"spatialCabinet": {
"spatialId": "1",
},
},
],
"id": "1",
"number": 1,
},
},
],
},
}
`);

expect(subgraphCalls).toEqual({
inventory: 1,
spatial: 1,
});
});
2 changes: 1 addition & 1 deletion packages/stitch/src/createDelegationPlanBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function calculateDelegationStage(

// 2a. use uniqueFields map to assign fields to subschema if one of possible subschemas

const uniqueSubschema: Subschema = uniqueFields[fieldNode.name.value];
const uniqueSubschema: Subschema = uniqueFields[fieldName];
if (uniqueSubschema != null) {
if (!proxiableSubschemas.includes(uniqueSubschema)) {
unproxiableFieldNodes.push(fieldNode);
Expand Down
12 changes: 11 additions & 1 deletion packages/stitch/src/getFieldsNotInSubschema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,17 @@ export function getFieldsNotInSubschema(
} else {
const field = fields[fieldName];
for (const subFieldNode of subFieldNodes) {
const unavailableFields = extractUnavailableFields(schema, field, subFieldNode, () => true);
const unavailableFields = extractUnavailableFields(
schema,
field,
subFieldNode,
fieldType => {
if (stitchingInfo.mergedTypes[fieldType.name].resolvers.get(subschema)) {
return false;
}
return true;
},
);
if (unavailableFields.length) {
fieldNotInSchema = true;
fieldsNotInSchema.add({
Expand Down

0 comments on commit 180f3f0

Please sign in to comment.