diff --git a/prettier.config.js b/prettier.config.js index 54cc21356f..51aaf1963d 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -1,6 +1,6 @@ module.exports = { printWidth: 100, - semi: false, + semi: true, singleQuote: true, trailingComma: "all", } diff --git a/src/__tests__/utils/createTestInParallel.ts b/src/__tests__/utils/createTestInParallel.ts index 672ccefa66..b1163e0430 100644 --- a/src/__tests__/utils/createTestInParallel.ts +++ b/src/__tests__/utils/createTestInParallel.ts @@ -11,24 +11,24 @@ export default function createTestInParallel(): ( ) => void { // All of the test functions. We collect them in a single array so that we can // call them all at once. - const testFns: Array<() => void | Promise> = [] + const testFns: Array<() => void | Promise> = []; // The promised results of calling all of our test functions. The single serial // tests will await these values. - let testResults: Array> | undefined + let testResults: Array> | undefined; return (name: string, fn: () => void | Promise): void => { // Add the test function and record its position. - const index = testFns.length - testFns.push(fn) + const index = testFns.length; + testFns.push(fn); test(name, async () => { // If the tests have not yet been run then run all of our tests. if (!testResults) { - testResults = testFns.map(testFn => Promise.resolve(testFn())) + testResults = testFns.map(testFn => Promise.resolve(testFn())); } // Await the result. - await testResults[index] - }) - } + await testResults[index]; + }); + }; } diff --git a/src/__tests__/utils/kitchenSinkSchemaSql.ts b/src/__tests__/utils/kitchenSinkSchemaSql.ts index 8bbc36f941..e2e361817a 100644 --- a/src/__tests__/utils/kitchenSinkSchemaSql.ts +++ b/src/__tests__/utils/kitchenSinkSchemaSql.ts @@ -1,11 +1,11 @@ -import { readFile } from 'fs' -import * as minify from 'pg-minify' +import { readFile } from 'fs'; +import * as minify from 'pg-minify'; const kitchenSinkSchemaSql = new Promise((resolve, reject) => { readFile('examples/kitchen-sink/schema.sql', (error, data) => { - if (error) reject(error) - else resolve(minify(data.toString().replace(/begin;|commit;/g, ''))) - }) -}) + if (error) reject(error); + else resolve(minify(data.toString().replace(/begin;|commit;/g, ''))); + }); +}); -export default kitchenSinkSchemaSql +export default kitchenSinkSchemaSql; diff --git a/src/__tests__/utils/pgPool.ts b/src/__tests__/utils/pgPool.ts index b5c5aa344e..d044a4e0dc 100644 --- a/src/__tests__/utils/pgPool.ts +++ b/src/__tests__/utils/pgPool.ts @@ -1,12 +1,12 @@ -import { Pool } from 'pg' -import { parse as parsePgConnectionString } from 'pg-connection-string' +import { Pool } from 'pg'; +import { parse as parsePgConnectionString } from 'pg-connection-string'; -const pgUrl = process.env.TEST_PG_URL || 'postgres://localhost:5432/postgraphile_test' +const pgUrl = process.env.TEST_PG_URL || 'postgres://localhost:5432/postgraphile_test'; const pgPool = new Pool({ ...parsePgConnectionString(pgUrl), max: 15, idleTimeoutMillis: 500, -}) +}); -export default pgPool +export default pgPool; diff --git a/src/__tests__/utils/printSchemaOrdered.js b/src/__tests__/utils/printSchemaOrdered.js index 6edc5aae73..45097f4f58 100644 --- a/src/__tests__/utils/printSchemaOrdered.js +++ b/src/__tests__/utils/printSchemaOrdered.js @@ -1,37 +1,37 @@ -import { parse, buildASTSchema } from 'graphql' -import { printSchema } from 'graphql/utilities' +import { parse, buildASTSchema } from 'graphql'; +import { printSchema } from 'graphql/utilities'; export default function printSchemaOrdered(originalSchema) { // Clone schema so we don't damage anything - const schema = buildASTSchema(parse(printSchema(originalSchema))) + const schema = buildASTSchema(parse(printSchema(originalSchema))); - const typeMap = schema.getTypeMap() + const typeMap = schema.getTypeMap(); Object.keys(typeMap).forEach(name => { - const gqlType = typeMap[name] + const gqlType = typeMap[name]; // Object? if (gqlType.getFields) { - const fields = gqlType.getFields() - const keys = Object.keys(fields).sort() + const fields = gqlType.getFields(); + const keys = Object.keys(fields).sort(); keys.forEach(key => { - const value = fields[key] + const value = fields[key]; // Move the key to the end of the object - delete fields[key] - fields[key] = value + delete fields[key]; + fields[key] = value; // Sort args if (value.args) { - value.args.sort((a, b) => a.name.localeCompare(b.name)) + value.args.sort((a, b) => a.name.localeCompare(b.name)); } - }) + }); } // Enum? if (gqlType.getValues) { - gqlType.getValues().sort((a, b) => a.name.localeCompare(b.name)) + gqlType.getValues().sort((a, b) => a.name.localeCompare(b.name)); } - }) + }); - return printSchema(schema) + return printSchema(schema); } diff --git a/src/__tests__/utils/withPgClient.ts b/src/__tests__/utils/withPgClient.ts index 87feab19a0..4c94e7cd92 100644 --- a/src/__tests__/utils/withPgClient.ts +++ b/src/__tests__/utils/withPgClient.ts @@ -1,6 +1,6 @@ -import { PoolClient } from 'pg' -import pgPool from './pgPool' -import kitchenSinkSchemaSql from './kitchenSinkSchemaSql' +import { PoolClient } from 'pg'; +import pgPool from './pgPool'; +import kitchenSinkSchemaSql from './kitchenSinkSchemaSql'; /** * Takes a function implementation of a test, and provides it a Postgres @@ -11,47 +11,47 @@ export default function withPgClient( fn: (client: PoolClient) => T | Promise, ): () => Promise { return async (): Promise => { - let result: T | undefined + let result: T | undefined; // Connect a client from our pool and begin a transaction. - const client = await pgPool.connect() + const client = await pgPool.connect(); // There’s some wierd behavior with the `pg` module here where an error // is resolved correctly. // // @see https://github.com/brianc/node-postgres/issues/1142 - if ((client as object)['errno']) throw client + if ((client as object)['errno']) throw client; - await client.query('begin') - await client.query("set local timezone to '+04:00'") + await client.query('begin'); + await client.query("set local timezone to '+04:00'"); // Run our kichen sink schema Sql, if there is an error we should report it try { - await client.query(await kitchenSinkSchemaSql) + await client.query(await kitchenSinkSchemaSql); } catch (error) { // Release the client if an error was thrown. - await client.query('rollback') - client.release() + await client.query('rollback'); + client.release(); // Log the error for debugging purposes. - console.error(error.stack || error) // tslint:disable-line no-console - throw error + console.error(error.stack || error); // tslint:disable-line no-console + throw error; } // Mock the query function. - client.query = jest.fn(client.query) + client.query = jest.fn(client.query); // Try to run our test, if it fails we still want to cleanup the client. try { - result = await fn(client) + result = await fn(client); } finally { // Always rollback our changes and release the client, even if the test // fails. - await client.query('rollback') - client.release() + await client.query('rollback'); + client.release(); } // We will always define our result in the above block. It appears that // TypeScript cannot detect that so we need to tell it with the bang. - return result! - } + return result!; + }; } diff --git a/src/index.ts b/src/index.ts index 8424296051..44944c6ab8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,10 @@ import { createPostGraphileSchema, watchPostGraphileSchema, withPostGraphileContext, -} from './postgraphile' -import { makePluginHook, PostGraphilePlugin } from './postgraphile/pluginHook' +} from './postgraphile'; +import { makePluginHook, PostGraphilePlugin } from './postgraphile/pluginHook'; -export default postgraphile +export default postgraphile; export { postgraphile, @@ -20,4 +20,4 @@ export { createPostGraphileSchema as createPostGraphQLSchema, watchPostGraphileSchema as watchPostGraphQLSchema, withPostGraphileContext as withPostGraphQLContext, -} +}; diff --git a/src/interfaces.ts b/src/interfaces.ts index 5098f6e31a..ee0ac0343b 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,10 +1,10 @@ /* tslint:disable:no-any */ -import { EventEmitter } from 'events' -import { GraphQLError, GraphQLSchema } from 'graphql' -import { IncomingMessage, ServerResponse } from 'http' -import { PluginHookFn } from './postgraphile/pluginHook' -import { Pool } from 'pg' -import jwt = require('jsonwebtoken') +import { EventEmitter } from 'events'; +import { GraphQLError, GraphQLSchema } from 'graphql'; +import { IncomingMessage, ServerResponse } from 'http'; +import { PluginHookFn } from './postgraphile/pluginHook'; +import { Pool } from 'pg'; +import jwt = require('jsonwebtoken'); export namespace PostGraphile { /** @@ -24,7 +24,7 @@ export namespace PostGraphile { * @see https://github.com/Microsoft/TypeScript/issues/9999 * @see https://flowtype.org/docs/builtins.html#mixed */ - export type mixed = {} | string | number | boolean | undefined | null + export type mixed = {} | string | number | boolean | undefined | null; // Please note that the comments for this type are turned into documentation // automatically. We try and specify the options in the same order as the CLI. @@ -40,48 +40,48 @@ export namespace PostGraphile { // // `drop schema postgraphile_watch cascade;` /* @middlewareOnly */ - watchPg?: boolean + watchPg?: boolean; // The default Postgres role to use. If no role was provided in a provided // JWT token, this role will be used. - pgDefaultRole?: string + pgDefaultRole?: string; // By default, JSON and JSONB fields are presented as strings (JSON encoded) // from the GraphQL schema. Setting this to `true` (recommended) enables raw // JSON input and output, saving the need to parse / stringify JSON manually. - dynamicJson?: boolean + dynamicJson?: boolean; // If none of your `RETURNS SETOF compound_type` functions mix NULLs with the // results then you may set this true to reduce the nullables in the GraphQL // schema. - setofFunctionsContainNulls?: boolean + setofFunctionsContainNulls?: boolean; // Enables classic ids for Relay support. Instead of using the field name // `nodeId` for globally unique ids, PostGraphile will instead use the field // name `id` for its globally unique ids. This means that table `id` columns // will also get renamed to `rowId`. - classicIds?: boolean + classicIds?: boolean; // Setting this to `true` will prevent the creation of the default mutation // types & fields. Database mutation will only be possible through Postgres // functions. - disableDefaultMutations?: boolean + disableDefaultMutations?: boolean; // Set false (recommended) to exclude fields, queries and mutations that the // user isn't permitted to access from the generated GraphQL schema; set this // option true to skip these checks and create GraphQL fields and types for // everything. // The default is `true`, in v5 the default will change to `false`. - ignoreRBAC?: boolean + ignoreRBAC?: boolean; // By default, tables and functions that come from extensions are excluded // from the generated GraphQL schema as general applications don't need them // to be exposed to the end user. You can use this flag to include them in // the generated schema (not recommended). - includeExtensionResources?: boolean + includeExtensionResources?: boolean; // Enables adding a `stack` field to the error response. Can be either the // boolean `true` (which results in a single stack string) or the string // `json` (which causes the stack to become an array with elements for each // line of the stack). Recommended in development, not recommended in // production. - showErrorStack?: boolean | 'json' + showErrorStack?: boolean | 'json'; // Extends the error response with additional details from the Postgres // error. Can be any combination of `['hint', 'detail', 'errcode']`. // Default is `[]`. - extendedErrors?: Array + extendedErrors?: Array; // Enables ability to modify errors before sending them down to the client. // Optionally can send down custom responses. If you use this then // `showErrorStack` and `extendedError` may have no @@ -91,95 +91,95 @@ export namespace PostGraphile { errors: Array, req: IncomingMessage, res: ServerResponse, - ) => Array) + ) => Array); // An array of [Graphile Build](/graphile-build/plugins/) plugins to load // after the default plugins. - appendPlugins?: Array<(builder: mixed) => {}> + appendPlugins?: Array<(builder: mixed) => {}>; // An array of [Graphile Build](/graphile-build/plugins/) plugins to load // before the default plugins (you probably don't want this). - prependPlugins?: Array<(builder: mixed) => {}> + prependPlugins?: Array<(builder: mixed) => {}>; // The full array of [Graphile Build](/graphile-build/plugins/) plugins to // use for schema generation (you almost definitely don't want this!). - replaceAllPlugins?: Array<(builder: mixed) => {}> + replaceAllPlugins?: Array<(builder: mixed) => {}>; // A file path string. Reads cached values from local cache file to improve // startup time (you may want to do this in production). - readCache?: string + readCache?: string; // A file path string. Writes computed values to local cache file so startup // can be faster (do this during the build phase). - writeCache?: string + writeCache?: string; // Enables saving the detected schema, in JSON format, to the given location. // The directories must exist already, if the file exists it will be // overwritten. /* @middlewareOnly */ - exportJsonSchemaPath?: string + exportJsonSchemaPath?: string; // Enables saving the detected schema, in GraphQL schema format, to the given // location. The directories must exist already, if the file exists it will // be overwritten. /* @middlewareOnly */ - exportGqlSchemaPath?: string + exportGqlSchemaPath?: string; // The endpoint the GraphQL executer will listen on. Defaults to `/graphql`. /* @middlewareOnly */ - graphqlRoute?: string + graphqlRoute?: string; // The endpoint the GraphiQL query interface will listen on (**NOTE:** // GraphiQL will not be enabled unless the `graphiql` option is set to // `true`). Defaults to `/graphiql`. /* @middlewareOnly */ - graphiqlRoute?: string + graphiqlRoute?: string; // Set this to `true` to enable the GraphiQL interface. /* @middlewareOnly */ - graphiql?: boolean + graphiql?: boolean; // Enables some generous CORS settings for the GraphQL endpoint. There are // some costs associated when enabling this, if at all possible try to put // your API behind a reverse proxy. /* @middlewareOnly */ - enableCors?: boolean + enableCors?: boolean; // Set the maximum size of JSON bodies that can be parsed (default 100kB). // The size can be given as a human-readable string, such as '200kB' or '5MB' // (case insensitive). /* @middlewareOnly */ - bodySizeLimit?: string + bodySizeLimit?: string; // [Experimental] Enable the middleware to process multiple GraphQL queries // in one request /* @middlewareOnly */ - enableQueryBatching?: string + enableQueryBatching?: string; // The secret for your JSON web tokens. This will be used to verify tokens in // the `Authorization` header, and signing JWT tokens you return in // procedures. - jwtSecret?: string + jwtSecret?: string; // Options with which to perform JWT verification - see // https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback // If 'audience' property is unspecified, it will default to // ['postgraphile']; to prevent audience verification set it explicitly to // null. /* @middlewareOnly */ - jwtVerifyOptions?: jwt.VerifyOptions + jwtVerifyOptions?: jwt.VerifyOptions; // A comma separated list of strings that give a path in the jwt from which // to extract the postgres role. If none is provided it will use the key // `role` on the root of the jwt. /* @middlewareOnly */ - jwtRole?: Array + jwtRole?: Array; // The Postgres type identifier for the compound type which will be signed as // a JWT token if ever found as the return type of a procedure. Can be of the // form: `my_schema.my_type`. You may use quotes as needed: // `"my-special-schema".my_type`. - jwtPgTypeIdentifier?: string + jwtPgTypeIdentifier?: string; // [DEPRECATED] The audience to use when verifing the JWT token. Deprecated, // use `jwtVerifyOptions.audience` instead. /* @middlewareOnly */ - jwtAudiences?: Array + jwtAudiences?: Array; // Some one-to-one relations were previously detected as one-to-many - should // we export 'only' the old relation shapes, both new and old but mark the // old ones as 'deprecated' (default), or 'omit' (recommended) the old // relation shapes entirely. - legacyRelations?: 'only' | 'deprecated' | 'omit' + legacyRelations?: 'only' | 'deprecated' | 'omit'; // ONLY use this option if you require the v3 typenames 'Json' and 'Uuid' // over 'JSON' and 'UUID'. - legacyJsonUuid?: boolean + legacyJsonUuid?: boolean; // Turns off GraphQL query logging. By default PostGraphile will log every // GraphQL query it processes along with some other information. Set this to // `true` (recommended in production) to disable that feature. /* @middlewareOnly */ - disableQueryLog?: boolean + disableQueryLog?: boolean; // A plain object specifying custom config values to set in the PostgreSQL // transaction (accessed via `current_setting('my.custom.setting')`) or a // function which will return the same (or a Promise to the same) based on @@ -187,72 +187,75 @@ export namespace PostGraphile { /* @middlewareOnly */ pgSettings?: | { [key: string]: mixed } - | ((req: IncomingMessage) => Promise<{ [key: string]: mixed }>) + | ((req: IncomingMessage) => Promise<{ [key: string]: mixed }>); // Some graphile-build plugins may need additional information available on // the `context` argument to the resolver - you can use this function to // provide such information based on the incoming request - you can even use // this to change the response [experimental], e.g. setting cookies /* @middlewareOnly */ - additionalGraphQLContextFromRequest?: (req: IncomingMessage, res: ServerResponse) => Promise<{}> + additionalGraphQLContextFromRequest?: ( + req: IncomingMessage, + res: ServerResponse, + ) => Promise<{}>; // [experimental] Plugin hook function, enables functionality within // PostGraphile to be expanded with plugins. Generate with // `makePluginHook(plugins)` passing a list of plugin objects. /* @middlewareOnly */ - pluginHook?: PluginHookFn + pluginHook?: PluginHookFn; // Should we use relay pagination, or simple collections? // "omit" (default) - relay connections only, // "only" (not recommended) - simple collections only (no Relay connections), // "both" - both - simpleCollections?: 'omit' | 'both' | 'only' + simpleCollections?: 'omit' | 'both' | 'only'; // Max query cache size in MBs of queries. Default, 50MB /* @middlewareOnly */ - queryCacheMaxSize?: number + queryCacheMaxSize?: number; // allow arbitrary extensions for consumption by plugins - [propName: string]: any + [propName: string]: any; } export interface GraphQLFormattedErrorExtended { // This is ugly, really I just want `string | void` but apparently TypeScript doesn't support that. - [s: string]: Array | Array | string | void - message: string - locations: Array | void - path: Array | void + [s: string]: Array | Array | string | void; + message: string; + locations: Array | void; + path: Array | void; } export interface GraphQLErrorLocation { - line: number - column: number + line: number; + column: number; } export type GraphQLErrorExtended = GraphQLError & { - hint: string - detail: string - code: string - } + hint: string; + detail: string; + code: string; + }; // Used by `createPostGraphileHttpRequestHandler` export interface ICreateRequestHandler extends PostGraphile.PostGraphileOptions { // The actual GraphQL schema we will use. - getGqlSchema: () => Promise + getGqlSchema: () => Promise; // A Postgres client pool we use to connect Postgres clients. - pgPool: Pool - _emitter: EventEmitter + pgPool: Pool; + _emitter: EventEmitter; } /** * A request handler for one of many different `http` frameworks. */ export interface HttpRequestHandler { - (req: IncomingMessage, res: ServerResponse, next?: (error?: mixed) => void): Promise - (ctx: { req: IncomingMessage; res: ServerResponse }, next: () => void): Promise - formatError: (e: GraphQLError) => GraphQLFormattedErrorExtended - getGraphQLSchema: () => Promise - pgPool: Pool + (req: IncomingMessage, res: ServerResponse, next?: (error?: mixed) => void): Promise; + (ctx: { req: IncomingMessage; res: ServerResponse }, next: () => void): Promise; + formatError: (e: GraphQLError) => GraphQLFormattedErrorExtended; + getGraphQLSchema: () => Promise; + pgPool: Pool; withPostGraphileContextFromReqRes: ( req: IncomingMessage, res: ServerResponse, moreOptions: any, fn: (ctx: mixed) => any, - ) => Promise + ) => Promise; } } diff --git a/src/postgraphile/__tests__/__mocks__/fs.js b/src/postgraphile/__tests__/__mocks__/fs.js index 84c24aaa3f..a1fe5476e1 100644 --- a/src/postgraphile/__tests__/__mocks__/fs.js +++ b/src/postgraphile/__tests__/__mocks__/fs.js @@ -1,11 +1,11 @@ -const fs = require.requireActual('fs') +const fs = require.requireActual('fs'); // Mock the writeFile for the export tests const writeFile = jest.fn((path, contents, callback) => { - callback() -}) + callback(); +}); module.exports = { ...fs, writeFile, -} +}; diff --git a/src/postgraphile/__tests__/__mocks__/postgraphile-core.js b/src/postgraphile/__tests__/__mocks__/postgraphile-core.js index b17336aaf2..12cbb41cd9 100644 --- a/src/postgraphile/__tests__/__mocks__/postgraphile-core.js +++ b/src/postgraphile/__tests__/__mocks__/postgraphile-core.js @@ -1,4 +1,4 @@ -const { GraphQLSchema, GraphQLObjectType, GraphQLInt } = require('graphql') +const { GraphQLSchema, GraphQLObjectType, GraphQLInt } = require('graphql'); const dummySchema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -9,9 +9,9 @@ const dummySchema = new GraphQLSchema({ }, }, }), -}) +}); module.exports = { createPostGraphileSchema: jest.fn(async (a, b, c) => dummySchema), watchPostGraphileSchema: jest.fn(async (a, b, c, cb) => cb(dummySchema)), -} +}; diff --git a/src/postgraphile/__tests__/postgraphile-test.js b/src/postgraphile/__tests__/postgraphile-test.js index 9bffef6ef1..e124468d73 100644 --- a/src/postgraphile/__tests__/postgraphile-test.js +++ b/src/postgraphile/__tests__/postgraphile-test.js @@ -1,50 +1,50 @@ -jest.mock('pg') -jest.mock('pg-connection-string') -jest.mock('postgraphile-core') -jest.mock('../http/createPostGraphileHttpRequestHandler') +jest.mock('pg'); +jest.mock('pg-connection-string'); +jest.mock('postgraphile-core'); +jest.mock('../http/createPostGraphileHttpRequestHandler'); -import { Pool } from 'pg' -import { parse as parsePgConnectionString } from 'pg-connection-string' -import { createPostGraphileSchema, watchPostGraphileSchema } from '..' -import createPostGraphileHttpRequestHandler from '../http/createPostGraphileHttpRequestHandler' -import postgraphile from '../postgraphile' +import { Pool } from 'pg'; +import { parse as parsePgConnectionString } from 'pg-connection-string'; +import { createPostGraphileSchema, watchPostGraphileSchema } from '..'; +import createPostGraphileHttpRequestHandler from '../http/createPostGraphileHttpRequestHandler'; +import postgraphile from '../postgraphile'; -const chalk = require('chalk') +const chalk = require('chalk'); createPostGraphileHttpRequestHandler.mockImplementation(({ getGqlSchema }) => Promise.resolve(getGqlSchema()).then(() => null), -) +); test('will use a connected client from the pool, the schemas, and options to create a GraphQL schema', async () => { - createPostGraphileSchema.mockClear() - createPostGraphileHttpRequestHandler.mockClear() - const pgPool = new Pool() - const schemas = [Symbol('schemas')] - const options = Symbol('options') - await postgraphile(pgPool, schemas, options) - expect(createPostGraphileSchema.mock.calls).toEqual([[pgPool, schemas, options]]) -}) + createPostGraphileSchema.mockClear(); + createPostGraphileHttpRequestHandler.mockClear(); + const pgPool = new Pool(); + const schemas = [Symbol('schemas')]; + const options = Symbol('options'); + await postgraphile(pgPool, schemas, options); + expect(createPostGraphileSchema.mock.calls).toEqual([[pgPool, schemas, options]]); +}); test('will use a connected client from the pool, the default schema, and options to create a GraphQL schema', async () => { - createPostGraphileSchema.mockClear() - createPostGraphileHttpRequestHandler.mockClear() - const pgPool = new Pool() - const options = Symbol('options') - const pgClient = { release: jest.fn() } - await postgraphile(pgPool, options) - expect(createPostGraphileSchema.mock.calls).toEqual([[pgPool, ['public'], options]]) -}) + createPostGraphileSchema.mockClear(); + createPostGraphileHttpRequestHandler.mockClear(); + const pgPool = new Pool(); + const options = Symbol('options'); + const pgClient = { release: jest.fn() }; + await postgraphile(pgPool, options); + expect(createPostGraphileSchema.mock.calls).toEqual([[pgPool, ['public'], options]]); +}); test('will use a created GraphQL schema to create the HTTP request handler and pass down options', async () => { - createPostGraphileSchema.mockClear() - createPostGraphileHttpRequestHandler.mockClear() - const pgPool = new Pool() - const gqlSchema = Symbol('gqlSchema') - const options = { a: 1, b: 2, c: 3 } - createPostGraphileSchema.mockReturnValueOnce(Promise.resolve(gqlSchema)) - await postgraphile(pgPool, [], options) - expect(createPostGraphileHttpRequestHandler.mock.calls.length).toBe(1) - expect(createPostGraphileHttpRequestHandler.mock.calls[0].length).toBe(1) + createPostGraphileSchema.mockClear(); + createPostGraphileHttpRequestHandler.mockClear(); + const pgPool = new Pool(); + const gqlSchema = Symbol('gqlSchema'); + const options = { a: 1, b: 2, c: 3 }; + createPostGraphileSchema.mockReturnValueOnce(Promise.resolve(gqlSchema)); + await postgraphile(pgPool, [], options); + expect(createPostGraphileHttpRequestHandler.mock.calls.length).toBe(1); + expect(createPostGraphileHttpRequestHandler.mock.calls[0].length).toBe(1); expect(Object.keys(createPostGraphileHttpRequestHandler.mock.calls[0][0])).toEqual([ 'a', 'b', @@ -52,32 +52,34 @@ test('will use a created GraphQL schema to create the HTTP request handler and p 'getGqlSchema', 'pgPool', '_emitter', - ]) - expect(createPostGraphileHttpRequestHandler.mock.calls[0][0].pgPool).toBe(pgPool) - expect(createPostGraphileHttpRequestHandler.mock.calls[0][0].a).toBe(options.a) - expect(createPostGraphileHttpRequestHandler.mock.calls[0][0].b).toBe(options.b) - expect(createPostGraphileHttpRequestHandler.mock.calls[0][0].c).toBe(options.c) - expect(await createPostGraphileHttpRequestHandler.mock.calls[0][0].getGqlSchema()).toBe(gqlSchema) -}) + ]); + expect(createPostGraphileHttpRequestHandler.mock.calls[0][0].pgPool).toBe(pgPool); + expect(createPostGraphileHttpRequestHandler.mock.calls[0][0].a).toBe(options.a); + expect(createPostGraphileHttpRequestHandler.mock.calls[0][0].b).toBe(options.b); + expect(createPostGraphileHttpRequestHandler.mock.calls[0][0].c).toBe(options.c); + expect(await createPostGraphileHttpRequestHandler.mock.calls[0][0].getGqlSchema()).toBe( + gqlSchema, + ); +}); test('will watch Postgres schemas when `watchPg` is true', async () => { - createPostGraphileSchema.mockClear() - watchPostGraphileSchema.mockClear() - const pgPool = new Pool() - const pgSchemas = [Symbol('a'), Symbol('b'), Symbol('c')] - await postgraphile(pgPool, pgSchemas, { watchPg: false }) - await postgraphile(pgPool, pgSchemas, { watchPg: true }) - expect(createPostGraphileSchema.mock.calls).toEqual([[pgPool, pgSchemas, { watchPg: false }]]) + createPostGraphileSchema.mockClear(); + watchPostGraphileSchema.mockClear(); + const pgPool = new Pool(); + const pgSchemas = [Symbol('a'), Symbol('b'), Symbol('c')]; + await postgraphile(pgPool, pgSchemas, { watchPg: false }); + await postgraphile(pgPool, pgSchemas, { watchPg: true }); + expect(createPostGraphileSchema.mock.calls).toEqual([[pgPool, pgSchemas, { watchPg: false }]]); - expect(watchPostGraphileSchema.mock.calls.length).toBe(1) - expect(watchPostGraphileSchema.mock.calls[0].length).toBe(4) - expect(watchPostGraphileSchema.mock.calls[0][0]).toEqual(pgPool) - expect(watchPostGraphileSchema.mock.calls[0][1]).toEqual(pgSchemas) - expect(watchPostGraphileSchema.mock.calls[0][2]).toEqual({ watchPg: true }) - expect(typeof watchPostGraphileSchema.mock.calls[0][3]).toBe('function') -}) + expect(watchPostGraphileSchema.mock.calls.length).toBe(1); + expect(watchPostGraphileSchema.mock.calls[0].length).toBe(4); + expect(watchPostGraphileSchema.mock.calls[0][0]).toEqual(pgPool); + expect(watchPostGraphileSchema.mock.calls[0][1]).toEqual(pgSchemas); + expect(watchPostGraphileSchema.mock.calls[0][2]).toEqual({ watchPg: true }); + expect(typeof watchPostGraphileSchema.mock.calls[0][3]).toBe('function'); +}); test('will not error if jwtSecret is provided without jwtPgTypeIdentifier', async () => { - const pgPool = new Pool() - expect(() => postgraphile(pgPool, [], { jwtSecret: 'test' })).not.toThrow() -}) + const pgPool = new Pool(); + expect(() => postgraphile(pgPool, [], { jwtSecret: 'test' })).not.toThrow(); +}); diff --git a/src/postgraphile/__tests__/postgraphileIntegrationMutations-test.js b/src/postgraphile/__tests__/postgraphileIntegrationMutations-test.js index 4316a73836..e929ddea40 100644 --- a/src/postgraphile/__tests__/postgraphileIntegrationMutations-test.js +++ b/src/postgraphile/__tests__/postgraphileIntegrationMutations-test.js @@ -1,31 +1,31 @@ -jest.unmock('postgraphile-core') +jest.unmock('postgraphile-core'); -import { resolve as resolvePath } from 'path' -import { readFile, readdirSync } from 'fs' -import { graphql } from 'graphql' -import withPgClient from '../../__tests__/utils/withPgClient' -import { $$pgClient } from '../../postgres/inventory/pgClientFromContext' -import { createPostGraphileSchema } from '..' +import { resolve as resolvePath } from 'path'; +import { readFile, readdirSync } from 'fs'; +import { graphql } from 'graphql'; +import withPgClient from '../../__tests__/utils/withPgClient'; +import { $$pgClient } from '../../postgres/inventory/pgClientFromContext'; +import { createPostGraphileSchema } from '..'; // This test suite can be flaky. Increase it’s timeout. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 20 +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 20; const kitchenSinkData = new Promise((resolve, reject) => { readFile('examples/kitchen-sink/data.sql', (error, data) => { - if (error) reject(error) - else resolve(data.toString().replace(/begin;|commit;/g, '')) - }) -}) + if (error) reject(error); + else resolve(data.toString().replace(/begin;|commit;/g, '')); + }); +}); -const mutationsDir = resolvePath(__dirname, 'fixtures/mutations') -const mutationFileNames = readdirSync(mutationsDir) -let mutationResults = [] +const mutationsDir = resolvePath(__dirname, 'fixtures/mutations'); +const mutationFileNames = readdirSync(mutationsDir); +let mutationResults = []; beforeAll(() => { // Get a GraphQL schema instance that we can query. const gqlSchemaPromise = withPgClient(async pgClient => { - return await createPostGraphileSchema(pgClient, ['a', 'b', 'c']) - })() + return await createPostGraphileSchema(pgClient, ['a', 'b', 'c']); + })(); // Execute all of the mutations in parallel. We will not wait for them to // resolve or reject. The tests will do that. @@ -35,30 +35,30 @@ beforeAll(() => { mutationResults = mutationFileNames.map(async fileName => { // Wait for the schema to resolve. We need the schema to be introspected // before we can do anything else! - let gqlSchema = await gqlSchemaPromise + let gqlSchema = await gqlSchemaPromise; // Get a new Postgres client and run the mutation. return await withPgClient(async pgClient => { // Read the mutation from the file system. const mutation = await new Promise((resolve, reject) => { readFile(resolvePath(mutationsDir, fileName), 'utf8', (error, data) => { - if (error) reject(error) - else resolve(data) - }) - }) + if (error) reject(error); + else resolve(data); + }); + }); // Add data to the client instance we are using. - await pgClient.query(await kitchenSinkData) + await pgClient.query(await kitchenSinkData); // Return the result of our GraphQL query. return await graphql(gqlSchema, mutation, null, { [$$pgClient]: pgClient, - }) - })() - }) -}) + }); + })(); + }); +}); for (let i = 0; i < mutationFileNames.length; i++) { test(mutationFileNames[i], async () => { - expect(await mutationResults[i]).toMatchSnapshot() - }) + expect(await mutationResults[i]).toMatchSnapshot(); + }); } diff --git a/src/postgraphile/__tests__/postgraphileIntegrationQueries-test.js b/src/postgraphile/__tests__/postgraphileIntegrationQueries-test.js index 6d9ed7ac2c..78e48198b3 100644 --- a/src/postgraphile/__tests__/postgraphileIntegrationQueries-test.js +++ b/src/postgraphile/__tests__/postgraphileIntegrationQueries-test.js @@ -1,25 +1,25 @@ -jest.unmock('postgraphile-core') +jest.unmock('postgraphile-core'); -import { resolve as resolvePath } from 'path' -import { readFile, readdirSync } from 'fs' -import { graphql } from 'graphql' -import withPgClient from '../../__tests__/utils/withPgClient' -import { $$pgClient } from '../../postgres/inventory/pgClientFromContext' -import { createPostGraphileSchema } from '..' +import { resolve as resolvePath } from 'path'; +import { readFile, readdirSync } from 'fs'; +import { graphql } from 'graphql'; +import withPgClient from '../../__tests__/utils/withPgClient'; +import { $$pgClient } from '../../postgres/inventory/pgClientFromContext'; +import { createPostGraphileSchema } from '..'; // This test suite can be flaky. Increase it’s timeout. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 20 +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 20; const kitchenSinkData = new Promise((resolve, reject) => { readFile('examples/kitchen-sink/data.sql', (error, data) => { - if (error) reject(error) - else resolve(data.toString().replace(/begin;|commit;/g, '')) - }) -}) + if (error) reject(error); + else resolve(data.toString().replace(/begin;|commit;/g, '')); + }); +}); -const queriesDir = resolvePath(__dirname, 'fixtures/queries') -const queryFileNames = readdirSync(queriesDir) -let queryResults = [] +const queriesDir = resolvePath(__dirname, 'fixtures/queries'); +const queryFileNames = readdirSync(queriesDir); +let queryResults = []; beforeAll(() => { // Get a few GraphQL schema instance that we can query. @@ -33,13 +33,13 @@ beforeAll(() => { createPostGraphileSchema(pgClient, ['a', 'b', 'c'], { dynamicJson: true, }), - ]) + ]); return { normal, classicIds, dynamicJson, - } - })() + }; + })(); // Execute all of the queries in parallel. We will not wait for them to // resolve or reject. The tests will do that. @@ -48,21 +48,21 @@ beforeAll(() => { const queryResultsPromise = (async () => { // Wait for the schema to resolve. We need the schema to be introspected // before we can do anything else! - const gqlSchemas = await gqlSchemasPromise + const gqlSchemas = await gqlSchemasPromise; // Get a new Postgres client instance. return await withPgClient(async pgClient => { // Add data to the client instance we are using. - await pgClient.query(await kitchenSinkData) + await pgClient.query(await kitchenSinkData); // Run all of our queries in parallel. return await Promise.all( queryFileNames.map(async fileName => { // Read the query from the file system. const query = await new Promise((resolve, reject) => { readFile(resolvePath(queriesDir, fileName), 'utf8', (error, data) => { - if (error) reject(error) - else resolve(data) - }) - }) + if (error) reject(error); + else resolve(data); + }); + }); // Get the appropriate GraphQL schema for this fixture. We want to test // some specific fixtures against a schema configured slightly // differently. @@ -71,24 +71,24 @@ beforeAll(() => { ? gqlSchemas.classicIds : fileName === 'dynamic-json.graphql' ? gqlSchemas.dynamicJson - : gqlSchemas.normal + : gqlSchemas.normal; // Return the result of our GraphQL query. return await graphql(gqlSchema, query, null, { [$$pgClient]: pgClient, - }) + }); }), - ) - })() - })() + ); + })(); + })(); // Flatten out the query results promise. queryResults = queryFileNames.map(async (_, i) => { - return await (await queryResultsPromise)[i] - }) -}) + return await (await queryResultsPromise)[i]; + }); +}); for (let i = 0; i < queryFileNames.length; i++) { test(queryFileNames[i], async () => { - expect(await queryResults[i]).toMatchSnapshot() - }) + expect(await queryResults[i]).toMatchSnapshot(); + }); } diff --git a/src/postgraphile/__tests__/postgraphileIntegrationSchema-test.js b/src/postgraphile/__tests__/postgraphileIntegrationSchema-test.js index 4ba2bb929b..fb3fdd0569 100644 --- a/src/postgraphile/__tests__/postgraphileIntegrationSchema-test.js +++ b/src/postgraphile/__tests__/postgraphileIntegrationSchema-test.js @@ -1,16 +1,16 @@ // TODO: There may be some excessive waste, if we could somehow filter what // these guys see, that would be great 👍 -jest.unmock('postgraphile-core') +jest.unmock('postgraphile-core'); -import printSchemaOrdered from '../../__tests__/utils/printSchemaOrdered' -import withPgClient from '../../__tests__/utils/withPgClient' -import { createPostGraphileSchema } from '..' +import printSchemaOrdered from '../../__tests__/utils/printSchemaOrdered'; +import withPgClient from '../../__tests__/utils/withPgClient'; +import { createPostGraphileSchema } from '..'; // This test suite can be flaky. Increase it’s timeout. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 20 +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 20; -let testResults +let testResults; const testFixtures = [ { @@ -42,18 +42,18 @@ const testFixtures = [ legacyJsonUuid: true, }), }, -] +]; beforeAll(() => { testResults = testFixtures.map(testFixture => withPgClient(async client => { - return await testFixture.createSchema(client) + return await testFixture.createSchema(client); })(), - ) -}) + ); +}); for (let i = 0; i < testFixtures.length; i++) { test(testFixtures[i].name, async () => { - expect(printSchemaOrdered(await testResults[i])).toMatchSnapshot() - }) + expect(printSchemaOrdered(await testResults[i])).toMatchSnapshot(); + }); } diff --git a/src/postgraphile/__tests__/postgraphileIntegrationSchemaExport-test.js b/src/postgraphile/__tests__/postgraphileIntegrationSchemaExport-test.js index bcae880965..3cd7553147 100644 --- a/src/postgraphile/__tests__/postgraphileIntegrationSchemaExport-test.js +++ b/src/postgraphile/__tests__/postgraphileIntegrationSchemaExport-test.js @@ -1,43 +1,43 @@ -jest.mock('fs') -jest.unmock('postgraphile-core') +jest.mock('fs'); +jest.unmock('postgraphile-core'); -import withPgClient from '../../__tests__/utils/withPgClient' -import { createPostGraphileSchema } from '..' -import exportPostGraphileSchema from '../schema/exportPostGraphileSchema' -import { writeFile } from 'fs' +import withPgClient from '../../__tests__/utils/withPgClient'; +import { createPostGraphileSchema } from '..'; +import exportPostGraphileSchema from '../schema/exportPostGraphileSchema'; +import { writeFile } from 'fs'; // This test suite can be flaky. Increase it’s timeout. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 20 +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 20; const gqlSchemaPromise = withPgClient(async pgClient => { - return await createPostGraphileSchema(pgClient, ['a', 'b', 'c']) -})() + return await createPostGraphileSchema(pgClient, ['a', 'b', 'c']); +})(); test('exports a schema as JSON', async () => { - writeFile.mockClear() - const gqlSchema = await gqlSchemaPromise + writeFile.mockClear(); + const gqlSchema = await gqlSchemaPromise; await exportPostGraphileSchema(gqlSchema, { exportJsonSchemaPath: '/schema.json', - }) - expect(writeFile.mock.calls.length).toBe(1) - expect(writeFile.mock.calls[0][0]).toBe('/schema.json') - expect(writeFile.mock.calls[0][1]).toMatchSnapshot() -}) + }); + expect(writeFile.mock.calls.length).toBe(1); + expect(writeFile.mock.calls[0][0]).toBe('/schema.json'); + expect(writeFile.mock.calls[0][1]).toMatchSnapshot(); +}); test('exports a schema as GQL', async () => { - writeFile.mockClear() - const gqlSchema = await gqlSchemaPromise + writeFile.mockClear(); + const gqlSchema = await gqlSchemaPromise; await exportPostGraphileSchema(gqlSchema, { exportGqlSchemaPath: '/schema.gql', - }) - expect(writeFile.mock.calls.length).toBe(1) - expect(writeFile.mock.calls[0][0]).toBe('/schema.gql') - expect(writeFile.mock.calls[0][1]).toMatchSnapshot() -}) + }); + expect(writeFile.mock.calls.length).toBe(1); + expect(writeFile.mock.calls[0][0]).toBe('/schema.gql'); + expect(writeFile.mock.calls[0][1]).toMatchSnapshot(); +}); test('does not export a schema when not enabled', async () => { - writeFile.mockClear() - const gqlSchema = await gqlSchemaPromise - await exportPostGraphileSchema(gqlSchema) - expect(writeFile.mock.calls.length).toBe(0) -}) + writeFile.mockClear(); + const gqlSchema = await gqlSchemaPromise; + await exportPostGraphileSchema(gqlSchema); + expect(writeFile.mock.calls.length).toBe(0); +}); diff --git a/src/postgraphile/__tests__/withPostGraphileContext-test.js b/src/postgraphile/__tests__/withPostGraphileContext-test.js index 240d754195..cb9881cfe5 100644 --- a/src/postgraphile/__tests__/withPostGraphileContext-test.js +++ b/src/postgraphile/__tests__/withPostGraphileContext-test.js @@ -1,9 +1,9 @@ // tslint:disable no-empty -import { $$pgClient } from '../../postgres/inventory/pgClientFromContext' -import withPostGraphileContext from '../withPostGraphileContext' +import { $$pgClient } from '../../postgres/inventory/pgClientFromContext'; +import withPostGraphileContext from '../withPostGraphileContext'; -const jwt = require('jsonwebtoken') +const jwt = require('jsonwebtoken'); /** * Expects an Http error. Passes if there is an error of the correct form, @@ -12,83 +12,83 @@ const jwt = require('jsonwebtoken') function expectHttpError(promise, statusCode, message) { return promise.then( () => { - throw new Error('Expected a Http error.') + throw new Error('Expected a Http error.'); }, error => { - expect(error.statusCode).toBe(statusCode) - expect(error.message).toBe(message) + expect(error.statusCode).toBe(statusCode); + expect(error.message).toBe(message); }, - ) + ); } test('will be a noop for no token, secret, or default role', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } - await withPostGraphileContext({ pgPool }, () => {}) - expect(pgClient.query.mock.calls).toEqual([['begin'], ['commit']]) -}) + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; + await withPostGraphileContext({ pgPool }, () => {}); + expect(pgClient.query.mock.calls).toEqual([['begin'], ['commit']]); +}); test('will pass in a context object with the client', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext({ pgPool }, client => { - expect(client[$$pgClient]).toBe(pgClient) - }) -}) + expect(client[$$pgClient]).toBe(pgClient); + }); +}); test('will record queries run inside the transaction', async () => { - const query1 = Symbol() - const query2 = Symbol() - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const query1 = Symbol(); + const query2 = Symbol(); + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext({ pgPool }, client => { - client[$$pgClient].query(query1) - client[$$pgClient].query(query2) - }) - expect(pgClient.query.mock.calls).toEqual([['begin'], [query1], [query2], ['commit']]) -}) + client[$$pgClient].query(query1); + client[$$pgClient].query(query2); + }); + expect(pgClient.query.mock.calls).toEqual([['begin'], [query1], [query2], ['commit']]); +}); test('will return the value from the callback', async () => { - const value = Symbol() - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } - expect(await withPostGraphileContext({ pgPool }, () => value)).toBe(value) -}) + const value = Symbol(); + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; + expect(await withPostGraphileContext({ pgPool }, () => value)).toBe(value); +}); test('will return the asynchronous value from the callback', async () => { - const value = Symbol() - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } - expect(await withPostGraphileContext({ pgPool }, () => Promise.resolve(value))).toBe(value) -}) + const value = Symbol(); + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; + expect(await withPostGraphileContext({ pgPool }, () => Promise.resolve(value))).toBe(value); +}); test('will throw an error if there was a `jwtToken`, but no `jwtSecret`', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await expectHttpError( withPostGraphileContext({ pgPool, jwtToken: 'asd' }, () => {}), 403, 'Not allowed to provide a JWT token.', - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) -}) + expect(pgClient.query.mock.calls).toEqual([]); +}); test('will throw an error for a malformed `jwtToken`', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await expectHttpError( withPostGraphileContext({ pgPool, jwtToken: 'asd', jwtSecret: 'secret' }, () => {}), 403, 'jwt malformed', - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) -}) + expect(pgClient.query.mock.calls).toEqual([]); +}); test('will throw an error if the JWT token was signed with the wrong signature', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await expectHttpError( withPostGraphileContext( { @@ -102,14 +102,14 @@ test('will throw an error if the JWT token was signed with the wrong signature', ), 403, 'invalid signature', - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) -}) + expect(pgClient.query.mock.calls).toEqual([]); +}); test('will throw an error if the JWT token does not have an audience', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await expectHttpError( withPostGraphileContext( { @@ -123,14 +123,14 @@ test('will throw an error if the JWT token does not have an audience', async () ), 403, 'jwt audience invalid. expected: postgraphile', - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) -}) + expect(pgClient.query.mock.calls).toEqual([]); +}); test('will throw an error if the JWT token does not have an appropriate audience', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await expectHttpError( withPostGraphileContext( { @@ -144,14 +144,14 @@ test('will throw an error if the JWT token does not have an appropriate audience ), 403, 'jwt audience invalid. expected: postgraphile', - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) -}) + expect(pgClient.query.mock.calls).toEqual([]); +}); test('will succeed with all the correct things', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -161,7 +161,7 @@ test('will succeed with all the correct things', async () => { jwtSecret: 'secret', }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -171,12 +171,12 @@ test('will succeed with all the correct things', async () => { }, ], ['commit'], - ]) -}) + ]); +}); test('will add extra claims as available', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -186,7 +186,7 @@ test('will add extra claims as available', async () => { jwtSecret: 'secret', }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -206,12 +206,12 @@ test('will add extra claims as available', async () => { }, ], ['commit'], - ]) -}) + ]); +}); test('will add extra settings as available', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -225,7 +225,7 @@ test('will add extra settings as available', async () => { }, }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -242,12 +242,12 @@ test('will add extra settings as available', async () => { }, ], ['commit'], - ]) -}) + ]); +}); test('undefined and null extra settings are ignored while 0 is converted to a string', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -266,7 +266,7 @@ test('undefined and null extra settings are ignored while 0 is converted to a st }, }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -294,13 +294,13 @@ test('undefined and null extra settings are ignored while 0 is converted to a st }, ], ['commit'], - ]) -}) + ]); +}); test('extra pgSettings that are objects throw an error', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } - let message + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; + let message; try { await withPostGraphileContext( { @@ -314,19 +314,19 @@ test('extra pgSettings that are objects throw an error', async () => { }, }, () => {}, - ) + ); } catch (error) { - message = error.message + message = error.message; } expect(message).toBe( 'Error converting pgSetting: object needs to be of type string, number or boolean.', - ) -}) + ); +}); test('extra pgSettings that are symbols throw an error', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } - let message + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; + let message; try { await withPostGraphileContext( { @@ -340,18 +340,18 @@ test('extra pgSettings that are symbols throw an error', async () => { }, }, () => {}, - ) + ); } catch (error) { - message = error.message + message = error.message; } expect(message).toBe( 'Error converting pgSetting: symbol needs to be of type string, number or boolean.', - ) -}) + ); +}); test('will set the default role if available', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -359,7 +359,7 @@ test('will set the default role if available', async () => { pgDefaultRole: 'test_default_role', }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -369,12 +369,12 @@ test('will set the default role if available', async () => { }, ], ['commit'], - ]) -}) + ]); +}); test('will set the default role if no other role was provided in the JWT', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -385,7 +385,7 @@ test('will set the default role if no other role was provided in the JWT', async pgDefaultRole: 'test_default_role', }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -407,12 +407,12 @@ test('will set the default role if no other role was provided in the JWT', async }, ], ['commit'], - ]) -}) + ]); +}); test('will set a role provided in the JWT', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -426,7 +426,7 @@ test('will set a role provided in the JWT', async () => { jwtSecret: 'secret', }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -450,12 +450,12 @@ test('will set a role provided in the JWT', async () => { }, ], ['commit'], - ]) -}) + ]); +}); test('will set a role provided in the JWT superceding the default role', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -470,7 +470,7 @@ test('will set a role provided in the JWT superceding the default role', async ( pgDefaultRole: 'test_default_role', }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -494,12 +494,12 @@ test('will set a role provided in the JWT superceding the default role', async ( }, ], ['commit'], - ]) -}) + ]); +}); test('will set a role provided in the JWT', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -518,7 +518,7 @@ test('will set a role provided in the JWT', async () => { jwtRole: ['some', 'other', 'path'], }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -542,12 +542,12 @@ test('will set a role provided in the JWT', async () => { }, ], ['commit'], - ]) -}) + ]); +}); test('will set a role provided in the JWT superceding the default role', async () => { - const pgClient = { query: jest.fn(), release: jest.fn() } - const pgPool = { connect: jest.fn(() => pgClient) } + const pgClient = { query: jest.fn(), release: jest.fn() }; + const pgPool = { connect: jest.fn(() => pgClient) }; await withPostGraphileContext( { pgPool, @@ -567,7 +567,7 @@ test('will set a role provided in the JWT superceding the default role', async ( pgDefaultRole: 'test_default_role', }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -591,16 +591,16 @@ test('will set a role provided in the JWT superceding the default role', async ( }, ], ['commit'], - ]) -}) + ]); +}); describe('jwtVerifyOptions', () => { - let pgClient - let pgPool + let pgClient; + let pgPool; beforeEach(() => { - pgClient = { query: jest.fn(), release: jest.fn() } - pgPool = { connect: jest.fn(() => pgClient) } - }) + pgClient = { query: jest.fn(), release: jest.fn() }; + pgPool = { connect: jest.fn(() => pgClient) }; + }); test('will throw an error if jwtAudiences and jwtVerifyOptions.audience are both provided', async () => { await expectHttpError( @@ -616,10 +616,10 @@ describe('jwtVerifyOptions', () => { ), 403, "Provide either 'jwtAudiences' or 'jwtVerifyOptions.audience' but not both", - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) - }) + expect(pgClient.query.mock.calls).toEqual([]); + }); test('will throw an error if jwtAudiences is provided and jwtVerifyOptions.audience = undefined', async () => { await expectHttpError( @@ -635,10 +635,10 @@ describe('jwtVerifyOptions', () => { ), 403, "Provide either 'jwtAudiences' or 'jwtVerifyOptions.audience' but not both", - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) - }) + expect(pgClient.query.mock.calls).toEqual([]); + }); test('will succeed with both jwtAudiences and jwtVerifyOptions if jwtVerifyOptions does not have an audience field', async () => { await withPostGraphileContext( @@ -653,7 +653,7 @@ describe('jwtVerifyOptions', () => { jwtVerifyOptions: { subject: 'my-subject' }, }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -663,8 +663,8 @@ describe('jwtVerifyOptions', () => { }, ], ['commit'], - ]) - }) + ]); + }); test('will succeed with audience check disabled via null', async () => { await withPostGraphileContext( @@ -678,7 +678,7 @@ describe('jwtVerifyOptions', () => { jwtVerifyOptions: { subject: 'my-subject', audience: null }, }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -688,8 +688,8 @@ describe('jwtVerifyOptions', () => { }, ], ['commit'], - ]) - }) + ]); + }); test('will succeed with audience check disabled via empty array', async () => { await withPostGraphileContext( @@ -703,7 +703,7 @@ describe('jwtVerifyOptions', () => { jwtVerifyOptions: { subject: 'my-subject', audience: [] }, }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -713,8 +713,8 @@ describe('jwtVerifyOptions', () => { }, ], ['commit'], - ]) - }) + ]); + }); test('will succeed with audience check disabled via empty string', async () => { await withPostGraphileContext( @@ -728,7 +728,7 @@ describe('jwtVerifyOptions', () => { jwtVerifyOptions: { subject: 'my-subject', audience: '' }, }, () => {}, - ) + ); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -738,8 +738,8 @@ describe('jwtVerifyOptions', () => { }, ], ['commit'], - ]) - }) + ]); + }); test('will throw error if audience does not match', async () => { await expectHttpError( @@ -758,10 +758,10 @@ describe('jwtVerifyOptions', () => { ), 403, 'jwt audience invalid. expected: my-audience', - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) - }) + expect(pgClient.query.mock.calls).toEqual([]); + }); test('will throw an error if the JWT token does not have an appropriate audience', async () => { await expectHttpError( @@ -776,10 +776,10 @@ describe('jwtVerifyOptions', () => { ), 403, 'jwt audience invalid. expected: another-audience', - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) - }) + expect(pgClient.query.mock.calls).toEqual([]); + }); test('will throw an error from a mismatched subject', async () => { await expectHttpError( @@ -795,10 +795,10 @@ describe('jwtVerifyOptions', () => { ), 403, 'jwt subject invalid. expected: orangutan', - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) - }) + expect(pgClient.query.mock.calls).toEqual([]); + }); test('will throw an error from an issuer array that does not match iss', async () => { await expectHttpError( @@ -813,10 +813,10 @@ describe('jwtVerifyOptions', () => { ), 403, 'jwt issuer invalid. expected: alpha:aliens,alpha:ufo', - ) + ); // Never set up the transaction due to error - expect(pgClient.query.mock.calls).toEqual([]) - }) + expect(pgClient.query.mock.calls).toEqual([]); + }); test("will default to an audience of ['postgraphile'] if no audience params are provided", async () => { await expectHttpError( @@ -830,8 +830,8 @@ describe('jwtVerifyOptions', () => { ), 403, 'jwt audience invalid. expected: postgraphile', - ) + ); // No need for transaction since there's no settings - expect(pgClient.query.mock.calls).toEqual([]) - }) -}) + expect(pgClient.query.mock.calls).toEqual([]); + }); +}); diff --git a/src/postgraphile/cli.ts b/src/postgraphile/cli.ts index f7489338e7..9b6529e917 100755 --- a/src/postgraphile/cli.ts +++ b/src/postgraphile/cli.ts @@ -1,87 +1,87 @@ #!/usr/bin/env node -import { resolve as resolvePath } from 'path' -import { readFileSync } from 'fs' -import { createServer } from 'http' -import chalk = require('chalk') -import program = require('commander') -import jwt = require('jsonwebtoken') -import { parse as parsePgConnectionString } from 'pg-connection-string' -import postgraphile, { getPostgraphileSchemaBuilder } from './postgraphile' -import { Pool, PoolConfig } from 'pg' -import cluster = require('cluster') -import { makePluginHook, PostGraphilePlugin } from './pluginHook' -import debugFactory = require('debug') -import { PostGraphile } from '../interfaces' -import mixed = PostGraphile.mixed - -const debugCli = debugFactory('postgraphile:cli') +import { resolve as resolvePath } from 'path'; +import { readFileSync } from 'fs'; +import { createServer } from 'http'; +import chalk = require('chalk'); +import program = require('commander'); +import jwt = require('jsonwebtoken'); +import { parse as parsePgConnectionString } from 'pg-connection-string'; +import postgraphile, { getPostgraphileSchemaBuilder } from './postgraphile'; +import { Pool, PoolConfig } from 'pg'; +import cluster = require('cluster'); +import { makePluginHook, PostGraphilePlugin } from './pluginHook'; +import debugFactory = require('debug'); +import { PostGraphile } from '../interfaces'; +import mixed = PostGraphile.mixed; + +const debugCli = debugFactory('postgraphile:cli'); // tslint:disable no-console -let config = {} +let config = {}; try { - config = require(process.cwd() + '/.postgraphilerc') // tslint:disable-line no-var-requires + config = require(process.cwd() + '/.postgraphilerc'); // tslint:disable-line no-var-requires if (!config.hasOwnProperty('options')) { - console.warn('WARNING: Your configuration file does not export any options') + console.warn('WARNING: Your configuration file does not export any options'); } } catch (error) { // Use command line options } // TODO: Demo Postgres database -const DEMO_PG_URL = null +const DEMO_PG_URL = null; -const manifest = JSON.parse(readFileSync(resolvePath(__dirname, '../../package.json')).toString()) +const manifest = JSON.parse(readFileSync(resolvePath(__dirname, '../../package.json')).toString()); function extractPlugins( rawArgv: Array, ): { - argv: Array - plugins: Array + argv: Array; + plugins: Array; } { - let argv - let pluginStrings = [] + let argv; + let pluginStrings = []; if (rawArgv[2] === '--plugins') { - pluginStrings = rawArgv[3].split(',') - argv = [...rawArgv.slice(0, 2), ...rawArgv.slice(4)] + pluginStrings = rawArgv[3].split(','); + argv = [...rawArgv.slice(0, 2), ...rawArgv.slice(4)]; } else { - pluginStrings = (config && config['options'] && config['options']['plugins']) || [] - argv = rawArgv + pluginStrings = (config && config['options'] && config['options']['plugins']) || []; + argv = rawArgv; } const plugins = pluginStrings.map((pluginString: string) => { - debugCli('Loading plugin %s', pluginString) - const rawPlugin = require(pluginString) // tslint:disable-lin no-var-requires + debugCli('Loading plugin %s', pluginString); + const rawPlugin = require(pluginString); // tslint:disable-lin no-var-requires if (rawPlugin['default'] && typeof rawPlugin['default'] === 'object') { - return rawPlugin['default'] + return rawPlugin['default']; } else { - return rawPlugin + return rawPlugin; } - }) - return { argv, plugins } + }); + return { argv, plugins }; } -const { argv: argvSansPlugins, plugins: extractedPlugins } = extractPlugins(process.argv) +const { argv: argvSansPlugins, plugins: extractedPlugins } = extractPlugins(process.argv); -const pluginHook = makePluginHook(extractedPlugins) +const pluginHook = makePluginHook(extractedPlugins); program .version(manifest.version) .usage('[options...]') - .description(manifest.description) + .description(manifest.description); // .option('-d, --demo', 'run PostGraphile using the demo database connection') export type AddFlagFn = ( optionString: string, description: string, parse?: (option: string) => mixed, -) => AddFlagFn +) => AddFlagFn; function addFlag( optionString: string, description: string, parse?: (option: string) => mixed, ): AddFlagFn { - program.option(optionString, description, parse) - return addFlag + program.option(optionString, description, parse); + return addFlag; } // Standard options @@ -113,9 +113,9 @@ program .option( '-r, --default-role ', 'the default Postgres role to use when a request is made. supercedes the role used to connect to the database', - ) + ); -pluginHook('cli:flags:add:standard', addFlag) +pluginHook('cli:flags:add:standard', addFlag); // Schema configuration program @@ -143,9 +143,9 @@ program .option( '--include-extension-resources', 'by default, tables and functions that come from extensions are excluded; use this flag to include them (not recommended)', - ) + ); -pluginHook('cli:flags:add:schema', addFlag) +pluginHook('cli:flags:add:schema', addFlag); // Error enhancements program @@ -157,9 +157,9 @@ program '--extended-errors ', "a comma separated list of extended Postgres error fields to display in the GraphQL result. Recommended in development: 'hint,detail,errcode'. Default: none", (option: string) => option.split(',').filter(_ => _), - ) + ); -pluginHook('cli:flags:add:errorHandling', addFlag) +pluginHook('cli:flags:add:errorHandling', addFlag); // Plugin-related options program @@ -170,9 +170,9 @@ program .option( '--prepend-plugins ', 'a comma-separated list of plugins to prepend to the list of GraphQL schema plugins', - ) + ); -pluginHook('cli:flags:add:plugins', addFlag) +pluginHook('cli:flags:add:plugins', addFlag); // Things that relate to -X program @@ -195,9 +195,9 @@ program .option( '-X, --no-server', '[experimental] for when you just want to use --write-cache or --export-schema-* and not actually run a server (e.g. CI)', - ) + ); -pluginHook('cli:flags:add:noServer', addFlag) +pluginHook('cli:flags:add:noServer', addFlag); // Webserver configuration program @@ -231,9 +231,9 @@ program '--enable-query-batching', '[experimental] enable the server to process multiple GraphQL queries in one request', ) - .option('--disable-query-log', 'disable logging queries to console (recommended in production)') + .option('--disable-query-log', 'disable logging queries to console (recommended in production)'); -pluginHook('cli:flags:add:webserver', addFlag) +pluginHook('cli:flags:add:webserver', addFlag); // JWT-related options program @@ -279,12 +279,12 @@ program .option( '-t, --jwt-token-identifier ', 'the Postgres identifier for a composite type that will be used to create JWT tokens', - ) + ); -pluginHook('cli:flags:add:jwt', addFlag) +pluginHook('cli:flags:add:jwt', addFlag); // Any other options -pluginHook('cli:flags:add', addFlag) +pluginHook('cli:flags:add', addFlag); // Deprecated program @@ -294,9 +294,9 @@ program '--jwt-audiences ', 'DEPRECATED Use --jwt-verify-audience instead', (option: string) => option.split(','), - ) + ); -pluginHook('cli:flags:add:deprecated', addFlag) +pluginHook('cli:flags:add:deprecated', addFlag); // Awkward application workarounds / legacy support program @@ -307,9 +307,9 @@ program .option( '--legacy-json-uuid', `ONLY use this option if you require the v3 typenames 'Json' and 'Uuid' over 'JSON' and 'UUID'`, - ) + ); -pluginHook('cli:flags:add:workarounds', addFlag) +pluginHook('cli:flags:add:workarounds', addFlag); program.on('--help', () => { console.log(`\ @@ -318,18 +318,18 @@ program.on('--help', () => { $ postgraphile $ postgraphile -c postgres://localhost/my_db $ postgraphile --connection postgres://user:pass@localhost/my_db --schema my_schema --watch --dynamic-json -`) - process.exit(0) -}) +`); + process.exit(0); +}); -program.parse(argvSansPlugins) +program.parse(argvSansPlugins); if (program['plugins']) { - throw new Error(`--plugins must be the first argument to postgraphile if specified`) + throw new Error(`--plugins must be the first argument to postgraphile if specified`); } // Kill server on exit. -process.on('SIGINT', process.exit) +process.on('SIGINT', process.exit); // Destruct our configuration file and command line arguments, use defaults, and rename options to // something appropriate for JavaScript. @@ -385,23 +385,23 @@ const { disableQueryLog, simpleCollections, // tslint:disable-next-line no-any -} = { ...config['options'], ...program } as any +} = { ...config['options'], ...program } as any; -let legacyRelations: 'omit' | 'deprecated' | 'only' +let legacyRelations: 'omit' | 'deprecated' | 'only'; if (['omit', 'only', 'deprecated'].indexOf(rawLegacyRelations) < 0) { throw new Error( `Invalid argument to '--legacy-relations' - expected on of 'omit', 'deprecated', 'only'; but received '${rawLegacyRelations}'`, - ) + ); } else { - legacyRelations = rawLegacyRelations + legacyRelations = rawLegacyRelations; } -const noServer = !yesServer +const noServer = !yesServer; // Add custom logic for getting the schemas from our CLI. If we are in demo // mode, we want to use the `forum_example` schema. Otherwise the `public` // schema is what we want. -const schemas: Array = dbSchema || (isDemo ? ['forum_example'] : ['public']) +const schemas: Array = dbSchema || (isDemo ? ['forum_example'] : ['public']); // Create our Postgres config. const pgConfig: PoolConfig = { @@ -420,61 +420,61 @@ const pgConfig: PoolConfig = { }), // Add the max pool size to our config. max: maxPoolSize, -} +}; const loadPlugins = (rawNames: mixed) => { if (!rawNames) { - return undefined + return undefined; } - const names = Array.isArray(rawNames) ? rawNames : String(rawNames).split(',') + const names = Array.isArray(rawNames) ? rawNames : String(rawNames).split(','); return names.map(rawName => { if (typeof rawName === 'function') { - return rawName + return rawName; } - const name = String(rawName) - const parts = name.split(':') - let root + const name = String(rawName); + const parts = name.split(':'); + let root; try { - root = require(String(parts.shift())) + root = require(String(parts.shift())); } catch (e) { // tslint:disable-next-line no-console - console.error(`Failed to load plugin '${name}'`) - throw e + console.error(`Failed to load plugin '${name}'`); + throw e; } - let plugin = root + let plugin = root; while (true) { - const part = parts.shift() + const part = parts.shift(); if (part == null) { - break + break; } - plugin = root[part] + plugin = root[part]; if (plugin == null) { - throw new Error(`No plugin found matching spec '${name}' - failed at '${part}'`) + throw new Error(`No plugin found matching spec '${name}' - failed at '${part}'`); } } if (typeof plugin === 'function') { - return plugin + return plugin; } else if (plugin === root && typeof plugin.default === 'function') { - return plugin.default // ES6 workaround + return plugin.default; // ES6 workaround } else { throw new Error( `No plugin found matching spec '${name}' - expected function, found '${typeof plugin}'`, - ) + ); } - }) -} + }); +}; if (jwtAudiences != null && jwtVerifyAudience != null) { - throw new Error(`Provide either '--jwt-audiences' or '-A, --jwt-verify-audience' but not both`) + throw new Error(`Provide either '--jwt-audiences' or '-A, --jwt-verify-audience' but not both`); } function trimNulls(obj: object): object { return Object.keys(obj).reduce((memo, key) => { if (obj[key] != null) { - memo[key] = obj[key] + memo[key] = obj[key]; } - return memo - }, {}) + return memo; + }, {}); } const jwtVerifyOptions: jwt.VerifyOptions = trimNulls({ @@ -486,7 +486,7 @@ const jwtVerifyOptions: jwt.VerifyOptions = trimNulls({ ignoreNotBefore: jwtVerifyIgnoreNotBefore, issuer: jwtVerifyIssuer, subject: jwtVerifySubject, -}) +}); // The options to pass through to the schema builder, or the middleware const postgraphileOptions = pluginHook( @@ -527,105 +527,105 @@ const postgraphileOptions = pluginHook( simpleCollections, }, { config, cliOptions: program }, -) +); if (noServer) { // No need for a server, let's just spin up the schema builder - ;(async () => { - const pgPool = new Pool(pgConfig) - const { getGraphQLSchema } = getPostgraphileSchemaBuilder(pgPool, schemas, postgraphileOptions) - await getGraphQLSchema() + (async () => { + const pgPool = new Pool(pgConfig); + const { getGraphQLSchema } = getPostgraphileSchemaBuilder(pgPool, schemas, postgraphileOptions); + await getGraphQLSchema(); if (!watchPg) { - await pgPool.end() + await pgPool.end(); } })().then(null, e => { - console.error('Error occurred!') - console.error(e) - process.exit(1) - }) + console.error('Error occurred!'); + console.error(e); + process.exit(1); + }); } else { function killAllWorkers(signal = 'SIGTERM'): void { for (const id in cluster.workers) { if (cluster.workers.hasOwnProperty(id)) { - cluster.workers[id].kill(signal) + cluster.workers[id].kill(signal); } } } if (clusterWorkers >= 2 && cluster.isMaster) { - let shuttingDown = false + let shuttingDown = false; const shutdown = () => { if (!shuttingDown) { - shuttingDown = true - process.exitCode = 1 + shuttingDown = true; + process.exitCode = 1; const fallbackTimeout = setTimeout(() => { - const remainingCount = Object.keys(cluster.workers).length + const remainingCount = Object.keys(cluster.workers).length; if (remainingCount > 0) { console.log( ` [cluster] ${remainingCount} workers did not die fast enough, sending SIGKILL`, - ) - killAllWorkers('SIGKILL') + ); + killAllWorkers('SIGKILL'); const ultraFallbackTimeout = setTimeout(() => { console.log( ` [cluster] really should have exited automatically, but haven't - exiting`, - ) - process.exit(3) - }, 5000) - ultraFallbackTimeout.unref() + ); + process.exit(3); + }, 5000); + ultraFallbackTimeout.unref(); } else { - console.log(` [cluster] should have exited automatically, but haven't - exiting`) - process.exit(2) + console.log(` [cluster] should have exited automatically, but haven't - exiting`); + process.exit(2); } - }, 5000) - fallbackTimeout.unref() - console.log(` [cluster] killing other workers with SIGTERM`) - killAllWorkers('SIGTERM') + }, 5000); + fallbackTimeout.unref(); + console.log(` [cluster] killing other workers with SIGTERM`); + killAllWorkers('SIGTERM'); } - } + }; cluster.on('exit', (worker, code, signal) => { console.log( ` [cluster] worker pid=${worker.process.pid} exited (code=${code}, signal=${signal})`, - ) - shutdown() - }) + ); + shutdown(); + }); for (let i = 0; i < clusterWorkers; i++) { const worker = cluster.fork({ POSTGRAPHILE_WORKER_NUMBER: String(i + 1), - }) - console.log(` [cluster] started worker ${i + 1} (pid=${worker.process.pid})`) + }); + console.log(` [cluster] started worker ${i + 1} (pid=${worker.process.pid})`); } } else { // Create’s our PostGraphile server - const rawMiddleware = postgraphile(pgConfig, schemas, postgraphileOptions) + const rawMiddleware = postgraphile(pgConfig, schemas, postgraphileOptions); const middleware = pluginHook('cli:server:middleware', rawMiddleware, { options: postgraphileOptions, - }) - const server = createServer(middleware) + }); + const server = createServer(middleware); if (serverTimeout) { - server.timeout = serverTimeout + server.timeout = serverTimeout; } pluginHook('cli:server:created', server, { options: postgraphileOptions, middleware, - }) + }); // Start our server by listening to a specific port and host name. Also log // some instructions and other interesting information. server.listen(port, hostname, () => { const self = cluster.isMaster ? 'server' - : `worker ${process.env.POSTGRAPHILE_WORKER_NUMBER} (pid=${process.pid})` + : `worker ${process.env.POSTGRAPHILE_WORKER_NUMBER} (pid=${process.pid})`; if (cluster.isMaster || process.env.POSTGRAPHILE_WORKER_NUMBER === '1') { - console.log('') + console.log(''); console.log( `PostGraphile ${self} listening on port ${chalk.underline( server.address().port.toString(), )} 🚀`, - ) - console.log('') + ); + console.log(''); console.log( ` ‣ Connected to Postgres instance ${chalk.underline.blue( isDemo @@ -634,36 +634,36 @@ if (noServer) { pgConfig.database != null ? `/${pgConfig.database}` : '' }`, )}`, - ) + ); console.log( ` ‣ Introspected Postgres schema(s) ${schemas .map(schema => chalk.magenta(schema)) .join(', ')}`, - ) + ); console.log( ` ‣ GraphQL endpoint served at ${chalk.underline( `http://${hostname}:${port}${graphqlRoute}`, )}`, - ) + ); if (!disableGraphiql) console.log( ` ‣ GraphiQL endpoint served at ${chalk.underline( `http://${hostname}:${port}${graphiqlRoute}`, )}`, - ) + ); - console.log('') - console.log(chalk.gray('* * *')) + console.log(''); + console.log(chalk.gray('* * *')); } else { console.log( `PostGraphile ${self} listening on port ${chalk.underline( server.address().port.toString(), )} 🚀`, - ) + ); } - console.log('') - }) + console.log(''); + }); } } /* eslint-enable */ diff --git a/src/postgraphile/extendedFormatError.ts b/src/postgraphile/extendedFormatError.ts index 3d89fb2a34..214af41a63 100644 --- a/src/postgraphile/extendedFormatError.ts +++ b/src/postgraphile/extendedFormatError.ts @@ -1,34 +1,34 @@ -import { GraphQLError } from 'graphql' -import { PostGraphile } from '../interfaces' -import GraphQLErrorExtended = PostGraphile.GraphQLErrorExtended -import GraphQLFormattedErrorExtended = PostGraphile.GraphQLFormattedErrorExtended -import mixed = PostGraphile.mixed +import { GraphQLError } from 'graphql'; +import { PostGraphile } from '../interfaces'; +import GraphQLErrorExtended = PostGraphile.GraphQLErrorExtended; +import GraphQLFormattedErrorExtended = PostGraphile.GraphQLFormattedErrorExtended; +import mixed = PostGraphile.mixed; /** * Extracts the requested fields from a pg error object, handling 'code' -> 'errcode' mapping. */ function pickPgError(err: mixed, inFields: string | Array): { [s: string]: string | void } { - const result: mixed = {} - let fields + const result: mixed = {}; + let fields; if (Array.isArray(inFields)) { - fields = inFields + fields = inFields; } else if (typeof inFields === 'string') { - fields = inFields.split(',') + fields = inFields.split(','); } else { - throw new Error('Invalid argument to extendedErrors - expected array of strings') + throw new Error('Invalid argument to extendedErrors - expected array of strings'); } if (err && typeof err === 'object') { fields.forEach((field: string) => { // pg places 'errcode' on the 'code' property if (typeof field !== 'string') { - throw new Error('Invalid argument to extendedErrors - expected array of strings') + throw new Error('Invalid argument to extendedErrors - expected array of strings'); } - const errField = field === 'errcode' ? 'code' : field - result[field] = err[errField] != null ? String(err[errField]) : err[errField] - }) + const errField = field === 'errcode' ? 'code' : field; + result[field] = err[errField] != null ? String(err[errField]) : err[errField]; + }); } - return result + return result; } /** @@ -42,13 +42,13 @@ export function extendedFormatError( fields: Array, ): GraphQLFormattedErrorExtended { if (!error) { - throw new Error('Received null or undefined error.') + throw new Error('Received null or undefined error.'); } - const originalError = error.originalError as GraphQLErrorExtended + const originalError = error.originalError as GraphQLErrorExtended; return { ...(originalError && fields ? pickPgError(originalError, fields) : undefined), message: error.message, locations: error.locations, path: error.path, - } + }; } diff --git a/src/postgraphile/graphiql/src/components/PostGraphiQL.js b/src/postgraphile/graphiql/src/components/PostGraphiQL.js index 3ffe6c0b20..7aa74a3029 100644 --- a/src/postgraphile/graphiql/src/components/PostGraphiQL.js +++ b/src/postgraphile/graphiql/src/components/PostGraphiQL.js @@ -1,8 +1,8 @@ -import React from 'react' -import GraphiQL from 'graphiql' -import { buildClientSchema, introspectionQuery, isType, GraphQLObjectType } from 'graphql' +import React from 'react'; +import GraphiQL from 'graphiql'; +import { buildClientSchema, introspectionQuery, isType, GraphQLObjectType } from 'graphql'; -const { POSTGRAPHILE_CONFIG } = window +const { POSTGRAPHILE_CONFIG } = window; /** * The standard GraphiQL interface wrapped with some PostGraphile extensions. @@ -13,17 +13,17 @@ class PostGraphiQL extends React.Component { // Our GraphQL schema which GraphiQL will use to do its intelligence // stuffs. schema: null, - } + }; componentDidMount() { // Update the schema for the first time. Log an error if we fail. - this.updateSchema().catch(error => console.error(error)) // tslint:disable-line no-console + this.updateSchema().catch(error => console.error(error)); // tslint:disable-line no-console // If we were given a `streamUrl`, we want to construct an `EventSource` // and add listeners. if (POSTGRAPHILE_CONFIG.streamUrl) { // Starts listening to the event stream at the `sourceUrl`. - const eventSource = new EventSource(POSTGRAPHILE_CONFIG.streamUrl) + const eventSource = new EventSource(POSTGRAPHILE_CONFIG.streamUrl); // When we get a change notification, we want to update our schema. eventSource.addEventListener( @@ -31,37 +31,37 @@ class PostGraphiQL extends React.Component { () => { this.updateSchema() .then(() => console.log('PostGraphile: Schema updated')) // tslint:disable-line no-console - .catch(error => console.error(error)) // tslint:disable-line no-console + .catch(error => console.error(error)); // tslint:disable-line no-console }, false, - ) + ); // Add event listeners that just log things in the console. eventSource.addEventListener( 'open', () => { // tslint:disable-next-line no-console - console.log('PostGraphile: Listening for server sent events') - this.updateSchema() + console.log('PostGraphile: Listening for server sent events'); + this.updateSchema(); }, false, - ) + ); eventSource.addEventListener( 'error', // tslint:disable-next-line no-console () => console.log('PostGraphile: Failed to connect to server'), false, - ) + ); // Store our event source so we can unsubscribe later. - this._eventSource = eventSource + this._eventSource = eventSource; } } componentWillUnmount() { // Close out our event source so we get no more events. - this._eventSource.close() - this._eventSource = null + this._eventSource.close(); + this._eventSource = null; } /** @@ -84,11 +84,11 @@ class PostGraphiQL extends React.Component { ), credentials: 'same-origin', body: JSON.stringify(graphQLParams), - }) + }); - const result = await response.json() + const result = await response.json(); - return result + return result; } /** @@ -99,17 +99,17 @@ class PostGraphiQL extends React.Component { async updateSchema() { // Fetch the schema using our introspection query and report once that has // finished. - const { data } = await this.executeQuery({ query: introspectionQuery }) + const { data } = await this.executeQuery({ query: introspectionQuery }); // Use the data we got back from GraphQL to build a client schema (a // schema without resolvers). - const schema = buildClientSchema(data) + const schema = buildClientSchema(data); // Update our component with the new schema. - this.setState({ schema }) + this.setState({ schema }); // Do some hacky stuff to GraphiQL. - this._updateGraphiQLDocExplorerNavStack(schema) + this._updateGraphiQLDocExplorerNavStack(schema); } /** @@ -126,13 +126,13 @@ class PostGraphiQL extends React.Component { _updateGraphiQLDocExplorerNavStack(nextSchema) { // Get the documentation explorer component from GraphiQL. Unfortunately // for them this looks like public API. Muwahahahaha. - const { docExplorerComponent } = this.graphiql - const { navStack } = docExplorerComponent.state + const { docExplorerComponent } = this.graphiql; + const { navStack } = docExplorerComponent.state; // If one type/field isn’t find this will be set to false and the // `navStack` will just reset itself. - let allOk = true - let exitEarly = false + let allOk = true; + let exitEarly = false; // Ok, so if you look at GraphiQL source code, the `navStack` is made up of // objects that are either types or fields. Let’s use that to search in @@ -140,64 +140,64 @@ class PostGraphiQL extends React.Component { const nextNavStack = navStack .map((navStackItem, i) => { // If we are not ok, abort! - if (exitEarly || !allOk) return null + if (exitEarly || !allOk) return null; // Get the definition from the nav stack item. - const typeOrField = navStackItem.def + const typeOrField = navStackItem.def; // If there is no type or field then this is likely the root schema view, // or a search. If this is the case then just return that nav stack item! if (!typeOrField) { - return navStackItem + return navStackItem; } else if (isType(typeOrField)) { // If this is a type, let’s do some shenanigans... // Let’s see if we can get a type with the same name. - const nextType = nextSchema.getType(typeOrField.name) + const nextType = nextSchema.getType(typeOrField.name); // If there is no type with this name (it was removed), we are not ok // so set `allOk` to false and return undefined. if (!nextType) { - exitEarly = true - return null + exitEarly = true; + return null; } // If there is a type with the same name, let’s return it! This is the // new type with all our new information. - return { ...navStackItem, def: nextType } + return { ...navStackItem, def: nextType }; } else { // If you thought this function was already pretty bad, it’s about to get // worse. We want to update the information for an object field. // Ok, so since this is an object field, we will assume that the last // element in our stack was an object type. - const nextLastType = nextSchema.getType(navStack[i - 1] ? navStack[i - 1].name : null) + const nextLastType = nextSchema.getType(navStack[i - 1] ? navStack[i - 1].name : null); // If there is no type for the last type in the nav stack’s name. // Panic! if (!nextLastType) { - allOk = false - return null + allOk = false; + return null; } // If the last type is not an object type. Panic! if (!(nextLastType instanceof GraphQLObjectType)) { - allOk = false - return null + allOk = false; + return null; } // Next we will see if the new field exists in the last object type. - const nextField = nextLastType.getFields()[typeOrField.name] + const nextField = nextLastType.getFields()[typeOrField.name]; // If not, Panic! if (!nextField) { - allOk = false - return null + allOk = false; + return null; } // Otherwise we hope very much that it is correct. - return { ...navStackItem, def: nextField } + return { ...navStackItem, def: nextField }; } }) - .filter(_ => _) + .filter(_ => _); // This is very hacky but works. React is cool. if (allOk) { @@ -205,20 +205,20 @@ class PostGraphiQL extends React.Component { // If we are not ok, just reset the `navStack` with an empty array. // Otherwise use our new stack. navStack: nextNavStack, - }) + }); } } render() { - const { schema } = this.state + const { schema } = this.state; return ( (this.graphiql = ref)} schema={schema} fetcher={params => this.executeQuery(params)} /> - ) + ); } } -export default PostGraphiQL +export default PostGraphiQL; diff --git a/src/postgraphile/graphiql/src/index.js b/src/postgraphile/graphiql/src/index.js index cc46b3178c..a2ac7945ec 100644 --- a/src/postgraphile/graphiql/src/index.js +++ b/src/postgraphile/graphiql/src/index.js @@ -1,7 +1,7 @@ -import './index.css' +import './index.css'; -import React from 'react' -import ReactDOM from 'react-dom' -import PostGraphiQL from './components/PostGraphiQL' +import React from 'react'; +import ReactDOM from 'react-dom'; +import PostGraphiQL from './components/PostGraphiQL'; -ReactDOM.render(, document.getElementById('root')) +ReactDOM.render(, document.getElementById('root')); diff --git a/src/postgraphile/http/__tests__/createPostGraphileHttpRequestHandler-test.js b/src/postgraphile/http/__tests__/createPostGraphileHttpRequestHandler-test.js index 2c1d1a91e3..c02b924bac 100644 --- a/src/postgraphile/http/__tests__/createPostGraphileHttpRequestHandler-test.js +++ b/src/postgraphile/http/__tests__/createPostGraphileHttpRequestHandler-test.js @@ -1,27 +1,27 @@ -jest.mock('send') +jest.mock('send'); -import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql' -import { $$pgClient } from '../../../postgres/inventory/pgClientFromContext' +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql'; +import { $$pgClient } from '../../../postgres/inventory/pgClientFromContext'; import createPostGraphileHttpRequestHandler, { graphiqlDirectory, -} from '../createPostGraphileHttpRequestHandler' - -const path = require('path') -const http = require('http') -const request = require('supertest') -const connect = require('connect') -const express = require('express') -const sendFile = require('send') -const event = require('events') -const compress = require('koa-compress') -const koa = require('koa') +} from '../createPostGraphileHttpRequestHandler'; + +const path = require('path'); +const http = require('http'); +const request = require('supertest'); +const connect = require('connect'); +const express = require('express'); +const sendFile = require('send'); +const event = require('events'); +const compress = require('koa-compress'); +const koa = require('koa'); sendFile.mockImplementation(() => { - const stream = new event.EventEmitter() - stream.pipe = jest.fn(res => process.nextTick(() => res.end())) - process.nextTick(() => stream.emit('end')) - return stream -}) + const stream = new event.EventEmitter(); + stream.pipe = jest.fn(res => process.nextTick(() => res.end())); + process.nextTick(() => stream.emit('end')); + return stream; +}); const gqlSchema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -45,13 +45,13 @@ const gqlSchema = new GraphQLSchema({ testError: { type: GraphQLString, resolve: (source, args, context) => { - const err = new Error('test message') - err.detail = 'test detail' - err.hint = 'test hint' - err.code = '12345' - err.where = 'In your code somewhere' - err.file = 'alcchk.c' - throw err + const err = new Error('test message'); + err.detail = 'test detail'; + err.hint = 'test hint'; + err.code = '12345'; + err.where = 'In your code somewhere'; + err.file = 'alcchk.c'; + throw err; }, }, }, @@ -65,155 +65,155 @@ const gqlSchema = new GraphQLSchema({ }, }, }), -}) +}); const pgClient = { query: jest.fn(() => Promise.resolve()), release: jest.fn(), -} +}; const pgPool = { connect: jest.fn(() => pgClient), -} +}; const defaultOptions = { getGqlSchema: () => gqlSchema, pgPool, disableQueryLog: true, -} +}; const serverCreators = new Map([ [ 'http', handler => { - return http.createServer(handler) + return http.createServer(handler); }, ], [ 'connect', handler => { - const app = connect() - app.use(handler) - return http.createServer(app) + const app = connect(); + app.use(handler); + return http.createServer(app); }, ], [ 'express', handler => { - const app = express() - app.use(handler) - return http.createServer(app) + const app = express(); + app.use(handler); + return http.createServer(app); }, ], -]) +]); serverCreators.set('koa', (handler, options = {}) => { - const app = new koa() - if (options.onPreCreate) options.onPreCreate(app) - app.use(handler) - return http.createServer(app.callback()) -}) + const app = new koa(); + if (options.onPreCreate) options.onPreCreate(app); + app.use(handler); + return http.createServer(app.callback()); +}); for (const [name, createServerFromHandler] of Array.from(serverCreators)) { const createServer = (handlerOptions, serverOptions) => createServerFromHandler( createPostGraphileHttpRequestHandler(Object.assign({}, defaultOptions, handlerOptions)), serverOptions, - ) + ); describe(name, () => { test('will 404 for route other than that specified', async () => { - const server1 = createServer() - const server2 = createServer({ graphqlRoute: '/x' }) + const server1 = createServer(); + const server2 = createServer({ graphqlRoute: '/x' }); await request(server1) .post('/x') - .expect(404) + .expect(404); await request(server2) .post('/graphql') - .expect(404) - }) + .expect(404); + }); test('will respond to queries on a different route', async () => { - const server = createServer({ graphqlRoute: '/x' }) + const server = createServer({ graphqlRoute: '/x' }); await request(server) .post('/x') .send({ query: '{hello}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'world' } }) - }) + .expect({ data: { hello: 'world' } }); + }); test('will always respond with CORS to an OPTIONS request when enabled', async () => { - const server = createServer({ enableCors: true }) + const server = createServer({ enableCors: true }); await request(server) .options('/graphql') .expect(200) .expect('Access-Control-Allow-Origin', '*') .expect('Access-Control-Request-Method', 'HEAD, GET, POST') .expect('Access-Control-Allow-Headers', /Accept, Authorization/) - .expect('') - }) + .expect(''); + }); test('will always respond to any request with CORS headers when enabled', async () => { - const server = createServer({ enableCors: true }) + const server = createServer({ enableCors: true }); await request(server) .post('/graphql') .expect('Access-Control-Allow-Origin', '*') .expect('Access-Control-Request-Method', 'HEAD, GET, POST') - .expect('Access-Control-Allow-Headers', /Accept, Authorization/) - }) + .expect('Access-Control-Allow-Headers', /Accept, Authorization/); + }); test('will not allow requests other than POST', async () => { - const server = createServer() + const server = createServer(); await request(server) .get('/graphql') .expect(405) - .expect('Allow', 'POST, OPTIONS') + .expect('Allow', 'POST, OPTIONS'); await request(server) .delete('/graphql') .expect(405) - .expect('Allow', 'POST, OPTIONS') + .expect('Allow', 'POST, OPTIONS'); await request(server) .put('/graphql') .expect(405) - .expect('Allow', 'POST, OPTIONS') - }) + .expect('Allow', 'POST, OPTIONS'); + }); test('will run a query on a POST request with JSON data', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .set('Content-Type', 'application/json') .send(JSON.stringify({ query: '{hello}' })) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'world' } }) - }) + .expect({ data: { hello: 'world' } }); + }); test('will run a query on a POST request with form data', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .set('Content-Type', 'application/x-www-form-urlencoded') .send(`query=${encodeURIComponent('{hello}')}`) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'world' } }) - }) + .expect({ data: { hello: 'world' } }); + }); test('will run a query on a POST request with GraphQL data', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .set('Content-Type', 'application/graphql') .send('{hello}') .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'world' } }) - }) + .expect({ data: { hello: 'world' } }); + }); test('will error if query parse fails', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .send({ query: '{' }) @@ -226,11 +226,11 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { locations: [{ line: 1, column: 2 }], }, ], - }) - }) + }); + }); test('will error if validation fails', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .send({ query: '{notFound}' }) @@ -243,66 +243,66 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { locations: [{ line: 1, column: 2 }], }, ], - }) - }) + }); + }); test('will allow mutations with POST', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .send({ query: 'mutation {hello}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'world' } }) - }) + .expect({ data: { hello: 'world' } }); + }); test('will connect and release a Postgres client from the pool on every request', async () => { // Note: no BEGIN/END because we don't need it here - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() - const server = createServer() + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); + const server = createServer(); await request(server) .post('/graphql') .send({ query: '{hello}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'world' } }) - expect(pgPool.connect.mock.calls).toEqual([[]]) - expect(pgClient.query.mock.calls).toEqual([]) - expect(pgClient.release.mock.calls).toEqual([[]]) - }) + .expect({ data: { hello: 'world' } }); + expect(pgPool.connect.mock.calls).toEqual([[]]); + expect(pgClient.query.mock.calls).toEqual([]); + expect(pgClient.release.mock.calls).toEqual([[]]); + }); test('will connect and release a Postgres client with a transaction from the pool on every mutation request', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() - const server = createServer() + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); + const server = createServer(); await request(server) .post('/graphql') .send({ query: 'mutation{hello}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'world' } }) - expect(pgPool.connect.mock.calls).toEqual([[]]) - expect(pgClient.query.mock.calls).toEqual([['begin'], ['commit']]) - expect(pgClient.release.mock.calls).toEqual([[]]) - }) + .expect({ data: { hello: 'world' } }); + expect(pgPool.connect.mock.calls).toEqual([[]]); + expect(pgClient.query.mock.calls).toEqual([['begin'], ['commit']]); + expect(pgClient.release.mock.calls).toEqual([[]]); + }); test('will setup a transaction for requests that use the Postgres client and have config', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); const server = createServer({ pgDefaultRole: 'bob', - }) + }); await request(server) .post('/graphql') .send({ query: '{query}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { query: null } }) - expect(pgPool.connect.mock.calls).toEqual([[]]) + .expect({ data: { query: null } }); + expect(pgPool.connect.mock.calls).toEqual([[]]); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -313,24 +313,24 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { ], ['EXECUTE'], ['commit'], - ]) - expect(pgClient.release.mock.calls).toEqual([[]]) - }) + ]); + expect(pgClient.release.mock.calls).toEqual([[]]); + }); test('will setup a transaction and pass down options for requests that use the Postgres client', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() - const jwtSecret = 'secret' - const pgDefaultRole = 'pg_default_role' - const server = createServer({ jwtSecret, pgDefaultRole }) + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); + const jwtSecret = 'secret'; + const pgDefaultRole = 'pg_default_role'; + const server = createServer({ jwtSecret, pgDefaultRole }); await request(server) .post('/graphql') .send({ query: '{query}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { query: null } }) - expect(pgPool.connect.mock.calls).toEqual([[]]) + .expect({ data: { query: null } }); + expect(pgPool.connect.mock.calls).toEqual([[]]); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -341,17 +341,17 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { ], ['EXECUTE'], ['commit'], - ]) - expect(pgClient.release.mock.calls).toEqual([[]]) - }) + ]); + expect(pgClient.release.mock.calls).toEqual([[]]); + }); test('adds properties from the JWT to the session', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() - const jwtSecret = 'secret' - const pgDefaultRole = 'pg_default_role' - const server = createServer({ jwtSecret, pgDefaultRole }) + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); + const jwtSecret = 'secret'; + const pgDefaultRole = 'pg_default_role'; + const server = createServer({ jwtSecret, pgDefaultRole }); await request(server) .post('/graphql') /* @@ -374,8 +374,8 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { .send({ query: '{query}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { query: null } }) - expect(pgPool.connect.mock.calls).toEqual([[]]) + .expect({ data: { query: null } }); + expect(pgPool.connect.mock.calls).toEqual([[]]); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -406,12 +406,12 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { ], ['EXECUTE'], ['commit'], - ]) - expect(pgClient.release.mock.calls).toEqual([[]]) - }) + ]); + expect(pgClient.release.mock.calls).toEqual([[]]); + }); test('will respect an operation name', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .send({ @@ -420,7 +420,7 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { a: 'world' } }) + .expect({ data: { a: 'world' } }); await request(server) .post('/graphql') .send({ @@ -429,11 +429,11 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { b: 'world' } }) - }) + .expect({ data: { b: 'world' } }); + }); test('will use variables', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .send({ @@ -443,7 +443,7 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { greetings: 'Hello, Joe!' } }) + .expect({ data: { greetings: 'Hello, Joe!' } }); await request(server) .post('/graphql') .set('Content-Type', 'application/x-www-form-urlencoded') @@ -454,45 +454,45 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { ) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { greetings: 'Hello, Joe!' } }) - }) + .expect({ data: { greetings: 'Hello, Joe!' } }); + }); test('will ignore empty string variables', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .send({ query: '{hello}', variables: '' }) .expect(200) - .expect({ data: { hello: 'world' } }) - }) + .expect({ data: { hello: 'world' } }); + }); test('will error with variables of the incorrect type', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .send({ query: '{hello}', variables: 2 }) .expect(400) .expect({ errors: [{ message: "Variables must be an object, not 'number'." }], - }) - }) + }); + }); test('will error with an operation name of the incorrect type', async () => { - const server = createServer() + const server = createServer(); await request(server) .post('/graphql') .send({ query: '{hello}', operationName: 2 }) .expect(400) .expect({ errors: [{ message: "Operation name must be a string, not 'number'." }], - }) - }) + }); + }); test('will report a simple error in the default case', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() - const server = createServer() + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); + const server = createServer(); await request(server) .post('/graphql') .send({ query: '{testError}' }) @@ -507,16 +507,16 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { path: ['testError'], }, ], - }) - }) + }); + }); test('will report an extended error when extendedErrors is enabled', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); const server = createServer({ extendedErrors: ['hint', 'detail', 'errcode'], - }) + }); await request(server) .post('/graphql') .send({ query: '{testError}' }) @@ -534,23 +534,23 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { errcode: '12345', }, ], - }) - }) + }); + }); test('will allow user to customize errors when handleErrors is set', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); const server = createServer({ handleErrors: errors => { return errors.map(error => { - error.message = 'my custom error message' - error.hint = 'my custom error hint' - error.detail = 'my custom error detail' - return error - }) + error.message = 'my custom error message'; + error.hint = 'my custom error hint'; + error.detail = 'my custom error detail'; + return error; + }); }, - }) + }); await request(server) .post('/graphql') .send({ query: '{testError}' }) @@ -567,19 +567,19 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { detail: 'my custom error detail', }, ], - }) - }) + }); + }); test('will allow user to send custom responses when handleErrors is set and sends a response', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); const server = createServer({ handleErrors: (errors, req, res) => { - res.statusCode = 401 - return errors + res.statusCode = 401; + return errors; }, - }) + }); await request(server) .post('/graphql') .send({ query: '{testError}' }) @@ -592,162 +592,162 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { }, ], data: { testError: null }, - }) - }) + }); + }); test('will serve a favicon when graphiql is enabled', async () => { - const server1 = createServer({ graphiql: true }) - const server2 = createServer({ graphiql: true, route: '/graphql' }) + const server1 = createServer({ graphiql: true }); + const server2 = createServer({ graphiql: true, route: '/graphql' }); await request(server1) .get('/favicon.ico') .expect(200) .expect('Cache-Control', 'public, max-age=86400') - .expect('Content-Type', 'image/x-icon') + .expect('Content-Type', 'image/x-icon'); await request(server2) .get('/favicon.ico') .expect(200) .expect('Cache-Control', 'public, max-age=86400') - .expect('Content-Type', 'image/x-icon') - }) + .expect('Content-Type', 'image/x-icon'); + }); test('will not serve a favicon when graphiql is disabled', async () => { - const server1 = createServer({ graphiql: false }) - const server2 = createServer({ graphiql: false, route: '/graphql' }) + const server1 = createServer({ graphiql: false }); + const server2 = createServer({ graphiql: false, route: '/graphql' }); await request(server1) .get('/favicon.ico') - .expect(404) + .expect(404); await request(server2) .get('/favicon.ico') - .expect(404) - }) + .expect(404); + }); test('will serve any assets for graphiql', async () => { - sendFile.mockClear() - const server = createServer({ graphiql: true }) + sendFile.mockClear(); + const server = createServer({ graphiql: true }); await request(server) .get('/_postgraphile/graphiql/anything.css') - .expect(200) + .expect(200); await request(server) .get('/_postgraphile/graphiql/something.js') - .expect(200) + .expect(200); await request(server) .get('/_postgraphile/graphiql/very/deeply/nested') - .expect(200) + .expect(200); await request(server) .get('/_postgraphile/graphiql/index.html') - .expect(404) + .expect(404); await request(server) .get('/_postgraphile/graphiql/../../../../etc/passwd') - .expect(403) + .expect(403); expect(sendFile.mock.calls.map(([res, filepath, options]) => [filepath, options])).toEqual([ ['anything.css', { index: false, dotfiles: 'ignore', root: graphiqlDirectory }], ['something.js', { index: false, dotfiles: 'ignore', root: graphiqlDirectory }], ['very/deeply/nested', { index: false, dotfiles: 'ignore', root: graphiqlDirectory }], - ]) - }) + ]); + }); test('will not serve some graphiql assets', async () => { - const server = createServer({ graphiql: true }) + const server = createServer({ graphiql: true }); await request(server) .get('/_postgraphile/graphiql/index.html') - .expect(404) + .expect(404); await request(server) .get('/_postgraphile/graphiql/asset-manifest.json') - .expect(404) - }) + .expect(404); + }); test('will not serve any assets for graphiql when disabled', async () => { - sendFile.mockClear() - const server = createServer({ graphiql: false }) + sendFile.mockClear(); + const server = createServer({ graphiql: false }); await request(server) .get('/_postgraphile/graphiql/anything.css') - .expect(404) + .expect(404); await request(server) .get('/_postgraphile/graphiql/something.js') - .expect(404) - expect(sendFile.mock.calls.length).toEqual(0) - }) + .expect(404); + expect(sendFile.mock.calls.length).toEqual(0); + }); test('will not allow if no text/event-stream headers are set', async () => { - const server = createServer({ graphiql: true }) + const server = createServer({ graphiql: true }); await request(server) .get('/_postgraphile/stream') - .expect(405) - }) + .expect(405); + }); test('will render GraphiQL if enabled', async () => { - const server1 = createServer() - const server2 = createServer({ graphiql: true }) + const server1 = createServer(); + const server2 = createServer({ graphiql: true }); await request(server1) .get('/graphiql') - .expect(404) + .expect(404); await request(server2) .get('/graphiql') .expect(200) - .expect('Content-Type', 'text/html; charset=utf-8') - }) + .expect('Content-Type', 'text/html; charset=utf-8'); + }); test('will render GraphiQL on another route if desired', async () => { - const server1 = createServer({ graphiqlRoute: '/x' }) - const server2 = createServer({ graphiql: true, graphiqlRoute: '/x' }) - const server3 = createServer({ graphiql: false, graphiqlRoute: '/x' }) + const server1 = createServer({ graphiqlRoute: '/x' }); + const server2 = createServer({ graphiql: true, graphiqlRoute: '/x' }); + const server3 = createServer({ graphiql: false, graphiqlRoute: '/x' }); await request(server1) .get('/x') - .expect(404) + .expect(404); await request(server2) .get('/x') .expect(200) - .expect('Content-Type', 'text/html; charset=utf-8') + .expect('Content-Type', 'text/html; charset=utf-8'); await request(server3) .get('/x') - .expect(404) + .expect(404); await request(server3) .get('/graphiql') - .expect(404) - }) + .expect(404); + }); test('cannot use a rejected GraphQL schema', async () => { - const rejectedGraphQLSchema = Promise.reject(new Error('Uh oh!')) + const rejectedGraphQLSchema = Promise.reject(new Error('Uh oh!')); // We don’t want Jest to complain about uncaught promise rejections. rejectedGraphQLSchema.catch(() => { /* noop */ - }) + }); const server = createServer({ getGqlSchema: () => rejectedGraphQLSchema, - }) + }); // We want to hide `console.error` warnings because we are intentionally // generating some here. - const origConsoleError = console.error + const origConsoleError = console.error; console.error = () => { /* noop */ - } + }; try { await request(server) .post('/graphql') .send({ query: '{hello}' }) - .expect(500) + .expect(500); } finally { - console.error = origConsoleError + console.error = origConsoleError; } - }) + }); test('will correctly hand over pgSettings to the withPostGraphileContext call', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); const server = createServer({ pgSettings: { 'foo.string': 'test1', 'foo.number': 42, }, - }) + }); await request(server) .post('/graphql') .send({ query: '{hello}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'world' } }) - expect(pgPool.connect.mock.calls).toEqual([[]]) + .expect({ data: { hello: 'world' } }); + expect(pgPool.connect.mock.calls).toEqual([[]]); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -757,27 +757,27 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { }, ], ['commit'], - ]) - expect(pgClient.release.mock.calls).toEqual([[]]) - }) + ]); + expect(pgClient.release.mock.calls).toEqual([[]]); + }); test('will correctly hand over pgSettings function to the withPostGraphileContext call', async () => { - pgPool.connect.mockClear() - pgClient.query.mockClear() - pgClient.release.mockClear() + pgPool.connect.mockClear(); + pgClient.query.mockClear(); + pgClient.release.mockClear(); const server = createServer({ pgSettings: req => ({ 'foo.string': 'test1', 'foo.number': 42, }), - }) + }); await request(server) .post('/graphql') .send({ query: '{hello}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'world' } }) - expect(pgPool.connect.mock.calls).toEqual([[]]) + .expect({ data: { hello: 'world' } }); + expect(pgPool.connect.mock.calls).toEqual([[]]); expect(pgClient.query.mock.calls).toEqual([ ['begin'], [ @@ -787,12 +787,12 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { }, ], ['commit'], - ]) - expect(pgClient.release.mock.calls).toEqual([[]]) - }) + ]); + expect(pgClient.release.mock.calls).toEqual([[]]); + }); test('will call additionalGraphQLContextFromRequest if provided and add the response to the context', async () => { - const helloResolver = jest.fn((source, args, context) => context.additional) + const helloResolver = jest.fn((source, args, context) => context.additional); const contextCheckGqlSchema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', @@ -803,29 +803,29 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { }, }, }), - }) + }); const additionalGraphQLContextFromRequest = jest.fn(() => ({ additional: 'foo', - })) + })); const server = createServer({ additionalGraphQLContextFromRequest, getGqlSchema: () => Promise.resolve(contextCheckGqlSchema), - }) + }); await request(server) .post('/graphql') .send({ query: '{hello}' }) .expect(200) .expect('Content-Type', /json/) - .expect({ data: { hello: 'foo' } }) - expect(additionalGraphQLContextFromRequest).toHaveBeenCalledTimes(1) + .expect({ data: { hello: 'foo' } }); + expect(additionalGraphQLContextFromRequest).toHaveBeenCalledTimes(1); expect(additionalGraphQLContextFromRequest.mock.calls[0][0]).toBeInstanceOf( http.IncomingMessage, - ) + ); expect(additionalGraphQLContextFromRequest.mock.calls[0][1]).toBeInstanceOf( http.ServerResponse, - ) - }) + ); + }); if (name === 'koa') { const createKoaCompressionServer = () => @@ -837,26 +837,26 @@ for (const [name, createServerFromHandler] of Array.from(serverCreators)) { compress({ threshold: 0, }), - ) + ); }, }, - ) + ); test('koa serves & compresses graphiql route', async () => { - const server = createKoaCompressionServer() + const server = createKoaCompressionServer(); await request(server) .get('/graphiql') .expect(200) - .expect('Content-Encoding', /gzip/) - }) + .expect('Content-Encoding', /gzip/); + }); test('koa serves & compresses graphiql assets', async () => { - const server = createKoaCompressionServer() + const server = createKoaCompressionServer(); await request(server) .get('/_postgraphile/graphiql/anything.css') - .expect(200) + .expect(200); expect(sendFile.mock.calls.map(([res, filepath, options]) => [filepath, options])).toEqual([ ['anything.css', { index: false, dotfiles: 'ignore', root: graphiqlDirectory }], - ]) - }) + ]); + }); } - }) + }); } diff --git a/src/postgraphile/http/createPostGraphileHttpRequestHandler.ts b/src/postgraphile/http/createPostGraphileHttpRequestHandler.ts index 3728910b4e..ac52d1ce54 100644 --- a/src/postgraphile/http/createPostGraphileHttpRequestHandler.ts +++ b/src/postgraphile/http/createPostGraphileHttpRequestHandler.ts @@ -1,6 +1,6 @@ /* tslint:disable:no-any */ -import { join as joinPath, resolve as resolvePath, relative as relativePath } from 'path' -import { readFile } from 'fs' +import { join as joinPath, resolve as resolvePath, relative as relativePath } from 'path'; +import { readFile } from 'fs'; import { Source, parse as parseGraphql, @@ -12,53 +12,53 @@ import { print as printGraphql, specifiedRules, DocumentNode, -} from 'graphql' -import { extendedFormatError } from '../extendedFormatError' -import { IncomingMessage, ServerResponse } from 'http' -import { isKoaApp, middleware as koaMiddleware } from './koaMiddleware' -import { pluginHookFromOptions } from '../pluginHook' -import { PostGraphile } from '../../interfaces' -import HttpRequestHandler = PostGraphile.HttpRequestHandler -import ICreateRequestHandler = PostGraphile.ICreateRequestHandler -import mixed = PostGraphile.mixed -import setupServerSentEvents from './setupServerSentEvents' -import withPostGraphileContext from '../withPostGraphileContext' -import { Context as KoaContext } from 'koa' - -import chalk = require('chalk') -import Debugger = require('debug') // tslint:disable-line variable-name -import httpError = require('http-errors') -import parseUrl = require('parseurl') -import finalHandler = require('finalhandler') -import bodyParser = require('body-parser') -import sendFile = require('send') -import LRU = require('lru-cache') -import crypto = require('crypto') +} from 'graphql'; +import { extendedFormatError } from '../extendedFormatError'; +import { IncomingMessage, ServerResponse } from 'http'; +import { isKoaApp, middleware as koaMiddleware } from './koaMiddleware'; +import { pluginHookFromOptions } from '../pluginHook'; +import { PostGraphile } from '../../interfaces'; +import HttpRequestHandler = PostGraphile.HttpRequestHandler; +import ICreateRequestHandler = PostGraphile.ICreateRequestHandler; +import mixed = PostGraphile.mixed; +import setupServerSentEvents from './setupServerSentEvents'; +import withPostGraphileContext from '../withPostGraphileContext'; +import { Context as KoaContext } from 'koa'; + +import chalk = require('chalk'); +import Debugger = require('debug'); // tslint:disable-line variable-name +import httpError = require('http-errors'); +import parseUrl = require('parseurl'); +import finalHandler = require('finalhandler'); +import bodyParser = require('body-parser'); +import sendFile = require('send'); +import LRU = require('lru-cache'); +import crypto = require('crypto'); const calculateQueryHash = (queryString: string): string => crypto .createHash('sha1') .update(queryString) - .digest('base64') + .digest('base64'); // Fast way of checking if an object is empty, // faster than `Object.keys(value).length === 0` -const hasOwnProperty = Object.prototype.hasOwnProperty +const hasOwnProperty = Object.prototype.hasOwnProperty; function isEmpty(value: any): boolean { for (const key in value) { if (hasOwnProperty.call(value, key)) { - return false + return false; } } - return true + return true; } -const { POSTGRAPHILE_ENV } = process.env +const { POSTGRAPHILE_ENV } = process.env; -const debugGraphql = Debugger('postgraphile:graphql') -const debugRequest = Debugger('postgraphile:request') +const debugGraphql = Debugger('postgraphile:graphql'); +const debugRequest = Debugger('postgraphile:request'); -export const graphiqlDirectory = resolvePath(__dirname, '../graphiql/public') +export const graphiqlDirectory = resolvePath(__dirname, '../graphiql/public'); /** * The favicon file in `Buffer` format. We can send a `Buffer` directly to the @@ -68,10 +68,10 @@ export const graphiqlDirectory = resolvePath(__dirname, '../graphiql/public') */ const favicon = new Promise((resolve, reject) => { readFile(resolvePath(__dirname, '../../../resources/favicon.ico'), (error, data) => { - if (error) reject(error) - else resolve(data) - }) -}) + if (error) reject(error); + else resolve(data); + }); +}); /** * The GraphiQL HTML file as a string. We need it to be a string, because we @@ -79,10 +79,10 @@ const favicon = new Promise((resolve, reject) => { */ const origGraphiqlHtml: Promise = new Promise((resolve, reject) => { readFile(resolvePath(__dirname, '../graphiql/public/index.html'), 'utf8', (error, data) => { - if (error) reject(error) - else resolve(data) - }) -}) + if (error) reject(error); + else resolve(data); + }); +}); /** * We need to be able to share the withPostGraphileContext logic between HTTP @@ -96,13 +96,13 @@ function withPostGraphileContextFromReqResGenerator( moreOptions: any, fn: (ctx: mixed) => any, ) => Promise { - const { pgSettings, jwtSecret, additionalGraphQLContextFromRequest } = options + const { pgSettings, jwtSecret, additionalGraphQLContextFromRequest } = options; return async (req, res, moreOptions, fn) => { - const jwtToken = jwtSecret ? getJwtToken(req) : null + const jwtToken = jwtSecret ? getJwtToken(req) : null; const additionalContext = typeof additionalGraphQLContextFromRequest === 'function' ? await additionalGraphQLContextFromRequest(req, res) - : {} + : {}; return withPostGraphileContext( { ...options, @@ -111,11 +111,11 @@ function withPostGraphileContextFromReqResGenerator( ...moreOptions, }, context => { - const graphqlContext = { ...additionalContext, ...(context as object) } - return fn(graphqlContext) + const graphqlContext = { ...additionalContext, ...(context as object) }; + return fn(graphqlContext); }, - ) - } + ); + }; } /** @@ -129,20 +129,20 @@ function withPostGraphileContextFromReqResGenerator( export default function createPostGraphileHttpRequestHandler( options: ICreateRequestHandler, ): HttpRequestHandler { - const MEGABYTE = 1024 * 1024 + const MEGABYTE = 1024 * 1024; const { getGqlSchema, pgPool, pgSettings, pgDefaultRole, queryCacheMaxSize = 50 * MEGABYTE, - } = options - const pluginHook = pluginHookFromOptions(options) + } = options; + const pluginHook = pluginHookFromOptions(options); if (pgDefaultRole && typeof pgSettings === 'function') { throw new Error( 'pgDefaultRole cannot be combined with pgSettings(req) - please remove pgDefaultRole and instead always return a `role` key from pgSettings(req).', - ) + ); } if ( pgDefaultRole && @@ -154,18 +154,18 @@ export default function createPostGraphileHttpRequestHandler( ) { throw new Error( 'pgDefaultRole cannot be combined with pgSettings.role - please use one or the other.', - ) + ); } // Gets the route names for our GraphQL endpoint, and our GraphiQL endpoint. - const graphqlRoute = options.graphqlRoute || '/graphql' - const graphiqlRoute = options.graphiql === true ? options.graphiqlRoute || '/graphiql' : null + const graphqlRoute = options.graphqlRoute || '/graphql'; + const graphiqlRoute = options.graphiql === true ? options.graphiqlRoute || '/graphiql' : null; // Throw an error of the GraphQL and GraphiQL routes are the same. if (graphqlRoute === graphiqlRoute) throw new Error( `Cannot use the same route, '${graphqlRoute}', for both GraphQL and GraphiQL. Please use different routes.`, - ) + ); // Formats an error using the default GraphQL `formatError` function, and // custom formatting using some other options. @@ -175,7 +175,7 @@ export default function createPostGraphileHttpRequestHandler( const formattedError = options.extendedErrors && options.extendedErrors.length ? extendedFormatError(error, options.extendedErrors) - : defaultFormatError(error) + : defaultFormatError(error); // If the user wants to see the error’s stack, let’s add it to the // formatted error. @@ -183,20 +183,20 @@ export default function createPostGraphileHttpRequestHandler( formattedError.stack = error.stack != null && options.showErrorStack === 'json' ? error.stack.split('\n') - : error.stack + : error.stack; - return formattedError - } + return formattedError; + }; - const DEFAULT_HANDLE_ERRORS = (errors: Array) => errors.map(formatError) - const handleErrors = options.handleErrors || DEFAULT_HANDLE_ERRORS + const DEFAULT_HANDLE_ERRORS = (errors: Array) => errors.map(formatError); + const handleErrors = options.handleErrors || DEFAULT_HANDLE_ERRORS; function convertKoaBodyParserToConnect(req: any, _res: any, next: any): any { if (req._koaCtx && req._koaCtx.request && req._koaCtx.request.body) { - req._body = true - req.body = req._koaCtx.request.body + req._body = true; + req.body = req._koaCtx.request.body; } - next() + next(); } // Define a list of middlewares that will get run before our request handler. @@ -213,7 +213,7 @@ export default function createPostGraphileHttpRequestHandler( bodyParser.urlencoded({ extended: false }), // Parse `application/graphql` content type bodies as text. bodyParser.text({ type: 'application/graphql' }), - ] + ]; // We'll turn this into one function now so it can be better JIT optimised const bodyParserMiddlewaresComposed = bodyParserMiddlewares.reduce( @@ -224,26 +224,26 @@ export default function createPostGraphileHttpRequestHandler( return (req, res, next) => { parent(req, res, error => { if (error) { - return next(error) + return next(error); } - fn(req, res, next) - }) - } + fn(req, res, next); + }); + }; }, (_req: IncomingMessage, _res: ServerResponse, next: (err?: Error) => void) => next(), - ) + ); // And we really want that function to be await-able const parseBody = (req: IncomingMessage, res: ServerResponse) => new Promise((resolve, reject) => { bodyParserMiddlewaresComposed(req, res, (error: Error) => { if (error) { - reject(error) + reject(error); } else { - resolve() + resolve(); } - }) - }) + }); + }); // Takes the original GraphiQL HTML file and replaces the default config object. const graphiqlHtml = origGraphiqlHtml.then(html => @@ -253,79 +253,79 @@ export default function createPostGraphileHttpRequestHandler( options.watchPg ? "'/_postgraphile/stream'" : 'null' }}`, ), - ) + ); - const withPostGraphileContextFromReqRes = withPostGraphileContextFromReqResGenerator(options) + const withPostGraphileContextFromReqRes = withPostGraphileContextFromReqResGenerator(options); const staticValidationRules = pluginHook('postgraphile:validationRules:static', specifiedRules, { options, httpError, - }) + }); // Typically clients use static queries, so we can cache the parse and // validate stages for when we see the same query again. Limit the store size // to 50MB-worth of queries (or queryCacheMaxSize) so it doesn't consume too // much RAM. - const SHA1_BASE64_LENGTH = 28 + const SHA1_BASE64_LENGTH = 28; interface CacheEntry { - queryDocumentAst: DocumentNode - validationErrors: Array - length: number + queryDocumentAst: DocumentNode; + validationErrors: Array; + length: number; } const queryCache = LRU({ max: queryCacheMaxSize, length: (n: CacheEntry, _key: string) => n.length + SHA1_BASE64_LENGTH, - }) + }); - let lastGqlSchema: GraphQLSchema + let lastGqlSchema: GraphQLSchema; const parseQuery = ( gqlSchema: GraphQLSchema, queryString: string, ): { - queryDocumentAst: DocumentNode - validationErrors: Array + queryDocumentAst: DocumentNode; + validationErrors: Array; } => { if (gqlSchema !== lastGqlSchema) { - queryCache.reset() - lastGqlSchema = gqlSchema + queryCache.reset(); + lastGqlSchema = gqlSchema; } // Only cache queries that are less than 100kB, we don't want DOS attacks // attempting to exhaust our memory. - const canCache = queryString.length < 100000 + const canCache = queryString.length < 100000; - const hash = canCache ? calculateQueryHash(queryString) : null - const result = canCache ? queryCache.get(hash!) : null + const hash = canCache ? calculateQueryHash(queryString) : null; + const result = canCache ? queryCache.get(hash!) : null; if (result) { - return result + return result; } else { - const source = new Source(queryString, 'GraphQL Http Request') - let queryDocumentAst + const source = new Source(queryString, 'GraphQL Http Request'); + let queryDocumentAst; // Catch an errors while parsing so that we can set the `statusCode` to // 400. Otherwise we don’t need to parse this way. try { - queryDocumentAst = parseGraphql(source) + queryDocumentAst = parseGraphql(source); } catch (error) { - error.statusCode = 400 - throw error + error.statusCode = 400; + throw error; } - if (debugRequest.enabled) debugRequest('GraphQL query is parsed.') + if (debugRequest.enabled) debugRequest('GraphQL query is parsed.'); // Validate our GraphQL query using given rules. - const validationErrors = validateGraphql(gqlSchema, queryDocumentAst, staticValidationRules) + const validationErrors = validateGraphql(gqlSchema, queryDocumentAst, staticValidationRules); const cacheResult: CacheEntry = { queryDocumentAst, validationErrors, length: queryString.length, - } + }; if (canCache) { - queryCache.set(hash!, cacheResult) + queryCache.set(hash!, cacheResult); } - return cacheResult + return cacheResult; } - } + }; /** * The actual request handler. It’s an async function so it will return a @@ -345,9 +345,9 @@ export default function createPostGraphileHttpRequestHandler( options, res, next, - }) + }); if (req == null) { - return + return; } // Add our CORS headers to be good web citizens (there are perf @@ -355,10 +355,10 @@ export default function createPostGraphileHttpRequestHandler( // // Always enable CORS when developing PostGraphile because GraphiQL will be // on port 5783. - if (options.enableCors || POSTGRAPHILE_ENV === 'development') addCORSHeaders(res) + if (options.enableCors || POSTGRAPHILE_ENV === 'development') addCORSHeaders(res); - const { pathname = '' } = parseUrl(req) || {} - const isGraphqlRoute = pathname === graphqlRoute + const { pathname = '' } = parseUrl(req) || {}; + const isGraphqlRoute = pathname === graphqlRoute; // ======================================================================== // Serve GraphiQL and Related Assets @@ -374,25 +374,25 @@ export default function createPostGraphileHttpRequestHandler( if (pathname === '/favicon.ico') { // If this is the wrong method, we should let the client know. if (!(req.method === 'GET' || req.method === 'HEAD')) { - res.statusCode = req.method === 'OPTIONS' ? 200 : 405 - res.setHeader('Allow', 'GET, HEAD, OPTIONS') - res.end() - return + res.statusCode = req.method === 'OPTIONS' ? 200 : 405; + res.setHeader('Allow', 'GET, HEAD, OPTIONS'); + res.end(); + return; } // Otherwise we are good and should pipe the favicon to the browser. - res.statusCode = 200 - res.setHeader('Cache-Control', 'public, max-age=86400') - res.setHeader('Content-Type', 'image/x-icon') + res.statusCode = 200; + res.setHeader('Cache-Control', 'public, max-age=86400'); + res.setHeader('Content-Type', 'image/x-icon'); // End early if the method is `HEAD`. if (req.method === 'HEAD') { - res.end() - return + res.end(); + return; } - res.end(await favicon) - return + res.end(await favicon); + return; } // ====================================================================== @@ -404,45 +404,45 @@ export default function createPostGraphileHttpRequestHandler( if (pathname.startsWith('/_postgraphile/graphiql/')) { // If using the incorrect method, let the user know. if (!(req.method === 'GET' || req.method === 'HEAD')) { - res.statusCode = req.method === 'OPTIONS' ? 200 : 405 - res.setHeader('Allow', 'GET, HEAD, OPTIONS') - res.end() - return + res.statusCode = req.method === 'OPTIONS' ? 200 : 405; + res.setHeader('Allow', 'GET, HEAD, OPTIONS'); + res.end(); + return; } // Gets the asset path (the path name with the PostGraphile prefix // stripped off) and turns it into a real filesystem path const assetPath = resolvePath( joinPath(graphiqlDirectory, pathname.slice('/_postgraphile/graphiql/'.length)), - ) + ); // Figures out the relative path for assetPath within graphiqlDirectory // so we can correctly filter 'index.html' and 'asset-manifest.json' - const assetPathRelative = relativePath(graphiqlDirectory, assetPath) + const assetPathRelative = relativePath(graphiqlDirectory, assetPath); // Block any attempts at path traversal issues if ( assetPath.substr(0, graphiqlDirectory.length) !== graphiqlDirectory || assetPathRelative.substr(0, 2) === '..' ) { - res.statusCode = 403 - res.end() - return + res.statusCode = 403; + res.end(); + return; } // Don’t allow certain files generated by `create-react-app` to be // inspected. if (assetPathRelative === 'index.html' || assetPathRelative === 'asset-manifest.json') { - res.statusCode = 404 - res.end() - return + res.statusCode = 404; + res.end(); + return; } // Sends the asset at this path. Defaults to a `statusCode` of 200. - res.statusCode = 200 - const koaCtx = (req as object)['_koaCtx'] + res.statusCode = 200; + const koaCtx = (req as object)['_koaCtx']; if (koaCtx) { - koaCtx.compress = false + koaCtx.compress = false; } await new Promise((resolve, reject) => { sendFile(req, assetPathRelative, { @@ -452,9 +452,9 @@ export default function createPostGraphileHttpRequestHandler( }) .on('end', resolve) .on('error', reject) - .pipe(res) - }) - return + .pipe(res); + }); + return; } // ====================================================================== @@ -464,12 +464,12 @@ export default function createPostGraphileHttpRequestHandler( // Setup an event stream so we can broadcast events to graphiql, etc. if (pathname === '/_postgraphile/stream') { if (!options.watchPg || req.headers.accept !== 'text/event-stream') { - res.statusCode = 405 - res.end() - return + res.statusCode = 405; + res.end(); + return; } - setupServerSentEvents(req, res, options) - return + setupServerSentEvents(req, res, options); + return; } // ====================================================================== @@ -480,71 +480,71 @@ export default function createPostGraphileHttpRequestHandler( if (pathname === graphiqlRoute) { // If we are developing PostGraphile, instead just redirect. if (POSTGRAPHILE_ENV === 'development') { - res.statusCode = 302 - res.setHeader('Location', 'http://localhost:5783') - res.end() - return + res.statusCode = 302; + res.setHeader('Location', 'http://localhost:5783'); + res.end(); + return; } // If using the incorrect method, let the user know. if (!(req.method === 'GET' || req.method === 'HEAD')) { - res.statusCode = req.method === 'OPTIONS' ? 200 : 405 - res.setHeader('Allow', 'GET, HEAD, OPTIONS') - res.end() - return + res.statusCode = req.method === 'OPTIONS' ? 200 : 405; + res.setHeader('Allow', 'GET, HEAD, OPTIONS'); + res.end(); + return; } - res.statusCode = 200 - res.setHeader('Content-Type', 'text/html; charset=utf-8') + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html; charset=utf-8'); // End early if the method is `HEAD`. if (req.method === 'HEAD') { - res.end() - return + res.end(); + return; } // Actually renders GraphiQL. - res.end(await graphiqlHtml) - return + res.end(await graphiqlHtml); + return; } } // Don’t handle any requests if this is not the correct route. - if (!isGraphqlRoute) return next() + if (!isGraphqlRoute) return next(); // ======================================================================== // Execute GraphQL Queries // ======================================================================== // If we didn’t call `next` above, all requests will return 200 by default! - res.statusCode = 200 + res.statusCode = 200; if (options.watchPg) { // Inform GraphiQL and other clients that they can subscribe to events // (such as the schema being updated) at the following URL - res.setHeader('X-GraphQL-Event-Stream', '/_postgraphile/stream') + res.setHeader('X-GraphQL-Event-Stream', '/_postgraphile/stream'); } // Don’t execute our GraphQL stuffs for `OPTIONS` requests. if (req.method === 'OPTIONS') { - res.statusCode = 200 - res.end() - return + res.statusCode = 200; + res.end(); + return; } // The `result` will be used at the very end in our `finally` block. // Statements inside the `try` will assign to `result` when they get // a result. We also keep track of `params`. - let paramsList: any + let paramsList: any; let results: Array<{ - data?: any - errors?: Array - statusCode?: number - }> = [] - const queryTimeStart = !options.disableQueryLog && process.hrtime() - let pgRole: string + data?: any; + errors?: Array; + statusCode?: number; + }> = []; + const queryTimeStart = !options.disableQueryLog && process.hrtime(); + let pgRole: string; - if (debugRequest.enabled) debugRequest('GraphQL query request has begun.') - let returnArray = false + if (debugRequest.enabled) debugRequest('GraphQL query request has begun.'); + let returnArray = false; // This big `try`/`catch`/`finally` block represents the execution of our // GraphQL query. All errors thrown in this block will be returned to the @@ -552,7 +552,7 @@ export default function createPostGraphileHttpRequestHandler( try { // First thing we need to do is get the GraphQL schema for this request. // It should never really change unless we are in watch mode. - const gqlSchema = await getGqlSchema() + const gqlSchema = await getGqlSchema(); // Note that we run our middleware after we make sure we are on the // correct route. This is so that if our middleware modifies the `req` or @@ -560,12 +560,12 @@ export default function createPostGraphileHttpRequestHandler( // // We also run our middleware inside the `try` so that we get the GraphQL // error reporting style for syntax errors. - await parseBody(req, res) + await parseBody(req, res); // If this is not one of the correct methods, throw an error. if (req.method !== 'POST') { - res.setHeader('Allow', 'POST, OPTIONS') - throw httpError(405, 'Only `POST` requests are allowed.') + res.setHeader('Allow', 'POST, OPTIONS'); + throw httpError(405, 'Only `POST` requests are allowed.'); } // Get the parameters we will use to run a GraphQL request. `params` may @@ -575,25 +575,28 @@ export default function createPostGraphileHttpRequestHandler( // - `variables`: An optional JSON object containing GraphQL variables. // - `operationName`: The optional name of the GraphQL operation we will // be executing. - const body: string | object = (req as any).body - paramsList = typeof body === 'string' ? { query: body } : body + const body: string | object = (req as any).body; + paramsList = typeof body === 'string' ? { query: body } : body; // Validate our paramsList object a bit. if (paramsList == null) - throw httpError(400, 'Must provide an object parameters, not nullish value.') + throw httpError(400, 'Must provide an object parameters, not nullish value.'); if (typeof paramsList !== 'object') - throw httpError(400, `Expected parameter object, not value of type '${typeof paramsList}'.`) + throw httpError( + 400, + `Expected parameter object, not value of type '${typeof paramsList}'.`, + ); if (Array.isArray(paramsList)) { if (!options.enableQueryBatching) { throw httpError( 501, 'Batching queries as an array is currently unsupported. Please provide a single query object.', - ) + ); } else { - returnArray = true + returnArray = true; } } else { - paramsList = [paramsList] + paramsList = [paramsList]; } paramsList = pluginHook('postgraphile:httpParamsList', paramsList, { options, @@ -601,14 +604,14 @@ export default function createPostGraphileHttpRequestHandler( res, returnArray, httpError, - }) + }); results = await Promise.all( paramsList.map(async (params: any) => { - let queryDocumentAst: DocumentNode - let result: any - const meta = {} + let queryDocumentAst: DocumentNode; + let result: any; + const meta = {}; try { - if (!params.query) throw httpError(400, 'Must provide a query string.') + if (!params.query) throw httpError(400, 'Must provide a query string.'); // If variables is a string, we assume it is a JSON string and that it // needs to be parsed. @@ -616,31 +619,34 @@ export default function createPostGraphileHttpRequestHandler( // If variables is just an empty string, we should set it to null and // ignore it. if (params.variables === '') { - params.variables = null + params.variables = null; } else { // Otherwise, let us try to parse it as JSON. try { - params.variables = JSON.parse(params.variables) + params.variables = JSON.parse(params.variables); } catch (error) { - error.statusCode = 400 - throw error + error.statusCode = 400; + throw error; } } } // Throw an error if `variables` is not an object. if (params.variables != null && typeof params.variables !== 'object') - throw httpError(400, `Variables must be an object, not '${typeof params.variables}'.`) + throw httpError( + 400, + `Variables must be an object, not '${typeof params.variables}'.`, + ); // Throw an error if `operationName` is not a string. if (params.operationName != null && typeof params.operationName !== 'string') throw httpError( 400, `Operation name must be a string, not '${typeof params.operationName}'.`, - ) + ); - let validationErrors: Array - ;({ queryDocumentAst, validationErrors } = parseQuery(gqlSchema, params.query)) + let validationErrors: Array; + ({ queryDocumentAst, validationErrors } = parseQuery(gqlSchema, params.query)); if (validationErrors.length === 0) { // You are strongly encouraged to use @@ -653,18 +659,22 @@ export default function createPostGraphileHttpRequestHandler( variables: params.variables, operationName: params.operationName, meta, - }) + }); if (moreValidationRules.length) { - validationErrors = validateGraphql(gqlSchema, queryDocumentAst, moreValidationRules) + validationErrors = validateGraphql( + gqlSchema, + queryDocumentAst, + moreValidationRules, + ); } } // If we have some validation errors, don’t execute the query. Instead // send the errors to the client with a `400` code. if (validationErrors.length > 0) { - result = { errors: validationErrors, statusCode: 400 } + result = { errors: validationErrors, statusCode: 400 }; } else { - if (debugRequest.enabled) debugRequest('GraphQL query is validated.') + if (debugRequest.enabled) debugRequest('GraphQL query is validated.'); // Lazily log the query. If this debugger isn’t enabled, don’t run it. if (debugGraphql.enabled) @@ -672,7 +682,7 @@ export default function createPostGraphileHttpRequestHandler( printGraphql(queryDocumentAst) .replace(/\s+/g, ' ') .trim(), - ) + ); result = await withPostGraphileContextFromReqRes( req, @@ -684,7 +694,7 @@ export default function createPostGraphileHttpRequestHandler( operationName: params.operationName, }, (graphqlContext: any) => { - pgRole = graphqlContext.pgRole + pgRole = graphqlContext.pgRole; return executeGraphql( gqlSchema, queryDocumentAst, @@ -692,27 +702,27 @@ export default function createPostGraphileHttpRequestHandler( graphqlContext, params.variables, params.operationName, - ) + ); }, - ) + ); } } catch (error) { result = { errors: [error], statusCode: error.status || error.statusCode || 500, - } + }; // If the status code is 500, let’s log our error. if (result.statusCode === 500) // tslint:disable-next-line no-console - console.error(error.stack) + console.error(error.stack); } finally { // Format our errors so the client doesn’t get the full thing. if (result && result.errors) { - result.errors = (handleErrors as any)(result.errors, req, res) + result.errors = (handleErrors as any)(result.errors, req, res); } if (!isEmpty(meta)) { - result.meta = meta + result.meta = meta; } // Log the query. If this debugger isn’t enabled, don’t run it. if ( @@ -722,10 +732,10 @@ export default function createPostGraphileHttpRequestHandler( setTimeout(() => { const prettyQuery = printGraphql(queryDocumentAst) .replace(/\s+/g, ' ') - .trim() - const errorCount = (result.errors || []).length - const timeDiff = queryTimeStart && process.hrtime(queryTimeStart) - const ms = Math.round((timeDiff[0] * 1e9 + timeDiff[1]) * 10e-7 * 100) / 100 + .trim(); + const errorCount = (result.errors || []).length; + const timeDiff = queryTimeStart && process.hrtime(queryTimeStart); + const ms = Math.round((timeDiff[0] * 1e9 + timeDiff[1]) * 10e-7 * 100) / 100; // If we have enabled the query log for the Http handler, use that. // tslint:disable-next-line no-console @@ -733,43 +743,43 @@ export default function createPostGraphileHttpRequestHandler( `${chalk[errorCount === 0 ? 'green' : 'red'](`${errorCount} error(s)`)} ${ pgRole != null ? `as ${chalk.magenta(pgRole)} ` : '' }in ${chalk.grey(`${ms}ms`)} :: ${prettyQuery}`, - ) - }, 0) + ); + }, 0); } - if (debugRequest.enabled) debugRequest('GraphQL query has been executed.') + if (debugRequest.enabled) debugRequest('GraphQL query has been executed.'); } - return result + return result; }), - ) + ); } catch (error) { // Set our status code and send the client our results! - if (res.statusCode === 200) res.statusCode = error.status || error.statusCode || 500 + if (res.statusCode === 200) res.statusCode = error.status || error.statusCode || 500; // Overwrite entire response - returnArray = false - results = [{ errors: [error] }] + returnArray = false; + results = [{ errors: [error] }]; // If the status code is 500, let’s log our error. if (res.statusCode === 500) // tslint:disable-next-line no-console - console.error(error.stack) + console.error(error.stack); } finally { // Finally, we send the client the results. if (!returnArray) { if (res.statusCode === 200 && results[0].statusCode) { - res.statusCode = results[0].statusCode! + res.statusCode = results[0].statusCode!; } - delete results[0].statusCode + delete results[0].statusCode; } - res.setHeader('Content-Type', 'application/json; charset=utf-8') + res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.end(JSON.stringify(returnArray ? results : results[0]!)) + res.end(JSON.stringify(returnArray ? results : results[0]!)); if (debugRequest.enabled) - debugRequest('GraphQL ' + (returnArray ? 'queries' : 'query') + ' request finished.') + debugRequest('GraphQL ' + (returnArray ? 'queries' : 'query') + ' request finished.'); } - } + }; /** * A polymorphic request handler that should detect what `http` framework is @@ -787,16 +797,16 @@ export default function createPostGraphileHttpRequestHandler( // `koa` middleware. if (isKoaApp(a, b)) { // Set the correct `koa` variable names… - const ctx = a as KoaContext - const next = b as (err?: Error) => Promise - return koaMiddleware(ctx, next, requestHandler) + const ctx = a as KoaContext; + const next = b as (err?: Error) => Promise; + return koaMiddleware(ctx, next, requestHandler); } else { // Set the correct `connect` style variable names. If there was no `next` // defined (likely the case if the client is using `http`) we use the // final handler. - const req = a as IncomingMessage - const res = b as ServerResponse - const next = c || finalHandler(req, res) + const req = a as IncomingMessage; + const res = b as ServerResponse; + const next = c || finalHandler(req, res); // Execute our request handler. requestHandler(req, res, next).then( @@ -806,14 +816,14 @@ export default function createPostGraphileHttpRequestHandler( }, // If the request errored out, call `next` with the error. error => next(error), - ) + ); } - } - middleware.getGraphQLSchema = getGqlSchema - middleware.formatError = formatError - middleware.pgPool = pgPool - middleware.withPostGraphileContextFromReqRes = withPostGraphileContextFromReqRes - return middleware as HttpRequestHandler + }; + middleware.getGraphQLSchema = getGqlSchema; + middleware.formatError = formatError; + middleware.pgPool = pgPool; + middleware.withPostGraphileContextFromReqRes = withPostGraphileContextFromReqRes; + return middleware as HttpRequestHandler; } /** @@ -829,8 +839,8 @@ export default function createPostGraphileHttpRequestHandler( * [1]: http://www.html5rocks.com/static/images/cors_server_flowchart.png */ function addCORSHeaders(res: ServerResponse): void { - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Request-Method', 'HEAD, GET, POST') + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Request-Method', 'HEAD, GET, POST'); res.setHeader( 'Access-Control-Allow-Headers', [ @@ -846,11 +856,11 @@ function addCORSHeaders(res: ServerResponse): void { 'Content-Type', 'Content-Length', ].join(', '), - ) + ); } function createBadAuthorizationHeaderError(): httpError.HttpError { - return httpError(400, 'Authorization header is not of the correct bearer scheme format.') + return httpError(400, 'Authorization header is not of the correct bearer scheme format.'); } /** @@ -867,7 +877,7 @@ function createBadAuthorizationHeaderError(): httpError.HttpError { * * @private */ -const authorizationBearerRex = /^\s*bearer\s+([a-z0-9\-._~+/]+=*)\s*$/i +const authorizationBearerRex = /^\s*bearer\s+([a-z0-9\-._~+/]+=*)\s*$/i; /** * Gets the JWT token from the Http request’s headers. Specifically the @@ -880,18 +890,18 @@ const authorizationBearerRex = /^\s*bearer\s+([a-z0-9\-._~+/]+=*)\s*$/i * @returns {string | null} */ function getJwtToken(request: IncomingMessage): string | null { - const { authorization } = request.headers - if (Array.isArray(authorization)) throw createBadAuthorizationHeaderError() + const { authorization } = request.headers; + if (Array.isArray(authorization)) throw createBadAuthorizationHeaderError(); // If there was no authorization header, just return null. - if (authorization == null) return null + if (authorization == null) return null; - const match = authorizationBearerRex.exec(authorization) + const match = authorizationBearerRex.exec(authorization); // If we did not match the authorization header with our expected format, // throw a 400 error. - if (!match) throw createBadAuthorizationHeaderError() + if (!match) throw createBadAuthorizationHeaderError(); // Return the token from our match. - return match[1] + return match[1]; } diff --git a/src/postgraphile/http/koaMiddleware.ts b/src/postgraphile/http/koaMiddleware.ts index 4941cc1dd4..87ba5d3b82 100644 --- a/src/postgraphile/http/koaMiddleware.ts +++ b/src/postgraphile/http/koaMiddleware.ts @@ -1,8 +1,8 @@ /* tslint:disable:no-any */ -import { IncomingMessage, ServerResponse } from 'http' -import { Context as KoaContext } from 'koa' +import { IncomingMessage, ServerResponse } from 'http'; +import { Context as KoaContext } from 'koa'; -export const isKoaApp = (a: any, b: any) => a.req && a.res && typeof b === 'function' +export const isKoaApp = (a: any, b: any) => a.req && a.res && typeof b === 'function'; export const middleware = async ( ctx: KoaContext, @@ -14,32 +14,32 @@ export const middleware = async ( ) => Promise, ) => { // Hack the req object so we can get back to ctx - ;(ctx.req as object)['_koaCtx'] = ctx + (ctx.req as object)['_koaCtx'] = ctx; // Hack the end function to instead write the response body. // (This shouldn't be called by any PostGraphile code.) - const oldEnd = ctx.res.end - ;(ctx.res as object)['end'] = (body: any, cb: () => void) => { + const oldEnd = ctx.res.end; + (ctx.res as object)['end'] = (body: any, cb: () => void) => { // Setting ctx.response.body changes koa's status implicitly, unless it // already has one set: - ctx.status = ctx.res.statusCode - ctx.response.body = body === undefined ? '' : body + ctx.status = ctx.res.statusCode; + ctx.response.body = body === undefined ? '' : body; if (typeof cb === 'function') { - cb() + cb(); } - } + }; // Execute our request handler. If an error is thrown, we don’t call // `next` with an error. Instead we return the promise and let `koa` // handle the error. - let result + let result; try { - result = await requestHandler(ctx.req, ctx.res, next) + result = await requestHandler(ctx.req, ctx.res, next); } finally { - ;(ctx.res as object)['end'] = oldEnd + (ctx.res as object)['end'] = oldEnd; if (ctx.res.statusCode && ctx.res.statusCode !== 200) { - ctx.response.status = ctx.res.statusCode + ctx.response.status = ctx.res.statusCode; } } - return result -} + return result; +}; diff --git a/src/postgraphile/http/setupServerSentEvents.ts b/src/postgraphile/http/setupServerSentEvents.ts index 720b507f88..920bea94ed 100644 --- a/src/postgraphile/http/setupServerSentEvents.ts +++ b/src/postgraphile/http/setupServerSentEvents.ts @@ -1,64 +1,64 @@ /* tslint:disable:no-any */ -import { PassThrough } from 'stream' -import { IncomingMessage, ServerResponse } from 'http' -import { PostGraphile } from '../../interfaces' -import ICreateRequestHandler = PostGraphile.ICreateRequestHandler +import { PassThrough } from 'stream'; +import { IncomingMessage, ServerResponse } from 'http'; +import { PostGraphile } from '../../interfaces'; +import ICreateRequestHandler = PostGraphile.ICreateRequestHandler; export default function setupServerSentEvents( req: IncomingMessage, res: ServerResponse, options: ICreateRequestHandler, ): void { - const { _emitter } = options + const { _emitter } = options; // Making sure these options are set. - req.socket.setTimeout(0) - req.socket.setNoDelay(true) - req.socket.setKeepAlive(true) + req.socket.setTimeout(0); + req.socket.setNoDelay(true); + req.socket.setKeepAlive(true); // Set headers for Server-Sent Events. - res.statusCode = 200 - res.setHeader('Content-Type', 'text/event-stream') - res.setHeader('Cache-Control', 'no-cache') - res.setHeader('Connection', 'keep-alive') - const koaCtx = (req as object)['_koaCtx'] - const isKoa = !!koaCtx - const stream = isKoa ? new PassThrough() : null + res.statusCode = 200; + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + const koaCtx = (req as object)['_koaCtx']; + const isKoa = !!koaCtx; + const stream = isKoa ? new PassThrough() : null; if (isKoa) { - koaCtx.response.body = stream - koaCtx.compress = false + koaCtx.response.body = stream; + koaCtx.compress = false; } const sse = (str: string) => { if (isKoa) { - stream!.write(str) + stream!.write(str); } else { - res.write(str) + res.write(str); // support running within the compression middleware. // https://github.com/expressjs/compression#server-sent-events - if (typeof (res as any).flushHeaders === 'function') (res as any).flushHeaders() + if (typeof (res as any).flushHeaders === 'function') (res as any).flushHeaders(); } - } + }; // Notify client that connection is open. - sse('event: open\n\n') + sse('event: open\n\n'); // Setup listeners. - const schemaChangedCb = () => sse('event: change\ndata: schema\n\n') + const schemaChangedCb = () => sse('event: change\ndata: schema\n\n'); - if (options.watchPg) _emitter.on('schemas:changed', schemaChangedCb) + if (options.watchPg) _emitter.on('schemas:changed', schemaChangedCb); // Clean up when connection closes. const cleanup = () => { if (stream) { - stream.end() + stream.end(); } else { - res.end() + res.end(); } - _emitter.removeListener('schemas:changed', schemaChangedCb) - } - req.on('close', cleanup) - req.on('finish', cleanup) - req.on('error', cleanup) + _emitter.removeListener('schemas:changed', schemaChangedCb); + }; + req.on('close', cleanup); + req.on('finish', cleanup); + req.on('error', cleanup); } diff --git a/src/postgraphile/index.ts b/src/postgraphile/index.ts index 45e10b824f..4d4479fcea 100644 --- a/src/postgraphile/index.ts +++ b/src/postgraphile/index.ts @@ -1,5 +1,5 @@ -import postgraphile from './postgraphile' -import { createPostGraphileSchema, watchPostGraphileSchema } from 'postgraphile-core' -import withPostGraphileContext from './withPostGraphileContext' +import postgraphile from './postgraphile'; +import { createPostGraphileSchema, watchPostGraphileSchema } from 'postgraphile-core'; +import withPostGraphileContext from './withPostGraphileContext'; -export { postgraphile, createPostGraphileSchema, watchPostGraphileSchema, withPostGraphileContext } +export { postgraphile, createPostGraphileSchema, watchPostGraphileSchema, withPostGraphileContext }; diff --git a/src/postgraphile/pluginHook.ts b/src/postgraphile/pluginHook.ts index 28927f107b..e4d2a50a45 100644 --- a/src/postgraphile/pluginHook.ts +++ b/src/postgraphile/pluginHook.ts @@ -1,82 +1,82 @@ -import { AddFlagFn } from './cli' -import { Server } from 'http' -import { PostGraphile } from '../interfaces' -import { WithPostGraphileContextFn } from './withPostGraphileContext' -import HttpRequestHandler = PostGraphile.HttpRequestHandler -import PostGraphileOptions = PostGraphile.PostGraphileOptions +import { AddFlagFn } from './cli'; +import { Server } from 'http'; +import { PostGraphile } from '../interfaces'; +import { WithPostGraphileContextFn } from './withPostGraphileContext'; +import HttpRequestHandler = PostGraphile.HttpRequestHandler; +import PostGraphileOptions = PostGraphile.PostGraphileOptions; -export type HookFn = (arg: T, context: {}) => T -export type PluginHookFn = (hookName: string, argument: T, context?: {}) => T +export type HookFn = (arg: T, context: {}) => T; +export type PluginHookFn = (hookName: string, argument: T, context?: {}) => T; export interface PostGraphilePlugin { - pluginHook?: HookFn - 'cli:flags:add:standard'?: HookFn - 'cli:flags:add:schema'?: HookFn - 'cli:flags:add:errorHandling'?: HookFn - 'cli:flags:add:plugins'?: HookFn - 'cli:flags:add:noServer'?: HookFn - 'cli:flags:add:webserver'?: HookFn - 'cli:flags:add:jwt'?: HookFn - 'cli:flags:add'?: HookFn - 'cli:flags:add:deprecated'?: HookFn - 'cli:flags:add:workarounds'?: HookFn + pluginHook?: HookFn; + 'cli:flags:add:standard'?: HookFn; + 'cli:flags:add:schema'?: HookFn; + 'cli:flags:add:errorHandling'?: HookFn; + 'cli:flags:add:plugins'?: HookFn; + 'cli:flags:add:noServer'?: HookFn; + 'cli:flags:add:webserver'?: HookFn; + 'cli:flags:add:jwt'?: HookFn; + 'cli:flags:add'?: HookFn; + 'cli:flags:add:deprecated'?: HookFn; + 'cli:flags:add:workarounds'?: HookFn; - 'cli:server:middleware'?: HookFn - 'cli:server:created'?: HookFn + 'cli:server:middleware'?: HookFn; + 'cli:server:created'?: HookFn; - 'postgraphile:options'?: HookFn - withPostGraphileContext?: HookFn + 'postgraphile:options'?: HookFn; + withPostGraphileContext?: HookFn; } -type HookName = keyof PostGraphilePlugin +type HookName = keyof PostGraphilePlugin; -const identityHook = (input: T): T => input -const identityPluginHook = (_hookName: HookName, input: T): T => input +const identityHook = (input: T): T => input; +const identityPluginHook = (_hookName: HookName, input: T): T => input; function contextIsSame(context1: {}, context2: {}): boolean { // Shortcut if obvious if (context1 === context2) { - return true + return true; } // Blacklist approach from now on - const keys1 = Object.keys(context1) - const keys2 = Object.keys(context2) + const keys1 = Object.keys(context1); + const keys2 = Object.keys(context2); if (keys1.length !== keys2.length) { - return false + return false; } // tslint:disable-next-line one-variable-per-declaration for (let i = 0, l = keys1.length; i < l; i++) { - const key = keys1[i] + const key = keys1[i]; if (context1[key] !== context2[key]) { - return false + return false; } if (keys2.indexOf(key) === -1) { - return false + return false; } } - return true + return true; } // Caches the last value of the hook, in case it's called with exactly the same // arguments again. function memoizeHook(hook: HookFn): HookFn { let lastCall: { - argument: T - context: {} - result: T - } | null = null + argument: T; + context: {}; + result: T; + } | null = null; return (argument: T, context: {}): T => { if (lastCall && lastCall.argument === argument && contextIsSame(lastCall.context, context)) { - return lastCall.result + return lastCall.result; } else { - const result = hook(argument, context) + const result = hook(argument, context); lastCall = { argument, context, result, - } - return result + }; + return result; } - } + }; } function makeHook(plugins: Array, hookName: HookName): HookFn { @@ -84,33 +84,33 @@ function makeHook(plugins: Array, hookName: HookName): Ho plugins.reduce((previousHook: HookFn, plugin: {}) => { if (typeof plugin[hookName] === 'function') { return (argument: T, context: {}) => { - return plugin[hookName](previousHook(argument, context), context) - } + return plugin[hookName](previousHook(argument, context), context); + }; } else { - return previousHook + return previousHook; } }, identityHook), - ) + ); } export function makePluginHook(plugins: Array): PluginHookFn { - const hooks = {} - const emptyObject = {} // caching this makes memoization faster when no context is needed + const hooks = {}; + const emptyObject = {}; // caching this makes memoization faster when no context is needed function rawPluginHook(hookName: HookName, argument: T, context: {} = emptyObject): T { if (!hooks[hookName]) { - hooks[hookName] = makeHook(plugins, hookName) + hooks[hookName] = makeHook(plugins, hookName); } - return hooks[hookName](argument, context) + return hooks[hookName](argument, context); } - const pluginHook: PluginHookFn = rawPluginHook('pluginHook', rawPluginHook, {}) - return pluginHook + const pluginHook: PluginHookFn = rawPluginHook('pluginHook', rawPluginHook, {}); + return pluginHook; } export function pluginHookFromOptions(options: PostGraphileOptions): PluginHookFn { if (typeof options.pluginHook === 'function') { - return options.pluginHook + return options.pluginHook; } else { - return identityPluginHook + return identityPluginHook; } } diff --git a/src/postgraphile/postgraphile.ts b/src/postgraphile/postgraphile.ts index bebf4bf89b..579e475e7e 100644 --- a/src/postgraphile/postgraphile.ts +++ b/src/postgraphile/postgraphile.ts @@ -1,20 +1,20 @@ -import { Pool, PoolConfig } from 'pg' -import { parse as parsePgConnectionString } from 'pg-connection-string' -import { GraphQLSchema } from 'graphql' -import { EventEmitter } from 'events' -import { createPostGraphileSchema, watchPostGraphileSchema } from 'postgraphile-core' -import createPostGraphileHttpRequestHandler from './http/createPostGraphileHttpRequestHandler' -import exportPostGraphileSchema from './schema/exportPostGraphileSchema' -import { pluginHookFromOptions } from './pluginHook' -import { PostGraphile } from '../interfaces' - -import PostGraphileOptions = PostGraphile.PostGraphileOptions -import mixed = PostGraphile.mixed -import HttpRequestHandler = PostGraphile.HttpRequestHandler +import { Pool, PoolConfig } from 'pg'; +import { parse as parsePgConnectionString } from 'pg-connection-string'; +import { GraphQLSchema } from 'graphql'; +import { EventEmitter } from 'events'; +import { createPostGraphileSchema, watchPostGraphileSchema } from 'postgraphile-core'; +import createPostGraphileHttpRequestHandler from './http/createPostGraphileHttpRequestHandler'; +import exportPostGraphileSchema from './schema/exportPostGraphileSchema'; +import { pluginHookFromOptions } from './pluginHook'; +import { PostGraphile } from '../interfaces'; + +import PostGraphileOptions = PostGraphile.PostGraphileOptions; +import mixed = PostGraphile.mixed; +import HttpRequestHandler = PostGraphile.HttpRequestHandler; export interface PostgraphileSchemaBuilder { - _emitter: EventEmitter - getGraphQLSchema: () => Promise + _emitter: EventEmitter; + getGraphQLSchema: () => Promise; } /** @@ -27,72 +27,72 @@ export function getPostgraphileSchemaBuilder( schema: string | Array, incomingOptions: PostGraphileOptions, ): PostgraphileSchemaBuilder { - const pluginHook = pluginHookFromOptions(incomingOptions) + const pluginHook = pluginHookFromOptions(incomingOptions); const options = pluginHook('postgraphile:options', incomingOptions, { pgPool, schema, - }) + }); // Check for a jwtSecret without a jwtPgTypeIdentifier // a secret without a token identifier prevents JWT creation if (options.jwtSecret && !options.jwtPgTypeIdentifier) { // tslint:disable-next-line no-console console.warn( 'WARNING: jwtSecret provided, however jwtPgTypeIdentifier (token identifier) not provided.', - ) + ); } if (options.handleErrors && (options.extendedErrors || options.showErrorStack)) { - throw new Error(`You cannot combine 'handleErrors' with the other error options`) + throw new Error(`You cannot combine 'handleErrors' with the other error options`); } // Creates the Postgres schemas array. - const pgSchemas: Array = Array.isArray(schema) ? schema : [schema] + const pgSchemas: Array = Array.isArray(schema) ? schema : [schema]; - const _emitter = new EventEmitter() + const _emitter = new EventEmitter(); // Creates a promise which will resolve to a GraphQL schema. Connects a // client from our pool to introspect the database. // // This is not a constant because when we are in watch mode, we want to swap // out the `gqlSchema`. - let gqlSchema: GraphQLSchema - const gqlSchemaPromise: Promise = createGqlSchema() + let gqlSchema: GraphQLSchema; + const gqlSchemaPromise: Promise = createGqlSchema(); return { _emitter, getGraphQLSchema: () => Promise.resolve(gqlSchema || gqlSchemaPromise), - } + }; async function createGqlSchema(): Promise { try { if (options.watchPg) { await watchPostGraphileSchema(pgPool, pgSchemas, options, (newSchema: GraphQLSchema) => { - gqlSchema = newSchema - _emitter.emit('schemas:changed') - exportGqlSchema(gqlSchema) - }) + gqlSchema = newSchema; + _emitter.emit('schemas:changed'); + exportGqlSchema(gqlSchema); + }); if (!gqlSchema) { throw new Error( "Consistency error: watchPostGraphileSchema promises to call the callback before the promise resolves; but this hasn't happened", - ) + ); } } else { - gqlSchema = await createPostGraphileSchema(pgPool, pgSchemas, options) - exportGqlSchema(gqlSchema) + gqlSchema = await createPostGraphileSchema(pgPool, pgSchemas, options); + exportGqlSchema(gqlSchema); } - return gqlSchema + return gqlSchema; } catch (error) { // If we fail to build our schema, log the error and exit the process. - return handleFatalError(error) + return handleFatalError(error); } } async function exportGqlSchema(newGqlSchema: GraphQLSchema): Promise { try { - await exportPostGraphileSchema(newGqlSchema, options) + await exportPostGraphileSchema(newGqlSchema, options); } catch (error) { // If we fail to export our schema, log the error and exit the process. - handleFatalError(error) + handleFatalError(error); } } } @@ -100,37 +100,37 @@ export default function postgraphile( poolOrConfig?: Pool | PoolConfig | string, schema?: string | Array, options?: PostGraphileOptions, -): HttpRequestHandler +): HttpRequestHandler; export default function postgraphile( poolOrConfig?: Pool | PoolConfig | string, options?: PostGraphileOptions, -): HttpRequestHandler +): HttpRequestHandler; export default function postgraphile( poolOrConfig?: Pool | PoolConfig | string, schemaOrOptions?: string | Array | PostGraphileOptions, maybeOptions?: PostGraphileOptions, ): HttpRequestHandler { - let schema: string | Array - let options: PostGraphileOptions + let schema: string | Array; + let options: PostGraphileOptions; // If the second argument is undefined, use defaults for both `schema` and // `options`. if (typeof schemaOrOptions === 'undefined') { - schema = 'public' - options = {} + schema = 'public'; + options = {}; } // If the second argument is a string or array, it is the schemas so set the // `schema` value and try to use the third argument (or a default) for // `options`. else if (typeof schemaOrOptions === 'string' || Array.isArray(schemaOrOptions)) { - schema = schemaOrOptions - options = maybeOptions || {} + schema = schemaOrOptions; + options = maybeOptions || {}; } // Otherwise the second argument is the options so set `schema` to the // default and `options` to the second argument. else { - schema = 'public' - options = schemaOrOptions + schema = 'public'; + options = schemaOrOptions; } // Do some things with `poolOrConfig` so that in the end, we actually get a @@ -147,25 +147,25 @@ export default function postgraphile( : // Finally, it must just be a config itself. If it is undefined, we // will just use an empty config and let the defaults take over. poolOrConfig || {}, - ) + ); - const { getGraphQLSchema, _emitter } = getPostgraphileSchemaBuilder(pgPool, schema, options) + const { getGraphQLSchema, _emitter } = getPostgraphileSchemaBuilder(pgPool, schema, options); return createPostGraphileHttpRequestHandler({ ...options, getGqlSchema: getGraphQLSchema, pgPool, _emitter, - }) + }); } function handleFatalError(error: Error): never { - process.stderr.write(`${error.stack}\n`) // console.error fails under the tests - process.exit(1) + process.stderr.write(`${error.stack}\n`); // console.error fails under the tests + process.exit(1); // `process.exit` will mean all code below it will never get called. // However, we need to return a value with type `never` here for // TypeScript. - return null as never + return null as never; } function constructorName(obj: mixed): string | null { @@ -175,19 +175,19 @@ function constructorName(obj: mixed): string | null { obj.constructor.name && String(obj.constructor.name)) || null - ) + ); } // tslint:disable-next-line no-any function quacksLikePgPool(pgConfig: any): boolean { // A diagnosis of exclusion - if (!pgConfig || typeof pgConfig !== 'object') return false + if (!pgConfig || typeof pgConfig !== 'object') return false; if (constructorName(pgConfig) !== 'Pool' && constructorName(pgConfig) !== 'BoundPool') - return false - if (!pgConfig['Client']) return false - if (!pgConfig['options']) return false - if (typeof pgConfig['connect'] !== 'function') return false - if (typeof pgConfig['end'] !== 'function') return false - if (typeof pgConfig['query'] !== 'function') return false - return true + return false; + if (!pgConfig['Client']) return false; + if (!pgConfig['options']) return false; + if (typeof pgConfig['connect'] !== 'function') return false; + if (typeof pgConfig['end'] !== 'function') return false; + if (typeof pgConfig['query'] !== 'function') return false; + return true; } diff --git a/src/postgraphile/schema/exportPostGraphileSchema.ts b/src/postgraphile/schema/exportPostGraphileSchema.ts index 91c16f5a47..dfd4cf0de0 100644 --- a/src/postgraphile/schema/exportPostGraphileSchema.ts +++ b/src/postgraphile/schema/exportPostGraphileSchema.ts @@ -1,13 +1,13 @@ -import { writeFile } from 'fs' -import { graphql, GraphQLSchema, introspectionQuery, printSchema } from 'graphql' +import { writeFile } from 'fs'; +import { graphql, GraphQLSchema, introspectionQuery, printSchema } from 'graphql'; async function writeFileAsync(path: string, contents: string): Promise { await new Promise((resolve, reject) => { writeFile(path, contents, error => { - if (error) reject(error) - else resolve() - }) - }) + if (error) reject(error); + else resolve(); + }); + }); } /** @@ -16,18 +16,18 @@ async function writeFileAsync(path: string, contents: string): Promise { export default async function exportPostGraphileSchema( schema: GraphQLSchema, options: { - exportJsonSchemaPath?: string - exportGqlSchemaPath?: string + exportJsonSchemaPath?: string; + exportGqlSchemaPath?: string; } = {}, ): Promise { // JSON version if (typeof options.exportJsonSchemaPath === 'string') { - const result = await graphql(schema, introspectionQuery) - await writeFileAsync(options.exportJsonSchemaPath, JSON.stringify(result, null, 2)) + const result = await graphql(schema, introspectionQuery); + await writeFileAsync(options.exportJsonSchemaPath, JSON.stringify(result, null, 2)); } // Schema language version if (typeof options.exportGqlSchemaPath === 'string') { - await writeFileAsync(options.exportGqlSchemaPath, printSchema(schema)) + await writeFileAsync(options.exportGqlSchemaPath, printSchema(schema)); } } diff --git a/src/postgraphile/withPostGraphileContext.ts b/src/postgraphile/withPostGraphileContext.ts index e1a865514e..910a0df287 100644 --- a/src/postgraphile/withPostGraphileContext.ts +++ b/src/postgraphile/withPostGraphileContext.ts @@ -1,43 +1,43 @@ -import createDebugger = require('debug') -import jwt = require('jsonwebtoken') -import { Pool, PoolClient } from 'pg' -import { ExecutionResult, DocumentNode, OperationDefinitionNode, Kind } from 'graphql' -import * as sql from 'pg-sql2' -import { $$pgClient } from '../postgres/inventory/pgClientFromContext' -import { pluginHookFromOptions } from './pluginHook' -import { PostGraphile } from '../interfaces' -import mixed = PostGraphile.mixed +import createDebugger = require('debug'); +import jwt = require('jsonwebtoken'); +import { Pool, PoolClient } from 'pg'; +import { ExecutionResult, DocumentNode, OperationDefinitionNode, Kind } from 'graphql'; +import * as sql from 'pg-sql2'; +import { $$pgClient } from '../postgres/inventory/pgClientFromContext'; +import { pluginHookFromOptions } from './pluginHook'; +import { PostGraphile } from '../interfaces'; +import mixed = PostGraphile.mixed; const undefinedIfEmpty = (o?: Array | string): undefined | Array | string => - o && o.length ? o : undefined + o && o.length ? o : undefined; export type WithPostGraphileContextFn = ( options: { - pgPool: Pool - jwtToken?: string - jwtSecret?: string - jwtAudiences?: Array - jwtRole: Array - jwtVerifyOptions?: jwt.VerifyOptions - pgDefaultRole?: string - pgSettings?: { [key: string]: mixed } + pgPool: Pool; + jwtToken?: string; + jwtSecret?: string; + jwtAudiences?: Array; + jwtRole: Array; + jwtVerifyOptions?: jwt.VerifyOptions; + pgDefaultRole?: string; + pgSettings?: { [key: string]: mixed }; }, callback: (context: mixed) => Promise, -) => Promise +) => Promise; const withDefaultPostGraphileContext: WithPostGraphileContextFn = async ( options: { - pgPool: Pool - jwtToken?: string - jwtSecret?: string - jwtAudiences?: Array - jwtRole: Array - jwtVerifyOptions?: jwt.VerifyOptions - pgDefaultRole?: string - pgSettings?: { [key: string]: mixed } - queryDocumentAst?: DocumentNode - operationName?: string - pgForceTransaction?: boolean + pgPool: Pool; + jwtToken?: string; + jwtSecret?: string; + jwtAudiences?: Array; + jwtRole: Array; + jwtVerifyOptions?: jwt.VerifyOptions; + pgDefaultRole?: string; + pgSettings?: { [key: string]: mixed }; + queryDocumentAst?: DocumentNode; + operationName?: string; + pgForceTransaction?: boolean; }, callback: (context: mixed) => Promise, ): Promise => { @@ -53,25 +53,25 @@ const withDefaultPostGraphileContext: WithPostGraphileContextFn = async ( queryDocumentAst, operationName, pgForceTransaction, - } = options + } = options; - let operation: OperationDefinitionNode | void + let operation: OperationDefinitionNode | void; if (!pgForceTransaction && queryDocumentAst) { // tslint:disable-next-line for (let i = 0, l = queryDocumentAst.definitions.length; i < l; i++) { - const definition = queryDocumentAst.definitions[i] + const definition = queryDocumentAst.definitions[i]; if (definition.kind === Kind.OPERATION_DEFINITION) { if (!operationName && operation) { - throw new Error('Multiple unnamed operations present in GraphQL query.') + throw new Error('Multiple unnamed operations present in GraphQL query.'); } else if (!operationName || (definition.name && definition.name.value === operationName)) { - operation = definition + operation = definition; } } } } // Warning: this is only set if pgForceTransaction is falsy - const operationType = operation != null ? operation.operation : null + const operationType = operation != null ? operation.operation : null; const { role: pgRole, localSettings } = await getSettingsForPgClientTransaction({ jwtToken, @@ -81,25 +81,25 @@ const withDefaultPostGraphileContext: WithPostGraphileContextFn = async ( jwtVerifyOptions, pgDefaultRole, pgSettings, - }) + }); // If we can avoid transactions, we get greater performance. const needTransaction = pgForceTransaction || localSettings.length > 0 || - (operationType !== 'query' && operationType !== 'subscription') + (operationType !== 'query' && operationType !== 'subscription'); // Now we've caught as many errors as we can at this stage, let's create a DB connection. // Connect a new Postgres client - const pgClient = await pgPool.connect() + const pgClient = await pgPool.connect(); // Enhance our Postgres client with debugging stuffs. - if (debugPg.enabled || debugPgError.enabled) debugPgClient(pgClient) + if (debugPg.enabled || debugPgError.enabled) debugPgClient(pgClient); // Begin our transaction, if necessary. if (needTransaction) { - await pgClient.query('begin') + await pgClient.query('begin'); } try { @@ -115,24 +115,24 @@ const withDefaultPostGraphileContext: WithPostGraphileContextFn = async ( ), ', ', )}`, - ) + ); - await pgClient.query(query) + await pgClient.query(query); } return await callback({ [$$pgClient]: pgClient, pgRole, - }) + }); } finally { // Cleanup our Postgres client by ending the transaction and releasing // the client back to the pool. Always do this even if the query fails. if (needTransaction) { - await pgClient.query('commit') + await pgClient.query('commit'); } - pgClient.release() + pgClient.release(); } -} +}; /** * Creates a PostGraphile context object which should be passed into a GraphQL @@ -161,25 +161,25 @@ const withDefaultPostGraphileContext: WithPostGraphileContextFn = async ( */ const withPostGraphileContext: WithPostGraphileContextFn = async ( options: { - pgPool: Pool - jwtToken?: string - jwtSecret?: string - jwtAudiences?: Array - jwtRole: Array - jwtVerifyOptions?: jwt.VerifyOptions - pgDefaultRole?: string - pgSettings?: { [key: string]: mixed } + pgPool: Pool; + jwtToken?: string; + jwtSecret?: string; + jwtAudiences?: Array; + jwtRole: Array; + jwtVerifyOptions?: jwt.VerifyOptions; + pgDefaultRole?: string; + pgSettings?: { [key: string]: mixed }; }, callback: (context: mixed) => Promise, ): Promise => { - const pluginHook = pluginHookFromOptions(options) + const pluginHook = pluginHookFromOptions(options); const withContext = pluginHook('withPostGraphileContext', withDefaultPostGraphileContext, { options, - }) - return withContext(options, callback) -} + }); + return withContext(options, callback); +}; -export default withPostGraphileContext +export default withPostGraphileContext; /** * Sets up the Postgres client transaction by decoding the JSON web token and @@ -199,20 +199,20 @@ async function getSettingsForPgClientTransaction({ pgDefaultRole, pgSettings, }: { - jwtToken?: string - jwtSecret?: string - jwtAudiences?: Array - jwtRole: Array - jwtVerifyOptions?: jwt.VerifyOptions - pgDefaultRole?: string - pgSettings?: { [key: string]: mixed } + jwtToken?: string; + jwtSecret?: string; + jwtAudiences?: Array; + jwtRole: Array; + jwtVerifyOptions?: jwt.VerifyOptions; + pgDefaultRole?: string; + pgSettings?: { [key: string]: mixed }; }): Promise<{ - role: string | undefined - localSettings: Array<[string, string]> + role: string | undefined; + localSettings: Array<[string, string]>; }> { // Setup our default role. Once we decode our token, the role may change. - let role = pgDefaultRole - let jwtClaims: { [claimName: string]: mixed } = {} + let role = pgDefaultRole; + let jwtClaims: { [claimName: string]: mixed } = {}; // If we were provided a JWT token, let us try to verify it. If verification // fails we want to throw an error. @@ -222,10 +222,12 @@ async function getSettingsForPgClientTransaction({ try { // If a JWT token was defined, but a secret was not provided to the server // throw a 403 error. - if (typeof jwtSecret !== 'string') throw new Error('Not allowed to provide a JWT token.') + if (typeof jwtSecret !== 'string') throw new Error('Not allowed to provide a JWT token.'); if (jwtAudiences != null && jwtVerifyOptions && 'audience' in jwtVerifyOptions) - throw new Error(`Provide either 'jwtAudiences' or 'jwtVerifyOptions.audience' but not both`) + throw new Error( + `Provide either 'jwtAudiences' or 'jwtVerifyOptions.audience' but not both`, + ); jwtClaims = jwt.verify(jwtToken, jwtSecret, { ...jwtVerifyOptions, @@ -234,9 +236,9 @@ async function getSettingsForPgClientTransaction({ (jwtVerifyOptions && 'audience' in (jwtVerifyOptions as object) ? undefinedIfEmpty(jwtVerifyOptions.audience) : ['postgraphile']), - }) + }); - const roleClaim = getPath(jwtClaims, jwtRole) + const roleClaim = getPath(jwtClaims, jwtRole); // If there is a `role` property in the claims, use that instead of our // default role. @@ -244,9 +246,9 @@ async function getSettingsForPgClientTransaction({ if (typeof roleClaim !== 'string') throw new Error( `JWT \`role\` claim must be a string. Instead found '${typeof jwtClaims['role']}'.`, - ) + ); - role = roleClaim + role = roleClaim; } } catch (error) { // In case this error is thrown in an HTTP context, we want to add status code @@ -256,15 +258,15 @@ async function getSettingsForPgClientTransaction({ ? // The correct status code for an expired ( but otherwise acceptable token is 401 ) 401 : // All other authentication errors should get a 403 status code. - 403 + 403; - throw error + throw error; } } // Instantiate a map of local settings. This map will be transformed into a // Sql query. - const localSettings: Array<[string, string]> = [] + const localSettings: Array<[string, string]> = []; // Set the custom provided settings before jwt claims and role are set // this prevents an accidentional overwriting @@ -272,9 +274,9 @@ async function getSettingsForPgClientTransaction({ for (const key in pgSettings) { if (pgSettings.hasOwnProperty(key) && isPgSettingValid(pgSettings[key])) { if (key === 'role') { - role = String(pgSettings[key]) + role = String(pgSettings[key]); } else { - localSettings.push([key, String(pgSettings[key])]) + localSettings.push([key, String(pgSettings[key])]); } } } @@ -283,19 +285,19 @@ async function getSettingsForPgClientTransaction({ // If there is a rule, we want to set the root `role` setting locally // to be our role. The role may only be null if we have no default role. if (typeof role === 'string') { - localSettings.push(['role', role]) + localSettings.push(['role', role]); } // If we have some JWT claims, we want to set those claims as local // settings with the namespace `jwt.claims`. for (const key in jwtClaims) { if (jwtClaims.hasOwnProperty(key)) { - const rawValue = jwtClaims[key] + const rawValue = jwtClaims[key]; // Unsafe to pass raw object/array to pg.query -> set_config; instead JSONify const value: mixed = - rawValue != null && typeof rawValue === 'object' ? JSON.stringify(rawValue) : rawValue + rawValue != null && typeof rawValue === 'object' ? JSON.stringify(rawValue) : rawValue; if (isPgSettingValid(value)) { - localSettings.push([`jwt.claims.${key}`, String(value)]) + localSettings.push([`jwt.claims.${key}`, String(value)]); } } } @@ -303,13 +305,13 @@ async function getSettingsForPgClientTransaction({ return { localSettings, role, - } + }; } -const $$pgClientOrigQuery = Symbol() +const $$pgClientOrigQuery = Symbol(); -const debugPg = createDebugger('postgraphile:postgres') -const debugPgError = createDebugger('postgraphile:postgres:error') +const debugPg = createDebugger('postgraphile:postgres'); +const debugPgError = createDebugger('postgraphile:postgres:error'); /** * Adds debug logging funcionality to a Postgres client. @@ -323,25 +325,25 @@ function debugPgClient(pgClient: PoolClient): PoolClient { if (!pgClient[$$pgClientOrigQuery]) { // Set the original query method to a key on our client. If that key is // already set, use that. - pgClient[$$pgClientOrigQuery] = pgClient.query + pgClient[$$pgClientOrigQuery] = pgClient.query; // tslint:disable-next-line only-arrow-functions pgClient.query = function(...args: Array): any { // Debug just the query text. We don’t want to debug variables because // there may be passwords in there. - debugPg(args[0] && args[0].text ? args[0].text : args[0]) + debugPg(args[0] && args[0].text ? args[0].text : args[0]); // tslint:disable-next-line no-invalid-this - const promiseResult = pgClient[$$pgClientOrigQuery].apply(this, args) + const promiseResult = pgClient[$$pgClientOrigQuery].apply(this, args); // Report the error with our Postgres debugger. - promiseResult.catch((error: any) => debugPgError(error)) + promiseResult.catch((error: any) => debugPgError(error)); - return promiseResult - } + return promiseResult; + }; } - return pgClient + return pgClient; } /** @@ -350,15 +352,15 @@ function debugPgClient(pgClient: PoolClient): PoolClient { * @private */ function getPath(inObject: mixed, path: Array): any { - let object = inObject + let object = inObject; // From https://github.com/lodash/lodash/blob/master/.internal/baseGet.js - let index = 0 - const length = path.length + let index = 0; + const length = path.length; while (object && index < length) { - object = object[path[index++]] + object = object[path[index++]]; } - return index && index === length ? object : undefined + return index && index === length ? object : undefined; } /** @@ -370,19 +372,19 @@ function getPath(inObject: mixed, path: Array): any { */ function isPgSettingValid(pgSetting: mixed): boolean { if (pgSetting === undefined || pgSetting === null) { - return false + return false; } - const typeOfPgSetting = typeof pgSetting + const typeOfPgSetting = typeof pgSetting; if ( typeOfPgSetting === 'string' || typeOfPgSetting === 'number' || typeOfPgSetting === 'boolean' ) { - return true + return true; } // TODO: booleans! throw new Error( `Error converting pgSetting: ${typeof pgSetting} needs to be of type string, number or boolean.`, - ) + ); } // tslint:enable no-any diff --git a/src/postgres/inventory/pgClientFromContext.ts b/src/postgres/inventory/pgClientFromContext.ts index 61450d57b1..17712d01b2 100644 --- a/src/postgres/inventory/pgClientFromContext.ts +++ b/src/postgres/inventory/pgClientFromContext.ts @@ -1,23 +1,23 @@ // TODO: Refactor this module, it has code smell… -import { PostGraphile } from '../../interfaces' -import { ClientBase, PoolClient } from 'pg' -import mixed = PostGraphile.mixed +import { PostGraphile } from '../../interfaces'; +import { ClientBase, PoolClient } from 'pg'; +import mixed = PostGraphile.mixed; -export const $$pgClient = 'pgClient' +export const $$pgClient = 'pgClient'; /** * Retrieves a Postgres client from a context, throwing an error if such a * client does not exist. */ export default function getPgClientFromContext(context: mixed): PoolClient { - if (context == null || typeof context !== 'object') throw new Error('Context must be an object.') + if (context == null || typeof context !== 'object') throw new Error('Context must be an object.'); - const client = context[$$pgClient] + const client = context[$$pgClient]; - if (client == null) throw new Error('Postgres client does not exist on the context.') + if (client == null) throw new Error('Postgres client does not exist on the context.'); if (!(client instanceof ClientBase)) - throw new Error('Postgres client on context is of the incorrect type.') + throw new Error('Postgres client on context is of the incorrect type.'); - return client as PoolClient + return client as PoolClient; }