From 74ed74fc680b2ef92c5ade84ef5ee1e171ede6de Mon Sep 17 00:00:00 2001 From: seb Date: Wed, 30 Sep 2026 22:37:40 +0200 Subject: [PATCH] u --- src/providers/mssql/queries/article.js | 12 ++-- src/providers/mssql/queries/articleDelete.js | 6 ++ src/providers/sqlite3/queries/article.js | 6 +- .../sqlite3/queries/articleDelete.js | 2 +- src/store/article/addChild.js | 5 +- src/store/article/delete.js | 72 +++++++++++++------ src/store/article/removeChild.js | 4 +- src/store/article/setBrandIdBySku.js | 4 +- src/store/article/setEansBySku.js | 12 +++- src/store/article/setPricesBySku.js | 9 +-- src/store/articlesupplier/helpers.js | 7 +- .../resolveCustomerGroupAccess.js | 4 +- src/store/delivery/writeLines.js | 4 +- src/store/manifestDoc/helpers.js | 4 +- src/store/manifestLines.js | 10 +-- src/store/stock/adjust.js | 8 +-- src/store/tax/create.js | 3 + src/store/tax/ensure.js | 3 + src/store/tax/getByRate.js | 3 + 19 files changed, 122 insertions(+), 56 deletions(-) diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index b4ea674..b4d2368 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -140,8 +140,10 @@ SELECT av.value AS attributeValue FROM article child LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = @languageId -LEFT JOIN articleattributevalue aav ON aav.article_id = child.id -LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = @attributeId +LEFT JOIN ( + articleattributevalue aav + INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = @attributeId +) ON aav.article_id = child.id WHERE child.fatherarticle_id = @fatherArticleId ORDER BY child.fathersort ASC, child.id ASC `; @@ -499,8 +501,10 @@ SELECT av.value AS attributeValue FROM article child LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId - LEFT JOIN articleattributevalue aav ON aav.article_id = child.id - LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id + LEFT JOIN ( + articleattributevalue aav + INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id + ) ON aav.article_id = child.id WHERE child.fatherarticle_id = a.id ORDER BY child.fathersort ASC, child.id ASC FOR JSON PATH diff --git a/src/providers/mssql/queries/articleDelete.js b/src/providers/mssql/queries/articleDelete.js index 7bb01c5..5d63ed1 100644 --- a/src/providers/mssql/queries/articleDelete.js +++ b/src/providers/mssql/queries/articleDelete.js @@ -4,6 +4,12 @@ FROM article WHERE fatherarticle_id = @articleId `; +export const GET_CHILDREN_IDS = ` +SELECT id +FROM article +WHERE fatherarticle_id = @id +`; + export const GET_LIST_IDS = ` SELECT articlelist_id AS articlelistId, optionslist_id AS optionslistId FROM article diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index 4144124..27e301a 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -175,8 +175,10 @@ SELECT auth.*, 'attributeValue', av.value )), '[]') FROM article child LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId - LEFT JOIN articleattributevalue aav ON aav.article_id = child.id - LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id + LEFT JOIN ( + articleattributevalue aav + INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id + ) ON aav.article_id = child.id WHERE child.fatherarticle_id = a.id ORDER BY child.fathersort ASC, child.id ASC)), 'categoryIdsJson', json(${JSON_CATEGORIES}), diff --git a/src/providers/sqlite3/queries/articleDelete.js b/src/providers/sqlite3/queries/articleDelete.js index 15e4507..11cc3a0 100644 --- a/src/providers/sqlite3/queries/articleDelete.js +++ b/src/providers/sqlite3/queries/articleDelete.js @@ -4,7 +4,7 @@ import { translateQueries } from './translate.js'; const queries = translateQueries(source, 'articleDelete'); export const { DELETE_ARTICLELIST, DELETE_BY_ID, DELETE_OPTIONSLIST, GET_CHILD_COUNT, - GET_LIST_IDS, + GET_CHILDREN_IDS, GET_LIST_IDS, } = queries; export const DELETE_PICS_BY_ARTICLE = ` diff --git a/src/store/article/addChild.js b/src/store/article/addChild.js index f625e0e..2b52a9c 100644 --- a/src/store/article/addChild.js +++ b/src/store/article/addChild.js @@ -78,8 +78,11 @@ export default function addChild(provider) { let finalValue = null; if (options.attributeValueId != null) { + if (typeof options.attributeValueId !== 'number' || !Number.isInteger(options.attributeValueId) || options.attributeValueId <= 0) { + throw new Error('Invalid attributeValueId'); + } const valRes = await runner.query(attrValQueries.GET_BY_ID_AND_COMPANY, { - id: Number(options.attributeValueId), + id: options.attributeValueId, companyId, }); const row = valRes.rows[0]; diff --git a/src/store/article/delete.js b/src/store/article/delete.js index e1fe4ed..b4e8c48 100644 --- a/src/store/article/delete.js +++ b/src/store/article/delete.js @@ -9,43 +9,71 @@ export default function del(provider) { const queries = isSqlite ? sqliteQueries : mssqlQueries; /** - * Deletes an article by its SKU, refusing when it still has children. + * Deletes an article by its SKU, refusing when it still has children unless cascade is true. * * @param {number} googleAuthId - The id of the Google auth user. * @param {string} sku - The article SKU. - * @param {object} [tx] - Optional transaction to run within. + * @param {object|*} [optionsOrTx] - Options object ({ cascade?: boolean }) or optional transaction. + * @param {object} [maybeTx] - Optional transaction if options was passed. * @returns {Promise} - * @throws {Error} If the article has children or is not found. + * @throws {Error} If the article has children (when cascade=false) or is not found. * * @example * await store.article.delete(42, 'SKU-1'); + * await store.article.delete(42, 'SKU-1', { cascade: true }); */ - return async (googleAuthId, sku, tx) => { + return async (googleAuthId, sku, optionsOrTx, maybeTx) => { + let options = {}; + let tx; + if (optionsOrTx && typeof optionsOrTx === 'object' && typeof optionsOrTx.query !== 'function') { + options = optionsOrTx; + tx = maybeTx; + } else { + tx = optionsOrTx; + } + const cascade = Boolean(options.cascade); + const runDelete = async (runner) => { const { articleId } = await resolve(googleAuthId, sku, runner); + const deleteArticleAndLists = async (id) => { + const listIds = await runner.query(queries.GET_LIST_IDS, { articleId: id }); + const { articlelistId, optionslistId } = listIds.rows[0] ?? {}; + + await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id }); + + const deleted = await runner.query(queries.DELETE_BY_ID, { articleId: id }); + if (!deleted.rowsAffected) { + throw new Error('Article not found'); + } + + if (articlelistId != null) { + await runner.query(queries.DELETE_ARTICLELIST, { id: articlelistId }); + } + + if (optionslistId != null) { + await runner.query(queries.DELETE_OPTIONSLIST, { id: optionslistId }); + } + }; + + const deleteSubtree = async (id) => { + const children = await runner.query(queries.GET_CHILDREN_IDS, { id }); + for (const child of children.rows) { + await deleteSubtree(child.id); + } + await deleteArticleAndLists(id); + }; + const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId }); if (childCount.rows[0].count > 0) { - throw new Error('Article has children'); + if (!cascade) { + throw new Error('Article has children'); + } + await deleteSubtree(articleId); + return; } - const listIds = await runner.query(queries.GET_LIST_IDS, { articleId }); - const { articlelistId, optionslistId } = listIds.rows[0] ?? {}; - - await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId }); - - const deleted = await runner.query(queries.DELETE_BY_ID, { articleId }); - if (!deleted.rowsAffected) { - throw new Error('Article not found'); - } - - if (articlelistId != null) { - await runner.query(queries.DELETE_ARTICLELIST, { id: articlelistId }); - } - - if (optionslistId != null) { - await runner.query(queries.DELETE_OPTIONSLIST, { id: optionslistId }); - } + await deleteArticleAndLists(articleId); }; if (tx) { diff --git a/src/store/article/removeChild.js b/src/store/article/removeChild.js index 83c5d72..5913e12 100644 --- a/src/store/article/removeChild.js +++ b/src/store/article/removeChild.js @@ -22,6 +22,7 @@ export default function removeChild(provider) { * @param {string} childSku - The child article SKU. * @param {object} [options] * @param {boolean} [options.deleteArticle=false] - Whether to delete the child article entity entirely. + * @param {boolean} [options.cascade=true] - Whether to cascade delete child subtrees when deleteArticle is true. * @param {object} [tx] - Optional transaction. * @returns {Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>} */ @@ -45,7 +46,8 @@ export default function removeChild(provider) { } if (options.deleteArticle) { - await del(googleAuthId, childSku, runner); + const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true; + await del(googleAuthId, childSku, { cascade }, runner); return { success: true, fatherSku, diff --git a/src/store/article/setBrandIdBySku.js b/src/store/article/setBrandIdBySku.js index 1a21bb6..6ac1ec9 100644 --- a/src/store/article/setBrandIdBySku.js +++ b/src/store/article/setBrandIdBySku.js @@ -40,8 +40,8 @@ export default function setBrandIdBySku(provider) { runner, ); - if (brandId !== null && !Number.isFinite(brandId)) { - throw new Error('Brand not found'); + if (brandId !== null && (typeof brandId !== 'number' || !Number.isInteger(brandId) || brandId <= 0)) { + throw new Error('Invalid brandId'); } let resolvedBrandId = null; diff --git a/src/store/article/setEansBySku.js b/src/store/article/setEansBySku.js index ec21c1c..26bee3d 100644 --- a/src/store/article/setEansBySku.js +++ b/src/store/article/setEansBySku.js @@ -6,6 +6,7 @@ import { ADD_EAN as SQLITE_ADD_EAN, DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS, } from '../../providers/sqlite3/queries/article.js'; +import { isUniqueConstraintError } from '../../providers/provider.js'; import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; import makeRunner from '../runner.js'; @@ -55,8 +56,15 @@ export default function setEansBySku(provider) { await runner.query(deleteEansSql, { articleId }); - for (const ean of normalized) { - await runner.query(addEanSql, { articleId, ean }); + try { + for (const ean of normalized) { + await runner.query(addEanSql, { articleId, ean }); + } + } catch (err) { + if (isUniqueConstraintError(err)) { + throw new Error('EAN already in use'); + } + throw err; } const article = await load(articleId, languageId, runner); diff --git a/src/store/article/setPricesBySku.js b/src/store/article/setPricesBySku.js index cb7ffef..78056b9 100644 --- a/src/store/article/setPricesBySku.js +++ b/src/store/article/setPricesBySku.js @@ -27,10 +27,11 @@ function normalizePrices(prices) { ? 1 : entry.minmultiplier; - if (!Number.isFinite(price) || !Number.isFinite(tax) || !Number.isFinite(minmultiplier)) { - throw new Error('Invalid prices'); - } - if (minmultiplier <= 0) { + if ( + typeof price !== 'number' || !Number.isFinite(price) || price < 0 || + typeof tax !== 'number' || !Number.isFinite(tax) || tax < 0 || + typeof minmultiplier !== 'number' || !Number.isFinite(minmultiplier) || minmultiplier <= 0 + ) { throw new Error('Invalid prices'); } diff --git a/src/store/articlesupplier/helpers.js b/src/store/articlesupplier/helpers.js index 628d042..58c022a 100644 --- a/src/store/articlesupplier/helpers.js +++ b/src/store/articlesupplier/helpers.js @@ -53,8 +53,11 @@ export async function assertSupplier(runnerOrPool, txOrCompanyId, maybeCompanyId supplierId = maybeCompanyId; } - if (supplierId == null || !Number.isFinite(supplierId)) { - throw new Error('Supplier not found'); + if (supplierId == null) { + throw new Error('supplierId is required'); + } + if (typeof supplierId !== 'number' || !Number.isInteger(supplierId) || supplierId <= 0) { + throw new Error('Invalid supplierId'); } const isSqlite = runner?.dialect === 'sqlite3'; diff --git a/src/store/customergroup/resolveCustomerGroupAccess.js b/src/store/customergroup/resolveCustomerGroupAccess.js index 9b6709b..206d601 100644 --- a/src/store/customergroup/resolveCustomerGroupAccess.js +++ b/src/store/customergroup/resolveCustomerGroupAccess.js @@ -10,8 +10,8 @@ export default function resolveCustomerGroupAccess(provider) { : MSSQL_GET_BY_COMPANY_AND_ID; return async (companyId, customergroupId, tx) => { - if (!Number.isFinite(customergroupId)) { - throw new Error('Customer group not found'); + if (typeof customergroupId !== 'number' || !Number.isInteger(customergroupId) || customergroupId <= 0) { + throw new Error('Invalid customergroupId'); } const result = await makeRunner(provider, tx).query(sql, { diff --git a/src/store/delivery/writeLines.js b/src/store/delivery/writeLines.js index ad17a4e..814bfd6 100644 --- a/src/store/delivery/writeLines.js +++ b/src/store/delivery/writeLines.js @@ -6,8 +6,8 @@ export async function assertSupplier(provider, tx, companyId, supplierId) { if (supplierId == null) { return null; } - if (!Number.isFinite(supplierId)) { - throw new Error('Supplier not found'); + if (typeof supplierId !== 'number' || !Number.isInteger(supplierId) || supplierId <= 0) { + throw new Error('Invalid supplierId'); } const isSqlite = provider?.dialect === 'sqlite3'; diff --git a/src/store/manifestDoc/helpers.js b/src/store/manifestDoc/helpers.js index efd5b85..2aaa909 100644 --- a/src/store/manifestDoc/helpers.js +++ b/src/store/manifestDoc/helpers.js @@ -47,8 +47,8 @@ export async function assertCustomerEntity(runnerOrPool, txOrCompanyId, maybeCom if (entityId == null) { return null; } - if (!Number.isFinite(entityId)) { - throw new Error('Entity not found'); + if (typeof entityId !== 'number' || !Number.isInteger(entityId) || entityId <= 0) { + throw new Error('Invalid entityId'); } const isSqlite = runner?.dialect === 'sqlite3'; diff --git a/src/store/manifestLines.js b/src/store/manifestLines.js index c13e7a2..b1456bb 100644 --- a/src/store/manifestLines.js +++ b/src/store/manifestLines.js @@ -36,11 +36,11 @@ export function normalizeLines(lines) { : String(entry.comment); if ( - !Number.isFinite(pu) || - !Number.isFinite(amount) || - !Number.isFinite(net) || - !Number.isFinite(rebate) || - !Number.isFinite(taxrate) + typeof pu !== 'number' || !Number.isFinite(pu) || pu <= 0 || + typeof amount !== 'number' || !Number.isFinite(amount) || + typeof net !== 'number' || !Number.isFinite(net) || + typeof rebate !== 'number' || !Number.isFinite(rebate) || + typeof taxrate !== 'number' || !Number.isFinite(taxrate) || taxrate < 0 ) { throw new Error('Invalid lines'); } diff --git a/src/store/stock/adjust.js b/src/store/stock/adjust.js index c9c0f28..69e8d70 100644 --- a/src/store/stock/adjust.js +++ b/src/store/stock/adjust.js @@ -82,18 +82,18 @@ export default function adjust(provider) { if (rawWhId == null) { throw new Error(`Warehouse required${itemLabel} (${sku})`); } - const warehouseId = Number(rawWhId); - if (!Number.isInteger(warehouseId) || warehouseId <= 0) { + if (typeof rawWhId !== 'number' || !Number.isInteger(rawWhId) || rawWhId <= 0) { throw new Error(`Warehouse must be a positive integer${itemLabel} (${sku})`); } + const warehouseId = rawWhId; if (line.amount == null) { throw new Error(`Amount required${itemLabel} (${sku})`); } - const amount = Number(line.amount); - if (!Number.isFinite(amount) || amount === 0) { + if (typeof line.amount !== 'number' || !Number.isFinite(line.amount) || line.amount === 0) { throw new Error(`Invalid amount${itemLabel} (${sku}): must be non-zero number`); } + const amount = line.amount; const text = (line.text ?? line.transactionText ?? line.transaction_text ?? defaultText)?.trim?.() || (amount > 0 ? `Manual stock addition (+${amount})` : `Manual stock removal (${amount})`); diff --git a/src/store/tax/create.js b/src/store/tax/create.js index e9fe6f4..716a362 100644 --- a/src/store/tax/create.js +++ b/src/store/tax/create.js @@ -16,6 +16,9 @@ export default function create(provider) { * // => 1 */ return async (rate, tx) => { + if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0) { + throw new Error('Invalid tax rate'); + } const result = await makeRunner(provider, tx).query(sql, { rate }); return result.rows[0]?.id ?? result.insertId; }; diff --git a/src/store/tax/ensure.js b/src/store/tax/ensure.js index 3d2a6ef..79d3fdb 100644 --- a/src/store/tax/ensure.js +++ b/src/store/tax/ensure.js @@ -16,6 +16,9 @@ export default function ensure(provider) { * // => 1 */ return async (rate, tx) => { + if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0) { + throw new Error('Invalid tax rate'); + } const row = await get(rate, tx); if (row) return row.id; return add(rate, tx); diff --git a/src/store/tax/getByRate.js b/src/store/tax/getByRate.js index 02ae930..469aa13 100644 --- a/src/store/tax/getByRate.js +++ b/src/store/tax/getByRate.js @@ -16,6 +16,9 @@ export default function getByRate(provider) { * // => { id: 1, rate: 19 } */ return async (rate, tx) => { + if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0) { + throw new Error('Invalid tax rate'); + } const result = await makeRunner(provider, tx).query(sql, { rate }); return result.rows[0]; };