Skip to content

Nested writes

POST and PATCH /api/v1/records/{entity} accept a nested structure in one request: the record's own fields, its translatable _lng companion, and inline child-collection arrays — recursively, in a single transaction. The engine recognises child tables from metadata and wires the parent foreign key automatically, so you create or edit a whole tree with one call.

This is the same generic engine that backs every entity; there is no per-entity code.

The shape of a payload

A write body is the record's own fields, plus two optional kinds of nested data:

  • <entity>_lng — the translatable companion (see Concepts → Translations), keyed by language code.
  • One array per child table that references this entity — keyed by the child table name (e.g. syntec_group_role under syntec_group). The engine discovers these from the foreign keys in the metadata; any table with a reference back to the parent is eligible.

Anything else in the body is treated as one of the record's own fields (unknown keys are ignored).

Create (POST)

POST /api/v1/records/syntec_group
{
  "cid": "TST_GRP_1",
  "syntec_package_id": "019ebf06-f968-7a4b-bc8b-39c210b8be46",
  "syntec_group_lng": { "EN": { "name": "Test group 1" }, "NL": { "name": "Test groep 1" } },
  "syntec_group_role": [
    { "syntec_role_id": "019ec2fb-2d26-72d9-b350-17b0878277e8" },
    { "syntec_role_id": "019ec2fb-2d26-7e19-b350-17b087a555fa" }
  ]
}

Creates the syntec_group, its _lng rows (one per system language), and the two syntec_group_role rows. You do not send the child's parent FK (syntec_group_id) — the engine injects the new group's guid into each child, overriding anything you put there. References are always guids, never raw ids.

Response: 201 { "guid": "<group-guid>" }. Children are not echoed back — re-fetch the record if you need their guids.

Update (PATCH) — sync semantics

On PATCH, including a child-collection key syncs that table: the array is the desired set.

PATCH /api/v1/records/syntec_group/{guid}
{
  "syntec_group_role": [
    { "guid": "019ec3a1-…", "provenance": "package" },   // has guid  -> update this row
    { "syntec_role_id": "019ec2fb-…" }                    // no guid   -> create a new row
  ]
  // any existing role NOT listed here -> set inactive
}
Row in the array Result
has a guid that child is updated (must be an active child of this parent)
no guid a new child is created (parent FK wired)
an existing active child omitted from the array set inactive (status = 2) — not deleted

If the child key is absent from the payload, that child table is left completely untouched (exactly like _lng). So PATCH with only {"name": "…"} changes the parent and nothing else.

The _lng companion follows the usual PATCH rule: only the languages you send are updated; omit it to leave translations alone.

Recursion

Nesting is uniform at every level: a child can carry its own _lng and its own child collections, and they are written depth-first in the same transaction. For example, create a view with its columns and each column's label in one call:

POST /api/v1/records/syntec_view
{
  "cid": "demo_list",
  "syntec_view_column": [
    { "col_key": "name", "syntec_view_column_lng": { "EN": { "label": "Name" } } }
  ]
}

The nesting depth is capped (10 levels) as a safety limit; deeper payloads are rejected with 422.

Guarantees

  • Atomic. The whole tree is written in one transaction. If anything fails — a validation error, a permission denial, a bad child reference — nothing is persisted, parent included.
  • Validated, permission-checked, and audited per node. Every record in the tree goes through the same engine path as a flat write: field/row-level permissions, field validation, guid-reference resolution, and the audit trail all apply to each child independently.
  • No reparenting. Because the engine forces the parent FK and rejects child guids that don't belong to this parent, a client cannot move or hijack another record's children through a nested write.

Rules & caveats

  • The _lng companion is mandatory on create. If an entity has a <entity>_lng table, creating a record always writes companion rows and validates the required translatable columns — even if you omit the _lng key. On update it is optional (only touched when you send it).
  • A bad child guid fails the whole write. A guid in a child array that is not an active child of the record you're writing → 422, full rollback. (This includes a guid from a different parent, or an already-inactive child.)
  • Omitted children go inactive, not deleted. They are set to status = 2; they are not archived or removed. A later sync that lists them again re-creates an active row.
  • Scoped principals sync only their slice. For a row-scoped (e.g. portal) principal, the sync only considers and affects children visible within its row scope; children outside its scope are neither deactivated nor referenceable.
  • Response is the parent guid only (201 { "guid" } / 204). Re-fetch to read back the tree.

Where it lives

RecordApi::create/update delegate to NestedRecordWriter (app/src/Api), which walks the tree and composes the generic EntityRepository and CompanionLanguageWriter — so nested writes inherit everything those already enforce. ChildRelationResolver provides the "which tables are children of this entity" lookup from metadata. The interactive schema for the POST/PATCH request bodies is on the API reference page (the live Swagger UI is also at /api/docs).