Skip to content

fix(bug-089): recursive nested POST/PUT + link-only childFk routing#6

Merged
mosoriob merged 47 commits into
mainfrom
feat/bug-089-recursive-nested-writes
May 10, 2026
Merged

fix(bug-089): recursive nested POST/PUT + link-only childFk routing#6
mosoriob merged 47 commits into
mainfrom
feat/bug-089-recursive-nested-writes

Conversation

@mosoriob
Copy link
Copy Markdown
Contributor

@mosoriob mosoriob commented May 9, 2026

Summary

  • Two-pass nested write pipeline (nested-tree.tsmutation-compiler.ts) for recursive POST/PUT on every resource.
  • Replace-subtree semantics on PUT.
  • Dynamic update_columns per nested row (bug-087 safe upsert).
  • bug-089 mixed inline+ref fix: link-only id-only childFk refs partitioned out of nested-array data and surfaced as top-level aliased link_N: update_modelcatalog_<table>(...) FK updates. Hasura runs top-level mutation fields sequentially so parent rows materialize before FK moves.
  • String-id array form rejected with HTTP 400 STRING_ID_DEPRECATED (breaking — see CHANGELOG v2.1.0).
  • Optional targetFkColumn override on RelationshipConfig (bug-087 PUT FK fold-in).

Spec / plan in parent repo mint:

  • docs/superpowers/specs/2026-05-09-recursive-nested-writes-bug-089-design.md
  • docs/superpowers/plans/2026-05-09-recursive-nested-writes-bug-089.md

Test plan

  • Unit: npm test → 115/115 pass (38 skipped — integration env-gated)
  • E2E (local Hasura): npm run test:e2e → 12/12 pass
    • mixed inline+ref POST flipped FAIL → PASS (was the bug-089 regression target)
    • 6 junction-e2e + 5 nested-write-e2e + 1 smoke
  • Caller migration audit (string-id array → object form) — tracked in parent repo
  • Stakeholder comms on v2.1.0 breaking change

mosoriob added 30 commits May 7, 2026 21:41
POST /modelconfigurations|/modelconfigurationsetups|/datasetspecifications
with relation fields (hasInput/hasOutput/hasModelCategory/hasPresentation)
returned 400 'null value in column "label"' when the client linked an
existing target by id alone.

buildJunctionInserts emitted update_columns:['label'] on the nested
target-entity insert. With nestedData = {id} only, on Hasura PK conflict
the existing target row got UPDATE label=NULL and tripped the not-null
constraint. Outer junction insert already used update_columns:[]; the
inner inconsistency was the bug.

Switch to update_columns:[] for link-or-noop semantics on existing target
rows. New target rows still insert with all supplied fields. Renames
remain the responsibility of the target's own PUT endpoint.

Tests: updated existing assertion in request.test.ts (Test 1) and added
two regressions covering hasInput as bare-string id and {id}-only object.
…wance

Also extends buildTree with optional maxDepth override so DEPTH_EXCEEDED
can be triggered in tests without exceeding the real resource graph depth.
Round-trips POST → GET through Fastify → Apollo → real local Hasura
to prove the e2e harness is wired end-to-end. Uses persons (flat
entity, no relationships) to avoid the junction mutation compiler
bug surfaced by junction-e2e Task 6.
bug-100: mutation compiler emitted junctionRelName ('grid') as the
top-level array-relationship key on the parent insert input. Hasura
metadata exposes the relationship under hasuraRelName ('grids'), so
POST /softwareversions with hasGrid returned 500 'field grid not found
in modelcatalog_software_version_insert_input'.

JunctionEdge now carries hasuraRelName; buildInsertObject keys the
parent slot by hasuraRelName. Inner junction-row key (object rel to
target) keeps junctionRelName — that one was already correct.

Mocked junction tests asserted the wrong outer key, so they passed
while real Hasura rejected. Updated mocked-test fixtures to add
hasuraRelName and assert the correct outer key. E2E regression to
follow.
Fastify rejects DELETE with content-type:application/json and empty
body (FST_ERR_CTP_EMPTY_JSON_BODY), causing every cleanup() call to
return 400 and leave orphan rows. DELETE has no body — drop the header.
bug-101: POST/PUT with hasGrid: [{ id: gridId }] (link to existing
grid, no fields) returned 500 'null value in column label violates
not-null constraint'. Compiler always wrapped target as nested INSERT
({ grid: { data: { id }, on_conflict } }), so Hasura tried to insert a
new grid row before on_conflict could fire — PG validates NOT NULL on
the column before resolving the unique constraint.

When a junction child has no columns and no nested writes, emit the
junction row as { [targetFkColumn]: child.id, ...junctionColumns }
directly. When it has columns or nested writes, keep the existing
nested-insert path for legitimate inline-NEW children.

Applies to both compilePost and compilePut (extracted shared
buildJunctionRow). Added link-only unit tests for both paths.
mosoriob added 17 commits May 9, 2026 18:59
bug-102: PUT /softwareversions with hasGrid returned 400 'null value
in column software_version_id violates not-null constraint' on
insert_modelcatalog_software_version_grid.

compilePut emits a separate insert into the junction table (not a
nested write through the parent), so Hasura cannot infer the parent
FK. Each junction row needs parentFkColumn = parent.id explicitly.

compilePost is unaffected: it uses Hasura's nested-object pattern
through the parent insert, so the FK is auto-derived.
bug-103: POST /softwares with hasVersion (childFk relationship)
returned 500 'cannot insert software_id columns as their values are
already being determined by parent insert' (validation-failed).

compilePost was injecting childFkColumn = parent.id on each nested
child row after buildInsertObject. Hasura's nested-object insert
already auto-derives the FK from parent context, so the explicit
assignment double-binds the column and Hasura rejects the mutation.

Updated mocked tests that asserted the old explicit-FK behavior.
…089 target)

Currently FAILS: ID-only ref in hasVersion is treated as nested insert,
hits "null value in column \"label\" violates not-null constraint".
Confirms bug-089 gap. Test stays as regression target until bug-089 lands.
Link-only id-only childFk refs (e.g. POST software with mixed inline-new and
existing-ref hasVersion entries) flowed into the Hasura nested-array data and
hit Postgres NOT-NULL violations on child columns (label) before ON CONFLICT
could resolve.

Partition childFk children into inline-new vs link-only. Inline-new continues
to ride the nested array (POST) / upsert array (PUT). Link-only surfaces as a
top-level aliased link_N: update_modelcatalog_<table>(where:{id:{_in:...}}, _set:{<fk>:<parent>})
after the root insert/update. Hasura runs top-level mutation fields sequentially
so the parent row is materialized before the FK move.

Applies recursively to nested children at every depth on both POST and PUT.

Closes the bug-089 mixed inline+ref e2e regression target.
Adds 3 e2e read-shape tests covering bug-090 class:
- GET /softwareversions/{id} surfaces hasConfiguration array (id+label).
- Empty hasConfiguration omitted per v1.8.0 contract.
- Embedded version inside GET /softwares/{id} stays shallow (no hasConfiguration), locking field-maps depth contract.
@mosoriob mosoriob merged commit 3258186 into main May 10, 2026
2 checks passed
mosoriob added a commit to mintproject/monorepo that referenced this pull request May 10, 2026
Pointer updated to 3258186 (PR mintproject/model-catalog-api#6 merged).
Includes recursive nested POST/PUT and link-only childFk routing fix.
mosoriob added a commit to mintproject/monorepo that referenced this pull request May 10, 2026
* fix: update

* chore: add dynamo-experiment-may submodule

* fix: bump model-catalog-api submodule for bug-087 junction on_conflict fix

Bumps model-catalog-api to fix/bug-087-junction-on-conflict-label-clobber
which switches buildJunctionInserts nested target on_conflict.update_columns
from ['label'] to [] so linking by id alone no longer null-clobbers
existing target rows.

Updates .wolf/buglog.json bug-087 with the corrected root cause and the
applied fix (initial flush-ordering hypothesis was wrong). Adds matching
cerebrum entries: corrected key learning and a Do-Not-Repeat rule on
nested on_conflict semantics.

* docs: add design spec for bug-089 recursive nested POST/PUT

Architecture for two-pass tree-builder + Hasura mutation compiler
covering all 46 resources, replace-subtree PUT semantics, dynamic
update_columns (bug-087 safe), and bug-087 PUT FK fold-in.

* docs: add implementation plan for bug-089 recursive nested POST/PUT

15 TDD tasks covering nested-tree builder, mutation compiler (POST+PUT),
service.ts wiring, registry override, integration tests, openapi v2.1.0
bump, caller migration audit, and bug-089 buglog entry.

* docs: add design spec for local Hasura e2e integration tests

* docs: add implementation plan for local Hasura e2e integration tests

* chore: bump model-catalog-api submodule for bug-089 nested writes

* docs: add design spec for modeler MODFLOW-2000 nested registration notebook

* docs: add implementation plan for modeler MODFLOW-2000 nested registration notebook

* docs: switch notebook plan test runner to uv run pytest

* chore: pin dynamo-experiment-may submodule for notebook scaffold + uv bootstrap

* docs: add resume marker to notebook plan reflecting Task 1 + uv bootstrap

* chore: pin dynamo-experiment-may submodule for notebook completion (Tasks 2-6)

* fix(bug-090): notebook hasConfiguration KeyError + e2e read-shape tests

- dynamo-experiment-may: reassign version=r.json() after second GET; add separate GET /modelconfigurations/{slug} for hasInput/hasOutput.
- model-catalog-api: 3 e2e tests asserting hasConfiguration shape on GET /softwareversions/{id} and shallow embedded version on GET /softwares/{id}.
- Log bug-090 with root cause (response.ts depth<2 + field-maps modelcatalog_software selecting only id+label+description on versions).

* chore: bump model-catalog-api submodule for bug-089 nested writes merge

Pointer updated to 3258186 (PR mintproject/model-catalog-api#6 merged).
Includes recursive nested POST/PUT and link-only childFk routing fix.
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