Embedded fields and lists
Keep owned, fixed-shape data with its parent entity instead of giving every
nested value its own aggregate. This recipe covers both a single embedded
object and an embedded list rendered by a declarative entityEdit screen.
What it shows
Section titled “What it shows”- Embedded objects —
addressand optionalbillingAddressare validated sub-schemas stored in the contact row. - Embedded lists — invoice
linesrepeat a fixed row shape without their own identity or event stream. - Structured cells — a line uses a reference, select, number, and money
cell, with
amountderived fromquantity × unitPriceand a totals row. - Server-authoritative derived cells — the write schema recomputes
derivedvalues before validation, so a client cannot supply a stale amount; the edit view model exposes the definitions asembeddedListDerived. - Declarative editing — the
invoice-editscreen usescreateEmbeddedListField; its renderer receivesembeddedListCells, so no custom screen component is required.
When to reach for it
Section titled “When to reach for it”Use an embedded field or list when the nested data is created, changed, and deleted with its parent. Promote a row to its own entity when it needs an independent status, history, or handler.
Source
Section titled “Source”The feature entry point is src/feature.ts. The embedded object and invoice
line-item schemas live under src/entities/; integration tests cover schema
validation, derived values, totals, references, and the declarative edit
screen.
Source code
Section titled “Source code”The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):
// Embedded Object Sample// Shows: Embedded objects stored as JSONB, searchable sub-fields,// field access on sub-fields, required vs optional embedded objects//// Pattern: Address belongs 1:1 to contact — never shared, always read together.// Use embedded when: data is owned by the parent entity, not referenced elsewhere.//// Tables:// contact — with embedded address (required) and billingAddress (optional)// address.street and address.city are searchable// billingAddress.vatId has restricted field access// product — minimal entity, only exists so `invoice.lines` has something// to reference// invoice — embedded list with select/reference/derived/totals metadata// on its `lines` sub-fields (Issue #1835), plus a declarative// entityEdit screen that renders the list end-to-end via// EmbeddedListField, no custom-screen code required
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";import { contactEntity } from "./entities/contact";import { invoiceEntity } from "./entities/invoice";import { productEntity } from "./entities/product";import { contactCreate } from "./handlers/contact-create.write";import { contactDetail } from "./handlers/contact-detail.query";
export { contactEntity } from "./entities/contact";export { invoiceEntity } from "./entities/invoice";export { productEntity } from "./entities/product";
const adminWrite = { access: { roles: ["Admin"] } } as const;const openRead = { access: { openToAll: true } } as const;
export const embeddedFeature = defineFeature("contacts", (r) => { r.entity("contact", contactEntity);
r.writeHandler(contactCreate); r.queryHandler(contactDetail);
r.crud("product", productEntity, { write: adminWrite, read: openRead }); r.crud("invoice", invoiceEntity, { write: adminWrite, read: openRead });
r.screen({ id: "invoice-edit", type: "entityEdit", entity: "invoice", layout: { sections: [{ columns: 1, fields: ["customer", "lines"] }], }, });});📄 On GitHub: samples/recipes/embedded/src/feature.ts