From 4e308cc17741a897a20a9facfadceccd707558f0 Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 8 Oct 2026 03:02:56 +0200 Subject: [PATCH] i --- src/providers/mssql/queries/article.js | 14 +++++++++++--- src/providers/mssql/queries/pic.js | 12 +++++++++++- src/providers/mssql/schema.js | 6 ++++-- src/providers/sqlite3/queries/article.js | 2 +- src/providers/sqlite3/schema.js | 6 ++++-- src/store/article/setEansBySku.js | 17 ++++++++++++++++- src/store/pic/resolvePicAccess.js | 21 ++++++++++++++++++--- 7 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index 17b4e55..4a36289 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -82,11 +82,19 @@ FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId) `; -/** True if an EAN is already assigned to any article (duplicate guard). */ +/** True if an EAN is already assigned to any article in the company (duplicate guard). */ export const EAN_EXISTS = ` SELECT TOP 1 1 AS used -FROM articleean -WHERE ean = @ean +FROM articleean ae +INNER JOIN article a ON a.id = ae.article_id +WHERE ae.ean = @ean AND (@companyId IS NULL OR a.company_id = @companyId) +`; + +export const EAN_EXISTS_FOR_OTHER_ARTICLE = ` +SELECT TOP 1 1 AS used +FROM articleean ae +INNER JOIN article a ON a.id = ae.article_id +WHERE ae.ean = @ean AND a.company_id = @companyId AND a.id <> @articleId `; /** Link an article to its parent article and mark the parent. */ diff --git a/src/providers/mssql/queries/pic.js b/src/providers/mssql/queries/pic.js index 794671e..de2a6ad 100644 --- a/src/providers/mssql/queries/pic.js +++ b/src/providers/mssql/queries/pic.js @@ -9,7 +9,7 @@ VALUES ( `; export const GET_CATEGORY_ID_BY_HASH = ` -SELECT TOP 1 categoryId +SELECT categoryId FROM ( SELECT ca.category_id AS categoryId FROM pic p @@ -19,6 +19,15 @@ FROM ( UNION + SELECT c.root_category_id AS categoryId + FROM pic p + INNER JOIN articlepic ap ON ap.pic_id = p.id + INNER JOIN article a ON a.id = ap.article_id + INNER JOIN company c ON c.id = a.company_id + WHERE p.hash = @hash + + UNION + SELECT cp.category_id AS categoryId FROM pic p INNER JOIN categorypic cp ON cp.pic_id = p.id @@ -33,6 +42,7 @@ FROM ( INNER JOIN company c ON c.id = b.company_id WHERE p.hash = @hash ) AS combined +WHERE categoryId IS NOT NULL `; export const GET_FULL_BY_HASH = ` diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index 49b0169..eb45263 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -418,11 +418,13 @@ BEGIN article_id INT NOT NULL, ean NVARCHAR(50) NOT NULL, CONSTRAINT FK_articleean_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, - CONSTRAINT UQ_articleean_ean UNIQUE (ean) + CONSTRAINT UQ_articleean_article_ean UNIQUE (article_id, ean) ); - -- Hot path: getBySku EANs by article (UQ is ean-leading only) + -- Hot path: getBySku EANs by article CREATE INDEX IX_articleean_article_ean ON articleean (article_id, ean); + CREATE INDEX IX_articleean_ean + ON articleean (ean); END `; diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index f05c2f3..8dc96cd 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -4,7 +4,7 @@ import { translateQueries } from './translate.js'; const queries = translateQueries(source, 'article'); export const { ADD_CATEGORY, ADD_EAN, ARTICLE_LIST_COLUMNS, CREATE, CREATE_CHILD, - DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS, + DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS, EAN_EXISTS_FOR_OTHER_ARTICLE, GET_BRAND_BY_COMPANY_AND_ID, GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU, LIST_CHILDREN_BY_PARENT_ID, diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index c6ca976..56be07e 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -246,11 +246,13 @@ export const INIT_ARTICLEEAN = ` CREATE TABLE IF NOT EXISTS articleean ( id INTEGER PRIMARY KEY, article_id INTEGER NOT NULL, - ean TEXT NOT NULL UNIQUE, + ean TEXT NOT NULL, FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS IX_articleean_article_ean +CREATE UNIQUE INDEX IF NOT EXISTS UQ_articleean_article_ean ON articleean(article_id, ean); +CREATE INDEX IF NOT EXISTS IX_articleean_ean + ON articleean(ean); `; export const INIT_ARTICLEPIC = ` diff --git a/src/store/article/setEansBySku.js b/src/store/article/setEansBySku.js index 59de25a..0858474 100644 --- a/src/store/article/setEansBySku.js +++ b/src/store/article/setEansBySku.js @@ -1,10 +1,12 @@ import { ADD_EAN as MSSQL_ADD_EAN, DELETE_EANS_BY_ARTICLE as MSSQL_DELETE_EANS, + EAN_EXISTS_FOR_OTHER_ARTICLE as MSSQL_CHECK_CONFLICT, } from '../../providers/mssql/queries/article.js'; import { ADD_EAN as SQLITE_ADD_EAN, DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS, + EAN_EXISTS_FOR_OTHER_ARTICLE as SQLITE_CHECK_CONFLICT, } from '../../providers/sqlite3/queries/article.js'; import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js'; import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js'; @@ -21,6 +23,7 @@ export default function setEansBySku(provider) { const addEanSql = isSqlite ? SQLITE_ADD_EAN : MSSQL_ADD_EAN; const deleteEansSql = isSqlite ? SQLITE_DELETE_EANS : MSSQL_DELETE_EANS; const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY; + const checkConflictSql = isSqlite ? SQLITE_CHECK_CONFLICT : MSSQL_CHECK_CONFLICT; /** * Replaces the EAN list of an article by its SKU. @@ -55,7 +58,19 @@ export default function setEansBySku(provider) { } const runSet = async (runner) => { - const { articleId, languageId, companyuserId } = await resolve(googleAuthId, sku, runner); + const { articleId, languageId, companyuserId, companyId } = await resolve(googleAuthId, sku, runner); + + // Guard: EAN cannot be used by another article in the same company + for (const ean of normalized) { + const conflict = await runner.query(checkConflictSql, { + ean, + companyId, + articleId, + }); + if (conflict.rows[0]) { + throw new Error('EAN already in use'); + } + } // Read old EANs for history const oldEanRes = await runner.query( diff --git a/src/store/pic/resolvePicAccess.js b/src/store/pic/resolvePicAccess.js index c49dddd..3bf169b 100644 --- a/src/store/pic/resolvePicAccess.js +++ b/src/store/pic/resolvePicAccess.js @@ -10,11 +10,26 @@ export default function resolvePicAccess(provider) { return async (googleAuthId, fullHash, tx) => { const result = await makeRunner(provider, tx).query(sql, { hash: fullHash }); - const categoryId = result.rows[0]?.categoryId; - if (!categoryId) { + if (!result.rows || result.rows.length === 0) { throw new Error('Image not found'); } - await access(googleAuthId, categoryId, tx); + let authorized = false; + let lastError = null; + for (const row of result.rows) { + const categoryId = row.categoryId; + if (!categoryId) continue; + try { + await access(googleAuthId, categoryId, tx); + authorized = true; + break; + } catch (err) { + lastError = err; + } + } + + if (!authorized) { + throw lastError || new Error('Not authorized'); + } }; }