Skip to main content
Version: Next

makeWrapPlansPlugin

PostGraphile generates plan resolvers automatically for the fields that it generates across your GraphQL API. But sometimes, you want to change what these plans do. Commonly you might want to apply additional filters to a collection, or perform additional actions (or validations) before performing a mutation. Rather than rewriting the field and it's plans from scratch, you can "wrap" the plan that PostGraphile has made with one of your own.

makeWrapPlansPlugin helps you to easily generate a "schema plugin" for 'wrapping' the plan resolvers generated by PostGraphile. You can load the resulting schema plugin via your graphile.config.mjs (or similar) preset.

danger

Some of the fields in the GraphQL schema expect their parent value to be of a particular step class, so in these cases you must be careful to ensure that you don't break this expectation otherwise planning errors may occur. This is typically not a concern with modifying the result of "leaf" fields (e.g. masking a users email address) since generally these do not depend on the specific step type.

Signatures

There are two variants of makeWrapPlansPlugin with slightly different signatures (function overloading). These reflect the two methods of calling makeWrapPlansPlugin. If you want to wrap one or two specific resolvers (where you know the type name and field name) then method 1 is a handy shortcut. If, however, you want to wrap a number of resolvers in the same way then the more flexible method 2 is what you want.

// Method 1: wrap individual resolvers of known fields
function makeWrapPlansPlugin(
rulesOrGenerator: PlanWrapperRules | PlanWrapperRulesGenerator,
): GraphileConfig.Plugin;

interface PlanWrapperRules {
[typeName: string]: {
[fieldName: string]: PlanWrapperRule | PlanWrapperFn;
};
}

interface PlanWrapperRule {
plan?: PlanWrapperFn;
}

type PlanWrapperFn = (
plan: SmartFieldPlanResolver,
$source: ExecutableStep,
fieldArgs: FieldArgs,
info: FieldInfo,
) => any;

type PlanWrapperRulesGenerator = (
build: Partial<GraphileBuild.Build> & GraphileBuild.BuildBase,
) => PlanWrapperRules;

/****************************************/

// Method 2: wrap all resolvers that match a filter function
function makeWrapPlansPlugin<T>(
filter: (
context: GraphileBuild.ContextObjectFieldsField,
build: GraphileBuild.Build,
field: GrafastFieldConfig,
) => T | null,
rule: (match: T) => PlanWrapperRule | PlanWrapperFn,
): GraphileConfig.Plugin;

Method 1: wrapping individual resolvers of known fields

// Method 1: wrap individual resolvers of known fields
function makeWrapPlansPlugin(
rulesOrGenerator: PlanWrapperRules | PlanWrapperRulesGenerator,
): GraphileConfig.Plugin;

In this method, makeWrapPlansPlugin takes either the resolver wrapper rules object directly, or a generator for this rules object, and returns a plugin.

Example: convert to lower case

import { makeWrapPlansPlugin } from "postgraphile/utils";
import { lambda } from "postgraphile/grafast";

export default makeWrapPlansPlugin({
User: {
email(plan) {
const $email = plan();
return lambda($email, (email) => email.toLowerCase());
},
},
});

Example: multiple fields

When there's an explicit list of fields that you want to wrap in the same way, this method can still be useful:

import { sideEffect } from "postgraphile/grafast";

function assertValidUserData(data) {
if (!data || data.username?.length === 0) {
throw new Error("Invalid data");
}
}

const validateUserData = (propName) => {
return (plan, $source, fieldArgs) => {
const $user = fieldArgs.getRaw(["input", propName]);

// Callback throws error if invalid
sideEffect($user, (user) => assertValidUserData(user));

return plan();
};
};

export default makeWrapPlansPlugin({
Mutation: {
createUser: validateUserData("user"),
updateUser: validateUserData("userPatch"),
updateUserById: validateUserData("userPatch"),
updateUserByEmail: validateUserData("userPatch"),
},
});

Rules object

The rules object is a two-level map of typeName (the name of a GraphQLObjectType) and fieldName (the name of one of the fields of this type) to either a rule for that field, or a resolver wrapper function for that field. The generator function accepts the Build object which can sometimes be useful, e.g. to get the preset's schema options, or to retrieve things from the registry.

interface PlanWrapperRules {
[typeName: string]: {
[fieldName: string]: PlanWrapperRule | PlanWrapperFn;
};
}

type PlanWrapperRulesGenerator = (
build: Partial<GraphileBuild.Build> & GraphileBuild.BuildBase,
) => PlanWrapperRules;

Read about plan resolver wrapper functions below.

Example: null email unless own

For example, this plugin wraps the User.email field, returning null if the user requesting the field is not the same as the user for which the email was requested. (Note that the email is still retrieved from the database, it is just not returned to the user.)

import { makeWrapPlansPlugin } from "postgraphile/utils";
import { context, lambda } from "postgraphile/grafast";

export default makeWrapPlansPlugin({
User: {
email(plan, $user) {
const $userId = $user.get("id");
const $currentUserId = context().get("jwtClaims").get("user_id");
const $email = plan();
return lambda(
[$userId, $currentUserId, $email],
([userId, currentUserId, email]) =>
userId === currentUserId ? email : null,
);
},
},
});

Example: mask email

This example uses the default resolver of the User.email field to get the actual value, then masks the value instead of omitting it.

import { makeWrapPlansPlugin } from "postgraphile/utils";
import { lambda } from "postgraphile/grafast";

export default makeWrapPlansPlugin({
User: {
email(plan) {
const $email = plan();
return lambda($email, (email) =>
// [email protected] -> so***@su***.com
email.replace(
/^(.{1,2})[^@]*@(.{,2})[^.]*\.([A-z]{2,})$/,
"$1***@$2***.$3",
),
);
},
},
});

Method 2: wrap all resolvers matching a filter

function makeWrapPlansPlugin<T>(
filter: (
context: GraphileBuild.ContextObjectFieldsField,
build: GraphileBuild.Build,
field: GrafastFieldConfig,
) => T | null,
rule: (match: T) => PlanWrapperRule | PlanWrapperFn,
): GraphileConfig.Plugin;

In this method, makeWrapPlansPlugin takes two function arguments. The first function is a filter that is called for each field; it should return a truthy value if the field is to be wrapped (or null otherwise). The second function is called for each field that passes the filter, it will be passed the return value of the filter and must return a resolve wrapper function or rule (see plan resolver wrapper functions below).

The filter is called with the following arguments:

  • context: the Context value of the field, the context.scope property is the most likely to be used
  • build: the Build objects which contains a lot of helpers
  • field: the field specification itself

The value you return can be any arbitrary truthy value, it should contain anything from the above arguments that you need to create your resolver wrapper.

import { makeWrapPlansPlugin } from "postgraphile/utils";
import { sideEffect } from "postgraphile/grafast";

// Example: log before and after each mutation runs
export default makeWrapPlansPlugin(
(context) => {
if (context.scope.isRootMutation) {
return { scope: context.scope };
}
return null;
},
({ scope }) =>
(plan, _, fieldArgs) => {
sideEffect(fieldArgs.getRaw(), (args) => {
console.log(
`Mutation '${scope.fieldName}' starting with arguments:`,
args,
);
});

const $result = plan();

sideEffect($result, (result) => {
console.log(`Mutation '${scope.fieldName}' result:`, result);
});

return $result;
},
);

Plan resolver wrapper functions

A resolver wrapper function is similar to a Grafast plan resolver, except it takes an additional argument (at the start) which allows delegating to the plan resolver that is being wrapped. If and when you call the plan function, you may optionally pass one or more of the arguments $source, fieldArgs, info; these will then override the values that the resolver will be passed. Calling plan() with no arguments will just pass through the original values unmodified.

type PlanWrapperFn = (
plan: SmartFieldPlanResolver,
$source: ExecutableStep,
fieldArgs: FieldArgs,
info: FieldInfo,
) => any;