| name | graphile-v5-behaviors |
| description | Control PostGraphile v5 schema generation with the behavior system. Use when asked to "disable mutations", "hide fields", "control what gets generated", "disable unique lookups", "make features opt-in", or when you need fine-grained control over the GraphQL schema. |
| compatibility | PostGraphile v5+, graphile-config |
| metadata | {"author":"constructive-io","version":"1.0.0"} |
PostGraphile v5 Behaviors
Control what gets generated in your GraphQL schema using the behavior system.
Official Documentation
When to Apply
Use this skill when:
- You want to disable certain queries or mutations
- You need to hide specific fields or types
- You want to make features opt-in instead of opt-out
- You need to control unique constraint lookups
- You want to enable/disable filtering on specific columns
Understanding Behaviors
Behaviors are strings that control what gets generated. They can be:
- Positive:
single, update, delete, filterBy, manyToMany
- Negative:
-single, -update, -delete, -filterBy, -manyToMany
Behaviors are processed in phases:
- inferred - Default behaviors based on database structure
- override - Final say on what's enabled/disabled
Common Behaviors
| Behavior | Controls |
|---|
single | Single-row query lookups (e.g., user(id)) |
update | Update mutations |
delete | Delete mutations |
insert | Insert mutations |
filterBy | Column appears in filter arguments |
orderBy | Column appears in orderBy arguments |
manyToMany | Many-to-many relation fields |
constraint:resource:update | Update by specific constraint |
constraint:resource:delete | Delete by specific constraint |
Using entityBehavior
Basic Structure
export const MyBehaviorPlugin: GraphileConfig.Plugin = {
name: 'MyBehaviorPlugin',
version: '1.0.0',
schema: {
entityBehavior: {
pgResourceUnique: {
override: {
provides: ['myBehaviorControl'],
callback(behavior, [resource, unique]) {
return [behavior, '-single'];
},
},
},
},
},
};
Entity Types
| Entity Type | What it targets |
|---|
pgResource | Tables and views |
pgResourceUnique | Unique constraints (including primary keys) |
pgCodecAttribute | Table columns |
pgCodecRelation | Foreign key relations |
pgManyToMany | Many-to-many junction tables |
Common Patterns
Disable Non-Primary-Key Lookups
Only allow lookups by primary key, not by other unique constraints:
export const PrimaryKeyOnlyPlugin: GraphileConfig.Plugin = {
name: 'PrimaryKeyOnlyPlugin',
version: '1.0.0',
schema: {
entityBehavior: {
pgResourceUnique: {
override: {
provides: ['primaryKeyOnly'],
callback(behavior, [_resource, unique]) {
if (!unique.isPrimary) {
return [
behavior,
'-single',
'-constraint:resource:update',
'-constraint:resource:delete',
];
}
return behavior;
},
},
},
},
},
};
Disable All Unique Lookups (Use Filters Instead)
Force users to use collection queries with filters:
export const NoUniqueLookupPlugin: GraphileConfig.Plugin = {
name: 'NoUniqueLookupPlugin',
version: '1.0.0',
schema: {
entityBehavior: {
pgResourceUnique: {
override: {
provides: ['noUniqueLookups'],
callback(behavior, [_resource, unique]) {
if (unique.isPrimary) {
return [behavior, '-single'];
}
return [
behavior,
'-single',
'-constraint:resource:update',
'-constraint:resource:delete',
];
},
},
},
},
},
};
Make Many-to-Many Opt-In
By default, disable many-to-many fields; require explicit opt-in:
export const ManyToManyOptInPlugin: GraphileConfig.Plugin = {
name: 'ManyToManyOptInPlugin',
version: '1.0.0',
schema: {
entityBehavior: {
pgManyToMany: {
inferred: {
provides: ['manyToManyOptIn'],
before: ['default'],
callback(behavior) {
return ['-manyToMany', behavior];
},
},
},
},
},
};
Then enable for specific tables with smart tags:
COMMENT ON TABLE post_tags IS E'@behavior +manyToMany';
Enable Filtering on All Columns
Override the default index-based filtering restriction:
export const EnableAllFilterColumnsPlugin: GraphileConfig.Plugin = {
name: 'EnableAllFilterColumnsPlugin',
version: '1.0.0',
schema: {
entityBehavior: {
pgCodecAttribute: {
inferred: {
after: ['postInferred'],
provides: ['enableAllFilters'],
callback(behavior) {
return [behavior, 'filterBy'];
},
},
},
},
},
};
Disable Filtering on Specific Columns
export const DisableSensitiveFiltersPlugin: GraphileConfig.Plugin = {
name: 'DisableSensitiveFiltersPlugin',
version: '1.0.0',
schema: {
entityBehavior: {
pgCodecAttribute: {
override: {
provides: ['disableSensitiveFilters'],
callback(behavior, [codec, attributeName]) {
if (attributeName.includes('password') || attributeName.includes('secret')) {
return [behavior, '-filterBy'];
}
return behavior;
},
},
},
},
},
};
Inferred vs Override Phase
Inferred Phase
- Runs early in behavior processing
- Good for setting defaults
- Can be overridden by smart tags and later phases
inferred: {
provides: ['myDefault'],
before: ['default'],
callback(behavior) {
return ['-manyToMany', behavior];
},
},
Override Phase
- Runs after all other processing
- Has the final say
- Use for hard requirements that can't be overridden
override: {
provides: ['myOverride'],
callback(behavior, entity) {
return [behavior, '-single'];
},
},
Using Smart Tags
Smart tags provide per-entity behavior control without plugins:
COMMENT ON TABLE audit_logs IS E'@behavior -insert -update -delete';
COMMENT ON TABLE post_tags IS E'@behavior +manyToMany';
COMMENT ON COLUMN users.internal_id IS E'@behavior -filterBy';
COMMENT ON CONSTRAINT users_email_key ON users IS E'@behavior -single';
Complete Example: Primary Key Only Preset
import type { GraphileConfig } from 'graphile-config';
export interface UniqueLookupOptions {
disableAllUniqueLookups?: boolean;
}
export function createUniqueLookupPlugin(
options: UniqueLookupOptions = {}
): GraphileConfig.Plugin {
const { disableAllUniqueLookups = false } = options;
return {
name: 'UniqueLookupPlugin',
version: '1.0.0',
schema: {
entityBehavior: {
pgResourceUnique: {
override: {
provides: ['uniqueLookupControl'],
callback(behavior, [_resource, unique]) {
if (disableAllUniqueLookups) {
if (unique.isPrimary) {
return [behavior, '-single'];
}
return [
behavior,
'-single',
'-constraint:resource:update',
'-constraint:resource:delete',
];
}
if (!unique.isPrimary) {
return [
behavior,
'-single',
'-constraint:resource:update',
'-constraint:resource:delete',
];
}
return behavior;
},
},
},
},
},
};
}
export const PrimaryKeyOnlyPlugin = createUniqueLookupPlugin({ disableAllUniqueLookups: false });
export const NoUniqueLookupPlugin = createUniqueLookupPlugin({ disableAllUniqueLookups: true });
export const PrimaryKeyOnlyPreset: GraphileConfig.Preset = {
plugins: [PrimaryKeyOnlyPlugin],
};
export const NoUniqueLookupPreset: GraphileConfig.Preset = {
plugins: [NoUniqueLookupPlugin],
};
Troubleshooting
| Issue | Solution |
|---|
| Behavior not applied | Check phase (inferred vs override) |
| Smart tag overrides plugin | Use override phase instead of inferred |
| Can't find entity type | Check PostGraphile source for correct entity name |
| Behavior string format | Use array format: [behavior, '-single'] |
Source Code References
References
- PostGraphile v5 Behavior Docs: https://postgraphile.org/postgraphile/next/behavior
- See
graphile-v5-presets skill for combining behavior plugins
- See
graphile-v5-connection-filter skill for filter configuration
- See
graphile-v5-debugging skill for graphile behavior debug CLI command