Documents
A document register on top of existing records: captured files with a category, linked to records, visible only to whoever may read those records.
What it shows
Section titled “What it shows”createDocumentsFeature(@cosmicdrift/kumiko-bundled-features/documents) withcategories,linkTargetsandaccessroles.- A
capturerule turns a file field into a document source without the entity knowing about the documents feature: every savedcontract.fileis filed as a document of categorycontract, titled from the row, and linked to the contract’s property. - Visibility follows the linked records: a document is visible to users who may read the linked records and the contract that owns the file. The
documents:query:for-recordquery returns the documents of one record; a property reader sees none for the property because they may not read the contract that owns the file.
Source
Section titled “Source”Feature entry point: src/feature.ts.
// kumiko-feature-version: 1// Documents Sample//// A `contract` entity with a file field becomes a document source without knowing about the// documents feature: the capture rule below files every saved contract file as a document of// category "contract" and links it to the contract and to its property. Who sees the document// follows from who may read the linked records and the contract that owns the file.
import { createDocumentsFeature } from "@cosmicdrift/kumiko-bundled-features/documents";import { createEntity, createFileField, createTextField, defineEntityCreateHandler, defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateHandler, defineFeature,} from "@cosmicdrift/kumiko-framework/engine";
const text = () => createTextField({ personal: false, reason: "is_business_data", required: true, maxLength: 120 });
export const propertyEntity = createEntity({ table: "read_sample_documents_properties", fields: { name: text() }, access: { read: { PropertyReader: "all", TenantAdmin: "all" } },});
export const contractEntity = createEntity({ table: "read_sample_documents_contracts", fields: { name: text(), propertyId: text(), file: createFileField() }, access: { read: { ContractReader: "all", TenantAdmin: "all" } },});
const access = { roles: ["PropertyReader", "ContractReader", "TenantAdmin"] };
export const propertyManagement = defineFeature("property-management", (r) => { r.requires({ features: ["files", "documents"] }); r.entity({ name: "property", ...propertyEntity }); r.entity({ name: "contract", ...contractEntity }); for (const [name, entity] of [ ["property", propertyEntity], ["contract", contractEntity], ] as const) { r.writeHandler(defineEntityCreateHandler(name, entity, { access })); r.writeHandler(defineEntityUpdateHandler(name, entity, { access: { roles: ["TenantAdmin"] } })); r.queryHandler(defineEntityListHandler(name, entity, { access })); r.queryHandler(defineEntityDetailHandler(name, entity, { access })); }});
export const documents = createDocumentsFeature({ categories: ["contract", "insurance"], linkTargets: ["property", "contract"], access, capture: [ { entity: "contract", field: "file", category: "contract", title: (row) => `Contract ${String(row["name"])}`, links: (row) => [{ entityType: "property", entityId: String(row["propertyId"]) }], }, ],});Needs a running Postgres and TEST_DATABASE_URL set (e.g. postgres://postgres:postgres@127.0.0.1:5432/postgres, see demo/.env.example).
cd samples/recipes/documentsbun testSource 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):
// kumiko-feature-version: 1// Documents Sample//// A `contract` entity with a file field becomes a document source without knowing about the// documents feature: the capture rule below files every saved contract file as a document of// category "contract" and links it to the contract and to its property. Who sees the document// follows from who may read the linked records and the contract that owns the file.
import { createDocumentsFeature } from "@cosmicdrift/kumiko-bundled-features/documents";import { createEntity, createFileField, createTextField, defineEntityCreateHandler, defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateHandler, defineFeature,} from "@cosmicdrift/kumiko-framework/engine";
const text = () => createTextField({ personal: false, reason: "is_business_data", required: true, maxLength: 120 });
export const propertyEntity = createEntity({ table: "read_sample_documents_properties", fields: { name: text() }, access: { read: { PropertyReader: "all", TenantAdmin: "all" } },});
export const contractEntity = createEntity({ table: "read_sample_documents_contracts", fields: { name: text(), propertyId: text(), file: createFileField() }, access: { read: { ContractReader: "all", TenantAdmin: "all" } },});
const access = { roles: ["PropertyReader", "ContractReader", "TenantAdmin"] };
export const propertyManagement = defineFeature("property-management", (r) => { r.requires({ features: ["files", "documents"] }); r.entity({ name: "property", ...propertyEntity }); r.entity({ name: "contract", ...contractEntity }); for (const [name, entity] of [ ["property", propertyEntity], ["contract", contractEntity], ] as const) { r.writeHandler(defineEntityCreateHandler(name, entity, { access })); r.writeHandler(defineEntityUpdateHandler(name, entity, { access: { roles: ["TenantAdmin"] } })); r.queryHandler(defineEntityListHandler(name, entity, { access })); r.queryHandler(defineEntityDetailHandler(name, entity, { access })); }});
export const documents = createDocumentsFeature({ categories: ["contract", "insurance"], linkTargets: ["property", "contract"], access, capture: [ { entity: "contract", field: "file", category: "contract", title: (row) => `Contract ${String(row["name"])}`, links: (row) => [{ entityType: "property", entityId: String(row["propertyId"]) }], }, ],});📄 On GitHub: samples/recipes/documents/src/feature.ts