From 36dd81ee69ff87f438c1d661d66b042b8ce4a86b Mon Sep 17 00:00:00 2001 From: seb Date: Wed, 7 Oct 2026 21:23:03 +0200 Subject: [PATCH] u --- API.article.md | 6 ++- API.md | 2 +- src/store/article/addImage.js | 76 ++++++++++++++++++----------- src/store/article/setPricesBySku.js | 25 +++++++--- src/store/brand/addImage.js | 56 ++++++++++++--------- src/store/category/addImage.js | 71 +++++++++++++++++---------- src/util/newFullHash.js | 25 ++++++++-- 7 files changed, 170 insertions(+), 91 deletions(-) diff --git a/API.article.md b/API.article.md index c86ab17..9322122 100644 --- a/API.article.md +++ b/API.article.md @@ -548,7 +548,7 @@ Replaces the price list of an article by its SKU. | --- | --- | --- | | `googleAuthId` | `number` | The id of the Google auth user. | | `sku` | `string` | The article SKU. | -| `prices` | `Array<{price: number, tax: number, minmultiplier?: number}>` | The price entries (minmultiplier defaults to 1). | +| `prices` | `Array<{net?: number, gross?: number, price?: number, taxRate?: number, tax?: number, minmultiplier?: number}>` | The price entries (net or gross, taxRate or tax percentage, minmultiplier defaults to 1). | | `[tx]` | `object` | Optional transaction to run within. | **Returns** @@ -562,7 +562,9 @@ Replaces the price list of an article by its SKU. **Example** ```js -const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]); +const article = await store.article.setPricesBySku(42, 'SKU-1', [{ net: 10.0, taxRate: 19, minmultiplier: 1 }]); +// or with gross: +const article = await store.article.setPricesBySku(42, 'SKU-1', [{ gross: 11.9, taxRate: 19 }]); // => { id, sku, name, ... } ``` diff --git a/API.md b/API.md index 28ff6f5..f5199b1 100644 --- a/API.md +++ b/API.md @@ -105,7 +105,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `setCategoryIdsBySku(googleAuthId: number, sku: string, categoryIds: Array, tx?: object): Promise` - `setEansBySku(googleAuthId: number, sku: string, eans: Array, tx?: object): Promise` - `setParentBySku(googleAuthId: number, sku: string, parentSku: string|null, tx?: object): Promise` - - `setPricesBySku(googleAuthId: number, sku: string, prices: Array<{price: number, tax: number, minmultiplier?: number}>, tx?: object): Promise` + - `setPricesBySku(googleAuthId: number, sku: string, prices: Array<{net?: number, gross?: number, price?: number, taxRate?: number, tax?: number, minmultiplier?: number}>, tx?: object): Promise` - `setTrackStockBySku(googleAuthId: number, sku: string, trackStock: boolean|number, tx?: object): Promise` - `setWeightBySku(googleAuthId: number, sku: string, weight: number, tx?: object): Promise` - `toggleParent(googleAuthId: number, sku: string, options: {isParent: boolean, attributeNameId?: number, parentAttributeNameId?: number}, tx?: object): Promise` diff --git a/src/store/article/addImage.js b/src/store/article/addImage.js index ade2626..7d67713 100644 --- a/src/store/article/addImage.js +++ b/src/store/article/addImage.js @@ -42,42 +42,62 @@ export default function addImage(provider) { const sortResult = await runner.query(getSortSql, { articleId }); const sort = sortResult.rows[0].sort; - const fullHash = newFullHash(); + const fullHash = newFullHash(processed.full.buffer); - if (isSqlite) { - if (runner.writePicFull) { - await runner.writePicFull(fullHash, processed.full.buffer); - await runner.writePicThumb(fullHash, processed.thumb.buffer); - } else if (provider?.writePicFull) { - await provider.writePicFull(fullHash, processed.full.buffer); - await provider.writePicThumb(fullHash, processed.thumb.buffer); + let picId; + const existingPic = await runner.query( + 'SELECT id FROM pic WHERE hash = @fullHash', + { fullHash }, + ); + + if (existingPic.rows[0]) { + picId = existingPic.rows[0].id; + } else { + if (isSqlite) { + if (runner.writePicFull) { + await runner.writePicFull(fullHash, processed.full.buffer); + await runner.writePicThumb(fullHash, processed.thumb.buffer); + } else if (provider?.writePicFull) { + await provider.writePicFull(fullHash, processed.full.buffer); + await provider.writePicThumb(fullHash, processed.thumb.buffer); + } } + + const picResult = await runner.query(createPicSql, { + w: processed.full.w, + h: processed.full.h, + size: processed.full.size, + hash: fullHash, + thumbw: processed.thumb.w, + thumbh: processed.thumb.h, + thumbhash: processed.thumb.hash, + source: '', + fullbinary: processed.full.buffer, + thumbbinary: processed.thumb.buffer, + }); + + picId = picResult.rows[0]?.id ?? picResult.insertId; } - const picResult = await runner.query(createPicSql, { - w: processed.full.w, - h: processed.full.h, - size: processed.full.size, - hash: fullHash, - thumbw: processed.thumb.w, - thumbh: processed.thumb.h, - thumbhash: processed.thumb.hash, - source: '', - fullbinary: processed.full.buffer, - thumbbinary: processed.thumb.buffer, - }); + const existingArticlePic = await runner.query( + 'SELECT id, sort FROM articlepic WHERE article_id = @articleId AND pic_id = @picId', + { articleId, picId }, + ); - const picId = picResult.rows[0]?.id ?? picResult.insertId; - - await runner.query(addArticlePicSql, { - articleId, - picId, - sort, - }); + let effectiveSort = sort; + if (existingArticlePic.rows[0]) { + effectiveSort = existingArticlePic.rows[0].sort; + } else { + await runner.query(addArticlePicSql, { + articleId, + picId, + sort, + }); + } return { picId, - sort, + sort: effectiveSort, full: { w: processed.full.w, h: processed.full.h, diff --git a/src/store/article/setPricesBySku.js b/src/store/article/setPricesBySku.js index c605872..d5c7320 100644 --- a/src/store/article/setPricesBySku.js +++ b/src/store/article/setPricesBySku.js @@ -22,28 +22,37 @@ function normalizePrices(prices) { const normalized = []; for (const entry of prices) { - const price = entry?.price; - const tax = entry?.tax; + const rawTax = entry?.taxRate ?? entry?.tax; const minmultiplier = entry?.minmultiplier === undefined || entry?.minmultiplier === null ? 1 : entry.minmultiplier; if ( - typeof price !== 'number' || !Number.isFinite(price) || price < 0 || - typeof tax !== 'number' || !Number.isFinite(tax) || tax < 0 || + typeof rawTax !== 'number' || !Number.isFinite(rawTax) || rawTax < 0 || typeof minmultiplier !== 'number' || !Number.isFinite(minmultiplier) || minmultiplier <= 0 ) { throw new Error('Invalid prices'); } + let netPrice; + if (typeof entry?.net === 'number' && Number.isFinite(entry.net) && entry.net >= 0) { + netPrice = entry.net; + } else if (typeof entry?.price === 'number' && Number.isFinite(entry.price) && entry.price >= 0) { + netPrice = entry.price; + } else if (typeof entry?.gross === 'number' && Number.isFinite(entry.gross) && entry.gross >= 0) { + netPrice = +Number(entry.gross / (1 + rawTax / 100)).toFixed(4); + } else { + throw new Error('Invalid prices'); + } + const key = String(minmultiplier); if (seen.has(key)) { throw new Error('Invalid prices'); } seen.add(key); - normalized.push({ price, tax, minmultiplier }); + normalized.push({ price: netPrice, tax: rawTax, minmultiplier }); } return normalized; @@ -64,13 +73,15 @@ export default function setPricesBySku(provider) { * * @param {number} googleAuthId - The id of the Google auth user. * @param {string} sku - The article SKU. - * @param {Array<{price: number, tax: number, minmultiplier?: number}>} prices - The price entries (minmultiplier defaults to 1). + * @param {Array<{net?: number, gross?: number, price?: number, taxRate?: number, tax?: number, minmultiplier?: number}>} prices - The price entries (net or gross, taxRate or tax percentage, minmultiplier defaults to 1). * @param {object} [tx] - Optional transaction to run within. * @returns {Promise} The updated article. * @throws {Error} If the prices are invalid or the article is not found. * * @example - * const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]); + * const article = await store.article.setPricesBySku(42, 'SKU-1', [{ net: 10.0, taxRate: 19, minmultiplier: 1 }]); + * // or with gross: + * const article = await store.article.setPricesBySku(42, 'SKU-1', [{ gross: 11.9, taxRate: 19 }]); * // => { id, sku, name, ... } */ return async (googleAuthId, sku, prices, tx) => { diff --git a/src/store/brand/addImage.js b/src/store/brand/addImage.js index 5c01e01..12f3fb0 100644 --- a/src/store/brand/addImage.js +++ b/src/store/brand/addImage.js @@ -40,33 +40,43 @@ export default function addImage(provider) { await runner.query(deleteBrandPicSql, { brandId }); const sort = 0; - const fullHash = newFullHash(); + const fullHash = newFullHash(processed.full.buffer); - if (isSqlite) { - if (runner.writePicFull) { - await runner.writePicFull(fullHash, processed.full.buffer); - await runner.writePicThumb(fullHash, processed.thumb.buffer); - } else if (provider?.writePicFull) { - await provider.writePicFull(fullHash, processed.full.buffer); - await provider.writePicThumb(fullHash, processed.thumb.buffer); + let picId; + const existingPic = await runner.query( + 'SELECT id FROM pic WHERE hash = @fullHash', + { fullHash }, + ); + + if (existingPic.rows[0]) { + picId = existingPic.rows[0].id; + } else { + if (isSqlite) { + if (runner.writePicFull) { + await runner.writePicFull(fullHash, processed.full.buffer); + await runner.writePicThumb(fullHash, processed.thumb.buffer); + } else if (provider?.writePicFull) { + await provider.writePicFull(fullHash, processed.full.buffer); + await provider.writePicThumb(fullHash, processed.thumb.buffer); + } } + + const picResult = await runner.query(createPicSql, { + w: processed.full.w, + h: processed.full.h, + size: processed.full.size, + hash: fullHash, + thumbw: processed.thumb.w, + thumbh: processed.thumb.h, + thumbhash: processed.thumb.hash, + source: '', + fullbinary: processed.full.buffer, + thumbbinary: processed.thumb.buffer, + }); + + picId = picResult.rows[0]?.id ?? picResult.insertId; } - const picResult = await runner.query(createPicSql, { - w: processed.full.w, - h: processed.full.h, - size: processed.full.size, - hash: fullHash, - thumbw: processed.thumb.w, - thumbh: processed.thumb.h, - thumbhash: processed.thumb.hash, - source: '', - fullbinary: processed.full.buffer, - thumbbinary: processed.thumb.buffer, - }); - - const picId = picResult.rows[0]?.id ?? picResult.insertId; - const brandPicResult = await runner.query(addBrandPicSql, { brandId, picId, diff --git a/src/store/category/addImage.js b/src/store/category/addImage.js index eaab9e3..1448751 100644 --- a/src/store/category/addImage.js +++ b/src/store/category/addImage.js @@ -42,38 +42,55 @@ export default function addImage(provider) { const sortResult = await runner.query(getSortSql, { categoryId }); const sort = sortResult.rows[0].sort; - const fullHash = newFullHash(); + const fullHash = newFullHash(processed.full.buffer); - if (isSqlite) { - if (runner.writePicFull) { - await runner.writePicFull(fullHash, processed.full.buffer); - await runner.writePicThumb(fullHash, processed.thumb.buffer); - } else if (provider?.writePicFull) { - await provider.writePicFull(fullHash, processed.full.buffer); - await provider.writePicThumb(fullHash, processed.thumb.buffer); + let picId; + const existingPic = await runner.query( + 'SELECT id FROM pic WHERE hash = @fullHash', + { fullHash }, + ); + + if (existingPic.rows[0]) { + picId = existingPic.rows[0].id; + } else { + if (isSqlite) { + if (runner.writePicFull) { + await runner.writePicFull(fullHash, processed.full.buffer); + await runner.writePicThumb(fullHash, processed.thumb.buffer); + } else if (provider?.writePicFull) { + await provider.writePicFull(fullHash, processed.full.buffer); + await provider.writePicThumb(fullHash, processed.thumb.buffer); + } } + + const picResult = await runner.query(createPicSql, { + w: processed.full.w, + h: processed.full.h, + size: processed.full.size, + hash: fullHash, + thumbw: processed.thumb.w, + thumbh: processed.thumb.h, + thumbhash: processed.thumb.hash, + source: '', + fullbinary: processed.full.buffer, + thumbbinary: processed.thumb.buffer, + }); + + picId = picResult.rows[0]?.id ?? picResult.insertId; } - const picResult = await runner.query(createPicSql, { - w: processed.full.w, - h: processed.full.h, - size: processed.full.size, - hash: fullHash, - thumbw: processed.thumb.w, - thumbh: processed.thumb.h, - thumbhash: processed.thumb.hash, - source: '', - fullbinary: processed.full.buffer, - thumbbinary: processed.thumb.buffer, - }); + const existingCategoryPic = await runner.query( + 'SELECT id, sort FROM categorypic WHERE category_id = @categoryId AND pic_id = @picId', + { categoryId, picId }, + ); - const picId = picResult.rows[0]?.id ?? picResult.insertId; - - await runner.query(addCategoryPicSql, { - categoryId, - picId, - sort, - }); + if (!existingCategoryPic.rows[0]) { + await runner.query(addCategoryPicSql, { + categoryId, + picId, + sort, + }); + } return { id: categoryId, diff --git a/src/util/newFullHash.js b/src/util/newFullHash.js index bde7ffd..0daa210 100644 --- a/src/util/newFullHash.js +++ b/src/util/newFullHash.js @@ -1,5 +1,24 @@ -import { randomBytes } from 'node:crypto'; +import { createHash, randomBytes } from 'node:crypto'; -export default function newFullHash() { - return randomBytes(32).toString('hex'); +const CHARSET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +export function toBase62(buffer) { + let num = BigInt('0x' + buffer.toString('hex')); + if (num === 0n) return CHARSET[0].repeat(8); + let res = ''; + while (num > 0n) { + res = CHARSET[Number(num % 62n)] + res; + num /= 62n; + } + return res; } + +export default function newFullHash(buffer) { + if (buffer) { + const digest = createHash('sha256').update(buffer).digest(); + return toBase62(digest).slice(0, 8); + } + const bytes = randomBytes(16); + return toBase62(bytes).slice(0, 8); +} +