This commit is contained in:
seb
2026-10-04 21:22:05 +02:00
parent 0268583ae3
commit 4718cb5726
48 changed files with 758 additions and 758 deletions

View File

@@ -6,19 +6,19 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/article/addChild.js`*
Adds a child article to a father article (creates and attaches with auto-generated SKU).
Adds a child article to a parent article (creates and attaches with auto-generated SKU).
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `fatherSku` | `string` | The father article SKU. |
| `parentSku` | `string` | The parent article SKU. |
| `options` | `object` | |
| `[options.attach=false]` | `boolean` | If true, attaches an existing article instead of creating. |
| `[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.attributeValueId]` | `number` | Existing attribute value ID for parent's attribute. |
| `[options.value]` | `string` | String value to find or create under parent's attribute. |
| `[options.name]` | `string` | Custom name for child article (defaults to parent name + value). |
| `[options.unitamount]` | `number` | Optional unitamount override. |
| `[options.weight]` | `number` | Optional weight override. |
| `[options.track_stock]` | `boolean|number` | Optional track_stock override. |
@@ -58,20 +58,20 @@ const result = await store.article.addImage(42, 'SKU-1', imageBuffer);
*Source: `src/store/article/attachChild.js`*
Attaches an existing article as a child to a father article.
Attaches an existing article as a child to a parent article.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `fatherSku` | `string` | The father article SKU. |
| `parentSku` | `string` | The parent article SKU. |
| `options` | `object` | |
| `options.sku` | `string` | The child article SKU to attach. |
| `[options.childSku]` | `string` | Alias for options.sku. |
| `[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.reparent=false]` | `boolean` | Whether to allow re-parenting if child belongs to another father. |
| `[options.attributeValueId]` | `number` | Existing attribute value ID for parent's attribute. |
| `[options.value]` | `string` | String value to find or create under parent's attribute. |
| `[options.reparent=false]` | `boolean` | Whether to allow re-parenting if child belongs to another parent. |
| `[options.unitamount]` | `number` | Optional unitamount override. |
| `[options.weight]` | `number` | Optional weight override. |
| `[options.track_stock]` | `boolean|number` | Optional track_stock override. |
@@ -184,7 +184,7 @@ Returns the full detail of an article by its SKU, including unit, brand, categor
**Returns**
`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}}>` — The full article detail.
`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, parentArticleId: number|null, parentSku: string|null}}>` — The full article detail.
**Throws**
@@ -231,7 +231,7 @@ const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
*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.
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 `parentSku` 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**
@@ -242,7 +242,7 @@ Imports article rows, creating articles by their SKU and returning per-row impor
| `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[].vk]` | `string` | `'V'` (parent/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. |
@@ -250,7 +250,7 @@ Imports article rows, creating articles by their SKU and returning per-row impor
| `[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[].track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. |
| `[input.rows[].fatherSku]` | `string` | The father article SKU for a V/K compound link. |
| `[input.rows[].parentSku]` | `string` | The parent article SKU for a V/K compound link. |
| `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. |
| `[tx]` | `object` | Optional transaction. |
@@ -264,7 +264,7 @@ Imports article rows, creating articles by their SKU and returning per-row impor
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-1', name: '420Strainz 1g', vk: 'K', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '11,00', tax: 19, eans: ['4262557690455'], parentSku: '42001190' },
{ sku: '42001190', name: '420Strainz', vk: 'V', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '0,00', tax: 19 },
],
});
@@ -275,19 +275,19 @@ const result = await store.article.importRows({
*Source: `src/store/article/listChildren.js`*
Lists all children of a father article.
Lists all children of a parent article.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `fatherSku` | `string` | The father article SKU. |
| `parentSku` | `string` | The parent article SKU. |
| `[tx]` | `object` | 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}>>`
`Promise<Array<{id: number, sku: string, parentsort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>`
## `article.listExport`
@@ -377,14 +377,14 @@ const result = await store.article.orderImages(42, 'SKU-1', { fullHashes: ['a',
*Source: `src/store/article/removeChild.js`*
Removes a child article from a father article, either by unlinking or deleting it.
Removes a child article from a parent 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. |
| `parentSku` | `string` | The parent article SKU. |
| `childSku` | `string` | The child article SKU. |
| `[options]` | `object` | |
| `[options.deleteArticle=false]` | `boolean` | Whether to delete the child article entity entirely. |
@@ -393,7 +393,7 @@ Removes a child article from a father article, either by unlinking or deleting i
**Returns**
`Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>`
`Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>`
## `article.resolveUniqueSlug`
@@ -506,11 +506,11 @@ const article = await store.article.setEansBySku(42, 'SKU-1', ['4001234567890'])
// => { id, sku, name, ... }
```
## `article.setFatherBySku`
## `article.setParentBySku`
*Source: `src/store/article/setFatherBySku.js`*
*Source: `src/store/article/setParentBySku.js`*
Links an article to a father article (V/K compound) by their SKUs.
Links an article to a parent article (V/K compound) by their SKUs.
**Parameters**
@@ -518,7 +518,7 @@ Links an article to a father article (V/K compound) by their SKUs.
| --- | --- | --- |
| `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. |
| `parentSku` | `string|null` | The parent article SKU, or `null` to unlink. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
@@ -527,13 +527,13 @@ Links an article to a father article (V/K compound) by their SKUs.
**Throws**
`Error` — If the article or father is not found, or the link is invalid.
`Error` — If the article or parent 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', ... }
const article = await store.article.setParentBySku(42, '42001190-1', '42001190');
// => { id, sku, name, parentSku: '42001190', ... }
```
## `article.setPricesBySku`
@@ -626,11 +626,11 @@ const article = await store.article.setWeightBySku(42, 'SKU-1', 1.5);
// => { id, sku, name, ... }
```
## `article.toggleFather`
## `article.toggleParent`
*Source: `src/store/article/toggleFather.js`*
*Source: `src/store/article/toggleParent.js`*
Toggles an article into or out of father mode.
Toggles an article into or out of parent mode.
**Parameters**
@@ -639,9 +639,9 @@ Toggles an article into or out of father mode.
| `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. |
| `options.isParent` | `boolean` | Whether the article should be a parent article. |
| `[options.attributeNameId]` | `number` | The attribute name id required when enabling parent mode. |
| `[options.parentAttributeNameId]` | `number` | Alias for attributeNameId. |
| `[tx]` | `object` | Optional transaction. |
**Returns**

View File

@@ -156,7 +156,7 @@ Returns the articles belonging to a single category.
**Returns**
`Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null}>>` — The articles in the category.
`Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null}>>` — The articles in the category.
**Example**

18
API.md
View File

@@ -8,7 +8,7 @@ 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, fatherattributename_id?->attributename, 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_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)`
- **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`
@@ -80,35 +80,35 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `delete(input: {googleAuthId: number, id: number}, tx?: object): Promise<void>`
- `deleteImage(googleAuthId: number, categoryId: number, tx?: object): Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>`
- `ensureByPath(input: {googleAuthId: number, parentId: number, path: string}, tx?: object): Promise<number>`
- `getCategoryArticles(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null}>>`
- `getCategoryArticles(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null}>>`
- `getTreeForCompany(googleAuthId: number, tx?: object): Promise<{languageId: number, tree: Array<object>, articles: Array<object>}>`
- `move(input: {googleAuthId: number, id: number, parentId: number}, tx?: object): Promise<{id: number, parentId: number, sort: number}>`
- `order(input: {googleAuthId: number, parentId: number, categoryIds: Array<number>}, tx?: object): Promise<{categoryIds: Array<number>}>`
- `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: {attach?: boolean, attributeValueId?: number, value?: string, name?: string, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<object>`
- `addChild(googleAuthId: number, parentSku: string, options: {attach?: boolean, attributeValueId?: number, value?: string, name?: string, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<object>`
- `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}>`
- `attachChild(googleAuthId: number, fatherSku: string, options: {sku: string, childSku?: string, attributeValueId?: number, value?: string, reparent?: boolean, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<{article: object, children: Array<object>}>`
- `attachChild(googleAuthId: number, parentSku: string, options: {sku: string, childSku?: string, attributeValueId?: number, value?: string, reparent?: boolean, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<{article: object, children: Array<object>}>`
- `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, optionsOrTx?: object|*, maybeTx?: object): Promise<void>`
- `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>`
- `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}}>`
- `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, parentArticleId: number|null, parentSku: string|null}}>`
- `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
- `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>`
- `listChildren(googleAuthId: number, fatherSku: string, tx?: object): 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}>>`
- `listChildren(googleAuthId: number, parentSku: string, tx?: object): Promise<Array<{id: number, sku: string, parentsort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>`
- `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>`
- `listSearchIndex(googleAuthId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>`
- `orderImages(googleAuthId: number, sku: string, input: {fullHashes: Array<string>}, tx?: object): Promise<{fullHashes: Array<string>}>`
- `removeChild(googleAuthId: number, fatherSku: string, childSku: string, options?: {deleteArticle?: boolean, cascade?: boolean}, tx?: object): Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>`
- `removeChild(googleAuthId: number, parentSku: string, childSku: string, options?: {deleteArticle?: boolean, cascade?: boolean}, tx?: object): Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>`
- `resolveUniqueSlug(runner: object, params: {companyId: number, languageId: number, name: string, excludeArticleId?: number}): Promise<string>`
- `setBrandIdBySku(googleAuthId: number, sku: string, brandId: number|null, tx?: object): Promise<object>`
- `setCategoryIdsBySku(googleAuthId: number, sku: string, categoryIds: Array<number>, tx?: object): Promise<object>`
- `setEansBySku(googleAuthId: number, sku: string, eans: Array<string>, tx?: object): Promise<object>`
- `setFatherBySku(googleAuthId: number, sku: string, fatherSku: string|null, tx?: object): Promise<object>`
- `setParentBySku(googleAuthId: number, sku: string, parentSku: string|null, tx?: object): Promise<object>`
- `setPricesBySku(googleAuthId: number, sku: string, prices: Array<{price: number, tax: number, minmultiplier?: number}>, tx?: object): Promise<object>`
- `setTrackStockBySku(googleAuthId: number, sku: string, trackStock: boolean|number, tx?: object): Promise<object>`
- `setWeightBySku(googleAuthId: number, sku: string, weight: number, tx?: object): Promise<object>`
- `toggleFather(googleAuthId: number, sku: string, options: {isFather: boolean, attributeNameId?: number, fatherAttributeNameId?: number}, tx?: object): Promise<object>`
- `toggleParent(googleAuthId: number, sku: string, options: {isParent: boolean, attributeNameId?: number, parentAttributeNameId?: number}, tx?: object): Promise<object>`
- `updateBySku(googleAuthId: number, sku: string, patch: {name?: string, unitSi?: string, unitamount?: number, track_stock?: boolean|number}, tx?: object): Promise<object>`
- [lang](API.lang.md)
- `create(iso639: string, tx?: object): Promise<number>`

View File

@@ -28,7 +28,7 @@
| `article.setBrandIdBySku` | 18 | 15.50 | 24.98 | 0.57 | 0.81 | 27.21x |
| `article.setCategoryIdsBySku` | 18 | 15.08 | 33.45 | 0.74 | 1.01 | 20.24x |
| `article.setEansBySku` | 18 | 11.86 | 19.23 | 0.56 | 0.85 | 21.15x |
| `article.setFatherBySku` | 13 | 14.79 | 25.32 | 0.66 | 0.92 | 22.52x |
| `article.setParentBySku` | 13 | 14.79 | 25.32 | 0.66 | 0.92 | 22.52x |
| `article.setPricesBySku` | 18 | 14.16 | 29.51 | 0.69 | 1.12 | 20.48x |
| `article.setWeightBySku` | 18 | 14.18 | 36.45 | 0.52 | 0.68 | 27.02x |
| `article.updateBySku` | 18 | 18.71 | 84.26 | 0.83 | 1.65 | 22.52x |
@@ -153,7 +153,7 @@
| `article.setBrandIdBySku` | 18 | 15.50 | 9.85 | 24.98 | 14.23 | 24.98 | 0 |
| `article.setCategoryIdsBySku` | 18 | 15.08 | 9.51 | 33.45 | 13.12 | 33.45 | 0 |
| `article.setEansBySku` | 18 | 11.86 | 8.23 | 19.23 | 11.67 | 19.23 | 0 |
| `article.setFatherBySku` | 13 | 14.79 | 8.86 | 25.32 | 14.06 | 25.32 | 0 |
| `article.setParentBySku` | 13 | 14.79 | 8.86 | 25.32 | 14.06 | 25.32 | 0 |
| `article.setPricesBySku` | 18 | 14.16 | 8.82 | 29.51 | 12.62 | 29.51 | 0 |
| `article.setWeightBySku` | 18 | 14.18 | 9.15 | 36.45 | 12.90 | 36.45 | 0 |
| `article.updateBySku` | 18 | 18.71 | 8.88 | 84.26 | 13.70 | 84.26 | 0 |
@@ -278,7 +278,7 @@
| `article.setBrandIdBySku` | 18 | 0.57 | 0.53 | 0.81 | 0.55 | 0.81 | 0 |
| `article.setCategoryIdsBySku` | 18 | 0.74 | 0.69 | 1.01 | 0.73 | 1.01 | 0 |
| `article.setEansBySku` | 18 | 0.56 | 0.51 | 0.85 | 0.54 | 0.85 | 0 |
| `article.setFatherBySku` | 13 | 0.66 | 0.60 | 0.92 | 0.63 | 0.92 | 0 |
| `article.setParentBySku` | 13 | 0.66 | 0.60 | 0.92 | 0.63 | 0.92 | 0 |
| `article.setPricesBySku` | 18 | 0.69 | 0.63 | 1.12 | 0.65 | 1.12 | 0 |
| `article.setWeightBySku` | 18 | 0.52 | 0.49 | 0.68 | 0.51 | 0.68 | 0 |
| `article.updateBySku` | 18 | 0.83 | 0.73 | 1.65 | 0.78 | 1.65 | 0 |

View File

@@ -25,7 +25,7 @@ Nicht-Ziele (Phase 1): inkrementelle Backups, Replikation, Live-Sync, Verschlüs
| Zeitstempel: MSSQL `DATETIME2`, SQLite `TEXT` ISO-8601 (`%Y-%m-%dT%H:%M:%fZ`, ms-Präzision). | Schemas | Kanonisches Format: ISO-8601 UTC, `Z`, **genau 3 Nachkommastellen**. |
| Die **Ledger-Hashkette** (`prev_hash`) ist SHA-512 über `JSON.stringify({id, manifestarticle_id, warehouse_id, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at})`. Sie hängt von `id` und `created_at` (ms-genau) ab. | `src/store/ledger/add.js` | IDs und `created_at` müssen 1:1 erhalten bleiben. Import in aufsteigender `id`-Reihenfolge, Kette wird nach dem Import verifiziert. |
| MSSQL-Ledger ist `APPEND_ONLY`; `purge` hinterlässt `MSSQL_DroppedLedgerTable_*`. | `mssql/schema.js`, README | Import in MSSQL nur in eine **frisch initialisierte** Ledger-Tabelle. Kein Merge in vorhandene Ledger-Daten. |
| Zirkuläre FKs: `company.poscustomergroup_id → customergroup` und `customergroup.company_id → company`. Selbstreferenzen: `category.parent_id`, `article.fatherarticle_id`. Außerdem `company.root_category_id → category`. | Schemas | Die Importreihenfolge ist nicht rein topologisch; die FK-Prüfung muss zeitweise aufgeschoben werden (dialektspezifisch). |
| Zirkuläre FKs: `company.poscustomergroup_id → customergroup` und `customergroup.company_id → company`. Selbstreferenzen: `category.parent_id`, `article.parentarticle_id`. Außerdem `company.root_category_id → category`. | Schemas | Die Importreihenfolge ist nicht rein topologisch; die FK-Prüfung muss zeitweise aufgeschoben werden (dialektspezifisch). |
| SQLite hat `entity.company_id` (denormalisiert, per Trigger gepflegt). MSSQL löst das über die View `v_entity_company_number`. | `sqlite3/schema.js`, `mssql/schema.js` | `entity.company_id` ist eine **abgeleitete Spalte**: nicht exportieren, das Ziel setzt sie selbst. |
| SQLite-`initialize` seedet `tax` (0, 7, 19) und `meta.version`. | `sqlite3/schema.js` | Import-Modus „replace“ muss Seed-Zeilen entfernen oder überschreiben, sonst PK-/UQ-Konflikte. |
| Collation: SQLite-Spalten teils `COLLATE NOCASE`, MSSQL-Default ist case-insensitive. Nicht jede SQLite-UQ ist `NOCASE` (z. B. `attributename(company_id, name)`). | Schemas | SQLite → MSSQL kann an einer UQ scheitern (`Foo` vs. `foo`). Pre-Flight-Check (§8). |
@@ -113,7 +113,7 @@ Abgeleitet aus den Schemata (Eltern vor Kindern). Die Registry speichert sie exp
8. `manifest`, `manifestarticle`, `file`, `manifestattachment`, `warehouse`, `ledger`
9. `numbers`, `companyuser`, `articlehistory`, `entityhistory`, `syncpos`, `companysetting`, `posterminal`, `posterminalcategory`, `printer`, `labeltemplate`
Selbstreferenzen (`category.parent_id`, `article.fatherarticle_id`): Der Export sortiert nach `id`. Beim Import wird die FK-Prüfung zurückgestellt (§6.3), ein Zwei-Pass-Update ist nicht nötig.
Selbstreferenzen (`category.parent_id`, `article.parentarticle_id`): Der Export sortiert nach `id`. Beim Import wird die FK-Prüfung zurückgestellt (§6.3), ein Zwei-Pass-Update ist nicht nötig.
## 5. Öffentliche API (`store.transfer`)

View File

@@ -501,7 +501,7 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c
| `order` | `({ googleAuthId, parentId, categoryIds })` | `{ categoryIds }` |
| `delete` | `({ googleAuthId, id })` | `void` — `Category has children` if child categories remain; FK `categoryarticle` also blocks deletion |
| `getTreeForCompany` | `(googleAuthId)` | `{ languageId, tree, articles }` — tree rows carry `name` + `slug` |
| `getCategoryArticles` | `(googleAuthId, categoryId)` | `{ id, sku, name, slug, net, taxRate, optionsCount, articleListCount, childCount, fatherArticleId, fatherSku, fullHash, thumbW, thumbH, thumbHash }[]` |
| `getCategoryArticles` | `(googleAuthId, categoryId)` | `{ id, sku, name, slug, net, taxRate, optionsCount, articleListCount, childCount, parentArticleId, parentSku, fullHash, thumbW, thumbH, thumbHash }[]` |
| `addImage` | `(googleAuthId, categoryId, imageBuffer)` | `{ id, thumbW, thumbH, thumbHash }` |
| `deleteImage` | `(googleAuthId, categoryId)` | `{ id, thumbW: null, thumbH: null, thumbHash: null }` |
@@ -643,8 +643,8 @@ type ArticleDetail = {
optionsCount: number, // 0 when no optionslist
articleListCount: number, // 0 when no articlelist
childCount: number, // 0 when no children
fatherArticleId: number | null, // null when root article
fatherSku: string | null, // null when root article
parentArticleId: number | null, // null when root article
parentSku: string | null, // null when root article
}
type PriceEntry = {
@@ -681,7 +681,7 @@ Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Err
`orderImages` sets `articlepic.sort` to `0..n-1` in the given order. `fullHashes` must list every image of the article exactly once. Errors: `Article not found`, `Invalid image order`, `Not authorized`.
`delete` removes the article when it has no child articles (`fatherarticle_id` FK is `ON DELETE NO ACTION`). Cascades: `articlehistory`, `articleean`, `categoryarticle`, `price`, `articlesupplier`, `articletext`, `articlelistarticle`, `optionslistarticle`, `articlepic`; deletes linked `pic` rows and owned `articlelist` / `optionslist`. Errors: `Article not found`, `Article has children`, `Not authorized`.
`delete` removes the article when it has no child articles (`parentarticle_id` FK is `ON DELETE NO ACTION`). Cascades: `articlehistory`, `articleean`, `categoryarticle`, `price`, `articlesupplier`, `articletext`, `articlelistarticle`, `optionslistarticle`, `articlepic`; deletes linked `pic` rows and owned `articlelist` / `optionslist`. Errors: `Article not found`, `Article has children`, `Not authorized`.
---

View File

@@ -319,7 +319,7 @@ 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, fatherattributename_id?->attributename, 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_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)'],
['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'],

File diff suppressed because one or more lines are too long

View File

@@ -879,105 +879,105 @@
<!-- article -->
<g id="node24" class="node">
<title>article</title>
<polygon fill="#33415c" stroke="none" points="5984.24,-777.6 5984.24,-806 6190.72,-806 6190.72,-777.6 5984.24,-777.6"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-777.6 5984.24,-806 6190.72,-806 6190.72,-777.6 5984.24,-777.6"/>
<polygon fill="#33415c" stroke="none" points="5986.24,-800 5986.24,-804 6003.11,-804 6003.11,-800 5986.24,-800"/>
<polygon fill="#33415c" stroke="none" points="6003.11,-800 6003.11,-804 6019.98,-804 6019.98,-800 6003.11,-800"/>
<polygon fill="#33415c" stroke="none" points="6019.98,-800 6019.98,-804 6036.86,-804 6036.86,-800 6019.98,-800"/>
<polygon fill="#33415c" stroke="none" points="6036.86,-800 6036.86,-804 6053.73,-804 6053.73,-800 6036.86,-800"/>
<polygon fill="#33415c" stroke="none" points="6053.73,-800 6053.73,-804 6070.6,-804 6070.6,-800 6053.73,-800"/>
<polygon fill="#33415c" stroke="none" points="6070.6,-800 6070.6,-804 6087.48,-804 6087.48,-800 6070.6,-800"/>
<polygon fill="#33415c" stroke="none" points="6087.48,-800 6087.48,-804 6104.35,-804 6104.35,-800 6087.48,-800"/>
<polygon fill="#33415c" stroke="none" points="6104.35,-800 6104.35,-804 6121.22,-804 6121.22,-800 6104.35,-800"/>
<polygon fill="#33415c" stroke="none" points="6121.22,-800 6121.22,-804 6138.1,-804 6138.1,-800 6121.22,-800"/>
<polygon fill="#33415c" stroke="none" points="6138.1,-800 6138.1,-804 6154.97,-804 6154.97,-800 6138.1,-800"/>
<polygon fill="#33415c" stroke="none" points="6154.97,-800 6154.97,-804 6171.84,-804 6171.84,-800 6154.97,-800"/>
<polygon fill="#33415c" stroke="none" points="6171.84,-800 6171.84,-804 6188.72,-804 6188.72,-800 6171.84,-800"/>
<text xml:space="preserve" text-anchor="start" x="6069.8" y="-787.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00" fill="#e2e8f0">article</text>
<polygon fill="#2b3a52" stroke="none" points="5984.24,-750.8 5984.24,-777.6 6190.72,-777.6 6190.72,-750.8 5984.24,-750.8"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-750.8 5984.24,-777.6 6190.72,-777.6 6190.72,-750.8 5984.24,-750.8"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-761" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">id</text>
<text xml:space="preserve" text-anchor="start" x="6156.22" y="-762.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6173.21" y="-762.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">PK</text>
<polygon fill="#2b3a52" stroke="none" points="5984.24,-724 5984.24,-750.8 6190.72,-750.8 6190.72,-724 5984.24,-724"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-724 5984.24,-750.8 6190.72,-750.8 6190.72,-724 5984.24,-724"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-734.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">fatherattributename_id</text>
<text xml:space="preserve" text-anchor="start" x="6140.93" y="-734.2" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6156.72" y="-735.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6173.72" y="-735.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5984.24,-697.2 5984.24,-724 6190.72,-724 6190.72,-697.2 5984.24,-697.2"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-697.2 5984.24,-724 6190.72,-724 6190.72,-697.2 5984.24,-697.2"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-707.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">brand_id</text>
<text xml:space="preserve" text-anchor="start" x="6048.36" y="-707.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6156.72" y="-708.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6173.72" y="-708.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5984.24,-670.4 5984.24,-697.2 6190.72,-697.2 6190.72,-670.4 5984.24,-670.4"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-670.4 5984.24,-697.2 6190.72,-697.2 6190.72,-670.4 5984.24,-670.4"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-680.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">fatherarticle_id</text>
<text xml:space="preserve" text-anchor="start" x="6089.61" y="-680.6" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6156.72" y="-682.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6173.72" y="-682.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5984.24,-643.6 5984.24,-670.4 6190.72,-670.4 6190.72,-643.6 5984.24,-643.6"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-643.6 5984.24,-670.4 6190.72,-670.4 6190.72,-643.6 5984.24,-643.6"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-653.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">articlelist_id</text>
<text xml:space="preserve" text-anchor="start" x="6070.94" y="-653.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6156.72" y="-655.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6173.72" y="-655.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5984.24,-616.8 5984.24,-643.6 6190.72,-643.6 6190.72,-616.8 5984.24,-616.8"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-616.8 5984.24,-643.6 6190.72,-643.6 6190.72,-616.8 5984.24,-616.8"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-627" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">optionslist_id</text>
<text xml:space="preserve" text-anchor="start" x="6080.24" y="-627" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6156.72" y="-628.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6173.72" y="-628.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5984.24,-590 5984.24,-616.8 6190.72,-616.8 6190.72,-590 5984.24,-590"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-590 5984.24,-616.8 6190.72,-616.8 6190.72,-590 5984.24,-590"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-600.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">unit_id</text>
<text xml:space="preserve" text-anchor="start" x="6156.72" y="-601.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6173.72" y="-601.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5984.24,-563.2 5984.24,-590 6190.72,-590 6190.72,-563.2 5984.24,-563.2"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-563.2 5984.24,-590 6190.72,-590 6190.72,-563.2 5984.24,-563.2"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-573.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">company_id</text>
<text xml:space="preserve" text-anchor="start" x="6156.72" y="-574.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6173.72" y="-574.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2a3242" stroke="none" points="5984.24,-536.4 5984.24,-563.2 6190.72,-563.2 6190.72,-536.4 5984.24,-536.4"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-536.4 5984.24,-563.2 6190.72,-563.2 6190.72,-536.4 5984.24,-536.4"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-544.2" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">sku?</text>
<text xml:space="preserve" text-anchor="start" x="6114.2" y="-546.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">NVARCHAR(100)</text>
<polygon fill="#272e3d" stroke="none" points="5984.24,-509.6 5984.24,-536.4 6190.72,-536.4 6190.72,-509.6 5984.24,-509.6"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-509.6 5984.24,-536.4 6190.72,-536.4 6190.72,-509.6 5984.24,-509.6"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-517.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">unitamount</text>
<text xml:space="preserve" text-anchor="start" x="6156.71" y="-519.4" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">FLOAT</text>
<polygon fill="#2a3242" stroke="none" points="5984.24,-482.8 5984.24,-509.6 6190.72,-509.6 6190.72,-482.8 5984.24,-482.8"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-482.8 5984.24,-509.6 6190.72,-509.6 6190.72,-482.8 5984.24,-482.8"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-490.6" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">weight</text>
<text xml:space="preserve" text-anchor="start" x="6156.71" y="-492.6" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">FLOAT</text>
<polygon fill="#272e3d" stroke="none" points="5984.24,-456 5984.24,-482.8 6190.72,-482.8 6190.72,-456 5984.24,-456"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-456 5984.24,-482.8 6190.72,-482.8 6190.72,-456 5984.24,-456"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-463.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">track_stock</text>
<text xml:space="preserve" text-anchor="start" x="6171.72" y="-465.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">BIT</text>
<polygon fill="#2a3242" stroke="none" points="5984.24,-429.2 5984.24,-456 6190.72,-456 6190.72,-429.2 5984.24,-429.2"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-429.2 5984.24,-456 6190.72,-456 6190.72,-429.2 5984.24,-429.2"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-437" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">is_father</text>
<text xml:space="preserve" text-anchor="start" x="6171.72" y="-439" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">BIT</text>
<polygon fill="#272e3d" stroke="none" points="5984.24,-402.4 5984.24,-429.2 6190.72,-429.2 6190.72,-402.4 5984.24,-402.4"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-402.4 5984.24,-429.2 6190.72,-429.2 6190.72,-402.4 5984.24,-402.4"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-410.2" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">fathersort</text>
<text xml:space="preserve" text-anchor="start" x="6171.22" y="-412.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT</text>
<polygon fill="#2a3242" stroke="none" points="5984.24,-375.6 5984.24,-402.4 6190.72,-402.4 6190.72,-375.6 5984.24,-375.6"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-375.6 5984.24,-402.4 6190.72,-402.4 6190.72,-375.6 5984.24,-375.6"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-383.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">created_at</text>
<text xml:space="preserve" text-anchor="start" x="6135.21" y="-385.4" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">DATETIME2</text>
<polygon fill="#272e3d" stroke="none" points="5984.24,-348.8 5984.24,-375.6 6190.72,-375.6 6190.72,-348.8 5984.24,-348.8"/>
<polygon fill="none" stroke="#3f4c5f" points="5984.24,-348.8 5984.24,-375.6 6190.72,-375.6 6190.72,-348.8 5984.24,-348.8"/>
<text xml:space="preserve" text-anchor="start" x="5989.24" y="-356.6" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">updated_at</text>
<text xml:space="preserve" text-anchor="start" x="6135.21" y="-358.6" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">DATETIME2</text>
<polygon fill="#33415c" stroke="none" points="5983.29,-777.6 5983.29,-806 6193.66,-806 6193.66,-777.6 5983.29,-777.6"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-777.6 5983.29,-806 6193.66,-806 6193.66,-777.6 5983.29,-777.6"/>
<polygon fill="#33415c" stroke="none" points="5985.29,-800 5985.29,-804 6002.49,-804 6002.49,-800 5985.29,-800"/>
<polygon fill="#33415c" stroke="none" points="6002.49,-800 6002.49,-804 6019.69,-804 6019.69,-800 6002.49,-800"/>
<polygon fill="#33415c" stroke="none" points="6019.69,-800 6019.69,-804 6036.89,-804 6036.89,-800 6019.69,-800"/>
<polygon fill="#33415c" stroke="none" points="6036.89,-800 6036.89,-804 6054.08,-804 6054.08,-800 6036.89,-800"/>
<polygon fill="#33415c" stroke="none" points="6054.08,-800 6054.08,-804 6071.28,-804 6071.28,-800 6054.08,-800"/>
<polygon fill="#33415c" stroke="none" points="6071.28,-800 6071.28,-804 6088.48,-804 6088.48,-800 6071.28,-800"/>
<polygon fill="#33415c" stroke="none" points="6088.48,-800 6088.48,-804 6105.67,-804 6105.67,-800 6088.48,-800"/>
<polygon fill="#33415c" stroke="none" points="6105.67,-800 6105.67,-804 6122.87,-804 6122.87,-800 6105.67,-800"/>
<polygon fill="#33415c" stroke="none" points="6122.87,-800 6122.87,-804 6140.07,-804 6140.07,-800 6122.87,-800"/>
<polygon fill="#33415c" stroke="none" points="6140.07,-800 6140.07,-804 6157.27,-804 6157.27,-800 6140.07,-800"/>
<polygon fill="#33415c" stroke="none" points="6157.27,-800 6157.27,-804 6174.46,-804 6174.46,-800 6157.27,-800"/>
<polygon fill="#33415c" stroke="none" points="6174.46,-800 6174.46,-804 6191.66,-804 6191.66,-800 6174.46,-800"/>
<text xml:space="preserve" text-anchor="start" x="6070.8" y="-787.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00" fill="#e2e8f0">article</text>
<polygon fill="#2b3a52" stroke="none" points="5983.29,-750.8 5983.29,-777.6 6193.66,-777.6 6193.66,-750.8 5983.29,-750.8"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-750.8 5983.29,-777.6 6193.66,-777.6 6193.66,-750.8 5983.29,-750.8"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-761" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">id</text>
<text xml:space="preserve" text-anchor="start" x="6159.16" y="-762.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6176.16" y="-762.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">PK</text>
<polygon fill="#2b3a52" stroke="none" points="5983.29,-724 5983.29,-750.8 6193.66,-750.8 6193.66,-724 5983.29,-724"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-724 5983.29,-750.8 6193.66,-750.8 6193.66,-724 5983.29,-724"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-734.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">parentattributename_id</text>
<text xml:space="preserve" text-anchor="start" x="6143.88" y="-734.2" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6159.67" y="-735.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6176.66" y="-735.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5983.29,-697.2 5983.29,-724 6193.66,-724 6193.66,-697.2 5983.29,-697.2"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-697.2 5983.29,-724 6193.66,-724 6193.66,-697.2 5983.29,-697.2"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-707.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">brand_id</text>
<text xml:space="preserve" text-anchor="start" x="6047.41" y="-707.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6159.67" y="-708.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6176.66" y="-708.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5983.29,-670.4 5983.29,-697.2 6193.66,-697.2 6193.66,-670.4 5983.29,-670.4"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-670.4 5983.29,-697.2 6193.66,-697.2 6193.66,-670.4 5983.29,-670.4"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-680.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">parentarticle_id</text>
<text xml:space="preserve" text-anchor="start" x="6092.56" y="-680.6" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6159.67" y="-682.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6176.66" y="-682.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5983.29,-643.6 5983.29,-670.4 6193.66,-670.4 6193.66,-643.6 5983.29,-643.6"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-643.6 5983.29,-670.4 6193.66,-670.4 6193.66,-643.6 5983.29,-643.6"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-653.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">articlelist_id</text>
<text xml:space="preserve" text-anchor="start" x="6070" y="-653.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6159.67" y="-655.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6176.66" y="-655.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5983.29,-616.8 5983.29,-643.6 6193.66,-643.6 6193.66,-616.8 5983.29,-616.8"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-616.8 5983.29,-643.6 6193.66,-643.6 6193.66,-616.8 5983.29,-616.8"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-627" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">optionslist_id</text>
<text xml:space="preserve" text-anchor="start" x="6079.29" y="-627" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="6159.67" y="-628.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6176.66" y="-628.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5983.29,-590 5983.29,-616.8 6193.66,-616.8 6193.66,-590 5983.29,-590"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-590 5983.29,-616.8 6193.66,-616.8 6193.66,-590 5983.29,-590"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-600.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">unit_id</text>
<text xml:space="preserve" text-anchor="start" x="6159.67" y="-601.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6176.66" y="-601.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="5983.29,-563.2 5983.29,-590 6193.66,-590 6193.66,-563.2 5983.29,-563.2"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-563.2 5983.29,-590 6193.66,-590 6193.66,-563.2 5983.29,-563.2"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-573.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">company_id</text>
<text xml:space="preserve" text-anchor="start" x="6159.67" y="-574.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="6176.66" y="-574.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2a3242" stroke="none" points="5983.29,-536.4 5983.29,-563.2 6193.66,-563.2 6193.66,-536.4 5983.29,-536.4"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-536.4 5983.29,-563.2 6193.66,-563.2 6193.66,-536.4 5983.29,-536.4"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-544.2" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">sku?</text>
<text xml:space="preserve" text-anchor="start" x="6117.14" y="-546.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">NVARCHAR(100)</text>
<polygon fill="#272e3d" stroke="none" points="5983.29,-509.6 5983.29,-536.4 6193.66,-536.4 6193.66,-509.6 5983.29,-509.6"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-509.6 5983.29,-536.4 6193.66,-536.4 6193.66,-509.6 5983.29,-509.6"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-517.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">unitamount</text>
<text xml:space="preserve" text-anchor="start" x="6159.66" y="-519.4" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">FLOAT</text>
<polygon fill="#2a3242" stroke="none" points="5983.29,-482.8 5983.29,-509.6 6193.66,-509.6 6193.66,-482.8 5983.29,-482.8"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-482.8 5983.29,-509.6 6193.66,-509.6 6193.66,-482.8 5983.29,-482.8"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-490.6" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">weight</text>
<text xml:space="preserve" text-anchor="start" x="6159.66" y="-492.6" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">FLOAT</text>
<polygon fill="#272e3d" stroke="none" points="5983.29,-456 5983.29,-482.8 6193.66,-482.8 6193.66,-456 5983.29,-456"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-456 5983.29,-482.8 6193.66,-482.8 6193.66,-456 5983.29,-456"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-463.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">track_stock</text>
<text xml:space="preserve" text-anchor="start" x="6174.66" y="-465.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">BIT</text>
<polygon fill="#2a3242" stroke="none" points="5983.29,-429.2 5983.29,-456 6193.66,-456 6193.66,-429.2 5983.29,-429.2"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-429.2 5983.29,-456 6193.66,-456 6193.66,-429.2 5983.29,-429.2"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-437" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">is_parent</text>
<text xml:space="preserve" text-anchor="start" x="6174.66" y="-439" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">BIT</text>
<polygon fill="#272e3d" stroke="none" points="5983.29,-402.4 5983.29,-429.2 6193.66,-429.2 6193.66,-402.4 5983.29,-402.4"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-402.4 5983.29,-429.2 6193.66,-429.2 6193.66,-402.4 5983.29,-402.4"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-410.2" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">parentsort</text>
<text xml:space="preserve" text-anchor="start" x="6174.16" y="-412.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT</text>
<polygon fill="#2a3242" stroke="none" points="5983.29,-375.6 5983.29,-402.4 6193.66,-402.4 6193.66,-375.6 5983.29,-375.6"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-375.6 5983.29,-402.4 6193.66,-402.4 6193.66,-375.6 5983.29,-375.6"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-383.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">created_at</text>
<text xml:space="preserve" text-anchor="start" x="6138.15" y="-385.4" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">DATETIME2</text>
<polygon fill="#272e3d" stroke="none" points="5983.29,-348.8 5983.29,-375.6 6193.66,-375.6 6193.66,-348.8 5983.29,-348.8"/>
<polygon fill="none" stroke="#3f4c5f" points="5983.29,-348.8 5983.29,-375.6 6193.66,-375.6 6193.66,-348.8 5983.29,-348.8"/>
<text xml:space="preserve" text-anchor="start" x="5988.29" y="-356.6" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">updated_at</text>
<text xml:space="preserve" text-anchor="start" x="6138.15" y="-358.6" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">DATETIME2</text>
</g>
<!-- articletext&#45;&gt;article -->
<g id="edge47" class="edge">
<title>articletext:e&#45;&gt;article:n</title>
<g id="a_edge47"><a xlink:title="FK_articletext_article">
<path fill="none" stroke="#56d68a" stroke-width="1.4" d="M4940.16,-1075.6C5028.43,-1075.6 4917.3,-939.27 4984.48,-882 5080.64,-800.02 6097.21,-928.13 6144.81,-813.67"/>
<polygon fill="#56d68a" stroke="#56d68a" stroke-width="1.4" points="6147.15,-814.42 6146.12,-807.08 6142.35,-813.46 6147.15,-814.42"/>
<path fill="none" stroke="#56d68a" stroke-width="1.4" d="M4940.16,-1075.6C5028.43,-1075.6 4917.3,-939.26 4984.48,-882 5080.82,-799.87 6099.25,-928.35 6146.94,-813.68"/>
<polygon fill="#56d68a" stroke="#56d68a" stroke-width="1.4" points="6149.29,-814.42 6148.25,-807.08 6144.48,-813.46 6149.29,-814.42"/>
</a>
</g>
</g>
@@ -1181,8 +1181,8 @@
<g id="edge42" class="edge">
<title>categoryarticle:w&#45;&gt;article:n</title>
<g id="a_edge42"><a xlink:title="FK_categoryarticle_article">
<path fill="none" stroke="#ff7aa8" stroke-width="1.4" d="M5742.69,-995.2C5734.94,-995.2 5739.7,-984.44 5743.69,-977.8 5752.44,-963.24 5985.06,-845.39 6023.16,-811.84"/>
<polygon fill="#ff7aa8" stroke="#ff7aa8" stroke-width="1.4" points="6024.81,-813.72 6027.13,-806.68 6020.92,-810.74 6024.81,-813.72"/>
<path fill="none" stroke="#ff7aa8" stroke-width="1.4" d="M5742.69,-995.2C5734.94,-995.2 5739.7,-984.44 5743.69,-977.8 5752.44,-963.25 5984.95,-845.39 6023.03,-811.84"/>
<polygon fill="#ff7aa8" stroke="#ff7aa8" stroke-width="1.4" points="6024.67,-813.72 6027,-806.68 6020.79,-810.74 6024.67,-813.72"/>
</a>
</g>
</g>
@@ -1239,8 +1239,8 @@
<g id="edge27" class="edge">
<title>articlehistory:se&#45;&gt;article:n</title>
<g id="a_edge27"><a xlink:title="FK_articlehistory_article">
<path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M6120.5,-1035.4C6172.88,-983.02 6067.61,-851.52 6048.28,-813.4"/>
<polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="6050.65,-812.77 6046,-807 6046.04,-814.41 6050.65,-812.77"/>
<path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M6120.5,-1035.4C6172.78,-983.12 6067.75,-851.54 6048.46,-813.4"/>
<polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="6050.84,-812.78 6046.19,-807 6046.22,-814.41 6050.84,-812.78"/>
</a>
</g>
</g>
@@ -1269,8 +1269,8 @@
<g id="edge32" class="edge">
<title>articleean:se&#45;&gt;article:n</title>
<g id="a_edge32"><a xlink:title="FK_articleean_article">
<path fill="none" stroke="#5cd6d6" stroke-width="1.4" d="M6312.71,-1008.6C6322.4,-998.92 6317.84,-990.05 6311.71,-977.8 6285.56,-925.54 6113.92,-871.94 6097.22,-813.96"/>
<polygon fill="#5cd6d6" stroke="#5cd6d6" stroke-width="1.4" points="6099.65,-813.67 6096.22,-807.1 6094.8,-814.38 6099.65,-813.67"/>
<path fill="none" stroke="#5cd6d6" stroke-width="1.4" d="M6312.71,-1008.6C6322.4,-998.92 6317.83,-990.06 6311.71,-977.8 6285.71,-925.68 6114.99,-871.77 6098.38,-813.94"/>
<polygon fill="#5cd6d6" stroke="#5cd6d6" stroke-width="1.4" points="6100.81,-813.67 6097.38,-807.1 6095.96,-814.38 6100.81,-813.67"/>
</a>
</g>
</g>
@@ -1278,7 +1278,7 @@
<g id="edge19" class="edge">
<title>article:e&#45;&gt;company:n</title>
<g id="a_edge19"><a xlink:title="FK_article_company">
<path fill="none" stroke="#56c8c8" stroke-width="1.4" d="M6191.72,-576.6C6294.74,-576.6 6261.65,-419.51 6190.72,-344.8 6114.32,-264.34 6052.33,-335.35 5941.48,-330.8 5875.91,-328.11 3647.51,-335.03 3512.65,-273.74"/>
<path fill="none" stroke="#56c8c8" stroke-width="1.4" d="M6194.66,-576.6C6297.68,-576.6 6264.62,-419.49 6193.66,-344.8 6116.34,-263.42 6053.64,-335.36 5941.48,-330.8 5875.91,-328.13 3647.51,-335.03 3512.65,-273.74"/>
<polygon fill="#56c8c8" stroke="#56c8c8" stroke-width="1.4" points="3514.61,-272.22 3507.82,-269.24 3511.27,-275.81 3514.61,-272.22"/>
</a>
</g>
@@ -1287,7 +1287,7 @@
<g id="edge21" class="edge">
<title>article:e&#45;&gt;brand:n</title>
<g id="a_edge21"><a xlink:title="FK_article_brand">
<path fill="none" stroke="#ffb25a" stroke-width="1.4" d="M6191.72,-710.6C6340.54,-710.6 6365.68,-780.13 6511.48,-810 6664.24,-841.3 7096.32,-774.72 7209.48,-882 7280.66,-949.48 7169.89,-1035.5 7243.79,-1100 7257.33,-1111.82 7281.94,-1119.64 7290,-1109.65"/>
<path fill="none" stroke="#ffb25a" stroke-width="1.4" d="M6194.66,-710.6C6342.24,-710.6 6366.94,-780.19 6511.48,-810 6664.2,-841.5 7096.32,-774.72 7209.48,-882 7280.66,-949.48 7169.89,-1035.5 7243.79,-1100 7257.33,-1111.82 7281.94,-1119.64 7290,-1109.65"/>
<polygon fill="#ffb25a" stroke="#ffb25a" stroke-width="1.4" points="7292.32,-1110.44 7292.01,-1103.03 7287.63,-1109.01 7292.32,-1110.44"/>
</a>
</g>
@@ -1295,9 +1295,9 @@
<!-- article&#45;&gt;article -->
<g id="edge24" class="edge">
<title>article:e&#45;&gt;article:e</title>
<g id="a_edge24"><a xlink:title="FK_article_fatherarticle">
<path fill="none" stroke="#c79bff" stroke-width="1.4" d="M6190.72,-683.8C6229.26,-683.8 6229.26,-800.78 6200.36,-778.31"/>
<polygon fill="#c79bff" stroke="#c79bff" stroke-width="1.4" points="6193.26,-772.78 6198.22,-781.07 6202.51,-775.54 6193.26,-772.78"/>
<g id="a_edge24"><a xlink:title="FK_article_parentarticle">
<path fill="none" stroke="#c79bff" stroke-width="1.4" d="M6193.66,-683.8C6232.21,-683.8 6232.21,-800.78 6203.31,-778.31"/>
<polygon fill="#c79bff" stroke="#c79bff" stroke-width="1.4" points="6196.21,-772.78 6201.17,-781.07 6205.46,-775.54 6196.21,-772.78"/>
</a>
</g>
</g>
@@ -1319,7 +1319,7 @@
<g id="edge22" class="edge">
<title>article:e&#45;&gt;articlelist:n</title>
<g id="a_edge22"><a xlink:title="FK_article_articlelist">
<path fill="none" stroke="#56d68a" stroke-width="1.4" d="M6191.72,-657C6357.42,-657 6560.06,-768.28 6569.46,-615.46"/>
<path fill="none" stroke="#56d68a" stroke-width="1.4" d="M6194.66,-657C6359.1,-657 6560.13,-767.04 6569.47,-615.38"/>
<polygon fill="#56d68a" stroke="#56d68a" stroke-width="1.4" points="6571.98,-613.19 6569.74,-606.12 6567.08,-613.04 6571.98,-613.19"/>
</a>
</g>
@@ -1342,7 +1342,7 @@
<g id="edge23" class="edge">
<title>article:e&#45;&gt;optionslist:n</title>
<g id="a_edge23"><a xlink:title="FK_article_optionslist">
<path fill="none" stroke="#66b3ff" stroke-width="1.4" d="M6191.72,-630.2C6375.89,-630.2 6368.17,-796.19 6545.48,-846 6731.9,-898.37 6910.33,-803.57 6915.69,-612.86"/>
<path fill="none" stroke="#66b3ff" stroke-width="1.4" d="M6194.66,-630.2C6377.72,-630.2 6369.34,-796.16 6545.48,-846 6731.81,-898.72 6910.33,-803.58 6915.69,-612.86"/>
<polygon fill="#66b3ff" stroke="#66b3ff" stroke-width="1.4" points="6918.13,-613.15 6915.78,-606.12 6913.23,-613.08 6918.13,-613.15"/>
</a>
</g>
@@ -1351,7 +1351,7 @@
<g id="edge20" class="edge">
<title>article:w&#45;&gt;unit:n</title>
<g id="a_edge20"><a xlink:title="FK_article_unit">
<path fill="none" stroke="#ff5f9e" stroke-width="1.4" d="M5983.24,-603.4C5936.4,-603.4 5963.03,-768.41 5941.48,-810 5931.35,-829.54 5927.69,-837.29 5907.48,-846 5752.17,-912.94 4417.78,-832.67 4370.91,-666.6"/>
<path fill="none" stroke="#ff5f9e" stroke-width="1.4" d="M5982.29,-603.4C5935.5,-603.4 5962.92,-768.4 5941.48,-810 5931.39,-829.56 5927.69,-837.29 5907.48,-846 5752.17,-912.94 4417.78,-832.67 4370.91,-666.6"/>
<polygon fill="#ff5f9e" stroke="#ff5f9e" stroke-width="1.4" points="4373.34,-666.29 4369.94,-659.7 4368.49,-666.97 4373.34,-666.29"/>
</a>
</g>
@@ -1391,8 +1391,8 @@
<!-- article&#45;&gt;attributename -->
<g id="edge25" class="edge">
<title>article:sw&#45;&gt;attributename:n</title>
<g id="a_edge25"><a xlink:title="FK_article_fatherattributename">
<path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M5983.24,-724C5975.73,-716.49 5947.51,-801.26 5941.48,-810 5933.14,-822.07 5932.28,-828.19 5918.81,-834 5833.26,-870.88 2657.86,-843.08 2565.14,-834 2367.07,-814.6 1693.85,-858.33 1673.51,-666.61"/>
<g id="a_edge25"><a xlink:title="FK_article_parentattributename">
<path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M5982.29,-724C5974.81,-716.52 5947.45,-801.27 5941.48,-810 5933.19,-822.11 5932.28,-828.19 5918.81,-834 5833.26,-870.88 2657.86,-843.08 2565.14,-834 2367.07,-814.6 1693.85,-858.33 1673.51,-666.61"/>
<polygon fill="#ff6b6b" stroke="#ff6b6b" stroke-width="1.4" points="1675.96,-666.58 1673.15,-659.72 1671.07,-666.83 1675.96,-666.58"/>
</a>
</g>
@@ -1400,8 +1400,8 @@
<!-- article&#45;&gt;attributename -->
<g id="edge26" class="edge">
<title>article:sw&#45;&gt;attributename:n</title>
<g id="a_edge26"><a xlink:title="FK_article_fatherattributename">
<path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M5983.24,-724C5975.73,-716.49 5947.51,-801.26 5941.48,-810 5924.8,-834.14 5923.09,-846.38 5896.14,-858 5811.75,-894.38 2679.18,-867.89 2587.81,-858 2383.95,-835.93 1693.67,-864.83 1673.49,-666.59"/>
<g id="a_edge26"><a xlink:title="FK_article_parentattributename">
<path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M5982.29,-724C5974.81,-716.52 5947.45,-801.27 5941.48,-810 5924.91,-834.22 5923.09,-846.38 5896.14,-858 5811.75,-894.38 2679.18,-867.89 2587.81,-858 2383.95,-835.93 1693.67,-864.83 1673.49,-666.59"/>
<polygon fill="#ff6b6b" stroke="#ff6b6b" stroke-width="1.4" points="1675.94,-666.58 1673.15,-659.72 1671.05,-666.83 1675.94,-666.58"/>
</a>
</g>
@@ -1463,8 +1463,8 @@
<g id="edge52" class="edge">
<title>articlesupplier:sw&#45;&gt;article:n</title>
<g id="a_edge52"><a xlink:title="FK_articlesupplier_article">
<path fill="none" stroke="#5aa7ff" stroke-width="1.4" d="M6394.93,-1075.6C6332.71,-1013.38 6410.06,-949.39 6353.48,-882 6296.93,-814.65 6172.56,-890.43 6163.89,-813.98"/>
<polygon fill="#5aa7ff" stroke="#5aa7ff" stroke-width="1.4" points="6166.34,-813.98 6163.52,-807.12 6161.45,-814.24 6166.34,-813.98"/>
<path fill="none" stroke="#5aa7ff" stroke-width="1.4" d="M6394.93,-1075.6C6332.71,-1013.38 6409.91,-949.52 6353.48,-882 6297.71,-815.27 6174.9,-889.53 6166.34,-813.88"/>
<polygon fill="#5aa7ff" stroke="#5aa7ff" stroke-width="1.4" points="6168.8,-813.98 6165.98,-807.12 6163.9,-814.24 6168.8,-813.98"/>
</a>
</g>
</g>
@@ -1547,8 +1547,8 @@
<g id="edge38" class="edge">
<title>articlelistarticle:sw&#45;&gt;article:n</title>
<g id="a_edge38"><a xlink:title="FK_articlelistarticle_article">
<path fill="none" stroke="#a98cff" stroke-width="1.4" d="M6598.13,-1035.4C6548.26,-985.53 6609.33,-929.81 6557.48,-882 6488.82,-818.69 6153.04,-896.11 6130.82,-814.03"/>
<polygon fill="#a98cff" stroke="#a98cff" stroke-width="1.4" points="6133.25,-813.73 6129.93,-807.1 6128.39,-814.36 6133.25,-813.73"/>
<path fill="none" stroke="#a98cff" stroke-width="1.4" d="M6598.13,-1035.4C6548.26,-985.53 6609.47,-929.66 6557.48,-882 6486.09,-816.57 6137.08,-899.46 6115.34,-813.82"/>
<polygon fill="#a98cff" stroke="#a98cff" stroke-width="1.4" points="6117.8,-813.76 6114.53,-807.1 6112.93,-814.35 6117.8,-813.76"/>
</a>
</g>
</g>
@@ -1599,8 +1599,8 @@
<g id="edge40" class="edge">
<title>optionslistarticle:e&#45;&gt;article:n</title>
<g id="a_edge40"><a xlink:title="FK_optionslistarticle_article">
<path fill="none" stroke="#4ecb8f" stroke-width="1.4" d="M6950.47,-1062.2C7005.85,-1062.2 6982.53,-982.03 6949.47,-937.6 6899.5,-870.42 6851.28,-899.84 6769.48,-882 6707.87,-868.56 6226.49,-870.78 6183.37,-813.49"/>
<polygon fill="#4ecb8f" stroke="#4ecb8f" stroke-width="1.4" points="6185.7,-812.73 6181,-806.99 6181.09,-814.41 6185.7,-812.73"/>
<path fill="none" stroke="#4ecb8f" stroke-width="1.4" d="M6950.47,-1062.2C7005.85,-1062.2 6982.53,-982.03 6949.47,-937.6 6899.5,-870.42 6851.28,-899.87 6769.48,-882 6708.16,-868.6 6229.06,-870.5 6186.13,-813.45"/>
<polygon fill="#4ecb8f" stroke="#4ecb8f" stroke-width="1.4" points="6188.48,-812.73 6183.79,-806.99 6183.88,-814.41 6188.48,-812.73"/>
</a>
</g>
</g>
@@ -1786,8 +1786,8 @@
<g id="edge33" class="edge">
<title>articlepic:se&#45;&gt;article:n</title>
<g id="a_edge33"><a xlink:title="FK_articlepic_article">
<path fill="none" stroke="#ff7fc2" stroke-width="1.4" d="M5812.71,-1425.2C5869.84,-1368.07 5739.02,-1329.56 5701.48,-1211 5657.33,-1071.6 5609.51,-995.68 5701.48,-882 5802.67,-756.93 6052.66,-961.08 6061.9,-813.74"/>
<polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="6064.34,-814.19 6062.1,-807.12 6059.44,-814.04 6064.34,-814.19"/>
<path fill="none" stroke="#ff7fc2" stroke-width="1.4" d="M5812.71,-1425.2C5869.84,-1368.07 5739.02,-1329.56 5701.48,-1211 5657.33,-1071.6 5609.49,-995.66 5701.48,-882 5802.82,-756.77 6053.16,-961.29 6062.42,-813.75"/>
<polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="6064.86,-814.19 6062.62,-807.12 6059.96,-814.04 6064.86,-814.19"/>
</a>
</g>
</g>
@@ -2136,8 +2136,8 @@
<g id="edge50" class="edge">
<title>price:e&#45;&gt;article:n</title>
<g id="a_edge50"><a xlink:title="FK_price_article">
<path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M5384.43,-1060.2C5502.18,-1060.2 5473.98,-932.23 5580.48,-882 5685.73,-832.36 6090.64,-919.69 6111.92,-814.02"/>
<polygon fill="#ff6b6b" stroke="#ff6b6b" stroke-width="1.4" points="6114.35,-814.31 6112.58,-807.11 6109.47,-813.84 6114.35,-814.31"/>
<path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M5384.43,-1060.2C5502.18,-1060.2 5473.86,-931.97 5580.48,-882 5689.58,-830.86 6109.39,-923.7 6130.63,-813.99"/>
<polygon fill="#ff6b6b" stroke="#ff6b6b" stroke-width="1.4" points="6133.07,-814.31 6131.27,-807.11 6128.19,-813.85 6133.07,-814.31"/>
</a>
</g>
</g>
@@ -2340,8 +2340,8 @@
<g id="edge71" class="edge">
<title>posterminalcategory:w&#45;&gt;category:n</title>
<g id="a_edge71"><a xlink:title="FK_posterminalcategory_category">
<path fill="none" stroke="#56d68a" stroke-width="1.4" d="M7943.19,-1022C7878.39,-1022 7953.58,-921.85 7902.48,-882 7730.62,-748 6127.13,-925.74 5942.48,-810 5874.42,-767.34 5855.89,-730.45 5854.61,-653.3"/>
<polygon fill="#56d68a" stroke="#56d68a" stroke-width="1.4" points="5857.06,-653.3 5854.55,-646.32 5852.16,-653.34 5857.06,-653.3"/>
<path fill="none" stroke="#56d68a" stroke-width="1.4" d="M7943.19,-1022C7878.39,-1022 7953.58,-921.84 7902.48,-882 7730.53,-747.93 6125.95,-926.23 5941.48,-810 5873.69,-767.29 5855.83,-730.25 5854.6,-653.28"/>
<polygon fill="#56d68a" stroke="#56d68a" stroke-width="1.4" points="5857.05,-653.3 5854.55,-646.32 5852.15,-653.34 5857.05,-653.3"/>
</a>
</g>
</g>
@@ -2700,8 +2700,8 @@
<g id="edge77" class="edge">
<title>articleproperty:w&#45;&gt;article:n</title>
<g id="a_edge77"><a xlink:title="FK_articleproperty_article">
<path fill="none" stroke="#f5c15c" stroke-width="1.4" d="M1725.74,-1022C1694.18,-1022 1708.96,-977.08 1726.74,-951 1772.38,-884.05 1816.12,-898.33 1895.48,-882 2004.63,-859.53 5825.12,-916.35 5989.23,-812.1"/>
<polygon fill="#f5c15c" stroke="#f5c15c" stroke-width="1.4" points="5991.07,-813.73 5993.39,-806.68 5987.18,-810.75 5991.07,-813.73"/>
<path fill="none" stroke="#f5c15c" stroke-width="1.4" d="M1725.74,-1022C1694.18,-1022 1708.96,-977.08 1726.74,-951 1772.38,-884.05 1816.12,-898.33 1895.48,-882 2004.61,-859.54 5824.37,-916.33 5988.45,-812.1"/>
<polygon fill="#f5c15c" stroke="#f5c15c" stroke-width="1.4" points="5990.29,-813.73 5992.6,-806.68 5986.4,-810.75 5990.29,-813.73"/>
</a>
</g>
</g>
@@ -2744,8 +2744,8 @@
<g id="edge81" class="edge">
<title>articleattributevalue:w&#45;&gt;article:n</title>
<g id="a_edge81"><a xlink:title="FK_articleattributevalue_article">
<path fill="none" stroke="#ff7fc2" stroke-width="1.4" d="M1936.97,-1008.6C1917.32,-1008.6 1927.3,-980.9 1937.97,-964.4 1984.1,-893.04 2027.79,-901.55 2110.48,-882 2213.58,-857.62 5842.9,-910.89 6005.89,-812.06"/>
<polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="6007.75,-813.65 6010.22,-806.65 6003.93,-810.58 6007.75,-813.65"/>
<path fill="none" stroke="#ff7fc2" stroke-width="1.4" d="M1936.97,-1008.6C1917.32,-1008.6 1927.3,-980.9 1937.97,-964.4 1984.1,-893.04 2027.79,-901.55 2110.48,-882 2213.57,-857.62 5842.47,-910.88 6005.43,-812.06"/>
<polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="6007.3,-813.65 6009.76,-806.65 6003.47,-810.58 6007.3,-813.65"/>
</a>
</g>
</g>

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 294 KiB

View File

@@ -52,7 +52,7 @@ export async function fillDatabase(store, options = {}) {
secondaryCompany: null,
categories: [],
articles: [],
fatherArticles: [],
parentArticles: [],
childArticles: [],
customergroups: [],
entities: [],
@@ -326,7 +326,7 @@ export async function fillDatabase(store, options = {}) {
const numArticles = counts.articles;
for (let i = 1; i <= numArticles; i++) {
const targetCat = ctx.categories[i % ctx.categories.length];
const isFather = i % 4 === 1;
const isParent = i % 4 === 1;
const art = await store.article.createUnderParent({
googleAuthId: ctx.googleAuthId,
parentId: targetCat.id,
@@ -365,12 +365,12 @@ export async function fillDatabase(store, options = {}) {
ctx.categories[(i + 1) % ctx.categories.length].id,
]);
// V/K Father-Child hierarchy
if (isFather) {
ctx.fatherArticles.push(art);
} else if (ctx.fatherArticles.length > 0) {
const parentFather = ctx.fatherArticles[ctx.fatherArticles.length - 1];
await store.article.setFatherBySku(ctx.googleAuthId, sku, parentFather.sku);
// V/K Parent-Child hierarchy
if (isParent) {
ctx.parentArticles.push(art);
} else if (ctx.parentArticles.length > 0) {
const parentParent = ctx.parentArticles[ctx.parentArticles.length - 1];
await store.article.setParentBySku(ctx.googleAuthId, sku, parentParent.sku);
ctx.childArticles.push(art);
}
@@ -426,7 +426,7 @@ export async function fillDatabase(store, options = {}) {
brand: 'BenchBrand Pro',
net: '45,50',
tax: 19,
fatherSku: `IMP-${Date.now()}-1`,
parentSku: `IMP-${Date.now()}-1`,
},
];
await store.article.importRows({

View File

@@ -6,7 +6,7 @@ import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing Father History & AttributeValue Shapes ---');
console.log('--- Testing Parent History & AttributeValue Shapes ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-hist-attr-'));
const dbFile = join(tempDir, 'test.db');
@@ -31,11 +31,11 @@ async function run() {
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
console.log('1. Testing setFatherBySku history logging...');
const father = await store.article.createUnderParent({
console.log('1. Testing setParentBySku history logging...');
const parent = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Father Shirt',
name: 'Parent Shirt',
});
const child = await store.article.createUnderParent({
googleAuthId,
@@ -44,19 +44,19 @@ async function run() {
});
// Check history before linking
let fatherHist = await store.article.getHistoryBySku(googleAuthId, father.sku);
assert.equal(fatherHist.length, 1);
assert.match(fatherHist[0].text, /angelegt/);
let parentHist = await store.article.getHistoryBySku(googleAuthId, parent.sku);
assert.equal(parentHist.length, 1);
assert.match(parentHist[0].text, /angelegt/);
// Link child to father
await store.article.setFatherBySku(googleAuthId, child.sku, father.sku);
// Link child to parent
await store.article.setParentBySku(googleAuthId, child.sku, parent.sku);
// Father history must now contain the conversion entry and the child entry!
fatherHist = await store.article.getHistoryBySku(googleAuthId, father.sku);
const hasConversion = fatherHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
const hasVariantAdded = fatherHist.some((h) => h.text.includes('hinzugefügt'));
assert.ok(hasConversion, `Father history missing conversion row: ${JSON.stringify(fatherHist)}`);
assert.ok(hasVariantAdded, `Father history missing variant added row: ${JSON.stringify(fatherHist)}`);
// Parent history must now contain the conversion entry and the child entry!
parentHist = await store.article.getHistoryBySku(googleAuthId, parent.sku);
const hasConversion = parentHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
const hasVariantAdded = parentHist.some((h) => h.text.includes('hinzugefügt'));
assert.ok(hasConversion, `Parent history missing conversion row: ${JSON.stringify(parentHist)}`);
assert.ok(hasVariantAdded, `Parent history missing variant added row: ${JSON.stringify(parentHist)}`);
// Child history must contain the assignment entry
const childHist = await store.article.getHistoryBySku(googleAuthId, child.sku);
@@ -67,15 +67,15 @@ async function run() {
const importRes = await store.article.importRows({
googleAuthId,
rows: [
{ sku: 'IMP-FATHER', name: 'Imported Father', parentId: company.rootCategoryId },
{ sku: 'IMP-CHILD', name: 'Imported Child', parentId: company.rootCategoryId, fatherSku: 'IMP-FATHER' },
{ sku: 'IMP-PARENT', name: 'Imported Parent', parentId: company.rootCategoryId },
{ sku: 'IMP-CHILD', name: 'Imported Child', parentId: company.rootCategoryId, parentSku: 'IMP-PARENT' },
],
});
assert.equal(importRes.errors.length, 0);
const impFatherHist = await store.article.getHistoryBySku(googleAuthId, 'IMP-FATHER');
const impHasConversion = impFatherHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
assert.ok(impHasConversion, `Imported father missing conversion row: ${JSON.stringify(impFatherHist)}`);
const impParentHist = await store.article.getHistoryBySku(googleAuthId, 'IMP-PARENT');
const impHasConversion = impParentHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
assert.ok(impHasConversion, `Imported parent missing conversion row: ${JSON.stringify(impParentHist)}`);
console.log('3. Testing flexible attributevalue request shapes in setByArticleSku...');
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });

View File

@@ -119,31 +119,31 @@ async function run() {
assert.equal(tomb.taxrate, 19);
assert.ok(tomb.deleted_at);
// 5. Test cascading deletion with father and variants
console.log('5. Testing father with variant child deletion...');
const father = await store.article.createUnderParent({
// 5. Test cascading deletion with parent and variants
console.log('5. Testing parent with variant child deletion...');
const parent = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Pants',
});
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
await store.article.toggleFather(googleAuthId, father.sku, {
isFather: true,
await store.article.toggleParent(googleAuthId, parent.sku, {
isParent: true,
attributeNameId: attrName.id,
});
const child = await store.article.addChild(googleAuthId, father.sku, { value: 'M' });
const child = await store.article.addChild(googleAuthId, parent.sku, { value: 'M' });
await store.article.setPricesBySku(googleAuthId, child.article.sku, [
{ price: 49.99, tax: 19, minmultiplier: 1 },
]);
// Delete child via removeChild with deleteArticle
console.log('Deleting child via removeChild({ deleteArticle: true })...');
await store.article.removeChild(googleAuthId, father.sku, child.article.sku, { deleteArticle: true });
await store.article.removeChild(googleAuthId, parent.sku, child.article.sku, { deleteArticle: true });
// Add another child and delete father with cascade: true
const child2 = await store.article.addChild(googleAuthId, father.sku, { value: 'L' });
console.log('Deleting father with cascade: true...');
await store.article.delete(googleAuthId, father.sku, { cascade: true });
// Add another child and delete parent with cascade: true
const child2 = await store.article.addChild(googleAuthId, parent.sku, { value: 'L' });
console.log('Deleting parent with cascade: true...');
await store.article.delete(googleAuthId, parent.sku, { cascade: true });
const allTombstones = provider.db.prepare('SELECT * FROM articletombstone ORDER BY id ASC').all();
assert.equal(allTombstones.length, 4);
@@ -164,9 +164,9 @@ async function run() {
assert.equal(allTombstones[2].name, 'Pants - L');
assert.equal(allTombstones[2].sku, child2.article.sku);
// 4th tombstone: Pants (father deleted in cascade)
// 4th tombstone: Pants (parent deleted in cascade)
assert.equal(allTombstones[3].name, 'Pants');
assert.equal(allTombstones[3].sku, father.sku);
assert.equal(allTombstones[3].sku, parent.sku);
// 6. Test posterminal maxknowntombstoneid column and updates
console.log('6. Testing posterminal maxknowntombstoneid column...');

View File

@@ -35,65 +35,65 @@ async function run() {
const art1 = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Root Father Shirt',
name: 'Root Parent Shirt',
});
const art1Res = await store.article.getBySku(googleAuthId, art1.sku);
assert.equal(art1Res.article.slug, 'root-father-shirt', `Expected slug 'root-father-shirt', got '${art1Res.article.slug}'`);
assert.equal(art1Res.article.slug, 'root-parent-shirt', `Expected slug 'root-parent-shirt', got '${art1Res.article.slug}'`);
const art2 = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Root Father Shirt',
name: 'Root Parent Shirt',
});
const art2Res = await store.article.getBySku(googleAuthId, art2.sku);
assert.equal(art2Res.article.slug, 'root-father-shirt-2', `Expected slug 'root-father-shirt-2', got '${art2Res.article.slug}'`);
assert.equal(art2Res.article.slug, 'root-parent-shirt-2', `Expected slug 'root-parent-shirt-2', got '${art2Res.article.slug}'`);
const art3 = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Root Father Shirt',
name: 'Root Parent Shirt',
});
const art3Res = await store.article.getBySku(googleAuthId, art3.sku);
assert.equal(art3Res.article.slug, 'root-father-shirt-3', `Expected slug 'root-father-shirt-3', got '${art3Res.article.slug}'`);
assert.equal(art3Res.article.slug, 'root-parent-shirt-3', `Expected slug 'root-parent-shirt-3', got '${art3Res.article.slug}'`);
console.log('2. Testing updateBySku self-collision and renaming...');
// Update art2 without changing name
await store.article.updateBySku(googleAuthId, art2.sku, { unitamount: 2 });
let updated2 = await store.article.getBySku(googleAuthId, art2.sku);
assert.equal(updated2.article.slug, 'root-father-shirt-2', `Expected unchanged slug, got '${updated2.article.slug}'`);
assert.equal(updated2.article.slug, 'root-parent-shirt-2', `Expected unchanged slug, got '${updated2.article.slug}'`);
// Update art2 with same name
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Root Father Shirt' });
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Root Parent Shirt' });
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
assert.equal(updated2.article.slug, 'root-father-shirt-2', `Expected slug to remain 'root-father-shirt-2', got '${updated2.article.slug}'`);
assert.equal(updated2.article.slug, 'root-parent-shirt-2', `Expected slug to remain 'root-parent-shirt-2', got '${updated2.article.slug}'`);
// Rename art2 to something else
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Unique Shirt' });
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
assert.equal(updated2.article.slug, 'unique-shirt', `Expected slug 'unique-shirt', got '${updated2.article.slug}'`);
// Rename art3 to 'Root Father Shirt' again: should take root-father-shirt-2 now that it is free
await store.article.updateBySku(googleAuthId, art3.sku, { name: 'Root Father Shirt' });
// Rename art3 to 'Root Parent Shirt' again: should take root-parent-shirt-2 now that it is free
await store.article.updateBySku(googleAuthId, art3.sku, { name: 'Root Parent Shirt' });
const updated3 = await store.article.getBySku(googleAuthId, art3.sku);
assert.equal(updated3.article.slug, 'root-father-shirt-2', `Expected slug to fill gap 'root-father-shirt-2', got '${updated3.article.slug}'`);
assert.equal(updated3.article.slug, 'root-parent-shirt-2', `Expected slug to fill gap 'root-parent-shirt-2', got '${updated3.article.slug}'`);
console.log('3. Testing addChild duplicate slugs...');
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
await store.article.toggleFather(googleAuthId, art1.sku, {
isFather: true,
await store.article.toggleParent(googleAuthId, art1.sku, {
isParent: true,
attributeNameId: attrName.id,
});
const child1 = await store.article.addChild(googleAuthId, art1.sku, {
value: 'M',
});
assert.equal(child1.article.slug, 'root-father-shirt-m', `Expected child slug 'root-father-shirt-m', got '${child1.article.slug}'`);
assert.equal(child1.article.slug, 'root-parent-shirt-m', `Expected child slug 'root-parent-shirt-m', got '${child1.article.slug}'`);
// Add another child with name that yields same slug
const child2 = await store.article.addChild(googleAuthId, art1.sku, {
value: 'L',
name: 'Root Father Shirt - M',
name: 'Root Parent Shirt - M',
});
assert.equal(child2.article.slug, 'root-father-shirt-m-2', `Expected child slug 'root-father-shirt-m-2', got '${child2.article.slug}'`);
assert.equal(child2.article.slug, 'root-parent-shirt-m-2', `Expected child slug 'root-parent-shirt-m-2', got '${child2.article.slug}'`);
console.log('4. Testing importRows duplicate slugs in same batch...');
const importRes = await store.article.importRows({
@@ -128,10 +128,10 @@ async function run() {
const artCompany2 = await store.article.createUnderParent({
googleAuthId,
parentId: company2.rootCategoryId,
name: 'Root Father Shirt',
name: 'Root Parent Shirt',
});
const artCompany2Detail = await store.article.getBySku(googleAuthId, artCompany2.sku);
assert.equal(artCompany2Detail.article.slug, 'root-father-shirt', `Expected 'root-father-shirt' in company 2, got '${artCompany2Detail.article.slug}'`);
assert.equal(artCompany2Detail.article.slug, 'root-parent-shirt', `Expected 'root-parent-shirt' in company 2, got '${artCompany2Detail.article.slug}'`);
console.log('✅ ALL SLUG TESTS PASSED');
} finally {

View File

@@ -6,7 +6,7 @@ import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing Variant Flow: Next-Free SKU, Fathersort Reclaim, and Duplicate Protection ---');
console.log('--- Testing Variant Flow: Next-Free SKU, Parentsort Reclaim, and Duplicate Protection ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-variants-'));
const dbFile = join(tempDir, 'test.db');
@@ -31,35 +31,35 @@ async function run() {
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// Create father article
const father = await store.article.createUnderParent({
// Create parent article
const parent = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'T-Shirt',
});
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
await store.article.toggleFather(googleAuthId, father.sku, {
isFather: true,
await store.article.toggleParent(googleAuthId, parent.sku, {
isParent: true,
attributeNameId: attrName.id,
});
console.log('1. Add initial children (Rot, Blau, Grün)...');
const child1 = await store.article.addChild(googleAuthId, father.sku, { value: 'Rot' });
const child2 = await store.article.addChild(googleAuthId, father.sku, { value: 'Blau' });
const child3 = await store.article.addChild(googleAuthId, father.sku, { value: 'Grün' });
const child1 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Rot' });
const child2 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Blau' });
const child3 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Grün' });
assert.equal(child1.article.sku, `${father.sku}-1`);
assert.equal(child2.article.sku, `${father.sku}-2`);
assert.equal(child3.article.sku, `${father.sku}-3`);
assert.equal(child1.article.sku, `${parent.sku}-1`);
assert.equal(child2.article.sku, `${parent.sku}-2`);
assert.equal(child3.article.sku, `${parent.sku}-3`);
let list = await store.article.listChildren(googleAuthId, father.sku);
let list = await store.article.listChildren(googleAuthId, parent.sku);
assert.equal(list.length, 3);
assert.deepEqual(list.map((c) => c.fathersort), [0, 1, 2]);
assert.deepEqual(list.map((c) => c.parentsort), [0, 1, 2]);
console.log('2. Testing Duplicate Variant Protection (Defect 3)...');
await assert.rejects(
async () => {
await store.article.addChild(googleAuthId, father.sku, { value: 'Rot' });
await store.article.addChild(googleAuthId, parent.sku, { value: 'Rot' });
},
(err) => {
assert.match(err.message, /Variant with this attribute value already exists/);
@@ -69,57 +69,57 @@ async function run() {
);
console.log('3. Unlink middle child (child2 / SKU-2) -> leaves orphan holding SKU-2 (Bug A & B)...');
await store.article.removeChild(googleAuthId, father.sku, child2.article.sku, {
await store.article.removeChild(googleAuthId, parent.sku, child2.article.sku, {
deleteArticle: false,
});
// Check that remaining children's fathersort was recompacted
list = await store.article.listChildren(googleAuthId, father.sku);
// Check that remaining children's parentsort was recompacted
list = await store.article.listChildren(googleAuthId, parent.sku);
assert.equal(list.length, 2);
assert.deepEqual(list.map((c) => c.sku), [child1.article.sku, child3.article.sku]);
assert.deepEqual(list.map((c) => c.fathersort), [0, 1]);
assert.deepEqual(list.map((c) => c.parentsort), [0, 1]);
// Check orphan still exists in DB
const orphan = await store.article.getBySku(googleAuthId, child2.article.sku);
assert.equal(orphan.article.fatherArticleId, null);
assert.equal(orphan.article.sku, `${father.sku}-2`);
assert.equal(orphan.article.parentArticleId, null);
assert.equal(orphan.article.sku, `${parent.sku}-2`);
console.log('4. Add new child (Gelb) -> SKU must NOT collide with orphan (Bug A: next free index)...');
const child4 = await store.article.addChild(googleAuthId, father.sku, { value: 'Gelb' });
const child4 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Gelb' });
// Since SKU-1, SKU-2 (orphan), SKU-3 exist, next free index is 4
assert.equal(child4.article.sku, `${father.sku}-4`);
assert.equal(child4.article.sku, `${parent.sku}-4`);
list = await store.article.listChildren(googleAuthId, father.sku);
list = await store.article.listChildren(googleAuthId, parent.sku);
assert.equal(list.length, 3);
assert.deepEqual(list.map((c) => c.fathersort), [0, 1, 2]);
assert.deepEqual(list.map((c) => c.parentsort), [0, 1, 2]);
console.log('5. Testing gaps in fathersort (Evidence: father with child at fathersort: 1)...');
console.log('5. Testing gaps in parentsort (Evidence: parent with child at parentsort: 1)...');
// Unlink child3 and child4 so only child1 remains
await store.article.removeChild(googleAuthId, father.sku, child3.article.sku, { deleteArticle: true });
await store.article.removeChild(googleAuthId, father.sku, child4.article.sku, { deleteArticle: true });
await store.article.removeChild(googleAuthId, parent.sku, child3.article.sku, { deleteArticle: true });
await store.article.removeChild(googleAuthId, parent.sku, child4.article.sku, { deleteArticle: true });
// Manually force child1's fathersort: 1 to simulate pre-existing gap state (like father 94)
// Manually force child1's parentsort: 1 to simulate pre-existing gap state (like parent 94)
await provider.query(
`UPDATE article SET fathersort = 1 WHERE sku = @sku`,
`UPDATE article SET parentsort = 1 WHERE sku = @sku`,
{ sku: child1.article.sku }
);
list = await store.article.listChildren(googleAuthId, father.sku);
list = await store.article.listChildren(googleAuthId, parent.sku);
assert.equal(list.length, 1);
assert.equal(list[0].fathersort, 1); // exactly like father 94
assert.equal(list[0].parentsort, 1); // exactly like parent 94
// Now adding a variant MUST NOT hit UNIQUE constraint failed (409)
console.log('Adding variant to father with child at fathersort: 1...');
const child5 = await store.article.addChild(googleAuthId, father.sku, { value: 'Schwarz' });
console.log('Adding variant to parent with child at parentsort: 1...');
const child5 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Schwarz' });
assert.ok(child5);
list = await store.article.listChildren(googleAuthId, father.sku);
list = await store.article.listChildren(googleAuthId, parent.sku);
assert.equal(list.length, 2);
// Fathersort should have picked 0 (next free non-negative integer)
assert.deepEqual(list.map((c) => c.fathersort), [0, 1]);
// Parentsort should have picked 0 (next free non-negative integer)
assert.deepEqual(list.map((c) => c.parentsort), [0, 1]);
console.log('6. Testing rejecting sku when creating via addChild...');
await assert.rejects(
async () => {
await store.article.addChild(googleAuthId, father.sku, {
await store.article.addChild(googleAuthId, parent.sku, {
sku: 'SOME-CUSTOM-SKU',
value: 'Lila',
});
@@ -132,15 +132,15 @@ async function run() {
);
console.log('7. Testing attaching existing orphan article (SKU-2) via attachChild...');
const reconnected = await store.article.attachChild(googleAuthId, father.sku, {
const reconnected = await store.article.attachChild(googleAuthId, parent.sku, {
sku: child2.article.sku,
value: 'Blau',
});
assert.equal(reconnected.article.sku, child2.article.sku);
assert.equal(reconnected.article.fatherArticleId, father.id);
assert.equal(reconnected.article.parentArticleId, parent.id);
console.log('8. Testing idempotent attachChild on same father...');
const idempotentResult = await store.article.attachChild(googleAuthId, father.sku, {
console.log('8. Testing idempotent attachChild on same parent...');
const idempotentResult = await store.article.attachChild(googleAuthId, parent.sku, {
sku: child2.article.sku,
value: 'Blau',
});

View File

@@ -32,8 +32,8 @@ import articleSetBrandIdBySku from './store/article/setBrandIdBySku.js';
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 articleSetParentBySku from './store/article/setParentBySku.js';
import articleToggleParent from './store/article/toggleParent.js';
import articleAddChild from './store/article/addChild.js';
import articleAttachChild from './store/article/attachChild.js';
import articleRemoveChild from './store/article/removeChild.js';
@@ -267,8 +267,8 @@ export default class LibDashStore {
setWeightBySku: articleSetWeightBySku(provider),
setTrackStockBySku: articleSetTrackStockBySku(provider),
setCategoryIdsBySku: articleSetCategoryIdsBySku(provider),
setFatherBySku: articleSetFatherBySku(provider),
toggleFather: articleToggleFather(provider),
setParentBySku: articleSetParentBySku(provider),
toggleParent: articleToggleParent(provider),
addChild: articleAddChild(provider),
attachChild: articleAttachChild(provider),
removeChild: articleRemoveChild(provider),

View File

@@ -21,7 +21,7 @@ OUTER APPLY (
ORDER BY bp.sort ASC, bp.id ASC
) first_brand_pic
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
LEFT JOIN article father ON father.id = a.fatherarticle_id
LEFT JOIN article parent ON parent.id = a.parentarticle_id
`;
export const ARTICLE_LIST_COLUMNS = `
@@ -32,7 +32,7 @@ export const ARTICLE_LIST_COLUMNS = `
pr.net,
t.rate AS taxRate,
a.track_stock,
a.is_father AS isFather,
a.is_parent AS isParent,
CASE
WHEN a.optionslist_id IS NOT NULL THEN (
SELECT COUNT(*)
@@ -52,10 +52,10 @@ export const ARTICLE_LIST_COLUMNS = `
(
SELECT COUNT(*)
FROM article child
WHERE child.fatherarticle_id = a.id
WHERE child.parentarticle_id = a.id
) AS childCount,
a.fatherarticle_id AS fatherArticleId,
father.sku AS fatherSku,
a.parentarticle_id AS parentArticleId,
parent.sku AS parentSku,
pic.hash AS fullHash,
pic.thumbw AS thumbW,
pic.thumbh AS thumbH,
@@ -75,7 +75,7 @@ INNER JOIN categoryarticle ca ON ca.article_id = a.id
WHERE a.sku = @sku AND (@companyId IS NULL OR a.company_id = @companyId)
`;
/** Resolve an article id by sku (used to wire father/child compound links). */
/** Resolve an article id by sku (used to wire parent/child compound links). */
export const GET_ID_BY_SKU = `
SELECT TOP 1 id AS articleId
FROM article
@@ -89,49 +89,49 @@ FROM articleean
WHERE ean = @ean
`;
/** Link an article to its father article and mark the father. */
export const UPDATE_FATHER_BY_SKU = `
/** Link an article to its parent article and mark the parent. */
export const UPDATE_PARENT_BY_SKU = `
UPDATE article
SET fatherarticle_id = @fatherArticleId,
fathersort = COALESCE(@fathersort, 0)
SET parentarticle_id = @parentArticleId,
parentsort = COALESCE(@parentsort, 0)
WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)
`;
export const SET_IS_FATHER = `
export const SET_IS_PARENT = `
UPDATE article
SET is_father = 1
SET is_parent = 1
WHERE id = @articleId
`;
export const TOGGLE_FATHER = `
export const TOGGLE_PARENT = `
UPDATE article
SET is_father = @isFather,
fatherattributename_id = @fatherAttributeNameId
SET is_parent = @isParent,
parentattributename_id = @parentAttributeNameId
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
company_id, sku, unit_id, unitamount, weight, track_stock, brand_id, is_parent, parentarticle_id, parentsort
)
OUTPUT INSERTED.id
VALUES (
@companyId, @sku, @unitId, @unitamount, @weight, @trackStock, @brandId, 0, @fatherArticleId, @fathersort
@companyId, @sku, @unitId, @unitamount, @weight, @trackStock, @brandId, 0, @parentArticleId, @parentsort
)
`;
export const UNLINK_CHILD = `
UPDATE article
SET fatherarticle_id = NULL,
fathersort = 0
WHERE id = @articleId AND fatherarticle_id = @fatherArticleId
SET parentarticle_id = NULL,
parentsort = 0
WHERE id = @articleId AND parentarticle_id = @parentArticleId
`;
export const LIST_CHILDREN_BY_FATHER_ID = `
export const LIST_CHILDREN_BY_PARENT_ID = `
SELECT
child.id,
child.sku,
child.fathersort,
child.parentsort,
child.track_stock AS trackStock,
child.unitamount,
child.weight,
@@ -145,14 +145,14 @@ LEFT JOIN (
articleattributevalue aav
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = @attributeId
) ON aav.article_id = child.id
WHERE child.fatherarticle_id = @fatherArticleId
ORDER BY child.fathersort ASC, child.id ASC
WHERE child.parentarticle_id = @parentArticleId
ORDER BY child.parentsort ASC, child.id ASC
`;
export const SET_FATHER_SORT = `
export const SET_PARENT_SORT = `
UPDATE article
SET fathersort = @fathersort
WHERE id = @articleId AND fatherarticle_id = @fatherArticleId
SET parentsort = @parentsort
WHERE id = @articleId AND parentarticle_id = @parentArticleId
`;
/** SKU → article/category + company auth in one round-trip. */
@@ -484,19 +484,19 @@ SELECT
(
SELECT COUNT(*)
FROM article child
WHERE child.fatherarticle_id = a.id
WHERE child.parentarticle_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,
a.is_parent AS isParent,
a.parentattributename_id AS parentAttributeNameId,
fan.name AS parentAttributeName,
fan.exclusive AS parentAttributeExclusive,
a.parentarticle_id AS parentArticleId,
fa.sku AS parentSku,
(
SELECT
child.id,
child.sku,
child.fathersort,
child.parentsort,
at_child.name,
av.id AS attributeValueId,
av.value AS attributeValue
@@ -504,10 +504,10 @@ SELECT
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId
LEFT JOIN (
articleattributevalue aav
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.parentattributename_id
) ON aav.article_id = child.id
WHERE child.fatherarticle_id = a.id
ORDER BY child.fathersort ASC, child.id ASC
WHERE child.parentarticle_id = a.id
ORDER BY child.parentsort ASC, child.id ASC
FOR JSON PATH
) AS childrenJson,
(
@@ -637,8 +637,8 @@ SELECT
ORDER BY bp2.sort ASC, bp2.id ASC
) first_brand_pic
LEFT JOIN pic bp ON bp.id = first_brand_pic.pic_id
LEFT JOIN article fa ON fa.id = a.fatherarticle_id
LEFT JOIN attributename fan ON fan.id = a.fatherattributename_id
LEFT JOIN article fa ON fa.id = a.parentarticle_id
LEFT JOIN attributename fan ON fan.id = a.parentattributename_id
WHERE a.id = auth.articleId
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
) AS articleJson

View File

@@ -1,13 +1,13 @@
export const GET_CHILD_COUNT = `
SELECT COUNT(*) AS count
FROM article
WHERE fatherarticle_id = @articleId
WHERE parentarticle_id = @articleId
`;
export const GET_CHILDREN_IDS = `
SELECT id
FROM article
WHERE fatherarticle_id = @id
WHERE parentarticle_id = @id
`;
export const GET_LIST_IDS = `

View File

@@ -36,10 +36,10 @@ export const ARTICLE_DETAIL_COLUMNS = `
(
SELECT COUNT(*)
FROM article child
WHERE child.fatherarticle_id = a.id
WHERE child.parentarticle_id = a.id
) AS childCount,
a.fatherarticle_id AS fatherArticleId,
father.sku AS fatherSku
a.parentarticle_id AS parentArticleId,
parent.sku AS parentSku
`;
export const GET_DETAIL_BY_SKU = `
@@ -59,7 +59,7 @@ OUTER APPLY (
ORDER BY bp.sort ASC, bp.id ASC
) first_brand_pic
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
LEFT JOIN article father ON father.id = a.fatherarticle_id
LEFT JOIN article parent ON parent.id = a.parentarticle_id
WHERE a.sku = @sku AND (@companyId IS NULL OR a.company_id = @companyId)
`;
@@ -80,7 +80,7 @@ OUTER APPLY (
ORDER BY bp.sort ASC, bp.id ASC
) first_brand_pic
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
LEFT JOIN article father ON father.id = a.fatherarticle_id
LEFT JOIN article parent ON parent.id = a.parentarticle_id
WHERE a.id = @articleId
`;

View File

@@ -45,7 +45,7 @@ FROM categoryarticle ca
INNER JOIN article a ON a.id = ca.article_id
${ARTICLE_LIST_JOINS}
WHERE ca.category_id = @categoryId
AND a.fatherarticle_id IS NULL
AND a.parentarticle_id IS NULL
ORDER BY a.id
`;

View File

@@ -109,10 +109,10 @@ VALUES (
)
`;
export const CHECK_ARTICLE_IS_FATHER = `
export const CHECK_ARTICLE_IS_PARENT = `
SELECT
a.id,
a.is_father AS isFather,
a.is_parent AS isParent,
COALESCE(
(SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
(SELECT MIN(at2.name) FROM articletext at2 WHERE at2.article_id = a.id),

View File

@@ -253,7 +253,7 @@ SELECT TOP (@limit)
SELECT TOP 1 p2.hash
FROM articlepic ap2
INNER JOIN pic p2 ON p2.id = ap2.pic_id
WHERE a.fatherarticle_id IS NOT NULL AND ap2.article_id = a.fatherarticle_id
WHERE a.parentarticle_id IS NOT NULL AND ap2.article_id = a.parentarticle_id
ORDER BY ap2.sort ASC, ap2.id ASC
)
) AS imgHash,
@@ -263,15 +263,15 @@ SELECT TOP (@limit)
WHERE ae.article_id = a.id
ORDER BY ae.id ASC
) AS barcode,
a.is_father AS isParent,
a.fatherarticle_id AS parentArticleId,
a.is_parent AS isParent,
a.parentarticle_id AS parentArticleId,
(
SELECT TOP 1 CONCAT(fan.name, '|', av.value, '|', fan.sort, '|', a.fathersort)
SELECT TOP 1 CONCAT(fan.name, '|', av.value, '|', fan.sort, '|', a.parentsort)
FROM article fa
INNER JOIN attributename fan ON fan.id = fa.fatherattributename_id
INNER JOIN attributename fan ON fan.id = fa.parentattributename_id
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.id
WHERE a.fatherarticle_id IS NOT NULL AND fa.id = a.fatherarticle_id
WHERE a.parentarticle_id IS NOT NULL AND fa.id = a.parentarticle_id
) AS variantName,
CASE
WHEN a.articlelist_id IS NOT NULL AND EXISTS (

View File

@@ -323,10 +323,10 @@ BEGIN
brand_id INT NULL,
articlelist_id INT NULL,
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,
is_parent BIT NOT NULL CONSTRAINT DF_article_is_parent DEFAULT 0,
parentarticle_id INT NULL,
parentattributename_id INT NULL,
parentsort INT NOT NULL CONSTRAINT DF_article_parentsort 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(),
CONSTRAINT FK_article_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
@@ -334,23 +334,23 @@ BEGIN
CONSTRAINT FK_article_brand FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE SET NULL,
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 FK_article_parentarticle FOREIGN KEY (parentarticle_id) REFERENCES article(id) ON DELETE NO ACTION,
CONSTRAINT FK_article_parentattributename FOREIGN KEY (parentattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION,
CONSTRAINT UQ_article_company_sku UNIQUE (company_id, sku)
);
-- Only enforce sibling sort uniqueness among children of the same father.
-- Only enforce sibling sort uniqueness among children of the same parent.
-- (SQL Server UNIQUE treats NULLs as equal, which would block multiple root articles.)
CREATE UNIQUE INDEX UQ_article_fatherarticle_fathersort
ON article (fatherarticle_id, fathersort)
WHERE fatherarticle_id IS NOT NULL;
CREATE UNIQUE INDEX UQ_article_parentarticle_parentsort
ON article (parentarticle_id, parentsort)
WHERE parentarticle_id IS NOT NULL;
END
ELSE
BEGIN
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'fatherattributename_id')
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'parentattributename_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;
ALTER TABLE article ADD parentattributename_id INT NULL;
ALTER TABLE article ADD CONSTRAINT FK_article_parentattributename FOREIGN KEY (parentattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION;
END
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')

View File

@@ -434,9 +434,9 @@ 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')) {
if (articleCols.length > 0 && !articleCols.includes('parentattributename_id')) {
this.db.exec(
`ALTER TABLE article ADD COLUMN fatherattributename_id INTEGER REFERENCES attributename(id) ON DELETE NO ACTION`,
`ALTER TABLE article ADD COLUMN parentattributename_id INTEGER REFERENCES attributename(id) ON DELETE NO ACTION`,
);
}

View File

@@ -7,9 +7,9 @@ export const {
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, TOGGLE_FATHER, UNLINK_CHILD, UPDATE_BRAND, UPDATE_FATHER_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
LIST_CHILDREN_BY_PARENT_ID,
SET_PARENT_SORT,
SET_IS_PARENT, TOGGLE_PARENT, UNLINK_CHILD, UPDATE_BRAND, UPDATE_PARENT_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT,
} = queries;
@@ -30,7 +30,7 @@ LEFT JOIN pic brand_pic ON brand_pic.id = (
SELECT bp.pic_id FROM brandpic bp
WHERE bp.brand_id = b.id ORDER BY bp.sort, bp.id LIMIT 1
)
LEFT JOIN article father ON father.id = a.fatherarticle_id
LEFT JOIN article parent ON parent.id = a.parentarticle_id
`;
export const RESOLVE_ARTICLE_ACCESS_BY_SKU = `
@@ -160,16 +160,16 @@ SELECT auth.*,
LEFT JOIN articletext at_item ON at_item.article_id = a_item.id AND at_item.language_id = auth.languageId
WHERE ala.articlelist_id = a.articlelist_id
ORDER BY ala.sort ASC, ala.id ASC)),
'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,
'childCount', (SELECT COUNT(*) FROM article WHERE parentarticle_id = a.id),
'isParent', a.is_parent,
'parentAttributeNameId', a.parentattributename_id,
'parentAttributeName', fan.name,
'parentAttributeExclusive', fan.exclusive,
'parentArticleId', a.parentarticle_id, 'parentSku', parent.sku,
'childrenJson', json((SELECT COALESCE(json_group_array(json_object(
'id', child.id,
'sku', child.sku,
'fathersort', child.fathersort,
'parentsort', child.parentsort,
'name', at_child.name,
'attributeValueId', av.id,
'attributeValue', av.value
@@ -177,10 +177,10 @@ SELECT auth.*,
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId
LEFT JOIN (
articleattributevalue aav
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.fatherattributename_id
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.parentattributename_id
) ON aav.article_id = child.id
WHERE child.fatherarticle_id = a.id
ORDER BY child.fathersort ASC, child.id ASC)),
WHERE child.parentarticle_id = a.id
ORDER BY child.parentsort 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,
@@ -270,6 +270,6 @@ LEFT JOIN brand b ON b.id = a.brand_id
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
LEFT JOIN article parent ON parent.id = a.parentarticle_id
LEFT JOIN attributename fan ON fan.id = a.parentattributename_id
`;

View File

@@ -19,7 +19,7 @@ LEFT JOIN pic brand_pic ON brand_pic.id = (
WHERE bp.brand_id = b.id
ORDER BY bp.sort, bp.id LIMIT 1
)
LEFT JOIN article father ON father.id = a.fatherarticle_id
LEFT JOIN article parent ON parent.id = a.parentarticle_id
`;
export const GET_DETAIL_BY_SKU = `

View File

@@ -65,7 +65,7 @@ FROM categoryarticle ca
INNER JOIN article a ON a.id = ca.article_id
${ARTICLE_LIST_JOINS}
WHERE ca.category_id = @categoryId
AND a.fatherarticle_id IS NULL
AND a.parentarticle_id IS NULL
ORDER BY a.id
`;

View File

@@ -3,7 +3,7 @@ import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'manifestDoc');
export const {
CHECK_ARTICLE_IS_FATHER, CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST,
CHECK_ARTICLE_IS_PARENT, CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST,
DELETE_MANIFEST, GET_MANIFEST_HEADER, GET_MANIFEST_LINES, LIST_MANIFESTS,
ORDER_TYPE, QUOTE_TYPE, UPDATE_MANIFEST,
} = queries;

View File

@@ -78,7 +78,7 @@ SELECT
SELECT p2.hash
FROM articlepic ap2
INNER JOIN pic p2 ON p2.id = ap2.pic_id
WHERE a.fatherarticle_id IS NOT NULL AND ap2.article_id = a.fatherarticle_id
WHERE a.parentarticle_id IS NOT NULL AND ap2.article_id = a.parentarticle_id
ORDER BY ap2.sort ASC, ap2.id ASC
LIMIT 1
)
@@ -90,15 +90,15 @@ SELECT
ORDER BY ae.id ASC
LIMIT 1
) AS barcode,
a.is_father AS isParent,
a.fatherarticle_id AS parentArticleId,
a.is_parent AS isParent,
a.parentarticle_id AS parentArticleId,
(
SELECT fan.name || '|' || av.value || '|' || fan.sort || '|' || a.fathersort
SELECT fan.name || '|' || av.value || '|' || fan.sort || '|' || a.parentsort
FROM article fa
INNER JOIN attributename fan ON fan.id = fa.fatherattributename_id
INNER JOIN attributename fan ON fan.id = fa.parentattributename_id
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.id
WHERE a.fatherarticle_id IS NOT NULL AND fa.id = a.fatherarticle_id
WHERE a.parentarticle_id IS NOT NULL AND fa.id = a.parentarticle_id
LIMIT 1
) AS variantName,
CASE

View File

@@ -222,10 +222,10 @@ CREATE TABLE IF NOT EXISTS article (
brand_id INTEGER,
articlelist_id INTEGER,
optionslist_id INTEGER,
is_father INTEGER NOT NULL DEFAULT 0,
fatherarticle_id INTEGER,
fathersort INTEGER NOT NULL DEFAULT 0,
fatherattributename_id INTEGER,
is_parent INTEGER NOT NULL DEFAULT 0,
parentarticle_id INTEGER,
parentsort INTEGER NOT NULL DEFAULT 0,
parentattributename_id INTEGER,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
UNIQUE (company_id, sku),
@@ -234,12 +234,12 @@ 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 (fatherattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION
FOREIGN KEY (parentarticle_id) REFERENCES article(id) ON DELETE NO ACTION,
FOREIGN KEY (parentattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION
);
CREATE UNIQUE INDEX IF NOT EXISTS UQ_article_fatherarticle_fathersort
ON article(fatherarticle_id, fathersort)
WHERE fatherarticle_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS UQ_article_parentarticle_parentsort
ON article(parentarticle_id, parentsort)
WHERE parentarticle_id IS NOT NULL;
`;
export const INIT_ARTICLEEAN = `

View File

@@ -34,24 +34,24 @@ export default function addChild(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Adds a child article to a father article (creates and attaches with auto-generated SKU).
* Adds a child article to a parent article (creates and attaches with auto-generated SKU).
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {string} parentSku - The parent article SKU.
* @param {object} options
* @param {boolean} [options.attach=false] - If true, attaches an existing article instead of creating.
* @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.attributeValueId] - Existing attribute value ID for parent's attribute.
* @param {string} [options.value] - String value to find or create under parent's attribute.
* @param {string} [options.name] - Custom name for child article (defaults to parent 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) => {
return async (googleAuthId, parentSku, options = {}, tx) => {
if (options.attach) {
return attach(googleAuthId, fatherSku, options, tx);
return attach(googleAuthId, parentSku, options, tx);
}
const run = async (runner) => {
@@ -59,31 +59,31 @@ export default function addChild(provider) {
throw new Error('sku is not allowed when creating a child article; omit sku to auto-generate or use PUT to attach an existing article');
}
const { articleId: fatherId, companyId, languageId, companyuserId } = await resolve(
const { articleId: parentId, companyId, languageId, companyuserId } = await resolve(
googleAuthId,
fatherSku,
parentSku,
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 },
// Fetch parent article fields
const parentRes = await runner.query(
'SELECT id, sku, company_id, unit_id, unitamount, weight, track_stock, brand_id, is_parent, parentarticle_id, parentattributename_id FROM article WHERE id = @parentId',
{ parentId },
);
const father = fatherRes.rows[0];
if (!father) {
throw new Error('Father article not found');
const parent = parentRes.rows[0];
if (!parent) {
throw new Error('Parent article not found');
}
if (!father.is_father || !father.fatherattributename_id) {
throw new Error('Article is not a father article');
if (!parent.is_parent || !parent.parentattributename_id) {
throw new Error('Article is not a parent 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 },
// Fetch parent's text / name
const parentTextRes = await runner.query(
'SELECT name FROM articletext WHERE article_id = @parentId AND language_id = @languageId',
{ parentId, languageId },
);
const fatherName = fatherTextRes.rows[0]?.name || father.sku;
const parentName = parentTextRes.rows[0]?.name || parent.sku;
// Determine attribute value
let finalValueId = null;
@@ -98,15 +98,15 @@ export default function addChild(provider) {
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');
if (!row || row.attributeId !== parent.parentattributename_id) {
throw new Error('Attribute value not found or does not match parent 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,
attributeId: parent.parentattributename_id,
value: valStr,
});
if (existingValRes.rows[0]) {
@@ -114,11 +114,11 @@ export default function addChild(provider) {
finalValue = existingValRes.rows[0].value;
} else {
const sortRes = await runner.query(attrValQueries.GET_NEXT_SORT, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
});
const sort = sortRes.rows[0]?.sort ?? 0;
const createdValRes = await runner.query(attrValQueries.CREATE, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
value: valStr,
sort,
});
@@ -129,26 +129,26 @@ export default function addChild(provider) {
throw new Error('Attribute value is required for child article');
}
// Check if father already has a variant with this attribute value
// Check if parent already has a variant with this attribute value
const existingVariantRes = await runner.query(
`SELECT a.id
FROM article a
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
WHERE a.fatherarticle_id = @fatherId
WHERE a.parentarticle_id = @parentId
AND aav.attributevalue_id = @finalValueId`,
{ fatherId, finalValueId },
{ parentId, finalValueId },
);
if (existingVariantRes.rows.length > 0) {
throw new Error('Variant with this attribute value already exists');
}
// Compute child SKU using next free index: fatherSku-1, fatherSku-2, ...
// Compute child SKU using next free index: parentSku-1, parentSku-2, ...
const existingSkusRes = await runner.query(
'SELECT sku FROM article WHERE company_id = @companyId AND sku LIKE @prefix',
{ companyId, prefix: `${father.sku}-%` },
{ companyId, prefix: `${parent.sku}-%` },
);
const usedIndices = new Set();
const prefix = `${father.sku}-`;
const prefix = `${parent.sku}-`;
for (const row of existingSkusRes.rows) {
if (row.sku && row.sku.startsWith(prefix)) {
const numPart = row.sku.slice(prefix.length);
@@ -161,42 +161,42 @@ export default function addChild(provider) {
while (usedIndices.has(nextIndex)) {
nextIndex++;
}
const childSku = `${father.sku}-${nextIndex}`;
const childSku = `${parent.sku}-${nextIndex}`;
// Next free fathersort (smallest non-negative integer not used by any child of this father)
// Next free parentsort (smallest non-negative integer not used by any child of this parent)
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
{ parentId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
let fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
let parentsort = 0;
while (usedSorts.has(parentsort)) {
parentsort++;
}
// Auto-create child article
const rawTrack = options.track_stock ?? options.trackStock;
const finalTrackStock = rawTrack == null ? father.track_stock : (rawTrack ? 1 : 0);
const finalTrackStock = rawTrack == null ? parent.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,
unitId: parent.unit_id,
unitamount: options.unitamount ?? parent.unitamount,
weight: options.weight ?? parent.weight,
trackStock: finalTrackStock,
brandId: father.brand_id,
fatherArticleId: fatherId,
fathersort,
brandId: parent.brand_id,
parentArticleId: parentId,
parentsort,
});
const childId = childRes.rows[0]?.id ?? childRes.insertId;
// Copy existing attribute values from father (ancestor attributes that father has)
const fatherAttrsRes = await runner.query(
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @fatherId',
{ fatherId },
// Copy existing attribute values from parent (ancestor attributes that parent has)
const parentAttrsRes = await runner.query(
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @parentId',
{ parentId },
);
for (const faRow of fatherAttrsRes.rows) {
for (const faRow of parentAttrsRes.rows) {
if (faRow.attributevalue_id !== finalValueId) {
await runner.query(articleAttrValQueries.INSERT_ROW, {
articleId: childId,
@@ -213,10 +213,10 @@ export default function addChild(provider) {
sort: 0,
});
// Copy father's categories
// Copy parent's categories
const catRes = await runner.query(
'SELECT category_id FROM categoryarticle WHERE article_id = @fatherId',
{ fatherId },
'SELECT category_id FROM categoryarticle WHERE article_id = @parentId',
{ parentId },
);
for (const catRow of catRes.rows) {
await runner.query(addCategoryArticleSql, {
@@ -226,7 +226,7 @@ export default function addChild(provider) {
}
// Create article text
const childName = options.name?.trim() || `${fatherName} - ${finalValue}`;
const childName = options.name?.trim() || `${parentName} - ${finalValue}`;
const childSlug = await resolveSlug(runner, {
companyId,
languageId,
@@ -243,12 +243,12 @@ export default function addChild(provider) {
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" angelegt`,
text: `Artikel "${childName}" (${childSku}) als Variante von "${parent.sku}" angelegt`,
});
// History on father
// History on parent
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
});

View File

@@ -26,23 +26,23 @@ export default function attachChild(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Attaches an existing article as a child to a father article.
* Attaches an existing article as a child to a parent article.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {string} parentSku - The parent article SKU.
* @param {object} options
* @param {string} options.sku - The child article SKU to attach.
* @param {string} [options.childSku] - Alias for options.sku.
* @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 {boolean} [options.reparent=false] - Whether to allow re-parenting if child belongs to another father.
* @param {number} [options.attributeValueId] - Existing attribute value ID for parent's attribute.
* @param {string} [options.value] - String value to find or create under parent's attribute.
* @param {boolean} [options.reparent=false] - Whether to allow re-parenting if child belongs to another parent.
* @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<{article: object, children: Array<object>}>}
*/
return async (googleAuthId, fatherSku, options = {}, tx) => {
return async (googleAuthId, parentSku, options = {}, tx) => {
const run = async (runner) => {
const childSku = (typeof options.sku === 'string' && options.sku.trim())
|| (typeof options.childSku === 'string' && options.childSku.trim())
@@ -52,35 +52,35 @@ export default function attachChild(provider) {
throw new Error('sku is required to attach an existing article');
}
const { articleId: fatherId, companyId, languageId, companyuserId } = await resolve(
const { articleId: parentId, companyId, languageId, companyuserId } = await resolve(
googleAuthId,
fatherSku,
parentSku,
runner,
);
// Fetch father article fields
const fatherRes = await runner.query(
'SELECT id, sku, company_id, is_father, fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
// Fetch parent article fields
const parentRes = await runner.query(
'SELECT id, sku, company_id, is_parent, parentattributename_id FROM article WHERE id = @parentId',
{ parentId },
);
const father = fatherRes.rows[0];
if (!father) {
throw new Error('Father article not found');
const parent = parentRes.rows[0];
if (!parent) {
throw new Error('Parent article not found');
}
if (!father.is_father || !father.fatherattributename_id) {
throw new Error('Article is not a father article');
if (!parent.is_parent || !parent.parentattributename_id) {
throw new Error('Article is not a parent article');
}
// Check child article exists in company
const childRes = await runner.query(
'SELECT id, sku, company_id, fatherarticle_id, is_father FROM article WHERE sku = @childSku AND company_id = @companyId',
'SELECT id, sku, company_id, parentarticle_id, is_parent FROM article WHERE sku = @childSku AND company_id = @companyId',
{ childSku, companyId },
);
const child = childRes.rows[0];
if (!child) {
throw new Error('Article not found');
}
if (child.id === fatherId) {
if (child.id === parentId) {
throw new Error('An article cannot be a child of itself');
}
@@ -97,15 +97,15 @@ export default function attachChild(provider) {
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');
if (!row || row.attributeId !== parent.parentattributename_id) {
throw new Error('Attribute value not found or does not match parent 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,
attributeId: parent.parentattributename_id,
value: valStr,
});
if (existingValRes.rows[0]) {
@@ -113,11 +113,11 @@ export default function attachChild(provider) {
finalValue = existingValRes.rows[0].value;
} else {
const sortRes = await runner.query(attrValQueries.GET_NEXT_SORT, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
});
const sort = sortRes.rows[0]?.sort ?? 0;
const createdValRes = await runner.query(attrValQueries.CREATE, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
value: valStr,
sort,
});
@@ -128,78 +128,78 @@ export default function attachChild(provider) {
throw new Error('Attribute value is required for child article');
}
const isAlreadyChildOfThisFather = child.fatherarticle_id === fatherId;
const isAlreadyChildOfThisParent = child.parentarticle_id === parentId;
if (child.fatherarticle_id != null && !isAlreadyChildOfThisFather && !options.reparent) {
throw new Error('Article already belongs to another father');
if (child.parentarticle_id != null && !isAlreadyChildOfThisParent && !options.reparent) {
throw new Error('Article already belongs to another parent');
}
// Check if father already has a different child with this attribute value
// Check if parent already has a different child with this attribute value
const existingVariantRes = await runner.query(
`SELECT a.id
FROM article a
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
WHERE a.fatherarticle_id = @fatherId
WHERE a.parentarticle_id = @parentId
AND a.id != @childId
AND aav.attributevalue_id = @finalValueId`,
{ fatherId, childId: child.id, finalValueId },
{ parentId, childId: child.id, finalValueId },
);
if (existingVariantRes.rows.length > 0) {
throw new Error('Variant with this attribute value already exists');
}
// If re-parenting from another father, unparent and recompact previous father
if (child.fatherarticle_id != null && !isAlreadyChildOfThisFather && options.reparent) {
const prevFatherId = child.fatherarticle_id;
// If re-parenting from another parent, unparent and recompact previous parent
if (child.parentarticle_id != null && !isAlreadyChildOfThisParent && options.reparent) {
const prevParentId = child.parentarticle_id;
await runner.query(insertHistorySql, {
articleId: prevFatherId,
articleId: prevParentId,
companyuserId,
text: `Variante (${childSku}) getrennt (umgehängt zu "${father.sku}")`,
text: `Variante (${childSku}) getrennt (umgehängt zu "${parent.sku}")`,
});
// Recompact previous father's children
// Recompact previous parent's children
const prevRemainingRes = await runner.query(
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @prevFatherId AND id != @childId ORDER BY fathersort ASC, id ASC',
{ prevFatherId, childId: child.id },
'SELECT id, parentsort FROM article WHERE parentarticle_id = @prevParentId AND id != @childId ORDER BY parentsort ASC, id ASC',
{ prevParentId, childId: child.id },
);
for (let i = 0; i < prevRemainingRes.rows.length; i++) {
const row = prevRemainingRes.rows[i];
if (Number(row.fathersort) !== i) {
await runner.query(queries.SET_FATHER_SORT, {
if (Number(row.parentsort) !== i) {
await runner.query(queries.SET_PARENT_SORT, {
articleId: row.id,
fatherArticleId: prevFatherId,
fathersort: i,
parentArticleId: prevParentId,
parentsort: i,
});
}
}
}
// Assign fathersort if not already child of this father
let fathersort;
if (isAlreadyChildOfThisFather) {
// Assign parentsort if not already child of this parent
let parentsort;
if (isAlreadyChildOfThisParent) {
const currentSortRes = await runner.query(
'SELECT fathersort FROM article WHERE id = @childId',
'SELECT parentsort FROM article WHERE id = @childId',
{ childId: child.id },
);
fathersort = Number(currentSortRes.rows[0]?.fathersort ?? 0);
parentsort = Number(currentSortRes.rows[0]?.parentsort ?? 0);
} else {
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
{ parentId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
parentsort = 0;
while (usedSorts.has(parentsort)) {
parentsort++;
}
}
// Link child to father
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
// Link child to parent
await runner.query(queries.UPDATE_PARENT_BY_SKU, {
sku: childSku,
companyId,
fatherArticleId: fatherId,
fathersort,
parentArticleId: parentId,
parentsort,
});
// Apply optional field overrides
@@ -223,15 +223,15 @@ export default function attachChild(provider) {
});
}
// Remove existing attribute value for this father's attribute on child (if any)
if (father.fatherattributename_id) {
// Remove existing attribute value for this parent's attribute on child (if any)
if (parent.parentattributename_id) {
await runner.query(
`DELETE FROM articleattributevalue
WHERE article_id = @childId
AND attributevalue_id IN (
SELECT id FROM attributevalue WHERE attribute_id = @attributeId
)`,
{ childId: child.id, attributeId: father.fatherattributename_id },
{ childId: child.id, attributeId: parent.parentattributename_id },
);
}
await runner.query(articleAttrValQueries.INSERT_ROW, {
@@ -240,12 +240,12 @@ export default function attachChild(provider) {
sort: 0,
});
// Copy ancestor attributes from father (if missing on child)
const fatherAttrsRes = await runner.query(
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @fatherId',
{ fatherId },
// Copy ancestor attributes from parent (if missing on child)
const parentAttrsRes = await runner.query(
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @parentId',
{ parentId },
);
for (const faRow of fatherAttrsRes.rows) {
for (const faRow of parentAttrsRes.rows) {
if (faRow.attributevalue_id !== finalValueId) {
const hasAttrRes = await runner.query(
'SELECT 1 FROM articleattributevalue WHERE article_id = @childId AND attributevalue_id = @attrValId',
@@ -261,10 +261,10 @@ export default function attachChild(provider) {
}
}
// Copy father's categories (if missing on child)
// Copy parent's categories (if missing on child)
const catRes = await runner.query(
'SELECT category_id FROM categoryarticle WHERE article_id = @fatherId',
{ fatherId },
'SELECT category_id FROM categoryarticle WHERE article_id = @parentId',
{ parentId },
);
for (const catRow of catRes.rows) {
const hasCatRes = await runner.query(
@@ -287,22 +287,22 @@ export default function attachChild(provider) {
const childName = childTextRes.rows[0]?.name || childSku;
// History only if not already child
if (!isAlreadyChildOfThisFather) {
if (!isAlreadyChildOfThisParent) {
await runner.query(insertHistorySql, {
articleId: child.id,
companyuserId,
text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" zugewiesen`,
text: `Artikel "${childName}" (${childSku}) als Variante von "${parent.sku}" zugewiesen`,
});
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
});
}
const updated = await load(googleAuthId, childSku, runner);
const children = await list(googleAuthId, fatherSku, runner);
const children = await list(googleAuthId, parentSku, runner);
return {
article: updated.article,

View File

@@ -69,11 +69,11 @@ export default function del(provider) {
await deleteArticleAndLists(id);
};
const fatherRes = await runner.query(
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
const parentRes = await runner.query(
'SELECT parentarticle_id FROM article WHERE id = @articleId',
{ articleId },
);
const fatherArticleId = fatherRes.rows[0]?.fatherarticle_id;
const parentArticleId = parentRes.rows[0]?.parentarticle_id;
const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
if (childCount.rows[0].count > 0) {
@@ -85,18 +85,18 @@ export default function del(provider) {
await deleteArticleAndLists(articleId);
}
if (fatherArticleId != null) {
if (parentArticleId != null) {
const remainingChildrenRes = await runner.query(
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherArticleId ORDER BY fathersort ASC, id ASC',
{ fatherArticleId },
'SELECT id, parentsort FROM article WHERE parentarticle_id = @parentArticleId ORDER BY parentsort ASC, id ASC',
{ parentArticleId },
);
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
const childRow = remainingChildrenRes.rows[i];
if (Number(childRow.fathersort) !== i) {
await runner.query(articleQueries.SET_FATHER_SORT, {
if (Number(childRow.parentsort) !== i) {
await runner.query(articleQueries.SET_PARENT_SORT, {
articleId: childRow.id,
fatherArticleId,
fathersort: i,
parentArticleId,
parentsort: i,
});
}
}

View File

@@ -18,7 +18,7 @@ export default function getBySku(provider) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, 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}}>} The full article detail.
* @returns {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, parentArticleId: number|null, parentSku: string|null}}>} The full article detail.
* @throws {Error} If the article is not found or the user is not authorized.
*
* @example
@@ -118,16 +118,16 @@ export default function getBySku(provider) {
sort: Number(item.sort ?? 0),
})),
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,
isParent: Boolean(a.isParent),
parentAttributeNameId: a.parentAttributeNameId ?? null,
parentAttributeName: a.parentAttributeName ?? null,
parentAttributeExclusive: a.parentAttributeExclusive != null ? Boolean(a.parentAttributeExclusive) : null,
parentArticleId: a.parentArticleId,
parentSku: a.parentSku,
children: parseJsonArray(a.childrenJson).map((c) => ({
id: c.id,
sku: c.sku,
fathersort: c.fathersort,
parentsort: c.parentsort,
name: c.name ?? null,
attributeValueId: c.attributeValueId ?? null,
attributeValue: c.attributeValue ?? null,

View File

@@ -87,7 +87,7 @@ export default function importRows(provider) {
* 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
* The import runs in **two passes** so a `parentSku` 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.
@@ -97,7 +97,7 @@ export default function importRows(provider) {
* @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 {string} [input.rows[].vk] - `'V'` (parent/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.
@@ -105,7 +105,7 @@ export default function importRows(provider) {
* @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 {boolean|number} [input.rows[].track_stock=true] - Whether inventory stock is tracked.
* @param {string} [input.rows[].fatherSku] - The father article SKU for a V/K compound link.
* @param {string} [input.rows[].parentSku] - The parent article SKU for a V/K compound link.
* @param {number|string} [input.rows[].deposit] - The deposit (Pfand) value; currently ignored.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>} The import outcome.
@@ -114,7 +114,7 @@ export default function importRows(provider) {
* 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-1', name: '420Strainz 1g', vk: 'K', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '11,00', tax: 19, eans: ['4262557690455'], parentSku: '42001190' },
* { sku: '42001190', name: '420Strainz', vk: 'V', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '0,00', tax: 19 },
* ],
* });
@@ -275,63 +275,63 @@ export default function importRows(provider) {
}
}
// Pass 2: wire V/K father links (fathers may be created in pass 1).
// Pass 2: wire V/K parent links (parents 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;
const parentSku =
row?.parentSku != null ? String(row.parentSku).trim() : '';
if (!sku || !parentSku) continue;
try {
await runInTx(async (runner) => {
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
const childId = child.rows[0]?.articleId;
if (childId) {
const father = await runner.query(
'SELECT id, is_father FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
{ sku: fatherSku, companyId },
const parent = await runner.query(
'SELECT id, is_parent FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
{ sku: parentSku, companyId },
);
const fatherRow = father.rows[0];
const fatherId = fatherRow?.id;
if (fatherId && fatherId !== childId) {
const wasFather = Boolean(fatherRow.is_father);
const parentRow = parent.rows[0];
const parentId = parentRow?.id;
if (parentId && parentId !== childId) {
const wasParent = Boolean(parentRow.is_parent);
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
{ parentId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
let fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
let parentsort = 0;
while (usedSorts.has(parentsort)) {
parentsort++;
}
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
await runner.query(articleQueries.UPDATE_PARENT_BY_SKU, {
sku,
companyId,
fatherArticleId: fatherId,
fathersort,
parentArticleId: parentId,
parentsort,
});
await runner.query(articleQueries.SET_IS_FATHER, {
articleId: fatherId,
await runner.query(articleQueries.SET_IS_PARENT, {
articleId: parentId,
});
if (!wasFather) {
if (!wasParent) {
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: 'Artikel zu Vaterartikel konvertiert',
});
}
const childName = String(row?.name ?? sku).trim();
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${sku}) hinzugefügt`,
});
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childName}" (${sku}) als Variante von "${fatherSku}" zugewiesen`,
text: `Artikel "${childName}" (${sku}) als Variante von "${parentSku}" zugewiesen`,
});
}
}

View File

@@ -9,40 +9,40 @@ export default function listChildren(provider) {
const queries = isSqlite ? sqliteArticle : mssqlArticle;
/**
* Lists all children of a father article.
* Lists all children of a parent article.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {string} parentSku - The parent 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}>>}
* @returns {Promise<Array<{id: number, sku: string, parentsort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>}
*/
return async (googleAuthId, fatherSku, tx) => {
return async (googleAuthId, parentSku, tx) => {
const run = async (runner) => {
const { articleId: fatherId, languageId } = await resolve(
const { articleId: parentId, languageId } = await resolve(
googleAuthId,
fatherSku,
parentSku,
runner,
);
const fatherRes = await runner.query(
'SELECT is_father, fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
const parentRes = await runner.query(
'SELECT is_parent, parentattributename_id FROM article WHERE id = @parentId',
{ parentId },
);
const father = fatherRes.rows[0];
if (!father || !father.is_father) {
const parent = parentRes.rows[0];
if (!parent || !parent.is_parent) {
return [];
}
const result = await runner.query(queries.LIST_CHILDREN_BY_FATHER_ID, {
fatherArticleId: fatherId,
const result = await runner.query(queries.LIST_CHILDREN_BY_PARENT_ID, {
parentArticleId: parentId,
languageId,
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
});
return result.rows.map((row) => ({
id: row.id,
sku: row.sku,
fathersort: Number(row.fathersort ?? 0),
parentsort: Number(row.parentsort ?? 0),
trackStock: Boolean(row.trackStock ?? 1),
unitamount: Number(row.unitamount ?? 1),
weight: Number(row.weight ?? 0),

View File

@@ -35,8 +35,8 @@ function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
optionsCount: row.optionsCount,
articleListCount: row.articleListCount,
childCount: row.childCount,
fatherArticleId: row.fatherArticleId,
fatherSku: row.fatherSku,
parentArticleId: row.parentArticleId,
parentSku: row.parentSku,
};
}

View File

@@ -15,34 +15,34 @@ export default function removeChild(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Removes a child article from a father article, either by unlinking or deleting it.
* Removes a child article from a parent 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} parentSku - The parent 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 {boolean} [options.cascade=true] - Whether to cascade delete child subtrees when deleteArticle is true.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>}
* @returns {Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>}
*/
return async (googleAuthId, fatherSku, childSku, options = {}, tx) => {
return async (googleAuthId, parentSku, childSku, options = {}, tx) => {
const run = async (runner) => {
const { articleId: fatherId, languageId, companyuserId } = await resolve(
const { articleId: parentId, languageId, companyuserId } = await resolve(
googleAuthId,
fatherSku,
parentSku,
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',
'SELECT parentarticle_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 (!child || child.parentarticle_id !== parentId) {
throw new Error('Child article does not belong to this parent');
}
// Fetch child name before any deletion (needed for history regardless of path)
@@ -54,18 +54,18 @@ export default function removeChild(provider) {
);
const childName = childNameRes.rows[0]?.name || childSku;
const recompactFatherSort = async () => {
const recompactParentSort = async () => {
const remainingChildrenRes = await runner.query(
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC, id ASC',
{ fatherId },
'SELECT id, parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC, id ASC',
{ parentId },
);
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
const childRow = remainingChildrenRes.rows[i];
if (Number(childRow.fathersort) !== i) {
await runner.query(queries.SET_FATHER_SORT, {
if (Number(childRow.parentsort) !== i) {
await runner.query(queries.SET_PARENT_SORT, {
articleId: childRow.id,
fatherArticleId: fatherId,
fathersort: i,
parentArticleId: parentId,
parentsort: i,
});
}
}
@@ -75,67 +75,67 @@ export default function removeChild(provider) {
const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true;
await del(googleAuthId, childSku, { cascade }, runner);
await recompactFatherSort();
await recompactParentSort();
// History on father (child article is gone so we can only log on father)
// History on parent (child article is gone so we can only log on parent)
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) gelöscht`,
});
return {
success: true,
fatherSku,
parentSku,
childSku,
deleted: true,
};
}
// Fetch father's attribute name id
const fatherRes = await runner.query(
'SELECT fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
// Fetch parent's attribute name id
const parentRes = await runner.query(
'SELECT parentattributename_id FROM article WHERE id = @parentId',
{ parentId },
);
const fatherAttributeNameId = fatherRes.rows[0]?.fatherattributename_id;
const parentAttributeNameId = parentRes.rows[0]?.parentattributename_id;
// Unlink child from father
// Unlink child from parent
await runner.query(queries.UNLINK_CHILD, {
articleId: childId,
fatherArticleId: fatherId,
parentArticleId: parentId,
});
// Remove the child's attribute value link for the father's attribute
if (fatherAttributeNameId) {
// Remove the child's attribute value link for the parent's attribute
if (parentAttributeNameId) {
await runner.query(
`DELETE FROM articleattributevalue
WHERE article_id = @childId
AND attributevalue_id IN (
SELECT id FROM attributevalue WHERE attribute_id = @fatherAttributeNameId
SELECT id FROM attributevalue WHERE attribute_id = @parentAttributeNameId
)`,
{ childId, fatherAttributeNameId },
{ childId, parentAttributeNameId },
);
}
await recompactFatherSort();
await recompactParentSort();
// Add history on child
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childSku}" von Vaterartikel "${fatherSku}" getrennt`,
text: `Artikel "${childSku}" von Vaterartikel "${parentSku}" getrennt`,
});
// Add history on father
// Add history on parent
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) getrennt`,
});
return {
success: true,
fatherSku,
parentSku,
childSku,
deleted: false,
};

View File

@@ -8,7 +8,7 @@ import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function setFatherBySku(provider) {
export default function setParentBySku(provider) {
const resolve = resolveArticleBySku(provider);
const load = queryBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
@@ -18,20 +18,20 @@ export default function setFatherBySku(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Links an article to a father article (V/K compound) by their SKUs.
* Links an article to a parent 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.
* @param {string|null} parentSku - The parent article SKU, or `null` to unlink.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<object>} The updated child article.
* @throws {Error} If the article or father is not found, or the link is invalid.
* @throws {Error} If the article or parent is not found, or the link is invalid.
*
* @example
* const article = await store.article.setFatherBySku(42, '42001190-1', '42001190');
* // => { id, sku, name, fatherSku: '42001190', ... }
* const article = await store.article.setParentBySku(42, '42001190-1', '42001190');
* // => { id, sku, name, parentSku: '42001190', ... }
*/
return async (googleAuthId, sku, fatherSku, tx) => {
return async (googleAuthId, sku, parentSku, tx) => {
const runSet = async (runner) => {
const { articleId, languageId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
@@ -41,34 +41,34 @@ export default function setFatherBySku(provider) {
);
const childName = childTextRes.rows[0]?.name || sku;
if (fatherSku == null || fatherSku === '') {
const prevFatherRes = await runner.query(
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
if (parentSku == null || parentSku === '') {
const prevParentRes = await runner.query(
'SELECT parentarticle_id FROM article WHERE id = @articleId',
{ articleId },
);
const prevFatherId = prevFatherRes.rows[0]?.fatherarticle_id;
const prevParentId = prevParentRes.rows[0]?.parentarticle_id;
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
await runner.query(queries.UPDATE_PARENT_BY_SKU, {
sku,
companyId,
fatherArticleId: null,
parentArticleId: null,
});
if (prevFatherId) {
const prevFatherTextRes = await runner.query(
'SELECT sku FROM article WHERE id = @prevFatherId',
{ prevFatherId },
if (prevParentId) {
const prevParentTextRes = await runner.query(
'SELECT sku FROM article WHERE id = @prevParentId',
{ prevParentId },
);
const prevFatherSku = prevFatherTextRes.rows[0]?.sku || String(prevFatherId);
const prevParentSku = prevParentTextRes.rows[0]?.sku || String(prevParentId);
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Artikel "${sku}" von Vaterartikel "${prevFatherSku}" getrennt`,
text: `Artikel "${sku}" von Vaterartikel "${prevParentSku}" getrennt`,
});
await runner.query(insertHistorySql, {
articleId: prevFatherId,
articleId: prevParentId,
companyuserId,
text: `Variante "${childName}" (${sku}) getrennt`,
});
@@ -81,52 +81,52 @@ export default function setFatherBySku(provider) {
return unlinked;
}
const father = await runner.query(
'SELECT id, is_father, sku FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
{ sku: fatherSku, companyId },
const parent = await runner.query(
'SELECT id, is_parent, sku FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
{ sku: parentSku, companyId },
);
const fatherRow = father.rows[0];
const fatherId = fatherRow?.id;
if (!fatherId) {
throw new Error('Father not found');
const parentRow = parent.rows[0];
const parentId = parentRow?.id;
if (!parentId) {
throw new Error('Parent not found');
}
if (fatherId === articleId) {
throw new Error('Invalid father');
if (parentId === articleId) {
throw new Error('Invalid parent');
}
const wasFather = Boolean(fatherRow.is_father);
const wasParent = Boolean(parentRow.is_parent);
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
{ parentId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
let fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
let parentsort = 0;
while (usedSorts.has(parentsort)) {
parentsort++;
}
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
await runner.query(queries.UPDATE_PARENT_BY_SKU, {
sku,
companyId,
fatherArticleId: fatherId,
fathersort,
parentArticleId: parentId,
parentsort,
});
await runner.query(queries.SET_IS_FATHER, {
articleId: fatherId,
await runner.query(queries.SET_IS_PARENT, {
articleId: parentId,
});
if (!wasFather) {
if (!wasParent) {
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: 'Artikel zu Vaterartikel konvertiert',
});
}
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${sku}) hinzugefügt`,
});
@@ -134,7 +134,7 @@ export default function setFatherBySku(provider) {
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Artikel "${sku}" als Variante von "${fatherSku}" zugewiesen`,
text: `Artikel "${sku}" als Variante von "${parentSku}" zugewiesen`,
});
const article = await load(articleId, languageId, runner);

View File

@@ -86,9 +86,9 @@ export default function setPricesBySku(provider) {
throw new Error('POS customer group required');
}
const artCheck = await runner.query('SELECT is_father FROM article WHERE id = @articleId', { articleId });
if (artCheck.rows[0]?.is_father) {
throw new Error('A father article cannot have prices');
const artCheck = await runner.query('SELECT is_parent FROM article WHERE id = @articleId', { articleId });
if (artCheck.rows[0]?.is_parent) {
throw new Error('A parent article cannot have prices');
}
// Read old prices for history

View File

@@ -10,7 +10,7 @@ import resolveArticleBySku from './resolveArticleBySku.js';
import getBySku from './getBySku.js';
import makeRunner from '../runner.js';
export default function toggleFather(provider) {
export default function toggleParent(provider) {
const resolve = resolveArticleBySku(provider);
const load = getBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
@@ -21,39 +21,39 @@ export default function toggleFather(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Toggles an article into or out of father mode.
* Toggles an article into or out of parent 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 {boolean} options.isParent - Whether the article should be a parent article.
* @param {number} [options.attributeNameId] - The attribute name id required when enabling parent mode.
* @param {number} [options.parentAttributeNameId] - Alias for attributeNameId.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<object>} The updated article detail.
*/
return async (googleAuthId, sku, { isFather, attributeNameId, fatherAttributeNameId } = {}, tx) => {
return async (googleAuthId, sku, { isParent, attributeNameId, parentAttributeNameId } = {}, tx) => {
const run = async (runner) => {
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
const targetIsFather = Boolean(isFather);
const targetAttrId = attributeNameId ?? fatherAttributeNameId;
const targetIsParent = Boolean(isParent);
const targetAttrId = attributeNameId ?? parentAttributeNameId;
if (targetIsFather) {
if (targetIsParent) {
if (!targetAttrId || Number(targetAttrId) <= 0) {
throw new Error('attributeNameId is required to become a father article');
throw new Error('attributeNameId is required to become a parent article');
}
const currentRes = await runner.query(
'SELECT fatherarticle_id, articlelist_id, optionslist_id FROM article WHERE id = @articleId',
'SELECT parentarticle_id, articlelist_id, optionslist_id FROM article WHERE id = @articleId',
{ articleId },
);
const currentArt = currentRes.rows[0];
if (currentArt?.articlelist_id != null) {
throw new Error('A Stückliste article cannot become a father');
throw new Error('A Stückliste article cannot become a parent');
}
if (currentArt?.optionslist_id != null) {
throw new Error('An article with options cannot become a father');
throw new Error('An article with options cannot become a parent');
}
const stockRes = await runner.query(
@@ -70,7 +70,7 @@ export default function toggleFather(provider) {
);
const totalStock = Number(stockRes.rows[0]?.totalStock ?? 0);
if (totalStock !== 0) {
throw new Error('An article with existing stock cannot become a father article');
throw new Error('An article with existing stock cannot become a parent article');
}
const supRes = await runner.query(
@@ -78,7 +78,7 @@ export default function toggleFather(provider) {
{ articleId },
);
if ((supRes.rows[0]?.count ?? 0) > 0) {
throw new Error('An article with assigned suppliers cannot become a father article');
throw new Error('An article with assigned suppliers cannot become a parent article');
}
// Fetch existing prices before deletion to record in articlehistory
@@ -96,22 +96,22 @@ export default function toggleFather(provider) {
await runner.query('DELETE FROM price WHERE article_id = @articleId', { articleId });
await runner.query('UPDATE article SET track_stock = 0 WHERE id = @articleId', { articleId });
// If article is a child, verify attribute is not used by father or any father up the chain
let currentFatherId = currentArt?.fatherarticle_id;
// If article is a child, verify attribute is not used by parent or any parent up the chain
let currentParentId = currentArt?.parentarticle_id;
const visited = new Set();
while (currentFatherId != null && !visited.has(currentFatherId)) {
visited.add(currentFatherId);
while (currentParentId != null && !visited.has(currentParentId)) {
visited.add(currentParentId);
const fRes = await runner.query(
'SELECT a.fatherarticle_id, a.fatherattributename_id, an.name AS fatherAttributeName FROM article a LEFT JOIN attributename an ON an.id = a.fatherattributename_id WHERE a.id = @currentFatherId',
{ currentFatherId },
'SELECT a.parentarticle_id, a.parentattributename_id, an.name AS parentAttributeName FROM article a LEFT JOIN attributename an ON an.id = a.parentattributename_id WHERE a.id = @currentParentId',
{ currentParentId },
);
const fRow = fRes.rows[0];
if (!fRow) break;
if (fRow.fatherattributename_id != null && Number(fRow.fatherattributename_id) === Number(targetAttrId)) {
const attrName = fRow.fatherAttributeName || 'ancestor';
throw new Error(`Attribute "${attrName}" is already used by an ancestor father article`);
if (fRow.parentattributename_id != null && Number(fRow.parentattributename_id) === Number(targetAttrId)) {
const attrName = fRow.parentAttributeName || 'ancestor';
throw new Error(`Attribute "${attrName}" is already used by an ancestor parent article`);
}
currentFatherId = fRow.fatherarticle_id;
currentParentId = fRow.parentarticle_id;
}
// Verify attribute exists and belongs to company
@@ -132,16 +132,16 @@ export default function toggleFather(provider) {
{ articleId, attributeId: Number(targetAttrId) },
);
if (existingValRes.rows.length > 0) {
throw new Error('A father article cannot have attribute values for its variant attribute');
throw new Error('A parent article cannot have attribute values for its variant attribute');
}
await runner.query(queries.TOGGLE_FATHER, {
await runner.query(queries.TOGGLE_PARENT, {
articleId,
isFather: 1,
fatherAttributeNameId: Number(targetAttrId),
isParent: 1,
parentAttributeNameId: Number(targetAttrId),
});
// Add history entry recording father conversion and deleted prices
// Add history entry recording parent conversion and deleted prices
const historyLines = [
`Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`,
];
@@ -170,17 +170,17 @@ export default function toggleFather(provider) {
text: historyLines.join('\n'),
});
} else {
// Disabling father: verify zero children
// Disabling parent: 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');
throw new Error('Cannot disable parent mode on an article that still has children');
}
await runner.query(queries.TOGGLE_FATHER, {
await runner.query(queries.TOGGLE_PARENT, {
articleId,
isFather: 0,
fatherAttributeNameId: null,
isParent: 0,
parentAttributeNameId: null,
});
await runner.query(insertHistorySql, {

View File

@@ -123,39 +123,39 @@ export default function setByArticleSku(provider) {
});
}
// Check father / child constraints
// Check parent / child constraints
const artRes = await runner.query(
'SELECT id, is_father, fatherarticle_id, fatherattributename_id FROM article WHERE id = @articleId',
'SELECT id, is_parent, parentarticle_id, parentattributename_id FROM article WHERE id = @articleId',
{ articleId },
);
const article = artRes.rows[0];
if (article?.is_father && article.fatherattributename_id) {
const hasFatherAttr = validated.some((v) => v.attributeId === article.fatherattributename_id);
if (hasFatherAttr) {
throw new Error('A father article cannot have attribute values for its variant attribute');
if (article?.is_parent && article.parentattributename_id) {
const hasParentAttr = validated.some((v) => v.attributeId === article.parentattributename_id);
if (hasParentAttr) {
throw new Error('A parent article cannot have attribute values for its variant attribute');
}
}
if (article?.fatherarticle_id != null) {
let currentFatherId = article.fatherarticle_id;
if (article?.parentarticle_id != null) {
let currentParentId = article.parentarticle_id;
const visited = new Set();
while (currentFatherId != null && !visited.has(currentFatherId)) {
visited.add(currentFatherId);
const fatherRes = await runner.query(
'SELECT a.fatherarticle_id, a.fatherattributename_id, an.name AS fatherAttributeName FROM article a LEFT JOIN attributename an ON an.id = a.fatherattributename_id WHERE a.id = @currentFatherId',
{ currentFatherId },
while (currentParentId != null && !visited.has(currentParentId)) {
visited.add(currentParentId);
const parentRes = await runner.query(
'SELECT a.parentarticle_id, a.parentattributename_id, an.name AS parentAttributeName FROM article a LEFT JOIN attributename an ON an.id = a.parentattributename_id WHERE a.id = @currentParentId',
{ currentParentId },
);
const fRow = fatherRes.rows[0];
const fRow = parentRes.rows[0];
if (!fRow) break;
if (fRow.fatherattributename_id != null) {
const hasFatherAttr = validated.some((v) => v.attributeId === fRow.fatherattributename_id);
if (!hasFatherAttr) {
const fatherAttrName = fRow.fatherAttributeName || 'ancestor variant attribute';
throw new Error(`Child article must have an attribute value for the father attribute "${fatherAttrName}"`);
if (fRow.parentattributename_id != null) {
const hasParentAttr = validated.some((v) => v.attributeId === fRow.parentattributename_id);
if (!hasParentAttr) {
const parentAttrName = fRow.parentAttributeName || 'ancestor variant attribute';
throw new Error(`Child article must have an attribute value for the parent attribute "${parentAttrName}"`);
}
}
currentFatherId = fRow.fatherarticle_id;
currentParentId = fRow.parentarticle_id;
}
}

View File

@@ -64,7 +64,7 @@ export default function setItemsByArticleSku(provider) {
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
const artRes = await runner.query(
'SELECT id, is_father, articlelist_id, optionslist_id FROM article WHERE id = @articleId',
'SELECT id, is_parent, articlelist_id, optionslist_id FROM article WHERE id = @articleId',
{ articleId },
);
const article = artRes.rows[0];
@@ -72,8 +72,8 @@ export default function setItemsByArticleSku(provider) {
throw new Error('Article not found');
}
if (article.is_father) {
throw new Error('A father article cannot be a Stückliste');
if (article.is_parent) {
throw new Error('A parent article cannot be a Stückliste');
}
// If items is empty: clear Stückliste

View File

@@ -91,9 +91,9 @@ export default function create(provider) {
runner,
);
const artCheck = await runner.query('SELECT is_father FROM article WHERE id = @articleId', { articleId });
if (artCheck.rows[0]?.is_father) {
throw new Error('A father article cannot have suppliers');
const artCheck = await runner.query('SELECT is_parent FROM article WHERE id = @articleId', { articleId });
if (artCheck.rows[0]?.is_parent) {
throw new Error('A parent article cannot have suppliers');
}
await assertSupplier(runner, companyId, entityId);

View File

@@ -11,7 +11,7 @@ export default function getCategoryArticles(provider) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} categoryId - The id of the category.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null}>>} The articles in the category.
* @returns {Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null}>>} The articles in the category.
*
* @example
* const articles = await store.category.getCategoryArticles(42, 5);

View File

@@ -26,8 +26,8 @@ export default function queryCategoryArticles(provider) {
...rest,
track_stock: Boolean(rest.track_stock ?? 1),
// MSSQL returns BIT as boolean, SQLite as 0/1 integer. Normalize so both
// dialects expose isFather as a real boolean, matching article/getBySku.
isFather: Boolean(rest.isFather),
// dialects expose isParent as a real boolean, matching article/getBySku.
isParent: Boolean(rest.isParent),
brand: brandName
? {
id: brandId ?? null,

View File

@@ -1,9 +1,9 @@
import {
CHECK_ARTICLE_IS_FATHER as MSSQL_CHECK_ARTICLE_IS_FATHER,
CHECK_ARTICLE_IS_PARENT as MSSQL_CHECK_ARTICLE_IS_PARENT,
CREATE_LINE as MSSQL_CREATE_LINE,
} from '../providers/mssql/queries/manifestDoc.js';
import {
CHECK_ARTICLE_IS_FATHER as SQLITE_CHECK_ARTICLE_IS_FATHER,
CHECK_ARTICLE_IS_PARENT as SQLITE_CHECK_ARTICLE_IS_PARENT,
CREATE_LINE as SQLITE_CREATE_LINE,
} from '../providers/sqlite3/queries/manifestDoc.js';
import makeRunner from './runner.js';
@@ -122,20 +122,20 @@ export async function insertLines(
const { manifestId, lines, ensureTax } = params;
const isSqlite = runner?.dialect === 'sqlite3';
const createLineSql = isSqlite ? SQLITE_CREATE_LINE : MSSQL_CREATE_LINE;
const checkFatherSql = isSqlite ? SQLITE_CHECK_ARTICLE_IS_FATHER : MSSQL_CHECK_ARTICLE_IS_FATHER;
const checkParentSql = isSqlite ? SQLITE_CHECK_ARTICLE_IS_PARENT : MSSQL_CHECK_ARTICLE_IS_PARENT;
for (const line of lines) {
let resolvedName = line.name;
let resolvedPu = line.pu;
if (line.sku) {
const fatherCheck = await runner.query(checkFatherSql, {
const parentCheck = await runner.query(checkParentSql, {
manifestId,
sku: line.sku,
});
const art = fatherCheck.rows[0];
if (art?.isFather) {
throw new Error(`A father article cannot be booked on documents (${line.sku})`);
const art = parentCheck.rows[0];
if (art?.isParent) {
throw new Error(`A parent article cannot be booked on documents (${line.sku})`);
}
if (!resolvedName && art?.articleName) {
resolvedName = art.articleName;

View File

@@ -64,7 +64,7 @@ export default function setOptionsByArticleSku(provider) {
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
const artRes = await runner.query(
'SELECT id, is_father, articlelist_id, optionslist_id FROM article WHERE id = @articleId',
'SELECT id, is_parent, articlelist_id, optionslist_id FROM article WHERE id = @articleId',
{ articleId },
);
const article = artRes.rows[0];
@@ -72,8 +72,8 @@ export default function setOptionsByArticleSku(provider) {
throw new Error('Article not found');
}
if (article.is_father) {
throw new Error('A father article cannot have options');
if (article.is_parent) {
throw new Error('A parent article cannot have options');
}
// If options is empty: clear optionslist

View File

@@ -116,11 +116,11 @@ export default function adjust(provider) {
const resolvedArticles = [];
for (const item of normalizedLines) {
const resolved = await resolveArticle(googleAuthId, item.sku, activeTx);
const artRes = await runner.query('SELECT is_father, articlelist_id, track_stock FROM article WHERE id = @articleId', {
const artRes = await runner.query('SELECT is_parent, articlelist_id, track_stock FROM article WHERE id = @articleId', {
articleId: resolved.articleId,
});
if (artRes.rows[0]?.is_father) {
throw new Error(`A father article cannot have stock (${item.sku})`);
if (artRes.rows[0]?.is_parent) {
throw new Error(`A parent article cannot have stock (${item.sku})`);
}
if (artRes.rows[0]?.articlelist_id != null) {