From 686ee4e40a9f301d91cda7d42e393582fae94026 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Sat, 5 Jan 2019 21:32:20 +0000 Subject: [PATCH 1/4] Prettier commands --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 0954c4aa1..f97d59deb 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "lint": "eslint packages && lerna run tslint", "flow": "flow", "flow:check": "flow check", + "prettier": "prettier 'packages/*/src/**/*.{ts,js,tsx,jsx}'", + "prettier:fix": "yarn prettier --write", "test": "lerna run --concurrency 1 test", "prepack:all": "scripts/prepack-all", "watch": "for I in packages/*/; do echo \"cd $I && npm run watch\" | perl -p -e 's/\\n/\\0/;'; done | xargs -0 node_modules/.bin/concurrently --kill-others", From d171f5412593b6e174d0679782de3236d34cdf27 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 12 Mar 2019 21:56:53 +0000 Subject: [PATCH 2/4] Latest conversion --- .../src/{GraphQLJSON.js => GraphQLJSON.ts} | 8 +- .../{PgLiveProvider.js => PgLiveProvider.ts} | 12 +- .../src/{QueryBuilder.js => QueryBuilder.ts} | 314 +++++++----- .../src/{index.js => index.ts} | 15 +- .../src/{inflections.js => inflections.ts} | 200 +++++--- .../src/{omit.js => omit.ts} | 33 +- ...{parseIdentifier.js => parseIdentifier.ts} | 2 - ...EndCursor.js => PageInfoStartEndCursor.ts} | 18 +- .../plugins/{PgAllRows.js => PgAllRows.ts} | 31 +- ...nPlugin.js => PgBackwardRelationPlugin.ts} | 59 ++- .../{PgBasicsPlugin.js => PgBasicsPlugin.ts} | 175 +++++-- ...Plugin.js => PgColumnDeprecationPlugin.ts} | 10 +- ...{PgColumnsPlugin.js => PgColumnsPlugin.ts} | 33 +- ...nsPlugin.js => PgComputedColumnsPlugin.ts} | 27 +- ....js => PgConditionComputedColumnPlugin.ts} | 44 +- ...ndition.js => PgConnectionArgCondition.ts} | 20 +- ...=> PgConnectionArgFirstLastBeforeAfter.ts} | 16 +- ...rgOrderBy.js => PgConnectionArgOrderBy.ts} | 24 +- ... => PgConnectionArgOrderByDefaultValue.ts} | 11 +- ...otalCount.js => PgConnectionTotalCount.ts} | 11 +- ...onPlugin.js => PgForwardRelationPlugin.ts} | 38 +- ...tionPlugin.js => PgIntrospectionPlugin.ts} | 457 +++++++++--------- .../{PgJWTPlugin.js => PgJWTPlugin.ts} | 24 +- ...atePlugin.js => PgMutationCreatePlugin.ts} | 28 +- ...ugin.js => PgMutationPayloadEdgePlugin.ts} | 22 +- ...lugin.js => PgMutationProceduresPlugin.ts} | 11 +- ...gin.js => PgMutationUpdateDeletePlugin.ts} | 60 ++- ...Graphile.js => PgNodeAliasPostGraphile.ts} | 12 +- ...nsPlugin.js => PgOrderAllColumnsPlugin.ts} | 11 +- ...Plugin.js => PgOrderByPrimaryKeyPlugin.ts} | 12 +- ...gin.js => PgOrderComputedColumnsPlugin.ts} | 31 +- ...esPlugin.js => PgQueryProceduresPlugin.ts} | 18 +- ...js => PgRecordFunctionConnectionPlugin.ts} | 21 +- ...Plugin.js => PgRecordReturnTypesPlugin.ts} | 20 +- ...nstraint.js => PgRowByUniqueConstraint.ts} | 22 +- .../plugins/{PgRowNode.js => PgRowNode.ts} | 36 +- ...js => PgScalarFunctionConnectionPlugin.ts} | 21 +- .../{PgTablesPlugin.js => PgTablesPlugin.ts} | 44 +- .../{PgTypesPlugin.js => PgTypesPlugin.ts} | 333 ++++++++----- ...StartEndCursor.js => addStartEndCursor.ts} | 8 +- .../src/plugins/{debugSql.js => debugSql.ts} | 13 +- ...spectionQuery.js => introspectionQuery.ts} | 5 +- .../{makeProcField.js => makeProcField.ts} | 90 +++- .../src/plugins/{pgField.js => pgField.ts} | 18 +- ...TemporaryTable.js => viaTemporaryTable.ts} | 80 +-- ...tory.js => queryFromResolveDataFactory.ts} | 93 ++-- .../src/{utils.js => utils.ts} | 4 +- .../src/{withPgClient.js => withPgClient.ts} | 27 +- .../graphile-build/src/{Live.js => Live.ts} | 84 ++-- .../{SchemaBuilder.js => SchemaBuilder.ts} | 274 ++++++----- ...Iterator.js => callbackToAsyncIterator.ts} | 54 ++- .../src/{extend.js => extend.ts} | 13 +- .../graphile-build/src/{index.js => index.ts} | 30 +- .../src/{makeNewBuild.js => makeNewBuild.ts} | 213 +++++--- ....js => AddQueriesToSubscriptionsPlugin.ts} | 10 +- ...s => ClientMutationIdDescriptionPlugin.ts} | 32 +- ...lugin.js => MutationPayloadQueryPlugin.ts} | 19 +- .../{MutationPlugin.js => MutationPlugin.ts} | 10 +- .../plugins/{NodePlugin.js => NodePlugin.ts} | 88 ++-- .../{QueryPlugin.js => QueryPlugin.ts} | 17 +- ...dTypesPlugin.js => StandardTypesPlugin.ts} | 22 +- ...riptionPlugin.js => SubscriptionPlugin.ts} | 10 +- ...ErrorsPlugin.js => SwallowErrorsPlugin.ts} | 8 +- .../src/plugins/{index.js => index.ts} | 3 - .../src/{resolveNode.js => resolveNode.ts} | 4 + .../src/{swallowError.js => swallowError.ts} | 7 +- .../graphile-build/src/{utils.js => utils.ts} | 7 +- 67 files changed, 2127 insertions(+), 1400 deletions(-) rename packages/graphile-build-pg/src/{GraphQLJSON.js => GraphQLJSON.ts} (99%) rename packages/graphile-build-pg/src/{PgLiveProvider.js => PgLiveProvider.ts} (77%) rename packages/graphile-build-pg/src/{QueryBuilder.js => QueryBuilder.ts} (83%) rename packages/graphile-build-pg/src/{index.js => index.ts} (98%) rename packages/graphile-build-pg/src/{inflections.js => inflections.ts} (73%) rename packages/graphile-build-pg/src/{omit.js => omit.ts} (92%) rename packages/graphile-build-pg/src/{parseIdentifier.js => parseIdentifier.ts} (99%) rename packages/graphile-build-pg/src/plugins/{PageInfoStartEndCursor.js => PageInfoStartEndCursor.ts} (80%) rename packages/graphile-build-pg/src/plugins/{PgAllRows.js => PgAllRows.ts} (98%) rename packages/graphile-build-pg/src/plugins/{PgBackwardRelationPlugin.js => PgBackwardRelationPlugin.ts} (97%) rename packages/graphile-build-pg/src/plugins/{PgBasicsPlugin.js => PgBasicsPlugin.ts} (94%) rename packages/graphile-build-pg/src/plugins/{PgColumnDeprecationPlugin.js => PgColumnDeprecationPlugin.ts} (82%) rename packages/graphile-build-pg/src/plugins/{PgColumnsPlugin.js => PgColumnsPlugin.ts} (95%) rename packages/graphile-build-pg/src/plugins/{PgComputedColumnsPlugin.js => PgComputedColumnsPlugin.ts} (93%) rename packages/graphile-build-pg/src/plugins/{PgConditionComputedColumnPlugin.js => PgConditionComputedColumnPlugin.ts} (91%) rename packages/graphile-build-pg/src/plugins/{PgConnectionArgCondition.js => PgConnectionArgCondition.ts} (97%) rename packages/graphile-build-pg/src/plugins/{PgConnectionArgFirstLastBeforeAfter.js => PgConnectionArgFirstLastBeforeAfter.ts} (96%) rename packages/graphile-build-pg/src/plugins/{PgConnectionArgOrderBy.js => PgConnectionArgOrderBy.ts} (95%) rename packages/graphile-build-pg/src/plugins/{PgConnectionArgOrderByDefaultValue.js => PgConnectionArgOrderByDefaultValue.ts} (90%) rename packages/graphile-build-pg/src/plugins/{PgConnectionTotalCount.js => PgConnectionTotalCount.ts} (93%) rename packages/graphile-build-pg/src/plugins/{PgForwardRelationPlugin.js => PgForwardRelationPlugin.ts} (94%) rename packages/graphile-build-pg/src/plugins/{PgIntrospectionPlugin.js => PgIntrospectionPlugin.ts} (81%) rename packages/graphile-build-pg/src/plugins/{PgJWTPlugin.js => PgJWTPlugin.ts} (96%) rename packages/graphile-build-pg/src/plugins/{PgMutationCreatePlugin.js => PgMutationCreatePlugin.ts} (97%) rename packages/graphile-build-pg/src/plugins/{PgMutationPayloadEdgePlugin.js => PgMutationPayloadEdgePlugin.ts} (98%) rename packages/graphile-build-pg/src/plugins/{PgMutationProceduresPlugin.js => PgMutationProceduresPlugin.ts} (92%) rename packages/graphile-build-pg/src/plugins/{PgMutationUpdateDeletePlugin.js => PgMutationUpdateDeletePlugin.ts} (97%) rename packages/graphile-build-pg/src/plugins/{PgNodeAliasPostGraphile.js => PgNodeAliasPostGraphile.ts} (80%) rename packages/graphile-build-pg/src/plugins/{PgOrderAllColumnsPlugin.js => PgOrderAllColumnsPlugin.ts} (94%) rename packages/graphile-build-pg/src/plugins/{PgOrderByPrimaryKeyPlugin.js => PgOrderByPrimaryKeyPlugin.ts} (89%) rename packages/graphile-build-pg/src/plugins/{PgOrderComputedColumnsPlugin.js => PgOrderComputedColumnsPlugin.ts} (86%) rename packages/graphile-build-pg/src/plugins/{PgQueryProceduresPlugin.js => PgQueryProceduresPlugin.ts} (96%) rename packages/graphile-build-pg/src/plugins/{PgRecordFunctionConnectionPlugin.js => PgRecordFunctionConnectionPlugin.ts} (96%) rename packages/graphile-build-pg/src/plugins/{PgRecordReturnTypesPlugin.js => PgRecordReturnTypesPlugin.ts} (97%) rename packages/graphile-build-pg/src/plugins/{PgRowByUniqueConstraint.js => PgRowByUniqueConstraint.ts} (97%) rename packages/graphile-build-pg/src/plugins/{PgRowNode.js => PgRowNode.ts} (97%) rename packages/graphile-build-pg/src/plugins/{PgScalarFunctionConnectionPlugin.js => PgScalarFunctionConnectionPlugin.ts} (95%) rename packages/graphile-build-pg/src/plugins/{PgTablesPlugin.js => PgTablesPlugin.ts} (99%) rename packages/graphile-build-pg/src/plugins/{PgTypesPlugin.js => PgTypesPlugin.ts} (91%) rename packages/graphile-build-pg/src/plugins/{addStartEndCursor.js => addStartEndCursor.ts} (78%) rename packages/graphile-build-pg/src/plugins/{debugSql.js => debugSql.ts} (99%) rename packages/graphile-build-pg/src/plugins/{introspectionQuery.js => introspectionQuery.ts} (99%) rename packages/graphile-build-pg/src/plugins/{makeProcField.js => makeProcField.ts} (97%) rename packages/graphile-build-pg/src/plugins/{pgField.js => pgField.ts} (93%) rename packages/graphile-build-pg/src/plugins/{viaTemporaryTable.js => viaTemporaryTable.ts} (74%) rename packages/graphile-build-pg/src/{queryFromResolveDataFactory.js => queryFromResolveDataFactory.ts} (92%) rename packages/graphile-build-pg/src/{utils.js => utils.ts} (98%) rename packages/graphile-build-pg/src/{withPgClient.js => withPgClient.ts} (90%) rename packages/graphile-build/src/{Live.js => Live.ts} (89%) rename packages/graphile-build/src/{SchemaBuilder.js => SchemaBuilder.ts} (79%) rename packages/graphile-build/src/{callbackToAsyncIterator.js => callbackToAsyncIterator.ts} (69%) rename packages/graphile-build/src/{extend.js => extend.ts} (91%) rename packages/graphile-build/src/{index.js => index.ts} (88%) rename packages/graphile-build/src/{makeNewBuild.js => makeNewBuild.ts} (91%) rename packages/graphile-build/src/plugins/{AddQueriesToSubscriptionsPlugin.js => AddQueriesToSubscriptionsPlugin.ts} (95%) rename packages/graphile-build/src/plugins/{ClientMutationIdDescriptionPlugin.js => ClientMutationIdDescriptionPlugin.ts} (87%) rename packages/graphile-build/src/plugins/{MutationPayloadQueryPlugin.js => MutationPayloadQueryPlugin.ts} (71%) rename packages/graphile-build/src/plugins/{MutationPlugin.js => MutationPlugin.ts} (89%) rename packages/graphile-build/src/plugins/{NodePlugin.js => NodePlugin.ts} (83%) rename packages/graphile-build/src/plugins/{QueryPlugin.js => QueryPlugin.ts} (89%) rename packages/graphile-build/src/plugins/{StandardTypesPlugin.js => StandardTypesPlugin.ts} (86%) rename packages/graphile-build/src/plugins/{SubscriptionPlugin.js => SubscriptionPlugin.ts} (89%) rename packages/graphile-build/src/plugins/{SwallowErrorsPlugin.js => SwallowErrorsPlugin.ts} (83%) rename packages/graphile-build/src/plugins/{index.js => index.ts} (98%) rename packages/graphile-build/src/{resolveNode.js => resolveNode.ts} (99%) rename packages/graphile-build/src/{swallowError.js => swallowError.ts} (94%) rename packages/graphile-build/src/{utils.js => utils.ts} (99%) diff --git a/packages/graphile-build-pg/src/GraphQLJSON.js b/packages/graphile-build-pg/src/GraphQLJSON.ts similarity index 99% rename from packages/graphile-build-pg/src/GraphQLJSON.js rename to packages/graphile-build-pg/src/GraphQLJSON.ts index e84b770cd..a6a59dd7a 100644 --- a/packages/graphile-build-pg/src/GraphQLJSON.js +++ b/packages/graphile-build-pg/src/GraphQLJSON.ts @@ -4,6 +4,7 @@ // It only exists here (rather than using `graphql-type-json` directly) because // we need to export Json along with JSON. // + /* The MIT License (MIT) @@ -39,25 +40,30 @@ export default function makeGraphQLJSONType(graphql, name) { case Kind.STRING: case Kind.BOOLEAN: return ast.value; + case Kind.INT: case Kind.FLOAT: return parseFloat(ast.value); + case Kind.OBJECT: { const value = Object.create(null); ast.fields.forEach(field => { value[field.name.value] = parseLiteral(field.value, variables); }); - return value; } + case Kind.LIST: return ast.values.map(n => parseLiteral(n, variables)); + case Kind.NULL: return null; + case Kind.VARIABLE: { const name = ast.name.value; return variables ? variables[name] : undefined; } + default: return undefined; } diff --git a/packages/graphile-build-pg/src/PgLiveProvider.js b/packages/graphile-build-pg/src/PgLiveProvider.ts similarity index 77% rename from packages/graphile-build-pg/src/PgLiveProvider.js rename to packages/graphile-build-pg/src/PgLiveProvider.ts index 4a42c9a7e..4a4ab0f80 100644 --- a/packages/graphile-build-pg/src/PgLiveProvider.js +++ b/packages/graphile-build-pg/src/PgLiveProvider.ts @@ -1,7 +1,5 @@ -// @flow import { LiveProvider } from "graphile-build"; -import type { PgClass } from "./plugins/PgIntrospectionPlugin"; - +import { PgClass } from "./plugins/PgIntrospectionPlugin"; export default class PgLiveProvider extends LiveProvider { // eslint-disable-next-line flowtype/no-weak-types constructor(...args: any[]) { @@ -14,19 +12,19 @@ export default class PgLiveProvider extends LiveProvider { } recordIdentifierIsValid( - collectionIdentifier: PgClass, - // eslint-disable-next-line flowtype/no-weak-types + collectionIdentifier: PgClass, // eslint-disable-next-line flowtype/no-weak-types recordIdentifier: Array ) { if (!Array.isArray(recordIdentifier)) return false; if (!collectionIdentifier.primaryKeyConstraint) return false; + if ( recordIdentifier.length !== collectionIdentifier.primaryKeyConstraint.keyAttributes.length ) { return false; - } - // TODO: more validation would not go amiss + } // TODO: more validation would not go amiss + return true; } } diff --git a/packages/graphile-build-pg/src/QueryBuilder.js b/packages/graphile-build-pg/src/QueryBuilder.ts similarity index 83% rename from packages/graphile-build-pg/src/QueryBuilder.js rename to packages/graphile-build-pg/src/QueryBuilder.ts index 7087cb699..4426f9790 100644 --- a/packages/graphile-build-pg/src/QueryBuilder.js +++ b/packages/graphile-build-pg/src/QueryBuilder.ts @@ -1,17 +1,13 @@ -// @flow import * as sql from "pg-sql2"; -import type { SQL } from "pg-sql2"; +import { SQL } from "pg-sql2"; import isSafeInteger from "lodash/isSafeInteger"; import chunk from "lodash/chunk"; -import type { PgClass } from "./plugins/PgIntrospectionPlugin"; +import { PgClass } from "./plugins/PgIntrospectionPlugin"; // eslint-disable-next-line flowtype/no-weak-types -// eslint-disable-next-line flowtype/no-weak-types type GraphQLContext = any; - const isDev = process.env.POSTGRAPHILE_ENV === "development"; - type GenContext = { - queryBuilder: QueryBuilder, + queryBuilder: QueryBuilder; }; type Gen = (context: GenContext) => T; @@ -39,72 +35,76 @@ type SQLAlias = SQL; type SQLGen = Gen | SQL; type NumberGen = Gen | number; type CursorValue = {}; -type CursorComparator = (val: CursorValue, isAfter: boolean) => void; - +type CursorComparator = (val: CursorValue, isAfter: boolean) => undefined; export type QueryBuilderOptions = { - supportsJSONB?: boolean, // Defaults to true + supportsJSONB?: boolean; }; class QueryBuilder { - parentQueryBuilder: QueryBuilder | void; + parentQueryBuilder: QueryBuilder | undefined; context: GraphQLContext; supportsJSONB: boolean; locks: { - [string]: true | string, + [a: string]: true | string; }; finalized: boolean; selectedIdentifiers: boolean; data: { - cursorPrefix: Array, - select: Array<[SQLGen, RawAlias]>, - selectCursor: ?SQLGen, - from: ?[SQLGen, SQLAlias], - join: Array, - where: Array, + cursorPrefix: Array; + select: Array<[SQLGen, RawAlias]>; + selectCursor: SQLGen | null | undefined; + from: [SQLGen, SQLAlias] | null | undefined; + join: Array; + where: Array; whereBound: { - lower: Array, - upper: Array, - }, - orderBy: Array<[SQLGen, boolean, boolean | null]>, - orderIsUnique: boolean, - limit: ?NumberGen, - offset: ?NumberGen, - first: ?number, - last: ?number, + lower: Array; + upper: Array; + }; + orderBy: Array<[SQLGen, boolean, boolean | null]>; + orderIsUnique: boolean; + limit: NumberGen | null | undefined; + offset: NumberGen | null | undefined; + first: number | null | undefined; + last: number | null | undefined; beforeLock: { - [string]: Array<() => void> | null, - }, - cursorComparator: ?CursorComparator, + [a: string]: Array<() => undefined> | null; + }; + cursorComparator: CursorComparator | null | undefined; liveConditions: Array< - // eslint-disable-next-line flowtype/no-weak-types - [(data: {}) => (record: any) => boolean, { [key: string]: SQL } | void] - >, + [ + (data: {}) => (record: any) => boolean, + + | { + [key: string]: SQL; + } + | undefined + ] + >; }; compiledData: { - cursorPrefix: Array, - select: Array<[SQL, RawAlias]>, - selectCursor: ?SQL, - from: ?[SQL, SQLAlias], - join: Array, - where: Array, + cursorPrefix: Array; + select: Array<[SQL, RawAlias]>; + selectCursor: SQL | null | undefined; + from: [SQL, SQLAlias] | null | undefined; + join: Array; + where: Array; whereBound: { - lower: Array, - upper: Array, - }, - orderBy: Array<[SQL, boolean, boolean | null]>, - orderIsUnique: boolean, - limit: ?number, - offset: ?number, - first: ?number, - last: ?number, - cursorComparator: ?CursorComparator, + lower: Array; + upper: Array; + }; + orderBy: Array<[SQL, boolean, boolean | null]>; + orderIsUnique: boolean; + limit: number | null | undefined; + offset: number | null | undefined; + first: number | null | undefined; + last: number | null | undefined; + cursorComparator: CursorComparator | null | undefined; }; constructor(options: QueryBuilderOptions = {}, context: GraphQLContext = {}) { this.context = context || {}; this.supportsJSONB = options.supportsJSONB == null ? true : !!options.supportsJSONB; - this.locks = {}; this.finalized = false; this.selectedIdentifiers = false; @@ -151,11 +151,12 @@ class QueryBuilder { }; this.beforeLock("select", () => { this.lock("selectCursor"); + if (this.compiledData.selectCursor) { this.select(this.compiledData.selectCursor, "__cursor"); } - }); - // 'whereBound' and 'natural' order might set offset/limit + }); // 'whereBound' and 'natural' order might set offset/limit + this.beforeLock("where", () => { this.lock("whereBound"); }); @@ -173,14 +174,13 @@ class QueryBuilder { this.lock("limit"); this.lock("offset"); }); - } - - // ---------------------------------------- - + } // ---------------------------------------- // Helper function + jsonbBuildObject(fields: Array<[SQL, RawAlias]>) { if (this.supportsJSONB && fields.length > 50) { const fieldsChunks = chunk(fields, 50); + const chunkToJson = fieldsChunk => sql.fragment`jsonb_build_object(${sql.join( fieldsChunk.map( @@ -189,6 +189,7 @@ class QueryBuilder { ), ", " )})`; + return sql.fragment`(${sql.join( fieldsChunks.map(chunkToJson), " || " @@ -202,28 +203,28 @@ class QueryBuilder { ", " )})`; } - } - - // ---------------------------------------- + } // ---------------------------------------- - beforeLock(field: string, fn: () => void) { + beforeLock(field: string, fn: () => undefined) { this.checkLock(field); + if (!this.data.beforeLock[field]) { this.data.beforeLock[field] = []; - } - // $FlowFixMe + } // $FlowFixMe + this.data.beforeLock[field].push(fn); } makeLiveCollection( - table: PgClass, - // eslint-disable-next-line flowtype/no-weak-types - cb?: (checker: (data: any) => (record: any) => boolean) => void + table: PgClass, // eslint-disable-next-line flowtype/no-weak-types + cb?: (checker: (data: any) => (record: any) => boolean) => undefined ) { if (!this.context.liveCollection) return; if (!this.context.liveConditions) return; /* the actual condition doesn't matter hugely, 'select' should work */ + const liveConditions = this.data.liveConditions; + const checkerGenerator = data => { // Compute this once. const checkers = liveConditions.map(([checkerGenerator]) => @@ -231,21 +232,23 @@ class QueryBuilder { ); return record => checkers.every(checker => checker(record)); }; + if (this.parentQueryBuilder) { if (cb) { throw new Error( "Either use parentQueryBuilder or pass callback, not both." ); } + this.parentQueryBuilder.beforeLock("select", () => { - const id = this.context.liveConditions.push(checkerGenerator) - 1; - // BEWARE: it's easy to override others' conditions, and that will cause issues. Be sensible. + const id = this.context.liveConditions.push(checkerGenerator) - 1; // BEWARE: it's easy to override others' conditions, and that will cause issues. Be sensible. + const allRequirements = this.data.liveConditions.reduce( (memo, [_checkerGenerator, requirements]) => requirements ? Object.assign(memo, requirements) : memo, {} - ); - // $FlowFixMe + ); // $FlowFixMe + this.parentQueryBuilder.select( sql.fragment`json_build_object( '__id', ${sql.value(id)}::int @@ -256,7 +259,6 @@ class QueryBuilder { allRequirements[key] }` ), - ", " )} )`, @@ -275,13 +277,16 @@ class QueryBuilder { addLiveCondition( // eslint-disable-next-line flowtype/no-weak-types checkerGenerator: (data: {}) => (record: any) => boolean, - requirements?: { [key: string]: SQL } + requirements?: { + [key: string]: SQL; + } ) { if (requirements && !this.parentQueryBuilder) { throw new Error( "There's no parentQueryBuilder, so there cannot be requirements" ); } + this.data.liveConditions.push([checkerGenerator, requirements]); } @@ -290,17 +295,22 @@ class QueryBuilder { this.data.cursorComparator = fn; this.lock("cursorComparator"); } + addCursorCondition(cursorValue: CursorValue, isAfter: boolean) { this.beforeLock("whereBound", () => { this.lock("cursorComparator"); + if (!this.compiledData.cursorComparator) { throw new Error("No cursor comparator was set!"); } + this.compiledData.cursorComparator(cursorValue, isAfter); }); } + select(exprGen: SQLGen, alias: RawAlias) { this.checkLock("select"); + if (typeof alias === "string") { // To protect against vulnerabilities such as // @@ -316,8 +326,10 @@ class QueryBuilder { throw new Error(`Disallowed alias '${alias}'.`); } } + this.data.select.push([exprGen, alias]); } + selectIdentifiers(table: PgClass) { if (this.selectedIdentifiers) return; const primaryKey = table.primaryKeyConstraint; @@ -335,36 +347,45 @@ class QueryBuilder { ); this.selectedIdentifiers = true; } + selectCursor(exprGen: SQLGen) { this.checkLock("selectCursor"); this.data.selectCursor = exprGen; } + from(expr: SQLGen, alias?: SQLAlias = sql.identifier(Symbol())) { this.checkLock("from"); + if (!expr) { throw new Error("No from table source!"); } + if (!alias) { throw new Error("No from alias!"); } + this.data.from = [expr, alias]; this.lock("from"); - } - // XXX: join + } // XXX: join + where(exprGen: SQLGen) { this.checkLock("where"); this.data.where.push(exprGen); } + whereBound(exprGen: SQLGen, isLower: boolean) { if (typeof isLower !== "boolean") { throw new Error("isLower must be specified as a boolean"); } + this.checkLock("whereBound"); this.data.whereBound[isLower ? "lower" : "upper"].push(exprGen); } + setOrderIsUnique() { this.data.orderIsUnique = true; } + orderBy( exprGen: SQLGen, ascending: boolean = true, @@ -373,19 +394,24 @@ class QueryBuilder { this.checkLock("orderBy"); this.data.orderBy.push([exprGen, ascending, nullsFirst]); } + limit(limitGen: NumberGen) { this.checkLock("limit"); if (this.data.limit != null) { throw new Error("Must only set limit once"); } + this.data.limit = limitGen; } + offset(offsetGen: NumberGen) { this.checkLock("offset"); + if (this.data.offset != null) { // Add the offsets together (this should be able to recurse) const previous = this.data.offset; + this.data.offset = context => { return ( callIfNecessary(previous, context) + @@ -396,22 +422,26 @@ class QueryBuilder { this.data.offset = offsetGen; } } + first(first: number) { this.checkLock("first"); + if (this.data.first != null) { throw new Error("Must only set first once"); } + this.data.first = first; } + last(last: number) { this.checkLock("last"); + if (this.data.last != null) { throw new Error("Must only set last once"); } - this.data.last = last; - } - // ---------------------------------------- + this.data.last = last; + } // ---------------------------------------- isOrderUnique(lock?: boolean = true) { if (lock) { @@ -423,28 +453,37 @@ class QueryBuilder { return this.data.orderIsUnique; } } + getTableExpression(): SQL { this.lock("from"); + if (!this.compiledData.from) { throw new Error("No from table has been supplied"); } + return this.compiledData.from[0]; } + getTableAlias(): SQL { this.lock("from"); + if (!this.compiledData.from) { throw new Error("No from table has been supplied"); } + return this.compiledData.from[1]; } + getSelectCursor() { this.lock("selectCursor"); return this.compiledData.selectCursor; } + getOffset() { this.lock("offset"); return this.compiledData.offset || 0; } + getFinalLimitAndOffset() { this.lock("offset"); this.lock("limit"); @@ -453,6 +492,7 @@ class QueryBuilder { let limit = this.compiledData.limit; let offset = this.compiledData.offset || 0; let flip = false; + if (this.compiledData.first != null) { if (limit != null) { limit = Math.min(limit, this.compiledData.first); @@ -460,12 +500,14 @@ class QueryBuilder { limit = this.compiledData.first; } } + if (this.compiledData.last != null) { if (offset > 0 && limit != null) { throw new Error( "Issue within pagination, please report your query to graphile-build" ); } + if (limit != null) { if (this.compiledData.last < limit) { offset = limit - this.compiledData.last; @@ -484,26 +526,32 @@ class QueryBuilder { } } } + return { limit, offset, flip, }; } + getFinalOffset() { return this.getFinalLimitAndOffset().offset; } + getFinalLimit() { return this.getFinalLimitAndOffset().limit; } + getOrderByExpressionsAndDirections() { this.lock("orderBy"); return this.compiledData.orderBy; } + getSelectFieldsCount() { this.lockEverything(); return this.compiledData.select.length; } + buildSelectFields() { this.lockEverything(); return sql.join( @@ -514,51 +562,61 @@ class QueryBuilder { ", " ); } + buildSelectJson({ addNullCase }: { addNullCase?: boolean }) { this.lockEverything(); let buildObject = this.compiledData.select.length ? this.jsonbBuildObject(this.compiledData.select) : sql.fragment`to_json(${this.getTableAlias()})`; + if (addNullCase) { buildObject = sql.fragment`(case when (${this.getTableAlias()} is null) then null else ${buildObject} end)`; } + return buildObject; } + buildWhereBoundClause(isLower: boolean) { this.lock("whereBound"); const clauses = this.compiledData.whereBound[isLower ? "lower" : "upper"]; + if (clauses.length) { return sql.fragment`(${sql.join(clauses, ") and (")})`; } else { return sql.literal(true); } } + buildWhereClause( includeLowerBound: boolean, includeUpperBound: boolean, - { addNullCase }: { addNullCase?: boolean } + { + addNullCase, + }: { + addNullCase?: boolean; + } ) { this.lock("where"); const clauses = [ ...(addNullCase ? /* - * Okay... so this is quite interesting. When we're talking about - * composite types, `(foo is not null)` and `not (foo is null)` are - * NOT equivalent! Here's why: - * - * `(foo is null)` - * true if every field of the row is null - * - * `(foo is not null)` - * true if every field of the row is not null - * - * `not (foo is null)` - * true if there's at least one field that is not null - * - * So don't "simplify" the line below! We're probably checking if - * the result of a function call returning a compound type was - * indeed null. - */ + * Okay... so this is quite interesting. When we're talking about + * composite types, `(foo is not null)` and `not (foo is null)` are + * NOT equivalent! Here's why: + * + * `(foo is null)` + * true if every field of the row is null + * + * `(foo is not null)` + * true if every field of the row is not null + * + * `not (foo is null)` + * true if there's at least one field that is not null + * + * So don't "simplify" the line below! We're probably checking if + * the result of a function call returning a compound type was + * indeed null. + */ [sql.fragment`not (${this.getTableAlias()} is null)`] : []), ...this.compiledData.where, @@ -569,13 +627,14 @@ class QueryBuilder { ? sql.fragment`(${sql.join(clauses, ") and (")})` : sql.fragment`1 = 1`; } + build( options: { - asJson?: boolean, - asJsonAggregate?: boolean, - onlyJsonField?: boolean, - addNullCase?: boolean, - useAsterisk?: boolean, + asJson?: boolean; + asJsonAggregate?: boolean; + onlyJsonField?: boolean; + addNullCase?: boolean; + useAsterisk?: boolean; } = {} ) { const { @@ -585,17 +644,21 @@ class QueryBuilder { addNullCase = false, useAsterisk = false, } = options; - this.lockEverything(); + if (onlyJsonField) { - return this.buildSelectJson({ addNullCase }); + return this.buildSelectJson({ + addNullCase, + }); } + const { limit, offset, flip } = this.getFinalLimitAndOffset(); const fields = asJson || asJsonAggregate - ? sql.fragment`${this.buildSelectJson({ addNullCase })} as object` + ? sql.fragment`${this.buildSelectJson({ + addNullCase, + })} as object` : this.buildSelectFields(); - let fragment = sql.fragment` select ${useAsterisk ? sql.fragment`${this.getTableAlias()}.*` : fields} ${this.compiledData.from && @@ -628,6 +691,7 @@ class QueryBuilder { ${isSafeInteger(limit) && sql.fragment`limit ${sql.literal(limit)}`} ${offset && sql.fragment`offset ${sql.literal(offset)}`} `; + if (flip) { const flipAlias = Symbol(); fragment = sql.fragment` @@ -639,9 +703,11 @@ class QueryBuilder { order by (row_number() over (partition by 1)) desc `; } + if (useAsterisk) { fragment = sql.fragment`select ${fields} from (${fragment}) ${this.getTableAlias()}`; } + if (asJsonAggregate) { const aggAlias = Symbol(); fragment = sql.fragment`select json_agg(${sql.identifier( @@ -650,29 +716,35 @@ class QueryBuilder { )}) from (${fragment}) as ${sql.identifier(aggAlias)}`; fragment = sql.fragment`select coalesce((${fragment}), '[]'::json)`; } - return fragment; - } - // ---------------------------------------- + return fragment; + } // ---------------------------------------- _finalize() { this.finalized = true; } + lock(type: string) { if (this.locks[type]) return; + const getContext = () => ({ queryBuilder: this, }); + const beforeLocks = this.data.beforeLock[type]; + if (beforeLocks && beforeLocks.length) { this.data.beforeLock[type] = null; + for (const fn of beforeLocks) { fn(); } } + if (type !== "select") { this.locks[type] = isDev ? new Error("Initally locked here").stack : true; } + if (type === "cursorComparator") { // It's meant to be a function this.compiledData[type] = this.data[type]; @@ -693,31 +765,32 @@ class QueryBuilder { * length of this.data[type] may increase during the operation. This is * why we handle this.locks[type] separately. */ - // Assume that duplicate fields must be identical, don't output the same // key multiple times const seenFields = {}; const context = getContext(); const data = []; - const selects = this.data[type]; + const selects = this.data[type]; // DELIBERATE slow loop, see NOTICE above - // DELIBERATE slow loop, see NOTICE above for (let i = 0; i < selects.length; i++) { - const [valueOrGenerator, columnName] = selects[i]; - // $FlowFixMe + const [valueOrGenerator, columnName] = selects[i]; // $FlowFixMe + if (!seenFields[columnName]) { // $FlowFixMe seenFields[columnName] = true; data.push([callIfNecessary(valueOrGenerator, context), columnName]); const newBeforeLocks = this.data.beforeLock[type]; + if (newBeforeLocks && newBeforeLocks.length) { this.data.beforeLock[type] = null; + for (const fn of newBeforeLocks) { fn(); } } } } + this.locks[type] = isDev ? new Error("Initally locked here").stack : true; this.compiledData[type] = data; } else if (type === "orderBy") { @@ -757,6 +830,7 @@ class QueryBuilder { throw new Error(`Wasn't expecting to lock '${type}'`); } } + checkLock(type: string) { if (this.locks[type]) { if (typeof this.locks[type] === "string") { @@ -766,25 +840,27 @@ class QueryBuilder { "\n" ); } + throw new Error(`'${type}' has already been locked`); } } + lockEverything() { - this._finalize(); - // We must execute everything after `from` so we have the alias to reference + this._finalize(); // We must execute everything after `from` so we have the alias to reference + this.lock("from"); this.lock("join"); - this.lock("orderBy"); - // We must execute where after orderBy because cursor queries require all orderBy columns + this.lock("orderBy"); // We must execute where after orderBy because cursor queries require all orderBy columns + this.lock("cursorComparator"); this.lock("whereBound"); - this.lock("where"); - // 'where' -> 'whereBound' can affect 'offset'/'limit' + this.lock("where"); // 'where' -> 'whereBound' can affect 'offset'/'limit' + this.lock("offset"); this.lock("limit"); this.lock("first"); - this.lock("last"); - // We must execute select after orderBy otherwise we cannot generate a cursor + this.lock("last"); // We must execute select after orderBy otherwise we cannot generate a cursor + this.lock("selectCursor"); this.lock("select"); } diff --git a/packages/graphile-build-pg/src/index.js b/packages/graphile-build-pg/src/index.ts similarity index 98% rename from packages/graphile-build-pg/src/index.js rename to packages/graphile-build-pg/src/index.ts index 4701c1add..467d6c2eb 100644 --- a/packages/graphile-build-pg/src/index.js +++ b/packages/graphile-build-pg/src/index.ts @@ -1,4 +1,3 @@ -// @flow import PgBasicsPlugin from "./plugins/PgBasicsPlugin"; import PgIntrospectionPlugin from "./plugins/PgIntrospectionPlugin"; import PgTypesPlugin from "./plugins/PgTypesPlugin"; @@ -26,22 +25,17 @@ import PgRecordReturnTypesPlugin from "./plugins/PgRecordReturnTypesPlugin"; import PgRecordFunctionConnectionPlugin from "./plugins/PgRecordFunctionConnectionPlugin"; import PgScalarFunctionConnectionPlugin from "./plugins/PgScalarFunctionConnectionPlugin"; import PageInfoStartEndCursor from "./plugins/PageInfoStartEndCursor"; -import PgConnectionTotalCount from "./plugins/PgConnectionTotalCount"; +import PgConnectionTotalCount from "./plugins/PgConnectionTotalCount"; // Mutations -// Mutations import PgMutationCreatePlugin from "./plugins/PgMutationCreatePlugin"; import PgMutationUpdateDeletePlugin from "./plugins/PgMutationUpdateDeletePlugin"; import PgMutationProceduresPlugin from "./plugins/PgMutationProceduresPlugin"; import PgMutationPayloadEdgePlugin from "./plugins/PgMutationPayloadEdgePlugin"; - import * as inflections from "./inflections"; - import parseIdentifier from "./parseIdentifier"; import omit from "./omit"; export { formatSQLForDebugging } from "./plugins/debugSql"; - export { parseIdentifier, omit }; - export const defaultPlugins = [ PgBasicsPlugin, PgIntrospectionPlugin, @@ -70,17 +64,13 @@ export const defaultPlugins = [ PgRecordFunctionConnectionPlugin, PgScalarFunctionConnectionPlugin, // For PostGraphile compatibility PageInfoStartEndCursor, // For PostGraphile compatibility - PgConnectionTotalCount, - - // Mutations + PgConnectionTotalCount, // Mutations PgMutationCreatePlugin, PgMutationUpdateDeletePlugin, PgMutationProceduresPlugin, PgMutationPayloadEdgePlugin, ]; - export { inflections }; - export { PgBasicsPlugin, PgIntrospectionPlugin, @@ -116,5 +106,4 @@ export { PgMutationProceduresPlugin, PgMutationPayloadEdgePlugin, }; - export { upperFirst, camelCase, constantCase } from "graphile-build"; diff --git a/packages/graphile-build-pg/src/inflections.js b/packages/graphile-build-pg/src/inflections.ts similarity index 73% rename from packages/graphile-build-pg/src/inflections.js rename to packages/graphile-build-pg/src/inflections.ts index 97b8bb1e3..362cb6e99 100644 --- a/packages/graphile-build-pg/src/inflections.js +++ b/packages/graphile-build-pg/src/inflections.ts @@ -1,5 +1,4 @@ /* THIS ENTIRE FILE IS DEPRECATED. DO NOT USE THIS. DO NOT EDIT THIS. */ -// @flow import { upperCamelCase, camelCase, @@ -7,30 +6,26 @@ import { pluralize, singularize, } from "graphile-build"; - import { preventEmptyResult } from "./plugins/PgBasicsPlugin"; +const outputMessages = []; // eslint-disable-next-line flowtype/no-weak-types -const outputMessages = []; - -// eslint-disable-next-line flowtype/no-weak-types -function deprecate(fn: (...input: Array) => string, message: string) { +function deprecate(fn: () => string, message: string) { if (typeof fn !== "function") { return fn; } + return function(...args) { if (outputMessages.indexOf(message) === -1) { - outputMessages.push(message); - // eslint-disable-next-line no-console + outputMessages.push(message); // eslint-disable-next-line no-console + console.warn(new Error(message)); } + return fn.apply(this, args); }; } -function deprecateEverything(obj: { - // eslint-disable-next-line flowtype/no-weak-types - [string]: (...input: Array) => string, -}) { +function deprecateEverything(obj: { [a: string]: () => string }) { return Object.keys(obj).reduce((memo, key) => { memo[key] = deprecate( obj[key], @@ -41,19 +36,17 @@ function deprecateEverything(obj: { } type Keys = Array<{ - column: string, - table: string, - schema: ?string, + column: string; + table: string; + schema: string | null | undefined; }>; - -type InflectorUtils = {| - constantCase: string => string, - camelCase: string => string, - upperCamelCase: string => string, - pluralize: string => string, - singularize: string => string, -|}; - +type InflectorUtils = { + constantCase: (a: string) => string; + camelCase: (a: string) => string; + upperCamelCase: (a: string) => string; + pluralize: (a: string) => string; + singularize: (a: string) => string; +}; export const defaultUtils: InflectorUtils = { constantCase, camelCase, @@ -61,15 +54,16 @@ export const defaultUtils: InflectorUtils = { pluralize, singularize, }; - export type Inflector = { - // TODO: tighten this up! - // eslint-disable-next-line flowtype/no-weak-types - [string]: (...input: Array) => string, + [a: string]: () => string; }; - export const newInflector = ( - overrides: ?{ [string]: () => string } = undefined, + overrides: + | { + [a: string]: () => string; + } + | null + | undefined = undefined, { constantCase, camelCase, @@ -90,44 +84,47 @@ export const newInflector = ( Object.assign( { pluralize, - argument(name: ?string, index: number) { + + argument(name: string | null | undefined, index: number) { return camelCase(name || `arg${index}`); }, + orderByType(typeName: string) { return upperCamelCase(`${pluralize(typeName)}-order-by`); }, + orderByEnum( name: string, ascending: boolean, _table: string, - _schema: ?string + _schema: string | null | undefined ) { return constantCase(`${name}_${ascending ? "asc" : "desc"}`); }, + domainType(name: string) { return upperCamelCase(name); }, + enumName(inValue: string) { let value = inValue; if (value === "") { return "_EMPTY_"; - } - - // Some enums use asterisks to signify wildcards - this might be for + } // Some enums use asterisks to signify wildcards - this might be for // the whole item, or prefixes/suffixes, or even in the middle. This // is provided on a best efforts basis, if it doesn't suit your // purposes then please pass a custom inflector as mentioned below. + value = value .replace(/\*/g, "_ASTERISK_") .replace(/^(_?)_+ASTERISK/, "$1ASTERISK") - .replace(/ASTERISK_(_?)_*$/, "ASTERISK$1"); - - // This is a best efforts replacement for common symbols that you + .replace(/ASTERISK_(_?)_*$/, "ASTERISK$1"); // This is a best efforts replacement for common symbols that you // might find in enums. Generally we only support enums that are // alphanumeric, if these replacements don't work for you, you should // pass a custom inflector that replaces this `enumName` method // with one of your own chosing. + value = { // SQL comparison operators @@ -138,13 +135,11 @@ export const newInflector = ( "<>": "DIFFERENT", "<=": "LESS_THAN_OR_EQUAL", "<": "LESS_THAN", - // PostgreSQL LIKE shortcuts "~~": "LIKE", "~~*": "ILIKE", "!~~": "NOT_LIKE", "!~~*": "NOT_ILIKE", - // '~' doesn't necessarily represent regexps, but the three // operators following it likely do, so we'll use the word TILDE // in all for consistency. @@ -152,7 +147,6 @@ export const newInflector = ( "~*": "TILDE_ASTERISK", "!~": "NOT_TILDE", "!~*": "NOT_TILDE_ASTERISK", - // A number of other symbols where we're not sure of their // meaning. We give them common generic names so that they're // suitable for multiple purposes, e.g. favouring 'PLUS' over @@ -188,57 +182,80 @@ export const newInflector = ( }[value] || value; return value; }, + enumType(name: string) { return upperCamelCase(name); }, + conditionType(typeName: string) { return upperCamelCase(`${typeName}-condition`); }, + inputType(typeName: string) { return upperCamelCase(`${typeName}-input`); }, + rangeBoundType(typeName: string) { return upperCamelCase(`${typeName}-range-bound`); }, + rangeType(typeName: string) { return upperCamelCase(`${typeName}-range`); }, + patchType(typeName: string) { return upperCamelCase(`${typeName}-patch`); }, + patchField(itemName: string) { return camelCase(`${itemName}-patch`); }, - tableName(name: string, _schema: ?string) { + + tableName(name: string, _schema: string | null | undefined) { return camelCase(singularizeTable(name)); }, - tableNode(name: string, _schema: ?string) { + + tableNode(name: string, _schema: string | null | undefined) { return camelCase(singularizeTable(name)); }, - allRows(name: string, schema: ?string) { + + allRows(name: string, schema: string | null | undefined) { return camelCase( `all-${this.pluralize(this.tableName(name, schema))}` ); }, - functionName(name: string, _schema: ?string) { + + functionName(name: string, _schema: string | null | undefined) { return camelCase(name); }, - functionPayloadType(name: string, _schema: ?string) { + + functionPayloadType( + name: string, + _schema: string | null | undefined + ) { return upperCamelCase(`${name}-payload`); }, - functionInputType(name: string, _schema: ?string) { + + functionInputType(name: string, _schema: string | null | undefined) { return upperCamelCase(`${name}-input`); }, - tableType(name: string, schema: ?string) { + + tableType(name: string, schema: string | null | undefined) { return upperCamelCase(this.tableName(name, schema)); }, - column(name: string, _table: string, _schema: ?string) { + + column( + name: string, + _table: string, + _schema: string | null | undefined + ) { return camelCase(name); }, + singleRelationByKeys( detailedKeys: Keys, table: string, - schema: ?string + schema: string | null | undefined ) { return camelCase( `${this.tableName(table, schema)}-by-${detailedKeys @@ -246,37 +263,55 @@ export const newInflector = ( .join("-and-")}` ); }, - rowByUniqueKeys(detailedKeys: Keys, table: string, schema: ?string) { + + rowByUniqueKeys( + detailedKeys: Keys, + table: string, + schema: string | null | undefined + ) { return camelCase( `${this.tableName(table, schema)}-by-${detailedKeys .map(key => this.column(key.column, key.table, key.schema)) .join("-and-")}` ); }, - updateByKeys(detailedKeys: Keys, table: string, schema: ?string) { + + updateByKeys( + detailedKeys: Keys, + table: string, + schema: string | null | undefined + ) { return camelCase( `update-${this.tableName(table, schema)}-by-${detailedKeys .map(key => this.column(key.column, key.table, key.schema)) .join("-and-")}` ); }, - deleteByKeys(detailedKeys: Keys, table: string, schema: ?string) { + + deleteByKeys( + detailedKeys: Keys, + table: string, + schema: string | null | undefined + ) { return camelCase( `delete-${this.tableName(table, schema)}-by-${detailedKeys .map(key => this.column(key.column, key.table, key.schema)) .join("-and-")}` ); }, - updateNode(name: string, _schema: ?string) { + + updateNode(name: string, _schema: string | null | undefined) { return camelCase(`update-${singularizeTable(name)}`); }, - deleteNode(name: string, _schema: ?string) { + + deleteNode(name: string, _schema: string | null | undefined) { return camelCase(`delete-${singularizeTable(name)}`); }, + updateByKeysInputType( detailedKeys: Keys, name: string, - _schema: ?string + _schema: string | null | undefined ) { return upperCamelCase( `update-${singularizeTable(name)}-by-${detailedKeys @@ -284,10 +319,11 @@ export const newInflector = ( .join("-and-")}-input` ); }, + deleteByKeysInputType( detailedKeys: Keys, name: string, - _schema: ?string + _schema: string | null | undefined ) { return upperCamelCase( `delete-${singularizeTable(name)}-by-${detailedKeys @@ -295,18 +331,27 @@ export const newInflector = ( .join("-and-")}-input` ); }, - updateNodeInputType(name: string, _schema: ?string) { + + updateNodeInputType( + name: string, + _schema: string | null | undefined + ) { return upperCamelCase(`update-${singularizeTable(name)}-input`); }, - deleteNodeInputType(name: string, _schema: ?string) { + + deleteNodeInputType( + name: string, + _schema: string | null | undefined + ) { return upperCamelCase(`delete-${singularizeTable(name)}-input`); }, + manyRelationByKeys( detailedKeys: Keys, table: string, - schema: ?string, + schema: string | null | undefined, _foreignTable: string, - _foreignSchema: ?string + _foreignSchema: string | null | undefined ) { return camelCase( `${this.pluralize( @@ -316,34 +361,50 @@ export const newInflector = ( .join("-and-")}` ); }, + edge(typeName: string) { return upperCamelCase(`${pluralize(typeName)}-edge`); }, - edgeField(name: string, _schema: ?string) { + + edgeField(name: string, _schema: string | null | undefined) { return camelCase(`${singularizeTable(name)}-edge`); }, + connection(typeName: string) { return upperCamelCase(`${this.pluralize(typeName)}-connection`); }, - scalarFunctionConnection(procName: string, _procSchema: ?string) { + + scalarFunctionConnection( + procName: string, + _procSchema: string | null | undefined + ) { return upperCamelCase(`${procName}-connection`); }, - scalarFunctionEdge(procName: string, _procSchema: ?string) { + + scalarFunctionEdge( + procName: string, + _procSchema: string | null | undefined + ) { return upperCamelCase(`${procName}-edge`); }, - createField(name: string, _schema: ?string) { + + createField(name: string, _schema: string | null | undefined) { return camelCase(`create-${singularizeTable(name)}`); }, - createInputType(name: string, _schema: ?string) { + + createInputType(name: string, _schema: string | null | undefined) { return upperCamelCase(`create-${singularizeTable(name)}-input`); }, - createPayloadType(name: string, _schema: ?string) { + + createPayloadType(name: string, _schema: string | null | undefined) { return upperCamelCase(`create-${singularizeTable(name)}-payload`); }, - updatePayloadType(name: string, _schema: ?string) { + + updatePayloadType(name: string, _schema: string | null | undefined) { return upperCamelCase(`update-${singularizeTable(name)}-payload`); }, - deletePayloadType(name: string, _schema: ?string) { + + deletePayloadType(name: string, _schema: string | null | undefined) { return upperCamelCase(`delete-${singularizeTable(name)}-payload`); }, }, @@ -352,5 +413,4 @@ export const newInflector = ( ) ); }; - export const defaultInflection = newInflector(); diff --git a/packages/graphile-build-pg/src/omit.js b/packages/graphile-build-pg/src/omit.ts similarity index 92% rename from packages/graphile-build-pg/src/omit.js rename to packages/graphile-build-pg/src/omit.ts index 8c9491244..fca6dac9d 100644 --- a/packages/graphile-build-pg/src/omit.js +++ b/packages/graphile-build-pg/src/omit.ts @@ -1,15 +1,13 @@ -// @flow - -import type { +import { PgProc, PgClass, PgAttribute, PgConstraint, } from "./plugins/PgIntrospectionPlugin"; - /* * Please only use capitals for aliases and lower case for the values. */ + export const CREATE = "create"; export const READ = "read"; export const UPDATE = "update"; @@ -20,7 +18,6 @@ export const ALL = "all"; export const MANY = "many"; export const EXECUTE = "execute"; export const BASE = "base"; - const aliases = { C: CREATE, R: READ, @@ -33,13 +30,13 @@ const aliases = { X: EXECUTE, B: BASE, }; - const PERMISSIONS_THAT_REQUIRE_READ = [UPDATE, CREATE, DELETE, ALL, MANY]; function parse(arrOrNot, errorPrefix = "Error") { if (!arrOrNot) { return null; } + const arr = Array.isArray(arrOrNot) ? arrOrNot : [arrOrNot]; let all = false; const arrayNormalized = [].concat( @@ -48,28 +45,33 @@ function parse(arrOrNot, errorPrefix = "Error") { all = true; return []; } + if (str[0] === ":") { const perms = str .substr(1) .split("") .map(p => aliases[p]); const bad = perms.find(p => !p); + if (bad) { throw new Error( `${errorPrefix} - abbreviated parameter '${bad}' not understood` ); } + return perms; } else { - const perms = str.split(","); - // TODO: warning if not in list? + const perms = str.split(","); // TODO: warning if not in list? + return perms; } }) ); + if (all) { return true; } + return arrayNormalized; } @@ -78,12 +80,10 @@ export default function omit( permission: string ) { const tags = entity.tags; - const omitSpecRaw = tags.omit; - - // '@include' is not being released yet because it would mean every new + const omitSpecRaw = tags.omit; // '@include' is not being released yet because it would mean every new // filter we added would become a breaking change for people using @include. - const includeSpecRaw = null; - // const includeSpecRaw = tags.include; + + const includeSpecRaw = null; // const includeSpecRaw = tags.include; if (omitSpecRaw && includeSpecRaw) { throw new Error( @@ -92,6 +92,7 @@ export default function omit( }' - you must only specify @omit or @include, not both` ); } + const omitSpec = parse( omitSpecRaw, `Error when processing @omit instructions for ${entity.kind} '${ @@ -109,10 +110,12 @@ export default function omit( if (omitSpec === true) { return true; } + if (omitSpec.indexOf(READ) >= 0) { const bad = PERMISSIONS_THAT_REQUIRE_READ.filter( p => omitSpec.indexOf(p) === -1 ); + if (bad.length > 0) { throw new Error( `Processing @omit for ${entity.kind} '${entity.name}' - '${bad.join( @@ -123,6 +126,7 @@ export default function omit( ); } } + return omitSpec.indexOf(permission) >= 0; } else if (includeSpec) { if (includeSpec === true) { @@ -132,10 +136,12 @@ export default function omit( }' - @include should specify a list of actions` ); } + if (includeSpec.indexOf(READ) === -1) { const bad = PERMISSIONS_THAT_REQUIRE_READ.find( p => includeSpec.indexOf(p) >= 0 ); + if (bad) { throw new Error( `Error when processing @include for ${entity.kind} '${ @@ -144,6 +150,7 @@ export default function omit( ); } } + return includeSpec.indexOf(permission) === -1; } else { return false; diff --git a/packages/graphile-build-pg/src/parseIdentifier.js b/packages/graphile-build-pg/src/parseIdentifier.ts similarity index 99% rename from packages/graphile-build-pg/src/parseIdentifier.js rename to packages/graphile-build-pg/src/parseIdentifier.ts index e1d79ec6f..47c5b569d 100644 --- a/packages/graphile-build-pg/src/parseIdentifier.js +++ b/packages/graphile-build-pg/src/parseIdentifier.ts @@ -2,12 +2,10 @@ export default function parseIdentifier(typeIdentifier) { const match = typeIdentifier.match( /^(?:([a-zA-Z0-9_]+)|"([^"]*)")\.(?:([a-zA-Z0-9_]+)|"([^"]*)")$/ ); - if (!match) throw new Error( `Type identifier '${typeIdentifier}' is of the incorrect form.` ); - return { namespaceName: match[1] || match[2], entityName: match[3] || match[4], diff --git a/packages/graphile-build-pg/src/plugins/PageInfoStartEndCursor.js b/packages/graphile-build-pg/src/plugins/PageInfoStartEndCursor.ts similarity index 80% rename from packages/graphile-build-pg/src/plugins/PageInfoStartEndCursor.js rename to packages/graphile-build-pg/src/plugins/PageInfoStartEndCursor.ts index 25ed0101c..a659c2932 100644 --- a/packages/graphile-build-pg/src/plugins/PageInfoStartEndCursor.js +++ b/packages/graphile-build-pg/src/plugins/PageInfoStartEndCursor.ts @@ -1,15 +1,15 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PageInfoStartEndCursor(builder) { +import { Plugin } from "graphile-build"; +export default function PageInfoStartEndCursor(builder) { builder.hook( "GraphQLObjectType:fields", (fields, build, context) => { const { extend, getTypeByName, inflection } = build; const { Self, fieldWithHooks } = context; + if (Self.name !== inflection.builtin("PageInfo")) { return fields; } + const Cursor = getTypeByName("Cursor"); return extend( fields, @@ -17,7 +17,9 @@ export default (function PageInfoStartEndCursor(builder) { startCursor: fieldWithHooks( "startCursor", ({ addDataGenerator }) => { - addDataGenerator(() => ({ usesCursor: [true] })); + addDataGenerator(() => ({ + usesCursor: [true], + })); return { description: "When paginating backwards, the cursor to continue.", @@ -31,7 +33,9 @@ export default (function PageInfoStartEndCursor(builder) { endCursor: fieldWithHooks( "endCursor", ({ addDataGenerator }) => { - addDataGenerator(() => ({ usesCursor: [true] })); + addDataGenerator(() => ({ + usesCursor: [true], + })); return { description: "When paginating forwards, the cursor to continue.", @@ -50,4 +54,4 @@ export default (function PageInfoStartEndCursor(builder) { [], ["Cursor"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgAllRows.js b/packages/graphile-build-pg/src/plugins/PgAllRows.ts similarity index 98% rename from packages/graphile-build-pg/src/plugins/PgAllRows.js rename to packages/graphile-build-pg/src/plugins/PgAllRows.ts index b640c8e3f..51e17a7f9 100644 --- a/packages/graphile-build-pg/src/plugins/PgAllRows.js +++ b/packages/graphile-build-pg/src/plugins/PgAllRows.ts @@ -1,9 +1,6 @@ -// @flow - -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import debugSql from "./debugSql"; - -export default (async function PgAllRows( +export default async function PgAllRows( builder, { pgViewUniqueKey, pgSimpleCollections, subscriptions } ) { @@ -27,9 +24,11 @@ export default (async function PgAllRows( fieldWithHooks, scope: { isRootQuery }, } = context; + if (!isRootQuery) { return fields; } + return extend( fields, introspectionResultsByKind.class.reduce((memo, table) => { @@ -37,32 +36,38 @@ export default (async function PgAllRows( if (!table.isSelectable) return memo; if (!table.namespace) return memo; if (omit(table, "all")) return memo; - const TableType = pgGetGqlTypeByTypeIdAndModifier( table.type.id, null ); + if (!TableType) { return memo; } + const tableTypeName = TableType.name; const ConnectionType = getTypeByName( inflection.connection(TableType.name) ); + if (!TableType) { throw new Error( `Could not find GraphQL type for table '${table.name}'` ); } + const attributes = table.attributes; const primaryKeyConstraint = table.primaryKeyConstraint; const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes; + const isView = t => t.classKind === "v"; + const viewUniqueKey = table.tags.uniqueKey || pgViewUniqueKey; const uniqueIdAttribute = viewUniqueKey ? attributes.find(attr => attr.name === viewUniqueKey) : undefined; + if (isView && table.tags.uniqueKey && !uniqueIdAttribute) { throw new Error( `Could not find the named unique key '${ @@ -70,13 +75,16 @@ export default (async function PgAllRows( }' on view '${table.namespaceName}.${table.name}'` ); } + if (!ConnectionType) { throw new Error( `Could not find GraphQL connection type for table '${table.name}'` ); } + const schema = table.namespace; const sqlFullTableName = sql.identifier(schema.name, table.name); + function makeField(isConnection) { const fieldName = isConnection ? inflection.allRows(table) @@ -92,12 +100,14 @@ export default (async function PgAllRows( ? ConnectionType : new GraphQLList(new GraphQLNonNull(TableType)), args: {}, + async resolve(parent, args, resolveContext, resolveInfo) { const { pgClient } = resolveContext; const parsedResolveInfoFragment = parseResolveInfo( resolveInfo ); parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin + const resolveData = getDataFromParsedResolveInfoFragment( parsedResolveInfoFragment, resolveInfo.returnType @@ -120,10 +130,12 @@ export default (async function PgAllRows( } ); } + if (primaryKeys) { if (subscriptions) { queryBuilder.selectIdentifiers(table); } + queryBuilder.beforeLock("orderBy", () => { if (!queryBuilder.isOrderUnique(false)) { // Order by PK if no order specified @@ -190,6 +202,7 @@ export default (async function PgAllRows( ) ); } + return result.rows; } }, @@ -202,17 +215,21 @@ export default (async function PgAllRows( } ); } + const simpleCollections = table.tags.simpleCollections || pgSimpleCollections; const hasConnections = simpleCollections !== "only"; const hasSimpleCollections = simpleCollections === "only" || simpleCollections === "both"; + if (TableType && ConnectionType && hasConnections) { makeField(true); } + if (TableType && hasSimpleCollections) { makeField(false); } + return memo; }, {}), `Adding 'all*' relations to root Query` @@ -222,4 +239,4 @@ export default (async function PgAllRows( [], ["PgTables"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.js b/packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.ts similarity index 97% rename from packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.js rename to packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.ts index 3dfd7e547..81c65a9d8 100644 --- a/packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.ts @@ -1,15 +1,10 @@ -// @flow import debugFactory from "debug"; - -import type { Plugin } from "graphile-build"; - +import { Plugin } from "graphile-build"; const debug = debugFactory("graphile-build-pg"); - const OMIT = 0; const DEPRECATED = 1; const ONLY = 2; - -export default (function PgBackwardRelationPlugin( +export default function PgBackwardRelationPlugin( builder, { pgLegacyRelations, pgSimpleCollections, subscriptions } ) { @@ -42,10 +37,11 @@ export default (function PgBackwardRelationPlugin( fieldWithHooks, Self, } = context; + if (!isPgRowType || !foreignTable || foreignTable.kind !== "class") { return fields; - } - // This is a relation in which WE are foreign + } // This is a relation in which WE are foreign + const foreignKeyConstraints = foreignTable.foreignConstraints.filter( con => con.type === "f" ); @@ -54,6 +50,7 @@ export default (function PgBackwardRelationPlugin( foreignTable.type.id, null ); + if (!gqlForeignTableType) { debug( `Could not determine type for foreign table with id ${ @@ -69,6 +66,7 @@ export default (function PgBackwardRelationPlugin( if (omit(constraint, "read")) { return memo; } + const table = introspectionResultsByKind.classById[constraint.classId]; const tableTypeName = inflection.tableType(table); @@ -76,12 +74,14 @@ export default (function PgBackwardRelationPlugin( table.type.id, null ); + if (!gqlTableType) { debug( `Could not determine type for table with id ${constraint.classId}` ); return memo; } + if (!table) { throw new Error( `Could not find the table that referenced us (constraint: ${ @@ -89,28 +89,30 @@ export default (function PgBackwardRelationPlugin( })` ); } - const schema = table.namespace; + const schema = table.namespace; const keys = constraint.keyAttributes; const foreignKeys = constraint.foreignKeyAttributes; + if (!keys.every(_ => _) || !foreignKeys.every(_ => _)) { throw new Error("Could not find key columns!"); } + if (keys.some(key => omit(key, "read"))) { return memo; } + if (foreignKeys.some(key => omit(key, "read"))) { return memo; } + const isUnique = !!table.constraints.find( c => (c.type === "p" || c.type === "u") && c.keyAttributeNums.length === keys.length && c.keyAttributeNums.every((n, i) => keys[i].num === n) ); - const isDeprecated = isUnique && legacyRelationMode === DEPRECATED; - const singleRelationFieldName = isUnique ? inflection.singleRelationByKeysBackwards( keys, @@ -119,14 +121,11 @@ export default (function PgBackwardRelationPlugin( constraint ) : null; - const primaryKeyConstraint = table.primaryKeyConstraint; const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes; - const shouldAddSingleRelation = isUnique && legacyRelationMode !== ONLY; - const shouldAddManyRelation = !isUnique || legacyRelationMode === DEPRECATED || @@ -161,19 +160,22 @@ export default (function PgBackwardRelationPlugin( tableAlias, resolveData, { - useAsterisk: false, // Because it's only a single relation, no need + useAsterisk: false, + // Because it's only a single relation, no need asJson: true, addNullCase: true, withPagination: false, }, innerQueryBuilder => { innerQueryBuilder.parentQueryBuilder = queryBuilder; + if ( subscriptions && table.primaryKeyConstraint ) { innerQueryBuilder.selectIdentifiers(table); } + keys.forEach((key, i) => { innerQueryBuilder.where( sql.fragment`${tableAlias}.${sql.identifier( @@ -202,6 +204,7 @@ export default (function PgBackwardRelationPlugin( resolveInfo ); const record = data[safeAlias]; + if (record && resolveContext.liveRecord) { resolveContext.liveRecord( "pg", @@ -209,6 +212,7 @@ export default (function PgBackwardRelationPlugin( record.__identifiers ); } + return record; }, }; @@ -229,11 +233,13 @@ export default (function PgBackwardRelationPlugin( )}` ); } + function makeFields(isConnection) { if (isUnique && !isConnection) { // Don't need this, use the singular instead return; } + if (shouldAddManyRelation && !omit(table, "many")) { const manyRelationFieldName = isConnection ? inflection.manyRelationByKeys( @@ -248,7 +254,6 @@ export default (function PgBackwardRelationPlugin( foreignTable, constraint ); - memo = extend( memo, { @@ -280,6 +285,7 @@ export default (function PgBackwardRelationPlugin( }, innerQueryBuilder => { innerQueryBuilder.parentQueryBuilder = queryBuilder; + if (subscriptions) { innerQueryBuilder.makeLiveCollection(table); innerQueryBuilder.addLiveCondition( @@ -299,6 +305,7 @@ export default (function PgBackwardRelationPlugin( }, {}) ); } + if (primaryKeys) { if ( subscriptions && @@ -308,6 +315,7 @@ export default (function PgBackwardRelationPlugin( table ); } + innerQueryBuilder.beforeLock( "orderBy", () => { @@ -372,6 +380,7 @@ export default (function PgBackwardRelationPlugin( const safeAlias = getSafeAliasFromResolveInfo( resolveInfo ); + if ( subscriptions && resolveContext.liveCollection && @@ -381,13 +390,14 @@ export default (function PgBackwardRelationPlugin( const condition = resolveContext.liveConditions[__id]; const checker = condition(rest); - resolveContext.liveCollection("pg", table, checker); } + if (isConnection) { return addStartEndCursor(data[safeAlias]); } else { const records = data[safeAlias]; + if (resolveContext.liveRecord) { records.forEach( r => @@ -399,14 +409,14 @@ export default (function PgBackwardRelationPlugin( ) ); } + return records; } }, ...(isDeprecated ? { - deprecationReason: - // $FlowFixMe - `Please use ${singleRelationFieldName} instead`, + // $FlowFixMe + deprecationReason: `Please use ${singleRelationFieldName} instead`, } : null), }; @@ -419,7 +429,6 @@ export default (function PgBackwardRelationPlugin( } ), }, - `Backward relation (${ isConnection ? "connection" : "simple collection" }) for ${describePgEntity( @@ -435,6 +444,7 @@ export default (function PgBackwardRelationPlugin( ); } } + const simpleCollections = constraint.tags.simpleCollections || table.tags.simpleCollections || @@ -442,12 +452,15 @@ export default (function PgBackwardRelationPlugin( const hasConnections = simpleCollections !== "only"; const hasSimpleCollections = simpleCollections === "only" || simpleCollections === "both"; + if (hasConnections) { makeFields(true); } + if (hasSimpleCollections) { makeFields(false); } + return memo; }, {}), `Adding backward relations for ${Self.name}` @@ -455,4 +468,4 @@ export default (function PgBackwardRelationPlugin( }, ["PgBackwardRelation"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.js b/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.ts similarity index 94% rename from packages/graphile-build-pg/src/plugins/PgBasicsPlugin.js rename to packages/graphile-build-pg/src/plugins/PgBasicsPlugin.ts index 29820b199..fec40154a 100644 --- a/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.ts @@ -1,8 +1,7 @@ -// @flow import * as sql from "pg-sql2"; -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import { version } from "../../package.json"; -import type { +import { PgProc, PgType, PgClass, @@ -11,7 +10,6 @@ import type { PgEntity, } from "./PgIntrospectionPlugin"; import pgField from "./pgField"; - import queryFromResolveDataFactory from "../queryFromResolveDataFactory"; import addStartEndCursor from "./addStartEndCursor"; import baseOmit, { @@ -33,24 +31,29 @@ import pickBy from "lodash/pickBy"; import PgLiveProvider from "../PgLiveProvider"; const defaultPgColumnFilter = (_attr, _build, _context) => true; + type Keys = Array<{ - column: string, - table: string, - schema: ?string, + column: string; + table: string; + schema: string | null | undefined; }>; const identity = _ => _; export function preventEmptyResult< - // eslint-disable-next-line flowtype/no-weak-types - O: { [key: string]: (...args: Array) => string } ->(obj: O): $ObjMap(V) => V> { + O extends { + [key: string]: () => string; + } +>(obj: O): $ObjMap(a: V) => V> { return Object.keys(obj).reduce((memo, key) => { const fn = obj[key]; + memo[key] = function(...args) { const result = fn.apply(this, args); + if (typeof result !== "string" || result.length === 0) { const stringifiedArgs = require("util").inspect(args); + throw new Error( `Inflector for '${key}' returned '${String( result @@ -59,8 +62,10 @@ export function preventEmptyResult< `Arguments passed to ${key}:\n${stringifiedArgs}` ); } + return result; }; + return memo; }, {}); } @@ -72,34 +77,37 @@ const omitWithRBACChecks = omit => ( const ORDINARY_TABLE = "r"; const VIEW = "v"; const MATERIALIZED_VIEW = "m"; + const isTableLike = entity => entity && entity.kind === "class" && (entity.classKind === ORDINARY_TABLE || entity.classKind === VIEW || entity.classKind === MATERIALIZED_VIEW); + if (entity.kind === "procedure") { if (permission === EXECUTE && !entity.aclExecutable) { return true; } } else if (entity.kind === "class" && isTableLike(entity)) { const tableEntity: PgClass = entity; + if ( (permission === READ || permission === ALL || permission === MANY) && - (!tableEntity.aclSelectable && - !tableEntity.attributes.some(attr => attr.aclSelectable)) + !tableEntity.aclSelectable && + !tableEntity.attributes.some(attr => attr.aclSelectable) ) { return true; } else if ( permission === CREATE && - (!tableEntity.aclInsertable && - !tableEntity.attributes.some(attr => attr.aclInsertable)) + !tableEntity.aclInsertable && + !tableEntity.attributes.some(attr => attr.aclInsertable) ) { return true; } else if ( permission === UPDATE && - (!tableEntity.aclUpdatable && - !tableEntity.attributes.some(attr => attr.aclUpdatable)) + !tableEntity.aclUpdatable && + !tableEntity.attributes.some(attr => attr.aclUpdatable) ) { return true; } else if (permission === DELETE && !tableEntity.aclDeletable) { @@ -107,9 +115,8 @@ const omitWithRBACChecks = omit => ( } } else if (entity.kind === "attribute" && isTableLike(entity.class)) { const attributeEntity: PgAttribute = entity; + const klass = attributeEntity.class; // Have we got *any* permissions on the table? - const klass = attributeEntity.class; - // Have we got *any* permissions on the table? if ( klass.aclSelectable || klass.attributes.some(attr => attr.aclSelectable) @@ -134,6 +141,7 @@ const omitWithRBACChecks = omit => ( // explicitly @omit-ed. } } + return omit(entity, permission); }; @@ -148,6 +156,7 @@ const omitUnindexed = omit => ( ) { return true; } + if ( entity.kind === "constraint" && entity.type === "f" && @@ -155,11 +164,12 @@ const omitUnindexed = omit => ( permission === "read" ) { let klass = entity.class; + if (klass) { if (!entity._omitUnindexedReadWarningGiven) { // $FlowFixMe - entity._omitUnindexedReadWarningGiven = true; - // eslint-disable-next-line no-console + entity._omitUnindexedReadWarningGiven = true; // eslint-disable-next-line no-console + console.log( "%s", `Disabled 'read' permission for ${describePgEntity( @@ -172,8 +182,10 @@ const omitUnindexed = omit => ( ); } } + return true; } + return omit(entity, permission); }; @@ -185,6 +197,7 @@ function describePgEntity(entity: PgEntity, includeAlias = true) { entity.tags, (value, key) => key === "name" || key.endsWith("Name") ); + if (Object.keys(tags).length) { return ` (with smart comments: ${chalk.bold( Object.keys(tags) @@ -192,6 +205,7 @@ function describePgEntity(entity: PgEntity, includeAlias = true) { .join(" | ") )})`; } + return ""; }; @@ -226,10 +240,11 @@ function describePgEntity(entity: PgEntity, includeAlias = true) { } } catch (e) { // eslint-disable-next-line no-console - console.error("Error occurred while attempting to debug entity:", entity); - // eslint-disable-next-line no-console + console.error("Error occurred while attempting to debug entity:", entity); // eslint-disable-next-line no-console + console.error(e); } + return `entity of kind '${entity.kind}' with ${ typeof entity.id === "string" ? `oid '${entity.id}'` : "" }`; @@ -239,7 +254,6 @@ function sqlCommentByAddingTags(entity, tagsToAdd) { // NOTE: this function is NOT intended to be SQL safe; it's for // displaying in error messages. Nonetheless if you find issues with // SQL compatibility, please send a PR or issue. - // Ref: https://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-BACKSLASH-TABLE const escape = str => str.replace( @@ -252,11 +266,9 @@ function sqlCommentByAddingTags(entity, tagsToAdd) { "\r": "\\r", "\t": "\\t", }[chr] || "\\" + chr) - ); + ); // tagsToAdd is here twice to ensure that the keys in tagsToAdd come first, but that they also "win" any conflicts. - // tagsToAdd is here twice to ensure that the keys in tagsToAdd come first, but that they also "win" any conflicts. const tags = Object.assign({}, tagsToAdd, entity.tags, tagsToAdd); - const description = entity.description; const tagsSql = Object.keys(tags) .reduce((memo, tag) => { @@ -279,8 +291,10 @@ function sqlCommentByAddingTags(entity, tagsToAdd) { description ? "\\n" + escape(description) : "" }'`; let sqlThing; + if (entity.kind === "class") { const identifier = `"${entity.namespaceName}"."${entity.name}"`; + if (entity.classKind === "r") { sqlThing = `TABLE ${identifier}`; } else if (entity.classKind === "v") { @@ -310,23 +324,27 @@ function sqlCommentByAddingTags(entity, tagsToAdd) { return `COMMENT ON ${sqlThing} IS ${commentValue};`; } -export default (function PgBasicsPlugin( +export default function PgBasicsPlugin( builder, { pgStrictFunctions = false, pgColumnFilter = defaultPgColumnFilter, pgIgnoreRBAC = false, - pgIgnoreIndexes = true, // TODO:v5: change this to false + pgIgnoreIndexes = true, + // TODO:v5: change this to false pgLegacyJsonUuid = false, // TODO:v5: remove this } ) { let pgOmit = baseOmit; + if (!pgIgnoreRBAC) { pgOmit = omitWithRBACChecks(pgOmit); } + if (!pgIgnoreIndexes) { pgOmit = omitUnindexed(pgOmit); } + builder.hook( "build", build => { @@ -337,11 +355,9 @@ export default (function PgBasicsPlugin( pgSql: sql, pgStrictFunctions, pgColumnFilter, - // TODO:v5: remove this workaround // BEWARE: this may be overridden in PgIntrospectionPlugin for PG < 9.5 pgQueryFromResolveData: queryFromResolveDataFactory(), - pgAddStartEndCursor: addStartEndCursor, pgOmit, pgMakeProcField: makeProcField, @@ -354,12 +370,12 @@ export default (function PgBasicsPlugin( }, ["PgBasics"] ); - builder.hook( "inflection", (inflection, build) => { // TODO:v5: move this to postgraphile-core const oldBuiltin = inflection.builtin; + inflection.builtin = function(name) { if (pgLegacyJsonUuid && name === "JSON") return "Json"; if (pgLegacyJsonUuid && name === "UUID") return "Uuid"; @@ -373,30 +389,39 @@ export default (function PgBasicsPlugin( conditionType(typeName: string) { return this.upperCamelCase(`${typeName}-condition`); }, + inputType(typeName: string) { return this.upperCamelCase(`${typeName}-input`); }, + rangeBoundType(typeName: string) { return this.upperCamelCase(`${typeName}-range-bound`); }, + rangeType(typeName: string) { return this.upperCamelCase(`${typeName}-range`); }, + patchType(typeName: string) { return this.upperCamelCase(`${typeName}-patch`); }, + baseInputType(typeName: string) { return this.upperCamelCase(`${typeName}-base-input`); }, + patchField(itemName: string) { return this.camelCase(`${itemName}-patch`); }, + orderByType(typeName: string) { return this.upperCamelCase(`${this.pluralize(typeName)}-order-by`); }, + edge(typeName: string) { return this.upperCamelCase(`${this.pluralize(typeName)}-edge`); }, + connection(typeName: string) { return this.upperCamelCase( `${this.pluralize(typeName)}-connection` @@ -412,20 +437,29 @@ export default (function PgBasicsPlugin( _functionName(proc: PgProc) { return proc.tags.name || proc.name; }, + _typeName(type: PgType) { // 'type' introspection result return type.tags.name || type.name; }, + _tableName(table: PgClass) { return table.tags.name || table.type.tags.name || table.name; }, + _singularizedTableName(table: PgClass): string { return this.singularize(this._tableName(table)).replace( /.(?:(?:[_-]i|I)nput|(?:[_-]p|P)atch)$/, "$&_record" ); }, - _columnName(attr: PgAttribute, _options?: { skipRowId?: boolean }) { + + _columnName( + attr: PgAttribute, + _options?: { + skipRowId?: boolean; + } + ) { return attr.tags.name || attr.name; }, @@ -433,20 +467,25 @@ export default (function PgBasicsPlugin( enumType(type: PgType) { return this.upperCamelCase(this._typeName(type)); }, - argument(name: ?string, index: number) { + + argument(name: string | null | undefined, index: number) { return this.camelCase(name || `arg${index}`); }, + orderByEnum(columnName, ascending) { return this.constantCase( `${columnName}_${ascending ? "asc" : "desc"}` ); }, + orderByColumnEnum(attr: PgAttribute, ascending: boolean) { const columnName = this._columnName(attr, { skipRowId: true, // Because we messed up 😔 }); + return this.orderByEnum(columnName, ascending); }, + orderByComputedColumnEnum( pseudoColumnName: string, proc: PgProc, @@ -460,30 +499,30 @@ export default (function PgBasicsPlugin( ); return this.orderByEnum(columnName, ascending); }, + domainType(type: PgType) { return this.upperCamelCase(this._typeName(type)); }, + enumName(inValue: string) { let value = inValue; if (value === "") { return "_EMPTY_"; - } - - // Some enums use asterisks to signify wildcards - this might be for + } // Some enums use asterisks to signify wildcards - this might be for // the whole item, or prefixes/suffixes, or even in the middle. This // is provided on a best efforts basis, if it doesn't suit your // purposes then please pass a custom inflector as mentioned below. + value = value .replace(/\*/g, "_ASTERISK_") .replace(/^(_?)_+ASTERISK/, "$1ASTERISK") - .replace(/ASTERISK_(_?)_*$/, "ASTERISK$1"); - - // This is a best efforts replacement for common symbols that you + .replace(/ASTERISK_(_?)_*$/, "ASTERISK$1"); // This is a best efforts replacement for common symbols that you // might find in enums. Generally we only support enums that are // alphanumeric, if these replacements don't work for you, you should // pass a custom inflector that replaces this `enumName` method // with one of your own chosing. + value = { // SQL comparison operators @@ -494,13 +533,11 @@ export default (function PgBasicsPlugin( "<>": "DIFFERENT", "<=": "LESS_THAN_OR_EQUAL", "<": "LESS_THAN", - // PostgreSQL LIKE shortcuts "~~": "LIKE", "~~*": "ILIKE", "!~~": "NOT_LIKE", "!~~*": "NOT_ILIKE", - // '~' doesn't necessarily represent regexps, but the three // operators following it likely do, so we'll use the word TILDE // in all for consistency. @@ -508,7 +545,6 @@ export default (function PgBasicsPlugin( "~*": "TILDE_ASTERISK", "!~": "NOT_TILDE", "!~*": "NOT_TILDE_ASTERISK", - // A number of other symbols where we're not sure of their // meaning. We give them common generic names so that they're // suitable for multiple purposes, e.g. favouring 'PLUS' over @@ -548,22 +584,27 @@ export default (function PgBasicsPlugin( tableNode(table: PgClass) { return this.camelCase(this._singularizedTableName(table)); }, + tableFieldName(table: PgClass) { return this.camelCase(this._singularizedTableName(table)); }, + allRows(table: PgClass) { return this.camelCase( `all-${this.pluralize(this._singularizedTableName(table))}` ); }, + allRowsSimple(table: PgClass) { return this.camelCase( `all-${this.pluralize(this._singularizedTableName(table))}-list` ); }, + functionMutationName(proc: PgProc) { return this.camelCase(this._functionName(proc)); }, + functionMutationResultFieldName( proc: PgProc, gqlType, @@ -573,7 +614,9 @@ export default (function PgBasicsPlugin( if (proc.tags.resultFieldName) { return proc.tags.resultFieldName; } + let name; + if (outputArgNames.length === 1 && outputArgNames[0] !== "") { name = this.camelCase(outputArgNames[0]); } else if (gqlType.name === "Int") { @@ -590,20 +633,26 @@ export default (function PgBasicsPlugin( } else { name = this.camelCase(gqlType.name); } + return plural ? this.pluralize(name) : name; }, + functionQueryName(proc: PgProc) { return this.camelCase(this._functionName(proc)); }, + functionQueryNameList(proc: PgProc) { return this.camelCase(`${this._functionName(proc)}-list`); }, + functionPayloadType(proc: PgProc) { return this.upperCamelCase(`${this._functionName(proc)}-payload`); }, + functionInputType(proc: PgProc) { return this.upperCamelCase(`${this._functionName(proc)}-input`); }, + functionOutputFieldName( proc: PgProc, outputArgName: string, @@ -611,12 +660,15 @@ export default (function PgBasicsPlugin( ) { return this.argument(outputArgName, index); }, + tableType(table: PgClass) { return this.upperCamelCase(this._singularizedTableName(table)); }, + column(attr: PgAttribute) { return this.camelCase(this._columnName(attr)); }, + computedColumn( pseudoColumnName: string, proc: PgProc, @@ -624,6 +676,7 @@ export default (function PgBasicsPlugin( ) { return proc.tags.fieldName || this.camelCase(pseudoColumnName); }, + computedColumnList( pseudoColumnName: string, proc: PgProc, @@ -633,6 +686,7 @@ export default (function PgBasicsPlugin( ? proc.tags.fieldName + "List" : this.camelCase(`${pseudoColumnName}-list`); }, + singleRelationByKeys( detailedKeys: Keys, table: PgClass, @@ -642,12 +696,14 @@ export default (function PgBasicsPlugin( if (constraint.tags.fieldName) { return constraint.tags.fieldName; } + return this.camelCase( `${this._singularizedTableName(table)}-by-${detailedKeys .map(key => this.column(key)) .join("-and-")}` ); }, + singleRelationByKeysBackwards( detailedKeys: Keys, table: PgClass, @@ -657,9 +713,11 @@ export default (function PgBasicsPlugin( if (constraint.tags.foreignSingleFieldName) { return constraint.tags.foreignSingleFieldName; } + if (constraint.tags.foreignFieldName) { return constraint.tags.foreignFieldName; } + return this.singleRelationByKeys( detailedKeys, table, @@ -667,6 +725,7 @@ export default (function PgBasicsPlugin( constraint ); }, + manyRelationByKeys( detailedKeys: Keys, table: PgClass, @@ -676,12 +735,14 @@ export default (function PgBasicsPlugin( if (constraint.tags.foreignFieldName) { return constraint.tags.foreignFieldName; } + return this.camelCase( `${this.pluralize( this._singularizedTableName(table) )}-by-${detailedKeys.map(key => this.column(key)).join("-and-")}` ); }, + manyRelationByKeysSimple( detailedKeys: Keys, table: PgClass, @@ -691,9 +752,11 @@ export default (function PgBasicsPlugin( if (constraint.tags.foreignSimpleFieldName) { return constraint.tags.foreignSimpleFieldName; } + if (constraint.tags.foreignFieldName) { return constraint.tags.foreignFieldName; } + return this.camelCase( `${this.pluralize( this._singularizedTableName(table) @@ -702,6 +765,7 @@ export default (function PgBasicsPlugin( .join("-and-")}-list` ); }, + rowByUniqueKeys( detailedKeys: Keys, table: PgClass, @@ -710,12 +774,14 @@ export default (function PgBasicsPlugin( if (constraint.tags.fieldName) { return constraint.tags.fieldName; } + return this.camelCase( `${this._singularizedTableName(table)}-by-${detailedKeys .map(key => this.column(key)) .join("-and-")}` ); }, + updateByKeys( detailedKeys: Keys, table: PgClass, @@ -724,12 +790,14 @@ export default (function PgBasicsPlugin( if (constraint.tags.updateFieldName) { return constraint.tags.updateFieldName; } + return this.camelCase( `update-${this._singularizedTableName( table )}-by-${detailedKeys.map(key => this.column(key)).join("-and-")}` ); }, + deleteByKeys( detailedKeys: Keys, table: PgClass, @@ -738,12 +806,14 @@ export default (function PgBasicsPlugin( if (constraint.tags.deleteFieldName) { return constraint.tags.deleteFieldName; } + return this.camelCase( `delete-${this._singularizedTableName( table )}-by-${detailedKeys.map(key => this.column(key)).join("-and-")}` ); }, + updateByKeysInputType( detailedKeys: Keys, table: PgClass, @@ -754,6 +824,7 @@ export default (function PgBasicsPlugin( `${constraint.tags.updateFieldName}-input` ); } + return this.upperCamelCase( `update-${this._singularizedTableName( table @@ -762,6 +833,7 @@ export default (function PgBasicsPlugin( .join("-and-")}-input` ); }, + deleteByKeysInputType( detailedKeys: Keys, table: PgClass, @@ -772,6 +844,7 @@ export default (function PgBasicsPlugin( `${constraint.tags.deleteFieldName}-input` ); } + return this.upperCamelCase( `delete-${this._singularizedTableName( table @@ -780,78 +853,94 @@ export default (function PgBasicsPlugin( .join("-and-")}-input` ); }, + updateNode(table: PgClass) { return this.camelCase( `update-${this._singularizedTableName(table)}` ); }, + deleteNode(table: PgClass) { return this.camelCase( `delete-${this._singularizedTableName(table)}` ); }, + deletedNodeId(table: PgClass) { return this.camelCase(`deleted-${this.singularize(table.name)}-id`); }, + updateNodeInputType(table: PgClass) { return this.upperCamelCase( `update-${this._singularizedTableName(table)}-input` ); }, + deleteNodeInputType(table: PgClass) { return this.upperCamelCase( `delete-${this._singularizedTableName(table)}-input` ); }, + edgeField(table: PgClass) { return this.camelCase(`${this._singularizedTableName(table)}-edge`); }, + recordFunctionReturnType(proc: PgProc) { return ( proc.tags.resultTypeName || this.upperCamelCase(`${this._functionName(proc)}-record`) ); }, + recordFunctionConnection(proc: PgProc) { return this.upperCamelCase( `${this._functionName(proc)}-connection` ); }, + recordFunctionEdge(proc: PgProc) { return this.upperCamelCase( `${this.singularize(this._functionName(proc))}-edge` ); }, + scalarFunctionConnection(proc: PgProc) { return this.upperCamelCase( `${this._functionName(proc)}-connection` ); }, + scalarFunctionEdge(proc: PgProc) { return this.upperCamelCase( `${this.singularize(this._functionName(proc))}-edge` ); }, + createField(table: PgClass) { return this.camelCase( `create-${this._singularizedTableName(table)}` ); }, + createInputType(table: PgClass) { return this.upperCamelCase( `create-${this._singularizedTableName(table)}-input` ); }, + createPayloadType(table: PgClass) { return this.upperCamelCase( `create-${this._singularizedTableName(table)}-payload` ); }, + updatePayloadType(table: PgClass) { return this.upperCamelCase( `update-${this._singularizedTableName(table)}-payload` ); }, + deletePayloadType(table: PgClass) { return this.upperCamelCase( `delete-${this._singularizedTableName(table)}-payload` @@ -863,4 +952,4 @@ export default (function PgBasicsPlugin( }, ["PgBasics"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgColumnDeprecationPlugin.js b/packages/graphile-build-pg/src/plugins/PgColumnDeprecationPlugin.ts similarity index 82% rename from packages/graphile-build-pg/src/plugins/PgColumnDeprecationPlugin.js rename to packages/graphile-build-pg/src/plugins/PgColumnDeprecationPlugin.ts index d75d72767..b1a580aa6 100644 --- a/packages/graphile-build-pg/src/plugins/PgColumnDeprecationPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgColumnDeprecationPlugin.ts @@ -1,13 +1,12 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PgColumnDeprecationPlugin(builder) { +import { Plugin } from "graphile-build"; +export default function PgColumnDeprecationPlugin(builder) { builder.hook( "GraphQLObjectType:fields:field", (field, build, context) => { const { scope: { pgFieldIntrospection }, } = context; + if ( !pgFieldIntrospection || !pgFieldIntrospection.tags || @@ -15,6 +14,7 @@ export default (function PgColumnDeprecationPlugin(builder) { ) { return field; } + return { ...field, deprecationReason: Array.isArray(pgFieldIntrospection.tags.deprecated) @@ -24,4 +24,4 @@ export default (function PgColumnDeprecationPlugin(builder) { }, ["PgColumnDeprecation"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js b/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.ts similarity index 95% rename from packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js rename to packages/graphile-build-pg/src/plugins/PgColumnsPlugin.ts index 6ecd9685b..7f2bd4746 100644 --- a/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.ts @@ -1,10 +1,9 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; const nullableIf = (GraphQLNonNull, condition, Type) => condition ? Type : new GraphQLNonNull(Type); -export default (function PgColumnsPlugin(builder) { +export default function PgColumnsPlugin(builder) { builder.hook( "build", build => { @@ -13,6 +12,7 @@ export default (function PgColumnsPlugin(builder) { pgTweakFragmentForTypeAndModifier, pgQueryFromResolveData: queryFromResolveData, } = build; + const getSelectValueForFieldAndTypeAndModifier = ( ReturnType, fieldScope, @@ -22,6 +22,7 @@ export default (function PgColumnsPlugin(builder) { typeModifier ) => { const { getDataFromParsedResolveInfoFragment } = fieldScope; + if (type.isPgArray) { const ident = sql.identifier(Symbol()); return sql.fragment` @@ -49,12 +50,16 @@ export default (function PgColumnsPlugin(builder) { parsedResolveInfoFragment, ReturnType ); + if (type.type === "c") { const jsonBuildObject = queryFromResolveData( sql.identifier(Symbol()), // Ignore! sqlFullName, resolveData, - { onlyJsonField: true, addNullCase: true } + { + onlyJsonField: true, + addNullCase: true, + } ); return jsonBuildObject; } else { @@ -67,6 +72,7 @@ export default (function PgColumnsPlugin(builder) { } } }; + return build.extend(build, { pgGetSelectValueForFieldAndTypeAndModifier: getSelectValueForFieldAndTypeAndModifier, }); @@ -75,7 +81,6 @@ export default (function PgColumnsPlugin(builder) { [], ["PgTypes"] ); - builder.hook( "GraphQLObjectType:fields", (fields, build, context) => { @@ -111,8 +116,8 @@ export default (function PgColumnsPlugin(builder) { // PERFORMANCE: These used to be .filter(...) calls if (!pgColumnFilter(attr, build, context)) return memo; if (omit(attr, "read")) return memo; - const fieldName = inflection.column(attr); + if (memo[fieldName]) { throw new Error( `Two columns produce the same GraphQL field name '${fieldName}' on class '${ @@ -120,6 +125,7 @@ export default (function PgColumnsPlugin(builder) { }.${table.name}'; one of them is '${attr.name}'` ); } + memo = extend( memo, { @@ -165,7 +171,9 @@ export default (function PgColumnsPlugin(builder) { }, }; }, - { pgFieldIntrospection: attr } + { + pgFieldIntrospection: attr, + } ), }, `Adding field for ${describePgEntity( @@ -208,6 +216,7 @@ export default (function PgColumnsPlugin(builder) { }, fieldWithHooks, } = context; + if ( !(isPgRowType || isPgCompoundType) || !table || @@ -215,6 +224,7 @@ export default (function PgColumnsPlugin(builder) { ) { return fields; } + return extend( fields, table.attributes.reduce((memo, attr) => { @@ -227,8 +237,8 @@ export default (function PgColumnsPlugin(builder) { : "create"; if (omit(attr, action)) return memo; if (attr.identity === "a") return memo; - const fieldName = inflection.column(attr); + if (memo[fieldName]) { throw new Error( `Two columns produce the same GraphQL field name '${fieldName}' on input class '${ @@ -236,6 +246,7 @@ export default (function PgColumnsPlugin(builder) { }.${table.name}'; one of them is '${attr.name}'` ); } + memo = extend( memo, { @@ -265,7 +276,9 @@ export default (function PgColumnsPlugin(builder) { }, attr.typeModifier ), - { pgFieldIntrospection: attr } + { + pgFieldIntrospection: attr, + } ), }, `Adding input object field for ${describePgEntity( @@ -284,4 +297,4 @@ export default (function PgColumnsPlugin(builder) { }, ["PgColumns"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.js b/packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.ts similarity index 93% rename from packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.js rename to packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.ts index ba9f2ca84..1598ec5c2 100644 --- a/packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.ts @@ -1,8 +1,6 @@ -// @flow -import type { Plugin, Build } from "graphile-build"; -import type { PgClass, PgProc } from "./PgIntrospectionPlugin"; +import { Plugin, Build } from "graphile-build"; +import { PgClass, PgProc } from "./PgIntrospectionPlugin"; // This interface is not official yet, don't rely on it. -// This interface is not official yet, don't rely on it. export const getComputedColumnDetails = ( build: Build, table: PgClass, @@ -13,7 +11,6 @@ export const getComputedColumnDetails = ( if (!proc.name.startsWith(`${table.name}_`)) return null; if (proc.argTypeIds.length < 1) return null; if (proc.argTypeIds[0] !== table.type.id) return null; - const argTypes = proc.argTypeIds.reduce((prev, typeId, idx) => { if ( proc.argModes.length === 0 || // all args are `in` @@ -22,8 +19,10 @@ export const getComputedColumnDetails = ( ) { prev.push(build.pgIntrospectionResultsByKind.typeById[typeId]); } + return prev; }, []); + if ( argTypes .slice(1) @@ -34,10 +33,12 @@ export const getComputedColumnDetails = ( } const pseudoColumnName = proc.name.substr(table.name.length + 1); - return { argTypes, pseudoColumnName }; + return { + argTypes, + pseudoColumnName, + }; }; - -export default (function PgComputedColumnsPlugin( +export default function PgComputedColumnsPlugin( builder, { pgSimpleCollections } ) { @@ -76,9 +77,11 @@ export default (function PgComputedColumnsPlugin( sqlCommentByAddingTags, } = build; const tableType = table.type; + if (!tableType) { throw new Error("Could not determine the type for this table"); } + return extend( fields, introspectionResultsByKind.procedure.reduce((memo, proc) => { @@ -90,10 +93,12 @@ export default (function PgComputedColumnsPlugin( ); if (!computedColumnDetails) return memo; const { pseudoColumnName } = computedColumnDetails; + function makeField(forceList) { const fieldName = forceList ? inflection.computedColumnList(pseudoColumnName, proc, table) : inflection.computedColumn(pseudoColumnName, proc, table); + try { memo = extend( memo, @@ -117,17 +122,21 @@ export default (function PgComputedColumnsPlugin( swallowError(e); } } + const simpleCollections = proc.tags.simpleCollections || pgSimpleCollections; const hasConnections = simpleCollections !== "only"; const hasSimpleCollections = simpleCollections === "only" || simpleCollections === "both"; + if (!proc.returnsSet || hasConnections) { makeField(false); } + if (proc.returnsSet && hasSimpleCollections) { makeField(true); } + return memo; }, {}), `Adding computed column to '${Self.name}'` @@ -135,4 +144,4 @@ export default (function PgComputedColumnsPlugin( }, ["PgComputedColumns"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgConditionComputedColumnPlugin.js b/packages/graphile-build-pg/src/plugins/PgConditionComputedColumnPlugin.ts similarity index 91% rename from packages/graphile-build-pg/src/plugins/PgConditionComputedColumnPlugin.js rename to packages/graphile-build-pg/src/plugins/PgConditionComputedColumnPlugin.ts index a5ca895ba..ecb0cb6dd 100644 --- a/packages/graphile-build-pg/src/plugins/PgConditionComputedColumnPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgConditionComputedColumnPlugin.ts @@ -1,7 +1,7 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import { getComputedColumnDetails } from "./PgComputedColumnsPlugin"; import assert from "assert"; + function getCompatibleComputedColumns(build, table) { const { pgIntrospectionResultsByKind: introspectionResultsByKind, @@ -10,23 +10,20 @@ function getCompatibleComputedColumns(build, table) { return introspectionResultsByKind.procedure.reduce((memo, proc) => { /* ALSO SEE PgOrderComputedColumnsPlugin */ // Must be marked @filterable - if (!proc.tags.filterable) return memo; + if (!proc.tags.filterable) return memo; // Must not be omitted - // Must not be omitted - if (omit(proc, "execute")) return memo; + if (omit(proc, "execute")) return memo; // Must be a computed column - // Must be a computed column const computedColumnDetails = getComputedColumnDetails(build, table, proc); if (!computedColumnDetails) return memo; - const { pseudoColumnName } = computedColumnDetails; + const { pseudoColumnName } = computedColumnDetails; // Must have only one required argument - // Must have only one required argument const nonOptionalArgumentsCount = proc.argDefaultsNum - proc.inputArgsCount; + if (nonOptionalArgumentsCount > 1) { return memo; - } + } // Must return a scalar - // Must return a scalar if (proc.returnsSet) return memo; const returnType = introspectionResultsByKind.typeById[proc.returnTypeId]; if (returnType.isPgArray) return memo; @@ -36,15 +33,18 @@ function getCompatibleComputedColumns(build, table) { const isRecordLike = returnType.id === "2249"; if (isRecordLike) return memo; const isVoid = String(returnType.id) === "2278"; - if (isVoid) return memo; + if (isVoid) return memo; // Looks good - // Looks good - memo.push({ proc, pseudoColumnName, returnType }); + memo.push({ + proc, + pseudoColumnName, + returnType, + }); return memo; }, []); } -export default (function PgConditionComputedColumnPlugin(builder) { +export default function PgConditionComputedColumnPlugin(builder) { builder.hook( "GraphQLInputObjectType:fields", (fields, build, context) => { @@ -58,9 +58,11 @@ export default (function PgConditionComputedColumnPlugin(builder) { scope: { isPgCondition, pgIntrospection: table }, fieldWithHooks, } = context; + if (!isPgCondition || !table || table.kind !== "class") { return fields; } + const compatibleComputedColumns = getCompatibleComputedColumns( build, table @@ -103,7 +105,6 @@ export default (function PgConditionComputedColumnPlugin(builder) { }, ["PgConditionComputedColumn"] ); - builder.hook( "GraphQLObjectType:fields:field:args", (args, build, context) => { @@ -125,13 +126,14 @@ export default (function PgConditionComputedColumnPlugin(builder) { }, addArgDataGenerator, } = context; - const shouldAddCondition = isPgFieldConnection || isPgFieldSimpleCollection; if (!shouldAddCondition) return args; + if (!args.condition) { return args; } + const proc = pgFieldIntrospection.kind === "procedure" ? pgFieldIntrospection : null; const table = @@ -140,6 +142,7 @@ export default (function PgConditionComputedColumnPlugin(builder) { : proc ? pgFieldIntrospectionTable : null; + if ( !table || table.kind !== "class" || @@ -148,24 +151,25 @@ export default (function PgConditionComputedColumnPlugin(builder) { ) { return args; } + const TableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null); const TableConditionType = getTypeByName( inflection.conditionType(TableType.name) ); + if (!TableConditionType) { return args; } + assert( getNullableType(args.condition.type) === TableConditionType, "Condition is present, but doesn't match?" ); - const compatibleComputedColumns = getCompatibleComputedColumns( build, table ).map(o => { const { proc, pseudoColumnName } = o; - const fieldName = inflection.computedColumn( pseudoColumnName, proc, @@ -186,6 +190,7 @@ export default (function PgConditionComputedColumnPlugin(builder) { ({ fieldName, sqlFnName, returnType }) => { const val = condition[fieldName]; const sqlCall = sql.fragment`${sqlFnName}(${queryBuilder.getTableAlias()})`; + if (val != null) { queryBuilder.where( sql.fragment`${sqlCall} = ${gql2pg( @@ -203,9 +208,8 @@ export default (function PgConditionComputedColumnPlugin(builder) { }, }; }); - return args; }, ["PgConditionComputedColumn"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.js b/packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.ts similarity index 97% rename from packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.js rename to packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.ts index e647317a5..45843b9eb 100644 --- a/packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.js +++ b/packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PgConnectionArgCondition(builder) { +import { Plugin } from "graphile-build"; +export default function PgConnectionArgCondition(builder) { builder.hook( "init", (_, build) => { @@ -20,9 +18,9 @@ export default (function PgConnectionArgCondition(builder) { // PERFORMANCE: These used to be .filter(...) calls if (!table.isSelectable || omit(table, "filter")) return; if (!table.namespace) return; - const tableTypeName = inflection.tableType(table); /* const TableConditionType = */ + newWithHooks( GraphQLInputObjectType, { @@ -34,7 +32,6 @@ export default (function PgConnectionArgCondition(builder) { // PERFORMANCE: These used to be .filter(...) calls if (!pgColumnFilter(attr, build, context)) return memo; if (omit(attr, "filter")) return memo; - const fieldName = inflection.column(attr); memo = build.extend( memo, @@ -81,7 +78,6 @@ export default (function PgConnectionArgCondition(builder) { [], ["PgTypes"] ); - builder.hook( "GraphQLObjectType:fields:field:args", (args, build, context) => { @@ -106,11 +102,9 @@ export default (function PgConnectionArgCondition(builder) { Self, field, } = context; - const shouldAddCondition = isPgFieldConnection || isPgFieldSimpleCollection; if (!shouldAddCondition) return args; - const proc = pgFieldIntrospection.kind === "procedure" ? pgFieldIntrospection : null; const table = @@ -119,6 +113,7 @@ export default (function PgConnectionArgCondition(builder) { : proc ? pgFieldIntrospectionTable : null; + if ( !table || table.kind !== "class" || @@ -127,6 +122,7 @@ export default (function PgConnectionArgCondition(builder) { ) { return args; } + if (proc) { if (!proc.tags.filterable) { return args; @@ -137,6 +133,7 @@ export default (function PgConnectionArgCondition(builder) { const TableConditionType = getTypeByName( inflection.conditionType(TableType.name) ); + if (!TableConditionType) { return args; } @@ -144,7 +141,6 @@ export default (function PgConnectionArgCondition(builder) { const relevantAttributes = table.attributes.filter( attr => pgColumnFilter(attr, build, context) && !omit(attr, "filter") ); - addArgDataGenerator(function connectionCondition({ condition }) { return { pgQuery: queryBuilder => { @@ -152,6 +148,7 @@ export default (function PgConnectionArgCondition(builder) { relevantAttributes.forEach(attr => { const fieldName = inflection.column(attr); const val = condition[fieldName]; + if (val != null) { queryBuilder.addLiveCondition(() => record => record[attr.name] === val @@ -176,7 +173,6 @@ export default (function PgConnectionArgCondition(builder) { }, }; }); - return extend( args, { @@ -191,4 +187,4 @@ export default (function PgConnectionArgCondition(builder) { }, ["PgConnectionArgCondition"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgConnectionArgFirstLastBeforeAfter.js b/packages/graphile-build-pg/src/plugins/PgConnectionArgFirstLastBeforeAfter.ts similarity index 96% rename from packages/graphile-build-pg/src/plugins/PgConnectionArgFirstLastBeforeAfter.js rename to packages/graphile-build-pg/src/plugins/PgConnectionArgFirstLastBeforeAfter.ts index 602b0046e..2b457d7c7 100644 --- a/packages/graphile-build-pg/src/plugins/PgConnectionArgFirstLastBeforeAfter.js +++ b/packages/graphile-build-pg/src/plugins/PgConnectionArgFirstLastBeforeAfter.ts @@ -1,9 +1,8 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; const base64Decode = str => Buffer.from(String(str), "base64").toString("utf8"); -export default (function PgConnectionArgs(builder) { +export default function PgConnectionArgs(builder) { builder.hook( "GraphQLObjectType:fields:field:args", (args, build, context) => { @@ -30,8 +29,8 @@ export default (function PgConnectionArgs(builder) { ) { return args; } - const Cursor = getTypeByName("Cursor"); + const Cursor = getTypeByName("Cursor"); addArgDataGenerator(function connectionFirstLastBeforeAfter({ first, offset, @@ -44,27 +43,33 @@ export default (function PgConnectionArgs(builder) { if (first != null) { queryBuilder.first(first); } + if (offset != null) { queryBuilder.offset(offset); } + if (isPgFieldConnection) { if (after != null) { addCursorConstraint(after, true); } + if (before != null) { addCursorConstraint(before, false); } + if (last != null) { if (first != null) { throw new Error( "We don't support setting both first and last" ); } + if (offset != null) { throw new Error( "We don't support setting both offset and last" ); } + queryBuilder.last(last); } } @@ -82,7 +87,6 @@ export default (function PgConnectionArgs(builder) { }, }; }); - return extend( args, { @@ -130,4 +134,4 @@ export default (function PgConnectionArgs(builder) { }, ["PgConnectionArgFirstLastBeforeAfter"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.js b/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.ts similarity index 95% rename from packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.js rename to packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.ts index 9e3a4ff9e..b9d62d97b 100644 --- a/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.js +++ b/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.ts @@ -1,8 +1,6 @@ -// @flow import isString from "lodash/isString"; -import type { Plugin } from "graphile-build"; - -export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { +import { Plugin } from "graphile-build"; +export default function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { builder.hook( "init", (_, build) => { @@ -19,9 +17,9 @@ export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { // PERFORMANCE: These used to be .filter(...) calls if (!table.isSelectable || omit(table, "order")) return; if (!table.namespace) return; - const tableTypeName = inflection.tableType(table); /* const TableOrderByType = */ + newWithHooks( GraphQLEnumType, { @@ -54,7 +52,6 @@ export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { }, ["PgConnectionArgOrderBy"] ); - builder.hook( "GraphQLObjectType:fields:field:args", (args, build, context) => { @@ -91,6 +88,7 @@ export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { : proc ? pgFieldIntrospectionTable : null; + if ( !table || !table.namespace || @@ -99,28 +97,34 @@ export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { ) { return args; } + if (proc) { if (!proc.tags.sortable) { return args; } } + const TableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null); const tableTypeName = TableType.name; const TableOrderByType = getTypeByName( inflection.orderByType(tableTypeName) ); + const cursorPrefixFromOrderBy = orderBy => { if (orderBy) { let cursorPrefixes = []; + for (const item of orderBy) { if (item.alias) { cursorPrefixes.push(sql.literal(item.alias)); } } + if (cursorPrefixes.length > 0) { return cursorPrefixes; } } + return null; }; @@ -145,9 +149,9 @@ export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { ? sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( col )}` - : col; - // If the enum specifies null ordering, use that + : col; // If the enum specifies null ordering, use that // Otherwise, use the orderByNullsLast option if present + const nullsFirst = specNullsFirst != null ? specNullsFirst @@ -156,6 +160,7 @@ export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { : undefined; queryBuilder.orderBy(expr, ascending, nullsFirst); }); + if (unique) { queryBuilder.setOrderIsUnique(); } @@ -164,7 +169,6 @@ export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { }, }; }); - return extend( args, { @@ -178,4 +182,4 @@ export default (function PgConnectionArgOrderBy(builder, { orderByNullsLast }) { }, ["PgConnectionArgOrderBy"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderByDefaultValue.js b/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderByDefaultValue.ts similarity index 90% rename from packages/graphile-build-pg/src/plugins/PgConnectionArgOrderByDefaultValue.js rename to packages/graphile-build-pg/src/plugins/PgConnectionArgOrderByDefaultValue.ts index 1043bd245..392e0f393 100644 --- a/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderByDefaultValue.js +++ b/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderByDefaultValue.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PgConnectionArgOrderByDefaultValue(builder) { +import { Plugin } from "graphile-build"; +export default function PgConnectionArgOrderByDefaultValue(builder) { builder.hook( "GraphQLObjectType:fields:field:args", (args, build, context) => { @@ -27,11 +25,13 @@ export default (function PgConnectionArgOrderByDefaultValue(builder) { ) { return args; } + const TableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null); const tableTypeName = TableType.name; const TableOrderByType = getTypeByName( inflection.orderByType(tableTypeName) ); + if (!TableOrderByType) { return args; } @@ -39,7 +39,6 @@ export default (function PgConnectionArgOrderByDefaultValue(builder) { const defaultValueEnum = TableOrderByType.getValues().find(v => v.name === "PRIMARY_KEY_ASC") || TableOrderByType.getValues()[0]; - return extend(args, { orderBy: extend( args.orderBy, @@ -54,4 +53,4 @@ export default (function PgConnectionArgOrderByDefaultValue(builder) { }, ["PgConnectionArgOrderByDefaultValue"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgConnectionTotalCount.js b/packages/graphile-build-pg/src/plugins/PgConnectionTotalCount.ts similarity index 93% rename from packages/graphile-build-pg/src/plugins/PgConnectionTotalCount.js rename to packages/graphile-build-pg/src/plugins/PgConnectionTotalCount.ts index 279eafe9a..1be3fc53b 100644 --- a/packages/graphile-build-pg/src/plugins/PgConnectionTotalCount.js +++ b/packages/graphile-build-pg/src/plugins/PgConnectionTotalCount.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PgConnectionTotalCount(builder) { +import { Plugin } from "graphile-build"; +export default function PgConnectionTotalCount(builder) { builder.hook( "GraphQLObjectType:fields", (fields, build, context) => { @@ -25,8 +23,8 @@ export default (function PgConnectionTotalCount(builder) { ) { return fields; } - const tableTypeName = inflection.tableType(table); + const tableTypeName = inflection.tableType(table); return extend( fields, { @@ -46,6 +44,7 @@ export default (function PgConnectionTotalCount(builder) { return { description: `The count of *all* \`${tableTypeName}\` you could get from the connection.`, type: new GraphQLNonNull(GraphQLInt), + resolve(parent) { return ( (parent.aggregates && parent.aggregates.totalCount) || 0 @@ -63,4 +62,4 @@ export default (function PgConnectionTotalCount(builder) { }, ["PgConnectionTotalCount"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.js b/packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.ts similarity index 94% rename from packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.js rename to packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.ts index fe74584c3..d65a7321d 100644 --- a/packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.ts @@ -1,10 +1,7 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import debugFactory from "debug"; - const debug = debugFactory("graphile-build-pg"); - -export default (function PgForwardRelationPlugin(builder, { subscriptions }) { +export default function PgForwardRelationPlugin(builder, { subscriptions }) { builder.hook( "GraphQLObjectType:fields", (fields, build, context) => { @@ -31,8 +28,8 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { fieldWithHooks, Self, } = context; - const table = pgIntrospectionTable || pgIntrospection; + if ( !(isPgRowType || isMutationPayload) || !table || @@ -40,30 +37,31 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { !table.namespace ) { return fields; - } - // This is a relation in which we (table) are local, and there's a foreign table + } // This is a relation in which we (table) are local, and there's a foreign table const foreignKeyConstraints = table.constraints.filter( con => con.type === "f" ); - return extend( fields, foreignKeyConstraints.reduce((memo, constraint) => { if (omit(constraint, "read")) { return memo; } + const gqlTableType = pgGetGqlTypeByTypeIdAndModifier( table.type.id, null ); const tableTypeName = gqlTableType.name; + if (!gqlTableType) { debug( `Could not determine type for table with id ${constraint.classId}` ); return memo; } + const foreignTable = introspectionResultsByKind.classById[constraint.foreignClassId]; const gqlForeignTableType = pgGetGqlTypeByTypeIdAndModifier( @@ -71,6 +69,7 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { null ); const foreignTableTypeName = gqlForeignTableType.name; + if (!gqlForeignTableType) { debug( `Could not determine type for foreign table with id ${ @@ -79,6 +78,7 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { ); return memo; } + if (!foreignTable) { throw new Error( `Could not find the foreign table (constraint: ${ @@ -86,19 +86,23 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { })` ); } + if (omit(foreignTable, "read")) { return memo; } - const foreignSchema = foreignTable.namespace; + const foreignSchema = foreignTable.namespace; const keys = constraint.keyAttributes; const foreignKeys = constraint.foreignKeyAttributes; + if (!keys.every(_ => _) || !foreignKeys.every(_ => _)) { throw new Error("Could not find key columns!"); } + if (keys.some(key => omit(key, "read"))) { return memo; } + if (foreignKeys.some(key => omit(key, "read"))) { return memo; } @@ -109,7 +113,6 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { table, constraint ); - memo = extend( memo, { @@ -136,14 +139,17 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { foreignTableAlias, resolveData, { - useAsterisk: false, // Because it's only a single relation, no need + useAsterisk: false, + // Because it's only a single relation, no need asJson: true, }, innerQueryBuilder => { innerQueryBuilder.parentQueryBuilder = queryBuilder; + if (subscriptions && table.primaryKeyConstraint) { queryBuilder.selectIdentifiers(table); } + if ( subscriptions && foreignTable.primaryKeyConstraint @@ -152,6 +158,7 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { foreignTable ); } + keys.forEach((key, i) => { innerQueryBuilder.where( sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( @@ -173,7 +180,8 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { description: constraint.tags.forwardDescription || `Reads a single \`${foreignTableTypeName}\` that is related to this \`${tableTypeName}\`.`, - type: gqlForeignTableType, // Nullable since RLS may forbid fetching + type: gqlForeignTableType, + // Nullable since RLS may forbid fetching resolve: (rawData, _args, resolveContext, resolveInfo) => { const data = isMutationPayload ? rawData.data : rawData; if (!data) return null; @@ -181,6 +189,7 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { resolveInfo ); const record = data[safeAlias]; + if (record && resolveContext.liveRecord) { resolveContext.liveRecord( "pg", @@ -188,6 +197,7 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { record.__identifiers ); } + return record; }, }; @@ -214,4 +224,4 @@ export default (function PgForwardRelationPlugin(builder, { subscriptions }) { }, ["PgForwardRelation"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.ts similarity index 81% rename from packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js rename to packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.ts index 367192c18..b38bc95a2 100644 --- a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.ts @@ -1,5 +1,4 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import withPgClient, { quacksLikePgPool } from "../withPgClient"; import { parseTags } from "../utils"; import { readFile as rawReadFile } from "fs"; @@ -9,182 +8,178 @@ import chalk from "chalk"; import throttle from "lodash/throttle"; import flatMap from "lodash/flatMap"; import { makeIntrospectionQuery } from "./introspectionQuery"; - import { version } from "../../package.json"; import queryFromResolveDataFactory from "../queryFromResolveDataFactory"; - const debug = debugFactory("graphile-build-pg"); -const WATCH_FIXTURES_PATH = `${__dirname}/../../res/watch-fixtures.sql`; - -// Ref: https://github.com/graphile/postgraphile/tree/master/src/postgres/introspection/object +const WATCH_FIXTURES_PATH = `${__dirname}/../../res/watch-fixtures.sql`; // Ref: https://github.com/graphile/postgraphile/tree/master/src/postgres/introspection/object export type PgNamespace = { - kind: "namespace", - id: string, - name: string, - comment: ?string, - description: ?string, - tags: { [string]: string }, + kind: "namespace"; + id: string; + name: string; + comment: string | null | undefined; + description: string | null | undefined; + tags: { + [a: string]: string; + }; }; - export type PgProc = { - kind: "procedure", - id: string, - name: string, - comment: ?string, - description: ?string, - namespaceId: string, - namespaceName: string, - isStrict: boolean, - returnsSet: boolean, - isStable: boolean, - returnTypeId: string, - argTypeIds: Array, - argNames: Array, - argModes: Array<"i" | "o" | "b" | "v" | "t">, - inputArgsCount: number, - argDefaultsNum: number, - namespace: PgNamespace, - tags: { [string]: string }, - cost: number, - aclExecutable: boolean, - language: string, + kind: "procedure"; + id: string; + name: string; + comment: string | null | undefined; + description: string | null | undefined; + namespaceId: string; + namespaceName: string; + isStrict: boolean; + returnsSet: boolean; + isStable: boolean; + returnTypeId: string; + argTypeIds: Array; + argNames: Array; + argModes: Array<"i" | "o" | "b" | "v" | "t">; + inputArgsCount: number; + argDefaultsNum: number; + namespace: PgNamespace; + tags: { + [a: string]: string; + }; + cost: number; + aclExecutable: boolean; + language: string; }; - export type PgClass = { - kind: "class", - id: string, - name: string, - comment: ?string, - description: ?string, - classKind: string, - namespaceId: string, - namespaceName: string, - typeId: string, - isSelectable: boolean, - isInsertable: boolean, - isUpdatable: boolean, - isDeletable: boolean, - isExtensionConfigurationTable: boolean, - namespace: PgNamespace, - type: PgType, - tags: { [string]: string }, - attributes: [PgAttribute], - constraints: [PgConstraint], - foreignConstraints: [PgConstraint], - primaryKeyConstraint: ?PgConstraint, - aclSelectable: boolean, - aclInsertable: boolean, - aclUpdatable: boolean, - aclDeletable: boolean, - canUseAsterisk: boolean, + kind: "class"; + id: string; + name: string; + comment: string | null | undefined; + description: string | null | undefined; + classKind: string; + namespaceId: string; + namespaceName: string; + typeId: string; + isSelectable: boolean; + isInsertable: boolean; + isUpdatable: boolean; + isDeletable: boolean; + isExtensionConfigurationTable: boolean; + namespace: PgNamespace; + type: PgType; + tags: { + [a: string]: string; + }; + attributes: [PgAttribute]; + constraints: [PgConstraint]; + foreignConstraints: [PgConstraint]; + primaryKeyConstraint: PgConstraint | null | undefined; + aclSelectable: boolean; + aclInsertable: boolean; + aclUpdatable: boolean; + aclDeletable: boolean; + canUseAsterisk: boolean; }; - export type PgType = { - kind: "type", - id: string, - name: string, - comment: ?string, - description: ?string, - namespaceId: string, - namespaceName: string, - type: string, - category: string, - domainIsNotNull: boolean, - arrayItemTypeId: ?string, - arrayItemType: ?PgType, - arrayType: ?PgType, - typeLength: ?number, - isPgArray: boolean, - classId: ?string, - domainBaseTypeId: ?string, - domainTypeModifier: ?number, - tags: { [string]: string }, + kind: "type"; + id: string; + name: string; + comment: string | null | undefined; + description: string | null | undefined; + namespaceId: string; + namespaceName: string; + type: string; + category: string; + domainIsNotNull: boolean; + arrayItemTypeId: string | null | undefined; + arrayItemType: PgType | null | undefined; + arrayType: PgType | null | undefined; + typeLength: number | null | undefined; + isPgArray: boolean; + classId: string | null | undefined; + domainBaseTypeId: string | null | undefined; + domainTypeModifier: number | null | undefined; + tags: { + [a: string]: string; + }; }; - export type PgAttribute = { - kind: "attribute", - classId: string, - num: number, - name: string, - comment: ?string, - description: ?string, - typeId: string, - typeModifier: number, - isNotNull: boolean, - hasDefault: boolean, - identity: "" | "a" | "d", - class: PgClass, - type: PgType, - namespace: PgNamespace, - tags: { [string]: string }, - aclSelectable: boolean, - aclInsertable: boolean, - aclUpdatable: boolean, - isIndexed: ?boolean, - isUnique: ?boolean, - columnLevelSelectGrant: boolean, + kind: "attribute"; + classId: string; + num: number; + name: string; + comment: string | null | undefined; + description: string | null | undefined; + typeId: string; + typeModifier: number; + isNotNull: boolean; + hasDefault: boolean; + identity: "" | "a" | "d"; + class: PgClass; + type: PgType; + namespace: PgNamespace; + tags: { + [a: string]: string; + }; + aclSelectable: boolean; + aclInsertable: boolean; + aclUpdatable: boolean; + isIndexed: boolean | null | undefined; + isUnique: boolean | null | undefined; + columnLevelSelectGrant: boolean; }; - export type PgConstraint = { - kind: "constraint", - id: string, - name: string, - type: string, - classId: string, - class: PgClass, - foreignClassId: ?string, - foreignClass: ?PgClass, - comment: ?string, - description: ?string, - keyAttributeNums: Array, - keyAttributes: [PgAttribute], - foreignKeyAttributeNums: Array, - foreignKeyAttributes: [PgAttribute], - namespace: PgNamespace, - isIndexed: ?boolean, - tags: { [string]: string }, + kind: "constraint"; + id: string; + name: string; + type: string; + classId: string; + class: PgClass; + foreignClassId: string | null | undefined; + foreignClass: PgClass | null | undefined; + comment: string | null | undefined; + description: string | null | undefined; + keyAttributeNums: Array; + keyAttributes: [PgAttribute]; + foreignKeyAttributeNums: Array; + foreignKeyAttributes: [PgAttribute]; + namespace: PgNamespace; + isIndexed: boolean | null | undefined; + tags: { + [a: string]: string; + }; }; - export type PgExtension = { - kind: "extension", - id: string, - name: string, - namespaceId: string, - namespaceName: string, - relocatable: boolean, - version: string, - configurationClassIds?: Array, - comment: ?string, - description: ?string, - tags: { [string]: string }, + kind: "extension"; + id: string; + name: string; + namespaceId: string; + namespaceName: string; + relocatable: boolean; + version: string; + configurationClassIds?: Array; + comment: string | null | undefined; + description: string | null | undefined; + tags: { + [a: string]: string; + }; }; - export type PgIndex = { - kind: "index", - id: string, - name: string, - namespaceName: string, - classId: string, - numberOfAttributes: number, - indexType: string, - isUnique: boolean, - isPrimary: boolean, - /* - Though these exist, we don't want to officially - support them yet. - - isImmediate: boolean, - isReplicaIdentity: boolean, - isValid: boolean, - */ - attributeNums: Array, - attributePropertiesAsc: ?Array, - attributePropertiesNullsFirst: ?Array, - description: ?string, - tags: { [string]: string }, + kind: "index"; + id: string; + name: string; + namespaceName: string; + classId: string; + numberOfAttributes: number; + indexType: string; + isUnique: boolean; + isPrimary: boolean; + attributeNums: Array; + attributePropertiesAsc: Array | null | undefined; + attributePropertiesNullsFirst: Array | null | undefined; + description: string | null | undefined; + tags: { + [a: string]: string; + }; }; - export type PgEntity = | PgNamespace | PgProc @@ -206,12 +201,14 @@ function readFile(filename, encoding) { const removeQuotes = str => { const trimmed = str.trim(); + if (trimmed[0] === '"') { if (trimmed[trimmed.length - 1] !== '"') { throw new Error( `We failed to parse a quoted identifier '${str}'. Please avoid putting quotes or commas in smart comment identifiers (or file a PR to fix the parser).` ); } + return trimmed.substr(1, trimmed.length - 2); } else { // PostgreSQL lower-cases unquoted columns, so we should too. @@ -223,6 +220,7 @@ const parseSqlColumn = (str, array = false) => { if (!str) { throw new Error(`Cannot parse '${str}'`); } + const parts = array ? str.split(",") : [str]; const parsedParts = parts.map(removeQuotes); return array ? parsedParts : parsedParts[0]; @@ -243,10 +241,12 @@ function smartCommentConstraints(introspectionResults) { const attributes = introspectionResults.attribute .filter(a => a.classId === tbl.id) .sort((a, b) => a.num - b.num); + if (!cols) { const pk = introspectionResults.constraint.find( c => c.classId == tbl.id && c.type === "p" ); + if (pk) { return pk.keyAttributeNums.map(n => attributes.find(a => a.num === n)); } else { @@ -257,8 +257,10 @@ function smartCommentConstraints(introspectionResults) { ); } } + return cols.map(colName => { const attr = attributes.find(a => a.name === colName); + if (!attr) { throw new Error( `Could not find attribute '${colName}' in '${tbl.namespaceName}.${ @@ -266,18 +268,20 @@ function smartCommentConstraints(introspectionResults) { }'` ); } + return attr; }); - }; + }; // First: primary keys - // First: primary keys introspectionResults.class.forEach(klass => { const namespace = introspectionResults.namespace.find( n => n.id === klass.namespaceId ); + if (!namespace) { return; } + if (klass.tags.primaryKey) { if (typeof klass.tags.primaryKey !== "string") { throw new Error( @@ -286,10 +290,11 @@ function smartCommentConstraints(introspectionResults) { }' is invalid; please specify just once "@primaryKey col1,col2"` ); } + const { spec: pkSpec, tags, description } = parseConstraintSpec( klass.tags.primaryKey - ); - // $FlowFixMe + ); // $FlowFixMe + const columns: string[] = parseSqlColumn(pkSpec, true); const attributes = attributesByNames( klass, @@ -299,14 +304,15 @@ function smartCommentConstraints(introspectionResults) { attributes.forEach(attr => { attr.tags.notNull = true; }); - const keyAttributeNums = attributes.map(a => a.num); - // Now we need to fake a constraint for this: + const keyAttributeNums = attributes.map(a => a.num); // Now we need to fake a constraint for this: + const fakeConstraint = { kind: "constraint", isFake: true, id: Math.random(), name: `FAKE_${klass.namespaceName}_${klass.name}_primaryKey`, - type: "p", // primary key + type: "p", + // primary key classId: klass.id, foreignClassId: null, comment: null, @@ -317,20 +323,23 @@ function smartCommentConstraints(introspectionResults) { }; introspectionResults.constraint.push(fakeConstraint); } - }); - // Now primary keys are in place, we can apply foreign keys + }); // Now primary keys are in place, we can apply foreign keys + introspectionResults.class.forEach(klass => { const namespace = introspectionResults.namespace.find( n => n.id === klass.namespaceId ); + if (!namespace) { return; } + if (klass.tags.foreignKey) { const foreignKeys = typeof klass.tags.foreignKey === "string" ? [klass.tags.foreignKey] : klass.tags.foreignKey; + if (!Array.isArray(foreignKeys)) { throw new Error( `Invalid foreign key smart comment specified on '${ @@ -338,6 +347,7 @@ function smartCommentConstraints(introspectionResults) { }.${klass.name}'` ); } + foreignKeys.forEach((fkSpecRaw, index) => { if (typeof fkSpecRaw !== "string") { throw new Error( @@ -346,12 +356,14 @@ function smartCommentConstraints(introspectionResults) { }'` ); } + const { spec: fkSpec, tags, description } = parseConstraintSpec( fkSpecRaw ); const matches = fkSpec.match( /^\(([^()]+)\) references ([^().]+)(?:\.([^().]+))?(?:\s*\(([^()]+)\))?$/i ); + if (!matches) { throw new Error( `Invalid foreignKey syntax for '${klass.namespaceName}.${ @@ -359,6 +371,7 @@ function smartCommentConstraints(introspectionResults) { }'; expected something like "(col1,col2) references schema.table (c1, c2)", you passed '${fkSpecRaw}'` ); } + const [ , rawColumns, @@ -369,29 +382,31 @@ function smartCommentConstraints(introspectionResults) { const rawSchema = rawTableOnly ? rawSchemaOrTable : `"${klass.namespaceName}"`; - const rawTable = rawTableOnly || rawSchemaOrTable; - // $FlowFixMe - const columns: string[] = parseSqlColumn(rawColumns, true); - // $FlowFixMe - const foreignSchema: string = parseSqlColumn(rawSchema); - // $FlowFixMe - const foreignTable: string = parseSqlColumn(rawTable); - // $FlowFixMe + const rawTable = rawTableOnly || rawSchemaOrTable; // $FlowFixMe + + const columns: string[] = parseSqlColumn(rawColumns, true); // $FlowFixMe + + const foreignSchema: string = parseSqlColumn(rawSchema); // $FlowFixMe + + const foreignTable: string = parseSqlColumn(rawTable); // $FlowFixMe + const foreignColumns: string[] = rawForeignColumns ? parseSqlColumn(rawForeignColumns, true) : null; - const foreignKlass = introspectionResults.class.find( k => k.name === foreignTable && k.namespaceName === foreignSchema ); + if (!foreignKlass) { throw new Error( `@foreignKey smart comment referenced non-existant table/view '${foreignSchema}'.'${foreignTable}'. Note that this reference must use *database names* (i.e. it does not respect @name). (${fkSpecRaw})` ); } + const foreignNamespace = introspectionResults.namespace.find( n => n.id === foreignKlass.namespaceId ); + if (!foreignNamespace) { return; } @@ -405,15 +420,15 @@ function smartCommentConstraints(introspectionResults) { foreignKlass, foreignColumns, `@foreignKey ${fkSpecRaw}` - ).map(a => a.num); + ).map(a => a.num); // Now we need to fake a constraint for this: - // Now we need to fake a constraint for this: const fakeConstraint = { kind: "constraint", isFake: true, id: Math.random(), name: `FAKE_${klass.namespaceName}_${klass.name}_foreignKey_${index}`, - type: "f", // foreign key + type: "f", + // foreign key classId: klass.id, foreignClassId: foreignKlass.id, comment: null, @@ -428,7 +443,7 @@ function smartCommentConstraints(introspectionResults) { }); } -export default (async function PgIntrospectionPlugin( +export default async function PgIntrospectionPlugin( builder, { pgConfig, @@ -448,12 +463,15 @@ export default (async function PgIntrospectionPlugin( fn => (fn ? fn(introspectionResults) : null) ); }; + async function introspect() { // Perform introspection if (!Array.isArray(schemas)) { throw new Error("Argument 'schemas' (array) is required"); } + const cacheKey = `PgIntrospectionPlugin-introspectionResultsByKind-v${version}`; + const cloneResults = obj => { const result = Object.keys(obj).reduce((memo, k) => { memo[k] = Array.isArray(obj[k]) @@ -463,6 +481,7 @@ export default (async function PgIntrospectionPlugin( }, {}); return result; }; + const introspectionResultsByKind = cloneResults( await persistentMemoizeWithKey(cacheKey, () => withPgClient(pgConfig, async pgClient => { @@ -480,7 +499,6 @@ export default (async function PgIntrospectionPlugin( schemas, pgIncludeExtensionResources, ]); - const result = { __pgVersion: serverVersionNum, namespace: [], @@ -492,11 +510,11 @@ export default (async function PgIntrospectionPlugin( extension: [], index: [], }; + for (const { object } of rows) { result[object.kind].push(object); - } + } // Parse tags from comments - // Parse tags from comments [ "namespace", "class", @@ -510,6 +528,7 @@ export default (async function PgIntrospectionPlugin( result[kind].forEach(object => { // Keep a copy of the raw comment object.comment = object.description; + if (pgEnableTags && object.description) { const parsed = parseTags(object.description); object.tags = parsed.tags; @@ -519,7 +538,6 @@ export default (async function PgIntrospectionPlugin( } }); }); - const extensionConfigurationClassIds = flatMap( result.extension, e => e.configurationClassIds @@ -528,7 +546,6 @@ export default (async function PgIntrospectionPlugin( klass.isExtensionConfigurationTable = extensionConfigurationClassIds.indexOf(klass.id) >= 0; }); - [ "namespace", "class", @@ -541,20 +558,20 @@ export default (async function PgIntrospectionPlugin( ].forEach(k => { result[k].forEach(Object.freeze); }); - return Object.freeze(result); }) ) ); - const knownSchemas = introspectionResultsByKind.namespace.map(n => n.name); const missingSchemas = schemas.filter(s => knownSchemas.indexOf(s) < 0); + if (missingSchemas.length) { const errorMessage = `You requested to use schema '${schemas.join( "', '" )}'; however we couldn't find some of those! Missing schemas are: '${missingSchemas.join( "', '" )}'`; + if (pgThrowOnMissingSchema) { throw new Error(errorMessage); } else { @@ -567,12 +584,14 @@ export default (async function PgIntrospectionPlugin( memo[x[attrKey]] = x; return memo; }, {}); + const xByYAndZ = (arrayOfX, attrKey, attrKey2) => arrayOfX.reduce((memo, x) => { if (!memo[x[attrKey]]) memo[x[attrKey]] = {}; memo[x[attrKey]][x[attrKey2]] = x; return memo; }, {}); + introspectionResultsByKind.namespaceById = xByY( introspectionResultsByKind.namespace, "id" @@ -598,42 +617,48 @@ export default (async function PgIntrospectionPlugin( const relate = (array, newAttr, lookupAttr, lookup, missingOk = false) => { array.forEach(entry => { const key = entry[lookupAttr]; + if (Array.isArray(key)) { entry[newAttr] = key .map(innerKey => { const result = lookup[innerKey]; + if (innerKey && !result) { if (missingOk) { return; } + throw new Error( `Could not look up '${newAttr}' by '${lookupAttr}' ('${innerKey}') on '${JSON.stringify( entry )}'` ); } + return result; }) .filter(_ => _); } else { const result = lookup[key]; + if (key && !result) { if (missingOk) { return; } + throw new Error( `Could not look up '${newAttr}' by '${lookupAttr}' on '${JSON.stringify( entry )}'` ); } + entry[newAttr] = result; } }); }; augment(introspectionResultsByKind); - relate( introspectionResultsByKind.class, "namespace", @@ -641,35 +666,30 @@ export default (async function PgIntrospectionPlugin( introspectionResultsByKind.namespaceById, true // Because it could be a type defined in a different namespace - which is fine so long as we don't allow querying it directly ); - relate( introspectionResultsByKind.class, "type", "typeId", introspectionResultsByKind.typeById ); - relate( introspectionResultsByKind.attribute, "class", "classId", introspectionResultsByKind.classById ); - relate( introspectionResultsByKind.attribute, "type", "typeId", introspectionResultsByKind.typeById ); - relate( introspectionResultsByKind.procedure, "namespace", "namespaceId", introspectionResultsByKind.namespaceById ); - relate( introspectionResultsByKind.type, "class", @@ -677,7 +697,6 @@ export default (async function PgIntrospectionPlugin( introspectionResultsByKind.classById, true ); - relate( introspectionResultsByKind.type, "domainBaseType", @@ -685,7 +704,6 @@ export default (async function PgIntrospectionPlugin( introspectionResultsByKind.typeById, true // Because not all types are domains ); - relate( introspectionResultsByKind.type, "arrayItemType", @@ -693,14 +711,12 @@ export default (async function PgIntrospectionPlugin( introspectionResultsByKind.typeById, true // Because not all types are arrays ); - relate( introspectionResultsByKind.constraint, "class", "classId", introspectionResultsByKind.classById ); - relate( introspectionResultsByKind.constraint, "foreignClass", @@ -708,7 +724,6 @@ export default (async function PgIntrospectionPlugin( introspectionResultsByKind.classById, true // Because many constraints don't apply to foreign classes ); - relate( introspectionResultsByKind.extension, "namespace", @@ -716,7 +731,6 @@ export default (async function PgIntrospectionPlugin( introspectionResultsByKind.namespaceById, true // Because the extension could be a defined in a different namespace ); - relate( introspectionResultsByKind.extension, "configurationClasses", @@ -724,22 +738,19 @@ export default (async function PgIntrospectionPlugin( introspectionResultsByKind.classById, true // Because the configuration table could be a defined in a different namespace ); - relate( introspectionResultsByKind.index, "class", "classId", introspectionResultsByKind.classById - ); + ); // Reverse arrayItemType -> arrayType - // Reverse arrayItemType -> arrayType introspectionResultsByKind.type.forEach(type => { if (type.arrayItemType) { type.arrayItemType.arrayType = type; } - }); + }); // Table/type columns / constraints - // Table/type columns / constraints introspectionResultsByKind.class.forEach(klass => { klass.attributes = introspectionResultsByKind.attribute.filter( attr => attr.classId === klass.id @@ -756,9 +767,8 @@ export default (async function PgIntrospectionPlugin( klass.primaryKeyConstraint = klass.constraints.find( constraint => constraint.type === "p" ); - }); + }); // Constraint attributes - // Constraint attributes introspectionResultsByKind.constraint.forEach(constraint => { if (constraint.keyAttributeNums && constraint.class) { constraint.keyAttributes = constraint.keyAttributeNums.map(nr => @@ -767,6 +777,7 @@ export default (async function PgIntrospectionPlugin( } else { constraint.keyAttributes = []; } + if (constraint.foreignKeyAttributeNums && constraint.foreignClass) { constraint.foreignKeyAttributes = constraint.foreignKeyAttributeNums.map( nr => constraint.foreignClass.attributes.find(attr => attr.num === nr) @@ -774,24 +785,21 @@ export default (async function PgIntrospectionPlugin( } else { constraint.foreignKeyAttributes = []; } - }); + }); // Detect which columns and constraints are indexed - // Detect which columns and constraints are indexed introspectionResultsByKind.index.forEach(index => { const columns = index.attributeNums.map(nr => index.class.attributes.find(attr => attr.num === nr) - ); + ); // Indexed column (for orderBy / filter): - // Indexed column (for orderBy / filter): if (columns[0]) { columns[0].isIndexed = true; } if (columns[0] && columns.length === 1 && index.isUnique) { columns[0].isUnique = true; - } + } // Indexed constraints (for reverse relations): - // Indexed constraints (for reverse relations): index.class.constraints .filter(constraint => constraint.type === "f") .forEach(constraint => { @@ -804,12 +812,10 @@ export default (async function PgIntrospectionPlugin( } }); }); - return introspectionResultsByKind; } let introspectionResultsByKind = await introspect(); - let pgClient, releasePgClient, listener; function stopListening() { @@ -819,6 +825,7 @@ export default (async function PgIntrospectionPlugin( }); pgClient.removeListener("notification", listener); } + if (releasePgClient) { releasePgClient(); pgClient = null; @@ -827,22 +834,24 @@ export default (async function PgIntrospectionPlugin( builder.registerWatcher(async triggerRebuild => { // In case we started listening before, clean up - await stopListening(); + await stopListening(); // Check we can get a pgClient - // Check we can get a pgClient if (pgConfig instanceof pg.Pool || quacksLikePgPool(pgConfig)) { pgClient = await pgConfig.connect(); + releasePgClient = () => pgClient && pgClient.release(); } else if (typeof pgConfig === "string") { pgClient = new pg.Client(pgConfig); pgClient.on("error", e => { debug("pgClient error occurred: %s", e); }); + releasePgClient = () => new Promise((resolve, reject) => { if (pgClient) pgClient.end(err => (err ? reject(err) : resolve())); else resolve(); }); + await new Promise((resolve, reject) => { if (pgClient) { pgClient.connect(err => (err ? reject(err) : resolve())); @@ -854,11 +863,12 @@ export default (async function PgIntrospectionPlugin( throw new Error( "Cannot watch schema with this configuration - need a string or pg.Pool" ); - } - // Install the watch fixtures. + } // Install the watch fixtures. + if (!pgSkipInstallingWatchFixtures) { const watchSqlInner = await readFile(WATCH_FIXTURES_PATH, "utf8"); const sql = `begin; ${watchSqlInner}; commit;`; + try { await withPgClient(pgOwnerConnectionString || pgConfig, pgClient => pgClient.query(sql) @@ -885,12 +895,12 @@ export default (async function PgIntrospectionPlugin( ); debug(error); /* eslint-enable no-console */ + await pgClient.query("rollback"); } } await pgClient.query("listen postgraphile_watch"); - const handleChange = throttle( async () => { debug(`Schema change detected: re-inspecting schema...`); @@ -909,15 +919,18 @@ export default (async function PgIntrospectionPlugin( if (notification.channel !== "postgraphile_watch") { return; } + try { const payload = JSON.parse(notification.payload); payload.payload = payload.payload || []; + if (payload.type === "ddl") { const commands = payload.payload .filter( ({ schema }) => schema == null || schemas.indexOf(schema) >= 0 ) .map(({ command }) => command); + if (commands.length) { handleChange(); } @@ -925,6 +938,7 @@ export default (async function PgIntrospectionPlugin( const affectsOurSchemas = payload.payload.some( schemaName => schemas.indexOf(schemaName) >= 0 ); + if (affectsOurSchemas) { handleChange(); } @@ -935,10 +949,10 @@ export default (async function PgIntrospectionPlugin( debug(`Error occurred parsing notification payload: ${e}`); } }; + pgClient.on("notification", listener); introspectionResultsByKind = await introspect(); }, stopListening); - builder.hook( "build", build => { @@ -951,6 +965,7 @@ export default (async function PgIntrospectionPlugin( supportsJSONB: false, }); } + return build.extend(build, { pgIntrospectionResultsByKind: introspectionResultsByKind, }); @@ -959,4 +974,4 @@ export default (async function PgIntrospectionPlugin( [], ["PgBasics"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgJWTPlugin.js b/packages/graphile-build-pg/src/plugins/PgJWTPlugin.ts similarity index 96% rename from packages/graphile-build-pg/src/plugins/PgJWTPlugin.js rename to packages/graphile-build-pg/src/plugins/PgJWTPlugin.ts index 084cf7c55..b6b5bd8aa 100644 --- a/packages/graphile-build-pg/src/plugins/PgJWTPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgJWTPlugin.ts @@ -1,8 +1,6 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import { sign as signJwt } from "jsonwebtoken"; - -export default (function PgJWTPlugin( +export default function PgJWTPlugin( builder, { pgJwtTypeIdentifier, pgJwtSecret } ) { @@ -26,15 +24,16 @@ export default (function PgJWTPlugin( if (!pgJwtTypeIdentifier) { return _; } + if (!pgJwtSecret) { throw new Error( "pgJwtTypeIdentifier was specified without pgJwtSecret" ); } + const { namespaceName, entityName: typeName } = parseIdentifier( pgJwtTypeIdentifier ); - const compositeClass = introspectionResultsByKind.class.find( table => !table.isSelectable && @@ -44,23 +43,26 @@ export default (function PgJWTPlugin( table.name === typeName && table.namespaceName === namespaceName ); + if (!compositeClass) { throw new Error( `Could not find JWT type '"${namespaceName}"."${typeName}"'` ); } + const compositeType = compositeClass.type; + if (!compositeType) { throw new Error("Could not determine the type for JWT type"); } + if (pg2GqlMapper[compositeType.id]) { throw new Error("JWT type has already been overridden?"); } - const attributes = compositeClass.attributes; - const compositeTypeName = inflection.tableType(compositeClass); + const attributes = compositeClass.attributes; + const compositeTypeName = inflection.tableType(compositeClass); // NOTE: we deliberately do not create an input type - // NOTE: we deliberately do not create an input type pgRegisterGqlTypeByTypeId(compositeType.id, cb => { const JWTType = newWithHooks( GraphQLScalarType, @@ -68,6 +70,7 @@ export default (function PgJWTPlugin( name: compositeTypeName, description: "A JSON Web Token defined by [RFC 7519](https://tools.ietf.org/html/rfc7519) which securely represents claims between two parties.", + serialize(value) { const token = attributes.reduce((memo, attr) => { memo[attr.name] = value[attr.name]; @@ -105,14 +108,15 @@ export default (function PgJWTPlugin( } ); cb(JWTType); - pg2GqlMapper[compositeType.id] = { map: value => { if (!value) return null; const values = Object.values(value); + if (values.some(v => v != null)) { return value; } + return null; }, unmap: () => { @@ -144,4 +148,4 @@ export default (function PgJWTPlugin( [], ["PgIntrospection"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.js b/packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.ts similarity index 97% rename from packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.js rename to packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.ts index 7bc596516..f175174ef 100644 --- a/packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.ts @@ -1,10 +1,7 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import debugFactory from "debug"; - const debug = debugFactory("graphile-build-pg"); - -export default (function PgMutationCreatePlugin( +export default function PgMutationCreatePlugin( builder, { pgDisableDefaultMutations } ) { @@ -55,8 +52,8 @@ export default (function PgMutationCreatePlugin( if (!table.namespace) return memo; if (!table.isSelectable) return memo; if (!table.isInsertable || omit(table, "create")) return memo; - const Table = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null); + if (!Table) { debug( `There was no table type for table '${table.namespace.name}.${ @@ -65,10 +62,12 @@ export default (function PgMutationCreatePlugin( ); return memo; } + const TableInput = pgGetGqlInputTypeByTypeIdAndModifier( table.type.id, null ); + if (!TableInput) { debug( `There was no input type for table '${table.namespace.name}.${ @@ -76,6 +75,7 @@ export default (function PgMutationCreatePlugin( }', so we're going to omit it from the create mutation.` ); } + const tableTypeName = inflection.tableType(table); const InputType = newWithHooks( GraphQLInputObjectType, @@ -108,7 +108,8 @@ export default (function PgMutationCreatePlugin( } )}`, isPgCreateInputType: true, - pgInflection: table, // TODO:v5: remove - TYPO! + pgInflection: table, + // TODO:v5: remove - TYPO! pgIntrospection: table, } ); @@ -151,7 +152,9 @@ export default (function PgMutationCreatePlugin( } )}\n\nor disable the built-in create mutation via:\n\n ${sqlCommentByAddingTags( table, - { omit: "create" } + { + omit: "create", + } )}`, isMutationPayload: true, isPgCreatePayloadType: true, @@ -179,6 +182,7 @@ export default (function PgMutationCreatePlugin( type: new GraphQLNonNull(InputType), }, }, + async resolve(data, args, resolveContext, resolveInfo) { const { input } = args; const { pgClient } = resolveContext; @@ -186,6 +190,7 @@ export default (function PgMutationCreatePlugin( resolveInfo ); parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin + const resolveData = getDataFromParsedResolveInfoFragment( parsedResolveInfoFragment, PayloadType @@ -205,6 +210,7 @@ export default (function PgMutationCreatePlugin( relevantAttributes.forEach(attr => { const fieldName = inflection.column(attr); const val = inputData[fieldName]; + if ( Object.prototype.hasOwnProperty.call( inputData, @@ -217,7 +223,6 @@ export default (function PgMutationCreatePlugin( ); } }); - const mutationQuery = sql.query` insert into ${sql.identifier( table.namespace.name, @@ -229,8 +234,8 @@ export default (function PgMutationCreatePlugin( ) values(${sql.join(sqlValues, ", ")})` : sql.fragment`default values` } returning *`; - let row; + try { await pgClient.query("SAVEPOINT graphql_mutation"); const rows = await viaTemporaryTable( @@ -250,6 +255,7 @@ export default (function PgMutationCreatePlugin( ); throw e; } + return { clientMutationId: input.clientMutationId, data: row, @@ -281,4 +287,4 @@ export default (function PgMutationCreatePlugin( [], ["PgTables"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.js b/packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.ts similarity index 98% rename from packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.js rename to packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.ts index 34834963b..e5a173890 100644 --- a/packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.ts @@ -1,8 +1,6 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import isString from "lodash/isString"; - -export default (function PgMutationPayloadEdgePlugin( +export default function PgMutationPayloadEdgePlugin( builder, { pgSimpleCollections, disableIssue397Fix } ) { @@ -26,8 +24,8 @@ export default (function PgMutationPayloadEdgePlugin( fieldWithHooks, Self, } = context; - const table = pgIntrospectionTable || pgIntrospection; + if ( !isMutationPayload || !table || @@ -38,9 +36,11 @@ export default (function PgMutationPayloadEdgePlugin( ) { return fields; } + const simpleCollections = table.tags.simpleCollections || pgSimpleCollections; const hasConnections = simpleCollections !== "only"; + if (!hasConnections && !disableIssue397Fix) { return fields; } @@ -51,6 +51,7 @@ export default (function PgMutationPayloadEdgePlugin( inflection.orderByType(tableTypeName) ); const TableEdgeType = getTypeByName(inflection.edge(tableTypeName)); + if (!TableEdgeType) { return fields; } @@ -59,7 +60,6 @@ export default (function PgMutationPayloadEdgePlugin( const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes; const canOrderBy = !omit(table, "order"); - const fieldName = inflection.edgeField(table); const defaultValueEnum = canOrderBy && @@ -86,15 +86,19 @@ export default (function PgMutationPayloadEdgePlugin( }, } : {}, + resolve(data, { orderBy: rawOrderBy }, _context, resolveInfo) { if (!data.data) { return null; } + const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); const edge = data.data[safeAlias]; + if (!edge) { return null; } + const orderBy = canOrderBy && rawOrderBy ? Array.isArray(rawOrderBy) @@ -138,6 +142,7 @@ export default (function PgMutationPayloadEdgePlugin( ? rawOrderBy : [rawOrderBy] : null; + if (orderBy != null) { const aliases = []; const expressions = []; @@ -150,6 +155,7 @@ export default (function PgMutationPayloadEdgePlugin( if (!col) { return; } + const expr = isString(col) ? sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( col @@ -160,6 +166,7 @@ export default (function PgMutationPayloadEdgePlugin( if (alias == null) return; aliases.push(alias); }); + if (!unique && primaryKeys) { // Add PKs primaryKeys.forEach(key => { @@ -170,6 +177,7 @@ export default (function PgMutationPayloadEdgePlugin( ); }); } + if (aliases.length) { queryBuilder.select( sql.fragment`json_build_array(${sql.join( @@ -191,4 +199,4 @@ export default (function PgMutationPayloadEdgePlugin( }, ["PgMutationPayloadEdge"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.js b/packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.ts similarity index 92% rename from packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.js rename to packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.ts index ce97f062a..8b0375089 100644 --- a/packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PgMutationProceduresPlugin(builder) { +import { Plugin } from "graphile-build"; +export default function PgMutationProceduresPlugin(builder) { builder.hook( "GraphQLObjectType:fields", (fields, build, context) => { @@ -31,8 +29,8 @@ export default (function PgMutationProceduresPlugin(builder) { if (proc.isStable) return memo; if (!proc.namespace) return memo; if (omit(proc, "execute")) return memo; - const fieldName = inflection.functionMutationName(proc); + try { memo = extend( memo, @@ -54,6 +52,7 @@ export default (function PgMutationProceduresPlugin(builder) { } catch (e) { swallowError(e); } + return memo; }, {}), `Adding mutation procedure to root Mutation field` @@ -61,4 +60,4 @@ export default (function PgMutationProceduresPlugin(builder) { }, ["PgMutationProcedures"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.js b/packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.ts similarity index 97% rename from packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.js rename to packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.ts index 479faa4b8..45f03eb6f 100644 --- a/packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.ts @@ -1,10 +1,7 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import debugFactory from "debug"; - const debug = debugFactory("graphile-build-pg"); - -export default (async function PgMutationUpdateDeletePlugin( +export default async function PgMutationUpdateDeletePlugin( builder, { pgDisableDefaultMutations } ) { @@ -70,14 +67,15 @@ export default (async function PgMutationUpdateDeletePlugin( table.isDeletable && !omit(table, "delete"); if (!canUpdate && !canDelete) return memo; - const TableType = pgGetGqlTypeByTypeIdAndModifier( table.type.id, null ); + if (!TableType) { return memo; } + async function commonCodeRenameMe( pgClient, resolveInfo, @@ -91,17 +89,17 @@ export default (async function PgMutationUpdateDeletePlugin( const { input } = args; const parsedResolveInfoFragment = parseResolveInfo(resolveInfo); parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin + const resolveData = getDataFromParsedResolveInfoFragment( parsedResolveInfoFragment, PayloadType ); - const sqlTypeIdentifier = sql.identifier( table.namespace.name, table.name ); - let sqlMutationQuery; + if (mode === "update") { const sqlColumns = []; const sqlValues = []; @@ -113,19 +111,22 @@ export default (async function PgMutationUpdateDeletePlugin( // PERFORMANCE: These used to be .filter(...) calls if (!pgColumnFilter(attr, build, context)) return; if (omit(attr, "update")) return; - const fieldName = inflection.column(attr); + if ( - fieldName in inputData /* Because we care about null! */ + fieldName in inputData + /* Because we care about null! */ ) { const val = inputData[fieldName]; sqlColumns.push(sql.identifier(attr.name)); sqlValues.push(gql2pg(val, attr.type, attr.typeModifier)); } }); + if (sqlColumns.length === 0) { return null; } + sqlMutationQuery = sql.query` update ${sql.identifier( table.namespace.name, @@ -155,6 +156,7 @@ export default (async function PgMutationUpdateDeletePlugin( resolveContext ); let row; + try { await pgClient.query("SAVEPOINT graphql_mutation"); const rows = await viaTemporaryTable( @@ -172,6 +174,7 @@ export default (async function PgMutationUpdateDeletePlugin( ); throw e; } + if (!row) { throw new Error( `No values were ${mode}d in collection '${inflection.pluralize( @@ -179,11 +182,13 @@ export default (async function PgMutationUpdateDeletePlugin( )}' because no values you can ${mode} were found matching these criteria.` ); } + return { clientMutationId: input.clientMutationId, data: row, }; } + if (TableType) { const uniqueConstraints = table.constraints.filter( con => con.type === "u" || con.type === "p" @@ -206,8 +211,8 @@ export default (async function PgMutationUpdateDeletePlugin( ](table), description: `The output of our ${mode} \`${tableTypeName}\` mutation.`, fields: ({ fieldWithHooks }) => { - const tableName = inflection.tableFieldName(table); - // This should really be `-node-id` but for compatibility with PostGraphQL v3 we haven't made that change. + const tableName = inflection.tableFieldName(table); // This should really be `-node-id` but for compatibility with PostGraphQL v3 we haven't made that change. + const deletedNodeIdFieldName = inflection.deletedNodeId( table ); @@ -238,17 +243,19 @@ export default (async function PgMutationUpdateDeletePlugin( const fieldDataGeneratorsByTableType = fieldDataGeneratorsByType.get( TableType ); - const gens = fieldDataGeneratorsByTableType && fieldDataGeneratorsByTableType[ nodeIdFieldName ]; + if (gens) { gens.forEach(gen => addDataGenerator(gen)); } + return { type: GraphQLID, + resolve(data) { return ( data.data.__identifiers && @@ -283,10 +290,10 @@ export default (async function PgMutationUpdateDeletePlugin( isPgDeletePayloadType: mode === "delete", pgIntrospection: table, } - ); + ); // NodeId - // NodeId const primaryKeyConstraint = table.primaryKeyConstraint; + if (nodeIdFieldName && primaryKeyConstraint) { const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes; @@ -339,12 +346,12 @@ export default (async function PgMutationUpdateDeletePlugin( isPgUpdateNodeInputType: mode === "update", isPgDeleteInputType: mode === "delete", isPgDeleteNodeInputType: mode === "delete", - pgInflection: table, // TODO:v5: remove - TYPO! + pgInflection: table, + // TODO:v5: remove - TYPO! pgIntrospection: table, isMutationInput: true, } ); - memo = extend( memo, { @@ -365,6 +372,7 @@ export default (async function PgMutationUpdateDeletePlugin( type: new GraphQLNonNull(InputType), }, }, + async resolve( parent, args, @@ -374,14 +382,17 @@ export default (async function PgMutationUpdateDeletePlugin( const { input } = args; const { pgClient } = resolveContext; const nodeId = input[nodeIdFieldName]; + try { const { Type, identifiers, } = getTypeAndIdentifiersFromNodeId(nodeId); + if (Type !== TableType) { throw new Error("Mismatched type"); } + if (identifiers.length !== primaryKeys.length) { throw new Error("Invalid ID"); } @@ -426,14 +437,15 @@ export default (async function PgMutationUpdateDeletePlugin( }, "Adding ${mode} mutation for ${describePgEntity(table)}" ); - } + } // Unique - // Unique uniqueConstraints.forEach(constraint => { if (omit(constraint, mode)) { return; } + const keys = constraint.keyAttributes; + if (!keys.every(_ => _)) { throw new Error( `Consistency error: could not find an attribute in the constraint when building the ${mode} mutation for ${describePgEntity( @@ -441,9 +453,11 @@ export default (async function PgMutationUpdateDeletePlugin( )}!` ); } + if (keys.some(key => omit(key, "read"))) { return; } + const fieldName = inflection[ mode === "update" ? "updateByKeys" : "deleteByKeys" ](keys, table, constraint); @@ -499,13 +513,13 @@ export default (async function PgMutationUpdateDeletePlugin( isPgUpdateByKeysInputType: mode === "update", isPgDeleteInputType: mode === "delete", isPgDeleteByKeysInputType: mode === "delete", - pgInflection: table, // TODO:v5: remove - TYPO! + pgInflection: table, + // TODO:v5: remove - TYPO! pgIntrospection: table, pgKeys: keys, isMutationInput: true, } ); - memo = extend( memo, { @@ -526,6 +540,7 @@ export default (async function PgMutationUpdateDeletePlugin( type: new GraphQLNonNull(InputType), }, }, + async resolve( parent, args, @@ -575,6 +590,7 @@ export default (async function PgMutationUpdateDeletePlugin( ); }); } + return memo; }, outerMemo), {} @@ -584,4 +600,4 @@ export default (async function PgMutationUpdateDeletePlugin( }, ["PgMutationUpdateDelete"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgNodeAliasPostGraphile.js b/packages/graphile-build-pg/src/plugins/PgNodeAliasPostGraphile.ts similarity index 80% rename from packages/graphile-build-pg/src/plugins/PgNodeAliasPostGraphile.js rename to packages/graphile-build-pg/src/plugins/PgNodeAliasPostGraphile.ts index adaacc140..f84850408 100644 --- a/packages/graphile-build-pg/src/plugins/PgNodeAliasPostGraphile.js +++ b/packages/graphile-build-pg/src/plugins/PgNodeAliasPostGraphile.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (async function PgNodeAliasPostGraphile(builder) { +import { Plugin } from "graphile-build"; +export default async function PgNodeAliasPostGraphile(builder) { builder.hook( "GraphQLObjectType", (object, build, context) => { @@ -9,18 +7,22 @@ export default (async function PgNodeAliasPostGraphile(builder) { setNodeAlias, inflection: { pluralize }, } = build; + if (!setNodeAlias) { // Node plugin must be disabled. return object; } + const { scope: { isPgRowType, isPgCompoundType, pgIntrospection: table }, } = context; + if (isPgRowType || isPgCompoundType) { setNodeAlias(object.name, pluralize(table.name)); } + return object; }, ["PgNodeAliasPostGraphile"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.js b/packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.ts similarity index 94% rename from packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.js rename to packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.ts index fd8c12bd8..bdfbf7caa 100644 --- a/packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PgOrderAllColumnsPlugin(builder) { +import { Plugin } from "graphile-build"; +export default function PgOrderAllColumnsPlugin(builder) { builder.hook( "GraphQLEnumType:values", (values, build, context) => { @@ -16,9 +14,11 @@ export default (function PgOrderAllColumnsPlugin(builder) { const { scope: { isPgRowSortEnum, pgIntrospection: table }, } = context; + if (!isPgRowSortEnum || !table || table.kind !== "class") { return values; } + return extend( values, table.attributes.reduce((memo, attr) => { @@ -26,7 +26,6 @@ export default (function PgOrderAllColumnsPlugin(builder) { if (!pgColumnFilter(attr, build, context)) return memo; if (omit(attr, "order")) return memo; const unique = attr.isUnique; - const ascFieldName = inflection.orderByColumnEnum(attr, true); const descFieldName = inflection.orderByColumnEnum(attr, false); memo = extend( @@ -76,4 +75,4 @@ export default (function PgOrderAllColumnsPlugin(builder) { }, ["PgOrderAllColumns"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgOrderByPrimaryKeyPlugin.js b/packages/graphile-build-pg/src/plugins/PgOrderByPrimaryKeyPlugin.ts similarity index 89% rename from packages/graphile-build-pg/src/plugins/PgOrderByPrimaryKeyPlugin.js rename to packages/graphile-build-pg/src/plugins/PgOrderByPrimaryKeyPlugin.ts index dc905fa00..091c7a9b2 100644 --- a/packages/graphile-build-pg/src/plugins/PgOrderByPrimaryKeyPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgOrderByPrimaryKeyPlugin.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PgOrderByPrimaryKeyPlugin(builder) { +import { Plugin } from "graphile-build"; +export default function PgOrderByPrimaryKeyPlugin(builder) { builder.hook( "GraphQLEnumType:values", (values, build, context) => { @@ -13,12 +11,14 @@ export default (function PgOrderByPrimaryKeyPlugin(builder) { if (!isPgRowSortEnum || !table || table.kind !== "class") { return values; } + const primaryKeyConstraint = table.primaryKeyConstraint; + if (!primaryKeyConstraint) { return values; } - const primaryKeys = primaryKeyConstraint.keyAttributes; + const primaryKeys = primaryKeyConstraint.keyAttributes; return extend( values, { @@ -42,4 +42,4 @@ export default (function PgOrderByPrimaryKeyPlugin(builder) { }, ["PgOrderByPrimaryKey"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgOrderComputedColumnsPlugin.js b/packages/graphile-build-pg/src/plugins/PgOrderComputedColumnsPlugin.ts similarity index 86% rename from packages/graphile-build-pg/src/plugins/PgOrderComputedColumnsPlugin.js rename to packages/graphile-build-pg/src/plugins/PgOrderComputedColumnsPlugin.ts index 7ba59ea0f..68ce70132 100644 --- a/packages/graphile-build-pg/src/plugins/PgOrderComputedColumnsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgOrderComputedColumnsPlugin.ts @@ -1,8 +1,6 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import { getComputedColumnDetails } from "./PgComputedColumnsPlugin"; - -export default (function PgOrderComputedColumnsPlugin(builder) { +export default function PgOrderComputedColumnsPlugin(builder) { builder.hook( "GraphQLEnumType:values", (values, build, context) => { @@ -17,6 +15,7 @@ export default (function PgOrderComputedColumnsPlugin(builder) { const { scope: { isPgRowSortEnum, pgIntrospection: table }, } = context; + if (!isPgRowSortEnum || !table || table.kind !== "class") { return values; } @@ -25,28 +24,25 @@ export default (function PgOrderComputedColumnsPlugin(builder) { (memo, proc) => { /* ALSO SEE PgConditionComputedColumnPlugin */ // Must be marked @sortable - if (!proc.tags.sortable) return memo; + if (!proc.tags.sortable) return memo; // Must not be omitted - // Must not be omitted - if (omit(proc, "execute")) return memo; + if (omit(proc, "execute")) return memo; // Must be a computed column - // Must be a computed column const computedColumnDetails = getComputedColumnDetails( build, table, proc ); if (!computedColumnDetails) return memo; - const { pseudoColumnName } = computedColumnDetails; + const { pseudoColumnName } = computedColumnDetails; // Must have only one required argument - // Must have only one required argument const nonOptionalArgumentsCount = proc.argDefaultsNum - proc.inputArgsCount; + if (nonOptionalArgumentsCount > 1) { return memo; - } + } // Must return a scalar - // Must return a scalar if (proc.returnsSet) return memo; const returnType = introspectionResultsByKind.typeById[proc.returnTypeId]; @@ -57,10 +53,12 @@ export default (function PgOrderComputedColumnsPlugin(builder) { const isRecordLike = returnType.id === "2249"; if (isRecordLike) return memo; const isVoid = String(returnType.id) === "2278"; - if (isVoid) return memo; + if (isVoid) return memo; // Looks good - // Looks good - memo.push({ proc, pseudoColumnName }); + memo.push({ + proc, + pseudoColumnName, + }); return memo; }, [] @@ -80,7 +78,6 @@ export default (function PgOrderComputedColumnsPlugin(builder) { table, false ); - const unique = !!proc.tags.isUnique; const functionCall = ({ queryBuilder }) => @@ -126,4 +123,4 @@ export default (function PgOrderComputedColumnsPlugin(builder) { }, ["PgOrderComputedColumns"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.js b/packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.ts similarity index 96% rename from packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.js rename to packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.ts index 907bd39ab..de4a31cee 100644 --- a/packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PgQueryProceduresPlugin( +import { Plugin } from "graphile-build"; +export default function PgQueryProceduresPlugin( builder, { pgSimpleCollections } ) { @@ -34,7 +32,6 @@ export default (function PgQueryProceduresPlugin( if (!proc.isStable) return memo; if (!proc.namespace) return memo; if (omit(proc, "execute")) return memo; - const argTypes = proc.argTypeIds.reduce((prev, typeId, idx) => { if ( proc.argModes.length === 0 || // all args are `in` @@ -43,8 +40,10 @@ export default (function PgQueryProceduresPlugin( ) { prev.push(introspectionResultsByKind.typeById[typeId]); } + return prev; }, []); + if ( argTypes.some( type => type.type === "c" && type.class && type.class.isSelectable @@ -53,7 +52,9 @@ export default (function PgQueryProceduresPlugin( // Selects a table, ignore! return memo; } + const firstArgType = argTypes[0]; + if ( firstArgType && firstArgType.type === "c" && @@ -69,6 +70,7 @@ export default (function PgQueryProceduresPlugin( const fieldName = forceList ? inflection.functionQueryNameList(proc) : inflection.functionQueryName(proc); + try { memo = extend( memo, @@ -91,17 +93,21 @@ export default (function PgQueryProceduresPlugin( swallowError(e); } } + const simpleCollections = proc.tags.simpleCollections || pgSimpleCollections; const hasConnections = simpleCollections !== "only"; const hasSimpleCollections = simpleCollections === "only" || simpleCollections === "both"; + if (!proc.returnsSet || hasConnections) { makeField(false); } + if (proc.returnsSet && hasSimpleCollections) { makeField(true); } + return memo; }, {}), `Adding query procedures to root Query type` @@ -109,4 +115,4 @@ export default (function PgQueryProceduresPlugin( }, ["PgQueryProcedures"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgRecordFunctionConnectionPlugin.js b/packages/graphile-build-pg/src/plugins/PgRecordFunctionConnectionPlugin.ts similarity index 96% rename from packages/graphile-build-pg/src/plugins/PgRecordFunctionConnectionPlugin.js rename to packages/graphile-build-pg/src/plugins/PgRecordFunctionConnectionPlugin.ts index 684c8093a..520a5d3ad 100644 --- a/packages/graphile-build-pg/src/plugins/PgRecordFunctionConnectionPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgRecordFunctionConnectionPlugin.ts @@ -1,9 +1,8 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; const base64 = str => Buffer.from(String(str)).toString("base64"); -export default (function PgRecordFunctionConnectionPlugin( +export default function PgRecordFunctionConnectionPlugin( builder, { pgForbidSetofFunctionsToReturnNull = false } ) { @@ -25,8 +24,8 @@ export default (function PgRecordFunctionConnectionPlugin( const nullableIf = (condition, Type) => condition ? Type : new GraphQLNonNull(Type); - const Cursor = getTypeByName("Cursor"); + const Cursor = getTypeByName("Cursor"); introspectionResultsByKind.procedure.forEach(proc => { // PERFORMANCE: These used to be .filter(...) calls if (!proc.returnsSet) return; @@ -37,13 +36,14 @@ export default (function PgRecordFunctionConnectionPlugin( // Does not return a record type; defer handling to // PgTablesPlugin and PgScalarFunctionConnectionPlugin return; - } - // TODO: PG10 doesn't support the equivalent of pg_attribute.atttypemod + } // TODO: PG10 doesn't support the equivalent of pg_attribute.atttypemod // on function arguments and return types, however maybe a later // version of PG will? + const NodeType = getTypeByName( inflection.recordFunctionReturnType(proc) ); + if (!NodeType) { throw new Error( `Do not have a node type '${inflection.recordFunctionReturnType( @@ -51,6 +51,7 @@ export default (function PgRecordFunctionConnectionPlugin( )}' for '${proc.name}' so cannot create connection type` ); } + const EdgeType = newWithHooks( GraphQLObjectType, { @@ -67,6 +68,7 @@ export default (function PgRecordFunctionConnectionPlugin( return { description: "A cursor for use in pagination.", type: Cursor, + resolve(data) { return base64(JSON.stringify(data.__cursor)); }, @@ -88,6 +90,7 @@ export default (function PgRecordFunctionConnectionPlugin( !pgForbidSetofFunctionsToReturnNull, NodeType ), + resolve(data, _args, _context, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo( resolveInfo @@ -115,8 +118,8 @@ export default (function PgRecordFunctionConnectionPlugin( pgIntrospection: proc, } ); - /*const ConnectionType = */ + newWithHooks( GraphQLObjectType, { @@ -133,6 +136,7 @@ export default (function PgRecordFunctionConnectionPlugin( nullableIf(!pgForbidSetofFunctionsToReturnNull, NodeType) ) ), + resolve(data, _args, _context, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); return data.data.map(entry => entry[safeAlias]); @@ -149,6 +153,7 @@ export default (function PgRecordFunctionConnectionPlugin( type: new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(EdgeType)) ), + resolve(data, _args, _context, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo( resolveInfo @@ -188,4 +193,4 @@ export default (function PgRecordFunctionConnectionPlugin( }, ["PgRecordFunctionConnection"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgRecordReturnTypesPlugin.js b/packages/graphile-build-pg/src/plugins/PgRecordReturnTypesPlugin.ts similarity index 97% rename from packages/graphile-build-pg/src/plugins/PgRecordReturnTypesPlugin.js rename to packages/graphile-build-pg/src/plugins/PgRecordReturnTypesPlugin.ts index d13ca6879..0842d9428 100644 --- a/packages/graphile-build-pg/src/plugins/PgRecordReturnTypesPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgRecordReturnTypesPlugin.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - -export default (function PgRecordReturnTypesPlugin(builder) { +import { Plugin } from "graphile-build"; +export default function PgRecordReturnTypesPlugin(builder) { builder.hook( "init", (_, build) => { @@ -19,17 +17,17 @@ export default (function PgRecordReturnTypesPlugin(builder) { getSafeAliasFromResolveInfo, getSafeAliasFromAlias, } = build; - introspectionResultsByKind.procedure.forEach(proc => { // PERFORMANCE: These used to be .filter(...) calls if (!proc.namespace) return; if (omit(proc, "execute")) return; - const returnType = introspectionResultsByKind.typeById[proc.returnTypeId]; + if (returnType.id !== "2249") { return; } + const argTypes = proc.argTypeIds.reduce((prev, typeId, idx) => { if ( proc.argModes.length === 0 || // all args are `in` @@ -38,23 +36,26 @@ export default (function PgRecordReturnTypesPlugin(builder) { ) { prev.push(introspectionResultsByKind.typeById[typeId]); } + return prev; }, []); const argModesWithOutput = [ "o", // OUT, "b", // INOUT - "t", // TABLE + "t", ]; const outputArgNames = proc.argTypeIds.reduce((prev, _, idx) => { if (argModesWithOutput.includes(proc.argModes[idx])) { prev.push(proc.argNames[idx] || ""); } + return prev; }, []); const outputArgTypes = proc.argTypeIds.reduce((prev, typeId, idx) => { if (argModesWithOutput.includes(proc.argModes[idx])) { prev.push(introspectionResultsByKind.typeById[typeId]); } + return prev; }, []); const isMutation = !proc.isStable; @@ -91,6 +92,7 @@ export default (function PgRecordReturnTypesPlugin(builder) { outputArgTypes[idx].id, null ); + if (memo[fieldName]) { throw new Error( `Tried to register field name '${fieldName}' twice in '${describePgEntity( @@ -98,6 +100,7 @@ export default (function PgRecordReturnTypesPlugin(builder) { )}'; the argument names are too similar.` ); } + memo[fieldName] = fieldWithHooks( fieldName, fieldContext => { @@ -131,6 +134,7 @@ export default (function PgRecordReturnTypesPlugin(builder) { }); return { type: fieldType, + resolve(data, _args, _context, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo( resolveInfo @@ -168,4 +172,4 @@ export default (function PgRecordReturnTypesPlugin(builder) { }, ["PgRecordReturnTypes"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgRowByUniqueConstraint.js b/packages/graphile-build-pg/src/plugins/PgRowByUniqueConstraint.ts similarity index 97% rename from packages/graphile-build-pg/src/plugins/PgRowByUniqueConstraint.js rename to packages/graphile-build-pg/src/plugins/PgRowByUniqueConstraint.ts index f2afe95d7..0dc33e8c9 100644 --- a/packages/graphile-build-pg/src/plugins/PgRowByUniqueConstraint.js +++ b/packages/graphile-build-pg/src/plugins/PgRowByUniqueConstraint.ts @@ -1,8 +1,6 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import debugSql from "./debugSql"; - -export default (async function PgRowByUniqueConstraint( +export default async function PgRowByUniqueConstraint( builder, { subscriptions } ) { @@ -37,7 +35,6 @@ export default (async function PgRowByUniqueConstraint( // PERFORMANCE: These used to be .filter(...) calls if (!table.namespace) return memo; if (omit(table, "read")) return memo; - const TableType = pgGetGqlTypeByTypeIdAndModifier( table.type.id, null @@ -46,6 +43,7 @@ export default (async function PgRowByUniqueConstraint( table.namespace.name, table.name ); + if (TableType) { const uniqueConstraints = table.constraints.filter( con => con.type === "u" || con.type === "p" @@ -54,15 +52,19 @@ export default (async function PgRowByUniqueConstraint( if (omit(constraint, "read")) { return; } + const keys = constraint.keyAttributes; + if (keys.some(key => omit(key, "read"))) { return; } + if (!keys.every(_ => _)) { throw new Error( "Consistency error: could not find an attribute!" ); } + const fieldName = inflection.rowByUniqueKeys( keys, table, @@ -78,6 +80,7 @@ export default (async function PgRowByUniqueConstraint( key.typeId, key.typeModifier ); + if (!InputType) { throw new Error( `Could not find input type for key '${ @@ -85,17 +88,20 @@ export default (async function PgRowByUniqueConstraint( }' on type '${TableType.name}'` ); } + memo[inflection.column(key)] = { type: new GraphQLNonNull(InputType), }; return memo; }, {}), + async resolve(parent, args, resolveContext, resolveInfo) { const { pgClient, liveRecord } = resolveContext; const parsedResolveInfoFragment = parseResolveInfo( resolveInfo ); parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin + const resolveData = getDataFromParsedResolveInfoFragment( parsedResolveInfoFragment, TableType @@ -111,6 +117,7 @@ export default (async function PgRowByUniqueConstraint( if (subscriptions && table.primaryKeyConstraint) { queryBuilder.selectIdentifiers(table); } + keys.forEach(key => { queryBuilder.where( sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( @@ -130,9 +137,11 @@ export default (async function PgRowByUniqueConstraint( const { rows: [row], } = await pgClient.query(text, values); + if (subscriptions && liveRecord && row) { liveRecord("pg", table, row.__identifiers); } + return row; }, }; @@ -144,6 +153,7 @@ export default (async function PgRowByUniqueConstraint( ); }); } + return memo; }, {}), `Adding "row by unique constraint" fields to root Query type` @@ -151,4 +161,4 @@ export default (async function PgRowByUniqueConstraint( }, ["PgRowByUniqueConstraint"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgRowNode.js b/packages/graphile-build-pg/src/plugins/PgRowNode.ts similarity index 97% rename from packages/graphile-build-pg/src/plugins/PgRowNode.js rename to packages/graphile-build-pg/src/plugins/PgRowNode.ts index 64fc078b7..7758ba4c2 100644 --- a/packages/graphile-build-pg/src/plugins/PgRowNode.js +++ b/packages/graphile-build-pg/src/plugins/PgRowNode.ts @@ -1,8 +1,6 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; import debugSql from "./debugSql"; - -export default (async function PgRowNode(builder, { subscriptions }) { +export default async function PgRowNode(builder, { subscriptions }) { builder.hook( "GraphQLObjectType", (object, build, context) => { @@ -21,17 +19,20 @@ export default (async function PgRowNode(builder, { subscriptions }) { // Node plugin must be disabled. return object; } + if (!isPgRowType || !table.namespace || omit(table, "read")) { return object; } + const sqlFullTableName = sql.identifier(table.namespace.name, table.name); const primaryKeyConstraint = table.primaryKeyConstraint; + if (!primaryKeyConstraint) { return object; } + const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes; - addNodeFetcherForTypeName( object.name, async ( @@ -43,9 +44,11 @@ export default (async function PgRowNode(builder, { subscriptions }) { resolveData ) => { const { pgClient, liveRecord } = resolveContext; + if (identifiers.length !== primaryKeys.length) { throw new Error("Invalid ID"); } + const query = queryFromResolveData( sqlFullTableName, undefined, @@ -57,6 +60,7 @@ export default (async function PgRowNode(builder, { subscriptions }) { if (subscriptions && table.primaryKeyConstraint) { queryBuilder.selectIdentifiers(table); } + primaryKeys.forEach((key, idx) => { queryBuilder.where( sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( @@ -76,9 +80,11 @@ export default (async function PgRowNode(builder, { subscriptions }) { const { rows: [row], } = await pgClient.query(text, values); + if (subscriptions && liveRecord && row) { liveRecord("pg", table, row.__identifiers); } + return row; } ); @@ -86,7 +92,6 @@ export default (async function PgRowNode(builder, { subscriptions }) { }, ["PgRowNode"] ); - builder.hook( "GraphQLObjectType:fields", (fields, build, context) => { @@ -121,7 +126,6 @@ export default (async function PgRowNode(builder, { subscriptions }) { // PERFORMANCE: These used to be .filter(...) calls if (!table.namespace) return memo; if (omit(table, "read")) return memo; - const TableType = pgGetGqlTypeByTypeIdAndModifier( table.type.id, null @@ -130,11 +134,14 @@ export default (async function PgRowNode(builder, { subscriptions }) { table.namespace.name, table.name ); + if (TableType) { const primaryKeyConstraint = table.primaryKeyConstraint; + if (!primaryKeyConstraint) { return memo; } + const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes; const fieldName = inflection.tableNode(table); @@ -157,17 +164,21 @@ export default (async function PgRowNode(builder, { subscriptions }) { type: new GraphQLNonNull(GraphQLID), }, }, + async resolve(parent, args, resolveContext, resolveInfo) { const { pgClient, liveRecord } = resolveContext; const nodeId = args[nodeIdFieldName]; + try { const { Type, identifiers, } = getTypeAndIdentifiersFromNodeId(nodeId); + if (Type !== TableType) { throw new Error("Mismatched type"); } + if (identifiers.length !== primaryKeys.length) { throw new Error("Invalid ID"); } @@ -176,6 +187,7 @@ export default (async function PgRowNode(builder, { subscriptions }) { resolveInfo ); parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin + const resolveData = getDataFromParsedResolveInfoFragment( parsedResolveInfoFragment, TableType @@ -191,6 +203,7 @@ export default (async function PgRowNode(builder, { subscriptions }) { if (subscriptions && table.primaryKeyConstraint) { queryBuilder.selectIdentifiers(table); } + primaryKeys.forEach((key, idx) => { queryBuilder.where( sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( @@ -210,9 +223,11 @@ export default (async function PgRowNode(builder, { subscriptions }) { const { rows: [row], } = await pgClient.query(text, values); + if (liveRecord && row) { liveRecord("pg", table, row.__identifiers); } + return row; } catch (e) { return null; @@ -230,10 +245,13 @@ export default (async function PgRowNode(builder, { subscriptions }) { table )}. You can rename this table via:\n\n ${sqlCommentByAddingTags( table, - { name: "newNameHere" } + { + name: "newNameHere", + } )}` ); } + return memo; }, {}), `Adding "row by node ID" fields to root Query type` @@ -241,4 +259,4 @@ export default (async function PgRowNode(builder, { subscriptions }) { }, ["PgRowNode"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.js b/packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.ts similarity index 95% rename from packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.js rename to packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.ts index ad00ca704..3f0dc6636 100644 --- a/packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.ts @@ -1,9 +1,8 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; const base64 = str => Buffer.from(String(str)).toString("base64"); -export default (function PgScalarFunctionConnectionPlugin(builder) { +export default function PgScalarFunctionConnectionPlugin(builder) { builder.hook( "init", (_, build) => { @@ -24,29 +23,29 @@ export default (function PgScalarFunctionConnectionPlugin(builder) { sqlCommentByAddingTags, pgField, } = build; - const Cursor = getTypeByName("Cursor"); introspectionResultsByKind.procedure.forEach(proc => { // PERFORMANCE: These used to be .filter(...) calls if (!proc.returnsSet) return; if (!proc.namespace) return; if (omit(proc, "execute")) return; - const returnType = introspectionResultsByKind.typeById[proc.returnTypeId]; const returnTypeTable = introspectionResultsByKind.classById[returnType.classId]; + if (returnTypeTable) { // Just use the standard table connection from PgTablesPlugin return; } + if (returnType.id === "2249") { // Defer handling to PgRecordFunctionConnectionPlugin return; - } - // TODO: PG10 doesn't support the equivalent of pg_attribute.atttypemod + } // TODO: PG10 doesn't support the equivalent of pg_attribute.atttypemod // on function arguments and return types, however maybe a later // version of PG will? + const NodeType = pgGetGqlTypeByTypeIdAndModifier(returnType.id, null) || GraphQLString; const EdgeType = newWithHooks( @@ -65,6 +64,7 @@ export default (function PgScalarFunctionConnectionPlugin(builder) { return { description: "A cursor for use in pagination.", type: Cursor, + resolve(data) { return base64(JSON.stringify(data.__cursor)); }, @@ -79,6 +79,7 @@ export default (function PgScalarFunctionConnectionPlugin(builder) { NodeType.name }\` at the end of the edge.`, type: NodeType, + resolve(data) { return data.value; }, @@ -100,8 +101,8 @@ export default (function PgScalarFunctionConnectionPlugin(builder) { pgIntrospection: proc, } ); - /*const ConnectionType = */ + newWithHooks( GraphQLObjectType, { @@ -114,6 +115,7 @@ export default (function PgScalarFunctionConnectionPlugin(builder) { nodes: pgField(build, fieldWithHooks, "nodes", { description: `A list of \`${NodeType.name}\` objects.`, type: new GraphQLNonNull(new GraphQLList(NodeType)), + resolve(data) { return data.data.map(entry => entry.value); }, @@ -129,6 +131,7 @@ export default (function PgScalarFunctionConnectionPlugin(builder) { type: new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(EdgeType)) ), + resolve(data) { return data.data; }, @@ -164,4 +167,4 @@ export default (function PgScalarFunctionConnectionPlugin(builder) { [], ["PgTypes"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgTablesPlugin.js b/packages/graphile-build-pg/src/plugins/PgTablesPlugin.ts similarity index 99% rename from packages/graphile-build-pg/src/plugins/PgTablesPlugin.js rename to packages/graphile-build-pg/src/plugins/PgTablesPlugin.ts index 1cb63fee5..445741697 100644 --- a/packages/graphile-build-pg/src/plugins/PgTablesPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgTablesPlugin.ts @@ -1,5 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; + const base64 = str => Buffer.from(String(str)).toString("base64"); const hasNonNullKey = row => { @@ -9,6 +9,7 @@ const hasNonNullKey = row => { ) { return true; } + for (const k in row) { if (row.hasOwnProperty(k)) { if ((k[0] !== "_" || k[1] !== "_") && row[k] !== null) { @@ -16,10 +17,11 @@ const hasNonNullKey = row => { } } } + return false; }; -export default (function PgTablesPlugin( +export default function PgTablesPlugin( builder, { pgForbidSetofFunctionsToReturnNull = false } ) { @@ -32,7 +34,6 @@ export default (function PgTablesPlugin( return null; } }; - builder.hook( "init", (_, build) => { @@ -65,13 +66,15 @@ export default (function PgTablesPlugin( const nullableIf = (condition, Type) => condition ? Type : new GraphQLNonNull(Type); - const Cursor = getTypeByName("Cursor"); + const Cursor = getTypeByName("Cursor"); introspectionResultsByKind.class.forEach(table => { const tablePgType = table.type; + if (!tablePgType) { throw new Error("Could not determine the type for this table"); } + const arrayTablePgType = tablePgType.arrayType; const primaryKeyConstraint = table.primaryKeyConstraint; const primaryKeys = @@ -95,6 +98,7 @@ export default (function PgTablesPlugin( if (TableType) { return TableType; } + if (pg2GqlMapper[tablePgType.id]) { // Already handled throw new Error( @@ -103,6 +107,7 @@ export default (function PgTablesPlugin( }'!` ); } + TableType = newWithHooks( GraphQLObjectType, { @@ -117,6 +122,7 @@ export default (function PgTablesPlugin( }, fields: ({ addDataGeneratorForField, Self }) => { const fields = {}; + if (shouldHaveNodeId) { // Enable nodeId interface addDataGeneratorForField(nodeIdFieldName, () => { @@ -130,6 +136,7 @@ export default (function PgTablesPlugin( description: "A globally unique identifier. Can be used in various places throughout the system to identify this single value.", type: new GraphQLNonNull(GraphQLID), + resolve(data) { return ( data.__identifiers && @@ -141,6 +148,7 @@ export default (function PgTablesPlugin( }, }; } + return fields; }, }, @@ -181,6 +189,7 @@ export default (function PgTablesPlugin( isInputType: true, isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, + pgAddSubfield(fieldName, attrName, pgType, spec, typeModifier) { pgCreateInputFields[fieldName] = { name: attrName, @@ -218,6 +227,7 @@ export default (function PgTablesPlugin( isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, isPgPatch: true, + pgAddSubfield( fieldName, attrName, @@ -254,6 +264,7 @@ export default (function PgTablesPlugin( isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, isPgBaseInput: true, + pgAddSubfield( fieldName, attrName, @@ -276,6 +287,7 @@ export default (function PgTablesPlugin( map: _ => _, unmap: (obj, modifier) => { let fieldLookup; + if (modifier === "patch") { fieldLookup = pgPatchInputFields; } else if (modifier === "base") { @@ -289,6 +301,7 @@ export default (function PgTablesPlugin( const fieldName = inflection.column(attr); const inputField = fieldLookup[fieldName]; const v = obj[fieldName]; + if (inputField && v != null) { const { type, typeModifier } = inputField; return sql.fragment`${gql2pg( @@ -310,7 +323,6 @@ export default (function PgTablesPlugin( )}`; }, }; - const EdgeType = newWithHooks( GraphQLObjectType, { @@ -332,6 +344,7 @@ export default (function PgTablesPlugin( return { description: "A cursor for use in pagination.", type: Cursor, + resolve(data) { return ( data.__cursor && @@ -354,6 +367,7 @@ export default (function PgTablesPlugin( !pgForbidSetofFunctionsToReturnNull, TableType ), + resolve(data, _args, resolveContext, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo( resolveInfo @@ -362,6 +376,7 @@ export default (function PgTablesPlugin( data[safeAlias], data.__identifiers ); + if ( record && primaryKeys && @@ -374,6 +389,7 @@ export default (function PgTablesPlugin( data.__identifiers ); } + return record; }, }, @@ -399,8 +415,8 @@ export default (function PgTablesPlugin( } ); const PageInfo = getTypeByName(inflection.builtin("PageInfo")); - /*const ConnectionType = */ + newWithHooks( GraphQLObjectType, { @@ -423,6 +439,7 @@ export default (function PgTablesPlugin( ) ) ), + resolve(data, _args, resolveContext, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo( resolveInfo @@ -432,6 +449,7 @@ export default (function PgTablesPlugin( entry[safeAlias], entry.__identifiers ); + if ( record && resolveContext.liveRecord && @@ -461,6 +479,7 @@ export default (function PgTablesPlugin( type: new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(EdgeType)) ), + resolve(data, _args, _context, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo( resolveInfo @@ -480,6 +499,7 @@ export default (function PgTablesPlugin( pageInfo: PageInfo && { description: "Information to aid in pagination.", type: new GraphQLNonNull(PageInfo), + resolve(data) { return data; }, @@ -513,19 +533,22 @@ export default (function PgTablesPlugin( const TableType = pgGetGqlTypeByTypeIdAndModifier( tablePgType.id, null - ); - // This must come after the pgGetGqlTypeByTypeIdAndModifier call + ); // This must come after the pgGetGqlTypeByTypeIdAndModifier call + if (modifier === "patch") { // TODO: v5: move the definition from above down here return TablePatchType; } + if (modifier === "base") { // TODO: v5: move the definition from above down here return TableBaseInputType; } + if (TableType) { return getTypeByName(inflection.inputType(TableType)); } + return null; }, true @@ -557,6 +580,7 @@ export default (function PgTablesPlugin( tablePgType.id, modifier ); + if (RelevantTableInputType) { return new GraphQLList(RelevantTableInputType); } @@ -571,4 +595,4 @@ export default (function PgTablesPlugin( [], ["PgTypes"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js b/packages/graphile-build-pg/src/plugins/PgTypesPlugin.ts similarity index 91% rename from packages/graphile-build-pg/src/plugins/PgTypesPlugin.js rename to packages/graphile-build-pg/src/plugins/PgTypesPlugin.ts index 891a5603e..540db9250 100644 --- a/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgTypesPlugin.ts @@ -1,8 +1,5 @@ -// @flow -import type { Plugin } from "graphile-build"; - +import { Plugin } from "graphile-build"; import makeGraphQLJSONType from "../GraphQLJSON"; - import rawParseInterval from "postgres-interval"; import LRU from "lru-cache"; @@ -15,17 +12,20 @@ function identity(value) { } const parseCache = LRU(500); + function parseInterval(str) { let result = parseCache.get(str); + if (!result) { result = rawParseInterval(str); Object.freeze(result); parseCache.set(str, result); } + return result; } -export default (function PgTypesPlugin( +export default function PgTypesPlugin( builder, { pgExtendedTypes = true, @@ -45,7 +45,6 @@ export default (function PgTypesPlugin( inflection, graphql, } = build; - const addType = build.addType.bind(build); const { GraphQLNonNull, @@ -62,15 +61,16 @@ export default (function PgTypesPlugin( getNamedType, Kind, } = graphql; - const gqlTypeByTypeIdGenerator = {}; const gqlInputTypeByTypeIdGenerator = {}; + if (build.pgGqlTypeByTypeId || build.pgGqlInputTypeByTypeId) { // I don't expect anyone to receive this error, because I don't think anyone uses this interface. throw new Error( "Sorry! This interface is no longer supported because it is not granular enough. It's not hard to port it to the new system - please contact Benjie and he'll walk you through it." ); } + const gqlTypeByTypeIdAndModifier = Object.assign( {}, build.pgGqlTypeByTypeIdAndModifier @@ -80,13 +80,16 @@ export default (function PgTypesPlugin( build.pgGqlInputTypeByTypeIdAndModifier ); const pg2GqlMapper = {}; + const pg2gql = (val, type) => { if (val == null) { return val; } + if (val.__isNull) { return null; } + if (pg2GqlMapper[type.id]) { return pg2GqlMapper[type.id].map(val); } else if (type.domainBaseType) { @@ -99,30 +102,35 @@ export default (function PgTypesPlugin( }.${type.name}'` ); } + return val.map(v => pg2gql(v, type.arrayItemType)); } else { return val; } }; + const gql2pg = (val, type, modifier) => { if (modifier === undefined) { let stack; + try { throw new Error(); } catch (e) { stack = e.stack; - } - // eslint-disable-next-line no-console + } // eslint-disable-next-line no-console + console.warn( "gql2pg should be called with three arguments, the third being the type modifier (or `null`); " + (stack || "") - ); - // Hack for backwards compatibility: + ); // Hack for backwards compatibility: + modifier = null; } + if (val == null) { return sql.null; } + if (pg2GqlMapper[type.id]) { return pg2GqlMapper[type.id].unmap(val, modifier); } else if (type.domainBaseType) { @@ -135,6 +143,7 @@ export default (function PgTypesPlugin( }.${type.name}' (type: ${type === null ? "null" : typeof type})` ); } + return sql.fragment`array[${sql.join( val.map(v => gql2pg(v, type.arrayItemType, modifier)), ", " @@ -175,6 +184,7 @@ export default (function PgTypesPlugin( }, }; }; + const GQLInterval = new GraphQLObjectType({ name: inflection.builtin("Interval"), description: @@ -182,7 +192,6 @@ export default (function PgTypesPlugin( fields: makeIntervalFields(), }); addType(GQLInterval, "graphile-build-pg built-in"); - const GQLIntervalInput = new GraphQLInputObjectType({ name: inflection.inputType(inflection.builtin("Interval")), description: @@ -201,6 +210,7 @@ export default (function PgTypesPlugin( if (ast.kind !== Kind.STRING) { throw new Error("Can only parse string values"); } + return ast.value; }, }); @@ -215,20 +225,22 @@ export default (function PgTypesPlugin( ); addType(BigFloat, "graphile-build-pg built-in"); addType(BitString, "graphile-build-pg built-in"); - const rawTypes = [ 1186, // interval 1082, // date 1114, // timestamp 1184, // timestamptz 1083, // time - 1266, // timetz + 1266, ]; const tweakToJson = fragment => fragment; // Since everything is to_json'd now, just pass through + const tweakToText = fragment => sql.fragment`(${fragment})::text`; + const tweakToNumericText = fragment => sql.fragment`(${fragment})::numeric::text`; + const pgTweaksByTypeIdAndModifer = {}; const pgTweaksByTypeId = Object.assign( // ::text rawTypes @@ -261,6 +273,7 @@ export default (function PgTypesPlugin( (pgTweaksByTypeIdAndModifer[type.id] && pgTweaksByTypeIdAndModifer[type.id][typeModifierKey]) || pgTweaksByTypeId[type.id]; + if (tweaker) { return tweaker(fragment, resolveData); } else if (type.domainBaseType) { @@ -277,11 +290,12 @@ export default (function PgTypesPlugin( type.namespaceName }.${type.name}")` ); + if (process.env.NODE_ENV === "test") { // This is to ensure that Graphile core does not introduce these problems throw error; - } - // eslint-disable-next-line no-console + } // eslint-disable-next-line no-console + console.error(error); return fragment; } else { @@ -289,12 +303,11 @@ export default (function PgTypesPlugin( } }; /* - Determined by running: - - select oid, typname, typarray, typcategory, typtype from pg_catalog.pg_type where typtype = 'b' order by oid; - - We only need to add oidLookups for types that don't have the correct fallback + Determined by running: + select oid, typname, typarray, typcategory, typtype from pg_catalog.pg_type where typtype = 'b' order by oid; + We only need to add oidLookups for types that don't have the correct fallback */ + const SimpleDate = stringType( inflection.builtin("Date"), "The day, does not include a time." @@ -318,18 +331,20 @@ export default (function PgTypesPlugin( const InetType = stringType( inflection.builtin("InternetAddress"), "An IPv4 or IPv6 host address, and optionally its subnet." - ); + ); // pgExtendedTypes might change what types we use for things - // pgExtendedTypes might change what types we use for things const JSONType = pgExtendedTypes ? makeGraphQLJSONType(graphql, inflection.builtin("JSON")) : SimpleJSON; const UUIDType = SimpleUUID; // GraphQLUUID + const DateType = SimpleDate; // GraphQLDate + const DateTimeType = SimpleDatetime; // GraphQLDateTime - const TimeType = SimpleTime; // GraphQLTime + const TimeType = SimpleTime; // GraphQLTime // 'point' in PostgreSQL is a 16-byte type that's comprised of two 8-byte floats. + const Point = new GraphQLObjectType({ name: inflection.builtin("Point"), fields: { @@ -351,54 +366,71 @@ export default (function PgTypesPlugin( type: new GraphQLNonNull(GraphQLFloat), }, }, - }); + }); // Other plugins might want to use JSON - // Other plugins might want to use JSON addType(JSONType, "graphile-build-pg built-in"); addType(UUIDType, "graphile-build-pg built-in"); addType(DateType, "graphile-build-pg built-in"); addType(DateTimeType, "graphile-build-pg built-in"); addType(TimeType, "graphile-build-pg built-in"); - const oidLookup = { "20": stringType( inflection.builtin("BigInt"), "A signed eight-byte integer. The upper big integer values are greater then the max value for a JavaScript number. Therefore all big integers will be output as strings and not numbers." - ), // bitint - even though this is int8, it's too big for JS int, so cast to string. - "21": GraphQLInt, // int2 - "23": GraphQLInt, // int4 - "700": GraphQLFloat, // float4 - "701": GraphQLFloat, // float8 - "1700": BigFloat, // numeric - "790": GraphQLFloat, // money - - "1186": GQLInterval, // interval - "1082": DateType, // date - "1114": DateTimeType, // timestamp - "1184": DateTimeType, // timestamptz - "1083": TimeType, // time - "1266": TimeType, // timetz - - "114": JSONType, // json - "3802": JSONType, // jsonb - "2950": UUIDType, // uuid - - "1560": BitString, // bit - "1562": BitString, // varbit - - "18": GraphQLString, // char - "25": GraphQLString, // text - "1043": GraphQLString, // varchar - - "600": Point, // point - + ), + // bitint - even though this is int8, it's too big for JS int, so cast to string. + "21": GraphQLInt, + // int2 + "23": GraphQLInt, + // int4 + "700": GraphQLFloat, + // float4 + "701": GraphQLFloat, + // float8 + "1700": BigFloat, + // numeric + "790": GraphQLFloat, + // money + "1186": GQLInterval, + // interval + "1082": DateType, + // date + "1114": DateTimeType, + // timestamp + "1184": DateTimeType, + // timestamptz + "1083": TimeType, + // time + "1266": TimeType, + // timetz + "114": JSONType, + // json + "3802": JSONType, + // jsonb + "2950": UUIDType, + // uuid + "1560": BitString, + // bit + "1562": BitString, + // varbit + "18": GraphQLString, + // char + "25": GraphQLString, + // text + "1043": GraphQLString, + // varchar + "600": Point, + // point "869": InetType, }; const oidInputLookup = { - "1186": GQLIntervalInput, // interval + "1186": GQLIntervalInput, + // interval "600": PointInput, // point }; + const jsonStringify = o => JSON.stringify(o); + if (pgExtendedTypes) { pg2GqlMapper[114] = { map: identity, @@ -410,9 +442,10 @@ export default (function PgTypesPlugin( unmap: str => sql.value(str), }; } - pg2GqlMapper[3802] = pg2GqlMapper[114]; // jsonb + pg2GqlMapper[3802] = pg2GqlMapper[114]; // jsonb // interval + pg2GqlMapper[1186] = { map: str => parseInterval(str), unmap: o => { @@ -425,21 +458,21 @@ export default (function PgTypesPlugin( "years", ]; const parts = []; + for (const key of keys) { if (o[key]) { parts.push(`${o[key]} ${key}`); } } + return sql.value(parts.join(" ") || "0 seconds"); }, }; - pg2GqlMapper[790] = { map: _ => _, unmap: val => sql.fragment`(${sql.value(val)})::money`, - }; + }; // point - // point pg2GqlMapper[600] = { map: f => { if (f[0] === "(" && f[f.length - 1] === ")") { @@ -447,27 +480,30 @@ export default (function PgTypesPlugin( .substr(1, f.length - 2) .split(",") .map(f => parseFloat(f)); - return { x, y }; + return { + x, + y, + }; } }, unmap: o => sql.fragment`point(${sql.value(o.x)}, ${sql.value(o.y)})`, - }; - - // TODO: add more support for geometric types + }; // TODO: add more support for geometric types let depth = 0; - /* - * Enforce: this is the fallback when we can't find a specific GraphQL type - * for a specific PG type. Use the generators from - * `pgRegisterGqlTypeByTypeId` first, this is a last resort. - */ + * Enforce: this is the fallback when we can't find a specific GraphQL type + * for a specific PG type. Use the generators from + * `pgRegisterGqlTypeByTypeId` first, this is a last resort. + */ + const enforceGqlTypeByPgTypeId = (typeId, typeModifier) => { const type = introspectionResultsByKind.type.find(t => t.id === typeId); depth++; + if (depth > 50) { throw new Error("Type enforcement went too deep - infinite loop?"); } + try { return reallyEnforceGqlTypeByPgTypeAndModifier(type, typeModifier); } catch (e) { @@ -475,43 +511,50 @@ export default (function PgTypesPlugin( `Error occurred when processing database type '${ type.namespaceName }.${type.name}' (type=${type.type}):\n${indent(e.message)}` - ); - // $FlowFixMe + ); // $FlowFixMe + error.originalError = e; throw error; } finally { depth--; } }; + const reallyEnforceGqlTypeByPgTypeAndModifier = (type, typeModifier) => { if (!type.id) { throw new Error( `Invalid argument to enforceGqlTypeByPgTypeId - expected a full type, received '${type}'` ); } + if (!gqlTypeByTypeIdAndModifier[type.id]) { gqlTypeByTypeIdAndModifier[type.id] = {}; } + if (!gqlInputTypeByTypeIdAndModifier[type.id]) { gqlInputTypeByTypeIdAndModifier[type.id] = {}; } - const typeModifierKey = typeModifier != null ? typeModifier : -1; - // Explicit overrides + + const typeModifierKey = typeModifier != null ? typeModifier : -1; // Explicit overrides + if (!gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]) { const gqlType = oidLookup[type.id]; + if (gqlType) { gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = gqlType; } } + if (!gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey]) { const gqlInputType = oidInputLookup[type.id]; + if (gqlInputType) { gqlInputTypeByTypeIdAndModifier[type.id][ typeModifierKey ] = gqlInputType; } - } - // Enums + } // Enums + if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.type === "e" @@ -528,8 +571,8 @@ export default (function PgTypesPlugin( return memo; }, {}), }); - } - // Ranges + } // Ranges + if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.type === "r" @@ -540,11 +583,14 @@ export default (function PgTypesPlugin( subtype.id, typeModifier ); + if (!gqlRangeSubType) { throw new Error("Range of unsupported"); } + let Range = getTypeByName(inflection.rangeType(gqlRangeSubType.name)); let RangeInput; + if (!Range) { const RangeBound = new GraphQLObjectType({ name: inflection.rangeBoundType(gqlRangeSubType.name), @@ -611,13 +657,16 @@ export default (function PgTypesPlugin( } else { RangeInput = getTypeByName(inflection.inputType(Range.name)); } + gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = Range; gqlInputTypeByTypeIdAndModifier[type.id][ typeModifierKey ] = RangeInput; + if (pgTweaksByTypeIdAndModifer[type.id] === undefined) { pgTweaksByTypeIdAndModifer[type.id] = {}; } + pgTweaksByTypeIdAndModifer[type.id][ typeModifierKey ] = fragment => sql.fragment`case @@ -641,6 +690,7 @@ export default (function PgTypesPlugin( )}, 'inclusive', upper_inc(${fragment})) end ) end`; + pg2GqlMapper[type.id] = { map: identity, unmap: ({ start, end }) => { @@ -659,9 +709,8 @@ export default (function PgTypesPlugin( )})`; }, }; - } + } // Domains - // Domains if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.type === "d" && @@ -674,8 +723,8 @@ export default (function PgTypesPlugin( const baseInputType = gqlInputTypeByTypeIdAndModifier[type.domainBaseTypeId][ typeModifierKey - ]; - // Hack stolen from: https://github.com/graphile/postgraphile/blob/ade728ed8f8e3ecdc5fdad7d770c67aa573578eb/src/graphql/schema/type/aliasGqlType.ts#L16 + ]; // Hack stolen from: https://github.com/graphile/postgraphile/blob/ade728ed8f8e3ecdc5fdad7d770c67aa573578eb/src/graphql/schema/type/aliasGqlType.ts#L16 + gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = Object.assign( Object.create(baseType), { @@ -683,6 +732,7 @@ export default (function PgTypesPlugin( description: type.description, } ); + if (baseInputType && baseInputType !== baseType) { gqlInputTypeByTypeIdAndModifier[type.id][ typeModifierKey @@ -693,9 +743,8 @@ export default (function PgTypesPlugin( description: type.description, }); } - } + } // Arrays - // Arrays if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.category === "A" @@ -707,43 +756,42 @@ export default (function PgTypesPlugin( gqlTypeByTypeIdAndModifier[type.id][ typeModifierKey ] = new GraphQLList(arrayEntryOutputType); + if (!disableIssue390Fix) { const arrayEntryInputType = getGqlInputTypeByTypeIdAndModifier( type.arrayItemTypeId, typeModifier ); + if (arrayEntryInputType) { gqlInputTypeByTypeIdAndModifier[type.id][ typeModifierKey ] = new GraphQLList(arrayEntryInputType); } } - } + } // Booleans - // Booleans if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.category === "B" ) { gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = GraphQLBoolean; - } - - // Numbers may be too large for GraphQL/JS to handle, so stringify by + } // Numbers may be too large for GraphQL/JS to handle, so stringify by // default. + if ( !gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && type.category === "N" ) { pgTweaksByTypeId[type.id] = tweakToText; gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = BigFloat; - } + } // Nothing else worked; pass through as string! - // Nothing else worked; pass through as string! if (!gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]) { // XXX: consider using stringType(upperFirst(camelCase(`fallback_${type.name}`)), type.description)? gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = GraphQLString; - } - // Now for input types, fall back to output types if possible + } // Now for input types, fall back to output types if possible + if (!gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey]) { if ( isInputType(gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]) @@ -752,6 +800,7 @@ export default (function PgTypesPlugin( gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]; } } + addType( getNamedType(gqlTypeByTypeIdAndModifier[type.id][typeModifierKey]) ); @@ -764,27 +813,35 @@ export default (function PgTypesPlugin( useFallback = true ) { const typeModifierKey = typeModifier != null ? typeModifier : -1; + if (!gqlTypeByTypeIdAndModifier[typeId]) { gqlTypeByTypeIdAndModifier[typeId] = {}; } + if (!gqlInputTypeByTypeIdAndModifier[typeId]) { gqlInputTypeByTypeIdAndModifier[typeId] = {}; } + if (!gqlTypeByTypeIdAndModifier[typeId][typeModifierKey]) { const type = introspectionResultsByKind.type.find( t => t.id === typeId ); + if (!type) { throw new Error( `Type '${typeId}' not present in introspection results` ); } + const gen = gqlTypeByTypeIdGenerator[type.id]; + if (gen) { const set = Type => { gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = Type; }; + const result = gen(set, typeModifier); + if (result) { if ( gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] && @@ -796,10 +853,12 @@ export default (function PgTypesPlugin( }'` ); } + gqlTypeByTypeIdAndModifier[type.id][typeModifierKey] = result; } } } + if ( !gqlTypeByTypeIdAndModifier[typeId][typeModifierKey] && typeModifierKey > -1 @@ -807,27 +866,31 @@ export default (function PgTypesPlugin( // Fall back to `null` modifier, but if that still doesn't work, we // still want to pass the modifier to enforceGqlTypeByPgTypeId. const fallback = getGqlTypeByTypeIdAndModifier(typeId, null, false); + if (fallback) { return fallback; } } + if ( useFallback && !gqlTypeByTypeIdAndModifier[typeId][typeModifierKey] ) { return enforceGqlTypeByPgTypeId(typeId, typeModifier); } + return gqlTypeByTypeIdAndModifier[typeId][typeModifierKey]; } function getGqlInputTypeByTypeIdAndModifier(typeId, typeModifier = null) { // First, load the OUTPUT type (it might register an input type) getGqlTypeByTypeIdAndModifier(typeId, typeModifier); - const typeModifierKey = typeModifier != null ? typeModifier : -1; + if (!gqlInputTypeByTypeIdAndModifier[typeId]) { gqlInputTypeByTypeIdAndModifier[typeId] = {}; } + if (!gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey]) { const type = introspectionResultsByKind.typeById[typeId]; @@ -836,12 +899,16 @@ export default (function PgTypesPlugin( `Type '${typeId}' not present in introspection results` ); } + const gen = gqlInputTypeByTypeIdGenerator[type.id]; + if (gen) { const set = Type => { gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] = Type; }; + const result = gen(set, typeModifier); + if (result) { if ( gqlInputTypeByTypeIdAndModifier[type.id][typeModifierKey] && @@ -854,13 +921,14 @@ export default (function PgTypesPlugin( }'` ); } + gqlInputTypeByTypeIdAndModifier[type.id][ typeModifierKey ] = result; } } - } - // Use the same type as the output type if it's valid input + } // Use the same type as the output type if it's valid input + if ( !gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey] && gqlTypeByTypeIdAndModifier[typeId] && @@ -870,6 +938,7 @@ export default (function PgTypesPlugin( gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey] = gqlTypeByTypeIdAndModifier[typeId][typeModifierKey]; } + if ( !gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey] && typeModifierKey > -1 @@ -877,19 +946,24 @@ export default (function PgTypesPlugin( // Fall back to default return getGqlInputTypeByTypeIdAndModifier(typeId, null); } + return gqlInputTypeByTypeIdAndModifier[typeId][typeModifierKey]; } + function registerGqlTypeByTypeId(typeId, gen, yieldToExisting = false) { if (gqlTypeByTypeIdGenerator[typeId]) { if (yieldToExisting) { return; } + throw new Error( `There's already a type generator registered for '${typeId}'` ); } + gqlTypeByTypeIdGenerator[typeId] = gen; } + function registerGqlInputTypeByTypeId( typeId, gen, @@ -899,14 +973,15 @@ export default (function PgTypesPlugin( if (yieldToExisting) { return; } + throw new Error( `There's already an input type generator registered for '${typeId}'` ); } + gqlInputTypeByTypeIdGenerator[typeId] = gen; - } + } // DEPRECATIONS! - // DEPRECATIONS! function getGqlTypeByTypeId(typeId, typeModifier) { if (typeModifier === undefined) { // eslint-disable-next-line no-console @@ -914,8 +989,10 @@ export default (function PgTypesPlugin( "DEPRECATION WARNING: getGqlTypeByTypeId should not be used - for some columns we also require typeModifier to be specified. Please update your code ASAP to pass `attribute.typeModifier` through as the second parameter (or null if it's not available)." ); } + return getGqlTypeByTypeIdAndModifier(typeId, typeModifier); } + function getGqlInputTypeByTypeId(typeId, typeModifier) { if (typeModifier === undefined) { // eslint-disable-next-line no-console @@ -923,8 +1000,10 @@ export default (function PgTypesPlugin( "DEPRECATION WARNING: getGqlInputTypeByTypeId should not be used - for some columns we also require typeModifier to be specified. Please update your code ASAP to pass `attribute.typeModifier` through as the second parameter (or null if it's not available)." ); } + return getGqlInputTypeByTypeIdAndModifier(typeId, typeModifier); } + function pgTweakFragmentForType( fragment, type, @@ -937,14 +1016,14 @@ export default (function PgTypesPlugin( "DEPRECATION WARNING: pgTweakFragmentForType should not be used - for some columns we also require typeModifier to be specified. Please update your code ASAP to pass `attribute.typeModifier` through as the third parameter (or null if it's not available)." ); } + return pgTweakFragmentForTypeAndModifier( fragment, type, typeModifier, resolveData ); - } - // END OF DEPRECATIONS! + } // END OF DEPRECATIONS! return build.extend(build, { pgRegisterGqlTypeByTypeId: registerGqlTypeByTypeId, @@ -957,10 +1036,11 @@ export default (function PgTypesPlugin( pgTweakFragmentForTypeAndModifier, pgTweaksByTypeId, pgTweaksByTypeIdAndModifer, - // DEPRECATED METHODS: - pgGetGqlTypeByTypeId: getGqlTypeByTypeId, // DEPRECATED, replaced by getGqlTypeByTypeIdAndModifier - pgGetGqlInputTypeByTypeId: getGqlInputTypeByTypeId, // DEPRECATED, replaced by getGqlInputTypeByTypeIdAndModifier + pgGetGqlTypeByTypeId: getGqlTypeByTypeId, + // DEPRECATED, replaced by getGqlTypeByTypeIdAndModifier + pgGetGqlInputTypeByTypeId: getGqlInputTypeByTypeId, + // DEPRECATED, replaced by getGqlInputTypeByTypeIdAndModifier pgTweakFragmentForType, // DEPRECATED, replaced by pgTweakFragmentForTypeAndModifier }); }, @@ -968,8 +1048,8 @@ export default (function PgTypesPlugin( [], ["PgIntrospection", "StandardTypes"] ); - /* Start of hstore type */ + builder.hook( "inflection", (inflection, build) => { @@ -997,37 +1077,34 @@ export default (function PgTypesPlugin( pg2GqlMapper, pgSql: sql, graphql, - } = build; + } = build; // Check we have the hstore extension - // Check we have the hstore extension const hstoreExtension = introspectionResultsByKind.extension.find( e => e.name === "hstore" ); + if (!hstoreExtension) { return build; - } + } // Get the 'hstore' type itself: - // Get the 'hstore' type itself: const hstoreType = introspectionResultsByKind.type.find( t => t.name === "hstore" && t.namespaceId === hstoreExtension.namespaceId ); + if (!hstoreType) { return build; } - const hstoreTypeName = build.inflection.hstoreType(); - - // We're going to use our own special HStore type for this so that we get + const hstoreTypeName = build.inflection.hstoreType(); // We're going to use our own special HStore type for this so that we get // better validation; but you could just as easily use JSON directly if you // wanted to. - const GraphQLHStoreType = makeGraphQLHstoreType(graphql, hstoreTypeName); - // Now register the hstore type with the type system for both output and input. + const GraphQLHStoreType = makeGraphQLHstoreType(graphql, hstoreTypeName); // Now register the hstore type with the type system for both output and input. + pgRegisterGqlTypeByTypeId(hstoreType.id, () => GraphQLHStoreType); - pgRegisterGqlInputTypeByTypeId(hstoreType.id, () => GraphQLHStoreType); + pgRegisterGqlInputTypeByTypeId(hstoreType.id, () => GraphQLHStoreType); // Finally we must tell the system how to translate the data between PG-land and JS-land: - // Finally we must tell the system how to translate the data between PG-land and JS-land: pg2GqlMapper[hstoreType.id] = { // node-postgres parses hstore for us, no action required on map map: identity, @@ -1038,7 +1115,6 @@ export default (function PgTypesPlugin( hstoreType.name )})`, }; - return build; }, ["PgTypesHstore"], @@ -1046,7 +1122,7 @@ export default (function PgTypesPlugin( ["PgTypes"] ); /* End of hstore type */ -}: Plugin); +} as Plugin; function makeGraphQLHstoreType(graphql, hstoreTypeName) { const { GraphQLScalarType, Kind } = graphql; @@ -1058,8 +1134,10 @@ function makeGraphQLHstoreType(graphql, hstoreTypeName) { } else if (typeof obj === "object") { // A hash with string/null values is also okay const keys = Object.keys(obj); + for (const key of keys) { const val = obj[key]; + if (val === null) { // Null is okay } else if (typeof val === "string") { @@ -1069,6 +1147,7 @@ function makeGraphQLHstoreType(graphql, hstoreTypeName) { return false; } } + return true; } else { // Everything else is invalid. @@ -1080,6 +1159,7 @@ function makeGraphQLHstoreType(graphql, hstoreTypeName) { if (isValidHstoreObject(obj)) { return obj; } + throw new TypeError( `This is not a valid ${hstoreTypeName} object, it must be a key/value hash where keys and values are both strings (or null).` ); @@ -1091,21 +1171,27 @@ function makeGraphQLHstoreType(graphql, hstoreTypeName) { case Kind.FLOAT: // Number isn't really okay, but we'll coerce it to a string anyway. return String(parseFloat(ast.value)); + case Kind.STRING: // String is okay. return String(ast.value); + case Kind.NULL: // Null is okay. return null; + case Kind.VARIABLE: { // Variable is okay if that variable is either a string or null. const name = ast.name.value; const value = variables ? variables[name] : undefined; + if (value === null || typeof value === "string") { return value; } + return undefined; } + default: // Everything else is invalid. return undefined; @@ -1123,6 +1209,7 @@ function makeGraphQLHstoreType(graphql, hstoreTypeName) { if (!isValidHstoreObject(value)) { return undefined; } + return value; } @@ -1136,15 +1223,15 @@ function makeGraphQLHstoreType(graphql, hstoreTypeName) { if (!isValidHstoreObject(value)) { return undefined; } + return value; } default: return undefined; } - } + } // TODO: use newWithHooks instead - // TODO: use newWithHooks instead const GraphQLHStore = new GraphQLScalarType({ name: hstoreTypeName, description: @@ -1154,11 +1241,10 @@ function makeGraphQLHstoreType(graphql, hstoreTypeName) { parseLiteral, }); return GraphQLHStore; -} - -// To include a double quote or a backslash in a key or value, escape it +} // To include a double quote or a backslash in a key or value, escape it // with a backslash. // -- https://www.postgresql.org/docs/10/static/hstore.html + function toHstoreString(str) { return '"' + str.replace(/(["\\])/g, "\\$1") + '"'; } @@ -1167,17 +1253,22 @@ function hstoreStringify(o) { if (o === null) { return null; } + if (typeof o !== "object") { throw new TypeError("Expected an hstore object"); } + const keys = Object.keys(o); + const encodeKeyValue = key => { const value = o[key]; + if (value === null) { return `${toHstoreString(key)} => NULL`; } else { return `${toHstoreString(key)} => ${toHstoreString(String(value))}`; } }; + return keys.map(encodeKeyValue).join(", "); } diff --git a/packages/graphile-build-pg/src/plugins/addStartEndCursor.js b/packages/graphile-build-pg/src/plugins/addStartEndCursor.ts similarity index 78% rename from packages/graphile-build-pg/src/plugins/addStartEndCursor.js rename to packages/graphile-build-pg/src/plugins/addStartEndCursor.ts index b02a5f06d..62e5e3e6a 100644 --- a/packages/graphile-build-pg/src/plugins/addStartEndCursor.js +++ b/packages/graphile-build-pg/src/plugins/addStartEndCursor.ts @@ -1,12 +1,12 @@ -// @flow -import type { Plugin } from "graphile-build"; +import { Plugin } from "graphile-build"; + const base64 = str => Buffer.from(String(str)).toString("base64"); function cursorify(val) { return val && val.__cursor ? base64(JSON.stringify(val.__cursor)) : null; } -export default (function addStartEndCursor(value) { +export default function addStartEndCursor(value) { const data = value && value.data && value.data.length ? value.data : null; const startCursor = cursorify(data && data[0]); const endCursor = cursorify(data && data[value.data.length - 1]); @@ -14,4 +14,4 @@ export default (function addStartEndCursor(value) { startCursor, endCursor, }); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build-pg/src/plugins/debugSql.js b/packages/graphile-build-pg/src/plugins/debugSql.ts similarity index 99% rename from packages/graphile-build-pg/src/plugins/debugSql.js rename to packages/graphile-build-pg/src/plugins/debugSql.ts index db667a59e..9a4c4ca2c 100644 --- a/packages/graphile-build-pg/src/plugins/debugSql.js +++ b/packages/graphile-build-pg/src/plugins/debugSql.ts @@ -1,6 +1,5 @@ import debugFactory from "debug"; import chalk from "chalk"; - export function formatSQLForDebugging(sql) { let colourIndex = 0; let allowedColours = [ @@ -13,23 +12,28 @@ export function formatSQLForDebugging(sql) { chalk.white, chalk.black, ]; + function nextColor() { colourIndex = (colourIndex + 1) % allowedColours.length; return allowedColours[colourIndex]; } - const colours = {}; + const colours = {}; /* Yep - that's `colour` from English and `ize` from American */ + function colourize(str) { if (!colours[str]) { colours[str] = nextColor(); } + return colours[str].bold.call(null, str); } let indentLevel = 0; + function handleIndent(all, rawMatch) { const match = rawMatch.replace(/ $/, ""); + if (match === "(") { indentLevel++; return match + "\n" + " ".repeat(indentLevel); @@ -51,6 +55,7 @@ export function formatSQLForDebugging(sql) { return "\n" + " ".repeat(indentLevel) + match.replace(/^\s+/, ""); } } + const tidySql = sql .replace(/\s+/g, " ") .replace(/\s+(?=$|\n|\))/g, "") @@ -65,15 +70,15 @@ export function formatSQLForDebugging(sql) { const colouredSql = tidySql.replace(/__local_[0-9]+__/g, colourize); return colouredSql; } - const rawDebugSql = debugFactory("graphile-build-pg:sql"); function debugSql(sql) { if (!rawDebugSql.enabled) { return; } + rawDebugSql("%s", "\n" + formatSQLForDebugging(sql)); } -Object.assign(debugSql, rawDebugSql); +Object.assign(debugSql, rawDebugSql); export default debugSql; diff --git a/packages/graphile-build-pg/src/plugins/introspectionQuery.js b/packages/graphile-build-pg/src/plugins/introspectionQuery.ts similarity index 99% rename from packages/graphile-build-pg/src/plugins/introspectionQuery.js rename to packages/graphile-build-pg/src/plugins/introspectionQuery.ts index ad33974d9..abb100ca8 100644 --- a/packages/graphile-build-pg/src/plugins/introspectionQuery.js +++ b/packages/graphile-build-pg/src/plugins/introspectionQuery.ts @@ -1,4 +1,3 @@ -// @flow /* * IMPORTANT: when editing this file, ensure all operators (e.g. `@>`) are * specified in the correct namespace (e.g. `operator(pg_catalog.@>)`). It looks @@ -9,7 +8,9 @@ */ function makeIntrospectionQuery( serverVersionNum: number, - options: { pgLegacyFunctionsOnly?: boolean } = {} + options: { + pgLegacyFunctionsOnly?: boolean; + } = {} ): string { const { pgLegacyFunctionsOnly } = options; return `\ diff --git a/packages/graphile-build-pg/src/plugins/makeProcField.js b/packages/graphile-build-pg/src/plugins/makeProcField.ts similarity index 97% rename from packages/graphile-build-pg/src/plugins/makeProcField.js rename to packages/graphile-build-pg/src/plugins/makeProcField.ts index ecdf2cc3e..559a5b002 100644 --- a/packages/graphile-build-pg/src/plugins/makeProcField.js +++ b/packages/graphile-build-pg/src/plugins/makeProcField.ts @@ -1,37 +1,38 @@ -// @flow const nullableIf = (GraphQLNonNull, condition, Type) => condition ? Type : new GraphQLNonNull(Type); -import type { Build, FieldWithHooksFunction } from "graphile-build"; -import type { PgProc } from "./PgIntrospectionPlugin"; -import type { SQL } from "pg-sql2"; +import { Build, FieldWithHooksFunction } from "graphile-build"; +import { PgProc } from "./PgIntrospectionPlugin"; +import { SQL } from "pg-sql2"; import debugSql from "./debugSql"; import chalk from "chalk"; const firstValue = obj => { let firstKey; + for (const k in obj) { if (k[0] !== "_" && k[1] !== "_") { firstKey = k; } } + return obj[firstKey]; }; export default function makeProcField( fieldName: string, proc: PgProc, - build: {| ...Build |}, + build: Build, { fieldWithHooks, computed = false, isMutation = false, forceList = false, }: { - fieldWithHooks: FieldWithHooksFunction, - computed?: boolean, - isMutation?: boolean, - forceList?: boolean, + fieldWithHooks: FieldWithHooksFunction; + computed?: boolean; + isMutation?: boolean; + forceList?: boolean; } ) { const { @@ -70,6 +71,7 @@ export default function makeProcField( if (computed && isMutation) { throw new Error("Mutation procedure cannot be computed"); } + const sliceAmount = computed ? 1 : 0; const argNames = proc.argTypeIds.reduce((prev, _, idx) => { if ( @@ -80,6 +82,7 @@ export default function makeProcField( ) { prev.push(proc.argNames[idx] || ""); } + return prev; }, []); const argTypes = proc.argTypeIds.reduce((prev, typeId, idx) => { @@ -91,39 +94,48 @@ export default function makeProcField( ) { prev.push(introspectionResultsByKind.typeById[typeId]); } + return prev; }, []); const argModesWithOutput = [ "o", // OUT, "b", // INOUT - "t", // TABLE + "t", ]; const outputArgNames = proc.argTypeIds.reduce((prev, _, idx) => { if (argModesWithOutput.includes(proc.argModes[idx])) { prev.push(proc.argNames[idx] || ""); } + return prev; }, []); const outputArgTypes = proc.argTypeIds.reduce((prev, typeId, idx) => { if (argModesWithOutput.includes(proc.argModes[idx])) { prev.push(introspectionResultsByKind.typeById[typeId]); } + return prev; }, []); const requiredArgCount = Math.max(0, argNames.length - proc.argDefaultsNum); + const variantFromName = (name, _type) => { if (name.match(/(_p|P)atch$/)) { return "patch"; } + return null; }; + const variantFromTags = (tags, idx) => { const variant = tags[`arg${idx}variant`]; + if (variant && variant.match && variant.match(/^[0-9]+$/)) { return parseInt(variant, 10); } + return variant; }; + const notNullArgCount = proc.isStrict || strictFunctions ? requiredArgCount : 0; const argGqlTypes = argTypes.map((type, idx) => { @@ -131,6 +143,7 @@ export default function makeProcField( const variant = variantFromTags(proc.tags, idx) || variantFromName(argNames[idx], type); const Type = pgGetGqlInputTypeByTypeIdAndModifier(type.id, variant); + if (!Type) { const hint = type.class ? `; this might be because no INSERT column privileges are granted on ${describePgEntity( @@ -150,24 +163,26 @@ export default function makeProcField( }') of function ${describePgEntity(proc)}${hint}` ); } + if (idx >= notNullArgCount) { return Type; } else { return new GraphQLNonNull(Type); } }); - const rawReturnType = introspectionResultsByKind.typeById[proc.returnTypeId]; const returnType = rawReturnType.isPgArray ? rawReturnType.arrayItemType : rawReturnType; const returnTypeTable = introspectionResultsByKind.classById[returnType.classId]; + if (!returnType) { throw new Error( `Could not determine return type for function '${proc.name}'` ); } + let type; const fieldScope = {}; const payloadTypeScope = {}; @@ -177,10 +192,10 @@ export default function makeProcField( const TableType = returnTypeTable && pgGetGqlTypeByTypeIdAndModifier(returnTypeTable.type.id, null); - const isTableLike: boolean = (TableType && isCompositeType(TableType)) || false; const isRecordLike = returnType.id === "2249"; + if (isTableLike) { if (proc.returnsSet) { if (isMutation) { @@ -192,6 +207,7 @@ export default function makeProcField( const ConnectionType = getTypeByName( inflection.connection(TableType.name) ); + if (!ConnectionType) { throw new Error( `Do not have a connection type '${inflection.connection( @@ -199,21 +215,26 @@ export default function makeProcField( )}' for '${TableType.name}' so cannot create procedure field` ); } + type = new GraphQLNonNull(ConnectionType); fieldScope.isPgFieldConnection = true; } + fieldScope.pgFieldIntrospectionTable = returnTypeTable; payloadTypeScope.pgIntrospectionTable = returnTypeTable; } else { type = TableType; + if (rawReturnType.isPgArray) { type = new GraphQLList(type); } + fieldScope.pgFieldIntrospectionTable = returnTypeTable; payloadTypeScope.pgIntrospectionTable = returnTypeTable; } } else if (isRecordLike) { const RecordType = getTypeByName(inflection.recordFunctionReturnType(proc)); + if (!RecordType) { throw new Error( `Do not have a record type '${inflection.recordFunctionReturnType( @@ -221,6 +242,7 @@ export default function makeProcField( )}' for '${proc.name}' so cannot create procedure field` ); } + if (proc.returnsSet) { if (isMutation) { type = new GraphQLList(RecordType); @@ -231,6 +253,7 @@ export default function makeProcField( const ConnectionType = getTypeByName( inflection.recordFunctionConnection(proc) ); + if (!ConnectionType) { throw new Error( `Do not have a connection type '${inflection.recordFunctionConnection( @@ -238,11 +261,13 @@ export default function makeProcField( )}' for '${RecordType.name}' so cannot create procedure field` ); } + type = new GraphQLNonNull(ConnectionType); fieldScope.isPgFieldConnection = true; } } else { type = RecordType; + if (rawReturnType.isPgArray) { type = new GraphQLList(type); } @@ -255,6 +280,7 @@ export default function makeProcField( if (proc.returnsSet) { const connectionTypeName = inflection.scalarFunctionConnection(proc); const ConnectionType = getTypeByName(connectionTypeName); + if (isMutation) { // Cannot return a connection because it would have to run the mutation again type = new GraphQLList(Type); @@ -265,8 +291,7 @@ export default function makeProcField( fieldScope.isPgFieldSimpleCollection = true; } else { type = new GraphQLNonNull(ConnectionType); - fieldScope.isPgFieldConnection = true; - // We don't return the first value as the value here because it gets + fieldScope.isPgFieldConnection = true; // We don't return the first value as the value here because it gets // sent down into PgScalarFunctionConnectionPlugin so the relevant // EdgeType can return cursor / node; i.e. we might want to add an // `__cursor` field so we can't just use a scalar. @@ -274,11 +299,13 @@ export default function makeProcField( } else { returnFirstValueAsValue = true; type = Type; + if (rawReturnType.isPgArray) { type = new GraphQLList(type); } } } + return fieldWithHooks( fieldName, ({ @@ -299,6 +326,7 @@ export default function makeProcField( }; }); } + function makeMutationCall( parsedResolveInfoFragment, ReturnType, @@ -308,6 +336,7 @@ export default function makeProcField( const args = isMutation ? rawArgs.input : rawArgs; const sqlArgValues = []; let haveNames = true; + for (let argIndex = argNames.length - 1; argIndex >= 0; argIndex--) { const argName = argNames[argIndex]; const gqlArgName = inflection.argument(argName, argIndex); @@ -315,7 +344,6 @@ export default function makeProcField( const variant = variantFromTags(proc.tags, argIndex) || variantFromName(argNames[argIndex], type); - const sqlValue = gql2pg(value, argTypes[argIndex], variant); if (argIndex + 1 > requiredArgCount && haveNames && value == null) { @@ -323,6 +351,7 @@ export default function makeProcField( continue; } else if (argIndex + 1 > requiredArgCount && haveNames) { const sqlArgName = argName ? sql.identifier(argName) : null; + if (sqlArgName) { sqlArgValues.unshift(sql.fragment`${sqlArgName} := ${sqlValue}`); } else { @@ -333,6 +362,7 @@ export default function makeProcField( sqlArgValues.unshift(sqlValue); } } + const functionCall = sql.fragment`${sql.identifier( proc.namespace.name, proc.name @@ -341,6 +371,7 @@ export default function makeProcField( ? sql.fragment`unnest(${functionCall})` : functionCall; } + function makeQuery( parsedResolveInfoFragment, ReturnType, @@ -362,7 +393,8 @@ export default function makeProcField( !isMutation && (isTableLike || isRecordLike) && (forceList || proc.returnsSet || rawReturnType.isPgArray) && // only bother with lists - proc.language !== "sql", // sql functions can be inlined, so GRANTs still apply + proc.language !== "sql", + // sql functions can be inlined, so GRANTs still apply withPagination: !forceList && !isMutation && proc.returnsSet, withPaginationAsFields: !forceList && !isMutation && proc.returnsSet && !computed, @@ -379,6 +411,7 @@ export default function makeProcField( }, innerQueryBuilder => { innerQueryBuilder.parentQueryBuilder = parentQueryBuilder; + if (!isTableLike) { if (returnTypeTable) { innerQueryBuilder.select( @@ -413,6 +446,7 @@ export default function makeProcField( ); return query; } + if (computed) { addDataGenerator((parsedResolveInfoFragment, ReturnType) => { return { @@ -450,6 +484,7 @@ export default function makeProcField( }; return memo; }, {}); + if (isMutation) { const resultFieldName = inflection.functionMutationResultFieldName( proc, @@ -457,8 +492,8 @@ export default function makeProcField( proc.returnsSet || rawReturnType.isPgArray, outputArgNames ); - const isNotVoid = String(returnType.id) !== "2278"; - // If set then plural name + const isNotVoid = String(returnType.id) !== "2278"; // If set then plural name + PayloadType = newWithHooks( GraphQLObjectType, { @@ -495,8 +530,7 @@ export default function makeProcField( { pgType: returnType, } - ), - // Result + ), // Result } : null ); @@ -552,11 +586,11 @@ export default function makeProcField( type: new GraphQLNonNull(InputType), }, }; - } - // If this is a table we can process it directly; but if it's a scalar + } // If this is a table we can process it directly; but if it's a scalar // setof function we must dereference '.value' from it, because this // makes space for '__cursor' to exist alongside it (whereas on a table // the '__cursor' can just be on the table object itself) + const scalarAwarePg2gql = v => isTableLike ? pg2gql(v, returnType) @@ -579,6 +613,7 @@ export default function makeProcField( ? (data, _args, _context, resolveInfo) => { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); const value = data[safeAlias]; + if (returnFirstValueAsValue) { // Is not table like; is not record like. if (proc.returnsSet && !forceList) { @@ -607,14 +642,15 @@ export default function makeProcField( const { pgClient, liveRecord } = resolveContext; const parsedResolveInfoFragment = parseResolveInfo(resolveInfo); parsedResolveInfoFragment.args = args; // Allow overriding via makeWrapResolversPlugin + const functionAlias = sql.identifier(Symbol()); const sqlMutationQuery = makeMutationCall( parsedResolveInfoFragment, resolveInfo.returnType, {} ); - let queryResultRows; + if (isMutation) { const query = makeQuery( parsedResolveInfoFragment, @@ -630,6 +666,7 @@ export default function makeProcField( const isPgClass = !isPgRecord && (!returnFirstValueAsValue || returnTypeTable || false); + try { await pgClient.query("SAVEPOINT graphql_mutation"); queryResultRows = await viaTemporaryTable( @@ -678,8 +715,10 @@ export default function makeProcField( const queryResult = await pgClient.query(text, values); queryResultRows = queryResult.rows; } + const rows = queryResultRows; const [row] = rows; + const result = (() => { if (returnFirstValueAsValue) { if (proc.returnsSet && !isMutation && !forceList) { @@ -698,6 +737,7 @@ export default function makeProcField( const data = row.data ? row.data.map(scalarAwarePg2gql) : null; + if ( subscriptions && isTableLike && @@ -711,6 +751,7 @@ export default function makeProcField( liveRecord("pg", returnTypeTable, row.__identifiers) ); } + return addStartEndCursor({ ...row, data, @@ -728,6 +769,7 @@ export default function makeProcField( liveRecord("pg", returnTypeTable, row.__identifiers) ); } + return rows.map(row => pg2gql(row, returnType)); } else { if ( @@ -739,10 +781,12 @@ export default function makeProcField( ) { liveRecord("pg", returnTypeTable, row.__identifiers); } + return pg2gql(row, returnType); } } })(); + if (isMutation) { return { clientMutationId: args.input.clientMutationId, diff --git a/packages/graphile-build-pg/src/plugins/pgField.js b/packages/graphile-build-pg/src/plugins/pgField.ts similarity index 93% rename from packages/graphile-build-pg/src/plugins/pgField.js rename to packages/graphile-build-pg/src/plugins/pgField.ts index 515eff804..52f8b830b 100644 --- a/packages/graphile-build-pg/src/plugins/pgField.js +++ b/packages/graphile-build-pg/src/plugins/pgField.ts @@ -28,12 +28,14 @@ export default function pgField( nullableType !== namedType && nullableType.constructor === build.graphql.GraphQLList; const isLeafType = build.graphql.isLeafType(FieldType); + if (isLeafType && !options.pgType) { // eslint-disable-next-line no-console throw new Error( "pgField call omits options.pgType for a leaf type; certain tweaks may not be applied!" ); } + const { getDataFromParsedResolveInfoFragment, addDataGenerator, @@ -50,7 +52,9 @@ export default function pgField( ...(options.hoistCursor && resolveData.usesCursor && resolveData.usesCursor.length - ? { usesCursor: [true] } + ? { + usesCursor: [true], + } : null), pgQuery: queryBuilder => { queryBuilder.select(() => { @@ -70,10 +74,15 @@ export default function pgField( : tableAlias, resolveData, whereFrom === false - ? { onlyJsonField: true } - : { asJson: true }, + ? { + onlyJsonField: true, + } + : { + asJson: true, + }, innerQueryBuilder => { innerQueryBuilder.parentQueryBuilder = queryBuilder; + if (typeof options.withQueryBuilder === "function") { options.withQueryBuilder(innerQueryBuilder, { parsedResolveInfoFragment, @@ -87,17 +96,18 @@ export default function pgField( }, }; }); - return { resolve(data, _args, _context, resolveInfo) { const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); if (data.data == null) return null; + if (isListType) { return data.data.map(d => (d != null ? d[safeAlias] : null)); } else { return data.data[safeAlias]; } }, + ...fieldSpec, }; }, diff --git a/packages/graphile-build-pg/src/plugins/viaTemporaryTable.js b/packages/graphile-build-pg/src/plugins/viaTemporaryTable.ts similarity index 74% rename from packages/graphile-build-pg/src/plugins/viaTemporaryTable.js rename to packages/graphile-build-pg/src/plugins/viaTemporaryTable.ts index 686cf3757..9e6835f3f 100644 --- a/packages/graphile-build-pg/src/plugins/viaTemporaryTable.js +++ b/packages/graphile-build-pg/src/plugins/viaTemporaryTable.ts @@ -1,10 +1,7 @@ -// @flow - import * as sql from "pg-sql2"; -import type { Client } from "pg"; -import type { SQL, SQLQuery } from "pg-sql2"; +import { Client } from "pg"; +import { SQL, SQLQuery } from "pg-sql2"; import debugSql from "./debugSql"; - /* * Originally we tried this with a CTE, but: * @@ -37,16 +34,18 @@ import debugSql from "./debugSql"; export default async function viaTemporaryTable( pgClient: Client, - sqlTypeIdentifier: ?SQL, + sqlTypeIdentifier: SQL | null | undefined, sqlMutationQuery: SQL, sqlResultSourceAlias: SQL, sqlResultQuery: SQL, isPgClassLike: boolean = true, - pgRecordInfo: ?{ - // eslint-disable-next-line flowtype/no-weak-types - outputArgTypes: Array, - outputArgNames: Array, - } = undefined + pgRecordInfo: + | { + outputArgTypes: Array; + outputArgNames: Array; + } + | null + | undefined = undefined ) { const isPgRecord = pgRecordInfo != null; const { outputArgTypes, outputArgNames } = pgRecordInfo || {}; @@ -82,12 +81,12 @@ export default async function viaTemporaryTable( */ const selectionField = isPgClassLike ? /* - * This `when foo is null then null` check might *seem* redundant, but it - * is not - e.g. the compound type `(,,,,,,,)::my_type` and - * `null::my_type` differ; however the former also returns true to `foo - * is null`. We use this check to coalesce both into the canonical `null` - * representation to make it easier to deal with below. - */ + * This `when foo is null then null` check might *seem* redundant, but it + * is not - e.g. the compound type `(,,,,,,,)::my_type` and + * `null::my_type` differ; however the former also returns true to `foo + * is null`. We use this check to coalesce both into the canonical `null` + * representation to make it easier to deal with below. + */ sql.query`(case when ${sqlResultSourceAlias} is null then null else ${sqlResultSourceAlias} end)` : isPgRecord ? sql.query`array[${sql.join( @@ -112,10 +111,10 @@ export default async function viaTemporaryTable( select (${selectionField})::text from ${sqlResultSourceAlias}` ); const { rows } = result; - const firstNonNullRow = rows.find(row => row !== null); - // TODO: we should be able to have `pg` not interpret the results as + const firstNonNullRow = rows.find(row => row !== null); // TODO: we should be able to have `pg` not interpret the results as // objects and instead just return them as arrays - then we can just do // `row[0]`. PR welcome! + const firstKey = firstNonNullRow && Object.keys(firstNonNullRow)[0]; const rawValues = rows.map(row => row && row[firstKey]); const values = rawValues.filter(rawValue => rawValue !== null); @@ -128,19 +127,18 @@ export default async function viaTemporaryTable( ? sql.query`\ select ${sql.join( outputArgNames.map( - (outputArgName, idx) => - sql.query`\ + (outputArgName, idx) => sql.query`\ (${sqlValuesAlias}.output_value_list)[${sql.literal( - idx + 1 - )}]::${sql.identifier( - outputArgTypes[idx].namespaceName, - outputArgTypes[idx].name - )} as ${sql.identifier( - // According to https://www.postgresql.org/docs/10/static/sql-createfunction.html, - // "If you omit the name for an output argument, the system will choose a default column name." - // In PG 9.x and 10, the column names appear to be assigned with a `column` prefix. - outputArgName !== "" ? outputArgName : `column${idx + 1}` - )}` + idx + 1 + )}]::${sql.identifier( + outputArgTypes[idx].namespaceName, + outputArgTypes[idx].name + )} as ${sql.identifier( + // According to https://www.postgresql.org/docs/10/static/sql-createfunction.html, + // "If you omit the name for an output argument, the system will choose a default column name." + // In PG 9.x and 10, the column names appear to be assigned with a `column` prefix. + outputArgName !== "" ? outputArgName : `column${idx + 1}` + )}` ), ", " )} @@ -162,16 +160,22 @@ export default async function viaTemporaryTable( ${sqlResultQuery} ` ) - : { rows: [] }; + : { + rows: [], + }; const finalRows = rawValues.map( rawValue => /* - * We can't simply return 'null' here because this is expected to have - * come from PG, and that would never return 'null' for a row - only - * the fields within said row. Using `__isNull` here is a simple - * workaround to this, that's caught by `pg2gql`. - */ - rawValue === null ? { __isNull: true } : filteredValuesResults.shift() + * We can't simply return 'null' here because this is expected to have + * come from PG, and that would never return 'null' for a row - only + * the fields within said row. Using `__isNull` here is a simple + * workaround to this, that's caught by `pg2gql`. + */ + rawValue === null + ? { + __isNull: true, + } + : filteredValuesResults.shift() ); return finalRows; } diff --git a/packages/graphile-build-pg/src/queryFromResolveDataFactory.js b/packages/graphile-build-pg/src/queryFromResolveDataFactory.ts similarity index 92% rename from packages/graphile-build-pg/src/queryFromResolveDataFactory.js rename to packages/graphile-build-pg/src/queryFromResolveDataFactory.ts index d56fcef59..adc8bdc42 100644 --- a/packages/graphile-build-pg/src/queryFromResolveDataFactory.js +++ b/packages/graphile-build-pg/src/queryFromResolveDataFactory.ts @@ -1,34 +1,30 @@ -// @flow import QueryBuilder from "./QueryBuilder"; -import type QueryBuilderOptions from "./QueryBuilder"; -import type { RawAlias } from "./QueryBuilder"; +import QueryBuilderOptions from "./QueryBuilder"; +import { RawAlias } from "./QueryBuilder"; import * as sql from "pg-sql2"; -import type { SQL } from "pg-sql2"; -import type { DataForType } from "graphile-build"; +import { SQL } from "pg-sql2"; +import { DataForType } from "graphile-build"; import isSafeInteger from "lodash/isSafeInteger"; -import assert from "assert"; +import assert from "assert"; // eslint-disable-next-line flowtype/no-weak-types -// eslint-disable-next-line flowtype/no-weak-types type GraphQLContext = any; -const identity = _ => _ !== null && _ !== undefined; +const identity = _ => _ !== null && _ !== undefined; // $FlowFixMe -// $FlowFixMe export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( from: SQL, - fromAlias: ?SQL, + fromAlias: SQL | null | undefined, resolveData: DataForType, options: { - withPagination?: boolean, - withPaginationAsFields?: boolean, - asJson?: boolean, - asJsonAggregate?: boolean, - addNullCase?: boolean, - onlyJsonField?: boolean, - useAsterisk?: boolean, + withPagination?: boolean; + withPaginationAsFields?: boolean; + asJson?: boolean; + asJsonAggregate?: boolean; + addNullCase?: boolean; + onlyJsonField?: boolean; + useAsterisk?: boolean; }, - // TODO:v5: context is not optional - withBuilder?: ((builder: QueryBuilder) => void) | null | void, + withBuilder?: ((builder: QueryBuilder) => undefined) | null | undefined, context?: GraphQLContext = {} ) => { const { @@ -39,22 +35,21 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( calculateHasPreviousPage, usesCursor: explicitlyUsesCursor, } = resolveData; - const usesCursor: boolean = (explicitlyUsesCursor && explicitlyUsesCursor.length > 0) || (calculateHasNextPage && calculateHasNextPage.length > 0) || (calculateHasPreviousPage && calculateHasPreviousPage.length > 0) || false; const rawCursorPrefix = - reallyRawCursorPrefix && reallyRawCursorPrefix.filter(identity); + reallyRawCursorPrefix && reallyRawCursorPrefix.filter(identity); // $FlowFixMe - // $FlowFixMe const queryBuilder = new QueryBuilder(queryBuilderOptions, context); queryBuilder.from(from, fromAlias ? fromAlias : undefined); if (withBuilder) { withBuilder(queryBuilder); } + for (const fn of pgQuery || []) { fn(queryBuilder, resolveData); } @@ -134,7 +129,6 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( * hasNextPage we always pretend we're determining `hasNextPage`, and we * just invert everything. */ - const sqlCommonUnbounded = sql.fragment` select 1 from ${queryBuilder.getTableExpression()} as ${queryBuilder.getTableAlias()} @@ -153,28 +147,30 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( * upper bound. In hasPreviousPage mode (invert === true), it represents * everything from `(before || END)` backwards, with no lower bound. */ + const sqlCommon = sql.fragment` ${sqlCommonUnbounded} where ${queryBuilder.buildWhereClause(!invert, invert, options)} `; - /* * Since the offset makes the diagram asymmetric, if offset === 0 * then the diagram is symmetric and things are simplified a little. */ + const isForwardOrSymmetric = !invert || offset === 0; if (!isForwardOrSymmetric) { assert(invert); - assert(offset > 0); - // We're looking for a previous page, and there's an offset, so lets just + assert(offset > 0); // We're looking for a previous page, and there's an offset, so lets just // assume there's a previous page where offset is smaller. + return sql.literal(true); } else if (canHaveCursorInWhere) { assert(isForwardOrSymmetric); + if (!queryHasBefore && !queryHasFirst) { - assert(isForwardOrSymmetric); - // There can be no next page since there's no upper bound + assert(isForwardOrSymmetric); // There can be no next page since there's no upper bound + return sql.literal(false); } else if (queryHasBefore && !queryHasFirst) { /* @@ -193,8 +189,8 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( and not (${queryBuilder.buildWhereBoundClause(invert)}) )`; } else { - assert(queryHasFirst); - // queryHasBefore could be true or false. + assert(queryHasFirst); // queryHasBefore could be true or false. + /* * There's a few ways that we could determine if there's a next page. * @@ -214,6 +210,7 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( */ // Drop the `first` limit, see if there are any records that aren't // already in the list we've fetched. + return sql.fragment`exists( ${sqlCommon} and (${queryBuilder.getSelectCursor()})::text not in (select __cursor::text from ${sqlQueryAlias}) @@ -224,10 +221,11 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( } } else { assert(!invert || offset === 0); // isForwardOrSymmetric - assert(!canHaveCursorInWhere); - // We're dealing with LIMIT/OFFSET pagination here, which means `natural` + + assert(!canHaveCursorInWhere); // We're dealing with LIMIT/OFFSET pagination here, which means `natural` // cursors, so the `queryBuilder` factors the before/after, first/last // into the limit / offset. + const { limit } = queryBuilder.getFinalLimitAndOffset(); if (limit == null) { @@ -235,8 +233,8 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( // with. Unbounded, so there's no next page. return sql.fragment`false`; } else if (invert) { - assert(offset === 0); - // Paginating backwards and there's no offset (which factors in before/after), so there's no previous page. + assert(offset === 0); // Paginating backwards and there's no offset (which factors in before/after), so there's no previous page. + return sql.fragment`false`; } else { assert(!invert); @@ -246,6 +244,7 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( * * We want to see if there's more than limit+offset records in sqlCommon. */ + return sql.fragment`exists( ${sqlCommon} offset ${sql.literal(limit + offset)} @@ -253,10 +252,12 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( } } } + const getPgCursorPrefix = () => rawCursorPrefix && rawCursorPrefix.length > 0 ? rawCursorPrefix : queryBuilder.data.cursorPrefix.map(val => sql.literal(val)); + if ( options.withPagination || options.withPaginationAsFields || @@ -268,6 +269,7 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( const orderBy = queryBuilder .getOrderByExpressionsAndDirections() .map(([expr]) => expr); + if (queryBuilder.isOrderUnique() && orderBy.length > 0) { return sql.fragment`json_build_array(${sql.join( [ @@ -285,9 +287,11 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( }); } } + if (options.withPagination || options.withPaginationAsFields) { queryBuilder.setCursorComparator((cursorValue, isAfter) => { const orderByExpressionsAndDirections = queryBuilder.getOrderByExpressionsAndDirections(); + if ( orderByExpressionsAndDirections.length > 0 && queryBuilder.isOrderUnique() @@ -295,19 +299,21 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( const sqlCursors = cursorValue[getPgCursorPrefix().length].map(val => sql.value(val) ); + if (!Array.isArray(sqlCursors)) { queryBuilder.whereBound(sql.literal(false), isAfter); } + let sqlFilter = sql.fragment`false`; + for (let i = orderByExpressionsAndDirections.length - 1; i >= 0; i--) { - const [sqlExpression, ascending] = orderByExpressionsAndDirections[i]; - // If ascending and isAfter then > + const [sqlExpression, ascending] = orderByExpressionsAndDirections[i]; // If ascending and isAfter then > // If ascending and isBefore then < + const comparison = Number(ascending) ^ Number(!isAfter) ? sql.fragment`>` : sql.fragment`<`; - const sqlOldFilter = sqlFilter; sqlFilter = sql.fragment` ( @@ -325,6 +331,7 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( ) `; } + queryBuilder.whereBound(sqlFilter, isAfter); } else if ( cursorValue[0] === "natural" && @@ -343,12 +350,10 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( throw new Error("Cannot use cursors without orderBy"); } }); - const query = queryBuilder.build(options); const haveFields = queryBuilder.getSelectFieldsCount() > 0; const sqlQueryAlias = sql.identifier(Symbol()); - const sqlSummaryAlias = sql.identifier(Symbol()); - // + const sqlSummaryAlias = sql.identifier(Symbol()); // // Tables should ALWAYS push their PK onto the order stack, if this isn't // present then we're either dealing with a view or a table without a PK. // Either way, we don't have anything to guarantee uniqueness so we need to @@ -357,6 +362,7 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( // TODO: support unique keys in PgAllRows etc // TODO: add a warning for cursor-based pagination when using the fallback // TODO: if it is a view maybe add a warning encouraging pgViewUniqueKey + const canHaveCursorInWhere = queryBuilder.getOrderByExpressionsAndDirections().length > 0 && queryBuilder.isOrderUnique(); @@ -385,24 +391,27 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( queryBuilder.getFinalOffset() || 0, true ); - const sqlWith = haveFields ? sql.fragment`with ${sqlQueryAlias} as (${query}), ${sqlSummaryAlias} as (select json_agg(to_json(${sqlQueryAlias})) as data from ${sqlQueryAlias})` : sql.fragment``; const sqlFrom = sql.fragment``; const fields: Array<[SQL, RawAlias]> = []; + if (haveFields) { fields.push([ sql.fragment`coalesce((select ${sqlSummaryAlias}.data from ${sqlSummaryAlias}), '[]'::json)`, "data", ]); + if (calculateHasNextPage) { fields.push([hasNextPage, "hasNextPage"]); } + if (calculateHasPreviousPage) { fields.push([hasPreviousPage, "hasPreviousPage"]); } } + if (pgAggregateQuery && pgAggregateQuery.length) { const aggregateQueryBuilder = new QueryBuilder( queryBuilderOptions, @@ -416,6 +425,7 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( for (const fn of pgAggregateQuery) { fn(aggregateQueryBuilder); } + const aggregateJsonBuildObject = aggregateQueryBuilder.build({ onlyJsonField: true, }); @@ -428,6 +438,7 @@ export default (queryBuilderOptions: QueryBuilderOptions = {}) => ( `; fields.push([aggregatesSql, "aggregates"]); } + if (options.withPaginationAsFields) { return sql.fragment`${sqlWith} select ${sql.join( fields.map( diff --git a/packages/graphile-build-pg/src/utils.js b/packages/graphile-build-pg/src/utils.ts similarity index 98% rename from packages/graphile-build-pg/src/utils.js rename to packages/graphile-build-pg/src/utils.ts index f9c2e5f1a..40f22834a 100644 --- a/packages/graphile-build-pg/src/utils.js +++ b/packages/graphile-build-pg/src/utils.ts @@ -1,4 +1,3 @@ -// @flow export const parseTags = (str: string) => { return str.split(/\r?\n/).reduce( (prev, curr) => { @@ -7,12 +6,15 @@ export const parseTags = (str: string) => { text: `${prev.text}\n${curr}`, }); } + const match = curr.match(/^@[a-zA-Z][a-zA-Z0-9_]*($|\s)/); + if (!match) { return Object.assign({}, prev, { text: curr, }); } + const key = match[0].substr(1).trim(); const value = match[0] === curr ? true : curr.replace(match[0], ""); return Object.assign({}, prev, { diff --git a/packages/graphile-build-pg/src/withPgClient.js b/packages/graphile-build-pg/src/withPgClient.ts similarity index 90% rename from packages/graphile-build-pg/src/withPgClient.js rename to packages/graphile-build-pg/src/withPgClient.ts index 497575c6d..93ffb0f5a 100644 --- a/packages/graphile-build-pg/src/withPgClient.js +++ b/packages/graphile-build-pg/src/withPgClient.ts @@ -1,16 +1,12 @@ -// @flow import pg from "pg"; import debugFactory from "debug"; - const debug = debugFactory("graphile-build-pg"); function constructorName(obj) { return obj && typeof obj.constructor === "function" && obj.constructor.name; -} +} // Some duck-typing -// Some duck-typing - -function quacksLikePgClient(pgConfig: mixed): boolean { +function quacksLikePgClient(pgConfig: unknown): boolean { // A diagnosis of exclusion if (!pgConfig || typeof pgConfig !== "object") return false; if (constructorName(pgConfig) !== "Client") return false; @@ -21,15 +17,17 @@ function quacksLikePgClient(pgConfig: mixed): boolean { return true; } -export function quacksLikePgPool(pgConfig: mixed): boolean { +export function quacksLikePgPool(pgConfig: unknown): boolean { // A diagnosis of exclusion 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; @@ -40,35 +38,42 @@ export function quacksLikePgPool(pgConfig: mixed): boolean { const withPgClient = async ( pgConfig: pg.Client | pg.Pool | string = process.env.DATABASE_URL, - fn: (pgClient: pg.Client) => * + fn: (pgClient: pg.Client) => any ) => { if (!fn) { throw new Error("Nothing to do!"); } + let releasePgClient = () => {}; + let pgClient: pg.Client; let result; + try { if (pgConfig instanceof pg.Client || quacksLikePgClient(pgConfig)) { - pgClient = (pgConfig: pg.Client); + pgClient = pgConfig as pg.Client; + if (!pgClient.release) { throw new Error( "We only support PG clients from a PG pool (because otherwise the `await` call can hang indefinitely if an error occurs and there's no error handler)" ); } } else if (pgConfig instanceof pg.Pool || quacksLikePgPool(pgConfig)) { - const pgPool = (pgConfig: pg.Pool); + const pgPool = pgConfig as pg.Pool; pgClient = await pgPool.connect(); + releasePgClient = () => pgClient.release(); } else if (pgConfig === undefined || typeof pgConfig === "string") { pgClient = new pg.Client(pgConfig); pgClient.on("error", e => { debug("pgClient error occurred: %s", e); }); + releasePgClient = () => new Promise((resolve, reject) => pgClient.end(err => (err ? reject(err) : resolve())) ); + await new Promise((resolve, reject) => pgClient.connect(err => (err ? reject(err) : resolve())) ); @@ -77,6 +82,7 @@ const withPgClient = async ( "You must provide either a pg.Pool or pg.Client instance or a PostgreSQL connection string." ); } + result = await fn(pgClient); } finally { try { @@ -85,6 +91,7 @@ const withPgClient = async ( // Failed to release, assuming success } } + return result; }; diff --git a/packages/graphile-build/src/Live.js b/packages/graphile-build/src/Live.ts similarity index 89% rename from packages/graphile-build/src/Live.js rename to packages/graphile-build/src/Live.ts index 2762c86fd..fa8473f55 100644 --- a/packages/graphile-build/src/Live.js +++ b/packages/graphile-build/src/Live.ts @@ -1,22 +1,18 @@ -// @flow /* eslint-disable flowtype/no-weak-types */ import callbackToAsyncIterator from "./callbackToAsyncIterator"; -import type { GraphQLResolveInfo } from "graphql"; +import { GraphQLResolveInfo } from "graphql"; import { throttle } from "lodash"; - -type SubscriptionReleaser = () => void; -type SubscriptionCallback = () => void; - +type SubscriptionReleaser = () => undefined; +type SubscriptionCallback = () => undefined; type Predicate = (record: any) => boolean; type PredicateGenerator = (data: any) => Predicate; - const MONITOR_THROTTLE_DURATION = parseInt(process.env.LIVE_THROTTLE || "", 10) || 500; - /* * Sources are long-lived (i.e. in "watch" mode you just re-use the same one * over and over) because there is no release for them */ + export class LiveSource { subscribeCollection( _callback: SubscriptionCallback, @@ -34,13 +30,13 @@ export class LiveSource { return null; } } - /* * Providers enable a namespace, perform validation, and track the sources used * by that namespace within one single schema build. The should not directly use * any long-lived features as they do not have an explicit "release"/"close" * command when a new schema is built. */ + export class LiveProvider { sources: Array; namespace: string; @@ -65,16 +61,18 @@ export class LiveProvider { return false; } } - /* * During a single execution of GraphQL (specifically a subscription request), * the LiveMonitor tracks the resources viewed and subscribes to updates in them. */ + export class LiveMonitor { released: boolean; - providers: { [namespace: string]: LiveProvider }; - subscriptionReleasers: (() => void)[]; - changeCallback: (() => void) | null; + providers: { + [namespace: string]: LiveProvider; + }; + subscriptionReleasers: () => undefined[]; + changeCallback: (() => undefined) | null; liveConditions: Array; constructor(providers: { [namespace: string]: LiveProvider }) { @@ -83,9 +81,11 @@ export class LiveMonitor { this.subscriptionReleasers = []; this.changeCallback = null; this.liveConditions = []; + if (!this.handleChange) { throw new Error("This is just to make flow happy"); } + this.handleChange = throttle( this.handleChange.bind(this), MONITOR_THROTTLE_DURATION, @@ -102,8 +102,9 @@ export class LiveMonitor { for (const releaser of this.subscriptionReleasers) { releaser(); } - this.subscriptionReleasers = []; - // Delete everything from liveConditions, we'll be getting fresh conditions soon enough + + this.subscriptionReleasers = []; // Delete everything from liveConditions, we'll be getting fresh conditions soon enough + this.liveConditions.splice(0, this.liveCollection.length); } @@ -111,14 +112,15 @@ export class LiveMonitor { if (this.handleChange) { this.handleChange.cancel(); } + this.handleChange = null; this.reset(); this.providers = {}; this.released = true; - } + } // Tell Flow that we're okay with overwriting this + + handleChange: (() => undefined) | null; - // Tell Flow that we're okay with overwriting this - handleChange: (() => void) | null; handleChange() { if (this.changeCallback) { // Convince Flow this won't suddenly become null @@ -129,26 +131,30 @@ export class LiveMonitor { // eslint-disable-next-line no-console console.warn("Change occurred, but no-one was listening"); } - } + } // Tell Flow that we're okay with overwriting this - // Tell Flow that we're okay with overwriting this - onChange: (callback: () => void) => void; - onChange(callback: () => void) { + onChange: (callback: () => undefined) => undefined; + + onChange(callback: () => undefined) { if (this.released) { throw new Error("Monitors cannot be reused."); } + if (this.changeCallback) { throw new Error("Already monitoring for changes"); - } - // Throttle to every 250ms + } // Throttle to every 250ms + this.changeCallback = callback; + if (this.handleChange) { setImmediate(this.handleChange); } + return () => { if (this.changeCallback === callback) { this.changeCallback = null; } + this.release(); }; } @@ -159,22 +165,27 @@ export class LiveMonitor { predicate: (record: any) => boolean = () => true ) { const handleChange = this.handleChange; + if (this.released || !handleChange) { return; } + const provider = this.providers[namespace]; if (!provider || provider.sources.length === 0) return; + if (!provider.collectionIdentifierIsValid(collectionIdentifier)) { throw new Error( `Invalid collection identifier passed to LiveMonitor[${namespace}]: ${collectionIdentifier}` ); } + for (const source of provider.sources) { const releaser = source.subscribeCollection( handleChange, collectionIdentifier, predicate ); + if (releaser) { this.subscriptionReleasers.push(releaser); } @@ -187,17 +198,20 @@ export class LiveMonitor { recordIdentifier: any ) { const handleChange = this.handleChange; + if (this.released || !handleChange) { return; - } - // TODO: if (recordIdentifier == null) {return} + } // TODO: if (recordIdentifier == null) {return} + const provider = this.providers[namespace]; if (!provider || provider.sources.length === 0) return; + if (!provider.collectionIdentifierIsValid(collectionIdentifier)) { throw new Error( `Invalid collection identifier passed to LiveMonitor[${namespace}]: ${collectionIdentifier}` ); } + if ( !provider.recordIdentifierIsValid(collectionIdentifier, recordIdentifier) ) { @@ -205,26 +219,30 @@ export class LiveMonitor { `Invalid record identifier passed to LiveMonitor[${namespace}]: ${collectionIdentifier}` ); } + for (const source of provider.sources) { const releaser = source.subscribeRecord( handleChange, collectionIdentifier, recordIdentifier ); + if (releaser) { this.subscriptionReleasers.push(releaser); } } } } - /* * There is one coordinator for each build of the GraphQL schema, it tracks the providers * and gives a handy `subscribe` method that can be used for live queries (assuming * that the `resolve` is provided the same as in a Query). */ + export class LiveCoordinator { - providers: { [namespace: string]: LiveProvider }; + providers: { + [namespace: string]: LiveProvider; + }; constructor() { this.providers = {}; @@ -233,9 +251,11 @@ export class LiveCoordinator { registerProvider(provider: LiveProvider) { const { namespace } = provider; + if (this.providers[namespace]) { throw new Error(`Namespace ${namespace} already registered with Live`); } + this.providers[namespace] = provider; } @@ -247,6 +267,7 @@ export class LiveCoordinator { ); return; } + this.providers[namespace].registerSource(source); } @@ -260,26 +281,27 @@ export class LiveCoordinator { liveConditions: monitor.liveConditions, }, }; - } + } // Tell Flow that we're okay with overwriting this - // Tell Flow that we're okay with overwriting this subscribe: ( _parent: any, _args: any, context: any, _info: GraphQLResolveInfo ) => any; + subscribe(_parent: any, _args: any, context: any, _info: GraphQLResolveInfo) { const { monitor, context: additionalContext } = this.getMonitorAndContext(); Object.assign(context, additionalContext); const iterator = makeAsyncIteratorFromMonitor(monitor); + context.liveAbort = e => { iterator.throw(e); }; + return iterator; } } - export function makeAsyncIteratorFromMonitor(monitor: LiveMonitor) { return callbackToAsyncIterator(monitor.onChange, { onClose: release => { diff --git a/packages/graphile-build/src/SchemaBuilder.js b/packages/graphile-build/src/SchemaBuilder.ts similarity index 79% rename from packages/graphile-build/src/SchemaBuilder.js rename to packages/graphile-build/src/SchemaBuilder.ts index 53f380a93..969c0c236 100644 --- a/packages/graphile-build/src/SchemaBuilder.js +++ b/packages/graphile-build/src/SchemaBuilder.ts @@ -1,188 +1,164 @@ -// @flow import debugFactory from "debug"; import makeNewBuild from "./makeNewBuild"; import { bindAll } from "./utils"; import * as graphql from "graphql"; -import type { +import { GraphQLType, GraphQLNamedType, GraphQLInterfaceType, GraphQLObjectTypeConfig, } from "graphql"; -import EventEmitter from "events"; -// TODO: when we move to TypeScript, change this to: +import EventEmitter from "events"; // TODO: when we move to TypeScript, change this to: // import { EventEmitter } from "events"; -import type { + +import { simplifyParsedResolveInfoFragmentWithType, parseResolveInfo, } from "graphql-parse-resolve-info"; -import type { GraphQLResolveInfo } from "graphql/type/definition"; - -import type { FieldWithHooksFunction } from "./makeNewBuild"; +import { GraphQLResolveInfo } from "graphql/type/definition"; +import { FieldWithHooksFunction } from "./makeNewBuild"; const { GraphQLSchema } = graphql; - const debug = debugFactory("graphile-builder"); - const INDENT = " "; - export type Options = { - [string]: mixed, + [a: string]: unknown; }; - export type Plugin = ( builder: SchemaBuilder, options: Options -) => Promise | void; - -type TriggerChangeType = () => void; - +) => Promise | undefined; +type TriggerChangeType = () => undefined; export type DataForType = { - [string]: Array, + [a: string]: Array; }; - -export type Build = {| - graphileBuildVersion: string, - graphql: *, - parseResolveInfo: parseResolveInfo, - simplifyParsedResolveInfoFragmentWithType: simplifyParsedResolveInfoFragmentWithType, - // DEPRECATED: getAliasFromResolveInfo: (resolveInfo: GraphQLResolveInfo) => string, - getSafeAliasFromResolveInfo: (resolveInfo: GraphQLResolveInfo) => string, - getSafeAliasFromAlias: (alias: string) => string, - resolveAlias( +export type Build = { + graphileBuildVersion: string; + graphql: any; + parseResolveInfo: parseResolveInfo; + simplifyParsedResolveInfoFragmentWithType: simplifyParsedResolveInfoFragmentWithType; + getSafeAliasFromResolveInfo: (resolveInfo: GraphQLResolveInfo) => string; + getSafeAliasFromAlias: (alias: string) => string; + resolveAlias: ( data: {}, - _args: mixed, - _context: mixed, + _args: unknown, + _context: unknown, resolveInfo: GraphQLResolveInfo - ): string, - addType(type: GraphQLNamedType, origin?: ?string): void, - getTypeByName(typeName: string): ?GraphQLType, - extend(base: Obj1, extra: Obj2, hint?: string): Obj1 & Obj2, - newWithHooks( - Class, + ) => string; + addType: ( + type: GraphQLNamedType, + origin: string | null | undefined + ) => undefined; + getTypeByName: (typeName: string) => GraphQLType | null | undefined; + extend: ( + base: Obj1, + extra: Obj2, + hint: string + ) => Obj1 & Obj2; + newWithHooks: < + T extends GraphQLNamedType | GraphQLSchema, + ConfigType extends any + >( + a: Class, spec: ConfigType, scope: Scope, - performNonEmptyFieldsCheck?: boolean - ): ?T, - fieldDataGeneratorsByType: Map<*, *>, // @deprecated - use fieldDataGeneratorsByFieldNameByType instead - fieldDataGeneratorsByFieldNameByType: Map<*, *>, - fieldArgDataGeneratorsByFieldNameByType: Map<*, *>, + performNonEmptyFieldsCheck: boolean + ) => T | null | undefined; + fieldDataGeneratorsByType: Map; + fieldDataGeneratorsByFieldNameByType: Map; + fieldArgDataGeneratorsByFieldNameByType: Map; inflection: { - // eslint-disable-next-line flowtype/no-weak-types - [string]: (...args: Array) => string, - }, - swallowError: (e: Error) => void, + [a: string]: () => string; + }; + swallowError: (e: Error) => undefined; status: { - currentHookName: ?string, - currentHookEvent: ?string, - }, - scopeByType: Map, -|}; - -export type BuildExtensionQuery = {| - $$isQuery: Symbol, -|}; - + currentHookName: string | null | undefined; + currentHookEvent: string | null | undefined; + }; + scopeByType: Map; +}; +export type BuildExtensionQuery = { + $$isQuery: Symbol; +}; export type Scope = { - __origin: ?string, - [string]: mixed, + __origin: string | null | undefined; + [a: string]: unknown; +}; +export type Context = { + scope: Scope; + type: string; + [a: string]: unknown; }; - -export type Context = {| - scope: Scope, - type: string, - [string]: mixed, -|}; - type DataGeneratorFunction = () => {}; - -export type ContextGraphQLObjectTypeFields = {| +export type ContextGraphQLObjectTypeFields = { addDataGeneratorForField: ( fieldName: string, fn: DataGeneratorFunction - ) => void, - recurseDataGeneratorsForField: (fieldName: string) => void, // @deprecated - DO NOT USE! - Self: GraphQLNamedType, - GraphQLObjectType: GraphQLObjectTypeConfig<*, *>, - fieldWithHooks: FieldWithHooksFunction, -|}; - + ) => undefined; + recurseDataGeneratorsForField: (fieldName: string) => undefined; + Self: GraphQLNamedType; + GraphQLObjectType: GraphQLObjectTypeConfig; + fieldWithHooks: FieldWithHooksFunction; +}; type SupportedHookTypes = {} | Build | Array; - export type Hook< - Type: SupportedHookTypes, - BuildExtensions: *, - ContextExtensions: * + Type extends SupportedHookTypes, + BuildExtensions extends any, + ContextExtensions extends any > = { - ( - input: Type, - build: { ...Build, ...BuildExtensions }, - context: { ...Context, ...ContextExtensions } - ): Type, - displayName?: string, - provides?: Array, - before?: Array, - after?: Array, + displayName?: string; + provides?: Array; + before?: Array; + after?: Array; }; - -export type WatchUnwatch = (triggerChange: TriggerChangeType) => void; - -export type SchemaListener = (newSchema: GraphQLSchema) => void; +export type WatchUnwatch = (triggerChange: TriggerChangeType) => undefined; +export type SchemaListener = (newSchema: GraphQLSchema) => undefined; class SchemaBuilder extends EventEmitter { options: Options; watchers: Array; unwatchers: Array; - triggerChange: ?TriggerChangeType; + triggerChange: TriggerChangeType | null | undefined; depth: number; hooks: { - [string]: Array>, + [a: string]: Array>; }; - - _currentPluginName: ?string; - _generatedSchema: ?GraphQLSchema; - _explicitSchemaListener: ?SchemaListener; + _currentPluginName: string | null | undefined; + _generatedSchema: GraphQLSchema | null | undefined; + _explicitSchemaListener: SchemaListener | null | undefined; _busy: boolean; _watching: boolean; constructor(options: Options) { super(); - this.options = options; + if (!options) { throw new Error("Please pass options to SchemaBuilder"); } this._busy = false; this._watching = false; - this.watchers = []; - this.unwatchers = []; + this.unwatchers = []; // Because hooks can nest, this keeps track of how deep we are. - // Because hooks can nest, this keeps track of how deep we are. this.depth = -1; - this.hooks = { // The build object represents the current schema build and is passed to // all hooks, hook the 'build' event to extend this object: build: [], - // Inflection is used for naming resulting types/fields/args/etc - it's // hookable so that other plugins may extend it or override it inflection: [], - // 'build' phase should not generate any GraphQL objects (because the // build object isn't finalised yet so it risks weirdness occurring); so // if you need to set up any global types you can do so here. init: [], - // 'finalize' phase is called once the schema is built; typically you // shouldn't use this, but it's useful for interfacing with external // libraries that mutate an already constructed schema. finalize: [], - // Add 'query', 'mutation' or 'subscription' types in this hook: GraphQLSchema: [], - // When creating a GraphQLObjectType via `newWithHooks`, we'll // execute, the following hooks: // - 'GraphQLObjectType' to add any root-level attributes, e.g. add a description @@ -197,7 +173,6 @@ class SchemaBuilder extends EventEmitter { "GraphQLObjectType:fields": [], "GraphQLObjectType:fields:field": [], "GraphQLObjectType:fields:field:args": [], - // When creating a GraphQLInputObjectType via `newWithHooks`, we'll // execute, the following hooks: // - 'GraphQLInputObjectType' to add any root-level attributes, e.g. add a description @@ -208,7 +183,6 @@ class SchemaBuilder extends EventEmitter { GraphQLInputObjectType: [], "GraphQLInputObjectType:fields": [], "GraphQLInputObjectType:fields:field": [], - // When creating a GraphQLEnumType via `newWithHooks`, we'll // execute, the following hooks: // - 'GraphQLEnumType' to add any root-level attributes, e.g. add a description @@ -220,10 +194,9 @@ class SchemaBuilder extends EventEmitter { }; } - _setPluginName(name: ?string) { + _setPluginName(name: string | null | undefined) { this._currentPluginName = name; } - /* * Every hook `fn` takes three arguments: * - obj - the object currently being inspected @@ -232,9 +205,10 @@ class SchemaBuilder extends EventEmitter { * * The function must either return a replacement object for `obj` or `obj` itself */ - hook( + + hook( hookName: string, - fn: Hook, + fn: Hook, provides?: Array, before?: Array, after?: Array @@ -242,6 +216,7 @@ class SchemaBuilder extends EventEmitter { if (!this.hooks[hookName]) { throw new Error(`Sorry, '${hookName}' is not a supported hook`); } + if (this._currentPluginName) { fn.displayName = `${this._currentPluginName}/${hookName}/${(provides && provides.length && @@ -250,18 +225,23 @@ class SchemaBuilder extends EventEmitter { fn.name || "unnamed"}`; } + if (provides) { if (!fn.displayName && provides.length) { fn.displayName = `unknown/${hookName}/${provides[0]}`; } + fn.provides = provides; } + if (before) { fn.before = before; } + if (after) { fn.after = after; } + if (!fn.provides && !fn.before && !fn.after) { // No explicit dependencies - add to the end this.hooks[hookName].push(fn); @@ -274,16 +254,19 @@ class SchemaBuilder extends EventEmitter { let maxIndex = relevantHooks.length; let maxReason = null; const { provides: newProvides, before: newBefore, after: newAfter } = fn; + const describe = (hook, index) => { if (!hook) { return "-"; } + return `${hook.displayName || hook.name || "anonymous"} (${ index ? `index: ${index}, ` : "" }provides: ${hook.provides ? hook.provides.join(",") : "-"}, before: ${ hook.before ? hook.before.join(",") : "-" }, after: ${hook.after ? hook.after.join(",") : "-"})`; }; + const check = () => { if (minIndex > maxIndex) { throw new Error( @@ -299,6 +282,7 @@ class SchemaBuilder extends EventEmitter { ); } }; + const setMin = (newMin, reason) => { if (newMin > minIndex) { minIndex = newMin; @@ -306,6 +290,7 @@ class SchemaBuilder extends EventEmitter { check(); } }; + const setMax = (newMax, reason) => { if (newMax < maxIndex) { maxIndex = newMax; @@ -313,41 +298,45 @@ class SchemaBuilder extends EventEmitter { check(); } }; + relevantHooks.forEach((oldHook, idx) => { const { provides: oldProvides, before: oldBefore, after: oldAfter, } = oldHook; + if (newProvides) { if (oldBefore && oldBefore.some(dep => newProvides.includes(dep))) { // Old says it has to come before new setMin(idx + 1, oldHook); } + if (oldAfter && oldAfter.some(dep => newProvides.includes(dep))) { // Old says it has to be after new setMax(idx, oldHook); } } + if (oldProvides) { if (newBefore && newBefore.some(dep => oldProvides.includes(dep))) { // New says it has to come before old setMax(idx, oldHook); } + if (newAfter && newAfter.some(dep => oldProvides.includes(dep))) { // New says it has to be after old setMin(idx + 1, oldHook); } } - }); + }); // We've already validated everything, so we can now insert the record. - // We've already validated everything, so we can now insert the record. this.hooks[hookName].splice(maxIndex, 0, fn); } } - applyHooks( - build: { ...Build }, + applyHooks( + build: Build, hookName: string, input: T, context: Context, @@ -356,18 +345,22 @@ class SchemaBuilder extends EventEmitter { if (!input) { throw new Error("applyHooks was called with falsy input"); } + this.depth++; + try { debug(`${INDENT.repeat(this.depth)}[${hookName}${debugStr}]: Running...`); + const hooks: Array> = this.hooks[hookName]; - const hooks: Array> = this.hooks[hookName]; if (!hooks) { throw new Error(`Sorry, '${hookName}' is not a registered hook`); } let newObj = input; - for (const hook: Hook of hooks) { + + for (const hook: Hook of hooks) { this.depth++; + try { const hookDisplayName = hook.displayName || hook.name || "anonymous"; debug( @@ -375,7 +368,6 @@ class SchemaBuilder extends EventEmitter { this.depth )}[${hookName}${debugStr}]: Executing '${hookDisplayName}'` ); - const previousHookName = build.status.currentHookName; const previousHookEvent = build.status.currentHookEvent; build.status.currentHookName = hookDisplayName; @@ -391,6 +383,7 @@ class SchemaBuilder extends EventEmitter { "anonymous"}' for '${hookName}' returned falsy value '${newObj}'` ); } + debug( `${INDENT.repeat( this.depth @@ -402,7 +395,6 @@ class SchemaBuilder extends EventEmitter { } debug(`${INDENT.repeat(this.depth)}[${hookName}${debugStr}]: Complete`); - return newObj; } finally { this.depth--; @@ -413,13 +405,14 @@ class SchemaBuilder extends EventEmitter { if (!listen || !unlisten) { throw new Error("You must provide both a listener and an unlistener"); } + this.watchers.push(listen); this.unwatchers.push(unlisten); } - createBuild(): { ...Build } { - const initialBuild = makeNewBuild(this); - // Inflection needs to come first, in case 'build' hooks depend on it + createBuild(): Build { + const initialBuild = makeNewBuild(this); // Inflection needs to come first, in case 'build' hooks depend on it + initialBuild.inflection = this.applyHooks( initialBuild, "inflection", @@ -430,14 +423,21 @@ class SchemaBuilder extends EventEmitter { ); const build = this.applyHooks(initialBuild, "build", initialBuild, { scope: {}, - }); - // Bind all functions so they can be dereferenced + }); // Bind all functions so they can be dereferenced + bindAll( build, Object.keys(build).filter(key => typeof build[key] === "function") ); Object.freeze(build); - this.applyHooks(build, "init", {}, { scope: {} }); + this.applyHooks( + build, + "init", + {}, + { + scope: {}, + } + ); return build; } @@ -460,9 +460,11 @@ class SchemaBuilder extends EventEmitter { "Finalising GraphQL schema" ); } + if (!this._generatedSchema) { throw new Error("Schema generation failed"); } + return this._generatedSchema; } @@ -470,17 +472,20 @@ class SchemaBuilder extends EventEmitter { if (this._busy) { throw new Error("An operation is in progress"); } + if (this._watching) { throw new Error( "We're already watching this schema! Use `builder.on('schema', callback)` instead." ); } + try { this._busy = true; this._explicitSchemaListener = listener; + this.triggerChange = () => { - this._generatedSchema = null; - // XXX: optionally debounce + this._generatedSchema = null; // XXX: optionally debounce + try { const schema = this.buildSchema(); this.emit("schema", schema); @@ -490,17 +495,20 @@ class SchemaBuilder extends EventEmitter { // eslint-disable-next-line no-console console.error( "⚠️⚠️⚠️ An error occured when building the schema on watch:" - ); - // eslint-disable-next-line no-console + ); // eslint-disable-next-line no-console + console.error(e); } }; + for (const fn of this.watchers) { await fn(this.triggerChange); } + if (listener) { this.on("schema", listener); } + this.emit("schema", this.buildSchema()); this._watching = true; } finally { @@ -512,21 +520,27 @@ class SchemaBuilder extends EventEmitter { if (this._busy) { throw new Error("An operation is in progress"); } + if (!this._watching) { throw new Error("We're not watching this schema!"); } + this._busy = true; + try { const listener = this._explicitSchemaListener; this._explicitSchemaListener = null; + if (listener) { this.removeListener("schema", listener); } + if (this.triggerChange) { for (const fn of this.unwatchers) { await fn(this.triggerChange); } } + this.triggerChange = null; this._watching = false; } finally { diff --git a/packages/graphile-build/src/callbackToAsyncIterator.js b/packages/graphile-build/src/callbackToAsyncIterator.ts similarity index 69% rename from packages/graphile-build/src/callbackToAsyncIterator.js rename to packages/graphile-build/src/callbackToAsyncIterator.ts index 63250513c..d745cbc8a 100644 --- a/packages/graphile-build/src/callbackToAsyncIterator.js +++ b/packages/graphile-build/src/callbackToAsyncIterator.ts @@ -1,4 +1,3 @@ -// @flow /* eslint-disable flowtype/no-weak-types */ // Turn a callback-based listener into an async iterator // From https://raw.githubusercontent.com/withspectrum/callback-to-async-iterator/master/src/index.js @@ -11,14 +10,16 @@ const defaultOnError = (err: Error) => { }; export default function callbackToAsyncIterator< - CallbackInput: any, - ReturnVal: any + CallbackInput extends any, + ReturnVal extends any >( - listener: ((arg: CallbackInput) => any) => ?ReturnVal | Promise, + listener: ( + a: (arg: CallbackInput) => any + ) => (ReturnVal | null | undefined) | Promise, options?: { - onError?: (err: Error) => void, - onClose?: (arg?: ?ReturnVal) => void, - buffering?: boolean, + onError?: (err: Error) => undefined; + onClose?: (arg: ReturnVal | null | undefined) => undefined; + buffering?: boolean; } = {} ) { const { onError = defaultOnError, buffering = true, onClose } = options; @@ -29,7 +30,10 @@ export default function callbackToAsyncIterator< function pushValue(value) { if (pullQueue.length !== 0) { - pullQueue.shift()({ value, done: false }); + pullQueue.shift()({ + value, + done: false, + }); } else if (buffering === true) { pushQueue.push(value); } @@ -38,7 +42,10 @@ export default function callbackToAsyncIterator< function pullValue() { return new Promise(resolve => { if (pushQueue.length !== 0) { - resolve({ value: pushQueue.shift(), done: false }); + resolve({ + value: pushQueue.shift(), + done: false, + }); } else { pullQueue.push(resolve); } @@ -48,7 +55,12 @@ export default function callbackToAsyncIterator< function emptyQueue() { if (listening) { listening = false; - pullQueue.forEach(resolve => resolve({ value: undefined, done: true })); + pullQueue.forEach(resolve => + resolve({ + value: undefined, + done: true, + }) + ); pullQueue = []; pushQueue = []; onClose && onClose(listenerReturnValue); @@ -64,20 +76,31 @@ export default function callbackToAsyncIterator< .catch(err => { onError(err); }); - return { - next(): Promise<{ value?: CallbackInput, done: boolean }> { + next(): Promise<{ + value?: CallbackInput; + done: boolean; + }> { return listening ? pullValue() : this.return(); }, - return(): Promise<{ value: typeof undefined, done: boolean }> { + + return(): Promise<{ + value: typeof undefined; + done: boolean; + }> { emptyQueue(); - return Promise.resolve({ value: undefined, done: true }); + return Promise.resolve({ + value: undefined, + done: true, + }); }, + throw(error) { emptyQueue(); onError(error); return Promise.reject(error); }, + [$$asyncIterator]() { return this; }, @@ -88,12 +111,15 @@ export default function callbackToAsyncIterator< next() { return Promise.reject(err); }, + return() { return Promise.reject(err); }, + throw(error) { return Promise.reject(error); }, + [$$asyncIterator]() { return this; }, diff --git a/packages/graphile-build/src/extend.js b/packages/graphile-build/src/extend.ts similarity index 91% rename from packages/graphile-build/src/extend.js rename to packages/graphile-build/src/extend.ts index 48306ebde..7a5be91d3 100644 --- a/packages/graphile-build/src/extend.js +++ b/packages/graphile-build/src/extend.ts @@ -1,31 +1,28 @@ -// @flow import chalk from "chalk"; - const INDENT = " "; const $$hints = Symbol("hints"); - export function indent(text: string) { return ( INDENT + text.replace(/\n/g, "\n" + INDENT).replace(/\n +(?=\n|$)/g, "\n") ); } - -export default function extend( +export default function extend( base: Obj1, extra: Obj2, hint?: string ): Obj1 & Obj2 { // $FlowFixMe const hints = base[$$hints] || {}; - const keysB = Object.keys(extra); const extraHints = extra[$$hints] || {}; + for (const key of keysB) { const newValue = extra[key]; const hintB = extraHints[key] || hint; + if (key in base && base[key] !== newValue) { // $FlowFixMe - const hintA: ?string = hints[key]; + const hintA: string | null | undefined = hints[key]; const firstEntityDetails = !hintA ? "We don't have any information about the first entity." : `The first entity was:\n\n${indent(chalk.magenta(hintA))}`; @@ -38,10 +35,12 @@ export default function extend( )}'.\n\n${indent(firstEntityDetails)}\n\n${indent(secondEntityDetails)}` ); } + if (hintB) { hints[key] = hintB; } } + return Object.assign(base, extra, { // $FlowFixMe [$$hints]: hints, diff --git a/packages/graphile-build/src/index.js b/packages/graphile-build/src/index.ts similarity index 88% rename from packages/graphile-build/src/index.js rename to packages/graphile-build/src/index.ts index 5822caa86..063d6ad81 100644 --- a/packages/graphile-build/src/index.js +++ b/packages/graphile-build/src/index.ts @@ -1,5 +1,3 @@ -// @flow - import util from "util"; import SchemaBuilder from "./SchemaBuilder"; import { @@ -14,10 +12,8 @@ import { AddQueriesToSubscriptionsPlugin, } from "./plugins"; import resolveNode from "./resolveNode"; -import type { GraphQLSchema } from "graphql"; - -import type { Plugin, Options } from "./SchemaBuilder"; - +import { GraphQLSchema } from "graphql"; +import { Plugin, Options } from "./SchemaBuilder"; export { constantCaseAll, formatInsideUnderscores, @@ -28,10 +24,8 @@ export { pluralize, singularize, } from "./utils"; - -export type { SchemaBuilder }; - -export type { +export { SchemaBuilder }; +export { Plugin, Options, Build, @@ -41,17 +35,17 @@ export type { Hook, WatchUnwatch, SchemaListener, -} from "./SchemaBuilder"; - +}; export { LiveSource, LiveProvider, LiveMonitor, LiveCoordinator } from "./Live"; - export const getBuilder = async ( plugins: Array, options: Options = {} ): Promise => { const builder = new SchemaBuilder(options); + for (let i = 0, l = plugins.length; i < l; i++) { const plugin = plugins[i]; + if (typeof plugin !== "function") { throw new Error( `Expected a list of plugin functions, instead list contained a non-function at index ${i}: ${util.inspect( @@ -59,13 +53,16 @@ export const getBuilder = async ( )}` ); } + builder._setPluginName(plugin.displayName || plugin.name); + await plugin(builder, options); + builder._setPluginName(null); } + return builder; }; - export const buildSchema = async ( plugins: Array, options: Options = {} @@ -73,7 +70,6 @@ export const buildSchema = async ( const builder: SchemaBuilder = await getBuilder(plugins, options); return builder.buildSchema(); }; - export const defaultPlugins: Array = [ SwallowErrorsPlugin, StandardTypesPlugin, @@ -85,7 +81,6 @@ export const defaultPlugins: Array = [ MutationPayloadQueryPlugin, AddQueriesToSubscriptionsPlugin, ]; - export { SwallowErrorsPlugin, StandardTypesPlugin, @@ -95,7 +90,6 @@ export { SubscriptionPlugin, ClientMutationIdDescriptionPlugin, MutationPayloadQueryPlugin, - AddQueriesToSubscriptionsPlugin, - // resolveNode: EXPERIMENTAL, API might change! + AddQueriesToSubscriptionsPlugin, // resolveNode: EXPERIMENTAL, API might change! resolveNode, }; diff --git a/packages/graphile-build/src/makeNewBuild.js b/packages/graphile-build/src/makeNewBuild.ts similarity index 91% rename from packages/graphile-build/src/makeNewBuild.js rename to packages/graphile-build/src/makeNewBuild.ts index ce3cf06c4..2e8be03ac 100644 --- a/packages/graphile-build/src/makeNewBuild.js +++ b/packages/graphile-build/src/makeNewBuild.ts @@ -1,7 +1,5 @@ -// @flow - import * as graphql from "graphql"; -import type { +import { GraphQLNamedType, GraphQLInputField, GraphQLFieldResolver, @@ -13,7 +11,7 @@ import { getAliasFromResolveInfo as rawGetAliasFromResolveInfo, } from "graphql-parse-resolve-info"; import debugFactory from "debug"; -import type { ResolveTree } from "graphql-parse-resolve-info"; +import { ResolveTree } from "graphql-parse-resolve-info"; import pluralize from "pluralize"; import LRUCache from "lru-cache"; import semver from "semver"; @@ -21,26 +19,22 @@ import { upperCamelCase, camelCase, constantCase } from "./utils"; import swallowError from "./swallowError"; import resolveNode from "./resolveNode"; import { LiveCoordinator } from "./Live"; - -import type SchemaBuilder, { +import SchemaBuilder, { Build, Context, Scope, DataForType, } from "./SchemaBuilder"; - import extend, { indent } from "./extend"; import chalk from "chalk"; import { createHash } from "crypto"; - import { version } from "../package.json"; - let recurseDataGeneratorsForFieldWarned = false; const isString = str => typeof str === "string"; + const isDev = ["test", "development"].indexOf(process.env.NODE_ENV) >= 0; const debug = debugFactory("graphile-build"); - /* * This should be more than enough for normal usage. If you come under a * sophisticated attack then the attacker can empty this of useful values (with @@ -49,8 +43,8 @@ const debug = debugFactory("graphile-build"); * produce half a million hashes per second on my machine, the LRU only gives * us a 10x speedup! */ -const hashCache = new LRUCache(100000); +const hashCache = new LRUCache(100000); /* * This function must never return a string longer than 56 characters. * @@ -60,6 +54,7 @@ const hashCache = new LRUCache(100000); * for the user deliberately causing them, but that's their own fault!), so * we'll happily take the performance boost over SHA256. */ + function hashFieldAlias(str) { const precomputed = hashCache.get(str); if (precomputed) return precomputed; @@ -69,7 +64,6 @@ function hashFieldAlias(str) { hashCache.set(str, hash); return hash; } - /* * This function may be replaced at any time, but all versions of it will * always return a representation of `alias` (a valid GraphQL identifier) @@ -83,6 +77,7 @@ function hashFieldAlias(str) { * * It does not guarantee that this alias will be human readable! */ + function getSafeAliasFromAlias(alias) { if (alias.length <= 60 && !alias.startsWith("@")) { // Use the `@` to prevent conflicting with normal GraphQL field names, but otherwise let it through verbatim. @@ -95,55 +90,50 @@ function getSafeAliasFromAlias(alias) { return `@@${hashFieldAlias(alias)}`; } } - /* * This provides a "safe" version of the alias from ResolveInfo, guaranteed to * never be longer than 60 characters. This makes it suitable as a PostgreSQL * identifier. */ + function getSafeAliasFromResolveInfo(resolveInfo) { const alias = rawGetAliasFromResolveInfo(resolveInfo); return getSafeAliasFromAlias(alias); } type MetaData = { - [string]: Array, + [a: string]: Array; }; type DataGeneratorFunction = ( parsedResolveInfoFragment: ResolveTree, - ReturnType: GraphQLType, - ...args: Array + ReturnType: GraphQLType ) => Array; - type FieldSpecIsh = { - type?: GraphQLType, - args?: {}, - resolve?: GraphQLFieldResolver<*, *>, - deprecationReason?: string, - description?: ?string, + type?: GraphQLType; + args?: {}; + resolve?: GraphQLFieldResolver; + deprecationReason?: string; + description?: string | null | undefined; }; - type ContextAndGenerators = | Context | { - addDataGenerator: DataGeneratorFunction => void, - addArgDataGenerator: DataGeneratorFunction => void, + addDataGenerator: (a: DataGeneratorFunction) => undefined; + addArgDataGenerator: (a: DataGeneratorFunction) => undefined; getDataFromParsedResolveInfoFragment: ( parsedResolveInfoFragment: ResolveTree, Type: GraphQLType - ) => DataForType, + ) => DataForType; }; - export type FieldWithHooksFunction = ( fieldName: string, - spec: FieldSpecIsh | (ContextAndGenerators => FieldSpecIsh), - fieldScope?: {} + spec: FieldSpecIsh | ((a: ContextAndGenerators) => FieldSpecIsh), + fieldScope: {} ) => {}; - export type InputFieldWithHooksFunction = ( fieldName: string, spec: GraphQLInputField, - fieldScope?: {} + fieldScope: {} ) => GraphQLInputField; function getNameFromType(Type: GraphQLNamedType | GraphQLSchema) { @@ -171,15 +161,20 @@ const mergeData = ( ReturnType, arg ) => { - const results: ?Array = ensureArray(gen(arg, ReturnType, data)); + const results: Array | null | undefined = ensureArray( + gen(arg, ReturnType, data) + ); + if (!results) { return; } + for (const result: MetaData of results) { for (const k of Object.keys(result)) { data[k] = data[k] || []; - const value: mixed = result[k]; - const newData: ?Array = ensureArray(value); + const value: unknown = result[k]; + const newData: Array | null | undefined = ensureArray(value); + if (newData) { data[k].push(...newData); } @@ -195,7 +190,7 @@ const knownTypes = [ ]; const knownTypeNames = knownTypes.map(k => k.name); -function ensureArray(val: void | Array | T): void | Array { +function ensureArray(val: undefined | Array | T): undefined | Array { if (val == null) { return; } else if (Array.isArray(val)) { @@ -203,10 +198,10 @@ function ensureArray(val: void | Array | T): void | Array { } else { return [val]; } -} +} // eslint-disable-next-line no-unused-vars -// eslint-disable-next-line no-unused-vars let ensureName = fn => {}; + if (["development", "test"].indexOf(process.env.NODE_ENV) >= 0) { ensureName = fn => { if (isDev && !fn.displayName && !fn.name && debug.enabled) { @@ -218,7 +213,7 @@ if (["development", "test"].indexOf(process.env.NODE_ENV) >= 0) { }; } -export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { +export default function makeNewBuild(builder: SchemaBuilder): Build { const allTypes = { Int: graphql.GraphQLInt, Float: graphql.GraphQLFloat, @@ -232,23 +227,18 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { String: "GraphQL Built-in", Boolean: "GraphQL Built-in", ID: "GraphQL Built-in", - }; - - // Every object type gets fieldData associated with each of its + }; // Every object type gets fieldData associated with each of its // fields. - // When a field is defined, it may add to this field data. - // When something resolves referencing this type, the resolver may // request the fieldData, e.g. to perform optimisations. - // fieldData is an object whose keys are the fields on this // GraphQLObjectType and whose values are an object (whose keys are // arbitrary namespaced keys and whose values are arrays of // information of this kind) + const fieldDataGeneratorsByFieldNameByType = new Map(); const fieldArgDataGeneratorsByFieldNameByType = new Map(); - return { options: builder.options, graphileBuildVersion: version, @@ -256,37 +246,50 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { graphql: require("graphql/package.json").version, "graphile-build": version, }, + hasVersion( packageName: string, range: string, - options?: { includePrerelease?: boolean } = { includePrerelease: true } + options?: { + includePrerelease?: boolean; + } = { + includePrerelease: true, + } ): boolean { const packageVersion = this.versions[packageName]; if (!packageVersion) return false; return semver.satisfies(packageVersion, range, options); }, + graphql, parseResolveInfo, simplifyParsedResolveInfoFragmentWithType, getSafeAliasFromAlias, - getAliasFromResolveInfo: getSafeAliasFromResolveInfo, // DEPRECATED: do not use this! + getAliasFromResolveInfo: getSafeAliasFromResolveInfo, + // DEPRECATED: do not use this! getSafeAliasFromResolveInfo, + resolveAlias(data, _args, _context, resolveInfo) { const alias = getSafeAliasFromResolveInfo(resolveInfo); return data[alias]; }, - addType(type: GraphQLNamedType, origin?: ?string): void { + + addType( + type: GraphQLNamedType, + origin?: string | null | undefined + ): undefined { if (!type.name) { throw new Error( `addType must only be called with named types, try using require('graphql').getNamedType` ); } + const newTypeSource = - origin || - // 'this' is typically only available after the build is finalized + origin || // 'this' is typically only available after the build is finalized (this ? `'addType' call during hook '${this.status.currentHookName}'` : null); + if (allTypes[type.name]) { if (allTypes[type.name] !== type) { const oldTypeSource = allTypesSources[type.name]; @@ -313,17 +316,24 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { allTypesSources[type.name] = newTypeSource; } }, + getTypeByName(typeName) { return allTypes[typeName]; }, + extend, - newWithHooks( + + newWithHooks< + T extends GraphQLNamedType | GraphQLSchema, + ConfigType extends any + >( Type: Class, spec: ConfigType, inScope: Scope, performNonEmptyFieldsCheck = false - ): ?T { + ): T | null | undefined { const scope = inScope || {}; + if (!inScope) { // eslint-disable-next-line no-console console.warn( @@ -332,17 +342,21 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { }], it's highly recommended that you add a scope so other hooks can easily reference your object - please check usage of 'newWithHooks'. To mute this message, just pass an empty object.` ); } + if (!Type) { throw new Error("No type specified!"); } + if (!this.newWithHooks || !Object.isFrozen(this)) { throw new Error( "Please do not generate the schema during the build building phase, use 'init' instead" ); } + const fieldDataGeneratorsByFieldName = {}; const fieldArgDataGeneratorsByFieldName = {}; let newSpec = spec; + if ( knownTypes.indexOf(Type) === -1 && knownTypeNames.indexOf(Type.name) >= 0 @@ -353,6 +367,7 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { }' detected! Multiple versions of graphql exist in your node_modules?` ); } + if (Type === GraphQLSchema) { newSpec = builder.applyHooks(this, "GraphQLSchema", newSpec, { type: "GraphQLSchema", @@ -370,6 +385,7 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { fieldDataGeneratorsByFieldName[fieldName] || []; fieldDataGeneratorsByFieldName[fieldName].push(fn); }; + const recurseDataGeneratorsForField = ( fieldName, iKnowWhatIAmDoing @@ -383,12 +399,13 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { * do not rely on the GraphQL query alias to store the result. */ if (!iKnowWhatIAmDoing && !recurseDataGeneratorsForFieldWarned) { - recurseDataGeneratorsForFieldWarned = true; - // eslint-disable-next-line no-console + recurseDataGeneratorsForFieldWarned = true; // eslint-disable-next-line no-console + console.error( "Use of `recurseDataGeneratorsForField` is NOT SAFE. e.g. `{n1: node { a: field1 }, n2: node { a: field2 } }` cannot resolve correctly." ); } + const fn = (parsedResolveInfoFragment, ReturnType, ...rest) => { const { args } = parsedResolveInfoFragment; const { fields } = this.simplifyParsedResolveInfoFragmentWithType( @@ -403,31 +420,38 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { const argDataGeneratorsForSelfByFieldName = fieldArgDataGeneratorsByFieldNameByType.get( Self ); + if (argDataGeneratorsForSelfByFieldName) { const argDataGenerators = argDataGeneratorsForSelfByFieldName[fieldName]; + for (const gen of argDataGenerators) { const local = ensureArray(gen(args, ReturnType, ...rest)); + if (local) { results.push(...local); } } } + if ( fieldDataGeneratorsByFieldName && isCompositeType(StrippedType) && !isAbstractType(StrippedType) ) { const typeFields = StrippedType.getFields(); + for (const alias of Object.keys(fields)) { - const field = fields[alias]; - // Run generators with `field` as the `parsedResolveInfoFragment`, pushing results to `results` + const field = fields[alias]; // Run generators with `field` as the `parsedResolveInfoFragment`, pushing results to `results` + const gens = fieldDataGeneratorsByFieldName[field.name]; + if (gens) { for (const gen of gens) { const local = ensureArray( gen(field, typeFields[field.name].type, ...rest) ); + if (local) { results.push(...local); } @@ -435,13 +459,14 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { } } } + return results; }; + fn.displayName = `recurseDataGeneratorsForField(${getNameFromType( Self )}:${fieldName})`; - addDataGeneratorForField(fieldName, fn); - // get type from field, get + addDataGeneratorForField(fieldName, fn); // get type from field, get }; const commonContext = { @@ -458,7 +483,6 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { }), `|${newSpec.name}` ); - const rawSpec = newSpec; newSpec = Object.assign({}, newSpec, { interfaces: () => { @@ -467,9 +491,11 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { GraphQLObjectType: rawSpec, }); let rawInterfaces = rawSpec.interfaces || []; + if (typeof rawInterfaces === "function") { rawInterfaces = rawInterfaces(interfacesContext); } + return builder.applyHooks( this, "GraphQLObjectType:interfaces", @@ -491,6 +517,7 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { "It looks like you forgot to pass the fieldName to `fieldWithHooks`, we're sorry this is current necessary." ); } + if (!fieldScope) { throw new Error( "All calls to `fieldWithHooks` must specify a `fieldScope` " + @@ -507,33 +534,33 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { fieldArgDataGeneratorsByFieldName[ fieldName ] = argDataGenerators; - let newSpec = spec; let context = Object.assign({}, commonContext, { Self, + addDataGenerator(fn) { return addDataGeneratorForField(fieldName, fn); }, + addArgDataGenerator(fn) { ensureName(fn); argDataGenerators.push(fn); }, + getDataFromParsedResolveInfoFragment: ( parsedResolveInfoFragment, ReturnType ): DataForType => { const Type: GraphQLNamedType = getNamedType(ReturnType); const data = {}; - const { fields, args, } = this.simplifyParsedResolveInfoFragmentWithType( parsedResolveInfoFragment, ReturnType - ); + ); // Args -> argDataGenerators - // Args -> argDataGenerators for (const gen of argDataGenerators) { try { mergeData(data, gen, ReturnType, args); @@ -546,39 +573,46 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { ); throw e; } - } + } // finalSpec.type -> fieldData - // finalSpec.type -> fieldData if (!finalSpec) { throw new Error( "It's too early to call this! Call from within resolve" ); } + const fieldDataGeneratorsByFieldName = fieldDataGeneratorsByFieldNameByType.get( Type ); + if ( fieldDataGeneratorsByFieldName && isCompositeType(Type) && !isAbstractType(Type) ) { const typeFields = Type.getFields(); + for (const alias of Object.keys(fields)) { const field = fields[alias]; const gens = fieldDataGeneratorsByFieldName[field.name]; + if (gens) { const FieldReturnType = typeFields[field.name].type; + for (const gen of gens) { mergeData(data, gen, FieldReturnType, field); } } } } + return data; }, scope: extend( extend( - { ...scope }, + { + ...scope, + }, { fieldName, }, @@ -590,9 +624,11 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { }'` ), }); + if (typeof newSpec === "function") { newSpec = newSpec(context); } + newSpec = builder.applyHooks( this, "GraphQLObjectType:fields:field", @@ -616,12 +652,14 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { const finalSpec = newSpec; processedFields.push(finalSpec); return finalSpec; - }: FieldWithHooksFunction), + }) as FieldWithHooksFunction, }); let rawFields = rawSpec.fields || {}; + if (typeof rawFields === "function") { rawFields = rawFields(fieldsContext); } + const fieldsSpec = builder.applyHooks( this, "GraphQLObjectType:fields", @@ -634,10 +672,11 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { ), fieldsContext, `|${rawSpec.name}` - ); - // Finally, check through all the fields that they've all been processed; any that have not we should do so now. + ); // Finally, check through all the fields that they've all been processed; any that have not we should do so now. + for (const fieldName in fieldsSpec) { const fieldSpec = fieldsSpec[fieldName]; + if (processedFields.indexOf(fieldSpec) < 0) { // We've not processed this yet; process it now! fieldsSpec[fieldName] = fieldsContext.fieldWithHooks( @@ -649,6 +688,7 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { ); } } + return fieldsSpec; }, }); @@ -665,7 +705,6 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { `|${newSpec.name}` ); newSpec.fields = newSpec.fields || {}; - const rawSpec = newSpec; newSpec = Object.assign({}, newSpec, { fields: () => { @@ -679,11 +718,14 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { "It looks like you forgot to pass the fieldName to `fieldWithHooks`, we're sorry this is current necessary." ); } + let context = Object.assign({}, commonContext, { Self, scope: extend( extend( - { ...scope }, + { + ...scope, + }, { fieldName, }, @@ -698,9 +740,11 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { ), }); let newSpec = spec; + if (typeof newSpec === "function") { newSpec = newSpec(context); } + newSpec = builder.applyHooks( this, "GraphQLInputObjectType:fields:field", @@ -711,12 +755,14 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { const finalSpec = newSpec; processedFields.push(finalSpec); return finalSpec; - }: InputFieldWithHooksFunction), + }) as InputFieldWithHooksFunction, }); let rawFields = rawSpec.fields; + if (typeof rawFields === "function") { rawFields = rawFields(fieldsContext); } + const fieldsSpec = builder.applyHooks( this, "GraphQLInputObjectType:fields", @@ -729,10 +775,11 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { ), fieldsContext, `|${getNameFromType(Self)}` - ); - // Finally, check through all the fields that they've all been processed; any that have not we should do so now. + ); // Finally, check through all the fields that they've all been processed; any that have not we should do so now. + for (const fieldName in fieldsSpec) { const fieldSpec = fieldsSpec[fieldName]; + if (processedFields.indexOf(fieldSpec) < 0) { // We've not processed this yet; process it now! fieldsSpec[fieldName] = fieldsContext.fieldWithHooks( @@ -744,6 +791,7 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { ); } } + return fieldsSpec; }, }); @@ -759,7 +807,6 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { commonContext, `|${newSpec.name}` ); - newSpec.values = builder.applyHooks( this, "GraphQLEnumType:values", @@ -781,9 +828,10 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { return memo; }, {}); } - const finalSpec: ConfigType = newSpec; + const finalSpec: ConfigType = newSpec; const Self: T = new Type(finalSpec); + if (!(Self instanceof GraphQLSchema) && performNonEmptyFieldsCheck) { try { if ( @@ -795,8 +843,10 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { | GraphQLInterfaceType | GraphQLInputObjectType | GraphQLObjectType = Self; + if (typeof _Self.getFields === "function") { const fields = _Self.getFields(); + if (Object.keys(fields).length === 0) { // We require there's at least one field on GraphQLObjectType and GraphQLInputObjectType records return null; @@ -809,17 +859,21 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { if (inScope && inScope.isRootQuery) { throw e; } + const isProbablyAnEmptyObjectError = !!e.message.match( /function which returns such an object/ ); + if (!isProbablyAnEmptyObjectError) { this.swallowError(e); } + return null; } } this.scopeByType.set(Self, scope); + if (finalSpec.name) { this.addType( Self, @@ -831,6 +885,7 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { : null) ); } + fieldDataGeneratorsByFieldNameByType.set( Self, fieldDataGeneratorsByFieldName @@ -841,7 +896,9 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { ); return Self; }, - fieldDataGeneratorsByType: fieldDataGeneratorsByFieldNameByType, // @deprecated + + fieldDataGeneratorsByType: fieldDataGeneratorsByFieldNameByType, + // @deprecated fieldDataGeneratorsByFieldNameByType, fieldArgDataGeneratorsByFieldNameByType, inflection: { @@ -850,7 +907,6 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { upperCamelCase, camelCase, constantCase, - // Built-in names (allows you to override these in the output schema) builtin: name => { /* @@ -882,7 +938,6 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { */ return name; }, - // When converting a query field to a subscription (live query) field, this allows you to rename it live: name => name, }, diff --git a/packages/graphile-build/src/plugins/AddQueriesToSubscriptionsPlugin.js b/packages/graphile-build/src/plugins/AddQueriesToSubscriptionsPlugin.ts similarity index 95% rename from packages/graphile-build/src/plugins/AddQueriesToSubscriptionsPlugin.js rename to packages/graphile-build/src/plugins/AddQueriesToSubscriptionsPlugin.ts index 500b316ce..1f0de2b5d 100644 --- a/packages/graphile-build/src/plugins/AddQueriesToSubscriptionsPlugin.js +++ b/packages/graphile-build/src/plugins/AddQueriesToSubscriptionsPlugin.ts @@ -1,6 +1,5 @@ -// @flow -import type { Plugin } from "../SchemaBuilder"; -import type { GraphQLObjectType } from "graphql"; +import { Plugin } from "../SchemaBuilder"; +import { GraphQLObjectType } from "graphql"; const AddQueriesToSubscriptionsPlugin: Plugin = function( builder, @@ -9,6 +8,7 @@ const AddQueriesToSubscriptionsPlugin: Plugin = function( if (!subscriptions || !live) { return; } + builder.hook( "GraphQLObjectType:fields", (fields, build, context) => { @@ -17,6 +17,7 @@ const AddQueriesToSubscriptionsPlugin: Plugin = function( scope: { isRootSubscription }, fieldWithHooks, } = context; + if (!isRootSubscription) { return fields; } @@ -50,9 +51,11 @@ const AddQueriesToSubscriptionsPlugin: Plugin = function( return await oldResolve(...args); } catch (e) { const context = args[2]; + if (typeof context.liveAbort === "function") { context.liveAbort(e); } + throw e; } }, @@ -77,4 +80,5 @@ const AddQueriesToSubscriptionsPlugin: Plugin = function( ["AddQueriesToSubscriptions"] ); }; + export default AddQueriesToSubscriptionsPlugin; diff --git a/packages/graphile-build/src/plugins/ClientMutationIdDescriptionPlugin.js b/packages/graphile-build/src/plugins/ClientMutationIdDescriptionPlugin.ts similarity index 87% rename from packages/graphile-build/src/plugins/ClientMutationIdDescriptionPlugin.js rename to packages/graphile-build/src/plugins/ClientMutationIdDescriptionPlugin.ts index d8f1cc119..6b2aee38f 100644 --- a/packages/graphile-build/src/plugins/ClientMutationIdDescriptionPlugin.js +++ b/packages/graphile-build/src/plugins/ClientMutationIdDescriptionPlugin.ts @@ -1,17 +1,22 @@ -// @flow -import type SchemaBuilder, { Plugin } from "../SchemaBuilder"; - -export default (function ClientMutationIdDescriptionPlugin( +import SchemaBuilder, { Plugin } from "../SchemaBuilder"; +export default function ClientMutationIdDescriptionPlugin( builder: SchemaBuilder ) { builder.hook( "GraphQLInputObjectType:fields:field", - (field: { name?: string }, build, context) => { + ( + field: { + name?: string; + }, + build, + context + ) => { const { extend } = build; const { scope: { isMutationInput, fieldName }, Self, } = context; + if ( !isMutationInput || fieldName !== "clientMutationId" || @@ -19,6 +24,7 @@ export default (function ClientMutationIdDescriptionPlugin( ) { return field; } + return extend( field, { @@ -30,15 +36,21 @@ export default (function ClientMutationIdDescriptionPlugin( }, ["ClientMutationIdDescription"] ); - builder.hook( "GraphQLObjectType:fields:field", - (field: { name?: string }, build, context) => { + ( + field: { + name?: string; + }, + build, + context + ) => { const { extend } = build; const { scope: { isMutationPayload, fieldName }, Self, } = context; + if ( !isMutationPayload || fieldName !== "clientMutationId" || @@ -46,6 +58,7 @@ export default (function ClientMutationIdDescriptionPlugin( ) { return field; } + return extend( field, { @@ -57,7 +70,6 @@ export default (function ClientMutationIdDescriptionPlugin( }, ["ClientMutationIdDescription"] ); - builder.hook( "GraphQLObjectType:fields:field:args", (args: {}, build, context) => { @@ -67,9 +79,11 @@ export default (function ClientMutationIdDescriptionPlugin( Self, field, } = context; + if (!isRootMutation || !args.input || args.input.description) { return args; } + return Object.assign({}, args, { input: extend( args.input, @@ -85,4 +99,4 @@ export default (function ClientMutationIdDescriptionPlugin( }, ["ClientMutationIdDescription"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build/src/plugins/MutationPayloadQueryPlugin.js b/packages/graphile-build/src/plugins/MutationPayloadQueryPlugin.ts similarity index 71% rename from packages/graphile-build/src/plugins/MutationPayloadQueryPlugin.js rename to packages/graphile-build/src/plugins/MutationPayloadQueryPlugin.ts index 32bda96b0..3c37efe8e 100644 --- a/packages/graphile-build/src/plugins/MutationPayloadQueryPlugin.js +++ b/packages/graphile-build/src/plugins/MutationPayloadQueryPlugin.ts @@ -1,23 +1,19 @@ -// @flow -import type { Plugin, Build } from "../SchemaBuilder"; -import type { BuildExtensionQuery } from "./QueryPlugin"; - -export default (function MutationPayloadQueryPlugin(builder) { +import { Plugin, Build } from "../SchemaBuilder"; +import { BuildExtensionQuery } from "./QueryPlugin"; +export default function MutationPayloadQueryPlugin(builder) { builder.hook( "GraphQLObjectType:fields", - ( - fields: {}, - build: {| ...Build, ...BuildExtensionQuery |}, - context - ): {} => { + (fields: {}, build: Build & BuildExtensionQuery, context): {} => { const { $$isQuery, extend, getTypeByName, inflection } = build; const { scope: { isMutationPayload }, Self, } = context; + if (!isMutationPayload) { return fields; } + const Query = getTypeByName(inflection.builtin("Query")); return extend( fields, @@ -26,6 +22,7 @@ export default (function MutationPayloadQueryPlugin(builder) { description: "Our root query field type. Allows us to run any query from our mutation payload.", type: Query, + resolve() { return $$isQuery; }, @@ -36,4 +33,4 @@ export default (function MutationPayloadQueryPlugin(builder) { }, ["MutationPayloadQuery"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build/src/plugins/MutationPlugin.js b/packages/graphile-build/src/plugins/MutationPlugin.ts similarity index 89% rename from packages/graphile-build/src/plugins/MutationPlugin.js rename to packages/graphile-build/src/plugins/MutationPlugin.ts index aa3e53d2f..db19e080b 100644 --- a/packages/graphile-build/src/plugins/MutationPlugin.js +++ b/packages/graphile-build/src/plugins/MutationPlugin.ts @@ -1,21 +1,22 @@ -// @flow -import type { Plugin } from "../SchemaBuilder"; +import { Plugin } from "../SchemaBuilder"; function isValidMutation(Mutation) { try { if (!Mutation) { return false; } + if (Object.keys(Mutation.getFields()).length === 0) { return false; } } catch (e) { return false; } + return true; } -export default (async function MutationPlugin(builder) { +export default async function MutationPlugin(builder) { builder.hook( "GraphQLSchema", (schema: {}, build) => { @@ -38,6 +39,7 @@ export default (async function MutationPlugin(builder) { }, true ); + if (isValidMutation(Mutation)) { return extend( schema, @@ -54,4 +56,4 @@ export default (async function MutationPlugin(builder) { [], ["Query"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build/src/plugins/NodePlugin.js b/packages/graphile-build/src/plugins/NodePlugin.ts similarity index 83% rename from packages/graphile-build/src/plugins/NodePlugin.js rename to packages/graphile-build/src/plugins/NodePlugin.ts index 4bfdeb90c..1d0feb258 100644 --- a/packages/graphile-build/src/plugins/NodePlugin.js +++ b/packages/graphile-build/src/plugins/NodePlugin.ts @@ -1,5 +1,4 @@ -// @flow -import type { +import { Plugin, Build, DataForType, @@ -7,43 +6,44 @@ import type { ContextGraphQLObjectTypeFields, } from "../SchemaBuilder"; import resolveNode from "../resolveNode"; -import type { ResolveTree } from "graphql-parse-resolve-info"; -import type { GraphQLType, GraphQLInterfaceType } from "graphql"; -import type { BuildExtensionQuery } from "./QueryPlugin"; +import { ResolveTree } from "graphql-parse-resolve-info"; +import { GraphQLType, GraphQLInterfaceType } from "graphql"; +import { BuildExtensionQuery } from "./QueryPlugin"; const base64 = str => Buffer.from(String(str)).toString("base64"); + const base64Decode = str => Buffer.from(String(str), "base64").toString("utf8"); export type NodeFetcher = ( - data: mixed, - identifiers: Array, - context: mixed, + data: unknown, + identifiers: Array, + context: unknown, parsedResolveInfoFragment: ResolveTree, type: GraphQLType, resolveData: DataForType ) => {}; - -export type BuildExtensionNode = {| - nodeIdFieldName: string, - $$nodeType: Symbol, - nodeFetcherByTypeName: { [string]: NodeFetcher }, - getNodeIdForTypeAndIdentifiers( - Type: GraphQLType, - ...identifiers: Array - ): string, - getTypeAndIdentifiersFromNodeId( +export type BuildExtensionNode = { + nodeIdFieldName: string; + $$nodeType: Symbol; + nodeFetcherByTypeName: { + [a: string]: NodeFetcher; + }; + getNodeIdForTypeAndIdentifiers: (Type: GraphQLType) => string; + getTypeAndIdentifiersFromNodeId: ( nodeId: string - ): { - Type: GraphQLType, - identifiers: Array, - }, - addNodeFetcherForTypeName(typeName: string, fetcher: NodeFetcher): void, - getNodeAlias(typeName: string): string, - getNodeType(alias: string): GraphQLType, - setNodeAlias(typeName: string, alias: string): void, -|}; - -export default (function NodePlugin( + ) => { + Type: GraphQLType; + identifiers: Array; + }; + addNodeFetcherForTypeName: ( + typeName: string, + fetcher: NodeFetcher + ) => undefined; + getNodeAlias: (typeName: string) => string; + getNodeType: (alias: string) => GraphQLType; + setNodeAlias: (typeName: string, alias: string) => undefined; +}; +export default function NodePlugin( builder, { nodeIdFieldName: inNodeIdFieldName } ) { @@ -62,11 +62,13 @@ export default (function NodePlugin( nodeIdFieldName, $$nodeType: Symbol("nodeType"), nodeFetcherByTypeName, + getNodeIdForTypeAndIdentifiers(Type, ...identifiers) { return base64( JSON.stringify([this.getNodeAlias(Type), ...identifiers]) ); }, + getTypeAndIdentifiersFromNodeId(nodeId) { const [alias, ...identifiers] = JSON.parse(base64Decode(nodeId)); return { @@ -74,21 +76,27 @@ export default (function NodePlugin( identifiers, }; }, + addNodeFetcherForTypeName(typeName, fetcher) { if (nodeFetcherByTypeName[typeName]) { throw new Error("There's already a fetcher for this type"); } + if (!fetcher) { throw new Error("No fetcher specified"); } + nodeFetcherByTypeName[typeName] = fetcher; }, + getNodeAlias(typeName) { return nodeAliasByTypeName[typeName] || typeName; }, + getNodeType(alias) { return this.getTypeByName(nodeTypeNameByAlias[alias] || alias); }, + setNodeAlias(typeName, alias) { nodeAliasByTypeName[typeName] = alias; nodeTypeNameByAlias[alias] = typeName; @@ -99,12 +107,11 @@ export default (function NodePlugin( }, ["Node"] ); - builder.hook( "init", function defineNodeInterfaceType( _: {}, - build: {| ...Build, ...BuildExtensionQuery, ...BuildExtensionNode |} + build: Build & BuildExtensionQuery & BuildExtensionNode ) { const { $$isQuery, @@ -149,7 +156,6 @@ export default (function NodePlugin( }, ["Node"] ); - builder.hook( "GraphQLObjectType:interfaces", function addNodeIdToQuery( @@ -161,10 +167,13 @@ export default (function NodePlugin( const { scope: { isRootQuery }, } = context; + if (!isRootQuery) { return interfaces; } + const Type = getTypeByName(inflection.builtin("Node")); + if (Type) { return [...interfaces, Type]; } else { @@ -173,21 +182,22 @@ export default (function NodePlugin( }, ["Node"] ); - builder.hook( "GraphQLObjectType:fields", ( fields: {}, - build: {| ...Build, ...BuildExtensionQuery, ...BuildExtensionNode |}, - context: {| ...Context, ...ContextGraphQLObjectTypeFields |} + build: Build & BuildExtensionQuery & BuildExtensionNode, + context: Context & ContextGraphQLObjectTypeFields ) => { const { scope: { isRootQuery }, fieldWithHooks, } = context; + if (!isRootQuery) { return fields; } + const { getTypeByName, extend, @@ -201,6 +211,7 @@ export default (function NodePlugin( description: "The root query type must be a `Node` to work well with Relay 1 mutations. This just resolves to `query`.", type: new GraphQLNonNull(GraphQLID), + resolve() { return "query"; }, @@ -216,12 +227,15 @@ export default (function NodePlugin( type: new GraphQLNonNull(GraphQLID), }, }, + resolve(data, args, context, resolveInfo) { const nodeId = args[nodeIdFieldName]; return resolveNode( nodeId, build, - { getDataFromParsedResolveInfoFragment }, + { + getDataFromParsedResolveInfoFragment, + }, data, context, resolveInfo @@ -238,4 +252,4 @@ export default (function NodePlugin( }, ["Node"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build/src/plugins/QueryPlugin.js b/packages/graphile-build/src/plugins/QueryPlugin.ts similarity index 89% rename from packages/graphile-build/src/plugins/QueryPlugin.js rename to packages/graphile-build/src/plugins/QueryPlugin.ts index 0c9d46756..bfe07ac9b 100644 --- a/packages/graphile-build/src/plugins/QueryPlugin.js +++ b/packages/graphile-build/src/plugins/QueryPlugin.ts @@ -1,11 +1,8 @@ -// @flow -import type { Plugin, Build } from "../SchemaBuilder"; - -export type BuildExtensionQuery = {| - $$isQuery: Symbol, -|}; - -export default (async function QueryPlugin(builder) { +import { Plugin, Build } from "../SchemaBuilder"; +export type BuildExtensionQuery = { + $$isQuery: Symbol; +}; +export default async function QueryPlugin(builder) { builder.hook( "build", (build: Build): Build & BuildExtensionQuery => @@ -41,6 +38,7 @@ export default (async function QueryPlugin(builder) { description: "Exposes the root query type nested one level down. This is helpful for Relay 1 which can only query top level fields if they are in a particular form.", type: new GraphQLNonNull(Self), + resolve() { return $$isQuery; }, @@ -53,6 +51,7 @@ export default (async function QueryPlugin(builder) { }, true ); + if (queryType) { return extend( schema, @@ -67,4 +66,4 @@ export default (async function QueryPlugin(builder) { }, ["Query"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build/src/plugins/StandardTypesPlugin.js b/packages/graphile-build/src/plugins/StandardTypesPlugin.ts similarity index 86% rename from packages/graphile-build/src/plugins/StandardTypesPlugin.js rename to packages/graphile-build/src/plugins/StandardTypesPlugin.ts index de841dc87..63dcb4bf2 100644 --- a/packages/graphile-build/src/plugins/StandardTypesPlugin.js +++ b/packages/graphile-build/src/plugins/StandardTypesPlugin.ts @@ -1,8 +1,6 @@ -// @flow -import type { Plugin, Build } from "../SchemaBuilder"; +import { Plugin, Build } from "../SchemaBuilder"; import { Kind } from "graphql/language"; - -export default (function StandardTypesPlugin(builder) { +export default function StandardTypesPlugin(builder) { // XXX: this should be in an "init" plugin, but PgTypesPlugin requires it in build - fix that, then fix this builder.hook( "build", @@ -17,6 +15,7 @@ export default (function StandardTypesPlugin(builder) { if (ast.kind !== Kind.STRING) { throw new Error("Can only parse string values"); } + return ast.value; }, }); @@ -37,9 +36,10 @@ export default (function StandardTypesPlugin(builder) { newWithHooks, graphql: { GraphQLNonNull, GraphQLObjectType, GraphQLBoolean }, inflection, - } = build; - // https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo + } = build; // https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo + /* const PageInfo = */ + newWithHooks( GraphQLObjectType, { @@ -60,7 +60,9 @@ export default (function StandardTypesPlugin(builder) { type: new GraphQLNonNull(GraphQLBoolean), }; }, - { isPageInfoHasNextPageField: true } + { + isPageInfoHasNextPageField: true, + } ), hasPreviousPage: fieldWithHooks( "hasPreviousPage", @@ -76,7 +78,9 @@ export default (function StandardTypesPlugin(builder) { type: new GraphQLNonNull(GraphQLBoolean), }; }, - { isPageInfoHasPreviousPageField: true } + { + isPageInfoHasPreviousPageField: true, + } ), }), }, @@ -89,4 +93,4 @@ export default (function StandardTypesPlugin(builder) { }, ["StandardTypes", "PageInfo"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build/src/plugins/SubscriptionPlugin.js b/packages/graphile-build/src/plugins/SubscriptionPlugin.ts similarity index 89% rename from packages/graphile-build/src/plugins/SubscriptionPlugin.js rename to packages/graphile-build/src/plugins/SubscriptionPlugin.ts index 3467836e6..94acfc27f 100644 --- a/packages/graphile-build/src/plugins/SubscriptionPlugin.js +++ b/packages/graphile-build/src/plugins/SubscriptionPlugin.ts @@ -1,21 +1,22 @@ -// @flow -import type { Plugin } from "../SchemaBuilder"; +import { Plugin } from "../SchemaBuilder"; function isValidSubscription(Subscription) { try { if (!Subscription) { return false; } + if (Object.keys(Subscription.getFields()).length === 0) { return false; } } catch (e) { return false; } + return true; } -export default (async function SubscriptionPlugin(builder) { +export default async function SubscriptionPlugin(builder) { builder.hook( "GraphQLSchema", (schema: {}, build) => { @@ -38,6 +39,7 @@ export default (async function SubscriptionPlugin(builder) { }, true ); + if (isValidSubscription(Subscription)) { return extend( schema, @@ -54,4 +56,4 @@ export default (async function SubscriptionPlugin(builder) { [], ["Query"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build/src/plugins/SwallowErrorsPlugin.js b/packages/graphile-build/src/plugins/SwallowErrorsPlugin.ts similarity index 83% rename from packages/graphile-build/src/plugins/SwallowErrorsPlugin.js rename to packages/graphile-build/src/plugins/SwallowErrorsPlugin.ts index ec2359f80..623ab6872 100644 --- a/packages/graphile-build/src/plugins/SwallowErrorsPlugin.js +++ b/packages/graphile-build/src/plugins/SwallowErrorsPlugin.ts @@ -1,7 +1,5 @@ -// @flow -import type { Plugin, Build } from "../SchemaBuilder"; - -export default (function SwallowErrorsPlugin( +import { Plugin, Build } from "../SchemaBuilder"; +export default function SwallowErrorsPlugin( builder, { dontSwallowErrors = false } ) { @@ -26,4 +24,4 @@ export default (function SwallowErrorsPlugin( }, ["SwallowErrors"] ); -}: Plugin); +} as Plugin; diff --git a/packages/graphile-build/src/plugins/index.js b/packages/graphile-build/src/plugins/index.ts similarity index 98% rename from packages/graphile-build/src/plugins/index.js rename to packages/graphile-build/src/plugins/index.ts index c5a039834..ca32c8b00 100644 --- a/packages/graphile-build/src/plugins/index.js +++ b/packages/graphile-build/src/plugins/index.ts @@ -1,5 +1,3 @@ -// @flow - import ClientMutationIdDescriptionPlugin from "./ClientMutationIdDescriptionPlugin"; import MutationPayloadQueryPlugin from "./MutationPayloadQueryPlugin"; import MutationPlugin from "./MutationPlugin"; @@ -9,7 +7,6 @@ import QueryPlugin from "./QueryPlugin"; import StandardTypesPlugin from "./StandardTypesPlugin"; import SwallowErrorsPlugin from "./SwallowErrorsPlugin"; import AddQueriesToSubscriptionsPlugin from "./AddQueriesToSubscriptionsPlugin"; - export { ClientMutationIdDescriptionPlugin, MutationPayloadQueryPlugin, diff --git a/packages/graphile-build/src/resolveNode.js b/packages/graphile-build/src/resolveNode.ts similarity index 99% rename from packages/graphile-build/src/resolveNode.js rename to packages/graphile-build/src/resolveNode.ts index 03b50b58a..a9534562c 100644 --- a/packages/graphile-build/src/resolveNode.js +++ b/packages/graphile-build/src/resolveNode.ts @@ -14,14 +14,18 @@ export default async function resolveNode( getTypeAndIdentifiersFromNodeId, graphql: { getNamedType }, } = build; + if (nodeId === "query") { return $$isQuery; } + try { const { Type, identifiers } = getTypeAndIdentifiersFromNodeId(nodeId); + if (!Type) { throw new Error("Type not found"); } + const resolver = nodeFetcherByTypeName[getNamedType(Type).name]; const parsedResolveInfoFragment = parseResolveInfo(resolveInfo, {}, Type); const resolveData = getDataFromParsedResolveInfoFragment( diff --git a/packages/graphile-build/src/swallowError.js b/packages/graphile-build/src/swallowError.ts similarity index 94% rename from packages/graphile-build/src/swallowError.js rename to packages/graphile-build/src/swallowError.ts index 92035e860..67f61831d 100644 --- a/packages/graphile-build/src/swallowError.js +++ b/packages/graphile-build/src/swallowError.ts @@ -1,9 +1,6 @@ -// @flow import debugFactory from "debug"; - const debugWarn = debugFactory("graphile-build:warn"); - -export default function swallowError(e: Error): void { +export default function swallowError(e: Error): undefined { // BE VERY CAREFUL NOT TO THROW! // XXX: Improve this if (debugWarn.enabled) { @@ -18,6 +15,7 @@ export default function swallowError(e: Error): void { .substr(0, 320) .trim() : null; + if (errorSnippet) { // eslint-disable-next-line no-console console.warn( @@ -29,6 +27,7 @@ export default function swallowError(e: Error): void { `Recoverable error occurred; use envvar 'DEBUG="graphile-build:warn"' for error (see: https://graphile.org/postgraphile/debugging )` ); } + debugWarn(e); } } diff --git a/packages/graphile-build/src/utils.js b/packages/graphile-build/src/utils.ts similarity index 99% rename from packages/graphile-build/src/utils.js rename to packages/graphile-build/src/utils.ts index faa874273..cf2e25650 100644 --- a/packages/graphile-build/src/utils.js +++ b/packages/graphile-build/src/utils.ts @@ -1,4 +1,3 @@ -// @flow import upperFirstAll from "lodash/upperFirst"; import camelCaseAll from "lodash/camelCase"; import plz from "pluralize"; @@ -11,7 +10,6 @@ const bindAll = (obj: {}, keys: Array) => { }; export { bindAll }; - export const constantCaseAll = (str: string) => str .replace(/[^a-zA-Z0-9_]+/g, "_") @@ -20,23 +18,22 @@ export const constantCaseAll = (str: string) => .replace(/^[^a-zA-Z0-9]+/, "") .replace(/^[0-9]/, "_$&") // GraphQL enums must not start with a number .toUpperCase(); - export const formatInsideUnderscores = (fn: (input: string) => string) => ( str: string ) => { const matches = str.match(/^(_*)([\s\S]*?)(_*)$/); + if (!matches) { throw new Error("Impossible?"); // Satiate Flow } + const [, start, middle, end] = matches; return `${start}${fn(middle)}${end}`; }; - export const upperFirst = formatInsideUnderscores(upperFirstAll); export const camelCase = formatInsideUnderscores(camelCaseAll); export const constantCase = formatInsideUnderscores(constantCaseAll); export const upperCamelCase = (str: string): string => upperFirst(camelCase(str)); - export const pluralize = (str: string) => plz(str); export const singularize = (str: string) => plz.singular(str); From 379e4cfd5e9fd7c0547dafd4beba8579ed06556f Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 12 Mar 2019 22:28:37 +0000 Subject: [PATCH 3/4] Rest params and some comments --- packages/graphile-build-pg/src/inflections.ts | 8 +++++--- packages/graphile-build-pg/src/plugins/PgBasicsPlugin.ts | 2 +- .../src/plugins/PgIntrospectionPlugin.ts | 8 ++++++++ .../graphile-build-pg/src/plugins/viaTemporaryTable.ts | 1 + packages/graphile-build/src/SchemaBuilder.ts | 5 ++++- packages/graphile-build/src/makeNewBuild.ts | 3 ++- packages/graphile-build/src/plugins/NodePlugin.ts | 5 ++++- 7 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/graphile-build-pg/src/inflections.ts b/packages/graphile-build-pg/src/inflections.ts index 362cb6e99..0258e704a 100644 --- a/packages/graphile-build-pg/src/inflections.ts +++ b/packages/graphile-build-pg/src/inflections.ts @@ -9,7 +9,7 @@ import { import { preventEmptyResult } from "./plugins/PgBasicsPlugin"; const outputMessages = []; // eslint-disable-next-line flowtype/no-weak-types -function deprecate(fn: () => string, message: string) { +function deprecate(fn: (...input: Array) => string, message: string) { if (typeof fn !== "function") { return fn; } @@ -25,7 +25,9 @@ function deprecate(fn: () => string, message: string) { }; } -function deprecateEverything(obj: { [a: string]: () => string }) { +function deprecateEverything(obj: { + [a: string]: (...input: Array) => string; +}) { return Object.keys(obj).reduce((memo, key) => { memo[key] = deprecate( obj[key], @@ -55,7 +57,7 @@ export const defaultUtils: InflectorUtils = { singularize, }; export type Inflector = { - [a: string]: () => string; + [a: string]: (...input: Array) => string; }; export const newInflector = ( overrides: diff --git a/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.ts b/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.ts index fec40154a..29b5bf243 100644 --- a/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.ts +++ b/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.ts @@ -42,7 +42,7 @@ const identity = _ => _; export function preventEmptyResult< O extends { - [key: string]: () => string; + [key: string]: (...args: Array) => string; } >(obj: O): $ObjMap(a: V) => V> { return Object.keys(obj).reduce((memo, key) => { diff --git a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.ts b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.ts index b38bc95a2..66a8ba53a 100644 --- a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.ts +++ b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.ts @@ -172,6 +172,14 @@ export type PgIndex = { indexType: string; isUnique: boolean; isPrimary: boolean; + + /* + Though these exist, we don't want to officially + support them yet. + isImmediate: boolean, + isReplicaIdentity: boolean, + isValid: boolean, + */ attributeNums: Array; attributePropertiesAsc: Array | null | undefined; attributePropertiesNullsFirst: Array | null | undefined; diff --git a/packages/graphile-build-pg/src/plugins/viaTemporaryTable.ts b/packages/graphile-build-pg/src/plugins/viaTemporaryTable.ts index 9e6835f3f..4dd64228f 100644 --- a/packages/graphile-build-pg/src/plugins/viaTemporaryTable.ts +++ b/packages/graphile-build-pg/src/plugins/viaTemporaryTable.ts @@ -41,6 +41,7 @@ export default async function viaTemporaryTable( isPgClassLike: boolean = true, pgRecordInfo: | { + // eslint-disable-next-line flowtype/no-weak-types outputArgTypes: Array; outputArgNames: Array; } diff --git a/packages/graphile-build/src/SchemaBuilder.ts b/packages/graphile-build/src/SchemaBuilder.ts index 969c0c236..8bdfbbbac 100644 --- a/packages/graphile-build/src/SchemaBuilder.ts +++ b/packages/graphile-build/src/SchemaBuilder.ts @@ -36,6 +36,7 @@ export type Build = { graphql: any; parseResolveInfo: parseResolveInfo; simplifyParsedResolveInfoFragmentWithType: simplifyParsedResolveInfoFragmentWithType; + // DEPRECATED: getAliasFromResolveInfo: (resolveInfo: GraphQLResolveInfo) => string, getSafeAliasFromResolveInfo: (resolveInfo: GraphQLResolveInfo) => string; getSafeAliasFromAlias: (alias: string) => string; resolveAlias: ( @@ -64,10 +65,11 @@ export type Build = { performNonEmptyFieldsCheck: boolean ) => T | null | undefined; fieldDataGeneratorsByType: Map; + // @deprecated - use fieldDataGeneratorsByFieldNameByType instead fieldDataGeneratorsByFieldNameByType: Map; fieldArgDataGeneratorsByFieldNameByType: Map; inflection: { - [a: string]: () => string; + [a: string]: (...args: Array) => string; }; swallowError: (e: Error) => undefined; status: { @@ -95,6 +97,7 @@ export type ContextGraphQLObjectTypeFields = { fn: DataGeneratorFunction ) => undefined; recurseDataGeneratorsForField: (fieldName: string) => undefined; + // @deprecated - DO NOT USE! Self: GraphQLNamedType; GraphQLObjectType: GraphQLObjectTypeConfig; fieldWithHooks: FieldWithHooksFunction; diff --git a/packages/graphile-build/src/makeNewBuild.ts b/packages/graphile-build/src/makeNewBuild.ts index 2e8be03ac..18f465fb3 100644 --- a/packages/graphile-build/src/makeNewBuild.ts +++ b/packages/graphile-build/src/makeNewBuild.ts @@ -106,7 +106,8 @@ type MetaData = { }; type DataGeneratorFunction = ( parsedResolveInfoFragment: ResolveTree, - ReturnType: GraphQLType + ReturnType: GraphQLType, + ...args: Array ) => Array; type FieldSpecIsh = { type?: GraphQLType; diff --git a/packages/graphile-build/src/plugins/NodePlugin.ts b/packages/graphile-build/src/plugins/NodePlugin.ts index 1d0feb258..edf2d6c9f 100644 --- a/packages/graphile-build/src/plugins/NodePlugin.ts +++ b/packages/graphile-build/src/plugins/NodePlugin.ts @@ -28,7 +28,10 @@ export type BuildExtensionNode = { nodeFetcherByTypeName: { [a: string]: NodeFetcher; }; - getNodeIdForTypeAndIdentifiers: (Type: GraphQLType) => string; + getNodeIdForTypeAndIdentifiers: ( + Type: GraphQLType, + ...identifiers: Array + ) => string; getTypeAndIdentifiersFromNodeId: ( nodeId: string ) => { From fcc7d1e450b167f79fbd28c8b2f2c3bba5f0caed Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 12 Mar 2019 22:50:35 +0000 Subject: [PATCH 4/4] Copy more comments across --- packages/graphile-build-pg/src/QueryBuilder.ts | 1 + packages/graphile-build-pg/src/inflections.ts | 3 +++ packages/graphile-build/src/SchemaBuilder.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/packages/graphile-build-pg/src/QueryBuilder.ts b/packages/graphile-build-pg/src/QueryBuilder.ts index 4426f9790..2ac4ccbca 100644 --- a/packages/graphile-build-pg/src/QueryBuilder.ts +++ b/packages/graphile-build-pg/src/QueryBuilder.ts @@ -71,6 +71,7 @@ class QueryBuilder { }; cursorComparator: CursorComparator | null | undefined; liveConditions: Array< + // eslint-disable-next-line flowtype/no-weak-types [ (data: {}) => (record: any) => boolean, diff --git a/packages/graphile-build-pg/src/inflections.ts b/packages/graphile-build-pg/src/inflections.ts index 0258e704a..fc2f19022 100644 --- a/packages/graphile-build-pg/src/inflections.ts +++ b/packages/graphile-build-pg/src/inflections.ts @@ -26,6 +26,7 @@ function deprecate(fn: (...input: Array) => string, message: string) { } function deprecateEverything(obj: { + // eslint-disable-next-line flowtype/no-weak-types [a: string]: (...input: Array) => string; }) { return Object.keys(obj).reduce((memo, key) => { @@ -57,6 +58,8 @@ export const defaultUtils: InflectorUtils = { singularize, }; export type Inflector = { + // TODO: tighten this up! + // eslint-disable-next-line flowtype/no-weak-types [a: string]: (...input: Array) => string; }; export const newInflector = ( diff --git a/packages/graphile-build/src/SchemaBuilder.ts b/packages/graphile-build/src/SchemaBuilder.ts index 8bdfbbbac..0c3e6199d 100644 --- a/packages/graphile-build/src/SchemaBuilder.ts +++ b/packages/graphile-build/src/SchemaBuilder.ts @@ -69,6 +69,7 @@ export type Build = { fieldDataGeneratorsByFieldNameByType: Map; fieldArgDataGeneratorsByFieldNameByType: Map; inflection: { + // eslint-disable-next-line flowtype/no-weak-types [a: string]: (...args: Array) => string; }; swallowError: (e: Error) => undefined;