Skip to content

Commit d0949f7

Browse files
committed
api: add CategoriesController, wire category routes in Router
1 parent 0f1d9bd commit d0949f7

3 files changed

Lines changed: 123 additions & 2 deletions

File tree

src/api/CategoriesController.res

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Categories HTTP handlers — mirrors ItemsController pattern
2+
// Each handler takes ~svc explicitly — no global registry access
3+
// Returns Handler.t (curried) so Router can bind at startup
4+
5+
// ─── Param helpers ──────────────────────────────────────────────────────────────────────────────
6+
7+
let getIdParam = (params: Js.Dict.t<string>): result<int, AppError.t> =>
8+
params
9+
->Js.Dict.get("id")
10+
->Result.fromOption(AppError.BadRequest("Missing id"))
11+
->Result.flatMap(s =>
12+
Int.fromString(s)->Result.fromOption(AppError.BadRequest("id must be an integer"))
13+
)
14+
15+
// ─── Handlers ───────────────────────────────────────────────────────────────────────────────────
16+
17+
let list = (~svc: CategoryService.t): Handler.t =>
18+
async (_req, _params) =>
19+
svc.findAll()
20+
->Result.map(cats => cats->Array.map(Category.toJson)->Js.Json.array)
21+
->AppError.toResponse
22+
23+
let get = (~svc: CategoryService.t): Handler.t =>
24+
async (_req, params) =>
25+
getIdParam(params)
26+
->Result.flatMap(svc.findById)
27+
->Result.map(Category.toJson)
28+
->AppError.toResponse
29+
30+
let create = (~svc: CategoryService.t): Handler.t =>
31+
async (req, _params) => {
32+
let json = await req->Bun.json
33+
json
34+
->Schemas.parseCategoryBody
35+
->Result.flatMap(inputs =>
36+
switch inputs {
37+
| [single] => svc.insert(single)->Result.map(cat => [cat])
38+
| many => svc.insertMany(many)
39+
}
40+
)
41+
->Result.map(cats => cats->Array.map(Category.toJson)->Js.Json.array)
42+
->AppError.toResponse
43+
}
44+
45+
let replace = (~svc: CategoryService.t): Handler.t =>
46+
async (req, params) => {
47+
let json = await req->Bun.json
48+
getIdParam(params)
49+
->Result.flatMap(id =>
50+
json
51+
->Schemas.parseCategoryReplace
52+
->Result.map(input => ({id, name: input.name, description: input.description}: Schema.Categories.replaceRow))
53+
->Result.flatMap(svc.replace)
54+
)
55+
->Result.map(Category.toJson)
56+
->AppError.toResponse
57+
}
58+
59+
let replaceMany = (~svc: CategoryService.t): Handler.t =>
60+
async (req, _params) => {
61+
let json = await req->Bun.json
62+
json
63+
->Schemas.parseCategoryReplaceMany
64+
->Result.flatMap(svc.replaceMany)
65+
->Result.map(cats => cats->Array.map(Category.toJson)->Js.Json.array)
66+
->AppError.toResponse
67+
}
68+
69+
let patch = (~svc: CategoryService.t): Handler.t =>
70+
async (req, params) => {
71+
let json = await req->Bun.json
72+
getIdParam(params)
73+
->Result.flatMap(id =>
74+
json
75+
->Schemas.parseCategoryPatch
76+
->Result.flatMap(input => svc.patch(id, input))
77+
)
78+
->Result.map(Category.toJson)
79+
->AppError.toResponse
80+
}
81+
82+
let delete = (~svc: CategoryService.t): Handler.t =>
83+
async (_req, params) =>
84+
getIdParam(params)
85+
->Result.flatMap(svc.delete)
86+
->Result.map(_ => Js.Json.null)
87+
->AppError.toResponse
88+
89+
let deleteMany = (~svc: CategoryService.t): Handler.t =>
90+
async (req, _params) => {
91+
let json = await req->Bun.json
92+
json
93+
->Schemas.parseCategoryDeleteMany
94+
->Result.flatMap(svc.deleteMany)
95+
->Result.map(_ => Js.Json.null)
96+
->AppError.toResponse
97+
}

src/api/ItemsController.res

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ let replaceMany = (~svc: ItemService.t): Handler.t =>
8080
->AppError.toResponse
8181
}
8282

83+
let patch = (~svc: ItemService.t): Handler.t =>
84+
async (req, params) => {
85+
let json = await req->Bun.json
86+
getIdParam(params)
87+
->Result.flatMap(id =>
88+
json
89+
->Schemas.parsePatch
90+
->Result.flatMap(input => svc.patch(id, input))
91+
)
92+
->Result.map(Item.toJson)
93+
->AppError.toResponse
94+
}
95+
8396
let delete = (~svc: ItemService.t): Handler.t =>
8497
async (_req, params) =>
8598
getIdParam(params)

src/api/Router.res

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,30 @@ let extractParams = (pattern: string, path: string): option<Js.Dict.t<string>> =
2020
}
2121

2222
let make = (registry: ServiceRegistry.t): (Bun.request => promise<Bun.response>) => {
23-
let items = registry.items
24-
let cats = registry.categories
23+
let items = registry.items
24+
let cats = registry.categories
2525

2626
// Exact routes first, parameterised after — longest patterns before shorter ones
2727
let routes: array<(string, string, Handler.t)> = [
28+
// Items
2829
("GET", "/rest/items", ItemsController.list(~svc=items)),
2930
("POST", "/rest/items", ItemsController.create(~svc=items)),
3031
("PUT", "/rest/items", ItemsController.replaceMany(~svc=items)),
3132
("DELETE", "/rest/items", ItemsController.deleteMany(~svc=items)),
3233
("GET", "/rest/items/:id/categories", ItemsController.getCategories(~svc=items, ~catSvc=cats)),
3334
("GET", "/rest/items/:id", ItemsController.get(~svc=items)),
3435
("PUT", "/rest/items/:id", ItemsController.replace(~svc=items)),
36+
("PATCH", "/rest/items/:id", ItemsController.patch(~svc=items)),
3537
("DELETE", "/rest/items/:id", ItemsController.delete(~svc=items)),
38+
// Categories
39+
("GET", "/rest/categories", CategoriesController.list(~svc=cats)),
40+
("POST", "/rest/categories", CategoriesController.create(~svc=cats)),
41+
("PUT", "/rest/categories", CategoriesController.replaceMany(~svc=cats)),
42+
("DELETE", "/rest/categories", CategoriesController.deleteMany(~svc=cats)),
43+
("GET", "/rest/categories/:id", CategoriesController.get(~svc=cats)),
44+
("PUT", "/rest/categories/:id", CategoriesController.replace(~svc=cats)),
45+
("PATCH", "/rest/categories/:id", CategoriesController.patch(~svc=cats)),
46+
("DELETE", "/rest/categories/:id", CategoriesController.delete(~svc=cats)),
3647
]
3748

3849
async (req: Bun.request): promise<Bun.response> => {

0 commit comments

Comments
 (0)