Files
summercms/.planning/phases/12.2-admin-form-fields-date-file-upload-relation-editing-with-def/12.2-03-SUMMARY.md

17 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_head_after, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals plan_head_before plan_head_after tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
12.2-admin-form-fields-date-file-upload-relation-editing-with-def 03 admin
cabana
relation-manager
hasMany
belongsToMany
pivot
deferred-binding
fileupload
openapi
phase provides
12.2-01 deferred_bindings store ops (DeferredBind/Unbind/Bindings/Forget/Slaves, DeferredEnvelope), MorphType, pact Relation{Before,After}{Create,Update,Delete} hooks, PurgeDeferred over Models()
phase provides
12.2-02 X-Session-Key, fileScope and the seven file handlers, commitDeferred in CRUDService.save, fileupload/datepicker compile
RelationContract.Kind / ForeignKey with RelationHasMany and RelationBelongsToMany (empty Kind is belongsToMany)
manage.form / view.form / top-level form and pivot.form compiled against the related or pivot model; $/ paths inside the plugin
view toolbarButtons create|update|delete|link|unlink as route capabilities
child create/show/update/delete and pivot show/update routes, parent-scoped through loadChild (404 on a miss)
hasMany link/unlink, pivot values on link through the pivot form whitelist
record id 0 relation routes held against X-Session-Key and committed by the record's create save
child file routes keyed by X-Child-Session-Key (ChildSessionKeyHeader), committed by the child save
RelationSchema kind, deferrable, manageForm, viewForm, pivotForm; FormField.deferrable; 17 relation message keys
boot check that a deferrable relation with create has its related model in some plugin's Models()
12.2-04 admin SPA (child
pivot and create-screen UI on these routes)
12.2-05 unit and security tests
tokens tasks commits
82200 3 3
0b4ef9c311 fe9e8baaf1
added patterns
A relation form is compiled as a CompiledController of its own (relationModelController around the related or pivot model), so Fill, Validate, file and date pipelines are reused unchanged
Every child, pivot and child-file query carries the parent predicate (FK, pivot EXISTS, or the session's bound slaves); a miss is recordNotFound
File handlers run on a fileRoute (fields, key header, scope), shared by record and relation child file routes
Link and unlink go through linkRelated/unlinkRelated, used by the routes and by the deferred commit
created modified
modules/cabana/relation_form.go
modules/cabana/relation_child.go
modules/cabana/relation_child_smoke_test.go
modules/cabana/example_relation_test.go
modules/cabana/relation.go
modules/cabana/deferred.go
modules/cabana/field_file.go
modules/cabana/http.go
modules/cabana/registry.go
modules/cabana/schema.go
modules/cabana/form_schema.go
modules/cabana/schema_types.go
modules/cabana/messages.go
modules/cabana/admin_openapi.go
modules/cabana/security_coverage_test.go
modules/cabana/openapi_conformance_test.go
modules/cabana/phase10_csrf_test.go
modules/cabana/phase10_coverage_test.go
modules/cabana/datepicker_smoke_test.go
modules/phrasebook/backend/lang/en/lang.yaml
modules/phrasebook/backend/lang/pl/lang.yaml
admin/openapi/admin.json
admin/src/api/schema.d.ts
admin/tests/fixtures/widgets.relation-schema.json
modules/cabana/README.md
docs/backend/relation-manager.md
12.2-03: a relation form is compiled into its own CompiledController (relationModelController), so the child save reuses projectOperation, fillAllowed, mergedRules, dateBoundDetails and commitDeferred
12.2-03: a hasMany child's ForeignKey is set from the scoped parent before Fill and Validate, so a model rule on the key passes; a body naming the key is dropped
12.2-03: pivot values are filled through the pivot form's writable fields only (the pivot model needs no Fillable), and only the rules of those fields are checked
12.2-03: unknown pivot keys are a 422 on PUT .../pivot/{child} as on link, not silently dropped
12.2-03: at commit a bind of a child the session created attaches directly (hasMany: key set if still NULL; belongsToMany: pivot row with RelationBeforeLink); a bind of an existing record re-runs the full link eligibility
12.2-03: unlink on an unsaved parent only cancels ids bound in the session; no stray unbind rows are written
12.2-03: child file routes on a saved child answer 403 without update (writes) or without update or a view form (reads); child 0 without create is 404, as a record's id 0
12.2-03: the Models() boot check matches the related model by Go type, or by morph type when a database is published
12.2-03: the pending-created exclusion matches pivot_data LIKE '{"created":true%', the envelope's encoded prefix, so a malformed pivot_data row cannot break candidate queries
relationParent: a saved record (id) or the unsaved one (DeferredKey + related morph); relationQuery and loadChild branch on it
Route capability: relationAllowed(cr predicate) writes 404 for an unknown relation and 403 for an undeclared capability before any SQL
SC-3
SC-4
id description requirement verification human_judgment
D1 A hasMany child created under gadget A carries A's id, lists under A only, and a body naming gadget_id cannot move it SC-3
kind ref status
integration modules/cabana/relation_child_smoke_test.go#TestRelationChildSmokeCreate pass
false
id description requirement verification human_judgment
D2 Show, update, delete and pivot routes answer 404 for a child of another parent and for a FormExtendQuery-hidden parent; a delete naming a foreign child deletes nothing; hasMany link/unlink/delete SC-3
kind ref status
integration modules/cabana/relation_child_smoke_test.go#TestRelationChildSmokeScope pass
false
id description requirement verification human_judgment
D3 Link with a pivot note stores it, RelationBeforeLink still stamps its hook column, pivot keys outside the form and multi-id pivot links are 422 SC-3
kind ref status
integration modules/cabana/relation_child_smoke_test.go#TestRelationChildSmokePivot pass
false
id description requirement verification human_judgment
D4 On id 0 a created part and a member linked with a pivot note are attached by the gadget's create save; an unlinked pending part is deleted; no binding is left SC-4
kind ref status
integration modules/cabana/relation_child_smoke_test.go#TestRelationChildSmokeDeferredCreate pass
false
id description requirement verification human_judgment
D5 A deferred link the saved gadget excludes answers 422 on the relation-manager field, nothing is created, the binding stays SC-4
kind ref status
integration modules/cabana/relation_child_smoke_test.go#TestRelationChildSmokeDeferredRollback pass
false
id description requirement verification human_judgment
D6 An image uploaded to child 0 with X-Child-Session-Key is attached to the part its create makes SC-4
kind ref status
integration modules/cabana/relation_child_smoke_test.go#TestRelationChildSmokeChildFile pass
false
id description requirement verification human_judgment
D7 Boot stops when a deferrable relation with create has no Models() entry for its related model SC-4
kind ref status
unit modules/cabana/relation_child_smoke_test.go#TestRelationChildSmokePurgeModels pass
false
id description verification human_judgment
D8 All 13 new routes inventoried, guarded, CSRF-walked, in admin.json and called once by the conformance suite; SPA types, fixture and dist in sync
kind ref status
integration modules/cabana#TestPhase09PermissionMatrix|TestPhase09ContractInventory|TestPhase10CSRF|TestPhase10Coverage|TestPhase10OpenAPIConformance pass
kind ref status
other scripts/check-admin-openapi.sh --check && scripts/check-admin-dist.sh && npm --prefix admin test pass
false
id description verification human_judgment
D9 Existing belongsToMany contracts unchanged: fonoteka.go builds, vets and passes its admin tests with no change
kind ref status
integration go -C ../fonoteka.go build ./... && go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run Admin pass
false
id description verification human_judgment
D10 Docs and README name only existing identifiers and commands
kind ref status
other go test ./cmd/summer -run TestDocsTree && go run ./cmd/summer docs:build --check pass
false
45min 2026-10-02 complete

Phase 12.2 Plan 03: Relation child CRUD, pivot forms and deferral Summary

hasMany relation contracts, WinterCMS manage/view/pivot forms and toolbar buttons, parent-scoped child and pivot routes, and relation work on a record being created held against X-Session-Key and committed in its create transaction, with child-form uploads keyed by X-Child-Session-Key

Performance

  • Duration: about 45 min
  • Started: 2026-10-02T16:23:05Z
  • Completed: 2026-10-02T17:08:20Z
  • Tasks: 3
  • Files modified: 26 (summercms.go only; fonoteka.go untouched)

Accomplishments

  • RelationContract gains Kind (empty is belongsToMany) and ForeignKey. A hasMany contract with pivot fields, a non-integer ForeignKey, or an unknown kind stops boot. Existing contracts compile and behave unchanged, and the fonoteka.go admin tests pass with no change.
  • manage.form, view.form (falling back to a top-level form) and pivot.form compile against the related or pivot model, with fail-loud errors that name the plugin, controller and file. $/<vendor>/<plugin>/... resolves only inside the calling plugin. relation, relation-manager, widget and partial fields, and a field named like the hasMany key, stop boot. pivot[x] names compile to x, and server-owned pivot columns are refused.
  • The view panel's toolbarButtons accept create|update|delete|link|unlink, and each button is the capability of its routes. create and update need a manage form, and unlink on a non-nullable hasMany stops boot.
  • New routes: child create, show, update and delete, pivot show and update, and the seven child file routes. Every child is found with one query that carries the parent predicate, so a foreign child or a hidden parent is 404. A delete is all or nothing.
  • hasMany link adopts rows whose key is NULL, and unlink clears the key. Rows that another session created and has not saved yet are never candidates. A link may carry pivot values for one id, filled through the pivot form whitelist before RelationBeforeLink stamps its hook columns.
  • On record id 0 (with X-Session-Key, a deferrable relation, create declared and the field in the create context), create, link, unlink, delete and pivot edits are held in deferred_bindings. The record's create save applies them in id order with the file bindings. An ineligible link answers 422 on the relation-manager field and keeps the bindings.
  • The relation schema carries kind, deferrable and the localized forms, and the relation-manager field carries deferrable. There are 17 new message keys in en and pl. Boot refuses a deferrable create relation whose related model no plugin lists in Models().

Task Commits

  1. Task 1: hasMany contracts, relation forms and child create - 48a5b80 (feat)
  2. Task 2: parent-scoped child show, update, delete and pivot routes - afb05b6 (feat)
  3. Task 3: deferral on unsaved records and child file routes - fe9e8ba (feat)

Files Created/Modified

  • modules/cabana/relation_form.go: compileRelationForms, compileRelationForm, relationModelController, pivot field normalisation, checkFormDates
  • modules/cabana/relation_child.go: relationParent, loadParent, relationQuery, loadChild, childFileScope, the RelationService child and pivot methods, and the handlers
  • modules/cabana/relation.go: contract kinds and validation, schema fields, hasMany queries, linkRelated/unlinkRelated/linkDeferred/unlinkDeferred, fillPivot, excludePendingCreated
  • modules/cabana/deferred.go: ChildSessionKeyHeader, childSessionKeyFrom, relation bindings in commitDeferred, applyRelationBinding
  • modules/cabana/field_file.go: the fileRoute abstraction, the child file handlers and childFiles
  • modules/cabana/registry.go: checkDeferredModels
  • modules/cabana/http.go: 13 routes, relationsFor (session key), the boot check call
  • modules/cabana/admin_openapi.go: 13 doc funcs, AdminRelationLinkRequest, and session-key headers on the relation routes
  • Tests: inventories, CSRF counts, conformance fixture (conformPart hasMany with protected attachMany images, members pivot form with hook stamp and ExcludedRelatedIDs, Models()), 7 smoke tests
  • Docs: docs/backend/relation-manager.md (kinds, forms, buttons, child routes, pivot forms, create screen, child files, messages), modules/cabana/README.md

Decisions Made

See key-decisions in the frontmatter.

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] The SPA's typed relation schema fixture failed vue-tsc

  • Found during: Task 1
  • Issue: admin/tests/fixtures/widgets.relation-schema.json is typed against the generated RelationSchema, which now requires kind, deferrable and the 17 new message keys.
  • Fix: Added those keys to the fixture. The SPA typecheck, the vitest suite and check-admin-dist.sh pass.
  • Commit: 48a5b80

2. [Rule 3 - Blocking] The datepicker smoke fixture copies the gadget form

  • Found during: Task 1
  • Issue: datepickerFields rebuilds the conform gadget's fields.yaml. With the new parts relation in config_relation.yaml, boot failed with "relation parts has no relation-manager field".
  • Fix: Added the parts manager to that fixture.
  • Commit: 48a5b80

3. [Rule 1 - Bug] Doc snippet placement

  • Found during: Task 1
  • Issue: docs/backend/admin-controllers.md embeds the whole example_controller_test.go, so a contract example there would have changed an unrelated page.
  • Fix: The hasMany example lives in its own example_relation_test.go, referenced with src=.
  • Commit: 48a5b80

4. [Fixture shape] The child file relation is attachMany, not attachOne

  • Found during: Task 3
  • Issue: The plan's fixture names an attachOne image. The conformance suite must call the reorder route with 200, and reorder is attachMany only.
  • Fix: conformPart declares a protected attachMany images with useCaption. All seven child file routes are exercised on a protected relation.
  • Commit: fe9e8ba

5. [Fixture shape] The members manager is visible on create

  • Found during: Task 3
  • Issue: The deferred link tests need the members manager in the create context, but the fixture had context: [update].
  • Fix: Dropped that context from the conform gadget form. The behaviour change for existing managers is documented.
  • Commit: fe9e8ba

Total deviations: 5 (2 blocking, 1 bug, 2 fixture shape) Impact on plan: None adds scope. The shipped API matches the plan's artifacts list.

Issues Encountered

  • The shell's rm is interactive, so one cleanup command waited for input. It was rerun as /usr/bin/rm -f. No effect on the code.
  • gofmt -l reports six files outside this plan (compass, party, tide, wristband). They were left untouched.

Known Stubs

None.

Threat Flags

None. Every new endpoint and trust boundary is in the plan's threat register (T-12.2-19 to T-12.2-28).

User Setup Required

None. A host application whose deferrable relation declares create must list the related model in a plugin's Models(), and boot names the relation when it does not.

Next Phase Readiness

  • Plan 04 (SPA) can read kind, deferrable, manageForm, viewForm and pivotForm from the relation schema and deferrable from the relation-manager field. It can use /records, /records/{child}, /delete and /pivot/{child}, send X-Session-Key with id 0, and send X-Child-Session-Key with the child modal's file calls and its create or update.
  • Plan 05 should cover:
    • another admin's pending children (id 0 with a foreign key is 404),
    • unlink and delete of pending belongsToMany links,
    • pivot edits on id 0,
    • commit of unbinds,
    • an attachOne child field,
    • the 413 on child bodies,
    • concurrency of two saves with one key across files and relations.

Self-Check: PASSED

  • Created files exist: relation_form.go, relation_child.go, relation_child_smoke_test.go, example_relation_test.go.
  • Commits 48a5b80, afb05b6 and fe9e8ba are on master in summercms.go. fonoteka.go has no changes.