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.
This commit is contained in:
seb
2026-08-02 19:42:54 +02:00
parent aed0857f23
commit a0f9a8a716
6 changed files with 264 additions and 0 deletions

View File

@@ -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

View File

@@ -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}` |

View File

@@ -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": [

View File

@@ -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`._

View File

@@ -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 });
}
});
}

View File

@@ -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 });
}