From 9885c489f0620ec8ef3c11b92f84faf8a4434fe2 Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 29 Sep 2026 20:10:49 +0200 Subject: [PATCH] u --- API.articlelist.md | 40 +++++ API.md | 6 + API.optionslist.md | 40 +++++ src/index.js | 18 +++ src/providers/mssql/queries/article.js | 33 ++++ src/providers/mssql/queries/articlelist.js | 41 +++++ src/providers/mssql/queries/optionslist.js | 42 +++++ src/providers/sqlite3/queries/article.js | 27 ++++ src/providers/sqlite3/queries/articlelist.js | 14 ++ src/providers/sqlite3/queries/optionslist.js | 14 ++ src/providers/sqlite3/queries/translate.js | 2 +- src/store/article/getBySku.js | 19 +++ src/store/article/toggleFather.js | 11 +- src/store/articlelist/getItemsByArticleSku.js | 55 +++++++ src/store/articlelist/setItemsByArticleSku.js | 148 +++++++++++++++++ .../optionslist/getOptionsByArticleSku.js | 56 +++++++ .../optionslist/setOptionsByArticleSku.js | 152 ++++++++++++++++++ 17 files changed, 715 insertions(+), 3 deletions(-) create mode 100644 API.articlelist.md create mode 100644 API.optionslist.md create mode 100644 src/providers/mssql/queries/articlelist.js create mode 100644 src/providers/mssql/queries/optionslist.js create mode 100644 src/providers/sqlite3/queries/articlelist.js create mode 100644 src/providers/sqlite3/queries/optionslist.js create mode 100644 src/store/articlelist/getItemsByArticleSku.js create mode 100644 src/store/articlelist/setItemsByArticleSku.js create mode 100644 src/store/optionslist/getOptionsByArticleSku.js create mode 100644 src/store/optionslist/setOptionsByArticleSku.js diff --git a/API.articlelist.md b/API.articlelist.md new file mode 100644 index 0000000..58b419c --- /dev/null +++ b/API.articlelist.md @@ -0,0 +1,40 @@ +# articlelist + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `articlelist.getItemsByArticleSku` + +*Source: `src/store/articlelist/getItemsByArticleSku.js`* + +Returns the Stückliste (bundle items) for an article by its SKU. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` + +## `articlelist.setItemsByArticleSku` + +*Source: `src/store/articlelist/setItemsByArticleSku.js`* + +Sets or clears the Stückliste (bundle items) for an article. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The master article SKU. | +| `[items=[]]` | `Array<{articleId?: number, sku?: string, count?: number, sort?: number}>` | Array of component items. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` diff --git a/API.md b/API.md index acf287b..7dc1881 100644 --- a/API.md +++ b/API.md @@ -259,5 +259,11 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>` - `list(googleAuthId: number, tx?: object): Promise>` - `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>` +- [articlelist](API.articlelist.md) + - `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise>` + - `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise>` +- [optionslist](API.optionslist.md) + - `getOptionsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise>` + - `setOptionsByArticleSku(googleAuthId: number, sku: string, options?: Array<{articleId?: number, sku?: string, count?: number, sort?: number, mandated?: boolean}>, tx?: object): Promise>` - [runner.js](API.runner.js.md) - `runner(provider: import('../providers/provider.js').DatabaseProvider, tx?: object): ((sql: string, params?: object) => Promise) & { query: Function, dialect?: string }` diff --git a/API.optionslist.md b/API.optionslist.md new file mode 100644 index 0000000..235252a --- /dev/null +++ b/API.optionslist.md @@ -0,0 +1,40 @@ +# optionslist + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `optionslist.getOptionsByArticleSku` + +*Source: `src/store/optionslist/getOptionsByArticleSku.js`* + +Returns the options list for an article by its SKU. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` + +## `optionslist.setOptionsByArticleSku` + +*Source: `src/store/optionslist/setOptionsByArticleSku.js`* + +Sets or clears the options list for an article. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The master article SKU. | +| `[options=[]]` | `Array<{articleId?: number, sku?: string, count?: number, sort?: number, mandated?: boolean}>` | Array of options items. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` diff --git a/src/index.js b/src/index.js index 89839cc..9e62759 100644 --- a/src/index.js +++ b/src/index.js @@ -171,6 +171,10 @@ import attributevalueOrder from './store/attributevalue/order.js'; import attributevalueDelete from './store/attributevalue/delete.js'; import articleattributevalueListByArticleSku from './store/articleattributevalue/listByArticleSku.js'; import articleattributevalueSetByArticleSku from './store/articleattributevalue/setByArticleSku.js'; +import articlelistGetItems from './store/articlelist/getItemsByArticleSku.js'; +import articlelistSetItems from './store/articlelist/setItemsByArticleSku.js'; +import optionslistGetOptions from './store/optionslist/getOptionsByArticleSku.js'; +import optionslistSetOptions from './store/optionslist/setOptionsByArticleSku.js'; export default class LibDashStore { /** @@ -234,6 +238,10 @@ export default class LibDashStore { addChild: articleAddChild(provider), removeChild: articleRemoveChild(provider), listChildren: articleListChildren(provider), + getArticlelist: articlelistGetItems(provider), + setArticlelist: articlelistSetItems(provider), + getOptionslist: optionslistGetOptions(provider), + setOptionslist: optionslistSetOptions(provider), importRows: articleImportRows(provider), addImage: articleAddImage(provider), deleteImage: articleDeleteImage(provider), @@ -462,6 +470,16 @@ export default class LibDashStore { setByArticleSku: articleattributevalueSetByArticleSku(provider), }; + this.articlelist = { + getItemsByArticleSku: articlelistGetItems(provider), + setItemsByArticleSku: articlelistSetItems(provider), + }; + + this.optionslist = { + getOptionsByArticleSku: optionslistGetOptions(provider), + setOptionsByArticleSku: optionslistSetOptions(provider), + }; + this.purge = storePurge(provider); this.initialize = storeInitialize(provider); diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index a5b0551..0550dd9 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -440,11 +440,44 @@ SELECT FROM optionslistarticle ola WHERE ola.optionslist_id = a.optionslist_id ) AS optionsCount, + a.optionslist_id AS optionslistId, + ( + SELECT + ola.id, + ola.article_id AS articleId, + a_opt.sku, + at_opt.name, + ola.count, + ola.sort, + ola.mandated + FROM optionslistarticle ola + INNER JOIN article a_opt ON a_opt.id = ola.article_id + LEFT JOIN articletext at_opt ON at_opt.article_id = a_opt.id AND at_opt.language_id = auth.languageId + WHERE ola.optionslist_id = a.optionslist_id + ORDER BY ola.sort ASC, ola.id ASC + FOR JSON PATH + ) AS optionslistItemsJson, ( SELECT COUNT(*) FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id ) AS articleListCount, + a.articlelist_id AS articlelistId, + ( + SELECT + ala.id, + ala.article_id AS articleId, + a_item.sku, + at_item.name, + ala.count, + ala.sort + FROM articlelistarticle ala + INNER JOIN article a_item ON a_item.id = ala.article_id + LEFT JOIN articletext at_item ON at_item.article_id = a_item.id AND at_item.language_id = auth.languageId + WHERE ala.articlelist_id = a.articlelist_id + ORDER BY ala.sort ASC, ala.id ASC + FOR JSON PATH + ) AS articlelistItemsJson, ( SELECT COUNT(*) FROM article child diff --git a/src/providers/mssql/queries/articlelist.js b/src/providers/mssql/queries/articlelist.js new file mode 100644 index 0000000..f0a9fdd --- /dev/null +++ b/src/providers/mssql/queries/articlelist.js @@ -0,0 +1,41 @@ +export const CREATE_ARTICLELIST = ` +INSERT INTO articlelist OUTPUT INSERTED.id DEFAULT VALUES; +`; + +export const SET_ARTICLE_ARTICLELIST_ID = ` +UPDATE article +SET articlelist_id = @articlelistId +WHERE id = @articleId; +`; + +export const GET_ARTICLELIST_ITEMS = ` +SELECT + ala.id, + ala.articlelist_id AS articlelistId, + ala.article_id AS articleId, + a.sku, + at.name, + ala.count, + ala.sort +FROM articlelistarticle ala +INNER JOIN article a ON a.id = ala.article_id +LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId +WHERE ala.articlelist_id = @articlelistId +ORDER BY ala.sort ASC, ala.id ASC; +`; + +export const INSERT_ARTICLELIST_ARTICLE = ` +INSERT INTO articlelistarticle (articlelist_id, article_id, count, sort) +OUTPUT INSERTED.id, INSERTED.articlelist_id AS articlelistId, INSERTED.article_id AS articleId, INSERTED.count, INSERTED.sort +VALUES (@articlelistId, @articleId, @count, @sort); +`; + +export const DELETE_ARTICLELIST_ARTICLES = ` +DELETE FROM articlelistarticle +WHERE articlelist_id = @articlelistId; +`; + +export const DELETE_ARTICLELIST = ` +DELETE FROM articlelist +WHERE id = @id; +`; diff --git a/src/providers/mssql/queries/optionslist.js b/src/providers/mssql/queries/optionslist.js new file mode 100644 index 0000000..0cccd15 --- /dev/null +++ b/src/providers/mssql/queries/optionslist.js @@ -0,0 +1,42 @@ +export const CREATE_OPTIONSLIST = ` +INSERT INTO optionslist OUTPUT INSERTED.id DEFAULT VALUES; +`; + +export const SET_ARTICLE_OPTIONSLIST_ID = ` +UPDATE article +SET optionslist_id = @optionslistId +WHERE id = @articleId; +`; + +export const GET_OPTIONSLIST_ITEMS = ` +SELECT + ola.id, + ola.optionslist_id AS optionslistId, + ola.article_id AS articleId, + a.sku, + at.name, + ola.count, + ola.sort, + ola.mandated +FROM optionslistarticle ola +INNER JOIN article a ON a.id = ola.article_id +LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId +WHERE ola.optionslist_id = @optionslistId +ORDER BY ola.sort ASC, ola.id ASC; +`; + +export const INSERT_OPTIONSLIST_ARTICLE = ` +INSERT INTO optionslistarticle (optionslist_id, article_id, count, sort, mandated) +OUTPUT INSERTED.id, INSERTED.optionslist_id AS optionslistId, INSERTED.article_id AS articleId, INSERTED.count, INSERTED.sort, INSERTED.mandated +VALUES (@optionslistId, @articleId, @count, @sort, @mandated); +`; + +export const DELETE_OPTIONSLIST_ARTICLES = ` +DELETE FROM optionslistarticle +WHERE optionslist_id = @optionslistId; +`; + +export const DELETE_OPTIONSLIST = ` +DELETE FROM optionslist +WHERE id = @id; +`; diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index 5938ba8..d6ef48d 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -132,7 +132,34 @@ SELECT auth.*, 'brandThumbW', bp.thumbw, 'brandThumbH', bp.thumbh, 'brandThumbHash', bp.thumbhash, 'optionsCount', (SELECT COUNT(*) FROM optionslistarticle WHERE optionslist_id = a.optionslist_id), + 'optionslistId', a.optionslist_id, + 'optionslistItemsJson', json((SELECT COALESCE(json_group_array(json_object( + 'id', ola.id, + 'articleId', ola.article_id, + 'sku', a_opt.sku, + 'name', at_opt.name, + 'count', ola.count, + 'sort', ola.sort, + 'mandated', ola.mandated + )), '[]') FROM optionslistarticle ola + INNER JOIN article a_opt ON a_opt.id = ola.article_id + LEFT JOIN articletext at_opt ON at_opt.article_id = a_opt.id AND at_opt.language_id = auth.languageId + WHERE ola.optionslist_id = a.optionslist_id + ORDER BY ola.sort ASC, ola.id ASC)), 'articleListCount', (SELECT COUNT(*) FROM articlelistarticle WHERE articlelist_id = a.articlelist_id), + 'articlelistId', a.articlelist_id, + 'articlelistItemsJson', json((SELECT COALESCE(json_group_array(json_object( + 'id', ala.id, + 'articleId', ala.article_id, + 'sku', a_item.sku, + 'name', at_item.name, + 'count', ala.count, + 'sort', ala.sort + )), '[]') FROM articlelistarticle ala + INNER JOIN article a_item ON a_item.id = ala.article_id + LEFT JOIN articletext at_item ON at_item.article_id = a_item.id AND at_item.language_id = auth.languageId + WHERE ala.articlelist_id = a.articlelist_id + ORDER BY ala.sort ASC, ala.id ASC)), 'childCount', (SELECT COUNT(*) FROM article WHERE fatherarticle_id = a.id), 'isFather', a.is_father, 'fatherAttributeNameId', a.fatherattributename_id, diff --git a/src/providers/sqlite3/queries/articlelist.js b/src/providers/sqlite3/queries/articlelist.js new file mode 100644 index 0000000..6d0386a --- /dev/null +++ b/src/providers/sqlite3/queries/articlelist.js @@ -0,0 +1,14 @@ +import * as source from '../../mssql/queries/articlelist.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'articlelist'); +export const { + CREATE_ARTICLELIST, + SET_ARTICLE_ARTICLELIST_ID, + GET_ARTICLELIST_ITEMS, + INSERT_ARTICLELIST_ARTICLE, + DELETE_ARTICLELIST_ARTICLES, + DELETE_ARTICLELIST, +} = queries; + +export default queries; diff --git a/src/providers/sqlite3/queries/optionslist.js b/src/providers/sqlite3/queries/optionslist.js new file mode 100644 index 0000000..8b1780e --- /dev/null +++ b/src/providers/sqlite3/queries/optionslist.js @@ -0,0 +1,14 @@ +import * as source from '../../mssql/queries/optionslist.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'optionslist'); +export const { + CREATE_OPTIONSLIST, + SET_ARTICLE_OPTIONSLIST_ID, + GET_OPTIONSLIST_ITEMS, + INSERT_OPTIONSLIST_ARTICLE, + DELETE_OPTIONSLIST_ARTICLES, + DELETE_OPTIONSLIST, +} = queries; + +export default queries; diff --git a/src/providers/sqlite3/queries/translate.js b/src/providers/sqlite3/queries/translate.js index 044c662..01f3bc8 100644 --- a/src/providers/sqlite3/queries/translate.js +++ b/src/providers/sqlite3/queries/translate.js @@ -3,7 +3,7 @@ function quoteIdentifiers(sql) { } function moveReturning(sql) { - const output = /\bOUTPUT\s+([\s\S]*?)(?=\bVALUES\b|\bWHERE\b)/i.exec(sql); + const output = /\bOUTPUT\s+([\s\S]*?)(?=\bVALUES\b|\bWHERE\b|\bDEFAULT\b)/i.exec(sql); if (!output) return sql; const columns = output[1] diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index bfaf28d..a1294ed 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -97,7 +97,26 @@ export default function getBySku(provider) { stock: Number(w.stock ?? 0), })), optionsCount: a.optionsCount, + optionslistId: a.optionslistId ?? null, + options: parseJsonArray(a.optionslistItemsJson).map((o) => ({ + id: o.id, + articleId: o.articleId, + sku: o.sku, + name: o.name ?? null, + count: Number(o.count ?? 1), + sort: Number(o.sort ?? 0), + mandated: Boolean(o.mandated), + })), articleListCount: a.articleListCount, + articlelistId: a.articlelistId ?? null, + articlelist: parseJsonArray(a.articlelistItemsJson).map((item) => ({ + id: item.id, + articleId: item.articleId, + sku: item.sku, + name: item.name ?? null, + count: Number(item.count ?? 1), + sort: Number(item.sort ?? 0), + })), childCount: a.childCount, isFather: Boolean(a.isFather), fatherAttributeNameId: a.fatherAttributeNameId ?? null, diff --git a/src/store/article/toggleFather.js b/src/store/article/toggleFather.js index d467311..31f99a9 100644 --- a/src/store/article/toggleFather.js +++ b/src/store/article/toggleFather.js @@ -42,12 +42,19 @@ export default function toggleFather(provider) { } const currentRes = await runner.query( - 'SELECT fatherarticle_id FROM article WHERE id = @articleId', + 'SELECT fatherarticle_id, articlelist_id, optionslist_id FROM article WHERE id = @articleId', { articleId }, ); + const currentArt = currentRes.rows[0]; + if (currentArt?.articlelist_id != null) { + throw new Error('A Stückliste article cannot become a father'); + } + if (currentArt?.optionslist_id != null) { + throw new Error('An article with options cannot become a father'); + } // If article is a child, verify attribute is not used by father or any father up the chain - let currentFatherId = currentRes.rows[0]?.fatherarticle_id; + let currentFatherId = currentArt?.fatherarticle_id; const visited = new Set(); while (currentFatherId != null && !visited.has(currentFatherId)) { visited.add(currentFatherId); diff --git a/src/store/articlelist/getItemsByArticleSku.js b/src/store/articlelist/getItemsByArticleSku.js new file mode 100644 index 0000000..4f0da98 --- /dev/null +++ b/src/store/articlelist/getItemsByArticleSku.js @@ -0,0 +1,55 @@ +import * as mssqlQueries from '../../providers/mssql/queries/articlelist.js'; +import * as sqliteQueries from '../../providers/sqlite3/queries/articlelist.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import makeRunner from '../runner.js'; + +export default function getItemsByArticleSku(provider) { + const resolve = resolveArticleBySku(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const queries = isSqlite ? sqliteQueries : mssqlQueries; + + /** + * Returns the Stückliste (bundle items) for an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, sku, tx) => { + const run = async (runner) => { + const { articleId, languageId } = await resolve(googleAuthId, sku, runner); + + const artRes = await runner.query( + 'SELECT articlelist_id FROM article WHERE id = @articleId', + { articleId }, + ); + const articlelistId = artRes.rows[0]?.articlelist_id; + if (!articlelistId) { + return []; + } + + const itemsRes = await runner.query(queries.GET_ARTICLELIST_ITEMS, { + articlelistId, + languageId, + }); + + return itemsRes.rows.map((row) => ({ + id: row.id, + articleId: row.articleId, + sku: row.sku, + name: row.name ?? null, + count: Number(row.count ?? 1), + sort: Number(row.sort ?? 0), + })); + }; + + if (tx) { + return run(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(run); + } + return run(makeRunner(provider)); + }; +} diff --git a/src/store/articlelist/setItemsByArticleSku.js b/src/store/articlelist/setItemsByArticleSku.js new file mode 100644 index 0000000..4118141 --- /dev/null +++ b/src/store/articlelist/setItemsByArticleSku.js @@ -0,0 +1,148 @@ +import * as mssqlQueries from '../../providers/mssql/queries/articlelist.js'; +import * as sqliteQueries from '../../providers/sqlite3/queries/articlelist.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import getItemsByArticleSku from './getItemsByArticleSku.js'; +import makeRunner from '../runner.js'; + +export default function setItemsByArticleSku(provider) { + const resolve = resolveArticleBySku(provider); + const getItems = getItemsByArticleSku(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const queries = isSqlite ? sqliteQueries : mssqlQueries; + + /** + * Sets or clears the Stückliste (bundle items) for an article. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The master article SKU. + * @param {Array<{articleId?: number, sku?: string, count?: number, sort?: number}>} [items=[]] - Array of component items. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, sku, items = [], tx) => { + if (!Array.isArray(items)) { + throw new Error('Invalid items array'); + } + + const run = async (runner) => { + const { articleId, companyId } = await resolve(googleAuthId, sku, runner); + + const artRes = await runner.query( + 'SELECT id, is_father, articlelist_id, optionslist_id FROM article WHERE id = @articleId', + { articleId }, + ); + const article = artRes.rows[0]; + if (!article) { + throw new Error('Article not found'); + } + + if (article.is_father) { + throw new Error('A father article cannot be a Stückliste'); + } + if (article.optionslist_id != null) { + throw new Error('An article with options cannot be a Stückliste'); + } + + // If items is empty: clear Stückliste + if (items.length === 0) { + if (article.articlelist_id != null) { + await runner.query(queries.SET_ARTICLE_ARTICLELIST_ID, { + articleId, + articlelistId: null, + }); + await runner.query(queries.DELETE_ARTICLELIST, { + id: article.articlelist_id, + }); + } + return []; + } + + // Validate items + const validated = []; + const seenArticleIds = new Set(); + const seenSorts = new Set(); + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + let itemArticleId = item?.articleId != null ? Number(item.articleId) : null; + + if (itemArticleId == null && item?.sku) { + const res = await runner.query( + 'SELECT id FROM article WHERE sku = @itemSku AND company_id = @companyId', + { itemSku: item.sku, companyId }, + ); + itemArticleId = res.rows[0]?.id; + } + + if (!itemArticleId || Number.isNaN(itemArticleId) || itemArticleId <= 0) { + throw new Error(`Item at index ${i} must specify a valid articleId or sku`); + } + + if (itemArticleId === articleId) { + throw new Error('A Stückliste article cannot contain itself'); + } + + if (seenArticleIds.has(itemArticleId)) { + throw new Error(`Duplicate article in Stückliste: ${itemArticleId}`); + } + seenArticleIds.add(itemArticleId); + + // Verify component article exists in same company + const itemArtRes = await runner.query( + 'SELECT id FROM article WHERE id = @itemArticleId AND company_id = @companyId', + { itemArticleId, companyId }, + ); + if (!itemArtRes.rows[0]) { + throw new Error(`Article not found for component: ${itemArticleId}`); + } + + const count = item?.count != null && Number(item.count) > 0 ? Number(item.count) : 1; + let sort = item?.sort != null && Number.isInteger(Number(item.sort)) ? Number(item.sort) : i; + + while (seenSorts.has(sort)) { + sort++; + } + seenSorts.add(sort); + + validated.push({ + articleId: itemArticleId, + count, + sort, + }); + } + + let targetListId = article.articlelist_id; + if (targetListId == null) { + const listRes = await runner.query(queries.CREATE_ARTICLELIST); + targetListId = listRes.rows[0]?.id ?? listRes.insertId; + await runner.query(queries.SET_ARTICLE_ARTICLELIST_ID, { + articleId, + articlelistId: targetListId, + }); + } else { + await runner.query(queries.DELETE_ARTICLELIST_ARTICLES, { + articlelistId: targetListId, + }); + } + + for (const val of validated) { + await runner.query(queries.INSERT_ARTICLELIST_ARTICLE, { + articlelistId: targetListId, + articleId: val.articleId, + count: val.count, + sort: val.sort, + }); + } + + return getItems(googleAuthId, sku, runner); + }; + + if (tx) { + return run(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(run); + } + return run(makeRunner(provider)); + }; +} diff --git a/src/store/optionslist/getOptionsByArticleSku.js b/src/store/optionslist/getOptionsByArticleSku.js new file mode 100644 index 0000000..2588172 --- /dev/null +++ b/src/store/optionslist/getOptionsByArticleSku.js @@ -0,0 +1,56 @@ +import * as mssqlQueries from '../../providers/mssql/queries/optionslist.js'; +import * as sqliteQueries from '../../providers/sqlite3/queries/optionslist.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import makeRunner from '../runner.js'; + +export default function getOptionsByArticleSku(provider) { + const resolve = resolveArticleBySku(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const queries = isSqlite ? sqliteQueries : mssqlQueries; + + /** + * Returns the options list for an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, sku, tx) => { + const run = async (runner) => { + const { articleId, languageId } = await resolve(googleAuthId, sku, runner); + + const artRes = await runner.query( + 'SELECT optionslist_id FROM article WHERE id = @articleId', + { articleId }, + ); + const optionslistId = artRes.rows[0]?.optionslist_id; + if (!optionslistId) { + return []; + } + + const itemsRes = await runner.query(queries.GET_OPTIONSLIST_ITEMS, { + optionslistId, + languageId, + }); + + return itemsRes.rows.map((row) => ({ + id: row.id, + articleId: row.articleId, + sku: row.sku, + name: row.name ?? null, + count: Number(row.count ?? 1), + sort: Number(row.sort ?? 0), + mandated: Boolean(row.mandated), + })); + }; + + if (tx) { + return run(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(run); + } + return run(makeRunner(provider)); + }; +} diff --git a/src/store/optionslist/setOptionsByArticleSku.js b/src/store/optionslist/setOptionsByArticleSku.js new file mode 100644 index 0000000..fe87bf4 --- /dev/null +++ b/src/store/optionslist/setOptionsByArticleSku.js @@ -0,0 +1,152 @@ +import * as mssqlQueries from '../../providers/mssql/queries/optionslist.js'; +import * as sqliteQueries from '../../providers/sqlite3/queries/optionslist.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import getOptionsByArticleSku from './getOptionsByArticleSku.js'; +import makeRunner from '../runner.js'; + +export default function setOptionsByArticleSku(provider) { + const resolve = resolveArticleBySku(provider); + const getOptions = getOptionsByArticleSku(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const queries = isSqlite ? sqliteQueries : mssqlQueries; + + /** + * Sets or clears the options list for an article. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The master article SKU. + * @param {Array<{articleId?: number, sku?: string, count?: number, sort?: number, mandated?: boolean}>} [options=[]] - Array of options items. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, sku, options = [], tx) => { + if (!Array.isArray(options)) { + throw new Error('Invalid options array'); + } + + const run = async (runner) => { + const { articleId, companyId } = await resolve(googleAuthId, sku, runner); + + const artRes = await runner.query( + 'SELECT id, is_father, articlelist_id, optionslist_id FROM article WHERE id = @articleId', + { articleId }, + ); + const article = artRes.rows[0]; + if (!article) { + throw new Error('Article not found'); + } + + if (article.is_father) { + throw new Error('A father article cannot have options'); + } + if (article.articlelist_id != null) { + throw new Error('A Stückliste article cannot have options'); + } + + // If options is empty: clear optionslist + if (options.length === 0) { + if (article.optionslist_id != null) { + await runner.query(queries.SET_ARTICLE_OPTIONSLIST_ID, { + articleId, + optionslistId: null, + }); + await runner.query(queries.DELETE_OPTIONSLIST, { + id: article.optionslist_id, + }); + } + return []; + } + + // Validate options + const validated = []; + const seenArticleIds = new Set(); + const seenSorts = new Set(); + + for (let i = 0; i < options.length; i++) { + const item = options[i]; + let itemArticleId = item?.articleId != null ? Number(item.articleId) : null; + + if (itemArticleId == null && item?.sku) { + const res = await runner.query( + 'SELECT id FROM article WHERE sku = @itemSku AND company_id = @companyId', + { itemSku: item.sku, companyId }, + ); + itemArticleId = res.rows[0]?.id; + } + + if (!itemArticleId || Number.isNaN(itemArticleId) || itemArticleId <= 0) { + throw new Error(`Option at index ${i} must specify a valid articleId or sku`); + } + + if (itemArticleId === articleId) { + throw new Error('An article cannot have itself as an option'); + } + + if (seenArticleIds.has(itemArticleId)) { + throw new Error(`Duplicate article in options: ${itemArticleId}`); + } + seenArticleIds.add(itemArticleId); + + // Verify component article exists in same company + const itemArtRes = await runner.query( + 'SELECT id FROM article WHERE id = @itemArticleId AND company_id = @companyId', + { itemArticleId, companyId }, + ); + if (!itemArtRes.rows[0]) { + throw new Error(`Article not found for option: ${itemArticleId}`); + } + + const count = item?.count != null && Number(item.count) > 0 ? Number(item.count) : 1; + let sort = item?.sort != null && Number.isInteger(Number(item.sort)) ? Number(item.sort) : i; + + while (seenSorts.has(sort)) { + sort++; + } + seenSorts.add(sort); + + const mandated = Boolean(item?.mandated); + + validated.push({ + articleId: itemArticleId, + count, + sort, + mandated, + }); + } + + let targetListId = article.optionslist_id; + if (targetListId == null) { + const listRes = await runner.query(queries.CREATE_OPTIONSLIST); + targetListId = listRes.rows[0]?.id ?? listRes.insertId; + await runner.query(queries.SET_ARTICLE_OPTIONSLIST_ID, { + articleId, + optionslistId: targetListId, + }); + } else { + await runner.query(queries.DELETE_OPTIONSLIST_ARTICLES, { + optionslistId: targetListId, + }); + } + + for (const val of validated) { + await runner.query(queries.INSERT_OPTIONSLIST_ARTICLE, { + optionslistId: targetListId, + articleId: val.articleId, + count: val.count, + sort: val.sort, + mandated: val.mandated ? 1 : 0, + }); + } + + return getOptions(googleAuthId, sku, runner); + }; + + if (tx) { + return run(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(run); + } + return run(makeRunner(provider)); + }; +}