diff --git a/packages/graphile-build-pg/src/index.js b/packages/graphile-build-pg/src/index.js index bc12423ff..4701c1add 100644 --- a/packages/graphile-build-pg/src/index.js +++ b/packages/graphile-build-pg/src/index.js @@ -34,6 +34,8 @@ import PgMutationUpdateDeletePlugin from "./plugins/PgMutationUpdateDeletePlugin import PgMutationProceduresPlugin from "./plugins/PgMutationProceduresPlugin"; import PgMutationPayloadEdgePlugin from "./plugins/PgMutationPayloadEdgePlugin"; +import * as inflections from "./inflections"; + import parseIdentifier from "./parseIdentifier"; import omit from "./omit"; export { formatSQLForDebugging } from "./plugins/debugSql"; @@ -77,6 +79,8 @@ export const defaultPlugins = [ PgMutationPayloadEdgePlugin, ]; +export { inflections }; + export { PgBasicsPlugin, PgIntrospectionPlugin, diff --git a/packages/graphile-build-pg/src/inflections.js b/packages/graphile-build-pg/src/inflections.js new file mode 100644 index 000000000..97b8bb1e3 --- /dev/null +++ b/packages/graphile-build-pg/src/inflections.js @@ -0,0 +1,356 @@ +/* THIS ENTIRE FILE IS DEPRECATED. DO NOT USE THIS. DO NOT EDIT THIS. */ +// @flow +import { + upperCamelCase, + camelCase, + constantCase, + pluralize, + singularize, +} from "graphile-build"; + +import { preventEmptyResult } from "./plugins/PgBasicsPlugin"; + +const outputMessages = []; + +// eslint-disable-next-line flowtype/no-weak-types +function deprecate(fn: (...input: Array) => string, message: string) { + if (typeof fn !== "function") { + return fn; + } + return function(...args) { + if (outputMessages.indexOf(message) === -1) { + outputMessages.push(message); + // eslint-disable-next-line no-console + console.warn(new Error(message)); + } + return fn.apply(this, args); + }; +} + +function deprecateEverything(obj: { + // eslint-disable-next-line flowtype/no-weak-types + [string]: (...input: Array) => string, +}) { + return Object.keys(obj).reduce((memo, key) => { + memo[key] = deprecate( + obj[key], + `Something (probably a plugin) called the old inflection system (inflector: '${key}'). This system has been deprecated since 4.0.0-beta.6 (4th May 2018) and is not used internally so using it may cause inconsistencies, instead please use the plugin-capable inflection system https://www.graphile.org/postgraphile/inflection/` + ); + return memo; + }, {}); +} + +type Keys = Array<{ + column: string, + table: string, + schema: ?string, +}>; + +type InflectorUtils = {| + constantCase: string => string, + camelCase: string => string, + upperCamelCase: string => string, + pluralize: string => string, + singularize: string => string, +|}; + +export const defaultUtils: InflectorUtils = { + constantCase, + camelCase, + upperCamelCase, + pluralize, + singularize, +}; + +export type Inflector = { + // TODO: tighten this up! + // eslint-disable-next-line flowtype/no-weak-types + [string]: (...input: Array) => string, +}; + +export const newInflector = ( + overrides: ?{ [string]: () => string } = undefined, + { + constantCase, + camelCase, + upperCamelCase, + pluralize, + singularize, + }: InflectorUtils = defaultUtils +): Inflector => { + function singularizeTable(tableName: string): string { + return singularize(tableName).replace( + /.(?:(?:[_-]i|I)nput|(?:[_-]p|P)atch)$/, + "$&_record" + ); + } + + return deprecateEverything( + preventEmptyResult( + Object.assign( + { + pluralize, + argument(name: ?string, index: number) { + return camelCase(name || `arg${index}`); + }, + orderByType(typeName: string) { + return upperCamelCase(`${pluralize(typeName)}-order-by`); + }, + orderByEnum( + name: string, + ascending: boolean, + _table: string, + _schema: ?string + ) { + return constantCase(`${name}_${ascending ? "asc" : "desc"}`); + }, + domainType(name: string) { + return upperCamelCase(name); + }, + enumName(inValue: string) { + let value = inValue; + + if (value === "") { + return "_EMPTY_"; + } + + // Some enums use asterisks to signify wildcards - this might be for + // the whole item, or prefixes/suffixes, or even in the middle. This + // is provided on a best efforts basis, if it doesn't suit your + // purposes then please pass a custom inflector as mentioned below. + value = value + .replace(/\*/g, "_ASTERISK_") + .replace(/^(_?)_+ASTERISK/, "$1ASTERISK") + .replace(/ASTERISK_(_?)_*$/, "ASTERISK$1"); + + // This is a best efforts replacement for common symbols that you + // might find in enums. Generally we only support enums that are + // alphanumeric, if these replacements don't work for you, you should + // pass a custom inflector that replaces this `enumName` method + // with one of your own chosing. + value = + { + // SQL comparison operators + ">": "GREATER_THAN", + ">=": "GREATER_THAN_OR_EQUAL", + "=": "EQUAL", + "!=": "NOT_EQUAL", + "<>": "DIFFERENT", + "<=": "LESS_THAN_OR_EQUAL", + "<": "LESS_THAN", + + // PostgreSQL LIKE shortcuts + "~~": "LIKE", + "~~*": "ILIKE", + "!~~": "NOT_LIKE", + "!~~*": "NOT_ILIKE", + + // '~' doesn't necessarily represent regexps, but the three + // operators following it likely do, so we'll use the word TILDE + // in all for consistency. + "~": "TILDE", + "~*": "TILDE_ASTERISK", + "!~": "NOT_TILDE", + "!~*": "NOT_TILDE_ASTERISK", + + // A number of other symbols where we're not sure of their + // meaning. We give them common generic names so that they're + // suitable for multiple purposes, e.g. favouring 'PLUS' over + // 'ADDITION' and 'DOT' over 'FULL_STOP' + "%": "PERCENT", + "+": "PLUS", + "-": "MINUS", + "/": "SLASH", + "\\": "BACKSLASH", + _: "UNDERSCORE", + "#": "POUND", + "£": "STERLING", + $: "DOLLAR", + "&": "AMPERSAND", + "@": "AT", + "'": "APOSTROPHE", + '"': "QUOTE", + "`": "BACKTICK", + ":": "COLON", + ";": "SEMICOLON", + "!": "EXCLAMATION_POINT", + "?": "QUESTION_MARK", + ",": "COMMA", + ".": "DOT", + "^": "CARET", + "|": "BAR", + "[": "OPEN_BRACKET", + "]": "CLOSE_BRACKET", + "(": "OPEN_PARENTHESIS", + ")": "CLOSE_PARENTHESIS", + "{": "OPEN_BRACE", + "}": "CLOSE_BRACE", + }[value] || value; + return value; + }, + enumType(name: string) { + return upperCamelCase(name); + }, + conditionType(typeName: string) { + return upperCamelCase(`${typeName}-condition`); + }, + inputType(typeName: string) { + return upperCamelCase(`${typeName}-input`); + }, + rangeBoundType(typeName: string) { + return upperCamelCase(`${typeName}-range-bound`); + }, + rangeType(typeName: string) { + return upperCamelCase(`${typeName}-range`); + }, + patchType(typeName: string) { + return upperCamelCase(`${typeName}-patch`); + }, + patchField(itemName: string) { + return camelCase(`${itemName}-patch`); + }, + tableName(name: string, _schema: ?string) { + return camelCase(singularizeTable(name)); + }, + tableNode(name: string, _schema: ?string) { + return camelCase(singularizeTable(name)); + }, + allRows(name: string, schema: ?string) { + return camelCase( + `all-${this.pluralize(this.tableName(name, schema))}` + ); + }, + functionName(name: string, _schema: ?string) { + return camelCase(name); + }, + functionPayloadType(name: string, _schema: ?string) { + return upperCamelCase(`${name}-payload`); + }, + functionInputType(name: string, _schema: ?string) { + return upperCamelCase(`${name}-input`); + }, + tableType(name: string, schema: ?string) { + return upperCamelCase(this.tableName(name, schema)); + }, + column(name: string, _table: string, _schema: ?string) { + return camelCase(name); + }, + singleRelationByKeys( + detailedKeys: Keys, + table: string, + schema: ?string + ) { + return camelCase( + `${this.tableName(table, schema)}-by-${detailedKeys + .map(key => this.column(key.column, key.table, key.schema)) + .join("-and-")}` + ); + }, + rowByUniqueKeys(detailedKeys: Keys, table: string, schema: ?string) { + return camelCase( + `${this.tableName(table, schema)}-by-${detailedKeys + .map(key => this.column(key.column, key.table, key.schema)) + .join("-and-")}` + ); + }, + updateByKeys(detailedKeys: Keys, table: string, schema: ?string) { + return camelCase( + `update-${this.tableName(table, schema)}-by-${detailedKeys + .map(key => this.column(key.column, key.table, key.schema)) + .join("-and-")}` + ); + }, + deleteByKeys(detailedKeys: Keys, table: string, schema: ?string) { + return camelCase( + `delete-${this.tableName(table, schema)}-by-${detailedKeys + .map(key => this.column(key.column, key.table, key.schema)) + .join("-and-")}` + ); + }, + updateNode(name: string, _schema: ?string) { + return camelCase(`update-${singularizeTable(name)}`); + }, + deleteNode(name: string, _schema: ?string) { + return camelCase(`delete-${singularizeTable(name)}`); + }, + updateByKeysInputType( + detailedKeys: Keys, + name: string, + _schema: ?string + ) { + return upperCamelCase( + `update-${singularizeTable(name)}-by-${detailedKeys + .map(key => this.column(key.column, key.table, key.schema)) + .join("-and-")}-input` + ); + }, + deleteByKeysInputType( + detailedKeys: Keys, + name: string, + _schema: ?string + ) { + return upperCamelCase( + `delete-${singularizeTable(name)}-by-${detailedKeys + .map(key => this.column(key.column, key.table, key.schema)) + .join("-and-")}-input` + ); + }, + updateNodeInputType(name: string, _schema: ?string) { + return upperCamelCase(`update-${singularizeTable(name)}-input`); + }, + deleteNodeInputType(name: string, _schema: ?string) { + return upperCamelCase(`delete-${singularizeTable(name)}-input`); + }, + manyRelationByKeys( + detailedKeys: Keys, + table: string, + schema: ?string, + _foreignTable: string, + _foreignSchema: ?string + ) { + return camelCase( + `${this.pluralize( + this.tableName(table, schema) + )}-by-${detailedKeys + .map(key => this.column(key.column, key.table, key.schema)) + .join("-and-")}` + ); + }, + edge(typeName: string) { + return upperCamelCase(`${pluralize(typeName)}-edge`); + }, + edgeField(name: string, _schema: ?string) { + return camelCase(`${singularizeTable(name)}-edge`); + }, + connection(typeName: string) { + return upperCamelCase(`${this.pluralize(typeName)}-connection`); + }, + scalarFunctionConnection(procName: string, _procSchema: ?string) { + return upperCamelCase(`${procName}-connection`); + }, + scalarFunctionEdge(procName: string, _procSchema: ?string) { + return upperCamelCase(`${procName}-edge`); + }, + createField(name: string, _schema: ?string) { + return camelCase(`create-${singularizeTable(name)}`); + }, + createInputType(name: string, _schema: ?string) { + return upperCamelCase(`create-${singularizeTable(name)}-input`); + }, + createPayloadType(name: string, _schema: ?string) { + return upperCamelCase(`create-${singularizeTable(name)}-payload`); + }, + updatePayloadType(name: string, _schema: ?string) { + return upperCamelCase(`update-${singularizeTable(name)}-payload`); + }, + deletePayloadType(name: string, _schema: ?string) { + return upperCamelCase(`delete-${singularizeTable(name)}-payload`); + }, + }, + overrides + ) + ) + ); +}; + +export const defaultInflection = newInflector(); diff --git a/packages/postgraphile-core/src/index.ts b/packages/postgraphile-core/src/index.ts index 64488bc5c..320b4d38c 100644 --- a/packages/postgraphile-core/src/index.ts +++ b/packages/postgraphile-core/src/index.ts @@ -14,6 +14,8 @@ import { import { GraphQLSchema } from "graphql"; import { defaultPlugins as pgDefaultPlugins, + inflections, + Inflector, PgAttribute, formatSQLForDebugging, } from "graphile-build-pg"; @@ -32,17 +34,6 @@ export { export type mixed = {} | string | number | boolean | undefined | null; -function errorProxy(message: string) { - return new Proxy( - {}, - { - get: () => { - throw new Error(message); - }, - } - ); -} - const ensureValidPlugins = (name: string, arr: Array) => { if (!Array.isArray(arr)) { throw new Error(`Option '${name}' should be an array`); @@ -72,6 +63,7 @@ export interface PostGraphileCoreOptions { skipPlugins?: Array; jwtPgTypeIdentifier?: string; jwtSecret?: string; + inflector?: Inflector; // NO LONGER SUPPORTED! pgColumnFilter?: ( attr: mixed, build: Build, @@ -97,26 +89,30 @@ type PgConfig = Client | Pool | string; /* * BELOW HERE IS DEPRECATED!! */ +export { inflections }; -export const inflections = errorProxy( - "`inflections` was deprecated in v4.0.0-beta.7; instead please write an inflector plugin: https://www.graphile.org/postgraphile/inflection/" -); - -export const postGraphileBaseOverrides = errorProxy( - "Passing an inflector via PostGraphile options was deprecated in v4.0.0-beta.7; instead please write an inflector plugin: https://www.graphile.org/postgraphile/inflection/" -); +export const postGraphileBaseOverrides = { + enumName(value: string) { + return inflections.defaultUtils.constantCase( + inflections.defaultInflection.enumName(value) + ); + }, +}; -export const postGraphileClassicIdsOverrides = errorProxy( - "Passing an inflector via PostGraphile options was deprecated in v4.0.0-beta.7; instead please write an inflector plugin: https://www.graphile.org/postgraphile/inflection/" -); +export const postGraphileClassicIdsOverrides = { + column(name: string, _table: string, _schema?: string) { + return name === "id" ? "rowId" : inflections.defaultUtils.camelCase(name); + }, +}; -export const postGraphileInflection = errorProxy( - "Passing an inflector via PostGraphile options was deprecated in v4.0.0-beta.7; instead please write an inflector plugin: https://www.graphile.org/postgraphile/inflection/" +export const postGraphileInflection = inflections.newInflector( + postGraphileBaseOverrides ); -export const postGraphileClassicIdsInflection = errorProxy( - "Passing an inflector via PostGraphile options was deprecated in v4.0.0-beta.7; instead please write an inflector plugin: https://www.graphile.org/postgraphile/inflection/" -); +export const postGraphileClassicIdsInflection = inflections.newInflector({ + ...postGraphileBaseOverrides, + ...postGraphileClassicIdsOverrides, +}); /* * ABOVE HERE IS DEPRECATED. */ @@ -186,6 +182,7 @@ const getPostGraphileBuilder = async ( disableDefaultMutations, graphileBuildOptions, graphqlBuildOptions, // DEPRECATED! + inflector, // NO LONGER SUPPORTED! pgColumnFilter, viewUniqueKey, enableTags = true, @@ -296,6 +293,11 @@ const getPostGraphileBuilder = async ( ensureValidPlugins("prependPlugins", prependPlugins); ensureValidPlugins("appendPlugins", appendPlugins); ensureValidPlugins("skipPlugins", skipPlugins); + if (inflector) { + throw new Error( + "Custom inflector arguments are not supported, please use the inflector plugin API instead: https://www.graphile.org/postgraphile/inflection/" + ); + } const inflectionOverridePlugins = classicIds ? [PostGraphileInflectionPlugin, PostGraphileClassicIdsInflectionPlugin] : [PostGraphileInflectionPlugin]; @@ -334,9 +336,9 @@ const getPostGraphileBuilder = async ( pgSchemas: Array.isArray(schemas) ? schemas : [schemas], pgExtendedTypes: !!dynamicJson, pgColumnFilter: pgColumnFilter || (() => true), - pgInflection: errorProxy( - "The PostGraphile options `pgInflection` key was deprecated in v4.0.0-beta.7; instead please use `build.inflection` (and note that the call signatures are now simpler): https://www.graphile.org/postgraphile/inflection/" - ), + pgInflection: + inflector || + (classicIds ? postGraphileClassicIdsInflection : postGraphileInflection), nodeIdFieldName: nodeIdFieldName || (classicIds ? "id" : "nodeId"), pgJwtTypeIdentifier: jwtPgTypeIdentifier, pgJwtSecret: jwtSecret,