From c2e128cb55bd500477f3f6b2a8e3755d8432cadb Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 31 Jul 2026 17:50:06 +0200 Subject: [PATCH] u --- README.md | 8 +++- scripts/test-api.js | 21 +++++++++ src/index.js | 2 + src/sql/article.js | 5 ++- src/sql/articletext.js | 6 +++ src/store/article/updateBySku.js | 77 ++++++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 src/store/article/updateBySku.js diff --git a/README.md b/README.md index ccc2ab4..78871d6 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Backend mapping: | `createUnderParent` | `({ googleAuthId, parentId, name })` | `{ id, parentId, sort, name }` | | `delete` | `({ googleAuthId, id })` | `void` — blocked by FK if `categoryarticle` or child categories remain | | `getTreeForCompany` | `(googleAuthId, companyId)` | `{ languageId, tree, articles }` | -| `getCategoryArticles` | `(googleAuthId, categoryId)` | `{ id, sku, name, slug, net, taxRate, optionsCount, articleListCount, childCount, thumbW, thumbH, thumbHash }[]` | +| `getCategoryArticles` | `(googleAuthId, categoryId)` | `{ id, sku, name, slug, net, taxRate, optionsCount, articleListCount, childCount, fatherArticleId, fatherSku, thumbW, thumbH, thumbHash }[]` | ### `createUnderParent` (transaction) @@ -142,12 +142,15 @@ Backend mapping: | Method | Signature | Returns | | --- | --- | --- | | `createUnderParent` | `({ googleAuthId, parentId, name })` | `{ id, parentId, name, sku }` | -| `getBySku` | `(googleAuthId, sku)` | `{ id, sku, name, slug, net, taxRate, optionsCount, articleListCount, childCount, thumbW, thumbH, thumbHash }` | +| `getBySku` | `(googleAuthId, sku)` | `{ id, sku, name, slug, net, taxRate, optionsCount, articleListCount, childCount, fatherArticleId, fatherSku, thumbW, thumbH, thumbHash }` | +| `updateBySku` | `(googleAuthId, sku, { name })` | same shape as `getBySku` | `parentId` is the category id. Creates `article` + `articletext` (company default language) + `categoryarticle` in one transaction. Allocates `sku` via `getnextnumberwithprefixandpadding` (`type` `sku`; creates `numbers` row with empty prefix if missing). Default unit `pc` is ensured if missing (`unitamount` 1, `weight` 0). Auth via `resolveCategoryAccess(googleAuthId, parentId)` — `Parent not found` / `Not authorized`. Same article shape as `getCategoryArticles` / `getTreeForCompany().articles[]`. Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Errors: `Article not found`, `Not authorized`. +`updateBySku` updates `articletext` (company default language) in a transaction (`name` + slugified `slug`), then returns the same select as `getBySku`. Errors: `Article not found`, `Not authorized`, `Name required`. + --- ## Backend route cheatsheet @@ -164,6 +167,7 @@ Same article shape as `getCategoryArticles` / `getTreeForCompany().articles[]`. | `DELETE /api/category/:id` | `category.delete({ googleAuthId, id })` | | `POST /api/article` | `article.createUnderParent({ googleAuthId, parentId, name })` | | `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` | +| `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name })` | --- diff --git a/scripts/test-api.js b/scripts/test-api.js index b42fe72..0f92760 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -114,6 +114,11 @@ const MENU = { label: 'getBySku', hint: 'googleAuthId, sku → row', }, + { + value: 'article.updateBySku', + label: 'updateBySku', + hint: 'googleAuthId, sku, name → row', + }, ], }, companyuser: { @@ -399,6 +404,22 @@ async function runMethod(client, action) { await client.article.getBySku(Number(googleAuthId), sku), ]; } + case 'article.updateBySku': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const sku = await askText('sku', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (sku == null) return; + const name = await askText('name', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (name == null) return; + return [ + 'result', + await client.article.updateBySku(Number(googleAuthId), sku, { name }), + ]; + } case 'companyuser.add': { const companyId = await askInt('Company id'); if (companyId == null) return; diff --git a/src/index.js b/src/index.js index 75b6a04..ee010ef 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,7 @@ import categoryCreateUnderParent from './store/category/createUnderParent.js'; import categoryDelete from './store/category/delete.js'; import articleCreateUnderParent from './store/article/createUnderParent.js'; import articleGetBySku from './store/article/getBySku.js'; +import articleUpdateBySku from './store/article/updateBySku.js'; import langCreate from './store/lang/create.js'; import langDelete from './store/lang/delete.js'; import langList from './store/lang/list.js'; @@ -55,6 +56,7 @@ export default class LibDashStore { this.article = { createUnderParent: articleCreateUnderParent(pool), getBySku: articleGetBySku(pool), + updateBySku: articleUpdateBySku(pool), }; this.lang = { diff --git a/src/sql/article.js b/src/sql/article.js index b74941d..8740be4 100644 --- a/src/sql/article.js +++ b/src/sql/article.js @@ -13,6 +13,7 @@ OUTER APPLY ( ORDER BY ap.sort ASC, ap.id ASC ) first_pic LEFT JOIN pic ON pic.id = first_pic.pic_id +LEFT JOIN article father ON father.id = a.fatherarticle_id `; export const ARTICLE_LIST_COLUMNS = ` @@ -43,13 +44,15 @@ export const ARTICLE_LIST_COLUMNS = ` FROM article child WHERE child.fatherarticle_id = a.id ) AS childCount, + a.fatherarticle_id AS fatherArticleId, + father.sku AS fatherSku, pic.thumbw AS thumbW, pic.thumbh AS thumbH, pic.thumbhash AS thumbHash `; export const GET_CATEGORY_ID_BY_SKU = ` -SELECT TOP 1 ca.category_id AS categoryId +SELECT TOP 1 ca.category_id AS categoryId, a.id AS articleId FROM article a INNER JOIN categoryarticle ca ON ca.article_id = a.id WHERE a.sku = @sku diff --git a/src/sql/articletext.js b/src/sql/articletext.js index c42540e..bcfae68 100644 --- a/src/sql/articletext.js +++ b/src/sql/articletext.js @@ -2,3 +2,9 @@ export const CREATE = ` INSERT INTO articletext (article_id, language_id, name, slug) VALUES (@articleId, @languageId, @name, @slug) `; + +export const UPDATE_BY_ARTICLE_AND_LANGUAGE = ` +UPDATE articletext +SET name = @name, slug = @slug +WHERE article_id = @articleId AND language_id = @languageId +`; diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js new file mode 100644 index 0000000..1199635 --- /dev/null +++ b/src/store/article/updateBySku.js @@ -0,0 +1,77 @@ +import { GET_BY_SKU, GET_CATEGORY_ID_BY_SKU } from '../../sql/article.js'; +import { UPDATE_BY_ARTICLE_AND_LANGUAGE } from '../../sql/articletext.js'; +import resolveCategoryAccess from '../category/resolveCategoryAccess.js'; +import request from '../request.js'; + +function slugify(name) { + return name + .trim() + .toLowerCase() + .normalize('NFD') + .replace(/\p{M}/gu, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +export default function updateBySku(pool) { + const access = resolveCategoryAccess(pool); + + return async (googleAuthId, sku, { name }) => { + if (!name?.trim()) { + throw new Error('Name required'); + } + + const categoryLookup = await pool + .request() + .input('sku', sku) + .query(GET_CATEGORY_ID_BY_SKU); + + const row = categoryLookup.recordset[0]; + if (!row?.categoryId || !row?.articleId) { + throw new Error('Article not found'); + } + + const { languageId, posCustomerGroupId } = await access( + googleAuthId, + row.categoryId, + ); + + const slug = slugify(name); + const tx = pool.transaction(); + await tx.begin(); + + try { + const update = await request(pool, tx) + .input('articleId', row.articleId) + .input('languageId', languageId) + .input('name', name.trim()) + .input('slug', slug) + .query(UPDATE_BY_ARTICLE_AND_LANGUAGE); + + if (!update.rowsAffected[0]) { + throw new Error('Article not found'); + } + + const result = await request(pool, tx) + .input('sku', sku) + .input('languageId', languageId) + .input('posCustomerGroupId', posCustomerGroupId) + .query(GET_BY_SKU); + + const article = result.recordset[0]; + if (!article) { + throw new Error('Article not found'); + } + + await tx.commit(); + return article; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +}