Skip to content

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.

  • createDocumentsFeature (@cosmicdrift/kumiko-bundled-features/documents) with categories, linkTargets and access roles.
  • A capture rule turns a file field into a document source without the entity knowing about the documents feature: every saved contract.file is filed as a document of category contract, 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-record query 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.

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

Terminal window
cd samples/recipes/documents
bun test

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