PostGraphile v5 Plugins
Create custom plugins to extend PostGraphile's functionality using hooks.
Official Documentation
- Plugin Introduction: https://postgraphile.org/postgraphile/next/plugins
- Extending Schema: https://postgraphile.org/postgraphile/next/extending
When to Apply
Use this skill when:
- Adding custom query or mutation fields
- Detecting naming conflicts in multi-schema setups
- Adding metadata or introspection queries
- Modifying the build process
- Extending GraphQL types
Plugin Structure
import type { GraphileConfig } from 'graphile-config';
export const MyPlugin: GraphileConfig.Plugin = {
name: 'MyPlugin',
version: '1.0.0',
description: 'What this plugin does',
// Inflection overrides
inflection: {
replace: {
// Override inflectors
},
},
// Schema hooks
schema: {
hooks: {
// Build-time hooks
},
entityBehavior: {
// Behavior modifications
},
},
};
When Do Plugins Run?
Important: Plugins run at BUILD TIME, not on every request. The schema is built once and cached. This means:
- Hooks only execute during schema generation
- Logging in hooks only appears at startup
- Changes require server restart
Schema Hooks
Available Hooks
| Hook | When it runs | Use case |
|---|---|---|
init | Start of build | Collect metadata, setup |
build | After init | Analyze codecs, detect conflicts |
GraphQLObjectType_fields | When adding fields to types | Add custom fields |
GraphQLSchema | Final schema | Modify complete schema |
Hook: init
Runs at the start of schema building. Good for collecting metadata:
schema: {
hooks: {
init(_, build) {
const { pgRegistry } = build.input;
// Access all tables/resources
for (const resource of Object.values(pgRegistry.pgResources)) {
console.log('Table:', resource.name);
}
return _;
},
},
},
Hook: build
Runs after init. Good for analyzing the schema:
schema: {
hooks: {
build(build) {
// Access inflection
const inflection = build.inflection;
// Access codecs (types)
for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
if (codec.attributes) {
const typeName = inflection.tableType(codec);
console.log('Type:', typeName);
}
}
return build;
},
},
},
Hook: GraphQLObjectType_fields
Add or modify fields on GraphQL types:
schema: {
hooks: {
GraphQLObjectType_fields(fields, build, context) {
const { Self } = context;
// Only modify Query type
if (Self.name !== 'Query') {
return fields;
}
// Add a custom field
return {
...fields,
hello: {
type: build.graphql.GraphQLString,
resolve() {
return 'world';
},
},
};
},
},
},
Common Plugin Patterns
Conflict Detector Plugin
Detect naming conflicts between tables in different schemas:
import type { GraphileConfig } from 'graphile-config';
export const ConflictDetectorPlugin: GraphileConfig.Plugin = {
name: 'ConflictDetectorPlugin',
version: '1.0.0',
schema: {
hooks: {
build(build) {
const codecsByName = new Map<string, Array<{ schema: string; table: string }>>();
for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
if (!codec.attributes || codec.isAnonymous) continue;
const pgExtensions = codec.extensions?.pg as { schemaName?: string } | undefined;
const schemaName = pgExtensions?.schemaName || 'unknown';
const graphqlName = build.inflection.tableType(codec);
if (!codecsByName.has(graphqlName)) {
codecsByName.set(graphqlName, []);
}
codecsByName.get(graphqlName)!.push({
schema: schemaName,
table: codec.name,
});
}
// Log conflicts
for (const [graphqlName, tables] of codecsByName) {
if (tables.length > 1) {
const locations = tables.map(t => `${t.schema}.${t.table}`).join(', ');
console.warn(`NAMING CONFLICT: "${graphqlName}" from: ${locations}`);
}
}
return build;
},
},
},
};
Custom Mutation Plugin
Add a custom mutation:
import type { GraphileConfig } from 'graphile-config';
import {
GraphQLObjectType,
GraphQLNonNull,
GraphQLString,
GraphQLInputObjectType,
} from 'graphql';
export const CustomMutationPlugin: GraphileConfig.Plugin = {
name: 'CustomMutationPlugin',
version: '1.0.0',
schema: {
hooks: {
GraphQLObjectType_fields(fields, build, context) {
const { Self } = context;
if (Self.name !== 'Mutation') return fields;
const SendEmailInput = new GraphQLInputObjectType({
name: 'SendEmailInput',
fields: {
to: { type: new GraphQLNonNull(GraphQLString) },
subject: { type: new GraphQLNonNull(GraphQLString) },
body: { type: new GraphQLNonNull(GraphQLString) },
},
});
const SendEmailPayload = new GraphQLObjectType({
name: 'SendEmailPayload',
fields: {
success: { type: new GraphQLNonNull(build.graphql.GraphQLBoolean) },
messageId: { type: GraphQLString },
},
});
return {
...fields,
sendEmail: {
type: SendEmailPayload,
args: {
input: { type: new GraphQLNonNull(SendEmailInput) },
},
async resolve(_parent, args, context) {
const { to, subject, body } = args.input;
// Implement email sending logic
return { success: true, messageId: 'msg-123' };
},
},
};
},
},
},
};
Combining Inflection and Hooks
export const CompletePlugin: GraphileConfig.Plugin = {
name: 'CompletePlugin',
version: '1.0.0',
// Inflection overrides
inflection: {
replace: {
_schemaPrefix(_previous, _options, _details) {
return '';
},
},
},
// Schema hooks
schema: {
hooks: {
build(build) {
console.log('Schema building...');
return build;
},
},
// Behavior modifications
entityBehavior: {
pgResourceUnique: {
override: {
provides: ['myBehavior'],
callback(behavior, [_resource, unique]) {
if (!unique.isPrimary) {
return [behavior, '-single'];
}
return behavior;
},
},
},
},
},
};
Creating a Preset from Plugins
export const MyPluginPreset: GraphileConfig.Preset = {
plugins: [
ConflictDetectorPlugin,
MetaSchemaPlugin,
CustomMutationPlugin,
],
};
// Usage
const preset: GraphileConfig.Preset = {
extends: [
PostGraphileAmberPreset,
MyPluginPreset,
],
};
Troubleshooting
| Issue | Solution |
|---|---|
| Hook not called | Check plugin is in preset's plugins array |
| Changes not visible | Restart server (schema is cached) |
| Type errors | Import types from graphile-config |
| Can't access build properties | Check hook signature and available properties |
| Conflict with other plugins | Check plugin order in plugins array |
Source Code References
- graphile-build hooks: https://github.com/graphile/crystal/tree/main/graphile-build/graphile-build
- PgTablesPlugin (example): https://github.com/graphile/crystal/blob/main/graphile-build/graphile-build-pg/src/plugins/PgTablesPlugin.ts
References
- PostGraphile v5 Plugin Docs: https://postgraphile.org/postgraphile/next/plugins
- PostGraphile v5 Extending Schema: https://postgraphile.org/postgraphile/next/extending
- See
graphile-v5-inflectionskill for inflector customization - See
graphile-v5-behaviorsskill for behavior modifications - See
graphile-v5-debuggingskill for debugging plugins
