From 63af77eab3c1cf783139f864e1c139f41c8af6ad Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 29 Sep 2026 18:26:40 +0200 Subject: [PATCH] u --- docs/api/README.md | 4 + docs/api/openapi.json | 271 +++++++++++++++++ docs/api/routes/article/_sku/children.get.md | 42 +++ docs/api/routes/article/_sku/children.post.md | 44 +++ .../routes/article/_sku/children/_childSku.md | 35 +++ docs/api/routes/article/_sku/father.md | 43 +++ src/api/routes/article.js | 272 ++++++++++++++++++ src/db/article.js | 21 ++ 8 files changed, 732 insertions(+) create mode 100644 docs/api/routes/article/_sku/children.get.md create mode 100644 docs/api/routes/article/_sku/children.post.md create mode 100644 docs/api/routes/article/_sku/children/_childSku.md create mode 100644 docs/api/routes/article/_sku/father.md diff --git a/docs/api/README.md b/docs/api/README.md index aa940f4..f9a2b0a 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -41,7 +41,11 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`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` | | [`routes/article/set-categories.md`](routes/article/set-categories.md) | `PUT /article/{sku}/categories` | +| [`routes/article/_sku/children.get.md`](routes/article/_sku/children.get.md) | `GET /article/{sku}/children` | +| [`routes/article/_sku/children.post.md`](routes/article/_sku/children.post.md) | `POST /article/{sku}/children` | +| [`routes/article/_sku/children/_childSku.md`](routes/article/_sku/children/_childSku.md) | `DELETE /article/{sku}/children/{childSku}` | | [`routes/article/set-eans.md`](routes/article/set-eans.md) | `PUT /article/{sku}/eans` | +| [`routes/article/_sku/father.md`](routes/article/_sku/father.md) | `POST /article/{sku}/father` | | [`routes/article/history.md`](routes/article/history.md) | `GET /article/{sku}/history` | | [`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}` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 0ff970e..b0f6a27 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -12676,6 +12676,277 @@ } } }, + "/article/{sku}/father": { + "post": { + "summary": "Toggle father mode for an article", + "description": "Enables or disables father article status. Enabling requires specifying an attributeNameId; disabling requires the article to have zero children.", + "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": [ + "isFather" + ], + "properties": { + "isFather": { + "type": "boolean", + "description": "True to make article a father, false to revert to regular article" + }, + "attributeNameId": { + "type": "integer", + "description": "Required when isFather is true. The attributename id for variants." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated article", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "article": { + "$ref": "#/components/schemas/Article" + } + } + } + } + } + }, + "400": { + "description": "Invalid input or article still has children" + }, + "404": { + "description": "Article or attribute name not found" + } + } + } + }, + "/article/{sku}/children": { + "get": { + "summary": "List child articles of a father article", + "description": "Returns variants/children linked to this father article.", + "tags": [ + "Article" + ], + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Father article SKU" + } + ], + "responses": { + "200": { + "description": "List of children", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "fathersort": { + "type": "integer" + }, + "attributeValueId": { + "type": "integer" + }, + "attributeValue": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Article not found" + } + } + }, + "post": { + "summary": "Add a child article to a father article", + "description": "Automatically generates SKU as fatherSku-attributeValueId and assigns the variant attribute.", + "tags": [ + "Article" + ], + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Father article SKU" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attributeValueId": { + "type": "integer", + "description": "ID of existing attribute value for the father's attribute" + }, + "value": { + "type": "string", + "description": "String value to find or create under the father's attribute" + }, + "name": { + "type": "string", + "description": "Optional custom variant name" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created child article", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "article": { + "$ref": "#/components/schemas/Article" + } + } + } + } + } + }, + "400": { + "description": "Father not configured as father or missing value" + }, + "409": { + "description": "Child SKU already exists" + } + } + } + }, + "/article/{sku}/children/{childSku}": { + "delete": { + "summary": "Remove a child article from a father article", + "description": "Unlinks the child from the father (or deletes if deleteArticle query/body is true).", + "tags": [ + "Article" + ], + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Father article SKU" + }, + { + "in": "path", + "name": "childSku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Child article SKU" + }, + { + "in": "query", + "name": "deleteArticle", + "schema": { + "type": "boolean" + }, + "description": "If true, permanently deletes the child article instead of just unlinking" + } + ], + "responses": { + "200": { + "description": "Child removed", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "deleted": { + "type": "boolean" + } + } + } + } + } + } + } + } + }, "/auth/selected-company": { "put": { "tags": [ diff --git a/docs/api/routes/article/_sku/children.get.md b/docs/api/routes/article/_sku/children.get.md new file mode 100644 index 0000000..cfd4dc4 --- /dev/null +++ b/docs/api/routes/article/_sku/children.get.md @@ -0,0 +1,42 @@ +# `GET /article/{sku}/children` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List child articles of a father article + +Returns variants/children linked to this father article. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Father article SKU | + +## Response `200` + +List of children + +```json +{ + "children": [ + { + "id": 1, + "sku": "…", + "name": "…", + "fathersort": 1, + "attributeValueId": 1, + "attributeValue": "…" + } + ] +} +``` + +## 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/children.post.md b/docs/api/routes/article/_sku/children.post.md new file mode 100644 index 0000000..bb36431 --- /dev/null +++ b/docs/api/routes/article/_sku/children.post.md @@ -0,0 +1,44 @@ +# `POST /article/{sku}/children` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Add a child article to a father article + +Automatically generates SKU as fatherSku-attributeValueId and assigns the variant attribute. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Father article SKU | + +## Body (JSON) + +```json +{ + "attributeValueId": 1, + "value": "…", + "name": "…" +} +``` + +## Response `201` + +Created child article + +```json +{ + "article": {} +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Father not configured as father or missing value | +| 409 | Child SKU already exists | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/_sku/children/_childSku.md b/docs/api/routes/article/_sku/children/_childSku.md new file mode 100644 index 0000000..058435f --- /dev/null +++ b/docs/api/routes/article/_sku/children/_childSku.md @@ -0,0 +1,35 @@ +# `DELETE /article/{sku}/children/{childSku}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Remove a child article from a father article + +Unlinks the child from the father (or deletes if deleteArticle query/body is true). + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Father article SKU | +| `childSku` | yes | string | Child article SKU | + +## Query parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `deleteArticle` | no | boolean | If true, permanently deletes the child article instead of just unlinking | + +## Response `200` + +Child removed + +```json +{ + "success": true, + "deleted": true +} +``` + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/_sku/father.md b/docs/api/routes/article/_sku/father.md new file mode 100644 index 0000000..2ade8b4 --- /dev/null +++ b/docs/api/routes/article/_sku/father.md @@ -0,0 +1,43 @@ +# `POST /article/{sku}/father` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Toggle father mode for an article + +Enables or disables father article status. Enabling requires specifying an attributeNameId; disabling requires the article to have zero children. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Body (JSON) + +```json +{ + "isFather": true, + "attributeNameId": 1 +} +``` + +## Response `200` + +Updated article + +```json +{ + "article": {} +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid input or article still has children | +| 404 | Article or attribute name 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 5d63def..cbb4f1c 100644 --- a/src/api/routes/article.js +++ b/src/api/routes/article.js @@ -69,6 +69,15 @@ function articleErrorStatus(error) { if (error.message === 'Invalid weight') return 400; if (error.message === 'Invalid track_stock') return 400; if (error.message === 'Invalid categoryIds') return 400; + if (error.message === 'Attribute name not found') return 404; + if (error.message === 'attributeNameId is required to become a father article') return 400; + if (error.message === 'A child article cannot become a father') return 400; + if (error.message === 'Cannot disable father mode on an article that still has children') return 400; + if (error.message === 'Article is not a father article') return 400; + if (error.message === 'Attribute value is required for child article' || error.message === 'Attribute value is required') return 400; + if (error.message === 'Attribute value not found or does not match father attribute') return 400; + 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; return 500; } @@ -1659,4 +1668,267 @@ export function register(app, basePath = '') { res.status(500).json({ error: error.message }); } }); + + /** + * @openapi + * /article/{sku}/father: + * post: + * summary: Toggle father mode for an article + * description: Enables or disables father article status. Enabling requires specifying an attributeNameId; disabling requires the article to have zero children. + * 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: [isFather] + * properties: + * isFather: + * type: boolean + * description: True to make article a father, false to revert to regular article + * attributeNameId: + * type: integer + * description: Required when isFather is true. The attributename id for variants. + * responses: + * 200: + * description: Updated article + * content: + * application/json: + * schema: + * type: object + * properties: + * article: + * $ref: '#/components/schemas/Article' + * 400: + * description: Invalid input or article still has children + * 404: + * description: Article or attribute name not found + */ + app.post(`${basePath}/article/:sku/father`, async (req, res) => { + const parsed = parseSku(req.params.sku); + if (parsed.error) return res.status(400).json({ error: parsed.error }); + + const isFather = req.body?.isFather; + if (typeof isFather !== 'boolean') { + return res.status(400).json({ error: 'isFather boolean is required' }); + } + const attributeNameId = req.body?.attributeNameId ?? req.body?.fatherAttributeNameId; + + try { + const result = await db.toggleFather(req.user.id, parsed.sku, { + isFather, + attributeNameId, + }); + emitArticleSignal(req, parsed.sku); + emitArticlesSignal(req); + res.status(200).json(result); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('Toggle father failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /article/{sku}/children: + * get: + * summary: List child articles of a father article + * description: Returns variants/children linked to this father article. + * tags: + * - Article + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Father article SKU + * responses: + * 200: + * description: List of children + * content: + * application/json: + * schema: + * type: object + * properties: + * children: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * sku: + * type: string + * name: + * type: string + * fathersort: + * type: integer + * attributeValueId: + * type: integer + * attributeValue: + * type: string + * 404: + * description: Article not found + */ + app.get(`${basePath}/article/:sku/children`, async (req, res) => { + const parsed = parseSku(req.params.sku); + if (parsed.error) return res.status(400).json({ error: parsed.error }); + + try { + const children = await db.listChildren(req.user.id, parsed.sku); + res.status(200).json({ children }); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('List children failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /article/{sku}/children: + * post: + * summary: Add a child article to a father article + * description: Automatically generates SKU as fatherSku-attributeValueId and assigns the variant attribute. + * tags: + * - Article + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Father article SKU + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * attributeValueId: + * type: integer + * description: ID of existing attribute value for the father's attribute + * value: + * type: string + * description: String value to find or create under the father's attribute + * name: + * type: string + * description: Optional custom variant name + * responses: + * 201: + * description: Created child article + * content: + * application/json: + * schema: + * type: object + * properties: + * article: + * $ref: '#/components/schemas/Article' + * 400: + * description: Father not configured as father or missing value + * 409: + * description: Child SKU already exists + */ + app.post(`${basePath}/article/:sku/children`, async (req, res) => { + const parsed = parseSku(req.params.sku); + if (parsed.error) return res.status(400).json({ error: parsed.error }); + + try { + const result = await db.addChild(req.user.id, parsed.sku, req.body || {}); + emitArticleSignal(req, parsed.sku); + if (result?.article?.sku) { + emitArticleSignal(req, result.article.sku); + } + emitArticlesSignal(req); + res.status(201).json(result); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('Add child failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /article/{sku}/children/{childSku}: + * delete: + * summary: Remove a child article from a father article + * description: Unlinks the child from the father (or deletes if deleteArticle query/body is true). + * tags: + * - Article + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Father article SKU + * - in: path + * name: childSku + * required: true + * schema: + * type: string + * description: Child article SKU + * - in: query + * name: deleteArticle + * schema: + * type: boolean + * description: If true, permanently deletes the child article instead of just unlinking + * responses: + * 200: + * description: Child removed + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * deleted: + * type: boolean + */ + app.delete(`${basePath}/article/:sku/children/:childSku`, async (req, res) => { + const parsed = parseSku(req.params.sku); + if (parsed.error) return res.status(400).json({ error: parsed.error }); + + const parsedChild = parseSku(req.params.childSku); + if (parsedChild.error) return res.status(400).json({ error: 'childSku is required' }); + + const deleteArticle = req.query.deleteArticle === 'true' || req.body?.deleteArticle === true; + + try { + const result = await db.removeChild(req.user.id, parsed.sku, parsedChild.sku, { + deleteArticle, + }); + emitArticleSignal(req, parsed.sku); + emitArticleSignal(req, parsedChild.sku); + emitArticlesSignal(req); + res.status(200).json(result); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('Remove child failed:', error); + res.status(status).json({ error: error.message }); + } + }); } + diff --git a/src/db/article.js b/src/db/article.js index 32f669c..e89b5e1 100644 --- a/src/db/article.js +++ b/src/db/article.js @@ -79,3 +79,24 @@ export async function listExport(googleAuthId, categoryId) { const store = await getStore(); return store.article.listExport(googleAuthId, categoryId); } + +export async function toggleFather(googleAuthId, sku, options) { + const store = await getStore(); + return store.article.toggleFather(googleAuthId, sku, options); +} + +export async function addChild(googleAuthId, sku, options) { + const store = await getStore(); + return store.article.addChild(googleAuthId, sku, options); +} + +export async function removeChild(googleAuthId, sku, childSku, options) { + const store = await getStore(); + return store.article.removeChild(googleAuthId, sku, childSku, options); +} + +export async function listChildren(googleAuthId, sku) { + const store = await getStore(); + return store.article.listChildren(googleAuthId, sku); +} +