updateRelations (purchaseOrder)
updateRelations lets you replace one or more relations of a purchase order after it was created โ requester, organization, requestingUnit, product, process, budgetLine, tender, or the attachments array. Every relation is swapped with replace: true, so the old embedded snapshot is dropped and the new one takes its place, with back-references kept in sync. This is how you fix a wrong budgetLine before submit, or attach a tender to a PO. Only Manager/Admin, or UnitHead with the canRegisterPurchaseOrder feature, can call 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 (updateRelations.val.ts)โ
The _id plus every relinkable relation as optional ObjectIds; attachments is an optional array of ObjectIds. The get is a depth-2 projection (this act returns the refreshed PO at the end).
import { array, object, objectIdValidation, optional } from "lesan";
import { selectStruct } from "../../../mod.ts";
import { activeRoleMixin } from "@lib";
export const updateRelationsValidator = () => {
return object({
set: object({
...activeRoleMixin,
_id: objectIdValidation,
requester: optional(objectIdValidation),
organization: optional(objectIdValidation),
requestingUnit: optional(objectIdValidation),
product: optional(objectIdValidation),
process: optional(objectIdValidation),
attachments: optional(array(objectIdValidation)),
budgetLine: optional(objectIdValidation),
tender: optional(objectIdValidation),
}),
get: selectStruct("purchaseOrder", 2),
});
};
The implementation (updateRelations.fn.ts)โ
- Collects all single relations into a
singlemap and iterates them. Each present value becomes anaddRelationcall withreplace: trueand the appropriaterelatedRelationsback-reference (purchaseOrders: truefor the document relations,{}for none). - Handles
attachmentsseparately โ it's a multiple relation, so the fn passes an array of ObjectIds, again withreplace: true. - Returns the refreshed PO via
findOnewith the requestedgetprojection.
Because every call uses replace: true, this act replaces rather than appends: a PO that had tender A will now point to tender B, and the old tender's back-reference is dropped.
import { type ActFn, ObjectId } from "lesan";
import { purchaseOrder } from "../../../mod.ts";
export const updateRelationsFn: ActFn = async (body) => {
const {
set: { _id, requester, organization, requestingUnit, product, process, attachments, budgetLine, tender },
get,
} = body.details;
const modelId = new ObjectId(_id as string);
const single = {
requester,
organization,
requestingUnit,
product,
process,
budgetLine,
tender,
} as Record<string, unknown>;
for (const [rel, value] of Object.entries(single)) {
if (!value) continue;
const related = (() => {
switch (rel) {
case "requester":
return { purchaseOrders: true };
case "organization":
return { purchaseOrders: true };
case "requestingUnit":
return { purchaseOrders: true };
case "product":
return { purchaseOrders: true };
case "process":
return { purchaseOrders: true };
case "budgetLine":
return { purchaseOrders: true };
case "tender":
return { purchaseOrders: true };
default:
return {};
}
})();
await purchaseOrder.addRelation({
filters: { _id: modelId },
relations: {
[rel]: {
_ids: new ObjectId(value as string),
relatedRelations: related,
},
},
projection: get,
replace: true,
});
}
if (attachments) {
await purchaseOrder.addRelation({
filters: { _id: modelId },
relations: {
attachments: {
_ids: (attachments as string[]).map((id: string) => new ObjectId(id)),
relatedRelations: {},
},
},
projection: get,
replace: true,
});
}
return await purchaseOrder.findOne({
filters: { _id: modelId },
projection: get,
});
};
In the workflowโ
updateRelations is the corrective tool alongside add. Typical uses:
- Fix a wrong
budgetLinebefore submit encumbers it. - Attach a
tenderawarded during the finance chapter. - Replace the
processif the wrong workflow was resolved at creation.
Note that swapping a process on an order that's already flowing has no effect on existing stepApproval docs โ they point at their own processStep.
Links: overview, purchaseOrder model, po-add, po-submit.
Run itโ
curl -X POST http://localhost:1380/lesan \
-H "Content-Type: application/json" \
-H "token: $TOKEN" \
-d '{
"model": "purchaseOrder",
"act": "updateRelations",
"details": {
"set": {
"activeRoleId": "ghost-role",
"_id": "<poId>",
"budgetLine": "<budgetLineId>",
"tender": "<tenderId>"
},
"get": {
"_id": 1,
"status": 1,
"budgetLine": { "_id": 1, "code": 1 },
"tender": { "_id": 1, "status": 1 }
}
}
}'
Expect the PO back with the new budgetLine/tender snapshots in place.
Errors & fixesโ
The fn throws nothing of its own โ failures come from validation (an invalid ObjectId on any relation field) and the auth chain. Missing feature: canRegisterPurchaseOrder appears for a UnitHead without the feature. If a provided relation id doesn't exist, the insert of the relation may still succeed but the snapshot will be empty โ verify the referenced document exists first.