This commit is contained in:
seb
2026-09-29 18:26:42 +02:00
parent 30820ba8c7
commit 69bd0935cc
14 changed files with 710 additions and 20 deletions

View File

@@ -31,6 +31,10 @@ import articleSetWeightBySku from './store/article/setWeightBySku.js';
import articleSetTrackStockBySku from './store/article/setTrackStockBySku.js';
import articleSetCategoryIdsBySku from './store/article/setCategoryIdsBySku.js';
import articleSetFatherBySku from './store/article/setFatherBySku.js';
import articleToggleFather from './store/article/toggleFather.js';
import articleAddChild from './store/article/addChild.js';
import articleRemoveChild from './store/article/removeChild.js';
import articleListChildren from './store/article/listChildren.js';
import articleImportRows from './store/article/importRows.js';
import categoryEnsureByPath from './store/category/ensureByPath.js';
import unitList from './store/unit/list.js';
@@ -226,6 +230,10 @@ export default class LibDashStore {
setTrackStockBySku: articleSetTrackStockBySku(provider),
setCategoryIdsBySku: articleSetCategoryIdsBySku(provider),
setFatherBySku: articleSetFatherBySku(provider),
toggleFather: articleToggleFather(provider),
addChild: articleAddChild(provider),
removeChild: articleRemoveChild(provider),
listChildren: articleListChildren(provider),
importRows: articleImportRows(provider),
addImage: articleAddImage(provider),
deleteImage: articleDeleteImage(provider),

View File

@@ -102,6 +102,50 @@ SET is_father = 1
WHERE id = @articleId
`;
export const TOGGLE_FATHER = `
UPDATE article
SET is_father = @isFather,
fatherattributename_id = @fatherAttributeNameId
WHERE id = @articleId
`;
export const CREATE_CHILD = `
INSERT INTO article (
company_id, sku, unit_id, unitamount, weight, track_stock, brand_id, is_father, fatherarticle_id, fathersort
)
OUTPUT INSERTED.id
VALUES (
@companyId, @sku, @unitId, @unitamount, @weight, @trackStock, @brandId, 0, @fatherArticleId, @fathersort
)
`;
export const UNLINK_CHILD = `
UPDATE article
SET fatherarticle_id = NULL,
fathersort = 0
WHERE id = @articleId AND fatherarticle_id = @fatherArticleId
`;
export const LIST_CHILDREN_BY_FATHER_ID = `
SELECT
child.id,
child.sku,
child.fathersort,
child.track_stock AS trackStock,
child.unitamount,
child.weight,
at_child.name,
at_child.slug,
av.id AS attributeValueId,
av.value AS attributeValue
FROM article child
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = @languageId
LEFT JOIN articleattributevalue aav ON aav.article_id = child.id
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = @attributeId
WHERE child.fatherarticle_id = @fatherArticleId
ORDER BY child.fathersort ASC, child.id ASC
`;
export const SET_FATHER_SORT = `
UPDATE article
SET fathersort = @fathersort
@@ -406,8 +450,28 @@ SELECT
FROM article child
WHERE child.fatherarticle_id = a.id
) AS childCount,
a.is_father AS isFather,
a.fatherattributename_id AS fatherAttributeNameId,
fan.name AS fatherAttributeName,
fan.exclusive AS fatherAttributeExclusive,
a.fatherarticle_id AS fatherArticleId,
fa.sku AS fatherSku,
(
SELECT
child.id,
child.sku,
child.fathersort,
at_child.name,
av.id AS attributeValueId,
av.value AS attributeValue
FROM article child
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId
LEFT JOIN articleattributevalue aav ON aav.article_id = child.id
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id
WHERE child.fatherarticle_id = a.id
ORDER BY child.fathersort ASC, child.id ASC
FOR JSON PATH
) AS childrenJson,
(
SELECT ca2.category_id
FROM categoryarticle ca2
@@ -510,6 +574,7 @@ SELECT
) first_brand_pic
LEFT JOIN pic bp ON bp.id = first_brand_pic.pic_id
LEFT JOIN article fa ON fa.id = a.fatherarticle_id
LEFT JOIN attributename fan ON fan.id = a.fatherattributename_id
WHERE a.id = auth.articleId
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
) AS articleJson

View File

@@ -323,6 +323,7 @@ BEGIN
optionslist_id INT NULL,
is_father BIT NOT NULL CONSTRAINT DF_article_is_father DEFAULT 0,
fatherarticle_id INT NULL,
fatherattributename_id INT NULL,
fathersort INT NOT NULL CONSTRAINT DF_article_fathersort DEFAULT 0,
created_at DATETIME2 NOT NULL CONSTRAINT DF_article_created_at DEFAULT SYSUTCDATETIME(),
updated_at DATETIME2 NOT NULL CONSTRAINT DF_article_updated_at DEFAULT SYSUTCDATETIME(),
@@ -332,6 +333,7 @@ BEGIN
CONSTRAINT FK_article_articlelist FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE SET NULL,
CONSTRAINT FK_article_optionslist FOREIGN KEY (optionslist_id) REFERENCES optionslist(id) ON DELETE SET NULL,
CONSTRAINT FK_article_fatherarticle FOREIGN KEY (fatherarticle_id) REFERENCES article(id) ON DELETE NO ACTION,
CONSTRAINT FK_article_fatherattributename FOREIGN KEY (fatherattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION,
CONSTRAINT UQ_article_company_sku UNIQUE (company_id, sku)
);
@@ -341,6 +343,14 @@ BEGIN
ON article (fatherarticle_id, fathersort)
WHERE fatherarticle_id IS NOT NULL;
END
ELSE
BEGIN
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'fatherattributename_id')
BEGIN
ALTER TABLE article ADD fatherattributename_id INT NULL;
ALTER TABLE article ADD CONSTRAINT FK_article_fatherattributename FOREIGN KEY (fatherattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION;
END
END
ELSE IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
BEGIN
ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1;
@@ -988,6 +998,9 @@ export const SCHEMA_SCRIPTS = [
INIT_OPTIONSLIST,
INIT_ARTICLELIST,
INIT_BRAND,
INIT_PROPERTYNAME,
INIT_ATTRIBUTENAME,
INIT_ATTRIBUTEVALUE,
INIT_ARTICLE,
INIT_ARTICLEEAN,
INIT_ARTICLEPIC,
@@ -1000,10 +1013,7 @@ export const SCHEMA_SCRIPTS = [
INIT_ARTICLENAME,
INIT_PRICE,
INIT_ARTICLE_SUPPLIER,
INIT_PROPERTYNAME,
INIT_ARTICLEPROPERTY,
INIT_ATTRIBUTENAME,
INIT_ATTRIBUTEVALUE,
INIT_ARTICLEATTRIBUTEVALUE,
INIT_MANIFEST,
INIT_MANIFESTARTICLE,

View File

@@ -406,6 +406,11 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
`ALTER TABLE article ADD COLUMN track_stock INTEGER NOT NULL DEFAULT 1`,
);
}
if (articleCols.length > 0 && !articleCols.includes('fatherattributename_id')) {
this.db.exec(
`ALTER TABLE article ADD COLUMN fatherattributename_id INTEGER REFERENCES attributename(id) ON DELETE NO ACTION`,
);
}
const warehouseCols = this.db.pragma('table_info("warehouse")').map((c) => c.name);
if (warehouseCols.length > 0 && !warehouseCols.includes('sort')) {

View File

@@ -3,12 +3,13 @@ import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'article');
export const {
ADD_CATEGORY, ADD_EAN, ARTICLE_LIST_COLUMNS, CREATE,
ADD_CATEGORY, ADD_EAN, ARTICLE_LIST_COLUMNS, CREATE, CREATE_CHILD,
DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS,
GET_BRAND_BY_COMPANY_AND_ID,
GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU,
LIST_CHILDREN_BY_FATHER_ID,
SET_FATHER_SORT,
SET_IS_FATHER, UPDATE_BRAND, UPDATE_FATHER_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
SET_IS_FATHER, TOGGLE_FATHER, UNLINK_CHILD, UPDATE_BRAND, UPDATE_FATHER_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT,
} = queries;
@@ -133,7 +134,24 @@ SELECT auth.*,
'optionsCount', (SELECT COUNT(*) FROM optionslistarticle WHERE optionslist_id = a.optionslist_id),
'articleListCount', (SELECT COUNT(*) FROM articlelistarticle WHERE articlelist_id = a.articlelist_id),
'childCount', (SELECT COUNT(*) FROM article WHERE fatherarticle_id = a.id),
'isFather', a.is_father,
'fatherAttributeNameId', a.fatherattributename_id,
'fatherAttributeName', fan.name,
'fatherAttributeExclusive', fan.exclusive,
'fatherArticleId', a.fatherarticle_id, 'fatherSku', father.sku,
'childrenJson', json((SELECT COALESCE(json_group_array(json_object(
'id', child.id,
'sku', child.sku,
'fathersort', child.fathersort,
'name', at_child.name,
'attributeValueId', av.id,
'attributeValue', av.value
)), '[]') FROM article child
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId
LEFT JOIN articleattributevalue aav ON aav.article_id = child.id
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id
WHERE child.fatherarticle_id = a.id
ORDER BY child.fathersort ASC, child.id ASC)),
'categoryIdsJson', json(${JSON_CATEGORIES}),
'photosJson', json((SELECT COALESCE(json_group_array(json_object(
'sort', sort, 'fullHash', hash, 'fullW', w, 'fullH', h,
@@ -197,4 +215,5 @@ LEFT JOIN pic bp ON bp.id = (
SELECT pic_id FROM brandpic WHERE brand_id = b.id ORDER BY sort, id LIMIT 1
)
LEFT JOIN article father ON father.id = a.fatherarticle_id
LEFT JOIN attributename fan ON fan.id = a.fatherattributename_id
`;

View File

@@ -223,6 +223,7 @@ CREATE TABLE IF NOT EXISTS article (
is_father INTEGER NOT NULL DEFAULT 0,
fatherarticle_id INTEGER,
fathersort INTEGER NOT NULL DEFAULT 0,
fatherattributename_id INTEGER,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
UNIQUE (company_id, sku),
@@ -231,7 +232,8 @@ CREATE TABLE IF NOT EXISTS article (
FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE SET NULL,
FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE SET NULL,
FOREIGN KEY (optionslist_id) REFERENCES optionslist(id) ON DELETE SET NULL,
FOREIGN KEY (fatherarticle_id) REFERENCES article(id) ON DELETE NO ACTION
FOREIGN KEY (fatherarticle_id) REFERENCES article(id) ON DELETE NO ACTION,
FOREIGN KEY (fatherattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION
);
CREATE UNIQUE INDEX IF NOT EXISTS UQ_article_fatherarticle_fathersort
ON article(fatherarticle_id, fathersort)
@@ -699,6 +701,9 @@ export const SCHEMA_SCRIPTS = [
INIT_OPTIONSLIST,
INIT_ARTICLELIST,
INIT_BRAND,
INIT_PROPERTYNAME,
INIT_ATTRIBUTENAME,
INIT_ATTRIBUTEVALUE,
INIT_ARTICLE,
INIT_ARTICLEEAN,
INIT_ARTICLEPIC,
@@ -711,10 +716,7 @@ export const SCHEMA_SCRIPTS = [
INIT_ARTICLENAME,
INIT_PRICE,
INIT_ARTICLE_SUPPLIER,
INIT_PROPERTYNAME,
INIT_ARTICLEPROPERTY,
INIT_ATTRIBUTENAME,
INIT_ATTRIBUTEVALUE,
INIT_ARTICLEATTRIBUTEVALUE,
INIT_MANIFEST,
INIT_MANIFESTARTICLE,

View File

@@ -0,0 +1,197 @@
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 * as mssqlAttrVal from '../../providers/mssql/queries/attributevalue.js';
import * as sqliteAttrVal from '../../providers/sqlite3/queries/attributevalue.js';
import * as mssqlArticleAttrVal from '../../providers/mssql/queries/articleattributevalue.js';
import * as sqliteArticleAttrVal from '../../providers/sqlite3/queries/articleattributevalue.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 resolveArticleBySku from './resolveArticleBySku.js';
import getBySku from './getBySku.js';
import makeRunner from '../runner.js';
import slugify from '../slugify.js';
export default function addChild(provider) {
const resolve = resolveArticleBySku(provider);
const load = getBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteArticle : mssqlArticle;
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
const attrValQueries = isSqlite ? sqliteAttrVal : mssqlAttrVal;
const articleAttrValQueries = isSqlite ? sqliteArticleAttrVal : mssqlArticleAttrVal;
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;
/**
* Adds a child article to a father article.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {object} options
* @param {number} [options.attributeValueId] - Existing attribute value ID for father's attribute.
* @param {string} [options.value] - String value to find or create under father's attribute.
* @param {string} [options.name] - Custom name for child article (defaults to father name + value).
* @param {number} [options.unitamount] - Optional unitamount override.
* @param {number} [options.weight] - Optional weight override.
* @param {boolean|number} [options.track_stock] - Optional track_stock override.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<object>} The created child article detail.
*/
return async (googleAuthId, fatherSku, options = {}, tx) => {
const run = async (runner) => {
const { articleId: fatherId, companyId, languageId, companyuserId } = await resolve(
googleAuthId,
fatherSku,
runner,
);
// Fetch father article fields
const fatherRes = await runner.query(
'SELECT id, sku, company_id, unit_id, unitamount, weight, track_stock, brand_id, is_father, fatherarticle_id, fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
);
const father = fatherRes.rows[0];
if (!father) {
throw new Error('Father article not found');
}
if (!father.is_father || !father.fatherattributename_id) {
throw new Error('Article is not a father article');
}
// Fetch father's text / name
const fatherTextRes = await runner.query(
'SELECT name FROM articletext WHERE article_id = @fatherId AND language_id = @languageId',
{ fatherId, languageId },
);
const fatherName = fatherTextRes.rows[0]?.name || father.sku;
// Determine attribute value
let finalValueId = null;
let finalValue = null;
if (options.attributeValueId != null) {
const valRes = await runner.query(attrValQueries.GET_BY_ID_AND_COMPANY, {
id: Number(options.attributeValueId),
companyId,
});
const row = valRes.rows[0];
if (!row || row.attributeId !== father.fatherattributename_id) {
throw new Error('Attribute value not found or does not match father attribute');
}
finalValueId = row.id;
finalValue = row.value;
} else if (typeof options.value === 'string' && options.value.trim()) {
const valStr = options.value.trim();
const existingValRes = await runner.query(attrValQueries.GET_BY_ATTRIBUTE_AND_VALUE, {
attributeId: father.fatherattributename_id,
value: valStr,
});
if (existingValRes.rows[0]) {
finalValueId = existingValRes.rows[0].id;
finalValue = existingValRes.rows[0].value;
} else {
const sortRes = await runner.query(attrValQueries.GET_NEXT_SORT, {
attributeId: father.fatherattributename_id,
});
const sort = sortRes.rows[0]?.sort ?? 0;
const createdValRes = await runner.query(attrValQueries.CREATE, {
attributeId: father.fatherattributename_id,
value: valStr,
sort,
});
finalValueId = createdValRes.rows[0]?.id ?? createdValRes.insertId;
finalValue = valStr;
}
} else {
throw new Error('Attribute value is required for child article');
}
// Compute child SKU: father.sku + '-' + finalValueId
const childSku = `${father.sku}-${finalValueId}`;
// Check if child SKU already exists
const existingChildRes = await runner.query(
'SELECT id FROM article WHERE sku = @childSku AND company_id = @companyId',
{ childSku, companyId },
);
if (existingChildRes.rows[0]) {
throw new Error(`Child article "${childSku}" already exists`);
}
// Next fathersort
const countRes = await runner.query(getChildCountSql, { articleId: fatherId });
const fathersort = countRes.rows[0]?.count ?? 0;
// Create child article
const rawTrack = options.track_stock ?? options.trackStock;
const finalTrackStock = rawTrack == null ? father.track_stock : (rawTrack ? 1 : 0);
const childRes = await runner.query(queries.CREATE_CHILD, {
companyId,
sku: childSku,
unitId: father.unit_id,
unitamount: options.unitamount ?? father.unitamount,
weight: options.weight ?? father.weight,
trackStock: finalTrackStock,
brandId: father.brand_id,
fatherArticleId: fatherId,
fathersort,
});
const childId = childRes.rows[0]?.id ?? childRes.insertId;
// Link articleattributevalue
await runner.query(articleAttrValQueries.INSERT_ROW, {
articleId: childId,
attributeValueId: finalValueId,
sort: 0,
});
// Copy father's categories
const catRes = await runner.query(
'SELECT category_id FROM categoryarticle WHERE article_id = @fatherId',
{ fatherId },
);
for (const catRow of catRes.rows) {
await runner.query(addCategoryArticleSql, {
categoryId: catRow.category_id,
articleId: childId,
});
}
// Create article text
const childName = options.name?.trim() || `${fatherName} - ${finalValue}`;
const childSlug = slugify(childName);
await runner.query(createArticleTextSql, {
articleId: childId,
languageId,
name: childName,
slug: childSlug,
});
// History
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" angelegt`,
});
return load(googleAuthId, childSku, runner);
};
if (tx) {
return run(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(run);
}
return run(makeRunner(provider));
};
}

View File

@@ -99,8 +99,20 @@ export default function getBySku(provider) {
optionsCount: a.optionsCount,
articleListCount: a.articleListCount,
childCount: a.childCount,
isFather: Boolean(a.isFather),
fatherAttributeNameId: a.fatherAttributeNameId ?? null,
fatherAttributeName: a.fatherAttributeName ?? null,
fatherAttributeExclusive: a.fatherAttributeExclusive != null ? Boolean(a.fatherAttributeExclusive) : null,
fatherArticleId: a.fatherArticleId,
fatherSku: a.fatherSku,
children: parseJsonArray(a.childrenJson).map((c) => ({
id: c.id,
sku: c.sku,
fathersort: c.fathersort,
name: c.name ?? null,
attributeValueId: c.attributeValueId ?? null,
attributeValue: c.attributeValue ?? null,
})),
},
};
};

View File

@@ -0,0 +1,64 @@
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function listChildren(provider) {
const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteArticle : mssqlArticle;
/**
* Lists all children of a father article.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, sku: string, fathersort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>}
*/
return async (googleAuthId, fatherSku, tx) => {
const run = async (runner) => {
const { articleId: fatherId, languageId } = await resolve(
googleAuthId,
fatherSku,
runner,
);
const fatherRes = await runner.query(
'SELECT is_father, fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
);
const father = fatherRes.rows[0];
if (!father || !father.is_father) {
return [];
}
const result = await runner.query(queries.LIST_CHILDREN_BY_FATHER_ID, {
fatherArticleId: fatherId,
languageId,
attributeId: father.fatherattributename_id,
});
return result.rows.map((row) => ({
id: row.id,
sku: row.sku,
fathersort: Number(row.fathersort ?? 0),
trackStock: Boolean(row.trackStock ?? 1),
unitamount: Number(row.unitamount ?? 1),
weight: Number(row.weight ?? 0),
name: row.name ?? null,
slug: row.slug ?? null,
attributeValueId: row.attributeValueId ?? null,
attributeValue: row.attributeValue ?? null,
}));
};
if (tx) {
return run(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(run);
}
return run(makeRunner(provider));
};
}

View File

@@ -0,0 +1,105 @@
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 resolveArticleBySku from './resolveArticleBySku.js';
import articleDelete from './delete.js';
import makeRunner from '../runner.js';
export default function removeChild(provider) {
const resolve = resolveArticleBySku(provider);
const del = articleDelete(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteArticle : mssqlArticle;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Removes a child article from a father article, either by unlinking or deleting it.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {string} childSku - The child article SKU.
* @param {object} [options]
* @param {boolean} [options.deleteArticle=false] - Whether to delete the child article entity entirely.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>}
*/
return async (googleAuthId, fatherSku, childSku, options = {}, tx) => {
const run = async (runner) => {
const { articleId: fatherId, companyuserId } = await resolve(
googleAuthId,
fatherSku,
runner,
);
const { articleId: childId } = await resolve(googleAuthId, childSku, runner);
// Verify child relationship
const childRes = await runner.query(
'SELECT fatherarticle_id FROM article WHERE id = @childId',
{ childId },
);
const child = childRes.rows[0];
if (!child || child.fatherarticle_id !== fatherId) {
throw new Error('Child article does not belong to this father');
}
if (options.deleteArticle) {
await del(googleAuthId, childSku, runner);
return {
success: true,
fatherSku,
childSku,
deleted: true,
};
}
// Fetch father's attribute name id
const fatherRes = await runner.query(
'SELECT fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
);
const fatherAttributeNameId = fatherRes.rows[0]?.fatherattributename_id;
// Unlink child from father
await runner.query(queries.UNLINK_CHILD, {
articleId: childId,
fatherArticleId: fatherId,
});
// Remove the child's attribute value link for the father's attribute
if (fatherAttributeNameId) {
await runner.query(
`DELETE FROM articleattributevalue
WHERE article_id = @childId
AND attributevalue_id IN (
SELECT id FROM attributevalue WHERE attribute_id = @fatherAttributeNameId
)`,
{ childId, fatherAttributeNameId },
);
}
// Add history
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childSku}" von Vaterartikel "${fatherSku}" getrennt`,
});
return {
success: true,
fatherSku,
childSku,
deleted: false,
};
};
if (tx) {
return run(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(run);
}
return run(makeRunner(provider));
};
}

View File

@@ -0,0 +1,93 @@
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 * as mssqlAttr from '../../providers/mssql/queries/attributename.js';
import * as sqliteAttr from '../../providers/sqlite3/queries/attributename.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import getBySku from './getBySku.js';
import makeRunner from '../runner.js';
export default function toggleFather(provider) {
const resolve = resolveArticleBySku(provider);
const load = getBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteArticle : mssqlArticle;
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
const attrQueries = isSqlite ? sqliteAttr : mssqlAttr;
/**
* Toggles an article into or out of father mode.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {object} options
* @param {boolean} options.isFather - Whether the article should be a father article.
* @param {number} [options.attributeNameId] - The attribute name id required when enabling father mode.
* @param {number} [options.fatherAttributeNameId] - Alias for attributeNameId.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<object>} The updated article detail.
*/
return async (googleAuthId, sku, { isFather, attributeNameId, fatherAttributeNameId } = {}, tx) => {
const run = async (runner) => {
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
const targetIsFather = Boolean(isFather);
const targetAttrId = attributeNameId ?? fatherAttributeNameId;
if (targetIsFather) {
if (!targetAttrId || Number(targetAttrId) <= 0) {
throw new Error('attributeNameId is required to become a father article');
}
// Verify article is not a child of another article
const currentRes = await runner.query(
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
{ articleId },
);
if (currentRes.rows[0]?.fatherarticle_id != null) {
throw new Error('A child article cannot become a father');
}
// Verify attribute exists and belongs to company
const attrRes = await runner.query(attrQueries.GET_BY_COMPANY_AND_ID, {
id: Number(targetAttrId),
companyId,
});
if (!attrRes.rows[0]) {
throw new Error('Attribute name not found');
}
await runner.query(queries.TOGGLE_FATHER, {
articleId,
isFather: 1,
fatherAttributeNameId: Number(targetAttrId),
});
} else {
// Disabling father: verify zero children
const childCountRes = await runner.query(getChildCountSql, { articleId });
const childCount = childCountRes.rows[0]?.count ?? 0;
if (childCount > 0) {
throw new Error('Cannot disable father mode on an article that still has children');
}
await runner.query(queries.TOGGLE_FATHER, {
articleId,
isFather: 0,
fatherAttributeNameId: null,
});
}
return load(googleAuthId, sku, runner);
};
if (tx) {
return run(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(run);
}
return run(makeRunner(provider));
};
}