Skip to content

feat: v5 relations — M:N ORM methods, orm-test package, filter type-safety + composite PK support#880

Merged
pyramation merged 19 commits intomainfrom
devin/1774088288-v5-relations-unified
Mar 21, 2026
Merged

feat: v5 relations — M:N ORM methods, orm-test package, filter type-safety + composite PK support#880
pyramation merged 19 commits intomainfrom
devin/1774088288-v5-relations-unified

Conversation

@pyramation
Copy link
Contributor

@pyramation pyramation commented Mar 21, 2026

Summary

Unified PR combining four feature branches for v5 M:N relation support. PR #863 (_meta plumbing + Clean* rename) was already merged to main; this combines the three remaining open PRs (#867, #869, #879).

1. M:N ORM add/remove methods (model-generator.ts, query-builder.ts, enrich-relations.ts, pipeline/index.ts)

  • Pipeline wires tablesMeta from source.fetch() through a new enrichManyToManyRelations step that populates junction key fields on ManyToManyRelation
  • Generates add<Singular>/remove<Singular> class methods on ORM models for each enriched M:N relation
  • Uses getPrimaryKeyInfo() + tsTypeFromPrimitive() for correct PK typing, inflekt.singularize() for method names, existing getCreateMutationName/getDeleteMutationName utilities for naming
  • buildJunctionRemoveDocument added to query-builder for composite-key junction deletion

2. Composite PK detection & delete support (infer-tables.ts, model-generator.ts, query-builder.ts)

  • inferPrimaryKeyFromInputObject now returns IntrospectionInputValue[] instead of | null when multiple candidates exist
  • Enables detection of composite PKs on junction tables like PostTag(postId, tagId)
  • Prefers DeleteInput over UpdateInput as signal source (fewer non-PK fields)
  • matchMutationOperations broadened to match deleteXxxBy* and updateXxxBy* patterns (PostGraphile generates deletePostTagByPostIdAndTagId for composite PK tables)
  • buildDeleteByPkDocument takes a keys object ({ id: args.where.id } or { postId, tagId }) instead of separate id/idFieldName params — supports both single and composite PK deletes

3. Consolidated input type name derivation (utils.ts, infer-tables.ts, model-generator.ts)

  • getDeleteInputTypeName(table) and getUpdateInputTypeName(table) in utils.ts now derive from actual mutation names (table.query?.delete/table.query?.update) when available, handling composite PK naming like DeletePostTagByPostIdAndTagIdInput
  • New inputTypeFromMutation() helper in infer-tables.ts replaces 3 ad-hoc ucFirst(mutationName) + 'Input' calls
  • model-generator.ts now calls getDeleteInputTypeName(table) instead of inline derivation (2 sites)
  • 5 scattered ad-hoc derivations → 2 shared helpers. One function controls each name, one place to make a mistake.

4. Filter type-safety (input-types-generator.ts)

  • buildTableFilterProperties uses the schema's {Entity}Filter input type as sole source of truth via TypeRegistry when available (same pattern as buildOrderByValues)
  • Plugin-injected filter fields (bm25, tsvector, trgm, pgvector, PostGIS, connection-filter) now appear typed in generated code
  • collectFilterExtraInputTypes discovers custom filter types referenced by plugin fields for generation
  • Falls back to existing table.fields-based generation when TypeRegistry is absent

5. orm-test package (graphql/orm-test/)

  • New test package running generated ORM models against real PostgreSQL via graphile-test
  • Uses ConstructivePreset in both test suites — same preset as production, ensuring tests exercise the full plugin stack including InflektPlugin's clean M:N field names
  • M:N integration tests (12 tests): seeds posts/tags/post_tags (composite PK, no surrogate id), runs full codegen pipeline, exercises ORM CRUD + junction mutations including ORM-based composite PK deletes (no raw GraphQL)
  • Mega query tests (40 tests): exercises all 7 ConstructivePreset plugins (tsvector, BM25, pg_trgm, pgvector, PostGIS, connection-filter, M:N) with concrete score assertions

Updates since last revision

  • Input type naming consolidated into shared helpers: getDeleteInputTypeName and getUpdateInputTypeName in utils.ts now derive from actual mutation names when table.query?.delete/table.query?.update is set. inputTypeFromMutation() helper added to infer-tables.ts. Eliminated 5 ad-hoc ucFirst(mutationName) + 'Input' scattered across infer-tables.ts (3 sites) and model-generator.ts (2 sites).
  • Snapshot update: Custom mutation names now correctly derive their own input types (e.g. removeOrganizationRemoveOrganizationInput instead of DeleteOrganizationInput).

Review & Testing Checklist for Human

  • Verify getDeleteInputTypeName/getUpdateInputTypeName behavioral change for existing callers — These helpers in utils.ts now consult table.query?.delete/table.query?.update. Other callers (mutations.ts, cli-generator.ts) previously always got Delete${table.name}Input. If those code paths receive tables with table.query?.delete set (e.g. composite PK tables from introspection), they'll now get the mutation-derived name. Verify this is correct or that those callers don't encounter composite-PK tables.
  • Verify composite PK delete works for both single-PK and composite-PK tablesbuildDeleteByPkDocument API changed from (entityName, mutation, field, id, inputType, idFieldName, select, map) to (entityName, mutation, field, keysObject, inputType, select, map). The snapshot shows single-PK tables pass { id: args.where.id }. Confirm no callers outside codegen use the old signature directly.
  • Validate startsWith matching in matchMutationOperationsfield.name.startsWith(${expectedDelete}By) could theoretically match unintended mutations if entity names overlap. The By suffix prevents false positives between e.g. deletePost and deletePostTag (checks deletePostBy* vs deletePostTagBy*), but verify this holds for your schema.
  • Validate composite PK assumption — the fix assumes all non-clientMutationId/non-patch fields in DeleteInput are PK fields. Confirm no plugins inject extra non-PK fields into Delete input types.
  • Recommended test plan: cd graphql/codegen && pnpm test (317 tests + 121 snapshots), cd graphql/query && pnpm test (18 tests), cd graphql/orm-test && pnpm test (52 tests against postgres-plus:18). Also run pnpm generate against a real schema with M:N junction tables using composite PKs and verify the generated delete methods compile and work correctly.

Notes

Link to Devin session: https://app.devin.ai/sessions/745d2d10b699452091e24131ba5edef2
Requested by: @pyramation


Open with Devin

pyramation and others added 14 commits March 21, 2026 00:27
… add/remove junction methods

- Destructure tablesMeta in runCodegenPipeline() alongside introspection
- Create enrich-relations.ts to populate ManyToManyRelation junction key fields from _meta
- Add buildJunctionRemoveDocument helper to query-builder template
- Generate add<Relation>/remove<Relation> methods on ORM models for M:N relations
- Methods delegate to junction table's existing create/delete CRUD mutations
… for M:N methods

- Use getCreateInputTypeName/getDeleteInputTypeName/getCreateMutationName/getDeleteMutationName instead of manual string construction
- Resolve actual PK types via getPrimaryKeyInfo() instead of hardcoding string
- Only import buildJunctionRemoveDocument when remove methods are actually generated
- Remove redundant leftTable lookup (table IS the left table)
…reSQL

New graphql/orm-test package that exercises ORM M:N patterns against a
live PostGraphile schema via graphile-test:

- SQL fixtures: posts, tags, post_tags junction with @behavior +manyToMany
- ManyToManyOptInPreset enabled for M:N connection fields
- Tests: addTag (createPostTag), removeTag (deletePostTagByPostIdAndTagId,
  deletePostTagByRowId), CRUD, reverse M:N direction, unique constraint
- 10/10 tests pass locally
Exercises tsvector, BM25 (pg_textsearch), pg_trgm, pgvector, PostGIS,
relation filters, scalar filters, and M:N junctions in one test suite.

Crown jewel: a single GraphQL request combining all 7 plugin types
with multi-signal ORDER BY (BM25 score + trgm similarity).

36 mega-query tests + 10 M:N tests = 46 total, all passing.
- Add codegen-helper.ts: orchestrates introspection → inferTables → generateOrm → compile → load at test runtime
- Add graphile-adapter.ts: wraps graphile-test query() as GraphQLAdapter for ORM client
- Rewrite orm-m2n.test.ts: uses generated ORM for findMany/create, raw GraphQL for delete (junction PK not inferred)
- Rewrite mega-query.test.ts: uses generated ORM for all 7 ConstructivePreset plugins
- Fix vectorEmbedding filter shape: VectorNearbyInput is flat {vector, distance}, not nested {nearby: {embedding}}
- Fix BM25 assertions: BM25 returns all rows with scores (0 for non-matches), doesn't filter
- Each test suite gets isolated __generated__/<name> directory to avoid parallel collisions
- Add @constructive-io/graphql-codegen dependency to orm-test package.json
- 48 tests pass (12 M:N + 36 mega query)
…er seed data

- Richer seed body texts for dramatic BM25 score separation
- Semantic vector embeddings clustered by category (restaurant/park/museum)
- Concrete assertions: BM25 ranking order, trgm similarity thresholds, tsvRank ranges
- Fix vectorEmbedding filter shape: use flat {vector, distance} (not nested nearby/embedding)
- 3 new pgvector cluster tests (restaurant, park, museum directions)
- searchScore composite signal test combining tsvector + BM25 + trgm
- 52 tests total, all passing
…f truth

Issue 1: buildTableFilterProperties now uses the schema's filter input type
as the sole source of truth (via TypeRegistry), capturing plugin-injected
filter fields (bm25Body, tsvTsv, trgmName, vectorEmbedding, geom, etc.)
that are not present on the entity type. Same pattern as buildOrderByValues.
Adds collectFilterExtraInputTypes to discover custom filter types referenced
by plugin fields.

Issue 2: inferPrimaryKeyFromInputObject now returns all candidate key fields
instead of null when multiple exist, enabling composite PK detection for
junction tables like PostTag(postId, tagId). Prefers DeleteInput over
UpdateInput as signal source (fewer non-PK fields to filter).
…' into devin/1774088288-v5-relations-unified
…safety-composite-pk' into devin/1774088288-v5-relations-unified
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@devin-ai-integration devin-ai-integration bot changed the title feat: v5 relations — M:N ORM methods, orm-test package, filter type-safety + composite PK detection feat: v5 relations — M:N ORM methods, orm-test package, filter type-safety + composite PK support Mar 21, 2026
- Update getDeleteInputTypeName/getUpdateInputTypeName in utils.ts to derive
  from actual mutation name (table.query?.delete/update) when available
- Add inputTypeFromMutation() helper in infer-tables.ts for the 3 local sites
- Remove 5 ad-hoc ucFirst(mutationName) + 'Input' scattered across files
- One function controls each name, one place to make a mistake
@pyramation pyramation merged commit e4c8987 into main Mar 21, 2026
44 checks passed
@pyramation pyramation deleted the devin/1774088288-v5-relations-unified branch March 21, 2026 20:27
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

@@ -181,7 +189,6 @@ export function generateModelFile(
const orderByTypeName = getOrderByTypeName(table);
const createInputTypeName = `Create${typeName}Input`;
const updateInputTypeName = `Update${typeName}Input`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 updateInputTypeName is hardcoded, inconsistent with deleteInputTypeName fix

The PR correctly changed deleteInputTypeName to use getDeleteInputTypeName(table) (line 203), which derives the input type name from the actual mutation name to support composite PKs and custom mutation names. However, updateInputTypeName on line 191 is still hardcoded as `Update${typeName}Input` instead of using the analogous getUpdateInputTypeName(table). For tables with custom update mutation names (e.g., modifyOrganization), the generated code will reference UpdateOrganizationInput (which doesn't exist in the schema) instead of ModifyOrganizationInput. The snapshot at model-generator.test.ts.snap:373 confirms the bug: the Organization model's update method emits "UpdateOrganizationInput" even though the mutation is "modifyOrganization". The same issue affects composite PK tables where the mutation might be updatePostTagByPostIdAndTagId — the generated code would reference UpdatePostTagInput instead of UpdatePostTagByPostIdAndTagIdInput.

Suggested change
const updateInputTypeName = `Update${typeName}Input`;
const updateInputTypeName = getUpdateInputTypeName(table);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant