Table of Contents
makeAddPgTableConditionPlugin (graphile-utils v4.4.5+)
WARNING: this plugin generator doesn't currently have any tests, so it's status is experimental. If you can spare the time to write some tests (or sponsor me to do so) then we can promote it to stable.
PostGraphile adds condition
arguments to various of the table collection
fields it builds so that you can filter the result set down to just the records
you're interested in. By default we add the table's columns (or, if
--no-ignore-indexes
is enabled, only the columns that are indexed) to the
condition input, where you can specify their value, or null
if you only want
the records where that column IS NULL
.
Many GraphQL experts would opine that GraphQL filters should not be overly complicated, and should not reveal too much of the underlying data store. This is why we don't have advanced filtering built in by default; however, should you desire that, please check out the filter plugin documented on our Filtering page.
Here's an example of filtering forums to those created by a particular user:
query ForumsCreatedByUser1 {
allForums(condition: { creator_id: 1 }) {
nodes {
id
name
}
}
}
Sometimes, however, you want to filter by something a little more complex than the fields on that table; maybe you want to filter by a field on a related table, or by a computation, or something else.
This plugin generator helps you build new condition
values so that you can
filter more flexibly. Let's make this clearer with an example:
Example 1
To return a list of forums which match a list of primary keys:
/* TODO: test this plugin works! */
module.exports = makeAddPgTableConditionPlugin(
"app_public",
"forums",
"idIn",
build => {
const { GraphQLList, GraphQLNonNull, GraphQLInt } = build.graphql;
return {
description: "Filters to records matching one of these ids",
// This is graphql-js for `[Int!]`; assumes you're using
// an integer primary key.
type: new GraphQLList(new GraphQLNonNull(GraphQLInt)),
};
},
(value, helpers, build) => {
const { sql, sqlTableAlias } = helpers;
// Note sqlTableAlias represents our table (app_public.forums),
// but because it might be requested more than once in the
// generated query we need to match this specific instance, so
// we use an alias.
// This SQL fragment will be merged into the `WHERE` clause, so
// it must be valid in that context.
return sql.fragment`${sqlTableAlias}.id = ANY (${sql.value(value)}::int[])`;
}
);
Example 2
To filter a list of forums (stored in the table app_public.forums
) to just
those where a particular user has posted in (posts are stored in
app_public.posts
) you might create a plugin like this:
/* TODO: test this plugin works! */
module.exports = makeAddPgTableConditionPlugin(
"app_public",
"forums",
"containsPostsByUserId",
build => ({
description:
"Filters the list of forums to only those which " +
"contain posts written by the specified user.",
type: build.graphql.GraphQLInt,
}),
(value, helpers, build) => {
const { sql, sqlTableAlias } = helpers;
const sqlIdentifier = sql.identifier(Symbol("postsByUser"));
// This is merged into the `WHERE` clause, so we end up with
// something like:
//
// ```sql
// SELECT ...
// FROM app_public.forums AS <sqlTableAlias>
// WHERE ...
// AND (
// -- This is our returned fragment:
// exists (select 1 from ...)
// )
// ```
return sql.fragment`exists(
select 1
from app_public.posts as ${sqlIdentifier}
where ${sqlIdentifier}.forum_id = ${sqlTableAlias}.id
and ${sqlIdentifier}.user_id = ${sql.value(value)}
)`;
}
);
The above plugin adds the containsPostsByUserId
condition to collection fields
for the app_public.forums
table. You might use it like this:
query ForumsContainingPostsByUser1 {
allForums(condition: { containsPostsByUserId: 1 }) {
nodes {
id
name
}
}
}
NOTE: sqlTableAlias
represents the app_public.forums
table in the example
above (i.e. the schemaName.tableName table); if you don't use it in your
implementation then there's a good chance your plugin is incorrect.
NOTE: for more complex values, you may need to invoke
build.gql2pg(value, databaseType)
instead of sql.value(value)
in order to
convert the GraphQL value to the equivalent SQL value. If you should need this,
reach out on our Discord chat for advice.
Example with ordering
It's also possible for condition plugins to change the order of results by using
QueryBuilder's
orderBy
method. The following example both limits the list of quizzes returned
to only those with a certain number of entries, and orders the results such
that the quizzes with the most entries are listed first.
This example if quite contrived, but this functionality can be useful for a number of purposes: filtering and ordering by full text search results, filtering and ordering by proximity, etc.
Note: prior to graphile-utils
v4.9.1 (unreleased at time of writing), a plugin
like this should be loaded via --prepend-plugins
(or prependPlugins
in the
library mode) because otherwise the default ordering plugin dominates the order.
const { makeAddPgTableConditionPlugin } = require("graphile-utils");
module.exports = makeAddPgTableConditionPlugin(
"app_public",
"quiz",
"entryCountMin",
build => ({
type: build.graphql.GraphQLInt,
}),
(value, { queryBuilder, sql, sqlTableAlias }) => {
if (value == null) {
return;
}
// Order the result set by the number of entries the quiz has
queryBuilder.orderBy(
sql.fragment`(select count(*) from app_public.quiz_entry where quiz_entry.quiz_id = ${sqlTableAlias}.id)`,
false,
false
);
// Filter to only quizzes that have at least `value` entries.
return sql.fragment`(select count(*) from app_public.quiz_entry where quiz_entry.quiz_id = ${sqlTableAlias}.id) >= ${sql.value(
value
)}`;
}
);
Function signature
makeAddPgTableConditionPlugin
The signature of the makeAddPgTableConditionPlugin
function is:
export default function makeAddPgTableConditionPlugin(
schemaName: string,
tableName: string,
conditionFieldName: string,
conditionFieldSpecGenerator: (build: Build) => GraphQLInputFieldConfig,
conditionGenerator: (
value: unknown,
helpers: {
queryBuilder: QueryBuilder;
sql: typeof pgsql2 /* the 'pg-sql2' module */;
sqlTableAlias: SQL;
},
build: Build
) => SQL | null | void
): Plugin;
The table to match is the table named tableName
in the schema named
schemaName
.
A new condition is added, named conditionFieldName
, whose GraphQL
representation is specified by the result of conditionFieldSpecGenerator
.
When the field named in conditionFieldName
is used in a query, the
conditionGenerator
is called with the value passed, and the result of that
function is used as an additional WHERE
clause on the generated SQL (combined
using AND
). If null
or undefined
are returned then no condition is added.