diff --git a/docs/api/README.md b/docs/api/README.md index da84259..5535159 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -115,6 +115,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/category/_id/attributevalue.put.md`](routes/category/_id/attributevalue.put.md) | `PUT /category/{id}/attributevalue` | | [`routes/category/delete-image.md`](routes/category/delete-image.md) | `DELETE /category/{id}/image` | | [`routes/category/add-image.md`](routes/category/add-image.md) | `POST /category/{id}/image` | +| [`routes/category/_id/image/generate.md`](routes/category/_id/image/generate.md) | `POST /category/{id}/image/generate` | | [`routes/category/_id/property.get.md`](routes/category/_id/property.get.md) | `GET /category/{id}/property` | | [`routes/category/_id/property.put.md`](routes/category/_id/property.put.md) | `PUT /category/{id}/property` | | [`routes/category/_id/property/_propertyId.md`](routes/category/_id/property/_propertyId.md) | `DELETE /category/{id}/property/{propertyId}` | @@ -315,7 +316,7 @@ Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). A | `attributename` | `POST /attributename`, `PUT /attributename/order`, `DELETE /attributename/{id}`, `PATCH /attributename/{id}`, `DELETE /attributevalue/{id}/image`, `POST /attributevalue/{id}/image` | | `brand` | `POST /brand`, `DELETE /brand/{id}`, `PATCH /brand/{id}`, `DELETE /brand/{id}/image`, `POST /brand/{id}/image` | | `carrier` | `POST /carrier`, `PUT /carrier/order`, `DELETE /carrier/{id}`, `PATCH /carrier/{id}` | -| `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image` | +| `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image`, `POST /category/{id}/image/generate` | | `delivery` | `DELETE /delivery/{id}/file/{fileId}`, `POST /delivery/{id}/pdf`, `POST /stock/accept` | | `labeltemplate` | `POST /labeltemplate`, `DELETE /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `PUT /labeltemplate/{id}` | | `ledger` | `DELETE /shipment/{id}`, `POST /stock/accept`, `POST /stock/adjust`, `POST /stock/ship` | diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index cc44c99..0c7b943 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -1,6 +1,6 @@ # Dash API Routes Overview (All) -Overview of all 269 API endpoints across all modules. +Overview of all 270 API endpoints across all modules. ### Notation @@ -329,6 +329,9 @@ _none_ #### Add category image — `POST /category/{id}/image` - Params: `id: int, image/avif` - Returns: `201 { category: { id: int, thumbW: int, thumbH: int, thumbHash: str } }` +#### Generate category mosaic image from contents — `POST /category/{id}/image/generate` +- Params: `id: int, body?: { size?: int }` +- Returns: `200 { category: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, size: int, grid: str, articleCount: int } }` #### List category properties — `GET /category/{id}/property` - Params: `id: int` - Returns: `200 { properties: { id: int, propertyId: int, name: str, exclusive: bool, value: str, sort: int, categoryId?: int }[] }` diff --git a/docs/api/routes.md b/docs/api/routes.md index 3a4fda6..824e02e 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -1,6 +1,6 @@ # Dash API Routes Overview -Overview of all 189 API endpoints across all modules. +Overview of all 190 API endpoints across all modules. ### Notation @@ -253,6 +253,9 @@ _none_ #### Delete category image — `DELETE /category/{id}/image` - Params: `id: int` - Returns: `200 { category: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null } }` +#### Generate category mosaic image from contents — `POST /category/{id}/image/generate` +- Params: `id: int, body?: { size?: int }` +- Returns: `200 { category: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, size: int, grid: str, articleCount: int } }` #### List category properties — `GET /category/{id}/property` - Params: `id: int` - Returns: `200 { properties: { id: int, propertyId: int, name: str, exclusive: bool, value: str, sort: int, categoryId?: int }[] }` diff --git a/docs/api/routes/category/_id/image/generate.md b/docs/api/routes/category/_id/image/generate.md new file mode 100644 index 0000000..8e832cc --- /dev/null +++ b/docs/api/routes/category/_id/image/generate.md @@ -0,0 +1,64 @@ +# `POST /category/{id}/image/generate` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Generate category mosaic image from contents + +Generates a 2x2, 3x3, or 4x4 mosaic image from the category's and its subcategories' +articles (grouped by subcategory) and sets it as the new main picture for the category. +If size is not specified, defaults to 3 (3x3), falling back to 2 (2x2) if fewer than 7 images are available. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Category id | + +## Body (JSON) — _optional_ + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `size` | no | integer | Mosaic grid size (2 for 2x2, 3 for 3x3, 4 for 4x4). Defaults to 3x3 (or 2x2 if < 7 images). | + +```json +{ + "size": 1 +} +``` + +## Signals + +On success, emits SSE signal: `category` (see [`GET /events`](../events.md)). + +## Response `200` + +Mosaic image generated and linked as category main picture + +```json +{ + "category": { + "id": 1, + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…", + "size": 1, + "grid": "…", + "articleCount": 1 + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid category ID or size — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Category not found or no article images 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 bbb9ba6..3b78bee 100644 --- a/src/api/routes/category.js +++ b/src/api/routes/category.js @@ -907,4 +907,122 @@ export function register(app, basePath = '') { }); // #endregion + + /** + * @openapi + * /category/{id}/image/generate: + * post: + * tags: [Category] + * summary: Generate category mosaic image from contents + * description: | + * Generates a 2x2, 3x3, or 4x4 mosaic image from the category's and its subcategories' + * articles (grouped by subcategory) and sets it as the new main picture for the category. + * If size is not specified, defaults to 3 (3x3), falling back to 2 (2x2) if fewer than 7 images are available. + * security: + * - cookieAuth: [] + * x-signals: [category] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Category id + * requestBody: + * required: false + * content: + * application/json: + * schema: + * type: object + * properties: + * size: + * type: integer + * enum: [2, 3, 4] + * description: Mosaic grid size (2 for 2x2, 3 for 3x3, 4 for 4x4). Defaults to 3x3 (or 2x2 if < 7 images). + * responses: + * 200: + * description: Mosaic image generated and linked as category main picture + * content: + * application/json: + * schema: + * type: object + * properties: + * category: + * type: object + * properties: + * id: + * type: integer + * fullHash: + * type: string + * thumbW: + * type: integer + * thumbH: + * type: integer + * thumbHash: + * type: string + * size: + * type: integer + * grid: + * type: string + * articleCount: + * type: integer + * 400: + * description: Invalid category ID or size + * 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: Category not found or no article images found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + // #region POST /category/:id/image/generate + + app.post( + `${basePath}/category/:id/image/generate`, + express.json(), + async (req, res) => { + const parsed = parsePositiveInt(req.params.id, 'id'); + if (parsed.error) { + return res.status(400).json({ error: parsed.error }); + } + + const rawSize = req.body?.size; + let size = undefined; + if (rawSize !== undefined && rawSize !== null && rawSize !== '') { + size = Number(rawSize); + if (![2, 3, 4].includes(size)) { + return res.status(400).json({ error: 'Invalid size, must be 2, 3, or 4' }); + } + } + + try { + const category = await db.generateImage(req.user.id, parsed.id, { size }); + emitCategorySignal(req); + res.status(200).json({ category }); + } catch (error) { + const status = categoryErrorStatus(error); + if (status === 500) console.error('Generate category image failed:', error); + res.status(status).json({ error: error.message }); + } + }, + ); + + // #endregion } + diff --git a/src/db/category.js b/src/db/category.js index bf20865..779501e 100644 --- a/src/db/category.js +++ b/src/db/category.js @@ -44,3 +44,9 @@ export async function deleteImage(googleAuthId, categoryId) { const store = await getStore(); return store.category.deleteImage(googleAuthId, categoryId); } + +export async function generateImage(googleAuthId, categoryId, options) { + const store = await getStore(); + return store.category.generateImage(googleAuthId, categoryId, options); +} +