From c8f6936aa6b7e355f8fd133c557fc09ab57e93d7 Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 3 Aug 2026 03:05:45 +0200 Subject: [PATCH] Extend PATCH /category/:id to also move (parentId) alongside rename. PATCH now accepts name (rename, re-slugify) and/or parentId (move under a new parent, cycle/self guarded), merging both results. Renames the PATCH docs file to category/update.md and regenerates API docs. --- README.md | 2 +- docs/api/README.md | 2 +- docs/api/openapi.json | 29 +++++--- .../routes/category/{rename.md => update.md} | 14 ++-- src/api/routes/category.js | 69 ++++++++++++++----- 5 files changed, 82 insertions(+), 34 deletions(-) rename docs/api/routes/category/{rename.md => update.md} (59%) diff --git a/README.md b/README.md index 967aad2..7a43fdf 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,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) | +| `PATCH` | `/api/category/:id` | `category.rename` und/oder `category.move` (Name+Slug und/oder Verschieben unter neuen Eltern) | | `PUT` | `/api/category/order` | `category.order` (Geschwister-Sortierung unter einem Elternteil) | | `POST` | `/api/category/:id/move` | `category.move` (Kategorie unter neues Elternteil verschieben) | diff --git a/docs/api/README.md b/docs/api/README.md index 8e182d1..cc58bb1 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -53,7 +53,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`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}` | +| [`routes/category/update.md`](routes/category/update.md) | `PATCH /category/{id}` | | [`routes/category/articles.md`](routes/category/articles.md) | `GET /category/{id}/articles` | | [`routes/category/move.md`](routes/category/move.md) | `POST /category/{id}/move` | | [`routes/company/list.md`](routes/company/list.md) | `GET /company` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 4ec0813..fef5e58 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -1140,14 +1140,14 @@ "tags": [ "Category" ], - "summary": "Rename category", - "description": "Renames a category the user is authorized for (updates name and re-slugifies in the company default language).", + "summary": "Update category (rename or move)", + "description": "Updates a category the user is authorized for.\n`name` renames the category (re-slugifies in the company default language).\n`parentId` moves it under a new parent (cycle/self → `Invalid parent`).\nEither `name` or `parentId` (or both) is required.\n", "security": [ { "cookieAuth": [] } ], - "x-markdown": "category/rename.md", + "x-markdown": "category/update.md", "parameters": [ { "in": "path", @@ -1165,16 +1165,17 @@ "application/json": { "schema": { "type": "object", - "required": [ - "name" - ], "properties": { "name": { "type": "string" + }, + "parentId": { + "type": "integer", + "description": "New parent category id" } }, "example": { - "name": "Renamed category" + "parentId": 3 } } } @@ -1182,7 +1183,7 @@ }, "responses": { "200": { - "description": "Renamed category", + "description": "Updated category (renamed and/or moved)", "content": { "application/json": { "schema": { @@ -1193,7 +1194,9 @@ "required": [ "id", "name", - "slug" + "slug", + "parentId", + "sort" ], "properties": { "id": { @@ -1204,6 +1207,12 @@ }, "slug": { "type": "string" + }, + "parentId": { + "type": "integer" + }, + "sort": { + "type": "integer" } } } @@ -1213,7 +1222,7 @@ } }, "400": { - "description": "Validation error (name required)", + "description": "Validation error (name or parentId required, or invalid parent)", "content": { "application/json": { "schema": { diff --git a/docs/api/routes/category/rename.md b/docs/api/routes/category/update.md similarity index 59% rename from docs/api/routes/category/rename.md rename to docs/api/routes/category/update.md index b922fff..29dd95a 100644 --- a/docs/api/routes/category/rename.md +++ b/docs/api/routes/category/update.md @@ -2,9 +2,13 @@ Protected. Requires a valid session cookie (`credentials: 'include'`). -Rename category +Update category (rename or move) + +Updates a category the user is authorized for. +`name` renames the category (re-slugifies in the company default language). +`parentId` moves it under a new parent (cycle/self → `Invalid parent`). +Either `name` or `parentId` (or both) is required. -Renames a category the user is authorized for (updates name and re-slugifies in the company default language). ## Path parameters @@ -16,13 +20,13 @@ Renames a category the user is authorized for (updates name and re-slugifies in ```json { - "name": "Renamed category" + "parentId": 3 } ``` ## Response `200` -Renamed category +Updated category (renamed and/or moved) ```json { @@ -34,7 +38,7 @@ Renamed category | Status | Description | |--------|-------------| -| 400 | Validation error (name required) — `{"error":"…"}` | +| 400 | Validation error (name or parentId required, or invalid parent) — `{"error":"…"}` | | 401 | Unauthorized — `{"error":"…"}` | | 403 | Not authorized — `{"error":"…"}` | | 404 | Category not found — `{"error":"…"}` | diff --git a/src/api/routes/category.js b/src/api/routes/category.js index dabffa9..6deaf5c 100644 --- a/src/api/routes/category.js +++ b/src/api/routes/category.js @@ -252,11 +252,15 @@ export function register(app, basePath = '') { * /category/{id}: * patch: * tags: [Category] - * summary: Rename category - * description: Renames a category the user is authorized for (updates name and re-slugifies in the company default language). + * summary: Update category (rename or move) + * description: | + * Updates a category the user is authorized for. + * `name` renames the category (re-slugifies in the company default language). + * `parentId` moves it under a new parent (cycle/self → `Invalid parent`). + * Either `name` or `parentId` (or both) is required. * security: * - cookieAuth: [] - * x-markdown: category/rename.md + * x-markdown: category/update.md * parameters: * - in: path * name: id @@ -270,15 +274,17 @@ export function register(app, basePath = '') { * application/json: * schema: * type: object - * required: [name] * properties: * name: * type: string + * parentId: + * type: integer + * description: New parent category id * example: - * name: Renamed category + * parentId: 3 * responses: * 200: - * description: Renamed category + * description: Updated category (renamed and/or moved) * content: * application/json: * schema: @@ -286,7 +292,7 @@ export function register(app, basePath = '') { * properties: * category: * type: object - * required: [id, name, slug] + * required: [id, name, slug, parentId, sort] * properties: * id: * type: integer @@ -294,8 +300,12 @@ export function register(app, basePath = '') { * type: string * slug: * type: string + * parentId: + * type: integer + * sort: + * type: integer * 400: - * description: Validation error (name required) + * description: Validation error (name or parentId required, or invalid parent) * content: * application/json: * schema: @@ -379,21 +389,46 @@ export function register(app, basePath = '') { return res.status(400).json({ error: parsed.error }); } - const name = typeof req.body?.name === 'string' ? req.body.name.trim() : ''; - if (!name) { - return res.status(400).json({ error: 'name is required' }); + const body = req.body ?? {}; + const hasName = Object.prototype.hasOwnProperty.call(body, 'name'); + const hasParentId = Object.prototype.hasOwnProperty.call(body, 'parentId'); + + if (!hasName && !hasParentId) { + return res.status(400).json({ error: 'name or parentId is required' }); } try { - const category = await db.rename({ - googleAuthId: req.user.id, - id: parsed.id, - name, - }); + let category; + if (hasParentId) { + const parentId = Number(body.parentId); + if (!Number.isInteger(parentId) || parentId <= 0) { + return res.status(400).json({ error: 'parentId is required' }); + } + category = await db.move({ googleAuthId: req.user.id, id: parsed.id, parentId }); + } + + if (hasName) { + const name = typeof body.name === 'string' ? body.name.trim() : ''; + if (!name) { + return res.status(400).json({ error: 'name is required' }); + } + if (category) { + // Merge: preserve the parentId/sort from the move, apply the new name/slug. + const renamed = await db.rename({ + googleAuthId: req.user.id, + id: parsed.id, + name, + }); + category = { ...category, ...renamed }; + } else { + category = await db.rename({ googleAuthId: req.user.id, id: parsed.id, name }); + } + } + res.status(200).json({ category }); } catch (error) { const status = categoryErrorStatus(error); - if (status === 500) console.error('Rename category failed:', error); + if (status === 500) console.error('Update category failed:', error); res.status(status).json({ error: error.message }); } });