Skip to content

Commit

Permalink
fix(supergraph-handler): handle subgraphs individually with interpola…
Browse files Browse the repository at this point in the history
…ted endpoint and headers (#6758)

* fix(supergraph-handler): handle subgraphs individually with interpolated endpoint and headers

* Changeset
  • Loading branch information
ardatan authored Mar 28, 2024
1 parent 708c184 commit 425afee
Show file tree
Hide file tree
Showing 12 changed files with 495 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/spicy-olives-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-mesh/supergraph": patch
---

Fix interpolation of headers and endpoint configuration for each subgraph
42 changes: 34 additions & 8 deletions packages/legacy/handlers/supergraph/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { DocumentNode, parse } from 'graphql';
import { DocumentNode, EnumTypeDefinitionNode, parse } from 'graphql';
import { process } from '@graphql-mesh/cross-helpers';
import { PredefinedProxyOptions, StoreProxy } from '@graphql-mesh/store';
import {
getInterpolatedHeadersFactory,
getInterpolatedStringFactory,
ResolverData,
stringInterpolator,
} from '@graphql-mesh/string-interpolation';
Expand Down Expand Up @@ -89,22 +90,46 @@ export default class SupergraphHandler implements MeshHandler {
this.config.operationHeaders != null
? getInterpolatedHeadersFactory(this.config.operationHeaders)
: undefined;
const joingraphEnum = supergraphSdl.definitions.find(
def => def.kind === 'EnumTypeDefinition' && def.name.value === 'join__Graph',
) as EnumTypeDefinitionNode;
const subgraphNameIdMap = new Map<string, string>();
if (joingraphEnum) {
joingraphEnum.values?.forEach(value => {
value.directives?.forEach(directive => {
if (directive.name.value === 'join__graph') {
const nameArg = directive.arguments?.find(arg => arg.name.value === 'name');
if (nameArg?.value?.kind === 'StringValue') {
subgraphNameIdMap.set(value.name.value, nameArg.value.value);
}
}
});
});
}
const schema = getStitchedSchemaFromSupergraphSdl({
supergraphSdl,
onExecutor: ({ subgraphName, endpoint }) => {
onExecutor: ({ subgraphName, endpoint: nonInterpolatedEndpoint }) => {
const subgraphRealName = subgraphNameIdMap.get(subgraphName);
const subgraphConfiguration: YamlConfig.SubgraphConfiguration = subgraphConfigs.find(
subgraphConfig => subgraphConfig.name === subgraphName,
subgraphConfig => subgraphConfig.name === subgraphRealName,
) || {
name: subgraphName,
};
nonInterpolatedEndpoint = subgraphConfiguration.endpoint || nonInterpolatedEndpoint;
const endpointFactory = getInterpolatedStringFactory(nonInterpolatedEndpoint);
return buildHTTPExecutor({
endpoint,
...(subgraphConfiguration as any),
fetch: fetchFn,
endpoint: nonInterpolatedEndpoint,
fetch(url: string, init: any, context: any, info: any) {
const endpoint = endpointFactory({
env: process.env,
context,
info,
});
url = url.replace(nonInterpolatedEndpoint, endpoint);
return fetchFn(url, init, context, info);
},
headers(executorRequest) {
const subgraphConfiguration = subgraphConfigs.find(
subgraphConfig => subgraphConfig.name === subgraphName,
);
const headers = {};
const resolverData: ResolverData = {
root: executorRequest.rootValue,
Expand All @@ -122,6 +147,7 @@ export default class SupergraphHandler implements MeshHandler {
if (operationHeadersFactory) {
Object.assign(headers, operationHeadersFactory(resolverData));
}
return headers;
},
} as HTTPExecutorOptions);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const resolvers = {
Author: {
__resolveReference(author: { id: string }) {
return authors.find(a => a.id === author.id);
},
},
Query: {
author(_: any, { id }: { id: string }) {
return authors.find(a => a.id === id);
},
authors() {
return authors;
},
},
};

const authors = [
{
id: '1',
name: 'Jane Doe',
},
{
id: '2',
name: 'John Doe',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable import/no-nodejs-modules */
/* eslint-disable import/no-extraneous-dependencies */
import { readFileSync } from 'fs';
import { join } from 'path';
import { GraphQLError, parse } from 'graphql';
import { createYoga } from 'graphql-yoga';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { resolvers } from './resolvers';

export const AUTH_HEADER = 'Bearer AUTHORS_SECRET';

export const server = createYoga({
schema: buildSubgraphSchema({
typeDefs: parse(readFileSync(join(__dirname, 'typeDefs.graphql'), 'utf-8')),
resolvers,
}),
plugins: [
{
onRequest({ request }) {
if (request.url.includes('skip-auth')) {
return;
}
const authHeader = request.headers.get('authorization');
if (authHeader !== AUTH_HEADER) {
throw new GraphQLError('Unauthorized', {
extensions: {
http: {
status: 401,
},
},
});
}
},
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type Author @key(fields: "id") {
id: ID!
name: String!
birthDate: String
}

type Query {
authors: [Author]
author(id: ID!): Author
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export const resolvers = {
Book: {
__resolveReference(book: { id: string }) {
return books.find(b => b.id === book.id);
},
author(book: { authorId: string }) {
return { __typename: 'Author', id: book.authorId };
},
},
Author: {
__resolveReference(author: { id: string }) {
return books.filter(b => b.authorId === author.id);
},
books(author: { id: string }) {
return books.filter(b => b.authorId === author.id);
},
},
Query: {
book(_: any, { id }: { id: string }) {
return books.find(b => b.id === id);
},
books() {
return books;
},
},
};

const books = [
{
id: '1',
title: 'Awesome Book',
authorId: '1',
},
{
id: '2',
title: 'Book of Secrets',
authorId: '2',
},
{
id: '3',
title: 'Book of Mystery',
authorId: '2',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable import/no-nodejs-modules */
/* eslint-disable import/no-extraneous-dependencies */
import { readFileSync } from 'fs';
import { join } from 'path';
import { GraphQLError, parse } from 'graphql';
import { createYoga } from 'graphql-yoga';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { resolvers } from './resolvers';

export const AUTH_HEADER = 'Bearer BOOKS_SECRET';

export const server = createYoga({
schema: buildSubgraphSchema({
typeDefs: parse(readFileSync(join(__dirname, 'typeDefs.graphql'), 'utf-8')),
resolvers,
}),
plugins: [
{
onRequest({ request }) {
if (request.url.includes('skip-auth')) {
return;
}
const authHeader = request.headers.get('authorization');
if (authHeader !== AUTH_HEADER) {
throw new GraphQLError('Unauthorized', {
extensions: {
http: {
status: 401,
},
},
});
}
},
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
type Book @key(fields: "id") {
id: ID!
title: String!
genre: String
author: Author!
}

type Author @extends @key(fields: "id") {
id: ID! @external
books: [Book]
}

type Query {
books: [Book]
book(id: ID!): Book
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
schema
@core(feature: "https://specs.apollo.dev/core/v0.2")
@core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) {
query: Query
}

directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA

directive @join__field(
graph: join__Graph
provides: join__FieldSet
requires: join__FieldSet
) on FIELD_DEFINITION

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT

directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT

type Author
@join__owner(graph: AUTHORS)
@join__type(graph: AUTHORS, key: "id")
@join__type(graph: BOOKS, key: "id") {
birthDate: String @join__field(graph: AUTHORS)
books: [Book] @join__field(graph: BOOKS)
id: ID! @join__field(graph: AUTHORS)
name: String! @join__field(graph: AUTHORS)
}

type Book @join__owner(graph: BOOKS) @join__type(graph: BOOKS, key: "id") {
author: Author! @join__field(graph: BOOKS)
genre: String @join__field(graph: BOOKS)
id: ID! @join__field(graph: BOOKS)
title: String! @join__field(graph: BOOKS)
}

type Query {
author(id: ID!): Author @join__field(graph: AUTHORS)
authors: [Author] @join__field(graph: AUTHORS)
book(id: ID!): Book @join__field(graph: BOOKS)
books: [Book] @join__field(graph: BOOKS)
}

enum core__Purpose {
"""
`EXECUTION` features provide metadata necessary to for operation execution.
"""
EXECUTION

"""
`SECURITY` features provide metadata necessary to securely resolve fields.
"""
SECURITY
}

scalar join__FieldSet

enum join__Graph {
AUTHORS @join__graph(name: "authors", url: "http://authors/graphql")
BOOKS @join__graph(name: "books", url: "http://books/graphql")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
subgraphs:
authors:
routing_url: http://authors/graphql
schema:
file: ./service-author/typeDefs.graphql
books:
routing_url: http://books/graphql
schema:
file: ./service-book/typeDefs.graphql
Loading

0 comments on commit 425afee

Please sign in to comment.