This commit is contained in:
seb
2026-09-22 07:25:16 +02:00
parent 4ed6260e91
commit cca131d83a
148 changed files with 5343 additions and 3466 deletions

View File

@@ -1,12 +1,25 @@
import { ADD as ADD_ARTICLEPIC, GET_NEXT_SORT } from '../../providers/mssql/queries/articlepic.js';
import { CREATE as CREATE_PIC } from '../../providers/mssql/queries/pic.js';
import {
ADD as MSSQL_ADD_ARTICLEPIC,
GET_NEXT_SORT as MSSQL_GET_NEXT_SORT,
} from '../../providers/mssql/queries/articlepic.js';
import {
ADD as SQLITE_ADD_ARTICLEPIC,
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
} from '../../providers/sqlite3/queries/articlepic.js';
import { CREATE as MSSQL_CREATE_PIC } from '../../providers/mssql/queries/pic.js';
import { CREATE as SQLITE_CREATE_PIC } from '../../providers/sqlite3/queries/pic.js';
import processAvifImage from '../../util/processAvifImage.js';
import newFullHash from '../../util/newFullHash.js';
import request from '../request.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function addImage(pool) {
const resolve = resolveArticleBySku(pool);
export default function addImage(provider) {
const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
const createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC;
const addArticlePicSql = isSqlite ? SQLITE_ADD_ARTICLEPIC : MSSQL_ADD_ARTICLEPIC;
/**
* Adds an image to an article by its SKU, processing it into AVIF variants.
@@ -14,50 +27,53 @@ export default function addImage(pool) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {Buffer} image - The source image buffer.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>} The added image info.
*
* @example
* const result = await store.article.addImage(42, 'SKU-1', imageBuffer);
* // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash }
*/
return async (googleAuthId, sku, image) => {
const { articleId } = await resolve(googleAuthId, sku);
return async (googleAuthId, sku, image, tx) => {
const runAdd = async (runner) => {
const { articleId } = await resolve(googleAuthId, sku, runner);
const processed = await processAvifImage(image);
const processed = await processAvifImage(image);
const tx = pool.transaction();
await tx.begin();
try {
const sortResult = await request(pool, tx)
.input('articleId', articleId)
.query(GET_NEXT_SORT);
const sort = sortResult.recordset[0].sort;
const sortResult = await runner.query(getSortSql, { articleId });
const sort = sortResult.rows[0].sort;
const fullHash = newFullHash();
const picResult = await request(pool, tx)
.input('w', processed.full.w)
.input('h', processed.full.h)
.input('size', processed.full.size)
.input('hash', fullHash)
.input('thumbw', processed.thumb.w)
.input('thumbh', processed.thumb.h)
.input('thumbhash', processed.thumb.hash)
.input('source', '')
.input('fullbinary', processed.full.buffer)
.input('thumbbinary', processed.thumb.buffer)
.query(CREATE_PIC);
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 picId = picResult.recordset[0].id;
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,
});
await request(pool, tx)
.input('articleId', articleId)
.input('picId', picId)
.input('sort', sort)
.query(ADD_ARTICLEPIC);
const picId = picResult.rows[0]?.id ?? picResult.insertId;
await tx.commit();
await runner.query(addArticlePicSql, {
articleId,
picId,
sort,
});
return {
picId,
@@ -74,13 +90,15 @@ export default function addImage(pool) {
fullHash,
thumbHash: processed.thumb.hash,
};
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
}
throw err;
};
if (tx) {
return runAdd(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runAdd);
}
return runAdd(makeRunner(provider));
};
}

View File

@@ -1,17 +1,27 @@
import { CREATE } from '../../providers/mssql/queries/article.js';
import { ADD as ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
import { CREATE as CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
import { INSERT as INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { CREATE as MSSQL_CREATE_ARTICLE } from '../../providers/mssql/queries/article.js';
import { CREATE as SQLITE_CREATE_ARTICLE } from '../../providers/sqlite3/queries/article.js';
import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.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';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import ensureDefaultUnit from '../unit/ensureDefault.js';
import nextSku from '../numbers/nextSku.js';
import request from '../request.js';
import makeRunner from '../runner.js';
import slugify from '../slugify.js';
export default function createUnderParent(pool) {
const access = resolveCategoryAccess(pool);
const ensureUnit = ensureDefaultUnit(pool);
const allocateSku = nextSku(pool);
export default function createUnderParent(provider) {
const access = resolveCategoryAccess(provider);
const ensureUnit = ensureDefaultUnit(provider);
const allocateSku = nextSku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Creates a new article under a given parent category, allocating a SKU and
@@ -21,54 +31,51 @@ export default function createUnderParent(pool) {
* @param {number} input.googleAuthId - The id of the Google auth user.
* @param {number} input.parentId - The id of the parent category.
* @param {string} input.name - The article name.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, parentId: number, name: string, sku: string}>} The created article.
*
* @example
* const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' });
* // => { id, parentId, name, sku }
*/
return async ({ googleAuthId, parentId, name }) => {
const { companyId, languageId, companyuserId } = await access(
googleAuthId,
parentId
);
return async ({ googleAuthId, parentId, name }, tx) => {
const runCreate = async (runner) => {
const { companyId, languageId, companyuserId } = await access(
googleAuthId,
parentId,
runner,
);
const tx = pool.transaction();
await tx.begin();
try {
const unitId = await ensureUnit(tx, languageId, companyId);
const sku = await allocateSku(companyId, tx);
const unitId = await ensureUnit(runner, languageId, companyId);
const sku = await allocateSku(companyId, runner);
const slug = slugify(name);
const article = await request(pool, tx)
.input('sku', sku)
.input('unitId', unitId)
.input('unitamount', 1)
.input('weight', 0)
.query(CREATE);
const article = await runner.query(createArticleSql, {
sku,
unitId,
unitamount: 1,
weight: 0,
});
const id = article.recordset[0].id;
const id = article.rows[0]?.id ?? article.insertId;
await request(pool, tx)
.input('categoryId', parentId)
.input('articleId', id)
.query(ADD_CATEGORYARTICLE);
await runner.query(addCategoryArticleSql, {
categoryId: parentId,
articleId: id,
});
await request(pool, tx)
.input('articleId', id)
.input('languageId', languageId)
.input('name', name)
.input('slug', slug)
.query(CREATE_ARTICLETEXT);
await runner.query(createArticleTextSql, {
articleId: id,
languageId,
name,
slug,
});
await request(pool, tx)
.input('articleId', id)
.input('companyuserId', companyuserId)
.input('text', `Artikel "${name}" angelegt`)
.query(INSERT_HISTORY);
await tx.commit();
await runner.query(insertHistorySql, {
articleId: id,
companyuserId,
text: `Artikel "${name}" angelegt`,
});
return {
id,
@@ -76,13 +83,15 @@ export default function createUnderParent(pool) {
name,
sku,
};
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
}
throw err;
};
if (tx) {
return runCreate(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runCreate);
}
return runCreate(makeRunner(provider));
};
}

View File

@@ -1,83 +1,60 @@
import {
DELETE_ARTICLELIST,
DELETE_BY_ID,
DELETE_OPTIONSLIST,
DELETE_PICS_BY_ARTICLE,
GET_CHILD_COUNT,
GET_LIST_IDS,
} from '../../providers/mssql/queries/articleDelete.js';
import * as mssqlQueries from '../../providers/mssql/queries/articleDelete.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/articleDelete.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import request from '../request.js';
import makeRunner from '../runner.js';
export default function del(pool) {
const resolve = resolveArticleBySku(pool);
export default function del(provider) {
const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
/**
* Deletes an article by its SKU, refusing when it still has children.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<void>}
* @throws {Error} If the article has children or is not found.
*
* @example
* await store.article.delete(42, 'SKU-1');
*/
return async (googleAuthId, sku) => {
const { articleId } = await resolve(googleAuthId, sku);
return async (googleAuthId, sku, tx) => {
const runDelete = async (runner) => {
const { articleId } = await resolve(googleAuthId, sku, runner);
const childCount = await pool
.request()
.input('articleId', articleId)
.query(GET_CHILD_COUNT);
const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
if (childCount.rows[0].count > 0) {
throw new Error('Article has children');
}
if (childCount.recordset[0].count > 0) {
throw new Error('Article has children');
}
const listIds = await runner.query(queries.GET_LIST_IDS, { articleId });
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
const listIds = await pool
.request()
.input('articleId', articleId)
.query(GET_LIST_IDS);
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId });
const { articlelistId, optionslistId } = listIds.recordset[0] ?? {};
const tx = pool.transaction();
await tx.begin();
try {
await request(pool, tx)
.input('articleId', articleId)
.query(DELETE_PICS_BY_ARTICLE);
const deleted = await request(pool, tx)
.input('articleId', articleId)
.query(DELETE_BY_ID);
if (!deleted.rowsAffected[0]) {
const deleted = await runner.query(queries.DELETE_BY_ID, { articleId });
if (!deleted.rowsAffected) {
throw new Error('Article not found');
}
if (articlelistId != null) {
await request(pool, tx)
.input('id', articlelistId)
.query(DELETE_ARTICLELIST);
await runner.query(queries.DELETE_ARTICLELIST, { id: articlelistId });
}
if (optionslistId != null) {
await request(pool, tx)
.input('id', optionslistId)
.query(DELETE_OPTIONSLIST);
await runner.query(queries.DELETE_OPTIONSLIST, { id: optionslistId });
}
};
await tx.commit();
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
}
throw err;
if (tx) {
return runDelete(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runDelete);
}
return runDelete(makeRunner(provider));
};
}

View File

@@ -1,8 +1,11 @@
import { DELETE_BY_ARTICLE_AND_HASH } from '../../providers/mssql/queries/pic.js';
import { DELETE_BY_ARTICLE_AND_HASH as MSSQL_QUERY } from '../../providers/mssql/queries/pic.js';
import { DELETE_BY_ARTICLE_AND_HASH as SQLITE_QUERY } from '../../providers/sqlite3/queries/pic.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function deleteImage(pool) {
const resolve = resolveArticleBySku(pool);
export default function deleteImage(provider) {
const resolve = resolveArticleBySku(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
/**
* Deletes an image from an article by its SKU and full image hash.
@@ -10,23 +13,29 @@ export default function deleteImage(pool) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {string} fullHash - The full image hash to delete.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<void>}
* @throws {Error} If the image is not found.
*
* @example
* await store.article.deleteImage(42, 'SKU-1', 'abc123');
*/
return async (googleAuthId, sku, fullHash) => {
const { articleId } = await resolve(googleAuthId, sku);
return async (googleAuthId, sku, fullHash, tx) => {
const { articleId } = await resolve(googleAuthId, sku, tx);
const runner = makeRunner(provider, tx);
const result = await runner.query(sql, { articleId, fullHash });
const result = await pool
.request()
.input('articleId', articleId)
.input('fullHash', fullHash)
.query(DELETE_BY_ARTICLE_AND_HASH);
if (!result.rowsAffected[0]) {
if (!result.rowsAffected) {
throw new Error('Image not found');
}
if (provider?.dialect === 'sqlite3') {
if (runner.deletePic) {
await runner.deletePic(fullHash);
} else if (provider?.deletePic) {
await provider.deletePic(fullHash);
}
}
};
}

View File

@@ -1,4 +1,6 @@
import { GET_ARTICLE_BY_SKU_FULL } from '../../providers/mssql/queries/article.js';
import { GET_ARTICLE_BY_SKU_FULL as MSSQL_QUERY } from '../../providers/mssql/queries/article.js';
import { GET_ARTICLE_BY_SKU_FULL as SQLITE_QUERY } from '../../providers/sqlite3/queries/article.js';
import makeRunner from '../runner.js';
function parseJsonArray(raw) {
if (!raw) return [];
@@ -6,13 +8,16 @@ function parseJsonArray(raw) {
return Array.isArray(arr) ? arr : [];
}
export default function getBySku(pool) {
export default function getBySku(provider) {
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
/**
* Returns the full detail of an article by its SKU, including unit, brand,
* categories, photos, EANs and prices.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {name: string, thumbW: number, thumbH: number, thumbHash: string}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>} The full article detail.
* @throws {Error} If the article is not found or the user is not authorized.
*
@@ -20,16 +25,13 @@ export default function getBySku(pool) {
* const article = await store.article.getBySku(42, 'SKU-1');
* // => { article: { id, sku, name, unit, brand, categoryIds, photos, eans, prices, ... } }
*/
return async (googleAuthId, sku) => {
return async (googleAuthId, sku, tx) => {
const t0 = performance.now();
const result = await pool
.request()
.input('sku', sku)
.input('googleAuthId', googleAuthId)
.query(GET_ARTICLE_BY_SKU_FULL);
const runner = makeRunner(provider, tx);
const result = await runner.query(sql, { sku, googleAuthId });
console.log(`[getBySku] single query ${(performance.now() - t0).toFixed(1)}ms`);
const row = result.recordset[0];
const row = result.rows[0];
if (!row) {
throw new Error('Article not found');
}
@@ -43,6 +45,7 @@ export default function getBySku(pool) {
throw new Error('Not authorized');
}
const a = typeof row.articleJson === 'string' ? JSON.parse(row.articleJson) : row.articleJson;
return {

View File

@@ -1,29 +1,30 @@
import { LIST_BY_ARTICLE_ID } from '../../providers/mssql/queries/articlehistory.js';
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY } from '../../providers/mssql/queries/articlehistory.js';
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function getHistoryBySku(pool) {
const resolve = resolveArticleBySku(pool);
export default function getHistoryBySku(provider) {
const resolve = resolveArticleBySku(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
/**
* Returns the change history of an article by its SKU.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
*
* @example
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
* // => [{ id, companyuserId, date, text }]
*/
return async (googleAuthId, sku) => {
const { articleId } = await resolve(googleAuthId, sku);
return async (googleAuthId, sku, tx) => {
const { articleId } = await resolve(googleAuthId, sku, tx);
const runner = makeRunner(provider, tx);
const result = await runner.query(sql, { articleId });
const result = await pool
.request()
.input('articleId', articleId)
.query(LIST_BY_ARTICLE_ID);
return result.recordset.map((row) => ({
return result.rows.map((row) => ({
id: row.id,
companyuserId: row.companyuserId ?? null,
date: row.date,
@@ -31,3 +32,4 @@ export default function getHistoryBySku(pool) {
}));
};
}

View File

@@ -1,24 +1,35 @@
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.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';
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
import {
ADD_EAN,
CREATE as CREATE_ARTICLE,
EAN_EXISTS,
GET_ID_BY_SKU,
SET_FATHER_SORT,
SET_IS_FATHER,
UPDATE_BRAND,
UPDATE_FATHER_BY_SKU,
} from '../../providers/mssql/queries/article.js';
import { ADD as ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
import { CREATE as CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
import { INSERT as INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
import { CREATE as CREATE_BRAND, GET_BY_COMPANY_AND_NAME } from '../../providers/mssql/queries/brand.js';
import { CREATE as CREATE_PRICE } from '../../providers/mssql/queries/price.js';
import { CREATE as CREATE_TAX, GET_BY_RATE } from '../../providers/mssql/queries/tax.js';
import request from '../request.js';
CREATE as MSSQL_CREATE_BRAND,
GET_BY_COMPANY_AND_NAME as MSSQL_GET_BRAND_BY_NAME,
} from '../../providers/mssql/queries/brand.js';
import {
CREATE as SQLITE_CREATE_BRAND,
GET_BY_COMPANY_AND_NAME as SQLITE_GET_BRAND_BY_NAME,
} from '../../providers/sqlite3/queries/brand.js';
import { CREATE as MSSQL_CREATE_PRICE } from '../../providers/mssql/queries/price.js';
import { CREATE as SQLITE_CREATE_PRICE } from '../../providers/sqlite3/queries/price.js';
import {
CREATE as MSSQL_CREATE_TAX,
GET_BY_RATE as MSSQL_GET_TAX_BY_RATE,
} from '../../providers/mssql/queries/tax.js';
import {
CREATE as SQLITE_CREATE_TAX,
GET_BY_RATE as SQLITE_GET_TAX_BY_RATE,
} from '../../providers/sqlite3/queries/tax.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import ensureByPath from '../category/ensureByPath.js';
import ensureDefaultUnit from '../unit/ensureDefault.js';
import makeRunner from '../runner.js';
import slugify from '../slugify.js';
// Normalizes a numeric value that may arrive as a string using German decimal
@@ -34,29 +45,41 @@ function toNumber(value) {
return Number.isFinite(n) ? n : null;
}
export default function importRows(pool) {
const companyAccess = resolveSelectedCompanyAccess(pool);
const ensureUnit = ensureDefaultUnit(pool);
const ensureCategoryByPath = ensureByPath(pool);
export default function importRows(provider) {
const companyAccess = resolveSelectedCompanyAccess(provider);
const ensureUnit = ensureDefaultUnit(provider);
const ensureCategoryByPath = ensureByPath(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
const getBrandByNameSql = isSqlite ? SQLITE_GET_BRAND_BY_NAME : MSSQL_GET_BRAND_BY_NAME;
const createBrandSql = isSqlite ? SQLITE_CREATE_BRAND : MSSQL_CREATE_BRAND;
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
const getTaxByRateSql = isSqlite ? SQLITE_GET_TAX_BY_RATE : MSSQL_GET_TAX_BY_RATE;
const createTaxSql = isSqlite ? SQLITE_CREATE_TAX : MSSQL_CREATE_TAX;
// Find or create a brand for a company by name (within the current tx).
async function ensureBrand(tx, companyId, name) {
const existing = await request(pool, tx)
.input('companyId', companyId)
.input('name', name)
.query(GET_BY_COMPANY_AND_NAME);
async function ensureBrand(runner, companyId, name) {
const existing = await runner.query(getBrandByNameSql, {
companyId,
name,
});
if (existing.recordset[0]) {
return existing.recordset[0].id;
if (existing.rows[0]) {
return existing.rows[0].id;
}
const created = await request(pool, tx)
.input('companyId', companyId)
.input('name', name)
.input('gsprpinfo', null)
.query(CREATE_BRAND);
const created = await runner.query(createBrandSql, {
companyId,
name,
gsprpinfo: null,
});
return created.recordset[0].id;
return created.rows[0]?.id ?? created.insertId;
}
/**
@@ -82,6 +105,7 @@ export default function importRows(pool) {
* @param {Array<string>|string} [input.rows[].eans] - The EANs to set.
* @param {string} [input.rows[].fatherSku] - The father article SKU for a V/K compound link.
* @param {number|string} [input.rows[].deposit] - The deposit (Pfand) value; currently ignored.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>} The import outcome.
*
* @example
@@ -94,14 +118,15 @@ export default function importRows(pool) {
* });
* // => { imported: [...], skipped: [...], errors: [...] }
*/
return async ({ googleAuthId, rows }) => {
return async ({ googleAuthId, rows }, tx) => {
const baseRunner = makeRunner(provider, tx);
const {
companyId,
languageId,
rootCategoryId,
posCustomerGroupId,
companyuserId,
} = await companyAccess(googleAuthId);
} = await companyAccess(googleAuthId, baseRunner);
if (!Array.isArray(rows)) {
throw new Error('Invalid rows');
@@ -112,6 +137,16 @@ export default function importRows(pool) {
const errors = [];
const usedEans = new Set();
const runInTx = async (fn) => {
if (tx) {
return fn(baseRunner);
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(fn);
}
return fn(baseRunner);
};
// Pass 1: create articles (skip existing SKUs).
for (const row of rows) {
const sku = String(row?.sku ?? '').trim();
@@ -122,123 +157,113 @@ export default function importRows(pool) {
continue;
}
const existing = await pool.request().input('sku', sku).query(GET_ID_BY_SKU);
if (existing.recordset[0]) {
const existing = await baseRunner.query(articleQueries.GET_ID_BY_SKU, { sku });
if (existing.rows[0]) {
skipped.push({ sku, name, reason: 'duplicate sku' });
continue;
}
let tx;
try {
tx = pool.transaction();
await tx.begin();
await runInTx(async (runner) => {
const unitId = await ensureUnit(runner, companyId, languageId);
const slug = slugify(name);
const unitId = await ensureUnit(tx, companyId, languageId);
const slug = slugify(name);
const article = await runner.query(articleQueries.CREATE, {
sku,
unitId,
unitamount: 1,
weight: 0,
});
const article = await request(pool, tx)
.input('sku', sku)
.input('unitId', unitId)
.input('unitamount', 1)
.input('weight', 0)
.query(CREATE_ARTICLE);
const articleId = article.rows[0]?.id ?? article.insertId;
const articleId = article.recordset[0].id;
// Resolve the parent category id (explicit id or breadcrumb path).
let parentId = row.parentId;
if (parentId == null && row.parentCategory) {
parentId = await ensureCategoryByPath(
{ googleAuthId, parentId: rootCategoryId, path: row.parentCategory },
tx,
);
}
if (parentId == null) {
throw new Error('Parent category required');
}
await request(pool, tx)
.input('categoryId', parentId)
.input('articleId', articleId)
.query(ADD_CATEGORYARTICLE);
await request(pool, tx)
.input('articleId', articleId)
.input('languageId', languageId)
.input('name', name)
.input('slug', slug)
.query(CREATE_ARTICLETEXT);
await request(pool, tx)
.input('articleId', articleId)
.input('companyuserId', companyuserId)
.input('text', `Artikel "${name}" importiert`)
.query(INSERT_HISTORY);
// Brand (upsert by name).
const brandName = row.brand != null ? String(row.brand).trim() : '';
if (brandName) {
const brandId = await ensureBrand(tx, companyId, brandName);
await request(pool, tx)
.input('articleId', articleId)
.input('brandId', brandId)
.query(UPDATE_BRAND);
}
// POS price (net/tax).
const net = toNumber(row.net);
const taxRate = toNumber(row.tax) ?? 0;
if (net != null) {
const taxResult = await request(pool, tx)
.input('rate', taxRate)
.query(GET_BY_RATE);
let taxId = taxResult.recordset[0]?.id;
if (!taxId) {
const created = await request(pool, tx)
.input('rate', taxRate)
.query(CREATE_TAX);
taxId = created.recordset[0].id;
// Resolve the parent category id (explicit id or breadcrumb path).
let parentId = row.parentId;
if (parentId == null && row.parentCategory) {
parentId = await ensureCategoryByPath(
{ googleAuthId, parentId: rootCategoryId, path: row.parentCategory },
runner,
);
}
if (parentId == null) {
throw new Error('Parent category required');
}
await request(pool, tx)
.input('taxId', taxId)
.input('articleId', articleId)
.input('minmultiplier', 1)
.input('customerGroupId', posCustomerGroupId)
.input('net', net)
.query(CREATE_PRICE);
}
// EANs (array, or a single value; skip duplicates within the import).
const rawEans = row.eans;
const eans = Array.isArray(rawEans)
? rawEans
: rawEans == null || rawEans === ''
? []
: [rawEans];
for (const ean of eans) {
const clean = String(ean ?? '').trim();
if (!clean || usedEans.has(clean)) continue;
const used = await request(pool, tx)
.input('ean', clean)
.query(EAN_EXISTS);
if (used.recordset[0]) continue;
await request(pool, tx)
.input('articleId', articleId)
.input('ean', clean)
.query(ADD_EAN);
usedEans.add(clean);
}
await runner.query(addCategoryArticleSql, {
categoryId: parentId,
articleId,
});
await tx.commit();
imported.push({ id: articleId, sku, name });
await runner.query(createArticleTextSql, {
articleId,
languageId,
name,
slug,
});
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Artikel "${name}" importiert`,
});
// Brand (upsert by name).
const brandName = row.brand != null ? String(row.brand).trim() : '';
if (brandName) {
const brandId = await ensureBrand(runner, companyId, brandName);
await runner.query(articleQueries.UPDATE_BRAND, {
articleId,
brandId,
});
}
// POS price (net/tax).
const net = toNumber(row.net);
const taxRate = toNumber(row.tax) ?? 0;
if (net != null) {
const taxResult = await runner.query(getTaxByRateSql, {
rate: taxRate,
});
let taxId = taxResult.rows[0]?.id;
if (!taxId) {
const created = await runner.query(createTaxSql, {
rate: taxRate,
});
taxId = created.rows[0]?.id ?? created.insertId;
}
await runner.query(createPriceSql, {
taxId,
articleId,
minmultiplier: 1,
customerGroupId: posCustomerGroupId,
net,
});
}
// EANs (array, or a single value; skip duplicates within the import).
const rawEans = row.eans;
const eans = Array.isArray(rawEans)
? rawEans
: rawEans == null || rawEans === ''
? []
: [rawEans];
for (const ean of eans) {
const clean = String(ean ?? '').trim();
if (!clean || usedEans.has(clean)) continue;
const used = await runner.query(articleQueries.EAN_EXISTS, {
ean: clean,
});
if (used.rows[0]) continue;
await runner.query(articleQueries.ADD_EAN, {
articleId,
ean: clean,
});
usedEans.add(clean);
}
imported.push({ id: articleId, sku, name });
});
} catch (err) {
if (tx) {
try {
await tx.rollback();
} catch {
// ignore
}
}
errors.push({ sku, name, error: err.message ?? String(err) });
}
}
@@ -250,50 +275,35 @@ export default function importRows(pool) {
row?.fatherSku != null ? String(row.fatherSku).trim() : '';
if (!sku || !fatherSku) continue;
let tx;
try {
tx = pool.transaction();
await tx.begin();
const child = await request(pool, tx)
.input('sku', sku)
.query(GET_ID_BY_SKU);
const childId = child.recordset[0]?.articleId;
if (childId) {
const father = await request(pool, tx)
.input('sku', fatherSku)
.query(GET_ID_BY_SKU);
const fatherId = father.recordset[0]?.articleId;
if (fatherId && fatherId !== childId) {
await request(pool, tx)
.input('sku', sku)
.input('fatherArticleId', fatherId)
.query(UPDATE_FATHER_BY_SKU);
await request(pool, tx)
.input('articleId', fatherId)
.query(SET_IS_FATHER);
const childCount = await request(pool, tx)
.input('articleId', fatherId)
.query(GET_CHILD_COUNT);
const count = childCount.recordset[0].count;
// The child's sort is its 0-based position among the father's children.
await request(pool, tx)
.input('articleId', childId)
.input('fatherArticleId', fatherId)
.input('fathersort', count - 1)
.query(SET_FATHER_SORT);
await runInTx(async (runner) => {
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku });
const childId = child.rows[0]?.articleId;
if (childId) {
const father = await runner.query(articleQueries.GET_ID_BY_SKU, { sku: fatherSku });
const fatherId = father.rows[0]?.articleId;
if (fatherId && fatherId !== childId) {
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
sku,
fatherArticleId: fatherId,
});
await runner.query(articleQueries.SET_IS_FATHER, {
articleId: fatherId,
});
const childCount = await runner.query(getChildCountSql, {
articleId: fatherId,
});
const count = childCount.rows[0].count;
// The child's sort is its 0-based position among the father's children.
await runner.query(articleQueries.SET_FATHER_SORT, {
articleId: childId,
fatherArticleId: fatherId,
fathersort: count - 1,
});
}
}
}
await tx.commit();
});
} catch {
if (tx) {
try {
await tx.rollback();
} catch {
// ignore
}
}
// Non-fatal: article was already imported in pass 1.
}
}

View File

@@ -1,6 +1,8 @@
import { GET_ARTICLE_EXPORT, GET_CATEGORY_TREE_FLAT } from '../../providers/mssql/queries/article.js';
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import makeRunner from '../runner.js';
const ROOT_CATEGORY_NAME = 'Home';
@@ -10,9 +12,11 @@ function parseJsonArray(raw) {
return Array.isArray(arr) ? arr : [];
}
export default function listExport(pool) {
const companyAccess = resolveSelectedCompanyAccess(pool);
const categoryAccess = resolveCategoryAccess(pool);
export default function listExport(provider) {
const companyAccess = resolveSelectedCompanyAccess(provider);
const categoryAccess = resolveCategoryAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteArticle : mssqlArticle;
/**
* Returns articles for export, optionally scoped to a category, including
@@ -20,6 +24,7 @@ export default function listExport(pool) {
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number|null} [categoryId] - Optional category id to scope the export to; defaults to the company root.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<object>>} The articles for export.
* @throws {Error} If the category is not reachable from the selected company.
*
@@ -27,16 +32,17 @@ export default function listExport(pool) {
* const articles = await store.article.listExport(42, null);
* // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }]
*/
return async (googleAuthId, categoryId) => {
return async (googleAuthId, categoryId, tx) => {
const runner = makeRunner(provider, tx);
const { languageId, posCustomerGroupId, rootCategoryId } =
await companyAccess(googleAuthId);
await companyAccess(googleAuthId, runner);
// Default to the company's root category when no specific category is given.
let exportCategoryId = rootCategoryId;
if (categoryId != null) {
// The requested category must be reachable via the selected company's root.
const access = await categoryAccess(googleAuthId, categoryId);
const access = await categoryAccess(googleAuthId, categoryId, runner);
if (access.rootCategoryId !== rootCategoryId) {
throw new Error('Not authorized');
}
@@ -44,22 +50,20 @@ export default function listExport(pool) {
}
const [result, treeResult] = await Promise.all([
pool
.request()
.input('categoryId', exportCategoryId)
.input('languageId', languageId)
.input('posCustomerGroupId', posCustomerGroupId)
.query(GET_ARTICLE_EXPORT),
pool
.request()
.input('rootId', rootCategoryId)
.input('languageId', languageId)
.query(GET_CATEGORY_TREE_FLAT),
runner.query(queries.GET_ARTICLE_EXPORT, {
categoryId: exportCategoryId,
languageId,
posCustomerGroupId,
}),
runner.query(queries.GET_CATEGORY_TREE_FLAT, {
rootId: rootCategoryId,
languageId,
}),
]);
// id -> { id, parentId, name } for breadcrumb resolution.
const catById = new Map();
for (const node of treeResult.recordset) {
for (const node of treeResult.rows) {
catById.set(String(node.id), node);
}
@@ -67,10 +71,10 @@ export default function listExport(pool) {
const displayName = (node) =>
String(node.id) === String(rootCategoryId) ? ROOT_CATEGORY_NAME : (node.name ?? '').trim();
const breadcrumb = (categoryId) => {
const breadcrumb = (cId) => {
const crumbs = [];
const seen = new Set();
let id = categoryId;
let id = cId;
while (id != null && !seen.has(String(id))) {
seen.add(String(id));
const node = catById.get(String(id));
@@ -82,7 +86,7 @@ export default function listExport(pool) {
return crumbs.join('·');
};
return result.recordset.map((row) => ({
return result.rows.map((row) => ({
id: row.id,
sku: row.sku,
name: row.name,
@@ -102,3 +106,4 @@ export default function listExport(pool) {
}));
};
}

View File

@@ -1,29 +1,33 @@
import { SEARCH_ARTICLES } from '../../providers/mssql/queries/article.js';
import { SEARCH_ARTICLES as MSSQL_SEARCH_ARTICLES } from '../../providers/mssql/queries/article.js';
import { SEARCH_ARTICLES as SQLITE_SEARCH_ARTICLES } from '../../providers/sqlite3/queries/article.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
export default function listSearchIndex(pool) {
const access = resolveSelectedCompanyAccess(pool);
export default function listSearchIndex(provider) {
const access = resolveSelectedCompanyAccess(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SEARCH_ARTICLES : MSSQL_SEARCH_ARTICLES;
/**
* Returns a lightweight search index of all articles for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>} The article search index.
*
* @example
* const articles = await store.article.listSearchIndex(42);
* // => [{ id, sku, name, eans, categoryIds }]
*/
return async (googleAuthId) => {
const { companyId, rootCategoryId, languageId } = await access(googleAuthId);
return async (googleAuthId, tx) => {
const runner = makeRunner(provider, tx);
const { rootCategoryId, languageId } = await access(googleAuthId, runner);
const result = await pool
.request()
.input('rootCategoryId', rootCategoryId)
.input('languageId', languageId)
.query(SEARCH_ARTICLES);
const result = await runner.query(sql, {
rootCategoryId,
languageId,
});
return result.recordset.map((row) => ({
return result.rows.map((row) => ({
id: row.id,
sku: row.sku,
name: row.name,
@@ -34,3 +38,4 @@ export default function listSearchIndex(pool) {
}));
};
}

View File

@@ -1,13 +1,12 @@
import {
LIST_HASHES_BY_ARTICLE,
SET_TEMP_SORTS,
UPDATE_SORT_BY_HASH,
} from '../../providers/mssql/queries/articlepic.js';
import * as mssqlPic from '../../providers/mssql/queries/articlepic.js';
import * as sqlitePic from '../../providers/sqlite3/queries/articlepic.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import request from '../request.js';
import makeRunner from '../runner.js';
export default function orderImages(pool) {
const resolve = resolveArticleBySku(pool);
export default function orderImages(provider) {
const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqlitePic : mssqlPic;
/**
* Reorders the images of an article by its SKU.
@@ -16,6 +15,7 @@ export default function orderImages(pool) {
* @param {string} sku - The article SKU.
* @param {object} input - The reorder input.
* @param {Array<string>} input.fullHashes - The full image hashes in their new order.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{fullHashes: Array<string>}>} The applied order.
* @throws {Error} If the order is invalid.
*
@@ -23,62 +23,53 @@ export default function orderImages(pool) {
* const result = await store.article.orderImages(42, 'SKU-1', { fullHashes: ['a', 'b'] });
* // => { fullHashes: ['a', 'b'] }
*/
return async (googleAuthId, sku, { fullHashes }) => {
return async (googleAuthId, sku, { fullHashes }, tx) => {
if (!Array.isArray(fullHashes)) {
throw new Error('Invalid image order');
}
const { articleId } = await resolve(googleAuthId, sku);
const runOrder = async (runner) => {
const { articleId } = await resolve(googleAuthId, sku, runner);
const current = await pool
.request()
.input('articleId', articleId)
.query(LIST_HASHES_BY_ARTICLE);
const current = await runner.query(queries.LIST_HASHES_BY_ARTICLE, { articleId });
const existing = new Set(current.rows.map((r) => r.fullHash));
const existing = new Set(current.recordset.map((r) => r.fullHash));
if (fullHashes.length !== existing.size) {
throw new Error('Invalid image order');
}
const seen = new Set();
for (const fullHash of fullHashes) {
if (!existing.has(fullHash) || seen.has(fullHash)) {
if (fullHashes.length !== existing.size) {
throw new Error('Invalid image order');
}
seen.add(fullHash);
}
const tx = pool.transaction();
await tx.begin();
const seen = new Set();
for (const fullHash of fullHashes) {
if (!existing.has(fullHash) || seen.has(fullHash)) {
throw new Error('Invalid image order');
}
seen.add(fullHash);
}
try {
await request(pool, tx)
.input('articleId', articleId)
.query(SET_TEMP_SORTS);
await runner.query(queries.SET_TEMP_SORTS, { articleId });
for (let sort = 0; sort < fullHashes.length; sort++) {
const update = await request(pool, tx)
.input('articleId', articleId)
.input('fullHash', fullHashes[sort])
.input('sort', sort)
.query(UPDATE_SORT_BY_HASH);
const update = await runner.query(queries.UPDATE_SORT_BY_HASH, {
articleId,
fullHash: fullHashes[sort],
sort,
});
if (!update.rowsAffected[0]) {
if (!update.rowsAffected) {
throw new Error('Invalid image order');
}
}
await tx.commit();
return { fullHashes };
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
}
throw err;
};
if (tx) {
return runOrder(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runOrder);
}
return runOrder(makeRunner(provider));
};
}

View File

@@ -1,10 +1,6 @@
import {
GET_CATEGORY_IDS_BY_ARTICLE_ID,
GET_DETAIL_BY_ARTICLE_ID,
GET_EANS_BY_ARTICLE_ID,
GET_PHOTOS_BY_ARTICLE_ID,
GET_PRICES_BY_ARTICLE_ID,
} from '../../providers/mssql/queries/articleDetail.js';
import * as mssqlDetail from '../../providers/mssql/queries/articleDetail.js';
import * as sqliteDetail from '../../providers/sqlite3/queries/articleDetail.js';
import makeRunner from '../runner.js';
function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
return {
@@ -41,31 +37,32 @@ function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
};
}
export default function queryBySku(pool) {
return async (articleId, languageId) => {
export default function queryBySku(provider) {
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteDetail : mssqlDetail;
return async (articleId, languageId, tx) => {
const runner = makeRunner(provider, tx);
const [detail, categories, photos, eans, prices] = await Promise.all([
pool
.request()
.input('articleId', articleId)
.input('languageId', languageId)
.query(GET_DETAIL_BY_ARTICLE_ID),
pool.request().input('articleId', articleId).query(GET_CATEGORY_IDS_BY_ARTICLE_ID),
pool.request().input('articleId', articleId).query(GET_PHOTOS_BY_ARTICLE_ID),
pool.request().input('articleId', articleId).query(GET_EANS_BY_ARTICLE_ID),
pool.request().input('articleId', articleId).query(GET_PRICES_BY_ARTICLE_ID),
runner.query(queries.GET_DETAIL_BY_ARTICLE_ID, { articleId, languageId }),
runner.query(queries.GET_CATEGORY_IDS_BY_ARTICLE_ID, { articleId }),
runner.query(queries.GET_PHOTOS_BY_ARTICLE_ID, { articleId }),
runner.query(queries.GET_EANS_BY_ARTICLE_ID, { articleId }),
runner.query(queries.GET_PRICES_BY_ARTICLE_ID, { articleId }),
]);
const row = detail.recordset[0];
const row = detail.rows[0];
if (!row) {
return null;
}
return shapeArticleDetail(
row,
categories.recordset,
photos.recordset,
eans.recordset,
prices.recordset,
categories.rows,
photos.rows,
eans.rows,
prices.rows,
);
};
}

View File

@@ -1,14 +1,16 @@
import { RESOLVE_ARTICLE_ACCESS_BY_SKU } from '../../providers/mssql/queries/article.js';
import { RESOLVE_ARTICLE_ACCESS_BY_SKU as MSSQL_RESOLVE } from '../../providers/mssql/queries/article.js';
import { RESOLVE_ARTICLE_ACCESS_BY_SKU as SQLITE_RESOLVE } from '../../providers/sqlite3/queries/article.js';
import makeRunner from '../runner.js';
export default function resolveArticleBySku(pool) {
return async (googleAuthId, sku) => {
const result = await pool
.request()
.input('sku', sku)
.input('googleAuthId', googleAuthId)
.query(RESOLVE_ARTICLE_ACCESS_BY_SKU);
export default function resolveArticleBySku(provider) {
const sql =
provider?.dialect === 'sqlite3' ? SQLITE_RESOLVE : MSSQL_RESOLVE;
const row = result.recordset[0];
return async (googleAuthId, sku, tx) => {
const runner = makeRunner(provider, tx);
const result = await runner.query(sql, { sku, googleAuthId });
const row = result.rows[0];
if (!row?.articleId || !row?.categoryId) {
throw new Error('Article not found');
}
@@ -30,3 +32,4 @@ export default function resolveArticleBySku(pool) {
};
};
}

View File

@@ -1,13 +1,22 @@
import {
GET_BRAND_BY_COMPANY_AND_ID,
UPDATE_BRAND,
GET_BRAND_BY_COMPANY_AND_ID as MSSQL_GET_BRAND,
UPDATE_BRAND as MSSQL_UPDATE_BRAND,
} from '../../providers/mssql/queries/article.js';
import {
GET_BRAND_BY_COMPANY_AND_ID as SQLITE_GET_BRAND,
UPDATE_BRAND as SQLITE_UPDATE_BRAND,
} from '../../providers/sqlite3/queries/article.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function setBrandIdBySku(pool) {
const resolve = resolveArticleBySku(pool);
const load = queryBySku(pool);
export default function setBrandIdBySku(provider) {
const resolve = resolveArticleBySku(provider);
const load = queryBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const getBrandSql = isSqlite ? SQLITE_GET_BRAND : MSSQL_GET_BRAND;
const updateBrandSql = isSqlite ? SQLITE_UPDATE_BRAND : MSSQL_UPDATE_BRAND;
/**
* Sets (or clears) the brand of an article by its SKU.
@@ -15,6 +24,7 @@ export default function setBrandIdBySku(pool) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {number|null} brandId - The brand id, or `null` to clear the brand.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<object>} The updated article.
* @throws {Error} If the brand or article is not found.
*
@@ -22,10 +32,12 @@ export default function setBrandIdBySku(pool) {
* const article = await store.article.setBrandIdBySku(42, 'SKU-1', 3);
* // => { id, sku, name, ... }
*/
return async (googleAuthId, sku, brandId) => {
return async (googleAuthId, sku, brandId, tx) => {
const runner = makeRunner(provider, tx);
const { articleId, languageId, companyId } = await resolve(
googleAuthId,
sku,
runner,
);
if (brandId !== null && !Number.isFinite(brandId)) {
@@ -34,28 +46,26 @@ export default function setBrandIdBySku(pool) {
let resolvedBrandId = null;
if (brandId !== null) {
const brand = await pool
.request()
.input('brandId', brandId)
.input('companyId', companyId)
.query(GET_BRAND_BY_COMPANY_AND_ID);
if (!brand.recordset[0]) {
const brand = await runner.query(getBrandSql, {
brandId,
companyId,
});
if (!brand.rows[0]) {
throw new Error('Brand not found');
}
resolvedBrandId = brandId;
}
const result = await pool
.request()
.input('articleId', articleId)
.input('brandId', resolvedBrandId)
.query(UPDATE_BRAND);
const result = await runner.query(updateBrandSql, {
articleId,
brandId: resolvedBrandId,
});
if (!result.rowsAffected[0]) {
if (!result.rowsAffected) {
throw new Error('Article not found');
}
const article = await load(articleId, languageId);
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');
}
@@ -63,3 +73,4 @@ export default function setBrandIdBySku(pool) {
return article;
};
}

View File

@@ -1,16 +1,24 @@
import {
ADD_CATEGORY,
DELETE_CATEGORIES_BY_ARTICLE,
ADD_CATEGORY as MSSQL_ADD_CATEGORY,
DELETE_CATEGORIES_BY_ARTICLE as MSSQL_DELETE_CATEGORIES,
} from '../../providers/mssql/queries/article.js';
import request from '../request.js';
import {
ADD_CATEGORY as SQLITE_ADD_CATEGORY,
DELETE_CATEGORIES_BY_ARTICLE as SQLITE_DELETE_CATEGORIES,
} from '../../providers/sqlite3/queries/article.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function setCategoryIdsBySku(pool) {
const resolve = resolveArticleBySku(pool);
const categoryAccess = resolveCategoryAccess(pool);
const load = queryBySku(pool);
export default function setCategoryIdsBySku(provider) {
const resolve = resolveArticleBySku(provider);
const categoryAccess = resolveCategoryAccess(provider);
const load = queryBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const addCategorySql = isSqlite ? SQLITE_ADD_CATEGORY : MSSQL_ADD_CATEGORY;
const deleteCategoriesSql = isSqlite ? SQLITE_DELETE_CATEGORIES : MSSQL_DELETE_CATEGORIES;
/**
* Replaces the category membership of an article by its SKU.
@@ -18,6 +26,7 @@ export default function setCategoryIdsBySku(pool) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {Array<number>} categoryIds - The category ids the article should belong to (at least one).
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<object>} The updated article.
* @throws {Error} If the category ids are invalid or the article is not found.
*
@@ -25,7 +34,7 @@ export default function setCategoryIdsBySku(pool) {
* const article = await store.article.setCategoryIdsBySku(42, 'SKU-1', [5, 9]);
* // => { id, sku, name, ... }
*/
return async (googleAuthId, sku, categoryIds) => {
return async (googleAuthId, sku, categoryIds, tx) => {
if (!Array.isArray(categoryIds)) {
throw new Error('Invalid categoryIds');
}
@@ -42,49 +51,45 @@ export default function setCategoryIdsBySku(pool) {
throw new Error('Article must have at least one category');
}
const { articleId, languageId, companyId } = await resolve(
googleAuthId,
sku,
);
const runSet = async (runner) => {
const { articleId, languageId, companyId } = await resolve(
googleAuthId,
sku,
runner,
);
const tx = pool.transaction();
await tx.begin();
try {
for (const categoryId of normalized) {
// Ensures the category belongs to the same company as the article.
const access = await categoryAccess(googleAuthId, categoryId);
const access = await categoryAccess(googleAuthId, categoryId, runner);
if (access.companyId !== companyId) {
throw new Error('Invalid categoryIds');
}
}
await request(pool, tx)
.input('articleId', articleId)
.query(DELETE_CATEGORIES_BY_ARTICLE);
await runner.query(deleteCategoriesSql, { articleId });
for (const categoryId of normalized) {
await request(pool, tx)
.input('categoryId', categoryId)
.input('articleId', articleId)
.query(ADD_CATEGORY);
await runner.query(addCategorySql, {
categoryId,
articleId,
});
}
await tx.commit();
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');
}
throw err;
}
const article = await load(articleId, languageId);
if (!article) {
throw new Error('Article not found');
}
return article;
};
return article;
if (tx) {
return runSet(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runSet);
}
return runSet(makeRunner(provider));
};
}

View File

@@ -1,14 +1,22 @@
import {
ADD_EAN,
DELETE_EANS_BY_ARTICLE,
ADD_EAN as MSSQL_ADD_EAN,
DELETE_EANS_BY_ARTICLE as MSSQL_DELETE_EANS,
} from '../../providers/mssql/queries/article.js';
import request from '../request.js';
import {
ADD_EAN as SQLITE_ADD_EAN,
DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS,
} from '../../providers/sqlite3/queries/article.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function setEansBySku(pool) {
const resolve = resolveArticleBySku(pool);
const load = queryBySku(pool);
export default function setEansBySku(provider) {
const resolve = resolveArticleBySku(provider);
const load = queryBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const addEanSql = isSqlite ? SQLITE_ADD_EAN : MSSQL_ADD_EAN;
const deleteEansSql = isSqlite ? SQLITE_DELETE_EANS : MSSQL_DELETE_EANS;
/**
* Replaces the EAN list of an article by its SKU.
@@ -16,6 +24,7 @@ export default function setEansBySku(pool) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {Array<string>} eans - The EANs to set (must be unique, non-empty strings).
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<object>} The updated article.
* @throws {Error} If the EANs are invalid or the article is not found.
*
@@ -23,7 +32,7 @@ export default function setEansBySku(pool) {
* const article = await store.article.setEansBySku(42, 'SKU-1', ['4001234567890']);
* // => { id, sku, name, ... }
*/
return async (googleAuthId, sku, eans) => {
return async (googleAuthId, sku, eans, tx) => {
if (!Array.isArray(eans)) {
throw new Error('Invalid eans');
}
@@ -41,38 +50,30 @@ export default function setEansBySku(pool) {
normalized.push(clean);
}
const { articleId, languageId } = await resolve(googleAuthId, sku);
const runSet = async (runner) => {
const { articleId, languageId } = await resolve(googleAuthId, sku, runner);
const tx = pool.transaction();
await tx.begin();
try {
await request(pool, tx)
.input('articleId', articleId)
.query(DELETE_EANS_BY_ARTICLE);
await runner.query(deleteEansSql, { articleId });
for (const ean of normalized) {
await request(pool, tx)
.input('articleId', articleId)
.input('ean', ean)
.query(ADD_EAN);
await runner.query(addEanSql, { articleId, ean });
}
await tx.commit();
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');
}
throw err;
}
const article = await load(articleId, languageId);
if (!article) {
throw new Error('Article not found');
}
return article;
};
return article;
if (tx) {
return runSet(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runSet);
}
return runSet(makeRunner(provider));
};
}

View File

@@ -1,17 +1,18 @@
import {
GET_ID_BY_SKU,
SET_FATHER_SORT,
SET_IS_FATHER,
UPDATE_FATHER_BY_SKU,
} from '../../providers/mssql/queries/article.js';
import { GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
import request from '../request.js';
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function setFatherBySku(pool) {
const resolve = resolveArticleBySku(pool);
const load = queryBySku(pool);
export default function setFatherBySku(provider) {
const resolve = resolveArticleBySku(provider);
const load = queryBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteArticle : mssqlArticle;
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
/**
* Links an article to a father article (V/K compound) by their SKUs.
@@ -19,6 +20,7 @@ export default function setFatherBySku(pool) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The child article SKU.
* @param {string|null} fatherSku - The father article SKU, or `null` to unlink.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<object>} The updated child article.
* @throws {Error} If the article or father is not found, or the link is invalid.
*
@@ -26,73 +28,67 @@ export default function setFatherBySku(pool) {
* const article = await store.article.setFatherBySku(42, '42001190-1', '42001190');
* // => { id, sku, name, fatherSku: '42001190', ... }
*/
return async (googleAuthId, sku, fatherSku) => {
const { articleId, languageId } = await resolve(googleAuthId, sku);
return async (googleAuthId, sku, fatherSku, tx) => {
const runSet = async (runner) => {
const { articleId, languageId } = await resolve(googleAuthId, sku, runner);
if (fatherSku == null || fatherSku === '') {
await request(pool)
.input('sku', sku)
.input('fatherArticleId', null)
.query(UPDATE_FATHER_BY_SKU);
const unlinked = await load(articleId, languageId);
if (!unlinked) {
throw new Error('Article not found');
if (fatherSku == null || fatherSku === '') {
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
sku,
fatherArticleId: null,
});
const unlinked = await load(articleId, languageId, runner);
if (!unlinked) {
throw new Error('Article not found');
}
return unlinked;
}
return unlinked;
}
const father = await pool
.request()
.input('sku', fatherSku)
.query(GET_ID_BY_SKU);
const fatherId = father.recordset[0]?.articleId;
if (!fatherId) {
throw new Error('Father not found');
}
if (fatherId === articleId) {
throw new Error('Invalid father');
}
const father = await runner.query(queries.GET_ID_BY_SKU, { sku: fatherSku });
const fatherId = father.rows[0]?.articleId;
if (!fatherId) {
throw new Error('Father not found');
}
if (fatherId === articleId) {
throw new Error('Invalid father');
}
const tx = pool.transaction();
await tx.begin();
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
sku,
fatherArticleId: fatherId,
});
try {
await request(pool, tx)
.input('sku', sku)
.input('fatherArticleId', fatherId)
.query(UPDATE_FATHER_BY_SKU);
await runner.query(queries.SET_IS_FATHER, {
articleId: fatherId,
});
await request(pool, tx)
.input('articleId', fatherId)
.query(SET_IS_FATHER);
const childCount = await request(pool, tx)
.input('articleId', fatherId)
.query(GET_CHILD_COUNT);
const count = childCount.recordset[0].count;
const childCount = await runner.query(getChildCountSql, {
articleId: fatherId,
});
const count = childCount.rows[0].count;
// The child's sort is its 0-based position among the father's children.
await request(pool, tx)
.input('articleId', articleId)
.input('fatherArticleId', fatherId)
.input('fathersort', count - 1)
.query(SET_FATHER_SORT);
await runner.query(queries.SET_FATHER_SORT, {
articleId,
fatherArticleId: fatherId,
fathersort: count - 1,
});
await tx.commit();
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');
}
throw err;
}
const article = await load(articleId, languageId);
if (!article) {
throw new Error('Article not found');
}
return article;
};
return article;
if (tx) {
return runSet(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runSet);
}
return runSet(makeRunner(provider));
};
}

View File

@@ -1,11 +1,15 @@
import {
CREATE as CREATE_PRICE,
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP,
CREATE as MSSQL_CREATE_PRICE,
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP as MSSQL_DELETE_PRICE,
} from '../../providers/mssql/queries/price.js';
import request from '../request.js';
import {
CREATE as SQLITE_CREATE_PRICE,
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP as SQLITE_DELETE_PRICE,
} from '../../providers/sqlite3/queries/price.js';
import taxEnsure from '../tax/ensure.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
function normalizePrices(prices) {
if (!Array.isArray(prices)) {
@@ -42,10 +46,14 @@ function normalizePrices(prices) {
return normalized;
}
export default function setPricesBySku(pool) {
const resolve = resolveArticleBySku(pool);
const ensureTax = taxEnsure(pool);
const load = queryBySku(pool);
export default function setPricesBySku(provider) {
const resolve = resolveArticleBySku(provider);
const ensureTax = taxEnsure(provider);
const load = queryBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
const deletePriceSql = isSqlite ? SQLITE_DELETE_PRICE : MSSQL_DELETE_PRICE;
/**
* Replaces the price list of an article by its SKU.
@@ -53,6 +61,7 @@ export default function setPricesBySku(pool) {
* @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 {object} [tx] - Optional transaction to run within.
* @returns {Promise<object>} The updated article.
* @throws {Error} If the prices are invalid or the article is not found.
*
@@ -60,58 +69,56 @@ export default function setPricesBySku(pool) {
* const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]);
* // => { id, sku, name, ... }
*/
return async (googleAuthId, sku, prices) => {
return async (googleAuthId, sku, prices, tx) => {
const normalized = normalizePrices(prices);
const { articleId, languageId, posCustomerGroupId } = await resolve(
googleAuthId,
sku,
);
if (!posCustomerGroupId) {
throw new Error('POS customer group required');
}
const runSet = async (runner) => {
const { articleId, languageId, posCustomerGroupId } = await resolve(
googleAuthId,
sku,
runner,
);
if (!posCustomerGroupId) {
throw new Error('POS customer group required');
}
const tx = pool.transaction();
await tx.begin();
try {
const taxIds = new Map();
for (const entry of normalized) {
if (!taxIds.has(entry.tax)) {
taxIds.set(entry.tax, await ensureTax(entry.tax, tx));
taxIds.set(entry.tax, await ensureTax(entry.tax, runner));
}
}
await request(pool, tx)
.input('articleId', articleId)
.input('customerGroupId', posCustomerGroupId)
.query(DELETE_BY_ARTICLE_AND_CUSTOMERGROUP);
await runner.query(deletePriceSql, {
articleId,
customerGroupId: posCustomerGroupId,
});
for (const entry of normalized) {
await request(pool, tx)
.input('taxId', taxIds.get(entry.tax))
.input('articleId', articleId)
.input('minmultiplier', entry.minmultiplier)
.input('customerGroupId', posCustomerGroupId)
.input('net', entry.price)
.query(CREATE_PRICE);
await runner.query(createPriceSql, {
taxId: taxIds.get(entry.tax),
articleId,
minmultiplier: entry.minmultiplier,
customerGroupId: posCustomerGroupId,
net: entry.price,
});
}
await tx.commit();
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');
}
throw err;
}
const article = await load(articleId, languageId);
if (!article) {
throw new Error('Article not found');
}
return article;
};
return article;
if (tx) {
return runSet(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runSet);
}
return runSet(makeRunner(provider));
};
}

View File

@@ -1,10 +1,13 @@
import { UPDATE_WEIGHT } from '../../providers/mssql/queries/article.js';
import { UPDATE_WEIGHT as MSSQL_UPDATE_WEIGHT } from '../../providers/mssql/queries/article.js';
import { UPDATE_WEIGHT as SQLITE_UPDATE_WEIGHT } from '../../providers/sqlite3/queries/article.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function setWeightBySku(pool) {
const resolve = resolveArticleBySku(pool);
const load = queryBySku(pool);
export default function setWeightBySku(provider) {
const resolve = resolveArticleBySku(provider);
const load = queryBySku(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPDATE_WEIGHT : MSSQL_UPDATE_WEIGHT;
/**
* Sets the weight of an article by its SKU.
@@ -12,6 +15,7 @@ export default function setWeightBySku(pool) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {number} weight - The weight (must be >= 0).
* @param {object} [tx] - Optional transaction.
* @returns {Promise<object>} The updated article.
* @throws {Error} If the weight is invalid or the article is not found.
*
@@ -19,24 +23,24 @@ export default function setWeightBySku(pool) {
* const article = await store.article.setWeightBySku(42, 'SKU-1', 1.5);
* // => { id, sku, name, ... }
*/
return async (googleAuthId, sku, weight) => {
return async (googleAuthId, sku, weight, tx) => {
if (!Number.isFinite(weight) || weight < 0) {
throw new Error('Invalid weight');
}
const { articleId, languageId } = await resolve(googleAuthId, sku);
const runner = makeRunner(provider, tx);
const { articleId, languageId } = await resolve(googleAuthId, sku, runner);
const result = await pool
.request()
.input('articleId', articleId)
.input('weight', weight)
.query(UPDATE_WEIGHT);
const result = await runner.query(sql, {
articleId,
weight,
});
if (!result.rowsAffected[0]) {
if (!result.rowsAffected) {
throw new Error('Article not found');
}
const article = await load(articleId, languageId);
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');
}
@@ -44,3 +48,4 @@ export default function setWeightBySku(pool) {
return article;
};
}

View File

@@ -1,15 +1,16 @@
import {
UPDATE_UNIT,
UPDATE_UNITAMOUNT,
UPDATE_UNIT_AND_AMOUNT,
} from '../../providers/mssql/queries/article.js';
import { INSERT as INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { UPDATE_BY_ARTICLE_AND_LANGUAGE } from '../../providers/mssql/queries/articletext.js';
import { GET_DETAIL_BY_ARTICLE_ID } from '../../providers/mssql/queries/articleDetail.js';
import { GET_FULL_BY_SI } from '../../providers/mssql/queries/unit.js';
import request from '../request.js';
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle 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';
import { UPDATE_BY_ARTICLE_AND_LANGUAGE as MSSQL_UPDATE_TEXT } from '../../providers/mssql/queries/articletext.js';
import { UPDATE_BY_ARTICLE_AND_LANGUAGE as SQLITE_UPDATE_TEXT } from '../../providers/sqlite3/queries/articletext.js';
import { GET_DETAIL_BY_ARTICLE_ID as MSSQL_GET_DETAIL } from '../../providers/mssql/queries/articleDetail.js';
import { GET_DETAIL_BY_ARTICLE_ID as SQLITE_GET_DETAIL } from '../../providers/sqlite3/queries/articleDetail.js';
import { GET_FULL_BY_SI as MSSQL_GET_UNIT } from '../../providers/mssql/queries/unit.js';
import { GET_FULL_BY_SI as SQLITE_GET_UNIT } from '../../providers/sqlite3/queries/unit.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
import slugify from '../slugify.js';
function formatUnitLabel(amount, symbol) {
@@ -20,9 +21,16 @@ function formatUnitLabel(amount, symbol) {
return `${Number(amount)}${sym}`;
}
export default function updateBySku(pool) {
const resolve = resolveArticleBySku(pool);
const load = queryBySku(pool);
export default function updateBySku(provider) {
const resolve = resolveArticleBySku(provider);
const load = queryBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
const updateTextSql = isSqlite ? SQLITE_UPDATE_TEXT : MSSQL_UPDATE_TEXT;
const getDetailSql = isSqlite ? SQLITE_GET_DETAIL : MSSQL_GET_DETAIL;
const getUnitSql = isSqlite ? SQLITE_GET_UNIT : MSSQL_GET_UNIT;
/**
* Updates an article's name and/or unit (SI and amount), recording history.
@@ -33,6 +41,7 @@ export default function updateBySku(pool) {
* @param {string} [patch.name] - The new article name.
* @param {string} [patch.unitSi] - The new unit SI code.
* @param {number} [patch.unitamount] - The new unit amount (must be > 0).
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<object>} The updated article.
* @throws {Error} If no valid field is provided or the article is not found.
*
@@ -40,7 +49,7 @@ export default function updateBySku(pool) {
* const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 });
* // => { id, sku, name, ... }
*/
return async (googleAuthId, sku, patch = {}) => {
return async (googleAuthId, sku, patch = {}, tx) => {
const hasName = Object.prototype.hasOwnProperty.call(patch, 'name');
const hasUnitamount = Object.prototype.hasOwnProperty.call(patch, 'unitamount');
const hasUnitSi = Object.prototype.hasOwnProperty.call(patch, 'unitSi');
@@ -57,122 +66,118 @@ export default function updateBySku(pool) {
throw new Error('Invalid unitamount');
}
const { articleId, languageId, companyId, companyuserId } = await resolve(
googleAuthId,
sku,
);
const currentResult = await pool
.request()
.input('articleId', articleId)
.input('languageId', languageId)
.query(GET_DETAIL_BY_ARTICLE_ID);
const current = currentResult.recordset[0];
if (!current) {
throw new Error('Article not found');
}
let unitId;
let newUnitSymbol = current.unitSymbol ?? current.unitSi ?? '';
if (hasUnitSi) {
if (!patch.unitSi?.trim()) {
throw new Error('Unit not found');
}
const unit = await pool
.request()
.input('companyId', companyId)
.input('si', patch.unitSi.trim())
.input('languageId', languageId)
.query(GET_FULL_BY_SI);
const unitRow = unit.recordset[0];
unitId = unitRow?.id;
if (!unitId) {
throw new Error('Unit not found');
}
newUnitSymbol = unitRow.symbol ?? unitRow.si ?? '';
}
const historyLines = [];
if (hasName) {
const oldName = current.name ?? '';
const newName = patch.name.trim();
if (oldName !== newName) {
historyLines.push(`Name geändert von "${oldName}" auf "${newName}"`);
}
}
if (hasUnitSi || hasUnitamount) {
const oldLabel = formatUnitLabel(
current.unitamount,
current.unitSymbol ?? current.unitSi ?? '',
const runUpdate = async (runner) => {
const { articleId, languageId, companyId, companyuserId } = await resolve(
googleAuthId,
sku,
runner,
);
const newLabel = formatUnitLabel(
hasUnitamount ? patch.unitamount : current.unitamount,
newUnitSymbol,
);
if (oldLabel !== newLabel) {
historyLines.push(`Einheit geändert von "${oldLabel}" auf "${newLabel}"`);
const currentResult = await runner.query(getDetailSql, {
articleId,
languageId,
});
const current = currentResult.rows[0];
if (!current) {
throw new Error('Article not found');
}
}
const tx = pool.transaction();
await tx.begin();
let unitId;
let newUnitSymbol = current.unitSymbol ?? current.unitSi ?? '';
if (hasUnitSi) {
if (!patch.unitSi?.trim()) {
throw new Error('Unit not found');
}
const unit = await runner.query(getUnitSql, {
companyId,
si: patch.unitSi.trim(),
languageId,
});
const unitRow = unit.rows[0];
unitId = unitRow?.id;
if (!unitId) {
throw new Error('Unit not found');
}
newUnitSymbol = unitRow.symbol ?? unitRow.si ?? '';
}
const historyLines = [];
if (hasName) {
const oldName = current.name ?? '';
const newName = patch.name.trim();
if (oldName !== newName) {
historyLines.push(`Name geändert von "${oldName}" auf "${newName}"`);
}
}
if (hasUnitSi || hasUnitamount) {
const oldLabel = formatUnitLabel(
current.unitamount,
current.unitSymbol ?? current.unitSi ?? '',
);
const newLabel = formatUnitLabel(
hasUnitamount ? patch.unitamount : current.unitamount,
newUnitSymbol,
);
if (oldLabel !== newLabel) {
historyLines.push(`Einheit geändert von "${oldLabel}" auf "${newLabel}"`);
}
}
try {
if (hasName) {
const slug = slugify(patch.name);
const update = await request(pool, tx)
.input('articleId', articleId)
.input('languageId', languageId)
.input('name', patch.name.trim())
.input('slug', slug)
.query(UPDATE_BY_ARTICLE_AND_LANGUAGE);
const update = await runner.query(updateTextSql, {
articleId,
languageId,
name: patch.name.trim(),
slug,
});
if (!update.rowsAffected[0]) {
if (!update.rowsAffected) {
throw new Error('Article not found');
}
}
if (hasUnitSi && hasUnitamount) {
await request(pool, tx)
.input('articleId', articleId)
.input('unitId', unitId)
.input('unitamount', patch.unitamount)
.query(UPDATE_UNIT_AND_AMOUNT);
await runner.query(articleQueries.UPDATE_UNIT_AND_AMOUNT, {
articleId,
unitId,
unitamount: patch.unitamount,
});
} else if (hasUnitSi) {
await request(pool, tx)
.input('articleId', articleId)
.input('unitId', unitId)
.query(UPDATE_UNIT);
await runner.query(articleQueries.UPDATE_UNIT, {
articleId,
unitId,
});
} else if (hasUnitamount) {
await request(pool, tx)
.input('articleId', articleId)
.input('unitamount', patch.unitamount)
.query(UPDATE_UNITAMOUNT);
await runner.query(articleQueries.UPDATE_UNITAMOUNT, {
articleId,
unitamount: patch.unitamount,
});
}
if (historyLines.length) {
await request(pool, tx)
.input('articleId', articleId)
.input('companyuserId', companyuserId)
.input('text', historyLines.join('\n'))
.query(INSERT_HISTORY);
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: historyLines.join('\n'),
});
}
await tx.commit();
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');
}
throw err;
}
const article = await load(articleId, languageId);
if (!article) {
throw new Error('Article not found');
}
return article;
};
return article;
if (tx) {
return runUpdate(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runUpdate);
}
return runUpdate(makeRunner(provider));
};
}