Skip to content

Basic entity

Wire one event-sourced aggregate end-to-end with the framework’s built-in CRUD helpers. The recipe ships a task entity with the standard verbs — create, update, delete, restore, list, detail — via registerEntityCrud, plus soft-delete enabled.

This is the smallest useful sample: it demonstrates the path from createEntity({ fields }) to a working API surface without writing any Zod schemas or handler bodies by hand.

  • createEntity with field factories — typed text, boolean, number fields with options like required, sortable, softDelete.
  • registerEntityCrud — entity + standard handlers in one call; skip verbs in verbs and register explicitly when access differs per verb.
  • Per-verb access rules — different roles can create vs. update vs. delete. Editor and User roles can write; only Admin can delete or restore.
  • Soft deletesoftDelete: true on the entity gives the executor an isDeleted column and the delete handler flips it instead of dropping the row. restore flips it back.
task-management → single feature, single `task` entity, six CRUD handlers

No bundled features required — this is the baseline before you add r.requires("tenant"), auth, or cross-feature extensions.

  1. Define fields with createEntity({ fields: { … } }).
  2. Call registerEntityCrud(r, "task", taskEntity, { write, read, verbs }).
  3. Client calls task-management:write:task:create → row + event appended.
  4. delete soft-flips isDeleted; restore flips back; list excludes deleted rows by default.

You’re starting a new feature with a single entity and want CRUD without inventing your own handlers. Replace any single line with an explicit r.writeHandler({ name, schema, handler }) when you outgrow the defaults — see custom-handlers for that path.

Terminal window
bun kumiko test integration samples/basic-entity

Integration tests under src/__tests__/ exercise list-with-sort, soft-delete + restore, and per-verb access boundaries (Editor vs Admin).


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):

// Basic Entity Sample
// Shows: how to wire one event-sourced aggregate end-to-end via r.crud.
// Custom logic or per-verb access? Skip a verb in `verbs` and register explicitly.
import {
createBooleanField,
createEntity,
createTextField,
defineEntityDeleteHandler,
defineEntityRestoreHandler,
defineFeature,
} from "@cosmicdrift/kumiko-framework/engine";
export const taskEntity = createEntity({
table: "read_sample_tasks",
fields: {
title: createTextField({ required: true }),
description: createTextField(),
// sortable: true so the integration test can exercise list-with-sort.
status: createTextField({ sortable: true }),
isArchived: createBooleanField({ default: false }),
},
softDelete: true,
});
const editorWrite = { access: { roles: ["Admin", "User"] } } as const;
const adminWrite = { access: { roles: ["Admin"] } } as const;
const openRead = { access: { openToAll: true } } as const;
export const taskFeature = defineFeature("tasks", (r) => {
r.crud("task", taskEntity, {
write: editorWrite,
read: openRead,
verbs: { delete: false, restore: false },
});
r.writeHandler(defineEntityDeleteHandler("task", taskEntity, adminWrite));
r.writeHandler(defineEntityRestoreHandler("task", taskEntity, adminWrite));
});

📄 On GitHub: samples/recipes/basic-entity/src/feature.ts