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
215 lines (203 loc) • 6.38 kB
Flow
// @flow
import type { Plugin } from "graphile-build";
import { getComputedColumnDetails } from "./PgComputedColumnsPlugin";
import assert from "assert";
function getCompatibleComputedColumns(build, table) {
const {
pgIntrospectionResultsByKind: introspectionResultsByKind,
pgOmit: omit,
} = build;
return introspectionResultsByKind.procedure.reduce((memo, proc) => {
/* ALSO SEE PgOrderComputedColumnsPlugin */
// Must be marked @filterable
if (!proc.tags.filterable) return memo;
// Must not be omitted
if (omit(proc, "execute")) return memo;
// Must be a computed column
const computedColumnDetails = getComputedColumnDetails(build, table, proc);
if (!computedColumnDetails) return memo;
const { pseudoColumnName } = computedColumnDetails;
// Must have only one required argument
const nonOptionalArgumentsCount = proc.inputArgsCount - proc.argDefaultsNum;
if (nonOptionalArgumentsCount > 1) {
return memo;
}
// Must return a scalar
if (proc.returnsSet) return memo;
const returnType = introspectionResultsByKind.typeById[proc.returnTypeId];
if (returnType.isPgArray) return memo;
const returnTypeTable =
introspectionResultsByKind.classById[returnType.classId];
if (returnTypeTable) return memo;
const isRecordLike = returnType.id === "2249";
if (isRecordLike) return memo;
const isVoid = String(returnType.id) === "2278";
if (isVoid) return memo;
// Looks good
memo.push({ proc, pseudoColumnName, returnType });
return memo;
}, []);
}
export default (function PgConditionComputedColumnPlugin(builder) {
builder.hook(
"GraphQLInputObjectType:fields",
(fields, build, context) => {
const {
extend,
pgGetGqlInputTypeByTypeIdAndModifier,
inflection,
describePgEntity,
} = build;
const {
scope: { isPgCondition, pgIntrospection: table },
fieldWithHooks,
} = context;
if (!isPgCondition || !table || table.kind !== "class") {
return fields;
}
const compatibleComputedColumns = getCompatibleComputedColumns(
build,
table
);
return extend(
fields,
compatibleComputedColumns.reduce((memo, { proc, pseudoColumnName }) => {
const fieldName = inflection.computedColumn(
pseudoColumnName,
proc,
table
);
const Type = pgGetGqlInputTypeByTypeIdAndModifier(
proc.returnTypeId,
null
);
if (!Type) return memo;
memo = build.extend(
memo,
{
[fieldName]: fieldWithHooks(
fieldName,
{
description: build.wrapDescription(
`Checks for equality with the object’s \`${fieldName}\` field.`,
"field"
),
type: Type,
},
{
isPgConnectionConditionInputField: true,
pgFieldIntrospection: proc,
}
),
},
`Adding computed column condition argument for ${describePgEntity(
proc
)}`
);
return memo;
}, {})
);
},
["PgConditionComputedColumn"]
);
builder.hook(
"GraphQLObjectType:fields:field:args",
(args, build, context) => {
const {
pgSql: sql,
gql2pg,
getTypeByName,
pgGetGqlTypeByTypeIdAndModifier,
inflection,
pgOmit: omit,
graphql: { getNullableType },
} = build;
const {
scope: {
isPgFieldConnection,
isPgFieldSimpleCollection,
pgFieldIntrospection,
pgFieldIntrospectionTable,
},
addArgDataGenerator,
} = context;
const shouldAddCondition =
isPgFieldConnection || isPgFieldSimpleCollection;
if (!shouldAddCondition) return args;
if (!args.condition) {
return args;
}
const proc =
pgFieldIntrospection.kind === "procedure" ? pgFieldIntrospection : null;
const table =
pgFieldIntrospection.kind === "class"
? pgFieldIntrospection
: proc
? pgFieldIntrospectionTable
: null;
if (
!table ||
table.kind !== "class" ||
!table.namespace ||
omit(table, "filter")
) {
return args;
}
const TableType = pgGetGqlTypeByTypeIdAndModifier(table.type.id, null);
const TableConditionType = getTypeByName(
inflection.conditionType(TableType.name)
);
if (!TableConditionType) {
return args;
}
assert(
getNullableType(args.condition.type) === TableConditionType,
"Condition is present, but doesn't match?"
);
const compatibleComputedColumns = getCompatibleComputedColumns(
build,
table
).map(o => {
const { proc, pseudoColumnName } = o;
const fieldName = inflection.computedColumn(
pseudoColumnName,
proc,
table
);
const sqlFnName = sql.identifier(proc.namespaceName, proc.name);
return {
...o,
fieldName,
sqlFnName,
};
});
addArgDataGenerator(function connectionCondition({ condition }) {
return {
pgQuery: queryBuilder => {
if (condition != null) {
compatibleComputedColumns.forEach(
({ fieldName, sqlFnName, returnType }) => {
const val = condition[fieldName];
const sqlCall = sql.fragment`${sqlFnName}(${queryBuilder.getTableAlias()})`;
if (val != null) {
queryBuilder.where(
sql.fragment`${sqlCall} = ${gql2pg(
val,
returnType,
null
)}`
);
} else if (val === null) {
queryBuilder.where(sql.fragment`${sqlCall} IS NULL`);
}
}
);
}
},
};
});
return args;
},
["PgConditionComputedColumn"]
);
}: Plugin);