Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Helper package to create a basic Stitching gateway based on directives via HTTP #5115

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/many-stingrays-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/stitching-directives-http': patch
---

Helper package to create a basic Stitching gateway based on directives via HTTP
72 changes: 72 additions & 0 deletions packages/stitching-directives-http/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "@graphql-tools/stitching-directives-http",
"version": "0.0.0",
"description": "A set of utils for faster development of GraphQL tools",
"repository": {
"type": "git",
"url": "ardatan/graphql-tools",
"directory": "packages/stitching-directives-http"
},
"license": "MIT",
"sideEffects": false,
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"exports": {
".": {
"require": {
"types": "./dist/typings/index.d.cts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
},
"default": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"./*": {
"require": {
"types": "./dist/typings/*.d.cts",
"default": "./dist/cjs/*.js"
},
"import": {
"types": "./dist/typings/*.d.ts",
"default": "./dist/esm/*.js"
},
"default": {
"types": "./dist/typings/*.d.ts",
"default": "./dist/esm/*.js"
}
},
"./package.json": "./package.json"
},
"typings": "dist/typings/index.d.ts",
"typescript": {
"definition": "dist/typings/index.d.ts"
},
"peerDependencies": {
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
},
"buildOptions": {
"input": "./src/index.ts"
},
"dependencies": {
"@graphql-tools/stitching-directives": "^2.3.31",
"@graphql-tools/executor-http": "^0.1.9",
"@graphql-tools/delegate": "^9.0.28",
"@graphql-tools/stitch": "^8.7.43",
"@graphql-tools/wrap": "^9.3.8",
"@graphql-tools/utils": "^9.2.1",
"tslib": "^2.4.0"
},
"devDependencies": {
"@graphql-tools/schema": "9.0.16"
},
"publishConfig": {
"directory": "dist",
"access": "public"
},
"type": "module"
}
86 changes: 86 additions & 0 deletions packages/stitching-directives-http/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { SubschemaConfig } from '@graphql-tools/delegate';
import { buildHTTPExecutor, HTTPExecutorOptions } from '@graphql-tools/executor-http';
import { stitchSchemas } from '@graphql-tools/stitch';
import { stitchingDirectives, StitchingDirectivesOptions } from '@graphql-tools/stitching-directives';
import { ExecutionResult, isAsyncIterable } from '@graphql-tools/utils';
import { buildASTSchema, buildSchema, DocumentNode, GraphQLSchema, parse } from 'graphql';

export type StitchingDirectivesHTTPService = (
| {
sdl: string;
url: string;
}
| {
schema: GraphQLSchema;
url: string;
}
| {
ast: DocumentNode;
url: string;
}
| {
sdlQuery: string | DocumentNode;
getSdlFromResult: (result: ExecutionResult) => string;
url: string;
}
) &
Omit<HTTPExecutorOptions, 'endpoint'> &
Omit<SubschemaConfig, 'schema' | 'executor'>;

async function getSubschemaConfig(service: StitchingDirectivesHTTPService): Promise<SubschemaConfig> {
const executor = buildHTTPExecutor({
endpoint: service.url,
...service,
});
let schema: GraphQLSchema;
if ('sdl' in service) {
schema = buildSchema(service.sdl, {
assumeValidSDL: true,
assumeValid: true,
});
} else if ('schema' in service) {
schema = service.schema;
} else if ('ast' in service) {
schema = buildASTSchema(service.ast, {
assumeValidSDL: true,
assumeValid: true,
});
} else {
const sdlQueryResult = await executor({
document: typeof service.sdlQuery === 'string' ? parse(service.sdlQuery) : service.sdlQuery,
});
if (isAsyncIterable(sdlQueryResult)) {
throw new Error('sdlQuery must return a single result');
}
const sdl = service.getSdlFromResult(sdlQueryResult);
schema = buildSchema(sdl, {
assumeValidSDL: true,
assumeValid: true,
});
}
return {
schema,
executor,
};
}

export async function createStitchingDirectivesHTTPGateway(
services: StitchingDirectivesHTTPService[],
opts?: StitchingDirectivesOptions & {
sdlQuery?: string | DocumentNode;
}
): Promise<GraphQLSchema> {
const { stitchingDirectivesTransformer } = stitchingDirectives(opts);
const subschemas = await Promise.all(
services.map(service =>
getSubschemaConfig({
sdlQuery: opts?.sdlQuery,
...service,
})
)
);
return stitchSchemas({
subschemas,
subschemaConfigTransforms: [stitchingDirectivesTransformer],
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { createYoga, createSchema } from 'graphql-yoga';
import { stitchingDirectives } from '@graphql-tools/stitching-directives';
import { GraphQLSchema, parse } from 'graphql';
import { createStitchingDirectivesHTTPGateway } from '@graphql-tools/stitching-directives-http';
import { normalizedExecutor } from '@graphql-tools/executor';
import { printSchemaWithDirectives } from '@graphql-tools/utils';

describe('stitching-directives-http', () => {
const { stitchingDirectivesTypeDefs } = stitchingDirectives();

const books = [
{
id: '1',
title: 'Harry Potter and the Chamber of Secrets',
},
{
id: '2',
title: 'Jurassic Park',
},
{
id: '3',
title: 'The Hobbit',
},
];

const authors = [
{
id: '1',
name: 'J.K. Rowling',
},
{
id: '2',
name: 'Michael Crichton',
},
{
id: '3',
name: 'J.R.R. Tolkien',
},
];

const booksWithAuthors = [
{
id: '1',
author: {
id: '1',
},
},
{
id: '2',
author: {
id: '2',
},
},
{
id: '3',
author: {
id: '3',
},
},
];

const bookSchema = createSchema({
typeDefs: /* GraphQL */ `
${stitchingDirectivesTypeDefs}
type Query {
book(id: ID!): Book! @merge(keyField: "id") @canonical
}
type Book {
id: ID!
title: String!
}
`,
resolvers: {
Query: {
book: (parent, args) => books.find(book => book.id === args.id),
},
},
});

const authorSchema = createSchema({
typeDefs: /* GraphQL */ `
${stitchingDirectivesTypeDefs}
type Query {
author(id: ID!): Author! @merge(keyField: "id") @canonical
}
type Author {
id: ID!
name: String!
}
`,
resolvers: {
Query: {
author: (parent, args) => authors.find(author => author.id === args.id),
},
},
});

const bookWithAuthor = createSchema({
typeDefs: /* GraphQL */ `
${stitchingDirectivesTypeDefs}
type Query {
book(id: ID!): Book! @merge(keyField: "id")
}
type Book {
id: ID!
author: Author!
}
type Author {
id: ID!
}
`,
resolvers: {
Query: {
book: (parent, args) => booksWithAuthors.find(bookWithAuthor => bookWithAuthor.id === args.id),
},
},
});

const bookServer = createYoga({
schema: bookSchema,
});

const authorServer = createYoga({
schema: authorSchema,
});

const bookWithAuthorServer = createYoga({
schema: bookWithAuthor,
});

let gateway: GraphQLSchema;
beforeAll(async () => {
gateway = await createStitchingDirectivesHTTPGateway([
{
url: 'http://localhost:4001/graphql',
sdl: printSchemaWithDirectives(bookSchema),
fetch: bookServer.fetch,
},
{
url: 'http://localhost:4002/graphql',
sdl: printSchemaWithDirectives(authorSchema),
fetch: authorServer.fetch,
},
{
url: 'http://localhost:4003/graphql',
sdl: printSchemaWithDirectives(bookWithAuthor),
fetch: bookWithAuthorServer.fetch,
},
]);
});
it('should work', async () => {
const result = await normalizedExecutor({
schema: gateway,
document: parse(/* GraphQL */ `
query {
book(id: "1") {
id
title
author {
id
name
}
}
}
`),
});
expect(result).toMatchInlineSnapshot(`
{
"data": {
"book": {
"author": {
"id": "1",
"name": "J.K. Rowling",
},
"id": "1",
"title": "Harry Potter and the Chamber of Secrets",
},
},
}
`);
});
});
Loading