diff --git a/README.md b/README.md index fdaaa5d..b7c6134 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,7 @@ Company-scoped POS terminals. Auth via `resolveCompanyAccess`. | Method | Signature | Returns | | --- | --- | --- | +| `listSearchIndex` | `(googleAuthId, companyId)` | `[{ id, sku, name, eans }]` | | `createUnderParent` | `({ googleAuthId, parentId, name })` | `{ id, parentId, name, sku }` | | `getBySku` | `(googleAuthId, sku)` | detail object (see below) | | `updateBySku` | `(googleAuthId, sku, { name?, unitamount?, unitSi? })` | same shape as `getBySku` | @@ -310,6 +311,8 @@ Company-scoped POS terminals. Auth via `resolveCompanyAccess`. `getCategoryArticles` / `getTreeForCompany().articles[]` keep the flat list shape (`net`, `taxRate`, single `fullHash`/`thumbW`/`thumbH`/`thumbHash`). +`listSearchIndex` returns every article in the company (reachable via the category tree) with `id`, `sku`, `name`, and `eans` (all EANs as a flat string array). Designed for client-side search / autocomplete preload. Auth via `resolveCompanyAccess`. Errors: `Company not found`, `Not authorized`. + `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`. Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Errors: `Article not found`, `Not authorized`. @@ -324,7 +327,7 @@ Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Err `setWeightBySku` sets the article’s `weight`. Must be finite and `>= 0` (else `Invalid weight`). Errors: `Article not found`, `Not authorized`, `Invalid weight`. -`setCategoryIdsBySku` replaces the article’s category list (`categoryarticle`) in one transaction. Each category must belong to the article’s company and ids must be unique (else `Invalid categoryIds`); empty array removes the article from all categories. Returns the same object as `getBySku` (whose `categoryIds` reflects the new list). Errors: `Article not found`, `Not authorized`, `Invalid categoryIds`. +`setCategoryIdsBySku` replaces the article’s category list (`categoryarticle`) in one transaction. Each category must belong to the article’s company and ids must be unique (else `Invalid categoryIds`); empty array is rejected — an article must have at least one category (else `Article must have at least one category`). Returns the same object as `getBySku` (whose `categoryIds` reflects the new list). Errors: `Article not found`, `Not authorized`, `Invalid categoryIds`, `Article must have at least one category`. `deleteImage` removes `articlepic` and the `pic` row for this article (`fullHash`). Errors: `Article not found`, `Image not found`, `Not authorized`. @@ -394,6 +397,7 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `DELETE /api/posterminal/:id/categories` | `posterminal.setOffAll(googleAuthId, companyId, id)` | | `POST /api/article` | `article.createUnderParent({ googleAuthId, parentId, name })` | | `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` | +| `GET /api/articles/search-index` | `article.listSearchIndex(googleAuthId, companyId)` | | `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name?, unitamount?, unitSi? })` | | `PUT /api/article/:sku/prices` | `article.setPricesBySku(googleAuthId, sku, [{ price, tax, minmultiplier? }])` | | `PUT /api/article/:sku/eans` | `article.setEansBySku(googleAuthId, sku, string[])` | diff --git a/src/index.js b/src/index.js index c1ff815..63f6f1c 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ import articleOrderImages from './store/article/orderImages.js'; import articleAddImage from './store/article/addImage.js'; import articleCreateUnderParent from './store/article/createUnderParent.js'; import articleGetBySku from './store/article/getBySku.js'; +import articleListSearchIndex from './store/article/listSearchIndex.js'; import articleUpdateBySku from './store/article/updateBySku.js'; import articleSetPricesBySku from './store/article/setPricesBySku.js'; import articleSetEansBySku from './store/article/setEansBySku.js'; @@ -95,6 +96,7 @@ export default class LibDashStore { this.article = { createUnderParent: articleCreateUnderParent(pool), getBySku: articleGetBySku(pool), + listSearchIndex: articleListSearchIndex(pool), updateBySku: articleUpdateBySku(pool), setPricesBySku: articleSetPricesBySku(pool), setEansBySku: articleSetEansBySku(pool), diff --git a/src/sql/article.js b/src/sql/article.js index 903c15a..c7d38f8 100644 --- a/src/sql/article.js +++ b/src/sql/article.js @@ -160,6 +160,46 @@ FROM brand WHERE id = @brandId AND company_id = @companyId `; +/** + * Company-wide article search index. + * Returns one row per article with id, sku, name, and all eans as a JSON array. + * Used by the client for local search / autocomplete. + */ +export const SEARCH_ARTICLES = ` +;WITH category_tree AS ( + SELECT id + FROM category + WHERE id = @rootCategoryId + + UNION ALL + + SELECT c.id + FROM category c + INNER JOIN category_tree ct ON c.parent_id = ct.id +) +SELECT + a.id, + a.sku, + at.name, + ( + SELECT ae.ean + FROM articleean ae + WHERE ae.article_id = a.id + ORDER BY ae.ean + FOR JSON PATH + ) AS eansJson +FROM article a +LEFT JOIN articletext at + ON at.article_id = a.id AND at.language_id = @languageId +WHERE EXISTS ( + SELECT 1 + FROM categoryarticle ca + INNER JOIN category_tree ct ON ct.id = ca.category_id + WHERE ca.article_id = a.id +) +ORDER BY a.id +`; + /** * Single-query replacement for resolveArticleBySku + queryBySku. * Resolves SKU → article + company + auth, then loads full article detail diff --git a/src/store/article/listSearchIndex.js b/src/store/article/listSearchIndex.js new file mode 100644 index 0000000..7d37835 --- /dev/null +++ b/src/store/article/listSearchIndex.js @@ -0,0 +1,23 @@ +import { SEARCH_ARTICLES } from '../../sql/article.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function listSearchIndex(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId) => { + const { rootCategoryId, languageId } = await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('rootCategoryId', rootCategoryId) + .input('languageId', languageId) + .query(SEARCH_ARTICLES); + + return result.recordset.map((row) => ({ + id: row.id, + sku: row.sku, + name: row.name, + eans: row.eansJson ? JSON.parse(row.eansJson).map((r) => r.ean) : [], + })); + }; +}