Skip to content

Commit

Permalink
Helper package to create a basic Stitching gateway based on directive…
Browse files Browse the repository at this point in the history
…s via HTTP
  • Loading branch information
ardatan committed Mar 14, 2023
1 parent 37b2afe commit 7e1791f
Show file tree
Hide file tree
Showing 5 changed files with 431 additions and 0 deletions.
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

0 comments on commit 7e1791f

Please sign in to comment.