fix(bug-089): recursive nested POST/PUT + link-only childFk routing#6
Merged
Conversation
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.
…d dynamic update_columns
…link-only, FK error
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.
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
nested-tree.ts→mutation-compiler.ts) for recursive POST/PUT on every resource.update_columnsper nested row (bug-087 safe upsert).link_N: update_modelcatalog_<table>(...)FK updates. Hasura runs top-level mutation fields sequentially so parent rows materialize before FK moves.STRING_ID_DEPRECATED(breaking — see CHANGELOG v2.1.0).targetFkColumnoverride onRelationshipConfig(bug-087 PUT FK fold-in).Spec / plan in parent repo
mint:docs/superpowers/specs/2026-05-09-recursive-nested-writes-bug-089-design.mddocs/superpowers/plans/2026-05-09-recursive-nested-writes-bug-089.mdTest plan
npm test→ 115/115 pass (38 skipped — integration env-gated)npm run test:e2e→ 12/12 pass