refactor: introduce database provider abstraction, isolate MSSQL layer
- Add provider contract (src/providers/provider.js): query() -> {rows,
rowsAffected, insertId}, callback transaction(fn), exec() for stored
procs, initialize/purge lifecycle
- Add MssqlProvider (src/providers/mssql/index.js) wrapping an mssql
ConnectionPool with normalized results, tx commit/rollback and
constraint-error classifiers (unique 2627/2601, FK 547)
- LibDashStore constructor now takes a provider instead of a pool
(breaking); adds LibDashStore.create() async factory; keeps pool as
transitional bridge until store modules are converted (see TODO.md)
- Move MSSQL-specific code into the provider: src/sql/* ->
src/providers/mssql/queries/*, src/schema.js ->
src/providers/mssql/schema.js, purge SQL -> purgeSql.js
- Update scripts, generate-schema-graph.js and package.json exports
(./providers/mssql, ./providers/provider)
- Add TODO.md with the remaining provider/SQLite migration plan
No SQL or behavioral changes.
This commit is contained in:
506
src/providers/mssql/queries/article.js
Normal file
506
src/providers/mssql/queries/article.js
Normal file
@@ -0,0 +1,506 @@
|
||||
export const ARTICLE_LIST_JOINS = `
|
||||
LEFT JOIN articletext at
|
||||
ON at.article_id = a.id AND at.language_id = @languageId
|
||||
LEFT JOIN price pr
|
||||
ON pr.article_id = a.id
|
||||
AND pr.customergroup_id = @posCustomerGroupId
|
||||
AND pr.minmultiplier = 1
|
||||
LEFT JOIN tax t ON t.id = pr.tax_id
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 ap.pic_id
|
||||
FROM articlepic ap
|
||||
WHERE ap.article_id = a.id
|
||||
ORDER BY ap.sort ASC, ap.id ASC
|
||||
) first_pic
|
||||
LEFT JOIN pic ON pic.id = first_pic.pic_id
|
||||
LEFT JOIN article father ON father.id = a.fatherarticle_id
|
||||
`;
|
||||
|
||||
export const ARTICLE_LIST_COLUMNS = `
|
||||
a.id,
|
||||
a.sku,
|
||||
at.name,
|
||||
at.slug,
|
||||
pr.net,
|
||||
t.rate AS taxRate,
|
||||
CASE
|
||||
WHEN a.optionslist_id IS NOT NULL THEN (
|
||||
SELECT COUNT(*)
|
||||
FROM optionslistarticle ola
|
||||
WHERE ola.optionslist_id = a.optionslist_id
|
||||
)
|
||||
ELSE 0
|
||||
END AS optionsCount,
|
||||
CASE
|
||||
WHEN a.articlelist_id IS NOT NULL THEN (
|
||||
SELECT COUNT(*)
|
||||
FROM articlelistarticle ala
|
||||
WHERE ala.articlelist_id = a.articlelist_id
|
||||
)
|
||||
ELSE 0
|
||||
END AS articleListCount,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM article child
|
||||
WHERE child.fatherarticle_id = a.id
|
||||
) AS childCount,
|
||||
a.fatherarticle_id AS fatherArticleId,
|
||||
father.sku AS fatherSku,
|
||||
pic.hash AS fullHash,
|
||||
pic.thumbw AS thumbW,
|
||||
pic.thumbh AS thumbH,
|
||||
pic.thumbhash AS thumbHash
|
||||
`;
|
||||
|
||||
export const GET_CATEGORY_ID_BY_SKU = `
|
||||
SELECT TOP 1 ca.category_id AS categoryId, a.id AS articleId
|
||||
FROM article a
|
||||
INNER JOIN categoryarticle ca ON ca.article_id = a.id
|
||||
WHERE a.sku = @sku
|
||||
`;
|
||||
|
||||
/** Resolve an article id by sku (used to wire father/child compound links). */
|
||||
export const GET_ID_BY_SKU = `
|
||||
SELECT TOP 1 id AS articleId
|
||||
FROM article
|
||||
WHERE sku = @sku
|
||||
`;
|
||||
|
||||
/** True if an EAN is already assigned to any article (duplicate guard). */
|
||||
export const EAN_EXISTS = `
|
||||
SELECT TOP 1 1 AS used
|
||||
FROM articleean
|
||||
WHERE ean = @ean
|
||||
`;
|
||||
|
||||
/** Link an article to its father article and mark the father. */
|
||||
export const UPDATE_FATHER_BY_SKU = `
|
||||
UPDATE article
|
||||
SET fatherarticle_id = @fatherArticleId
|
||||
WHERE sku = @sku
|
||||
`;
|
||||
|
||||
export const SET_IS_FATHER = `
|
||||
UPDATE article
|
||||
SET is_father = 1
|
||||
WHERE id = @articleId
|
||||
`;
|
||||
|
||||
export const SET_FATHER_SORT = `
|
||||
UPDATE article
|
||||
SET fathersort = @fathersort
|
||||
WHERE id = @articleId AND fatherarticle_id = @fatherArticleId
|
||||
`;
|
||||
|
||||
/** SKU → article/category + company auth in one round-trip. */
|
||||
export const RESOLVE_ARTICLE_ACCESS_BY_SKU = `
|
||||
WITH article_cat AS (
|
||||
SELECT TOP 1 a.id AS articleId, ca.category_id AS categoryId
|
||||
FROM article a
|
||||
INNER JOIN categoryarticle ca ON ca.article_id = a.id
|
||||
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.default_language_id AS languageId,
|
||||
co.poscustomergroup_id AS posCustomerGroupId,
|
||||
cu.id AS companyuserId
|
||||
FROM article_cat ac
|
||||
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
|
||||
`;
|
||||
|
||||
export const UPDATE_WEIGHT = `
|
||||
UPDATE article
|
||||
SET weight = @weight
|
||||
WHERE id = @articleId
|
||||
`;
|
||||
|
||||
export const UPDATE_BRAND = `
|
||||
UPDATE article
|
||||
SET brand_id = @brandId
|
||||
WHERE id = @articleId
|
||||
`;
|
||||
|
||||
export const UPDATE_UNIT = `
|
||||
UPDATE article
|
||||
SET unit_id = @unitId
|
||||
WHERE id = @articleId
|
||||
`;
|
||||
|
||||
export const UPDATE_UNITAMOUNT = `
|
||||
UPDATE article
|
||||
SET unitamount = @unitamount
|
||||
WHERE id = @articleId
|
||||
`;
|
||||
|
||||
export const UPDATE_UNIT_AND_AMOUNT = `
|
||||
UPDATE article
|
||||
SET unit_id = @unitId, unitamount = @unitamount
|
||||
WHERE id = @articleId
|
||||
`;
|
||||
|
||||
export const CREATE = `
|
||||
INSERT INTO article (sku, unit_id, unitamount, weight)
|
||||
OUTPUT INSERTED.id
|
||||
VALUES (@sku, @unitId, @unitamount, @weight)
|
||||
`;
|
||||
|
||||
export const DELETE_EANS_BY_ARTICLE = `
|
||||
DELETE FROM articleean
|
||||
WHERE article_id = @articleId
|
||||
`;
|
||||
|
||||
export const ADD_EAN = `
|
||||
INSERT INTO articleean (article_id, ean)
|
||||
VALUES (@articleId, @ean)
|
||||
`;
|
||||
|
||||
export const DELETE_CATEGORIES_BY_ARTICLE = `
|
||||
DELETE FROM categoryarticle
|
||||
WHERE article_id = @articleId
|
||||
`;
|
||||
|
||||
export const ADD_CATEGORY = `
|
||||
INSERT INTO categoryarticle (category_id, article_id)
|
||||
VALUES (@categoryId, @articleId)
|
||||
`;
|
||||
|
||||
export const GET_BRAND_BY_COMPANY_AND_ID = `
|
||||
SELECT id
|
||||
FROM brand
|
||||
WHERE id = @brandId AND company_id = @companyId
|
||||
`;
|
||||
|
||||
/**
|
||||
* Company-wide article search index.
|
||||
* Returns one row per article with id, sku, name, and all eans as a JSON array.
|
||||
* Used by the client for local search / autocomplete.
|
||||
*/
|
||||
export const SEARCH_ARTICLES = `
|
||||
;WITH category_tree AS (
|
||||
SELECT id
|
||||
FROM category
|
||||
WHERE id = @rootCategoryId
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT c.id
|
||||
FROM category c
|
||||
INNER JOIN category_tree ct ON c.parent_id = ct.id
|
||||
)
|
||||
SELECT
|
||||
a.id,
|
||||
a.sku,
|
||||
at.name,
|
||||
(
|
||||
SELECT ae.ean
|
||||
FROM articleean ae
|
||||
WHERE ae.article_id = a.id
|
||||
ORDER BY ae.ean
|
||||
FOR JSON PATH
|
||||
) AS eansJson,
|
||||
(
|
||||
SELECT ca2.category_id
|
||||
FROM categoryarticle ca2
|
||||
WHERE ca2.article_id = a.id
|
||||
ORDER BY ca2.category_id
|
||||
FOR JSON PATH
|
||||
) AS categoryIdsJson
|
||||
FROM article a
|
||||
LEFT JOIN articletext at
|
||||
ON at.article_id = a.id AND at.language_id = @languageId
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM categoryarticle ca
|
||||
INNER JOIN category_tree ct ON ct.id = ca.category_id
|
||||
WHERE ca.article_id = a.id
|
||||
)
|
||||
ORDER BY a.id
|
||||
`;
|
||||
|
||||
/**
|
||||
* Flat category tree (id, parentId, name) for a root category, used to resolve
|
||||
* breadcrumbs for the article export.
|
||||
*/
|
||||
export const GET_CATEGORY_TREE_FLAT = `
|
||||
;WITH tree AS (
|
||||
SELECT id, parent_id, sort
|
||||
FROM category
|
||||
WHERE id = @rootId
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT c.id, c.parent_id, c.sort
|
||||
FROM category c
|
||||
INNER JOIN tree t ON c.parent_id = t.id
|
||||
)
|
||||
SELECT
|
||||
t.id,
|
||||
t.parent_id AS parentId,
|
||||
ct.name
|
||||
FROM tree t
|
||||
LEFT JOIN categorytext ct
|
||||
ON ct.category_id = t.id AND ct.language_id = @languageId
|
||||
`;
|
||||
|
||||
/**
|
||||
* Export rows for every article in a category subtree (root = @categoryId).
|
||||
* One row per article with the columns used by the export CSV, including the
|
||||
* POS price (minmultiplier = 1 for the company poscustomergroup), brand, unit,
|
||||
* eans, categoryIds and all photo hashes as JSON arrays.
|
||||
*/
|
||||
export const GET_ARTICLE_EXPORT = `
|
||||
;WITH category_tree AS (
|
||||
SELECT id
|
||||
FROM category
|
||||
WHERE id = @categoryId
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT c.id
|
||||
FROM category c
|
||||
INNER JOIN category_tree ct ON c.parent_id = ct.id
|
||||
)
|
||||
SELECT
|
||||
a.id,
|
||||
a.sku,
|
||||
at.name,
|
||||
at.slug,
|
||||
u.si AS unitSi,
|
||||
ut.symbol AS unitSymbol,
|
||||
a.unitamount,
|
||||
b.name AS brandName,
|
||||
b.name AS brandName,
|
||||
pr.net AS net,
|
||||
t.rate AS taxRate,
|
||||
(
|
||||
SELECT ae.ean
|
||||
FROM articleean ae
|
||||
WHERE ae.article_id = a.id
|
||||
ORDER BY ae.ean
|
||||
FOR JSON PATH
|
||||
) AS eansJson,
|
||||
(
|
||||
SELECT ca2.category_id
|
||||
FROM categoryarticle ca2
|
||||
WHERE ca2.article_id = a.id
|
||||
ORDER BY ca2.category_id
|
||||
FOR JSON PATH
|
||||
) AS categoryIdsJson,
|
||||
(
|
||||
SELECT p.hash
|
||||
FROM articlepic ap
|
||||
INNER JOIN pic p ON p.id = ap.pic_id
|
||||
WHERE ap.article_id = a.id
|
||||
ORDER BY ap.sort ASC, ap.id ASC
|
||||
FOR JSON PATH
|
||||
) AS fullHashesJson
|
||||
FROM article a
|
||||
INNER JOIN categoryarticle ca ON ca.article_id = a.id
|
||||
INNER JOIN category_tree ct ON ct.id = ca.category_id
|
||||
LEFT JOIN articletext at
|
||||
ON at.article_id = a.id AND at.language_id = @languageId
|
||||
LEFT JOIN unit u ON u.id = a.unit_id
|
||||
LEFT JOIN unittext ut
|
||||
ON ut.unit_id = u.id AND ut.language_id = @languageId
|
||||
LEFT JOIN brand b ON b.id = a.brand_id
|
||||
LEFT JOIN price pr
|
||||
ON pr.article_id = a.id
|
||||
AND pr.customergroup_id = @posCustomerGroupId
|
||||
AND pr.minmultiplier = 1
|
||||
LEFT JOIN tax t ON t.id = pr.tax_id
|
||||
ORDER BY a.id
|
||||
`;
|
||||
|
||||
/**
|
||||
* Single-query replacement for resolveArticleBySku + queryBySku.
|
||||
* Resolves SKU → article + company + auth, then loads full article detail
|
||||
* including categories, photos, eans, and prices via FOR JSON sub-queries.
|
||||
* Returns one row with articleJson, or empty if not found/unauthorized.
|
||||
*/
|
||||
export const GET_ARTICLE_BY_SKU_FULL = `
|
||||
;WITH article_cat AS (
|
||||
SELECT TOP 1 a.id AS articleId, ca.category_id AS categoryId
|
||||
FROM article a
|
||||
INNER JOIN categoryarticle ca ON ca.article_id = a.id
|
||||
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
|
||||
),
|
||||
auth AS (
|
||||
SELECT
|
||||
ac.articleId,
|
||||
ac.categoryId,
|
||||
r.rootCategoryId,
|
||||
co.id AS companyId,
|
||||
co.default_language_id AS languageId,
|
||||
co.poscustomergroup_id AS posCustomerGroupId,
|
||||
cu.id AS companyuserId
|
||||
FROM article_cat ac
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
auth.articleId,
|
||||
auth.categoryId,
|
||||
auth.rootCategoryId,
|
||||
auth.companyId,
|
||||
auth.languageId,
|
||||
auth.posCustomerGroupId,
|
||||
auth.companyuserId,
|
||||
(
|
||||
SELECT
|
||||
a.id,
|
||||
a.sku,
|
||||
at.name,
|
||||
at.slug,
|
||||
a.unitamount,
|
||||
a.weight,
|
||||
u.si AS unitSi,
|
||||
u.basesi AS unitBasesi,
|
||||
u.basesifactor AS unitBasesifactor,
|
||||
ut.symbol AS unitSymbol,
|
||||
ut.name AS unitName,
|
||||
b.name AS brandName,
|
||||
bp.thumbw AS brandThumbW,
|
||||
bp.thumbh AS brandThumbH,
|
||||
bp.thumbhash AS brandThumbHash,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM optionslistarticle ola
|
||||
WHERE ola.optionslist_id = a.optionslist_id
|
||||
) AS optionsCount,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM articlelistarticle ala
|
||||
WHERE ala.articlelist_id = a.articlelist_id
|
||||
) AS articleListCount,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM article child
|
||||
WHERE child.fatherarticle_id = a.id
|
||||
) AS childCount,
|
||||
a.fatherarticle_id AS fatherArticleId,
|
||||
fa.sku AS fatherSku,
|
||||
(
|
||||
SELECT ca2.category_id
|
||||
FROM categoryarticle ca2
|
||||
WHERE ca2.article_id = a.id
|
||||
ORDER BY ca2.category_id
|
||||
FOR JSON PATH
|
||||
) AS categoryIdsJson,
|
||||
(
|
||||
SELECT
|
||||
ap.sort,
|
||||
p.hash AS fullHash,
|
||||
p.w AS fullW,
|
||||
p.h AS fullH,
|
||||
p.size AS fullSize,
|
||||
p.thumbw AS thumbW,
|
||||
p.thumbh AS thumbH,
|
||||
p.thumbhash AS thumbHash
|
||||
FROM articlepic ap
|
||||
INNER JOIN pic p ON p.id = ap.pic_id
|
||||
WHERE ap.article_id = a.id
|
||||
ORDER BY ap.sort ASC, ap.id ASC
|
||||
FOR JSON PATH
|
||||
) AS photosJson,
|
||||
(
|
||||
SELECT ae.ean
|
||||
FROM articleean ae
|
||||
WHERE ae.article_id = a.id
|
||||
ORDER BY ae.ean
|
||||
FOR JSON PATH
|
||||
) AS eansJson,
|
||||
(
|
||||
SELECT
|
||||
pr.minmultiplier,
|
||||
pr.net,
|
||||
t.rate AS taxRate,
|
||||
pr.customergroup_id AS customergroupId
|
||||
FROM price pr
|
||||
INNER JOIN tax t ON t.id = pr.tax_id
|
||||
WHERE pr.article_id = a.id
|
||||
ORDER BY pr.customergroup_id, pr.minmultiplier
|
||||
FOR JSON PATH
|
||||
) AS pricesJson,
|
||||
(
|
||||
SELECT
|
||||
asup.id,
|
||||
asup.article_id AS articleId,
|
||||
asup.entity_id AS entityId,
|
||||
e.name AS supplierName,
|
||||
asup.sku,
|
||||
asup.pu,
|
||||
asup.stock,
|
||||
asup.sort,
|
||||
asup.price_id AS priceId,
|
||||
p.net AS [price.net],
|
||||
p.tax_id AS [price.taxId],
|
||||
t.rate AS [price.taxRate]
|
||||
FROM articlesupplier asup
|
||||
INNER JOIN entity e ON e.id = asup.entity_id
|
||||
LEFT JOIN price p ON p.id = asup.price_id
|
||||
LEFT JOIN tax t ON t.id = p.tax_id
|
||||
WHERE asup.article_id = a.id
|
||||
ORDER BY asup.sort ASC, asup.id ASC
|
||||
FOR JSON PATH
|
||||
) AS suppliersJson
|
||||
FROM article a
|
||||
LEFT JOIN articletext at
|
||||
ON at.article_id = a.id AND at.language_id = auth.languageId
|
||||
LEFT JOIN unit u ON u.id = a.unit_id
|
||||
LEFT JOIN unittext ut
|
||||
ON ut.unit_id = u.id AND ut.language_id = auth.languageId
|
||||
LEFT JOIN brand b ON b.id = a.brand_id
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 bp2.pic_id
|
||||
FROM brandpic bp2
|
||||
WHERE bp2.brand_id = b.id
|
||||
ORDER BY bp2.sort ASC, bp2.id ASC
|
||||
) first_brand_pic
|
||||
LEFT JOIN pic bp ON bp.id = first_brand_pic.pic_id
|
||||
LEFT JOIN article fa ON fa.id = a.fatherarticle_id
|
||||
WHERE a.id = auth.articleId
|
||||
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
|
||||
) AS articleJson
|
||||
FROM auth
|
||||
`;
|
||||
Reference in New Issue
Block a user