graphile-build-pg
Version:
Build a GraphQL schema by reflection over a PostgreSQL schema. Easy to customize since it's built with plugins on graphile-build
885 lines (856 loc) • 30.5 kB
Flow
// @flow
import * as sql from "pg-sql2";
import type { Plugin } from "graphile-build";
import { version } from "../../package.json";
import type {
PgProc,
PgType,
PgClass,
PgAttribute,
PgConstraint,
PgEntity,
} from "./PgIntrospectionPlugin";
import pgField from "./pgField";
import queryFromResolveDataFactory from "../queryFromResolveDataFactory";
import addStartEndCursor from "./addStartEndCursor";
import baseOmit, {
CREATE,
READ,
UPDATE,
DELETE,
ALL,
MANY,
ORDER,
FILTER,
EXECUTE,
} from "../omit";
import makeProcField, { procFieldDetails } from "./makeProcField";
import { getComputedColumnDetails } from "./PgComputedColumnsPlugin";
import parseIdentifier from "../parseIdentifier";
import viaTemporaryTable from "./viaTemporaryTable";
import chalk from "chalk";
import pickBy from "lodash/pickBy";
import PgLiveProvider from "../PgLiveProvider";
import pgPrepareAndRun from "../pgPrepareAndRun";
import { formatSQLForDebugging } from "./debugSql";
const defaultPgColumnFilter = (_attr, _build, _context) => true;
type Keys = Array<{
column: string,
table: string,
schema: ?string,
}>;
const identity = _ => _;
export function preventEmptyResult<
// eslint-disable-next-line flowtype/no-weak-types
O: { [key: string]: (...args: Array<any>) => string }
>(obj: O): $ObjMap<O, <V>(V) => V> {
return Object.keys(obj).reduce((memo, key) => {
const fn = obj[key];
memo[key] = function (...args) {
const result = fn.apply(this, args);
if (typeof result !== "string" || result.length === 0) {
const stringifiedArgs = require("util").inspect(args);
throw new Error(
`Inflector for '${key}' returned '${String(
result
)}'; expected non-empty string\n` +
`See: https://github.com/graphile/graphile-engine/blob/master/packages/graphile-build-pg/src/plugins/PgBasicsPlugin.js\n` +
`Arguments passed to ${key}:\n${stringifiedArgs}`
);
}
return result;
};
return memo;
}, {});
}
const omitWithRBACChecks =
omit =>
(
entity: PgProc | PgClass | PgAttribute | PgConstraint,
permission: string
) => {
const ORDINARY_TABLE = "r";
const VIEW = "v";
const MATERIALIZED_VIEW = "m";
const isTableLike = entity =>
entity &&
entity.kind === "class" &&
(entity.classKind === ORDINARY_TABLE ||
entity.classKind === VIEW ||
entity.classKind === MATERIALIZED_VIEW);
if (entity.kind === "procedure") {
if (permission === EXECUTE && !entity.aclExecutable) {
return true;
}
} else if (entity.kind === "class" && isTableLike(entity)) {
const tableEntity: PgClass = entity;
if (
(permission === READ || permission === ALL || permission === MANY) &&
!tableEntity.aclSelectable &&
!tableEntity.attributes.some(attr => attr.aclSelectable)
) {
return true;
} else if (
permission === CREATE &&
!tableEntity.aclInsertable &&
!tableEntity.attributes.some(attr => attr.aclInsertable)
) {
return true;
} else if (
permission === UPDATE &&
!tableEntity.aclUpdatable &&
!tableEntity.attributes.some(attr => attr.aclUpdatable)
) {
return true;
} else if (permission === DELETE && !tableEntity.aclDeletable) {
return true;
}
} else if (entity.kind === "attribute" && isTableLike(entity.class)) {
const attributeEntity: PgAttribute = entity;
const klass = attributeEntity.class;
// Have we got *any* permissions on the table?
if (
klass.aclSelectable ||
klass.attributes.some(attr => attr.aclSelectable)
) {
// Yes; this is a regular table; omit if RBAC permissions tell us to.
if (
(permission === READ ||
permission === FILTER ||
permission === ORDER) &&
!attributeEntity.aclSelectable
) {
return true;
} else if (permission === CREATE && !attributeEntity.aclInsertable) {
return true;
} else if (permission === UPDATE && !attributeEntity.aclUpdatable) {
return true;
}
} else {
// No permissions on the table at all, so normal connections will skip
// over it. Thus we must be being exposed via a security definer function
// or similar, so we should expose all fields except those that are
// explicitly @omit-ed.
}
}
return omit(entity, permission);
};
const omitUnindexed =
(omit, hideIndexWarnings) =>
(
entity: PgProc | PgClass | PgAttribute | PgConstraint,
permission: string
) => {
if (
entity.kind === "attribute" &&
!entity.isIndexed &&
(permission === "filter" || permission === "order")
) {
return true;
}
if (
entity.kind === "constraint" &&
entity.type === "f" &&
!entity.isIndexed &&
permission === "read"
) {
const klass = entity.class;
if (klass) {
const shouldOutputWarning =
// $FlowFixMe
!entity._omitUnindexedReadWarningGiven && !hideIndexWarnings;
if (shouldOutputWarning) {
// $FlowFixMe
entity._omitUnindexedReadWarningGiven = true;
// eslint-disable-next-line no-console
console.log(
"%s",
`Disabled 'read' permission for ${describePgEntity(
entity
)} because it isn't indexed. For more information see https://graphile.org/postgraphile/best-practices/ To fix, perform\n\n CREATE INDEX ON ${`"${klass.namespaceName}"."${klass.name}"`}("${entity.keyAttributes
.map(a => a.name)
.join('", "')}");`
);
}
}
return true;
}
return omit(entity, permission);
};
function describePgEntity(entity: PgEntity, 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 ${
typeof entity.id === "string" ? `oid '${entity.id}'` : ""
}`;
}
function sqlCommentByAddingTags(entity, tagsToAdd) {
// NOTE: this function is NOT intended to be SQL safe; it's for
// displaying in error messages. Nonetheless if you find issues with
// SQL compatibility, please send a PR or issue.
// Ref: https://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-BACKSLASH-TABLE
const escape = str =>
str.replace(
/['\\\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 = {
...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.green : 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 if (entity.classKind === "c") {
sqlThing = `TYPE ${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};`;
}
export default (function PgBasicsPlugin(
builder,
{
pgStrictFunctions = false,
pgColumnFilter = defaultPgColumnFilter,
pgIgnoreRBAC = false,
pgIgnoreIndexes = true, // TODO:v5: change this to false
pgHideIndexWarnings = false,
pgLegacyJsonUuid = false, // TODO:v5: remove this
pgAugmentIntrospectionResults,
}
) {
let pgOmit = baseOmit;
if (!pgIgnoreRBAC) {
pgOmit = omitWithRBACChecks(pgOmit);
}
if (!pgIgnoreIndexes) {
pgOmit = omitUnindexed(pgOmit, pgHideIndexWarnings);
}
builder.hook(
"build",
build => {
build.versions["graphile-build-pg"] = version;
build.liveCoordinator.registerProvider(new PgLiveProvider());
return build.extend(build, {
graphileBuildPgVersion: version,
pgSql: sql,
pgStrictFunctions,
pgColumnFilter,
// TODO:v5: remove this workaround
// BEWARE: this may be overridden in PgIntrospectionPlugin for PG < 9.5
pgQueryFromResolveData: queryFromResolveDataFactory(),
pgAddStartEndCursor: addStartEndCursor,
pgOmit,
pgMakeProcField: makeProcField,
pgProcFieldDetails: procFieldDetails,
pgGetComputedColumnDetails: getComputedColumnDetails,
pgParseIdentifier: parseIdentifier,
pgViaTemporaryTable: viaTemporaryTable,
describePgEntity,
pgField,
sqlCommentByAddingTags,
pgPrepareAndRun,
pgAugmentIntrospectionResults,
formatSQLForDebugging,
});
},
["PgBasics"]
);
builder.hook(
"inflection",
(inflection, build) => {
// TODO:v5: move this to postgraphile-core
const oldBuiltin = inflection.builtin;
inflection.builtin = function (name) {
if (pgLegacyJsonUuid && name === "JSON") return "Json";
if (pgLegacyJsonUuid && name === "UUID") return "Uuid";
return oldBuiltin.call(this, name);
};
return build.extend(
inflection,
preventEmptyResult({
// These helpers are passed GraphQL type names as strings
conditionType(typeName: string) {
return this.upperCamelCase(`${typeName}-condition`);
},
inputType(typeName: string) {
return this.upperCamelCase(`${typeName}-input`);
},
rangeBoundType(typeName: string) {
return this.upperCamelCase(`${typeName}-range-bound`);
},
rangeType(typeName: string) {
return this.upperCamelCase(`${typeName}-range`);
},
patchType(typeName: string) {
return this.upperCamelCase(`${typeName}-patch`);
},
baseInputType(typeName: string) {
return this.upperCamelCase(`${typeName}-base-input`);
},
patchField(itemName: string) {
return this.camelCase(`${itemName}-patch`);
},
orderByType(typeName: string) {
return this.upperCamelCase(`${this.pluralize(typeName)}-order-by`);
},
edge(typeName: string) {
return this.upperCamelCase(`${this.pluralize(typeName)}-edge`);
},
connection(typeName: string) {
return this.upperCamelCase(
`${this.pluralize(typeName)}-connection`
);
},
// These helpers handle overrides via smart comments. They should only
// be used in other inflectors, hence the underscore prefix.
//
// IMPORTANT: do NOT do case transforms here, because detail can be
// lost, e.g.
// `constantCase(camelCase('foo_1')) !== constantCase('foo_1')`
_functionName(proc: PgProc) {
return this.coerceToGraphQLName(proc.tags.name || proc.name);
},
_typeName(type: PgType) {
// 'type' introspection result
return this.coerceToGraphQLName(type.tags.name || type.name);
},
_tableName(table: PgClass) {
return this.coerceToGraphQLName(
table.tags.name || table.type.tags.name || table.name
);
},
_singularizedTableName(table: PgClass): string {
return this.singularize(this._tableName(table)).replace(
/.(?:(?:[_-]i|I)nput|(?:[_-]p|P)atch)$/,
"$&_record"
);
},
_columnName(attr: PgAttribute, _options?: { skipRowId?: boolean }) {
return this.coerceToGraphQLName(attr.tags.name || attr.name);
},
// From here down, functions are passed database introspection results
enumType(type: PgType) {
if (type.tags.enumName) {
return type.tags.enumName;
}
return this.upperCamelCase(this._typeName(type));
},
argument(name: ?string, index: number) {
return this.coerceToGraphQLName(
this.camelCase(name || `arg${index}`)
);
},
orderByEnum(columnName, ascending) {
return this.constantCase(
`${columnName}_${ascending ? "asc" : "desc"}`
);
},
orderByColumnEnum(attr: PgAttribute, ascending: boolean) {
const columnName = this._columnName(attr, {
skipRowId: true, // Because we messed up 😔
});
return this.orderByEnum(columnName, ascending);
},
orderByComputedColumnEnum(
pseudoColumnName: string,
proc: PgProc,
table: PgClass,
ascending: boolean
) {
const columnName = this.computedColumn(
pseudoColumnName,
proc,
table
);
return this.orderByEnum(columnName, ascending);
},
domainType(type: PgType) {
return this.upperCamelCase(this._typeName(type));
},
enumName(inValue: string) {
let value = inValue;
if (value === "") {
return "_EMPTY_";
}
// Some enums use asterisks to signify wildcards - this might be for
// 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;
},
tableNode(table: PgClass) {
return this.camelCase(this._singularizedTableName(table));
},
tableFieldName(table: PgClass) {
return this.camelCase(this._singularizedTableName(table));
},
allRows(table: PgClass) {
return this.camelCase(
`all-${this.pluralize(this._singularizedTableName(table))}`
);
},
allRowsSimple(table: PgClass) {
return this.camelCase(
`all-${this.pluralize(this._singularizedTableName(table))}-list`
);
},
functionMutationName(proc: PgProc) {
return this.camelCase(this._functionName(proc));
},
functionMutationResultFieldName(
proc: PgProc,
gqlType,
plural: boolean = false,
outputArgNames: Array<string> = []
) {
if (proc.tags.resultFieldName) {
return proc.tags.resultFieldName;
}
let name;
if (outputArgNames.length === 1 && outputArgNames[0] !== "") {
name = this.camelCase(outputArgNames[0]);
} else if (gqlType.name === "Int") {
name = "integer";
} else if (gqlType.name === "Float") {
name = "float";
} else if (gqlType.name === "Boolean") {
name = "boolean";
} else if (gqlType.name === "String") {
name = "string";
} else if (proc.returnTypeId === "2249") {
// returns a record type
name = "result";
} else {
name = this.camelCase(gqlType.name);
}
return plural ? this.pluralize(name) : name;
},
functionQueryName(proc: PgProc) {
return this.camelCase(this._functionName(proc));
},
functionQueryNameList(proc: PgProc) {
return this.camelCase(`${this._functionName(proc)}-list`);
},
functionPayloadType(proc: PgProc) {
return this.upperCamelCase(`${this._functionName(proc)}-payload`);
},
functionInputType(proc: PgProc) {
return this.upperCamelCase(`${this._functionName(proc)}-input`);
},
functionOutputFieldName(
proc: PgProc,
outputArgName: string,
index: number
) {
return this.argument(outputArgName, index);
},
tableType(table: PgClass) {
return this.upperCamelCase(this._singularizedTableName(table));
},
column(attr: PgAttribute) {
return this.camelCase(this._columnName(attr));
},
computedColumn(
pseudoColumnName: string,
proc: PgProc,
_table: PgClass
) {
return proc.tags.fieldName || this.camelCase(pseudoColumnName);
},
computedColumnList(
pseudoColumnName: string,
proc: PgProc,
_table: PgClass
) {
return proc.tags.fieldName
? proc.tags.fieldName + "List"
: this.camelCase(`${pseudoColumnName}-list`);
},
singleRelationByKeys(
detailedKeys: Keys,
table: PgClass,
_foreignTable: PgClass,
constraint: PgConstraint
) {
if (constraint.tags.fieldName) {
return constraint.tags.fieldName;
}
return this.camelCase(
`${this._singularizedTableName(table)}-by-${detailedKeys
.map(key => this.column(key))
.join("-and-")}`
);
},
singleRelationByKeysBackwards(
detailedKeys: Keys,
table: PgClass,
_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,
_foreignTable: PgClass,
constraint: PgConstraint
) {
if (constraint.tags.foreignFieldName) {
return constraint.tags.foreignFieldName;
}
return this.camelCase(
`${this.pluralize(
this._singularizedTableName(table)
)}-by-${detailedKeys.map(key => this.column(key)).join("-and-")}`
);
},
manyRelationByKeysSimple(
detailedKeys: Keys,
table: PgClass,
_foreignTable: PgClass,
constraint: PgConstraint
) {
if (constraint.tags.foreignSimpleFieldName) {
return constraint.tags.foreignSimpleFieldName;
}
if (constraint.tags.foreignFieldName) {
return constraint.tags.foreignFieldName;
}
return this.camelCase(
`${this.pluralize(
this._singularizedTableName(table)
)}-by-${detailedKeys
.map(key => this.column(key))
.join("-and-")}-list`
);
},
rowByUniqueKeys(
detailedKeys: Keys,
table: PgClass,
constraint: PgConstraint
) {
if (constraint.tags.fieldName) {
return constraint.tags.fieldName;
}
return this.camelCase(
`${this._singularizedTableName(table)}-by-${detailedKeys
.map(key => this.column(key))
.join("-and-")}`
);
},
updateByKeys(
detailedKeys: Keys,
table: PgClass,
constraint: PgConstraint
) {
if (constraint.tags.updateFieldName) {
return constraint.tags.updateFieldName;
}
return this.camelCase(
`update-${this._singularizedTableName(table)}-by-${detailedKeys
.map(key => this.column(key))
.join("-and-")}`
);
},
deleteByKeys(
detailedKeys: Keys,
table: PgClass,
constraint: PgConstraint
) {
if (constraint.tags.deleteFieldName) {
return constraint.tags.deleteFieldName;
}
return this.camelCase(
`delete-${this._singularizedTableName(table)}-by-${detailedKeys
.map(key => this.column(key))
.join("-and-")}`
);
},
updateByKeysInputType(
detailedKeys: Keys,
table: PgClass,
constraint: PgConstraint
) {
if (constraint.tags.updateFieldName) {
return this.upperCamelCase(
`${constraint.tags.updateFieldName}-input`
);
}
return this.upperCamelCase(
`update-${this._singularizedTableName(table)}-by-${detailedKeys
.map(key => this.column(key))
.join("-and-")}-input`
);
},
deleteByKeysInputType(
detailedKeys: Keys,
table: PgClass,
constraint: PgConstraint
) {
if (constraint.tags.deleteFieldName) {
return this.upperCamelCase(
`${constraint.tags.deleteFieldName}-input`
);
}
return this.upperCamelCase(
`delete-${this._singularizedTableName(table)}-by-${detailedKeys
.map(key => this.column(key))
.join("-and-")}-input`
);
},
updateNode(table: PgClass) {
return this.camelCase(
`update-${this._singularizedTableName(table)}`
);
},
deleteNode(table: PgClass) {
return this.camelCase(
`delete-${this._singularizedTableName(table)}`
);
},
deletedNodeId(table: PgClass) {
return this.camelCase(`deleted-${this.singularize(table.name)}-id`);
},
updateNodeInputType(table: PgClass) {
return this.upperCamelCase(
`update-${this._singularizedTableName(table)}-input`
);
},
deleteNodeInputType(table: PgClass) {
return this.upperCamelCase(
`delete-${this._singularizedTableName(table)}-input`
);
},
edgeField(table: PgClass) {
return this.camelCase(`${this._singularizedTableName(table)}-edge`);
},
recordFunctionReturnType(proc: PgProc) {
return (
proc.tags.resultTypeName ||
this.upperCamelCase(`${this._functionName(proc)}-record`)
);
},
recordFunctionConnection(proc: PgProc) {
return this.upperCamelCase(
`${this._functionName(proc)}-connection`
);
},
recordFunctionEdge(proc: PgProc) {
return this.upperCamelCase(
`${this.singularize(this._functionName(proc))}-edge`
);
},
scalarFunctionConnection(proc: PgProc) {
return this.upperCamelCase(
`${this._functionName(proc)}-connection`
);
},
scalarFunctionEdge(proc: PgProc) {
return this.upperCamelCase(
`${this.singularize(this._functionName(proc))}-edge`
);
},
createField(table: PgClass) {
return this.camelCase(
`create-${this._singularizedTableName(table)}`
);
},
createInputType(table: PgClass) {
return this.upperCamelCase(
`create-${this._singularizedTableName(table)}-input`
);
},
createPayloadType(table: PgClass) {
return this.upperCamelCase(
`create-${this._singularizedTableName(table)}-payload`
);
},
updatePayloadType(table: PgClass) {
return this.upperCamelCase(
`update-${this._singularizedTableName(table)}-payload`
);
},
deletePayloadType(table: PgClass) {
return this.upperCamelCase(
`delete-${this._singularizedTableName(table)}-payload`
);
},
}),
"Default inflectors from PgBasicsPlugin. You can override these with `makeAddInflectorsPlugin(..., true)`."
);
},
["PgBasics"]
);
}: Plugin);