makeExtendSchemaPlugin (graphile-utils)
This documentation applies to PostGraphile v4.1.0+
The graphile-utils module contains some helpers for extending your
PostGraphile (or Graphile Engine) GraphQL schema without having to understand
the complex plugin system.
The main one you'll care about to start with is makeExtendSchemaPlugin.
Using makeExtendSchemaPlugin you can write a plugin that will merge additional
GraphQL types and resolvers into your schema using a similar syntax to
graphql-tools. You need to provide the typeDefs schema definition and
resolvers function to use. Your plugin will likely take a shape like this:
const { makeExtendSchemaPlugin, gql } = require("graphile-utils");
const MyPlugin = makeExtendSchemaPlugin((build) => {
// Get any helpers we need from `build`
const { pgSql: sql, inflection } = build;
return {
typeDefs: gql`...`,
resolvers: {
/*...*/
},
};
});
module.exports = MyPlugin;
And would be added to your PostGraphile instance via
- CLI:
--append-plugins `pwd`/MySchemaExtensionPlugin.js - Library:
appendPlugins: [require('./MySchemaExtensionPlugin')]
The build argument to the makeExtendSchemaPlugin callback contains lots of
information and helpers defined by various plugins, most importantly it includes
the introspection results (build.pgIntrospectionResultsByKind), inflection
functions (build.inflection), and SQL helper (build.pgSql, which is an
instance of pg-sql2).
The callback should return an object with two keys:
typeDefs: a GraphQL AST generated with thegqlhelper fromgraphile-utils(note this is NOT from thegraphql-taglibrary, ours works in a slightly different way).resolvers: an object that's keyed by the GraphQL type names of types defined (or extended) intypeDefs, the values of which are objects keyed by the field names with values that are resolver functions.
For a larger example of how typeDefs and resolvers work, have a look at the graphql-tools docs - ours work in a similar way.
Note that the resolve functions defined in resolvers will be sent the standard
4 GraphQL resolve arguments (parent, args, context, resolveInfo); but
the 4th argument (resolveInfo) will also contain graphile-specific helpers.
The gql and embed helpers
The gql helper is responsible for turning the human-readable GraphQL schema
language you write into an abstract syntax tree (AST) that the application can
understand. Our gql help differs slightly from the one you may be familiar
with in the graphql-tag npm module, namely in how the placeholders work. Ours
is designed to work with PostGraphile's inflection system, so
you can embed strings directly. You may also embed other gql tags directly. For
example:
const nameOfType = "MyType"; // Or use the inflection system to generate a type
// This tag interpolates the string `nameOfType` to allow dynamic naming of the
// type.
const Type = gql`
type ${nameOfType} {
str: String
int: Int
}
`;
// This tag interpolates the entire definition in `Type` above.
const typeDefs = gql`
${Type}
extend type Query {
fieldName: Type
}
`;
The embed helper is for use with gql when you want to embed a raw JavaScript
value (anything: regexp, function, string, object, etc) into the document; for
example to pass it to a directive. We use this with the @pgQuery directive
further down this page. Here's a simple example of embedding an object.
const meta = {
/* arbitrary data */
name: "fieldName",
added: "2019-04-29T16:15:00Z",
};
const typeDefs = gql`
extend type Query {
fieldName: Int @scope(meta: ${embed(fieldNameMeta)})
}
`;
Querying the database inside a resolver
PostGraphile provisions, sets up and tears down a PostgreSQL client
automatically for each GraphQL query. Setup involves beginning a transaction and
setting the relevant session variables, e.g. using your JWT or the pgSettings
function. You can access this client on context.pgClient; it's currently an
instance of
pg.Client from the pg module;
however you should only use it like this to maintain future compatibility:
const { rows } = await context.pgClient.query(
sqlText, // e.g. "select * from users where id = $1"
optionalVariables, // e.g. [27]
);
NOTE: context is the third argument passed to a GraphQL resolver
(function myResolver(parentObject, args, context, info) { /* ... */ }).
Since you're already in a transaction, issuing BEGIN; or COMMIT; inside your
resolver is a Really Bad Idea™. Should you need a sub-transaction,
use a SAVEPOINT.
However, please be aware that PostGraphile only sets up a transaction when it
needs to (e.g. when it's a mutation, or when there are config variables or a
role to set); so you cannot rely on SAVEPOINT working inside of queries unless
you know these conditions are met.
Because the entire GraphQL operation is executed within a single transaction, be
very wary that you don't cause an SQL error which causes the entire transaction
to fail. This could leave things in a very odd state - particularly for
mutations - e.g. where you return a partial success to the user, but actually
roll back the results. It's recommended that all mutations are wrapped in
SAVEPOINT / RELEASE SAVEPOINT / ROLLBACK TO SAVEPOINT calls.
When your resolver returns results that will be used by autogenerated types and
fields, you must not return query results such as these directly. Instead use
the selectGraphQLResultFromTable helper
documented below. The results of your pgClient.query should be used within the
resolver only, and should not "leak" (in general).
Reading database column values
When extending a schema, it's often because you want to expose data from Node.js that would be too difficult (or impossible) to access from PostgreSQL. When defining a field on an existing table-backed type defined by PostGraphile, it's useful to access data from the underlying table in the resolver.
To do this you can use the @requires(columns: […]) field directive to declare
the data dependencies of your resolver. This guarantees that when the resolver
is executed, the data is immediately available.
Here's an example to illustrate.
In the database you have a product table (imagine an online store), that
PostGraphile will include in the GraphQL schema by creating a type Product
with fields id, name, price_in_us_cents.
create table product (
id uuid primary key,
name text not null,
price_in_us_cents integer not null
);
This may result in the following GraphQL type:
type Product {
id: UUID!
name: String!
priceInUsCents: Int!
}
However imagine you're selling internationally, and you want to expose the price
in other currencies directly from the Product type itself. This kind of
functionality is well suited to being performed in Node.js (e.g. by making a
REST call to a foreign exchange service over the internet) but might be a
struggle from with PostgreSQL.
const { postgraphile } = require("postgraphile");
const { makeExtendSchemaPlugin, gql } = require("graphile-utils");
const express = require("express");
const { convertUsdToAud } = require("ficticious-npm-library");
const MyForeignExchangePlugin = makeExtendSchemaPlugin((build) => {
return {
typeDefs: gql`
extend type Product {
priceInAuCents: Int! @requires(columns: ["price_in_us_cents"])
}
`,
resolvers: {
Product: {
priceInAuCents: async (product) => {
// Note that the columns are converted to fields, so the case changes
// from `price_in_us_cents` to `priceInUsCents`
const { priceInUsCents } = product;
return await convertUsdToAud(priceInUsCents);
},
},
},
};
});
const app = express();
app.use(
postgraphile(process.env.DATABASE_URL, ["app_public"], {
graphiql: true,
appendPlugins: [MyForeignExchangePlugin],
}),
);
app.listen(3030);
The selectGraphQLResultFromTable helper
this helper is for populating data you return from your
*resolver; you should not use selectGraphQLResultFromTable to retrieve data
for your resolver to process. Instead use context.pgClient directly.
selectGraphQLResultFromTable should only be called once per
resolver; it doesn't make sense to call it multiple times, and attempting to
combine the results is liable to cause issues. If you feel the need to call it
multiple times, please read the IMPORTANT note above, and/or consider
implementing your requirement via multiple fields/resolvers rather than trying
to do it all in one.
Resolvers are passed 4 arguments: parent, args, context, resolveInfo. In the
context.pgClient is an instance of a database client from the pg module
that's already in a transaction configured with the settings for this particular
GraphQL request. You can use this client to make requests to the database within
this transaction.
However, because PostGraphile uses Graphile Engine's look-ahead features, you
will not be able to easily build a query that will return the data PostGraphile
requires to represent nested relations/etc using pgClient directly. That is
why resolveInfo.graphile.selectGraphQLResultFromTable exists.
The resolveInfo.graphile.selectGraphQLResultFromTable function is vital if you
want to return PostGraphile database table/view/function/etc-related types from
your GraphQL field. It is responsible for hooking into the query look-ahead
features of Graphile Engine to inspect the incoming GraphQL query and pull down
the relevant data from the database (including nested relations). You are then
expected to return the result of this fetch via your resolver. You can use the
queryBuilder object to customize the generated query, changing the order,
adding where clauses, limits, etc (see below). Note that if you are not
returning a record type directly (for example you're returning a mutation
payload, or a connection interface), you should use the @pgField directive as
shown below so that the Look Ahead feature continues to work.
Usage for non-tables
Despite the (unfortunate) name; selectGraphQLResultFromTable can be used with
any table-like source, including a table-defining sub-query, however it should
only be used where the type perfectly matches the expected return type of the
GraphQL field.
This non-table support is particularly useful when it comes to calling
functions; for example if you had a function match_user() that returns a
users record, you could define a makeExtendSchemaPlugin resolver that
queries it like this:
// type Query { matchingUser(searchText: String!): User }
const matchingUserResolver = async (parent, args, context, resolveInfo) => {
const [row] = await resolveInfo.graphile.selectGraphQLResultFromTable(
sql.fragment`(select * from match_user(${sql.value(args.searchText)}))`,
() => {}, // no-op
);
return row;
};
QueryBuilder
queryBuilder is an instance of QueryBuilder, a helper that uses an SQL AST
constructed via pg-sql2 methods
to dynamically create powerful SQL queries without risking SQL injection
attacks. The queryBuilder has a number of methods which affect the query which
will be generated. The main ones you're likely to want are:
where(sqlFragment); e.g.queryBuilder.where(build.pgSql.fragment`is_admin is true`)orderBy(() => sqlFragment, ascending); e.g.queryBuilder.orderBy(() => build.pgSql.fragment`created_at`, false)limit(number); e.g.queryBuilder.limit(1)offset(number); e.g.queryBuilder.offset(1)select(() => sqlFragment, alias); e.g.queryBuilder.select(() => build.pgSql.fragment`gen_random_uuid()`, '__my_random_uuid')- it's advised to start your alias with two underscores to prevent it clashing with any potential columns exposed as GraphQL fields.
On top of these methods, QueryBuilder has the following useful properties:
parentQueryBuilder: gives access to the parent QueryBuilder instance; primarily (and possibly only) useful for executingqueryBuilder.parentQueryBuilder.getTableAlias()so you can reference a field on the parent record (e.g. to perform filtering based on a relation).
There are many other internal properties and methods, but you probably shouldn't call them. Only rely on the methods and properties documented above.
QueryBuilder named children
In very rare circumstances you might also need to use the following methods:
buildChild(); builds a child query builder, automatically passing through the relevant options and settingparentQueryBuilderfor you - useful for constructing subqueries (normally you'd usebuild.pgQueryFromResolveDatarather than using thebuildChildmethod directly)buildNamedChildSelecting(name, from, selectExpression); creates a child query builder that's namedname, selecting onlyselectExpressionusing the table (or subquery) described infrom.getNamedChild(name); gets the named child created bybuildNamedChildSelecting
An example of these methods being used can be found here: https://github.com/singingwolfboy/graphile-engine/blob/44a2496102267ce664c1286860b6368283463063/packages/postgraphile-core/__tests__/integration/ToyCategoriesPlugin.js
In this example we have a many-to-many relationship with three tables: toys,
categories and the join table between them: toy_categories. We add a
categories field onto the Toy type, which constructs a subquery called
toyCategoriesSubquery to determine the categories the current toy is in from
the join table toy_categories. Later, in a different plugin (just a different
hook in this example), we want to be able to filter this list of categories to
only the list of categories where the join table's toy_categories.approved
field is true; to do so we need to be able to get access to this "named"
subquery so that we can add conditions to it's WHERE clause.
In most cases you're only dealing with one or two tables so you won't need this level of complexity.
Query Example
The below is a simple example which would have been better served by
Custom Query SQL Procedures;
however it demonstrates using makeExtendSchemaPlugin with a database record,
table connection, and list of database records.
You can also use this system to define mutations or to call out to external services — see below.
const { postgraphile } = require("postgraphile");
const { makeExtendSchemaPlugin, gql } = require("graphile-utils");
const express = require("express");
const app = express();
const MyRandomUserPlugin = makeExtendSchemaPlugin((build) => {
const { pgSql: sql } = build;
return {
typeDefs: gql`
extend type Query {
# Individual record
randomUser: User
# Connection record
randomUsersConnection: UsersConnection
# List record
randomUsersList: [User!]
}
`,
resolvers: {
Query: {
/*
* Individual record needs to return just one row but
* `selectGraphQLResultFromTable` always returns an array; so the
* resolver is responsible for turning the array into a single record.
*/
randomUser: async (_query, args, context, resolveInfo) => {
// Remember: resolveInfo.graphile.selectGraphQLResultFromTable is where the PostGraphile
// look-ahead magic happens!
const rows = await resolveInfo.graphile.selectGraphQLResultFromTable(
sql.fragment`app_public.users`,
(tableAlias, queryBuilder) => {
queryBuilder.orderBy(sql.fragment`random()`);
queryBuilder.limit(1);
},
);
return rows[0];
},
/*
* Connection and list resolvers are identical; PostGraphile handles
* the complexities for you. We've simplified these down to a direct
* call to `selectGraphQLResultFromTable` but you may wish to wrap this
* with additional logic.
*/
randomUsersConnection: (_query, args, context, resolveInfo) =>
resolveInfo.graphile.selectGraphQLResultFromTable(
sql.fragment`app_public.users`,
(tableAlias, queryBuilder) => {
queryBuilder.orderBy(sql.fragment`random()`);
},
),
randomUsersList: (_query, args, context, resolveInfo) =>
resolveInfo.graphile.selectGraphQLResultFromTable(
sql.fragment`app_public.users`,
(tableAlias, queryBuilder) => {
queryBuilder.orderBy(sql.fragment`random()`);
},
),
},
},
};
});
app.use(
postgraphile(process.env.DATABASE_URL, ["app_public"], {
graphiql: true,
appendPlugins: [MyRandomUserPlugin],
}),
);
app.listen(3030);
Mutation Example
For example, you might want to add a custom registerUser mutation which
inserts the new user into the database and also sends them an email:
const MyRegisterUserMutationPlugin = makeExtendSchemaPlugin((build) => {
const { pgSql: sql } = build;
return {
typeDefs: gql`
input RegisterUserInput {
name: String!
email: String!
bio: String
}
type RegisterUserPayload {
user: User @pgField
query: Query
}
extend type Mutation {
registerUser(input: RegisterUserInput!): RegisterUserPayload
}
`,
resolvers: {
Mutation: {
registerUser: async (_query, args, context, resolveInfo) => {
const { pgClient } = context;
// Start a sub-transaction
await pgClient.query("SAVEPOINT graphql_mutation");
try {
// Our custom logic to register the user:
const {
rows: [user],
} = await pgClient.query(
`INSERT INTO app_public.users(
name, email, bio
) VALUES ($1, $2, $3)
RETURNING *`,
[args.input.name, args.input.email, args.input.bio],
);
// Now we fetch the result that the GraphQL
// client requested, using the new user
// account as the source of the data. You
// should always use
// `resolveInfo.graphile.selectGraphQLResultFromTable` if you return database
// data from your custom field.
const [row] =
await resolveInfo.graphile.selectGraphQLResultFromTable(
sql.fragment`app_public.users`,
(tableAlias, queryBuilder) => {
queryBuilder.where(
sql.fragment`${tableAlias}.id = ${sql.value(user.id)}`,
);
},
);
// Finally we send the email. If this
// fails then we'll catch the error
// and roll back the transaction, and
// it will be as if the user never
// registered
await mockSendEmail(
args.input.email,
"Welcome to my site",
`You're user ${user.id} - thanks for being awesome`,
);
// If the return type is a database record type, like User, then
// you would return `row` directly. However if it's an indirect
// interface such as a connection or mutation payload then
// you return an object with a `data` property. You can add
// additional properties too, that can be used by other fields
// on the result type.
return {
data: row,
query: build.$$isQuery,
};
} catch (e) {
// Oh noes! If at first you don't succeed,
// destroy all evidence you ever tried.
await pgClient.query("ROLLBACK TO SAVEPOINT graphql_mutation");
throw e;
} finally {
// Release our savepoint so it doesn't conflict with other mutations
await pgClient.query("RELEASE SAVEPOINT graphql_mutation");
}
},
},
},
};
});
Note that the @pgField directive here is necessary for PostGraphile to "look
ahead" and determine what to request from the database.
Working with arrays via json_array_elements
Here's an example of working with a join table, and bulk inserting multiple records from a GraphQL list.
...
typeDefs: gql`
input UpdatePersonsThingsInput {
personId: UUID!,
thingIds: [UUID!]!
}
type UpdatePersonThingsPayload {
personThings: [PersonThing!]
}
extend type Mutation {
updatePersonsThings(input: UpdatePersonsThingsInput!): UpdatePersonsThingsPayload
}
`,
resolvers: {
Mutation: {
updatePersonsThings: async (_query, { input: { personId, thingIds } }, { pgClient }, _resolveInfo) => {
await pgClient.query("SAVEPOINT graphql_mutation");
try {
// Ensure proper formatting. This may not be necessary if not modifying the input
const elements = JSON.stringify(thingIds.map(thingId => ({ thingId, personId })));
// Bulk insert
const { rows } = await pgClient.query(`
INSERT INTO public.persons_things (person_id, thing_id)
SELECT
(el->>'personId')::uuid,
(el->>'thingId')::uuid
FROM json_array_elements($1::json) el
RETURNING id
`, [elements]);
// Return data for next layer to use
return { personThingIds: rows.map(({ id }) => id) };
} catch (e) {
await pgClient.query("ROLLBACK TO SAVEPOINT graphql_mutation");
console.error(e);
throw e;
} finally {
await pgClient.query("RELEASE SAVEPOINT graphql_mutation");
}
},
},
UpdatePersonThingsPayload: {
personThings: ({ personThingIds }, _args, _context, { graphile: { selectGraphQLResultFromTable } }) => {
return selectGraphQLResultFromTable(
sql.fragment`public.persons_things`,
(tableAlias, queryBuilder) => {
queryBuilder.where(
sql.fragment`${tableAlias}.id = ANY (${sql.value(personThingIds)}::int[])`
);
}
);
}
}
}
Mutation Example with Node ID
In this example we'll use a GraphQL Global Object Identifier (aka Node ID) to
soft-delete an entry from our app_public.items table. We're also going to
check that the user performing the soft-delete is the owner of the record.
If you're interested in soft-deletes, check out @graphile-contrib/pg-omit-archived
const DeleteItemByNodeIdPlugin = makeExtendSchemaPlugin((build) => {
const typeDefs = gql`
input DeleteItemInput {
nodeId: ID!
}
type DeleteItemPayload {
success: Boolean
}
extend type Mutation {
deleteItem(input: DeleteItemInput!): DeleteItemPayload
}
`;
const resolvers = {
Mutation: {
deleteItem: async (_query, args, context) => {
// jwtClaims is decrypted jwt token data
const { pgClient, jwtClaims } = context;
// Decode the node ID
const { Type, identifiers } = build.getTypeAndIdentifiersFromNodeId(
args.input.nodeId,
);
// Check it applies to our type
if (Type !== build.getTypeByName("Item")) {
throw new Error("Invalid nodeId for Item");
}
// Assuming there's a single primary-key column, the PK will
// be the first and only entry in identifiers.
const itemId = identifiers[0];
// All mutations that issue SQL must be wrapped in savepoints
await pgClient.query("SAVEPOINT graphql_mutation");
try {
const { rowCount } = await pgClient.query(
`UPDATE app_public.items SET is_archived = true
WHERE id = $1
AND user_id = $2;`,
[itemId, jwtClaims.user_id],
);
return {
success: rowCount === 1,
};
} catch (e) {
await pgClient.query("ROLLBACK TO SAVEPOINT graphql_mutation");
throw e;
} finally {
await pgClient.query("RELEASE SAVEPOINT graphql_mutation");
}
},
},
};
return {
typeDefs,
resolvers,
};
});
Using the @pgQuery directive for non-root queries and better performance
If your field is not defined on the Query/Mutation type directly (i.e. it's
not defined at the root level) then for performance reasons you should hook into
the "look-ahead" system when adding a custom connection/list/record, rather than
using a resolver. You can achieve this with the @pgQuery directive, as shown
below. Alternative approaches you may wish to consider are
Smart Comments and Computed Columns.
@pgQuery with an object type
This section applies to PostGraphile v4.4.0+
When returning an object type (e.g. a table/composite type, connection, etc),
the @pgQuery directive accepts the following inputs:
source: the source of the row(s) used in the result; can be a table name, subquery, or function call (but must always return the relevant table type and nothing more); currently this requires the boilerplate syntax below, but this may be simplified in futurewithQueryBuilder(queryBuilder, args): this optional callback function is how you customise which rows will be returned from thesource; you may addwhere,orderBy,limitandoffsetconstraints. Theargsargument contains the arguments that the field was passed, if any. This may be useful when constructing the query constraints.
The @pgQuery directive may be used with connections, lists of table records,
or individual table records. (When used with individual records you must ensure
that at most one row is returned; you can do so with the queryBuilder.limit
constraint.) You can see examples of these three use cases
in the tests.
const { makeExtendSchemaPlugin, gql, embed } = require("graphile-utils");
module.exports = makeExtendSchemaPlugin((build) => {
const { pgSql: sql } = build;
return {
typeDefs: gql`
extend type User {
pets: PetsConnection @pgQuery(
source: ${embed(sql.fragment`app_public.pets`)}
withQueryBuilder: ${embed((queryBuilder, args) => {
queryBuilder.where(
sql.fragment`${queryBuilder.getTableAlias()}.user_id = ${queryBuilder.parentQueryBuilder.getTableAlias()}.id`,
);
})}
)
}
`,
};
});
Notes:
PetsConnectionis just one type from the schema, as an examplequeryBuilder.getTableAlias()refers to theapp_public.petsreferenced in thesourcefieldqueryBuilder.parentQueryBuilder.getTableAlias()refers to the table/function/view/etc from which theUser(the parent type) was retrieved- Regular connection arguments are added automatically thanks to the plugin system
@pgQuery with a leaf type
This section applies to PostGraphile v4.4.6+
It seems @pgQuery only supports scalars (not enums) right now:
https://github.com/graphile/postgraphile/issues/1601**
The @pgQuery directive can also be used with leaf fields (those returning a
scalar or list thereof). To do so, we pass @pgQuery a fragment: argument.
This argument can take two forms:
- an
sql.fragment - a function
f(queryBuilder, args)that returns asql.fragment.queryBuilderis aQueryBuilderinstance, andargsis the arguments that were passed to the GraphQL field.
const { makeExtendSchemaPlugin, gql, embed } = require("graphile-utils");
module.exports = makeExtendSchemaPlugin((build) => {
const { pgSql: sql } = build;
return {
typeDefs: gql`
extend type User {
nameWithSuffix(suffix: String!): String! @pgQuery(
fragment: ${embed(
(queryBuilder, args) =>
sql.fragment`(${queryBuilder.getTableAlias()}.name || ' ' || ${sql.value(
args.suffix,
)}::text)`,
)}
)
}
`,
};
});
Notes:
queryBuilder.getTableAlias()refers to the table/function/view/etc from which theUser(the parent type) was retrieved- there is no
queryBuilder.parentQueryBuilder
You can see more examples of these use cases in the tests.
Plugin SQL Privileges
Plugins access the database with the same privileges as everything else - they
are subject to RLS/RBAC/etc. If your user does not have privileges to perform
the action your plugin is attempting to achieve then you may need to create a
companion database function that is marked as SECURITY DEFINER in order to
perform the action with elevated privileges; alternatively you could use this
database function directly - see Custom Mutations for
more details.