This commit is contained in:
seb
2026-09-30 22:37:40 +02:00
parent 3d2f8e9296
commit 74ed74fc68
19 changed files with 122 additions and 56 deletions

View File

@@ -140,8 +140,10 @@ SELECT
av.value AS attributeValue av.value AS attributeValue
FROM article child FROM article child
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = @languageId 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 (
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = @attributeId 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 WHERE child.fatherarticle_id = @fatherArticleId
ORDER BY child.fathersort ASC, child.id ASC ORDER BY child.fathersort ASC, child.id ASC
`; `;
@@ -499,8 +501,10 @@ SELECT
av.value AS attributeValue av.value AS attributeValue
FROM article child FROM article child
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId 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 (
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id 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 WHERE child.fatherarticle_id = a.id
ORDER BY child.fathersort ASC, child.id ASC ORDER BY child.fathersort ASC, child.id ASC
FOR JSON PATH FOR JSON PATH

View File

@@ -4,6 +4,12 @@ FROM article
WHERE fatherarticle_id = @articleId WHERE fatherarticle_id = @articleId
`; `;
export const GET_CHILDREN_IDS = `
SELECT id
FROM article
WHERE fatherarticle_id = @id
`;
export const GET_LIST_IDS = ` export const GET_LIST_IDS = `
SELECT articlelist_id AS articlelistId, optionslist_id AS optionslistId SELECT articlelist_id AS articlelistId, optionslist_id AS optionslistId
FROM article FROM article

View File

@@ -175,8 +175,10 @@ SELECT auth.*,
'attributeValue', av.value 'attributeValue', av.value
)), '[]') FROM article child )), '[]') FROM article child
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId 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 (
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id 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 WHERE child.fatherarticle_id = a.id
ORDER BY child.fathersort ASC, child.id ASC)), ORDER BY child.fathersort ASC, child.id ASC)),
'categoryIdsJson', json(${JSON_CATEGORIES}), 'categoryIdsJson', json(${JSON_CATEGORIES}),

View File

@@ -4,7 +4,7 @@ import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'articleDelete'); const queries = translateQueries(source, 'articleDelete');
export const { export const {
DELETE_ARTICLELIST, DELETE_BY_ID, DELETE_OPTIONSLIST, GET_CHILD_COUNT, DELETE_ARTICLELIST, DELETE_BY_ID, DELETE_OPTIONSLIST, GET_CHILD_COUNT,
GET_LIST_IDS, GET_CHILDREN_IDS, GET_LIST_IDS,
} = queries; } = queries;
export const DELETE_PICS_BY_ARTICLE = ` export const DELETE_PICS_BY_ARTICLE = `

View File

@@ -78,8 +78,11 @@ export default function addChild(provider) {
let finalValue = null; let finalValue = null;
if (options.attributeValueId != 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, { const valRes = await runner.query(attrValQueries.GET_BY_ID_AND_COMPANY, {
id: Number(options.attributeValueId), id: options.attributeValueId,
companyId, companyId,
}); });
const row = valRes.rows[0]; const row = valRes.rows[0];

View File

@@ -9,43 +9,71 @@ export default function del(provider) {
const queries = isSqlite ? sqliteQueries : mssqlQueries; 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 {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU. * @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<void>} * @returns {Promise<void>}
* @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 * @example
* await store.article.delete(42, 'SKU-1'); * 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 runDelete = async (runner) => {
const { articleId } = await resolve(googleAuthId, sku, 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 }); const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
if (childCount.rows[0].count > 0) { 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 }); await deleteArticleAndLists(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 });
}
}; };
if (tx) { if (tx) {

View File

@@ -22,6 +22,7 @@ export default function removeChild(provider) {
* @param {string} childSku - The child article SKU. * @param {string} childSku - The child article SKU.
* @param {object} [options] * @param {object} [options]
* @param {boolean} [options.deleteArticle=false] - Whether to delete the child article entity entirely. * @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. * @param {object} [tx] - Optional transaction.
* @returns {Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>} * @returns {Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>}
*/ */
@@ -45,7 +46,8 @@ export default function removeChild(provider) {
} }
if (options.deleteArticle) { if (options.deleteArticle) {
await del(googleAuthId, childSku, runner); const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true;
await del(googleAuthId, childSku, { cascade }, runner);
return { return {
success: true, success: true,
fatherSku, fatherSku,

View File

@@ -40,8 +40,8 @@ export default function setBrandIdBySku(provider) {
runner, runner,
); );
if (brandId !== null && !Number.isFinite(brandId)) { if (brandId !== null && (typeof brandId !== 'number' || !Number.isInteger(brandId) || brandId <= 0)) {
throw new Error('Brand not found'); throw new Error('Invalid brandId');
} }
let resolvedBrandId = null; let resolvedBrandId = null;

View File

@@ -6,6 +6,7 @@ import {
ADD_EAN as SQLITE_ADD_EAN, ADD_EAN as SQLITE_ADD_EAN,
DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS, DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS,
} from '../../providers/sqlite3/queries/article.js'; } from '../../providers/sqlite3/queries/article.js';
import { isUniqueConstraintError } from '../../providers/provider.js';
import queryBySku from './queryBySku.js'; import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
@@ -55,8 +56,15 @@ export default function setEansBySku(provider) {
await runner.query(deleteEansSql, { articleId }); await runner.query(deleteEansSql, { articleId });
for (const ean of normalized) { try {
await runner.query(addEanSql, { articleId, ean }); 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); const article = await load(articleId, languageId, runner);

View File

@@ -27,10 +27,11 @@ function normalizePrices(prices) {
? 1 ? 1
: entry.minmultiplier; : entry.minmultiplier;
if (!Number.isFinite(price) || !Number.isFinite(tax) || !Number.isFinite(minmultiplier)) { if (
throw new Error('Invalid prices'); typeof price !== 'number' || !Number.isFinite(price) || price < 0 ||
} typeof tax !== 'number' || !Number.isFinite(tax) || tax < 0 ||
if (minmultiplier <= 0) { typeof minmultiplier !== 'number' || !Number.isFinite(minmultiplier) || minmultiplier <= 0
) {
throw new Error('Invalid prices'); throw new Error('Invalid prices');
} }

View File

@@ -53,8 +53,11 @@ export async function assertSupplier(runnerOrPool, txOrCompanyId, maybeCompanyId
supplierId = maybeCompanyId; supplierId = maybeCompanyId;
} }
if (supplierId == null || !Number.isFinite(supplierId)) { if (supplierId == null) {
throw new Error('Supplier not found'); 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'; const isSqlite = runner?.dialect === 'sqlite3';

View File

@@ -10,8 +10,8 @@ export default function resolveCustomerGroupAccess(provider) {
: MSSQL_GET_BY_COMPANY_AND_ID; : MSSQL_GET_BY_COMPANY_AND_ID;
return async (companyId, customergroupId, tx) => { return async (companyId, customergroupId, tx) => {
if (!Number.isFinite(customergroupId)) { if (typeof customergroupId !== 'number' || !Number.isInteger(customergroupId) || customergroupId <= 0) {
throw new Error('Customer group not found'); throw new Error('Invalid customergroupId');
} }
const result = await makeRunner(provider, tx).query(sql, { const result = await makeRunner(provider, tx).query(sql, {

View File

@@ -6,8 +6,8 @@ export async function assertSupplier(provider, tx, companyId, supplierId) {
if (supplierId == null) { if (supplierId == null) {
return null; return null;
} }
if (!Number.isFinite(supplierId)) { if (typeof supplierId !== 'number' || !Number.isInteger(supplierId) || supplierId <= 0) {
throw new Error('Supplier not found'); throw new Error('Invalid supplierId');
} }
const isSqlite = provider?.dialect === 'sqlite3'; const isSqlite = provider?.dialect === 'sqlite3';

View File

@@ -47,8 +47,8 @@ export async function assertCustomerEntity(runnerOrPool, txOrCompanyId, maybeCom
if (entityId == null) { if (entityId == null) {
return null; return null;
} }
if (!Number.isFinite(entityId)) { if (typeof entityId !== 'number' || !Number.isInteger(entityId) || entityId <= 0) {
throw new Error('Entity not found'); throw new Error('Invalid entityId');
} }
const isSqlite = runner?.dialect === 'sqlite3'; const isSqlite = runner?.dialect === 'sqlite3';

View File

@@ -36,11 +36,11 @@ export function normalizeLines(lines) {
: String(entry.comment); : String(entry.comment);
if ( if (
!Number.isFinite(pu) || typeof pu !== 'number' || !Number.isFinite(pu) || pu <= 0 ||
!Number.isFinite(amount) || typeof amount !== 'number' || !Number.isFinite(amount) ||
!Number.isFinite(net) || typeof net !== 'number' || !Number.isFinite(net) ||
!Number.isFinite(rebate) || typeof rebate !== 'number' || !Number.isFinite(rebate) ||
!Number.isFinite(taxrate) typeof taxrate !== 'number' || !Number.isFinite(taxrate) || taxrate < 0
) { ) {
throw new Error('Invalid lines'); throw new Error('Invalid lines');
} }

View File

@@ -82,18 +82,18 @@ export default function adjust(provider) {
if (rawWhId == null) { if (rawWhId == null) {
throw new Error(`Warehouse required${itemLabel} (${sku})`); throw new Error(`Warehouse required${itemLabel} (${sku})`);
} }
const warehouseId = Number(rawWhId); if (typeof rawWhId !== 'number' || !Number.isInteger(rawWhId) || rawWhId <= 0) {
if (!Number.isInteger(warehouseId) || warehouseId <= 0) {
throw new Error(`Warehouse must be a positive integer${itemLabel} (${sku})`); throw new Error(`Warehouse must be a positive integer${itemLabel} (${sku})`);
} }
const warehouseId = rawWhId;
if (line.amount == null) { if (line.amount == null) {
throw new Error(`Amount required${itemLabel} (${sku})`); throw new Error(`Amount required${itemLabel} (${sku})`);
} }
const amount = Number(line.amount); if (typeof line.amount !== 'number' || !Number.isFinite(line.amount) || line.amount === 0) {
if (!Number.isFinite(amount) || amount === 0) {
throw new Error(`Invalid amount${itemLabel} (${sku}): must be non-zero number`); 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?.() || const text = (line.text ?? line.transactionText ?? line.transaction_text ?? defaultText)?.trim?.() ||
(amount > 0 ? `Manual stock addition (+${amount})` : `Manual stock removal (${amount})`); (amount > 0 ? `Manual stock addition (+${amount})` : `Manual stock removal (${amount})`);

View File

@@ -16,6 +16,9 @@ export default function create(provider) {
* // => 1 * // => 1
*/ */
return async (rate, tx) => { 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 }); const result = await makeRunner(provider, tx).query(sql, { rate });
return result.rows[0]?.id ?? result.insertId; return result.rows[0]?.id ?? result.insertId;
}; };

View File

@@ -16,6 +16,9 @@ export default function ensure(provider) {
* // => 1 * // => 1
*/ */
return async (rate, tx) => { 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); const row = await get(rate, tx);
if (row) return row.id; if (row) return row.id;
return add(rate, tx); return add(rate, tx);

View File

@@ -16,6 +16,9 @@ export default function getByRate(provider) {
* // => { id: 1, rate: 19 } * // => { id: 1, rate: 19 }
*/ */
return async (rate, tx) => { 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 }); const result = await makeRunner(provider, tx).query(sql, { rate });
return result.rows[0]; return result.rows[0];
}; };