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

828 lines (792 loc) 35.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.PgEntityKind = void 0; var _withPgClient = _interopRequireWildcard(require("../withPgClient")); var _utils = require("../utils"); var _fs = require("fs"); var _debug = _interopRequireDefault(require("debug")); var _chalk = _interopRequireDefault(require("chalk")); var _throttle = _interopRequireDefault(require("lodash/throttle")); var _flatMap = _interopRequireDefault(require("lodash/flatMap")); var _introspectionQuery = require("./introspectionQuery"); var pgSql = _interopRequireWildcard(require("pg-sql2")); var _package = require("../../package.json"); var _queryFromResolveDataFactory = _interopRequireDefault(require("../queryFromResolveDataFactory")); 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 debug = (0, _debug.default)("graphile-build-pg"); const WATCH_FIXTURES_PATH = `${__dirname}/../../res/watch-fixtures.sql`; // Ref: https://github.com/graphile/postgraphile/tree/master/src/postgres/introspection/object const fakeEnumIdentifier = (namespaceName, name, identifierExtra = "", list = false) => { const baseEnumIdentifier = `FAKE_ENUM_${namespaceName}_${name}${identifierExtra}`; if (list) { return `${baseEnumIdentifier}_list`; } else { return baseEnumIdentifier; } }; function readFile(filename, encoding) { return new Promise((resolve, reject) => { (0, _fs.readFile)(filename, encoding, (err, res) => { if (err) reject(err);else resolve(res); }); }); } const removeQuotes = str => { const trimmed = str.trim(); if (trimmed[0] === '"') { if (trimmed[trimmed.length - 1] !== '"') { throw new Error(`We failed to parse a quoted identifier '${str}'. Please avoid putting quotes or commas in smart comment identifiers (or file a PR to fix the parser).`); } return trimmed.slice(1, -1); } else { // PostgreSQL lower-cases unquoted columns, so we should too. return trimmed.toLowerCase(); } }; const parseSqlColumnArray = str => { if (!str) { throw new Error(`Cannot parse '${str}'`); } const parts = str.split(","); return parts.map(removeQuotes); }; const parseSqlColumnString = str => { if (!str) { throw new Error(`Cannot parse '${str}'`); } return removeQuotes(str); }; function parseConstraintSpec(rawSpec) { const [spec, ...tagComponents] = rawSpec.split(/\|/); const parsed = (0, _utils.parseTags)(tagComponents.join("\n")); return { spec, tags: parsed.tags, description: parsed.text }; } function smartCommentConstraints(introspectionResults) { const attributesByNames = (tbl, cols, debugStr) => { const attributes = introspectionResults.attribute.filter(a => a.classId === tbl.id).sort((a, b) => a.num - b.num); if (!cols) { const pk = introspectionResults.constraint.find(c => c.classId == tbl.id && c.type === "p"); if (pk) { return pk.keyAttributeNums.map(n => attributes.find(a => a.num === n)); } else { throw new Error(`No columns specified for '${tbl.namespaceName}.${tbl.name}' (oid: ${tbl.id}) and no PK found (${debugStr}).`); } } return cols.map(colName => { const attr = attributes.find(a => a.name === colName); if (!attr) { throw new Error(`Could not find attribute '${colName}' in '${tbl.namespaceName}.${tbl.name}'`); } return attr; }); }; // First: primary and unique keys introspectionResults.class.forEach(klass => { const namespace = introspectionResults.namespace.find(n => n.id === klass.namespaceId); if (!namespace) { return; } function addKey(key, isPrimary = false) { const tag = isPrimary ? "@primaryKey" : "@unique"; if (typeof key !== "string") { if (isPrimary) { throw new Error(`${tag} configuration of '${klass.namespaceName}.${klass.name}' is invalid; please specify just once "${tag} col1,col2"`); } throw new Error(`${tag} configuration of '${klass.namespaceName}.${klass.name}' is invalid; expected ${isPrimary ? "a string" : "a string or string array"} but found ${typeof key}`); } const { spec: keySpec, tags, description } = parseConstraintSpec(key); const columns = parseSqlColumnArray(keySpec); const attributes = attributesByNames(klass, columns, `${tag} ${key}`); if (isPrimary) { attributes.forEach(attr => { attr.tags.notNull = true; }); } const keyAttributeNums = attributes.map(a => a.num); // Now we need to fake a constraint for this: const fakeConstraint = { kind: "constraint", isFake: true, isIndexed: true, // otherwise it gets ignored by ignoreIndexes id: Math.random(), name: `FAKE_${klass.namespaceName}_${klass.name}_${tag}`, type: isPrimary ? "p" : "u", classId: klass.id, foreignClassId: null, comment: null, description, keyAttributeNums, foreignKeyAttributeNums: null, tags }; introspectionResults.constraint.push(fakeConstraint); } if (klass.tags.primaryKey) { addKey(klass.tags.primaryKey, true); } if (klass.tags.unique) { if (Array.isArray(klass.tags.unique)) { klass.tags.unique.forEach(key => addKey(key)); } else { addKey(klass.tags.unique); } } }); // Now primary keys are in place, we can apply foreign keys introspectionResults.class.forEach(klass => { const namespace = introspectionResults.namespace.find(n => n.id === klass.namespaceId); if (!namespace) { return; } const getType = () => introspectionResults.type.find(t => t.id === klass.typeId); const foreignKey = klass.tags.foreignKey || getType().tags.foreignKey; if (foreignKey) { const foreignKeys = typeof foreignKey === "string" ? [foreignKey] : foreignKey; if (!Array.isArray(foreignKeys)) { throw new Error(`Invalid foreign key smart comment specified on '${klass.namespaceName}.${klass.name}'`); } foreignKeys.forEach((fkSpecRaw, index) => { if (typeof fkSpecRaw !== "string") { throw new Error(`Invalid foreign key spec (${index}) on '${klass.namespaceName}.${klass.name}'`); } const { spec: fkSpec, tags, description } = parseConstraintSpec(fkSpecRaw); const matches = fkSpec.match(/^\(([^()]+)\) references ([^().]+)(?:\.([^().]+))?(?:\s*\(([^()]+)\))?$/i); if (!matches) { throw new Error(`Invalid foreignKey syntax for '${klass.namespaceName}.${klass.name}'; expected something like "(col1,col2) references schema.table (c1, c2)", you passed '${fkSpecRaw}'`); } const [, rawColumns, rawSchemaOrTable, rawTableOnly, rawForeignColumns] = matches; const rawSchema = rawTableOnly ? rawSchemaOrTable : `"${klass.namespaceName}"`; const rawTable = rawTableOnly || rawSchemaOrTable; const columns = parseSqlColumnArray(rawColumns); const foreignSchema = parseSqlColumnString(rawSchema); const foreignTable = parseSqlColumnString(rawTable); const foreignColumns = rawForeignColumns ? parseSqlColumnArray(rawForeignColumns) : null; const foreignKlass = introspectionResults.class.find(k => k.name === foreignTable && k.namespaceName === foreignSchema); if (!foreignKlass) { throw new Error(`@foreignKey smart comment referenced non-existant table/view '${foreignSchema}'.'${foreignTable}'. Note that this reference must use *database names* (i.e. it does not respect @name). (${fkSpecRaw})`); } const foreignNamespace = introspectionResults.namespace.find(n => n.id === foreignKlass.namespaceId); if (!foreignNamespace) { return; } const keyAttributeNums = attributesByNames(klass, columns, `@foreignKey ${fkSpecRaw}`).map(a => a.num); const foreignKeyAttributeNums = attributesByNames(foreignKlass, foreignColumns, `@foreignKey ${fkSpecRaw}`).map(a => a.num); // Now we need to fake a constraint for this: const fakeConstraint = { kind: "constraint", isFake: true, isIndexed: true, // otherwise it gets ignored by ignoreIndexes id: Math.random(), name: `FAKE_${klass.namespaceName}_${klass.name}_foreignKey_${index}`, type: "f", // foreign key classId: klass.id, foreignClassId: foreignKlass.id, comment: null, description, keyAttributeNums, foreignKeyAttributeNums, tags }; introspectionResults.constraint.push(fakeConstraint); }); } }); } function isEnumConstraint(klass, con, isEnumTable) { if (con.classId === klass.id) { const isPrimaryKey = con.type === "p"; const isUniqueConstraint = con.type === "u"; if (isPrimaryKey || isUniqueConstraint) { const isExplicitEnumConstraint = con.tags.enum === true || typeof con.tags.enum === "string"; const isPrimaryKeyOfEnumTableConstraint = con.type === "p" && isEnumTable; if (isExplicitEnumConstraint || isPrimaryKeyOfEnumTableConstraint) { const hasExactlyOneColumn = con.keyAttributeNums.length === 1; if (!hasExactlyOneColumn) { throw new Error(`Enum table "${klass.namespaceName}"."${klass.name}" enum constraint '${con.name}' is composite; it should have exactly one column (found: ${con.keyAttributeNums.length})`); } return true; } } } return false; } function enumTables(introspectionResults) { introspectionResults.class.map(async klass => { const isEnumTable = klass.tags.enum === true || typeof klass.tags.enum === "string"; if (isEnumTable) { // Prevent the table being recognised as a table // eslint-disable-next-line require-atomic-updates klass.tags.omit = true; // eslint-disable-next-line require-atomic-updates klass.isSelectable = false; // eslint-disable-next-line require-atomic-updates klass.isInsertable = false; // eslint-disable-next-line require-atomic-updates klass.isUpdatable = false; // eslint-disable-next-line require-atomic-updates klass.isDeletable = false; } // By this point, even views should have "fake" constraints we can use // (e.g. `@primaryKey`) const enumConstraints = introspectionResults.constraint.filter(con => isEnumConstraint(klass, con, isEnumTable)); // Get all the columns const enumTableColumns = introspectionResults.attribute.filter(attr => attr.classId === klass.id); // Get description column const descriptionColumn = enumTableColumns.find(attr => attr.name === "description" || attr.tags.enumDescription); const allData = klass._internalEnumData || []; enumConstraints.forEach(constraint => { const col = enumTableColumns.find(col => col.num === constraint.keyAttributeNums[0]); if (!col) { // Should never happen throw new Error("Graphile Engine error - could not find column for enum constraint"); } const data = allData.filter(row => row[col.name] != null); if (data.length < 1) { throw new Error(`Enum table "${klass.namespaceName}"."${klass.name}" contains no visible entries for enum constraint '${constraint.name}'. Check that the table contains at least one row and that the rows are not hidden by row-level security policies.`); } // Create fake enum type const constraintIdent = constraint.type === "p" ? "" : `_${constraint.name}`; const enumTypeArray = { kind: "type", isFake: true, id: fakeEnumIdentifier(klass.namespaceName, klass.name, constraintIdent, true), name: `_${klass.name}${constraintIdent}`, description: null, tags: {}, namespaceId: klass.namespaceId, namespaceName: klass.namespaceName, type: "b", category: "A", domainIsNotNull: null, arrayItemTypeId: null, typeLength: -1, isPgArray: true, classId: null, domainBaseTypeId: null, domainTypeModifier: null, domainHasDefault: false, enumVariants: null, enumDescriptions: null, rangeSubTypeId: null }; const enumType = { kind: "type", isFake: true, id: fakeEnumIdentifier(klass.namespaceName, klass.name, constraintIdent, false), name: `${klass.name}${constraintIdent}`, description: klass.description, tags: { ...klass.tags, ...constraint.tags }, namespaceId: klass.namespaceId, namespaceName: klass.namespaceName, type: "e", category: "E", domainIsNotNull: null, arrayItemTypeId: enumTypeArray.id, typeLength: 4, // ??? isPgArray: false, classId: null, domainBaseTypeId: null, domainTypeModifier: null, domainHasDefault: false, enumVariants: data.map(r => r[col.name]), enumDescriptions: descriptionColumn ? data.map(r => r[descriptionColumn.name]) : null, // TODO: enumDescriptions rangeSubTypeId: null }; introspectionResults.type.push(enumType, enumTypeArray); introspectionResults.typeById[enumType.id] = enumType; introspectionResults.typeById[enumTypeArray.id] = enumTypeArray; // Change type of all attributes that reference this table to // reference this enum type introspectionResults.constraint.forEach(c => { if (c.type === "f" && c.foreignClassId === klass.id && c.foreignKeyAttributeNums.length === 1 && c.foreignKeyAttributeNums[0] === col.num) { // Get the attribute const fkattr = introspectionResults.attribute.find(attr => attr.classId === c.classId && attr.num === c.keyAttributeNums[0]); if (fkattr) { // Override the detected type to pretend to be our enum fkattr.typeId = enumType.id; } } }); }); }); } /* The argument to this must not contain cyclic references! */ const deepClone = value => { if (Array.isArray(value)) { return value.map(val => deepClone(val)); } else if (typeof value === "object" && value) { return Object.keys(value).reduce((memo, k) => { memo[k] = deepClone(value[k]); return memo; }, {}); } else { return value; } }; var PgIntrospectionPlugin = async function PgIntrospectionPlugin(builder, { pgConfig, pgSchemas: schemas, pgEnableTags, persistentMemoizeWithKey = (key, fn) => fn(), pgThrowOnMissingSchema = false, pgIncludeExtensionResources = false, pgLegacyFunctionsOnly = false, pgIgnoreRBAC = true, pgSkipInstallingWatchFixtures = false, pgOwnerConnectionString, pgUsePartitionedParent = false }) { /** * Introspect database and get the table/view/constraints. */ async function introspect() { // Perform introspection if (!Array.isArray(schemas)) { throw new Error("Argument 'schemas' (array) is required"); } const cacheKey = `PgIntrospectionPlugin-introspectionResultsByKind-v${_package.version}`; const introspectionResultsByKind = deepClone(await persistentMemoizeWithKey(cacheKey, () => (0, _withPgClient.default)(pgConfig, async pgClient => { const versionResult = await pgClient.query("show server_version_num;"); const serverVersionNum = parseInt(versionResult.rows[0].server_version_num, 10); const introspectionQuery = (0, _introspectionQuery.makeIntrospectionQuery)(serverVersionNum, { pgLegacyFunctionsOnly, pgIgnoreRBAC, pgUsePartitionedParent }); const { rows } = await pgClient.query(introspectionQuery, [schemas, pgIncludeExtensionResources]); const result = { __pgVersion: serverVersionNum, namespace: [], class: [], attribute: [], type: [], constraint: [], procedure: [], extension: [], index: [] }; for (const { object } of rows) { result[object.kind].push(object); } // Parse tags from comments ["namespace", "class", "attribute", "type", "constraint", "procedure", "extension", "index"].forEach(kind => { result[kind].forEach(object => { // Keep a copy of the raw comment object.comment = object.description; if (pgEnableTags && object.description) { const parsed = (0, _utils.parseTags)(object.description); object.tags = parsed.tags; object.description = parsed.text; } else { object.tags = {}; } }); }); const extensionConfigurationClassIds = (0, _flatMap.default)(result.extension, e => e.configurationClassIds); result.class.forEach(klass => { klass.isExtensionConfigurationTable = extensionConfigurationClassIds.indexOf(klass.id) >= 0; }); // Assert the columns are text const VARCHAR_ID = "1043"; const TEXT_ID = "25"; const CHAR_ID = "18"; const BPCHAR_ID = "1042"; const VALID_TYPE_IDS = [VARCHAR_ID, TEXT_ID, CHAR_ID, BPCHAR_ID]; await Promise.all(result.class.map(async klass => { if (!schemas.includes(klass.namespaceName)) { // Only support enums in public tables/views return; } const isEnumTable = klass.tags.enum === true || typeof klass.tags.enum === "string"; // NOTE: this only matches on tables (not views, since they don't // have constraints), which is why we repeat the isEnumTable check below. const hasEnumConstraints = result.constraint.some(con => isEnumConstraint(klass, con, isEnumTable)); if (isEnumTable || hasEnumConstraints) { // Get the list of columns enums are defined for const enumTableColumns = result.attribute.filter(attr => attr.classId === klass.id && VALID_TYPE_IDS.includes(attr.typeId)).sort((a, z) => a.num - z.num); // Load data from the table/view. const query = pgSql.compile(pgSql.fragment`select ${pgSql.join(enumTableColumns.map(col => pgSql.identifier(col.name)), ", ")} from ${pgSql.identifier(klass.namespaceName, klass.name)};`); let allData; try { ({ rows: allData } = await pgClient.query(query)); } catch (e) { let role = "RELEVANT_POSTGRES_USER"; try { const { rows: [{ user }] } = await pgClient.query("select user;"); role = user; } catch (e) { /* * Ignore; this is likely a 25P02 (transaction aborted) * error caused by the statement above failing. */ } throw new Error(`Introspection could not read from enum table "${klass.namespaceName}"."${klass.name}", perhaps you need to grant access: GRANT USAGE ON SCHEMA "${klass.namespaceName}" TO "${role}"; GRANT SELECT ON "${klass.namespaceName}"."${klass.name}" TO "${role}"; Original error: ${e.message} `); } klass._internalEnumData = allData; } })); ["namespace", "class", "attribute", "type", "constraint", "procedure", "extension", "index"].forEach(k => { result[k].forEach(Object.freeze); }); return Object.freeze(result); }))); const knownSchemas = introspectionResultsByKind.namespace.map(n => n.name); const missingSchemas = schemas.filter(s => knownSchemas.indexOf(s) < 0); if (missingSchemas.length) { const errorMessage = `You requested to use schema '${schemas.join("', '")}'; however we couldn't find some of those! Missing schemas are: '${missingSchemas.join("', '")}'`; if (pgThrowOnMissingSchema) { throw new Error(errorMessage); } else { console.warn("⚠️ WARNING⚠️ " + errorMessage); // eslint-disable-line no-console } } return introspectionResultsByKind; } function introspectionResultsFromRaw(rawResults, pgAugmentIntrospectionResults) { const introspectionResultsByKind = deepClone(rawResults); const xByY = (arrayOfX, attrKey) => arrayOfX.reduce((memo, x) => { memo[x[attrKey]] = x; return memo; }, {}); const xByYAndZ = (arrayOfX, attrKey, attrKey2) => arrayOfX.reduce((memo, x) => { if (!memo[x[attrKey]]) memo[x[attrKey]] = {}; memo[x[attrKey]][x[attrKey2]] = x; return memo; }, {}); introspectionResultsByKind.namespaceById = xByY(introspectionResultsByKind.namespace, "id"); introspectionResultsByKind.classById = xByY(introspectionResultsByKind.class, "id"); introspectionResultsByKind.typeById = xByY(introspectionResultsByKind.type, "id"); introspectionResultsByKind.attributeByClassIdAndNum = xByYAndZ(introspectionResultsByKind.attribute, "classId", "num"); introspectionResultsByKind.extensionById = xByY(introspectionResultsByKind.extension, "id"); const relate = (array, newAttr, lookupAttr, lookup, missingOk = false) => { array.forEach(entry => { const key = entry[lookupAttr]; if (Array.isArray(key)) { entry[newAttr] = key.map(innerKey => { const result = lookup[innerKey]; if (innerKey && !result) { if (missingOk) { return; } throw new Error(`Could not look up '${newAttr}' by '${lookupAttr}' ('${innerKey}') on '${JSON.stringify(entry)}'`); } return result; }).filter(_ => _); } else { const result = lookup[key]; if (key && !result) { if (missingOk) { return; } throw new Error(`Could not look up '${newAttr}' by '${lookupAttr}' (= '${key}') on '${JSON.stringify(entry)}'`); } entry[newAttr] = result; } }); }; const augment = introspectionResults => { [pgAugmentIntrospectionResults, smartCommentConstraints, enumTables].forEach(fn => fn ? fn(introspectionResults) : null); }; augment(introspectionResultsByKind); relate(introspectionResultsByKind.class, "namespace", "namespaceId", introspectionResultsByKind.namespaceById, true // Because it could be a type defined in a different namespace - which is fine so long as we don't allow querying it directly ); relate(introspectionResultsByKind.class, "type", "typeId", introspectionResultsByKind.typeById); relate(introspectionResultsByKind.attribute, "class", "classId", introspectionResultsByKind.classById); relate(introspectionResultsByKind.attribute, "type", "typeId", introspectionResultsByKind.typeById); relate(introspectionResultsByKind.procedure, "namespace", "namespaceId", introspectionResultsByKind.namespaceById); relate(introspectionResultsByKind.type, "class", "classId", introspectionResultsByKind.classById, true); relate(introspectionResultsByKind.type, "domainBaseType", "domainBaseTypeId", introspectionResultsByKind.typeById, true // Because not all types are domains ); relate(introspectionResultsByKind.type, "arrayItemType", "arrayItemTypeId", introspectionResultsByKind.typeById, true // Because not all types are arrays ); relate(introspectionResultsByKind.constraint, "class", "classId", introspectionResultsByKind.classById); relate(introspectionResultsByKind.constraint, "foreignClass", "foreignClassId", introspectionResultsByKind.classById, true // Because many constraints don't apply to foreign classes ); relate(introspectionResultsByKind.extension, "namespace", "namespaceId", introspectionResultsByKind.namespaceById, true // Because the extension could be a defined in a different namespace ); relate(introspectionResultsByKind.extension, "configurationClasses", "configurationClassIds", introspectionResultsByKind.classById, true // Because the configuration table could be a defined in a different namespace ); relate(introspectionResultsByKind.index, "class", "classId", introspectionResultsByKind.classById); // Reverse arrayItemType -> arrayType introspectionResultsByKind.type.forEach(type => { if (type.arrayItemType) { type.arrayItemType.arrayType = type; } }); // Table/type columns / constraints introspectionResultsByKind.class.forEach(klass => { klass.attributes = introspectionResultsByKind.attribute.filter(attr => attr.classId === klass.id); klass.canUseAsterisk = !klass.attributes.some(attr => attr.columnLevelSelectGrant); klass.constraints = introspectionResultsByKind.constraint.filter(constraint => constraint.classId === klass.id); klass.foreignConstraints = introspectionResultsByKind.constraint.filter(constraint => constraint.foreignClassId === klass.id); klass.primaryKeyConstraint = klass.constraints.find(constraint => constraint.type === "p"); }); // Constraint attributes introspectionResultsByKind.constraint.forEach(constraint => { if (constraint.keyAttributeNums && constraint.class) { constraint.keyAttributes = constraint.keyAttributeNums.map(nr => constraint.class.attributes.find(attr => attr.num === nr)); } else { constraint.keyAttributes = []; } if (constraint.foreignKeyAttributeNums && constraint.foreignClass) { constraint.foreignKeyAttributes = constraint.foreignKeyAttributeNums.map(nr => constraint.foreignClass.attributes.find(attr => attr.num === nr)); } else { constraint.foreignKeyAttributes = []; } }); // Detect which columns and constraints are indexed introspectionResultsByKind.index.forEach(index => { const columns = index.attributeNums.map(nr => index.class.attributes.find(attr => attr.num === nr)); // Indexed column (for orderBy / filter): if (columns[0]) { columns[0].isIndexed = true; } if (columns[0] && columns.length === 1 && index.isUnique) { columns[0].isUnique = true; } // Indexed constraints (for reverse relations): index.class.constraints.filter(constraint => constraint.type === "f").forEach(constraint => { if (constraint.keyAttributeNums.every((nr, idx) => index.attributeNums[idx] === nr)) { constraint.isIndexed = true; } }); }); return introspectionResultsByKind; } let rawIntrospectionResultsByKind = await introspect(); let listener; class Listener { constructor(triggerRebuild) { this.stopped = false; this._handleChange = (0, _throttle.default)(async () => { debug(`Schema change detected: re-inspecting schema...`); try { rawIntrospectionResultsByKind = await introspect(); debug(`Schema change detected: re-inspecting schema complete`); triggerRebuild(); } catch (e) { // eslint-disable-next-line no-console console.error(`Schema introspection failed: ${e.message}`); } }, 750, { leading: true, trailing: true }); this._listener = this._listener.bind(this); this._handleClientError = this._handleClientError.bind(this); this._start(); } async _start(isReconnect = false) { if (this.stopped) { return; } // Connect to DB try { const { pgClient, releasePgClient } = await (0, _withPgClient.getPgClientAndReleaserFromConfig)(pgConfig); this.client = pgClient; // $FlowFixMe: hack property this._reallyReleaseClient = releasePgClient; pgClient.on("notification", this._listener); pgClient.on("error", this._handleClientError); if (this.stopped) { // In case watch mode was cancelled in the interrim. return this._releaseClient(); } else { await pgClient.query("listen postgraphile_watch"); // Install the watch fixtures. if (!pgSkipInstallingWatchFixtures) { const watchSqlInner = await readFile(WATCH_FIXTURES_PATH, "utf8"); const sql = `begin; ${watchSqlInner};`; await (0, _withPgClient.default)(pgOwnerConnectionString || pgConfig, async pgClient => { try { await pgClient.query(sql); } catch (error) { if (!this._haveDisplayedError) { this._haveDisplayedError = true; /* eslint-disable no-console */ console.warn(`${_chalk.default.bold.yellow("Failed to setup watch fixtures in Postgres database")} ️️⚠️`); console.warn(_chalk.default.yellow("This is likely because the PostgreSQL user in the connection string does not have sufficient privileges; you can solve this by passing the 'owner' connection string via '--owner-connection' / 'ownerConnectionString'. If the fixtures already exist, the watch functionality may still work.")); console.warn(_chalk.default.yellow("Enable DEBUG='graphile-build-pg' to see the error")); /* eslint-enable no-console */ } debug(error); } finally { await pgClient.query("commit;"); } }); } // Trigger re-introspection on server reconnect if (isReconnect) { this._handleChange(); } } } catch (e) { // If something goes wrong, disconnect and try again after a short delay this._reconnect(e); } } _handleClientError(e) { this._releaseClient(false); this._reconnect(e); } async _reconnect(e) { if (this.stopped) { return; } // eslint-disable-next-line no-console console.error("Error occurred for PG watching client; reconnecting in 2 seconds.", e.message); await this._releaseClient(); setTimeout(() => { if (!this.stopped) { // Listen for further changes this._start(true); } }, 2000); } // eslint-disable-next-line flowtype/no-weak-types // eslint-disable-next-line flowtype/no-weak-types async _listener(notification) { if (notification.channel !== "postgraphile_watch") { return; } try { const payload = JSON.parse(notification.payload); payload.payload = payload.payload || []; if (payload.type === "ddl") { const commands = payload.payload.filter(({ schema }) => schema == null || schemas.indexOf(schema) >= 0).map(({ command }) => command); if (commands.length) { this._handleChange(); } } else if (payload.type === "drop") { const affectsOurSchemas = payload.payload.some(schemaName => schemas.indexOf(schemaName) >= 0); if (affectsOurSchemas) { this._handleChange(); } } else if (payload.type === "manual") { this._handleChange(); } else { throw new Error(`Payload type '${payload.type}' not recognised`); } } catch (e) { debug(`Error occurred parsing notification payload: ${e}`); } } async stop() { this.stopped = true; this._handleChange.cancel(); await this._releaseClient(); } /** * Only pass `false` to this function if you know the client is going to be * terminated; otherwise we risk leaving listeners running. */ async _releaseClient(clientIsStillViable = true) { // $FlowFixMe const pgClient = this.client; const reallyReleaseClient = this._reallyReleaseClient; this.client = null; this._reallyReleaseClient = null; if (pgClient) { // Don't attempt to run a query after a client has errored. if (clientIsStillViable) { pgClient.query("unlisten postgraphile_watch").catch(e => { debug(`Error occurred trying to unlisten watch: ${e}`); }); } pgClient.removeListener("notification", this._listener); pgClient.removeListener("error", this._handleClientError); } if (reallyReleaseClient) { await reallyReleaseClient(); } } } builder.registerWatcher(async triggerRebuild => { // In case we started listening before, clean up if (listener) { await listener.stop(); } // We're not worried about a race condition here. // eslint-disable-next-line require-atomic-updates listener = new Listener(triggerRebuild); }, async () => { const l = listener; listener = null; if (l) { await l.stop(); } }); builder.hook("build", build => { const introspectionResultsByKind = introspectionResultsFromRaw(rawIntrospectionResultsByKind, build.pgAugmentIntrospectionResults); if (introspectionResultsByKind.__pgVersion < 90500) { // TODO:v5: remove this workaround // This is a bit of a hack, but until we have plugin priorities it's the // easiest way to conditionally support PG9.4. build.pgQueryFromResolveData = (0, _queryFromResolveDataFactory.default)({ supportsJSONB: false }); } return build.extend(build, { pgIntrospectionResultsByKind: introspectionResultsByKind, getPgFakeEnumIdentifier: fakeEnumIdentifier }); }, ["PgIntrospection"], [], ["PgBasics"]); }; // TypeScript compatibility exports.default = PgIntrospectionPlugin; const PgEntityKind = { NAMESPACE: "namespace", PROCEDURE: "procedure", CLASS: "class", TYPE: "type", ATTRIBUTE: "attribute", CONSTRAINT: "constraint", EXTENSION: "extension", INDEX: "index" }; exports.PgEntityKind = PgEntityKind; //# sourceMappingURL=PgIntrospectionPlugin.js.map