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:
@@ -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
|
||||
|
||||
|
||||
@@ -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}` |
|
||||
|
||||
@@ -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": [
|
||||
|
||||
48
docs/api/routes/category/order.md
Normal file
48
docs/api/routes/category/order.md
Normal 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`._
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user