From 69bd0935cc51d3201a3b365046e8c46d4d604127 Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 29 Sep 2026 18:26:42 +0200 Subject: [PATCH] u --- API.article.md | 86 ++++++++++ API.md | 31 ++-- generate-api-doc.js | 15 +- src/index.js | 8 + src/providers/mssql/queries/article.js | 65 ++++++++ src/providers/mssql/schema.js | 16 +- src/providers/sqlite3/index.js | 5 + src/providers/sqlite3/queries/article.js | 23 ++- src/providers/sqlite3/schema.js | 10 +- src/store/article/addChild.js | 197 +++++++++++++++++++++++ src/store/article/getBySku.js | 12 ++ src/store/article/listChildren.js | 64 ++++++++ src/store/article/removeChild.js | 105 ++++++++++++ src/store/article/toggleFather.js | 93 +++++++++++ 14 files changed, 710 insertions(+), 20 deletions(-) create mode 100644 src/store/article/addChild.js create mode 100644 src/store/article/listChildren.js create mode 100644 src/store/article/removeChild.js create mode 100644 src/store/article/toggleFather.js diff --git a/API.article.md b/API.article.md index 031b5c9..0ed7296 100644 --- a/API.article.md +++ b/API.article.md @@ -2,6 +2,31 @@ This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. +## `article.addChild` + +*Source: `src/store/article/addChild.js`* + +Adds a child article to a father article. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `fatherSku` | `string` | The father article SKU. | +| `options` | `object` | | +| `[options.attributeValueId]` | `number` | Existing attribute value ID for father's attribute. | +| `[options.value]` | `string` | String value to find or create under father's attribute. | +| `[options.name]` | `string` | Custom name for child article (defaults to father name + value). | +| `[options.unitamount]` | `number` | Optional unitamount override. | +| `[options.weight]` | `number` | Optional weight override. | +| `[options.track_stock]` | `boolean|number` | Optional track_stock override. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise` — The created child article detail. + ## `article.addImage` *Source: `src/store/article/addImage.js`* @@ -211,6 +236,24 @@ const result = await store.article.importRows({ // => { imported: [...], skipped: [...], errors: [...] } ``` +## `article.listChildren` + +*Source: `src/store/article/listChildren.js`* + +Lists all children of a father article. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `fatherSku` | `string` | The father article SKU. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` + ## `article.listExport` *Source: `src/store/article/listExport.js`* @@ -295,6 +338,27 @@ const result = await store.article.orderImages(42, 'SKU-1', { fullHashes: ['a', // => { fullHashes: ['a', 'b'] } ``` +## `article.removeChild` + +*Source: `src/store/article/removeChild.js`* + +Removes a child article from a father article, either by unlinking or deleting it. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `fatherSku` | `string` | The father article SKU. | +| `childSku` | `string` | The child article SKU. | +| `[options]` | `object` | | +| `[options.deleteArticle=false]` | `boolean` | Whether to delete the child article entity entirely. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>` + ## `article.setBrandIdBySku` *Source: `src/store/article/setBrandIdBySku.js`* @@ -505,6 +569,28 @@ const article = await store.article.setWeightBySku(42, 'SKU-1', 1.5); // => { id, sku, name, ... } ``` +## `article.toggleFather` + +*Source: `src/store/article/toggleFather.js`* + +Toggles an article into or out of father mode. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `options` | `object` | | +| `options.isFather` | `boolean` | Whether the article should be a father article. | +| `[options.attributeNameId]` | `number` | The attribute name id required when enabling father mode. | +| `[options.fatherAttributeNameId]` | `number` | Alias for attributeNameId. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise` — The updated article detail. + ## `article.updateBySku` *Source: `src/store/article/updateBySku.js`* diff --git a/API.md b/API.md index 2bd25b6..acf287b 100644 --- a/API.md +++ b/API.md @@ -8,14 +8,18 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met > **Convention**: `PK` primary key · `?` nullable · `->tbl` foreign key · `UQ` unique · `s(N)` string · `txt` text · `i` int · `bi` bigint · `f` float · `d` decimal · `b` bit · `dt` datetime · `bin` binary · `=val` default -- **article**: `id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_father:b=0, fatherarticle_id?->article, fathersort:i=0, created_at:dt, updated_at:dt, UQ(fatherarticle_id, fathersort)` +- **article**: `id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_father:b=0, fatherarticle_id?->article, fatherattributename_id?->attributename, fathersort:i=0, created_at:dt, updated_at:dt, UQ(fatherarticle_id, fathersort)` +- **articleattributevalue**: `id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)` - **articleean**: `id PK, article_id->article, ean:s(50) UQ` - **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt` - **articlelist**: `id PK` - **articlelistarticle**: `id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)` - **articlepic**: `id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)` +- **articleproperty**: `id PK, article_id->article, property_id->propertyname, value:txt, sort:i=0` - **articlesupplier**: `id PK, article_id->article, entity_id->entity, price_id?->price, sku?:s(100), stock?:f, pu:f=1, sort:i=0, UQ(article_id, entity_id), UQ(article_id, sort)` - **articletext**: `id PK, article_id->article, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(article_id, language_id)` +- **attributename**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)` +- **attributevalue**: `id PK, attribute_id->attributename, value:s(255), sort:i=0, UQ(attribute_id, value)` - **brand**: `id PK, company_id->company, name:s(255), gsprpinfo?:txt, UQ(company_id, name)` - **brandpic**: `id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)` - **category**: `id PK, parent_id?->category, sort:i=0` @@ -32,6 +36,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - **entityhistory**: `id PK, entity_id->entity, companyuser_id?->companyuser, date:dt, text:txt` - **file**: `id PK, hash:s(64), mimetype:s(255), name:s(255), size:bi, created:dt, modified:dt, content:bin, thumbavif?:bin, thumbx?:i, thumby?:i, meta?:txt` - **google_auth**: `id PK, email:s(320) UQ, picture?:s(2048), picturebinary?:bin, sub?:s(255) UQ, created_at:dt, selected_company?->company` +- **labeltemplate**: `id PK, company_id->company, name:s(255), type:s(50), width:f, height:f, data:txt, sort:i=0` - **language**: `id PK, iso639:s(10) UQ` - **ledger**: `id PK, manifestarticle_id->manifestarticle, warehouse_id?->warehouse, transaction_type:s(100), amount:f, transaction_text:txt, hash_version:i, prev_hash?:s(128) [append-only]` - **manifest**: `id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt` @@ -45,6 +50,8 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - **posterminal**: `id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin:s(50)` - **posterminalcategory**: `id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)` - **price**: `id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f` +- **printer**: `id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt` +- **propertyname**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)` - **syncpos**: `id PK, company_id->company, paircode:s(50) UQ, token?:s(255) UQ` - **tax**: `id PK, rate:d(18,4) UQ` - **unit**: `id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)` @@ -76,6 +83,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `order(input: {googleAuthId: number, parentId: number, categoryIds: Array}, tx?: object): Promise<{categoryIds: Array}>` - `rename(input: {googleAuthId: number, id: number, name: string}, tx?: object): Promise<{id: number, name: string, slug: string}>` - [article](API.article.md) + - `addChild(googleAuthId: number, fatherSku: string, options: {attributeValueId?: number, value?: string, name?: string, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise` - `addImage(googleAuthId: number, sku: string, image: Buffer, tx?: object): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>` - `delete(googleAuthId: number, sku: string, tx?: object): Promise` @@ -83,9 +91,11 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|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}}>` - `getHistoryBySku(googleAuthId: number, sku: string, tx?: object): Promise>` - `importRows(input: {googleAuthId: number, rows: Array}, tx?: object): Promise<{imported: Array, skipped: Array, errors: Array}>` + - `listChildren(googleAuthId: number, fatherSku: string, tx?: object): Promise>` - `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise>` - `listSearchIndex(googleAuthId: number, tx?: object): Promise, categoryIds: Array}>>` - `orderImages(googleAuthId: number, sku: string, input: {fullHashes: Array}, tx?: object): Promise<{fullHashes: Array}>` + - `removeChild(googleAuthId: number, fatherSku: string, childSku: string, options?: {deleteArticle?: boolean}, tx?: object): Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>` - `setBrandIdBySku(googleAuthId: number, sku: string, brandId: number|null, tx?: object): Promise` - `setCategoryIdsBySku(googleAuthId: number, sku: string, categoryIds: Array, tx?: object): Promise` - `setEansBySku(googleAuthId: number, sku: string, eans: Array, tx?: object): Promise` @@ -93,6 +103,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `setPricesBySku(googleAuthId: number, sku: string, prices: Array<{price: number, tax: number, minmultiplier?: number}>, tx?: object): Promise` - `setTrackStockBySku(googleAuthId: number, sku: string, trackStock: boolean|number, tx?: object): Promise` - `setWeightBySku(googleAuthId: number, sku: string, weight: number, tx?: object): Promise` + - `toggleFather(googleAuthId: number, sku: string, options: {isFather: boolean, attributeNameId?: number, fatherAttributeNameId?: number}, tx?: object): Promise` - `updateBySku(googleAuthId: number, sku: string, patch: {name?: string, unitSi?: string, unitamount?: number, track_stock?: boolean|number}, tx?: object): Promise` - [lang](API.lang.md) - `create(iso639: string, tx?: object): Promise` @@ -217,9 +228,12 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - [numbers](API.numbers.md) - `list(googleAuthId: number, tx?: object): Promise>` - `update(googleAuthId: number, id: number, input: {prefix?: string, padding?: number, next?: number}, tx?: object): Promise<{id: number, type: string, prefix: string, padding: number, next: number}>` -- [articleattributevalue](API.articleattributevalue.md) - - `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise>` - - `setByArticleSku(googleAuthId: number, sku: string, values: Array, tx?: object): Promise>` +- [propertyname](API.propertyname.md) + - `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>` + - `delete(googleAuthId: number, id: number, tx?: object): Promise` + - `list(googleAuthId: number, tx?: object): Promise>` + - `order(googleAuthId: number, input: {propertynameIds: Array}, tx?: object): Promise<{propertynameIds: Array}>` + - `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>` - [articleproperty](API.articleproperty.md) - `delete(googleAuthId: number, sku: string, propertyId: number, tx?: object): Promise` - `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise>` @@ -236,17 +250,14 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `listByAttributeId(googleAuthId: number, attributeId: number, tx?: object): Promise>` - `order(googleAuthId: number, attributeId: number, input: {attributevalueIds: Array}, tx?: object): Promise<{attributevalueIds: Array}>` - `update(googleAuthId: number, id: number, input: {value?: string, sort?: number}, tx?: object): Promise<{id: number, attributeId: number, value: string, sort: number}>` +- [articleattributevalue](API.articleattributevalue.md) + - `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise>` + - `setByArticleSku(googleAuthId: number, sku: string, values: Array, tx?: object): Promise>` - [labeltemplate](API.labeltemplate.md) - `create(googleAuthId: number, input: {name: string, width: number, height: number, dpi: number, type: string, content: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>` - `list(googleAuthId: number, tx?: object): Promise>` - `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>` -- [propertyname](API.propertyname.md) - - `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>` - - `delete(googleAuthId: number, id: number, tx?: object): Promise` - - `list(googleAuthId: number, tx?: object): Promise>` - - `order(googleAuthId: number, input: {propertynameIds: Array}, tx?: object): Promise<{propertynameIds: Array}>` - - `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>` - [runner.js](API.runner.js.md) - `runner(provider: import('../providers/provider.js').DatabaseProvider, tx?: object): ((sql: string, params?: object) => Promise) & { query: Function, dialect?: string }` diff --git a/generate-api-doc.js b/generate-api-doc.js index 9fc94ea..8453ad8 100644 --- a/generate-api-doc.js +++ b/generate-api-doc.js @@ -36,6 +36,12 @@ const MODULE_ORDER = [ 'google_auth', 'pic', 'numbers', + 'propertyname', + 'articleproperty', + 'attributename', + 'attributevalue', + 'articleattributevalue', + 'labeltemplate', ]; // Files that are internal helpers and should not appear as public API entries. @@ -313,14 +319,18 @@ function render() { } const SCHEMA_TABLES = [ - ['article', 'id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_father:b=0, fatherarticle_id?->article, fathersort:i=0, created_at:dt, updated_at:dt, UQ(fatherarticle_id, fathersort)'], + ['article', 'id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_father:b=0, fatherarticle_id?->article, fatherattributename_id?->attributename, fathersort:i=0, created_at:dt, updated_at:dt, UQ(fatherarticle_id, fathersort)'], + ['articleattributevalue', 'id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)'], ['articleean', 'id PK, article_id->article, ean:s(50) UQ'], ['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt'], ['articlelist', 'id PK'], ['articlelistarticle', 'id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)'], ['articlepic', 'id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)'], + ['articleproperty', 'id PK, article_id->article, property_id->propertyname, value:txt, sort:i=0'], ['articlesupplier', 'id PK, article_id->article, entity_id->entity, price_id?->price, sku?:s(100), stock?:f, pu:f=1, sort:i=0, UQ(article_id, entity_id), UQ(article_id, sort)'], ['articletext', 'id PK, article_id->article, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(article_id, language_id)'], + ['attributename', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'], + ['attributevalue', 'id PK, attribute_id->attributename, value:s(255), sort:i=0, UQ(attribute_id, value)'], ['brand', 'id PK, company_id->company, name:s(255), gsprpinfo?:txt, UQ(company_id, name)'], ['brandpic', 'id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)'], ['category', 'id PK, parent_id?->category, sort:i=0'], @@ -337,6 +347,7 @@ const SCHEMA_TABLES = [ ['entityhistory', 'id PK, entity_id->entity, companyuser_id?->companyuser, date:dt, text:txt'], ['file', 'id PK, hash:s(64), mimetype:s(255), name:s(255), size:bi, created:dt, modified:dt, content:bin, thumbavif?:bin, thumbx?:i, thumby?:i, meta?:txt'], ['google_auth', 'id PK, email:s(320) UQ, picture?:s(2048), picturebinary?:bin, sub?:s(255) UQ, created_at:dt, selected_company?->company'], + ['labeltemplate', 'id PK, company_id->company, name:s(255), type:s(50), width:f, height:f, data:txt, sort:i=0'], ['language', 'id PK, iso639:s(10) UQ'], ['ledger', 'id PK, manifestarticle_id->manifestarticle, warehouse_id?->warehouse, transaction_type:s(100), amount:f, transaction_text:txt, hash_version:i, prev_hash?:s(128) [append-only]'], ['manifest', 'id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt'], @@ -350,6 +361,8 @@ const SCHEMA_TABLES = [ ['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin:s(50)'], ['posterminalcategory', 'id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)'], ['price', 'id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f'], + ['printer', 'id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt'], + ['propertyname', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'], ['syncpos', 'id PK, company_id->company, paircode:s(50) UQ, token?:s(255) UQ'], ['tax', 'id PK, rate:d(18,4) UQ'], ['unit', 'id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)'], diff --git a/src/index.js b/src/index.js index 21afe36..89839cc 100644 --- a/src/index.js +++ b/src/index.js @@ -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), diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index 97153ae..a5b0551 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -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 diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index d28ce86..4d1f8c9 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -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, diff --git a/src/providers/sqlite3/index.js b/src/providers/sqlite3/index.js index 86ce7c9..764863a 100644 --- a/src/providers/sqlite3/index.js +++ b/src/providers/sqlite3/index.js @@ -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')) { diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index f848d37..5938ba8 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -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 `; \ No newline at end of file diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index a029849..0b7108c 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -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, diff --git a/src/store/article/addChild.js b/src/store/article/addChild.js new file mode 100644 index 0000000..9f463f4 --- /dev/null +++ b/src/store/article/addChild.js @@ -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} 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)); + }; +} diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index ac71097..bfaf28d 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -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, + })), }, }; }; diff --git a/src/store/article/listChildren.js b/src/store/article/listChildren.js new file mode 100644 index 0000000..2c15217 --- /dev/null +++ b/src/store/article/listChildren.js @@ -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>} + */ + 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)); + }; +} diff --git a/src/store/article/removeChild.js b/src/store/article/removeChild.js new file mode 100644 index 0000000..83c5d72 --- /dev/null +++ b/src/store/article/removeChild.js @@ -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)); + }; +} diff --git a/src/store/article/toggleFather.js b/src/store/article/toggleFather.js new file mode 100644 index 0000000..c4da910 --- /dev/null +++ b/src/store/article/toggleFather.js @@ -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} 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)); + }; +}