Skip to main content

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โ€‹

ArchetypePatternExample
CRUDinsertOne / findOne / find / findOneAndUpdate / deleteOnereport.add, report.get, report.gets
Status flipfindOneAndUpdate with $set on an enumapprove, reject, inReview, publishBlogPost
Protected readpreAct: [setUser], read from contextuser.getMe
Search$text/regex filter + limit/skipuser.getUsers, blogPost.getBySlug
Geo$geoWithin: $centerSpherereport.getRelatedByGeo
Uploadset.formData + disk write + file.insertOnefile.uploadFile
RelationsupdateRelations moves relation idsreport.updateRelations, user.updateUserRelations
CountcountDocumentsreport.count, user.countUsers
Stataggregate for dashboardsdashboardStatistic, 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.
  • preAct arrays compose โ€” auth is [setUser, grantAccess(roles)], added as a one-liner, not sprinkled through the function body.
  • selectStruct builds get for 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.