Get Budget Lines
getBudgetLines lists the budgetLine collection, newest year first, with optional filters by organization and year. It's the read-side counterpart to addBudgetLine โ use it for any screen that shows the budget book: dashboards, finance reports, or the dropdown a purchase order picks its budget line from.
Imports
Throughout this tutorial the framework imports use the "lesan" alias, matching the app's deno.json. In your own project import from @hemedani/lesan (npm/Bun) or jsr:@hemedani/lesan (Deno) instead.
The validator (getBudgetLines.val.ts)โ
Both filters are optional strings. Note that year is declared as string() here even though the model stores it as a number โ the function converts it. The get projection is selectStruct("budgetLine", 2), one level deeper than addBudgetLine, so you can reach into the embedded organization relation's own fields.
import { object, optional, string } from "lesan";
import { selectStruct } from "../../../mod.ts";
import { activeRoleMixin } from "@lib";
export const getBudgetLinesValidator = () => {
return object({
set: object({
...activeRoleMixin,
organizationId: optional(string()),
year: optional(string()),
}),
get: selectStruct("budgetLine", 2),
});
};
The implementation (getBudgetLines.fn.ts)โ
The function builds a filters document and only adds a $match stage if something was actually passed:
organizationIdโfilters["organization._id"]โ the dot-path targets the embedded relation snapshot's_id, and it's cast tonew ObjectId(...)because the stored snapshot keeps it as an ObjectId.yearโfilters.yearโ cast withNumber(...), matching the model's numeric type.
The pipeline always sorts by year descending then code ascending, so the newest fiscal year (and within it, alphabetical codes) come first. The filtered results flow through the get projection via .toArray().
import { type ActFn, type Document, ObjectId } from "lesan";
import { budgetLine } from "../../../mod.ts";
export const getBudgetLinesFn: ActFn = async (body) => {
const {
set: { organizationId, year },
get,
} = body.details;
const filters: Document = {};
organizationId && (filters["organization._id"] = new ObjectId(organizationId as string));
year && (filters.year = Number(year));
return await budgetLine
.aggregation({
pipeline: [
...(Object.keys(filters).length > 0 ? [{ $match: filters }] : []),
{ $sort: { year: -1, code: 1 } },
] as Document[],
projection: get,
})
.toArray();
};
In the workflowโ
getBudgetLines is the listing page of the Finance chapter. It answers "what did we allocate and what's left?" across organizations and years โ and pairs naturally with getBudgetLineBreakdown, which drills into a single line's purchase orders. The budgetLine model shows the exact fields this projection can return, including the embedded organization snapshot.
Run itโ
With the server on http://localhost:1380 and a valid token:
curl -X POST http://localhost:1380/lesan \
-H "Content-Type: application/json" \
-H "token: <your-token>" \
-d '{
"model": "budgetLine",
"act": "getBudgetLines",
"details": {
"set": {
"activeRoleId": "ghost-role",
"year": "2024"
},
"get": {
"_id": 1,
"code": 1,
"title": 1,
"year": 1,
"remainingBudget": 1,
"organization": { "_id": 1, "name": 1 }
}
}
}'
Both filters are optional โ drop set to just the activeRoleId and you get every budget line, newest year first. The response is an array under "body".
Errors & fixesโ
getBudgetLines throws no custom errors. Two validator gotchas to watch:
yeararrives as a string ("2024", not2024) โ the validator enforcesstring(), and the function converts it to a number for the$match.organizationIdisn't pre-validated โ unlikeaddBudgetLine, here it's a plainoptional(string()). The cast tonew ObjectId(...)inside the function assumes a well-formed ID, so pass the real_idstring from theorganizationcollection to keep the$matchon"organization._id"working.