Skip to content

Commit 96e0a18

Browse files
committed
improvements
1 parent 3ecd0fa commit 96e0a18

File tree

2 files changed

+113
-73
lines changed

2 files changed

+113
-73
lines changed

packages/federation/src/globalObjectIdentification.ts

Lines changed: 85 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { batchDelegateToSchema } from '@graphql-tools/batch-delegate';
22
import { StitchingInfo, SubschemaConfig } from '@graphql-tools/delegate';
3-
import { IResolvers } from '@graphql-tools/utils';
3+
import { IResolvers, parseSelectionSet } from '@graphql-tools/utils';
44
import {
55
DefinitionNode,
66
FieldDefinitionNode,
77
GraphQLList,
88
GraphQLObjectType,
99
InterfaceTypeDefinitionNode,
10+
isObjectType,
1011
Kind,
1112
ObjectTypeExtensionNode,
13+
SelectionSetNode,
1214
} from 'graphql';
1315
import { fromGlobalId, toGlobalId } from 'graphql-relay';
14-
import { MergedTypeConfigFromEntities } from './supergraph';
16+
import { isMergedEntityConfig, MergedEntityConfig } from './supergraph';
1517

1618
export interface GlobalObjectIdentificationOptions {
1719
nodeIdField: string;
@@ -64,7 +66,7 @@ export function createNodeDefinitions({
6466

6567
// extend type X implements Node
6668

67-
for (const { typeName } of getDistinctResolvableTypes(subschemas)) {
69+
for (const { typeName } of getDistinctEntities(subschemas)) {
6870
const typeExtensionDef: ObjectTypeExtensionNode = {
6971
kind: Kind.OBJECT_TYPE_EXTENSION,
7072
name: {
@@ -147,14 +149,14 @@ export function createResolvers({
147149
nodeIdField,
148150
subschemas,
149151
}: GlobalObjectIdentificationOptions): IResolvers {
150-
const types = getDistinctResolvableTypes(subschemas);
152+
const types = getDistinctEntities(subschemas);
151153
return {
152154
...types.reduce(
153-
(resolvers, { typeName, keyFieldNames }) => ({
155+
(resolvers, { typeName, merge, keyFieldNames }) => ({
154156
...resolvers,
155157
[typeName]: {
156158
[nodeIdField]: {
157-
selectionSet: `{ ${keyFieldNames.join(' ')} }`,
159+
selectionSet: merge.selectionSet,
158160
resolve(source) {
159161
if (keyFieldNames.length === 1) {
160162
// single field key
@@ -183,9 +185,7 @@ export function createResolvers({
183185
}
184186

185187
// we must use otherwise different schema
186-
const types = getDistinctResolvableTypes(
187-
stitchingInfo.subschemaMap.values(),
188-
);
188+
const types = getDistinctEntities(stitchingInfo.subschemaMap.values());
189189

190190
const { id: idOrFields, type: typeName } = fromGlobalId(nodeId);
191191
const type = types.find((t) => t.typeName === typeName);
@@ -211,85 +211,106 @@ export function createResolvers({
211211
}
212212

213213
return batchDelegateToSchema({
214-
...type.merge,
215-
info,
216214
context,
215+
info,
217216
schema: type.subschema,
217+
fieldName: type.merge.fieldName,
218+
argsFromKeys: type.merge.argsFromKeys,
219+
key: { ...keyFields, __typename: typeName }, // we already have all the necessary keys
218220
returnType: new GraphQLList(
219221
// wont ever be undefined, we ensured the subschema has the type above
220222
type.subschema.schema.getType(typeName) as GraphQLObjectType,
221223
),
222-
selectionSet: undefined, // selectionSet is not needed here
223-
key: { ...keyFields, __typename: typeName }, // we already have all the necessary keys
224+
dataLoaderOptions: type.merge.dataLoaderOptions,
224225
});
225226
},
226227
},
227228
};
228229
}
229230

230-
interface DistinctResolvableType {
231+
interface DistinctEntity {
231232
typeName: string;
232233
subschema: SubschemaConfig;
233-
merge: MergedTypeConfigFromEntities;
234+
merge: MergedEntityConfig;
234235
keyFieldNames: string[];
235236
}
236237

237-
function getDistinctResolvableTypes(
238-
subschemas: Iterable<SubschemaConfig>,
239-
): DistinctResolvableType[] {
240-
const visitedTypeNames = new Set<string>();
241-
const types: DistinctResolvableType[] = [];
242-
for (const subschema of subschemas) {
243-
// TODO: respect canonical types
244-
for (const [typeName, merge] of Object.entries(subschema.merge || {})
245-
.filter(
246-
// make sure selectionset is defined for the sort to work
247-
([, merge]) => merge.selectionSet,
238+
function getDistinctEntities(
239+
subschemasIter: Iterable<SubschemaConfig>,
240+
): DistinctEntity[] {
241+
const distinctEntities: DistinctEntity[] = [];
242+
243+
const subschemas = Array.from(subschemasIter);
244+
const types = subschemas.flatMap((subschema) =>
245+
Object.values(subschema.schema.getTypeMap()),
246+
);
247+
248+
const objects = types.filter(isObjectType);
249+
for (const obj of objects) {
250+
if (
251+
distinctEntities.find(
252+
(distinctType) => distinctType.typeName === obj.name,
248253
)
249-
.sort(
250-
// sort by shortest keys first
251-
([, a], [, b]) => a.selectionSet!.length - b.selectionSet!.length,
252-
)) {
253-
if (visitedTypeNames.has(typeName)) {
254-
// already yielded this type, all types can only have one resolution
254+
) {
255+
// already added this type
256+
continue;
257+
}
258+
let candidate: {
259+
subschema: SubschemaConfig;
260+
merge: MergedEntityConfig;
261+
} | null = null;
262+
for (const subschema of subschemas) {
263+
const merge = subschema.merge?.[obj.name];
264+
if (!merge) {
265+
// not resolvable from this subschema
255266
continue;
256267
}
257-
258-
if (
259-
!merge.selectionSet ||
260-
!merge.argsFromKeys ||
261-
!merge.key ||
262-
!merge.fieldName ||
263-
!merge.dataLoaderOptions
264-
) {
265-
// cannot be resolved globally
268+
if (!isMergedEntityConfig(merge)) {
269+
// not a merged entity config, cannot be resolved globally
266270
continue;
267271
}
268-
269-
// remove first and last characters from the selection set making up the key (curly braces, `{ id } -> id`)
270-
const key = merge.selectionSet.trim().slice(1, -1).trim();
271-
if (
272-
// the key for fetching this object contains other objects
273-
key.includes('{') ||
274-
// the key for fetching this object contains arguments
275-
key.includes('(') ||
276-
// the key contains aliases
277-
key.includes(':')
278-
) {
279-
// it's too complex to use global object identification
280-
// TODO: do it anyways when need arises
272+
if (merge.canonical) {
273+
// this subschema is canonical (owner) for this type, no need to check other schemas
274+
candidate = { subschema, merge };
275+
break;
276+
}
277+
if (!candidate) {
278+
// first merge candidate
279+
candidate = { subschema, merge };
281280
continue;
282281
}
283-
// what we're left in the "key" are simple field(s) like "id" or "email"
284-
285-
visitedTypeNames.add(typeName);
286-
types.push({
287-
typeName,
288-
subschema,
289-
merge: merge as MergedTypeConfigFromEntities,
290-
keyFieldNames: key.trim().split(/\s+/),
291-
});
282+
if (merge.selectionSet.length < candidate.merge.selectionSet.length) {
283+
// found a better candidate
284+
candidate = { subschema, merge };
285+
}
286+
}
287+
if (!candidate) {
288+
// no merge candidate found, cannot be resolved globally
289+
continue;
292290
}
291+
// is an entity that can efficiently be resolved globally
292+
distinctEntities.push({
293+
...candidate,
294+
typeName: obj.name,
295+
keyFieldNames: (function getRootFieldNames(
296+
selectionSet: SelectionSetNode,
297+
): string[] {
298+
const fieldNames: string[] = [];
299+
for (const sel of selectionSet.selections) {
300+
if (sel.kind === Kind.FRAGMENT_SPREAD) {
301+
throw new Error('Fragment spreads cannot appear in @key fields');
302+
}
303+
if (sel.kind === Kind.INLINE_FRAGMENT) {
304+
fieldNames.push(...getRootFieldNames(sel.selectionSet));
305+
continue;
306+
}
307+
// Kind.FIELD
308+
fieldNames.push(sel.alias?.value || sel.name.value);
309+
}
310+
return fieldNames;
311+
})(parseSelectionSet(candidate.merge.selectionSet)),
312+
});
293313
}
294-
return types;
314+
315+
return distinctEntities;
295316
}

packages/federation/src/supergraph.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -871,9 +871,7 @@ export function getStitchingOptionsFromSupergraphSdl(
871871
mergedTypeConfig.canonical = true;
872872
}
873873

874-
function getMergedTypeConfigFromKey(
875-
key: string,
876-
): MergedTypeConfigFromEntities {
874+
function getMergedTypeConfigFromKey(key: string): MergedEntityConfig {
877875
return {
878876
selectionSet: `{ ${key} }`,
879877
argsFromKeys: getArgsFromKeysForFederation,
@@ -1748,9 +1746,30 @@ function mergeResults(results: unknown[], getFieldNames: () => Set<string>) {
17481746
return null;
17491747
}
17501748

1751-
export type MergedTypeConfigFromEntities = Required<
1752-
Pick<
1753-
MergedTypeConfig,
1754-
'selectionSet' | 'argsFromKeys' | 'key' | 'fieldName' | 'dataLoaderOptions'
1755-
>
1756-
>;
1749+
/**
1750+
* A merge type configuration for resolving types that are Apollo Federation entities.
1751+
* @see https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/entities/intro
1752+
*/
1753+
export type MergedEntityConfig = MergedTypeConfig &
1754+
Required<
1755+
Pick<
1756+
MergedTypeConfig,
1757+
| 'selectionSet'
1758+
| 'argsFromKeys'
1759+
| 'key'
1760+
| 'fieldName'
1761+
| 'dataLoaderOptions'
1762+
>
1763+
>;
1764+
1765+
export function isMergedEntityConfig(
1766+
merge: MergedTypeConfig,
1767+
): merge is MergedEntityConfig {
1768+
return (
1769+
'selectionSet' in merge &&
1770+
'argsFromKeys' in merge &&
1771+
'key' in merge &&
1772+
'fieldName' in merge &&
1773+
'dataLoaderOptions' in merge
1774+
);
1775+
}

0 commit comments

Comments
 (0)