AgentSkillsCN

graphile-v5-plugins

利用钩子创建自定义PostGraphile v5插件。当被要求“创建插件”、“添加自定义字段”、“扩展Schema”、“检测冲突”、“添加元数据查询”,或当你需要进一步拓展PostGraphile的功能时,此功能将为你打开无限可能。

SKILL.md
--- frontmatter
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

typescript
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

HookWhen it runsUse case
initStart of buildCollect metadata, setup
buildAfter initAnalyze codecs, detect conflicts
GraphQLObjectType_fieldsWhen adding fields to typesAdd custom fields
GraphQLSchemaFinal schemaModify complete schema

Hook: init

Runs at the start of schema building. Good for collecting metadata:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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

typescript
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

typescript
export const MyPluginPreset: GraphileConfig.Preset = {
  plugins: [
    ConflictDetectorPlugin,
    MetaSchemaPlugin,
    CustomMutationPlugin,
  ],
};

// Usage
const preset: GraphileConfig.Preset = {
  extends: [
    PostGraphileAmberPreset,
    MyPluginPreset,
  ],
};

Troubleshooting

IssueSolution
Hook not calledCheck plugin is in preset's plugins array
Changes not visibleRestart server (schema is cached)
Type errorsImport types from graphile-config
Can't access build propertiesCheck hook signature and available properties
Conflict with other pluginsCheck plugin order in plugins array

Source Code References

References