This commit is contained in:
seb
2026-09-25 12:30:45 +02:00
parent b5255bdd17
commit 607e6cc0ac
10 changed files with 98 additions and 124 deletions

View File

@@ -56,14 +56,14 @@ export const GET_CATEGORY_ID_BY_SKU = `
SELECT TOP 1 ca.category_id AS categoryId, a.id AS articleId SELECT TOP 1 ca.category_id AS categoryId, a.id AS articleId
FROM article a FROM article a
INNER JOIN categoryarticle ca ON ca.article_id = a.id INNER JOIN categoryarticle ca ON ca.article_id = a.id
WHERE a.sku = @sku WHERE a.sku = @sku AND (@companyId IS NULL OR a.company_id = @companyId)
`; `;
/** Resolve an article id by sku (used to wire father/child compound links). */ /** Resolve an article id by sku (used to wire father/child compound links). */
export const GET_ID_BY_SKU = ` export const GET_ID_BY_SKU = `
SELECT TOP 1 id AS articleId SELECT TOP 1 id AS articleId
FROM article FROM article
WHERE sku = @sku 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 (duplicate guard). */
@@ -78,7 +78,7 @@ export const UPDATE_FATHER_BY_SKU = `
UPDATE article UPDATE article
SET fatherarticle_id = @fatherArticleId, SET fatherarticle_id = @fatherArticleId,
fathersort = COALESCE(@fathersort, 0) fathersort = COALESCE(@fathersort, 0)
WHERE sku = @sku WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)
`; `;
export const SET_IS_FATHER = ` export const SET_IS_FATHER = `
@@ -95,41 +95,22 @@ WHERE id = @articleId AND fatherarticle_id = @fatherArticleId
/** SKU → article/category + company auth in one round-trip. */ /** SKU → article/category + company auth in one round-trip. */
export const RESOLVE_ARTICLE_ACCESS_BY_SKU = ` export const RESOLVE_ARTICLE_ACCESS_BY_SKU = `
WITH article_cat AS ( SELECT TOP 1
SELECT TOP 1 a.id AS articleId, ca.category_id AS categoryId a.id AS articleId,
FROM article a ca.category_id AS categoryId,
INNER JOIN categoryarticle ca ON ca.article_id = a.id co.root_category_id AS rootCategoryId,
WHERE a.sku = @sku
),
ancestors AS (
SELECT c.id, c.parent_id
FROM category c
INNER JOIN article_cat ac ON c.id = ac.categoryId
UNION ALL
SELECT c.id, c.parent_id
FROM category c
INNER JOIN ancestors a ON c.id = a.parent_id
),
root AS (
SELECT TOP 1 id AS rootCategoryId
FROM ancestors
WHERE parent_id IS NULL
)
SELECT
ac.articleId,
ac.categoryId,
r.rootCategoryId,
co.id AS companyId, co.id AS companyId,
co.default_language_id AS languageId, co.default_language_id AS languageId,
co.poscustomergroup_id AS posCustomerGroupId, co.poscustomergroup_id AS posCustomerGroupId,
cu.id AS companyuserId cu.id AS companyuserId
FROM article_cat ac FROM article a
LEFT JOIN root r ON 1 = 1 INNER JOIN company co ON co.id = a.company_id
LEFT JOIN company co ON co.root_category_id = r.rootCategoryId INNER JOIN google_auth ga ON ga.id = @googleAuthId
LEFT JOIN companyuser cu INNER JOIN companyuser cu
ON cu.company_id = co.id AND cu.google_auth_id = @googleAuthId ON cu.company_id = a.company_id AND cu.google_auth_id = @googleAuthId
LEFT JOIN categoryarticle ca ON ca.article_id = a.id
WHERE a.sku = @sku
AND (ga.selected_company IS NULL OR a.company_id = ga.selected_company)
`; `;
export const UPDATE_WEIGHT = ` export const UPDATE_WEIGHT = `
@@ -163,9 +144,9 @@ WHERE id = @articleId
`; `;
export const CREATE = ` export const CREATE = `
INSERT INTO article (sku, unit_id, unitamount, weight) INSERT INTO article (company_id, sku, unit_id, unitamount, weight)
OUTPUT INSERTED.id OUTPUT INSERTED.id
VALUES (@sku, @unitId, @unitamount, @weight) VALUES (@companyId, @sku, @unitId, @unitamount, @weight)
`; `;
export const DELETE_EANS_BY_ARTICLE = ` export const DELETE_EANS_BY_ARTICLE = `
@@ -342,42 +323,23 @@ ORDER BY a.id
* Returns one row with articleJson, or empty if not found/unauthorized. * Returns one row with articleJson, or empty if not found/unauthorized.
*/ */
export const GET_ARTICLE_BY_SKU_FULL = ` export const GET_ARTICLE_BY_SKU_FULL = `
;WITH article_cat AS ( ;WITH auth AS (
SELECT TOP 1 a.id AS articleId, ca.category_id AS categoryId SELECT TOP 1
FROM article a a.id AS articleId,
INNER JOIN categoryarticle ca ON ca.article_id = a.id ca.category_id AS categoryId,
WHERE a.sku = @sku co.root_category_id AS rootCategoryId,
),
ancestors AS (
SELECT c.id, c.parent_id
FROM category c
INNER JOIN article_cat ac ON c.id = ac.categoryId
UNION ALL
SELECT c.id, c.parent_id
FROM category c
INNER JOIN ancestors a ON c.id = a.parent_id
),
root AS (
SELECT TOP 1 id AS rootCategoryId
FROM ancestors
WHERE parent_id IS NULL
),
auth AS (
SELECT
ac.articleId,
ac.categoryId,
r.rootCategoryId,
co.id AS companyId, co.id AS companyId,
co.default_language_id AS languageId, co.default_language_id AS languageId,
co.poscustomergroup_id AS posCustomerGroupId, co.poscustomergroup_id AS posCustomerGroupId,
cu.id AS companyuserId cu.id AS companyuserId
FROM article_cat ac FROM article a
LEFT JOIN root r ON 1 = 1 INNER JOIN company co ON co.id = a.company_id
LEFT JOIN company co ON co.root_category_id = r.rootCategoryId INNER JOIN google_auth ga ON ga.id = @googleAuthId
LEFT JOIN companyuser cu INNER JOIN companyuser cu
ON cu.company_id = co.id AND cu.google_auth_id = @googleAuthId ON cu.company_id = a.company_id AND cu.google_auth_id = @googleAuthId
LEFT JOIN categoryarticle ca ON ca.article_id = a.id
WHERE a.sku = @sku
AND (ga.selected_company IS NULL OR a.company_id = ga.selected_company)
) )
SELECT SELECT
auth.articleId, auth.articleId,

View File

@@ -57,7 +57,7 @@ OUTER APPLY (
) first_brand_pic ) first_brand_pic
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
LEFT JOIN article father ON father.id = a.fatherarticle_id LEFT JOIN article father ON father.id = a.fatherarticle_id
WHERE a.sku = @sku WHERE a.sku = @sku AND (@companyId IS NULL OR a.company_id = @companyId)
`; `;
export const GET_DETAIL_BY_ARTICLE_ID = ` export const GET_DETAIL_BY_ARTICLE_ID = `

View File

@@ -312,6 +312,7 @@ IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'article')
BEGIN BEGIN
CREATE TABLE article ( CREATE TABLE article (
id INT IDENTITY(1,1) PRIMARY KEY, id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
sku NVARCHAR(100) NULL, sku NVARCHAR(100) NULL,
unit_id INT NOT NULL, unit_id INT NOT NULL,
unitamount FLOAT NOT NULL, unitamount FLOAT NOT NULL,
@@ -324,12 +325,13 @@ BEGIN
fathersort INT NOT NULL CONSTRAINT DF_article_fathersort DEFAULT 0, fathersort INT NOT NULL CONSTRAINT DF_article_fathersort DEFAULT 0,
created_at DATETIME2 NOT NULL CONSTRAINT DF_article_created_at DEFAULT SYSUTCDATETIME(), created_at DATETIME2 NOT NULL CONSTRAINT DF_article_created_at DEFAULT SYSUTCDATETIME(),
updated_at DATETIME2 NOT NULL CONSTRAINT DF_article_updated_at DEFAULT SYSUTCDATETIME(), updated_at DATETIME2 NOT NULL CONSTRAINT DF_article_updated_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_article_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
CONSTRAINT FK_article_unit FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE NO ACTION, CONSTRAINT FK_article_unit FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE NO ACTION,
CONSTRAINT FK_article_brand FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE SET NULL, CONSTRAINT FK_article_brand FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE SET NULL,
CONSTRAINT FK_article_articlelist FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE SET NULL, CONSTRAINT FK_article_articlelist FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE SET NULL,
CONSTRAINT FK_article_optionslist FOREIGN KEY (optionslist_id) REFERENCES optionslist(id) ON DELETE SET NULL, CONSTRAINT FK_article_optionslist FOREIGN KEY (optionslist_id) REFERENCES optionslist(id) ON DELETE SET NULL,
CONSTRAINT FK_article_fatherarticle FOREIGN KEY (fatherarticle_id) REFERENCES article(id) ON DELETE NO ACTION, CONSTRAINT FK_article_fatherarticle FOREIGN KEY (fatherarticle_id) REFERENCES article(id) ON DELETE NO ACTION,
CONSTRAINT UQ_article_sku UNIQUE (sku) CONSTRAINT UQ_article_company_sku UNIQUE (company_id, sku)
); );
-- Only enforce sibling sort uniqueness among children of the same father. -- Only enforce sibling sort uniqueness among children of the same father.

View File

@@ -28,27 +28,23 @@ LEFT JOIN article father ON father.id = a.fatherarticle_id
`; `;
export const RESOLVE_ARTICLE_ACCESS_BY_SKU = ` export const RESOLVE_ARTICLE_ACCESS_BY_SKU = `
WITH RECURSIVE article_cat AS ( SELECT
SELECT a.id AS articleId, ca.category_id AS categoryId a.id AS articleId,
FROM article a INNER JOIN categoryarticle ca ON ca.article_id = a.id ca.category_id AS categoryId,
WHERE a.sku = @sku LIMIT 1 co.root_category_id AS rootCategoryId,
), ancestors AS ( co.id AS companyId,
SELECT c.id, c.parent_id FROM category c co.default_language_id AS languageId,
INNER JOIN article_cat ac ON c.id = ac.categoryId co.poscustomergroup_id AS posCustomerGroupId,
UNION ALL cu.id AS companyuserId
SELECT c.id, c.parent_id FROM category c FROM article a
INNER JOIN ancestors a ON c.id = a.parent_id INNER JOIN company co ON co.id = a.company_id
), root AS ( INNER JOIN google_auth ga ON ga.id = @googleAuthId
SELECT id AS rootCategoryId FROM ancestors WHERE parent_id IS NULL LIMIT 1 INNER JOIN companyuser cu
) ON cu.company_id = a.company_id AND cu.google_auth_id = @googleAuthId
SELECT ac.articleId, ac.categoryId, r.rootCategoryId, LEFT JOIN categoryarticle ca ON ca.article_id = a.id
co.id AS companyId, co.default_language_id AS languageId, WHERE a.sku = @sku
co.poscustomergroup_id AS posCustomerGroupId, cu.id AS companyuserId AND (ga.selected_company IS NULL OR a.company_id = ga.selected_company)
FROM article_cat ac LIMIT 1
LEFT JOIN root r ON 1 = 1
LEFT JOIN company co ON co.root_category_id = r.rootCategoryId
LEFT JOIN companyuser cu
ON cu.company_id = co.id AND cu.google_auth_id = @googleAuthId
`; `;
const JSON_EANS = `(SELECT COALESCE(json_group_array(json_object('ean', ean)), '[]') const JSON_EANS = `(SELECT COALESCE(json_group_array(json_object('ean', ean)), '[]')
@@ -103,27 +99,19 @@ ORDER BY a.id
`; `;
export const GET_ARTICLE_BY_SKU_FULL = ` export const GET_ARTICLE_BY_SKU_FULL = `
WITH RECURSIVE article_cat AS ( WITH auth AS (
SELECT a.id AS articleId, ca.category_id AS categoryId SELECT a.id AS articleId, ca.category_id AS categoryId, co.root_category_id AS rootCategoryId,
FROM article a INNER JOIN categoryarticle ca ON ca.article_id = a.id
WHERE a.sku = @sku LIMIT 1
), ancestors AS (
SELECT c.id, c.parent_id FROM category c
INNER JOIN article_cat ac ON c.id = ac.categoryId
UNION ALL
SELECT c.id, c.parent_id FROM category c
INNER JOIN ancestors a ON c.id = a.parent_id
), root AS (
SELECT id AS rootCategoryId FROM ancestors WHERE parent_id IS NULL LIMIT 1
), auth AS (
SELECT ac.articleId, ac.categoryId, r.rootCategoryId,
co.id AS companyId, co.default_language_id AS languageId, co.id AS companyId, co.default_language_id AS languageId,
co.poscustomergroup_id AS posCustomerGroupId, cu.id AS companyuserId co.poscustomergroup_id AS posCustomerGroupId, cu.id AS companyuserId
FROM article_cat ac FROM article a
LEFT JOIN root r ON 1 = 1 INNER JOIN company co ON co.id = a.company_id
LEFT JOIN company co ON co.root_category_id = r.rootCategoryId INNER JOIN google_auth ga ON ga.id = @googleAuthId
LEFT JOIN companyuser cu INNER JOIN companyuser cu
ON cu.company_id = co.id AND cu.google_auth_id = @googleAuthId ON cu.company_id = a.company_id AND cu.google_auth_id = @googleAuthId
LEFT JOIN categoryarticle ca ON ca.article_id = a.id
WHERE a.sku = @sku
AND (ga.selected_company IS NULL OR a.company_id = ga.selected_company)
LIMIT 1
) )
SELECT auth.*, SELECT auth.*,
json_object( json_object(

View File

@@ -26,7 +26,7 @@ export const GET_DETAIL_BY_SKU = `
SELECT ${ARTICLE_DETAIL_COLUMNS} SELECT ${ARTICLE_DETAIL_COLUMNS}
FROM article a FROM article a
${DETAIL_JOINS} ${DETAIL_JOINS}
WHERE a.sku = @sku WHERE a.sku = @sku AND (@companyId IS NULL OR a.company_id = @companyId)
`; `;
export const GET_DETAIL_BY_ARTICLE_ID = ` export const GET_DETAIL_BY_ARTICLE_ID = `

View File

@@ -211,7 +211,8 @@ CREATE TABLE IF NOT EXISTS brand (
export const INIT_ARTICLE = ` export const INIT_ARTICLE = `
CREATE TABLE IF NOT EXISTS article ( CREATE TABLE IF NOT EXISTS article (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
sku TEXT COLLATE NOCASE UNIQUE, company_id INTEGER NOT NULL,
sku TEXT COLLATE NOCASE,
unit_id INTEGER NOT NULL, unit_id INTEGER NOT NULL,
unitamount REAL NOT NULL, unitamount REAL NOT NULL,
weight REAL NOT NULL, weight REAL NOT NULL,
@@ -223,6 +224,8 @@ CREATE TABLE IF NOT EXISTS article (
fathersort INTEGER NOT NULL DEFAULT 0, fathersort INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}), updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
UNIQUE (company_id, sku),
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE NO ACTION, FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE NO ACTION,
FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE SET NULL, FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE SET NULL,
FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE SET NULL, FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE SET NULL,

View File

@@ -54,6 +54,7 @@ export default function createUnderParent(provider) {
let article; let article;
try { try {
article = await runner.query(createArticleSql, { article = await runner.query(createArticleSql, {
companyId,
sku, sku,
unitId, unitId,
unitamount: 1, unitamount: 1,

View File

@@ -157,7 +157,7 @@ export default function importRows(provider) {
continue; continue;
} }
const existing = await baseRunner.query(articleQueries.GET_ID_BY_SKU, { sku }); const existing = await baseRunner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
if (existing.rows[0]) { if (existing.rows[0]) {
skipped.push({ sku, name, reason: 'duplicate sku' }); skipped.push({ sku, name, reason: 'duplicate sku' });
continue; continue;
@@ -169,6 +169,7 @@ export default function importRows(provider) {
const slug = slugify(name); const slug = slugify(name);
const article = await runner.query(articleQueries.CREATE, { const article = await runner.query(articleQueries.CREATE, {
companyId,
sku, sku,
unitId, unitId,
unitamount: 1, unitamount: 1,
@@ -277,10 +278,10 @@ export default function importRows(provider) {
try { try {
await runInTx(async (runner) => { await runInTx(async (runner) => {
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku }); const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
const childId = child.rows[0]?.articleId; const childId = child.rows[0]?.articleId;
if (childId) { if (childId) {
const father = await runner.query(articleQueries.GET_ID_BY_SKU, { sku: fatherSku }); const father = await runner.query(articleQueries.GET_ID_BY_SKU, { sku: fatherSku, companyId });
const fatherId = father.rows[0]?.articleId; const fatherId = father.rows[0]?.articleId;
if (fatherId && fatherId !== childId) { if (fatherId && fatherId !== childId) {
const childCount = await runner.query(getChildCountSql, { const childCount = await runner.query(getChildCountSql, {
@@ -289,6 +290,7 @@ export default function importRows(provider) {
const count = childCount.rows[0].count; const count = childCount.rows[0].count;
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, { await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
sku, sku,
companyId,
fatherArticleId: fatherId, fatherArticleId: fatherId,
fathersort: count, fathersort: count,
}); });

View File

@@ -30,11 +30,12 @@ export default function setFatherBySku(provider) {
*/ */
return async (googleAuthId, sku, fatherSku, tx) => { return async (googleAuthId, sku, fatherSku, tx) => {
const runSet = async (runner) => { const runSet = async (runner) => {
const { articleId, languageId } = await resolve(googleAuthId, sku, runner); const { articleId, languageId, companyId } = await resolve(googleAuthId, sku, runner);
if (fatherSku == null || fatherSku === '') { if (fatherSku == null || fatherSku === '') {
await runner.query(queries.UPDATE_FATHER_BY_SKU, { await runner.query(queries.UPDATE_FATHER_BY_SKU, {
sku, sku,
companyId,
fatherArticleId: null, fatherArticleId: null,
}); });
const unlinked = await load(articleId, languageId, runner); const unlinked = await load(articleId, languageId, runner);
@@ -44,7 +45,7 @@ export default function setFatherBySku(provider) {
return unlinked; return unlinked;
} }
const father = await runner.query(queries.GET_ID_BY_SKU, { sku: fatherSku }); const father = await runner.query(queries.GET_ID_BY_SKU, { sku: fatherSku, companyId });
const fatherId = father.rows[0]?.articleId; const fatherId = father.rows[0]?.articleId;
if (!fatherId) { if (!fatherId) {
throw new Error('Father not found'); throw new Error('Father not found');
@@ -60,6 +61,7 @@ export default function setFatherBySku(provider) {
await runner.query(queries.UPDATE_FATHER_BY_SKU, { await runner.query(queries.UPDATE_FATHER_BY_SKU, {
sku, sku,
companyId,
fatherArticleId: fatherId, fatherArticleId: fatherId,
fathersort: count, fathersort: count,
}); });

View File

@@ -1,8 +1,16 @@
import { LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY } from '../../providers/mssql/queries/numbers.js'; import {
import { LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY } from '../../providers/sqlite3/queries/numbers.js'; ENSURE_TYPE as MSSQL_ENSURE_TYPE,
LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY,
} from '../../providers/mssql/queries/numbers.js';
import {
ENSURE_TYPE as SQLITE_ENSURE_TYPE,
LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY,
} from '../../providers/sqlite3/queries/numbers.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
const DEFAULT_TYPES = ['sku', 'entity', 'delivery'];
function shape(row) { function shape(row) {
return { return {
id: row.id, id: row.id,
@@ -15,13 +23,14 @@ function shape(row) {
export default function list(provider) { export default function list(provider) {
const access = resolveSelectedCompanyAccess(provider); const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const ensureSql = isSqlite ? SQLITE_ENSURE_TYPE : MSSQL_ENSURE_TYPE;
const sql = const sql =
provider?.dialect === 'sqlite3' isSqlite ? SQLITE_LIST_BY_COMPANY : MSSQL_LIST_BY_COMPANY;
? SQLITE_LIST_BY_COMPANY
: MSSQL_LIST_BY_COMPANY;
/** /**
* Lists the number sequences of the user's selected company. * Lists the number sequences of the user's selected company.
* Ensures that default sequences ('sku', 'entity', 'delivery') exist.
* *
* @param {number} googleAuthId - The id of the Google auth user. * @param {number} googleAuthId - The id of the Google auth user.
* @param {object} [tx] - Optional transaction to run within. * @param {object} [tx] - Optional transaction to run within.
@@ -33,8 +42,13 @@ export default function list(provider) {
*/ */
return async (googleAuthId, tx) => { return async (googleAuthId, tx) => {
const { companyId } = await access(googleAuthId, tx); const { companyId } = await access(googleAuthId, tx);
const runner = makeRunner(provider, tx);
const result = await makeRunner(provider, tx).query(sql, { companyId }); for (const type of DEFAULT_TYPES) {
await runner.query(ensureSql, { companyId, type });
}
const result = await runner.query(sql, { companyId });
return result.rows.map(shape); return result.rows.map(shape);
}; };