From 38856ddb4fb28d8aa1775084552a1b063d3bc74b Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Mon, 13 Apr 2020 18:27:52 -0400 Subject: [PATCH 01/21] Allow http to be optional --- packages/graphyne-core/src/core.ts | 5 +++-- packages/graphyne-core/src/types.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/graphyne-core/src/core.ts b/packages/graphyne-core/src/core.ts index 3140d07..c146876 100644 --- a/packages/graphyne-core/src/core.ts +++ b/packages/graphyne-core/src/core.ts @@ -102,7 +102,7 @@ export abstract class GraphyneServerBase { variables, operationName, context: integrationContext, - http: { request }, + http: { request } = {}, } = requestCtx; if (!query) { @@ -164,7 +164,8 @@ export abstract class GraphyneServerBase { }); } - if (request.method === 'GET' && operation !== 'query') { + // http.request is not available in ws + if (request && request.method === 'GET' && operation !== 'query') { // Mutation is not allowed with GET request return createResponse(405, { errors: [ diff --git a/packages/graphyne-core/src/types.ts b/packages/graphyne-core/src/types.ts index 1cfcf99..8968719 100644 --- a/packages/graphyne-core/src/types.ts +++ b/packages/graphyne-core/src/types.ts @@ -35,8 +35,8 @@ export interface HTTPQueryBody { export interface HttpQueryRequest extends HTTPQueryBody { context: IntegrationContext; - http: { - request: IncomingMessage; + http?: { + request: Pick; response: ServerResponse; }; } From 1b0ca7e2066e759fdcc2515cfbcb9e787b63e016 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Mon, 13 Apr 2020 23:28:11 -0400 Subject: [PATCH 02/21] Change type names --- packages/graphyne-core/src/core.ts | 10 +++++----- packages/graphyne-core/src/types.ts | 6 +++--- packages/graphyne-core/src/utils.ts | 8 ++++---- packages/graphyne-express/src/graphyneExpress.ts | 2 +- packages/graphyne-server/src/graphyneServer.ts | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/graphyne-core/src/core.ts b/packages/graphyne-core/src/core.ts index c146876..ce31d64 100644 --- a/packages/graphyne-core/src/core.ts +++ b/packages/graphyne-core/src/core.ts @@ -12,9 +12,9 @@ import lru, { Lru } from 'tiny-lru'; import { Config, QueryCache, - HttpQueryRequest, + QueryRequest, HTTPHeaders, - HttpQueryResponse, + QueryResponse, } from './types'; function buildCache(opts: Config) { @@ -74,9 +74,9 @@ export abstract class GraphyneServerBase { } } - protected runHTTPQuery( - requestCtx: HttpQueryRequest, - cb: (err: any, result: HttpQueryResponse) => void + protected runQuery( + requestCtx: QueryRequest, + cb: (err: any, result: QueryResponse) => void ): void { let compiledQuery: CompiledQuery | ExecutionResult; const headers: HTTPHeaders = { 'content-type': 'application/json' }; diff --git a/packages/graphyne-core/src/types.ts b/packages/graphyne-core/src/types.ts index 8968719..f085c1c 100644 --- a/packages/graphyne-core/src/types.ts +++ b/packages/graphyne-core/src/types.ts @@ -27,13 +27,13 @@ export type HTTPHeaders = Record; export type VariableValues = { [name: string]: any }; -export interface HTTPQueryBody { +export interface QueryBody { query?: string; variables?: VariableValues; operationName?: string; } -export interface HttpQueryRequest extends HTTPQueryBody { +export interface QueryRequest extends QueryBody { context: IntegrationContext; http?: { request: Pick; @@ -41,7 +41,7 @@ export interface HttpQueryRequest extends HTTPQueryBody { }; } -export interface HttpQueryResponse { +export interface QueryResponse { status: number; body: string; headers: HTTPHeaders; diff --git a/packages/graphyne-core/src/utils.ts b/packages/graphyne-core/src/utils.ts index 424678a..a9c9798 100644 --- a/packages/graphyne-core/src/utils.ts +++ b/packages/graphyne-core/src/utils.ts @@ -1,10 +1,10 @@ import { IncomingMessage } from 'http'; -import { VariableValues, HTTPQueryBody, HttpQueryRequest } from './types'; +import { VariableValues, QueryBody, QueryRequest } from './types'; -type GraphQLParams = Partial; +type GraphQLParams = Partial; type GraphQLParamsInput = { queryParams: Record; - body: HTTPQueryBody | string | undefined; + body: QueryBody | string | undefined; }; export function getGraphQLParams({ @@ -29,7 +29,7 @@ export function parseNodeRequest( req: IncomingMessage & { body?: any; }, - cb: (err: any, parsedBody?: HTTPQueryBody) => void + cb: (err: any, parsedBody?: QueryBody) => void ): void { // If body has been parsed as a keyed object, use it. if (typeof req.body === 'object' && !(req.body instanceof Buffer)) { diff --git a/packages/graphyne-express/src/graphyneExpress.ts b/packages/graphyne-express/src/graphyneExpress.ts index 1a4fba6..a94b297 100644 --- a/packages/graphyne-express/src/graphyneExpress.ts +++ b/packages/graphyne-express/src/graphyneExpress.ts @@ -32,7 +32,7 @@ export class GraphyneServer extends GraphyneServerBase { queryParams: req.query as Record, }); - this.runHTTPQuery( + this.runQuery( { query, context, diff --git a/packages/graphyne-server/src/graphyneServer.ts b/packages/graphyne-server/src/graphyneServer.ts index b34f71e..6487f0c 100644 --- a/packages/graphyne-server/src/graphyneServer.ts +++ b/packages/graphyne-server/src/graphyneServer.ts @@ -32,7 +32,7 @@ export class GraphyneServer extends GraphyneServerBase { queryParams: queryParams || {}, body: parsedBody, }); - this.runHTTPQuery( + this.runQuery( { query, context, From 97c96b56d00a8be0f623360b0ceb2630f2aab2e1 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Mon, 13 Apr 2020 23:50:57 -0400 Subject: [PATCH 03/21] Remove graphyne-express --- packages/graphyne-express/README.md | 70 ------------------ packages/graphyne-express/package.json | 40 ---------- .../graphyne-express/src/graphyneExpress.ts | 74 ------------------- packages/graphyne-express/src/index.ts | 1 - packages/graphyne-express/tsconfig.json | 14 ---- 5 files changed, 199 deletions(-) delete mode 100644 packages/graphyne-express/README.md delete mode 100644 packages/graphyne-express/package.json delete mode 100644 packages/graphyne-express/src/graphyneExpress.ts delete mode 100644 packages/graphyne-express/src/index.ts delete mode 100644 packages/graphyne-express/tsconfig.json diff --git a/packages/graphyne-express/README.md b/packages/graphyne-express/README.md deleted file mode 100644 index 1e0b6f2..0000000 --- a/packages/graphyne-express/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# Graphyne Server Express - -The Express and Connect integration of [Graphyne](/). - -## Install - -```shell -npm i graphyne-express graphql -// or -yarn add graphyne-express graphql -``` - -## Usage - -Check out the [example](/examples/with-express). - -```javascript -const express = require('express'); -const { GraphyneServer } = require('graphyne-express'); - -const graphyne = new GraphyneServer(options); - -var app = express(); - -// GraphQL API -app.all('/graphql', graphyne.createHandler()); -// GraphiQL -app.get('/___graphql', graphyle.createHandler({ - path: '/graphql' // Must be set: GraphQL's path, not GraphiQL - graphiql: true -})) - -app.listen(4000); -console.log('Running a GraphQL API server at http://localhost:4000/graphql'); -``` - -## API - -### `new GraphyneServer(options)` - -Refer to [API](/#new-graphyneserveroptions). - -### `GraphyneServer#createHandler(options)` - -Create a handler for Express/Connect Router. Refer to [API](/#graphyneservercreatehandleroptions). - -When `options.path` and `options.graphiql.path` is not set, they do not default to `/graphql` and `/___graphql` respectively. Instead, `graphyne-express` wouuld response to all incoming requests. This is because routing is delegated to Express/Connect Router (see in [Usage](#usage)). - -#### Using with GraphiQL - -If `options.graphiql` is to be used, `options.path` **must** be set. This is because routing is delegated to Express/Connect Router, so `Graphyle` is not aware of where the GraphQL endpoint is exposed. - -#### Using with Connect - -Connect router only has `.use`, which matches only the beginning of the URL. Therefore, GraphQL endpoints will also be exposed in sub-directories if `path` is not set. - -```javascript -connect.use('/graphql', graphyne.createHandler()) -// `/graphql`, `/graphql/foo`, `/graphql/foo/bar` are all GraphQL endpoint. -// You should explicitly defining options.path -connect.use('/graphql', graphyne.createHandler({ - path: '/graphql' -})) -``` - -Same things applies to `options.graphiql.path`. - -## License - -[MIT](/LICENSE) diff --git a/packages/graphyne-express/package.json b/packages/graphyne-express/package.json deleted file mode 100644 index c949054..0000000 --- a/packages/graphyne-express/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "graphyne-express", - "version": "0.1.2", - "description": "Lightning-fast JavaScript GraphQL Server for Express.js", - "author": "Hoang Vo (https://hoangvvo.com)", - "keywords": [ - "graphql", - "server", - "api", - "express", - "connect" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/hoangvvo/graphyne.git" - }, - "bugs": { - "url": "https://github.com/hoangvvo/graphyne/issues" - }, - "homepage": "https://github.com/hoangvvo/graphyne#readme", - "main": "lib/index.js", - "types": "lib/index.d.ts", - "files": [ - "lib" - ], - "scripts": { - "build": "tsc" - }, - "license": "MIT", - "dependencies": { - "graphyne-core": "^0.1.2" - }, - "devDependencies": { - "@types/express": "^4.17.4", - "express": "^4.17.1" - }, - "engines": { - "node": ">=10.0.0" - } -} diff --git a/packages/graphyne-express/src/graphyneExpress.ts b/packages/graphyne-express/src/graphyneExpress.ts deleted file mode 100644 index a94b297..0000000 --- a/packages/graphyne-express/src/graphyneExpress.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - GraphyneServerBase, - Config, - getGraphQLParams, - renderGraphiQL, - parseNodeRequest, - HandlerConfig, -} from 'graphyne-core'; -import { Request, Response, NextFunction, RequestHandler } from 'express'; - -export class GraphyneServer extends GraphyneServerBase { - constructor(options: Config) { - super(options); - } - - createHandler(options?: HandlerConfig): RequestHandler { - if (options?.graphiql && !options.path) - throw new Error( - 'createHandler: options.path must be set to use options.graphiql' - ); - - return (req: Request, res: Response, next: NextFunction) => { - const path = options?.path; - - // serve GraphQL - if (!path || path === req.path) { - return parseNodeRequest(req, (err, parsedBody) => { - if (err) return next(err); - const context: Record = { req, res }; - const { query, variables, operationName } = getGraphQLParams({ - body: parsedBody, - queryParams: req.query as Record, - }); - - this.runQuery( - { - query, - context, - variables, - operationName, - http: { - request: req, - response: res, - }, - }, - (err, { status, body, headers }) => { - for (const key in headers) { - const headVal = headers[key]; - if (headVal) res.setHeader(key, headVal); - } - res.status(status).end(body); - } - ); - }); - } - - // serve GraphiQL - if (options?.graphiql) { - const graphiql = options.graphiql; - const graphiqlPath = - typeof graphiql === 'object' ? graphiql.path : null; - if (!graphiqlPath || req.path === graphiqlPath) { - const defaultQuery = - typeof graphiql === 'object' ? graphiql.defaultQuery : undefined; - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - return res.send(renderGraphiQL({ path, defaultQuery })); - } - } - - // If path not matched - next(); - }; - } -} diff --git a/packages/graphyne-express/src/index.ts b/packages/graphyne-express/src/index.ts deleted file mode 100644 index 1172137..0000000 --- a/packages/graphyne-express/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { GraphyneServer } from './graphyneExpress'; diff --git a/packages/graphyne-express/tsconfig.json b/packages/graphyne-express/tsconfig.json deleted file mode 100644 index 52529f5..0000000 --- a/packages/graphyne-express/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2018"], - "module": "commonjs", - "target": "es2018", - "rootDir": "src", - "outDir": "lib", - "strict": true, - "declaration": true, - "skipLibCheck": true - }, - "include": ["./src/**/*"], - "exclude": ["node_modules", "lib"] -} From 8f6fe0e5d292077d607c77bc7167adf8972d5881 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Mon, 13 Apr 2020 23:54:38 -0400 Subject: [PATCH 04/21] Remove default path etc. --- packages/graphyne-core/src/core.ts | 2 -- packages/graphyne-server/src/graphyneServer.ts | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/graphyne-core/src/core.ts b/packages/graphyne-core/src/core.ts index ce31d64..6823e61 100644 --- a/packages/graphyne-core/src/core.ts +++ b/packages/graphyne-core/src/core.ts @@ -48,8 +48,6 @@ export abstract class GraphyneServerBase { private lruErrors: Lru> | null; private schema: GraphQLSchema; protected options: Config; - protected DEFAULT_PATH = '/graphql'; - protected DEFAULT_GRAPHIQL_PATH = '/___graphql'; constructor(options: Config) { // validate options if (!options) { diff --git a/packages/graphyne-server/src/graphyneServer.ts b/packages/graphyne-server/src/graphyneServer.ts index 6487f0c..5255f94 100644 --- a/packages/graphyne-server/src/graphyneServer.ts +++ b/packages/graphyne-server/src/graphyneServer.ts @@ -17,11 +17,11 @@ export class GraphyneServer extends GraphyneServerBase { createHandler(options?: HandlerConfig): RequestListener { return (req: IncomingMessage, res: ServerResponse) => { - const path = options?.path || this.DEFAULT_PATH; - + const path = options?.path; + // TODO: Avoid unneccessary parsing const { pathname, query: queryParams } = parseUrl(req, true) || {}; // serve GraphQL - if (pathname === path) { + if (!path || pathname === path) { return parseNodeRequest(req, (err, parsedBody) => { if (err) { res.statusCode = err.status || 500; From ba90b2fcce8be0c7231ce0217d11154d946315c5 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Mon, 13 Apr 2020 23:56:52 -0400 Subject: [PATCH 05/21] Remove graphyne-express example --- .../{graphyne-express.js => graphyne-server-express.js} | 2 +- bench/package.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) rename bench/benchmarks/{graphyne-express.js => graphyne-server-express.js} (78%) diff --git a/bench/benchmarks/graphyne-express.js b/bench/benchmarks/graphyne-server-express.js similarity index 78% rename from bench/benchmarks/graphyne-express.js rename to bench/benchmarks/graphyne-server-express.js index 714a8f1..9af18a4 100644 --- a/bench/benchmarks/graphyne-express.js +++ b/bench/benchmarks/graphyne-server-express.js @@ -1,5 +1,5 @@ const express = require('express'); -const { GraphyneServer } = require('graphyne-express'); +const { GraphyneServer } = require('graphyne-server'); const { schema } = require('../buildSchema'); const graphyne = new GraphyneServer({ diff --git a/bench/package.json b/bench/package.json index 90cec4f..8b608b8 100644 --- a/bench/package.json +++ b/bench/package.json @@ -10,7 +10,6 @@ "express": "^4.17.1", "express-graphql": "latest", "faker": "^4.1.0", - "graphyne-express": ">0.0.0", "graphyne-server": ">0.0.0", "markdown-table": "^2.0.0", "md5": "^2.2.1", From 3c89a8ca5c6b3416bae6504ef5b7fd72435f1a14 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 11:51:11 -0400 Subject: [PATCH 06/21] Remove graphyne-express from benchmark --- .github/actions/graphql-bench-pr/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/graphql-bench-pr/src/index.js b/.github/actions/graphql-bench-pr/src/index.js index 414fcbf..87bd7e4 100644 --- a/.github/actions/graphql-bench-pr/src/index.js +++ b/.github/actions/graphql-bench-pr/src/index.js @@ -51,7 +51,7 @@ async function runManyBenches(dir, packages) { async function getStats(repo, ref) { const dir = repo.replace('/', '-'); await prepareRepo(repo, ref, dir); - await runManyBenches(dir, ['graphyne-express', 'graphyne-server']); + await runManyBenches(dir, ['graphyne-server-express', 'graphyne-server']); // Get the result const resultsPath = join(cwd, dir, 'bench', 'results'); const resultObj = {}; From bc4214b9ada24f5607a859c31b17f8b533699da7 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 13:05:43 -0400 Subject: [PATCH 07/21] Refactor and done new graphyneServer --- packages/graphyne-core/src/core.ts | 45 ++-------- packages/graphyne-core/src/index.ts | 6 +- packages/graphyne-core/src/types.ts | 20 ++--- packages/graphyne-core/src/utils.ts | 13 +++ .../graphyne-server/src/graphyneServer.ts | 87 ++++++++++++------- 5 files changed, 89 insertions(+), 82 deletions(-) diff --git a/packages/graphyne-core/src/core.ts b/packages/graphyne-core/src/core.ts index 6823e61..8ce526d 100644 --- a/packages/graphyne-core/src/core.ts +++ b/packages/graphyne-core/src/core.ts @@ -16,6 +16,7 @@ import { HTTPHeaders, QueryResponse, } from './types'; +import { resolveMaybePromise } from './utils'; function buildCache(opts: Config) { if (opts.cache) { @@ -28,19 +29,6 @@ function buildCache(opts: Config) { return lru(1024); } -function resolveMaybePromise( - value: T | Promise, - cb: (err: any, result: T) => void -): void { - // @ts-ignore - if (value && typeof value.then === 'function') { - (value as Promise).then( - (resolve: any) => cb(null, resolve), - (reject: any) => cb(reject, reject) - ); - } else cb(null, value as T); -} - export abstract class GraphyneServerBase { private lru: Lru< Pick @@ -90,7 +78,6 @@ export abstract class GraphyneServerBase { }); } - let context: Record; let rootValue = {}; let document; let operation; @@ -99,7 +86,7 @@ export abstract class GraphyneServerBase { query, variables, operationName, - context: integrationContext, + context, http: { request } = {}, } = requestCtx; @@ -109,7 +96,7 @@ export abstract class GraphyneServerBase { }); } - const { context: contextFn, rootValue: rootValueFn } = this.options; + const { rootValue: rootValueFn } = this.options; // Get graphql-jit compiled query and parsed document let cached = this.lru !== null && this.lru.get(query); @@ -180,28 +167,10 @@ export abstract class GraphyneServerBase { } else rootValue = rootValueFn; } - if (contextFn) { - if (typeof contextFn === 'function') { - context = contextFn(integrationContext); - } else context = contextFn; - } else { - context = integrationContext; - } - - return resolveMaybePromise(context, (err, contextVal) => { - if (err) { - err.message = `Error creating context: ${err.message}`; - return createResponse(err.status || 500, { errors: [err] }); - } - return resolveMaybePromise( - (compiledQuery as CompiledQuery).query( - rootValue, - contextVal, - variables - ), - (err, result) => createResponse(200, result) - ); - }); + return resolveMaybePromise( + (compiledQuery as CompiledQuery).query(rootValue, context, variables), + (err, result) => createResponse(200, result) + ); } abstract createHandler(...args: any[]): any; diff --git a/packages/graphyne-core/src/index.ts b/packages/graphyne-core/src/index.ts index 73ce8b9..72e5cd7 100644 --- a/packages/graphyne-core/src/index.ts +++ b/packages/graphyne-core/src/index.ts @@ -1,4 +1,8 @@ export { GraphyneServerBase } from './core'; export * from './types'; -export { getGraphQLParams, parseNodeRequest } from './utils'; +export { + getGraphQLParams, + parseNodeRequest, + resolveMaybePromise, +} from './utils'; export { renderGraphiQL } from './graphiql'; diff --git a/packages/graphyne-core/src/types.ts b/packages/graphyne-core/src/types.ts index f085c1c..5aa9afd 100644 --- a/packages/graphyne-core/src/types.ts +++ b/packages/graphyne-core/src/types.ts @@ -2,27 +2,23 @@ import { GraphQLError, GraphQLSchema, DocumentNode } from 'graphql'; import { CompiledQuery } from 'graphql-jit'; import { IncomingMessage, ServerResponse } from 'http'; -type IntegrationContext = Record; - export interface Config, TRootValue = any> { schema: GraphQLSchema; - context?: TContext | ((intergrationContext: IntegrationContext) => TContext); + context?: TContext | ((...args: any[]) => TContext); rootValue?: (parsedQuery: DocumentNode) => TRootValue | TRootValue; cache?: number | boolean; } export interface HandlerConfig { path?: string; - graphiql?: boolean | GraphiQLConfig; + graphiql: + | boolean + | { + path?: string; + defaultQuery?: string; + }; } -export type GraphiQLConfig = - | boolean - | { - path?: string; - defaultQuery?: string; - }; - export type HTTPHeaders = Record; export type VariableValues = { [name: string]: any }; @@ -34,7 +30,7 @@ export interface QueryBody { } export interface QueryRequest extends QueryBody { - context: IntegrationContext; + context: Record; http?: { request: Pick; response: ServerResponse; diff --git a/packages/graphyne-core/src/utils.ts b/packages/graphyne-core/src/utils.ts index a9c9798..eba8b75 100644 --- a/packages/graphyne-core/src/utils.ts +++ b/packages/graphyne-core/src/utils.ts @@ -76,3 +76,16 @@ export function parseNodeRequest( export function safeSerialize(data?: string) { return data ? JSON.stringify(data).replace(/\//g, '\\/') : ''; } + +export function resolveMaybePromise( + value: T | Promise, + cb: (err: any, result: T) => void +): void { + // @ts-ignore + if (value && typeof value.then === 'function') { + (value as Promise).then( + (resolve: any) => cb(null, resolve), + (reject: any) => cb(reject, reject) + ); + } else cb(null, value as T); +} diff --git a/packages/graphyne-server/src/graphyneServer.ts b/packages/graphyne-server/src/graphyneServer.ts index 5255f94..c545fae 100644 --- a/packages/graphyne-server/src/graphyneServer.ts +++ b/packages/graphyne-server/src/graphyneServer.ts @@ -4,63 +4,88 @@ import { Config, parseNodeRequest, getGraphQLParams, - renderGraphiQL, HandlerConfig, + renderGraphiQL, + resolveMaybePromise, } from 'graphyne-core'; // @ts-ignore import parseUrl from '@polka/url'; +const DEFAULT_PATH = '/graphql'; +const DEFAULT_GRAPHIQL_PATH = '/___graphql'; + export class GraphyneServer extends GraphyneServerBase { constructor(options: Config) { super(options); } createHandler(options?: HandlerConfig): RequestListener { - return (req: IncomingMessage, res: ServerResponse) => { - const path = options?.path; - // TODO: Avoid unneccessary parsing - const { pathname, query: queryParams } = parseUrl(req, true) || {}; + return (...args: any[]) => { + // Integration mapping + const req: IncomingMessage & { + path: string; + query: Record; + } = args[0]; + const res: ServerResponse = args[1]; + + // Parse req.url + let pathname = req.path; + let queryParams = req.query; + if (!pathname || !queryParams) { + const parsedUrl = parseUrl(req, true); + pathname = parsedUrl.pathname; + queryParams = parsedUrl.queryParams; + } + // serve GraphQL - if (!path || pathname === path) { + const path = options?.path ?? DEFAULT_PATH; + if (pathname === path) { return parseNodeRequest(req, (err, parsedBody) => { if (err) { res.statusCode = err.status || 500; return res.end(Buffer.from(err)); } - const context: Record = { req, res }; const { query, variables, operationName } = getGraphQLParams({ queryParams: queryParams || {}, body: parsedBody, }); - this.runQuery( - { - query, - context, - variables, - operationName, - http: { - request: req, - response: res, + const contextFn = this.options.context; + const context = contextFn + ? Promise.resolve( + typeof contextFn === 'function' ? contextFn(...args) : contextFn + ) + : {}; + + return resolveMaybePromise(context, (err, contextVal) => { + this.runQuery( + { + query, + context, + variables, + operationName, + http: { + request: req, + response: res, + }, }, - }, - (err, { status, body, headers }) => { - for (const key in headers) { - const headVal = headers[key]; - if (headVal) res.setHeader(key, headVal); + (err, { status, body, headers }) => { + for (const key in headers) { + const headVal = headers[key]; + if (headVal) res.setHeader(key, headVal); + } + res.statusCode = status; + return res.end(body); } - res.statusCode = status; - return res.end(body); - } - ); + ); + }); }); } - - // serve GraphiQL + // server GraphiQL if (options?.graphiql) { const graphiql = options.graphiql; const graphiqlPath = - (typeof graphiql === 'object' ? graphiql.path : null) || - this.DEFAULT_GRAPHIQL_PATH; + (typeof graphiql === 'object' && graphiql.path) || + DEFAULT_GRAPHIQL_PATH; if (pathname === graphiqlPath) { const defaultQuery = typeof graphiql === 'object' ? graphiql.defaultQuery : undefined; @@ -68,8 +93,8 @@ export class GraphyneServer extends GraphyneServerBase { return res.end(renderGraphiQL({ path, defaultQuery })); } } - - // serve 404 + // onNoMatch + // TODO: Allow user defined response res.statusCode = 404; res.end('not found'); }; From 6e11b874d613fc31c25eb3c93bb4353fde7ccd9d Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 13:08:53 -0400 Subject: [PATCH 08/21] Update example --- examples/with-express/index.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/examples/with-express/index.js b/examples/with-express/index.js index 384ad54..6ab6742 100644 --- a/examples/with-express/index.js +++ b/examples/with-express/index.js @@ -1,5 +1,5 @@ const express = require('express'); -const { GraphyneServer } = require('graphyne-express'); +const { GraphyneServer } = require('graphyne-server'); const { makeExecutableSchema } = require('graphql-tools'); const typeDefs = ` @@ -20,20 +20,23 @@ var schema = makeExecutableSchema({ const graphyne = new GraphyneServer({ schema, - context: () => ({ world: 'world' }), + context: (req, res) => ({ world: 'world' }), }); const app = express(); -app.all('/graphql', graphyne.createHandler()); + +const graphyneHandler = graphyne.createHandler({ + path: '/graphql', + graphiql: { + path: '/___graphql', + defaultQuery: 'query { hello }', + }, +}); + +// GraphQL API +app.all('/graphql', graphyneHandler); // Use GraphiQL -app.get( - '/___graphql', - graphyne.createHandler({ - path: '/graphql', // This must be set for graphiql to work - graphiql: { - defaultQuery: 'query { hello }', - }, - }) -); +app.get('/___graphql', graphyneHandler); + app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql'); From a4fab42eafaff18a5aea0426d6cb06ef05c0a406 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 13:35:42 -0400 Subject: [PATCH 09/21] Add onNoMatch --- packages/graphyne-core/src/core.ts | 2 +- packages/graphyne-core/src/types.ts | 3 +- packages/graphyne-server/README.md | 52 ++++++++++++------- .../graphyne-server/src/graphyneServer.ts | 20 +++++-- 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/packages/graphyne-core/src/core.ts b/packages/graphyne-core/src/core.ts index 8ce526d..de0e4d4 100644 --- a/packages/graphyne-core/src/core.ts +++ b/packages/graphyne-core/src/core.ts @@ -46,7 +46,7 @@ export abstract class GraphyneServerBase { typeof options.context !== 'function' && typeof options.context !== 'object' ) { - throw new TypeError('opts.context must be an object or function'); + throw new TypeError('options.context must be an object or function'); } this.options = options; // build cache diff --git a/packages/graphyne-core/src/types.ts b/packages/graphyne-core/src/types.ts index 5aa9afd..7d9c86e 100644 --- a/packages/graphyne-core/src/types.ts +++ b/packages/graphyne-core/src/types.ts @@ -11,12 +11,13 @@ export interface Config, TRootValue = any> { export interface HandlerConfig { path?: string; - graphiql: + graphiql?: | boolean | { path?: string; defaultQuery?: string; }; + onNoMatch?: (...args: any[]) => void; } export type HTTPHeaders = Record; diff --git a/packages/graphyne-server/README.md b/packages/graphyne-server/README.md index 580483d..bc09b76 100644 --- a/packages/graphyne-server/README.md +++ b/packages/graphyne-server/README.md @@ -23,18 +23,8 @@ npm i graphyne-server graphql yarn add graphyne-server graphql ``` -In addition, instead of the above, Graphyne Server [integration packages](#integration) can also be used with specific frameworks and runtimes. - -```shell -npm i graphyne-{integration} graphql -// or -yarn add graphyne-{integration} graphql -``` - ## Usage -You can create a HTTP GraphQL server with `graphyne-server`: - ```javascript const http = require('http'); const { GraphyneServer } = require('graphyne-server'); @@ -50,14 +40,6 @@ server.listen(3000, () => { }); ``` -For integration packages, check out their respective document below: - -## Integration - -`graphyne` offers integration packages to use with specific frameworks and runtimes: - -- [Express](/packages/graphyne-express) - ## API ### `new GraphyneServer(options)` @@ -77,5 +59,37 @@ Create a handler for HTTP server, `options` accepts the following: - `graphiql`: Pass in `true` to present [GraphiQL](https://github.com/graphql/graphiql) when being loaded in a browser. Alternatively, you can also pass in an options object: - `path`: Specify a custom path for `GraphiQL`. It defaults to `/___graphql` if no path is specified. - `defaultQuery`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. +- `onNoMatch`: A handler when `req.url` does not match `options.path` nor `options.graphiql.path`. Its arguments depend on a framework's *signature function*. By default, `graphyne` tries to call `req.statusCode = 404` and `res.end('not found')`. + +#### Framework integration + +`Graphyne` works out-of-the-box for frameworks that resolve Node.js signature function `(req, res)`. **Signature function** refers to framework-specific's handler function. For example in `Express.js`, it is `(req, res, next)`. In `Hapi`, it is `(request, h)`. In `Micro` on `Node HTTP Server`, it is simply `(req, res)`. + +##### options.onNoMatch + +This is what you may do in `Express.js`. + +```javascript +createHandler({ + //... + onNoMatch: (req, res, next) => { + next(); + } +} +``` + +In frameworks like `Micro` or bare `Node HTTP Server`, you usually do: + +```javascript +createHandler({ + //... + onNoMatch: (req, res) => { + res.statusCode = 404; + res.end('meh'); + } +} +``` + +##### Frameworks with non-standard signature function -Respective integration packages may have different requirement for `options`. Please refer to their respective documentations. +For frameworks that require different set of handler, define `options.handlerMapping`. **WIP** diff --git a/packages/graphyne-server/src/graphyneServer.ts b/packages/graphyne-server/src/graphyneServer.ts index c545fae..24aae72 100644 --- a/packages/graphyne-server/src/graphyneServer.ts +++ b/packages/graphyne-server/src/graphyneServer.ts @@ -20,6 +20,11 @@ export class GraphyneServer extends GraphyneServerBase { } createHandler(options?: HandlerConfig): RequestListener { + // Validate options + if (options?.onNoMatch && typeof options.onNoMatch !== 'function') { + throw new Error('createHandler: options.onNoMatch must be a function'); + } + return (...args: any[]) => { // Integration mapping const req: IncomingMessage & { @@ -80,7 +85,8 @@ export class GraphyneServer extends GraphyneServerBase { }); }); } - // server GraphiQL + + // serve GraphiQL if (options?.graphiql) { const graphiql = options.graphiql; const graphiqlPath = @@ -89,14 +95,18 @@ export class GraphyneServer extends GraphyneServerBase { if (pathname === graphiqlPath) { const defaultQuery = typeof graphiql === 'object' ? graphiql.defaultQuery : undefined; - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(renderGraphiQL({ path, defaultQuery })); } } + // onNoMatch - // TODO: Allow user defined response - res.statusCode = 404; - res.end('not found'); + if (options?.onNoMatch) { + return options.onNoMatch(...args); + } else { + res.statusCode = 404; + res.end('not found'); + } }; } } From c2ab0b7c6881530764b5a8bd4abee111b4032f0a Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 13:44:03 -0400 Subject: [PATCH 10/21] update bench --- .github/actions/graphql-bench-pr/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/graphql-bench-pr/src/index.js b/.github/actions/graphql-bench-pr/src/index.js index 87bd7e4..87d7459 100644 --- a/.github/actions/graphql-bench-pr/src/index.js +++ b/.github/actions/graphql-bench-pr/src/index.js @@ -43,7 +43,7 @@ async function prepareRepo(repo, ref, outDir) { async function runManyBenches(dir, packages) { // Run benchmark let packagesArgs = packages.join(' '); - const benchCmd = `node ${dir}/bench/bench -c 100 -d 5 -p 10 --packages ${packagesArgs} --silent` + const benchCmd = `node ${dir}/bench/bench -c 100 -d 10 -p 10 --packages ${packagesArgs} --silent` console.log(benchCmd) await exec(benchCmd); } From a9219c4cc303a85058f9cae86d2307c92c711094 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 14:17:27 -0400 Subject: [PATCH 11/21] Update examples and readme --- examples/with-express/index.js | 5 +- packages/graphyne-server/README.md | 75 +++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/examples/with-express/index.js b/examples/with-express/index.js index 6ab6742..f685b91 100644 --- a/examples/with-express/index.js +++ b/examples/with-express/index.js @@ -33,10 +33,7 @@ const graphyneHandler = graphyne.createHandler({ }, }); -// GraphQL API -app.all('/graphql', graphyneHandler); -// Use GraphiQL -app.get('/___graphql', graphyneHandler); +app.use(graphyneHandler); app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql'); diff --git a/packages/graphyne-server/README.md b/packages/graphyne-server/README.md index bc09b76..997f981 100644 --- a/packages/graphyne-server/README.md +++ b/packages/graphyne-server/README.md @@ -23,7 +23,7 @@ npm i graphyne-server graphql yarn add graphyne-server graphql ``` -## Usage +## Usage (with bare Node HTTP Server) ```javascript const http = require('http'); @@ -40,6 +40,8 @@ server.listen(3000, () => { }); ``` +For framework specific integration, see the last section. + ## API ### `new GraphyneServer(options)` @@ -47,7 +49,7 @@ server.listen(3000, () => { Constructing a Graphyne GraphQL server. It accepts the following options: - `schema`: (required) A `GraphQLSchema` instance. It can be created using `makeExecutableSchema` from [graphql-tools](https://github.com/apollographql/graphql-tools). -- `context`: An object or function called to creates a context shared accross resolvers per request. The function accepts an integration context signature depends on which integration packages is used. If not provided, the context will be the one provided by integration packages +- `context`: An object or function called to creates a context shared accross resolvers per request. The function accepts the framework's **signature function** (see below). - `rootValue`: A value or function called with the parsed `Document` that creates the root value passed to the GraphQL executor. - `cache`: `GraphyneServer` creates **two** in-memory LRU cache: One for compiled queries and another for invalid queries. This value defines max items to hold in **each** cache. Pass `false` to disable cache. @@ -59,37 +61,64 @@ Create a handler for HTTP server, `options` accepts the following: - `graphiql`: Pass in `true` to present [GraphiQL](https://github.com/graphql/graphiql) when being loaded in a browser. Alternatively, you can also pass in an options object: - `path`: Specify a custom path for `GraphiQL`. It defaults to `/___graphql` if no path is specified. - `defaultQuery`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. -- `onNoMatch`: A handler when `req.url` does not match `options.path` nor `options.graphiql.path`. Its arguments depend on a framework's *signature function*. By default, `graphyne` tries to call `req.statusCode = 404` and `res.end('not found')`. +- `onNoMatch`: A handler when `req.url` does not match `options.path` nor `options.graphiql.path`. Its arguments depend on a framework's *signature function* (see below). By default, `graphyne` tries to call `req.statusCode = 404` and `res.end('not found')`. -#### Framework integration +`createHandler` creates Node.js signature function of `(req, res)`, which work out-of-the-box for most frameworks, including `Express.js` and `Micro`. -`Graphyne` works out-of-the-box for frameworks that resolve Node.js signature function `(req, res)`. **Signature function** refers to framework-specific's handler function. For example in `Express.js`, it is `(req, res, next)`. In `Hapi`, it is `(request, h)`. In `Micro` on `Node HTTP Server`, it is simply `(req, res)`. +## Framework-specific Usage -##### options.onNoMatch +**Signature function** (as seen in `options.context` or createHandler's `options.onNoMatch`) refers to framework-specific's handler function. For example in `Express.js`, it is `(req, res, next)`. In `Hapi`, it is `(request, h)`. In `Micro` or `Node HTTP Server`, it is simply `(req, res)`. -This is what you may do in `Express.js`. +### [Express.js](https://github.com/expressjs/express) ```javascript -createHandler({ - //... - onNoMatch: (req, res, next) => { - next(); - } -} +/* Setup Graphyne */ +const graphyneHandler = graphyne.createHandler({ + path: '/graphql', + graphiql: { + path: '/___graphql', + defaultQuery: 'query { hello }', + }, + // Continue to next handler in middleware chain + onNoMatch: (req, res, next) => next() +}); +app.use(graphyneHandler); +app.listen(3000); +console.log('Running a GraphQL API server at http://localhost:3000/graphql'); ``` -In frameworks like `Micro` or bare `Node HTTP Server`, you usually do: +### [Micro](https://github.com/zeit/micro) ```javascript -createHandler({ - //... - onNoMatch: (req, res) => { - res.statusCode = 404; - res.end('meh'); - } -} +const {send} = require('micro') +/* Setup Graphyne */ +module.exports = graphyne.createHandler({ + path: "/graphql", + graphiql: { + path: "/___graphql", + defaultQuery: "query { hello }", + }, + onNoMatch: async (req, res) => { + const statusCode = 400; + send(res, statusCode, "not found"); + }, +}); ``` -##### Frameworks with non-standard signature function +### [Fastify](https://github.com/fastify/fastify) -For frameworks that require different set of handler, define `options.handlerMapping`. **WIP** +**Note:** This is an unofficial integration and may change in the future. + +```javascript +/* Setup Graphyne */ +fastify.use( + ["/graphql", "/___graphql"], + graphyne.createHandler({ + path: "/graphql", + graphiql: { + path: "/___graphql", + defaultQuery: "query { hello }", + }, + }) +); +``` \ No newline at end of file From 35d5a6719b704af7d9c0db320bfa94dd724660ca Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 14:38:50 -0400 Subject: [PATCH 12/21] Add options.integrationFn --- packages/graphyne-core/src/types.ts | 8 ++++ packages/graphyne-server/README.md | 46 ++++++++++++++++--- .../graphyne-server/src/graphyneServer.ts | 21 +++++++-- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/packages/graphyne-core/src/types.ts b/packages/graphyne-core/src/types.ts index 7d9c86e..b84ff6d 100644 --- a/packages/graphyne-core/src/types.ts +++ b/packages/graphyne-core/src/types.ts @@ -9,6 +9,13 @@ export interface Config, TRootValue = any> { cache?: number | boolean; } +type IntegrationFunction = ( + ...args: any[] +) => { + request: IncomingMessage; + response: ServerResponse; +}; + export interface HandlerConfig { path?: string; graphiql?: @@ -18,6 +25,7 @@ export interface HandlerConfig { defaultQuery?: string; }; onNoMatch?: (...args: any[]) => void; + integrationFn?: IntegrationFunction; } export type HTTPHeaders = Record; diff --git a/packages/graphyne-server/README.md b/packages/graphyne-server/README.md index 997f981..43bd455 100644 --- a/packages/graphyne-server/README.md +++ b/packages/graphyne-server/README.md @@ -63,7 +63,21 @@ Create a handler for HTTP server, `options` accepts the following: - `defaultQuery`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. - `onNoMatch`: A handler when `req.url` does not match `options.path` nor `options.graphiql.path`. Its arguments depend on a framework's *signature function* (see below). By default, `graphyne` tries to call `req.statusCode = 404` and `res.end('not found')`. -`createHandler` creates Node.js signature function of `(req, res)`, which work out-of-the-box for most frameworks, including `Express.js` and `Micro`. +`createHandler` creates Node.js signature function of `(req, res)`, which work out-of-the-box for most frameworks which have handlers of similar signature, including `Express.js` and `Micro`. + +If the framework has non-standard signature function (such as `Hapi` (`(request, h)`), `Koa` (`(ctx, next)`), etc.), you can supply `options.integrationFn`, which maps the supplied arguments into an object of Node.js `request` (`IncomingMessage`) and `response` (`ServerResponse`). (see below for examples) + +```javascript +createHandler({ + integrationFn: (...args) => { + // Return an object with `request` and `response` + return { + request: IncomingMessage, + response: ServerResponse + } + } +}) +``` ## Framework-specific Usage @@ -72,7 +86,6 @@ Create a handler for HTTP server, `options` accepts the following: ### [Express.js](https://github.com/expressjs/express) ```javascript -/* Setup Graphyne */ const graphyneHandler = graphyne.createHandler({ path: '/graphql', graphiql: { @@ -82,16 +95,15 @@ const graphyneHandler = graphyne.createHandler({ // Continue to next handler in middleware chain onNoMatch: (req, res, next) => next() }); + app.use(graphyneHandler); -app.listen(3000); -console.log('Running a GraphQL API server at http://localhost:3000/graphql'); ``` ### [Micro](https://github.com/zeit/micro) ```javascript const {send} = require('micro') -/* Setup Graphyne */ + module.exports = graphyne.createHandler({ path: "/graphql", graphiql: { @@ -110,7 +122,6 @@ module.exports = graphyne.createHandler({ **Note:** This is an unofficial integration and may change in the future. ```javascript -/* Setup Graphyne */ fastify.use( ["/graphql", "/___graphql"], graphyne.createHandler({ @@ -121,4 +132,25 @@ fastify.use( }, }) ); -``` \ No newline at end of file +``` + +### [Koa](https://github.com/koajs/koa) + +```javascript +const graphyneHandler = graphyne.createHandler({ + path: "/graphql", + graphiql: { + path: "/___graphql", + defaultQuery: "query { hello }", + }, + integrationFn: (ctx, next) => { + // https://github.com/koajs/koa#context-request-and-response + return { + response: ctx.request, + response: ctx.response, + }; + }, +}); +``` + +(If there is any framework you fail to integrate, feel free to create an issue) diff --git a/packages/graphyne-server/src/graphyneServer.ts b/packages/graphyne-server/src/graphyneServer.ts index 24aae72..eb401a1 100644 --- a/packages/graphyne-server/src/graphyneServer.ts +++ b/packages/graphyne-server/src/graphyneServer.ts @@ -27,11 +27,22 @@ export class GraphyneServer extends GraphyneServerBase { return (...args: any[]) => { // Integration mapping - const req: IncomingMessage & { - path: string; - query: Record; - } = args[0]; - const res: ServerResponse = args[1]; + let req: IncomingMessage & { + path?: string; + query?: Record; + }; + let res: ServerResponse; + + if (options?.integrationFn) { + const { + request: mappedRequest, + response: mappedResponse, + } = options.integrationFn(...args); + req = mappedRequest; + res = mappedResponse; + } else { + [req, res] = args; + } // Parse req.url let pathname = req.path; From 9af817f85535d23ccd594219a7e010871d629a42 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 14:56:42 -0400 Subject: [PATCH 13/21] Update README --- packages/graphyne-server/README.md | 80 +++++++++++++----------------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/packages/graphyne-server/README.md b/packages/graphyne-server/README.md index 43bd455..ac1f0d4 100644 --- a/packages/graphyne-server/README.md +++ b/packages/graphyne-server/README.md @@ -4,8 +4,9 @@ A lightning-fast JavaScript GraphQL Server, featuring: -- Caching of query validation and compilation +- Caching of query validation and compilation with LRU strategy. - Highly performant Just-In-Time compiler via [graphql-jit](https://github.com/zalando-incubator/graphql-jit) +- **framework agnostic**: Works out-of-the-box with most JavaScript frameworks, such as Express, Micro. Others require minimum configuration. ## Why @@ -40,7 +41,7 @@ server.listen(3000, () => { }); ``` -For framework specific integration, see the last section. +If you do not use Node HTTP Server (which is likely), see [framework-specific integration](#framework-specific-integration). ## API @@ -49,7 +50,7 @@ For framework specific integration, see the last section. Constructing a Graphyne GraphQL server. It accepts the following options: - `schema`: (required) A `GraphQLSchema` instance. It can be created using `makeExecutableSchema` from [graphql-tools](https://github.com/apollographql/graphql-tools). -- `context`: An object or function called to creates a context shared accross resolvers per request. The function accepts the framework's **signature function** (see below). +- `context`: An object or function called to creates a context shared across resolvers per request. The function accepts the framework's [signature function](#framework-specific-integration). - `rootValue`: A value or function called with the parsed `Document` that creates the root value passed to the GraphQL executor. - `cache`: `GraphyneServer` creates **two** in-memory LRU cache: One for compiled queries and another for invalid queries. This value defines max items to hold in **each** cache. Pass `false` to disable cache. @@ -61,11 +62,12 @@ Create a handler for HTTP server, `options` accepts the following: - `graphiql`: Pass in `true` to present [GraphiQL](https://github.com/graphql/graphiql) when being loaded in a browser. Alternatively, you can also pass in an options object: - `path`: Specify a custom path for `GraphiQL`. It defaults to `/___graphql` if no path is specified. - `defaultQuery`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. -- `onNoMatch`: A handler when `req.url` does not match `options.path` nor `options.graphiql.path`. Its arguments depend on a framework's *signature function* (see below). By default, `graphyne` tries to call `req.statusCode = 404` and `res.end('not found')`. +- `onNoMatch`: A handler function when `req.url` does not match `options.path` nor `options.graphiql.path`. Its *arguments* depend on a framework's [signature function](#framework-specific-integration). By default, `graphyne` tries to call `req.statusCode = 404` and `res.end('not found')`. +- `integrationFn`: A function to resolve mapping for frameworks with non-standard signature function. `createHandler` creates Node.js signature function of `(req, res)`, which work out-of-the-box for most frameworks which have handlers of similar signature, including `Express.js` and `Micro`. -If the framework has non-standard signature function (such as `Hapi` (`(request, h)`), `Koa` (`(ctx, next)`), etc.), you can supply `options.integrationFn`, which maps the supplied arguments into an object of Node.js `request` (`IncomingMessage`) and `response` (`ServerResponse`). (see below for examples) +If the framework has non-standard signature function (such as `Hapi` (`(request, h)`), `Koa` (`(ctx, next)`), etc.), you can supply `options.integrationFn`, which maps the supplied arguments into an object of Node.js `request` (`IncomingMessage`) and `response` (`ServerResponse`). ```javascript createHandler({ @@ -79,40 +81,34 @@ createHandler({ }) ``` -## Framework-specific Usage +## Framework-specific integration -**Signature function** (as seen in `options.context` or createHandler's `options.onNoMatch`) refers to framework-specific's handler function. For example in `Express.js`, it is `(req, res, next)`. In `Hapi`, it is `(request, h)`. In `Micro` or `Node HTTP Server`, it is simply `(req, res)`. +**Signature function** refers to framework-specific's handler function. For example in `Express.js`, it is `(req, res, next)`. In `Hapi`, it is `(request, h)`. In `Micro` or `Node HTTP Server`, it is simply `(req, res)`. ### [Express.js](https://github.com/expressjs/express) ```javascript -const graphyneHandler = graphyne.createHandler({ - path: '/graphql', - graphiql: { - path: '/___graphql', - defaultQuery: 'query { hello }', - }, - // Continue to next handler in middleware chain - onNoMatch: (req, res, next) => next() -}); - -app.use(graphyneHandler); +app.use( + graphyne.createHandler({ + // other options + onNoMatch: (req, res, next) => { + // Continue to next handler in middleware chain + next(); + } + }) +); ``` ### [Micro](https://github.com/zeit/micro) ```javascript -const {send} = require('micro') +const { send } = require('micro'); module.exports = graphyne.createHandler({ - path: "/graphql", - graphiql: { - path: "/___graphql", - defaultQuery: "query { hello }", - }, + // other options onNoMatch: async (req, res) => { const statusCode = 400; - send(res, statusCode, "not found"); + send(res, statusCode, 'not found'); }, }); ``` @@ -123,13 +119,9 @@ module.exports = graphyne.createHandler({ ```javascript fastify.use( - ["/graphql", "/___graphql"], + ['/graphql', '/___graphql'], graphyne.createHandler({ - path: "/graphql", - graphiql: { - path: "/___graphql", - defaultQuery: "query { hello }", - }, + // other options }) ); ``` @@ -137,20 +129,18 @@ fastify.use( ### [Koa](https://github.com/koajs/koa) ```javascript -const graphyneHandler = graphyne.createHandler({ - path: "/graphql", - graphiql: { - path: "/___graphql", - defaultQuery: "query { hello }", - }, - integrationFn: (ctx, next) => { - // https://github.com/koajs/koa#context-request-and-response - return { - response: ctx.request, - response: ctx.response, - }; - }, -}); +app.use( + graphyne.createHandler({ + // ...other options + integrationFn: (ctx, next) => { + // https://github.com/koajs/koa#context-request-and-response + return { + response: ctx.request, + response: ctx.response, + }; + }, + }) +); ``` (If there is any framework you fail to integrate, feel free to create an issue) From 6e26c977b5c6686fa63a6de30b1171d5fb0affe3 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 15:28:54 -0400 Subject: [PATCH 14/21] Update example and add with-koa example --- examples/with-express/index.js | 19 ++++++------ examples/with-express/package.json | 2 +- examples/with-graphyne/package.json | 2 +- examples/with-koa/index.js | 46 +++++++++++++++++++++++++++++ examples/with-koa/package.json | 14 +++++++++ 5 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 examples/with-koa/index.js create mode 100644 examples/with-koa/package.json diff --git a/examples/with-express/index.js b/examples/with-express/index.js index f685b91..3ee8f29 100644 --- a/examples/with-express/index.js +++ b/examples/with-express/index.js @@ -25,15 +25,16 @@ const graphyne = new GraphyneServer({ const app = express(); -const graphyneHandler = graphyne.createHandler({ - path: '/graphql', - graphiql: { - path: '/___graphql', - defaultQuery: 'query { hello }', - }, -}); - -app.use(graphyneHandler); +app.use( + graphyne.createHandler({ + path: '/graphql', + graphiql: { + path: '/___graphql', + defaultQuery: 'query { hello }', + }, + onNoMatch: (req, res, next) => next(), + }) +); app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql'); diff --git a/examples/with-express/package.json b/examples/with-express/package.json index 0dc4273..f0aeeb8 100644 --- a/examples/with-express/package.json +++ b/examples/with-express/package.json @@ -8,7 +8,7 @@ "express": "^4.17.1", "graphql": "^15.0.0", "graphql-tools": "^4.0.7", - "graphyne-express": "latest" + "graphyne-server": "^0.2.0" }, "license": "ISC" } diff --git a/examples/with-graphyne/package.json b/examples/with-graphyne/package.json index 1e2d9c0..09d11d2 100644 --- a/examples/with-graphyne/package.json +++ b/examples/with-graphyne/package.json @@ -7,7 +7,7 @@ "dependencies": { "graphql": "^15.0.0", "graphql-tools": "^4.0.7", - "graphyne-server": "latest" + "graphyne-server": "^0.2.0" }, "license": "ISC" } diff --git a/examples/with-koa/index.js b/examples/with-koa/index.js new file mode 100644 index 0000000..f975a17 --- /dev/null +++ b/examples/with-koa/index.js @@ -0,0 +1,46 @@ +const Koa = require('koa'); +const { GraphyneServer } = require('graphyne-server'); +const { makeExecutableSchema } = require('graphql-tools'); + +const typeDefs = ` + type Query { + hello: String + } +`; +const resolvers = { + Query: { + hello: (obj, variables, context) => `Hello ${context.world}!`, + }, +}; + +var schema = makeExecutableSchema({ + typeDefs, + resolvers, +}); + +const graphyne = new GraphyneServer({ + schema, + context: (req, res) => ({ world: 'world' }), +}); + +const app = new Koa(); + +app.use( + graphyne.createHandler({ + path: '/graphql', + graphiql: { + path: '/___graphql', + defaultQuery: 'query { hello }', + }, + integrationFn: (ctx, next) => { + // https://github.com/koajs/koa/blob/master/lib/context.js#L54 + return { + request: ctx.req, + response: ctx.res, + }; + }, + }) +); + +app.listen(3000); +console.log('Running a GraphQL API server at http://localhost:3000/graphql'); diff --git a/examples/with-koa/package.json b/examples/with-koa/package.json new file mode 100644 index 0000000..93a6693 --- /dev/null +++ b/examples/with-koa/package.json @@ -0,0 +1,14 @@ +{ + "name": "with-koa", + "private": true, + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "graphql": "^15.0.0", + "graphql-tools": "^4.0.7", + "graphyne-server": "^0.2.0", + "koa": "^2.11.0" + }, + "license": "ISC" +} From 839a867866a3650f754919fb98fda56defe780b5 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 15:29:16 -0400 Subject: [PATCH 15/21] Fix core --- packages/graphyne-core/src/core.ts | 7 ++++--- packages/graphyne-server/src/graphyneServer.ts | 13 ++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/graphyne-core/src/core.ts b/packages/graphyne-core/src/core.ts index de0e4d4..5dcd242 100644 --- a/packages/graphyne-core/src/core.ts +++ b/packages/graphyne-core/src/core.ts @@ -68,9 +68,10 @@ export abstract class GraphyneServerBase { const headers: HTTPHeaders = { 'content-type': 'application/json' }; function createResponse(code: number, obj: ExecutionResult): void { - const stringify = isCompiledQuery(compiledQuery) - ? compiledQuery.stringify - : JSON.stringify; + const stringify = + compiledQuery && isCompiledQuery(compiledQuery) + ? compiledQuery.stringify + : JSON.stringify; cb(null, { status: code, body: stringify(obj), diff --git a/packages/graphyne-server/src/graphyneServer.ts b/packages/graphyne-server/src/graphyneServer.ts index eb401a1..a19126a 100644 --- a/packages/graphyne-server/src/graphyneServer.ts +++ b/packages/graphyne-server/src/graphyneServer.ts @@ -50,7 +50,7 @@ export class GraphyneServer extends GraphyneServerBase { if (!pathname || !queryParams) { const parsedUrl = parseUrl(req, true); pathname = parsedUrl.pathname; - queryParams = parsedUrl.queryParams; + queryParams = parsedUrl.query; } // serve GraphQL @@ -66,13 +66,12 @@ export class GraphyneServer extends GraphyneServerBase { body: parsedBody, }); const contextFn = this.options.context; - const context = contextFn - ? Promise.resolve( - typeof contextFn === 'function' ? contextFn(...args) : contextFn - ) - : {}; + const contextVal = + (typeof contextFn === 'function' + ? contextFn(...args) + : contextFn) || {}; - return resolveMaybePromise(context, (err, contextVal) => { + return resolveMaybePromise(contextVal, (err, context) => { this.runQuery( { query, From fe53b29992f9bcdad43092c6646c2d3bf1d09c39 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 15:34:53 -0400 Subject: [PATCH 16/21] Add with-micro example --- examples/with-micro/index.js | 32 ++++++++++++++++++++++++++++++++ examples/with-micro/package.json | 15 +++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 examples/with-micro/index.js create mode 100644 examples/with-micro/package.json diff --git a/examples/with-micro/index.js b/examples/with-micro/index.js new file mode 100644 index 0000000..39501cd --- /dev/null +++ b/examples/with-micro/index.js @@ -0,0 +1,32 @@ +const { send } = require('micro'); +const { GraphyneServer } = require('graphyne-server'); +const { makeExecutableSchema } = require('graphql-tools'); + +const typeDefs = ` + type Query { + hello: String + } +`; +const resolvers = { + Query: { + hello: (obj, variables, context) => `Hello ${context.world}!`, + }, +}; + +var schema = makeExecutableSchema({ + typeDefs, + resolvers, +}); + +const graphyne = new GraphyneServer({ + schema, + context: (req, res) => ({ world: 'world' }), +}); + +module.exports = graphyne.createHandler({ + // other options + onNoMatch: async (req, res) => { + const statusCode = 400; + send(res, statusCode, 'not found'); + }, +}); diff --git a/examples/with-micro/package.json b/examples/with-micro/package.json new file mode 100644 index 0000000..2fe10aa --- /dev/null +++ b/examples/with-micro/package.json @@ -0,0 +1,15 @@ +{ + "name": "with-koa", + "private": true, + "main": "index.js", + "scripts": { + "start": "micro" + }, + "dependencies": { + "graphql": "^15.0.0", + "graphql-tools": "^4.0.7", + "graphyne-server": "^0.2.0", + "micro": "^9.3.4" + }, + "license": "ISC" +} From ab4da6b353b4c6a91f58dd35416132300cc97942 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 15:55:35 -0400 Subject: [PATCH 17/21] Add Fastify example --- examples/with-fastify/index.js | 42 ++++++++++++++++++++++++++++++ examples/with-fastify/package.json | 14 ++++++++++ 2 files changed, 56 insertions(+) create mode 100644 examples/with-fastify/index.js create mode 100644 examples/with-fastify/package.json diff --git a/examples/with-fastify/index.js b/examples/with-fastify/index.js new file mode 100644 index 0000000..55129cb --- /dev/null +++ b/examples/with-fastify/index.js @@ -0,0 +1,42 @@ +const fastify = require('fastify')({ + logger: true, +}); +const { GraphyneServer } = require('graphyne-server'); +const { makeExecutableSchema } = require('graphql-tools'); + +const typeDefs = ` + type Query { + hello: String + } +`; +const resolvers = { + Query: { + hello: (obj, variables, context) => `Hello ${context.world}!`, + }, +}; + +var schema = makeExecutableSchema({ + typeDefs, + resolvers, +}); + +const graphyne = new GraphyneServer({ + schema, + context: (req, res) => ({ world: 'world' }), +}); + +fastify.use( + graphyne.createHandler({ + path: '/graphql', + graphiql: { + path: '/___graphql', + defaultQuery: 'query { hello }', + }, + onNoMatch: (req, res, next) => next(), + }) +); + +fastify.listen(3000, (err, address) => { + if (err) throw err; + fastify.log.info(`server listening on ${address}`); +}); diff --git a/examples/with-fastify/package.json b/examples/with-fastify/package.json new file mode 100644 index 0000000..e5645b3 --- /dev/null +++ b/examples/with-fastify/package.json @@ -0,0 +1,14 @@ +{ + "name": "with-fastify", + "private": true, + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "fastify": "^2.13.1", + "graphql": "^15.0.0", + "graphql-tools": "^4.0.7", + "graphyne-server": "^0.2.0" + }, + "license": "ISC" +} From 64bbb7bf060a9c50784b323311596f8909b1e969 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 15:57:19 -0400 Subject: [PATCH 18/21] Update README --- packages/graphyne-server/README.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/graphyne-server/README.md b/packages/graphyne-server/README.md index ac1f0d4..adc8eab 100644 --- a/packages/graphyne-server/README.md +++ b/packages/graphyne-server/README.md @@ -62,7 +62,7 @@ Create a handler for HTTP server, `options` accepts the following: - `graphiql`: Pass in `true` to present [GraphiQL](https://github.com/graphql/graphiql) when being loaded in a browser. Alternatively, you can also pass in an options object: - `path`: Specify a custom path for `GraphiQL`. It defaults to `/___graphql` if no path is specified. - `defaultQuery`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. -- `onNoMatch`: A handler function when `req.url` does not match `options.path` nor `options.graphiql.path`. Its *arguments* depend on a framework's [signature function](#framework-specific-integration). By default, `graphyne` tries to call `req.statusCode = 404` and `res.end('not found')`. +- `onNoMatch`: A handler function when `req.url` does not match `options.path` nor `options.graphiql.path`. Its *arguments* depend on a framework's [signature function](#framework-specific-integration). By default, `graphyne` tries to call `req.statusCode = 404` and `res.end('not found')`. See examples in [framework-specific integration](#framework-specific-integration). - `integrationFn`: A function to resolve mapping for frameworks with non-standard signature function. `createHandler` creates Node.js signature function of `(req, res)`, which work out-of-the-box for most frameworks which have handlers of similar signature, including `Express.js` and `Micro`. @@ -87,6 +87,8 @@ createHandler({ ### [Express.js](https://github.com/expressjs/express) +[Example](/examples/with-express) + ```javascript app.use( graphyne.createHandler({ @@ -101,6 +103,8 @@ app.use( ### [Micro](https://github.com/zeit/micro) +[Example](/examples/with-micro) + ```javascript const { send } = require('micro'); @@ -115,28 +119,35 @@ module.exports = graphyne.createHandler({ ### [Fastify](https://github.com/fastify/fastify) -**Note:** This is an unofficial integration and may change in the future. +**Note:** This is an unofficial integration. For a solution in the ecosystem, check out [fastify-gql](https://github.com/mcollina/fastify-gql). + +[Example](/examples/with-fastify) ```javascript fastify.use( ['/graphql', '/___graphql'], graphyne.createHandler({ // other options + onNoMatch: (req, res, next) => { + next(); + } }) ); ``` ### [Koa](https://github.com/koajs/koa) +[Example](/examples/with-koa) + ```javascript app.use( graphyne.createHandler({ // ...other options integrationFn: (ctx, next) => { - // https://github.com/koajs/koa#context-request-and-response + // https://github.com/koajs/koa/blob/master/lib/context.js#L54 return { - response: ctx.request, - response: ctx.response, + request: ctx.req, + response: ctx.res, }; }, }) From 4f0670ba7962d93be7eee50452d10b76f1054b8c Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 16:15:06 -0400 Subject: [PATCH 19/21] Update README --- packages/graphyne-server/README.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/graphyne-server/README.md b/packages/graphyne-server/README.md index adc8eab..4ff5880 100644 --- a/packages/graphyne-server/README.md +++ b/packages/graphyne-server/README.md @@ -2,15 +2,15 @@ **This is a work in progress.** -A lightning-fast JavaScript GraphQL Server, featuring: +A **lightning-fast** JavaScript GraphQL Server, featuring: - Caching of query validation and compilation with LRU strategy. - Highly performant Just-In-Time compiler via [graphql-jit](https://github.com/zalando-incubator/graphql-jit) -- **framework agnostic**: Works out-of-the-box with most JavaScript frameworks, such as Express, Micro. Others require minimum configuration. +- Framework-agnostic: Works out-of-the-box with most JavaScript frameworks, such as Express, Micro. ## Why -`Graphyne` uses `graphql-jit` under the hood to compile queries into optimized functions that significantly improve performance ([> 10 times better than `graphql-js`](https://github.com/zalando-incubator/graphql-jit#benchmarks)). By furthur caching the compiled queries in memory using a LRU strategy, `Graphyne` manages to become lightning-fast. +`Graphyne` uses `graphql-jit` under the hood to compile queries into optimized functions that significantly improve performance ([more than 10 times better than `graphql-js`](https://github.com/zalando-incubator/graphql-jit#benchmarks)). By furthur caching the compiled queries in memory using a LRU strategy, `Graphyne` manages to become lightning-fast. Check out the [benchmarks](/bench). @@ -63,11 +63,7 @@ Create a handler for HTTP server, `options` accepts the following: - `path`: Specify a custom path for `GraphiQL`. It defaults to `/___graphql` if no path is specified. - `defaultQuery`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. - `onNoMatch`: A handler function when `req.url` does not match `options.path` nor `options.graphiql.path`. Its *arguments* depend on a framework's [signature function](#framework-specific-integration). By default, `graphyne` tries to call `req.statusCode = 404` and `res.end('not found')`. See examples in [framework-specific integration](#framework-specific-integration). -- `integrationFn`: A function to resolve mapping for frameworks with non-standard signature function. - -`createHandler` creates Node.js signature function of `(req, res)`, which work out-of-the-box for most frameworks which have handlers of similar signature, including `Express.js` and `Micro`. - -If the framework has non-standard signature function (such as `Hapi` (`(request, h)`), `Koa` (`(ctx, next)`), etc.), you can supply `options.integrationFn`, which maps the supplied arguments into an object of Node.js `request` (`IncomingMessage`) and `response` (`ServerResponse`). +- `integrationFn`: A function to resolve frameworks with non-standard signature function. It should return an object of Node.js `request` (`IncomingMessage`) and `response` (`ServerResponse`). Its *arguments* depend on the framework's [signature function](#framework-specific-integration). ```javascript createHandler({ From 8fe638ce2695207cb3ac4403637543d1242cdae0 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 16:18:25 -0400 Subject: [PATCH 20/21] Only test against graphyne-server using Node HTTP --- .github/actions/graphql-bench-pr/src/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/actions/graphql-bench-pr/src/index.js b/.github/actions/graphql-bench-pr/src/index.js index 87d7459..caf3a59 100644 --- a/.github/actions/graphql-bench-pr/src/index.js +++ b/.github/actions/graphql-bench-pr/src/index.js @@ -44,14 +44,13 @@ async function runManyBenches(dir, packages) { // Run benchmark let packagesArgs = packages.join(' '); const benchCmd = `node ${dir}/bench/bench -c 100 -d 10 -p 10 --packages ${packagesArgs} --silent` - console.log(benchCmd) await exec(benchCmd); } async function getStats(repo, ref) { const dir = repo.replace('/', '-'); await prepareRepo(repo, ref, dir); - await runManyBenches(dir, ['graphyne-server-express', 'graphyne-server']); + await runManyBenches(dir, ['graphyne-server']); // Get the result const resultsPath = join(cwd, dir, 'bench', 'results'); const resultObj = {}; From 5bce31b75547fe188e35e893b237e3d5a01d42e6 Mon Sep 17 00:00:00 2001 From: Hoang Vo <40987398+hoangvvo@users.noreply.github.com> Date: Tue, 14 Apr 2020 16:21:57 -0400 Subject: [PATCH 21/21] refactor --- packages/graphyne-server/src/graphyneServer.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/graphyne-server/src/graphyneServer.ts b/packages/graphyne-server/src/graphyneServer.ts index a19126a..a0d1d6a 100644 --- a/packages/graphyne-server/src/graphyneServer.ts +++ b/packages/graphyne-server/src/graphyneServer.ts @@ -45,13 +45,8 @@ export class GraphyneServer extends GraphyneServerBase { } // Parse req.url - let pathname = req.path; - let queryParams = req.query; - if (!pathname || !queryParams) { - const parsedUrl = parseUrl(req, true); - pathname = parsedUrl.pathname; - queryParams = parsedUrl.query; - } + const pathname = req.path || parseUrl(req, true).pathname; + const queryParams = req.query || parseUrl(req, true).query; // serve GraphQL const path = options?.path ?? DEFAULT_PATH;