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
289 lines (288 loc) • 14.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _debug = _interopRequireDefault(require("debug"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const debug = (0, _debug.default)("graphile-build-pg");
const OMIT = 0;
const DEPRECATED = 1;
const ONLY = 2;
var PgBackwardRelationPlugin = function PgBackwardRelationPlugin(builder, {
pgLegacyRelations,
pgSimpleCollections,
subscriptions
}) {
const legacyRelationMode = {
only: ONLY,
deprecated: DEPRECATED
}[pgLegacyRelations] || OMIT;
builder.hook("GraphQLObjectType:fields", (fields, build, context) => {
const {
extend,
getTypeByName,
pgGetGqlTypeByTypeIdAndModifier,
pgIntrospectionResultsByKind: introspectionResultsByKind,
pgSql: sql,
getSafeAliasFromResolveInfo,
getSafeAliasFromAlias,
graphql: {
GraphQLNonNull,
GraphQLList
},
inflection,
pgQueryFromResolveData: queryFromResolveData,
pgAddStartEndCursor: addStartEndCursor,
pgOmit: omit,
sqlCommentByAddingTags,
describePgEntity
} = build;
const {
scope: {
isPgRowType,
pgIntrospection: foreignTable
},
fieldWithHooks,
Self
} = context;
if (!isPgRowType || !foreignTable || foreignTable.kind !== "class") {
return fields;
}
// This is a relation in which WE are foreign
const foreignKeyConstraints = foreignTable.foreignConstraints.filter(con => con.type === "f");
const foreignTableTypeName = inflection.tableType(foreignTable);
const gqlForeignTableType = pgGetGqlTypeByTypeIdAndModifier(foreignTable.type.id, null);
if (!gqlForeignTableType) {
debug(`Could not determine type for foreign table with id ${foreignTable.type.id}`);
return fields;
}
return extend(fields, foreignKeyConstraints.reduce((memo, constraint) => {
if (omit(constraint, "read")) {
return memo;
}
const table = introspectionResultsByKind.classById[constraint.classId];
if (!table) {
throw new Error(`Could not find the table that referenced us (constraint: ${constraint.name})`);
}
if (!table.isSelectable) {
// Could be a composite type
return memo;
}
const tableTypeName = inflection.tableType(table);
const gqlTableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null);
if (!gqlTableType) {
debug(`Could not determine type for table with id ${constraint.classId}`);
return memo;
}
const schema = table.namespace;
const keys = constraint.keyAttributes;
const foreignKeys = constraint.foreignKeyAttributes;
if (!keys.every(_ => _) || !foreignKeys.every(_ => _)) {
throw new Error("Could not find key columns!");
}
if (keys.some(key => omit(key, "read"))) {
return memo;
}
if (foreignKeys.some(key => omit(key, "read"))) {
return memo;
}
const isUnique = !!table.constraints.find(c => (c.type === "p" || c.type === "u") && c.keyAttributeNums.length === keys.length && c.keyAttributeNums.every((n, i) => keys[i].num === n));
const isDeprecated = isUnique && legacyRelationMode === DEPRECATED;
const singleRelationFieldName = isUnique ? inflection.singleRelationByKeysBackwards(keys, table, foreignTable, constraint) : null;
const primaryKeyConstraint = table.primaryKeyConstraint;
const primaryKeys = primaryKeyConstraint && primaryKeyConstraint.keyAttributes;
const shouldAddSingleRelation = isUnique && legacyRelationMode !== ONLY;
const shouldAddManyRelation = !isUnique || legacyRelationMode === DEPRECATED || legacyRelationMode === ONLY;
if (shouldAddSingleRelation && !omit(table, "read") && singleRelationFieldName) {
memo = extend(memo, {
[singleRelationFieldName]: fieldWithHooks(singleRelationFieldName, ({
getDataFromParsedResolveInfoFragment,
addDataGenerator
}) => {
const sqlFrom = sql.identifier(schema.name, table.name);
addDataGenerator(parsedResolveInfoFragment => {
return {
pgQuery: queryBuilder => {
queryBuilder.select(() => {
const resolveData = getDataFromParsedResolveInfoFragment(parsedResolveInfoFragment, gqlTableType);
const tableAlias = sql.identifier(Symbol());
const foreignTableAlias = queryBuilder.getTableAlias();
const query = queryFromResolveData(sqlFrom, tableAlias, resolveData, {
useAsterisk: false,
// Because it's only a single relation, no need
asJson: true,
addNullCase: true,
withPagination: false
}, innerQueryBuilder => {
innerQueryBuilder.parentQueryBuilder = queryBuilder;
if (subscriptions && table.primaryKeyConstraint) {
innerQueryBuilder.selectIdentifiers(table);
innerQueryBuilder.makeLiveCollection(table);
innerQueryBuilder.addLiveCondition(data => record => {
return keys.every(key => record[key.name] === data[key.name]);
}, keys.reduce((memo, key, i) => {
memo[key.name] = sql.fragment`${foreignTableAlias}.${sql.identifier(foreignKeys[i].name)}`;
return memo;
}, {}));
}
keys.forEach((key, i) => {
innerQueryBuilder.where(sql.fragment`${tableAlias}.${sql.identifier(key.name)} = ${foreignTableAlias}.${sql.identifier(foreignKeys[i].name)}`);
});
}, queryBuilder.context, queryBuilder.rootValue);
return sql.fragment`(${query})`;
}, getSafeAliasFromAlias(parsedResolveInfoFragment.alias));
}
};
});
return {
description: constraint.tags.backwardDescription || build.wrapDescription(`Reads a single \`${tableTypeName}\` that is related to this \`${foreignTableTypeName}\`.`, "field"),
type: gqlTableType,
args: {},
resolve: (data, _args, resolveContext, resolveInfo) => {
const safeAlias = getSafeAliasFromResolveInfo(resolveInfo);
const record = data[safeAlias];
const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord;
const liveCollection = resolveInfo.rootValue && resolveInfo.rootValue.liveCollection;
const liveConditions = resolveInfo.rootValue && resolveInfo.rootValue.liveConditions;
if (subscriptions && liveCollection && liveConditions && data.__live) {
const {
__id,
...rest
} = data.__live;
const condition = liveConditions[__id];
const checker = condition(rest);
liveCollection("pg", table, checker);
}
if (record && liveRecord) {
liveRecord("pg", table, record.__identifiers);
}
return record;
}
};
}, {
pgFieldIntrospection: table,
isPgBackwardSingleRelationField: true
})
}, `Backward relation (single) for ${describePgEntity(constraint)}. To rename this relation with a 'Smart Comment':\n\n ${sqlCommentByAddingTags(constraint, {
foreignSingleFieldName: "newNameHere"
})}`);
}
function makeFields(isConnection) {
const manyRelationFieldName = isConnection ? inflection.manyRelationByKeys(keys, table, foreignTable, constraint) : inflection.manyRelationByKeysSimple(keys, table, foreignTable, constraint);
memo = extend(memo, {
[manyRelationFieldName]: fieldWithHooks(manyRelationFieldName, ({
getDataFromParsedResolveInfoFragment,
addDataGenerator
}) => {
const sqlFrom = sql.identifier(schema.name, table.name);
const queryOptions = {
useAsterisk: table.canUseAsterisk,
withPagination: isConnection,
withPaginationAsFields: false,
asJsonAggregate: !isConnection
};
addDataGenerator(parsedResolveInfoFragment => {
return {
pgQuery: queryBuilder => {
queryBuilder.select(() => {
const resolveData = getDataFromParsedResolveInfoFragment(parsedResolveInfoFragment, isConnection ? ConnectionType : TableType);
const tableAlias = sql.identifier(Symbol());
const foreignTableAlias = queryBuilder.getTableAlias();
const query = queryFromResolveData(sqlFrom, tableAlias, resolveData, queryOptions, innerQueryBuilder => {
innerQueryBuilder.parentQueryBuilder = queryBuilder;
if (subscriptions) {
innerQueryBuilder.makeLiveCollection(table);
innerQueryBuilder.addLiveCondition(data => record => {
return keys.every(key => record[key.name] === data[key.name]);
}, keys.reduce((memo, key, i) => {
memo[key.name] = sql.fragment`${foreignTableAlias}.${sql.identifier(foreignKeys[i].name)}`;
return memo;
}, {}));
}
if (primaryKeys) {
if (subscriptions && !isConnection && table.primaryKeyConstraint) {
innerQueryBuilder.selectIdentifiers(table);
}
innerQueryBuilder.beforeLock("orderBy", () => {
// append order by primary key to the list of orders
if (!innerQueryBuilder.isOrderUnique(false)) {
innerQueryBuilder.data.cursorPrefix = ["primary_key_asc"];
primaryKeys.forEach(key => {
innerQueryBuilder.orderBy(sql.fragment`${innerQueryBuilder.getTableAlias()}.${sql.identifier(key.name)}`, true);
});
innerQueryBuilder.setOrderIsUnique();
}
});
}
keys.forEach((key, i) => {
innerQueryBuilder.where(sql.fragment`${tableAlias}.${sql.identifier(key.name)} = ${foreignTableAlias}.${sql.identifier(foreignKeys[i].name)}`);
});
}, queryBuilder.context, queryBuilder.rootValue);
return sql.fragment`(${query})`;
}, getSafeAliasFromAlias(parsedResolveInfoFragment.alias));
}
};
});
const ConnectionType = getTypeByName(inflection.connection(gqlTableType.name));
const TableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null);
return {
description: constraint.tags.backwardDescription || build.wrapDescription(`Reads and enables pagination through a set of \`${tableTypeName}\`.`, "field"),
type: isConnection ? new GraphQLNonNull(ConnectionType) : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(TableType))),
args: {},
resolve: (data, _args, resolveContext, resolveInfo) => {
const safeAlias = getSafeAliasFromResolveInfo(resolveInfo);
const liveCollection = resolveInfo.rootValue && resolveInfo.rootValue.liveCollection;
const liveConditions = resolveInfo.rootValue && resolveInfo.rootValue.liveConditions;
if (subscriptions && liveCollection && liveConditions && data.__live) {
const {
__id,
...rest
} = data.__live;
const condition = liveConditions[__id];
const checker = condition(rest);
liveCollection("pg", table, checker);
}
if (isConnection) {
return addStartEndCursor(data[safeAlias]);
} else {
const records = data[safeAlias];
const liveRecord = resolveInfo.rootValue && resolveInfo.rootValue.liveRecord;
if (primaryKeys && subscriptions && liveRecord) {
records.forEach(r => r && r.__identifiers && liveRecord("pg", table, r.__identifiers));
}
return records;
}
},
...(isDeprecated ? {
deprecationReason: singleRelationFieldName ? `Please use ${singleRelationFieldName} instead` : `Please use singular instead` // This should never happen
} : null)
};
}, {
isPgFieldConnection: isConnection,
isPgFieldSimpleCollection: !isConnection,
isPgBackwardRelationField: true,
pgFieldIntrospection: table
})
}, `Backward relation (${isConnection ? "connection" : "simple collection"}) for ${describePgEntity(constraint)}. To rename this relation with a 'Smart Comment':\n\n ${sqlCommentByAddingTags(constraint, {
[isConnection ? "foreignFieldName" : "foreignSimpleFieldName"]: "newNameHere"
})}`);
}
if (shouldAddManyRelation && !omit(table, "many") && !omit(constraint, "many")) {
const simpleCollections = constraint.tags.simpleCollections || table.tags.simpleCollections || pgSimpleCollections;
const hasConnections = simpleCollections !== "only";
const hasSimpleCollections = simpleCollections === "only" || simpleCollections === "both";
if (hasConnections) {
makeFields(true);
}
if (hasSimpleCollections && !isUnique // if unique, use the singular instead
) {
makeFields(false);
}
}
return memo;
}, {}), `Adding backward relations for ${Self.name}`);
}, ["PgBackwardRelation"]);
};
exports.default = PgBackwardRelationPlugin;
//# sourceMappingURL=PgBackwardRelationPlugin.js.map