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
JavaScript
;
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