Compare commits
8 Commits
3b74c12f4e
...
4718cb5726
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4718cb5726 | ||
|
|
0268583ae3 | ||
|
|
c80bfee8e8 | ||
|
|
067ddc088c | ||
|
|
2158ac475b | ||
|
|
9a1c19cf60 | ||
|
|
54398d90f2 | ||
|
|
89fe6e6451 |
@@ -6,18 +6,19 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
|||||||
|
|
||||||
*Source: `src/store/article/addChild.js`*
|
*Source: `src/store/article/addChild.js`*
|
||||||
|
|
||||||
Adds a child article to a father article.
|
Adds a child article to a parent article (creates and attaches with auto-generated SKU).
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
| `fatherSku` | `string` | The father article SKU. |
|
| `parentSku` | `string` | The parent article SKU. |
|
||||||
| `options` | `object` | |
|
| `options` | `object` | |
|
||||||
| `[options.attributeValueId]` | `number` | Existing attribute value ID for father's attribute. |
|
| `[options.attach=false]` | `boolean` | If true, attaches an existing article instead of creating. |
|
||||||
| `[options.value]` | `string` | String value to find or create under father's attribute. |
|
| `[options.attributeValueId]` | `number` | Existing attribute value ID for parent's attribute. |
|
||||||
| `[options.name]` | `string` | Custom name for child article (defaults to father name + value). |
|
| `[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.unitamount]` | `number` | Optional unitamount override. |
|
||||||
| `[options.weight]` | `number` | Optional weight override. |
|
| `[options.weight]` | `number` | Optional weight override. |
|
||||||
| `[options.track_stock]` | `boolean|number` | Optional track_stock override. |
|
| `[options.track_stock]` | `boolean|number` | Optional track_stock override. |
|
||||||
@@ -53,6 +54,33 @@ const result = await store.article.addImage(42, 'SKU-1', imageBuffer);
|
|||||||
// => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash }
|
// => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `article.attachChild`
|
||||||
|
|
||||||
|
*Source: `src/store/article/attachChild.js`*
|
||||||
|
|
||||||
|
Attaches an existing article as a child to a parent article.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `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 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. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{article: object, children: Array<object>}>`
|
||||||
|
|
||||||
## `article.createUnderParent`
|
## `article.createUnderParent`
|
||||||
|
|
||||||
*Source: `src/store/article/createUnderParent.js`*
|
*Source: `src/store/article/createUnderParent.js`*
|
||||||
@@ -156,7 +184,7 @@ Returns the full detail of an article by its SKU, including unit, brand, categor
|
|||||||
|
|
||||||
**Returns**
|
**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**
|
**Throws**
|
||||||
|
|
||||||
@@ -203,7 +231,7 @@ const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
|
|||||||
|
|
||||||
*Source: `src/store/article/importRows.js`*
|
*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**
|
**Parameters**
|
||||||
|
|
||||||
@@ -214,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` | `Array<object>` | The article rows to import. |
|
||||||
| `input.rows[].sku` | `string` | The article SKU. |
|
| `input.rows[].sku` | `string` | The article SKU. |
|
||||||
| `input.rows[].name` | `string` | The article name. |
|
| `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[].parentId]` | `number|string` | The id of the parent category. |
|
||||||
| `[input.rows[].parentCategory]` | `string` | The category breadcrumb path, e.g. `'Flowers | CBN'`. |
|
| `[input.rows[].parentCategory]` | `string` | The category breadcrumb path, e.g. `'Flowers | CBN'`. |
|
||||||
| `[input.rows[].brand]` | `string` | The brand name to upsert. |
|
| `[input.rows[].brand]` | `string` | The brand name to upsert. |
|
||||||
@@ -222,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[].tax]` | `number|string` | The tax rate in percent (e.g. `19`). |
|
||||||
| `[input.rows[].eans]` | `Array<string>|string` | The EANs to set. |
|
| `[input.rows[].eans]` | `Array<string>|string` | The EANs to set. |
|
||||||
| `[input.rows[].track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. |
|
| `[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. |
|
| `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
@@ -236,7 +264,7 @@ Imports article rows, creating articles by their SKU and returning per-row impor
|
|||||||
const result = await store.article.importRows({
|
const result = await store.article.importRows({
|
||||||
googleAuthId: 42,
|
googleAuthId: 42,
|
||||||
rows: [
|
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 },
|
{ sku: '42001190', name: '420Strainz', vk: 'V', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '0,00', tax: 19 },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -247,19 +275,19 @@ const result = await store.article.importRows({
|
|||||||
|
|
||||||
*Source: `src/store/article/listChildren.js`*
|
*Source: `src/store/article/listChildren.js`*
|
||||||
|
|
||||||
Lists all children of a father article.
|
Lists all children of a parent article.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
| `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. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
**Returns**
|
**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`
|
## `article.listExport`
|
||||||
|
|
||||||
@@ -349,14 +377,14 @@ const result = await store.article.orderImages(42, 'SKU-1', { fullHashes: ['a',
|
|||||||
|
|
||||||
*Source: `src/store/article/removeChild.js`*
|
*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**
|
**Parameters**
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
| `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. |
|
| `childSku` | `string` | The child article SKU. |
|
||||||
| `[options]` | `object` | |
|
| `[options]` | `object` | |
|
||||||
| `[options.deleteArticle=false]` | `boolean` | Whether to delete the child article entity entirely. |
|
| `[options.deleteArticle=false]` | `boolean` | Whether to delete the child article entity entirely. |
|
||||||
@@ -365,7 +393,7 @@ Removes a child article from a father article, either by unlinking or deleting i
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>`
|
`Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>`
|
||||||
|
|
||||||
## `article.resolveUniqueSlug`
|
## `article.resolveUniqueSlug`
|
||||||
|
|
||||||
@@ -478,11 +506,11 @@ const article = await store.article.setEansBySku(42, 'SKU-1', ['4001234567890'])
|
|||||||
// => { id, sku, name, ... }
|
// => { 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**
|
**Parameters**
|
||||||
|
|
||||||
@@ -490,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. |
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
| `sku` | `string` | The child article SKU. |
|
| `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. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -499,13 +527,13 @@ Links an article to a father article (V/K compound) by their SKUs.
|
|||||||
|
|
||||||
**Throws**
|
**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**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const article = await store.article.setFatherBySku(42, '42001190-1', '42001190');
|
const article = await store.article.setParentBySku(42, '42001190-1', '42001190');
|
||||||
// => { id, sku, name, fatherSku: '42001190', ... }
|
// => { id, sku, name, parentSku: '42001190', ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
## `article.setPricesBySku`
|
## `article.setPricesBySku`
|
||||||
@@ -598,11 +626,11 @@ const article = await store.article.setWeightBySku(42, 'SKU-1', 1.5);
|
|||||||
// => { id, sku, name, ... }
|
// => { 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**
|
**Parameters**
|
||||||
|
|
||||||
@@ -611,9 +639,9 @@ Toggles an article into or out of father mode.
|
|||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
| `sku` | `string` | The article SKU. |
|
| `sku` | `string` | The article SKU. |
|
||||||
| `options` | `object` | |
|
| `options` | `object` | |
|
||||||
| `options.isFather` | `boolean` | Whether the article should be a father article. |
|
| `options.isParent` | `boolean` | Whether the article should be a parent article. |
|
||||||
| `[options.attributeNameId]` | `number` | The attribute name id required when enabling father mode. |
|
| `[options.attributeNameId]` | `number` | The attribute name id required when enabling parent mode. |
|
||||||
| `[options.fatherAttributeNameId]` | `number` | Alias for attributeNameId. |
|
| `[options.parentAttributeNameId]` | `number` | Alias for attributeNameId. |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Lists the properties of an article by its SKU.
|
|||||||
|
|
||||||
*Source: `src/store/articleproperty/setByArticleSku.js`*
|
*Source: `src/store/articleproperty/setByArticleSku.js`*
|
||||||
|
|
||||||
Replaces all properties for an article by its SKU. Enforces exclusive flag on property names.
|
Replaces all properties for an article by its SKU. Enforces exclusive flag on property names. An exclusive property that already has a value is never overwritten: a different value throws `Exclusive property cannot be overwritten: <name>`.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ Creates an attribute name for the user's selected company.
|
|||||||
|
|
||||||
`Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>`
|
`Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the name is missing or already exists for the company.
|
||||||
|
|
||||||
## `attributename.delete`
|
## `attributename.delete`
|
||||||
|
|
||||||
*Source: `src/store/attributename/delete.js`*
|
*Source: `src/store/attributename/delete.js`*
|
||||||
@@ -41,6 +45,10 @@ Deletes an attribute name for the user's selected company.
|
|||||||
|
|
||||||
`Promise<void>`
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If attribute name not found or in use.
|
||||||
|
|
||||||
## `attributename.list`
|
## `attributename.list`
|
||||||
|
|
||||||
*Source: `src/store/attributename/list.js`*
|
*Source: `src/store/attributename/list.js`*
|
||||||
@@ -98,3 +106,7 @@ Updates an attribute name for the user's selected company.
|
|||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>`
|
`Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the attribute name is unknown, the name is missing, or the name is already taken.
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ Creates an attribute value under a given attribute.
|
|||||||
|
|
||||||
`Promise<{id: number, attributeId: number, value: string, sort: number}>`
|
`Promise<{id: number, attributeId: number, value: string, sort: number}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the value is missing, the attribute is unknown, or the value already exists for the attribute.
|
||||||
|
|
||||||
## `attributevalue.delete`
|
## `attributevalue.delete`
|
||||||
|
|
||||||
*Source: `src/store/attributevalue/delete.js`*
|
*Source: `src/store/attributevalue/delete.js`*
|
||||||
@@ -41,6 +45,10 @@ Deletes an attribute value.
|
|||||||
|
|
||||||
`Promise<void>`
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If attribute value not found or in use.
|
||||||
|
|
||||||
## `attributevalue.listByAttributeId`
|
## `attributevalue.listByAttributeId`
|
||||||
|
|
||||||
*Source: `src/store/attributevalue/listByAttributeId.js`*
|
*Source: `src/store/attributevalue/listByAttributeId.js`*
|
||||||
@@ -99,3 +107,7 @@ Updates an attribute value.
|
|||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, attributeId: number, value: string, sort: number}>`
|
`Promise<{id: number, attributeId: number, value: string, sort: number}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the attribute value is unknown, the value is missing, or the value is already taken.
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ Returns the articles belonging to a single category.
|
|||||||
|
|
||||||
**Returns**
|
**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**
|
**Example**
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,36 @@
|
|||||||
|
|
||||||
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `company.addHistory`
|
||||||
|
|
||||||
|
*Source: `src/store/company/addHistory.js`*
|
||||||
|
|
||||||
|
Adds an entry to the company history for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object|string` | The history input object or text string. |
|
||||||
|
| `[input.text]` | `string` | The history entry text. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyuserId: number|null, date: string, text: string}>` — The inserted entry.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If text is missing or user is not authorized.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const entry = await store.company.addHistory(42, { text: 'API suggestion: add barcode scanner endpoint' });
|
||||||
|
// => { id, companyuserId, date, text }
|
||||||
|
```
|
||||||
|
|
||||||
## `company.create`
|
## `company.create`
|
||||||
|
|
||||||
*Source: `src/store/company/create.js`*
|
*Source: `src/store/company/create.js`*
|
||||||
@@ -112,6 +142,35 @@ const company = await store.company.getByName('Acme');
|
|||||||
// => { id, name, ... }
|
// => { id, name, ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `company.getHistory`
|
||||||
|
|
||||||
|
*Source: `src/store/company/getHistory.js`*
|
||||||
|
|
||||||
|
Returns the company history for the user's selected company, newest first.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[opts]` | `object` | Optional options. |
|
||||||
|
| `[opts.limit]` | `number` | Maximum number of entries to return (newest first). |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>` — The company history entries.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const history = await store.company.getHistory(42);
|
||||||
|
// => [{ id, companyuserId, date, text }]
|
||||||
|
|
||||||
|
const recent = await store.company.getHistory(42, { limit: 5 });
|
||||||
|
// => up to 5 most recent entries
|
||||||
|
```
|
||||||
|
|
||||||
## `company.list`
|
## `company.list`
|
||||||
|
|
||||||
*Source: `src/store/company/list.js`*
|
*Source: `src/store/company/list.js`*
|
||||||
|
|||||||
20
API.md
20
API.md
@@ -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
|
> **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)`
|
- **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`
|
- **articleean**: `id PK, article_id->article, ean:s(50) UQ`
|
||||||
- **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt`
|
- **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt`
|
||||||
@@ -27,6 +27,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- **categorypic**: `id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)`
|
- **categorypic**: `id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)`
|
||||||
- **categorytext**: `id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)`
|
- **categorytext**: `id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)`
|
||||||
- **company**: `id PK, name:s(255) UQ, root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup`
|
- **company**: `id PK, name:s(255) UQ, root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup`
|
||||||
|
- **companyhistory**: `id PK, company_id->company, companyuser_id?->companyuser, date:dt, text:txt`
|
||||||
- **companysetting**: `id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)`
|
- **companysetting**: `id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)`
|
||||||
- **companyuser**: `id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)`
|
- **companyuser**: `id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)`
|
||||||
- **customergroup**: `id PK, name:s(255), sort:i=0, company_id?->company`
|
- **customergroup**: `id PK, name:s(255), sort:i=0, company_id?->company`
|
||||||
@@ -65,10 +66,12 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `delete(id: number, tx?: object): Promise<void>`
|
- `delete(id: number, tx?: object): Promise<void>`
|
||||||
- `getByEmail(email: string, tx?: object): Promise<Array<number>>`
|
- `getByEmail(email: string, tx?: object): Promise<Array<number>>`
|
||||||
- [company](API.company.md)
|
- [company](API.company.md)
|
||||||
|
- `addHistory(googleAuthId: number, input: {text?: string}, tx?: object): Promise<{id: number, companyuserId: number|null, date: string, text: string}>`
|
||||||
- `create(input: {iso639: string, name: string, taxrate: number, reducedtaxrate: number, googleAuthId: number}, tx?: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>`
|
- `create(input: {iso639: string, name: string, taxrate: number, reducedtaxrate: number, googleAuthId: number}, tx?: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>`
|
||||||
- `delete(companyId: number, tx?: object): Promise<void>`
|
- `delete(companyId: number, tx?: object): Promise<void>`
|
||||||
- `getById(id: number, tx?: object): Promise<object|undefined>`
|
- `getById(id: number, tx?: object): Promise<object|undefined>`
|
||||||
- `getByName(name: string, tx?: object): Promise<object|undefined>`
|
- `getByName(name: string, tx?: object): Promise<object|undefined>`
|
||||||
|
- `getHistory(googleAuthId: number, opts?: {limit?: number}, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<object>>`
|
- `list(googleAuthId: number, tx?: object): Promise<Array<object>>`
|
||||||
- `update(id: number, input: {iso639: string, name: string, taxrate: number, reducedtaxrate: number}, tx?: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number}>`
|
- `update(id: number, input: {iso639: string, name: string, taxrate: number, reducedtaxrate: number}, tx?: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number}>`
|
||||||
- [category](API.category.md)
|
- [category](API.category.md)
|
||||||
@@ -77,34 +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>`
|
- `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}>`
|
- `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>`
|
- `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>}>`
|
- `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}>`
|
- `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>}>`
|
- `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}>`
|
- `rename(input: {googleAuthId: number, id: number, name: string}, tx?: object): Promise<{id: number, name: string, slug: string}>`
|
||||||
- [article](API.article.md)
|
- [article](API.article.md)
|
||||||
- `addChild(googleAuthId: number, fatherSku: string, options: {attributeValueId?: number, value?: string, name?: string, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<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}>`
|
- `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, 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}>`
|
- `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>`
|
- `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise<void>`
|
||||||
- `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: 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}>>`
|
- `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>}>`
|
- `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>>`
|
- `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>}>>`
|
- `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>}>`
|
- `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>`
|
- `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>`
|
- `setBrandIdBySku(googleAuthId: number, sku: string, brandId: number|null, tx?: object): Promise<object>`
|
||||||
- `setCategoryIdsBySku(googleAuthId: number, sku: string, categoryIds: Array<number>, 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>`
|
- `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>`
|
- `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>`
|
- `setTrackStockBySku(googleAuthId: number, sku: string, trackStock: boolean|number, tx?: object): Promise<object>`
|
||||||
- `setWeightBySku(googleAuthId: number, sku: string, weight: 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>`
|
- `updateBySku(googleAuthId: number, sku: string, patch: {name?: string, unitSi?: string, unitamount?: number, track_stock?: boolean|number}, tx?: object): Promise<object>`
|
||||||
- [lang](API.lang.md)
|
- [lang](API.lang.md)
|
||||||
- `create(iso639: string, tx?: object): Promise<number>`
|
- `create(iso639: string, tx?: object): Promise<number>`
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ Creates a property name for the user's selected company.
|
|||||||
|
|
||||||
`Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
|
`Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the name is missing or already exists for the company.
|
||||||
|
|
||||||
## `propertyname.delete`
|
## `propertyname.delete`
|
||||||
|
|
||||||
*Source: `src/store/propertyname/delete.js`*
|
*Source: `src/store/propertyname/delete.js`*
|
||||||
@@ -41,6 +45,10 @@ Deletes a property name for the user's selected company.
|
|||||||
|
|
||||||
`Promise<void>`
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If property name not found or in use.
|
||||||
|
|
||||||
## `propertyname.list`
|
## `propertyname.list`
|
||||||
|
|
||||||
*Source: `src/store/propertyname/list.js`*
|
*Source: `src/store/propertyname/list.js`*
|
||||||
@@ -98,3 +106,7 @@ Updates a property name for the user's selected company.
|
|||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
|
`Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the property name is unknown, the name is missing, or the name is already taken.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
| `article.setBrandIdBySku` | 18 | 15.50 | 24.98 | 0.57 | 0.81 | 27.21x |
|
| `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.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.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.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.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 |
|
| `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.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.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.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.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.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 |
|
| `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.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.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.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.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.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 |
|
| `article.updateBySku` | 18 | 0.83 | 0.73 | 1.65 | 0.78 | 1.65 | 0 |
|
||||||
|
|||||||
@@ -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**. |
|
| 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. |
|
| 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. |
|
| 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 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. |
|
| 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). |
|
| 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`
|
8. `manifest`, `manifestarticle`, `file`, `manifestattachment`, `warehouse`, `ledger`
|
||||||
9. `numbers`, `companyuser`, `articlehistory`, `entityhistory`, `syncpos`, `companysetting`, `posterminal`, `posterminalcategory`, `printer`, `labeltemplate`
|
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`)
|
## 5. Öffentliche API (`store.transfer`)
|
||||||
|
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c
|
|||||||
| `order` | `({ googleAuthId, parentId, categoryIds })` | `{ categoryIds }` |
|
| `order` | `({ googleAuthId, parentId, categoryIds })` | `{ categoryIds }` |
|
||||||
| `delete` | `({ googleAuthId, id })` | `void` — `Category has children` if child categories remain; FK `categoryarticle` also blocks deletion |
|
| `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` |
|
| `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 }` |
|
| `addImage` | `(googleAuthId, categoryId, imageBuffer)` | `{ id, thumbW, thumbH, thumbHash }` |
|
||||||
| `deleteImage` | `(googleAuthId, categoryId)` | `{ id, thumbW: null, thumbH: null, thumbHash: null }` |
|
| `deleteImage` | `(googleAuthId, categoryId)` | `{ id, thumbW: null, thumbH: null, thumbHash: null }` |
|
||||||
|
|
||||||
@@ -643,8 +643,8 @@ type ArticleDetail = {
|
|||||||
optionsCount: number, // 0 when no optionslist
|
optionsCount: number, // 0 when no optionslist
|
||||||
articleListCount: number, // 0 when no articlelist
|
articleListCount: number, // 0 when no articlelist
|
||||||
childCount: number, // 0 when no children
|
childCount: number, // 0 when no children
|
||||||
fatherArticleId: number | null, // null when root article
|
parentArticleId: number | null, // null when root article
|
||||||
fatherSku: string | null, // null when root article
|
parentSku: string | null, // null when root article
|
||||||
}
|
}
|
||||||
|
|
||||||
type PriceEntry = {
|
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`.
|
`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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ function render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SCHEMA_TABLES = [
|
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)'],
|
['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'],
|
['articleean', 'id PK, article_id->article, ean:s(50) UQ'],
|
||||||
['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt'],
|
['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt'],
|
||||||
@@ -338,6 +338,7 @@ const SCHEMA_TABLES = [
|
|||||||
['categorypic', 'id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)'],
|
['categorypic', 'id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)'],
|
||||||
['categorytext', 'id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)'],
|
['categorytext', 'id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)'],
|
||||||
['company', 'id PK, name:s(255) UQ, root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup'],
|
['company', 'id PK, name:s(255) UQ, root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup'],
|
||||||
|
['companyhistory', 'id PK, company_id->company, companyuser_id?->companyuser, date:dt, text:txt'],
|
||||||
['companysetting', 'id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)'],
|
['companysetting', 'id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)'],
|
||||||
['companyuser', 'id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)'],
|
['companyuser', 'id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)'],
|
||||||
['customergroup', 'id PK, name:s(255), sort:i=0, company_id?->company'],
|
['customergroup', 'id PK, name:s(255), sort:i=0, company_id?->company'],
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ const CLUSTERS = [
|
|||||||
'companyuser',
|
'companyuser',
|
||||||
'syncpos',
|
'syncpos',
|
||||||
'company',
|
'company',
|
||||||
|
'companyhistory',
|
||||||
'companysetting',
|
'companysetting',
|
||||||
'customergroup',
|
'customergroup',
|
||||||
'entity',
|
'entity',
|
||||||
|
|||||||
@@ -25,9 +25,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^1.8.1",
|
"@clack/prompts": "^1.8.1",
|
||||||
"@hpcc-js/wasm-graphviz": "^1.29.1",
|
"@hpcc-js/wasm-graphviz": "^1.29.2",
|
||||||
"chalk": "^6.0.1",
|
"chalk": "^6.0.1",
|
||||||
"dotenv": "^18.0.4",
|
"dotenv": "^18.0.5",
|
||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"sharp": "^0.35.5",
|
"sharp": "^0.35.5",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
|
|||||||
141
schema-graph.dot
141
schema-graph.dot
File diff suppressed because one or more lines are too long
3019
schema-graph.svg
3019
schema-graph.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 294 KiB |
@@ -52,7 +52,7 @@ export async function fillDatabase(store, options = {}) {
|
|||||||
secondaryCompany: null,
|
secondaryCompany: null,
|
||||||
categories: [],
|
categories: [],
|
||||||
articles: [],
|
articles: [],
|
||||||
fatherArticles: [],
|
parentArticles: [],
|
||||||
childArticles: [],
|
childArticles: [],
|
||||||
customergroups: [],
|
customergroups: [],
|
||||||
entities: [],
|
entities: [],
|
||||||
@@ -326,7 +326,7 @@ export async function fillDatabase(store, options = {}) {
|
|||||||
const numArticles = counts.articles;
|
const numArticles = counts.articles;
|
||||||
for (let i = 1; i <= numArticles; i++) {
|
for (let i = 1; i <= numArticles; i++) {
|
||||||
const targetCat = ctx.categories[i % ctx.categories.length];
|
const targetCat = ctx.categories[i % ctx.categories.length];
|
||||||
const isFather = i % 4 === 1;
|
const isParent = i % 4 === 1;
|
||||||
const art = await store.article.createUnderParent({
|
const art = await store.article.createUnderParent({
|
||||||
googleAuthId: ctx.googleAuthId,
|
googleAuthId: ctx.googleAuthId,
|
||||||
parentId: targetCat.id,
|
parentId: targetCat.id,
|
||||||
@@ -365,12 +365,12 @@ export async function fillDatabase(store, options = {}) {
|
|||||||
ctx.categories[(i + 1) % ctx.categories.length].id,
|
ctx.categories[(i + 1) % ctx.categories.length].id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// V/K Father-Child hierarchy
|
// V/K Parent-Child hierarchy
|
||||||
if (isFather) {
|
if (isParent) {
|
||||||
ctx.fatherArticles.push(art);
|
ctx.parentArticles.push(art);
|
||||||
} else if (ctx.fatherArticles.length > 0) {
|
} else if (ctx.parentArticles.length > 0) {
|
||||||
const parentFather = ctx.fatherArticles[ctx.fatherArticles.length - 1];
|
const parentParent = ctx.parentArticles[ctx.parentArticles.length - 1];
|
||||||
await store.article.setFatherBySku(ctx.googleAuthId, sku, parentFather.sku);
|
await store.article.setParentBySku(ctx.googleAuthId, sku, parentParent.sku);
|
||||||
ctx.childArticles.push(art);
|
ctx.childArticles.push(art);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ export async function fillDatabase(store, options = {}) {
|
|||||||
brand: 'BenchBrand Pro',
|
brand: 'BenchBrand Pro',
|
||||||
net: '45,50',
|
net: '45,50',
|
||||||
tax: 19,
|
tax: 19,
|
||||||
fatherSku: `IMP-${Date.now()}-1`,
|
parentSku: `IMP-${Date.now()}-1`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await store.article.importRows({
|
await store.article.importRows({
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import LibDashStore from '../src/index.js';
|
|||||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
|
|
||||||
async function run() {
|
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 tempDir = mkdtempSync(join(tmpdir(), 'test-hist-attr-'));
|
||||||
const dbFile = join(tempDir, 'test.db');
|
const dbFile = join(tempDir, 'test.db');
|
||||||
@@ -31,11 +31,11 @@ async function run() {
|
|||||||
});
|
});
|
||||||
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
console.log('1. Testing setFatherBySku history logging...');
|
console.log('1. Testing setParentBySku history logging...');
|
||||||
const father = await store.article.createUnderParent({
|
const parent = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Father Shirt',
|
name: 'Parent Shirt',
|
||||||
});
|
});
|
||||||
const child = await store.article.createUnderParent({
|
const child = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
@@ -44,19 +44,19 @@ async function run() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check history before linking
|
// Check history before linking
|
||||||
let fatherHist = await store.article.getHistoryBySku(googleAuthId, father.sku);
|
let parentHist = await store.article.getHistoryBySku(googleAuthId, parent.sku);
|
||||||
assert.equal(fatherHist.length, 1);
|
assert.equal(parentHist.length, 1);
|
||||||
assert.match(fatherHist[0].text, /angelegt/);
|
assert.match(parentHist[0].text, /angelegt/);
|
||||||
|
|
||||||
// Link child to father
|
// Link child to parent
|
||||||
await store.article.setFatherBySku(googleAuthId, child.sku, father.sku);
|
await store.article.setParentBySku(googleAuthId, child.sku, parent.sku);
|
||||||
|
|
||||||
// Father history must now contain the conversion entry and the child entry!
|
// Parent history must now contain the conversion entry and the child entry!
|
||||||
fatherHist = await store.article.getHistoryBySku(googleAuthId, father.sku);
|
parentHist = await store.article.getHistoryBySku(googleAuthId, parent.sku);
|
||||||
const hasConversion = fatherHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
|
const hasConversion = parentHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
|
||||||
const hasVariantAdded = fatherHist.some((h) => h.text.includes('hinzugefügt'));
|
const hasVariantAdded = parentHist.some((h) => h.text.includes('hinzugefügt'));
|
||||||
assert.ok(hasConversion, `Father history missing conversion row: ${JSON.stringify(fatherHist)}`);
|
assert.ok(hasConversion, `Parent history missing conversion row: ${JSON.stringify(parentHist)}`);
|
||||||
assert.ok(hasVariantAdded, `Father history missing variant added row: ${JSON.stringify(fatherHist)}`);
|
assert.ok(hasVariantAdded, `Parent history missing variant added row: ${JSON.stringify(parentHist)}`);
|
||||||
|
|
||||||
// Child history must contain the assignment entry
|
// Child history must contain the assignment entry
|
||||||
const childHist = await store.article.getHistoryBySku(googleAuthId, child.sku);
|
const childHist = await store.article.getHistoryBySku(googleAuthId, child.sku);
|
||||||
@@ -67,15 +67,15 @@ async function run() {
|
|||||||
const importRes = await store.article.importRows({
|
const importRes = await store.article.importRows({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
rows: [
|
rows: [
|
||||||
{ sku: 'IMP-FATHER', name: 'Imported Father', parentId: company.rootCategoryId },
|
{ sku: 'IMP-PARENT', name: 'Imported Parent', parentId: company.rootCategoryId },
|
||||||
{ sku: 'IMP-CHILD', name: 'Imported Child', parentId: company.rootCategoryId, fatherSku: 'IMP-FATHER' },
|
{ sku: 'IMP-CHILD', name: 'Imported Child', parentId: company.rootCategoryId, parentSku: 'IMP-PARENT' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
assert.equal(importRes.errors.length, 0);
|
assert.equal(importRes.errors.length, 0);
|
||||||
|
|
||||||
const impFatherHist = await store.article.getHistoryBySku(googleAuthId, 'IMP-FATHER');
|
const impParentHist = await store.article.getHistoryBySku(googleAuthId, 'IMP-PARENT');
|
||||||
const impHasConversion = impFatherHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
|
const impHasConversion = impParentHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
|
||||||
assert.ok(impHasConversion, `Imported father missing conversion row: ${JSON.stringify(impFatherHist)}`);
|
assert.ok(impHasConversion, `Imported parent missing conversion row: ${JSON.stringify(impParentHist)}`);
|
||||||
|
|
||||||
console.log('3. Testing flexible attributevalue request shapes in setByArticleSku...');
|
console.log('3. Testing flexible attributevalue request shapes in setByArticleSku...');
|
||||||
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
|
||||||
|
|||||||
@@ -119,31 +119,31 @@ async function run() {
|
|||||||
assert.equal(tomb.taxrate, 19);
|
assert.equal(tomb.taxrate, 19);
|
||||||
assert.ok(tomb.deleted_at);
|
assert.ok(tomb.deleted_at);
|
||||||
|
|
||||||
// 5. Test cascading deletion with father and variants
|
// 5. Test cascading deletion with parent and variants
|
||||||
console.log('5. Testing father with variant child deletion...');
|
console.log('5. Testing parent with variant child deletion...');
|
||||||
const father = await store.article.createUnderParent({
|
const parent = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Pants',
|
name: 'Pants',
|
||||||
});
|
});
|
||||||
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
|
||||||
await store.article.toggleFather(googleAuthId, father.sku, {
|
await store.article.toggleParent(googleAuthId, parent.sku, {
|
||||||
isFather: true,
|
isParent: true,
|
||||||
attributeNameId: attrName.id,
|
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, [
|
await store.article.setPricesBySku(googleAuthId, child.article.sku, [
|
||||||
{ price: 49.99, tax: 19, minmultiplier: 1 },
|
{ price: 49.99, tax: 19, minmultiplier: 1 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Delete child via removeChild with deleteArticle
|
// Delete child via removeChild with deleteArticle
|
||||||
console.log('Deleting child via removeChild({ deleteArticle: true })...');
|
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
|
// Add another child and delete parent with cascade: true
|
||||||
const child2 = await store.article.addChild(googleAuthId, father.sku, { value: 'L' });
|
const child2 = await store.article.addChild(googleAuthId, parent.sku, { value: 'L' });
|
||||||
console.log('Deleting father with cascade: true...');
|
console.log('Deleting parent with cascade: true...');
|
||||||
await store.article.delete(googleAuthId, father.sku, { cascade: true });
|
await store.article.delete(googleAuthId, parent.sku, { cascade: true });
|
||||||
|
|
||||||
const allTombstones = provider.db.prepare('SELECT * FROM articletombstone ORDER BY id ASC').all();
|
const allTombstones = provider.db.prepare('SELECT * FROM articletombstone ORDER BY id ASC').all();
|
||||||
assert.equal(allTombstones.length, 4);
|
assert.equal(allTombstones.length, 4);
|
||||||
@@ -164,9 +164,9 @@ async function run() {
|
|||||||
assert.equal(allTombstones[2].name, 'Pants - L');
|
assert.equal(allTombstones[2].name, 'Pants - L');
|
||||||
assert.equal(allTombstones[2].sku, child2.article.sku);
|
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].name, 'Pants');
|
||||||
assert.equal(allTombstones[3].sku, father.sku);
|
assert.equal(allTombstones[3].sku, parent.sku);
|
||||||
|
|
||||||
// 6. Test posterminal maxknowntombstoneid column and updates
|
// 6. Test posterminal maxknowntombstoneid column and updates
|
||||||
console.log('6. Testing posterminal maxknowntombstoneid column...');
|
console.log('6. Testing posterminal maxknowntombstoneid column...');
|
||||||
|
|||||||
109
scripts/test-unique-names.js
Normal file
109
scripts/test-unique-names.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import LibDashStore from '../src/index.js';
|
||||||
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('--- Testing uniqueness guards: attributename / attributevalue / propertyname ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-unique-names-'));
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: join(tempDir, 'test.db'),
|
||||||
|
storageDir: join(tempDir, 'storage'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('unique-tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Unique Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
console.log('1. POST /attributename with a duplicate name must throw a stable conflict error...');
|
||||||
|
const an1 = await store.attributename.create(googleAuthId, { name: 'Farbe' });
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.attributename.create(googleAuthId, { name: 'Farbe' }),
|
||||||
|
{ message: 'Attribute name already exists' },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('2. POST /attributename/{id}/value with a duplicate value...');
|
||||||
|
const av1 = await store.attributevalue.create(googleAuthId, an1.id, { value: 'Rot' });
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.attributevalue.create(googleAuthId, an1.id, { value: 'Rot' }),
|
||||||
|
{ message: 'Attribute value already exists' },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('3. POST /propertyname with a duplicate name...');
|
||||||
|
const pn1 = await store.propertyname.create(googleAuthId, { name: 'Notiz' });
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.propertyname.create(googleAuthId, { name: 'Notiz' }),
|
||||||
|
{ message: 'Property name already exists' },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('4. PATCH rename onto an existing name/value...');
|
||||||
|
const an2 = await store.attributename.create(googleAuthId, { name: 'Größe' });
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.attributename.update(googleAuthId, an2.id, { name: 'Farbe' }),
|
||||||
|
{ message: 'Attribute name already exists' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const av2 = await store.attributevalue.create(googleAuthId, an1.id, { value: 'Blau' });
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.attributevalue.update(googleAuthId, av2.id, { value: 'Rot' }),
|
||||||
|
{ message: 'Attribute value already exists' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const pn2 = await store.propertyname.create(googleAuthId, { name: 'Länge' });
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.propertyname.update(googleAuthId, pn2.id, { name: 'Notiz' }),
|
||||||
|
{ message: 'Property name already exists' },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('5. Nothing was persisted by the rejected writes...');
|
||||||
|
const anList = await store.attributename.list(googleAuthId);
|
||||||
|
assert.deepEqual(anList.map((a) => a.name).sort(), ['Farbe', 'Größe']);
|
||||||
|
const avList = await store.attributevalue.listByAttributeId(googleAuthId, an1.id);
|
||||||
|
assert.deepEqual(avList.map((v) => v.value).sort(), ['Blau', 'Rot']);
|
||||||
|
const pnList = await store.propertyname.list(googleAuthId);
|
||||||
|
assert.deepEqual(pnList.map((p) => p.name).sort(), ['Länge', 'Notiz']);
|
||||||
|
|
||||||
|
console.log('6. Renames to a free name still work...');
|
||||||
|
await store.attributename.update(googleAuthId, an2.id, { name: 'Größe 2' });
|
||||||
|
await store.propertyname.update(googleAuthId, pn2.id, { name: 'Länge 2' });
|
||||||
|
await store.attributevalue.update(googleAuthId, av2.id, { value: 'Grün' });
|
||||||
|
const renamed = await store.attributevalue.listByAttributeId(googleAuthId, an1.id);
|
||||||
|
assert.ok(renamed.some((v) => v.value === 'Grün'));
|
||||||
|
|
||||||
|
console.log('7. Same name in a different company is allowed (uniqueness is company-scoped)...');
|
||||||
|
const otherId = await store.google_auth.add('other-tester@example.com');
|
||||||
|
const otherCompany = await store.company.create({
|
||||||
|
name: 'Other Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId: otherId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(otherId, otherCompany.id);
|
||||||
|
const foreign = await store.attributename.create(otherId, { name: 'Farbe' });
|
||||||
|
assert.ok(foreign.id);
|
||||||
|
|
||||||
|
console.log('\nPASS - all uniqueness guards throw stable, greppable errors and persist nothing.');
|
||||||
|
} finally {
|
||||||
|
await provider.purge();
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('\nFAIL:', err?.message ?? err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -35,65 +35,65 @@ async function run() {
|
|||||||
const art1 = await store.article.createUnderParent({
|
const art1 = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Root Father Shirt',
|
name: 'Root Parent Shirt',
|
||||||
});
|
});
|
||||||
const art1Res = await store.article.getBySku(googleAuthId, art1.sku);
|
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({
|
const art2 = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Root Father Shirt',
|
name: 'Root Parent Shirt',
|
||||||
});
|
});
|
||||||
const art2Res = await store.article.getBySku(googleAuthId, art2.sku);
|
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({
|
const art3 = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Root Father Shirt',
|
name: 'Root Parent Shirt',
|
||||||
});
|
});
|
||||||
const art3Res = await store.article.getBySku(googleAuthId, art3.sku);
|
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...');
|
console.log('2. Testing updateBySku self-collision and renaming...');
|
||||||
// Update art2 without changing name
|
// Update art2 without changing name
|
||||||
await store.article.updateBySku(googleAuthId, art2.sku, { unitamount: 2 });
|
await store.article.updateBySku(googleAuthId, art2.sku, { unitamount: 2 });
|
||||||
let updated2 = await store.article.getBySku(googleAuthId, art2.sku);
|
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
|
// 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);
|
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
|
// Rename art2 to something else
|
||||||
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Unique Shirt' });
|
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Unique Shirt' });
|
||||||
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
|
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
|
||||||
assert.equal(updated2.article.slug, 'unique-shirt', `Expected slug 'unique-shirt', got '${updated2.article.slug}'`);
|
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
|
// 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 Father Shirt' });
|
await store.article.updateBySku(googleAuthId, art3.sku, { name: 'Root Parent Shirt' });
|
||||||
const updated3 = await store.article.getBySku(googleAuthId, art3.sku);
|
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...');
|
console.log('3. Testing addChild duplicate slugs...');
|
||||||
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
|
||||||
await store.article.toggleFather(googleAuthId, art1.sku, {
|
await store.article.toggleParent(googleAuthId, art1.sku, {
|
||||||
isFather: true,
|
isParent: true,
|
||||||
attributeNameId: attrName.id,
|
attributeNameId: attrName.id,
|
||||||
});
|
});
|
||||||
const child1 = await store.article.addChild(googleAuthId, art1.sku, {
|
const child1 = await store.article.addChild(googleAuthId, art1.sku, {
|
||||||
value: 'M',
|
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
|
// Add another child with name that yields same slug
|
||||||
const child2 = await store.article.addChild(googleAuthId, art1.sku, {
|
const child2 = await store.article.addChild(googleAuthId, art1.sku, {
|
||||||
value: 'L',
|
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...');
|
console.log('4. Testing importRows duplicate slugs in same batch...');
|
||||||
const importRes = await store.article.importRows({
|
const importRes = await store.article.importRows({
|
||||||
@@ -128,10 +128,10 @@ async function run() {
|
|||||||
const artCompany2 = await store.article.createUnderParent({
|
const artCompany2 = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company2.rootCategoryId,
|
parentId: company2.rootCategoryId,
|
||||||
name: 'Root Father Shirt',
|
name: 'Root Parent Shirt',
|
||||||
});
|
});
|
||||||
const artCompany2Detail = await store.article.getBySku(googleAuthId, artCompany2.sku);
|
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');
|
console.log('✅ ALL SLUG TESTS PASSED');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import LibDashStore from '../src/index.js';
|
|||||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
|
|
||||||
async function run() {
|
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 tempDir = mkdtempSync(join(tmpdir(), 'test-variants-'));
|
||||||
const dbFile = join(tempDir, 'test.db');
|
const dbFile = join(tempDir, 'test.db');
|
||||||
@@ -31,35 +31,35 @@ async function run() {
|
|||||||
});
|
});
|
||||||
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
// Create father article
|
// Create parent article
|
||||||
const father = await store.article.createUnderParent({
|
const parent = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'T-Shirt',
|
name: 'T-Shirt',
|
||||||
});
|
});
|
||||||
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
|
||||||
await store.article.toggleFather(googleAuthId, father.sku, {
|
await store.article.toggleParent(googleAuthId, parent.sku, {
|
||||||
isFather: true,
|
isParent: true,
|
||||||
attributeNameId: attrName.id,
|
attributeNameId: attrName.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('1. Add initial children (Rot, Blau, Grün)...');
|
console.log('1. Add initial children (Rot, Blau, Grün)...');
|
||||||
const child1 = await store.article.addChild(googleAuthId, father.sku, { value: 'Rot' });
|
const child1 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Rot' });
|
||||||
const child2 = await store.article.addChild(googleAuthId, father.sku, { value: 'Blau' });
|
const child2 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Blau' });
|
||||||
const child3 = await store.article.addChild(googleAuthId, father.sku, { value: 'Grün' });
|
const child3 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Grün' });
|
||||||
|
|
||||||
assert.equal(child1.article.sku, `${father.sku}-1`);
|
assert.equal(child1.article.sku, `${parent.sku}-1`);
|
||||||
assert.equal(child2.article.sku, `${father.sku}-2`);
|
assert.equal(child2.article.sku, `${parent.sku}-2`);
|
||||||
assert.equal(child3.article.sku, `${father.sku}-3`);
|
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.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)...');
|
console.log('2. Testing Duplicate Variant Protection (Defect 3)...');
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
async () => {
|
async () => {
|
||||||
await store.article.addChild(googleAuthId, father.sku, { value: 'Rot' });
|
await store.article.addChild(googleAuthId, parent.sku, { value: 'Rot' });
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
assert.match(err.message, /Variant with this attribute value already exists/);
|
assert.match(err.message, /Variant with this attribute value already exists/);
|
||||||
@@ -69,52 +69,82 @@ async function run() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
console.log('3. Unlink middle child (child2 / SKU-2) -> leaves orphan holding SKU-2 (Bug A & B)...');
|
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,
|
deleteArticle: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that remaining children's fathersort was recompacted
|
// Check that remaining children's parentsort was recompacted
|
||||||
list = await store.article.listChildren(googleAuthId, father.sku);
|
list = await store.article.listChildren(googleAuthId, parent.sku);
|
||||||
assert.equal(list.length, 2);
|
assert.equal(list.length, 2);
|
||||||
assert.deepEqual(list.map((c) => c.sku), [child1.article.sku, child3.article.sku]);
|
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
|
// Check orphan still exists in DB
|
||||||
const orphan = await store.article.getBySku(googleAuthId, child2.article.sku);
|
const orphan = await store.article.getBySku(googleAuthId, child2.article.sku);
|
||||||
assert.equal(orphan.article.fatherArticleId, null);
|
assert.equal(orphan.article.parentArticleId, null);
|
||||||
assert.equal(orphan.article.sku, `${father.sku}-2`);
|
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)...');
|
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
|
// 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.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
|
// 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, parent.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, 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(
|
await provider.query(
|
||||||
`UPDATE article SET fathersort = 1 WHERE sku = @sku`,
|
`UPDATE article SET parentsort = 1 WHERE sku = @sku`,
|
||||||
{ sku: child1.article.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.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)
|
// Now adding a variant MUST NOT hit UNIQUE constraint failed (409)
|
||||||
console.log('Adding variant to father with child at fathersort: 1...');
|
console.log('Adding variant to parent with child at parentsort: 1...');
|
||||||
const child5 = await store.article.addChild(googleAuthId, father.sku, { value: 'Schwarz' });
|
const child5 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Schwarz' });
|
||||||
assert.ok(child5);
|
assert.ok(child5);
|
||||||
list = await store.article.listChildren(googleAuthId, father.sku);
|
list = await store.article.listChildren(googleAuthId, parent.sku);
|
||||||
assert.equal(list.length, 2);
|
assert.equal(list.length, 2);
|
||||||
// Fathersort should have picked 0 (next free non-negative integer)
|
// Parentsort should have picked 0 (next free non-negative integer)
|
||||||
assert.deepEqual(list.map((c) => c.fathersort), [0, 1]);
|
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, parent.sku, {
|
||||||
|
sku: 'SOME-CUSTOM-SKU',
|
||||||
|
value: 'Lila',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
assert.match(err.message, /sku is not allowed when creating a child article/);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'Expected sku on addChild to be rejected'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('7. Testing attaching existing orphan article (SKU-2) via attachChild...');
|
||||||
|
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.parentArticleId, parent.id);
|
||||||
|
|
||||||
|
console.log('8. Testing idempotent attachChild on same parent...');
|
||||||
|
const idempotentResult = await store.article.attachChild(googleAuthId, parent.sku, {
|
||||||
|
sku: child2.article.sku,
|
||||||
|
value: 'Blau',
|
||||||
|
});
|
||||||
|
assert.equal(idempotentResult.article.sku, child2.article.sku);
|
||||||
|
|
||||||
console.log('✅ ALL VARIANT FLOW TESTS PASSED');
|
console.log('✅ ALL VARIANT FLOW TESTS PASSED');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
14
src/index.js
14
src/index.js
@@ -5,6 +5,8 @@ import companyCreate from './store/company/create.js';
|
|||||||
import companyUpdate from './store/company/update.js';
|
import companyUpdate from './store/company/update.js';
|
||||||
import companyDelete from './store/company/delete.js';
|
import companyDelete from './store/company/delete.js';
|
||||||
import companyList from './store/company/list.js';
|
import companyList from './store/company/list.js';
|
||||||
|
import companyAddHistory from './store/company/addHistory.js';
|
||||||
|
import companyGetHistory from './store/company/getHistory.js';
|
||||||
import categoryGetTreeForCompany from './store/category/getTreeForCompany.js';
|
import categoryGetTreeForCompany from './store/category/getTreeForCompany.js';
|
||||||
import categoryGetCategoryArticles from './store/category/getCategoryArticles.js';
|
import categoryGetCategoryArticles from './store/category/getCategoryArticles.js';
|
||||||
import categoryCreateUnderParent from './store/category/createUnderParent.js';
|
import categoryCreateUnderParent from './store/category/createUnderParent.js';
|
||||||
@@ -30,9 +32,10 @@ import articleSetBrandIdBySku from './store/article/setBrandIdBySku.js';
|
|||||||
import articleSetWeightBySku from './store/article/setWeightBySku.js';
|
import articleSetWeightBySku from './store/article/setWeightBySku.js';
|
||||||
import articleSetTrackStockBySku from './store/article/setTrackStockBySku.js';
|
import articleSetTrackStockBySku from './store/article/setTrackStockBySku.js';
|
||||||
import articleSetCategoryIdsBySku from './store/article/setCategoryIdsBySku.js';
|
import articleSetCategoryIdsBySku from './store/article/setCategoryIdsBySku.js';
|
||||||
import articleSetFatherBySku from './store/article/setFatherBySku.js';
|
import articleSetParentBySku from './store/article/setParentBySku.js';
|
||||||
import articleToggleFather from './store/article/toggleFather.js';
|
import articleToggleParent from './store/article/toggleParent.js';
|
||||||
import articleAddChild from './store/article/addChild.js';
|
import articleAddChild from './store/article/addChild.js';
|
||||||
|
import articleAttachChild from './store/article/attachChild.js';
|
||||||
import articleRemoveChild from './store/article/removeChild.js';
|
import articleRemoveChild from './store/article/removeChild.js';
|
||||||
import articleListChildren from './store/article/listChildren.js';
|
import articleListChildren from './store/article/listChildren.js';
|
||||||
import articleImportRows from './store/article/importRows.js';
|
import articleImportRows from './store/article/importRows.js';
|
||||||
@@ -234,6 +237,8 @@ export default class LibDashStore {
|
|||||||
update: companyUpdate(provider),
|
update: companyUpdate(provider),
|
||||||
delete: companyDelete(provider),
|
delete: companyDelete(provider),
|
||||||
list: companyList(provider),
|
list: companyList(provider),
|
||||||
|
addHistory: companyAddHistory(provider),
|
||||||
|
getHistory: companyGetHistory(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.category = {
|
this.category = {
|
||||||
@@ -262,9 +267,10 @@ export default class LibDashStore {
|
|||||||
setWeightBySku: articleSetWeightBySku(provider),
|
setWeightBySku: articleSetWeightBySku(provider),
|
||||||
setTrackStockBySku: articleSetTrackStockBySku(provider),
|
setTrackStockBySku: articleSetTrackStockBySku(provider),
|
||||||
setCategoryIdsBySku: articleSetCategoryIdsBySku(provider),
|
setCategoryIdsBySku: articleSetCategoryIdsBySku(provider),
|
||||||
setFatherBySku: articleSetFatherBySku(provider),
|
setParentBySku: articleSetParentBySku(provider),
|
||||||
toggleFather: articleToggleFather(provider),
|
toggleParent: articleToggleParent(provider),
|
||||||
addChild: articleAddChild(provider),
|
addChild: articleAddChild(provider),
|
||||||
|
attachChild: articleAttachChild(provider),
|
||||||
removeChild: articleRemoveChild(provider),
|
removeChild: articleRemoveChild(provider),
|
||||||
listChildren: articleListChildren(provider),
|
listChildren: articleListChildren(provider),
|
||||||
getArticlelist: articlelistGetItems(provider),
|
getArticlelist: articlelistGetItems(provider),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const TABLES = [
|
|||||||
'articleean',
|
'articleean',
|
||||||
'articlehistory',
|
'articlehistory',
|
||||||
'entityhistory',
|
'entityhistory',
|
||||||
|
'companyhistory',
|
||||||
'entityaddress',
|
'entityaddress',
|
||||||
'entitycontact',
|
'entitycontact',
|
||||||
'article',
|
'article',
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ OUTER APPLY (
|
|||||||
ORDER BY bp.sort ASC, bp.id ASC
|
ORDER BY bp.sort ASC, bp.id ASC
|
||||||
) first_brand_pic
|
) first_brand_pic
|
||||||
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
|
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 = `
|
export const ARTICLE_LIST_COLUMNS = `
|
||||||
@@ -32,6 +32,7 @@ export const ARTICLE_LIST_COLUMNS = `
|
|||||||
pr.net,
|
pr.net,
|
||||||
t.rate AS taxRate,
|
t.rate AS taxRate,
|
||||||
a.track_stock,
|
a.track_stock,
|
||||||
|
a.is_parent AS isParent,
|
||||||
CASE
|
CASE
|
||||||
WHEN a.optionslist_id IS NOT NULL THEN (
|
WHEN a.optionslist_id IS NOT NULL THEN (
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
@@ -51,10 +52,10 @@ export const ARTICLE_LIST_COLUMNS = `
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM article child
|
FROM article child
|
||||||
WHERE child.fatherarticle_id = a.id
|
WHERE child.parentarticle_id = a.id
|
||||||
) AS childCount,
|
) AS childCount,
|
||||||
a.fatherarticle_id AS fatherArticleId,
|
a.parentarticle_id AS parentArticleId,
|
||||||
father.sku AS fatherSku,
|
parent.sku AS parentSku,
|
||||||
pic.hash AS fullHash,
|
pic.hash AS fullHash,
|
||||||
pic.thumbw AS thumbW,
|
pic.thumbw AS thumbW,
|
||||||
pic.thumbh AS thumbH,
|
pic.thumbh AS thumbH,
|
||||||
@@ -74,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)
|
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 = `
|
export const GET_ID_BY_SKU = `
|
||||||
SELECT TOP 1 id AS articleId
|
SELECT TOP 1 id AS articleId
|
||||||
FROM article
|
FROM article
|
||||||
@@ -88,49 +89,49 @@ FROM articleean
|
|||||||
WHERE ean = @ean
|
WHERE ean = @ean
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/** Link an article to its father article and mark the father. */
|
/** Link an article to its parent article and mark the parent. */
|
||||||
export const UPDATE_FATHER_BY_SKU = `
|
export const UPDATE_PARENT_BY_SKU = `
|
||||||
UPDATE article
|
UPDATE article
|
||||||
SET fatherarticle_id = @fatherArticleId,
|
SET parentarticle_id = @parentArticleId,
|
||||||
fathersort = COALESCE(@fathersort, 0)
|
parentsort = COALESCE(@parentsort, 0)
|
||||||
WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)
|
WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SET_IS_FATHER = `
|
export const SET_IS_PARENT = `
|
||||||
UPDATE article
|
UPDATE article
|
||||||
SET is_father = 1
|
SET is_parent = 1
|
||||||
WHERE id = @articleId
|
WHERE id = @articleId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TOGGLE_FATHER = `
|
export const TOGGLE_PARENT = `
|
||||||
UPDATE article
|
UPDATE article
|
||||||
SET is_father = @isFather,
|
SET is_parent = @isParent,
|
||||||
fatherattributename_id = @fatherAttributeNameId
|
parentattributename_id = @parentAttributeNameId
|
||||||
WHERE id = @articleId
|
WHERE id = @articleId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CREATE_CHILD = `
|
export const CREATE_CHILD = `
|
||||||
INSERT INTO article (
|
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
|
OUTPUT INSERTED.id
|
||||||
VALUES (
|
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 = `
|
export const UNLINK_CHILD = `
|
||||||
UPDATE article
|
UPDATE article
|
||||||
SET fatherarticle_id = NULL,
|
SET parentarticle_id = NULL,
|
||||||
fathersort = 0
|
parentsort = 0
|
||||||
WHERE id = @articleId AND fatherarticle_id = @fatherArticleId
|
WHERE id = @articleId AND parentarticle_id = @parentArticleId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LIST_CHILDREN_BY_FATHER_ID = `
|
export const LIST_CHILDREN_BY_PARENT_ID = `
|
||||||
SELECT
|
SELECT
|
||||||
child.id,
|
child.id,
|
||||||
child.sku,
|
child.sku,
|
||||||
child.fathersort,
|
child.parentsort,
|
||||||
child.track_stock AS trackStock,
|
child.track_stock AS trackStock,
|
||||||
child.unitamount,
|
child.unitamount,
|
||||||
child.weight,
|
child.weight,
|
||||||
@@ -144,14 +145,14 @@ LEFT JOIN (
|
|||||||
articleattributevalue aav
|
articleattributevalue aav
|
||||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = @attributeId
|
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = @attributeId
|
||||||
) ON aav.article_id = child.id
|
) ON aav.article_id = child.id
|
||||||
WHERE child.fatherarticle_id = @fatherArticleId
|
WHERE child.parentarticle_id = @parentArticleId
|
||||||
ORDER BY child.fathersort ASC, child.id ASC
|
ORDER BY child.parentsort ASC, child.id ASC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SET_FATHER_SORT = `
|
export const SET_PARENT_SORT = `
|
||||||
UPDATE article
|
UPDATE article
|
||||||
SET fathersort = @fathersort
|
SET parentsort = @parentsort
|
||||||
WHERE id = @articleId AND fatherarticle_id = @fatherArticleId
|
WHERE id = @articleId AND parentarticle_id = @parentArticleId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/** SKU → article/category + company auth in one round-trip. */
|
/** SKU → article/category + company auth in one round-trip. */
|
||||||
@@ -483,19 +484,19 @@ SELECT
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM article child
|
FROM article child
|
||||||
WHERE child.fatherarticle_id = a.id
|
WHERE child.parentarticle_id = a.id
|
||||||
) AS childCount,
|
) AS childCount,
|
||||||
a.is_father AS isFather,
|
a.is_parent AS isParent,
|
||||||
a.fatherattributename_id AS fatherAttributeNameId,
|
a.parentattributename_id AS parentAttributeNameId,
|
||||||
fan.name AS fatherAttributeName,
|
fan.name AS parentAttributeName,
|
||||||
fan.exclusive AS fatherAttributeExclusive,
|
fan.exclusive AS parentAttributeExclusive,
|
||||||
a.fatherarticle_id AS fatherArticleId,
|
a.parentarticle_id AS parentArticleId,
|
||||||
fa.sku AS fatherSku,
|
fa.sku AS parentSku,
|
||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
child.id,
|
child.id,
|
||||||
child.sku,
|
child.sku,
|
||||||
child.fathersort,
|
child.parentsort,
|
||||||
at_child.name,
|
at_child.name,
|
||||||
av.id AS attributeValueId,
|
av.id AS attributeValueId,
|
||||||
av.value AS attributeValue
|
av.value AS attributeValue
|
||||||
@@ -503,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 articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
articleattributevalue aav
|
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
|
) ON aav.article_id = child.id
|
||||||
WHERE child.fatherarticle_id = a.id
|
WHERE child.parentarticle_id = a.id
|
||||||
ORDER BY child.fathersort ASC, child.id ASC
|
ORDER BY child.parentsort ASC, child.id ASC
|
||||||
FOR JSON PATH
|
FOR JSON PATH
|
||||||
) AS childrenJson,
|
) AS childrenJson,
|
||||||
(
|
(
|
||||||
@@ -636,8 +637,8 @@ SELECT
|
|||||||
ORDER BY bp2.sort ASC, bp2.id ASC
|
ORDER BY bp2.sort ASC, bp2.id ASC
|
||||||
) first_brand_pic
|
) first_brand_pic
|
||||||
LEFT JOIN pic bp ON bp.id = first_brand_pic.pic_id
|
LEFT JOIN pic bp ON bp.id = first_brand_pic.pic_id
|
||||||
LEFT JOIN article fa ON fa.id = a.fatherarticle_id
|
LEFT JOIN article fa ON fa.id = a.parentarticle_id
|
||||||
LEFT JOIN attributename fan ON fan.id = a.fatherattributename_id
|
LEFT JOIN attributename fan ON fan.id = a.parentattributename_id
|
||||||
WHERE a.id = auth.articleId
|
WHERE a.id = auth.articleId
|
||||||
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
|
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
|
||||||
) AS articleJson
|
) AS articleJson
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
export const GET_CHILD_COUNT = `
|
export const GET_CHILD_COUNT = `
|
||||||
SELECT COUNT(*) AS count
|
SELECT COUNT(*) AS count
|
||||||
FROM article
|
FROM article
|
||||||
WHERE fatherarticle_id = @articleId
|
WHERE parentarticle_id = @articleId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GET_CHILDREN_IDS = `
|
export const GET_CHILDREN_IDS = `
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM article
|
FROM article
|
||||||
WHERE fatherarticle_id = @id
|
WHERE parentarticle_id = @id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GET_LIST_IDS = `
|
export const GET_LIST_IDS = `
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ export const ARTICLE_DETAIL_COLUMNS = `
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM article child
|
FROM article child
|
||||||
WHERE child.fatherarticle_id = a.id
|
WHERE child.parentarticle_id = a.id
|
||||||
) AS childCount,
|
) AS childCount,
|
||||||
a.fatherarticle_id AS fatherArticleId,
|
a.parentarticle_id AS parentArticleId,
|
||||||
father.sku AS fatherSku
|
parent.sku AS parentSku
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GET_DETAIL_BY_SKU = `
|
export const GET_DETAIL_BY_SKU = `
|
||||||
@@ -59,7 +59,7 @@ OUTER APPLY (
|
|||||||
ORDER BY bp.sort ASC, bp.id ASC
|
ORDER BY bp.sort ASC, bp.id ASC
|
||||||
) first_brand_pic
|
) first_brand_pic
|
||||||
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
|
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)
|
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
|
ORDER BY bp.sort ASC, bp.id ASC
|
||||||
) first_brand_pic
|
) first_brand_pic
|
||||||
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
|
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
|
WHERE a.id = @articleId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ FROM categoryarticle ca
|
|||||||
INNER JOIN article a ON a.id = ca.article_id
|
INNER JOIN article a ON a.id = ca.article_id
|
||||||
${ARTICLE_LIST_JOINS}
|
${ARTICLE_LIST_JOINS}
|
||||||
WHERE ca.category_id = @categoryId
|
WHERE ca.category_id = @categoryId
|
||||||
AND a.fatherarticle_id IS NULL
|
AND a.parentarticle_id IS NULL
|
||||||
ORDER BY a.id
|
ORDER BY a.id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
19
src/providers/mssql/queries/companyhistory.js
Normal file
19
src/providers/mssql/queries/companyhistory.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const INSERT = `
|
||||||
|
INSERT INTO companyhistory (company_id, companyuser_id, [text])
|
||||||
|
OUTPUT INSERTED.id, INSERTED.companyuser_id AS companyuserId, INSERTED.[date], INSERTED.[text]
|
||||||
|
VALUES (@companyId, @companyuserId, @text)
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LIST_BY_COMPANY_ID = `
|
||||||
|
SELECT id, companyuser_id AS companyuserId, [date], [text]
|
||||||
|
FROM companyhistory
|
||||||
|
WHERE company_id = @companyId
|
||||||
|
ORDER BY [date] DESC, id DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LIST_BY_COMPANY_ID_LIMITED = `
|
||||||
|
SELECT TOP (@limit) id, companyuser_id AS companyuserId, [date], [text]
|
||||||
|
FROM companyhistory
|
||||||
|
WHERE company_id = @companyId
|
||||||
|
ORDER BY [date] DESC, id DESC
|
||||||
|
`;
|
||||||
@@ -109,10 +109,10 @@ VALUES (
|
|||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CHECK_ARTICLE_IS_FATHER = `
|
export const CHECK_ARTICLE_IS_PARENT = `
|
||||||
SELECT
|
SELECT
|
||||||
a.id,
|
a.id,
|
||||||
a.is_father AS isFather,
|
a.is_parent AS isParent,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
(SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
|
(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),
|
(SELECT MIN(at2.name) FROM articletext at2 WHERE at2.article_id = a.id),
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ SELECT TOP (@limit)
|
|||||||
SELECT TOP 1 p2.hash
|
SELECT TOP 1 p2.hash
|
||||||
FROM articlepic ap2
|
FROM articlepic ap2
|
||||||
INNER JOIN pic p2 ON p2.id = ap2.pic_id
|
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
|
ORDER BY ap2.sort ASC, ap2.id ASC
|
||||||
)
|
)
|
||||||
) AS imgHash,
|
) AS imgHash,
|
||||||
@@ -263,15 +263,15 @@ SELECT TOP (@limit)
|
|||||||
WHERE ae.article_id = a.id
|
WHERE ae.article_id = a.id
|
||||||
ORDER BY ae.id ASC
|
ORDER BY ae.id ASC
|
||||||
) AS barcode,
|
) AS barcode,
|
||||||
a.is_father AS isParent,
|
a.is_parent AS isParent,
|
||||||
a.fatherarticle_id AS parentArticleId,
|
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
|
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 articleattributevalue aav ON aav.article_id = a.id
|
||||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.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,
|
) AS variantName,
|
||||||
CASE
|
CASE
|
||||||
WHEN a.articlelist_id IS NOT NULL AND EXISTS (
|
WHEN a.articlelist_id IS NOT NULL AND EXISTS (
|
||||||
|
|||||||
@@ -323,10 +323,10 @@ BEGIN
|
|||||||
brand_id INT NULL,
|
brand_id INT NULL,
|
||||||
articlelist_id INT NULL,
|
articlelist_id INT NULL,
|
||||||
optionslist_id INT NULL,
|
optionslist_id INT NULL,
|
||||||
is_father BIT NOT NULL CONSTRAINT DF_article_is_father DEFAULT 0,
|
is_parent BIT NOT NULL CONSTRAINT DF_article_is_parent DEFAULT 0,
|
||||||
fatherarticle_id INT NULL,
|
parentarticle_id INT NULL,
|
||||||
fatherattributename_id INT NULL,
|
parentattributename_id INT NULL,
|
||||||
fathersort INT NOT NULL CONSTRAINT DF_article_fathersort DEFAULT 0,
|
parentsort INT NOT NULL CONSTRAINT DF_article_parentsort DEFAULT 0,
|
||||||
created_at DATETIME2 NOT NULL CONSTRAINT DF_article_created_at DEFAULT SYSUTCDATETIME(),
|
created_at DATETIME2 NOT NULL CONSTRAINT DF_article_created_at DEFAULT SYSUTCDATETIME(),
|
||||||
updated_at DATETIME2 NOT NULL CONSTRAINT DF_article_updated_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,
|
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_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_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_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_parentarticle FOREIGN KEY (parentarticle_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_parentattributename FOREIGN KEY (parentattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION,
|
||||||
CONSTRAINT UQ_article_company_sku UNIQUE (company_id, sku)
|
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.)
|
-- (SQL Server UNIQUE treats NULLs as equal, which would block multiple root articles.)
|
||||||
CREATE UNIQUE INDEX UQ_article_fatherarticle_fathersort
|
CREATE UNIQUE INDEX UQ_article_parentarticle_parentsort
|
||||||
ON article (fatherarticle_id, fathersort)
|
ON article (parentarticle_id, parentsort)
|
||||||
WHERE fatherarticle_id IS NOT NULL;
|
WHERE parentarticle_id IS NOT NULL;
|
||||||
END
|
END
|
||||||
ELSE
|
ELSE
|
||||||
BEGIN
|
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
|
BEGIN
|
||||||
ALTER TABLE article ADD fatherattributename_id INT NULL;
|
ALTER TABLE article ADD parentattributename_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 CONSTRAINT FK_article_parentattributename FOREIGN KEY (parentattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION;
|
||||||
END
|
END
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
|
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
|
||||||
@@ -374,6 +374,20 @@ BEGIN
|
|||||||
);
|
);
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
export const INIT_COMPANYHISTORY = `
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'companyhistory')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE companyhistory (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
company_id INT NOT NULL,
|
||||||
|
companyuser_id INT NULL,
|
||||||
|
[date] DATETIME2 NOT NULL CONSTRAINT DF_companyhistory_date DEFAULT SYSUTCDATETIME(),
|
||||||
|
[text] NVARCHAR(MAX) NOT NULL,
|
||||||
|
CONSTRAINT FK_companyhistory_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT FK_companyhistory_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
|
||||||
export const INIT_ARTICLETOMBSTONE = `
|
export const INIT_ARTICLETOMBSTONE = `
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articletombstone')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articletombstone')
|
||||||
@@ -1015,7 +1029,7 @@ BEGIN
|
|||||||
value NVARCHAR(MAX) NOT NULL,
|
value NVARCHAR(MAX) NOT NULL,
|
||||||
sort INT NOT NULL CONSTRAINT DF_articleproperty_sort DEFAULT 0,
|
sort INT NOT NULL CONSTRAINT DF_articleproperty_sort DEFAULT 0,
|
||||||
CONSTRAINT FK_articleproperty_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
CONSTRAINT FK_articleproperty_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||||
CONSTRAINT FK_articleproperty_property FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
|
CONSTRAINT FK_articleproperty_property FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE NO ACTION
|
||||||
);
|
);
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
@@ -1058,7 +1072,7 @@ BEGIN
|
|||||||
attributevalue_id INT NOT NULL,
|
attributevalue_id INT NOT NULL,
|
||||||
sort INT NOT NULL CONSTRAINT DF_articleattributevalue_sort DEFAULT 0,
|
sort INT NOT NULL CONSTRAINT DF_articleattributevalue_sort DEFAULT 0,
|
||||||
CONSTRAINT FK_articleattributevalue_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
CONSTRAINT FK_articleattributevalue_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||||
CONSTRAINT FK_articleattributevalue_attributevalue FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE CASCADE,
|
CONSTRAINT FK_articleattributevalue_attributevalue FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE NO ACTION,
|
||||||
CONSTRAINT UQ_articleattributevalue_article_attributevalue UNIQUE (article_id, attributevalue_id)
|
CONSTRAINT UQ_articleattributevalue_article_attributevalue UNIQUE (article_id, attributevalue_id)
|
||||||
);
|
);
|
||||||
END
|
END
|
||||||
@@ -1074,7 +1088,7 @@ BEGIN
|
|||||||
value NVARCHAR(MAX) NOT NULL,
|
value NVARCHAR(MAX) NOT NULL,
|
||||||
sort INT NOT NULL CONSTRAINT DF_categoryproperty_sort DEFAULT 0,
|
sort INT NOT NULL CONSTRAINT DF_categoryproperty_sort DEFAULT 0,
|
||||||
CONSTRAINT FK_categoryproperty_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
CONSTRAINT FK_categoryproperty_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
||||||
CONSTRAINT FK_categoryproperty_property FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
|
CONSTRAINT FK_categoryproperty_property FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE NO ACTION
|
||||||
);
|
);
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
@@ -1147,6 +1161,7 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_ARTICLEHISTORY,
|
INIT_ARTICLEHISTORY,
|
||||||
INIT_ARTICLETOMBSTONE,
|
INIT_ARTICLETOMBSTONE,
|
||||||
INIT_ENTITYHISTORY,
|
INIT_ENTITYHISTORY,
|
||||||
|
INIT_COMPANYHISTORY,
|
||||||
INIT_SYNCPOS,
|
INIT_SYNCPOS,
|
||||||
INIT_COMPANYSETTING,
|
INIT_COMPANYSETTING,
|
||||||
INIT_POSTERMINAL,
|
INIT_POSTERMINAL,
|
||||||
|
|||||||
@@ -434,9 +434,9 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
|
|||||||
`ALTER TABLE article ADD COLUMN track_stock INTEGER NOT NULL DEFAULT 1`,
|
`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(
|
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`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ export const {
|
|||||||
DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS,
|
DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS,
|
||||||
GET_BRAND_BY_COMPANY_AND_ID,
|
GET_BRAND_BY_COMPANY_AND_ID,
|
||||||
GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU,
|
GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU,
|
||||||
LIST_CHILDREN_BY_FATHER_ID,
|
LIST_CHILDREN_BY_PARENT_ID,
|
||||||
SET_FATHER_SORT,
|
SET_PARENT_SORT,
|
||||||
SET_IS_FATHER, TOGGLE_FATHER, UNLINK_CHILD, UPDATE_BRAND, UPDATE_FATHER_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
|
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,
|
UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ LEFT JOIN pic brand_pic ON brand_pic.id = (
|
|||||||
SELECT bp.pic_id FROM brandpic bp
|
SELECT bp.pic_id FROM brandpic bp
|
||||||
WHERE bp.brand_id = b.id ORDER BY bp.sort, bp.id LIMIT 1
|
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 = `
|
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
|
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
|
WHERE ala.articlelist_id = a.articlelist_id
|
||||||
ORDER BY ala.sort ASC, ala.id ASC)),
|
ORDER BY ala.sort ASC, ala.id ASC)),
|
||||||
'childCount', (SELECT COUNT(*) FROM article WHERE fatherarticle_id = a.id),
|
'childCount', (SELECT COUNT(*) FROM article WHERE parentarticle_id = a.id),
|
||||||
'isFather', a.is_father,
|
'isParent', a.is_parent,
|
||||||
'fatherAttributeNameId', a.fatherattributename_id,
|
'parentAttributeNameId', a.parentattributename_id,
|
||||||
'fatherAttributeName', fan.name,
|
'parentAttributeName', fan.name,
|
||||||
'fatherAttributeExclusive', fan.exclusive,
|
'parentAttributeExclusive', fan.exclusive,
|
||||||
'fatherArticleId', a.fatherarticle_id, 'fatherSku', father.sku,
|
'parentArticleId', a.parentarticle_id, 'parentSku', parent.sku,
|
||||||
'childrenJson', json((SELECT COALESCE(json_group_array(json_object(
|
'childrenJson', json((SELECT COALESCE(json_group_array(json_object(
|
||||||
'id', child.id,
|
'id', child.id,
|
||||||
'sku', child.sku,
|
'sku', child.sku,
|
||||||
'fathersort', child.fathersort,
|
'parentsort', child.parentsort,
|
||||||
'name', at_child.name,
|
'name', at_child.name,
|
||||||
'attributeValueId', av.id,
|
'attributeValueId', av.id,
|
||||||
'attributeValue', av.value
|
'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 articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
articleattributevalue aav
|
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
|
) ON aav.article_id = child.id
|
||||||
WHERE child.fatherarticle_id = a.id
|
WHERE child.parentarticle_id = a.id
|
||||||
ORDER BY child.fathersort ASC, child.id ASC)),
|
ORDER BY child.parentsort ASC, child.id ASC)),
|
||||||
'categoryIdsJson', json(${JSON_CATEGORIES}),
|
'categoryIdsJson', json(${JSON_CATEGORIES}),
|
||||||
'photosJson', json((SELECT COALESCE(json_group_array(json_object(
|
'photosJson', json((SELECT COALESCE(json_group_array(json_object(
|
||||||
'sort', sort, 'fullHash', hash, 'fullW', w, 'fullH', h,
|
'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 = (
|
LEFT JOIN pic bp ON bp.id = (
|
||||||
SELECT pic_id FROM brandpic WHERE brand_id = b.id ORDER BY sort, id LIMIT 1
|
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 article parent ON parent.id = a.parentarticle_id
|
||||||
LEFT JOIN attributename fan ON fan.id = a.fatherattributename_id
|
LEFT JOIN attributename fan ON fan.id = a.parentattributename_id
|
||||||
`;
|
`;
|
||||||
@@ -19,7 +19,7 @@ LEFT JOIN pic brand_pic ON brand_pic.id = (
|
|||||||
WHERE bp.brand_id = b.id
|
WHERE bp.brand_id = b.id
|
||||||
ORDER BY bp.sort, bp.id LIMIT 1
|
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 = `
|
export const GET_DETAIL_BY_SKU = `
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ FROM categoryarticle ca
|
|||||||
INNER JOIN article a ON a.id = ca.article_id
|
INNER JOIN article a ON a.id = ca.article_id
|
||||||
${ARTICLE_LIST_JOINS}
|
${ARTICLE_LIST_JOINS}
|
||||||
WHERE ca.category_id = @categoryId
|
WHERE ca.category_id = @categoryId
|
||||||
AND a.fatherarticle_id IS NULL
|
AND a.parentarticle_id IS NULL
|
||||||
ORDER BY a.id
|
ORDER BY a.id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
13
src/providers/sqlite3/queries/companyhistory.js
Normal file
13
src/providers/sqlite3/queries/companyhistory.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as source from '../../mssql/queries/companyhistory.js';
|
||||||
|
import { translateQueries } from './translate.js';
|
||||||
|
|
||||||
|
const queries = translateQueries(source, 'companyhistory');
|
||||||
|
export const { INSERT, LIST_BY_COMPANY_ID } = queries;
|
||||||
|
|
||||||
|
export const LIST_BY_COMPANY_ID_LIMITED = `
|
||||||
|
SELECT id, companyuser_id AS companyuserId, date, text
|
||||||
|
FROM companyhistory
|
||||||
|
WHERE company_id = @companyId
|
||||||
|
ORDER BY date DESC, id DESC
|
||||||
|
LIMIT @limit
|
||||||
|
`;
|
||||||
@@ -3,7 +3,7 @@ import { translateQueries } from './translate.js';
|
|||||||
|
|
||||||
const queries = translateQueries(source, 'manifestDoc');
|
const queries = translateQueries(source, 'manifestDoc');
|
||||||
export const {
|
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,
|
DELETE_MANIFEST, GET_MANIFEST_HEADER, GET_MANIFEST_LINES, LIST_MANIFESTS,
|
||||||
ORDER_TYPE, QUOTE_TYPE, UPDATE_MANIFEST,
|
ORDER_TYPE, QUOTE_TYPE, UPDATE_MANIFEST,
|
||||||
} = queries;
|
} = queries;
|
||||||
@@ -78,7 +78,7 @@ SELECT
|
|||||||
SELECT p2.hash
|
SELECT p2.hash
|
||||||
FROM articlepic ap2
|
FROM articlepic ap2
|
||||||
INNER JOIN pic p2 ON p2.id = ap2.pic_id
|
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
|
ORDER BY ap2.sort ASC, ap2.id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
@@ -90,15 +90,15 @@ SELECT
|
|||||||
ORDER BY ae.id ASC
|
ORDER BY ae.id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
) AS barcode,
|
) AS barcode,
|
||||||
a.is_father AS isParent,
|
a.is_parent AS isParent,
|
||||||
a.fatherarticle_id AS parentArticleId,
|
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
|
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 articleattributevalue aav ON aav.article_id = a.id
|
||||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.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
|
LIMIT 1
|
||||||
) AS variantName,
|
) AS variantName,
|
||||||
CASE
|
CASE
|
||||||
|
|||||||
@@ -222,10 +222,10 @@ CREATE TABLE IF NOT EXISTS article (
|
|||||||
brand_id INTEGER,
|
brand_id INTEGER,
|
||||||
articlelist_id INTEGER,
|
articlelist_id INTEGER,
|
||||||
optionslist_id INTEGER,
|
optionslist_id INTEGER,
|
||||||
is_father INTEGER NOT NULL DEFAULT 0,
|
is_parent INTEGER NOT NULL DEFAULT 0,
|
||||||
fatherarticle_id INTEGER,
|
parentarticle_id INTEGER,
|
||||||
fathersort INTEGER NOT NULL DEFAULT 0,
|
parentsort INTEGER NOT NULL DEFAULT 0,
|
||||||
fatherattributename_id INTEGER,
|
parentattributename_id INTEGER,
|
||||||
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||||
updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||||
UNIQUE (company_id, sku),
|
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 (brand_id) REFERENCES brand(id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (articlelist_id) REFERENCES articlelist(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 (optionslist_id) REFERENCES optionslist(id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (fatherarticle_id) REFERENCES article(id) ON DELETE NO ACTION,
|
FOREIGN KEY (parentarticle_id) REFERENCES article(id) ON DELETE NO ACTION,
|
||||||
FOREIGN KEY (fatherattributename_id) REFERENCES attributename(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
|
CREATE UNIQUE INDEX IF NOT EXISTS UQ_article_parentarticle_parentsort
|
||||||
ON article(fatherarticle_id, fathersort)
|
ON article(parentarticle_id, parentsort)
|
||||||
WHERE fatherarticle_id IS NOT NULL;
|
WHERE parentarticle_id IS NOT NULL;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const INIT_ARTICLEEAN = `
|
export const INIT_ARTICLEEAN = `
|
||||||
@@ -536,6 +536,17 @@ CREATE TABLE IF NOT EXISTS articlehistory (
|
|||||||
FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL
|
FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
export const INIT_COMPANYHISTORY = `
|
||||||
|
CREATE TABLE IF NOT EXISTS companyhistory (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
company_id INTEGER NOT NULL,
|
||||||
|
companyuser_id INTEGER,
|
||||||
|
"date" TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
export const INIT_ARTICLETOMBSTONE = `
|
export const INIT_ARTICLETOMBSTONE = `
|
||||||
CREATE TABLE IF NOT EXISTS articletombstone (
|
CREATE TABLE IF NOT EXISTS articletombstone (
|
||||||
@@ -705,7 +716,7 @@ CREATE TABLE IF NOT EXISTS articleproperty (
|
|||||||
value TEXT NOT NULL,
|
value TEXT NOT NULL,
|
||||||
sort INTEGER NOT NULL DEFAULT 0,
|
sort INTEGER NOT NULL DEFAULT 0,
|
||||||
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
|
FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE NO ACTION
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -739,7 +750,7 @@ CREATE TABLE IF NOT EXISTS articleattributevalue (
|
|||||||
attributevalue_id INTEGER NOT NULL,
|
attributevalue_id INTEGER NOT NULL,
|
||||||
sort INTEGER NOT NULL DEFAULT 0,
|
sort INTEGER NOT NULL DEFAULT 0,
|
||||||
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE CASCADE,
|
FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE NO ACTION,
|
||||||
UNIQUE (article_id, attributevalue_id)
|
UNIQUE (article_id, attributevalue_id)
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
@@ -752,7 +763,7 @@ CREATE TABLE IF NOT EXISTS categoryproperty (
|
|||||||
value TEXT NOT NULL,
|
value TEXT NOT NULL,
|
||||||
sort INTEGER NOT NULL DEFAULT 0,
|
sort INTEGER NOT NULL DEFAULT 0,
|
||||||
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
|
FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE NO ACTION
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -818,6 +829,7 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_ARTICLEHISTORY,
|
INIT_ARTICLEHISTORY,
|
||||||
INIT_ARTICLETOMBSTONE,
|
INIT_ARTICLETOMBSTONE,
|
||||||
INIT_ENTITYHISTORY,
|
INIT_ENTITYHISTORY,
|
||||||
|
INIT_COMPANYHISTORY,
|
||||||
INIT_SYNCPOS,
|
INIT_SYNCPOS,
|
||||||
INIT_COMPANYSETTING,
|
INIT_COMPANYSETTING,
|
||||||
INIT_POSTERMINAL,
|
INIT_POSTERMINAL,
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import resolveArticleBySku from './resolveArticleBySku.js';
|
|||||||
import getBySku from './getBySku.js';
|
import getBySku from './getBySku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||||
|
import attachChild from './attachChild.js';
|
||||||
|
|
||||||
export default function addChild(provider) {
|
export default function addChild(provider) {
|
||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = getBySku(provider);
|
const load = getBySku(provider);
|
||||||
const resolveSlug = resolveUniqueSlug(provider);
|
const resolveSlug = resolveUniqueSlug(provider);
|
||||||
|
const attach = attachChild(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
@@ -32,47 +34,56 @@ export default function addChild(provider) {
|
|||||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a child article to a father article.
|
* 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 {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 {object} options
|
||||||
* @param {number} [options.attributeValueId] - Existing attribute value ID for father's attribute.
|
* @param {boolean} [options.attach=false] - If true, attaches an existing article instead of creating.
|
||||||
* @param {string} [options.value] - String value to find or create under father's attribute.
|
* @param {number} [options.attributeValueId] - Existing attribute value ID for parent's attribute.
|
||||||
* @param {string} [options.name] - Custom name for child article (defaults to father name + value).
|
* @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.unitamount] - Optional unitamount override.
|
||||||
* @param {number} [options.weight] - Optional weight override.
|
* @param {number} [options.weight] - Optional weight override.
|
||||||
* @param {boolean|number} [options.track_stock] - Optional track_stock override.
|
* @param {boolean|number} [options.track_stock] - Optional track_stock override.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<object>} The created child article detail.
|
* @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, parentSku, options, tx);
|
||||||
|
}
|
||||||
|
|
||||||
const run = async (runner) => {
|
const run = async (runner) => {
|
||||||
const { articleId: fatherId, companyId, languageId, companyuserId } = await resolve(
|
if (options.sku || options.childSku) {
|
||||||
|
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: parentId, companyId, languageId, companyuserId } = await resolve(
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
fatherSku,
|
parentSku,
|
||||||
runner,
|
runner,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch father article fields
|
// Fetch parent article fields
|
||||||
const fatherRes = await runner.query(
|
const parentRes = 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',
|
'SELECT id, sku, company_id, unit_id, unitamount, weight, track_stock, brand_id, is_parent, parentarticle_id, parentattributename_id FROM article WHERE id = @parentId',
|
||||||
{ fatherId },
|
{ parentId },
|
||||||
);
|
);
|
||||||
const father = fatherRes.rows[0];
|
const parent = parentRes.rows[0];
|
||||||
if (!father) {
|
if (!parent) {
|
||||||
throw new Error('Father article not found');
|
throw new Error('Parent article not found');
|
||||||
}
|
}
|
||||||
if (!father.is_father || !father.fatherattributename_id) {
|
if (!parent.is_parent || !parent.parentattributename_id) {
|
||||||
throw new Error('Article is not a father article');
|
throw new Error('Article is not a parent article');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch father's text / name
|
// Fetch parent's text / name
|
||||||
const fatherTextRes = await runner.query(
|
const parentTextRes = await runner.query(
|
||||||
'SELECT name FROM articletext WHERE article_id = @fatherId AND language_id = @languageId',
|
'SELECT name FROM articletext WHERE article_id = @parentId AND language_id = @languageId',
|
||||||
{ fatherId, languageId },
|
{ parentId, languageId },
|
||||||
);
|
);
|
||||||
const fatherName = fatherTextRes.rows[0]?.name || father.sku;
|
const parentName = parentTextRes.rows[0]?.name || parent.sku;
|
||||||
|
|
||||||
// Determine attribute value
|
// Determine attribute value
|
||||||
let finalValueId = null;
|
let finalValueId = null;
|
||||||
@@ -87,15 +98,15 @@ export default function addChild(provider) {
|
|||||||
companyId,
|
companyId,
|
||||||
});
|
});
|
||||||
const row = valRes.rows[0];
|
const row = valRes.rows[0];
|
||||||
if (!row || row.attributeId !== father.fatherattributename_id) {
|
if (!row || row.attributeId !== parent.parentattributename_id) {
|
||||||
throw new Error('Attribute value not found or does not match father attribute');
|
throw new Error('Attribute value not found or does not match parent attribute');
|
||||||
}
|
}
|
||||||
finalValueId = row.id;
|
finalValueId = row.id;
|
||||||
finalValue = row.value;
|
finalValue = row.value;
|
||||||
} else if (typeof options.value === 'string' && options.value.trim()) {
|
} else if (typeof options.value === 'string' && options.value.trim()) {
|
||||||
const valStr = options.value.trim();
|
const valStr = options.value.trim();
|
||||||
const existingValRes = await runner.query(attrValQueries.GET_BY_ATTRIBUTE_AND_VALUE, {
|
const existingValRes = await runner.query(attrValQueries.GET_BY_ATTRIBUTE_AND_VALUE, {
|
||||||
attributeId: father.fatherattributename_id,
|
attributeId: parent.parentattributename_id,
|
||||||
value: valStr,
|
value: valStr,
|
||||||
});
|
});
|
||||||
if (existingValRes.rows[0]) {
|
if (existingValRes.rows[0]) {
|
||||||
@@ -103,11 +114,11 @@ export default function addChild(provider) {
|
|||||||
finalValue = existingValRes.rows[0].value;
|
finalValue = existingValRes.rows[0].value;
|
||||||
} else {
|
} else {
|
||||||
const sortRes = await runner.query(attrValQueries.GET_NEXT_SORT, {
|
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 sort = sortRes.rows[0]?.sort ?? 0;
|
||||||
const createdValRes = await runner.query(attrValQueries.CREATE, {
|
const createdValRes = await runner.query(attrValQueries.CREATE, {
|
||||||
attributeId: father.fatherattributename_id,
|
attributeId: parent.parentattributename_id,
|
||||||
value: valStr,
|
value: valStr,
|
||||||
sort,
|
sort,
|
||||||
});
|
});
|
||||||
@@ -118,88 +129,74 @@ export default function addChild(provider) {
|
|||||||
throw new Error('Attribute value is required for child article');
|
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(
|
const existingVariantRes = await runner.query(
|
||||||
`SELECT a.id
|
`SELECT a.id
|
||||||
FROM article a
|
FROM article a
|
||||||
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
|
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`,
|
AND aav.attributevalue_id = @finalValueId`,
|
||||||
{ fatherId, finalValueId },
|
{ parentId, finalValueId },
|
||||||
);
|
);
|
||||||
if (existingVariantRes.rows.length > 0) {
|
if (existingVariantRes.rows.length > 0) {
|
||||||
throw new Error('Variant with this attribute value already exists');
|
throw new Error('Variant with this attribute value already exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute child SKU:
|
// Compute child SKU using next free index: parentSku-1, parentSku-2, ...
|
||||||
// If options.sku is provided, use it.
|
const existingSkusRes = await runner.query(
|
||||||
// Otherwise, derive using next free index: fatherSku-1, fatherSku-2, ...
|
'SELECT sku FROM article WHERE company_id = @companyId AND sku LIKE @prefix',
|
||||||
let childSku = (typeof options.sku === 'string' && options.sku.trim()) || null;
|
{ companyId, prefix: `${parent.sku}-%` },
|
||||||
if (!childSku) {
|
);
|
||||||
const existingSkusRes = await runner.query(
|
const usedIndices = new Set();
|
||||||
'SELECT sku FROM article WHERE company_id = @companyId AND sku LIKE @prefix',
|
const prefix = `${parent.sku}-`;
|
||||||
{ companyId, prefix: `${father.sku}-%` },
|
for (const row of existingSkusRes.rows) {
|
||||||
);
|
if (row.sku && row.sku.startsWith(prefix)) {
|
||||||
const usedIndices = new Set();
|
const numPart = row.sku.slice(prefix.length);
|
||||||
const prefix = `${father.sku}-`;
|
if (/^\d+$/.test(numPart)) {
|
||||||
for (const row of existingSkusRes.rows) {
|
usedIndices.add(parseInt(numPart, 10));
|
||||||
if (row.sku && row.sku.startsWith(prefix)) {
|
|
||||||
const numPart = row.sku.slice(prefix.length);
|
|
||||||
if (/^\d+$/.test(numPart)) {
|
|
||||||
usedIndices.add(parseInt(numPart, 10));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let nextIndex = 1;
|
|
||||||
while (usedIndices.has(nextIndex)) {
|
|
||||||
nextIndex++;
|
|
||||||
}
|
|
||||||
childSku = `${father.sku}-${nextIndex}`;
|
|
||||||
}
|
}
|
||||||
|
let nextIndex = 1;
|
||||||
// Check if child SKU already exists (e.g. orphan holding SKU or custom SKU)
|
while (usedIndices.has(nextIndex)) {
|
||||||
const existingChildRes = await runner.query(
|
nextIndex++;
|
||||||
'SELECT id FROM article WHERE sku = @childSku AND company_id = @companyId',
|
|
||||||
{ childSku, companyId },
|
|
||||||
);
|
|
||||||
if (existingChildRes.rows[0]) {
|
|
||||||
throw new Error(`Child article "${childSku}" already exists`);
|
|
||||||
}
|
}
|
||||||
|
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(
|
const existingSortsRes = await runner.query(
|
||||||
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
|
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
|
||||||
{ fatherId },
|
{ parentId },
|
||||||
);
|
);
|
||||||
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
|
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
|
||||||
let fathersort = 0;
|
let parentsort = 0;
|
||||||
while (usedSorts.has(fathersort)) {
|
while (usedSorts.has(parentsort)) {
|
||||||
fathersort++;
|
parentsort++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create child article
|
// Auto-create child article
|
||||||
const rawTrack = options.track_stock ?? options.trackStock;
|
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, {
|
const childRes = await runner.query(queries.CREATE_CHILD, {
|
||||||
companyId,
|
companyId,
|
||||||
sku: childSku,
|
sku: childSku,
|
||||||
unitId: father.unit_id,
|
unitId: parent.unit_id,
|
||||||
unitamount: options.unitamount ?? father.unitamount,
|
unitamount: options.unitamount ?? parent.unitamount,
|
||||||
weight: options.weight ?? father.weight,
|
weight: options.weight ?? parent.weight,
|
||||||
trackStock: finalTrackStock,
|
trackStock: finalTrackStock,
|
||||||
brandId: father.brand_id,
|
brandId: parent.brand_id,
|
||||||
fatherArticleId: fatherId,
|
parentArticleId: parentId,
|
||||||
fathersort,
|
parentsort,
|
||||||
});
|
});
|
||||||
const childId = childRes.rows[0]?.id ?? childRes.insertId;
|
const childId = childRes.rows[0]?.id ?? childRes.insertId;
|
||||||
|
|
||||||
// Copy existing attribute values from father (ancestor attributes that father has)
|
// Copy existing attribute values from parent (ancestor attributes that parent has)
|
||||||
const fatherAttrsRes = await runner.query(
|
const parentAttrsRes = await runner.query(
|
||||||
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @fatherId',
|
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @parentId',
|
||||||
{ fatherId },
|
{ parentId },
|
||||||
);
|
);
|
||||||
for (const faRow of fatherAttrsRes.rows) {
|
for (const faRow of parentAttrsRes.rows) {
|
||||||
if (faRow.attributevalue_id !== finalValueId) {
|
if (faRow.attributevalue_id !== finalValueId) {
|
||||||
await runner.query(articleAttrValQueries.INSERT_ROW, {
|
await runner.query(articleAttrValQueries.INSERT_ROW, {
|
||||||
articleId: childId,
|
articleId: childId,
|
||||||
@@ -216,10 +213,10 @@ export default function addChild(provider) {
|
|||||||
sort: 0,
|
sort: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy father's categories
|
// Copy parent's categories
|
||||||
const catRes = await runner.query(
|
const catRes = await runner.query(
|
||||||
'SELECT category_id FROM categoryarticle WHERE article_id = @fatherId',
|
'SELECT category_id FROM categoryarticle WHERE article_id = @parentId',
|
||||||
{ fatherId },
|
{ parentId },
|
||||||
);
|
);
|
||||||
for (const catRow of catRes.rows) {
|
for (const catRow of catRes.rows) {
|
||||||
await runner.query(addCategoryArticleSql, {
|
await runner.query(addCategoryArticleSql, {
|
||||||
@@ -229,7 +226,7 @@ export default function addChild(provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create article text
|
// Create article text
|
||||||
const childName = options.name?.trim() || `${fatherName} - ${finalValue}`;
|
const childName = options.name?.trim() || `${parentName} - ${finalValue}`;
|
||||||
const childSlug = await resolveSlug(runner, {
|
const childSlug = await resolveSlug(runner, {
|
||||||
companyId,
|
companyId,
|
||||||
languageId,
|
languageId,
|
||||||
@@ -246,12 +243,12 @@ export default function addChild(provider) {
|
|||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: childId,
|
articleId: childId,
|
||||||
companyuserId,
|
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, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: fatherId,
|
articleId: parentId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
|
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
|
||||||
});
|
});
|
||||||
|
|||||||
321
src/store/article/attachChild.js
Normal file
321
src/store/article/attachChild.js
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||||
|
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||||
|
import * as mssqlAttrVal from '../../providers/mssql/queries/attributevalue.js';
|
||||||
|
import * as sqliteAttrVal from '../../providers/sqlite3/queries/attributevalue.js';
|
||||||
|
import * as mssqlArticleAttrVal from '../../providers/mssql/queries/articleattributevalue.js';
|
||||||
|
import * as sqliteArticleAttrVal from '../../providers/sqlite3/queries/articleattributevalue.js';
|
||||||
|
import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
|
||||||
|
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
|
||||||
|
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||||
|
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||||
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import getBySku from './getBySku.js';
|
||||||
|
import listChildren from './listChildren.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function attachChild(provider) {
|
||||||
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const load = getBySku(provider);
|
||||||
|
const list = listChildren(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
|
const attrValQueries = isSqlite ? sqliteAttrVal : mssqlAttrVal;
|
||||||
|
const articleAttrValQueries = isSqlite ? sqliteArticleAttrVal : mssqlArticleAttrVal;
|
||||||
|
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
||||||
|
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches an existing article as a child to a parent article.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @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 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, parentSku, options = {}, tx) => {
|
||||||
|
const run = async (runner) => {
|
||||||
|
const childSku = (typeof options.sku === 'string' && options.sku.trim())
|
||||||
|
|| (typeof options.childSku === 'string' && options.childSku.trim())
|
||||||
|
|| null;
|
||||||
|
|
||||||
|
if (!childSku) {
|
||||||
|
throw new Error('sku is required to attach an existing article');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { articleId: parentId, companyId, languageId, companyuserId } = await resolve(
|
||||||
|
googleAuthId,
|
||||||
|
parentSku,
|
||||||
|
runner,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 parent = parentRes.rows[0];
|
||||||
|
if (!parent) {
|
||||||
|
throw new Error('Parent article not found');
|
||||||
|
}
|
||||||
|
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, 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 === parentId) {
|
||||||
|
throw new Error('An article cannot be a child of itself');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine attribute value
|
||||||
|
let finalValueId = null;
|
||||||
|
let finalValue = null;
|
||||||
|
|
||||||
|
if (options.attributeValueId != null) {
|
||||||
|
if (typeof options.attributeValueId !== 'number' || !Number.isInteger(options.attributeValueId) || options.attributeValueId <= 0) {
|
||||||
|
throw new Error('Invalid attributeValueId');
|
||||||
|
}
|
||||||
|
const valRes = await runner.query(attrValQueries.GET_BY_ID_AND_COMPANY, {
|
||||||
|
id: options.attributeValueId,
|
||||||
|
companyId,
|
||||||
|
});
|
||||||
|
const row = valRes.rows[0];
|
||||||
|
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: parent.parentattributename_id,
|
||||||
|
value: valStr,
|
||||||
|
});
|
||||||
|
if (existingValRes.rows[0]) {
|
||||||
|
finalValueId = existingValRes.rows[0].id;
|
||||||
|
finalValue = existingValRes.rows[0].value;
|
||||||
|
} else {
|
||||||
|
const sortRes = await runner.query(attrValQueries.GET_NEXT_SORT, {
|
||||||
|
attributeId: parent.parentattributename_id,
|
||||||
|
});
|
||||||
|
const sort = sortRes.rows[0]?.sort ?? 0;
|
||||||
|
const createdValRes = await runner.query(attrValQueries.CREATE, {
|
||||||
|
attributeId: parent.parentattributename_id,
|
||||||
|
value: valStr,
|
||||||
|
sort,
|
||||||
|
});
|
||||||
|
finalValueId = createdValRes.rows[0]?.id ?? createdValRes.insertId;
|
||||||
|
finalValue = valStr;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Attribute value is required for child article');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAlreadyChildOfThisParent = child.parentarticle_id === parentId;
|
||||||
|
|
||||||
|
if (child.parentarticle_id != null && !isAlreadyChildOfThisParent && !options.reparent) {
|
||||||
|
throw new Error('Article already belongs to another parent');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.parentarticle_id = @parentId
|
||||||
|
AND a.id != @childId
|
||||||
|
AND aav.attributevalue_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 parent, unparent and recompact previous parent
|
||||||
|
if (child.parentarticle_id != null && !isAlreadyChildOfThisParent && options.reparent) {
|
||||||
|
const prevParentId = child.parentarticle_id;
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: prevParentId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Variante (${childSku}) getrennt (umgehängt zu "${parent.sku}")`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recompact previous parent's children
|
||||||
|
const prevRemainingRes = await runner.query(
|
||||||
|
'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.parentsort) !== i) {
|
||||||
|
await runner.query(queries.SET_PARENT_SORT, {
|
||||||
|
articleId: row.id,
|
||||||
|
parentArticleId: prevParentId,
|
||||||
|
parentsort: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign parentsort if not already child of this parent
|
||||||
|
let parentsort;
|
||||||
|
if (isAlreadyChildOfThisParent) {
|
||||||
|
const currentSortRes = await runner.query(
|
||||||
|
'SELECT parentsort FROM article WHERE id = @childId',
|
||||||
|
{ childId: child.id },
|
||||||
|
);
|
||||||
|
parentsort = Number(currentSortRes.rows[0]?.parentsort ?? 0);
|
||||||
|
} else {
|
||||||
|
const existingSortsRes = await runner.query(
|
||||||
|
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
|
||||||
|
{ parentId },
|
||||||
|
);
|
||||||
|
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
|
||||||
|
parentsort = 0;
|
||||||
|
while (usedSorts.has(parentsort)) {
|
||||||
|
parentsort++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link child to parent
|
||||||
|
await runner.query(queries.UPDATE_PARENT_BY_SKU, {
|
||||||
|
sku: childSku,
|
||||||
|
companyId,
|
||||||
|
parentArticleId: parentId,
|
||||||
|
parentsort,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply optional field overrides
|
||||||
|
if (options.unitamount != null) {
|
||||||
|
await runner.query('UPDATE article SET unitamount = @unitamount WHERE id = @childId', {
|
||||||
|
childId: child.id,
|
||||||
|
unitamount: options.unitamount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (options.weight != null) {
|
||||||
|
await runner.query(queries.UPDATE_WEIGHT, {
|
||||||
|
articleId: child.id,
|
||||||
|
weight: options.weight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const rawTrack = options.track_stock ?? options.trackStock;
|
||||||
|
if (rawTrack != null) {
|
||||||
|
await runner.query(queries.UPDATE_TRACK_STOCK, {
|
||||||
|
articleId: child.id,
|
||||||
|
trackStock: rawTrack ? 1 : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: parent.parentattributename_id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await runner.query(articleAttrValQueries.INSERT_ROW, {
|
||||||
|
articleId: child.id,
|
||||||
|
attributeValueId: finalValueId,
|
||||||
|
sort: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 parentAttrsRes.rows) {
|
||||||
|
if (faRow.attributevalue_id !== finalValueId) {
|
||||||
|
const hasAttrRes = await runner.query(
|
||||||
|
'SELECT 1 FROM articleattributevalue WHERE article_id = @childId AND attributevalue_id = @attrValId',
|
||||||
|
{ childId: child.id, attrValId: faRow.attributevalue_id },
|
||||||
|
);
|
||||||
|
if (hasAttrRes.rows.length === 0) {
|
||||||
|
await runner.query(articleAttrValQueries.INSERT_ROW, {
|
||||||
|
articleId: child.id,
|
||||||
|
attributeValueId: faRow.attributevalue_id,
|
||||||
|
sort: faRow.sort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy parent's categories (if missing on child)
|
||||||
|
const catRes = await runner.query(
|
||||||
|
'SELECT category_id FROM categoryarticle WHERE article_id = @parentId',
|
||||||
|
{ parentId },
|
||||||
|
);
|
||||||
|
for (const catRow of catRes.rows) {
|
||||||
|
const hasCatRes = await runner.query(
|
||||||
|
'SELECT 1 FROM categoryarticle WHERE article_id = @childId AND category_id = @categoryId',
|
||||||
|
{ childId: child.id, categoryId: catRow.category_id },
|
||||||
|
);
|
||||||
|
if (hasCatRes.rows.length === 0) {
|
||||||
|
await runner.query(addCategoryArticleSql, {
|
||||||
|
categoryId: catRow.category_id,
|
||||||
|
articleId: child.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch child name for history
|
||||||
|
const childTextRes = await runner.query(
|
||||||
|
'SELECT name FROM articletext WHERE article_id = @childId AND language_id = @languageId',
|
||||||
|
{ childId: child.id, languageId },
|
||||||
|
);
|
||||||
|
const childName = childTextRes.rows[0]?.name || childSku;
|
||||||
|
|
||||||
|
// History only if not already child
|
||||||
|
if (!isAlreadyChildOfThisParent) {
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: child.id,
|
||||||
|
companyuserId,
|
||||||
|
text: `Artikel "${childName}" (${childSku}) als Variante von "${parent.sku}" zugewiesen`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: parentId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await load(googleAuthId, childSku, runner);
|
||||||
|
const children = await list(googleAuthId, parentSku, runner);
|
||||||
|
|
||||||
|
return {
|
||||||
|
article: updated.article,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
return run(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(run);
|
||||||
|
}
|
||||||
|
return run(makeRunner(provider));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -69,11 +69,11 @@ export default function del(provider) {
|
|||||||
await deleteArticleAndLists(id);
|
await deleteArticleAndLists(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fatherRes = await runner.query(
|
const parentRes = await runner.query(
|
||||||
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
|
'SELECT parentarticle_id FROM article WHERE id = @articleId',
|
||||||
{ 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 });
|
const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
|
||||||
if (childCount.rows[0].count > 0) {
|
if (childCount.rows[0].count > 0) {
|
||||||
@@ -85,18 +85,18 @@ export default function del(provider) {
|
|||||||
await deleteArticleAndLists(articleId);
|
await deleteArticleAndLists(articleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fatherArticleId != null) {
|
if (parentArticleId != null) {
|
||||||
const remainingChildrenRes = await runner.query(
|
const remainingChildrenRes = await runner.query(
|
||||||
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherArticleId ORDER BY fathersort ASC, id ASC',
|
'SELECT id, parentsort FROM article WHERE parentarticle_id = @parentArticleId ORDER BY parentsort ASC, id ASC',
|
||||||
{ fatherArticleId },
|
{ parentArticleId },
|
||||||
);
|
);
|
||||||
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
|
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
|
||||||
const childRow = remainingChildrenRes.rows[i];
|
const childRow = remainingChildrenRes.rows[i];
|
||||||
if (Number(childRow.fathersort) !== i) {
|
if (Number(childRow.parentsort) !== i) {
|
||||||
await runner.query(articleQueries.SET_FATHER_SORT, {
|
await runner.query(articleQueries.SET_PARENT_SORT, {
|
||||||
articleId: childRow.id,
|
articleId: childRow.id,
|
||||||
fatherArticleId,
|
parentArticleId,
|
||||||
fathersort: i,
|
parentsort: i,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function getBySku(provider) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @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.
|
* @throws {Error} If the article is not found or the user is not authorized.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -118,16 +118,16 @@ export default function getBySku(provider) {
|
|||||||
sort: Number(item.sort ?? 0),
|
sort: Number(item.sort ?? 0),
|
||||||
})),
|
})),
|
||||||
childCount: a.childCount,
|
childCount: a.childCount,
|
||||||
isFather: Boolean(a.isFather),
|
isParent: Boolean(a.isParent),
|
||||||
fatherAttributeNameId: a.fatherAttributeNameId ?? null,
|
parentAttributeNameId: a.parentAttributeNameId ?? null,
|
||||||
fatherAttributeName: a.fatherAttributeName ?? null,
|
parentAttributeName: a.parentAttributeName ?? null,
|
||||||
fatherAttributeExclusive: a.fatherAttributeExclusive != null ? Boolean(a.fatherAttributeExclusive) : null,
|
parentAttributeExclusive: a.parentAttributeExclusive != null ? Boolean(a.parentAttributeExclusive) : null,
|
||||||
fatherArticleId: a.fatherArticleId,
|
parentArticleId: a.parentArticleId,
|
||||||
fatherSku: a.fatherSku,
|
parentSku: a.parentSku,
|
||||||
children: parseJsonArray(a.childrenJson).map((c) => ({
|
children: parseJsonArray(a.childrenJson).map((c) => ({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
sku: c.sku,
|
sku: c.sku,
|
||||||
fathersort: c.fathersort,
|
parentsort: c.parentsort,
|
||||||
name: c.name ?? null,
|
name: c.name ?? null,
|
||||||
attributeValueId: c.attributeValueId ?? null,
|
attributeValueId: c.attributeValueId ?? null,
|
||||||
attributeValue: c.attributeValue ?? null,
|
attributeValue: c.attributeValue ?? null,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function importRows(provider) {
|
|||||||
* Imports article rows, creating articles by their SKU and returning per-row
|
* Imports article rows, creating articles by their SKU and returning per-row
|
||||||
* import, skip and error outcomes.
|
* 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
|
* 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
|
* SKU already exists in the company (upsert mode is not applied), and an EAN
|
||||||
* is skipped when already used by another row.
|
* 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 {Array<object>} input.rows - The article rows to import.
|
||||||
* @param {string} input.rows[].sku - The article SKU.
|
* @param {string} input.rows[].sku - The article SKU.
|
||||||
* @param {string} input.rows[].name - The article name.
|
* @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 {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[].parentCategory] - The category breadcrumb path, e.g. `'Flowers | CBN'`.
|
||||||
* @param {string} [input.rows[].brand] - The brand name to upsert.
|
* @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 {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 {Array<string>|string} [input.rows[].eans] - The EANs to set.
|
||||||
* @param {boolean|number} [input.rows[].track_stock=true] - Whether inventory stock is tracked.
|
* @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 {number|string} [input.rows[].deposit] - The deposit (Pfand) value; currently ignored.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>} The import outcome.
|
* @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({
|
* const result = await store.article.importRows({
|
||||||
* googleAuthId: 42,
|
* googleAuthId: 42,
|
||||||
* rows: [
|
* 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 },
|
* { 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) {
|
for (const row of rows) {
|
||||||
const sku = String(row?.sku ?? '').trim();
|
const sku = String(row?.sku ?? '').trim();
|
||||||
const fatherSku =
|
const parentSku =
|
||||||
row?.fatherSku != null ? String(row.fatherSku).trim() : '';
|
row?.parentSku != null ? String(row.parentSku).trim() : '';
|
||||||
if (!sku || !fatherSku) continue;
|
if (!sku || !parentSku) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await runInTx(async (runner) => {
|
await runInTx(async (runner) => {
|
||||||
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
|
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
|
||||||
const childId = child.rows[0]?.articleId;
|
const childId = child.rows[0]?.articleId;
|
||||||
if (childId) {
|
if (childId) {
|
||||||
const father = await runner.query(
|
const parent = await runner.query(
|
||||||
'SELECT id, is_father FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
|
'SELECT id, is_parent FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
|
||||||
{ sku: fatherSku, companyId },
|
{ sku: parentSku, companyId },
|
||||||
);
|
);
|
||||||
const fatherRow = father.rows[0];
|
const parentRow = parent.rows[0];
|
||||||
const fatherId = fatherRow?.id;
|
const parentId = parentRow?.id;
|
||||||
if (fatherId && fatherId !== childId) {
|
if (parentId && parentId !== childId) {
|
||||||
const wasFather = Boolean(fatherRow.is_father);
|
const wasParent = Boolean(parentRow.is_parent);
|
||||||
|
|
||||||
const existingSortsRes = await runner.query(
|
const existingSortsRes = await runner.query(
|
||||||
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
|
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
|
||||||
{ fatherId },
|
{ parentId },
|
||||||
);
|
);
|
||||||
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
|
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
|
||||||
let fathersort = 0;
|
let parentsort = 0;
|
||||||
while (usedSorts.has(fathersort)) {
|
while (usedSorts.has(parentsort)) {
|
||||||
fathersort++;
|
parentsort++;
|
||||||
}
|
}
|
||||||
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
|
await runner.query(articleQueries.UPDATE_PARENT_BY_SKU, {
|
||||||
sku,
|
sku,
|
||||||
companyId,
|
companyId,
|
||||||
fatherArticleId: fatherId,
|
parentArticleId: parentId,
|
||||||
fathersort,
|
parentsort,
|
||||||
});
|
});
|
||||||
await runner.query(articleQueries.SET_IS_FATHER, {
|
await runner.query(articleQueries.SET_IS_PARENT, {
|
||||||
articleId: fatherId,
|
articleId: parentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!wasFather) {
|
if (!wasParent) {
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: fatherId,
|
articleId: parentId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: 'Artikel zu Vaterartikel konvertiert',
|
text: 'Artikel zu Vaterartikel konvertiert',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const childName = String(row?.name ?? sku).trim();
|
const childName = String(row?.name ?? sku).trim();
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: fatherId,
|
articleId: parentId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
||||||
});
|
});
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: childId,
|
articleId: childId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Artikel "${childName}" (${sku}) als Variante von "${fatherSku}" zugewiesen`,
|
text: `Artikel "${childName}" (${sku}) als Variante von "${parentSku}" zugewiesen`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,40 +9,40 @@ export default function listChildren(provider) {
|
|||||||
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
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 {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.
|
* @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 run = async (runner) => {
|
||||||
const { articleId: fatherId, languageId } = await resolve(
|
const { articleId: parentId, languageId } = await resolve(
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
fatherSku,
|
parentSku,
|
||||||
runner,
|
runner,
|
||||||
);
|
);
|
||||||
|
|
||||||
const fatherRes = await runner.query(
|
const parentRes = await runner.query(
|
||||||
'SELECT is_father, fatherattributename_id FROM article WHERE id = @fatherId',
|
'SELECT is_parent, parentattributename_id FROM article WHERE id = @parentId',
|
||||||
{ fatherId },
|
{ parentId },
|
||||||
);
|
);
|
||||||
const father = fatherRes.rows[0];
|
const parent = parentRes.rows[0];
|
||||||
if (!father || !father.is_father) {
|
if (!parent || !parent.is_parent) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await runner.query(queries.LIST_CHILDREN_BY_FATHER_ID, {
|
const result = await runner.query(queries.LIST_CHILDREN_BY_PARENT_ID, {
|
||||||
fatherArticleId: fatherId,
|
parentArticleId: parentId,
|
||||||
languageId,
|
languageId,
|
||||||
attributeId: father.fatherattributename_id,
|
attributeId: parent.parentattributename_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.rows.map((row) => ({
|
return result.rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
sku: row.sku,
|
sku: row.sku,
|
||||||
fathersort: Number(row.fathersort ?? 0),
|
parentsort: Number(row.parentsort ?? 0),
|
||||||
trackStock: Boolean(row.trackStock ?? 1),
|
trackStock: Boolean(row.trackStock ?? 1),
|
||||||
unitamount: Number(row.unitamount ?? 1),
|
unitamount: Number(row.unitamount ?? 1),
|
||||||
weight: Number(row.weight ?? 0),
|
weight: Number(row.weight ?? 0),
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
|
|||||||
optionsCount: row.optionsCount,
|
optionsCount: row.optionsCount,
|
||||||
articleListCount: row.articleListCount,
|
articleListCount: row.articleListCount,
|
||||||
childCount: row.childCount,
|
childCount: row.childCount,
|
||||||
fatherArticleId: row.fatherArticleId,
|
parentArticleId: row.parentArticleId,
|
||||||
fatherSku: row.fatherSku,
|
parentSku: row.parentSku,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,34 +15,34 @@ export default function removeChild(provider) {
|
|||||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
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 {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 {string} childSku - The child article SKU.
|
||||||
* @param {object} [options]
|
* @param {object} [options]
|
||||||
* @param {boolean} [options.deleteArticle=false] - Whether to delete the child article entity entirely.
|
* @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 {boolean} [options.cascade=true] - Whether to cascade delete child subtrees when deleteArticle is true.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @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 run = async (runner) => {
|
||||||
const { articleId: fatherId, languageId, companyuserId } = await resolve(
|
const { articleId: parentId, languageId, companyuserId } = await resolve(
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
fatherSku,
|
parentSku,
|
||||||
runner,
|
runner,
|
||||||
);
|
);
|
||||||
const { articleId: childId } = await resolve(googleAuthId, childSku, runner);
|
const { articleId: childId } = await resolve(googleAuthId, childSku, runner);
|
||||||
|
|
||||||
// Verify child relationship
|
// Verify child relationship
|
||||||
const childRes = await runner.query(
|
const childRes = await runner.query(
|
||||||
'SELECT fatherarticle_id FROM article WHERE id = @childId',
|
'SELECT parentarticle_id FROM article WHERE id = @childId',
|
||||||
{ childId },
|
{ childId },
|
||||||
);
|
);
|
||||||
const child = childRes.rows[0];
|
const child = childRes.rows[0];
|
||||||
if (!child || child.fatherarticle_id !== fatherId) {
|
if (!child || child.parentarticle_id !== parentId) {
|
||||||
throw new Error('Child article does not belong to this father');
|
throw new Error('Child article does not belong to this parent');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch child name before any deletion (needed for history regardless of path)
|
// 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 childName = childNameRes.rows[0]?.name || childSku;
|
||||||
|
|
||||||
const recompactFatherSort = async () => {
|
const recompactParentSort = async () => {
|
||||||
const remainingChildrenRes = await runner.query(
|
const remainingChildrenRes = await runner.query(
|
||||||
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC, id ASC',
|
'SELECT id, parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC, id ASC',
|
||||||
{ fatherId },
|
{ parentId },
|
||||||
);
|
);
|
||||||
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
|
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
|
||||||
const childRow = remainingChildrenRes.rows[i];
|
const childRow = remainingChildrenRes.rows[i];
|
||||||
if (Number(childRow.fathersort) !== i) {
|
if (Number(childRow.parentsort) !== i) {
|
||||||
await runner.query(queries.SET_FATHER_SORT, {
|
await runner.query(queries.SET_PARENT_SORT, {
|
||||||
articleId: childRow.id,
|
articleId: childRow.id,
|
||||||
fatherArticleId: fatherId,
|
parentArticleId: parentId,
|
||||||
fathersort: i,
|
parentsort: i,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,67 +75,67 @@ export default function removeChild(provider) {
|
|||||||
const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true;
|
const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true;
|
||||||
await del(googleAuthId, childSku, { cascade }, runner);
|
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, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: fatherId,
|
articleId: parentId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Variante "${childName}" (${childSku}) gelöscht`,
|
text: `Variante "${childName}" (${childSku}) gelöscht`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
fatherSku,
|
parentSku,
|
||||||
childSku,
|
childSku,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch father's attribute name id
|
// Fetch parent's attribute name id
|
||||||
const fatherRes = await runner.query(
|
const parentRes = await runner.query(
|
||||||
'SELECT fatherattributename_id FROM article WHERE id = @fatherId',
|
'SELECT parentattributename_id FROM article WHERE id = @parentId',
|
||||||
{ fatherId },
|
{ 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, {
|
await runner.query(queries.UNLINK_CHILD, {
|
||||||
articleId: childId,
|
articleId: childId,
|
||||||
fatherArticleId: fatherId,
|
parentArticleId: parentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove the child's attribute value link for the father's attribute
|
// Remove the child's attribute value link for the parent's attribute
|
||||||
if (fatherAttributeNameId) {
|
if (parentAttributeNameId) {
|
||||||
await runner.query(
|
await runner.query(
|
||||||
`DELETE FROM articleattributevalue
|
`DELETE FROM articleattributevalue
|
||||||
WHERE article_id = @childId
|
WHERE article_id = @childId
|
||||||
AND attributevalue_id IN (
|
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
|
// Add history on child
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: childId,
|
articleId: childId,
|
||||||
companyuserId,
|
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, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: fatherId,
|
articleId: parentId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Variante "${childName}" (${childSku}) getrennt`,
|
text: `Variante "${childName}" (${childSku}) getrennt`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
fatherSku,
|
parentSku,
|
||||||
childSku,
|
childSku,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import queryBySku from './queryBySku.js';
|
|||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function setFatherBySku(provider) {
|
export default function setParentBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = queryBySku(provider);
|
const load = queryBySku(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
@@ -18,20 +18,20 @@ export default function setFatherBySku(provider) {
|
|||||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
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 {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The child article SKU.
|
* @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.
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<object>} The updated child article.
|
* @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
|
* @example
|
||||||
* const article = await store.article.setFatherBySku(42, '42001190-1', '42001190');
|
* const article = await store.article.setParentBySku(42, '42001190-1', '42001190');
|
||||||
* // => { id, sku, name, fatherSku: '42001190', ... }
|
* // => { id, sku, name, parentSku: '42001190', ... }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, fatherSku, tx) => {
|
return async (googleAuthId, sku, parentSku, tx) => {
|
||||||
const runSet = async (runner) => {
|
const runSet = async (runner) => {
|
||||||
const { articleId, languageId, companyId, companyuserId } = await resolve(googleAuthId, sku, 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;
|
const childName = childTextRes.rows[0]?.name || sku;
|
||||||
|
|
||||||
if (fatherSku == null || fatherSku === '') {
|
if (parentSku == null || parentSku === '') {
|
||||||
const prevFatherRes = await runner.query(
|
const prevParentRes = await runner.query(
|
||||||
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
|
'SELECT parentarticle_id FROM article WHERE id = @articleId',
|
||||||
{ 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,
|
sku,
|
||||||
companyId,
|
companyId,
|
||||||
fatherArticleId: null,
|
parentArticleId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (prevFatherId) {
|
if (prevParentId) {
|
||||||
const prevFatherTextRes = await runner.query(
|
const prevParentTextRes = await runner.query(
|
||||||
'SELECT sku FROM article WHERE id = @prevFatherId',
|
'SELECT sku FROM article WHERE id = @prevParentId',
|
||||||
{ prevFatherId },
|
{ prevParentId },
|
||||||
);
|
);
|
||||||
const prevFatherSku = prevFatherTextRes.rows[0]?.sku || String(prevFatherId);
|
const prevParentSku = prevParentTextRes.rows[0]?.sku || String(prevParentId);
|
||||||
|
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId,
|
articleId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Artikel "${sku}" von Vaterartikel "${prevFatherSku}" getrennt`,
|
text: `Artikel "${sku}" von Vaterartikel "${prevParentSku}" getrennt`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: prevFatherId,
|
articleId: prevParentId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Variante "${childName}" (${sku}) getrennt`,
|
text: `Variante "${childName}" (${sku}) getrennt`,
|
||||||
});
|
});
|
||||||
@@ -81,52 +81,52 @@ export default function setFatherBySku(provider) {
|
|||||||
return unlinked;
|
return unlinked;
|
||||||
}
|
}
|
||||||
|
|
||||||
const father = await runner.query(
|
const parent = await runner.query(
|
||||||
'SELECT id, is_father, sku FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
|
'SELECT id, is_parent, sku FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
|
||||||
{ sku: fatherSku, companyId },
|
{ sku: parentSku, companyId },
|
||||||
);
|
);
|
||||||
const fatherRow = father.rows[0];
|
const parentRow = parent.rows[0];
|
||||||
const fatherId = fatherRow?.id;
|
const parentId = parentRow?.id;
|
||||||
if (!fatherId) {
|
if (!parentId) {
|
||||||
throw new Error('Father not found');
|
throw new Error('Parent not found');
|
||||||
}
|
}
|
||||||
if (fatherId === articleId) {
|
if (parentId === articleId) {
|
||||||
throw new Error('Invalid father');
|
throw new Error('Invalid parent');
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasFather = Boolean(fatherRow.is_father);
|
const wasParent = Boolean(parentRow.is_parent);
|
||||||
|
|
||||||
const existingSortsRes = await runner.query(
|
const existingSortsRes = await runner.query(
|
||||||
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
|
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
|
||||||
{ fatherId },
|
{ parentId },
|
||||||
);
|
);
|
||||||
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
|
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
|
||||||
let fathersort = 0;
|
let parentsort = 0;
|
||||||
while (usedSorts.has(fathersort)) {
|
while (usedSorts.has(parentsort)) {
|
||||||
fathersort++;
|
parentsort++;
|
||||||
}
|
}
|
||||||
|
|
||||||
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
await runner.query(queries.UPDATE_PARENT_BY_SKU, {
|
||||||
sku,
|
sku,
|
||||||
companyId,
|
companyId,
|
||||||
fatherArticleId: fatherId,
|
parentArticleId: parentId,
|
||||||
fathersort,
|
parentsort,
|
||||||
});
|
});
|
||||||
|
|
||||||
await runner.query(queries.SET_IS_FATHER, {
|
await runner.query(queries.SET_IS_PARENT, {
|
||||||
articleId: fatherId,
|
articleId: parentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!wasFather) {
|
if (!wasParent) {
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: fatherId,
|
articleId: parentId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: 'Artikel zu Vaterartikel konvertiert',
|
text: 'Artikel zu Vaterartikel konvertiert',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: fatherId,
|
articleId: parentId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
||||||
});
|
});
|
||||||
@@ -134,7 +134,7 @@ export default function setFatherBySku(provider) {
|
|||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId,
|
articleId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Artikel "${sku}" als Variante von "${fatherSku}" zugewiesen`,
|
text: `Artikel "${sku}" als Variante von "${parentSku}" zugewiesen`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const article = await load(articleId, languageId, runner);
|
const article = await load(articleId, languageId, runner);
|
||||||
@@ -86,9 +86,9 @@ export default function setPricesBySku(provider) {
|
|||||||
throw new Error('POS customer group required');
|
throw new Error('POS customer group required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const artCheck = await runner.query('SELECT is_father FROM article WHERE id = @articleId', { articleId });
|
const artCheck = await runner.query('SELECT is_parent FROM article WHERE id = @articleId', { articleId });
|
||||||
if (artCheck.rows[0]?.is_father) {
|
if (artCheck.rows[0]?.is_parent) {
|
||||||
throw new Error('A father article cannot have prices');
|
throw new Error('A parent article cannot have prices');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read old prices for history
|
// Read old prices for history
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import resolveArticleBySku from './resolveArticleBySku.js';
|
|||||||
import getBySku from './getBySku.js';
|
import getBySku from './getBySku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function toggleFather(provider) {
|
export default function toggleParent(provider) {
|
||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = getBySku(provider);
|
const load = getBySku(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
@@ -21,39 +21,39 @@ export default function toggleFather(provider) {
|
|||||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
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 {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {boolean} options.isFather - Whether the article should be a father article.
|
* @param {boolean} options.isParent - Whether the article should be a parent article.
|
||||||
* @param {number} [options.attributeNameId] - The attribute name id required when enabling father mode.
|
* @param {number} [options.attributeNameId] - The attribute name id required when enabling parent mode.
|
||||||
* @param {number} [options.fatherAttributeNameId] - Alias for attributeNameId.
|
* @param {number} [options.parentAttributeNameId] - Alias for attributeNameId.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<object>} The updated article detail.
|
* @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 run = async (runner) => {
|
||||||
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const targetIsFather = Boolean(isFather);
|
const targetIsParent = Boolean(isParent);
|
||||||
const targetAttrId = attributeNameId ?? fatherAttributeNameId;
|
const targetAttrId = attributeNameId ?? parentAttributeNameId;
|
||||||
|
|
||||||
if (targetIsFather) {
|
if (targetIsParent) {
|
||||||
if (!targetAttrId || Number(targetAttrId) <= 0) {
|
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(
|
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 },
|
{ articleId },
|
||||||
);
|
);
|
||||||
const currentArt = currentRes.rows[0];
|
const currentArt = currentRes.rows[0];
|
||||||
if (currentArt?.articlelist_id != null) {
|
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) {
|
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(
|
const stockRes = await runner.query(
|
||||||
@@ -70,7 +70,7 @@ export default function toggleFather(provider) {
|
|||||||
);
|
);
|
||||||
const totalStock = Number(stockRes.rows[0]?.totalStock ?? 0);
|
const totalStock = Number(stockRes.rows[0]?.totalStock ?? 0);
|
||||||
if (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(
|
const supRes = await runner.query(
|
||||||
@@ -78,7 +78,7 @@ export default function toggleFather(provider) {
|
|||||||
{ articleId },
|
{ articleId },
|
||||||
);
|
);
|
||||||
if ((supRes.rows[0]?.count ?? 0) > 0) {
|
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
|
// 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('DELETE FROM price WHERE article_id = @articleId', { articleId });
|
||||||
await runner.query('UPDATE article SET track_stock = 0 WHERE 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
|
// If article is a child, verify attribute is not used by parent or any parent up the chain
|
||||||
let currentFatherId = currentArt?.fatherarticle_id;
|
let currentParentId = currentArt?.parentarticle_id;
|
||||||
const visited = new Set();
|
const visited = new Set();
|
||||||
while (currentFatherId != null && !visited.has(currentFatherId)) {
|
while (currentParentId != null && !visited.has(currentParentId)) {
|
||||||
visited.add(currentFatherId);
|
visited.add(currentParentId);
|
||||||
const fRes = await runner.query(
|
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',
|
'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',
|
||||||
{ currentFatherId },
|
{ currentParentId },
|
||||||
);
|
);
|
||||||
const fRow = fRes.rows[0];
|
const fRow = fRes.rows[0];
|
||||||
if (!fRow) break;
|
if (!fRow) break;
|
||||||
if (fRow.fatherattributename_id != null && Number(fRow.fatherattributename_id) === Number(targetAttrId)) {
|
if (fRow.parentattributename_id != null && Number(fRow.parentattributename_id) === Number(targetAttrId)) {
|
||||||
const attrName = fRow.fatherAttributeName || 'ancestor';
|
const attrName = fRow.parentAttributeName || 'ancestor';
|
||||||
throw new Error(`Attribute "${attrName}" is already used by an ancestor father article`);
|
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
|
// Verify attribute exists and belongs to company
|
||||||
@@ -132,16 +132,16 @@ export default function toggleFather(provider) {
|
|||||||
{ articleId, attributeId: Number(targetAttrId) },
|
{ articleId, attributeId: Number(targetAttrId) },
|
||||||
);
|
);
|
||||||
if (existingValRes.rows.length > 0) {
|
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,
|
articleId,
|
||||||
isFather: 1,
|
isParent: 1,
|
||||||
fatherAttributeNameId: Number(targetAttrId),
|
parentAttributeNameId: Number(targetAttrId),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add history entry recording father conversion and deleted prices
|
// Add history entry recording parent conversion and deleted prices
|
||||||
const historyLines = [
|
const historyLines = [
|
||||||
`Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`,
|
`Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`,
|
||||||
];
|
];
|
||||||
@@ -170,17 +170,17 @@ export default function toggleFather(provider) {
|
|||||||
text: historyLines.join('\n'),
|
text: historyLines.join('\n'),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Disabling father: verify zero children
|
// Disabling parent: verify zero children
|
||||||
const childCountRes = await runner.query(getChildCountSql, { articleId });
|
const childCountRes = await runner.query(getChildCountSql, { articleId });
|
||||||
const childCount = childCountRes.rows[0]?.count ?? 0;
|
const childCount = childCountRes.rows[0]?.count ?? 0;
|
||||||
if (childCount > 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,
|
articleId,
|
||||||
isFather: 0,
|
isParent: 0,
|
||||||
fatherAttributeNameId: null,
|
parentAttributeNameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
@@ -123,39 +123,39 @@ export default function setByArticleSku(provider) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check father / child constraints
|
// Check parent / child constraints
|
||||||
const artRes = await runner.query(
|
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 },
|
{ articleId },
|
||||||
);
|
);
|
||||||
const article = artRes.rows[0];
|
const article = artRes.rows[0];
|
||||||
|
|
||||||
if (article?.is_father && article.fatherattributename_id) {
|
if (article?.is_parent && article.parentattributename_id) {
|
||||||
const hasFatherAttr = validated.some((v) => v.attributeId === article.fatherattributename_id);
|
const hasParentAttr = validated.some((v) => v.attributeId === article.parentattributename_id);
|
||||||
if (hasFatherAttr) {
|
if (hasParentAttr) {
|
||||||
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (article?.fatherarticle_id != null) {
|
if (article?.parentarticle_id != null) {
|
||||||
let currentFatherId = article.fatherarticle_id;
|
let currentParentId = article.parentarticle_id;
|
||||||
const visited = new Set();
|
const visited = new Set();
|
||||||
while (currentFatherId != null && !visited.has(currentFatherId)) {
|
while (currentParentId != null && !visited.has(currentParentId)) {
|
||||||
visited.add(currentFatherId);
|
visited.add(currentParentId);
|
||||||
const fatherRes = await runner.query(
|
const parentRes = 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',
|
'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',
|
||||||
{ currentFatherId },
|
{ currentParentId },
|
||||||
);
|
);
|
||||||
const fRow = fatherRes.rows[0];
|
const fRow = parentRes.rows[0];
|
||||||
if (!fRow) break;
|
if (!fRow) break;
|
||||||
if (fRow.fatherattributename_id != null) {
|
if (fRow.parentattributename_id != null) {
|
||||||
const hasFatherAttr = validated.some((v) => v.attributeId === fRow.fatherattributename_id);
|
const hasParentAttr = validated.some((v) => v.attributeId === fRow.parentattributename_id);
|
||||||
if (!hasFatherAttr) {
|
if (!hasParentAttr) {
|
||||||
const fatherAttrName = fRow.fatherAttributeName || 'ancestor variant attribute';
|
const parentAttrName = fRow.parentAttributeName || 'ancestor variant attribute';
|
||||||
throw new Error(`Child article must have an attribute value for the father attribute "${fatherAttrName}"`);
|
throw new Error(`Child article must have an attribute value for the parent attribute "${parentAttrName}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentFatherId = fRow.fatherarticle_id;
|
currentParentId = fRow.parentarticle_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function setItemsByArticleSku(provider) {
|
|||||||
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const artRes = await runner.query(
|
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 },
|
{ articleId },
|
||||||
);
|
);
|
||||||
const article = artRes.rows[0];
|
const article = artRes.rows[0];
|
||||||
@@ -72,8 +72,8 @@ export default function setItemsByArticleSku(provider) {
|
|||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (article.is_father) {
|
if (article.is_parent) {
|
||||||
throw new Error('A father article cannot be a Stückliste');
|
throw new Error('A parent article cannot be a Stückliste');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If items is empty: clear Stückliste
|
// If items is empty: clear Stückliste
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export default function setByArticleSku(provider) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces all properties for an article by its SKU.
|
* Replaces all properties for an article by its SKU.
|
||||||
* Enforces exclusive flag on property names.
|
* Enforces exclusive flag on property names. An exclusive property that already has a value
|
||||||
|
* is never overwritten: a different value throws `Exclusive property cannot be overwritten: <name>`.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
@@ -79,6 +80,12 @@ export default function setByArticleSku(provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const value = item.value != null ? String(item.value).trim() : '';
|
const value = item.value != null ? String(item.value).trim() : '';
|
||||||
|
if (isExclusive) {
|
||||||
|
const existing = oldProps.find((p) => p.propertyId === propertyId);
|
||||||
|
if (existing && existing.value !== '' && existing.value !== value) {
|
||||||
|
throw new Error(`Exclusive property cannot be overwritten: ${prop.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
const sort = item.sort != null && Number.isInteger(item.sort) ? item.sort : i;
|
const sort = item.sort != null && Number.isInteger(item.sort) ? item.sort : i;
|
||||||
validated.push({
|
validated.push({
|
||||||
propertyId,
|
propertyId,
|
||||||
|
|||||||
@@ -91,9 +91,9 @@ export default function create(provider) {
|
|||||||
runner,
|
runner,
|
||||||
);
|
);
|
||||||
|
|
||||||
const artCheck = await runner.query('SELECT is_father FROM article WHERE id = @articleId', { articleId });
|
const artCheck = await runner.query('SELECT is_parent FROM article WHERE id = @articleId', { articleId });
|
||||||
if (artCheck.rows[0]?.is_father) {
|
if (artCheck.rows[0]?.is_parent) {
|
||||||
throw new Error('A father article cannot have suppliers');
|
throw new Error('A parent article cannot have suppliers');
|
||||||
}
|
}
|
||||||
|
|
||||||
await assertSupplier(runner, companyId, entityId);
|
await assertSupplier(runner, companyId, entityId);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CREATE as SQLITE_CREATE,
|
CREATE as SQLITE_CREATE,
|
||||||
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
|
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
|
||||||
} from '../../providers/sqlite3/queries/attributename.js';
|
} from '../../providers/sqlite3/queries/attributename.js';
|
||||||
|
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ export default function create(provider) {
|
|||||||
* @param {boolean} [input.exclusive=false] - Whether values are mutually exclusive on an article.
|
* @param {boolean} [input.exclusive=false] - Whether values are mutually exclusive on an article.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>}
|
* @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>}
|
||||||
|
*
|
||||||
|
* @throws {Error} If the name is missing or already exists for the company.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, input = {}, tx) => {
|
return async (googleAuthId, input = {}, tx) => {
|
||||||
const name = input.name?.trim();
|
const name = input.name?.trim();
|
||||||
@@ -43,12 +46,20 @@ export default function create(provider) {
|
|||||||
|
|
||||||
const exclusive = Boolean(input.exclusive) ? 1 : 0;
|
const exclusive = Boolean(input.exclusive) ? 1 : 0;
|
||||||
|
|
||||||
const result = await runner.query(createSql, {
|
let result;
|
||||||
companyId,
|
try {
|
||||||
name,
|
result = await runner.query(createSql, {
|
||||||
sort,
|
companyId,
|
||||||
exclusive,
|
name,
|
||||||
});
|
sort,
|
||||||
|
exclusive,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isUniqueConstraintError(err)) {
|
||||||
|
throw new Error('Attribute name already exists');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const id = result.rows[0]?.id ?? result.insertId;
|
const id = result.rows[0]?.id ?? result.insertId;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GET_BY_COMPANY_AND_ID as SQLITE_GET,
|
GET_BY_COMPANY_AND_ID as SQLITE_GET,
|
||||||
DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE,
|
DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE,
|
||||||
} from '../../providers/sqlite3/queries/attributename.js';
|
} from '../../providers/sqlite3/queries/attributename.js';
|
||||||
|
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export default function deleteAttributename(provider) {
|
|||||||
* @param {number} id - The attribute name id.
|
* @param {number} id - The attribute name id.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If attribute name not found or in use.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id, tx) => {
|
return async (googleAuthId, id, tx) => {
|
||||||
const runner = makeRunner(provider, tx);
|
const runner = makeRunner(provider, tx);
|
||||||
@@ -32,6 +34,13 @@ export default function deleteAttributename(provider) {
|
|||||||
throw new Error('Attribute name not found');
|
throw new Error('Attribute name not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await runner.query(deleteSql, { id, companyId });
|
try {
|
||||||
|
await runner.query(deleteSql, { id, companyId });
|
||||||
|
} catch (err) {
|
||||||
|
if (isForeignKeyConstraintError(err)) {
|
||||||
|
throw new Error('Attribute name in use');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GET_BY_COMPANY_AND_ID as SQLITE_GET,
|
GET_BY_COMPANY_AND_ID as SQLITE_GET,
|
||||||
UPDATE_BY_ID as SQLITE_UPDATE,
|
UPDATE_BY_ID as SQLITE_UPDATE,
|
||||||
} from '../../providers/sqlite3/queries/attributename.js';
|
} from '../../providers/sqlite3/queries/attributename.js';
|
||||||
|
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -32,6 +33,8 @@ export default function update(provider) {
|
|||||||
* @param {boolean} [input.exclusive] - New exclusive flag.
|
* @param {boolean} [input.exclusive] - New exclusive flag.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>}
|
* @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>}
|
||||||
|
*
|
||||||
|
* @throws {Error} If the attribute name is unknown, the name is missing, or the name is already taken.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id, input = {}, tx) => {
|
return async (googleAuthId, id, input = {}, tx) => {
|
||||||
if (!id || !Number.isInteger(Number(id))) {
|
if (!id || !Number.isInteger(Number(id))) {
|
||||||
@@ -53,13 +56,20 @@ export default function update(provider) {
|
|||||||
const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort;
|
const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort;
|
||||||
const exclusive = input.exclusive !== undefined ? (Boolean(input.exclusive) ? 1 : 0) : existing.rows[0].exclusive;
|
const exclusive = input.exclusive !== undefined ? (Boolean(input.exclusive) ? 1 : 0) : existing.rows[0].exclusive;
|
||||||
|
|
||||||
await runner.query(updateSql, {
|
try {
|
||||||
id,
|
await runner.query(updateSql, {
|
||||||
companyId,
|
id,
|
||||||
name,
|
companyId,
|
||||||
sort,
|
name,
|
||||||
exclusive,
|
sort,
|
||||||
});
|
exclusive,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isUniqueConstraintError(err)) {
|
||||||
|
throw new Error('Attribute name already exists');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
GET_BY_COMPANY_AND_ID as SQLITE_GET_ATTR,
|
GET_BY_COMPANY_AND_ID as SQLITE_GET_ATTR,
|
||||||
} from '../../providers/sqlite3/queries/attributename.js';
|
} from '../../providers/sqlite3/queries/attributename.js';
|
||||||
|
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -32,6 +33,8 @@ export default function create(provider) {
|
|||||||
* @param {number} [input.sort] - Optional sort order.
|
* @param {number} [input.sort] - Optional sort order.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<{id: number, attributeId: number, value: string, sort: number}>}
|
* @returns {Promise<{id: number, attributeId: number, value: string, sort: number}>}
|
||||||
|
*
|
||||||
|
* @throws {Error} If the value is missing, the attribute is unknown, or the value already exists for the attribute.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, attributeId, input = {}, tx) => {
|
return async (googleAuthId, attributeId, input = {}, tx) => {
|
||||||
const attrId = Number(attributeId);
|
const attrId = Number(attributeId);
|
||||||
@@ -57,11 +60,19 @@ export default function create(provider) {
|
|||||||
sort = next.rows[0].sort;
|
sort = next.rows[0].sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await runner.query(createSql, {
|
let result;
|
||||||
attributeId: attrId,
|
try {
|
||||||
value,
|
result = await runner.query(createSql, {
|
||||||
sort,
|
attributeId: attrId,
|
||||||
});
|
value,
|
||||||
|
sort,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isUniqueConstraintError(err)) {
|
||||||
|
throw new Error('Attribute value already exists');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const id = result.rows[0]?.id ?? result.insertId;
|
const id = result.rows[0]?.id ?? result.insertId;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GET_BY_ID_AND_COMPANY as SQLITE_GET,
|
GET_BY_ID_AND_COMPANY as SQLITE_GET,
|
||||||
DELETE_BY_ID as SQLITE_DELETE,
|
DELETE_BY_ID as SQLITE_DELETE,
|
||||||
} from '../../providers/sqlite3/queries/attributevalue.js';
|
} from '../../providers/sqlite3/queries/attributevalue.js';
|
||||||
|
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export default function deleteAttributeValue(provider) {
|
|||||||
* @param {number} id - The attribute value id.
|
* @param {number} id - The attribute value id.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If attribute value not found or in use.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id, tx) => {
|
return async (googleAuthId, id, tx) => {
|
||||||
const valId = Number(id);
|
const valId = Number(id);
|
||||||
@@ -37,6 +39,13 @@ export default function deleteAttributeValue(provider) {
|
|||||||
throw new Error('Attribute value not found');
|
throw new Error('Attribute value not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await runner.query(deleteSql, { id: valId });
|
try {
|
||||||
|
await runner.query(deleteSql, { id: valId });
|
||||||
|
} catch (err) {
|
||||||
|
if (isForeignKeyConstraintError(err)) {
|
||||||
|
throw new Error('Attribute value in use');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GET_BY_ID_AND_COMPANY as SQLITE_GET,
|
GET_BY_ID_AND_COMPANY as SQLITE_GET,
|
||||||
UPDATE_BY_ID as SQLITE_UPDATE,
|
UPDATE_BY_ID as SQLITE_UPDATE,
|
||||||
} from '../../providers/sqlite3/queries/attributevalue.js';
|
} from '../../providers/sqlite3/queries/attributevalue.js';
|
||||||
|
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ export default function update(provider) {
|
|||||||
* @param {number} [input.sort] - New sort order.
|
* @param {number} [input.sort] - New sort order.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<{id: number, attributeId: number, value: string, sort: number}>}
|
* @returns {Promise<{id: number, attributeId: number, value: string, sort: number}>}
|
||||||
|
*
|
||||||
|
* @throws {Error} If the attribute value is unknown, the value is missing, or the value is already taken.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id, input = {}, tx) => {
|
return async (googleAuthId, id, input = {}, tx) => {
|
||||||
const valId = Number(id);
|
const valId = Number(id);
|
||||||
@@ -46,11 +49,18 @@ export default function update(provider) {
|
|||||||
}
|
}
|
||||||
const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort;
|
const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort;
|
||||||
|
|
||||||
await runner.query(updateSql, {
|
try {
|
||||||
id: valId,
|
await runner.query(updateSql, {
|
||||||
value,
|
id: valId,
|
||||||
sort,
|
value,
|
||||||
});
|
sort,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isUniqueConstraintError(err)) {
|
||||||
|
throw new Error('Attribute value already exists');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: valId,
|
id: valId,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function getCategoryArticles(provider) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} categoryId - The id of the category.
|
* @param {number} categoryId - The id of the category.
|
||||||
* @param {object} [tx] - Optional transaction to run within.
|
* @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
|
* @example
|
||||||
* const articles = await store.category.getCategoryArticles(42, 5);
|
* const articles = await store.category.getCategoryArticles(42, 5);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export default function queryCategoryArticles(provider) {
|
|||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
track_stock: Boolean(rest.track_stock ?? 1),
|
track_stock: Boolean(rest.track_stock ?? 1),
|
||||||
|
// MSSQL returns BIT as boolean, SQLite as 0/1 integer. Normalize so both
|
||||||
|
// dialects expose isParent as a real boolean, matching article/getBySku.
|
||||||
|
isParent: Boolean(rest.isParent),
|
||||||
brand: brandName
|
brand: brandName
|
||||||
? {
|
? {
|
||||||
id: brandId ?? null,
|
id: brandId ?? null,
|
||||||
|
|||||||
54
src/store/company/addHistory.js
Normal file
54
src/store/company/addHistory.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { INSERT as MSSQL_INSERT } from '../../providers/mssql/queries/companyhistory.js';
|
||||||
|
import { INSERT as SQLITE_INSERT } from '../../providers/sqlite3/queries/companyhistory.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function addHistory(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const sql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an entry to the company history for the user's selected company.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object|string} input - The history input object or text string.
|
||||||
|
* @param {string} [input.text] - The history entry text.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<{id: number, companyuserId: number|null, date: string, text: string}>} The inserted entry.
|
||||||
|
* @throws {Error} If text is missing or user is not authorized.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const entry = await store.company.addHistory(42, { text: 'API suggestion: add barcode scanner endpoint' });
|
||||||
|
* // => { id, companyuserId, date, text }
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, input, tx) => {
|
||||||
|
let rawText = typeof input === 'string' ? input : input?.text;
|
||||||
|
const text = typeof rawText === 'string' ? rawText.trim() : '';
|
||||||
|
if (!text) {
|
||||||
|
throw new Error('Text required');
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedTx = tx;
|
||||||
|
if (input != null && typeof input === 'object' && input._isTx) {
|
||||||
|
resolvedTx = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = makeRunner(provider, resolvedTx);
|
||||||
|
const { companyId, companyuserId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
|
const result = await runner.query(sql, {
|
||||||
|
companyId,
|
||||||
|
companyuserId: companyuserId ?? null,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = result.rows[0];
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
companyuserId: row.companyuserId ?? null,
|
||||||
|
date: row.date,
|
||||||
|
text: row.text,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
54
src/store/company/getHistory.js
Normal file
54
src/store/company/getHistory.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { LIST_BY_COMPANY_ID as MSSQL_QUERY, LIST_BY_COMPANY_ID_LIMITED as MSSQL_QUERY_LIMITED } from '../../providers/mssql/queries/companyhistory.js';
|
||||||
|
import { LIST_BY_COMPANY_ID as SQLITE_QUERY, LIST_BY_COMPANY_ID_LIMITED as SQLITE_QUERY_LIMITED } from '../../providers/sqlite3/queries/companyhistory.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function getHistory(provider) {
|
||||||
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const sql = isSqlite ? SQLITE_QUERY : MSSQL_QUERY;
|
||||||
|
const sqlLimited = isSqlite ? SQLITE_QUERY_LIMITED : MSSQL_QUERY_LIMITED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the company history for the user's selected company, newest first.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [opts] - Optional options.
|
||||||
|
* @param {number} [opts.limit] - Maximum number of entries to return (newest first).
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The company history entries.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const history = await store.company.getHistory(42);
|
||||||
|
* // => [{ id, companyuserId, date, text }]
|
||||||
|
*
|
||||||
|
* const recent = await store.company.getHistory(42, { limit: 5 });
|
||||||
|
* // => up to 5 most recent entries
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, opts, tx) => {
|
||||||
|
let limit = null;
|
||||||
|
let resolvedTx = tx;
|
||||||
|
if (opts != null && typeof opts === 'object' && !opts._isTx) {
|
||||||
|
limit = opts.limit != null ? Number(opts.limit) : null;
|
||||||
|
} else if (opts != null) {
|
||||||
|
resolvedTx = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = makeRunner(provider, resolvedTx);
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (limit != null && Number.isInteger(limit) && limit > 0) {
|
||||||
|
result = await runner.query(sqlLimited, { companyId, limit });
|
||||||
|
} else {
|
||||||
|
result = await runner.query(sql, { companyId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
companyuserId: row.companyuserId ?? null,
|
||||||
|
date: row.date,
|
||||||
|
text: row.text,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
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,
|
CREATE_LINE as MSSQL_CREATE_LINE,
|
||||||
} from '../providers/mssql/queries/manifestDoc.js';
|
} from '../providers/mssql/queries/manifestDoc.js';
|
||||||
import {
|
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,
|
CREATE_LINE as SQLITE_CREATE_LINE,
|
||||||
} from '../providers/sqlite3/queries/manifestDoc.js';
|
} from '../providers/sqlite3/queries/manifestDoc.js';
|
||||||
import makeRunner from './runner.js';
|
import makeRunner from './runner.js';
|
||||||
@@ -122,20 +122,20 @@ export async function insertLines(
|
|||||||
const { manifestId, lines, ensureTax } = params;
|
const { manifestId, lines, ensureTax } = params;
|
||||||
const isSqlite = runner?.dialect === 'sqlite3';
|
const isSqlite = runner?.dialect === 'sqlite3';
|
||||||
const createLineSql = isSqlite ? SQLITE_CREATE_LINE : MSSQL_CREATE_LINE;
|
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) {
|
for (const line of lines) {
|
||||||
let resolvedName = line.name;
|
let resolvedName = line.name;
|
||||||
let resolvedPu = line.pu;
|
let resolvedPu = line.pu;
|
||||||
|
|
||||||
if (line.sku) {
|
if (line.sku) {
|
||||||
const fatherCheck = await runner.query(checkFatherSql, {
|
const parentCheck = await runner.query(checkParentSql, {
|
||||||
manifestId,
|
manifestId,
|
||||||
sku: line.sku,
|
sku: line.sku,
|
||||||
});
|
});
|
||||||
const art = fatherCheck.rows[0];
|
const art = parentCheck.rows[0];
|
||||||
if (art?.isFather) {
|
if (art?.isParent) {
|
||||||
throw new Error(`A father article cannot be booked on documents (${line.sku})`);
|
throw new Error(`A parent article cannot be booked on documents (${line.sku})`);
|
||||||
}
|
}
|
||||||
if (!resolvedName && art?.articleName) {
|
if (!resolvedName && art?.articleName) {
|
||||||
resolvedName = art.articleName;
|
resolvedName = art.articleName;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function setOptionsByArticleSku(provider) {
|
|||||||
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const artRes = await runner.query(
|
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 },
|
{ articleId },
|
||||||
);
|
);
|
||||||
const article = artRes.rows[0];
|
const article = artRes.rows[0];
|
||||||
@@ -72,8 +72,8 @@ export default function setOptionsByArticleSku(provider) {
|
|||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (article.is_father) {
|
if (article.is_parent) {
|
||||||
throw new Error('A father article cannot have options');
|
throw new Error('A parent article cannot have options');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If options is empty: clear optionslist
|
// If options is empty: clear optionslist
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CREATE as SQLITE_CREATE,
|
CREATE as SQLITE_CREATE,
|
||||||
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
|
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
|
||||||
} from '../../providers/sqlite3/queries/propertyname.js';
|
} from '../../providers/sqlite3/queries/propertyname.js';
|
||||||
|
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ export default function create(provider) {
|
|||||||
* @param {boolean} [input.exclusive=false] - Whether values are mutually exclusive on an article.
|
* @param {boolean} [input.exclusive=false] - Whether values are mutually exclusive on an article.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean}>}
|
* @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean}>}
|
||||||
|
*
|
||||||
|
* @throws {Error} If the name is missing or already exists for the company.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, input = {}, tx) => {
|
return async (googleAuthId, input = {}, tx) => {
|
||||||
const name = input.name?.trim();
|
const name = input.name?.trim();
|
||||||
@@ -43,12 +46,20 @@ export default function create(provider) {
|
|||||||
|
|
||||||
const exclusive = Boolean(input.exclusive) ? 1 : 0;
|
const exclusive = Boolean(input.exclusive) ? 1 : 0;
|
||||||
|
|
||||||
const result = await runner.query(createSql, {
|
let result;
|
||||||
companyId,
|
try {
|
||||||
name,
|
result = await runner.query(createSql, {
|
||||||
sort,
|
companyId,
|
||||||
exclusive,
|
name,
|
||||||
});
|
sort,
|
||||||
|
exclusive,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isUniqueConstraintError(err)) {
|
||||||
|
throw new Error('Property name already exists');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const id = result.rows[0]?.id ?? result.insertId;
|
const id = result.rows[0]?.id ?? result.insertId;
|
||||||
return { id, name, sort, exclusive: Boolean(exclusive) };
|
return { id, name, sort, exclusive: Boolean(exclusive) };
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GET_BY_COMPANY_AND_ID as SQLITE_GET,
|
GET_BY_COMPANY_AND_ID as SQLITE_GET,
|
||||||
DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE,
|
DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE,
|
||||||
} from '../../providers/sqlite3/queries/propertyname.js';
|
} from '../../providers/sqlite3/queries/propertyname.js';
|
||||||
|
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export default function deletePropertyname(provider) {
|
|||||||
* @param {number} id - The property name id.
|
* @param {number} id - The property name id.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If property name not found or in use.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id, tx) => {
|
return async (googleAuthId, id, tx) => {
|
||||||
const runner = makeRunner(provider, tx);
|
const runner = makeRunner(provider, tx);
|
||||||
@@ -32,6 +34,13 @@ export default function deletePropertyname(provider) {
|
|||||||
throw new Error('Property name not found');
|
throw new Error('Property name not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await runner.query(deleteSql, { id, companyId });
|
try {
|
||||||
|
await runner.query(deleteSql, { id, companyId });
|
||||||
|
} catch (err) {
|
||||||
|
if (isForeignKeyConstraintError(err)) {
|
||||||
|
throw new Error('Property name in use');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GET_BY_COMPANY_AND_ID as SQLITE_GET,
|
GET_BY_COMPANY_AND_ID as SQLITE_GET,
|
||||||
UPDATE_BY_ID as SQLITE_UPDATE,
|
UPDATE_BY_ID as SQLITE_UPDATE,
|
||||||
} from '../../providers/sqlite3/queries/propertyname.js';
|
} from '../../providers/sqlite3/queries/propertyname.js';
|
||||||
|
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -26,6 +27,8 @@ export default function update(provider) {
|
|||||||
* @param {boolean} [input.exclusive] - New exclusive flag.
|
* @param {boolean} [input.exclusive] - New exclusive flag.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean}>}
|
* @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean}>}
|
||||||
|
*
|
||||||
|
* @throws {Error} If the property name is unknown, the name is missing, or the name is already taken.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id, input = {}, tx) => {
|
return async (googleAuthId, id, input = {}, tx) => {
|
||||||
if (!id || !Number.isInteger(Number(id))) {
|
if (!id || !Number.isInteger(Number(id))) {
|
||||||
@@ -47,13 +50,30 @@ export default function update(provider) {
|
|||||||
const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort;
|
const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort;
|
||||||
const exclusive = input.exclusive !== undefined ? (Boolean(input.exclusive) ? 1 : 0) : existing.rows[0].exclusive;
|
const exclusive = input.exclusive !== undefined ? (Boolean(input.exclusive) ? 1 : 0) : existing.rows[0].exclusive;
|
||||||
|
|
||||||
await runner.query(updateSql, {
|
if (exclusive && !existing.rows[0].exclusive) {
|
||||||
id,
|
const multi = await runner.query(
|
||||||
companyId,
|
`SELECT article_id FROM articleproperty WHERE property_id = @id GROUP BY article_id HAVING COUNT(*) > 1`,
|
||||||
name,
|
{ id },
|
||||||
sort,
|
);
|
||||||
exclusive,
|
if (multi.rows.length > 0) {
|
||||||
});
|
throw new Error(`Cannot set property exclusive: ${multi.rows.length} article(s) have multiple values`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runner.query(updateSql, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
name,
|
||||||
|
sort,
|
||||||
|
exclusive,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (isUniqueConstraintError(err)) {
|
||||||
|
throw new Error('Property name already exists');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
return { id, name, sort, exclusive: Boolean(exclusive) };
|
return { id, name, sort, exclusive: Boolean(exclusive) };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -116,11 +116,11 @@ export default function adjust(provider) {
|
|||||||
const resolvedArticles = [];
|
const resolvedArticles = [];
|
||||||
for (const item of normalizedLines) {
|
for (const item of normalizedLines) {
|
||||||
const resolved = await resolveArticle(googleAuthId, item.sku, activeTx);
|
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,
|
articleId: resolved.articleId,
|
||||||
});
|
});
|
||||||
if (artRes.rows[0]?.is_father) {
|
if (artRes.rows[0]?.is_parent) {
|
||||||
throw new Error(`A father article cannot have stock (${item.sku})`);
|
throw new Error(`A parent article cannot have stock (${item.sku})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (artRes.rows[0]?.articlelist_id != null) {
|
if (artRes.rows[0]?.articlelist_id != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user