From 1de802fbaacc43fcbc7a6f888fa269fe46bc8b4d Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 29 Sep 2026 20:10:46 +0200 Subject: [PATCH] u --- docs/api/README.md | 4 + docs/api/openapi.json | 313 ++++++++++++++++++ .../routes/article/_sku/articlelist.get.md | 42 +++ .../routes/article/_sku/articlelist.put.md | 51 +++ .../routes/article/_sku/optionslist.get.md | 43 +++ .../routes/article/_sku/optionslist.put.md | 52 +++ src/api/routes/article.js | 279 ++++++++++++++++ src/db/article.js | 21 ++ 8 files changed, 805 insertions(+) create mode 100644 docs/api/routes/article/_sku/articlelist.get.md create mode 100644 docs/api/routes/article/_sku/articlelist.put.md create mode 100644 docs/api/routes/article/_sku/optionslist.get.md create mode 100644 docs/api/routes/article/_sku/optionslist.put.md diff --git a/docs/api/README.md b/docs/api/README.md index f9a2b0a..c40eacb 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -37,6 +37,8 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/article/delete.md`](routes/article/delete.md) | `DELETE /article/{sku}` | | [`routes/article/get.md`](routes/article/get.md) | `GET /article/{sku}` | | [`routes/article/update.md`](routes/article/update.md) | `PATCH /article/{sku}` | +| [`routes/article/_sku/articlelist.get.md`](routes/article/_sku/articlelist.get.md) | `GET /article/{sku}/articlelist` | +| [`routes/article/_sku/articlelist.put.md`](routes/article/_sku/articlelist.put.md) | `PUT /article/{sku}/articlelist` | | [`routes/article/_sku/attributevalue.get.md`](routes/article/_sku/attributevalue.get.md) | `GET /article/{sku}/attributevalue` | | [`routes/article/_sku/attributevalue.put.md`](routes/article/_sku/attributevalue.put.md) | `PUT /article/{sku}/attributevalue` | | [`routes/article/set-brand.md`](routes/article/set-brand.md) | `PUT /article/{sku}/brand` | @@ -50,6 +52,8 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/article/add-image.md`](routes/article/add-image.md) | `POST /article/{sku}/image` | | [`routes/article/delete-image.md`](routes/article/delete-image.md) | `DELETE /article/{sku}/image/{fullHash}` | | [`routes/article/order-images.md`](routes/article/order-images.md) | `PUT /article/{sku}/images/order` | +| [`routes/article/_sku/optionslist.get.md`](routes/article/_sku/optionslist.get.md) | `GET /article/{sku}/optionslist` | +| [`routes/article/_sku/optionslist.put.md`](routes/article/_sku/optionslist.put.md) | `PUT /article/{sku}/optionslist` | | [`routes/article/set-prices.md`](routes/article/set-prices.md) | `PUT /article/{sku}/prices` | | [`routes/article/_sku/print.md`](routes/article/_sku/print.md) | `POST /article/{sku}/print` | | [`routes/article/_sku/property.get.md`](routes/article/_sku/property.get.md) | `GET /article/{sku}/property` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index b0f6a27..7326da5 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -12947,6 +12947,319 @@ } } }, + "/article/{sku}/articlelist": { + "get": { + "summary": "Get Stückliste (bundle items) for an article", + "description": "Returns the list of component items for a Stückliste article.", + "tags": [ + "Article" + ], + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "responses": { + "200": { + "description": "List of bundle items", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "articleId": { + "type": "integer" + }, + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "count": { + "type": "integer" + }, + "sort": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Article not found" + } + } + }, + "put": { + "summary": "Set Stückliste (bundle items) for an article", + "description": "Configures the article as a Stückliste covering a list of count * article. An article that is a father or has options cannot be a Stückliste. Pass an empty array to remove the Stückliste.", + "tags": [ + "Article" + ], + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "articleId": { + "type": "integer" + }, + "sku": { + "type": "string" + }, + "count": { + "type": "integer", + "default": 1 + }, + "sort": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated Stückliste items", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + }, + "400": { + "description": "Article is a father, has options, or contains itself" + }, + "404": { + "description": "Article or component article not found" + } + } + } + }, + "/article/{sku}/optionslist": { + "get": { + "summary": "Get options list for an article", + "description": "Returns the available options for an article.", + "tags": [ + "Article" + ], + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "responses": { + "200": { + "description": "List of options", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "articleId": { + "type": "integer" + }, + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "count": { + "type": "integer" + }, + "sort": { + "type": "integer" + }, + "mandated": { + "type": "boolean" + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Article not found" + } + } + }, + "put": { + "summary": "Set options list for an article", + "description": "Configures options for an article. Some options can be mandated (come with the article in any case). An article that is a father or a Stückliste cannot have options. Pass an empty array to remove options.", + "tags": [ + "Article" + ], + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "options" + ], + "properties": { + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "articleId": { + "type": "integer" + }, + "sku": { + "type": "string" + }, + "count": { + "type": "integer", + "default": 1 + }, + "sort": { + "type": "integer" + }, + "mandated": { + "type": "boolean", + "default": false + } + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated options list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "options": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + }, + "400": { + "description": "Article is a father, a Stückliste, or contains itself" + }, + "404": { + "description": "Article or option article not found" + } + } + } + }, "/auth/selected-company": { "put": { "tags": [ diff --git a/docs/api/routes/article/_sku/articlelist.get.md b/docs/api/routes/article/_sku/articlelist.get.md new file mode 100644 index 0000000..e63e69f --- /dev/null +++ b/docs/api/routes/article/_sku/articlelist.get.md @@ -0,0 +1,42 @@ +# `GET /article/{sku}/articlelist` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Get Stückliste (bundle items) for an article + +Returns the list of component items for a Stückliste article. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Response `200` + +List of bundle items + +```json +{ + "items": [ + { + "id": 1, + "articleId": 1, + "sku": "…", + "name": "…", + "count": 1, + "sort": 1 + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 404 | Article not found | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/_sku/articlelist.put.md b/docs/api/routes/article/_sku/articlelist.put.md new file mode 100644 index 0000000..57a49c5 --- /dev/null +++ b/docs/api/routes/article/_sku/articlelist.put.md @@ -0,0 +1,51 @@ +# `PUT /article/{sku}/articlelist` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Set Stückliste (bundle items) for an article + +Configures the article as a Stückliste covering a list of count * article. An article that is a father or has options cannot be a Stückliste. Pass an empty array to remove the Stückliste. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Body (JSON) + +```json +{ + "items": [ + { + "articleId": 1, + "sku": "…", + "count": 1, + "sort": 1 + } + ] +} +``` + +## Response `200` + +Updated Stückliste items + +```json +{ + "items": [ + {} + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Article is a father, has options, or contains itself | +| 404 | Article or component article not found | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/_sku/optionslist.get.md b/docs/api/routes/article/_sku/optionslist.get.md new file mode 100644 index 0000000..c12a4c3 --- /dev/null +++ b/docs/api/routes/article/_sku/optionslist.get.md @@ -0,0 +1,43 @@ +# `GET /article/{sku}/optionslist` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Get options list for an article + +Returns the available options for an article. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Response `200` + +List of options + +```json +{ + "options": [ + { + "id": 1, + "articleId": 1, + "sku": "…", + "name": "…", + "count": 1, + "sort": 1, + "mandated": true + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 404 | Article not found | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/_sku/optionslist.put.md b/docs/api/routes/article/_sku/optionslist.put.md new file mode 100644 index 0000000..b90a82b --- /dev/null +++ b/docs/api/routes/article/_sku/optionslist.put.md @@ -0,0 +1,52 @@ +# `PUT /article/{sku}/optionslist` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Set options list for an article + +Configures options for an article. Some options can be mandated (come with the article in any case). An article that is a father or a Stückliste cannot have options. Pass an empty array to remove options. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Body (JSON) + +```json +{ + "options": [ + { + "articleId": 1, + "sku": "…", + "count": 1, + "sort": 1, + "mandated": true + } + ] +} +``` + +## Response `200` + +Updated options list + +```json +{ + "options": [ + {} + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Article is a father, a Stückliste, or contains itself | +| 404 | Article or option article not found | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/src/api/routes/article.js b/src/api/routes/article.js index 5bd443a..a883634 100644 --- a/src/api/routes/article.js +++ b/src/api/routes/article.js @@ -80,6 +80,7 @@ function articleErrorStatus(error) { if (error.message === 'Child article does not belong to this father') return 400; if (error.message === 'Father article not found') return 404; if (error.message?.includes('already exists') || error.code === 'SQLITE_CONSTRAINT_UNIQUE') return 409; + if (error.message?.includes('Stückliste') || error.message?.includes('options') || error.message?.includes('Invalid items array') || error.message?.includes('Invalid options array')) return 400; return 500; } @@ -1931,5 +1932,283 @@ export function register(app, basePath = '') { res.status(status).json({ error: error.message }); } }); + + /** + * @openapi + * /article/{sku}/articlelist: + * get: + * summary: Get Stückliste (bundle items) for an article + * description: Returns the list of component items for a Stückliste article. + * tags: + * - Article + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * responses: + * 200: + * description: List of bundle items + * content: + * application/json: + * schema: + * type: object + * properties: + * items: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * articleId: + * type: integer + * sku: + * type: string + * name: + * type: string + * count: + * type: integer + * sort: + * type: integer + * 404: + * description: Article not found + */ + app.get(`${basePath}/article/:sku/articlelist`, async (req, res) => { + const parsed = parseSku(req.params.sku); + if (parsed.error) return res.status(400).json({ error: parsed.error }); + + try { + const items = await db.getArticlelist(req.user.id, parsed.sku); + res.status(200).json({ items }); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('Get articlelist failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /article/{sku}/articlelist: + * put: + * summary: Set Stückliste (bundle items) for an article + * description: Configures the article as a Stückliste covering a list of count * article. An article that is a father or has options cannot be a Stückliste. Pass an empty array to remove the Stückliste. + * tags: + * - Article + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [items] + * properties: + * items: + * type: array + * items: + * type: object + * properties: + * articleId: + * type: integer + * sku: + * type: string + * count: + * type: integer + * default: 1 + * sort: + * type: integer + * responses: + * 200: + * description: Updated Stückliste items + * content: + * application/json: + * schema: + * type: object + * properties: + * items: + * type: array + * items: + * type: object + * 400: + * description: Article is a father, has options, or contains itself + * 404: + * description: Article or component article not found + */ + app.put(`${basePath}/article/:sku/articlelist`, async (req, res) => { + const parsed = parseSku(req.params.sku); + if (parsed.error) return res.status(400).json({ error: parsed.error }); + + const items = req.body?.items; + if (!Array.isArray(items)) { + return res.status(400).json({ error: 'items array is required' }); + } + + try { + const updated = await db.setArticlelist(req.user.id, parsed.sku, items); + emitArticleSignal(req, parsed.sku); + emitArticlesSignal(req); + res.status(200).json({ items: updated }); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('Set articlelist failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /article/{sku}/optionslist: + * get: + * summary: Get options list for an article + * description: Returns the available options for an article. + * tags: + * - Article + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * responses: + * 200: + * description: List of options + * content: + * application/json: + * schema: + * type: object + * properties: + * options: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * articleId: + * type: integer + * sku: + * type: string + * name: + * type: string + * count: + * type: integer + * sort: + * type: integer + * mandated: + * type: boolean + * 404: + * description: Article not found + */ + app.get(`${basePath}/article/:sku/optionslist`, async (req, res) => { + const parsed = parseSku(req.params.sku); + if (parsed.error) return res.status(400).json({ error: parsed.error }); + + try { + const options = await db.getOptionslist(req.user.id, parsed.sku); + res.status(200).json({ options }); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('Get optionslist failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /article/{sku}/optionslist: + * put: + * summary: Set options list for an article + * description: Configures options for an article. Some options can be mandated (come with the article in any case). An article that is a father or a Stückliste cannot have options. Pass an empty array to remove options. + * tags: + * - Article + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [options] + * properties: + * options: + * type: array + * items: + * type: object + * properties: + * articleId: + * type: integer + * sku: + * type: string + * count: + * type: integer + * default: 1 + * sort: + * type: integer + * mandated: + * type: boolean + * default: false + * responses: + * 200: + * description: Updated options list + * content: + * application/json: + * schema: + * type: object + * properties: + * options: + * type: array + * items: + * type: object + * 400: + * description: Article is a father, a Stückliste, or contains itself + * 404: + * description: Article or option article not found + */ + app.put(`${basePath}/article/:sku/optionslist`, async (req, res) => { + const parsed = parseSku(req.params.sku); + if (parsed.error) return res.status(400).json({ error: parsed.error }); + + const options = req.body?.options; + if (!Array.isArray(options)) { + return res.status(400).json({ error: 'options array is required' }); + } + + try { + const updated = await db.setOptionslist(req.user.id, parsed.sku, options); + emitArticleSignal(req, parsed.sku); + emitArticlesSignal(req); + res.status(200).json({ options: updated }); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('Set optionslist failed:', error); + res.status(status).json({ error: error.message }); + } + }); } + diff --git a/src/db/article.js b/src/db/article.js index e89b5e1..9e41270 100644 --- a/src/db/article.js +++ b/src/db/article.js @@ -100,3 +100,24 @@ export async function listChildren(googleAuthId, sku) { return store.article.listChildren(googleAuthId, sku); } +export async function getArticlelist(googleAuthId, sku) { + const store = await getStore(); + return store.article.getArticlelist(googleAuthId, sku); +} + +export async function setArticlelist(googleAuthId, sku, items) { + const store = await getStore(); + return store.article.setArticlelist(googleAuthId, sku, items); +} + +export async function getOptionslist(googleAuthId, sku) { + const store = await getStore(); + return store.article.getOptionslist(googleAuthId, sku); +} + +export async function setOptionslist(googleAuthId, sku, options) { + const store = await getStore(); + return store.article.setOptionslist(googleAuthId, sku, options); +} + +