diff --git a/packages/graphile-build-pg/res/introspection-query.sql b/packages/graphile-build-pg/res/introspection-query.sql index 176c0e579..c4af70b3f 100644 --- a/packages/graphile-build-pg/res/introspection-query.sql +++ b/packages/graphile-build-pg/res/introspection-query.sql @@ -41,6 +41,7 @@ with pro.proname as "name", dsc.description as "description", pro.pronamespace as "namespaceId", + nsp.nspname as "namespaceName", pro.proisstrict as "isStrict", pro.proretset as "returnsSet", case @@ -56,6 +57,7 @@ with from pg_catalog.pg_proc as pro left join pg_catalog.pg_description as dsc on dsc.objoid = pro.oid + left join pg_catalog.pg_namespace as nsp on nsp.oid = pro.pronamespace where pro.pronamespace in (select "id" from namespace) and -- Currently we don’t support functions with variadic arguments. In the diff --git a/packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.js b/packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.js index 266c32d95..bad7a9cef 100644 --- a/packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgBackwardRelationPlugin.js @@ -35,6 +35,8 @@ export default (function PgBackwardRelationPlugin( pgQueryFromResolveData: queryFromResolveData, pgAddStartEndCursor: addStartEndCursor, pgOmit: omit, + sqlCommentByAddingTags, + describePgEntity, } = build; const { scope: { isPgRowType, pgIntrospection: foreignTable }, @@ -128,7 +130,7 @@ export default (function PgBackwardRelationPlugin( const isDeprecated = isUnique && legacyRelationMode === DEPRECATED; const singleRelationFieldName = isUnique - ? inflection.singleRelationByKeys( + ? inflection.singleRelationByKeysBackwards( keys, table, foreignTable, @@ -152,63 +154,85 @@ export default (function PgBackwardRelationPlugin( legacyRelationMode === DEPRECATED || legacyRelationMode === ONLY; - if (shouldAddSingleRelation && !omit(table, "read")) { - memo[singleRelationFieldName] = fieldWithHooks( - singleRelationFieldName, - ({ getDataFromParsedResolveInfoFragment, addDataGenerator }) => { - addDataGenerator(parsedResolveInfoFragment => { - return { - pgQuery: queryBuilder => { - queryBuilder.select(() => { - const resolveData = getDataFromParsedResolveInfoFragment( - parsedResolveInfoFragment, - gqlTableType - ); - const tableAlias = sql.identifier(Symbol()); - const foreignTableAlias = queryBuilder.getTableAlias(); - const query = queryFromResolveData( - sql.identifier(schema.name, table.name), - tableAlias, - resolveData, - { - asJson: true, - addNullCase: true, - withPagination: false, - }, - innerQueryBuilder => { - innerQueryBuilder.parentQueryBuilder = queryBuilder; - keys.forEach((key, i) => { - innerQueryBuilder.where( - sql.fragment`${tableAlias}.${sql.identifier( - key.name - )} = ${foreignTableAlias}.${sql.identifier( - foreignKeys[i].name - )}` - ); - }); - } + if ( + shouldAddSingleRelation && + !omit(table, "read") && + singleRelationFieldName + ) { + memo = extend( + memo, + { + [singleRelationFieldName]: fieldWithHooks( + singleRelationFieldName, + ({ + getDataFromParsedResolveInfoFragment, + addDataGenerator, + }) => { + addDataGenerator(parsedResolveInfoFragment => { + return { + pgQuery: queryBuilder => { + queryBuilder.select(() => { + const resolveData = getDataFromParsedResolveInfoFragment( + parsedResolveInfoFragment, + gqlTableType + ); + const tableAlias = sql.identifier(Symbol()); + const foreignTableAlias = queryBuilder.getTableAlias(); + const query = queryFromResolveData( + sql.identifier(schema.name, table.name), + tableAlias, + resolveData, + { + asJson: true, + addNullCase: true, + withPagination: false, + }, + innerQueryBuilder => { + innerQueryBuilder.parentQueryBuilder = queryBuilder; + keys.forEach((key, i) => { + innerQueryBuilder.where( + sql.fragment`${tableAlias}.${sql.identifier( + key.name + )} = ${foreignTableAlias}.${sql.identifier( + foreignKeys[i].name + )}` + ); + }); + } + ); + return sql.fragment`(${query})`; + }, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); + }, + }; + }); + return { + description: + constraint.tags.backwardDescription || + `Reads a single \`${tableTypeName}\` that is related to this \`${foreignTableTypeName}\`.`, + type: gqlTableType, + args: {}, + resolve: (data, _args, _context, resolveInfo) => { + const safeAlias = getSafeAliasFromResolveInfo( + resolveInfo ); - return sql.fragment`(${query})`; - }, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); - }, - }; - }); - return { - description: - constraint.tags.backwardDescription || - `Reads a single \`${tableTypeName}\` that is related to this \`${foreignTableTypeName}\`.`, - type: gqlTableType, - args: {}, - resolve: (data, _args, _context, resolveInfo) => { - const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); - return data[safeAlias]; + return data[safeAlias]; + }, + }; }, - }; + { + pgFieldIntrospection: table, + isPgBackwardSingleRelationField: true, + } + ), }, - { - pgFieldIntrospection: table, - isPgBackwardSingleRelationField: true, - } + `Backward relation (single) for ${describePgEntity( + constraint + )}. To rename this relation with smart comments:\n\n ${sqlCommentByAddingTags( + constraint, + { + foreignSingleFieldName: "newNameHere", + } + )}` ); } function makeFields(isConnection) { @@ -231,103 +255,131 @@ export default (function PgBackwardRelationPlugin( constraint ); - memo[manyRelationFieldName] = fieldWithHooks( - manyRelationFieldName, - ({ getDataFromParsedResolveInfoFragment, addDataGenerator }) => { - addDataGenerator(parsedResolveInfoFragment => { - return { - pgQuery: queryBuilder => { - queryBuilder.select(() => { - const resolveData = getDataFromParsedResolveInfoFragment( - parsedResolveInfoFragment, - isConnection ? ConnectionType : TableType - ); - const tableAlias = sql.identifier(Symbol()); - const foreignTableAlias = queryBuilder.getTableAlias(); - const query = queryFromResolveData( - sql.identifier(schema.name, table.name), - tableAlias, - resolveData, - { - withPagination: isConnection, - withPaginationAsFields: false, - asJsonAggregate: !isConnection, - }, - innerQueryBuilder => { - innerQueryBuilder.parentQueryBuilder = queryBuilder; - if (primaryKeys) { - innerQueryBuilder.beforeLock("orderBy", () => { - // append order by primary key to the list of orders - if (!innerQueryBuilder.isOrderUnique(false)) { - innerQueryBuilder.data.cursorPrefix = [ - "primary_key_asc", - ]; - primaryKeys.forEach(key => { - innerQueryBuilder.orderBy( - sql.fragment`${innerQueryBuilder.getTableAlias()}.${sql.identifier( - key.name - )}`, - true - ); - }); - innerQueryBuilder.setOrderIsUnique(); + memo = extend( + memo, + { + [manyRelationFieldName]: fieldWithHooks( + manyRelationFieldName, + ({ + getDataFromParsedResolveInfoFragment, + addDataGenerator, + }) => { + addDataGenerator(parsedResolveInfoFragment => { + return { + pgQuery: queryBuilder => { + queryBuilder.select(() => { + const resolveData = getDataFromParsedResolveInfoFragment( + parsedResolveInfoFragment, + isConnection ? ConnectionType : TableType + ); + const tableAlias = sql.identifier(Symbol()); + const foreignTableAlias = queryBuilder.getTableAlias(); + const query = queryFromResolveData( + sql.identifier(schema.name, table.name), + tableAlias, + resolveData, + { + withPagination: isConnection, + withPaginationAsFields: false, + asJsonAggregate: !isConnection, + }, + innerQueryBuilder => { + innerQueryBuilder.parentQueryBuilder = queryBuilder; + if (primaryKeys) { + innerQueryBuilder.beforeLock( + "orderBy", + () => { + // append order by primary key to the list of orders + if ( + !innerQueryBuilder.isOrderUnique(false) + ) { + innerQueryBuilder.data.cursorPrefix = [ + "primary_key_asc", + ]; + primaryKeys.forEach(key => { + innerQueryBuilder.orderBy( + sql.fragment`${innerQueryBuilder.getTableAlias()}.${sql.identifier( + key.name + )}`, + true + ); + }); + innerQueryBuilder.setOrderIsUnique(); + } + } + ); } - }); - } - keys.forEach((key, i) => { - innerQueryBuilder.where( - sql.fragment`${tableAlias}.${sql.identifier( - key.name - )} = ${foreignTableAlias}.${sql.identifier( - foreignKeys[i].name - )}` - ); - }); - } + keys.forEach((key, i) => { + innerQueryBuilder.where( + sql.fragment`${tableAlias}.${sql.identifier( + key.name + )} = ${foreignTableAlias}.${sql.identifier( + foreignKeys[i].name + )}` + ); + }); + } + ); + return sql.fragment`(${query})`; + }, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); + }, + }; + }); + const ConnectionType = getTypeByName( + inflection.connection(gqlTableType.name) + ); + const TableType = pgGetGqlTypeByTypeIdAndModifier( + table.type.id, + null + ); + return { + description: + constraint.tags.backwardDescription || + `Reads and enables pagination through a set of \`${tableTypeName}\`.`, + type: isConnection + ? new GraphQLNonNull(ConnectionType) + : new GraphQLNonNull( + new GraphQLList(new GraphQLNonNull(TableType)) + ), + args: {}, + resolve: (data, _args, _context, resolveInfo) => { + const safeAlias = getSafeAliasFromResolveInfo( + resolveInfo ); - return sql.fragment`(${query})`; - }, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); - }, - }; - }); - const ConnectionType = getTypeByName( - inflection.connection(gqlTableType.name) - ); - const TableType = pgGetGqlTypeByTypeIdAndModifier( - table.type.id, - null - ); - return { - description: - constraint.tags.backwardDescription || - `Reads and enables pagination through a set of \`${tableTypeName}\`.`, - type: isConnection - ? new GraphQLNonNull(ConnectionType) - : new GraphQLNonNull( - new GraphQLList(new GraphQLNonNull(TableType)) - ), - args: {}, - resolve: (data, _args, _context, resolveInfo) => { - const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); - if (isConnection) { - return addStartEndCursor(data[safeAlias]); - } else { - return data[safeAlias]; - } + if (isConnection) { + return addStartEndCursor(data[safeAlias]); + } else { + return data[safeAlias]; + } + }, + deprecationReason: isDeprecated + ? // $FlowFixMe + `Please use ${singleRelationFieldName} instead` + : undefined, + }; }, - deprecationReason: isDeprecated - ? // $FlowFixMe - `Please use ${singleRelationFieldName} instead` - : undefined, - }; + { + isPgFieldConnection: isConnection, + isPgFieldSimpleCollection: !isConnection, + isPgBackwardRelationField: true, + pgFieldIntrospection: table, + } + ), }, - { - isPgFieldConnection: isConnection, - isPgFieldSimpleCollection: !isConnection, - isPgBackwardRelationField: true, - pgFieldIntrospection: table, - } + + `Backward relation (${ + isConnection ? "connection" : "simple collection" + }) for ${describePgEntity( + constraint + )}. To rename this relation with smart comments:\n\n ${sqlCommentByAddingTags( + constraint, + { + [isConnection + ? "foreignFieldName" + : "foreignSimpleFieldName"]: "newNameHere", + } + )}` ); } } diff --git a/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.js b/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.js index 0e75d3f3f..6f30d9225 100644 --- a/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.js @@ -26,6 +26,8 @@ import omit, { import makeProcField from "./makeProcField"; import parseIdentifier from "../parseIdentifier"; import viaTemporaryTable from "./viaTemporaryTable"; +import chalk from "chalk"; +import pickBy from "lodash/pickBy"; const defaultPgColumnFilter = (_attr, _build, _context) => true; type Keys = Array<{ @@ -34,6 +36,8 @@ type Keys = Array<{ schema: ?string, }>; +const identity = _ => _; + export function preventEmptyResult< // eslint-disable-next-line flowtype/no-weak-types O: { [key: string]: (...args: Array) => string } @@ -130,6 +134,62 @@ function omitWithRBACChecks( return omit(entity, permission); } +function describePgEntity(entity, includeAlias = true) { + const getAlias = !includeAlias + ? () => "" + : () => { + const tags = pickBy( + entity.tags, + (value, key) => key === "name" || key.endsWith("Name") + ); + if (Object.keys(tags).length) { + return ` (with smart comments: ${chalk.bold( + Object.keys(tags) + .map(t => `@${t} ${tags[t]}`) + .join(" | ") + )})`; + } + return ""; + }; + + try { + if (entity.kind === "constraint") { + return `constraint ${chalk.bold( + `"${entity.name}"` + )} on ${describePgEntity(entity.class, false)}${getAlias()}`; + } else if (entity.kind === "class") { + // see pg_class.relkind https://www.postgresql.org/docs/10/static/catalog-pg-class.html + const kind = + { + c: "composite type", + f: "foreign table", + p: "partitioned table", + r: "table", + v: "view", + m: "materialized view", + }[entity.classKind] || "table-like"; + return `${kind} ${chalk.bold( + `"${entity.namespaceName}"."${entity.name}"` + )}${getAlias()}`; + } else if (entity.kind === "procedure") { + return `function ${chalk.bold( + `"${entity.namespaceName}"."${entity.name}"(...args...)` + )}${getAlias()}`; + } else if (entity.kind === "attribute") { + return `column ${chalk.bold(`"${entity.name}"`)} on ${describePgEntity( + entity.class, + false + )}${getAlias()}`; + } + } 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(e); + } + return `entity of kind '${entity.kind}' with oid '${entity.oid}'`; +} + export default (function PgBasicsPlugin( builder, { @@ -151,6 +211,81 @@ export default (function PgBasicsPlugin( pgMakeProcField: makeProcField, pgParseIdentifier: parseIdentifier, pgViaTemporaryTable: viaTemporaryTable, + describePgEntity, + 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( + /['\\\b\f\n\r\t]/g, + chr => + ({ + "\b": "\\b", + "\f": "\\f", + "\n": "\\n", + "\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. + const tags = Object.assign({}, tagsToAdd, entity.tags, tagsToAdd); + + const description = entity.description; + const tagsSql = Object.keys(tags) + .reduce((memo, tag) => { + const tagValue = tags[tag]; + const valueArray = Array.isArray(tagValue) ? tagValue : [tagValue]; + const highlightOrNot = tag in tagsToAdd ? chalk.bold : identity; + valueArray.forEach(value => { + memo.push( + highlightOrNot( + `@${escape(escape(tag))}${ + value === true ? "" : " " + escape(escape(value)) + }` + ) + ); + }); + return memo; + }, []) + .join("\\n"); + const commentValue = `E'${tagsSql}${ + 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") { + sqlThing = `VIEW ${identifier}`; + } else if (entity.classKind === "m") { + sqlThing = `MATERIALIZED VIEW ${identifier}`; + } else { + sqlThing = `PLEASE_SEND_A_PULL_REQUEST_TO_FIX_THIS ${identifier}`; + } + } else if (entity.kind === "attribute") { + sqlThing = `COLUMN "${entity.class.namespaceName}"."${ + entity.class.name + }"."${entity.name}"`; + } else if (entity.kind === "procedure") { + sqlThing = `FUNCTION "${entity.namespaceName}"."${ + entity.name + }"(...arg types go here...)`; + } else if (entity.kind === "constraint") { + // TODO: TEST! + sqlThing = `CONSTRAINT "${entity.name}" ON "${ + entity.class.namespaceName + }"."${entity.class.name}"`; + } else { + sqlThing = `UNKNOWN_ENTITY_PLEASE_SEND_A_PULL_REQUEST`; + } + + return `COMMENT ON ${sqlThing} IS ${commentValue};`; + }, }); }); @@ -384,6 +519,25 @@ export default (function PgBasicsPlugin( .join("-and-")}` ); }, + singleRelationByKeysBackwards( + detailedKeys: Keys, + table: PgClass, + _foreignTable: PgClass, + constraint: PgConstraint + ) { + if (constraint.tags.foreignSingleFieldName) { + return constraint.tags.foreignSingleFieldName; + } + if (constraint.tags.foreignFieldName) { + return constraint.tags.foreignFieldName; + } + return this.singleRelationByKeys( + detailedKeys, + table, + _foreignTable, + constraint + ); + }, manyRelationByKeys( detailedKeys: Keys, table: PgClass, @@ -405,6 +559,9 @@ export default (function PgBasicsPlugin( _foreignTable: PgClass, constraint: PgConstraint ) { + if (constraint.tags.foreignSimpleFieldName) { + return constraint.tags.foreignSimpleFieldName; + } if (constraint.tags.foreignFieldName) { return constraint.tags.foreignFieldName; } diff --git a/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js b/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js index d5d9cf7ef..e4f5cafd8 100644 --- a/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgColumnsPlugin.js @@ -81,11 +81,12 @@ export default (function PgColumnsPlugin(builder) { inflection, pgOmit: omit, pgGetSelectValueForFieldAndTypeAndModifier: getSelectValueForFieldAndTypeAndModifier, + describePgEntity, + sqlCommentByAddingTags, } = build; const { scope: { isPgRowType, isPgCompoundType, pgIntrospection: table }, fieldWithHooks, - Self, } = context; if ( !(isPgRowType || isPgCompoundType) || @@ -120,51 +121,64 @@ export default (function PgColumnsPlugin(builder) { }.${table.name}'; one of them is '${attr.name}'` ); } - memo[fieldName] = fieldWithHooks( - fieldName, - fieldContext => { - const { addDataGenerator } = fieldContext; - const ReturnType = - pgGetGqlTypeByTypeIdAndModifier( - attr.typeId, - attr.typeModifier - ) || GraphQLString; - addDataGenerator(parsedResolveInfoFragment => { - return { - pgQuery: queryBuilder => { - queryBuilder.select( - getSelectValueForFieldAndTypeAndModifier( - ReturnType, - fieldContext, - parsedResolveInfoFragment, - sql.fragment`(${queryBuilder.getTableAlias()}.${sql.identifier( - attr.name - )})`, // The brackets are necessary to stop the parser getting confused, ref: https://www.postgresql.org/docs/9.6/static/rowtypes.html#ROWTYPES-ACCESSING - attr.type, - attr.typeModifier - ), - fieldName - ); - }, - }; - }); - return { - description: attr.description, - type: nullableIf( - GraphQLNonNull, - !attr.isNotNull && !attr.type.domainIsNotNull, - ReturnType - ), - resolve: (data, _args, _context, _resolveInfo) => { - return pg2gql(data[fieldName], attr.type); + memo = extend( + memo, + { + [fieldName]: fieldWithHooks( + fieldName, + fieldContext => { + const { addDataGenerator } = fieldContext; + const ReturnType = + pgGetGqlTypeByTypeIdAndModifier( + attr.typeId, + attr.typeModifier + ) || GraphQLString; + addDataGenerator(parsedResolveInfoFragment => { + return { + pgQuery: queryBuilder => { + queryBuilder.select( + getSelectValueForFieldAndTypeAndModifier( + ReturnType, + fieldContext, + parsedResolveInfoFragment, + sql.fragment`(${queryBuilder.getTableAlias()}.${sql.identifier( + attr.name + )})`, // The brackets are necessary to stop the parser getting confused, ref: https://www.postgresql.org/docs/9.6/static/rowtypes.html#ROWTYPES-ACCESSING + attr.type, + attr.typeModifier + ), + fieldName + ); + }, + }; + }); + return { + description: attr.description, + type: nullableIf( + GraphQLNonNull, + !attr.isNotNull && !attr.type.domainIsNotNull, + ReturnType + ), + resolve: (data, _args, _context, _resolveInfo) => { + return pg2gql(data[fieldName], attr.type); + }, + }; }, - }; + { pgFieldIntrospection: attr } + ), }, - { pgFieldIntrospection: attr } + `Adding field for ${describePgEntity( + attr + )}. You can rename this field with:\n\n ${sqlCommentByAddingTags( + attr, + { + name: "newNameHere", + } + )}` ); return memo; }, {}), - `Adding columns to '${Self.name}'` + `Adding columns to '${describePgEntity(table)}'` ); }); builder.hook("GraphQLInputObjectType:fields", (fields, build, context) => { @@ -176,6 +190,8 @@ export default (function PgColumnsPlugin(builder) { pgColumnFilter, inflection, pgOmit: omit, + describePgEntity, + sqlCommentByAddingTags, } = build; const { scope: { @@ -187,7 +203,6 @@ export default (function PgColumnsPlugin(builder) { pgAddSubfield, }, fieldWithHooks, - Self, } = context; if ( !(isPgRowType || isPgCompoundType) || @@ -217,33 +232,46 @@ export default (function PgColumnsPlugin(builder) { }.${table.name}'; one of them is '${attr.name}'` ); } - memo[fieldName] = fieldWithHooks( - fieldName, - pgAddSubfield( - fieldName, - attr.name, - attr.type, - { - description: attr.description, - type: nullableIf( - GraphQLNonNull, - isPgBaseInput || - isPgPatch || - (!attr.isNotNull && !attr.type.domainIsNotNull) || - attr.hasDefault, - pgGetGqlInputTypeByTypeIdAndModifier( - attr.typeId, - attr.typeModifier - ) || GraphQLString + memo = extend( + memo, + { + [fieldName]: fieldWithHooks( + fieldName, + pgAddSubfield( + fieldName, + attr.name, + attr.type, + { + description: attr.description, + type: nullableIf( + GraphQLNonNull, + isPgBaseInput || + isPgPatch || + (!attr.isNotNull && !attr.type.domainIsNotNull) || + attr.hasDefault, + pgGetGqlInputTypeByTypeIdAndModifier( + attr.typeId, + attr.typeModifier + ) || GraphQLString + ), + }, + attr.typeModifier ), - }, - attr.typeModifier - ), - { pgFieldIntrospection: attr } + { pgFieldIntrospection: attr } + ), + }, + `Adding input object field for ${describePgEntity( + attr + )}. You can rename this field with:\n\n ${sqlCommentByAddingTags( + attr, + { + name: "newNameHere", + } + )}` ); return memo; }, {}), - `Adding columns to input object '${Self.name}'` + `Adding columns to input object for ${describePgEntity(table)}` ); }); }: Plugin); diff --git a/packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.js b/packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.js index 72723ab70..43ae758d8 100644 --- a/packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgComputedColumnsPlugin.js @@ -35,6 +35,8 @@ export default (function PgComputedColumnsPlugin( pgOmit: omit, pgMakeProcField: makeProcField, swallowError, + describePgEntity, + sqlCommentByAddingTags, } = build; const tableType = introspectionResultsByKind.type.filter( type => @@ -90,11 +92,24 @@ export default (function PgComputedColumnsPlugin( ? inflection.computedColumnList(pseudoColumnName, proc, table) : inflection.computedColumn(pseudoColumnName, proc, table); try { - memo[fieldName] = makeProcField(fieldName, proc, build, { - fieldWithHooks, - computed: true, - forceList, - }); + memo = extend( + memo, + { + [fieldName]: makeProcField(fieldName, proc, build, { + fieldWithHooks, + computed: true, + forceList, + }), + }, + `Adding computed column for ${describePgEntity( + proc + )}. You can rename this field with:\n\n ${sqlCommentByAddingTags( + proc, + { + fieldName: "newNameHere", + } + )}` + ); } catch (e) { swallowError(e); } diff --git a/packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.js b/packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.js index 68067a3a1..ca53f8400 100644 --- a/packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.js +++ b/packages/graphile-build-pg/src/plugins/PgConnectionArgCondition.js @@ -11,6 +11,8 @@ export default (function PgConnectionArgCondition(builder) { pgColumnFilter, inflection, pgOmit: omit, + describePgEntity, + sqlCommentByAddingTags, } = build; introspectionResultsByKind.class .filter(table => table.isSelectable && !omit(table, "filter")) @@ -31,25 +33,39 @@ export default (function PgConnectionArgCondition(builder) { .filter(attr => !omit(attr, "filter")) .reduce((memo, attr) => { const fieldName = inflection.column(attr); - memo[fieldName] = fieldWithHooks( - fieldName, + memo = build.extend( + memo, { - description: `Checks for equality with the object’s \`${fieldName}\` field.`, - type: - pgGetGqlInputTypeByTypeIdAndModifier( - attr.typeId, - attr.typeModifier - ) || GraphQLString, + [fieldName]: fieldWithHooks( + fieldName, + { + description: `Checks for equality with the object’s \`${fieldName}\` field.`, + type: + pgGetGqlInputTypeByTypeIdAndModifier( + attr.typeId, + attr.typeModifier + ) || GraphQLString, + }, + { + isPgConnectionConditionInputField: true, + } + ), }, - { - isPgConnectionConditionInputField: true, - } + `Adding condition argument for ${describePgEntity(attr)}` ); return memo; }, {}); }, }, { + __origin: `Adding condition type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, pgIntrospection: table, isPgCondition: true, } diff --git a/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.js b/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.js index 4bb0f2065..9efde12dd 100644 --- a/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.js +++ b/packages/graphile-build-pg/src/plugins/PgConnectionArgOrderBy.js @@ -10,6 +10,8 @@ export default (function PgConnectionArgOrderBy(builder) { graphql: { GraphQLEnumType }, inflection, pgOmit: omit, + sqlCommentByAddingTags, + describePgEntity, } = build; introspectionResultsByKind.class .filter(table => table.isSelectable && !omit(table, "order")) @@ -32,6 +34,14 @@ export default (function PgConnectionArgOrderBy(builder) { }, }, { + __origin: `Adding connection "orderBy" argument for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, pgIntrospection: table, isPgRowSortEnum: true, } @@ -133,7 +143,7 @@ export default (function PgConnectionArgOrderBy(builder) { type: new GraphQLList(new GraphQLNonNull(TableOrderByType)), }, }, - `Adding 'orderBy' to field '${field.name}' of '${Self.name}'` + `Adding 'orderBy' argument to field '${field.name}' of '${Self.name}'` ); } ); diff --git a/packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.js b/packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.js index e3857d2de..1ccece29b 100644 --- a/packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgForwardRelationPlugin.js @@ -16,6 +16,7 @@ export default (function PgForwardRelationPlugin(builder) { inflection, pgQueryFromResolveData: queryFromResolveData, pgOmit: omit, + describePgEntity, } = build; const { scope: { @@ -115,57 +116,63 @@ export default (function PgForwardRelationPlugin(builder) { constraint ); - memo[fieldName] = fieldWithHooks( - fieldName, - ({ getDataFromParsedResolveInfoFragment, addDataGenerator }) => { - addDataGenerator(parsedResolveInfoFragment => { - return { - pgQuery: queryBuilder => { - queryBuilder.select(() => { - const resolveData = getDataFromParsedResolveInfoFragment( - parsedResolveInfoFragment, - gqlForeignTableType - ); - const foreignTableAlias = sql.identifier(Symbol()); - const query = queryFromResolveData( - sql.identifier(foreignSchema.name, foreignTable.name), - foreignTableAlias, - resolveData, - { asJson: true }, - innerQueryBuilder => { - innerQueryBuilder.parentQueryBuilder = queryBuilder; - keys.forEach((key, i) => { - innerQueryBuilder.where( - sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( - key.name - )} = ${foreignTableAlias}.${sql.identifier( - foreignKeys[i].name - )}` - ); - }); - } - ); - return sql.fragment`(${query})`; - }, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); - }, - }; - }); - return { - description: - constraint.tags.forwardDescription || - `Reads a single \`${foreignTableTypeName}\` that is related to this \`${tableTypeName}\`.`, - type: gqlForeignTableType, // Nullable since RLS may forbid fetching - resolve: (rawData, _args, _context, resolveInfo) => { - const data = isMutationPayload ? rawData.data : rawData; - const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); - return data[safeAlias]; + memo = extend( + memo, + { + [fieldName]: fieldWithHooks( + fieldName, + ({ getDataFromParsedResolveInfoFragment, addDataGenerator }) => { + addDataGenerator(parsedResolveInfoFragment => { + return { + pgQuery: queryBuilder => { + queryBuilder.select(() => { + const resolveData = getDataFromParsedResolveInfoFragment( + parsedResolveInfoFragment, + gqlForeignTableType + ); + const foreignTableAlias = sql.identifier(Symbol()); + const query = queryFromResolveData( + sql.identifier(foreignSchema.name, foreignTable.name), + foreignTableAlias, + resolveData, + { asJson: true }, + innerQueryBuilder => { + innerQueryBuilder.parentQueryBuilder = queryBuilder; + keys.forEach((key, i) => { + innerQueryBuilder.where( + sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( + key.name + )} = ${foreignTableAlias}.${sql.identifier( + foreignKeys[i].name + )}` + ); + }); + } + ); + return sql.fragment`(${query})`; + }, getSafeAliasFromAlias(parsedResolveInfoFragment.alias)); + }, + }; + }); + return { + description: + constraint.tags.forwardDescription || + `Reads a single \`${foreignTableTypeName}\` that is related to this \`${tableTypeName}\`.`, + type: gqlForeignTableType, // Nullable since RLS may forbid fetching + resolve: (rawData, _args, _context, resolveInfo) => { + const data = isMutationPayload ? rawData.data : rawData; + const safeAlias = getSafeAliasFromResolveInfo(resolveInfo); + return data[safeAlias]; + }, + }; }, - }; + { + pgFieldIntrospection: constraint, + isPgForwardRelationField: true, + } + ), }, - { - pgFieldIntrospection: constraint, - isPgForwardRelationField: true, - } + `Adding forward relation for ${describePgEntity(constraint)}` ); return memo; }, {}), diff --git a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.d.ts b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.d.ts index 228578185..219a45e18 100644 --- a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.d.ts +++ b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.d.ts @@ -2,6 +2,7 @@ export interface PgNamespace { kind: "namespace"; id: string; name: string; + comment: string | void; description: string | void; tags: { [tag: string]: string }; } @@ -9,8 +10,10 @@ export interface PgNamespace { export interface PgProc { kind: "procedure"; name: string; + comment: string | void; description: string | void; namespaceId: string; + namespaceName: string; isStrict: boolean; returnsSet: boolean; isStable: boolean; @@ -27,6 +30,7 @@ export interface PgClass { kind: "class"; id: string; name: string; + comment: string | void; description: string | void; classKind: string; namespaceId: string; @@ -51,6 +55,7 @@ export interface PgType { kind: "type"; id: string; name: string; + comment: string | void; description: string | void; namespaceId: string; namespaceName: string; @@ -71,6 +76,7 @@ export interface PgAttribute { classId: string; num: number; name: string; + comment: string | void; description: string | void; typeId: string; typeModifier: number; @@ -90,7 +96,9 @@ export interface PgConstraint { name: string; type: string; classId: string; + class: PgClass | void; foreignClassId: string | void; + comment: string | void; description: string | void; keyAttributeNums: Array; foreignKeyAttributeNums: Array; @@ -106,6 +114,7 @@ export interface PgExtension { relocatable: boolean; version: string; configurationClassIds?: Array; + comment: string | void; description: string | void; tags: { [tag: string]: string | Array }; } diff --git a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js index 6c45fa392..8c68f13b5 100644 --- a/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgIntrospectionPlugin.js @@ -22,6 +22,7 @@ export type PgNamespace = { kind: "namespace", id: string, name: string, + comment: ?string, description: ?string, tags: { [string]: string }, }; @@ -29,8 +30,10 @@ export type PgNamespace = { export type PgProc = { kind: "procedure", name: string, + comment: ?string, description: ?string, namespaceId: string, + namespaceName: string, isStrict: boolean, returnsSet: boolean, isStable: boolean, @@ -47,6 +50,7 @@ export type PgClass = { kind: "class", id: string, name: string, + comment: ?string, description: ?string, classKind: string, namespaceId: string, @@ -71,6 +75,7 @@ export type PgType = { kind: "type", id: string, name: string, + comment: ?string, description: ?string, namespaceId: string, namespaceName: string, @@ -91,6 +96,7 @@ export type PgAttribute = { classId: string, num: number, name: string, + comment: ?string, description: ?string, typeId: string, typeModifier: number, @@ -110,7 +116,9 @@ export type PgConstraint = { name: string, type: string, classId: string, + class: ?PgClass, foreignClassId: ?string, + comment: ?string, description: ?string, keyAttributeNums: Array, foreignKeyAttributeNums: Array, @@ -126,6 +134,7 @@ export type PgExtension = { relocatable: boolean, version: string, configurationClassIds?: Array, + comment: ?string, description: ?string, tags: { [string]: string }, }; @@ -199,6 +208,8 @@ export default (async function PgIntrospectionPlugin( "extension", ].forEach(kind => { 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; @@ -371,6 +382,14 @@ export default (async function PgIntrospectionPlugin( true // Because not all types are arrays ); + relate( + introspectionResultsByKind.constraint, + "class", + "classId", + introspectionResultsByKind.classById, + true + ); + relate( introspectionResultsByKind.extension, "namespace", diff --git a/packages/graphile-build-pg/src/plugins/PgJWTPlugin.js b/packages/graphile-build-pg/src/plugins/PgJWTPlugin.js index 9c797d78b..f1b6ca063 100644 --- a/packages/graphile-build-pg/src/plugins/PgJWTPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgJWTPlugin.js @@ -17,6 +17,7 @@ export default (function PgJWTPlugin( graphql: { GraphQLScalarType }, inflection, pgParseIdentifier: parseIdentifier, + describePgEntity, } = build; if (!pgJwtTypeIdentifier) { return _; @@ -100,6 +101,9 @@ export default (function PgJWTPlugin( }, }, { + __origin: `Adding JWT type based on ${describePgEntity( + compositeType + )}`, isPgJwtType: true, } ); diff --git a/packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.js b/packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.js index 515a912cb..350de9390 100644 --- a/packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgMutationCreatePlugin.js @@ -32,6 +32,8 @@ export default (function PgMutationCreatePlugin( pgQueryFromResolveData: queryFromResolveData, pgOmit: omit, pgViaTemporaryTable: viaTemporaryTable, + describePgEntity, + sqlCommentByAddingTags, } = build; const { scope: { isRootMutation }, @@ -91,6 +93,14 @@ export default (function PgMutationCreatePlugin( }, }, { + __origin: `Adding table create input type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, isPgCreateInputType: true, pgInflection: table, } @@ -120,103 +130,126 @@ export default (function PgMutationCreatePlugin( }, }, { + __origin: `Adding table create payload type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, isMutationPayload: true, isPgCreatePayloadType: true, pgIntrospection: table, } ); const fieldName = inflection.createField(table); - memo[fieldName] = fieldWithHooks( - fieldName, - context => { - const { getDataFromParsedResolveInfoFragment } = context; - return { - description: `Creates a single \`${tableTypeName}\`.`, - type: PayloadType, - args: { - input: { - type: new GraphQLNonNull(InputType), - }, - }, - async resolve(data, { input }, { pgClient }, resolveInfo) { - const parsedResolveInfoFragment = parseResolveInfo( - resolveInfo - ); - const resolveData = getDataFromParsedResolveInfoFragment( - parsedResolveInfoFragment, - PayloadType - ); - const insertedRowAlias = sql.identifier(Symbol()); - const query = queryFromResolveData( - insertedRowAlias, - insertedRowAlias, - resolveData, - {} - ); - const sqlColumns = []; - const sqlValues = []; - const inputData = input[inflection.tableFieldName(table)]; - pgIntrospectionResultsByKind.attribute - .filter(attr => attr.classId === table.id) - .filter(attr => pgColumnFilter(attr, build, context)) - .filter(attr => !omit(attr, "create")) - .forEach(attr => { - const fieldName = inflection.column(attr); - const val = inputData[fieldName]; - if ( - Object.prototype.hasOwnProperty.call( - inputData, - fieldName - ) - ) { - sqlColumns.push(sql.identifier(attr.name)); - sqlValues.push( - gql2pg(val, attr.type, attr.typeModifier) - ); - } - }); + memo = build.extend( + memo, + { + [fieldName]: fieldWithHooks( + fieldName, + context => { + const { getDataFromParsedResolveInfoFragment } = context; + return { + description: `Creates a single \`${tableTypeName}\`.`, + type: PayloadType, + args: { + input: { + type: new GraphQLNonNull(InputType), + }, + }, + async resolve(data, { input }, { pgClient }, resolveInfo) { + const parsedResolveInfoFragment = parseResolveInfo( + resolveInfo + ); + const resolveData = getDataFromParsedResolveInfoFragment( + parsedResolveInfoFragment, + PayloadType + ); + const insertedRowAlias = sql.identifier(Symbol()); + const query = queryFromResolveData( + insertedRowAlias, + insertedRowAlias, + resolveData, + {} + ); + const sqlColumns = []; + const sqlValues = []; + const inputData = input[inflection.tableFieldName(table)]; + pgIntrospectionResultsByKind.attribute + .filter(attr => attr.classId === table.id) + .filter(attr => pgColumnFilter(attr, build, context)) + .filter(attr => !omit(attr, "create")) + .forEach(attr => { + const fieldName = inflection.column(attr); + const val = inputData[fieldName]; + if ( + Object.prototype.hasOwnProperty.call( + inputData, + fieldName + ) + ) { + sqlColumns.push(sql.identifier(attr.name)); + sqlValues.push( + gql2pg(val, attr.type, attr.typeModifier) + ); + } + }); - const mutationQuery = sql.query` + const mutationQuery = sql.query` insert into ${sql.identifier( table.namespace.name, table.name )} ${ - sqlColumns.length - ? sql.fragment`( + sqlColumns.length + ? sql.fragment`( ${sql.join(sqlColumns, ", ")} ) values(${sql.join(sqlValues, ", ")})` - : sql.fragment`default values` - } returning *`; + : sql.fragment`default values` + } returning *`; - let row; - try { - await pgClient.query("SAVEPOINT graphql_mutation"); - const rows = await viaTemporaryTable( - pgClient, - sql.identifier(table.namespace.name, table.name), - mutationQuery, - insertedRowAlias, - query - ); - row = rows[0]; - await pgClient.query("RELEASE SAVEPOINT graphql_mutation"); - } catch (e) { - await pgClient.query( - "ROLLBACK TO SAVEPOINT graphql_mutation" - ); - throw e; - } - return { - clientMutationId: input.clientMutationId, - data: row, + let row; + try { + await pgClient.query("SAVEPOINT graphql_mutation"); + const rows = await viaTemporaryTable( + pgClient, + sql.identifier(table.namespace.name, table.name), + mutationQuery, + insertedRowAlias, + query + ); + row = rows[0]; + await pgClient.query( + "RELEASE SAVEPOINT graphql_mutation" + ); + } catch (e) { + await pgClient.query( + "ROLLBACK TO SAVEPOINT graphql_mutation" + ); + throw e; + } + return { + clientMutationId: input.clientMutationId, + data: row, + }; + }, }; }, - }; + { + pgFieldIntrospection: table, + isPgCreateMutationField: true, + } + ), }, - { - pgFieldIntrospection: table, - isPgCreateMutationField: true, - } + `Adding create mutation for ${describePgEntity( + table + )}. You can omit this default mutation with:\n\n ${sqlCommentByAddingTags( + table, + { + omit: "create", + } + )}` ); return memo; }, {}), diff --git a/packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.js b/packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.js index 7a38c1d08..73622d63a 100644 --- a/packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgMutationPayloadEdgePlugin.js @@ -13,6 +13,7 @@ export default (function PgMutationPayloadEdgePlugin(builder) { pgIntrospectionResultsByKind: introspectionResultsByKind, inflection, pgOmit: omit, + describePgEntity, } = build; const { scope: { isMutationPayload, pgIntrospection, pgIntrospectionTable }, @@ -177,7 +178,9 @@ export default (function PgMutationPayloadEdgePlugin(builder) { } ), }, - `Adding edge field to mutation payload '${Self.name}'` + `Adding edge field for table ${describePgEntity( + table + )} to mutation payload '${Self.name}'` ); }); }: Plugin); diff --git a/packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.js b/packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.js index f4eabe09c..9130fca77 100644 --- a/packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgMutationProceduresPlugin.js @@ -10,6 +10,8 @@ export default (function PgMutationProceduresPlugin(builder) { pgMakeProcField: makeProcField, pgOmit: omit, swallowError, + describePgEntity, + sqlCommentByAddingTags, } = build; const { scope: { isRootMutation }, @@ -42,10 +44,23 @@ export default (function PgMutationProceduresPlugin(builder) { const fieldName = inflection.functionMutationName(proc); try { - memo[fieldName] = makeProcField(fieldName, proc, build, { - fieldWithHooks, - isMutation: true, - }); + memo = extend( + memo, + { + [fieldName]: makeProcField(fieldName, proc, build, { + fieldWithHooks, + isMutation: true, + }), + }, + `Adding mutation field for ${describePgEntity( + proc + )}. You can rename this field with:\n\n ${sqlCommentByAddingTags( + proc, + { + name: "newNameHere", + } + )}` + ); } catch (e) { swallowError(e); } diff --git a/packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.js b/packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.js index fe01e0608..283c3bdd7 100644 --- a/packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgMutationUpdateDeletePlugin.js @@ -39,6 +39,8 @@ export default (async function PgMutationUpdateDeletePlugin( pgQueryFromResolveData: queryFromResolveData, pgOmit: omit, pgViaTemporaryTable: viaTemporaryTable, + describePgEntity, + sqlCommentByAddingTags, } = build; const { scope: { isRootMutation }, @@ -266,6 +268,14 @@ export default (async function PgMutationUpdateDeletePlugin( }, }, { + __origin: `Adding table ${mode} mutation payload type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, isMutationPayload: true, isPgUpdatePayloadType: mode === "update", isPgDeletePayloadType: mode === "delete", @@ -320,6 +330,14 @@ export default (async function PgMutationUpdateDeletePlugin( ), }, { + __origin: `Adding table ${mode} (by node ID) mutation input type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, isPgUpdateInputType: mode === "update", isPgUpdateNodeInputType: mode === "update", isPgDeleteInputType: mode === "delete", @@ -329,75 +347,83 @@ export default (async function PgMutationUpdateDeletePlugin( } ); - memo[fieldName] = fieldWithHooks( - fieldName, - context => { - const { getDataFromParsedResolveInfoFragment } = context; - return { - description: - mode === "update" - ? `Updates a single \`${tableTypeName}\` using its globally unique id and a patch.` - : `Deletes a single \`${tableTypeName}\` using its globally unique id.`, - type: PayloadType, - args: { - input: { - type: new GraphQLNonNull(InputType), - }, - }, - async resolve( - parent, - { input }, - { pgClient }, - resolveInfo - ) { - const nodeId = input[nodeIdFieldName]; - try { - const [alias, ...identifiers] = JSON.parse( - base64Decode(nodeId) - ); - const NodeTypeByAlias = getNodeType(alias); - if (NodeTypeByAlias !== TableType) { - throw new Error("Mismatched type"); - } - if (identifiers.length !== primaryKeys.length) { - throw new Error("Invalid ID"); - } + memo = extend( + memo, + { + [fieldName]: fieldWithHooks( + fieldName, + context => { + const { + getDataFromParsedResolveInfoFragment, + } = context; + return { + description: + mode === "update" + ? `Updates a single \`${tableTypeName}\` using its globally unique id and a patch.` + : `Deletes a single \`${tableTypeName}\` using its globally unique id.`, + type: PayloadType, + args: { + input: { + type: new GraphQLNonNull(InputType), + }, + }, + async resolve( + parent, + { input }, + { pgClient }, + resolveInfo + ) { + const nodeId = input[nodeIdFieldName]; + try { + const [alias, ...identifiers] = JSON.parse( + base64Decode(nodeId) + ); + const NodeTypeByAlias = getNodeType(alias); + if (NodeTypeByAlias !== TableType) { + throw new Error("Mismatched type"); + } + if (identifiers.length !== primaryKeys.length) { + throw new Error("Invalid ID"); + } - return commonCodeRenameMe( - pgClient, - resolveInfo, - getDataFromParsedResolveInfoFragment, - PayloadType, - input, - sql.fragment`(${sql.join( - primaryKeys.map( - (key, idx) => - sql.fragment`${sql.identifier( - key.name - )} = ${gql2pg( - identifiers[idx], - key.type, - key.typeModifier - )}` - ), - ") and (" - )})`, - context - ); - } catch (e) { - debug(e); - return null; - } + return commonCodeRenameMe( + pgClient, + resolveInfo, + getDataFromParsedResolveInfoFragment, + PayloadType, + input, + sql.fragment`(${sql.join( + primaryKeys.map( + (key, idx) => + sql.fragment`${sql.identifier( + key.name + )} = ${gql2pg( + identifiers[idx], + key.type, + key.typeModifier + )}` + ), + ") and (" + )})`, + context + ); + } catch (e) { + debug(e); + return null; + } + }, + }; }, - }; + { + isPgNodeMutation: true, + pgFieldIntrospection: table, + [mode === "update" + ? "isPgUpdateMutationField" + : "isPgDeleteMutationField"]: true, + } + ), }, - { - isPgNodeMutation: true, - pgFieldIntrospection: table, - [mode === "update" - ? "isPgUpdateMutationField" - : "isPgDeleteMutationField"]: true, - } + "Adding ${mode} mutation for ${describePgEntity(table)}" ); } @@ -460,6 +486,14 @@ export default (async function PgMutationUpdateDeletePlugin( ), }, { + __origin: `Adding table ${mode} mutation input type for ${describePgEntity( + constraint + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, isPgUpdateInputType: mode === "update", isPgUpdateByKeysInputType: mode === "update", isPgDeleteInputType: mode === "delete", @@ -470,58 +504,68 @@ export default (async function PgMutationUpdateDeletePlugin( } ); - memo[fieldName] = fieldWithHooks( - fieldName, - context => { - const { getDataFromParsedResolveInfoFragment } = context; - return { - description: - mode === "update" - ? `Updates a single \`${tableTypeName}\` using a unique key and a patch.` - : `Deletes a single \`${tableTypeName}\` using a unique key.`, - type: PayloadType, - args: { - input: { - type: new GraphQLNonNull(InputType), - }, - }, - async resolve( - parent, - { input }, - { pgClient }, - resolveInfo - ) { - return commonCodeRenameMe( - pgClient, - resolveInfo, + memo = extend( + memo, + { + [fieldName]: fieldWithHooks( + fieldName, + context => { + const { getDataFromParsedResolveInfoFragment, - PayloadType, - input, - sql.fragment`(${sql.join( - keys.map( - key => - sql.fragment`${sql.identifier( - key.name - )} = ${gql2pg( - input[inflection.column(key)], - key.type, - key.typeModifier - )}` - ), - ") and (" - )})`, - context - ); + } = context; + return { + description: + mode === "update" + ? `Updates a single \`${tableTypeName}\` using a unique key and a patch.` + : `Deletes a single \`${tableTypeName}\` using a unique key.`, + type: PayloadType, + args: { + input: { + type: new GraphQLNonNull(InputType), + }, + }, + async resolve( + parent, + { input }, + { pgClient }, + resolveInfo + ) { + return commonCodeRenameMe( + pgClient, + resolveInfo, + getDataFromParsedResolveInfoFragment, + PayloadType, + input, + sql.fragment`(${sql.join( + keys.map( + key => + sql.fragment`${sql.identifier( + key.name + )} = ${gql2pg( + input[inflection.column(key)], + key.type, + key.typeModifier + )}` + ), + ") and (" + )})`, + context + ); + }, + }; }, - }; + { + isPgNodeMutation: false, + pgFieldIntrospection: table, + [mode === "update" + ? "isPgUpdateMutationField" + : "isPgDeleteMutationField"]: true, + } + ), }, - { - isPgNodeMutation: false, - pgFieldIntrospection: table, - [mode === "update" - ? "isPgUpdateMutationField" - : "isPgDeleteMutationField"]: true, - } + `Adding ${mode} mutation for ${describePgEntity( + constraint + )}` ); }); } diff --git a/packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.js b/packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.js index e54511c7d..9b6594eff 100644 --- a/packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgOrderAllColumnsPlugin.js @@ -9,6 +9,8 @@ export default (function PgOrderAllColumnsPlugin(builder) { pgColumnFilter, inflection, pgOmit: omit, + describePgEntity, + sqlCommentByAddingTags, } = build; const { scope: { isPgRowSortEnum, pgIntrospection: table }, @@ -27,18 +29,44 @@ export default (function PgOrderAllColumnsPlugin(builder) { } const ascFieldName = inflection.orderByColumnEnum(attr, true); const descFieldName = inflection.orderByColumnEnum(attr, false); - memo[ascFieldName] = { - value: { - alias: ascFieldName.toLowerCase(), - specs: [[attr.name, true]], + memo = extend( + memo, + { + [ascFieldName]: { + value: { + alias: ascFieldName.toLowerCase(), + specs: [[attr.name, true]], + }, + }, }, - }; - memo[descFieldName] = { - value: { - alias: descFieldName.toLowerCase(), - specs: [[attr.name, false]], + `Adding ascending orderBy enum value for ${describePgEntity( + attr + )}. You can rename this field with:\n\n ${sqlCommentByAddingTags( + attr, + { + name: "newNameHere", + } + )}` + ); + memo = extend( + memo, + { + [descFieldName]: { + value: { + alias: descFieldName.toLowerCase(), + specs: [[attr.name, false]], + }, + }, }, - }; + `Adding descending orderBy enum value for ${describePgEntity( + attr + )}. You can rename this field with:\n\n ${sqlCommentByAddingTags( + attr, + { + name: "newNameHere", + } + )}` + ); return memo; }, {}), `Adding order values from table '${table.name}'` diff --git a/packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.js b/packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.js index e9fee5b46..870d10546 100644 --- a/packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgQueryProceduresPlugin.js @@ -19,6 +19,8 @@ export default (function PgQueryProceduresPlugin( pgIntrospectionResultsByKind: introspectionResultsByKind, pgMakeProcField: makeProcField, pgOmit: omit, + describePgEntity, + sqlCommentByAddingTags, } = build; const { scope: { isRootQuery }, @@ -76,10 +78,23 @@ export default (function PgQueryProceduresPlugin( ? inflection.functionQueryNameList(proc) : inflection.functionQueryName(proc); try { - memo[fieldName] = makeProcField(fieldName, proc, build, { - fieldWithHooks, - forceList, - }); + memo = extend( + memo, + { + [fieldName]: makeProcField(fieldName, proc, build, { + fieldWithHooks, + forceList, + }), + }, + `Adding query field for ${describePgEntity( + proc + )}. You can rename this field with:\n\n ${sqlCommentByAddingTags( + proc, + { + name: "newNameHere", + } + )}` + ); } catch (e) { // eslint-disable-next-line no-console console.warn( diff --git a/packages/graphile-build-pg/src/plugins/PgRowNode.js b/packages/graphile-build-pg/src/plugins/PgRowNode.js index c03c1b454..12ee3e088 100644 --- a/packages/graphile-build-pg/src/plugins/PgRowNode.js +++ b/packages/graphile-build-pg/src/plugins/PgRowNode.js @@ -93,6 +93,8 @@ export default (async function PgRowNode(builder) { inflection, pgQueryFromResolveData: queryFromResolveData, pgOmit: omit, + describePgEntity, + sqlCommentByAddingTags, } = build; const { scope: { isRootQuery }, @@ -131,78 +133,89 @@ export default (async function PgRowNode(builder) { num => attributes.filter(attr => attr.num === num)[0] ); const fieldName = inflection.tableNode(table); - memo[fieldName] = fieldWithHooks( - fieldName, - ({ getDataFromParsedResolveInfoFragment }) => { - return { - description: `Reads a single \`${ - TableType.name - }\` using its globally unique \`ID\`.`, - type: TableType, - args: { - [nodeIdFieldName]: { - description: `The globally unique \`ID\` to be used in selecting a single \`${ + memo = extend( + memo, + { + [fieldName]: fieldWithHooks( + fieldName, + ({ getDataFromParsedResolveInfoFragment }) => { + return { + description: `Reads a single \`${ TableType.name - }\`.`, - type: new GraphQLNonNull(GraphQLID), - }, - }, - async resolve(parent, args, { pgClient }, resolveInfo) { - const nodeId = args[nodeIdFieldName]; - try { - const [alias, ...identifiers] = JSON.parse( - base64Decode(nodeId) - ); - const NodeTypeByAlias = getNodeType(alias); - if (NodeTypeByAlias !== TableType) { - throw new Error("Mismatched type"); - } - if (identifiers.length !== primaryKeys.length) { - throw new Error("Invalid ID"); - } + }\` using its globally unique \`ID\`.`, + type: TableType, + args: { + [nodeIdFieldName]: { + description: `The globally unique \`ID\` to be used in selecting a single \`${ + TableType.name + }\`.`, + type: new GraphQLNonNull(GraphQLID), + }, + }, + async resolve(parent, args, { pgClient }, resolveInfo) { + const nodeId = args[nodeIdFieldName]; + try { + const [alias, ...identifiers] = JSON.parse( + base64Decode(nodeId) + ); + const NodeTypeByAlias = getNodeType(alias); + if (NodeTypeByAlias !== TableType) { + throw new Error("Mismatched type"); + } + if (identifiers.length !== primaryKeys.length) { + throw new Error("Invalid ID"); + } - const parsedResolveInfoFragment = parseResolveInfo( - resolveInfo - ); - const resolveData = getDataFromParsedResolveInfoFragment( - parsedResolveInfoFragment, - TableType - ); - const query = queryFromResolveData( - sqlFullTableName, - undefined, - resolveData, - {}, - queryBuilder => { - primaryKeys.forEach((key, idx) => { - queryBuilder.where( - sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( - key.name - )} = ${gql2pg( - identifiers[idx], - primaryKeys[idx].type, - primaryKeys[idx].typeModifier - )}` - ); - }); + const parsedResolveInfoFragment = parseResolveInfo( + resolveInfo + ); + const resolveData = getDataFromParsedResolveInfoFragment( + parsedResolveInfoFragment, + TableType + ); + const query = queryFromResolveData( + sqlFullTableName, + undefined, + resolveData, + {}, + queryBuilder => { + primaryKeys.forEach((key, idx) => { + queryBuilder.where( + sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( + key.name + )} = ${gql2pg( + identifiers[idx], + primaryKeys[idx].type, + primaryKeys[idx].typeModifier + )}` + ); + }); + } + ); + const { text, values } = sql.compile(query); + if (debugSql.enabled) debugSql(text); + const { + rows: [row], + } = await pgClient.query(text, values); + return row; + } catch (e) { + return null; } - ); - const { text, values } = sql.compile(query); - if (debugSql.enabled) debugSql(text); - const { - rows: [row], - } = await pgClient.query(text, values); - return row; - } catch (e) { - return null; - } + }, + }; }, - }; + { + isPgNodeQuery: true, + pgFieldIntrospection: table, + } + ), }, - { - isPgNodeQuery: true, - pgFieldIntrospection: table, - } + `Adding row by globally unique identifier field for ${describePgEntity( + table + )}. You can rename this table via:\n\n ${sqlCommentByAddingTags( + table, + { name: "newNameHere" } + )}` ); } return memo; diff --git a/packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.js b/packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.js index a2c7326ad..8c0225bf2 100644 --- a/packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgScalarFunctionConnectionPlugin.js @@ -20,6 +20,8 @@ export default (function PgScalarFunctionConnectionPlugin( }, inflection, pgOmit: omit, + describePgEntity, + sqlCommentByAddingTags, } = build; const nullableIf = (condition, Type) => condition ? Type : new GraphQLNonNull(Type); @@ -80,6 +82,14 @@ export default (function PgScalarFunctionConnectionPlugin( }, }, { + __origin: `Adding function result edge type for ${describePgEntity( + proc + )}. You can rename the function's GraphQL field (and its dependent types) via:\n\n ${sqlCommentByAddingTags( + proc, + { + name: "newNameHere", + } + )}`, isEdgeType: true, nodeType: NodeType, pgIntrospection: proc, @@ -123,6 +133,14 @@ export default (function PgScalarFunctionConnectionPlugin( }, }, { + __origin: `Adding function connection type for ${describePgEntity( + proc + )}. You can rename the function's GraphQL field (and its dependent types) via:\n\n ${sqlCommentByAddingTags( + proc, + { + name: "newNameHere", + } + )}`, isConnectionType: true, edgeType: EdgeType, nodeType: NodeType, diff --git a/packages/graphile-build-pg/src/plugins/PgTablesPlugin.js b/packages/graphile-build-pg/src/plugins/PgTablesPlugin.js index 04424f476..478e5c9a5 100644 --- a/packages/graphile-build-pg/src/plugins/PgTablesPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgTablesPlugin.js @@ -55,6 +55,8 @@ export default (function PgTablesPlugin( GraphQLInputObjectType, }, inflection, + describePgEntity, + sqlCommentByAddingTags, } = build; const nullableIf = (condition, Type) => condition ? Type : new GraphQLNonNull(Type); @@ -177,6 +179,14 @@ export default (function PgTablesPlugin( }, }, { + __origin: `Adding table type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, pgIntrospection: table, isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, @@ -193,6 +203,14 @@ export default (function PgTablesPlugin( name: inflection.inputType(TableType), }, { + __origin: `Adding table input type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, pgIntrospection: table, isInputType: true, isPgRowType: table.isSelectable, @@ -222,6 +240,14 @@ export default (function PgTablesPlugin( name: inflection.patchType(TableType), }, { + __origin: `Adding table patch type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, pgIntrospection: table, isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, @@ -244,6 +270,14 @@ export default (function PgTablesPlugin( name: inflection.baseInputType(TableType), }, { + __origin: `Adding table base input type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, pgIntrospection: table, isPgRowType: table.isSelectable, isPgCompoundType: !table.isSelectable, @@ -342,6 +376,14 @@ export default (function PgTablesPlugin( }, }, { + __origin: `Adding table edge type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, isEdgeType: true, isPgRowEdgeType: true, nodeType: TableType, @@ -394,6 +436,14 @@ export default (function PgTablesPlugin( }, }, { + __origin: `Adding table connection type for ${describePgEntity( + table + )}. You can rename the table's GraphQL type via:\n\n ${sqlCommentByAddingTags( + table, + { + name: "newNameHere", + } + )}`, isConnectionType: true, isPgRowConnectionType: true, edgeType: EdgeType, diff --git a/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js b/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js index d7689f180..dab5a75db 100644 --- a/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js +++ b/packages/graphile-build-pg/src/plugins/PgTypesPlugin.js @@ -78,11 +78,11 @@ export default (function PgTypesPlugin( const { pgIntrospectionResultsByKind: introspectionResultsByKind, getTypeByName, - addType, pgSql: sql, inflection, graphql, } = build; + const addType = build.addType.bind(build); const { GraphQLNonNull, GraphQLString, @@ -217,7 +217,7 @@ export default (function PgTypesPlugin( "An interval of time that has passed where the smallest distinct unit is a second.", fields: makeIntervalFields(), }); - addType(GQLInterval); + addType(GQLInterval, "graphile-build-pg built-in"); const GQLIntervalInput = new GraphQLInputObjectType({ name: "IntervalInput", @@ -225,7 +225,7 @@ export default (function PgTypesPlugin( "An interval of time that has passed where the smallest distinct unit is a second.", fields: makeIntervalFields(), }); - addType(GQLIntervalInput); + addType(GQLIntervalInput, "graphile-build-pg built-in"); const stringType = (name, description) => new GraphQLScalarType({ @@ -249,8 +249,8 @@ export default (function PgTypesPlugin( "BitString", "A string representing a series of binary bits" ); - addType(BigFloat); - addType(BitString); + addType(BigFloat, "graphile-build-pg built-in"); + addType(BitString, "graphile-build-pg built-in"); const rawTypes = [ 1186, // interval @@ -407,11 +407,11 @@ export default (function PgTypesPlugin( }); // Other plugins might want to use JSON - addType(JSONType); - addType(UUIDType); - addType(DateType); - addType(DateTimeType); - addType(TimeType); + 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( @@ -652,8 +652,8 @@ export default (function PgTypesPlugin( }, }, }); - addType(Range); - addType(RangeInput); + addType(Range, "graphile-build-pg built-in"); + addType(RangeInput, "graphile-build-pg built-in"); } else { RangeInput = getTypeByName(inflection.inputType(Range.name)); } diff --git a/packages/graphile-build-pg/src/plugins/makeProcField.js b/packages/graphile-build-pg/src/plugins/makeProcField.js index 641654d0a..f88cc8aad 100644 --- a/packages/graphile-build-pg/src/plugins/makeProcField.js +++ b/packages/graphile-build-pg/src/plugins/makeProcField.js @@ -49,6 +49,8 @@ export default function makeProcField( pgQueryFromResolveData: queryFromResolveData, pgAddStartEndCursor: addStartEndCursor, pgViaTemporaryTable: viaTemporaryTable, + describePgEntity, + sqlCommentByAddingTags, }: {| ...Build |}, { fieldWithHooks, @@ -419,6 +421,14 @@ export default function makeProcField( Object.assign( {}, { + __origin: `Adding mutation function payload type for ${describePgEntity( + proc + )}. You can rename the function's GraphQL field (and its dependent types) via:\n\n ${sqlCommentByAddingTags( + proc, + { + name: "newNameHere", + } + )}`, isMutationPayload: true, }, payloadTypeScope @@ -442,6 +452,14 @@ export default function makeProcField( ), }, { + __origin: `Adding mutation function input type for ${describePgEntity( + proc + )}. You can rename the function's GraphQL field (and its dependent types) via:\n\n ${sqlCommentByAddingTags( + proc, + { + name: "newNameHere", + } + )}`, isMutationInput: true, } ); diff --git a/packages/graphile-build/src/SchemaBuilder.js b/packages/graphile-build/src/SchemaBuilder.js index 79d71f2d8..ff63ab911 100644 --- a/packages/graphile-build/src/SchemaBuilder.js +++ b/packages/graphile-build/src/SchemaBuilder.js @@ -54,14 +54,14 @@ export type Build = {| _context: mixed, resolveInfo: GraphQLResolveInfo ): string, - addType(type: GraphQLNamedType): void, + addType(type: GraphQLNamedType, origin?: ?string): void, getTypeByName(typeName: string): ?GraphQLType, extend(base: Obj1, extra: Obj2, hint?: string): Obj1 & Obj2, - newWithHooks( + newWithHooks( Class, - spec: {}, - scope: {}, - returnNullOnInvalid?: boolean + spec: ConfigType, + scope: Scope, + performNonEmptyFieldsCheck?: boolean ): ?T, fieldDataGeneratorsByType: Map<*, *>, // @deprecated - use fieldDataGeneratorsByFieldNameByType instead fieldDataGeneratorsByFieldNameByType: Map<*, *>, @@ -71,6 +71,10 @@ export type Build = {| [string]: (...args: Array) => string, }, swallowError: (e: Error) => void, + status: { + currentHookName: ?string, + currentHookEvent: ?string, + }, |}; export type BuildExtensionQuery = {| @@ -78,6 +82,7 @@ export type BuildExtensionQuery = {| |}; export type Scope = { + __origin: ?string, [string]: mixed, }; @@ -250,7 +255,15 @@ class SchemaBuilder extends EventEmitter { this.depth )}[${hookName}${debugStr}]: Executing '${hookDisplayName}'` ); + + const previousHookName = build.status.currentHookName; + const previousHookEvent = build.status.currentHookEvent; + build.status.currentHookName = hookDisplayName; + build.status.currentHookEvent = hookName; newObj = hook(newObj, build, context); + build.status.currentHookName = previousHookName; + build.status.currentHookEvent = previousHookEvent; + if (!newObj) { throw new Error( `Hook '${hook.displayName || @@ -314,7 +327,10 @@ class SchemaBuilder extends EventEmitter { this._generatedSchema = build.newWithHooks( GraphQLSchema, {}, - { isSchema: true } + { + __origin: `GraphQL built-in`, + isSchema: true, + } ); } if (!this._generatedSchema) { diff --git a/packages/graphile-build/src/extend.js b/packages/graphile-build/src/extend.js index 4ede02652..7ff57fd34 100644 --- a/packages/graphile-build/src/extend.js +++ b/packages/graphile-build/src/extend.js @@ -1,6 +1,14 @@ // @flow +import chalk from "chalk"; const aExtendedB = new WeakMap(); +const INDENT = " "; + +export function indent(text: string) { + return ( + INDENT + text.replace(/\n/g, "\n" + INDENT).replace(/\n +(?=\n|$)/g, "\n") + ); +} export default function extend( base: Obj1, @@ -9,14 +17,47 @@ export default function extend( ): Obj1 & Obj2 { const keysA = Object.keys(base); const keysB = Object.keys(extra); + const hints = Object.create(null); + for (const key of keysA) { + const hintKey = `_source__${key}`; + if (base[hintKey]) { + hints[hintKey] = base[hintKey]; + } + } + for (const key of keysB) { const newValue = extra[key]; const oldValue = base[key]; + const hintKey = `_source__${key}`; + const hintB = extra[hintKey] || hint; if (aExtendedB.get(newValue) !== oldValue && keysA.indexOf(key) >= 0) { - throw new Error(`Overwriting key '${key}' is not allowed! ${hint || ""}`); + // $FlowFixMe + const hintA: ?string = base[hintKey]; + const firstEntityDetails = !hintA + ? "We don't have any information about the first entity." + : `The first entity was:\n\n${indent(chalk.magenta(hintA))}`; + const secondEntityDetails = !hintB + ? "We don't have any information about the second entity." + : `The second entity was:\n\n${indent(chalk.yellow(hintB))}`; + throw new Error( + `A naming conflict has occurred - two entities have tried to define the same key '${chalk.bold( + key + )}'.\n\n${indent(firstEntityDetails)}\n\n${indent(secondEntityDetails)}` + ); } + hints[hintKey] = hints[hintKey] || hintB || base[hintKey]; } const obj = Object.assign({}, base, extra); aExtendedB.set(obj, base); + for (const hintKey in hints) { + if (hints[hintKey]) { + Object.defineProperty(obj, hintKey, { + configurable: false, + enumerable: false, + value: hints[hintKey], + writable: false, + }); + } + } return obj; } diff --git a/packages/graphile-build/src/index.js b/packages/graphile-build/src/index.js index f28722612..b298e2107 100644 --- a/packages/graphile-build/src/index.js +++ b/packages/graphile-build/src/index.js @@ -3,6 +3,7 @@ import util from "util"; import SchemaBuilder from "./SchemaBuilder"; import { + SwallowErrorsPlugin, StandardTypesPlugin, NodePlugin, QueryPlugin, @@ -71,6 +72,7 @@ export const buildSchema = async ( }; export const defaultPlugins: Array = [ + SwallowErrorsPlugin, StandardTypesPlugin, NodePlugin, QueryPlugin, @@ -81,6 +83,7 @@ export const defaultPlugins: Array = [ ]; export { + SwallowErrorsPlugin, StandardTypesPlugin, NodePlugin, QueryPlugin, diff --git a/packages/graphile-build/src/makeNewBuild.js b/packages/graphile-build/src/makeNewBuild.js index 4daf82f73..1ee031bfd 100644 --- a/packages/graphile-build/src/makeNewBuild.js +++ b/packages/graphile-build/src/makeNewBuild.js @@ -27,7 +27,8 @@ import type SchemaBuilder, { DataForType, } from "./SchemaBuilder"; -import extend from "./extend"; +import extend, { indent } from "./extend"; +import chalk from "chalk"; import { createHash } from "crypto"; import { version } from "../package.json"; @@ -221,6 +222,13 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { Boolean: graphql.GraphQLBoolean, ID: graphql.GraphQLID, }; + const allTypesSources = { + Int: "GraphQL Built-in", + Float: "GraphQL Built-in", + String: "GraphQL Built-in", + Boolean: "GraphQL Built-in", + ID: "GraphQL Built-in", + }; // Every object type gets fieldData associated with each of its // fields. @@ -249,16 +257,43 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { const alias = getSafeAliasFromResolveInfo(resolveInfo); return data[alias]; }, - addType(type: GraphQLNamedType): void { + addType(type: GraphQLNamedType, origin?: ?string): void { if (!type.name) { throw new Error( `addType must only be called with named types, try using require('graphql').getNamedType` ); } - if (allTypes[type.name] && allTypes[type.name] !== type) { - throw new Error(`There's already a type with the name: ${type.name}`); + const newTypeSource = + 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]; + const firstEntityDetails = !oldTypeSource + ? "The first type was registered from an unknown origin." + : `The first entity was:\n\n${indent( + chalk.magenta(oldTypeSource) + )}`; + const secondEntityDetails = !newTypeSource + ? "The second type was registered from an unknown origin." + : `The second entity was:\n\n${indent( + chalk.yellow(newTypeSource) + )}`; + throw new Error( + `A type naming conflict has occurred - two entities have tried to define the same type '${chalk.bold( + type.name + )}'.\n\n${indent(firstEntityDetails)}\n\n${indent( + secondEntityDetails + )}` + ); + } + } else { + allTypes[type.name] = type; + allTypesSources[type.name] = newTypeSource; } - allTypes[type.name] = type; }, getTypeByName(typeName) { return allTypes[typeName]; @@ -554,7 +589,13 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { const fieldsSpec = builder.applyHooks( this, "GraphQLObjectType:fields", - rawFields, + this.extend( + {}, + rawFields, + `Default field included in newWithHooks call for '${ + rawSpec.name + }'. ${inScope.__origin || ""}` + ), fieldsContext, `|${rawSpec.name}` ); @@ -643,7 +684,13 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { const fieldsSpec = builder.applyHooks( this, "GraphQLInputObjectType:fields", - rawFields, + this.extend( + {}, + rawFields, + `Default field included in newWithHooks call for '${ + rawSpec.name + }'. ${inScope.__origin || ""}` + ), fieldsContext, `|${getNameFromType(Self)}` ); @@ -737,12 +784,15 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { } if (finalSpec.name) { - if (allTypes[finalSpec.name]) { - throw new Error( - `Type '${finalSpec.name}' has already been registered!` - ); - } - allTypes[finalSpec.name] = Self; + this.addType( + Self, + scope.__origin || + (this + ? `'newWithHooks' call during hook '${ + this.status.currentHookName + }'` + : null) + ); } fieldDataGeneratorsByFieldNameByType.set( Self, @@ -767,5 +817,9 @@ export default function makeNewBuild(builder: SchemaBuilder): { ...Build } { swallowError, // resolveNode: EXPERIMENTAL, API might change! resolveNode, + status: { + currentHookName: null, + currentHookEvent: null, + }, }; } diff --git a/packages/graphile-build/src/plugins/MutationPlugin.js b/packages/graphile-build/src/plugins/MutationPlugin.js index ac0eaae67..b8231fd46 100644 --- a/packages/graphile-build/src/plugins/MutationPlugin.js +++ b/packages/graphile-build/src/plugins/MutationPlugin.js @@ -29,13 +29,20 @@ export default (async function MutationPlugin(builder) { description: "The root mutation type which contains root level fields which mutate data.", }, - { isRootMutation: true }, + { + __origin: `graphile-build built-in (root mutation type)`, + isRootMutation: true, + }, true ); if (isValidMutation(Mutation)) { - return extend(schema, { - mutation: Mutation, - }); + return extend( + schema, + { + mutation: Mutation, + }, + "Adding mutation type to schema" + ); } else { return schema; } diff --git a/packages/graphile-build/src/plugins/NodePlugin.js b/packages/graphile-build/src/plugins/NodePlugin.js index 876609004..7c5a47490 100644 --- a/packages/graphile-build/src/plugins/NodePlugin.js +++ b/packages/graphile-build/src/plugins/NodePlugin.js @@ -135,7 +135,9 @@ export default (function NodePlugin( }, }, }, - {} + { + __origin: `graphile-build built-in (NodePlugin); you can omit this plugin if you like, but you'll lose compatibility with Relay`, + } ); return _; }); @@ -218,7 +220,7 @@ export default (function NodePlugin( } ), }, - `Adding node helpers to the root Query` + `Adding Relay Global Object Identification support to the root Query via 'node' and '${nodeIdFieldName}' fields` ); } ); diff --git a/packages/graphile-build/src/plugins/QueryPlugin.js b/packages/graphile-build/src/plugins/QueryPlugin.js index 35fd3979a..8f1402e69 100644 --- a/packages/graphile-build/src/plugins/QueryPlugin.js +++ b/packages/graphile-build/src/plugins/QueryPlugin.js @@ -43,7 +43,10 @@ export default (async function QueryPlugin(builder) { }, }), }, - { isRootQuery: true }, + { + __origin: `graphile-build built-in (root query type)`, + isRootQuery: true, + }, true ); if (queryType) { diff --git a/packages/graphile-build/src/plugins/StandardTypesPlugin.js b/packages/graphile-build/src/plugins/StandardTypesPlugin.js index 47874d806..785a82e89 100644 --- a/packages/graphile-build/src/plugins/StandardTypesPlugin.js +++ b/packages/graphile-build/src/plugins/StandardTypesPlugin.js @@ -25,7 +25,7 @@ export default (function StandardTypesPlugin(builder) { "Cursor", "A location in a connection that can be used for resuming pagination." ); - build.addType(Cursor); + build.addType(Cursor, "graphile-build built-in"); return build; } ); @@ -75,6 +75,7 @@ export default (function StandardTypesPlugin(builder) { }), }, { + __origin: `graphile-build built-in`, isPageInfo: true, } ); diff --git a/packages/graphile-build/src/plugins/SubscriptionPlugin.js b/packages/graphile-build/src/plugins/SubscriptionPlugin.js index 63e358b56..e63b221df 100644 --- a/packages/graphile-build/src/plugins/SubscriptionPlugin.js +++ b/packages/graphile-build/src/plugins/SubscriptionPlugin.js @@ -29,13 +29,20 @@ export default (async function SubscriptionPlugin(builder) { description: "The root subscription type which contains root level fields which mutate data.", }, - { isRootSubscription: true }, + { + __origin: `graphile-build built-in (root subscription type)`, + isRootSubscription: true, + }, true ); if (isValidSubscription(Subscription)) { - return extend(schema, { - subscription: Subscription, - }); + return extend( + schema, + { + subscription: Subscription, + }, + "Adding subscription type to schema" + ); } else { return schema; } diff --git a/packages/graphile-build/src/plugins/SwallowErrorsPlugin.js b/packages/graphile-build/src/plugins/SwallowErrorsPlugin.js new file mode 100644 index 000000000..d2668b37c --- /dev/null +++ b/packages/graphile-build/src/plugins/SwallowErrorsPlugin.js @@ -0,0 +1,28 @@ +// @flow +import type { Plugin, Build } from "../SchemaBuilder"; + +export default (function SwallowErrorsPlugin( + builder, + { dontSwallowErrors = false } +) { + builder.hook( + "build", + (build: Build): Build => { + if (dontSwallowErrors) { + // This plugin is a bit of a misnomer - to better maintain backwards + // compatibility, `swallowError` still exists on `makeNewBuild`; and + // thus this plugin is really `dontSwallowErrors`. + // $FlowFixMe + return Object.assign({}, build, { + swallowError(e) { + // $FlowFixMe + e.recoverable = true; + throw e; + }, + }); + } else { + return build; + } + } + ); +}: Plugin); diff --git a/packages/graphile-build/src/plugins/index.js b/packages/graphile-build/src/plugins/index.js index ba7fec9ae..1531bf48d 100644 --- a/packages/graphile-build/src/plugins/index.js +++ b/packages/graphile-build/src/plugins/index.js @@ -7,6 +7,7 @@ import SubscriptionPlugin from "./SubscriptionPlugin"; import NodePlugin from "./NodePlugin"; import QueryPlugin from "./QueryPlugin"; import StandardTypesPlugin from "./StandardTypesPlugin"; +import SwallowErrorsPlugin from "./SwallowErrorsPlugin"; export { ClientMutationIdDescriptionPlugin, @@ -16,4 +17,5 @@ export { NodePlugin, QueryPlugin, StandardTypesPlugin, + SwallowErrorsPlugin, }; diff --git a/packages/graphile-utils/__tests__/__snapshots__/ExtendSchemaPlugin.test.js.snap b/packages/graphile-utils/__tests__/__snapshots__/ExtendSchemaPlugin.test.js.snap index 4e0c3b3d3..5a1446aaa 100644 --- a/packages/graphile-utils/__tests__/__snapshots__/ExtendSchemaPlugin.test.js.snap +++ b/packages/graphile-utils/__tests__/__snapshots__/ExtendSchemaPlugin.test.js.snap @@ -79,6 +79,7 @@ type Query { exports[`supports @scope directive with simple values 1`] = ` Object { + "__origin": "graphile-build built-in (root query type)", "fieldDirectives": Object { "scope": Object { "floatTest": 3.141592, @@ -115,6 +116,7 @@ type Query { exports[`supports @scope directive with variable value 1`] = ` Object { + "__origin": "graphile-build built-in (root query type)", "embedTest": Object { "sub": Array [ Array [ diff --git a/packages/graphile-utils/src/makeExtendSchemaPlugin.ts b/packages/graphile-utils/src/makeExtendSchemaPlugin.ts index d638be9d3..4d8ce96b8 100644 --- a/packages/graphile-utils/src/makeExtendSchemaPlugin.ts +++ b/packages/graphile-utils/src/makeExtendSchemaPlugin.ts @@ -210,6 +210,7 @@ export default function makeExtendSchemaPlugin( const interfaces = getInterfaces(definition.interfaces, build); const directives = getDirectives(definition.directives); const scope = { + __origin: `makeExtendSchemaPlugin`, directives, ...(directives.scope || {}), }; @@ -244,6 +245,7 @@ export default function makeExtendSchemaPlugin( const description = getDescription(definition.description); const directives = getDirectives(definition.directives); const scope = { + __origin: `makeExtendSchemaPlugin`, directives, ...(directives.scope || {}), }; diff --git a/packages/postgraphile-core/__tests__/integration/__snapshots__/extend-errors.test.js.snap b/packages/postgraphile-core/__tests__/integration/__snapshots__/extend-errors.test.js.snap new file mode 100644 index 000000000..fdc3d79e8 --- /dev/null +++ b/packages/postgraphile-core/__tests__/integration/__snapshots__/extend-errors.test.js.snap @@ -0,0 +1,217 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`column naming clash - rename 1`] = ` +[Error: A naming conflict has occurred - two entities have tried to define the same key 'WONT_CAST_EASY_ASC'. + + The first entity was: + + Adding ascending orderBy enum value for column "wont_cast_easy" on table "c"."edge_case". You can rename this field with: +  +  COMMENT ON COLUMN "c"."edge_case"."wont_cast_easy" IS E'@name newNameHere'; + + The second entity was: + + Adding ascending orderBy enum value for column "row_id" on table "c"."edge_case" (with smart comments: @name wontCastEasy). You can rename this field with: +  +  COMMENT ON COLUMN "c"."edge_case"."row_id" IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`function naming clash - computed 1`] = ` +[Error: A naming conflict has occurred - two entities have tried to define the same key 'rowId'. + + The first entity was: + + Adding field for column "row_id" on table "c"."edge_case". You can rename this field with: +  +  COMMENT ON COLUMN "c"."edge_case"."row_id" IS E'@name newNameHere'; + + The second entity was: + + Adding computed column for function "c"."edge_case_computed"(...args...) (with smart comments: @fieldName rowId). You can rename this field with: +  +  COMMENT ON FUNCTION "c"."edge_case_computed"(...arg types go here...) IS E'@fieldName newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`function naming clash - createPost 1`] = ` +[Error: A type naming conflict has occurred - two entities have tried to define the same type 'CreatePostPayload'. + + The first entity was: + + Adding table create payload type for table "a"."post". You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere'; + + The second entity was: + + Adding mutation function payload type for function "a"."mutation_text_array"(...args...) (with smart comments: @name createPost). You can rename the function's GraphQL field (and its dependent types) via: +  +  COMMENT ON FUNCTION "a"."mutation_text_array"(...arg types go here...) IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`function naming clash - deletePost 1`] = ` +[Error: A type naming conflict has occurred - two entities have tried to define the same type 'DeletePostPayload'. + + The first entity was: + + Adding table delete mutation payload type for table "a"."post". You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere'; + + The second entity was: + + Adding mutation function payload type for function "a"."mutation_text_array"(...args...) (with smart comments: @name deletePost). You can rename the function's GraphQL field (and its dependent types) via: +  +  COMMENT ON FUNCTION "a"."mutation_text_array"(...arg types go here...) IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`function naming clash - nodeId 1`] = ` +[Error: A naming conflict has occurred - two entities have tried to define the same key 'nodeId'. + + The first entity was: + + Adding Relay Global Object Identification support to the root Query via 'node' and 'nodeId' fields + + The second entity was: + + Adding query field for function "c"."int_set_query"(...args...) (with smart comments: @name nodeId). You can rename this field with: +  +  COMMENT ON FUNCTION "c"."int_set_query"(...arg types go here...) IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`function naming clash - payload 1`] = ` +[Error: A type naming conflict has occurred - two entities have tried to define the same type 'QEdge'. + + The first entity was: + + Adding function result edge type for function "c"."int_set_query"(...args...) (with smart comments: @name q). You can rename the function's GraphQL field (and its dependent types) via: +  +  COMMENT ON FUNCTION "c"."int_set_query"(...arg types go here...) IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.'; + + The second entity was: + + Adding table type for table "a"."post" (with smart comments: @name q_edge). You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`function naming clash - updatePost 1`] = ` +[Error: A type naming conflict has occurred - two entities have tried to define the same type 'UpdatePostPayload'. + + The first entity was: + + Adding table update mutation payload type for table "a"."post". You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere'; + + The second entity was: + + Adding mutation function payload type for function "a"."mutation_text_array"(...args...) (with smart comments: @name updatePost). You can rename the function's GraphQL field (and its dependent types) via: +  +  COMMENT ON FUNCTION "a"."mutation_text_array"(...arg types go here...) IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`simple collections naming clash 1`] = ` +[Error: A naming conflict has occurred - two entities have tried to define the same key 'clash'. + + The first entity was: + + Backward relation (connection) for constraint "post_author_id_fkey" on table "a"."post" (with smart comments: @foreignFieldName clash). To rename this relation with smart comments: +  +  COMMENT ON CONSTRAINT "post_author_id_fkey" ON "a"."post" IS E'@foreignFieldName newNameHere\\nRest of existing \\'comment\\' \\nhere.'; + + The second entity was: + + Backward relation (simple collection) for constraint "post_author_id_fkey" on table "a"."post" (with smart comments: @foreignFieldName clash). To rename this relation with smart comments: +  +  COMMENT ON CONSTRAINT "post_author_id_fkey" ON "a"."post" IS E'@foreignSimpleFieldName newNameHere\\n@foreignFieldName clash\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`table naming clash - condition 1`] = ` +[Error: A type naming conflict has occurred - two entities have tried to define the same type 'PersonCondition'. + + The first entity was: + + Adding condition type for table "c"."person". You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "c"."person" IS E'@name newNameHere\\nPerson test comment'; + + The second entity was: + + Adding table type for table "a"."post" (with smart comments: @name person_condition). You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`table naming clash - direct 1`] = ` +[Error: A type naming conflict has occurred - two entities have tried to define the same type 'PeopleOrderBy'. + + The first entity was: + + Adding connection "orderBy" argument for table "a"."post" (with smart comments: @name person). You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.'; + + The second entity was: + + Adding connection "orderBy" argument for table "c"."person". You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "c"."person" IS E'@name newNameHere\\nPerson test comment';] +`; + +exports[`table naming clash - mutation 1`] = ` +[Error: A type naming conflict has occurred - two entities have tried to define the same type 'Mutation'. + + The first entity was: + + Adding table type for table "a"."post" (with smart comments: @name mutation). You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.'; + + The second entity was: + + graphile-build built-in (root mutation type)] +`; + +exports[`table naming clash - order 1`] = ` +[Error: A type naming conflict has occurred - two entities have tried to define the same type 'PeopleOrderBy'. + + The first entity was: + + Adding connection "orderBy" argument for table "c"."person". You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "c"."person" IS E'@name newNameHere\\nPerson test comment'; + + The second entity was: + + Adding table type for table "a"."post" (with smart comments: @name people_order_by). You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`table naming clash - query 1`] = ` +[Error: A naming conflict has occurred - two entities have tried to define the same key 'query'. + + The first entity was: + + Default field included in newWithHooks call for 'Query'. graphile-build built-in (root query type) + + The second entity was: + + Adding row by globally unique identifier field for table "a"."post" (with smart comments: @name query). You can rename this table via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.';] +`; + +exports[`table naming clash - subscription 1`] = ` +[Error: A type naming conflict has occurred - two entities have tried to define the same type 'Subscription'. + + The first entity was: + + Adding table type for table "a"."post" (with smart comments: @name subscription). You can rename the table's GraphQL type via: +  +  COMMENT ON TABLE "a"."post" IS E'@name newNameHere\\nRest of existing \\'comment\\' \\nhere.'; + + The second entity was: + + graphile-build built-in (root subscription type)] +`; diff --git a/packages/postgraphile-core/__tests__/integration/extend-errors.test.js b/packages/postgraphile-core/__tests__/integration/extend-errors.test.js new file mode 100644 index 000000000..0f4577114 --- /dev/null +++ b/packages/postgraphile-core/__tests__/integration/extend-errors.test.js @@ -0,0 +1,142 @@ +const { withPgClient } = require("../helpers"); +const { createPostGraphileSchema } = require("../.."); +// eslint-disable-next-line no-unused-vars +const { printSchema } = require("graphql"); + +function check(description, sql) { + test(description, async () => { + let error; + // eslint-disable-next-line no-unused-vars + let schema; + await withPgClient(async pgClient => { + await pgClient.query(sql); + try { + schema = await createPostGraphileSchema(pgClient, ["a", "b", "c"], { + graphileBuildOptions: { + dontSwallowErrors: true, + }, + simpleCollections: "both", + appendPlugins: [ + function DummySubPlugin(builder) { + builder.hook( + "GraphQLObjectType:fields", + (fields, build, context) => { + if (!context.scope.isRootSubscription) { + return fields; + } + return build.extend(fields, { + mySub: { + type: build.graphql.GraphQLInt, + }, + }); + } + ); + }, + ], + }); + } catch (e) { + error = e; + } + }); + // Debugging + if (!error) { + // eslint-disable-next-line no-console + // console.error(printSchema(schema)); + } + expect(error).toBeTruthy(); + expect(error).toMatchSnapshot(); + }); +} + +check( + "simple collections naming clash", + ` + comment on constraint post_author_id_fkey on a.post is E'@foreignFieldName clash\nRest of existing ''comment'' \nhere.'; + ` +); + +check( + "table naming clash - direct", + ` + comment on table a.post is E'@name person\nRest of existing ''comment'' \nhere.'; + ` +); + +check( + "table naming clash - condition", + ` + comment on table a.post is E'@name person_condition\nRest of existing ''comment'' \nhere.'; + ` +); + +check( + "table naming clash - order", + ` + comment on table a.post is E'@name people_order_by\nRest of existing ''comment'' \nhere.'; + ` +); + +check( + "table naming clash - query", + ` + comment on table a.post is E'@name query\nRest of existing ''comment'' \nhere.'; + ` +); + +check( + "table naming clash - mutation", + ` + comment on table a.post is E'@name mutation\nRest of existing ''comment'' \nhere.'; + ` +); + +check( + "table naming clash - subscription", + ` + comment on table a.post is E'@name subscription\nRest of existing ''comment'' \nhere.'; + ` +); + +check( + "function naming clash - payload", + ` + comment on table a.post is E'@name q_edge\nRest of existing ''comment'' \nhere.'; + comment on function c.int_set_query(int, int, int) is E'@name q\nRest of existing ''comment'' \nhere.'; + ` +); +check( + "function naming clash - nodeId", + ` + comment on function c.int_set_query(int, int, int) is E'@name nodeId\nRest of existing ''comment'' \nhere.'; + ` +); +check( + "function naming clash - createPost", + ` + comment on function a.mutation_text_array() is E'@name createPost\nRest of existing ''comment'' \nhere.'; + ` +); +check( + "function naming clash - updatePost", + ` + comment on function a.mutation_text_array() is E'@name updatePost\nRest of existing ''comment'' \nhere.'; + ` +); +check( + "function naming clash - deletePost", + ` + comment on function a.mutation_text_array() is E'@name deletePost\nRest of existing ''comment'' \nhere.'; + ` +); +check( + "function naming clash - computed", + ` + comment on function c.edge_case_computed(c.edge_case) is E'@fieldName rowId\nRest of existing ''comment'' \nhere.'; + ` +); +check( + "column naming clash - rename", + ` + comment on column c.edge_case.row_id is E'@name wontCastEasy\nRest of existing ''comment'' \nhere.'; + ` +); diff --git a/packages/postgraphile-core/scripts/test b/packages/postgraphile-core/scripts/test index 724882ae4..0be3ca70e 100755 --- a/packages/postgraphile-core/scripts/test +++ b/packages/postgraphile-core/scripts/test @@ -1,6 +1,8 @@ #!/bin/bash set -e +export FORCE_COLOR=1 + if [ -x ".env" ]; then set -a . ./.env