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
514 lines (496 loc) • 21.4 kB
Flow
// @flow
import debugFactory from "debug";
import type { Plugin } from "graphile-build";
const debug = debugFactory("graphile-build-pg");
const OMIT = 0;
const DEPRECATED = 1;
const ONLY = 2;
export default (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"]
);
}: Plugin);