graphile-v5-plugins

star 0

Create custom PostGraphile v5 plugins using hooks. Use when asked to "create a plugin", "add custom field", "extend schema", "detect conflicts", "add metadata query", or when you need to extend PostGraphile's functionality.

constructive-io By constructive-io schedule Updated 1/27/2026

name: graphile-v5-plugins description: Create custom PostGraphile v5 plugins using hooks. Use when asked to "create a plugin", "add custom field", "extend schema", "detect conflicts", "add metadata query", or when you need to extend PostGraphile's functionality. compatibility: PostGraphile v5+, graphile-config, graphile-build metadata: author: constructive-io version: "1.0.0"

PostGraphile v5 Plugins

Create custom plugins to extend PostGraphile's functionality using hooks.

Official Documentation

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

References

Install via CLI
npx skills add https://github.com/constructive-io/postgraphile-skills --skill graphile-v5-plugins
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
constructive-io
constructive-io Explore all skills →