UNPKG

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

580 lines (574 loc) 26.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; exports.preventEmptyResult = preventEmptyResult; var sql = _interopRequireWildcard(require("pg-sql2")); var _package = require("../../package.json"); var _pgField = _interopRequireDefault(require("./pgField")); var _queryFromResolveDataFactory = _interopRequireDefault(require("../queryFromResolveDataFactory")); var _addStartEndCursor = _interopRequireDefault(require("./addStartEndCursor")); var _omit = _interopRequireWildcard(require("../omit")); var _makeProcField = _interopRequireWildcard(require("./makeProcField")); var _PgComputedColumnsPlugin = require("./PgComputedColumnsPlugin"); var _parseIdentifier = _interopRequireDefault(require("../parseIdentifier")); var _viaTemporaryTable = _interopRequireDefault(require("./viaTemporaryTable")); var _chalk = _interopRequireDefault(require("chalk")); var _pickBy = _interopRequireDefault(require("lodash/pickBy")); var _PgLiveProvider = _interopRequireDefault(require("../PgLiveProvider")); var _pgPrepareAndRun = _interopRequireDefault(require("../pgPrepareAndRun")); var _debugSql = require("./debugSql"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } const defaultPgColumnFilter = (_attr, _build, _context) => true; const identity = _ => _; function preventEmptyResult(obj) { 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, permission) => { 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 === _omit.EXECUTE && !entity.aclExecutable) { return true; } } else if (entity.kind === "class" && isTableLike(entity)) { const tableEntity = entity; if ((permission === _omit.READ || permission === _omit.ALL || permission === _omit.MANY) && !tableEntity.aclSelectable && !tableEntity.attributes.some(attr => attr.aclSelectable)) { return true; } else if (permission === _omit.CREATE && !tableEntity.aclInsertable && !tableEntity.attributes.some(attr => attr.aclInsertable)) { return true; } else if (permission === _omit.UPDATE && !tableEntity.aclUpdatable && !tableEntity.attributes.some(attr => attr.aclUpdatable)) { return true; } else if (permission === _omit.DELETE && !tableEntity.aclDeletable) { return true; } } else if (entity.kind === "attribute" && isTableLike(entity.class)) { const attributeEntity = 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 === _omit.READ || permission === _omit.FILTER || permission === _omit.ORDER) && !attributeEntity.aclSelectable) { return true; } else if (permission === _omit.CREATE && !attributeEntity.aclInsertable) { return true; } else if (permission === _omit.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, permission) => { 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, includeAlias = true) { const getAlias = !includeAlias ? () => "" : () => { const tags = (0, _pickBy.default)(entity.tags, (value, key) => key === "name" || key.endsWith("Name")); if (Object.keys(tags).length) { return ` (with smart comments: ${_chalk.default.bold(Object.keys(tags).map(t => `@${t} ${tags[t]}`).join(" | "))})`; } return ""; }; try { if (entity.kind === "constraint") { return `constraint ${_chalk.default.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.default.bold(`"${entity.namespaceName}"."${entity.name}"`)}${getAlias()}`; } else if (entity.kind === "procedure") { return `function ${_chalk.default.bold(`"${entity.namespaceName}"."${entity.name}"(...args...)`)}${getAlias()}`; } else if (entity.kind === "attribute") { return `column ${_chalk.default.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.default.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};`; } var PgBasicsPlugin = 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 = _omit.default; if (!pgIgnoreRBAC) { pgOmit = omitWithRBACChecks(pgOmit); } if (!pgIgnoreIndexes) { pgOmit = omitUnindexed(pgOmit, pgHideIndexWarnings); } builder.hook("build", build => { build.versions["graphile-build-pg"] = _package.version; build.liveCoordinator.registerProvider(new _PgLiveProvider.default()); return build.extend(build, { graphileBuildPgVersion: _package.version, pgSql: sql, pgStrictFunctions, pgColumnFilter, // TODO:v5: remove this workaround // BEWARE: this may be overridden in PgIntrospectionPlugin for PG < 9.5 pgQueryFromResolveData: (0, _queryFromResolveDataFactory.default)(), pgAddStartEndCursor: _addStartEndCursor.default, pgOmit, pgMakeProcField: _makeProcField.default, pgProcFieldDetails: _makeProcField.procFieldDetails, pgGetComputedColumnDetails: _PgComputedColumnsPlugin.getComputedColumnDetails, pgParseIdentifier: _parseIdentifier.default, pgViaTemporaryTable: _viaTemporaryTable.default, describePgEntity, pgField: _pgField.default, sqlCommentByAddingTags, pgPrepareAndRun: _pgPrepareAndRun.default, pgAugmentIntrospectionResults, formatSQLForDebugging: _debugSql.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) { return this.upperCamelCase(`${typeName}-condition`); }, inputType(typeName) { return this.upperCamelCase(`${typeName}-input`); }, rangeBoundType(typeName) { return this.upperCamelCase(`${typeName}-range-bound`); }, rangeType(typeName) { return this.upperCamelCase(`${typeName}-range`); }, patchType(typeName) { return this.upperCamelCase(`${typeName}-patch`); }, baseInputType(typeName) { return this.upperCamelCase(`${typeName}-base-input`); }, patchField(itemName) { return this.camelCase(`${itemName}-patch`); }, orderByType(typeName) { return this.upperCamelCase(`${this.pluralize(typeName)}-order-by`); }, edge(typeName) { return this.upperCamelCase(`${this.pluralize(typeName)}-edge`); }, connection(typeName) { 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) { return this.coerceToGraphQLName(proc.tags.name || proc.name); }, _typeName(type) { // 'type' introspection result return this.coerceToGraphQLName(type.tags.name || type.name); }, _tableName(table) { return this.coerceToGraphQLName(table.tags.name || table.type.tags.name || table.name); }, _singularizedTableName(table) { return this.singularize(this._tableName(table)).replace(/.(?:(?:[_-]i|I)nput|(?:[_-]p|P)atch)$/, "$&_record"); }, _columnName(attr, _options) { return this.coerceToGraphQLName(attr.tags.name || attr.name); }, // From here down, functions are passed database introspection results enumType(type) { if (type.tags.enumName) { return type.tags.enumName; } return this.upperCamelCase(this._typeName(type)); }, argument(name, index) { return this.coerceToGraphQLName(this.camelCase(name || `arg${index}`)); }, orderByEnum(columnName, ascending) { return this.constantCase(`${columnName}_${ascending ? "asc" : "desc"}`); }, orderByColumnEnum(attr, ascending) { const columnName = this._columnName(attr, { skipRowId: true // Because we messed up 😔 }); return this.orderByEnum(columnName, ascending); }, orderByComputedColumnEnum(pseudoColumnName, proc, table, ascending) { const columnName = this.computedColumn(pseudoColumnName, proc, table); return this.orderByEnum(columnName, ascending); }, domainType(type) { return this.upperCamelCase(this._typeName(type)); }, enumName(inValue) { 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) { return this.camelCase(this._singularizedTableName(table)); }, tableFieldName(table) { return this.camelCase(this._singularizedTableName(table)); }, allRows(table) { return this.camelCase(`all-${this.pluralize(this._singularizedTableName(table))}`); }, allRowsSimple(table) { return this.camelCase(`all-${this.pluralize(this._singularizedTableName(table))}-list`); }, functionMutationName(proc) { return this.camelCase(this._functionName(proc)); }, functionMutationResultFieldName(proc, gqlType, plural = false, outputArgNames = []) { 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) { return this.camelCase(this._functionName(proc)); }, functionQueryNameList(proc) { return this.camelCase(`${this._functionName(proc)}-list`); }, functionPayloadType(proc) { return this.upperCamelCase(`${this._functionName(proc)}-payload`); }, functionInputType(proc) { return this.upperCamelCase(`${this._functionName(proc)}-input`); }, functionOutputFieldName(proc, outputArgName, index) { return this.argument(outputArgName, index); }, tableType(table) { return this.upperCamelCase(this._singularizedTableName(table)); }, column(attr) { return this.camelCase(this._columnName(attr)); }, computedColumn(pseudoColumnName, proc, _table) { return proc.tags.fieldName || this.camelCase(pseudoColumnName); }, computedColumnList(pseudoColumnName, proc, _table) { return proc.tags.fieldName ? proc.tags.fieldName + "List" : this.camelCase(`${pseudoColumnName}-list`); }, singleRelationByKeys(detailedKeys, table, _foreignTable, constraint) { 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, table, _foreignTable, constraint) { 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, table, _foreignTable, constraint) { 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, table, _foreignTable, constraint) { 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, table, constraint) { 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, table, constraint) { 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, table, constraint) { 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, table, constraint) { 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, table, constraint) { 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) { return this.camelCase(`update-${this._singularizedTableName(table)}`); }, deleteNode(table) { return this.camelCase(`delete-${this._singularizedTableName(table)}`); }, deletedNodeId(table) { return this.camelCase(`deleted-${this.singularize(table.name)}-id`); }, updateNodeInputType(table) { return this.upperCamelCase(`update-${this._singularizedTableName(table)}-input`); }, deleteNodeInputType(table) { return this.upperCamelCase(`delete-${this._singularizedTableName(table)}-input`); }, edgeField(table) { return this.camelCase(`${this._singularizedTableName(table)}-edge`); }, recordFunctionReturnType(proc) { return proc.tags.resultTypeName || this.upperCamelCase(`${this._functionName(proc)}-record`); }, recordFunctionConnection(proc) { return this.upperCamelCase(`${this._functionName(proc)}-connection`); }, recordFunctionEdge(proc) { return this.upperCamelCase(`${this.singularize(this._functionName(proc))}-edge`); }, scalarFunctionConnection(proc) { return this.upperCamelCase(`${this._functionName(proc)}-connection`); }, scalarFunctionEdge(proc) { return this.upperCamelCase(`${this.singularize(this._functionName(proc))}-edge`); }, createField(table) { return this.camelCase(`create-${this._singularizedTableName(table)}`); }, createInputType(table) { return this.upperCamelCase(`create-${this._singularizedTableName(table)}-input`); }, createPayloadType(table) { return this.upperCamelCase(`create-${this._singularizedTableName(table)}-payload`); }, updatePayloadType(table) { return this.upperCamelCase(`update-${this._singularizedTableName(table)}-payload`); }, deletePayloadType(table) { return this.upperCamelCase(`delete-${this._singularizedTableName(table)}-payload`); } }), "Default inflectors from PgBasicsPlugin. You can override these with `makeAddInflectorsPlugin(..., true)`."); }, ["PgBasics"]); }; exports.default = PgBasicsPlugin; //# sourceMappingURL=PgBasicsPlugin.js.map