From 90f054865b9996c764c4f01cfc154d466a0643e0 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Fri, 20 Mar 2026 04:18:50 +0000 Subject: [PATCH 1/2] feat(graphile-settings): enable many-to-many relations by default Junction tables (e.g. contact_events, deal_contacts) now automatically generate clean shortcut fields that skip the junction table: event.contacts (instead of event.contactEventsByEventId) contact.deals (instead of contact.dealContactsByContactId) The InflektPlugin's _manyToManyRelation inflector picks short names (pluralized target table) and falls back to verbose defaults only when there is a naming conflict (direct relation to same target, or multiple m2m paths to the same target). To opt out for a specific junction table: COMMENT ON TABLE post_tags IS E'@behavior -manyToMany'; BREAKING CHANGE: many-to-many fields are now generated by default. Previously required @behavior +manyToMany on each junction table. --- .../src/plugins/many-to-many-preset.ts | 70 +++++++------------ .../src/presets/constructive-preset.ts | 2 +- .../__tests__/schema-snapshot.test.ts | 2 +- 3 files changed, 28 insertions(+), 46 deletions(-) diff --git a/graphile/graphile-settings/src/plugins/many-to-many-preset.ts b/graphile/graphile-settings/src/plugins/many-to-many-preset.ts index 36f09b14a..fa697c2b1 100644 --- a/graphile/graphile-settings/src/plugins/many-to-many-preset.ts +++ b/graphile/graphile-settings/src/plugins/many-to-many-preset.ts @@ -2,27 +2,27 @@ import type { GraphileConfig } from 'graphile-config'; import { PgManyToManyPreset } from '@graphile-contrib/pg-many-to-many'; /** - * Many-to-Many Preset with OPT-IN behavior (disabled by default). + * Many-to-Many Preset — ENABLED by default for clean relation naming. * * WHY THIS EXISTS: - * The default @graphile-contrib/pg-many-to-many plugin adds many-to-many connection - * fields to EVERY junction table automatically. This can bloat the API with fields - * that may never be used. + * Junction tables (e.g., `contact_events`, `deal_contacts`) create verbose + * relation names like `contactEventsByEventId` with nested traversals through + * the junction row. By enabling many-to-many by default, PostGraphile generates + * clean shortcut fields that skip the junction table: * - * OUR FIX: - * We override the default behavior to be OPT-IN instead of OPT-OUT: - * - By default, NO many-to-many fields are generated - * - To enable for a specific junction table, use: @behavior +manyToMany + * event.contacts (instead of event.contactEventsByEventId) + * contact.deals (instead of contact.dealContactsByContactId) * - * USAGE: - * To enable many-to-many for a specific junction table, add a smart comment: + * The InflektPlugin's `_manyToManyRelation` inflector automatically picks short + * names (pluralized target table) and falls back to the verbose default only + * when there is a naming conflict (direct relation to the same target table, + * or multiple m2m paths to the same target). * - * ```sql - * -- Enable many-to-many through this junction table - * COMMENT ON TABLE post_tags IS E'@behavior +manyToMany'; + * OPTING OUT: + * To disable many-to-many for a specific junction table, add a smart comment: * - * -- Or enable on a specific constraint - * COMMENT ON CONSTRAINT post_tags_tag_id_fkey ON post_tags IS E'@behavior +manyToMany'; + * ```sql + * COMMENT ON TABLE post_tags IS E'@behavior -manyToMany'; * ``` * * SOURCE CODE REFERENCE: @@ -33,45 +33,27 @@ import { PgManyToManyPreset } from '@graphile-contrib/pg-many-to-many'; * - `manyToMany` - Controls whether many-to-many fields are generated * - `connection` - Controls whether connection fields are generated * - `list` - Controls whether list fields are generated - * - * By default, the plugin's entityBehavior for pgManyToMany includes: - * `["manyToMany", "connection", "list", behavior]` - * - * We override this to be `-manyToMany` by default, requiring explicit opt-in. */ /** - * Plugin that makes many-to-many fields opt-in by default. + * Plugin that keeps many-to-many fields enabled by default. + * + * The upstream @graphile-contrib/pg-many-to-many plugin already enables m2m + * by default. This plugin is now a no-op passthrough that preserves that + * behavior. It remains in the preset chain so that the export names stay + * stable and existing code that imports `ManyToManyOptInPlugin` continues + * to work. * - * This overrides the default behavior from @graphile-contrib/pg-many-to-many - * to require explicit `@behavior +manyToMany` smart tags. + * To disable m2m for a specific junction table, use: @behavior -manyToMany */ export const ManyToManyOptInPlugin: GraphileConfig.Plugin = { name: 'ManyToManyOptInPlugin', - version: '1.0.0', - description: 'Makes many-to-many fields opt-in by default (disabled unless explicitly enabled)', - - schema: { - entityBehavior: { - pgManyToMany: { - // Override the default behavior to be opt-out (disabled by default) - // The 'inferred' phase runs before 'override', so we use 'inferred' - // to set the default before any smart tags are processed. - inferred: { - provides: ['manyToManyOptIn'], - before: ['default'], - callback(behavior) { - // Default to disabled - require explicit @behavior +manyToMany - return ['-manyToMany', behavior]; - }, - }, - }, - }, - }, + version: '2.0.0', + description: 'Many-to-many fields enabled by default (opt-out with @behavior -manyToMany)', }; /** - * Preset that includes the many-to-many plugin with opt-in behavior. + * Preset that includes the many-to-many plugin (enabled by default). * * Use this in your main preset's `extends` array: * ```typescript diff --git a/graphile/graphile-settings/src/presets/constructive-preset.ts b/graphile/graphile-settings/src/presets/constructive-preset.ts index 5c7cd87d3..967193bcf 100644 --- a/graphile/graphile-settings/src/presets/constructive-preset.ts +++ b/graphile/graphile-settings/src/presets/constructive-preset.ts @@ -31,7 +31,7 @@ import { constructiveUploadFieldDefinitions } from '../upload-resolver'; * - Inflector logging for debugging (enable with INFLECTOR_LOG=1) * - Primary key only lookups (no *ByEmail, *ByUsername, etc.) * - Connection filter plugin with all columns filterable - * - Many-to-many relationships (opt-in via @behavior +manyToMany) + * - Many-to-many relationships (enabled by default, opt-out via @behavior -manyToMany) * - Meta schema plugin (_meta query for introspection of tables, fields, indexes) * - PostGIS support (geometry/geography types, GeoJSON scalar — auto-detects PostGIS extension) * - PostGIS connection filter operators (spatial filtering on geometry/geography columns) diff --git a/graphql/server-test/__tests__/schema-snapshot.test.ts b/graphql/server-test/__tests__/schema-snapshot.test.ts index a2b9b2c22..b1552f7c9 100644 --- a/graphql/server-test/__tests__/schema-snapshot.test.ts +++ b/graphql/server-test/__tests__/schema-snapshot.test.ts @@ -13,7 +13,7 @@ * The snapshot captures how our plugins sculpt the GraphQL API: * - InflektPreset: table/field naming conventions * - PostGraphileConnectionFilterPreset: filter fields on connections - * - ManyToManyOptInPreset: many-to-many fields via @behavior +manyToMany + * - ManyToManyOptInPreset: many-to-many fields (enabled by default, opt-out via @behavior -manyToMany) * - MetaSchemaPreset: _meta query for introspection * - MinimalPreset: no Node/Relay (id stays as id) * - NoUniqueLookupPreset: no *ByEmail, *ByUsername lookups From de1a614fa5b25430d2cbb5fe46c3987a3271f758 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Fri, 20 Mar 2026 04:25:37 +0000 Subject: [PATCH 2/2] Revert "feat(graphile-settings): enable many-to-many relations by default" This reverts commit 90f054865b9996c764c4f01cfc154d466a0643e0. --- .../src/plugins/many-to-many-preset.ts | 70 ++++++++++++------- .../src/presets/constructive-preset.ts | 2 +- .../__tests__/schema-snapshot.test.ts | 2 +- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/graphile/graphile-settings/src/plugins/many-to-many-preset.ts b/graphile/graphile-settings/src/plugins/many-to-many-preset.ts index fa697c2b1..36f09b14a 100644 --- a/graphile/graphile-settings/src/plugins/many-to-many-preset.ts +++ b/graphile/graphile-settings/src/plugins/many-to-many-preset.ts @@ -2,27 +2,27 @@ import type { GraphileConfig } from 'graphile-config'; import { PgManyToManyPreset } from '@graphile-contrib/pg-many-to-many'; /** - * Many-to-Many Preset — ENABLED by default for clean relation naming. + * Many-to-Many Preset with OPT-IN behavior (disabled by default). * * WHY THIS EXISTS: - * Junction tables (e.g., `contact_events`, `deal_contacts`) create verbose - * relation names like `contactEventsByEventId` with nested traversals through - * the junction row. By enabling many-to-many by default, PostGraphile generates - * clean shortcut fields that skip the junction table: + * The default @graphile-contrib/pg-many-to-many plugin adds many-to-many connection + * fields to EVERY junction table automatically. This can bloat the API with fields + * that may never be used. * - * event.contacts (instead of event.contactEventsByEventId) - * contact.deals (instead of contact.dealContactsByContactId) + * OUR FIX: + * We override the default behavior to be OPT-IN instead of OPT-OUT: + * - By default, NO many-to-many fields are generated + * - To enable for a specific junction table, use: @behavior +manyToMany * - * The InflektPlugin's `_manyToManyRelation` inflector automatically picks short - * names (pluralized target table) and falls back to the verbose default only - * when there is a naming conflict (direct relation to the same target table, - * or multiple m2m paths to the same target). - * - * OPTING OUT: - * To disable many-to-many for a specific junction table, add a smart comment: + * USAGE: + * To enable many-to-many for a specific junction table, add a smart comment: * * ```sql - * COMMENT ON TABLE post_tags IS E'@behavior -manyToMany'; + * -- Enable many-to-many through this junction table + * COMMENT ON TABLE post_tags IS E'@behavior +manyToMany'; + * + * -- Or enable on a specific constraint + * COMMENT ON CONSTRAINT post_tags_tag_id_fkey ON post_tags IS E'@behavior +manyToMany'; * ``` * * SOURCE CODE REFERENCE: @@ -33,27 +33,45 @@ import { PgManyToManyPreset } from '@graphile-contrib/pg-many-to-many'; * - `manyToMany` - Controls whether many-to-many fields are generated * - `connection` - Controls whether connection fields are generated * - `list` - Controls whether list fields are generated + * + * By default, the plugin's entityBehavior for pgManyToMany includes: + * `["manyToMany", "connection", "list", behavior]` + * + * We override this to be `-manyToMany` by default, requiring explicit opt-in. */ /** - * Plugin that keeps many-to-many fields enabled by default. - * - * The upstream @graphile-contrib/pg-many-to-many plugin already enables m2m - * by default. This plugin is now a no-op passthrough that preserves that - * behavior. It remains in the preset chain so that the export names stay - * stable and existing code that imports `ManyToManyOptInPlugin` continues - * to work. + * Plugin that makes many-to-many fields opt-in by default. * - * To disable m2m for a specific junction table, use: @behavior -manyToMany + * This overrides the default behavior from @graphile-contrib/pg-many-to-many + * to require explicit `@behavior +manyToMany` smart tags. */ export const ManyToManyOptInPlugin: GraphileConfig.Plugin = { name: 'ManyToManyOptInPlugin', - version: '2.0.0', - description: 'Many-to-many fields enabled by default (opt-out with @behavior -manyToMany)', + version: '1.0.0', + description: 'Makes many-to-many fields opt-in by default (disabled unless explicitly enabled)', + + schema: { + entityBehavior: { + pgManyToMany: { + // Override the default behavior to be opt-out (disabled by default) + // The 'inferred' phase runs before 'override', so we use 'inferred' + // to set the default before any smart tags are processed. + inferred: { + provides: ['manyToManyOptIn'], + before: ['default'], + callback(behavior) { + // Default to disabled - require explicit @behavior +manyToMany + return ['-manyToMany', behavior]; + }, + }, + }, + }, + }, }; /** - * Preset that includes the many-to-many plugin (enabled by default). + * Preset that includes the many-to-many plugin with opt-in behavior. * * Use this in your main preset's `extends` array: * ```typescript diff --git a/graphile/graphile-settings/src/presets/constructive-preset.ts b/graphile/graphile-settings/src/presets/constructive-preset.ts index 967193bcf..5c7cd87d3 100644 --- a/graphile/graphile-settings/src/presets/constructive-preset.ts +++ b/graphile/graphile-settings/src/presets/constructive-preset.ts @@ -31,7 +31,7 @@ import { constructiveUploadFieldDefinitions } from '../upload-resolver'; * - Inflector logging for debugging (enable with INFLECTOR_LOG=1) * - Primary key only lookups (no *ByEmail, *ByUsername, etc.) * - Connection filter plugin with all columns filterable - * - Many-to-many relationships (enabled by default, opt-out via @behavior -manyToMany) + * - Many-to-many relationships (opt-in via @behavior +manyToMany) * - Meta schema plugin (_meta query for introspection of tables, fields, indexes) * - PostGIS support (geometry/geography types, GeoJSON scalar — auto-detects PostGIS extension) * - PostGIS connection filter operators (spatial filtering on geometry/geography columns) diff --git a/graphql/server-test/__tests__/schema-snapshot.test.ts b/graphql/server-test/__tests__/schema-snapshot.test.ts index b1552f7c9..a2b9b2c22 100644 --- a/graphql/server-test/__tests__/schema-snapshot.test.ts +++ b/graphql/server-test/__tests__/schema-snapshot.test.ts @@ -13,7 +13,7 @@ * The snapshot captures how our plugins sculpt the GraphQL API: * - InflektPreset: table/field naming conventions * - PostGraphileConnectionFilterPreset: filter fields on connections - * - ManyToManyOptInPreset: many-to-many fields (enabled by default, opt-out via @behavior -manyToMany) + * - ManyToManyOptInPreset: many-to-many fields via @behavior +manyToMany * - MetaSchemaPreset: _meta query for introspection * - MinimalPreset: no Node/Relay (id stays as id) * - NoUniqueLookupPreset: no *ByEmail, *ByUsername lookups