Skip to content

Commit

Permalink
fix(federation): federation 2 support (#4646)
Browse files Browse the repository at this point in the history
* fix(federation): federation 2 support

* chore(dependencies): updated changesets for modified dependencies

* Go

* More

* Go

* More

* Hmm

* Go

* Update config

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
ardatan and github-actions[bot] authored Oct 7, 2022
1 parent 2be9409 commit 637e9e9
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 242 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphql-mesh/transform-federation": patch
---
dependencies updates:
- Added dependency [`@graphql-mesh/[email protected]` ↗︎](https://www.npmjs.com/package/@graphql-mesh/string-interpolation/v/0.3.2) (to `dependencies`)
- Added dependency [`[email protected]` ↗︎](https://www.npmjs.com/package/lodash.set/v/4.3.2) (to `dependencies`)
7 changes: 7 additions & 0 deletions .changeset/selfish-humans-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-mesh/http': patch
'@graphql-mesh/transform-federation': patch
'@graphql-mesh/types': patch
---

Fixes for Federation 2 support
9 changes: 4 additions & 5 deletions examples/federation-example/gateway/.meshrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ sources:
transforms:
- federation:
types:
- name: Query
config:
extend: true
- name: User
config:
keyFields:
- id
key:
- fields: id
resolveReference:
queryFieldName: user
args:
id: '{root.id}'
- name: reviews
handler:
graphql:
Expand Down
4 changes: 2 additions & 2 deletions packages/http/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function createMeshHTTPHandler<TServerContext>({
const indexPath = path.join(baseDir, staticFiles, 'index.html');
serverAdapter.get('*', async request => {
const url = new URL(request.url);
if (url.pathname === '/' && (await pathExists(indexPath))) {
if (graphqlPath !== '/' && url.pathname === '/' && (await pathExists(indexPath))) {
const indexFile = await fs.promises.readFile(indexPath);
return new Response(indexFile, {
status: 200,
Expand All @@ -99,7 +99,7 @@ export function createMeshHTTPHandler<TServerContext>({
}
return undefined;
});
} else {
} else if (graphqlPath !== '/') {
serverAdapter.get(
'/',
() =>
Expand Down
19 changes: 12 additions & 7 deletions packages/mergers/stitching/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { addResolversToSchema } from '@graphql-tools/schema';
import { buildSchema, ExecutionResult, GraphQLSchema, parse } from 'graphql';
import { MeshStore, PredefinedProxyOptions } from '@graphql-mesh/store';
import { AggregateError, Executor } from '@graphql-tools/utils';
import { AggregateError, Executor, printSchemaWithDirectives } from '@graphql-tools/utils';

const APOLLO_GET_SERVICE_DEFINITION_QUERY = /* GraphQL */ `
query __ApolloGetServiceDefinition__ {
Expand Down Expand Up @@ -53,13 +53,18 @@ export default class StitchingMerger implements MeshMerger {
.proxy(`${name}_stitching`, PredefinedProxyOptions.GraphQLSchemaWithDiffing)
.getWithSet(async () => {
this.logger.debug(`Fetching Apollo Federated Service SDL for ${name}`);
const sdlQueryResult: any = (await executor({
document: parse(APOLLO_GET_SERVICE_DEFINITION_QUERY),
})) as ExecutionResult;
if (sdlQueryResult.errors?.length) {
throw new AggregateError(sdlQueryResult.errors, `Failed on fetching Federated SDL for ${name}`);
let federationSdl: string;
if ((oldSchema.extensions?.directives as any)?.link) {
federationSdl = printSchemaWithDirectives(oldSchema);
} else {
const sdlQueryResult: any = (await executor({
document: parse(APOLLO_GET_SERVICE_DEFINITION_QUERY),
})) as ExecutionResult;
if (sdlQueryResult.errors?.length) {
throw new AggregateError(sdlQueryResult.errors, `Failed on fetching Federated SDL for ${name}`);
}
federationSdl = sdlQueryResult.data._service.sdl;
}
const federationSdl = sdlQueryResult.data._service.sdl;
this.logger.debug(`Generating Stitching SDL for ${name}`);
const stitchingSdl = federationToStitchingSDL(federationSdl, stitchingDirectives);
return buildSchema(stitchingSdl, {
Expand Down
11 changes: 10 additions & 1 deletion packages/runtime/src/get-mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,16 @@ export async function getMesh(options: GetMeshOptions): Promise<MeshInstance> {
});

unifiedSubschema.transforms = unifiedSubschema.transforms || [];
unifiedSubschema.transforms.push(...transforms);

for (const rootLevelTransform of transforms) {
if (rootLevelTransform.noWrap) {
if (rootLevelTransform.transformSchema) {
unifiedSubschema.schema = rootLevelTransform.transformSchema(unifiedSubschema.schema, unifiedSubschema);
}
} else {
unifiedSubschema.transforms.push(rootLevelTransform);
}
}

let inContextSDK$: Promise<Record<string, any>>;

Expand Down
4 changes: 3 additions & 1 deletion packages/transforms/federation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@
"dependencies": {
"@apollo/subgraph": "2.1.3",
"@graphql-mesh/types": "0.84.9",
"@graphql-mesh/string-interpolation": "0.3.2",
"@graphql-mesh/utils": "0.41.20",
"graphql-transform-federation": "2.2.0",
"@graphql-tools/utils": "8.12.0",
"@graphql-tools/stitching-directives": "2.3.11",
"@graphql-tools/delegate": "9.0.8",
"tslib": "^2.4.0"
"tslib": "^2.4.0",
"lodash.set": "4.3.2"
},
"publishConfig": {
"access": "public",
Expand Down
160 changes: 81 additions & 79 deletions packages/transforms/federation/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLID,
isNonNullType,
GraphQLNonNull,
isObjectType,
GraphQLUnionType,
isListType,
} from 'graphql';
import { GraphQLSchema, GraphQLObjectType, GraphQLUnionType } from 'graphql';
import { MeshTransform, YamlConfig, MeshTransformOptions, ImportFn } from '@graphql-mesh/types';
import { loadFromModuleExportExpression } from '@graphql-mesh/utils';
import { FederationConfig, FederationFieldsConfig } from 'graphql-transform-federation';
import { addFederationAnnotations } from 'graphql-transform-federation/dist/transform-sdl.js';
import { entitiesField, EntityType, serviceField } from '@apollo/subgraph/dist/types.js';
import { mapSchema, MapperKind, printSchemaWithDirectives } from '@graphql-tools/utils';
import { MergedTypeResolver, SubschemaConfig } from '@graphql-tools/delegate';
import set from 'lodash.set';
import { stringInterpolator } from '@graphql-mesh/string-interpolation';

export default class FederationTransform implements MeshTransform {
private apiName: string;
private config: YamlConfig.Transform['federation'];
private baseDir: string;
private importFn: ImportFn;

noWrap = true;

constructor({ apiName, baseDir, config, importFn }: MeshTransformOptions<YamlConfig.Transform['federation']>) {
this.apiName = apiName;
this.config = config;
Expand All @@ -30,18 +23,34 @@ export default class FederationTransform implements MeshTransform {
}

transformSchema(schema: GraphQLSchema, rawSource: SubschemaConfig) {
const federationConfig: FederationConfig<any> = {};

rawSource.merge = {};
if (this.config?.types) {
const queryType = schema.getQueryType();
const queryTypeFields = queryType.getFields();
for (const type of this.config.types) {
rawSource.merge[type.name] = {};
const fields: FederationFieldsConfig = {};
const typeObj = schema.getType(type.name) as GraphQLObjectType;
typeObj.extensions = typeObj.extensions || {};
const typeDirectivesObj: any = ((typeObj.extensions as any).directives = typeObj.extensions.directives || {});
if (type.config?.key) {
typeDirectivesObj.key = type.config.key;
}
if (type.config?.shareable) {
typeDirectivesObj.shareable = type.config.shareable;
}
if (type.config?.extends) {
typeDirectivesObj.extends = type.config.extends;
}
const typeFieldObjs = typeObj.getFields();
if (type.config?.fields) {
for (const field of type.config.fields) {
fields[field.name] = field.config;
const typeField = typeFieldObjs[field.name];
if (typeField) {
typeField.extensions = typeField.extensions || {};
const directivesObj: any = ((typeField.extensions as any).directives =
typeField.extensions.directives || {});
Object.assign(directivesObj, field.config);
}
rawSource.merge[type.name].fields = rawSource.merge[type.name].fields || {};
rawSource.merge[type.name].fields[field.name] = rawSource.merge[type.name].fields[field.name] || {};
if (field.config.requires) {
Expand All @@ -52,16 +61,14 @@ export default class FederationTransform implements MeshTransform {
}
// If a field is a key field, it should be GraphQLID

if (type.config?.keyFields) {
rawSource.merge[type.name].selectionSet = `{ ${type.config.keyFields.join(' ')} }`;
for (const fieldName of type.config.keyFields) {
const objectType = schema.getType(type.name) as GraphQLObjectType;
if (objectType) {
const existingType = objectType.getFields()[fieldName].type;
objectType.getFields()[fieldName].type = isNonNullType(existingType)
? new GraphQLNonNull(GraphQLID)
: GraphQLID;
}
if (type.config?.key) {
let selectionSetContent = '';
for (const keyField of type.config.key) {
selectionSetContent += '\n';
selectionSetContent += keyField.fields || '';
}
if (selectionSetContent) {
rawSource.merge[type.name].selectionSet = `{ ${selectionSetContent} }`;
}
}

Expand All @@ -79,24 +86,21 @@ export default class FederationTransform implements MeshTransform {
resolveReference = resolveReferenceConfig;
} else {
const queryField = queryTypeFields[resolveReferenceConfig.queryFieldName];
const keyArg = resolveReferenceConfig.keyArg || queryField.args[0].name;
const keyField = type.config.keyFields[0];
const isBatch = isListType(queryField.args.find(arg => arg.name === keyArg));
resolveReference = async (root, context, info) => {
const args = {};
for (const argName in resolveReferenceConfig.args) {
const argVal = stringInterpolator.parse(resolveReferenceConfig.args[argName], {
root,
args,
context,
info,
env: process.env,
});
set(args, argName, argVal);
}
const result = await context[this.apiName].Query[queryField.name]({
root,
...(isBatch
? {
key: root[keyField],
argsFromKeys: (keys: string[]) => ({
[keyArg]: keys,
}),
}
: {
args: {
[keyArg]: root[keyField],
},
}),
args,
context,
info,
});
Expand All @@ -108,44 +112,21 @@ export default class FederationTransform implements MeshTransform {
}
rawSource.merge[type.name].resolve = resolveReference;
}
federationConfig[type.name] = {
...type.config,
resolveReference,
fields,
};
}
}

const entityTypes = Object.fromEntries(
Object.entries(federationConfig)
.filter(([, { keyFields }]) => keyFields?.length)
.map(([objectName]) => {
const type = schema.getType(objectName);
if (!isObjectType(type)) {
throw new Error(`Type "${objectName}" is not an object type and can't have a key directive`);
}
return [objectName, type];
})
);

const hasEntities = !!Object.keys(entityTypes).length;

const sdlWithFederationDirectives = addFederationAnnotations(printSchemaWithDirectives(schema), federationConfig);

const schemaWithFederationQueryType = mapSchema(schema, {
[MapperKind.QUERY]: type => {
const config = type.toConfig();
return new GraphQLObjectType({
...config,
fields: {
...config.fields,
...(hasEntities && {
_entities: entitiesField,
_service: {
...serviceField,
resolve: () => ({ sdl: sdlWithFederationDirectives }),
},
}),
_entities: entitiesField,
_service: {
...serviceField,
resolve: (root, args, context, info) => ({ sdl: printSchemaWithDirectives(info.schema) }),
},
},
});
},
Expand All @@ -156,24 +137,45 @@ export default class FederationTransform implements MeshTransform {
if (type.name === EntityType.name) {
return new GraphQLUnionType({
...EntityType.toConfig(),
types: Object.values(entityTypes),
types: () => {
const entityTypes: GraphQLObjectType[] = [];
for (const typeConfig of this.config?.types || []) {
if (typeConfig.config?.key?.length) {
const type = schemaWithFederationQueryType.getType(typeConfig.name) as GraphQLObjectType;
entityTypes.push(type);
}
}
return entityTypes;
},
});
}
return type;
},
});

// Not using transformSchema since it will remove resolveReference
Object.entries(federationConfig).forEach(([objectName, currentFederationConfig]) => {
if (currentFederationConfig.resolveReference) {
const type = schemaWithUnionType.getType(objectName);
if (!isObjectType(type)) {
throw new Error(`Type "${objectName}" is not an object type and can't have a resolveReference function`);
}
(type as any).resolveObject = currentFederationConfig.resolveReference;
}
this.config?.types.forEach(typeConfig => {
const type = schemaWithUnionType.getType(typeConfig.name) as GraphQLObjectType;
set(type, 'extensions.apollo.subgraph.resolveReference', rawSource.merge[typeConfig.name].resolve);
});

schemaWithUnionType.extensions = schemaWithUnionType.extensions || {};
const directivesObj: any = ((schemaWithUnionType.extensions as any).directives =
schemaWithUnionType.extensions.directives || {});
directivesObj.link = {
url: 'https://specs.apollo.dev/federation/v2.0',
import: [
'@extends',
'@external',
'@inaccessible',
'@key',
'@override',
'@provides',
'@requires',
'@shareable',
'@tag',
],
};

return schemaWithUnionType;
}
}
Loading

0 comments on commit 637e9e9

Please sign in to comment.