From e8d8846de7bd05618ebc2159ea32fda2fa7f7a8f Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 1 Aug 2026 03:39:10 +0200 Subject: [PATCH] u --- docs/api/README.md | 1 + docs/api/openapi.json | 69 +++++++++++++++++++++++++++++++ docs/api/routes/article/delete.md | 35 ++++++++++++++++ src/api/routes/article.js | 61 +++++++++++++++++++++++++++ src/db/article.js | 5 +++ 5 files changed, 171 insertions(+) create mode 100644 docs/api/routes/article/delete.md diff --git a/docs/api/README.md b/docs/api/README.md index bf35e4e..8b10225 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -31,6 +31,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | File | Endpoint | |------|----------| | [`routes/article/create.md`](routes/article/create.md) | `POST /article` | +| [`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/add-image.md`](routes/article/add-image.md) | `POST /article/{sku}/image` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 5b6b00a..683e53d 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -1206,6 +1206,75 @@ } } } + }, + "delete": { + "tags": [ + "Article" + ], + "summary": "Delete article by SKU", + "description": "Deletes the article when it has no child articles. Cascades related rows;\nnulls `manifestarticle.article_id` and appends a delete comment.\nAuth like GET by SKU.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "article/delete.md", + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "Article has children", + "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": "Article not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } } }, "/article/{sku}/image": { diff --git a/docs/api/routes/article/delete.md b/docs/api/routes/article/delete.md new file mode 100644 index 0000000..352fc0d --- /dev/null +++ b/docs/api/routes/article/delete.md @@ -0,0 +1,35 @@ +# `DELETE /article/{sku}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete article by SKU + +Deletes the article when it has no child articles. Cascades related rows; +nulls `manifestarticle.article_id` and appends a delete comment. +Auth like GET by SKU. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Response `204` + +Deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Article has children — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Article not found — `{"error":"…"}` | + +--- + +_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 4419566..f5a27c3 100644 --- a/src/api/routes/article.js +++ b/src/api/routes/article.js @@ -33,6 +33,7 @@ function articleErrorStatus(error) { if (error.message === 'Image not found') return 404; if (error.message === 'Name required') return 400; if (error.message === 'Invalid image order') return 400; + if (error.message === 'Article has children') return 400; return 500; } @@ -239,6 +240,50 @@ export function register(app, basePath = '') { * application/json: * schema: * $ref: '#/components/schemas/Error' + * delete: + * tags: [Article] + * summary: Delete article by SKU + * description: | + * Deletes the article when it has no child articles. Cascades related rows; + * nulls `manifestarticle.article_id` and appends a delete comment. + * Auth like GET by SKU. + * security: + * - cookieAuth: [] + * x-markdown: article/delete.md + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * responses: + * 204: + * description: Deleted + * 400: + * description: Article has children + * 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: Article not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' */ app.get(`${basePath}/article/:sku`, async (req, res) => { const sku = typeof req.params.sku === 'string' ? req.params.sku.trim() : ''; @@ -277,6 +322,22 @@ export function register(app, basePath = '') { } }); + app.delete(`${basePath}/article/:sku`, async (req, res) => { + const parsed = parseSku(req.params.sku); + if (parsed.error) { + return res.status(400).json({ error: parsed.error }); + } + + try { + await db.remove(req.user.id, parsed.sku); + res.sendStatus(204); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('Delete article failed:', error); + res.status(status).json({ error: error.message }); + } + }); + /** * @openapi * /article/{sku}/image: diff --git a/src/db/article.js b/src/db/article.js index 5e7ba4d..9a1193f 100644 --- a/src/db/article.js +++ b/src/db/article.js @@ -29,3 +29,8 @@ export async function orderImages(googleAuthId, sku, data) { const store = await getStore(); return store.article.orderImages(googleAuthId, sku, data); } + +export async function remove(googleAuthId, sku) { + const store = await getStore(); + return store.article.delete(googleAuthId, sku); +}