extractgql
Advanced tools
Comparing version 0.1.14 to 0.2.0
import { OutputMap } from '../common'; | ||
import { Handler } from 'express'; | ||
export declare function createPersistedQueryMiddleware(queryMapPath: string, production?: boolean, lookupErrorHandler?: Handler): Promise<Handler>; | ||
export declare function getMiddlewareForQueryMap(queryMap: OutputMap, production?: boolean, lookupErrorHandler?: Handler): Handler; | ||
export declare function createPersistedQueryMiddleware(queryMapPath: string, enablePersistedQueries?: boolean, lookupErrorHandler?: Handler): Promise<Handler>; | ||
export declare function getMiddlewareForQueryMap(queryMap: OutputMap, enablePersistedQueries?: boolean, lookupErrorHandler?: Handler): Handler; | ||
export declare type QueryMapFunction = (queryId: (string | number)) => Promise<string>; | ||
export declare function getMiddlewareForQueryMapFunction(queryMapFunc: QueryMapFunction, enablePersistedQueries?: boolean, lookupErrorHandler?: Handler): Handler; |
"use strict"; | ||
var ExtractGQL_1 = require("../ExtractGQL"); | ||
var graphql_1 = require("graphql"); | ||
function createPersistedQueryMiddleware(queryMapPath, production, lookupErrorHandler) { | ||
if (production === void 0) { production = true; } | ||
function createPersistedQueryMiddleware(queryMapPath, enablePersistedQueries, lookupErrorHandler) { | ||
if (enablePersistedQueries === void 0) { enablePersistedQueries = true; } | ||
return new Promise(function (resolve, reject) { | ||
ExtractGQL_1.ExtractGQL.readFile(queryMapPath).then(function (queryMapString) { | ||
var queryMap = JSON.parse(queryMapString); | ||
resolve(getMiddlewareForQueryMap(queryMap, production, lookupErrorHandler)); | ||
resolve(getMiddlewareForQueryMap(queryMap, enablePersistedQueries, lookupErrorHandler)); | ||
}).catch(function (err) { | ||
@@ -16,5 +16,20 @@ reject(err); | ||
exports.createPersistedQueryMiddleware = createPersistedQueryMiddleware; | ||
function getMiddlewareForQueryMap(queryMap, production, lookupErrorHandler) { | ||
if (production === void 0) { production = true; } | ||
if (!production) { | ||
function getMiddlewareForQueryMap(queryMap, enablePersistedQueries, lookupErrorHandler) { | ||
if (enablePersistedQueries === void 0) { enablePersistedQueries = true; } | ||
var queryMapFunc = function (queryId) { | ||
var matchedKeys = Object.keys(queryMap).filter(function (key) { | ||
return (queryId !== undefined && | ||
queryMap[key].id.toString() === queryId.toString()); | ||
}); | ||
if (matchedKeys.length === 0 && lookupErrorHandler) { | ||
return Promise.reject(null); | ||
} | ||
return Promise.resolve(graphql_1.print(queryMap[matchedKeys[0]].transformedQuery)); | ||
}; | ||
return getMiddlewareForQueryMapFunction(queryMapFunc, enablePersistedQueries, lookupErrorHandler); | ||
} | ||
exports.getMiddlewareForQueryMap = getMiddlewareForQueryMap; | ||
function getMiddlewareForQueryMapFunction(queryMapFunc, enablePersistedQueries, lookupErrorHandler) { | ||
if (enablePersistedQueries === void 0) { enablePersistedQueries = true; } | ||
if (!enablePersistedQueries) { | ||
return (function (req, res, next) { | ||
@@ -26,15 +41,11 @@ next(); | ||
var queryId = req.body.id; | ||
var matchedKeys = Object.keys(queryMap).filter(function (key) { | ||
return (queryId !== undefined && | ||
queryMap[key].id.toString() === queryId.toString()); | ||
queryMapFunc(queryId).then(function (queryString) { | ||
req.body.query = queryString; | ||
next(); | ||
}).catch(function (err) { | ||
lookupErrorHandler(req, res, next); | ||
}); | ||
if (matchedKeys.length === 0 && lookupErrorHandler) { | ||
lookupErrorHandler(req, res, next); | ||
return; | ||
} | ||
req.body.query = graphql_1.print(queryMap[matchedKeys[0]].transformedQuery); | ||
next(); | ||
}); | ||
} | ||
exports.getMiddlewareForQueryMap = getMiddlewareForQueryMap; | ||
exports.getMiddlewareForQueryMapFunction = getMiddlewareForQueryMapFunction; | ||
//# sourceMappingURL=serverUtil.js.map |
@@ -98,3 +98,27 @@ "use strict"; | ||
}); | ||
describe('getMiddlewareForQueryFunction', function () { | ||
var expressRequest = { | ||
body: { | ||
id: 18, | ||
}, | ||
}; | ||
it('should call the query map function with correct id', function (done) { | ||
var queryMapFunc = function (queryId) { | ||
assert.equal(queryId, expressRequest.body.id); | ||
done(); | ||
return Promise.resolve(""); | ||
}; | ||
serverUtil_1.getMiddlewareForQueryMapFunction(queryMapFunc)(expressRequest, null, null); | ||
}); | ||
it('should call the error handler if the function rejects promise', function (done) { | ||
var queryMapFunc = function (queryId) { | ||
return Promise.reject(null); | ||
}; | ||
var errorHandler = function () { | ||
done(); | ||
}; | ||
serverUtil_1.getMiddlewareForQueryMapFunction(queryMapFunc, true, errorHandler)(expressRequest, null, null); | ||
}); | ||
}); | ||
}); | ||
//# sourceMappingURL=serverUtil.js.map |
{ | ||
"name": "extractgql", | ||
"version": "0.1.14", | ||
"version": "0.2.0", | ||
"description": "A build tool for GraphQL projects.", | ||
@@ -5,0 +5,0 @@ "main": "lib/src/index.js", |
# ExtractGQL | ||
This will be a simple build tool that enables query whitelisting for projects that use statically analyze-able GraphQL queries. | ||
`extractgql` is a simple build tool that enables query whitelisting and persisted queries for GraphQL projects that use statically analyze-able GraphQL queries. | ||
It scans a code directory and extracts GraphQL query documents from both `.graphql` files as well as queries contained in | ||
Javascript/Typescript files within a `gql` tag. It then assigns these queries ID values/hashes and produces a JSON file which maps | ||
from queries to hashes/IDs. This map can then be used by the client and server to perform query whitelisting, query lookups (i.e. | ||
client only sends the hash/id, the server just looks up the corresponding query), etc. | ||
It scans a code directory and extracts GraphQL query documents from `.graphql` files. It then assigns these queries ID values/hashes and produces a JSON file which maps from queries to hashes/IDs. This map can then be used by the client and server to perform query whitelisting, query lookups (i.e. client only sends the hash/id, the server just looks up the corresponding query), etc. | ||
The npm package also provides a network interface for [Apollo Client](https://github.com/apollostack/apollo-client) that manages the query lookups in `extractgql/lib/browser` and middleware for Express servers in `extractgql/lib/server`. These will likely be moved to their own packages in the future to reduce bundle size. | ||
# Installation | ||
For only the CLI tool: | ||
``` | ||
npm install -g extractgql | ||
``` | ||
As a dependency (for Express middlware or Apollo Client network interface): | ||
``` | ||
npm install --save extractgql | ||
``` | ||
# Build Tool Semantics | ||
The build tool will be called `extractgql`. Running it with no other arguments should give: | ||
The build tool binary is called `extractgql`. Running it with no other arguments should give: | ||
``` | ||
Usage: extractgql input_file [output file] | ||
Usage: extractgql input_file [output file] [--add_typename] | ||
``` | ||
It can be called on a Javascript/Typescript file or a file containing GraphQL query definitions with extension `.graphql`: | ||
It can be called on a file containing GraphQL query definitions with extension `.graphql`: | ||
```shell | ||
extractgql index.js | ||
extractgql index.ts | ||
extractgql queries.graphql | ||
@@ -38,16 +48,36 @@ ``` | ||
# Progress | ||
- Made Github Repo | ||
- Wrote some of this README | ||
- Wrote basic utility methods of `ExtractGQL` | ||
- Filled out the basic structure of `ExtractGQL` | ||
- Implemented the basic query extraction out of GraphQL files | ||
- Added argument parsing | ||
- Added output map construction from a query document | ||
- Got it to the point where it can recursively step through a directory, extract queries out of `.graphql` files and | ||
write an output JSON file from serialized query -> query | ||
- Huge gap where I forgot to update this README | ||
- Got extractgql working on `.graphql` files with some degree of named fragment support and support for query transformers. | ||
- Implemented middleware for Express and a persisted query network interface for Apollo Client, which will soon be integrated into a version of GitHunt that will be able to use persisted queries. | ||
- Added command line opt parsing to handle "--add-typename" which allows the user to select the query transformer that adds the typename to each level of the query. It seems unlikely that there'll be tons of query transformers straight off the bat so this seems like a reasonable approach for the timebeing. | ||
- Added handling of the README | ||
It can also take the `--add_typename` flag which will apply a query transformation to the query documents, adding the `__typename` field at every level of the query. You must pass this option if your client code uses this query transformation. | ||
``` | ||
extractgql src/ --add_typename | ||
``` | ||
# Apollo Client Network Interface | ||
This package also provides an implementation of an Apollo Client network interface that provides persisted query support. It serves as a drop-in replacement for the standard network interface and uses the query map given by `extractgql` in order to send only query hashes/ids to the serverather than the query document. | ||
See the implementation as well as some documentation for it within `src/network_interface/ApolloNetworkInterface.ts`. | ||
# Express middleware | ||
This package also provides middleware for Express servers that maps a JSON object such as the following: | ||
``` | ||
{ | ||
id: < query id >, | ||
variables: < query variables >, | ||
operationName: < query operation name > | ||
} | ||
``` | ||
to the following: | ||
``` | ||
{ | ||
query: < query >, | ||
variables: < query variables >, | ||
operationName: < query operation name > | ||
} | ||
``` | ||
That is, it maps query ids to the query document using the query map given by `extractgql`. See `src/server/serverUtil.ts` for the middleware methods and some documentation. |
@@ -38,3 +38,3 @@ // This file provides some basic utilities for servers that allow the server | ||
queryMapPath: string, | ||
production: boolean = true, | ||
enablePersistedQueries: boolean = true, | ||
lookupErrorHandler?: Handler, | ||
@@ -45,3 +45,3 @@ ): Promise<Handler> { | ||
const queryMap = JSON.parse(queryMapString); | ||
resolve(getMiddlewareForQueryMap(queryMap, production, lookupErrorHandler)); | ||
resolve(getMiddlewareForQueryMap(queryMap, enablePersistedQueries, lookupErrorHandler)); | ||
}).catch((err: Error) => { | ||
@@ -58,23 +58,14 @@ reject(err); | ||
// | ||
// @param production Boolean specifying whether this is a production environment. This middleware | ||
// will only perform query mapping if this option is true. | ||
// @param enablePersistedQueries Boolean specifying whether to perform query mapping. If set to | ||
// `false`, this middleware will just send the GraphQL document strings like a normal network | ||
// interface. | ||
// | ||
// @param lookupErrorHandler An Express handler that is called when a query cannot be found | ||
// in the query map. Only relevant if in a production environment. | ||
// in the query map. Only relevant if `enablePersistedQueries` is set to `true`. | ||
export function getMiddlewareForQueryMap( | ||
queryMap: OutputMap, | ||
production: boolean = true, | ||
enablePersistedQueries: boolean = true, | ||
lookupErrorHandler?: Handler, | ||
): Handler { | ||
// If we are not in a production environment, then we don't want to do any query mapping | ||
// and we move to the next request handler. | ||
if (!production) { | ||
return ((req: Request, res: Response, next: any) => { | ||
next(); | ||
}); | ||
} | ||
return ((req: Request, res: Response, next: any) => { | ||
const queryId = req.body.id as (number | string); | ||
const queryMapFunc = (queryId: (string | number)) => { | ||
// TODO this can be made O(1) if we have a reversible structure than a unidirectional | ||
@@ -87,12 +78,55 @@ // hash map. | ||
// If we find no keys with then given id, then we just let the lookupErrorHandler | ||
// take care of the situation. | ||
// If we find no keys with then given id, then we just return | ||
// a false-y value. | ||
if (matchedKeys.length === 0 && lookupErrorHandler) { | ||
lookupErrorHandler(req, res, next); | ||
return; | ||
return Promise.reject(null); | ||
} | ||
req.body.query = print(queryMap[matchedKeys[0]].transformedQuery); | ||
next(); | ||
return Promise.resolve(print(queryMap[matchedKeys[0]].transformedQuery)); | ||
} | ||
return getMiddlewareForQueryMapFunction(queryMapFunc, enablePersistedQueries, lookupErrorHandler); | ||
} | ||
// The same thing as `createPersistedQueryMiddleware` but takes a function that, | ||
// when evaluated for a particular query id, returns a Promise for the corresponding | ||
// query string. Can be used to load persisted queries from a database rather than from | ||
// a JSON file. | ||
// | ||
// @param queryMapFunc Function from query ids to Promises for graphql document strings. | ||
// Should reject the promise if the query id does not correspond to the graphql document | ||
// string. | ||
// | ||
// @param enablePersistedQueries Boolean specifying whether to perform query mapping. If set to | ||
// `false`, this middleware will just send the GraphQL document strings like a normal network | ||
// interface. | ||
// | ||
// @param lookupErrorHandler An Express handler that is called when a query cannot be | ||
// found in the query map. Only relevant if in ap roduction environment. | ||
export type QueryMapFunction = (queryId: (string | number)) => Promise<string>; | ||
export function getMiddlewareForQueryMapFunction( | ||
queryMapFunc: QueryMapFunction, | ||
enablePersistedQueries: boolean = true, | ||
lookupErrorHandler?: Handler, | ||
): Handler { | ||
// If we are not in a enablePersistedQueries environment, then we don't want to do any query mapping | ||
// and we move to the next request handler. | ||
if (!enablePersistedQueries) { | ||
return ((req: Request, res: Response, next: any) => { | ||
next(); | ||
}); | ||
} | ||
return ((req: Request, res: Response, next: any) => { | ||
const queryId = req.body.id as (number | string); | ||
queryMapFunc(queryId).then((queryString) => { | ||
req.body.query = queryString; | ||
next(); | ||
}).catch((err) => { | ||
// If we find no keys with then given id, then we just let the lookupErrorHandler | ||
// take care of the situation. | ||
lookupErrorHandler(req, res, next); | ||
}); | ||
}); | ||
} | ||
@@ -13,2 +13,4 @@ import * as chai from 'chai'; | ||
createPersistedQueryMiddleware, | ||
getMiddlewareForQueryMapFunction, | ||
QueryMapFunction, | ||
} from '../../src/server/serverUtil'; | ||
@@ -122,2 +124,36 @@ | ||
}); | ||
describe('getMiddlewareForQueryFunction', () => { | ||
const expressRequest = { | ||
body: { | ||
id: 18, | ||
}, | ||
}; | ||
it('should call the query map function with correct id', (done) => { | ||
const queryMapFunc = (queryId: (number | string)) => { | ||
assert.equal(queryId, expressRequest.body.id); | ||
done(); | ||
return Promise.resolve(""); | ||
}; | ||
getMiddlewareForQueryMapFunction(queryMapFunc)(expressRequest as Request, null, null); | ||
}); | ||
it('should call the error handler if the function rejects promise', (done) => { | ||
const queryMapFunc: QueryMapFunction = (queryId) => { | ||
return Promise.reject(null); | ||
}; | ||
const errorHandler = () => { | ||
done(); | ||
}; | ||
getMiddlewareForQueryMapFunction(queryMapFunc, true, errorHandler)( | ||
expressRequest as Request, | ||
null, | ||
null | ||
); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
204472
3722
83