From a0f9a8a716dfa0eef15039109b5e777fe9938a28 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 2 Aug 2026 19:42:54 +0200 Subject: [PATCH] Add PUT /category/order to reorder sibling categories. Wires up existing category.order store method (validates categoryIds are exactly the current children of parentId, sets each child sort to its index in one transaction) behind an authenticated HTTP route, plus @openapi docs and regenerated API docs. --- README.md | 1 + docs/api/README.md | 1 + docs/api/openapi.json | 110 ++++++++++++++++++++++++++++++ docs/api/routes/category/order.md | 48 +++++++++++++ src/api/routes/category.js | 99 +++++++++++++++++++++++++++ src/db/category.js | 5 ++ 6 files changed, 264 insertions(+) create mode 100644 docs/api/routes/category/order.md diff --git a/README.md b/README.md index 6723492..d071dde 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ That writes `docs/api/openapi.json` and per-route Markdown under `docs/api/route | Methode | Route | Store-Methode | | --- | --- | --- | | `PATCH` | `/api/category/:id` | `category.rename` (Name + Slug in Firmensprache) | +| `PUT` | `/api/category/order` | `category.order` (Geschwister-Sortierung unter einem Elternteil) | ## Users diff --git a/docs/api/README.md b/docs/api/README.md index 9f3778b..41d5567 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -50,6 +50,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/brand/upsert.md`](routes/brand/upsert.md) | `PUT /brand` | | [`routes/brand/delete.md`](routes/brand/delete.md) | `DELETE /brand/{id}` | | [`routes/category/create.md`](routes/category/create.md) | `POST /category` | +| [`routes/category/order.md`](routes/category/order.md) | `PUT /category/order` | | [`routes/category/tree.md`](routes/category/tree.md) | `GET /category/tree` | | [`routes/category/delete.md`](routes/category/delete.md) | `DELETE /category/{id}` | | [`routes/category/rename.md`](routes/category/rename.md) | `PATCH /category/{id}` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 0504022..5ac14f5 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -1314,6 +1314,116 @@ } } }, + "/category/order": { + "put": { + "tags": [ + "Category" + ], + "summary": "Reorder sibling categories", + "description": "Sets the sibling sort order of `categoryIds` under `parentId`.\n`categoryIds` must be exactly the current child set of `parentId`\n(same count, no foreign/duplicate ids). Each child's `sort` is set\nto its index `0..n-1` in one transaction.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "category/order.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "parentId", + "categoryIds" + ], + "properties": { + "parentId": { + "type": "integer", + "description": "Parent category under which sibling order applies" + }, + "categoryIds": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "example": { + "parentId": 1, + "categoryIds": [ + 2, + 4, + 3, + 5 + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "New sibling order", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "categoryIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid category order", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Parent category not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/brand": { "get": { "tags": [ diff --git a/docs/api/routes/category/order.md b/docs/api/routes/category/order.md new file mode 100644 index 0000000..7d19f7b --- /dev/null +++ b/docs/api/routes/category/order.md @@ -0,0 +1,48 @@ +# `PUT /category/order` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Reorder sibling categories + +Sets the sibling sort order of `categoryIds` under `parentId`. +`categoryIds` must be exactly the current child set of `parentId` +(same count, no foreign/duplicate ids). Each child's `sort` is set +to its index `0..n-1` in one transaction. + + +## Body (JSON) + +```json +{ + "parentId": 1, + "categoryIds": [ + 2, + 4, + 3, + 5 + ] +} +``` + +## Response `200` + +New sibling order + +```json +{ + "categoryIds": [] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid category order — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Parent category not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/src/api/routes/category.js b/src/api/routes/category.js index d4ba549..32d4d10 100644 --- a/src/api/routes/category.js +++ b/src/api/routes/category.js @@ -15,6 +15,7 @@ function categoryErrorStatus(error) { if (error.message === 'Cannot delete root category') return 400; if (error.message === 'Name required') return 400; if (error.message === 'Category not found') return 404; + if (error.message === 'Invalid category order') return 400; return 500; } @@ -395,4 +396,102 @@ export function register(app, basePath = '') { res.status(status).json({ error: error.message }); } }); + + /** + * @openapi + * /category/order: + * put: + * tags: [Category] + * summary: Reorder sibling categories + * description: | + * Sets the sibling sort order of `categoryIds` under `parentId`. + * `categoryIds` must be exactly the current child set of `parentId` + * (same count, no foreign/duplicate ids). Each child's `sort` is set + * to its index `0..n-1` in one transaction. + * security: + * - cookieAuth: [] + * x-markdown: category/order.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [parentId, categoryIds] + * properties: + * parentId: + * type: integer + * description: Parent category under which sibling order applies + * categoryIds: + * type: array + * items: + * type: integer + * example: + * parentId: 1 + * categoryIds: [2, 4, 3, 5] + * responses: + * 200: + * description: New sibling order + * content: + * application/json: + * schema: + * type: object + * properties: + * categoryIds: + * type: array + * items: + * type: integer + * 400: + * description: Invalid category order + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Parent category not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.put(`${basePath}/category/order`, async (req, res) => { + const parentId = Number(req.body?.parentId); + if (!Number.isInteger(parentId) || parentId <= 0) { + return res.status(400).json({ error: 'parentId is required' }); + } + + const rawIds = req.body?.categoryIds; + if (!Array.isArray(rawIds)) { + return res.status(400).json({ error: 'categoryIds is required' }); + } + const categoryIds = []; + for (const value of rawIds) { + const id = Number(value); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'categoryIds must be positive integers' }); + } + categoryIds.push(id); + } + + try { + const result = await db.order({ googleAuthId: req.user.id, parentId, categoryIds }); + res.status(200).json(result); + } catch (error) { + const status = categoryErrorStatus(error); + if (status === 500) console.error('Order categories failed:', error); + res.status(status).json({ error: error.message }); + } + }); } diff --git a/src/db/category.js b/src/db/category.js index 6e9b9f4..ded8692 100644 --- a/src/db/category.js +++ b/src/db/category.js @@ -24,3 +24,8 @@ export async function rename({ googleAuthId, id, name }) { const store = await getStore(); return store.category.rename({ googleAuthId, id, name }); } + +export async function order({ googleAuthId, parentId, categoryIds }) { + const store = await getStore(); + return store.category.order({ googleAuthId, parentId, categoryIds }); +}