Act Structure
ZiWound has 98 acts across 13 domains, and they all follow the same three-file structure. This consistency is what makes 98 acts maintainable โ if you've read one act folder, you can read them all. This page is the Rosetta Stone.
The three-file layoutโ
src/<domain>/<actName>/
โโโ mod.ts # registration: setAct({ schema, actName, fn, validator, preAct })
โโโ <actName>.fn.ts # implementation: the ActFn
โโโ <actName>.val.ts# validator: superstruct object({ set, get })
1. mod.ts โ registrationโ
// src/report/approve/approve.mod.ts
import { coreApp } from "../../../mod.ts";
import { approveFn } from "./approve.fn.ts";
import { approveValidator, approverRoles } from "./approve.val.ts";
import { setUser, grantAccess } from "@lib";
export const approve = () =>
coreApp.acts.setAct({
schema: "report",
actName: "approve",
fn: approveFn,
validator: approveValidator(),
preAct: [setUser, grantAccess(approverRoles)],
});
The domain's setup function (reportSetup()) calls this approve() among all its other acts.
2. *.fn.ts โ implementationโ
Every ActFn receives a body with body.details.set (validated input) and body.details.get (the projection), and returns the result via the model's ODM handle:
// src/report/approve/approve.fn.ts
export const approveFn: ActFn = async (body) => {
const { set } = body.details;
return await reports.findOneAndUpdate({
filter: { _id: new ObjectId(set._id) },
update: { $set: { report_status: "Approved" } },
projection: get,
});
};
3. *.val.ts โ validationโ
Validators are superstruct object({ set, get }). get is almost always built from coreApp.schemas.selectStruct("schema", depth) so the client controls the projection:
// src/report/approve/approve.val.ts
import { object, string, enums } from "lesan";
import { report_status_array } from "../../../models/report.ts";
export const approverRoles = ["Manager", "Editor"];
export const approveValidator = () =>
object({
set: object({ _id: string(), report_status: enums(report_status_array) }),
get: coreApp.schemas.selectStruct("report", { ... }),
});
Common act archetypes in ZiWoundโ
| Archetype | Pattern | Example |
|---|---|---|
| CRUD | insertOne / findOne / find / findOneAndUpdate / deleteOne | report.add, report.get, report.gets |
| Status flip | findOneAndUpdate with $set on an enum | approve, reject, inReview, publishBlogPost |
| Protected read | preAct: [setUser], read from context | user.getMe |
| Search | $text/regex filter + limit/skip | user.getUsers, blogPost.getBySlug |
| Geo | $geoWithin: $centerSphere | report.getRelatedByGeo |
| Upload | set.formData + disk write + file.insertOne | file.uploadFile |
| Relations | updateRelations moves relation ids | report.updateRelations, user.updateUserRelations |
| Count | countDocuments | report.count, user.countUsers |
| Stat | aggregate for dashboards | dashboardStatistic, report.statistics |
Why 98 acts stays manageableโ
- One act = one folder = one concern. No god-files.
- Validators live beside implementations โ the contract and the code can't drift.
preActarrays compose โ auth is[setUser, grantAccess(roles)], added as a one-liner, not sprinkled through the function body.selectStructbuildsgetfor free โ the act author never hand-writes a projection validator.
That's the complete ZiWound tour. From here, try reading a few real act folders in the repo side by side with this page โ the pattern will immediately feel familiar.