From 6b1701d4dc973ba60cbbffaa5b5154039f515b28 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 31 Jul 2026 18:15:15 +0200 Subject: [PATCH] u --- README.md | 25 +++++++-- src/sql/article.js | 8 --- src/sql/articleDetail.js | 93 ++++++++++++++++++++++++++++++++ src/store/article/getBySku.js | 22 +++----- src/store/article/queryBySku.js | 66 +++++++++++++++++++++++ src/store/article/updateBySku.js | 18 +++---- 6 files changed, 194 insertions(+), 38 deletions(-) create mode 100644 src/sql/articleDetail.js create mode 100644 src/store/article/queryBySku.js diff --git a/README.md b/README.md index 78871d6..0699800 100644 --- a/README.md +++ b/README.md @@ -142,14 +142,33 @@ 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, fatherArticleId, fatherSku, thumbW, thumbH, thumbHash }` | +| `getBySku` | `(googleAuthId, sku)` | detail object (see below) | | `updateBySku` | `(googleAuthId, sku, { name })` | same shape as `getBySku` | +`getBySku` / `updateBySku` return: + +```js +{ + id, sku, name, slug, + unitamount, weight, + unit: { si }, + brand: { name, thumbW, thumbH, thumbHash } | null, + categoryIds: number[], + photos: { sort, thumbW, thumbH, thumbHash }[], + eans: string[], + prices: { minmultiplier, net, taxRate, customergroupId }[], + optionsCount, articleListCount, childCount, + fatherArticleId, fatherSku, +} +``` + +`getCategoryArticles` / `getTreeForCompany().articles[]` keep the flat list shape (`net`, `taxRate`, single `thumbW`/`thumbH`/`thumbHash`). + `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`. +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`. +`updateBySku` updates `articletext` (company default language) in a transaction (`name` + slugified `slug`), then returns the same object as `getBySku`. Errors: `Article not found`, `Not authorized`, `Name required`. --- diff --git a/src/sql/article.js b/src/sql/article.js index 8740be4..ebc7694 100644 --- a/src/sql/article.js +++ b/src/sql/article.js @@ -58,14 +58,6 @@ INNER JOIN categoryarticle ca ON ca.article_id = a.id WHERE a.sku = @sku `; -export const GET_BY_SKU = ` -SELECT -${ARTICLE_LIST_COLUMNS} -FROM article a -${ARTICLE_LIST_JOINS} -WHERE a.sku = @sku -`; - export const CREATE = ` INSERT INTO article (sku, unit_id, unitamount, weight) OUTPUT INSERTED.id diff --git a/src/sql/articleDetail.js b/src/sql/articleDetail.js new file mode 100644 index 0000000..5d9b14e --- /dev/null +++ b/src/sql/articleDetail.js @@ -0,0 +1,93 @@ +export const ARTICLE_DETAIL_COLUMNS = ` + a.id, + a.sku, + at.name, + at.slug, + a.unitamount, + a.weight, + u.si AS unitSi, + b.name AS brandName, + brand_pic.thumbw AS brandThumbW, + brand_pic.thumbh AS brandThumbH, + brand_pic.thumbhash AS brandThumbHash, + CASE + WHEN a.optionslist_id IS NOT NULL THEN ( + SELECT COUNT(*) + FROM optionslistarticle ola + WHERE ola.optionslist_id = a.optionslist_id + ) + ELSE 0 + END AS optionsCount, + CASE + WHEN a.articlelist_id IS NOT NULL THEN ( + SELECT COUNT(*) + FROM articlelistarticle ala + WHERE ala.articlelist_id = a.articlelist_id + ) + ELSE 0 + END AS articleListCount, + ( + SELECT COUNT(*) + FROM article child + WHERE child.fatherarticle_id = a.id + ) AS childCount, + a.fatherarticle_id AS fatherArticleId, + father.sku AS fatherSku +`; + +export const GET_DETAIL_BY_SKU = ` +SELECT +${ARTICLE_DETAIL_COLUMNS} +FROM article a +LEFT JOIN articletext at + ON at.article_id = a.id AND at.language_id = @languageId +LEFT JOIN unit u ON u.id = a.unit_id +LEFT JOIN brand b ON b.id = a.brand_id +OUTER APPLY ( + SELECT TOP 1 bp.pic_id + FROM brandpic bp + WHERE bp.brand_id = b.id + ORDER BY bp.sort ASC, bp.id ASC +) first_brand_pic +LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id +LEFT JOIN article father ON father.id = a.fatherarticle_id +WHERE a.sku = @sku +`; + +export const GET_CATEGORY_IDS_BY_ARTICLE_ID = ` +SELECT ca.category_id AS categoryId +FROM categoryarticle ca +WHERE ca.article_id = @articleId +ORDER BY ca.category_id +`; + +export const GET_PHOTOS_BY_ARTICLE_ID = ` +SELECT + ap.sort, + p.thumbw AS thumbW, + p.thumbh AS thumbH, + p.thumbhash AS thumbHash +FROM articlepic ap +INNER JOIN pic p ON p.id = ap.pic_id +WHERE ap.article_id = @articleId +ORDER BY ap.sort ASC, ap.id ASC +`; + +export const GET_EANS_BY_ARTICLE_ID = ` +SELECT ae.ean +FROM articleean ae +WHERE ae.article_id = @articleId +ORDER BY ae.ean +`; + +export const GET_PRICES_BY_ARTICLE_ID = ` +SELECT + pr.minmultiplier AS minmultiplier, + pr.net, + t.rate AS taxRate, + pr.customergroup_id AS customergroupId +FROM price pr +INNER JOIN tax t ON t.id = pr.tax_id +WHERE pr.article_id = @articleId +ORDER BY pr.customergroup_id, pr.minmultiplier +`; diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index 694a77b..e32b888 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -1,8 +1,10 @@ -import { GET_BY_SKU, GET_CATEGORY_ID_BY_SKU } from '../../sql/article.js'; +import { GET_CATEGORY_ID_BY_SKU } from '../../sql/article.js'; import resolveCategoryAccess from '../category/resolveCategoryAccess.js'; +import queryBySku from './queryBySku.js'; export default function getBySku(pool) { const access = resolveCategoryAccess(pool); + const load = queryBySku(pool); return async (googleAuthId, sku) => { const categoryLookup = await pool @@ -15,23 +17,13 @@ export default function getBySku(pool) { throw new Error('Article not found'); } - const { languageId, posCustomerGroupId } = await access( - googleAuthId, - categoryId, - ); + const { languageId } = await access(googleAuthId, categoryId); - const result = await pool - .request() - .input('sku', sku) - .input('languageId', languageId) - .input('posCustomerGroupId', posCustomerGroupId) - .query(GET_BY_SKU); - - const row = result.recordset[0]; - if (!row) { + const article = await load(sku, languageId); + if (!article) { throw new Error('Article not found'); } - return row; + return article; }; } diff --git a/src/store/article/queryBySku.js b/src/store/article/queryBySku.js new file mode 100644 index 0000000..f11080c --- /dev/null +++ b/src/store/article/queryBySku.js @@ -0,0 +1,66 @@ +import { + GET_CATEGORY_IDS_BY_ARTICLE_ID, + GET_DETAIL_BY_SKU, + GET_EANS_BY_ARTICLE_ID, + GET_PHOTOS_BY_ARTICLE_ID, + GET_PRICES_BY_ARTICLE_ID, +} from '../../sql/articleDetail.js'; + +function shapeArticleDetail(row, categoryIds, photos, eans, prices) { + return { + id: row.id, + sku: row.sku, + name: row.name, + slug: row.slug, + unitamount: row.unitamount, + weight: row.weight, + unit: { si: row.unitSi }, + brand: row.brandName + ? { + name: row.brandName, + thumbW: row.brandThumbW, + thumbH: row.brandThumbH, + thumbHash: row.brandThumbHash, + } + : null, + categoryIds: categoryIds.map((r) => r.categoryId), + photos, + eans: eans.map((r) => r.ean), + prices, + optionsCount: row.optionsCount, + articleListCount: row.articleListCount, + childCount: row.childCount, + fatherArticleId: row.fatherArticleId, + fatherSku: row.fatherSku, + }; +} + +export default function queryBySku(pool) { + return async (sku, languageId) => { + const detail = await pool + .request() + .input('sku', sku) + .input('languageId', languageId) + .query(GET_DETAIL_BY_SKU); + + const row = detail.recordset[0]; + if (!row) { + return null; + } + + const [categories, photos, eans, prices] = await Promise.all([ + pool.request().input('articleId', row.id).query(GET_CATEGORY_IDS_BY_ARTICLE_ID), + pool.request().input('articleId', row.id).query(GET_PHOTOS_BY_ARTICLE_ID), + pool.request().input('articleId', row.id).query(GET_EANS_BY_ARTICLE_ID), + pool.request().input('articleId', row.id).query(GET_PRICES_BY_ARTICLE_ID), + ]); + + return shapeArticleDetail( + row, + categories.recordset, + photos.recordset, + eans.recordset, + prices.recordset, + ); + }; +} diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js index 1199635..410dced 100644 --- a/src/store/article/updateBySku.js +++ b/src/store/article/updateBySku.js @@ -1,7 +1,8 @@ -import { GET_BY_SKU, GET_CATEGORY_ID_BY_SKU } from '../../sql/article.js'; +import { 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'; +import queryBySku from './queryBySku.js'; function slugify(name) { return name @@ -15,6 +16,7 @@ function slugify(name) { export default function updateBySku(pool) { const access = resolveCategoryAccess(pool); + const load = queryBySku(pool); return async (googleAuthId, sku, { name }) => { if (!name?.trim()) { @@ -31,10 +33,7 @@ export default function updateBySku(pool) { throw new Error('Article not found'); } - const { languageId, posCustomerGroupId } = await access( - googleAuthId, - row.categoryId, - ); + const { languageId } = await access(googleAuthId, row.categoryId); const slug = slugify(name); const tx = pool.transaction(); @@ -52,18 +51,13 @@ export default function updateBySku(pool) { 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); + await tx.commit(); - const article = result.recordset[0]; + const article = await load(sku, languageId); if (!article) { throw new Error('Article not found'); } - await tx.commit(); return article; } catch (err) { try {