feat(article): add article import with V/K compound linking

Add article.importRows to create articles by SKU from CSV-style rows,
running in two passes so fatherSku links to rows later in the file still
resolve. Existing SKUs are skipped, duplicate EANs are dropped, prices and
taxes are ensured, brands are upserted by name, and parent categories are
resolved/created via breadcrumb path.

Also add:
- article.setFatherBySku to link/unlink a child to its father article
- category.ensureByPath to resolve or create categories by path (tx-aware)
- supporting SQL constants (GET_ID_BY_SKU, EAN_EXISTS, father-link helpers,
  GET_BY_PARENT_AND_NAME)
- regenerated API docs
This commit is contained in:
seb
2026-08-11 05:36:07 +02:00
parent fb88cb92a5
commit af416246b4
8 changed files with 655 additions and 0 deletions

View File

@@ -160,6 +160,48 @@ const history = await store.article.getHistoryBySku(42, 'SKU-1');
// => [{ id, companyuserId, date, text }] // => [{ id, companyuserId, date, text }]
``` ```
## `article.importRows`
*Source: `src/store/article/importRows.js`*
Imports article rows, creating articles by their SKU and returning per-row import, skip and error outcomes. The import runs in **two passes** so a `fatherSku` that references a row appearing later in the file still resolves. A row is **skipped** when its SKU already exists in the company (upsert mode is not applied), and an EAN is skipped when already used by another row.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `input` | `object` | The import input. |
| `input.googleAuthId` | `number` | The id of the Google auth user. |
| `input.rows` | `Array<object>` | The article rows to import. |
| `input.rows[].sku` | `string` | The article SKU. |
| `input.rows[].name` | `string` | The article name. |
| `[input.rows[].vk]` | `string` | `'V'` (father/compound) or `'K'` (variant) marker. |
| `[input.rows[].parentId]` | `number|string` | The id of the parent category. |
| `[input.rows[].parentCategory]` | `string` | The category breadcrumb path, e.g. `'Flowers | CBN'`. |
| `[input.rows[].brand]` | `string` | The brand name to upsert. |
| `[input.rows[].net]` | `number|string` | The POS net price (accepts German decimals). |
| `[input.rows[].tax]` | `number|string` | The tax rate in percent (e.g. `19`). |
| `[input.rows[].eans]` | `Array<string>|string` | The EANs to set. |
| `[input.rows[].fatherSku]` | `string` | The father article SKU for a V/K compound link. |
| `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. |
**Returns**
`Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>` — The import outcome.
**Example**
```js
const result = await store.article.importRows({
googleAuthId: 42,
rows: [
{ sku: '42001190-1', name: '420Strainz 1g', vk: 'K', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '11,00', tax: 19, eans: ['4262557690455'], fatherSku: '42001190' },
{ sku: '42001190', name: '420Strainz', vk: 'V', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '0,00', tax: 19 },
],
});
// => { imported: [...], skipped: [...], errors: [...] }
```
## `article.listExport` ## `article.listExport`
*Source: `src/store/article/listExport.js`* *Source: `src/store/article/listExport.js`*
@@ -328,6 +370,35 @@ const article = await store.article.setEansBySku(42, 'SKU-1', ['4001234567890'])
// => { id, sku, name, ... } // => { id, sku, name, ... }
``` ```
## `article.setFatherBySku`
*Source: `src/store/article/setFatherBySku.js`*
Links an article to a father article (V/K compound) by their SKUs.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `sku` | `string` | The child article SKU. |
| `fatherSku` | `string|null` | The father article SKU, or `null` to unlink. |
**Returns**
`Promise<object>` — The updated child article.
**Throws**
`Error` — If the article or father is not found, or the link is invalid.
**Example**
```js
const article = await store.article.setFatherBySku(42, '42001190-1', '42001190');
// => { id, sku, name, fatherSku: '42001190', ... }
```
## `article.setPricesBySku` ## `article.setPricesBySku`
*Source: `src/store/article/setPricesBySku.js`* *Source: `src/store/article/setPricesBySku.js`*

View File

@@ -109,6 +109,33 @@ const result = await store.category.deleteImage(42, 5);
// => { id: 5, thumbW: null, thumbH: null, thumbHash: null } // => { id: 5, thumbW: null, thumbH: null, thumbHash: null }
``` ```
## `category.ensureByPath`
*Source: `src/store/category/ensureByPath.js`*
Resolves a category by breadcrumb path under a given parent, creating any missing categories along the way. Returns the leaf category id.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `input` | `object` | The category resolution input. |
| `input.googleAuthId` | `number` | The id of the Google auth user. |
| `input.parentId` | `number` | The id of the parent category to start from. |
| `input.path` | `string` | The breadcrumb path, e.g. `'Haupt | Flowers | CBN'`. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<number>` — The id of the leaf category.
**Example**
```js
const id = await store.category.ensureByPath({ googleAuthId: 42, parentId: 5, path: 'Haupt | Flowers | CBN' });
// => 12
```
## `category.getCategoryArticles` ## `category.getCategoryArticles`
*Source: `src/store/category/getCategoryArticles.js`* *Source: `src/store/category/getCategoryArticles.js`*

View File

@@ -29,6 +29,9 @@ import articleSetEansBySku from './store/article/setEansBySku.js';
import articleSetBrandIdBySku from './store/article/setBrandIdBySku.js'; import articleSetBrandIdBySku from './store/article/setBrandIdBySku.js';
import articleSetWeightBySku from './store/article/setWeightBySku.js'; import articleSetWeightBySku from './store/article/setWeightBySku.js';
import articleSetCategoryIdsBySku from './store/article/setCategoryIdsBySku.js'; import articleSetCategoryIdsBySku from './store/article/setCategoryIdsBySku.js';
import articleSetFatherBySku from './store/article/setFatherBySku.js';
import articleImportRows from './store/article/importRows.js';
import categoryEnsureByPath from './store/category/ensureByPath.js';
import unitList from './store/unit/list.js'; import unitList from './store/unit/list.js';
import unitUpsert from './store/unit/upsert.js'; import unitUpsert from './store/unit/upsert.js';
import unitDelete from './store/unit/delete.js'; import unitDelete from './store/unit/delete.js';
@@ -129,6 +132,7 @@ export default class LibDashStore {
getTreeForCompany: categoryGetTreeForCompany(pool), getTreeForCompany: categoryGetTreeForCompany(pool),
getCategoryArticles: categoryGetCategoryArticles(pool), getCategoryArticles: categoryGetCategoryArticles(pool),
createUnderParent: categoryCreateUnderParent(pool), createUnderParent: categoryCreateUnderParent(pool),
ensureByPath: categoryEnsureByPath(pool),
rename: categoryRename(pool), rename: categoryRename(pool),
move: categoryMove(pool), move: categoryMove(pool),
order: categoryOrder(pool), order: categoryOrder(pool),
@@ -149,6 +153,8 @@ export default class LibDashStore {
setBrandIdBySku: articleSetBrandIdBySku(pool), setBrandIdBySku: articleSetBrandIdBySku(pool),
setWeightBySku: articleSetWeightBySku(pool), setWeightBySku: articleSetWeightBySku(pool),
setCategoryIdsBySku: articleSetCategoryIdsBySku(pool), setCategoryIdsBySku: articleSetCategoryIdsBySku(pool),
setFatherBySku: articleSetFatherBySku(pool),
importRows: articleImportRows(pool),
addImage: articleAddImage(pool), addImage: articleAddImage(pool),
deleteImage: articleDeleteImage(pool), deleteImage: articleDeleteImage(pool),
orderImages: articleOrderImages(pool), orderImages: articleOrderImages(pool),

View File

@@ -59,6 +59,39 @@ INNER JOIN categoryarticle ca ON ca.article_id = a.id
WHERE a.sku = @sku 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. */ /** 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 ( WITH article_cat AS (

View File

@@ -92,6 +92,16 @@ FROM category
WHERE parent_id = @parentId WHERE parent_id = @parentId
`; `;
/** Resolve a child category by its parent id and name (language-sensitive). */
export const GET_BY_PARENT_AND_NAME = `
SELECT c.id
FROM category c
INNER JOIN categorytext ct
ON ct.category_id = c.id AND ct.language_id = @languageId
WHERE c.parent_id = @parentId
AND ct.name = @name
`;
export const LIST_CHILD_IDS = ` export const LIST_CHILD_IDS = `
SELECT id SELECT id
FROM category FROM category

View File

@@ -0,0 +1,303 @@
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 '../../sql/article.js';
import { ADD as ADD_CATEGORYARTICLE } from '../../sql/categoryarticle.js';
import { CREATE as CREATE_ARTICLETEXT } from '../../sql/articletext.js';
import { INSERT as INSERT_HISTORY } from '../../sql/articlehistory.js';
import { GET_CHILD_COUNT } from '../../sql/articleDelete.js';
import { CREATE as CREATE_BRAND, GET_BY_COMPANY_AND_NAME } from '../../sql/brand.js';
import { CREATE as CREATE_PRICE } from '../../sql/price.js';
import { CREATE as CREATE_TAX, GET_BY_RATE } from '../../sql/tax.js';
import request from '../request.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import ensureByPath from '../category/ensureByPath.js';
import ensureDefaultUnit from '../unit/ensureDefault.js';
import slugify from '../slugify.js';
// Normalizes a numeric value that may arrive as a string using German decimal
// notation (e.g. "11,00"), a JS number, or null/undefined. Returns null when empty.
function toNumber(value) {
if (value === null || value === undefined) return null;
if (typeof value === 'number') {
return Number.isFinite(value) ? value : null;
}
const s = String(value).trim();
if (s === '') return null;
const n = Number(s.replace(',', '.'));
return Number.isFinite(n) ? n : null;
}
export default function importRows(pool) {
const companyAccess = resolveSelectedCompanyAccess(pool);
const ensureUnit = ensureDefaultUnit(pool);
const ensureCategoryByPath = ensureByPath(pool);
// 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);
if (existing.recordset[0]) {
return existing.recordset[0].id;
}
const created = await request(pool, tx)
.input('companyId', companyId)
.input('name', name)
.input('gsprpinfo', null)
.query(CREATE_BRAND);
return created.recordset[0].id;
}
/**
* Imports article rows, creating articles by their SKU and returning per-row
* import, skip and error outcomes.
*
* The import runs in **two passes** so a `fatherSku` that references a row
* appearing later in the file still resolves. A row is **skipped** when its
* SKU already exists in the company (upsert mode is not applied), and an EAN
* is skipped when already used by another row.
*
* @param {object} input - The import input.
* @param {number} input.googleAuthId - The id of the Google auth user.
* @param {Array<object>} input.rows - The article rows to import.
* @param {string} input.rows[].sku - The article SKU.
* @param {string} input.rows[].name - The article name.
* @param {string} [input.rows[].vk] - `'V'` (father/compound) or `'K'` (variant) marker.
* @param {number|string} [input.rows[].parentId] - The id of the parent category.
* @param {string} [input.rows[].parentCategory] - The category breadcrumb path, e.g. `'Flowers | CBN'`.
* @param {string} [input.rows[].brand] - The brand name to upsert.
* @param {number|string} [input.rows[].net] - The POS net price (accepts German decimals).
* @param {number|string} [input.rows[].tax] - The tax rate in percent (e.g. `19`).
* @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.
* @returns {Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>} The import outcome.
*
* @example
* const result = await store.article.importRows({
* googleAuthId: 42,
* rows: [
* { sku: '42001190-1', name: '420Strainz 1g', vk: 'K', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '11,00', tax: 19, eans: ['4262557690455'], fatherSku: '42001190' },
* { sku: '42001190', name: '420Strainz', vk: 'V', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '0,00', tax: 19 },
* ],
* });
* // => { imported: [...], skipped: [...], errors: [...] }
*/
return async ({ googleAuthId, rows }) => {
const {
companyId,
languageId,
rootCategoryId,
posCustomerGroupId,
companyuserId,
} = await companyAccess(googleAuthId);
if (!Array.isArray(rows)) {
throw new Error('Invalid rows');
}
const imported = [];
const skipped = [];
const errors = [];
const usedEans = new Set();
// Pass 1: create articles (skip existing SKUs).
for (const row of rows) {
const sku = String(row?.sku ?? '').trim();
const name = String(row?.name ?? '').trim();
if (!sku || !name) {
errors.push({ sku: sku || null, name: name || null, error: 'Invalid row' });
continue;
}
const existing = await pool.request().input('sku', sku).query(GET_ID_BY_SKU);
if (existing.recordset[0]) {
skipped.push({ sku, name, reason: 'duplicate sku' });
continue;
}
let tx;
try {
tx = pool.transaction();
await tx.begin();
const unitId = await ensureUnit(tx, companyId, languageId);
const slug = slugify(name);
const article = await request(pool, tx)
.input('sku', sku)
.input('unitId', unitId)
.input('unitamount', 1)
.input('weight', 0)
.query(CREATE_ARTICLE);
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;
}
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 tx.commit();
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) });
}
}
// Pass 2: wire V/K father links (fathers may be created in pass 1).
for (const row of rows) {
const sku = String(row?.sku ?? '').trim();
const fatherSku =
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 tx.commit();
} catch {
if (tx) {
try {
await tx.rollback();
} catch {
// ignore
}
}
// Non-fatal: article was already imported in pass 1.
}
}
return { imported, skipped, errors };
};
}

View File

@@ -0,0 +1,98 @@
import {
GET_ID_BY_SKU,
SET_FATHER_SORT,
SET_IS_FATHER,
UPDATE_FATHER_BY_SKU,
} from '../../sql/article.js';
import { GET_CHILD_COUNT } from '../../sql/articleDelete.js';
import request from '../request.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
export default function setFatherBySku(pool) {
const resolve = resolveArticleBySku(pool);
const load = queryBySku(pool);
/**
* Links an article to a father article (V/K compound) by their SKUs.
*
* @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.
* @returns {Promise<object>} The updated child article.
* @throws {Error} If the article or father is not found, or the link is invalid.
*
* @example
* 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);
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');
}
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 tx = pool.transaction();
await tx.begin();
try {
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', articleId)
.input('fatherArticleId', fatherId)
.input('fathersort', count - 1)
.query(SET_FATHER_SORT);
await tx.commit();
} catch (err) {
try {
await tx.rollback();
} catch {
// ignore
}
throw err;
}
const article = await load(articleId, languageId);
if (!article) {
throw new Error('Article not found');
}
return article;
};
}

View File

@@ -0,0 +1,107 @@
import {
CREATE,
GET_BY_PARENT_AND_NAME,
GET_NEXT_SIBLING_SORT,
} from '../../sql/category.js';
import { CREATE as CREATE_CATEGORYTEXT } from '../../sql/categorytext.js';
import resolveCategoryAccess from './resolveCategoryAccess.js';
import request from '../request.js';
import slugify from '../slugify.js';
// Splits a "A | B | C" breadcrumb string into trimmed, non-empty name parts,
// ordered root-first.
export function splitPath(path) {
return String(path ?? '')
.split('|')
.map((part) => part.trim())
.filter(Boolean);
}
export default function ensureByPath(pool) {
const access = resolveCategoryAccess(pool);
/**
* Resolves a category by breadcrumb path under a given parent, creating any
* missing categories along the way. Returns the leaf category id.
*
* @param {object} input - The category resolution input.
* @param {number} input.googleAuthId - The id of the Google auth user.
* @param {number} input.parentId - The id of the parent category to start from.
* @param {string} input.path - The breadcrumb path, e.g. `'Haupt | Flowers | CBN'`.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<number>} The id of the leaf category.
*
* @example
* const id = await store.category.ensureByPath({ googleAuthId: 42, parentId: 5, path: 'Haupt | Flowers | CBN' });
* // => 12
*/
return async ({ googleAuthId, parentId, path }, tx) => {
const names = splitPath(path);
if (names.length === 0) {
throw new Error('Invalid category path');
}
let currentId = parentId;
const createCategory = async (tx, parentId, name, languageId) => {
const sortResult = await request(pool, tx)
.input('parentId', parentId)
.query(GET_NEXT_SIBLING_SORT);
const sort = sortResult.recordset[0].sort;
const slug = slugify(name);
const category = await request(pool, tx)
.input('parentId', parentId)
.input('sort', sort)
.query(CREATE);
const id = category.recordset[0].id;
await request(pool, tx)
.input('categoryId', id)
.input('languageId', languageId)
.input('name', name)
.input('slug', slug)
.query(CREATE_CATEGORYTEXT);
return id;
};
for (const name of names) {
const { languageId } = await access(googleAuthId, currentId);
const existing = await request(pool, tx)
.input('parentId', currentId)
.input('languageId', languageId)
.input('name', name)
.query(GET_BY_PARENT_AND_NAME);
if (existing.recordset[0]) {
currentId = existing.recordset[0].id;
continue;
}
if (tx) {
// Run within the caller's transaction (e.g. article import row).
currentId = await createCategory(tx, currentId, name, languageId);
continue;
}
// Standalone use: own transaction per created category.
const ownTx = pool.transaction();
await ownTx.begin();
try {
currentId = await createCategory(ownTx, currentId, name, languageId);
await ownTx.commit();
} catch (err) {
try {
await ownTx.rollback();
} catch {
// ignore
}
throw err;
}
}
return currentId;
};
}