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