addProcessStep
addProcessStep adds one ordered step to a process. A step names an approval gate, gives it an order number (steps run in ascending order), and declares who must approve it via groupsOperator + assigneeGroups โ the AND/OR logic that evaluateStepStatus later interprets. Only Manager/Admin users can create steps. A process needs at least one step before activateProcess will accept it.
Package import
The tutorial source imports the framework as "lesan" โ in this repo that alias maps to the local framework source (deno.json โ ../../src/mod.ts). In your own app import from @hemedani/lesan (npm/Bun) or jsr:@hemedani/lesan (Deno). @lib and @model are the tutorial's aliases for utils/ and models/.
The validator (addProcessStep.val.ts)โ
The interesting fields are groupsOperator and assigneeGroups:
groupsOperator: "AND" | "OR"โ how the groups combine with each other.assigneeGroupsโ an array of groups, each with its ownoperator("AND" | "OR") and a list ofunitIds(the units that must vote).
Both use the shared group_operator_emums. stepType is restricted to the seven step types (Approval | Review | Notification | Action | Delivery | Receipt | Payment), order is a required number, and process is the parent process.
import { array, boolean, defaulted, number, object, objectIdValidation, optional, string } from "lesan";
import { selectStruct } from "../../../mod.ts";
import { activeRoleMixin } from "@lib";
import { group_operator_emums, step_type_emums } from "@model";
export const addProcessStepValidator = () => {
return object({
set: object({
...activeRoleMixin,
name: string(),
description: optional(string()),
stepType: optional(step_type_emums),
order: number(),
required: optional(boolean()),
groupsOperator: group_operator_emums,
assigneeGroups: array(
object({
operator: group_operator_emums,
unitIds: array(string()),
}),
),
process: objectIdValidation,
}),
get: selectStruct("processStep", 1),
});
};
The implementation (addProcessStep.fn.ts)โ
A tiny fn: it strips activeRoleId, builds a single process relation (with back-reference steps: true so the process embeds its steps), and inserts the step with projection: get. The order field is stored as-is; getProcessSteps and the submit flow rely on sorting by it.
import { type ActFn, type TInsertRelations, ObjectId } from "lesan";
import { processStep } from "../../../mod.ts";
import { stripActiveRole } from "@lib";
import type { processStep_relations } from "@model";
export const addProcessStepFn: ActFn = async (body) => {
const { set, get } = body.details;
const { process, ...rest } = stripActiveRole(set);
const relations: TInsertRelations<typeof processStep_relations> = {
process: {
_ids: new ObjectId(process as string),
relatedRelations: { steps: true },
},
};
return await processStep.insertOne({
doc: rest,
relations,
projection: get,
});
};
The registration in mod.ts uses validationRunType: "create" and a preAct chain of [setTokens, setUser, grantAccess([{ roles: ["Manager", "Admin"] }])].
In the workflowโ
The second step of the workflow chapter, right after addProcess. Design your approval flow here: step 1 might be Purchasing Manager Approval assigned to the Purchasing unit, step 2 Finance Review assigned to the Finance unit, and so on. When a PO is submitted, one stepApproval is created per unit of the first step, and each later step's approvals are created as the previous one is approved.
Links: overview, processStep model, process model, addProcess, activateProcess, step-evaluator.
Run itโ
curl -X POST http://localhost:1380/lesan \
-H "Content-Type: application/json" \
-H "token: $TOKEN" \
-d '{
"model": "processStep",
"act": "addProcessStep",
"details": {
"set": {
"activeRoleId": "ghost-role",
"name": "Purchasing Approval",
"description": "Approval by the purchasing unit",
"stepType": "Approval",
"order": 1,
"required": true,
"groupsOperator": "OR",
"assigneeGroups": [
{ "operator": "OR", "unitIds": ["<unitId>"] }
],
"process": "<processId>"
},
"get": {
"_id": 1,
"name": 1,
"order": 1,
"assigneeGroups": 1
}
}
}'
Add a second step with order: 2 (e.g. Finance Review) before activating, matching the two-step process the activateProcess example uses.
Errors & fixesโ
The fn throws nothing of its own. Failures come from validation and the auth chain:
| Error | Meaning | Fix |
|---|---|---|
order missing | order: number() is required | Provide an integer order |
assigneeGroups empty/invalid | Each group needs operator + unitIds (an array of strings) | Provide at least one group; unitIds entries must be strings |
process invalid | process: objectIdValidation failed | Pass a valid ObjectId for the process |
You cant do this | Active role isn't Manager/Admin | Use a Manager/Admin role |
Shared auth-chain errors (you should send your id with token key in req header, Invalid or expired token, activeRoleId is required, Active role not found) also apply โ see the addProcess table.