From d2d31de9e7748e14c27660ffc174bf4c9f1ae70d Mon Sep 17 00:00:00 2001 From: Mike Burgh Date: Fri, 21 Apr 2017 11:24:07 -0500 Subject: [PATCH 1/4] Adding schema injection --- package.json | 1 + src/graphql/schema/BuildToken.ts | 2 ++ src/graphql/schema/createGqlSchema.ts | 3 +++ src/graphql/schema/getMutationGqlType.ts | 5 +++- src/graphql/schema/getQueryGqlType.ts | 3 ++- src/graphql/utils/index.ts | 3 ++- src/graphql/utils/loadInjections.ts | 23 +++++++++++++++++++ src/postgraphql/cli.ts | 3 +++ src/postgraphql/postgraphql.ts | 1 + .../schema/createPostGraphQLSchema.ts | 2 ++ 10 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/graphql/utils/loadInjections.ts diff --git a/package.json b/package.json index 05ff53dfed..e904fbbb8b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "pg-minify": "^0.4.1", "pluralize": "^3.0.0", "postgres-interval": "^1.0.2", + "require-glob": "^3.2.0", "send": "^0.14.1", "tslib": "^1.5.0" }, diff --git a/src/graphql/schema/BuildToken.ts b/src/graphql/schema/BuildToken.ts index d1a77401b9..e6ff94c6a2 100644 --- a/src/graphql/schema/BuildToken.ts +++ b/src/graphql/schema/BuildToken.ts @@ -29,6 +29,8 @@ interface BuildToken { // If true then the default mutations for tables (e.g. createMyTable) will // not be created readonly disableDefaultMutations: boolean, + // Path to read shcema injections from + readonly schemaInjection: string, }, // Hooks for adding custom fields/types into our schema. readonly _hooks: _BuildTokenHooks, diff --git a/src/graphql/schema/createGqlSchema.ts b/src/graphql/schema/createGqlSchema.ts index a8e4d23b0b..152ae2cc29 100644 --- a/src/graphql/schema/createGqlSchema.ts +++ b/src/graphql/schema/createGqlSchema.ts @@ -20,6 +20,8 @@ export type SchemaOptions = { // GraphQL types that override the default type generation. Currently this // API is private. Use at your own risk. _typeOverrides?: _BuildTokenTypeOverrides, + // The path to read our injections from + schemaInjection?: string, } /** @@ -36,6 +38,7 @@ export default function createGqlSchema (inventory: Inventory, options: SchemaOp nodeIdFieldName: options.nodeIdFieldName || 'nodeId', dynamicJson: options.dynamicJson || false, disableDefaultMutations: options.disableDefaultMutations || false, + schemaInjection: options.schemaInjection || '', }, _hooks: options._hooks || {}, _typeOverrides: options._typeOverrides || new Map(), diff --git a/src/graphql/schema/getMutationGqlType.ts b/src/graphql/schema/getMutationGqlType.ts index 4515fb9272..ea880b5879 100644 --- a/src/graphql/schema/getMutationGqlType.ts +++ b/src/graphql/schema/getMutationGqlType.ts @@ -1,5 +1,5 @@ import { GraphQLObjectType, GraphQLFieldConfig } from 'graphql' -import { buildObject, memoize1 } from '../utils' +import { buildObject, memoize1, loadInjections } from '../utils' import BuildToken from './BuildToken' import createCollectionMutationFieldEntries from './collection/createCollectionMutationFieldEntries' @@ -38,6 +38,9 @@ function createMutationGqlType (buildToken: BuildToken): GraphQLObjectType | und .map(collection => createCollectionMutationFieldEntries(buildToken, collection)) .reduce((a, b) => a.concat(b), []) ), + ...(loadInjections(options.schemaInjection, 'mutation') + + ), ] // If there are no mutation fields, just return to avoid errors. diff --git a/src/graphql/schema/getQueryGqlType.ts b/src/graphql/schema/getQueryGqlType.ts index 21114edf6d..24bfb8c222 100644 --- a/src/graphql/schema/getQueryGqlType.ts +++ b/src/graphql/schema/getQueryGqlType.ts @@ -1,5 +1,5 @@ import { GraphQLObjectType, GraphQLFieldConfig, GraphQLNonNull, GraphQLID } from 'graphql' -import { buildObject, memoize1 } from '../utils' +import { buildObject, memoize1, loadInjections } from '../utils' import createNodeFieldEntry from './node/createNodeFieldEntry' import getNodeInterfaceType from './node/getNodeInterfaceType' import createCollectionQueryFieldEntries from './collection/createCollectionQueryFieldEntries' @@ -36,6 +36,7 @@ function createGqlQueryType (buildToken: BuildToken): GraphQLObjectType { .getCollections() .map(collection => createCollectionQueryFieldEntries(buildToken, collection)) .reduce((a, b) => a.concat(b), []), + loadInjections(options.schemaInjection, 'query'), [ // The root query type is useful for Relay 1 as it limits what fields // can be queried at the top level. diff --git a/src/graphql/utils/index.ts b/src/graphql/utils/index.ts index 302ea00278..956c74658b 100644 --- a/src/graphql/utils/index.ts +++ b/src/graphql/utils/index.ts @@ -3,7 +3,8 @@ import formatName from './formatName' import idSerde from './idSerde' import scrib from './scrib' import parseGqlLiteralToValue from './parseGqlLiteralToValue' +import loadInjections from './loadInjections' -export { buildObject, formatName, idSerde, scrib, parseGqlLiteralToValue } +export { buildObject, formatName, idSerde, scrib, parseGqlLiteralToValue, loadInjections } export * from './memoize' diff --git a/src/graphql/utils/loadInjections.ts b/src/graphql/utils/loadInjections.ts new file mode 100644 index 0000000000..9518a79087 --- /dev/null +++ b/src/graphql/utils/loadInjections.ts @@ -0,0 +1,23 @@ +/** + * A utility function for requiring all files in a provided directory + * Returns an array of double dimensions, as expected by mutationEntries + */ + +import { GraphQLFieldConfig } from 'graphql' +import * as requireGlob from 'require-glob' + +export default function loadInjections(dirToInject: string, type: string): Array<[string, GraphQLFieldConfig]> { + + if (dirToInject === '' ) return [] + + const injections = requireGlob.sync(dirToInject, { + cwd: process.cwd(), + reducer: function (options, tree, file) { + if (!Array.isArray(tree)) tree = [] + if (file.exports.type === type) tree.push([file.exports.name, file.exports.schema) + return tree + }, + }) + + return injections +} diff --git a/src/postgraphql/cli.ts b/src/postgraphql/cli.ts index 9ef1c0fde7..a1eac6f2f3 100755 --- a/src/postgraphql/cli.ts +++ b/src/postgraphql/cli.ts @@ -43,6 +43,7 @@ program .option('--export-schema-json [path]', 'enables exporting the detected schema, in JSON format, to the given location. The directories must exist already, if the file exists it will be overwritten.') .option('--export-schema-graphql [path]', 'enables exporting the detected schema, in GraphQL schema format, to the given location. The directories must exist already, if the file exists it will be overwritten.') .option('--show-error-stack [setting]', 'show JavaScript error stacks in the GraphQL result errors') + .option('--schema-injection [path]', 'requires the files in the specified path and injects them into the GraphQL schema (supports glob\'s)') program.on('--help', () => console.log(` Get Started: @@ -81,6 +82,7 @@ const { exportSchemaGraphql: exportGqlSchemaPath, showErrorStack, bodySizeLimit, + schemaInjection, // tslint:disable-next-line no-any } = program as any @@ -125,6 +127,7 @@ const server = createServer(postgraphql(pgConfig, schemas, { exportJsonSchemaPath, exportGqlSchemaPath, bodySizeLimit, + schemaInjection, })) // Start our server by listening to a specific port and host name. Also log diff --git a/src/postgraphql/postgraphql.ts b/src/postgraphql/postgraphql.ts index a026dcf8ee..8f911b7eb5 100644 --- a/src/postgraphql/postgraphql.ts +++ b/src/postgraphql/postgraphql.ts @@ -27,6 +27,7 @@ type PostGraphQLOptions = { exportGqlSchemaPath?: string, bodySizeLimit?: string, pgSettings?: { [key: string]: mixed }, + schemaInjection?: string, } /** diff --git a/src/postgraphql/schema/createPostGraphQLSchema.ts b/src/postgraphql/schema/createPostGraphQLSchema.ts index 83f3ed905f..6dcd834eea 100644 --- a/src/postgraphql/schema/createPostGraphQLSchema.ts +++ b/src/postgraphql/schema/createPostGraphQLSchema.ts @@ -26,6 +26,7 @@ export default async function createPostGraphQLSchema ( jwtSecret?: string, jwtPgTypeIdentifier?: string, disableDefaultMutations?: boolean, + schemaInjection?: string, } = {}, ): Promise { // Create our inventory. @@ -109,6 +110,7 @@ export default async function createPostGraphQLSchema ( nodeIdFieldName: options.classicIds ? 'id' : 'nodeId', dynamicJson: options.dynamicJson, disableDefaultMutations: options.disableDefaultMutations, + schemaInjection: options.schemaInjection, // If we have a JWT Postgres type, let us override the GraphQL output type // with our own. From d59eabbcea479f75ade68c62a2b64a5bc42a18f6 Mon Sep 17 00:00:00 2001 From: Mike Burgh Date: Fri, 21 Apr 2017 11:50:56 -0500 Subject: [PATCH 2/4] (fix) close array --- src/graphql/utils/loadInjections.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphql/utils/loadInjections.ts b/src/graphql/utils/loadInjections.ts index 9518a79087..95eb44c2ad 100644 --- a/src/graphql/utils/loadInjections.ts +++ b/src/graphql/utils/loadInjections.ts @@ -14,7 +14,7 @@ export default function loadInjections(dirToInject: string, type: string): Array cwd: process.cwd(), reducer: function (options, tree, file) { if (!Array.isArray(tree)) tree = [] - if (file.exports.type === type) tree.push([file.exports.name, file.exports.schema) + if (file.exports.type === type) tree.push([file.exports.name, file.exports.schema]) return tree }, }) From 47b40da8c669b09be11603a41f0a0dd489697d10 Mon Sep 17 00:00:00 2001 From: Mike Burgh Date: Tue, 9 May 2017 16:10:40 -0500 Subject: [PATCH 3/4] Changing injections to return a function for the schema Cleaning up the loading process --- src/graphql/schema/getMutationGqlType.ts | 4 +- src/graphql/schema/getQueryGqlType.ts | 1 + src/graphql/utils/loadInjections.ts | 55 ++++++++++++++++++------ 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/graphql/schema/getMutationGqlType.ts b/src/graphql/schema/getMutationGqlType.ts index ea880b5879..32755c2ead 100644 --- a/src/graphql/schema/getMutationGqlType.ts +++ b/src/graphql/schema/getMutationGqlType.ts @@ -38,9 +38,7 @@ function createMutationGqlType (buildToken: BuildToken): GraphQLObjectType | und .map(collection => createCollectionMutationFieldEntries(buildToken, collection)) .reduce((a, b) => a.concat(b), []) ), - ...(loadInjections(options.schemaInjection, 'mutation') - - ), + ...(loadInjections(options.schemaInjection, 'mutation')), ] // If there are no mutation fields, just return to avoid errors. diff --git a/src/graphql/schema/getQueryGqlType.ts b/src/graphql/schema/getQueryGqlType.ts index 24bfb8c222..c0fb0c0cce 100644 --- a/src/graphql/schema/getQueryGqlType.ts +++ b/src/graphql/schema/getQueryGqlType.ts @@ -36,6 +36,7 @@ function createGqlQueryType (buildToken: BuildToken): GraphQLObjectType { .getCollections() .map(collection => createCollectionQueryFieldEntries(buildToken, collection)) .reduce((a, b) => a.concat(b), []), + // Load injections loadInjections(options.schemaInjection, 'query'), [ // The root query type is useful for Relay 1 as it limits what fields diff --git a/src/graphql/utils/loadInjections.ts b/src/graphql/utils/loadInjections.ts index 95eb44c2ad..f518d32b9f 100644 --- a/src/graphql/utils/loadInjections.ts +++ b/src/graphql/utils/loadInjections.ts @@ -4,20 +4,51 @@ */ import { GraphQLFieldConfig } from 'graphql' -import * as requireGlob from 'require-glob' +import postgraphql from '../../postgraphql/postgraphql' +const requireGlob = require('require-glob') -export default function loadInjections(dirToInject: string, type: string): Array<[string, GraphQLFieldConfig]> { +let initialized = false +const queries: Array<[string, GraphQLFieldConfig]> = [] +const mutations: Array<[string, GraphQLFieldConfig]> = [] + +// Sync method to read the injections, populates our injections array +function initInjections(dirToInject: string): void { - if (dirToInject === '' ) return [] + const files: Array<{ type: string, name: string, schema: (object: {}) => GraphQLFieldConfig }> = [] - const injections = requireGlob.sync(dirToInject, { + requireGlob.sync(dirToInject, { cwd: process.cwd(), - reducer: function (options, tree, file) { - if (!Array.isArray(tree)) tree = [] - if (file.exports.type === type) tree.push([file.exports.name, file.exports.schema]) - return tree - }, - }) - - return injections + reducer (_options: {}, tree: {} , file: {exports: { type: string, name: string, schema: (object: {}) => GraphQLFieldConfig}}): {} { + files.push({type: file.exports.type, name: file.exports.name, schema: file.exports.schema}) + return tree + }, + }) + + // Resolve each injection to get schema and validate type. + files.forEach(file => { + + if (file.type === 'query') { + queries.push([file.name, file.schema(postgraphql)]) + } else if (file.type === 'mutation') { + mutations.push([file.name, file.schema(postgraphql)]) + } else { + throw new Error(`Invalid type '${file.type}' in injection named ${file.name}.`) + } + }) + + initialized = true +} + +export default function loadInjections(dirToInject: string, type: string): Array<[string, GraphQLFieldConfig]> { + + if (dirToInject === '' ) { + return [] + } + + if (!initialized) { + initInjections(dirToInject) + } + + return type === 'query' ? queries : mutations + } From 5f260f2085f01f5bea172c2a1ff8998ae81ef259 Mon Sep 17 00:00:00 2001 From: Mike Burgh Date: Tue, 9 May 2017 16:54:51 -0500 Subject: [PATCH 4/4] Exposing the JWT claims to context --- src/postgraphql/withPostGraphQLContext.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/postgraphql/withPostGraphQLContext.ts b/src/postgraphql/withPostGraphQLContext.ts index f2798cc4de..ed53325bfd 100644 --- a/src/postgraphql/withPostGraphQLContext.ts +++ b/src/postgraphql/withPostGraphQLContext.ts @@ -70,7 +70,8 @@ export default async function withPostGraphQLContext( return await callback({ [$$pgClient]: pgClient, - pgRole, + pgRole: pgRole.role, + jwtClaims: pgRole.jwtClaims, }) } // Cleanup our Postgres client by ending the transaction and releasing @@ -104,7 +105,7 @@ async function setupPgClientTransaction ({ jwtAudiences?: Array, pgDefaultRole?: string, pgSettings?: { [key: string]: mixed }, -}): Promise { +}): Promise<{role: string | undefined, jwtClaims: {} | undefined}> { // Setup our default role. Once we decode our token, the role may change. let role = pgDefaultRole let jwtClaims: { [claimName: string]: mixed } = {} @@ -180,7 +181,7 @@ async function setupPgClientTransaction ({ await pgClient.query(query) } - return role + return {role, jwtClaims} } const $$pgClientOrigQuery = Symbol()