Compare commits
136 Commits
74ed74fc68
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26f21cd7ac | ||
|
|
d0a33b1f82 | ||
|
|
55f04de8a8 | ||
|
|
76ea6642f4 | ||
|
|
f612c87b6e | ||
|
|
c7d1b1a5d2 | ||
|
|
745d0d0077 | ||
|
|
2e00f4adb3 | ||
|
|
60cf5873cd | ||
|
|
211acd651d | ||
|
|
a4718d81c0 | ||
|
|
0fdffb864f | ||
|
|
64050dffaf | ||
|
|
3cb5ec5a7a | ||
|
|
a5cef78d30 | ||
|
|
c7db2262b0 | ||
|
|
0fb5893a70 | ||
|
|
75a83bdc28 | ||
|
|
b0521a4ea6 | ||
|
|
2ded87abb7 | ||
|
|
2d4d894881 | ||
|
|
ccb81cdb26 | ||
|
|
bab5c151d0 | ||
|
|
563945394b | ||
|
|
358e3009b2 | ||
|
|
5fd7f3228c | ||
|
|
53f1b24ec0 | ||
|
|
6b6c473f9d | ||
|
|
c6171a1d90 | ||
|
|
62d0643b99 | ||
|
|
1da82e2eb9 | ||
|
|
feffe4e1ab | ||
|
|
c172f2f4b1 | ||
|
|
82ab27ef2b | ||
|
|
8002ca56e6 | ||
|
|
8a085986e5 | ||
|
|
919713fa53 | ||
|
|
5fc316461d | ||
|
|
e31566e6ee | ||
|
|
d6e5da93cb | ||
|
|
e9b4ac5d79 | ||
|
|
9495ad51cc | ||
|
|
bb880e3c84 | ||
|
|
eb1e9e2eb9 | ||
|
|
e085bcaeba | ||
|
|
b0014d6629 | ||
|
|
e9e98d041c | ||
|
|
e3761bc48b | ||
|
|
813e7c89b8 | ||
|
|
bcefd81af7 | ||
|
|
5cb38874ca | ||
|
|
4b34902b92 | ||
|
|
a5eee3f3d0 | ||
|
|
5259e3bef9 | ||
|
|
cce966d021 | ||
|
|
c730244902 | ||
|
|
9ed7e4ee3b | ||
|
|
8cc7d0eb0b | ||
|
|
f6b0ff7483 | ||
|
|
d587f31953 | ||
|
|
9e1edd5cad | ||
|
|
da2cdd5b23 | ||
|
|
bf4109d8ec | ||
|
|
97c909d10b | ||
|
|
c687553eea | ||
|
|
fda895f75a | ||
|
|
ce5da810a5 | ||
|
|
3c2b933f93 | ||
|
|
688fa4f3fd | ||
|
|
ab42a3ffb4 | ||
|
|
f8cc01baf0 | ||
|
|
a194cf6b8b | ||
|
|
59395f95d8 | ||
|
|
8ee080b744 | ||
|
|
e16c3b3f67 | ||
|
|
c7d709de1f | ||
|
|
7635821dad | ||
|
|
796f6a3272 | ||
|
|
70e3ae7a87 | ||
|
|
fcbcb243e3 | ||
|
|
0d726c76f8 | ||
|
|
2179b2e241 | ||
|
|
7c8c24eb4d | ||
|
|
f38d1f11a6 | ||
|
|
b718cdb047 | ||
|
|
3256ab233d | ||
|
|
8a626b17fc | ||
|
|
a5886b7c92 | ||
|
|
2ef2c0edbf | ||
|
|
4b2ca7ce0e | ||
|
|
8a8aeaedd7 | ||
|
|
28e62093b3 | ||
|
|
64d1772842 | ||
|
|
10b1d53f9d | ||
|
|
88ff9e08a9 | ||
|
|
2e961252f7 | ||
|
|
3d6bc97411 | ||
|
|
c614a67b97 | ||
|
|
17cb4f4c72 | ||
|
|
7f4092971d | ||
|
|
7ffb21aabb | ||
|
|
cf1c8d1126 | ||
|
|
906ebafbbe | ||
|
|
ba376686f3 | ||
|
|
00cf5b418e | ||
|
|
f840a12b78 | ||
|
|
178055f1d8 | ||
|
|
07c4f0bff9 | ||
|
|
4ae3439289 | ||
|
|
f3674edcb7 | ||
|
|
760f074b4e | ||
|
|
d36c2827cc | ||
|
|
46ea04c0cb | ||
|
|
b51149c7f0 | ||
|
|
4e308cc177 | ||
|
|
69eb2c9a0d | ||
|
|
36dd81ee69 | ||
|
|
e02bbd65c5 | ||
|
|
95ef6f2dd1 | ||
|
|
86ce3a3316 | ||
|
|
191a0b29e2 | ||
|
|
4718cb5726 | ||
|
|
0268583ae3 | ||
|
|
c80bfee8e8 | ||
|
|
067ddc088c | ||
|
|
2158ac475b | ||
|
|
9a1c19cf60 | ||
|
|
54398d90f2 | ||
|
|
89fe6e6451 | ||
|
|
3b74c12f4e | ||
|
|
6520665dcf | ||
|
|
397ae00735 | ||
|
|
856d5419a1 | ||
|
|
84e547660e | ||
|
|
62c3e69ed4 | ||
|
|
9c770d765e |
32
AGENTS.md
Normal file
32
AGENTS.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# AGENTS.md — dashdbschema (`libdashstore`)
|
||||||
|
|
||||||
|
MSSQL schema + domain store API. ESM (`type: module`). One class `LibDashStore`; methods mirror `src/store/<domain>/<action>.js`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- Entry: `src/index.js` wires namespaces (`article`, `category`, `company`, `delivery`, `order`, `quote`, `ledger`, `stock`, `numbers`, `warehouse`, …).
|
||||||
|
- Provider contract: `src/providers/provider.js` (`query`, `transaction`, `initialize`, `purge`). `src/providers/mssql/` production (raw SQL in `queries/<domain>.js`, DDL in `schema.js`, `OUTPUT INSERTED.*`, `TOP 1`, `WITH (UPDLOCK, HOLDLOCK)`); `src/providers/sqlite3/` dev/test (`better-sqlite3`, re-exports mssql queries via `translateQueries`, `RETURNING`/`LIMIT 1`).
|
||||||
|
- Store convention: each module `export default function <action>(provider) => async (googleAuthId, input, tx)`. `googleAuthId` is always `google_auth.id`. Company scoping via `resolveSelectedCompanyAccess` (`selected_company`) — no `companyId` arg except `setSelectedCompany`, `companyuser.add`, `company.delete`.
|
||||||
|
- Ledger is append-only + hash-chained (`store/ledger/add.js`); stock is derived (`SUM(ledger.amount)` filtered by company, matched via `article.sku` or `articlesupplier.sku`). `store/stock/adjust.js` writes implicit `adjustment` manifest + ledger + `articlehistory`. `track_stock==0` blocks warehouse-scoped writes (`Stock tracking is disabled…`).
|
||||||
|
- Errors are plain `Error` with stable strings; HTTP mapping lives in dashback route `*ErrorStatus()`. Keep messages greppable.
|
||||||
|
|
||||||
|
## Build / Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run reset-db # purge + init → fresh DB (source of truth)
|
||||||
|
npm run api-doc # regenerates API.md + API.<module>.md from JSDoc
|
||||||
|
node scripts/test-ledger.js # sqlite temp-DB regression (also: test-stueckliste-stock, test-variant-flow, test-sqlite-storage, …)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Never run `scripts/test-api.js` for verification — interactive `@clack/prompts` menu (MSSQL), hangs.
|
||||||
|
- Schema changes go directly in DDL (`CREATE TABLE … IF NOT EXISTS`); no migration/upgrade paths in code. Shared-DB fixes are manual `sqlcmd` only. See `.cursor/rules/no-schema-migrations.mdc`.
|
||||||
|
- After store changes run `npm run api-doc`; edit JSDoc source, never the generated `API.*.md`.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Sibling repos (`../dashback`, `../dashfront`) are **read-only**: may read docs/code + `git -C ../<repo> log --oneline -20`, never write/install/start. Missing HTTP/UI contract → ask human, don't guess. No fallbacks. See `.cursor/rules/sibling-repos.mdc`.
|
||||||
|
- Touching SQL → update **both** `mssql/queries/*` and check `sqlite3/queries/*` translation (`OUTPUT INSERTED` vs `RETURNING`, `TOP 1` vs `LIMIT 1`).
|
||||||
|
- Stock invariants belong in `src/store/` (covers both `stock.adjust` and direct `ledger.add`); mirror new error strings in dashback route mappers via human.
|
||||||
|
- Contracts: `README.md` (conventions), `API.md` (index), `src/store/` (implementation).
|
||||||
|
- Full conventions: `/home/seb/src/dashback/.agents/skills/project-overview/SKILL.md`.
|
||||||
84
API.account.md
Normal file
84
API.account.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# account
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `account.create`
|
||||||
|
|
||||||
|
*Source: `src/store/account/create.js`*
|
||||||
|
|
||||||
|
Creates an account for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | Account input. |
|
||||||
|
| `input.type` | `string` | Account type. |
|
||||||
|
| `input.name` | `string` | Account name. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyId: number, type: string, name: string}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If type or name is missing.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const account = await store.account.create(googleAuthId, { type: 'bank', name: 'Main' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## `account.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/account/delete.js`*
|
||||||
|
|
||||||
|
Deletes an account of the user's selected company. Its payments are removed by cascade.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The account id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the account id is invalid.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.account.delete(googleAuthId, 3);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `account.list`
|
||||||
|
|
||||||
|
*Source: `src/store/account/list.js`*
|
||||||
|
|
||||||
|
Lists accounts for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, companyId: number, type: string, name: string}>>`
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const accounts = await store.account.list(googleAuthId);
|
||||||
|
```
|
||||||
119
API.agentprinter.md
Normal file
119
API.agentprinter.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# agentprinter
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `agentprinter.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/agentprinter/delete.js`*
|
||||||
|
|
||||||
|
Deletes an agent printer by id for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The agent printer id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number}>`
|
||||||
|
|
||||||
|
## `agentprinter.findOrCreate`
|
||||||
|
|
||||||
|
*Source: `src/store/agentprinter/findOrCreate.js`*
|
||||||
|
|
||||||
|
Finds or creates an agentprinter by (printeragentId, type, model, serial) combo.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `printeragentId` | `number` | The parent printer agent id. |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.type` | `string` | Printer type ("zpl", "esc/pos", etc.). |
|
||||||
|
| `[input.model]` | `string|null` | Discovered model name. |
|
||||||
|
| `[input.serial]` | `string|null` | Discovered serial number. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{created: boolean, agentprinter: {id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}}>`
|
||||||
|
|
||||||
|
## `agentprinter.getById`
|
||||||
|
|
||||||
|
*Source: `src/store/agentprinter/getById.js`*
|
||||||
|
|
||||||
|
Retrieves an agent printer by id, scoped to the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The agent printer id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}|null>`
|
||||||
|
|
||||||
|
## `agentprinter.list`
|
||||||
|
|
||||||
|
*Source: `src/store/agentprinter/list.js`*
|
||||||
|
|
||||||
|
Lists agent printers of the user's selected company, optionally filtered by printer agent.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[printeragentId]` | `number` | Optional printer agent id filter. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>>`
|
||||||
|
|
||||||
|
## `agentprinter.order`
|
||||||
|
|
||||||
|
*Source: `src/store/agentprinter/order.js`*
|
||||||
|
|
||||||
|
Reorders agent printers for a given printer agent or globally across the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `[input.printeragentId]` | `number` | Optional printer agent id. If omitted, reorders all printers in company. |
|
||||||
|
| `input.agentprinterIds` | `number[]` | Complete ordered list of agent printer IDs. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{agentprinterIds: number[]}>`
|
||||||
|
|
||||||
|
## `agentprinter.update`
|
||||||
|
|
||||||
|
*Source: `src/store/agentprinter/update.js`*
|
||||||
|
|
||||||
|
Updates enabled and/or sort of an agent printer for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.id` | `number` | Agent printer id. |
|
||||||
|
| `[input.enabled]` | `boolean` | Whether the printer is enabled. |
|
||||||
|
| `[input.sort]` | `number` | Display order integer. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>`
|
||||||
185
API.article.md
185
API.article.md
@@ -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. |
|
||||||
@@ -31,7 +32,7 @@ Adds a child article to a father article.
|
|||||||
|
|
||||||
*Source: `src/store/article/addImage.js`*
|
*Source: `src/store/article/addImage.js`*
|
||||||
|
|
||||||
Adds an image to an article by its SKU, processing it into AVIF variants.
|
Adds an image to an article by its SKU, processing full, thumb, and 32px-edge small-thumb AVIF variants.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -44,15 +45,42 @@ Adds an image to an article by its SKU, processing it into AVIF variants.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` — The added image info.
|
`Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, smallThumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` — The added image info.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const result = await store.article.addImage(42, 'SKU-1', imageBuffer);
|
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 }, smallThumb: { 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`*
|
||||||
@@ -67,7 +95,9 @@ Creates a new article under a given parent category, allocating a SKU and a defa
|
|||||||
| `input.googleAuthId` | `number` | The id of the Google auth user. |
|
| `input.googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
| `input.parentId` | `number` | The id of the parent category. |
|
| `input.parentId` | `number` | The id of the parent category. |
|
||||||
| `input.name` | `string` | The article name. |
|
| `input.name` | `string` | The article name. |
|
||||||
| `[input.track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. |
|
| `[input.desc]` | `string` | The article description text/HTML. |
|
||||||
|
| `[input.seo]` | `string` | The article SEO text. |
|
||||||
|
| `[input.track_stock=false]` | `boolean|number` | Whether inventory stock is tracked. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -85,7 +115,7 @@ const article = await store.article.createUnderParent({ googleAuthId: 42, parent
|
|||||||
|
|
||||||
*Source: `src/store/article/delete.js`*
|
*Source: `src/store/article/delete.js`*
|
||||||
|
|
||||||
Deletes an article by its SKU, refusing when it still has children.
|
Deletes an article by its SKU, refusing when it still has children unless cascade is true.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -93,7 +123,8 @@ Deletes an article by its SKU, refusing when it still has children.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `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. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[optionsOrTx]` | `object|*` | Options object ({ cascade?: boolean }) or optional transaction. |
|
||||||
|
| `[maybeTx]` | `object` | Optional transaction if options was passed. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
@@ -101,12 +132,13 @@ Deletes an article by its SKU, refusing when it still has children.
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If the article has children or is not found.
|
`Error` — If the article has children (when cascade=false) or is not found.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await store.article.delete(42, 'SKU-1');
|
await store.article.delete(42, 'SKU-1');
|
||||||
|
await store.article.delete(42, 'SKU-1', { cascade: true });
|
||||||
```
|
```
|
||||||
|
|
||||||
## `article.deleteImage`
|
## `article.deleteImage`
|
||||||
@@ -149,12 +181,10 @@ Returns the full detail of an article by its SKU, including unit, brand, categor
|
|||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
| `sku` | `string` | The article SKU. |
|
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
|
||||||
|
|
||||||
**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, desc: string|null, seo: string|null, 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, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, warehouses: Array<{id: number, name: string, sort: number, stock: number}>, undeliveredOrders: Array<{orderId: number, orderNumber: string, date: string, customerName: string|null, amount: number}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` — The full article detail.
|
||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
@@ -179,24 +209,33 @@ Returns the change history of an article by its SKU.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `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. |
|
||||||
|
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of entries to skip. |
|
||||||
|
| `[opts.limit=50]` | `number` | Number of entries to return. |
|
||||||
|
| `[opts.search]` | `string` | Optional search filter for history text. |
|
||||||
|
| `[opts.seekMonths]` | `number` | Negative integer cutoff months ago. |
|
||||||
|
| `[opts.paged]` | `boolean` | Explicitly return paged result object. |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>` — The article history entries.
|
`Promise<{history: Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>` — The article history entries.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
const result = await store.article.getHistoryBySku(42, 'SKU-1', { offset: 0, limit: 50, search: 'Tee' });
|
||||||
|
// => { history: [...], total, unfilteredTotal, offset, limit }
|
||||||
|
|
||||||
const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
||||||
// => [{ id, companyuserId, date, text }]
|
// => [{ id, companyuserId, date, text, meta }]
|
||||||
```
|
```
|
||||||
|
|
||||||
## `article.importRows`
|
## `article.importRows`
|
||||||
|
|
||||||
*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**
|
||||||
|
|
||||||
@@ -207,15 +246,15 @@ 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. |
|
||||||
| `[input.rows[].net]` | `number|string` | The POS net price (accepts German decimals). |
|
| `[input.rows[].net]` | `number|string` | The POS net price (accepts German decimals). |
|
||||||
| `[input.rows[].tax]` | `number|string` | The tax rate in percent (e.g. `19`). |
|
| `[input.rows[].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=false]` | `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. |
|
||||||
|
|
||||||
@@ -229,7 +268,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 },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -240,19 +279,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`
|
||||||
|
|
||||||
@@ -283,6 +322,29 @@ const articles = await store.article.listExport(42, null);
|
|||||||
// => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }]
|
// => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `article.listHistory`
|
||||||
|
|
||||||
|
*Source: `src/store/article/listHistory.js`*
|
||||||
|
|
||||||
|
Lists article history entries for the user's selected company, supporting pagination, date cutoff via seekMonths, and search.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').Provider` | |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{history: Array<{id: number, date: string, sku: string, name: string|null, historytext: string}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const res = await store.article.listHistory(googleAuthId, { offset: 0, limit: 50, search: 'hjg', seekMonths: -13 });
|
||||||
|
// => { history: [...], total, unfilteredTotal, offset, limit }
|
||||||
|
```
|
||||||
|
|
||||||
## `article.listSearchIndex`
|
## `article.listSearchIndex`
|
||||||
|
|
||||||
*Source: `src/store/article/listSearchIndex.js`*
|
*Source: `src/store/article/listSearchIndex.js`*
|
||||||
@@ -342,22 +404,44 @@ 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. |
|
||||||
|
| `[options.cascade=true]` | `boolean` | Whether to cascade delete child subtrees when deleteArticle is true. |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>`
|
`Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>`
|
||||||
|
|
||||||
|
## `article.resolveUniqueSlug`
|
||||||
|
|
||||||
|
*Source: `src/store/article/resolveUniqueSlug.js`*
|
||||||
|
|
||||||
|
Resolves a company- and language-unique slug for an article. Disambiguates duplicate slugs with a numeric suffix (-2, -3, ...).
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `runner` | `object` | DB runner or tx. |
|
||||||
|
| `params` | `object` | |
|
||||||
|
| `params.companyId` | `number` | The company ID. |
|
||||||
|
| `params.languageId` | `number` | The language ID. |
|
||||||
|
| `params.name` | `string` | The article name. |
|
||||||
|
| `[params.excludeArticleId]` | `number` | Optional article ID to exclude (e.g. for updates). |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<string>` — The unique slug.
|
||||||
|
|
||||||
## `article.setBrandIdBySku`
|
## `article.setBrandIdBySku`
|
||||||
|
|
||||||
@@ -449,11 +533,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**
|
||||||
|
|
||||||
@@ -461,7 +545,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**
|
||||||
@@ -470,13 +554,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`
|
||||||
@@ -491,7 +575,7 @@ Replaces the price list of an article by its SKU.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `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. |
|
||||||
| `prices` | `Array<{price: number, tax: number, minmultiplier?: number}>` | The price entries (minmultiplier defaults to 1). |
|
| `prices` | `Array<{net?: number, gross?: number, price?: number, taxRate?: number, tax?: number, minmultiplier?: number}>` | The price entries (net or gross, taxRate or tax percentage, minmultiplier defaults to 1). |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -505,7 +589,9 @@ Replaces the price list of an article by its SKU.
|
|||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]);
|
const article = await store.article.setPricesBySku(42, 'SKU-1', [{ net: 10.0, taxRate: 19, minmultiplier: 1 }]);
|
||||||
|
// or with gross:
|
||||||
|
const article = await store.article.setPricesBySku(42, 'SKU-1', [{ gross: 11.9, taxRate: 19 }]);
|
||||||
// => { id, sku, name, ... }
|
// => { id, sku, name, ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -513,7 +599,7 @@ const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, t
|
|||||||
|
|
||||||
*Source: `src/store/article/setTrackStockBySku.js`*
|
*Source: `src/store/article/setTrackStockBySku.js`*
|
||||||
|
|
||||||
Sets the track_stock flag of an article by its SKU.
|
Sets the track_stock flag of an article by its SKU. If stock tracking is being disabled (1 -> 0), all non-zero warehouse stocks are adjusted to 0 on the ledger before disabling tracking.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -530,14 +616,7 @@ Sets the track_stock flag of an article by its SKU.
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If trackStock is invalid or the article is not found.
|
`Error` — If trackStock is invalid, article not found, or no warehouse exists when enabling.
|
||||||
|
|
||||||
**Example**
|
|
||||||
|
|
||||||
```js
|
|
||||||
const article = await store.article.setTrackStockBySku(42, 'SKU-1', true);
|
|
||||||
// => { id, sku, name, track_stock, ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
## `article.setWeightBySku`
|
## `article.setWeightBySku`
|
||||||
|
|
||||||
@@ -569,11 +648,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**
|
||||||
|
|
||||||
@@ -582,9 +661,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**
|
||||||
@@ -605,6 +684,8 @@ Updates an article's name and/or unit (SI and amount), recording history.
|
|||||||
| `sku` | `string` | The article SKU. |
|
| `sku` | `string` | The article SKU. |
|
||||||
| `patch` | `object` | The update patch. |
|
| `patch` | `object` | The update patch. |
|
||||||
| `[patch.name]` | `string` | The new article name. |
|
| `[patch.name]` | `string` | The new article name. |
|
||||||
|
| `[patch.desc]` | `string` | The article description text/HTML. |
|
||||||
|
| `[patch.seo]` | `string` | The article SEO text. |
|
||||||
| `[patch.unitSi]` | `string` | The new unit SI code. |
|
| `[patch.unitSi]` | `string` | The new unit SI code. |
|
||||||
| `[patch.unitamount]` | `number` | The new unit amount (must be > 0). |
|
| `[patch.unitamount]` | `number` | The new unit amount (must be > 0). |
|
||||||
| `[patch.track_stock]` | `boolean|number` | Whether inventory stock is tracked. |
|
| `[patch.track_stock]` | `boolean|number` | Whether inventory stock is tracked. |
|
||||||
|
|||||||
9
API.articlehistory.md
Normal file
9
API.articlehistory.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# articlehistory
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `articlehistory.record`
|
||||||
|
|
||||||
|
*Source: `src/store/articlehistory/record.js`*
|
||||||
|
|
||||||
|
============================================================================ ARTICLE HISTORY LOGGER ============================================================================ Recorded History Event Types (`meta.type`): 1. Orders & Inventory Ledger - `order` : Customer order placement (`recordOrderHistory`) - `acceptance` : Incoming supplier delivery without warehouse (`recordAcceptanceHistory`) - `shipment` : Outgoing customer shipment (`recordShipmentHistory`) - `warehouse_in` : Inbound warehouse movement (`recordWarehouseHistory`) - `warehouse_out` : Outbound warehouse movement (`recordWarehouseHistory`) - `delivery` : Legacy incoming delivery with warehouse (`recordDeliveryHistory`) - `movement` : Legacy generic warehouse movement (`recordDeliveryHistory`) - `stock_adjustment` : Manual stock correction (`recordStockAdjustmentHistory`) 2. Core Article Lifecycle & Master Data - `article_created` : Initial article creation (`recordArticleCreatedHistory`) - `article_imported` : Article created via import (`recordArticleImportedHistory`) - `article_update` : General field updates (`recordArticleUpdateHistory`) - `price_change` : Price tier / net / tax modifications (`recordPriceChangeHistory`) - `category_change` : Category assignment changes (`recordCategoryChangeHistory`) - `brand_change` : Brand name updates (`recordBrandChangeHistory`) - `ean_change` : EAN list updates (`recordEanChangeHistory`) - `weight_change` : Article weight changes (`recordWeightChangeHistory`) - `track_stock_change` : Stock tracking toggle (`recordTrackStockChangeHistory`) 3. Variants & Parent/Child Relationships - `parent_converted` : Article converted to parent (`recordParentConvertedHistory`) - `parent_deactivated` : Parent mode disabled (`recordParentDeactivatedHistory`) - `variant_added_to_parent` : Variant linked on parent record (`recordVariantAddedOnParentHistory`) - `child_created_under_parent` : New variant created under parent (`recordChildAssignedToParentHistory`) - `child_assigned_to_parent` : Existing article assigned as variant (`recordChildAssignedToParentHistory`) - `child_unlinked` : Child unlinked from parent - child log (`recordChildUnlinkedFromParentHistory`) - `variant_separated` : Variant separated from parent - parent log (`recordChildUnlinkedFromParentHistory`) - `variant_deleted` : Variant deleted from parent (`recordChildDeletedFromParentHistory`) 4. Suppliers - `supplier_add` : Supplier linked to article (`recordSupplierAddHistory`) - `supplier_update` : Supplier details modified (`recordSupplierUpdateHistory`) - `supplier_delete` : Supplier unlinked from article (`recordSupplierDeleteHistory`) 5. Properties, Attributes & Bills of Materials (BOM) - `property_change` : Property key/value changes (`recordPropertyChangeHistory`) - `property_delete` : Single property removed (`recordPropertyDeleteHistory`) - `attribute_change` : Variant/article attribute changes (`recordAttributeChangeHistory`) - `articlelist_change` : Stückliste / component BOM set (`recordArticleListChangeHistory`) - `optionslist_change` : Optionsliste set (`recordOptionsListChangeHistory`) 6. Media & Images - `image_add` : Image uploaded/linked (`recordImageAddHistory`) - `image_delete` : Image removed (`recordImageDeleteHistory`) - `image_order` : Image gallery reordered (`recordImageOrderHistory`) ============================================================================
|
||||||
@@ -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**
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Assigns a supplier to an article, optionally creating a purchase price.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{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}>` — The created assignment.
|
`Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>` — The created assignment.
|
||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ const created = await store.articlesupplier.create(42, 'SKU-1', {
|
|||||||
pu: 1,
|
pu: 1,
|
||||||
netPrice: 4.20,
|
netPrice: 4.20,
|
||||||
});
|
});
|
||||||
// => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }
|
// => { id, articleId, entityId, supplierName, sku, pu, stock, sort, price }
|
||||||
```
|
```
|
||||||
|
|
||||||
## `articlesupplier.delete`
|
## `articlesupplier.delete`
|
||||||
@@ -90,7 +90,7 @@ Lists the supplier assignments for an article by its SKU.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<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}>>` — The supplier assignments.
|
`Promise<Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>>` — The supplier assignments.
|
||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
@@ -100,14 +100,14 @@ Lists the supplier assignments for an article by its SKU.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
const suppliers = await store.articlesupplier.listByArticleSku(42, 'SKU-1');
|
const suppliers = await store.articlesupplier.listByArticleSku(42, 'SKU-1');
|
||||||
// => [{ id, articleId, entityId, supplierName, sku, pu, stock, priceId, price }]
|
// => [{ id, articleId, entityId, supplierName, sku, pu, stock, price }]
|
||||||
```
|
```
|
||||||
|
|
||||||
## `articlesupplier.listByEntityAndSkus`
|
## `articlesupplier.listByEntityAndSkus`
|
||||||
|
|
||||||
*Source: `src/store/articlesupplier/listByEntityAndSkus.js`*
|
*Source: `src/store/articlesupplier/listByEntityAndSkus.js`*
|
||||||
|
|
||||||
Lists article-supplier assignments for a given supplier (entityId) and SKU list, including linked article details (name, slug, photos/articlepic, eans/articleean).
|
Lists article-supplier assignments for a given supplier (entityId) and SKU list, including linked article details (name, slug, photos, eans).
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ Lists article-supplier assignments for a given supplier (entityId) and SKU list,
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<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, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, articlepic: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, eans: string[], articleean: string[]}}>>` — The supplier assignments with article details.
|
`Promise<Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[]}}>>` — The supplier assignments with article details.
|
||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ Updates an existing supplier assignment and its associated purchase price.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{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}>` — The updated assignment.
|
`Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>` — The updated assignment.
|
||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
@@ -203,5 +203,5 @@ const updated = await store.articlesupplier.update(42, 'SKU-1', 12, {
|
|||||||
pu: 6,
|
pu: 6,
|
||||||
netPrice: 22.50,
|
netPrice: 22.50,
|
||||||
});
|
});
|
||||||
// => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }
|
// => { id, articleId, entityId, supplierName, sku, pu, stock, sort, price }
|
||||||
```
|
```
|
||||||
|
|||||||
9
API.articlewarehousesummary.md
Normal file
9
API.articlewarehousesummary.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# articlewarehousesummary
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `articlewarehousesummary.syncSummary`
|
||||||
|
|
||||||
|
*Source: `src/store/articlewarehousesummary/syncSummary.js`*
|
||||||
|
|
||||||
|
Centralized sync and rebuild logic for the materialized `articlewarehousesummary` table. Keeps `articlewarehousesummary` in sync with ledger movements, track_stock status, and warehouses.
|
||||||
@@ -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`*
|
||||||
@@ -56,7 +64,7 @@ Lists the attribute names for the user's selected company, including values.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<Array<{id: number, name: string, sort: number, exclusive: boolean, values: Array<{id: number, value: string, sort: number}>}>>`
|
`Promise<Array<{id: number, name: string, sort: number, exclusive: boolean, articleCount: number, values: Array<{id: number, value: string, sort: number, articleCount: number}>}>>`
|
||||||
|
|
||||||
## `attributename.order`
|
## `attributename.order`
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
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`.
|
||||||
|
|
||||||
|
## `attributevalue.addImage`
|
||||||
|
|
||||||
|
*Source: `src/store/attributevalue/addImage.js`*
|
||||||
|
|
||||||
|
Adds an image to an attribute value, processing full, thumb, and 32px-edge small-thumb AVIF variants.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `attributeValueId` | `number` | The id of the attribute value. |
|
||||||
|
| `image` | `Buffer` | The source image buffer. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array<object>}>`
|
||||||
|
|
||||||
## `attributevalue.create`
|
## `attributevalue.create`
|
||||||
|
|
||||||
*Source: `src/store/attributevalue/create.js`*
|
*Source: `src/store/attributevalue/create.js`*
|
||||||
@@ -23,6 +42,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 +64,32 @@ Deletes an attribute value.
|
|||||||
|
|
||||||
`Promise<void>`
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If attribute value not found or in use.
|
||||||
|
|
||||||
|
## `attributevalue.deleteImage`
|
||||||
|
|
||||||
|
*Source: `src/store/attributevalue/deleteImage.js`*
|
||||||
|
|
||||||
|
Deletes the image of an attribute value.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `attributeValueId` | `number` | The id of the attribute value. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null, pics: Array}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If attribute value not found or image not found.
|
||||||
|
|
||||||
## `attributevalue.listByAttributeId`
|
## `attributevalue.listByAttributeId`
|
||||||
|
|
||||||
*Source: `src/store/attributevalue/listByAttributeId.js`*
|
*Source: `src/store/attributevalue/listByAttributeId.js`*
|
||||||
@@ -57,7 +106,7 @@ Lists the values for an attribute.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<Array<{id: number, attributeId: number, value: string, sort: number}>>`
|
`Promise<Array<{id: number, attributeId: number, value: string, sort: number, articleCount: number}>>`
|
||||||
|
|
||||||
## `attributevalue.order`
|
## `attributevalue.order`
|
||||||
|
|
||||||
@@ -99,3 +148,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.
|
||||||
|
|||||||
86
API.autoinvite.md
Normal file
86
API.autoinvite.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# autoinvite
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `autoinvite.add`
|
||||||
|
|
||||||
|
*Source: `src/store/autoinvite/add.js`*
|
||||||
|
|
||||||
|
Registers an email for automatic company membership. When that email is registered (`google_auth.add`) the invitation is consumed: it is removed from the list and the new user is added to the company with the given role.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
|
||||||
|
| `input` | `{email: string, role?: 'admin'|'user'}` | The email to invite and the role (default 'user'). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, email: string, role: 'admin'|'user'}>` — The created invitation.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Not authorized', 'Invalid email', 'Invalid role', 'User already registered', 'Invitation already exists'
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.autoinvite.add(42, { email: 'new@example.com', role: 'user' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## `autoinvite.list`
|
||||||
|
|
||||||
|
*Source: `src/store/autoinvite/list.js`*
|
||||||
|
|
||||||
|
Lists the pending auto-invitations of the admin's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user (must be admin of the selected company). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, email: string, role: 'admin'|'user', createdAt: string}>>` — The invitations ordered by email.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Not authorized' / 'No company selected'
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const invites = await store.autoinvite.list(42);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `autoinvite.remove`
|
||||||
|
|
||||||
|
*Source: `src/store/autoinvite/remove.js`*
|
||||||
|
|
||||||
|
Deletes a pending auto-invitation of the admin's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
|
||||||
|
| `id` | `number` | The invitation id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Not authorized', 'Invitation not found'
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.autoinvite.remove(42, 3);
|
||||||
|
```
|
||||||
81
API.brand.md
81
API.brand.md
@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
|||||||
|
|
||||||
*Source: `src/store/brand/addImage.js`*
|
*Source: `src/store/brand/addImage.js`*
|
||||||
|
|
||||||
Adds an image to a brand, processing it into AVIF full and thumb variants.
|
Adds an image to a brand, processing full, thumb, and 32px-edge small-thumb AVIF variants.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -19,7 +19,38 @@ Adds an image to a brand, processing it into AVIF full and thumb variants.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array<object>}>`
|
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array<object>}>`
|
||||||
|
|
||||||
|
## `brand.create`
|
||||||
|
|
||||||
|
*Source: `src/store/brand/create.js`*
|
||||||
|
|
||||||
|
Creates a brand for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | The brand input. |
|
||||||
|
| `input.name` | `string` | The brand name. |
|
||||||
|
| `[input.gsprpinfo]` | `string` | Optional GSPRP info. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The created brand.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the brand is invalid or already exists.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const brand = await store.brand.create(42, { name: 'Coca-Cola', gsprpinfo: 'x' });
|
||||||
|
// => { id, name, gsprpinfo, pics: [] }
|
||||||
|
```
|
||||||
|
|
||||||
## `brand.delete`
|
## `brand.delete`
|
||||||
|
|
||||||
@@ -53,24 +84,60 @@ await store.brand.delete(42, 3);
|
|||||||
|
|
||||||
*Source: `src/store/brand/list.js`*
|
*Source: `src/store/brand/list.js`*
|
||||||
|
|
||||||
Lists the brands of the user's selected company.
|
Lists the brands of the user's selected company, sorted by name.
|
||||||
|
|
||||||
**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. |
|
||||||
|
| `[opts]` | `object` | Optional pagination options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of brands to skip. |
|
||||||
|
| `[opts.limit=10]` | `number` | Number of brands to return (max 100). |
|
||||||
|
| `[opts.includeSmallThumbs=false]` | `boolean` | Include 32px-edge small thumbnail dimensions and base64 AVIF data. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>>` — The brands.
|
`Promise<{brands: Array<{id: number, name: string, articleCount: number, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW?: number|null, smallThumbH?: number|null, smallThumbBinary?: string|null}>}>, total: number, offset: number, limit: number}>` — The brands plus the total unpaged count. Small-thumbnail fields are present only when `includeSmallThumbs` is true; the binary is base64-encoded AVIF data.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const brands = await store.brand.list(42);
|
const result = await store.brand.list(42, { offset: 0, limit: 10 });
|
||||||
// => [{ id, name, gsprpinfo, pics: [{ sort, fullHash, thumbW, thumbH, thumbHash }] }]
|
// => { brands: [{ id, name, articleCount, gsprpinfo, pics: [...] }], total: 25, offset: 0, limit: 10 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## `brand.update`
|
||||||
|
|
||||||
|
*Source: `src/store/brand/update.js`*
|
||||||
|
|
||||||
|
Renames/updates a brand for the user's selected company by id. Touching `article.updated_at` for all articles linked via `brand_id` when the name actually changes, so list caches invalidate.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the brand to update. |
|
||||||
|
| `input` | `object` | The update input. |
|
||||||
|
| `[input.name]` | `string` | The new brand name. |
|
||||||
|
| `[input.gsprpinfo]` | `string` | The new GSPRP info. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, name: string, gsprpinfo: string|null, touchedArticleCount: number}>` — The updated brand plus the number of linked articles whose `updated_at` was bumped.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the brand is not found, invalid, or the name is already taken.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const brand = await store.brand.update(42, 3, { name: 'Coca-Cola GmbH' });
|
||||||
|
// => { id: 3, name, gsprpinfo, touchedArticleCount: 2 }
|
||||||
```
|
```
|
||||||
|
|
||||||
## `brand.upsert`
|
## `brand.upsert`
|
||||||
@@ -91,7 +158,7 @@ Creates or updates a brand for the user's selected company, keyed by name.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The upserted brand.
|
`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The upserted brand.
|
||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
|
|||||||
176
API.carrier.md
Normal file
176
API.carrier.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# carrier
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `carrier.create`
|
||||||
|
|
||||||
|
*Source: `src/store/carrier/create.js`*
|
||||||
|
|
||||||
|
Creates a new carrier for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | Carrier input. |
|
||||||
|
| `input.name` | `string` | The carrier name. |
|
||||||
|
| `[input.sort]` | `number` | Optional sort order (auto-increments if omitted). |
|
||||||
|
| `[input.hasPackages=false]` | `boolean` | Whether this carrier uses packages. |
|
||||||
|
| `[input.isReverse=false]` | `boolean` | Whether this carrier handles reverse shipments. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>` — The created carrier.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If name is missing, sort is invalid, or carrier already exists.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const c = await store.carrier.create(googleAuthId, { name: 'DHL', hasPackages: true });
|
||||||
|
// => { id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }
|
||||||
|
```
|
||||||
|
|
||||||
|
## `carrier.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/carrier/delete.js`*
|
||||||
|
|
||||||
|
Deletes a carrier for the user's selected company, unless referenced in shipping.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The carrier ID to delete. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If carrier not found or in use in shipping.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.carrier.delete(googleAuthId, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `carrier.getById`
|
||||||
|
|
||||||
|
*Source: `src/store/carrier/getById.js`*
|
||||||
|
|
||||||
|
Retrieves a carrier by its ID for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The carrier ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}|undefined>` — The carrier or undefined.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const c = await store.carrier.getById(googleAuthId, 1);
|
||||||
|
// => { id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }
|
||||||
|
```
|
||||||
|
|
||||||
|
## `carrier.list`
|
||||||
|
|
||||||
|
*Source: `src/store/carrier/list.js`*
|
||||||
|
|
||||||
|
Lists all carriers for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>>` — List of carriers.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const carriers = await store.carrier.list(googleAuthId);
|
||||||
|
// => [{ id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `carrier.order`
|
||||||
|
|
||||||
|
*Source: `src/store/carrier/order.js`*
|
||||||
|
|
||||||
|
Reorders the carriers of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | The reorder input. |
|
||||||
|
| `input.carrierIds` | `Array<number>` | The carrier ids in their new order. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{carrierIds: Array<number>}>` — The applied order.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the order is invalid.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.carrier.order(googleAuthId, { carrierIds: [2, 1, 3] });
|
||||||
|
```
|
||||||
|
|
||||||
|
## `carrier.update`
|
||||||
|
|
||||||
|
*Source: `src/store/carrier/update.js`*
|
||||||
|
|
||||||
|
Updates a carrier name, sort order, hasPackages, and/or isReverse for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The carrier ID to update. |
|
||||||
|
| `input` | `object` | Update payload. |
|
||||||
|
| `[input.name]` | `string` | The new carrier name. |
|
||||||
|
| `[input.sort]` | `number` | The new sort order. |
|
||||||
|
| `[input.hasPackages]` | `boolean` | Whether carrier uses packages. |
|
||||||
|
| `[input.isReverse]` | `boolean` | Whether carrier handles reverse shipments. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>` — The updated carrier.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If carrier not found, name is empty, sort is invalid, or carrier already exists.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const updated = await store.carrier.update(googleAuthId, 1, { name: 'DHL Express', hasPackages: true });
|
||||||
|
// => { id: 1, name: 'DHL Express', sort: 0, hasPackages: true, isReverse: false }
|
||||||
|
```
|
||||||
@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
|||||||
|
|
||||||
*Source: `src/store/category/addImage.js`*
|
*Source: `src/store/category/addImage.js`*
|
||||||
|
|
||||||
Adds an image to a category, processing it into AVIF full and thumb variants.
|
Adds an image to a category, processing full, thumb, and 32px-edge small-thumb AVIF variants.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ Adds an image to a category, processing it into AVIF full and thumb variants.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>` — The added image info.
|
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number}>` — The added image info.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ const category = await store.category.createUnderParent({ googleAuthId: 42, pare
|
|||||||
|
|
||||||
*Source: `src/store/category/delete.js`*
|
*Source: `src/store/category/delete.js`*
|
||||||
|
|
||||||
Deletes a category, refusing to delete the company's root category.
|
Deletes a category, refusing to delete the company's root category. If recursive is true, recursively deletes the subtree and articles that only belong to it.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ Deletes a category, refusing to delete the company's root category.
|
|||||||
| `input` | `object` | The delete input. |
|
| `input` | `object` | The delete input. |
|
||||||
| `input.googleAuthId` | `number` | The id of the Google auth user. |
|
| `input.googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
| `input.id` | `number` | The id of the category to delete. |
|
| `input.id` | `number` | The id of the category to delete. |
|
||||||
|
| `[input.recursive=false]` | `boolean` | Whether to delete the category subtree recursively. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -76,12 +77,13 @@ Deletes a category, refusing to delete the company's root category.
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If the category is the root category.
|
`Error` — If the category is the root category or contains articles/subcategories (when recursive is false).
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await store.category.delete({ googleAuthId: 42, id: 5 });
|
await store.category.delete({ googleAuthId: 42, id: 5 });
|
||||||
|
await store.category.delete({ googleAuthId: 42, id: 5, recursive: true });
|
||||||
```
|
```
|
||||||
|
|
||||||
## `category.deleteImage`
|
## `category.deleteImage`
|
||||||
@@ -140,6 +142,37 @@ const id = await store.category.ensureByPath({ googleAuthId: 42, parentId: 5, pa
|
|||||||
// => 12
|
// => 12
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `category.generateImage`
|
||||||
|
|
||||||
|
*Source: `src/store/category/generateImage.js`*
|
||||||
|
|
||||||
|
Generates a mosaic image directly from the category's and its subcategories' articles (grouped by subcategory) and sets it as the new main picture for the category.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `inputOrCategoryId` | `number|object` | The id of the category or an options object. |
|
||||||
|
| `[maybeOptsOrSize={}]` | `object|number` | Options object or numeric size. |
|
||||||
|
| `[maybeOptsOrSize.size]` | `number` | Grid size (2 for 2x2, 3 for 3x3, 4 for 4x4). Defaults to 3 (falls back to 2 if < 7 images). |
|
||||||
|
| `[maybeTx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, size: number, grid: string, articleCount: number}>` — The new main image info.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the category is not found, not authorized, size is invalid, or no images are found.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const result = await store.category.generateImage(42, 5, { size: 3 });
|
||||||
|
// => { id: 5, fullHash, thumbW, thumbH, thumbHash, smallThumbW, smallThumbH, size: 3, grid: '3x3', articleCount: 12 }
|
||||||
|
```
|
||||||
|
|
||||||
## `category.getCategoryArticles`
|
## `category.getCategoryArticles`
|
||||||
|
|
||||||
*Source: `src/store/category/getCategoryArticles.js`*
|
*Source: `src/store/category/getCategoryArticles.js`*
|
||||||
@@ -156,7 +189,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, smallThumbW: number|null, smallThumbH: number|null, smallThumbBinary: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, articlewarehousesummary: Array<{id: number, articleId: number, warehouseId: number, warehouseName: string, stock: number}>, warehouses: Array<{id: number, name: string, stock: number}>}>>` — The articles in the category.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
@@ -169,7 +202,7 @@ const articles = await store.category.getCategoryArticles(42, 5);
|
|||||||
|
|
||||||
*Source: `src/store/category/getTreeForCompany.js`*
|
*Source: `src/store/category/getTreeForCompany.js`*
|
||||||
|
|
||||||
Returns the full category tree for the user's selected company together with its articles and the posterminal ids assigned to each category.
|
Returns the full category tree for the user's selected company together with the article count and the posterminal ids assigned to each category.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -180,13 +213,13 @@ Returns the full category tree for the user's selected company together with its
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{languageId: number, tree: Array<object>, articles: Array<object>}>` — The category tree, articles and language.
|
`Promise<{languageId: number, tree: Array<{id: number, parentId: number|null, sort: number, name: string|null, slug: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, articleCount: number, posterminalIds: number[]}>, articleCount: number}>` — The category tree and article count.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const tree = await store.category.getTreeForCompany(42);
|
const tree = await store.category.getTreeForCompany(42);
|
||||||
// => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] }
|
// => { languageId, tree: [{ id, articleCount, posterminalIds, ... }], articleCount: 25 }
|
||||||
```
|
```
|
||||||
|
|
||||||
## `category.move`
|
## `category.move`
|
||||||
|
|||||||
40
API.categoryattributevalue.md
Normal file
40
API.categoryattributevalue.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# categoryattributevalue
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `categoryattributevalue.listByCategoryId`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryattributevalue/listByCategoryId.js`*
|
||||||
|
|
||||||
|
Lists the attribute values assigned to a category by its ID.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
|
||||||
|
|
||||||
|
## `categoryattributevalue.setByCategoryId`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryattributevalue/setByCategoryId.js`*
|
||||||
|
|
||||||
|
Replaces all attribute values for a category by its ID. Validates company ownership and enforces the `exclusive` flag on attributes.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `values` | `Array<number|{attributeValueId: number, sort?: number}>` | Attribute value IDs or items. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
|
||||||
59
API.categoryproperty.md
Normal file
59
API.categoryproperty.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# categoryproperty
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `categoryproperty.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryproperty/delete.js`*
|
||||||
|
|
||||||
|
Deletes a property from a category by categoryId and propertyId.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `propertyId` | `number` | The property id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
## `categoryproperty.listByCategoryId`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryproperty/listByCategoryId.js`*
|
||||||
|
|
||||||
|
Lists the properties of a category by its ID.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||||
|
|
||||||
|
## `categoryproperty.setByCategoryId`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryproperty/setByCategoryId.js`*
|
||||||
|
|
||||||
|
Replaces all properties for a category by its ID. Enforces exclusive flag on property names.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `properties` | `Array<{propertyId: number, value: string, sort?: number}>` | The properties to set. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||||
@@ -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`*
|
||||||
@@ -14,7 +44,7 @@ Creates a new company together with its root category, POS customer group, defau
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `input` | `object` | The company creation input. |
|
| `input` | `object` | The company creation input. |
|
||||||
| `input.iso639` | `string` | The ISO 639 language code for the company. |
|
| `input.iso639` | `string` | The ISO 639 language code for the company. |
|
||||||
| `input.name` | `string` | The company name (must be unique). |
|
| `input.name` | `string` | The company name. |
|
||||||
| `input.taxrate` | `number` | The standard tax rate in percent. |
|
| `input.taxrate` | `number` | The standard tax rate in percent. |
|
||||||
| `input.reducedtaxrate` | `number` | The reduced tax rate in percent. |
|
| `input.reducedtaxrate` | `number` | The reduced tax rate in percent. |
|
||||||
| `input.googleAuthId` | `number` | The id of the Google auth user to link. |
|
| `input.googleAuthId` | `number` | The id of the Google auth user to link. |
|
||||||
@@ -24,10 +54,6 @@ Creates a new company together with its root category, POS customer group, defau
|
|||||||
|
|
||||||
`Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>` — The created company ids.
|
`Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>` — The created company ids.
|
||||||
|
|
||||||
**Throws**
|
|
||||||
|
|
||||||
`Error` — If the company name is already taken.
|
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -112,6 +138,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`*
|
||||||
@@ -127,13 +182,13 @@ Lists the companies accessible to the given Google auth user.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<Array<object>>` — The accessible company rows.
|
`Promise<Array<{id: number, name: string, rootCategoryId: number, languageId: number, iso639: string|null, taxrate: number, reducedtaxrate: number, taxfreerate: number}>>` — The accessible company rows.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const companies = await store.company.list(42);
|
const companies = await store.company.list(42);
|
||||||
// => [{ id, name, ... }]
|
// => [{ id, name, rootCategoryId, languageId, iso639, taxrate, reducedtaxrate, taxfreerate }]
|
||||||
```
|
```
|
||||||
|
|
||||||
## `company.update`
|
## `company.update`
|
||||||
@@ -149,7 +204,7 @@ Updates an existing company's name, language and tax rates.
|
|||||||
| `id` | `number` | The id of the company to update. |
|
| `id` | `number` | The id of the company to update. |
|
||||||
| `input` | `object` | The update input. |
|
| `input` | `object` | The update input. |
|
||||||
| `input.iso639` | `string` | The ISO 639 language code. |
|
| `input.iso639` | `string` | The ISO 639 language code. |
|
||||||
| `input.name` | `string` | The new company name (must be unique). |
|
| `input.name` | `string` | The new company name. |
|
||||||
| `input.taxrate` | `number` | The standard tax rate in percent. |
|
| `input.taxrate` | `number` | The standard tax rate in percent. |
|
||||||
| `input.reducedtaxrate` | `number` | The reduced tax rate in percent. |
|
| `input.reducedtaxrate` | `number` | The reduced tax rate in percent. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
@@ -160,7 +215,7 @@ Updates an existing company's name, language and tax rates.
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If the company is not found or the name is taken.
|
`Error` — If the company is not found.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
|||||||
|
|
||||||
*Source: `src/store/companyuser/add.js`*
|
*Source: `src/store/companyuser/add.js`*
|
||||||
|
|
||||||
Adds a company user linking a company to a Google auth account.
|
Adds a company user linking a company to a Google auth account. No admin check: this is the low-level link primitive (company creation, invitations). Use `companyuser.addByEmail` for the admin-guarded flow.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -14,19 +14,52 @@ Adds a company user linking a company to a Google auth account.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `companyId` | `number` | The id of the company. |
|
| `companyId` | `number` | The id of the company. |
|
||||||
| `googleAuthId` | `number` | The id of the Google auth account. |
|
| `googleAuthId` | `number` | The id of the Google auth account. |
|
||||||
|
| `[role='user']` | `'admin'|'user'` | The role within the company. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<number>` — The id of the created company user link.
|
`Promise<number>` — The id of the created company user link.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Invalid role' if the role is not 'admin' or 'user'.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const id = await store.companyuser.add(1, 42);
|
const id = await store.companyuser.add(1, 42, 'admin');
|
||||||
// => 7
|
// => 7
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `companyuser.addByEmail`
|
||||||
|
|
||||||
|
*Source: `src/store/companyuser/addByEmail.js`*
|
||||||
|
|
||||||
|
Adds an existing Google auth user to the admin's selected company. The added user can then select the company as their own selected company. For emails that are not registered yet use `autoinvite.add`.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
|
||||||
|
| `input` | `{email: string, role?: 'admin'|'user'}` | Email of the existing user and role (default 'user'). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>` — The created company user.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Not authorized', 'Invalid email', 'Invalid role', 'User not found', 'User already in company'
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const member = await store.companyuser.addByEmail(42, { email: 'new@example.com', role: 'user' });
|
||||||
|
```
|
||||||
|
|
||||||
## `companyuser.delete`
|
## `companyuser.delete`
|
||||||
|
|
||||||
*Source: `src/store/companyuser/delete.js`*
|
*Source: `src/store/companyuser/delete.js`*
|
||||||
@@ -73,3 +106,134 @@ Returns the company user ids associated with the given email address.
|
|||||||
const ids = await store.companyuser.getByEmail('user@example.com');
|
const ids = await store.companyuser.getByEmail('user@example.com');
|
||||||
// => [1, 2, 3]
|
// => [1, 2, 3]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `companyuser.list`
|
||||||
|
|
||||||
|
*Source: `src/store/companyuser/list.js`*
|
||||||
|
|
||||||
|
Lists the members of the admin's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user (must be admin of the selected company). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>>` — The company users ordered by email.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Not authorized' / 'No company selected'
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const members = await store.companyuser.list(42);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `companyuser.remove`
|
||||||
|
|
||||||
|
*Source: `src/store/companyuser/remove.js`*
|
||||||
|
|
||||||
|
Removes a member from the admin's selected company. History entries keep existing but lose their author link; if the removed user had this company selected, their selection is cleared.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
|
||||||
|
| `id` | `number` | The company user id to remove. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, googleAuthId: number}>` — The removed company user.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Not authorized', 'Company user not found', 'Cannot remove the last admin'
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.companyuser.remove(42, 7);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `companyuser.requireAdmin`
|
||||||
|
|
||||||
|
*Source: `src/store/companyuser/requireAdmin.js`*
|
||||||
|
|
||||||
|
Resolves the selected company and requires the user to be its admin.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | |
|
||||||
|
| `[tx]` | `object` | |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{companyId: number, companyuserId: number, role: string}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Not authorized' if the user is not an admin of the selected company.
|
||||||
|
|
||||||
|
## `companyuser.roles`
|
||||||
|
|
||||||
|
*Source: `src/store/companyuser/roles.js`*
|
||||||
|
|
||||||
|
Validates a company user role.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `role` | `unknown` | |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`'admin'|'user'`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Invalid role'
|
||||||
|
|
||||||
|
## `companyuser.setRole`
|
||||||
|
|
||||||
|
*Source: `src/store/companyuser/setRole.js`*
|
||||||
|
|
||||||
|
Changes the role of a member of the admin's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
|
||||||
|
| `id` | `number` | The company user id. |
|
||||||
|
| `role` | `'admin'|'user'` | The new role. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>` — The updated company user.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Not authorized', 'Invalid role', 'Company user not found', 'Cannot demote the last admin'
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.companyuser.setRole(42, 7, 'admin');
|
||||||
|
```
|
||||||
|
|
||||||
|
## `companyuser.withTransaction`
|
||||||
|
|
||||||
|
*Source: `src/store/companyuser/withTransaction.js`*
|
||||||
|
|
||||||
|
Runs `fn(runner)` inside the given transaction, a new one, or (providers without transaction support) directly.
|
||||||
|
|||||||
@@ -7,3 +7,61 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
|||||||
*Source: `src/store/delivery/attachments.js`*
|
*Source: `src/store/delivery/attachments.js`*
|
||||||
|
|
||||||
Loads attachment metadata (without binary content) for a manifest.
|
Loads attachment metadata (without binary content) for a manifest.
|
||||||
|
|
||||||
|
## `delivery.book`
|
||||||
|
|
||||||
|
*Source: `src/store/delivery/book.js`*
|
||||||
|
|
||||||
|
Books all positions of an incoming delivery manifest to the ledger by delegating to store.stock.accept.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `object` | Storage provider instance. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Function` — `(googleAuthId, input, tx) => Promise<object>`
|
||||||
|
|
||||||
|
## `delivery.list`
|
||||||
|
|
||||||
|
*Source: `src/store/delivery/list.js`*
|
||||||
|
|
||||||
|
Lists the deliveries of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of deliveries to skip. |
|
||||||
|
| `[opts.limit=50]` | `number` | Number of deliveries to return. |
|
||||||
|
| `[opts.search]` | `string` | Free-text search matching delivery number, external number, or supplier name. |
|
||||||
|
| `[opts.onlyUnbooked=false]` | `boolean` | When true, returns only deliveries with unbooked stock items. |
|
||||||
|
| `[opts.supplierId]` | `number` | Filter by supplier entity id. |
|
||||||
|
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{deliveries: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
|
||||||
|
## `delivery.rebuildSummaries`
|
||||||
|
|
||||||
|
*Source: `src/store/delivery/rebuildSummaries.js`*
|
||||||
|
|
||||||
|
Rebuilds the `deliverysummary` table for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `delivery.syncSummary`
|
||||||
|
|
||||||
|
*Source: `src/store/delivery/syncSummary.js`*
|
||||||
|
|
||||||
|
Centralized sync and rebuild logic for the materialized `deliverysummary` table. Keeps `deliverysummary` in sync with manifest (delivery), manifestarticle, article (track_stock), and ledger (DELIVERY transactions).
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Deletes an entity by its id within the user's selected company.
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If the entity is not found.
|
`Error` — If the entity is not found or is in use.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
|||||||
|
|
||||||
*Source: `src/store/google_auth/add.js`*
|
*Source: `src/store/google_auth/add.js`*
|
||||||
|
|
||||||
Adds a new allowed Google auth email.
|
Adds a new allowed Google auth email (registration). Pending `autoinvite` rows for this email are consumed in the same transaction: each is deleted and turned into a `companyuser` with the invited role.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ await store.google_auth.setProfile(42, { picture: 'url', pictureBinary: buffer,
|
|||||||
|
|
||||||
*Source: `src/store/google_auth/setSelectedCompany.js`*
|
*Source: `src/store/google_auth/setSelectedCompany.js`*
|
||||||
|
|
||||||
Sets the currently selected company for a Google auth user.
|
Sets the currently selected company for a Google auth user. The user must be a member (`companyuser`) of the company.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -157,6 +157,10 @@ Sets the currently selected company for a Google auth user.
|
|||||||
|
|
||||||
`Promise<void>`
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Not authorized' if the user is not a member of the company.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
26
API.healthcheck.md
Normal file
26
API.healthcheck.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# healthcheck
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `healthcheck.check`
|
||||||
|
|
||||||
|
*Source: `src/store/healthcheck/check.js`*
|
||||||
|
|
||||||
|
Executes a database health check: 1. Ensures the healthcheck table has one row (creates with sequence 0 if not). 2. Selects that row for update with minimal, reasonable locking within a transaction. 3. Updates the sequence by 1 in a separate statement within the transaction.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{ok: boolean, sequence: number}>` — The health check result.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const result = await store.healthcheck.check();
|
||||||
|
// => { ok: true, sequence: 1 }
|
||||||
|
```
|
||||||
@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
|||||||
|
|
||||||
*Source: `src/store/ledger/add.js`*
|
*Source: `src/store/ledger/add.js`*
|
||||||
|
|
||||||
Appends a new append-only entry to the ledger table.
|
Appends new append-only entry/entries to the ledger table according to manifest rules: - quote / order: no bookings possible (throws error) - delivery: acceptance entry (warehouse_id=null) + warehouse entry (if tracked) in one transaction - shipment: shipment entry (warehouse_id=null, negative amount) + warehouse entry (if tracked) in one transaction - adjustment: single warehouse inventory movement
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -23,23 +23,7 @@ Appends a new append-only entry to the ledger table.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>` — The created ledger row.
|
`Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>` — The primary ledger row.
|
||||||
|
|
||||||
**Throws**
|
|
||||||
|
|
||||||
`Error` — If user not authorized, company not selected, manifest article not found, or input invalid.
|
|
||||||
|
|
||||||
**Example**
|
|
||||||
|
|
||||||
```js
|
|
||||||
const entry = await store.ledger.add(googleAuthId, {
|
|
||||||
manifestarticle_id: 1,
|
|
||||||
warehouse_id: 1,
|
|
||||||
transaction_type: 'DELIVERY',
|
|
||||||
amount: 100.5,
|
|
||||||
transaction_text: 'Invoice paid',
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## `ledger.listByManifestarticleId`
|
## `ledger.listByManifestarticleId`
|
||||||
|
|
||||||
@@ -100,3 +84,26 @@ Lists ledger entries for a given article SKU, optionally filtered by transaction
|
|||||||
const entries = await store.ledger.listBySku(googleAuthId, 'SKU-001', 'DELIVERY');
|
const entries = await store.ledger.listBySku(googleAuthId, 'SKU-001', 'DELIVERY');
|
||||||
// => [{ id, manifestarticle_id, warehouse_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }]
|
// => [{ id, manifestarticle_id, warehouse_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `ledger.listHistory`
|
||||||
|
|
||||||
|
*Source: `src/store/ledger/listHistory.js`*
|
||||||
|
|
||||||
|
Lists ledger entries for the user's selected company across all articles, supporting pagination, date cutoff via seekMonths, and search.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').Provider` | |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{ledger: Array<{id: number, date: string, sku: string|null, name: string|null, transaction_type: string, transaction_text: string, amount: number}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const res = await store.ledger.listHistory(googleAuthId, { offset: 0, limit: 50, search: 'kjh', seekMonths: -1 });
|
||||||
|
// => { ledger: [...], total, unfilteredTotal, offset, limit }
|
||||||
|
```
|
||||||
|
|||||||
210
API.mcp.md
Normal file
210
API.mcp.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
# mcp
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `mcp.consumeAuthCode`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/consumeAuthCode.js`*
|
||||||
|
|
||||||
|
Marks an authorization code as used.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `code` | `string` | The authorization code to mark used. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<boolean>` — True once marked.
|
||||||
|
|
||||||
|
## `mcp.createAuthCode`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/createAuthCode.js`*
|
||||||
|
|
||||||
|
Stores an OAuth 2.1 authorization code with PKCE parameters.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.code` | `string` | The generated authorization code. |
|
||||||
|
| `input.clientId` | `string` | The client ID. |
|
||||||
|
| `input.googleAuthId` | `number` | The ID of the authenticated user. |
|
||||||
|
| `input.redirectUri` | `string` | The redirect URI used in the authorize request. |
|
||||||
|
| `input.codeChallenge` | `string` | The PKCE code challenge. |
|
||||||
|
| `input.codeChallengeMethod` | `string` | The PKCE method (e.g. S256). |
|
||||||
|
| `[input.scope]` | `string|null` | Granted scope. |
|
||||||
|
| `input.expiresAt` | `string|Date` | Expiration ISO string or Date. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<string>` — The authorization code.
|
||||||
|
|
||||||
|
## `mcp.createRefreshToken`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/createRefreshToken.js`*
|
||||||
|
|
||||||
|
Stores an OAuth 2.1 refresh token.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.refreshToken` | `string` | The refresh token string. |
|
||||||
|
| `input.clientId` | `string` | The client ID. |
|
||||||
|
| `input.googleAuthId` | `number` | The user ID. |
|
||||||
|
| `[input.scope]` | `string|null` | Scopes granted. |
|
||||||
|
| `input.expiresAt` | `string|Date` | Expiration Date or ISO string. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — Created token record or id.
|
||||||
|
|
||||||
|
## `mcp.deleteClient`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/deleteClient.js`*
|
||||||
|
|
||||||
|
Deletes a registered MCP client by client ID.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `clientId` | `string` | The client identifier to delete. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<boolean>` — True if deleted.
|
||||||
|
|
||||||
|
## `mcp.getAuthCode`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/getAuthCode.js`*
|
||||||
|
|
||||||
|
Retrieves an authorization code record.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `code` | `string` | The authorization code. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object|null>` — The auth code record or null.
|
||||||
|
|
||||||
|
## `mcp.getClient`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/getClient.js`*
|
||||||
|
|
||||||
|
Retrieves an MCP client by client ID.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `clientId` | `string` | The client identifier. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object|null>` — The client record or null if not found.
|
||||||
|
|
||||||
|
## `mcp.getRefreshToken`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/getRefreshToken.js`*
|
||||||
|
|
||||||
|
Retrieves an OAuth 2.1 refresh token.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `refreshToken` | `string` | The refresh token. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object|null>` — The refresh token record or null.
|
||||||
|
|
||||||
|
## `mcp.listAuthorizations`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/listAuthorizations.js`*
|
||||||
|
|
||||||
|
Lists active authorizations for a Google Auth user.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The user ID. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<object>>` — List of authorized clients and token counts.
|
||||||
|
|
||||||
|
## `mcp.listClients`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/listClients.js`*
|
||||||
|
|
||||||
|
Lists all registered MCP clients.
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<object>>` — List of client records.
|
||||||
|
|
||||||
|
## `mcp.registerClient`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/registerClient.js`*
|
||||||
|
|
||||||
|
Registers a new OAuth MCP client.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.clientId` | `string` | Unique client identifier. |
|
||||||
|
| `[input.clientSecret]` | `string|null` | Optional client secret for confidential clients. |
|
||||||
|
| `input.clientName` | `string` | Human-readable client name. |
|
||||||
|
| `input.redirectUris` | `string|Array<string>` | JSON string or array of redirect URIs. |
|
||||||
|
| `[input.grantTypes]` | `string|Array<string>` | Supported grant types. |
|
||||||
|
| `[input.responseTypes]` | `string|Array<string>` | Supported response types. |
|
||||||
|
| `[input.scope]` | `string|null` | Allowed scope. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The created client record.
|
||||||
|
|
||||||
|
## `mcp.revokeAuthorizations`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/revokeAuthorizations.js`*
|
||||||
|
|
||||||
|
Revokes all active authorizations (tokens) for a user and MCP client.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The user ID. |
|
||||||
|
| `clientId` | `string` | The MCP client ID. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<boolean>` — True once revoked.
|
||||||
|
|
||||||
|
## `mcp.revokeRefreshToken`
|
||||||
|
|
||||||
|
*Source: `src/store/mcp/revokeRefreshToken.js`*
|
||||||
|
|
||||||
|
Revokes an OAuth 2.1 refresh token.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `refreshToken` | `string` | The refresh token to revoke. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<boolean>` — True once revoked.
|
||||||
276
API.md
276
API.md
@@ -8,28 +8,35 @@ 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)`
|
- **account**: `id PK, company_id->company, type:s(100), name:s(255)`
|
||||||
|
- **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, meta?:txt`
|
||||||
- **articlelist**: `id PK`
|
- **articlelist**: `id PK`
|
||||||
- **articlelistarticle**: `id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)`
|
- **articlelistarticle**: `id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)`
|
||||||
- **articlepic**: `id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)`
|
- **articlepic**: `id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)`
|
||||||
- **articleproperty**: `id PK, article_id->article, property_id->propertyname, value:txt, sort:i=0`
|
- **articleproperty**: `id PK, article_id->article, property_id->propertyname, value:txt, sort:i=0`
|
||||||
- **articlesupplier**: `id PK, article_id->article, entity_id->entity, price_id?->price, sku?:s(100), stock?:f, pu:f=1, sort:i=0, UQ(article_id, entity_id), UQ(article_id, sort)`
|
- **articlesupplier**: `id PK, article_id->article, entity_id->entity, net?:f, tax_id?->tax, sku?:s(100), stock?:f, pu:f=1, sort:i=0, UQ(article_id, entity_id), UQ(article_id, sort)`
|
||||||
- **articletext**: `id PK, article_id->article, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(article_id, language_id)`
|
- **articletext**: `id PK, article_id->article, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(article_id, language_id)`
|
||||||
- **attributename**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)`
|
- **attributename**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)`
|
||||||
- **attributevalue**: `id PK, attribute_id->attributename, value:s(255), sort:i=0, UQ(attribute_id, value)`
|
- **attributevalue**: `id PK, attribute_id->attributename, value:s(255), sort:i=0, UQ(attribute_id, value)`
|
||||||
|
- **attributevaluepic**: `id PK, attributevalue_id->attributevalue, pic_id->pic, sort:i=0, UQ(attributevalue_id, pic_id), UQ(attributevalue_id, sort)`
|
||||||
- **brand**: `id PK, company_id->company, name:s(255), gsprpinfo?:txt, UQ(company_id, name)`
|
- **brand**: `id PK, company_id->company, name:s(255), gsprpinfo?:txt, UQ(company_id, name)`
|
||||||
- **brandpic**: `id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)`
|
- **brandpic**: `id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)`
|
||||||
- **category**: `id PK, parent_id?->category, sort:i=0`
|
- **category**: `id PK, parent_id?->category, sort:i=0`
|
||||||
- **categoryarticle**: `id PK, category_id->category, article_id->article, UQ(category_id, article_id)`
|
- **categoryarticle**: `id PK, category_id->category, article_id->article, UQ(category_id, article_id)`
|
||||||
|
- **categoryattributevalue**: `id PK, category_id->category, attributevalue_id->attributevalue, sort:i=0, UQ(category_id, attributevalue_id)`
|
||||||
- **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)`
|
||||||
|
- **categoryproperty**: `id PK, category_id->category, property_id->propertyname, value:txt, sort:i=0`
|
||||||
- **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), 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`
|
||||||
|
- **articlewarehousesummary**: `id PK, article_id->article, warehouse_id->warehouse, stock:f=0, UQ(article_id, warehouse_id)`
|
||||||
|
- **deliverysummary**: `id PK, company_id->company, manifest_id->manifest UQ, delivery_number:s(100), external_number?:s(100), delivery_date:dt, created_at:dt, supplier_id?->entity, supplier_name?:s(255), positions:i=0, gross_total:f=0, net_total:f=0, total_quantity:f=0, stock_positions:i=0, booked_positions:i=0, unbooked_positions:i=0, stock_quantity:f=0, booked_quantity:f=0, unbooked_quantity:f=0, is_booked:b=0`
|
||||||
- **entity**: `id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)`
|
- **entity**: `id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)`
|
||||||
- **entityaddress**: `id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0`
|
- **entityaddress**: `id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0`
|
||||||
- **entitycontact**: `id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0`
|
- **entitycontact**: `id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0`
|
||||||
@@ -42,17 +49,31 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- **manifest**: `id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt`
|
- **manifest**: `id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt`
|
||||||
- **manifestarticle**: `id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0`
|
- **manifestarticle**: `id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0`
|
||||||
- **manifestattachment**: `id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)`
|
- **manifestattachment**: `id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)`
|
||||||
|
- **mcp_auth_code**: `code:s(128) PK, client_id:s(100), google_auth_id->google_auth, redirect_uri:s(2048), code_challenge:s(255), code_challenge_method:s(32), scope?:s(255), expires_at:dt, used:b=0, created_at:dt`
|
||||||
|
- **mcp_client**: `id PK, client_id:s(100) UQ, client_secret?:s(255), client_name:s(255), redirect_uris:txt, grant_types?:s(255), response_types?:s(255), scope?:s(255), created_at:dt`
|
||||||
|
- **mcp_token**: `id PK, refresh_token:s(128) UQ, client_id:s(100), google_auth_id->google_auth, scope?:s(255), expires_at:dt, revoked:b=0, created_at:dt`
|
||||||
- **meta**: `key:s(255) PK, value:txt`
|
- **meta**: `key:s(255) PK, value:txt`
|
||||||
|
- **modelconversation**: `id PK, companyuser_id->companyuser, provider:s(100), model:s(255), created_at:dt, updated_at:dt, state:s(20)='idle', started_by?:i`
|
||||||
|
- **modelmessage**: `id PK, conversation_id->modelconversation, created_at:dt, role:s(20), message:txt`
|
||||||
|
- **modelvfs**: `id PK, company_id->company, name:s(255), mime:s(100), content:bin, meta?:txt, created_at:dt, updated_at:dt, UQ(company_id, name)`
|
||||||
- **numbers**: `id PK, company_id->company, type:s(100), prefix:s(50)='', padding:i=0, next:i=1, UQ(company_id, type)`
|
- **numbers**: `id PK, company_id->company, type:s(100), prefix:s(50)='', padding:i=0, next:i=1, UQ(company_id, type)`
|
||||||
- **optionslist**: `id PK`
|
- **optionslist**: `id PK`
|
||||||
- **optionslistarticle**: `id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)`
|
- **optionslistarticle**: `id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)`
|
||||||
- **pic**: `id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), source:txt, fullbinary:bin, thumbbinary:bin`
|
- **ordersummary**: `id PK, company_id->company, manifest_id->manifest UQ, order_number:s(100), order_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, paid_total:f=0, ordered_quantity:f=0, shipped_quantity:f=0, is_paid:b=0, is_shipped:b=0`
|
||||||
- **posterminal**: `id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin:s(50)`
|
- **package**: `id PK, shipping_id->shipping, entityaddress_id->entityaddress, number:s(100), type:s(100), state:s(100), details?:txt`
|
||||||
|
- **payment**: `id PK, account_id->account, manifest_id->manifest, type:s(100), amount:f`
|
||||||
|
- **pic**: `id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), smallthumbw:i, smallthumbh:i, smallthumbbinary:bin, source:txt`
|
||||||
|
- **posterminal**: `id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), maxknownobituaryid?:i, root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt`
|
||||||
- **posterminalcategory**: `id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)`
|
- **posterminalcategory**: `id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)`
|
||||||
|
- **posterminalobituary**: `id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i, created_at:dt`
|
||||||
|
- **posterminalsyncqueue**: `id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i`
|
||||||
- **price**: `id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f`
|
- **price**: `id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f`
|
||||||
- **printer**: `id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt`
|
- **printer**: `id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt`
|
||||||
- **propertyname**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)`
|
- **propertyname**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)`
|
||||||
- **syncpos**: `id PK, company_id->company, paircode:s(50) UQ, token?:s(255) UQ`
|
- **quotesummary**: `id PK, company_id->company, manifest_id->manifest UQ, quote_number:s(100), quote_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, total_quantity:f=0`
|
||||||
|
- **shipmentsummary**: `id PK, company_id->company, manifest_id->manifest UQ, shipment_number:s(100), shipment_date:dt, created_at:dt, order_id?:i, order_number?:s(100), entity_id?->entity, customer_name?:s(255), positions:i=0, total_quantity:f=0, package_count:i=0, carrier?:s(100), tracking_code?:s(255), state:s(50)='new'`
|
||||||
|
- **shipping**: `id PK, manifest_id->manifest, number:s(100), tracking_code?:s(255), carrier?:s(100), type:s(100), state:s(100)`
|
||||||
|
- **subagentschedule**: `id PK, company_id->company, desc?:txt, prompt:txt, skills?:txt, lastagentnote?:txt, max_itr:i=15, cron:s(100), created_at:dt, lastrun_at?:dt`
|
||||||
- **tax**: `id PK, rate:d(18,4) UQ`
|
- **tax**: `id PK, rate:d(18,4) UQ`
|
||||||
- **unit**: `id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)`
|
- **unit**: `id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)`
|
||||||
- **unittext**: `id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)`
|
- **unittext**: `id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)`
|
||||||
@@ -61,50 +82,63 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [companyuser](API.companyuser.md)
|
- [companyuser](API.companyuser.md)
|
||||||
- `add(companyId: number, googleAuthId: number, tx?: object): Promise<number>`
|
- `add(companyId: number, googleAuthId: number, role?: 'admin'|'user', tx?: object): Promise<number>`
|
||||||
|
- `addByEmail(googleAuthId: number, input: {email: string, role?: 'admin'|'user'}, tx?: object): Promise<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>`
|
||||||
- `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>>`
|
||||||
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>>`
|
||||||
|
- `remove(googleAuthId: number, id: number, tx?: object): Promise<{id: number, googleAuthId: number}>`
|
||||||
|
- `requireAdmin(googleAuthId: number, tx?: object): Promise<{companyId: number, companyuserId: number, role: string}>`
|
||||||
|
- `roles(role: unknown): 'admin'|'user'`
|
||||||
|
- `setRole(googleAuthId: number, id: number, role: 'admin'|'user', tx?: object): Promise<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>`
|
||||||
|
- `withTransaction(): void`
|
||||||
- [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>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<object>>`
|
- `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<{id: number, name: string, rootCategoryId: number, languageId: number, iso639: string|null, taxrate: number, reducedtaxrate: number, taxfreerate: 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}>`
|
- `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)
|
||||||
- `addImage(googleAuthId: number, categoryId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>`
|
- `addImage(googleAuthId: number, categoryId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number}>`
|
||||||
- `createUnderParent(input: {googleAuthId: number, parentId: number, name: string}, tx?: object): Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>`
|
- `createUnderParent(input: {googleAuthId: number, parentId: number, name: string}, tx?: object): Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>`
|
||||||
- `delete(input: {googleAuthId: number, id: number}, tx?: object): Promise<void>`
|
- `delete(input: {googleAuthId: number, id: number, recursive?: boolean}, 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}>>`
|
- `generateImage(googleAuthId: number, inputOrCategoryId: number|object, maybeOptsOrSize?: {size?: number}, maybeTx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, size: number, grid: string, articleCount: number}>`
|
||||||
- `getTreeForCompany(googleAuthId: number, tx?: object): Promise<{languageId: number, tree: Array<object>, articles: Array<object>}>`
|
- `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, smallThumbW: number|null, smallThumbH: number|null, smallThumbBinary: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, articlewarehousesummary: Array<{id: number, articleId: number, warehouseId: number, warehouseName: string, stock: number}>, warehouses: Array<{id: number, name: string, stock: number}>}>>`
|
||||||
|
- `getTreeForCompany(googleAuthId: number, tx?: object): Promise<{languageId: number, tree: Array<{id: number, parentId: number|null, sort: number, name: string|null, slug: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, articleCount: number, posterminalIds: number[]}>, articleCount: number}>`
|
||||||
- `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}, smallThumb: {w: number, h: number}, fullHash: string, thumbHash: string}>`
|
||||||
- `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>`
|
- `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>}>`
|
||||||
- `delete(googleAuthId: number, sku: string, tx?: object): Promise<void>`
|
- `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, desc?: string, seo?: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>`
|
||||||
|
- `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise<void>`
|
||||||
- `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>`
|
- `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): Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, 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, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, warehouses: Array<{id: number, name: string, sort: number, stock: number}>, undeliveredOrders: Array<{orderId: number, orderNumber: string, date: string, customerName: string|null, amount: number}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>`
|
||||||
- `getHistoryBySku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
|
- `getHistoryBySku(googleAuthId: number, sku: string, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number, paged?: boolean}, tx?: object): Promise<{history: Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>`
|
||||||
- `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>>`
|
||||||
|
- `listHistory(provider: import('../../providers/provider.js').Provider): (googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{history: Array<{id: number, date: string, sku: string, name: string|null, historytext: string}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
|
||||||
- `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}, tx?: object): Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>`
|
- `removeChild(googleAuthId: number, parentSku: string, childSku: string, options?: {deleteArticle?: boolean, cascade?: boolean}, tx?: object): Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>`
|
||||||
|
- `resolveUniqueSlug(runner: object, params: {companyId: number, languageId: number, name: string, excludeArticleId?: number}): Promise<string>`
|
||||||
- `setBrandIdBySku(googleAuthId: number, sku: string, brandId: number|null, tx?: object): Promise<object>`
|
- `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<{net?: number, gross?: number, price?: number, taxRate?: 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, desc?: string, seo?: 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>`
|
||||||
- `delete(id: number): Promise<void>`
|
- `delete(id: number): Promise<void>`
|
||||||
@@ -125,10 +159,12 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `list(googleAuthId: number, options?: {frontendOnly?: boolean, maskWriteonly?: boolean}, tx?: object): Promise<Array<{key: string, value: string, frontend: boolean, writeonly: boolean}>>`
|
- `list(googleAuthId: number, options?: {frontendOnly?: boolean, maskWriteonly?: boolean}, tx?: object): Promise<Array<{key: string, value: string, frontend: boolean, writeonly: boolean}>>`
|
||||||
- `set(googleAuthId: number, input: {key: string, value: string, frontend?: boolean, writeonly?: boolean}, tx?: object): Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>`
|
- `set(googleAuthId: number, input: {key: string, value: string, frontend?: boolean, writeonly?: boolean}, tx?: object): Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>`
|
||||||
- [brand](API.brand.md)
|
- [brand](API.brand.md)
|
||||||
- `addImage(googleAuthId: number, brandId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array<object>}>`
|
- `addImage(googleAuthId: number, brandId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array<object>}>`
|
||||||
|
- `create(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>>`
|
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, includeSmallThumbs?: boolean}, tx?: object): Promise<{brands: Array<{id: number, name: string, articleCount: number, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW?: number|null, smallThumbH?: number|null, smallThumbBinary?: string|null}>}>, total: number, offset: number, limit: number}>`
|
||||||
- `upsert(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>`
|
- `update(googleAuthId: number, id: number, input: {name?: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, touchedArticleCount: number}>`
|
||||||
|
- `upsert(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>`
|
||||||
- [customergroup](API.customergroup.md)
|
- [customergroup](API.customergroup.md)
|
||||||
- `create(googleAuthId: number, input: {name: string}, tx?: object): Promise<{id: number, name: string, sort: number}>`
|
- `create(googleAuthId: number, input: {name: string}, tx?: object): Promise<{id: number, name: string, sort: number}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
@@ -159,26 +195,50 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, createdAt: string, updatedAt: string}>>`
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, createdAt: string, updatedAt: string}>>`
|
||||||
- `update(googleAuthId: number, id: number, input: {name: string}, tx?: object): Promise<{id: number, name: string, createdAt: string, updatedAt: string}>`
|
- `update(googleAuthId: number, id: number, input: {name: string}, tx?: object): Promise<{id: number, name: string, createdAt: string, updatedAt: string}>`
|
||||||
- [articlesupplier](API.articlesupplier.md)
|
- [articlesupplier](API.articlesupplier.md)
|
||||||
- `create(googleAuthId: number, sku: string, input: {entityId: number, sku?: string, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{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}>`
|
- `create(googleAuthId: number, sku: string, input: {entityId: number, sku?: string, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>`
|
||||||
- `delete(googleAuthId: number, sku: string, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, sku: string, id: number, tx?: object): Promise<void>`
|
||||||
- `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<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}>>`
|
- `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>>`
|
||||||
- `listByEntityAndSkus(googleAuthId: number, entityIdOrOptions: number|{entityId?: number, supplierId?: number, skus?: string[]|string}, maybeSkus?: string[]|string, tx?: object): Promise<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, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, articlepic: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, eans: string[], articleean: string[]}}>>`
|
- `listByEntityAndSkus(googleAuthId: number, entityIdOrOptions: number|{entityId?: number, supplierId?: number, skus?: string[]|string}, maybeSkus?: string[]|string, tx?: object): Promise<Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[]}}>>`
|
||||||
- `order(googleAuthId: number, sku: string, input: {ids: Array<number>}, tx?: object): Promise<{ids: Array<number>}>`
|
- `order(googleAuthId: number, sku: string, input: {ids: Array<number>}, tx?: object): Promise<{ids: Array<number>}>`
|
||||||
- `update(googleAuthId: number, sku: string, id: number, patch: {sku?: string|null, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{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}>`
|
- `update(googleAuthId: number, sku: string, id: number, patch: {sku?: string|null, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>`
|
||||||
- [delivery](API.delivery.md)
|
- [delivery](API.delivery.md)
|
||||||
- `attachments(): void`
|
- `attachments(): void`
|
||||||
|
- `book(provider: object): Function`
|
||||||
|
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnbooked?: boolean, supplierId?: number, seekMonths?: number}, tx?: object): Promise<{deliveries: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `syncSummary(): void`
|
||||||
- [quote](API.quote.md)
|
- [quote](API.quote.md)
|
||||||
- `create(googleAuthId: number, body: {number: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
- `create(googleAuthId: number, body: {number: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string}>>`
|
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object): Promise<{quotes: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>, positions?: number, grossTotal?: number, totalQuantity?: number, customerName?: string}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `syncSummary(): void`
|
||||||
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||||
- [order](API.order.md)
|
- [order](API.order.md)
|
||||||
- `create(googleAuthId: number, body: {number: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
- `create(googleAuthId: number, body: {number: string, date: string, entityId: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||||
|
- `createFromQuote(googleAuthId: number, quoteId: number, body?: {number?: string, date?: string, createdAt?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||||
|
- `createShipment(googleAuthId: number, orderId: number, input: {lines: Array<{sku: string, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>, date?: string|Date, externalNumber?: string, shipping: object}, tx?: object): Promise<object>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string}>>`
|
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `syncSummary(): void`
|
||||||
|
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||||
|
- [shipment](API.shipment.md)
|
||||||
|
- `create(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `delete(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `getById(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, state?: 'new'|'delivered'|'cancelled'|'all', includeCancelled?: boolean, seekMonths?: number, orderId?: number}, tx?: object): Promise<{shipments: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
- `listByOrderId(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `rebuildSummaries(googleAuthId: number, tx?: object): Promise<void>`
|
||||||
|
- `syncSummary(): void`
|
||||||
|
- `update(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- [shipping](API.shipping.md)
|
||||||
|
- `create(googleAuthId: number, input: {manifestId: number, carrierId: number, type: string, state: string, trackingCode?: string, warehouseId?: number}, tx?: object): Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrierId: number, carrier: string|null, type: string, state: string}>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, manifestId: number, number: string, type: string, state: string}>>`
|
||||||
|
- `update(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
- [manifestattachment](API.manifestattachment.md)
|
- [manifestattachment](API.manifestattachment.md)
|
||||||
- `getById(googleAuthId: number, manifestId: number, id: number, tx?: object): Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>`
|
- `getById(googleAuthId: number, manifestId: number, id: number, tx?: object): Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>`
|
||||||
- `listByManifestId(): void`
|
- `listByManifestId(): void`
|
||||||
@@ -188,6 +248,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `add(googleAuthId: number, input: {manifestarticle_id: number, warehouse_id?: number|null, transaction_type: string, amount: number, transaction_text: string}, tx?: object): Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>`
|
- `add(googleAuthId: number, input: {manifestarticle_id: number, warehouse_id?: number|null, transaction_type: string, amount: number, transaction_text: string}, tx?: object): Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>`
|
||||||
- `listByManifestarticleId(googleAuthId: number, manifestarticleId: number, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
|
- `listByManifestarticleId(googleAuthId: number, manifestarticleId: number, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
|
||||||
- `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
|
- `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
|
||||||
|
- `listHistory(provider: import('../../providers/provider.js').Provider): (googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{ledger: Array<{id: number, date: string, sku: string|null, name: string|null, transaction_type: string, transaction_text: string, amount: number}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
|
||||||
- [warehouse](API.warehouse.md)
|
- [warehouse](API.warehouse.md)
|
||||||
- `create(googleAuthId: number, input: {name: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>`
|
- `create(googleAuthId: number, input: {name: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
@@ -196,27 +257,38 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `order(googleAuthId: number, input: {warehouseIds: Array<number>}, tx?: object): Promise<{warehouseIds: Array<number>}>`
|
- `order(googleAuthId: number, input: {warehouseIds: Array<number>}, tx?: object): Promise<{warehouseIds: Array<number>}>`
|
||||||
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>`
|
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>`
|
||||||
- [stock](API.stock.md)
|
- [stock](API.stock.md)
|
||||||
|
- `accept(provider: object): Function`
|
||||||
- `adjust(googleAuthId: number, input: {sku?: string, warehouseId?: number, amount?: number, text?: string, externalNumber?: string, date?: string|Date, lines?: Array<object>}, tx?: object): Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>`
|
- `adjust(googleAuthId: number, input: {sku?: string, warehouseId?: number, amount?: number, text?: string, externalNumber?: string, date?: string|Date, lines?: Array<object>}, tx?: object): Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>`
|
||||||
|
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `ship(provider: object): Function`
|
||||||
- [posterminal](API.posterminal.md)
|
- [posterminal](API.posterminal.md)
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
- `findByPairingpin(pairingpin: string, tx?: object): Promise<object|null>`
|
- `findByPairingpin(pairingpin: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>`
|
||||||
- `findByToken(token: string, tx?: object): Promise<object|null>`
|
- `findByToken(token: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>`
|
||||||
- `getCategoryCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
- `getCategoryCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
||||||
- `getCategoryList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
|
- `getCategoryList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
|
||||||
- `getCompositeProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
- `getCompositeProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
||||||
- `getCustomerCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
- `getCustomerCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
||||||
- `getCustomerGroupCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
- `getCustomerGroupCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
||||||
- `getDeletedEntityCount(input?: {posterminalId?: number, cursor?: number}): Promise<number>`
|
- `getCustomerGroupList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<{customerGroupId: string, name: string, standard: string, discountPercent: string, lastChanged: string}>>`
|
||||||
|
- `getCustomerList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
|
||||||
|
- `getDeletedEntityCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
||||||
|
- `getDeletedEntityList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<{id: number, entityType: string, entityId: number}>>`
|
||||||
- `getMaxOrderIdCount(input: {posterminalId: number}, tx?: object): Promise<number>`
|
- `getMaxOrderIdCount(input: {posterminalId: number}, tx?: object): Promise<number>`
|
||||||
- `getProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
- `getProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
||||||
- `getProductList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
|
- `getProductList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, token: string, pairingpin: string}>>`
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}>>`
|
||||||
- `pair(id: number, input: {token: string, clientserial?: string, pairedAt?: string}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>`
|
- `pair(id: number, input: {token: string, clientserial?: string, pairedAt?: string, clientversion?: string|null}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null, clientversion: string|null}>`
|
||||||
|
- `recordObituary(input: {companyId: number, entityType: string, entityId: number}, tx?: object): Promise<void>`
|
||||||
|
- `recordSyncQueue(input: {companyId: number, entityType: string, entityId: number}, tx?: object): Promise<void>`
|
||||||
|
- `setClientversion(id: number, clientversion: string|null, tx?: object): Promise<{id: number, clientversion: string|null}>`
|
||||||
|
- `setMaxKnownObituaryId(id: number, maxknownobituaryid: number|null, tx?: object): Promise<{id: number, maxknownobituaryid: number|null}>`
|
||||||
- `setOff(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
- `setOff(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
||||||
- `setOffAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
- `setOffAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
||||||
- `setOn(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
- `setOn(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
||||||
- `setOnAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
- `setOnAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
||||||
- `upsert(googleAuthId: number, input: {name: string, pairingpin: string, token?: string, id?: number}, tx?: object): Promise<{id: number, name: string, token: string, pairingpin: string}>`
|
- `touchLastseen(id: number, tx?: object): Promise<{id: number}>`
|
||||||
|
- `upsert(googleAuthId: number, input: {name: string, pairingpin: string, token?: string, id?: number, sort?: number, enabled?: boolean}, tx?: object): Promise<{id: number, name: string, token: string, pairingpin: string, sort: number, enabled: number}>`
|
||||||
- [printer](API.printer.md)
|
- [printer](API.printer.md)
|
||||||
- `create(googleAuthId: number, input: {address: string, type: string, model?: string, serial?: string, sort?: number, desc?: string}, tx?: object): Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>`
|
- `create(googleAuthId: number, input: {address: string, type: string, model?: string, serial?: string, sort?: number, desc?: string}, tx?: object): Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
@@ -234,14 +306,16 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `setSelectedCompany(googleAuthId: number, companyId: number): Promise<void>`
|
- `setSelectedCompany(googleAuthId: number, companyId: number): Promise<void>`
|
||||||
- [pic](API.pic.md)
|
- [pic](API.pic.md)
|
||||||
- `getImageByHash(googleAuthId: number, fullHash: string, tx?: object): Promise<{buffer: Buffer, w: number, h: number, size: number, contentType: string}>`
|
- `getImageByHash(googleAuthId: number, fullHash: string, tx?: object): Promise<{buffer: Buffer, w: number, h: number, size: number, contentType: string}>`
|
||||||
|
- `getSmallThumbByHash(googleAuthId: number, fullHash: string, tx?: object): Promise<{buffer: Buffer, w: number, h: number, contentType: string}>`
|
||||||
- `getThumbByHash(googleAuthId: number, fullHash: string, tx?: object): Promise<{buffer: Buffer, w: number, h: number, contentType: string}>`
|
- `getThumbByHash(googleAuthId: number, fullHash: string, tx?: object): Promise<{buffer: Buffer, w: number, h: number, contentType: string}>`
|
||||||
- [numbers](API.numbers.md)
|
- [numbers](API.numbers.md)
|
||||||
|
- `ensureType(runner: object, companyId: number, type: string): Promise<{id: number, type: string, prefix: string, padding: number, next: number}>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, type: string, prefix: string, padding: number, next: number}>>`
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, type: string, prefix: string, padding: number, next: number}>>`
|
||||||
- `update(googleAuthId: number, id: number, input: {prefix?: string, padding?: number, next?: number}, tx?: object): Promise<{id: number, type: string, prefix: string, padding: number, next: number}>`
|
- `update(googleAuthId: number, id: number, input: {prefix?: string, padding?: number, next?: number}, tx?: object): Promise<{id: number, type: string, prefix: string, padding: number, next: number}>`
|
||||||
- [propertyname](API.propertyname.md)
|
- [propertyname](API.propertyname.md)
|
||||||
- `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
|
- `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number, exclusive: boolean}>>`
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number, exclusive: boolean, articleCount: number}>>`
|
||||||
- `order(googleAuthId: number, input: {propertynameIds: Array<number>}, tx?: object): Promise<{propertynameIds: Array<number>}>`
|
- `order(googleAuthId: number, input: {propertynameIds: Array<number>}, tx?: object): Promise<{propertynameIds: Array<number>}>`
|
||||||
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
|
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
|
||||||
- [articleproperty](API.articleproperty.md)
|
- [articleproperty](API.articleproperty.md)
|
||||||
@@ -251,13 +325,15 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- [attributename](API.attributename.md)
|
- [attributename](API.attributename.md)
|
||||||
- `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>`
|
- `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number, exclusive: boolean, values: Array<{id: number, value: string, sort: number}>}>>`
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number, exclusive: boolean, articleCount: number, values: Array<{id: number, value: string, sort: number, articleCount: number}>}>>`
|
||||||
- `order(googleAuthId: number, input: {attributenameIds: Array<number>}, tx?: object): Promise<{attributenameIds: Array<number>}>`
|
- `order(googleAuthId: number, input: {attributenameIds: Array<number>}, tx?: object): Promise<{attributenameIds: Array<number>}>`
|
||||||
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>`
|
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>`
|
||||||
- [attributevalue](API.attributevalue.md)
|
- [attributevalue](API.attributevalue.md)
|
||||||
|
- `addImage(googleAuthId: number, attributeValueId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array<object>}>`
|
||||||
- `create(googleAuthId: number, attributeId: number, input: {value: string, sort?: number}, tx?: object): Promise<{id: number, attributeId: number, value: string, sort: number}>`
|
- `create(googleAuthId: number, attributeId: number, input: {value: string, sort?: number}, tx?: object): Promise<{id: number, attributeId: number, value: string, sort: number}>`
|
||||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
- `listByAttributeId(googleAuthId: number, attributeId: number, tx?: object): Promise<Array<{id: number, attributeId: number, value: string, sort: number}>>`
|
- `deleteImage(googleAuthId: number, attributeValueId: number, tx?: object): Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null, pics: Array}>`
|
||||||
|
- `listByAttributeId(googleAuthId: number, attributeId: number, tx?: object): Promise<Array<{id: number, attributeId: number, value: string, sort: number, articleCount: number}>>`
|
||||||
- `order(googleAuthId: number, attributeId: number, input: {attributevalueIds: Array<number>}, tx?: object): Promise<{attributevalueIds: Array<number>}>`
|
- `order(googleAuthId: number, attributeId: number, input: {attributevalueIds: Array<number>}, tx?: object): Promise<{attributevalueIds: Array<number>}>`
|
||||||
- `update(googleAuthId: number, id: number, input: {value?: string, sort?: number}, tx?: object): Promise<{id: number, attributeId: number, value: string, sort: number}>`
|
- `update(googleAuthId: number, id: number, input: {value?: string, sort?: number}, tx?: object): Promise<{id: number, attributeId: number, value: string, sort: number}>`
|
||||||
- [articleattributevalue](API.articleattributevalue.md)
|
- [articleattributevalue](API.articleattributevalue.md)
|
||||||
@@ -269,11 +345,117 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>`
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>>`
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>>`
|
||||||
- `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>`
|
- `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>`
|
||||||
|
- [healthcheck](API.healthcheck.md)
|
||||||
|
- `check(tx?: object): Promise<{ok: boolean, sequence: number}>`
|
||||||
|
- [account](API.account.md)
|
||||||
|
- `create(googleAuthId: number, input: {type: string, name: string}, tx?: object): Promise<{id: number, companyId: number, type: string, name: string}>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyId: number, type: string, name: string}>>`
|
||||||
|
- [agentprinter](API.agentprinter.md)
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<{id: number}>`
|
||||||
|
- `findOrCreate(printeragentId: number, input: {type: string, model?: string|null, serial?: string|null}, tx?: object): Promise<{created: boolean, agentprinter: {id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}}>`
|
||||||
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}|null>`
|
||||||
|
- `list(googleAuthId: number, printeragentId?: number, tx?: object): Promise<Array<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>>`
|
||||||
|
- `order(googleAuthId: number, input: {printeragentId?: number, agentprinterIds: number[]}, tx?: object): Promise<{agentprinterIds: number[]}>`
|
||||||
|
- `update(googleAuthId: number, input: {id: number, enabled?: boolean, sort?: number}, tx?: object): Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>`
|
||||||
|
- [articlehistory](API.articlehistory.md)
|
||||||
|
- `record(): void`
|
||||||
- [articlelist](API.articlelist.md)
|
- [articlelist](API.articlelist.md)
|
||||||
- `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
- `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
||||||
- `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
- `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
||||||
|
- [articlewarehousesummary](API.articlewarehousesummary.md)
|
||||||
|
- `syncSummary(): void`
|
||||||
|
- [autoinvite](API.autoinvite.md)
|
||||||
|
- `add(googleAuthId: number, input: {email: string, role?: 'admin'|'user'}, tx?: object): Promise<{id: number, email: string, role: 'admin'|'user'}>`
|
||||||
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, email: string, role: 'admin'|'user', createdAt: string}>>`
|
||||||
|
- `remove(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- [carrier](API.carrier.md)
|
||||||
|
- `create(googleAuthId: number, input: {name: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}|undefined>`
|
||||||
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>>`
|
||||||
|
- `order(googleAuthId: number, input: {carrierIds: Array<number>}, tx?: object): Promise<{carrierIds: Array<number>}>`
|
||||||
|
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>`
|
||||||
|
- [categoryattributevalue](API.categoryattributevalue.md)
|
||||||
|
- `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
|
||||||
|
- `setByCategoryId(googleAuthId: number, categoryId: number, values: Array<number|{attributeValueId: number, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
|
||||||
|
- [categoryproperty](API.categoryproperty.md)
|
||||||
|
- `delete(googleAuthId: number, categoryId: number, propertyId: number, tx?: object): Promise<void>`
|
||||||
|
- `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||||
|
- `setByCategoryId(googleAuthId: number, categoryId: number, properties: Array<{propertyId: number, value: string, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||||
|
- [mcp](API.mcp.md)
|
||||||
|
- `consumeAuthCode(code: string): Promise<boolean>`
|
||||||
|
- `createAuthCode(input: {code: string, clientId: string, googleAuthId: number, redirectUri: string, codeChallenge: string, codeChallengeMethod: string, scope?: string|null, expiresAt: string|Date}): Promise<string>`
|
||||||
|
- `createRefreshToken(input: {refreshToken: string, clientId: string, googleAuthId: number, scope?: string|null, expiresAt: string|Date}): Promise<object>`
|
||||||
|
- `deleteClient(clientId: string): Promise<boolean>`
|
||||||
|
- `getAuthCode(code: string): Promise<object|null>`
|
||||||
|
- `getClient(clientId: string): Promise<object|null>`
|
||||||
|
- `getRefreshToken(refreshToken: string): Promise<object|null>`
|
||||||
|
- `listAuthorizations(googleAuthId: number): Promise<Array<object>>`
|
||||||
|
- `listClients(): Promise<Array<object>>`
|
||||||
|
- `registerClient(input: {clientId: string, clientSecret?: string|null, clientName: string, redirectUris: string|Array<string>, grantTypes?: string|Array<string>, responseTypes?: string|Array<string>, scope?: string|null}): Promise<object>`
|
||||||
|
- `revokeAuthorizations(googleAuthId: number, clientId: string): Promise<boolean>`
|
||||||
|
- `revokeRefreshToken(refreshToken: string): Promise<boolean>`
|
||||||
|
- [modelconversation](API.modelconversation.md)
|
||||||
|
- `create(googleAuthId: number, input: {provider: string, model: string, started_by?: number|null}, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>`
|
||||||
|
- `delete(googleAuthId: number, conversationId: number, tx?: object): Promise<void>`
|
||||||
|
- `get(googleAuthId: number, conversationId: number, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>`
|
||||||
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>>`
|
||||||
|
- `setState(googleAuthId: number, conversationId: number, state: string, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>`
|
||||||
|
- [modelmessage](API.modelmessage.md)
|
||||||
|
- `add(googleAuthId: number, conversationId: number, input: {message: string, role?: 'user'|'assistant'|'system'|'tool'|'reasoning'|'usage'}, tx?: object): Promise<{id: number, conversation_id: number, created_at: string, role: string, message: string}>`
|
||||||
|
- `list(googleAuthId: number, conversationId: number, tx?: object): Promise<Array<{id: number, conversation_id: number, created_at: string, role: string, message: string}>>`
|
||||||
|
- [modelvfs](API.modelvfs.md)
|
||||||
|
- `delete(googleAuthId: number, name: string, tx?: object): Promise<{deleted: boolean}>`
|
||||||
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, name: string, mime: string, meta: any, created_at: string, updated_at: string}>>`
|
||||||
|
- `read(googleAuthId: number, name: string, tx?: object): Promise<{id: number, company_id: number, name: string, mime: string, content: Buffer, meta: any, created_at: string, updated_at: string}>`
|
||||||
|
- `reset(googleAuthId: number, tx?: object): Promise<{deletedCount: number}>`
|
||||||
|
- `write(googleAuthId: number, input: {name: string, mime?: string, content: Buffer|string, meta?: object|string}, tx?: object): Promise<object>`
|
||||||
- [optionslist](API.optionslist.md)
|
- [optionslist](API.optionslist.md)
|
||||||
- `getOptionsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
- `getOptionsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
||||||
- `setOptionsByArticleSku(googleAuthId: number, sku: string, options?: Array<{articleId?: number, sku?: string, count?: number, sort?: number, mandated?: boolean}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
- `setOptionsByArticleSku(googleAuthId: number, sku: string, options?: Array<{articleId?: number, sku?: string, count?: number, sort?: number, mandated?: boolean}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
||||||
- [runner.js](API.runner.js.md)
|
- [package](API.package.md)
|
||||||
- `runner(provider: import('../providers/provider.js').DatabaseProvider, tx?: object): ((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }`
|
- `create(googleAuthId: number, input: {shippingId: number, entityaddressId: number, type: string, state: string, details?: string}, tx?: object): Promise<{id: number, shippingId: number, entityaddressId: number, number: string, type: string, state: string, details: string|null}>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- `listByShippingId(googleAuthId: number, shippingId: number, tx?: object): Promise<Array<{id: number, shippingId: number, entityaddressId: number, number: string, type: string, state: string, details: string|null}>>`
|
||||||
|
- [payment](API.payment.md)
|
||||||
|
- `create(googleAuthId: number, input: {accountId: number, manifestId: number, type: string, amount: number}, tx?: object): Promise<{id: number, accountId: number, manifestId: number, type: string, amount: number}>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, accountId: number, manifestId: number, type: string, amount: number}>>`
|
||||||
|
- [printeragent](API.printeragent.md)
|
||||||
|
- `create(googleAuthId: number, input: {name: string, pairingpin: string, sort?: number}, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- `findByPairingpin(pairingpin: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}|null>`
|
||||||
|
- `findByToken(token: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}|null>`
|
||||||
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>>`
|
||||||
|
- `pair(id: number, input: {token: string, clientserial?: string, clientmodel?: string, agentversion?: string, pairedAt?: string}, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
|
||||||
|
- `touchLastseen(id: number, agentversion?: string|null, tx?: object): Promise<{id: number}>`
|
||||||
|
- `update(googleAuthId: number, input: {id: number, name?: string, sort?: number}, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
|
||||||
|
- `updateState(id: number, clientstate: string|null, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
|
||||||
|
- [printerjob](API.printerjob.md)
|
||||||
|
- `create(googleAuthId: number, input: {agentprinterId: number, data: string, type?: string, state?: string}, tx?: object): Promise<object>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<object>`
|
||||||
|
- `getById(auth: number|{agentId: number}, id: number, tx?: object): Promise<object|null>`
|
||||||
|
- `list(auth: number|{agentId: number}, filters?: {agentprinterId?: number, state?: string}, tx?: object): Promise<Array<object>>`
|
||||||
|
- `updateState(auth: number|{agentId: number}, id: number, state: string, tx?: object): Promise<object>`
|
||||||
|
- [retoure](API.retoure.md)
|
||||||
|
- `create(googleAuthId: number, body: {number?: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<object>}, tx?: object): Promise<object>`
|
||||||
|
- `createShipment(googleAuthId: number, retoureId: number, input: object, tx?: object): Promise<object>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
|
||||||
|
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{retoures: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `syncSummary(): void`
|
||||||
|
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<object>}, tx?: object): Promise<object>`
|
||||||
|
- [return](API.return.md)
|
||||||
|
- `book(provider: object): Function`
|
||||||
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<object>`
|
||||||
|
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnbooked?: boolean, supplierId?: number, seekMonths?: number}, tx?: object): Promise<{returns: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
|
||||||
|
- `syncSummary(): void`
|
||||||
|
- [subagentschedule](API.subagentschedule.md)
|
||||||
|
- `create(googleAuthId: number, input: {prompt: string, cron: string, desc?: string, skills?: string|Array<string>, lastagentnote?: string, max_itr?: number}, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`
|
||||||
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}|null>`
|
||||||
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>>`
|
||||||
|
- `update(googleAuthId: number, id: number, input: {prompt?: string, cron?: string, desc?: string|null, skills?: string|Array<string>|null, lastagentnote?: string|null, max_itr?: number, lastrun_at?: string|null}, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`
|
||||||
|
|||||||
124
API.modelconversation.md
Normal file
124
API.modelconversation.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# modelconversation
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `modelconversation.create`
|
||||||
|
|
||||||
|
*Source: `src/store/modelconversation/create.js`*
|
||||||
|
|
||||||
|
Creates a new (idle) model conversation for the user in the selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | Conversation input. |
|
||||||
|
| `input.provider` | `string` | LLM provider name. |
|
||||||
|
| `input.model` | `string` | Model identifier. |
|
||||||
|
| `[input.started_by]` | `number|null` | Id of the conversation that starts this one as a subagent (unenforced reference; omit/null for a top-level conversation). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Provider required', 'Model required' or 'Invalid started_by'.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const c = await store.modelconversation.create(42, { provider: 'openrouter', model: 'openai/gpt-5' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## `modelconversation.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/modelconversation/delete.js`*
|
||||||
|
|
||||||
|
Deletes a model conversation by ID for the user in the selected company, together with every subagent conversation below it (`started_by` chain, recursively) and their messages.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `conversationId` | `number` | The conversation id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Conversation not found'.
|
||||||
|
|
||||||
|
## `modelconversation.get`
|
||||||
|
|
||||||
|
*Source: `src/store/modelconversation/get.js`*
|
||||||
|
|
||||||
|
Gets a single model conversation by ID for the user in the selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `conversationId` | `number` | The conversation id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>` — `first_user_message` is the stored `message` payload of the first `user` message (null if none yet).
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Conversation not found'.
|
||||||
|
|
||||||
|
## `modelconversation.list`
|
||||||
|
|
||||||
|
*Source: `src/store/modelconversation/list.js`*
|
||||||
|
|
||||||
|
Lists the user's own model conversations in the selected company, newest activity first.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null, first_user_message: string|null}>>` — `started_by` is the originating conversation id for subagent conversations (null otherwise), so clients can build a tree. `first_user_message` is the stored `message` payload of the conversation's first `user` message (null if none yet).
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const conversations = await store.modelconversation.list(42);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `modelconversation.setState`
|
||||||
|
|
||||||
|
*Source: `src/store/modelconversation/setState.js`*
|
||||||
|
|
||||||
|
Sets the state of a model conversation ('idle' | 'busy').
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `conversationId` | `number` | The conversation id. |
|
||||||
|
| `state` | `string` | The new state ('idle' | 'busy'). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string, started_by: number|null}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Invalid state' or 'Conversation not found'.
|
||||||
56
API.modelmessage.md
Normal file
56
API.modelmessage.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# modelmessage
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `modelmessage.add`
|
||||||
|
|
||||||
|
*Source: `src/store/modelmessage/add.js`*
|
||||||
|
|
||||||
|
Appends a message to one of the user's own conversations. `user` messages are refused unless the conversation state is `idle`; other roles ('assistant', 'system', 'tool', 'reasoning') touch `updated_at` unconditionally.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `conversationId` | `number` | The conversation id. |
|
||||||
|
| `input` | `object` | Message input. |
|
||||||
|
| `input.message` | `string` | Message text. |
|
||||||
|
| `[input.role='user']` | `'user'|'assistant'|'system'|'tool'|'reasoning'|'usage'` | Message role. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, conversation_id: number, created_at: string, role: string, message: string}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Conversation not found', 'Conversation is not idle', 'Message required', 'Invalid role'.
|
||||||
|
|
||||||
|
## `modelmessage.list`
|
||||||
|
|
||||||
|
*Source: `src/store/modelmessage/list.js`*
|
||||||
|
|
||||||
|
Lists messages of one of the user's own conversations, oldest first.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `conversationId` | `number` | The conversation id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, conversation_id: number, created_at: string, role: string, message: string}>>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Conversation not found' if missing or owned by another user.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const messages = await store.modelmessage.list(42, 7);
|
||||||
|
```
|
||||||
99
API.modelvfs.md
Normal file
99
API.modelvfs.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# modelvfs
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `modelvfs.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/modelvfs/delete.js`*
|
||||||
|
|
||||||
|
Deletes a virtual file by name for the selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `name` | `string` | File name. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{deleted: boolean}>`
|
||||||
|
|
||||||
|
## `modelvfs.list`
|
||||||
|
|
||||||
|
*Source: `src/store/modelvfs/list.js`*
|
||||||
|
|
||||||
|
Lists virtual files for the selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, company_id: number, name: string, mime: string, meta: any, created_at: string, updated_at: string}>>`
|
||||||
|
|
||||||
|
## `modelvfs.read`
|
||||||
|
|
||||||
|
*Source: `src/store/modelvfs/read.js`*
|
||||||
|
|
||||||
|
Reads a virtual file by name for the selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `name` | `string` | File name. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, company_id: number, name: string, mime: string, content: Buffer, meta: any, created_at: string, updated_at: string}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'File not found'
|
||||||
|
|
||||||
|
## `modelvfs.reset`
|
||||||
|
|
||||||
|
*Source: `src/store/modelvfs/reset.js`*
|
||||||
|
|
||||||
|
Deletes all virtual files for the selected company (reset VFS).
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{deletedCount: number}>`
|
||||||
|
|
||||||
|
## `modelvfs.write`
|
||||||
|
|
||||||
|
*Source: `src/store/modelvfs/write.js`*
|
||||||
|
|
||||||
|
Writes (inserts or overwrites) a virtual file for the selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.name` | `string` | File path/name (e.g. 'candidate.avif', 'report.json'). |
|
||||||
|
| `[input.mime='application/octet-stream']` | `string` | Mime type. |
|
||||||
|
| `input.content` | `Buffer|string` | File content buffer or string. |
|
||||||
|
| `[input.meta]` | `object|string` | Optional metadata object or JSON string. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The stored file record metadata.
|
||||||
@@ -2,11 +2,29 @@
|
|||||||
|
|
||||||
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`.
|
||||||
|
|
||||||
|
## `numbers.ensureType`
|
||||||
|
|
||||||
|
*Source: `src/store/numbers/ensureType.js`*
|
||||||
|
|
||||||
|
Ensures a number sequence row exists for a company and type, inferring the company's prefix style, padding, and next counter if not yet created or unconfigured.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `runner` | `object` | The database runner. |
|
||||||
|
| `companyId` | `number` | The id of the company. |
|
||||||
|
| `type` | `string` | The number type (e.g. 'order', 'quote', 'sku'). |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, type: string, prefix: string, padding: number, next: number}>` — The sequence row.
|
||||||
|
|
||||||
## `numbers.list`
|
## `numbers.list`
|
||||||
|
|
||||||
*Source: `src/store/numbers/list.js`*
|
*Source: `src/store/numbers/list.js`*
|
||||||
|
|
||||||
Lists the number sequences of the user's selected company. Ensures that default sequences ('sku', 'entity', 'delivery') exist.
|
Lists the number sequences of the user's selected company. Ensures that default sequences ('sku', 'entity', 'delivery', 'adjustment', 'shipment', 'quote', 'order', 'return', 'retoure') exist.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
|
|||||||
113
API.order.md
113
API.order.md
@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
|
|||||||
|
|
||||||
*Source: `src/store/order/create.js`*
|
*Source: `src/store/order/create.js`*
|
||||||
|
|
||||||
Creates a new order for the user's selected company with its lines.
|
Creates a new order for the user's selected company with its lines and records article history.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -16,18 +16,18 @@ Creates a new order for the user's selected company with its lines.
|
|||||||
| `body` | `object` | The order input. |
|
| `body` | `object` | The order input. |
|
||||||
| `body.number` | `string` | The order number. |
|
| `body.number` | `string` | The order number. |
|
||||||
| `body.date` | `string` | The order date. |
|
| `body.date` | `string` | The order date. |
|
||||||
| `[body.entityId]` | `number` | The id of the customer entity. |
|
| `body.entityId` | `number` | The id of the customer entity. |
|
||||||
| `[body.externalNumber]` | `string` | An external (customer) reference number. |
|
| `[body.externalNumber]` | `string` | An external (customer) reference number. |
|
||||||
| `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>` | The order lines. |
|
| `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>` | The order lines. `shippable` (default true) false marks non-shippable lines (e.g. shipping cost, rounding) that are excluded from shipment and order completeness. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` — The created order.
|
`Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` — The created order.
|
||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If the order is invalid.
|
`Error` — If the order is invalid, no entity is selected, or entity not found.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
@@ -36,11 +36,76 @@ const order = await store.order.create(42, { number: 'B-2026-001', date: '2026-0
|
|||||||
// => { id, number, type, entityId, externalNumber, date, lines: [...] }
|
// => { id, number, type, entityId, externalNumber, date, lines: [...] }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `order.createFromQuote`
|
||||||
|
|
||||||
|
*Source: `src/store/order/createFromQuote.js`*
|
||||||
|
|
||||||
|
Creates a new sales order from an existing quote for the user's selected company. Copies lines and customer entity from the quote. Sets external reference to the quote number (quote.number) unless explicitly overridden. If order number is omitted, it is auto-allocated from the 'order' number sequence.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `quoteId` | `number` | The id of the source quote. |
|
||||||
|
| `[body]` | `object` | Optional overrides for the order. |
|
||||||
|
| `[body.number]` | `string` | The order number; auto-allocated from sequence if omitted. |
|
||||||
|
| `[body.date]` | `string` | The order date; defaults to current date if omitted. |
|
||||||
|
| `[body.createdAt]` | `string` | Creation timestamp; defaults to current time if omitted. |
|
||||||
|
| `[body.entityId]` | `number` | Customer entity id override; defaults to quote's entityId. |
|
||||||
|
| `[body.externalNumber]` | `string` | Customer reference override; defaults to quote's number. |
|
||||||
|
| `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>` | Lines override; defaults to quote's lines. `shippable` (default true) false excludes a line from shipment and order completeness. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` — The created order.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the quote is not found, no entity is selected, or the order is invalid.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const order = await store.order.createFromQuote(42, 10);
|
||||||
|
// => { id, number, type: 'order', entityId, externalNumber, date, lines: [...] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## `order.createShipment`
|
||||||
|
|
||||||
|
*Source: `src/store/order/createShipment.js`*
|
||||||
|
|
||||||
|
Creates a new shipment manifest for an order. Can contain specified items from the parent order.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `orderId` | `number` | The parent order manifest id. |
|
||||||
|
| `input` | `object` | Shipment input. |
|
||||||
|
| `input.lines` | `Array<{sku: string, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>` | Positions to ship. |
|
||||||
|
| `[input.date]` | `string|Date` | Optional shipment date. |
|
||||||
|
| `[input.externalNumber]` | `string` | Optional external delivery reference. |
|
||||||
|
| `input.shipping` | `object` | Required shipping ({carrierId, type, state, trackingCode?, warehouseId?}); see shipment.create. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The created shipment.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const shipment = await store.order.createShipment(googleAuthId, 10, { lines: [{ sku: 'ITEM-A', amount: 5 }], shipping: { carrierId: 1, type: 'parcel', state: 'draft' } });
|
||||||
|
```
|
||||||
|
|
||||||
## `order.delete`
|
## `order.delete`
|
||||||
|
|
||||||
*Source: `src/store/order/delete.js`*
|
*Source: `src/store/order/delete.js`*
|
||||||
|
|
||||||
Deletes an order by its id within the user's selected company.
|
Deletes an order by its id within the user's selected company. Refuses to delete the order if any shipment manifest references it.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -56,7 +121,7 @@ Deletes an order by its id within the user's selected company.
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If the order is not found.
|
`Error` — If the order is not found or has shipment manifests.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
@@ -104,19 +169,47 @@ Lists the orders of the user's selected company.
|
|||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of orders to skip. |
|
||||||
|
| `[opts.limit=50]` | `number` | Number of orders to return. |
|
||||||
|
| `[opts.search]` | `string` | Free-text search matching order number, external number, or customer name. |
|
||||||
|
| `[opts.onlyUnpaid=false]` | `boolean` | When true, returns only unpaid orders. |
|
||||||
|
| `[opts.onlyUndelivered=false]` | `boolean` | When true, returns only undelivered orders. |
|
||||||
|
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string}>>` — The orders.
|
`Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>` — The orders.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
const result = await store.order.list(42, { offset: 0, limit: 50, search: '1002', onlyUnpaid: true, onlyUndelivered: true, seekMonths: -1 });
|
||||||
|
// => { orders: [...], total: 10, unfilteredTotal: 42, offset: 0, limit: 50 }
|
||||||
|
|
||||||
const orders = await store.order.list(42);
|
const orders = await store.order.list(42);
|
||||||
// => [{ id, number, type, entityId, externalNumber, date }]
|
// => [{ id, number, type, entityId, externalNumber, date, createdAt, lines: [...] }]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `order.rebuildSummaries`
|
||||||
|
|
||||||
|
*Source: `src/store/order/rebuildSummaries.js`*
|
||||||
|
|
||||||
|
Rebuilds the `ordersummary` table for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `order.syncSummary`
|
||||||
|
|
||||||
|
*Source: `src/store/order/syncSummary.js`*
|
||||||
|
|
||||||
|
Centralized sync and rebuild logic for the materialized `ordersummary` table.
|
||||||
|
|
||||||
## `order.update`
|
## `order.update`
|
||||||
|
|
||||||
*Source: `src/store/order/update.js`*
|
*Source: `src/store/order/update.js`*
|
||||||
@@ -134,7 +227,7 @@ Updates an order's header and/or lines.
|
|||||||
| `[patch.date]` | `string` | The new order date. |
|
| `[patch.date]` | `string` | The new order date. |
|
||||||
| `[patch.entityId]` | `number` | The new customer entity id. |
|
| `[patch.entityId]` | `number` | The new customer entity id. |
|
||||||
| `[patch.externalNumber]` | `string` | The new external (customer) reference number. |
|
| `[patch.externalNumber]` | `string` | The new external (customer) reference number. |
|
||||||
| `[patch.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>` | The new order lines. |
|
| `[patch.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>` | The new order lines. `shippable` (default true) false marks non-shippable lines excluded from shipment and order completeness. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|||||||
84
API.package.md
Normal file
84
API.package.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# package
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `package.create`
|
||||||
|
|
||||||
|
*Source: `src/store/package/create.js`*
|
||||||
|
|
||||||
|
Creates a package for a shipping. The number is allocated from the `package` numbers sequence (own prefix + counter). The package must target an entity address.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | Package input. |
|
||||||
|
| `input.shippingId` | `number` | The shipping id. |
|
||||||
|
| `input.entityaddressId` | `number` | The target entity address id (required). |
|
||||||
|
| `input.type` | `string` | Package type. |
|
||||||
|
| `input.state` | `string` | Package state. |
|
||||||
|
| `[input.details]` | `string` | Optional details. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, shippingId: number, entityaddressId: number, number: string, type: string, state: string, details: string|null}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If shipping or entity address is not found, or input is invalid.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.package.create(googleAuthId, { shippingId: 9, entityaddressId: 3, type: 'box', state: 'packed' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## `package.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/package/delete.js`*
|
||||||
|
|
||||||
|
Deletes a package of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The package id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.package.delete(googleAuthId, 12);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `package.listByShippingId`
|
||||||
|
|
||||||
|
*Source: `src/store/package/listByShippingId.js`*
|
||||||
|
|
||||||
|
Lists packages of a shipping in the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `shippingId` | `number` | The shipping id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, shippingId: number, entityaddressId: number, number: string, type: string, state: string, details: string|null}>>`
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const packages = await store.package.listByShippingId(googleAuthId, 9);
|
||||||
|
```
|
||||||
83
API.payment.md
Normal file
83
API.payment.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# payment
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `payment.create`
|
||||||
|
|
||||||
|
*Source: `src/store/payment/create.js`*
|
||||||
|
|
||||||
|
Adds a payment to a manifest (order) using an account of the selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | Payment input. |
|
||||||
|
| `input.accountId` | `number` | The account id. |
|
||||||
|
| `input.manifestId` | `number` | The manifest (order) id. |
|
||||||
|
| `input.type` | `string` | Payment type. |
|
||||||
|
| `input.amount` | `number` | Payment amount. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, accountId: number, manifestId: number, type: string, amount: number}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If account or manifest is not found, or input is invalid.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.payment.create(googleAuthId, { accountId: 1, manifestId: 42, type: 'cash', amount: 19.9 });
|
||||||
|
```
|
||||||
|
|
||||||
|
## `payment.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/payment/delete.js`*
|
||||||
|
|
||||||
|
Deletes a payment belonging to a manifest of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The payment id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.payment.delete(googleAuthId, 7);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `payment.listByManifestId`
|
||||||
|
|
||||||
|
*Source: `src/store/payment/listByManifestId.js`*
|
||||||
|
|
||||||
|
Lists payments of a manifest (order) in the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `manifestId` | `number` | The manifest id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, accountId: number, manifestId: number, type: string, amount: number}>>`
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const payments = await store.payment.listByManifestId(googleAuthId, 42);
|
||||||
|
```
|
||||||
29
API.pic.md
29
API.pic.md
@@ -31,6 +31,35 @@ const image = await store.pic.getImageByHash(42, 'abc123');
|
|||||||
// => { buffer, w, h, size, contentType: 'image/avif' }
|
// => { buffer, w, h, size, contentType: 'image/avif' }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `pic.getSmallThumbByHash`
|
||||||
|
|
||||||
|
*Source: `src/store/pic/getSmallThumbByHash.js`*
|
||||||
|
|
||||||
|
Returns the small thumbnail image (AVIF, 32px max edge) for a given hash.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `fullHash` | `string` | The full image hash. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{buffer: Buffer, w: number, h: number, contentType: string}>` — The small thumbnail image.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the image is not found.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const thumb = await store.pic.getSmallThumbByHash(42, 'abc123');
|
||||||
|
// => { buffer, w, h, contentType: 'image/avif' }
|
||||||
|
```
|
||||||
|
|
||||||
## `pic.getThumbByHash`
|
## `pic.getThumbByHash`
|
||||||
|
|
||||||
*Source: `src/store/pic/getThumbByHash.js`*
|
*Source: `src/store/pic/getThumbByHash.js`*
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Finds a posterminal by its pairingpin.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<object|null>` — The posterminal row, or null if not found.
|
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>` — The posterminal, or null if not found.
|
||||||
|
|
||||||
## `posterminal.findByToken`
|
## `posterminal.findByToken`
|
||||||
|
|
||||||
@@ -62,13 +62,13 @@ Finds a posterminal by its auth token.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<object|null>` — The posterminal row, or null if not found.
|
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>` — The posterminal, or null if not found.
|
||||||
|
|
||||||
## `posterminal.getCategoryCount`
|
## `posterminal.getCategoryCount`
|
||||||
|
|
||||||
*Source: `src/store/posterminal/getCategoryCount.js`*
|
*Source: `src/store/posterminal/getCategoryCount.js`*
|
||||||
|
|
||||||
Counts categories assigned to the POS terminal updated after the given unix timestamp cursor.
|
Counts categories assigned to the POS terminal queued after the given sync queue cursor ID.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ Counts categories assigned to the POS terminal updated after the given unix time
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `input` | `object` | |
|
| `input` | `object` | |
|
||||||
| `input.posterminalId` | `number` | The POS terminal id. |
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. |
|
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -87,7 +87,7 @@ Counts categories assigned to the POS terminal updated after the given unix time
|
|||||||
|
|
||||||
*Source: `src/store/posterminal/getCategoryList.js`*
|
*Source: `src/store/posterminal/getCategoryList.js`*
|
||||||
|
|
||||||
Lists categories for a POS terminal modified after the cursor.
|
Lists categories for a POS terminal queued after the sync queue cursor ID.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ Lists categories for a POS terminal modified after the cursor.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `input` | `object` | |
|
| `input` | `object` | |
|
||||||
| `input.posterminalId` | `number` | The POS terminal id. |
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
| `[input.cursor=0]` | `number` | Unix timestamp cursor. |
|
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
|
||||||
| `[input.limit=20]` | `number` | Max items to return. |
|
| `[input.limit=20]` | `number` | Max items to return. |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ Lists categories for a POS terminal modified after the cursor.
|
|||||||
|
|
||||||
*Source: `src/store/posterminal/getCompositeProductCount.js`*
|
*Source: `src/store/posterminal/getCompositeProductCount.js`*
|
||||||
|
|
||||||
Counts composite products (Stücklisten) assigned to the POS terminal updated after the given unix timestamp cursor.
|
Counts composite products (Stücklisten) assigned to the POS terminal queued after the given sync queue cursor ID.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ Counts composite products (Stücklisten) assigned to the POS terminal updated af
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `input` | `object` | |
|
| `input` | `object` | |
|
||||||
| `input.posterminalId` | `number` | The POS terminal id. |
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. |
|
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -126,7 +126,7 @@ Counts composite products (Stücklisten) assigned to the POS terminal updated af
|
|||||||
|
|
||||||
*Source: `src/store/posterminal/getCustomerCount.js`*
|
*Source: `src/store/posterminal/getCustomerCount.js`*
|
||||||
|
|
||||||
Counts customers of the POS terminal company updated after the given unix timestamp cursor.
|
Counts customers of the POS terminal company queued after the given sync queue cursor ID.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ Counts customers of the POS terminal company updated after the given unix timest
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `input` | `object` | |
|
| `input` | `object` | |
|
||||||
| `input.posterminalId` | `number` | The POS terminal id. |
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. |
|
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -145,7 +145,7 @@ Counts customers of the POS terminal company updated after the given unix timest
|
|||||||
|
|
||||||
*Source: `src/store/posterminal/getCustomerGroupCount.js`*
|
*Source: `src/store/posterminal/getCustomerGroupCount.js`*
|
||||||
|
|
||||||
Counts customer groups of the POS terminal company updated after the given unix timestamp cursor.
|
Counts customer groups of the POS terminal company queued after the given sync queue cursor ID.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -153,30 +153,91 @@ Counts customer groups of the POS terminal company updated after the given unix
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `input` | `object` | |
|
| `input` | `object` | |
|
||||||
| `input.posterminalId` | `number` | The POS terminal id. |
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. |
|
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<number>` — The count of changed records.
|
`Promise<number>` — The count of changed records.
|
||||||
|
|
||||||
## `posterminal.getDeletedEntityCount`
|
## `posterminal.getCustomerGroupList`
|
||||||
|
|
||||||
*Source: `src/store/posterminal/getDeletedEntityCount.js`*
|
*Source: `src/store/posterminal/getCustomerGroupList.js`*
|
||||||
|
|
||||||
Counts deleted entities after the given cursor (currently always 0).
|
Lists customer groups for a POS terminal queued after the sync queue cursor ID, excluding 'Lieferanten'.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `[input]` | `object` | |
|
| `input` | `object` | |
|
||||||
| `[input.posterminalId]` | `number` | The POS terminal id. |
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
| `[input.cursor=0]` | `number` | Unix timestamp cursor. |
|
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
|
||||||
|
| `[input.limit=50]` | `number` | Max items to return. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<number>` — Always 0.
|
`Promise<Array<{customerGroupId: string, name: string, standard: string, discountPercent: string, lastChanged: string}>>`
|
||||||
|
|
||||||
|
## `posterminal.getCustomerList`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/getCustomerList.js`*
|
||||||
|
|
||||||
|
Lists customers for a POS terminal queued after the sync queue cursor ID, excluding 'Lieferanten'.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
|
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
|
||||||
|
| `[input.limit=20]` | `number` | Max items to return. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<object>>`
|
||||||
|
|
||||||
|
## `posterminal.getDeletedEntityCount`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/getDeletedEntityCount.js`*
|
||||||
|
|
||||||
|
Counts deleted entities for a POS terminal after the given cursor ID.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
|
| `[input.cursor=0]` | `number` | The highest synced obituary ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<number>` — Count of deleted entities.
|
||||||
|
|
||||||
|
## `posterminal.getDeletedEntityList`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/getDeletedEntityList.js`*
|
||||||
|
|
||||||
|
Lists deleted entities for a POS terminal after the given cursor ID.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
|
| `[input.cursor=0]` | `number` | The highest synced obituary ID. |
|
||||||
|
| `[input.limit=200]` | `number` | Max items to return. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, entityType: string, entityId: number}>>`
|
||||||
|
|
||||||
## `posterminal.getMaxOrderIdCount`
|
## `posterminal.getMaxOrderIdCount`
|
||||||
|
|
||||||
@@ -200,7 +261,7 @@ Returns the maximum order manifest id for the POS terminal company.
|
|||||||
|
|
||||||
*Source: `src/store/posterminal/getProductCount.js`*
|
*Source: `src/store/posterminal/getProductCount.js`*
|
||||||
|
|
||||||
Counts products assigned to the POS terminal updated after the given unix timestamp cursor.
|
Counts products assigned to the POS terminal queued after the given sync queue cursor ID.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -208,7 +269,7 @@ Counts products assigned to the POS terminal updated after the given unix timest
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `input` | `object` | |
|
| `input` | `object` | |
|
||||||
| `input.posterminalId` | `number` | The POS terminal id. |
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. |
|
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -219,7 +280,7 @@ Counts products assigned to the POS terminal updated after the given unix timest
|
|||||||
|
|
||||||
*Source: `src/store/posterminal/getProductList.js`*
|
*Source: `src/store/posterminal/getProductList.js`*
|
||||||
|
|
||||||
Lists products for a POS terminal modified after the cursor.
|
Lists products for a POS terminal queued after the sync queue cursor ID.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -227,7 +288,7 @@ Lists products for a POS terminal modified after the cursor.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `input` | `object` | |
|
| `input` | `object` | |
|
||||||
| `input.posterminalId` | `number` | The POS terminal id. |
|
| `input.posterminalId` | `number` | The POS terminal id. |
|
||||||
| `[input.cursor=0]` | `number` | Unix timestamp cursor. |
|
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
|
||||||
| `[input.limit=20]` | `number` | Max items to return. |
|
| `[input.limit=20]` | `number` | Max items to return. |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
@@ -250,13 +311,13 @@ Lists the posterminals of the user's selected company.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<Array<{id: number, name: string, token: string, pairingpin: string}>>` — The posterminals.
|
`Promise<Array<{id: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}>>` — The posterminals.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const terminals = await store.posterminal.list(42);
|
const terminals = await store.posterminal.list(42);
|
||||||
// => [{ id, name, token, pairingpin }]
|
// => [{ id, name, token, pairingpin, pairedAt, clientserial, maxknownobituaryid, rootCategoryId, syncRootArticles, lastseenAt, updatedAt, sort, enabled, clientversion }]
|
||||||
```
|
```
|
||||||
|
|
||||||
## `posterminal.pair`
|
## `posterminal.pair`
|
||||||
@@ -274,11 +335,95 @@ Completes POS device pairing for a terminal.
|
|||||||
| `input.token` | `string` | The auth token for the paired device. |
|
| `input.token` | `string` | The auth token for the paired device. |
|
||||||
| `[input.clientserial]` | `string` | The client name or serial identifier. |
|
| `[input.clientserial]` | `string` | The client name or serial identifier. |
|
||||||
| `[input.pairedAt]` | `string` | Timestamp when paired. |
|
| `[input.pairedAt]` | `string` | Timestamp when paired. |
|
||||||
|
| `[input.clientversion]` | `string|null` | Client software version reported by the POS device during pairing. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>`
|
`Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null, clientversion: string|null}>`
|
||||||
|
|
||||||
|
## `posterminal.recordObituary`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/recordObituary.js`*
|
||||||
|
|
||||||
|
Records an obituary for all active paired POS terminals of a company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.companyId` | `number` | The company id. |
|
||||||
|
| `input.entityType` | `string` | 'category' | 'article' | 'customer' | 'customergroup' |
|
||||||
|
| `input.entityId` | `number` | The deleted entity's id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction/runner. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
## `posterminal.recordSyncQueue`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/recordSyncQueue.js`*
|
||||||
|
|
||||||
|
Enqueues a sync notification for all POS terminals of a company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.companyId` | `number` | The company id. |
|
||||||
|
| `input.entityType` | `string` | 'category' | 'article' | 'customer' | 'customergroup' |
|
||||||
|
| `input.entityId` | `number` | The entity's id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction/runner. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
## `posterminal.setClientversion`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/setClientversion.js`*
|
||||||
|
|
||||||
|
Stores the software version reported by a POS terminal.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `number` | The posterminal id. |
|
||||||
|
| `clientversion` | `string|null` | Version string reported by the client (trimmed; empty → null). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, clientversion: string|null}>`
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.posterminal.setClientversion(7, '1.10.12.0');
|
||||||
|
// => { id: 7, clientversion: '1.10.12.0' }
|
||||||
|
```
|
||||||
|
|
||||||
|
## `posterminal.setMaxKnownObituaryId`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/setMaxKnownObituaryId.js`*
|
||||||
|
|
||||||
|
Updates the max known obituary ID for a POS terminal.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `number` | The posterminal id. |
|
||||||
|
| `maxknownobituaryid` | `number|null` | The highest synced obituary ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, maxknownobituaryid: number|null}>`
|
||||||
|
|
||||||
## `posterminal.setOff`
|
## `posterminal.setOff`
|
||||||
|
|
||||||
@@ -398,6 +543,23 @@ const result = await store.posterminal.setOnAll(42, 5);
|
|||||||
// => { id, categoryIds: [...] }
|
// => { id, categoryIds: [...] }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `posterminal.touchLastseen`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/touchLastseen.js`*
|
||||||
|
|
||||||
|
Updates lastseen_at for a POS terminal to the current timestamp.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `number` | The posterminal id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number}>`
|
||||||
|
|
||||||
## `posterminal.upsert`
|
## `posterminal.upsert`
|
||||||
|
|
||||||
*Source: `src/store/posterminal/upsert.js`*
|
*Source: `src/store/posterminal/upsert.js`*
|
||||||
@@ -414,11 +576,13 @@ Creates or updates a posterminal for the user's selected company.
|
|||||||
| `input.pairingpin` | `string` | The pairing pin. |
|
| `input.pairingpin` | `string` | The pairing pin. |
|
||||||
| `[input.token]` | `string` | Optional token. |
|
| `[input.token]` | `string` | Optional token. |
|
||||||
| `[input.id]` | `number` | Optional id to update an existing posterminal. |
|
| `[input.id]` | `number` | Optional id to update an existing posterminal. |
|
||||||
|
| `[input.sort]` | `number` | Display order (integer, ascending). Default 0 on create. |
|
||||||
|
| `[input.enabled]` | `boolean` | Whether the terminal may sync. Default true on create. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{id: number, name: string, token: string, pairingpin: string}>` — The upserted posterminal.
|
`Promise<{id: number, name: string, token: string, pairingpin: string, sort: number, enabled: number}>` — The upserted posterminal.
|
||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
@@ -428,5 +592,5 @@ Creates or updates a posterminal for the user's selected company.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
const terminal = await store.posterminal.upsert(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' });
|
const terminal = await store.posterminal.upsert(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' });
|
||||||
// => { id, name, token, pairingpin }
|
// => { id, name, token, pairingpin, sort, enabled }
|
||||||
```
|
```
|
||||||
|
|||||||
222
API.printeragent.md
Normal file
222
API.printeragent.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# printeragent
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `printeragent.create`
|
||||||
|
|
||||||
|
*Source: `src/store/printeragent/create.js`*
|
||||||
|
|
||||||
|
Creates an unpaired printer agent for the user's selected company. The agent registers itself later using the pairing pin.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | The printer agent input. |
|
||||||
|
| `input.name` | `string` | The display name. |
|
||||||
|
| `input.pairingpin` | `string` | The pairing pin the agent uses to register. |
|
||||||
|
| `[input.sort]` | `number` | Display order (integer, ascending). Default 0. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>` — The created printer agent.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Invalid printeragent' if name or pairingpin is missing/blank or sort is not an integer.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const agent = await store.printeragent.create(42, { name: 'Bar printer', pairingpin: '123456' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## `printeragent.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/printeragent/delete.js`*
|
||||||
|
|
||||||
|
Deletes a printer agent of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The printer agent id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Printeragent not found' if the id is unknown in the company.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.printeragent.delete(42, 5);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `printeragent.findByPairingpin`
|
||||||
|
|
||||||
|
*Source: `src/store/printeragent/findByPairingpin.js`*
|
||||||
|
|
||||||
|
Finds a printer agent by its pairing pin.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `pairingpin` | `string` | The pairing pin to search for. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}|null>` — The printer agent, or null if not found.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const agent = await store.printeragent.findByPairingpin('123456');
|
||||||
|
```
|
||||||
|
|
||||||
|
## `printeragent.findByToken`
|
||||||
|
|
||||||
|
*Source: `src/store/printeragent/findByToken.js`*
|
||||||
|
|
||||||
|
Finds a printer agent by its authentication token.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `token` | `string` | The auth token to search for. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}|null>` — The printer agent, or null if not found.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const agent = await store.printeragent.findByToken('abc...');
|
||||||
|
```
|
||||||
|
|
||||||
|
## `printeragent.list`
|
||||||
|
|
||||||
|
*Source: `src/store/printeragent/list.js`*
|
||||||
|
|
||||||
|
Lists the printer agents of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>>` — The printer agents.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const agents = await store.printeragent.list(42);
|
||||||
|
// => [{ id, companyId, name, token, pairingpin, pairedAt, clientserial, clientmodel, clientstate, agentversion, lastseenAt, updatedAt, sort }]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `printeragent.pair`
|
||||||
|
|
||||||
|
*Source: `src/store/printeragent/pair.js`*
|
||||||
|
|
||||||
|
Completes device pairing for a printer agent.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `number` | The printer agent id. |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.token` | `string` | Auth token for the paired device. |
|
||||||
|
| `[input.clientserial]` | `string` | Device serial identifier. |
|
||||||
|
| `[input.clientmodel]` | `string` | Device model name. |
|
||||||
|
| `[input.agentversion]` | `string` | Device agent software version. |
|
||||||
|
| `[input.pairedAt]` | `string` | Timestamp when paired. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
|
||||||
|
|
||||||
|
## `printeragent.touchLastseen`
|
||||||
|
|
||||||
|
*Source: `src/store/printeragent/touchLastseen.js`*
|
||||||
|
|
||||||
|
Updates lastseen_at (and optionally agentversion) for a printer agent.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `number` | The printer agent id. |
|
||||||
|
| `[agentversion]` | `string|null` | Optional agent version to update. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number}>`
|
||||||
|
|
||||||
|
## `printeragent.update`
|
||||||
|
|
||||||
|
*Source: `src/store/printeragent/update.js`*
|
||||||
|
|
||||||
|
Updates name and/or sort of a printer agent of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | The update input. |
|
||||||
|
| `input.id` | `number` | The printer agent id. |
|
||||||
|
| `[input.name]` | `string` | The new display name. |
|
||||||
|
| `[input.sort]` | `number` | The new display order (integer). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>` — The updated printer agent.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — 'Invalid printeragent' if name is blank, sort is not an integer, or neither is given; 'Printeragent not found' if id is unknown in the company.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const agent = await store.printeragent.update(42, { id: 5, name: 'Kitchen printer', sort: 1 });
|
||||||
|
```
|
||||||
|
|
||||||
|
## `printeragent.updateState`
|
||||||
|
|
||||||
|
*Source: `src/store/printeragent/updateState.js`*
|
||||||
|
|
||||||
|
Updates the client state of a printer agent.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `number` | The printer agent id. |
|
||||||
|
| `clientstate` | `string|null` | The state payload/string. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
|
||||||
100
API.printerjob.md
Normal file
100
API.printerjob.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# printerjob
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `printerjob.create`
|
||||||
|
|
||||||
|
*Source: `src/store/printerjob/create.js`*
|
||||||
|
|
||||||
|
Creates a new printer job for an agent printer belonging to the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | |
|
||||||
|
| `input.agentprinterId` | `number` | Target agent printer ID. |
|
||||||
|
| `input.data` | `string` | Job payload (ZPL code, etc.). |
|
||||||
|
| `[input.type]` | `string` | Job type ("zpl", "pdf", etc., defaults to agentprinter type or "zpl"). |
|
||||||
|
| `[input.state]` | `string` | Initial state (defaults to "pending"). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — Created printer job with printeragentId attached.
|
||||||
|
|
||||||
|
## `printerjob.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/printerjob/delete.js`*
|
||||||
|
|
||||||
|
Deletes a printer job belonging to the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | Printer job ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The deleted job's snapshot.
|
||||||
|
|
||||||
|
## `printerjob.getById`
|
||||||
|
|
||||||
|
*Source: `src/store/printerjob/getById.js`*
|
||||||
|
|
||||||
|
Retrieves a printer job by ID. If caller is an agent ({ agentId: number }), checks that the job belongs to that printer agent. If caller is a user (googleAuthId: number), checks that the job belongs to the selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `auth` | `number|{agentId: number}` | User's googleAuthId or { agentId }. |
|
||||||
|
| `id` | `number` | Printer job ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object|null>`
|
||||||
|
|
||||||
|
## `printerjob.list`
|
||||||
|
|
||||||
|
*Source: `src/store/printerjob/list.js`*
|
||||||
|
|
||||||
|
Lists printer jobs. If caller is an agent ({ agentId: number }), lists jobs for that agent. If caller is a user (googleAuthId: number), lists jobs for the selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `auth` | `number|{agentId: number}` | User's googleAuthId or { agentId }. |
|
||||||
|
| `[filters]` | `object` | |
|
||||||
|
| `[filters.agentprinterId]` | `number` | Filter by agentprinterId (company callers only). |
|
||||||
|
| `[filters.state]` | `string` | Filter by job state (e.g. "pending"). |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<object>>`
|
||||||
|
|
||||||
|
## `printerjob.updateState`
|
||||||
|
|
||||||
|
*Source: `src/store/printerjob/updateState.js`*
|
||||||
|
|
||||||
|
Updates a printer job's state ("processing", "completed", "cancelled", etc.).
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `auth` | `number|{agentId: number}` | User's googleAuthId or { agentId }. |
|
||||||
|
| `id` | `number` | Printer job ID. |
|
||||||
|
| `state` | `string` | New state. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — Updated printer job.
|
||||||
@@ -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`*
|
||||||
@@ -56,7 +64,7 @@ Lists the property names for the user's selected company.
|
|||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<Array<{id: number, name: string, sort: number, exclusive: boolean}>>`
|
`Promise<Array<{id: number, name: string, sort: number, exclusive: boolean, articleCount: number}>>`
|
||||||
|
|
||||||
## `propertyname.order`
|
## `propertyname.order`
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|||||||
30
API.quote.md
30
API.quote.md
@@ -104,19 +104,45 @@ Lists the quotes of the user's selected company.
|
|||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of quotes to skip. |
|
||||||
|
| `[opts.limit=50]` | `number` | Number of quotes to return. |
|
||||||
|
| `[opts.search]` | `string` | Free-text search matching quote number, external number, or customer name. |
|
||||||
|
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string}>>` — The quotes.
|
`Promise<{quotes: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>, positions?: number, grossTotal?: number, totalQuantity?: number, customerName?: string}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>` — The quotes.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
const result = await store.quote.list(42, { offset: 0, limit: 50, search: 'A-1002' });
|
||||||
|
// => { quotes: [...], total: 10, unfilteredTotal: 42, offset: 0, limit: 50 }
|
||||||
|
|
||||||
const quotes = await store.quote.list(42);
|
const quotes = await store.quote.list(42);
|
||||||
// => [{ id, number, type, entityId, externalNumber, date }]
|
// => [{ id, number, type, entityId, externalNumber, date, createdAt, lines: [...] }]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `quote.rebuildSummaries`
|
||||||
|
|
||||||
|
*Source: `src/store/quote/rebuildSummaries.js`*
|
||||||
|
|
||||||
|
Rebuilds the `quotesummary` table for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `quote.syncSummary`
|
||||||
|
|
||||||
|
*Source: `src/store/quote/syncSummary.js`*
|
||||||
|
|
||||||
|
Centralized sync and rebuild logic for the materialized `quotesummary` table.
|
||||||
|
|
||||||
## `quote.update`
|
## `quote.update`
|
||||||
|
|
||||||
*Source: `src/store/quote/update.js`*
|
*Source: `src/store/quote/update.js`*
|
||||||
|
|||||||
155
API.retoure.md
Normal file
155
API.retoure.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# retoure
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `retoure.create`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/create.js`*
|
||||||
|
|
||||||
|
Creates a new retoure for the user's selected company with its lines.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `body` | `object` | The retoure input. |
|
||||||
|
| `[body.number]` | `string` | The retoure number. |
|
||||||
|
| `body.date` | `string` | The retoure date. |
|
||||||
|
| `[body.entityId]` | `number` | The id of the customer entity. |
|
||||||
|
| `[body.externalNumber]` | `string` | An external reference number. |
|
||||||
|
| `[body.lines]` | `Array<object>` | The retoure lines. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The created retoure.
|
||||||
|
|
||||||
|
## `retoure.createShipment`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/createShipment.js`*
|
||||||
|
|
||||||
|
Creates a shipment manifest for a retoure.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `retoureId` | `number` | The parent retoure manifest id. |
|
||||||
|
| `input` | `object` | Shipment input. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The created shipment.
|
||||||
|
|
||||||
|
## `retoure.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/delete.js`*
|
||||||
|
|
||||||
|
Deletes a retoure by its id within the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the retoure to delete. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the retoure is not found.
|
||||||
|
|
||||||
|
## `retoure.getById`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/getById.js`*
|
||||||
|
|
||||||
|
Returns a single retoure by its id within the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the retoure. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` — The retoure with its lines.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the retoure is not found.
|
||||||
|
|
||||||
|
## `retoure.list`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/list.js`*
|
||||||
|
|
||||||
|
Lists the retoures of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of retoures to skip. |
|
||||||
|
| `[opts.limit=50]` | `number` | Number of retoures to return. |
|
||||||
|
| `[opts.search]` | `string` | Free-text search matching retoure number, external number, or customer name. |
|
||||||
|
| `[opts.onlyUnpaid=false]` | `boolean` | When true, returns only unpaid retoures. |
|
||||||
|
| `[opts.onlyUndelivered=false]` | `boolean` | When true, returns only undelivered retoures. |
|
||||||
|
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{retoures: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
|
||||||
|
## `retoure.rebuildSummaries`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/rebuildSummaries.js`*
|
||||||
|
|
||||||
|
Rebuilds the `retouresummary` table for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `retoure.syncSummary`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/syncSummary.js`*
|
||||||
|
|
||||||
|
Centralized sync and rebuild logic for the materialized `retouresummary` table.
|
||||||
|
|
||||||
|
## `retoure.update`
|
||||||
|
|
||||||
|
*Source: `src/store/retoure/update.js`*
|
||||||
|
|
||||||
|
Updates a retoure's header and/or lines.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the retoure to update. |
|
||||||
|
| `patch` | `object` | The update patch. |
|
||||||
|
| `[patch.number]` | `string` | The new retoure number. |
|
||||||
|
| `[patch.date]` | `string` | The new retoure date. |
|
||||||
|
| `[patch.entityId]` | `number` | The new customer entity id. |
|
||||||
|
| `[patch.externalNumber]` | `string` | The new external reference number. |
|
||||||
|
| `[patch.lines]` | `Array<object>` | The new retoure lines. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The updated retoure.
|
||||||
83
API.return.md
Normal file
83
API.return.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# return
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `return.book`
|
||||||
|
|
||||||
|
*Source: `src/store/return/book.js`*
|
||||||
|
|
||||||
|
Books all positions of a return manifest to the ledger.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `object` | Storage provider instance. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Function` — `(googleAuthId, input, tx) => Promise<object>`
|
||||||
|
|
||||||
|
## `return.getById`
|
||||||
|
|
||||||
|
*Source: `src/store/return/getById.js`*
|
||||||
|
|
||||||
|
Returns a return manifest by its id within the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the return manifest. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<object>` — The return manifest with lines and summary.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the return is not found.
|
||||||
|
|
||||||
|
## `return.list`
|
||||||
|
|
||||||
|
*Source: `src/store/return/list.js`*
|
||||||
|
|
||||||
|
Lists the returns of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of returns to skip. |
|
||||||
|
| `[opts.limit=50]` | `number` | Number of returns to return. |
|
||||||
|
| `[opts.search]` | `string` | Free-text search matching return number, external number, or supplier name. |
|
||||||
|
| `[opts.onlyUnbooked=false]` | `boolean` | When true, returns only returns with unbooked stock items. |
|
||||||
|
| `[opts.supplierId]` | `number` | Filter by supplier entity id. |
|
||||||
|
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{returns: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
|
||||||
|
## `return.rebuildSummaries`
|
||||||
|
|
||||||
|
*Source: `src/store/return/rebuildSummaries.js`*
|
||||||
|
|
||||||
|
Rebuilds the `returnsummary` table for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `return.syncSummary`
|
||||||
|
|
||||||
|
*Source: `src/store/return/syncSummary.js`*
|
||||||
|
|
||||||
|
Centralized sync and rebuild logic for the materialized `returnsummary` table. Keeps `returnsummary` in sync with manifest (return), manifestarticle, article (track_stock), and ledger (RETURN transactions).
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# runner.js
|
|
||||||
|
|
||||||
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
|
||||||
|
|
||||||
## `runner.js.runner`
|
|
||||||
|
|
||||||
*Source: `src/store/runner.js`*
|
|
||||||
|
|
||||||
Resolves the object a store method should query through. Converted store modules call `runner(sql, params)` or `runner.query(sql, params)`. During the incremental migration they may still be constructed with a raw `mssql` pool, or called with a raw `mssql` Transaction as `tx`. This adapter exposes the unified runner contract over both so mixed call sites keep working until every module is converted.
|
|
||||||
|
|
||||||
**Parameters**
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `provider` | `import('../providers/provider.js').DatabaseProvider` | |
|
|
||||||
| `[tx]` | `object` | Optional provider runner or legacy mssql Transaction. |
|
|
||||||
|
|
||||||
**Returns**
|
|
||||||
|
|
||||||
`((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }`
|
|
||||||
111
API.shipment.md
Normal file
111
API.shipment.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# shipment
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `shipment.create`
|
||||||
|
|
||||||
|
*Source: `src/store/shipment/create.js`*
|
||||||
|
|
||||||
|
Creates a new shipment manifest for an order. Validates that all article positions belong to the order and do not exceed remaining ordered quantities.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `shipment.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/shipment/delete.js`*
|
||||||
|
|
||||||
|
Deletes or cancels a shipment manifest of the selected company. - If no ledger rows exist: physically deletes the shipment manifest. - If ledger rows exist: reverses the ledger entries (storno document entry + warehouse stock adjustment), marks the shipment as 'cancelled' in shipmentsummary, and updates the parent order's summary. - Shipped shipments (with packages in 'shipped' state) cannot be deleted.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `shipment.getById`
|
||||||
|
|
||||||
|
*Source: `src/store/shipment/getById.js`*
|
||||||
|
|
||||||
|
Retrieves a shipment manifest by ID for the selected company, including lines and packages.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `shipment.list`
|
||||||
|
|
||||||
|
*Source: `src/store/shipment/list.js`*
|
||||||
|
|
||||||
|
Lists the shipments of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[opts]` | `object` | Optional pagination and filter options. |
|
||||||
|
| `[opts.offset=0]` | `number` | Number of shipments to skip. |
|
||||||
|
| `[opts.limit=50]` | `number` | Number of shipments to return. |
|
||||||
|
| `[opts.search]` | `string` | Free-text search matching shipment number, order number, external number, tracking code, or customer name. |
|
||||||
|
| `[opts.state]` | `'new'|'delivered'|'cancelled'|'all'` | Filter by delivery state. |
|
||||||
|
| `[opts.includeCancelled=false]` | `boolean` | Whether to include cancelled shipments when state is not set. |
|
||||||
|
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
|
||||||
|
| `[opts.orderId]` | `number` | Optional filter for parent order manifest id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{shipments: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
|
||||||
|
|
||||||
|
## `shipment.listByOrderId`
|
||||||
|
|
||||||
|
*Source: `src/store/shipment/listByOrderId.js`*
|
||||||
|
|
||||||
|
Lists all shipment manifests belonging to an order.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `shipment.rebuildSummaries`
|
||||||
|
|
||||||
|
*Source: `src/store/shipment/rebuildSummaries.js`*
|
||||||
|
|
||||||
|
Rebuilds the `shipmentsummary` table for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
## `shipment.syncSummary`
|
||||||
|
|
||||||
|
*Source: `src/store/shipment/syncSummary.js`*
|
||||||
|
|
||||||
|
Centralized sync and rebuild logic for the materialized `shipmentsummary` table.
|
||||||
|
|
||||||
|
## `shipment.update`
|
||||||
|
|
||||||
|
*Source: `src/store/shipment/update.js`*
|
||||||
|
|
||||||
|
Updates a shipment's state or metadata.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
101
API.shipping.md
Normal file
101
API.shipping.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# shipping
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `shipping.create`
|
||||||
|
|
||||||
|
*Source: `src/store/shipping/create.js`*
|
||||||
|
|
||||||
|
Creates a shipping (package) for a shipment manifest. The number is allocated from the `shipping` numbers sequence (own prefix + counter). If state is 'shipped', adjusts stock by appending negative ledger movements for tracked shipment lines, writes article history for all items, and ensures stock is deducted once.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | Shipping input. |
|
||||||
|
| `input.manifestId` | `number` | The shipment manifest id. |
|
||||||
|
| `input.carrierId` | `number` | The carrier id. |
|
||||||
|
| `input.type` | `string` | Shipping type. |
|
||||||
|
| `input.state` | `string` | Shipping state ('shipped' or other). |
|
||||||
|
| `[input.trackingCode]` | `string` | Carrier tracking code. |
|
||||||
|
| `[input.warehouseId]` | `number` | Optional warehouse ID for stock deduction (defaults to company's first warehouse). |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrierId: number, carrier: string|null, type: string, state: string}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If manifest is not found, carrier is not found, manifest is not a shipment, shipment is already shipped, or input is invalid.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.shipping.create(googleAuthId, { manifestId: 42, carrierId: 1, type: 'parcel', state: 'shipped', trackingCode: '1Z999' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## `shipping.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/shipping/delete.js`*
|
||||||
|
|
||||||
|
Deletes a shipping of the user's selected company. Its packages are removed by cascade. Once shipped, a shipping cannot be deleted because stock movements have been ledgered.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The shipping id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If shipping is not found or is already shipped.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await store.shipping.delete(googleAuthId, 9);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `shipping.listByManifestId`
|
||||||
|
|
||||||
|
*Source: `src/store/shipping/listByManifestId.js`*
|
||||||
|
|
||||||
|
Lists shippings of a manifest (order) in the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `manifestId` | `number` | The manifest id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, manifestId: number, number: string, type: string, state: string}>>`
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const shippings = await store.shipping.listByManifestId(googleAuthId, 42);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `shipping.update`
|
||||||
|
|
||||||
|
*Source: `src/store/shipping/update.js`*
|
||||||
|
|
||||||
|
Updates a shipping record for the user's selected company. If transitioned to 'shipped', books ledger movements and adjusts stock. Once shipped, shipping cannot be undone (state cannot be changed away from 'shipped').
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
44
API.stock.md
44
API.stock.md
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
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`.
|
||||||
|
|
||||||
|
## `stock.accept`
|
||||||
|
|
||||||
|
*Source: `src/store/stock/accept.js`*
|
||||||
|
|
||||||
|
Accepts a delivery manifest item (or multiple delivery items). Requirements: - Every accepted delivery manifest item receives a document ledger entry: - If amount >= 0: transaction_type = 'ACCEPTANCE', amount = +amount, warehouse_id = null. - If amount < 0: transaction_type = 'RETURN', amount = amount (negative), warehouse_id = null. - Always records article history with delivery number, supplier, EK brutto, and quantity. - If the connected article has stock tracking (track_stock == 1): Calls adjust with the item's amount to adjust physical inventory in the target warehouse. - If the connected article does not have stock tracking: Only document acceptance is booked; no warehouse adjust is performed. - Double acceptance of an already accepted line is strictly prevented.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `object` | The database provider. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Function` — Async function accepting delivery items.
|
||||||
|
|
||||||
## `stock.adjust`
|
## `stock.adjust`
|
||||||
|
|
||||||
*Source: `src/store/stock/adjust.js`*
|
*Source: `src/store/stock/adjust.js`*
|
||||||
@@ -26,3 +42,31 @@ Adjusts article stock manually by creating an implicit manifest of type "adjustm
|
|||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>`
|
`Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>`
|
||||||
|
|
||||||
|
## `stock.rebuildSummaries`
|
||||||
|
|
||||||
|
*Source: `src/store/stock/rebuildSummaries.js`*
|
||||||
|
|
||||||
|
Rebuilds the `articlewarehousesummary` table for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
|
||||||
|
|
||||||
|
## `stock.ship`
|
||||||
|
|
||||||
|
*Source: `src/store/stock/ship.js`*
|
||||||
|
|
||||||
|
Ships a shipment manifest item (or multiple shipment items). Requirements: - Every shipped shipment manifest item receives a document ledger entry: - transaction_type = 'SHIPMENT', amount = -amount, warehouse_id = null. - Line amount must be positive (> 0); negative or zero line amounts are rejected. - Always records article history with shipment number, customer, and quantity. - If the connected article has stock tracking (track_stock == 1): Calls adjust with: - -amount (outgoing stock from warehouse). - If the connected article does not have stock tracking: Only document shipment is booked; no warehouse adjust is performed. - Double shipping of an already shipped line is strictly prevented.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `provider` | `object` | The database provider. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Function` — Async function shipping shipment items.
|
||||||
|
|||||||
130
API.subagentschedule.md
Normal file
130
API.subagentschedule.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# subagentschedule
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `subagentschedule.create`
|
||||||
|
|
||||||
|
*Source: `src/store/subagentschedule/create.js`*
|
||||||
|
|
||||||
|
Creates a new subagent schedule for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `input` | `object` | Schedule input. |
|
||||||
|
| `input.prompt` | `string` | The task prompt for the subagent. |
|
||||||
|
| `input.cron` | `string` | Standard 5-field cron timetable string. |
|
||||||
|
| `[input.desc]` | `string` | Optional schedule description. |
|
||||||
|
| `[input.skills]` | `string|Array<string>` | Optional skill names or JSON string. |
|
||||||
|
| `[input.lastagentnote]` | `string` | Optional initial agent note. |
|
||||||
|
| `[input.max_itr=15]` | `number` | Optional max iterations per run. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If prompt is missing, cron is invalid, or max_itr is invalid.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const schedule = await store.subagentschedule.create(googleAuthId, {
|
||||||
|
desc: 'Nightly inventory audit',
|
||||||
|
prompt: 'Check stock discrepancies across all warehouses and summarize.',
|
||||||
|
cron: '0 2 * * *',
|
||||||
|
skills: ['stock', 'reporting'],
|
||||||
|
max_itr: 20,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `subagentschedule.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/subagentschedule/delete.js`*
|
||||||
|
|
||||||
|
Deletes a subagent schedule by id for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The schedule ID to delete. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If schedule not found.
|
||||||
|
|
||||||
|
## `subagentschedule.getById`
|
||||||
|
|
||||||
|
*Source: `src/store/subagentschedule/getById.js`*
|
||||||
|
|
||||||
|
Retrieves a single subagent schedule by id for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The schedule ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}|null>`
|
||||||
|
|
||||||
|
## `subagentschedule.list`
|
||||||
|
|
||||||
|
*Source: `src/store/subagentschedule/list.js`*
|
||||||
|
|
||||||
|
Lists all subagent schedules for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>>`
|
||||||
|
|
||||||
|
## `subagentschedule.update`
|
||||||
|
|
||||||
|
*Source: `src/store/subagentschedule/update.js`*
|
||||||
|
|
||||||
|
Updates an existing subagent schedule for the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The schedule ID to update. |
|
||||||
|
| `input` | `object` | Update payload. |
|
||||||
|
| `[input.prompt]` | `string` | Optional updated task prompt. |
|
||||||
|
| `[input.cron]` | `string` | Optional updated cron expression. |
|
||||||
|
| `[input.desc]` | `string|null` | Optional updated description. |
|
||||||
|
| `[input.skills]` | `string|Array<string>|null` | Optional updated skills. |
|
||||||
|
| `[input.lastagentnote]` | `string|null` | Optional updated agent note. |
|
||||||
|
| `[input.max_itr]` | `number` | Optional updated max iterations. |
|
||||||
|
| `[input.lastrun_at]` | `string|null` | Optional updated last run timestamp. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If schedule not found, prompt is empty, cron is invalid, or max_itr is invalid.
|
||||||
@@ -52,7 +52,7 @@ Deletes a supplier by its id within the user's selected company.
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If the supplier is not found.
|
`Error` — If the supplier is not found or is in use.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
|||||||
|
|
||||||
*Source: `src/store/warehouse/delete.js`*
|
*Source: `src/store/warehouse/delete.js`*
|
||||||
|
|
||||||
Deletes a warehouse for the user's selected company, unless referenced in the ledger.
|
Deletes a warehouse for the user's selected company, unless referenced in the ledger or it is the last warehouse while articles have stock tracking enabled.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ Deletes a warehouse for the user's selected company, unless referenced in the le
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If warehouse not found or is in use in the ledger.
|
`Error` — If warehouse not found, in use in the ledger, or is the last warehouse with track_stock articles.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
|
|||||||
@@ -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). |
|
||||||
@@ -111,9 +111,9 @@ Abgeleitet aus den Schemata (Eltern vor Kindern). Die Registry speichert sie exp
|
|||||||
6. `article` (Selbstreferenz), `articletext`, `articleean`, `articlepic`, `articleproperty`, `articleattributevalue`, `articlelistarticle`, `optionslistarticle`, `brandpic`, `categoryarticle`, `categorypic`, `categorytext`
|
6. `article` (Selbstreferenz), `articletext`, `articleean`, `articlepic`, `articleproperty`, `articleattributevalue`, `articlelistarticle`, `optionslistarticle`, `brandpic`, `categoryarticle`, `categorypic`, `categorytext`
|
||||||
7. `price`, `articlesupplier`
|
7. `price`, `articlesupplier`
|
||||||
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`, `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`)
|
||||||
|
|
||||||
@@ -315,7 +315,6 @@ Betroffene Felder (Registry-Flag `secret`):
|
|||||||
|
|
||||||
- `google_auth`: `email`, `sub`, `picture`, `picturebinary` (personenbezogen)
|
- `google_auth`: `email`, `sub`, `picture`, `picturebinary` (personenbezogen)
|
||||||
- `posterminal`: `token`, `pairingpin`
|
- `posterminal`: `token`, `pairingpin`
|
||||||
- `syncpos`: `paircode`, `token`
|
|
||||||
- `companysetting`: Zeilen mit `writeonly = 1` (z. B. API-Schlüssel)
|
- `companysetting`: Zeilen mit `writeonly = 1` (z. B. API-Schlüssel)
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
|
|||||||
78
README.md
78
README.md
@@ -42,12 +42,11 @@ Company-scoped methods take **no** `companyId` argument: they use `google_auth.s
|
|||||||
|
|
||||||
For `POST /api/company`:
|
For `POST /api/company`:
|
||||||
|
|
||||||
1. Fails if `name` is taken (`Company name not available`)
|
1. Ensures language by `iso639` (get or create)
|
||||||
2. Ensures language by `iso639` (get or create)
|
2. Ensures tax rows for `taxrate`, `reducedtaxrate`, and `0` → `taxfreerateId`
|
||||||
3. Ensures tax rows for `taxrate`, `reducedtaxrate`, and `0` → `taxfreerateId`
|
3. Creates root category (`parentId = null`)
|
||||||
4. Creates root category (`parentId = null`)
|
4. Creates POS `customergroup` (name `POS`), then inserts company with `poscustomergroup_id` (**NOT NULL**), then links the group via `customergroup.company_id`
|
||||||
5. Creates POS `customergroup` (name `POS`), then inserts company with `poscustomergroup_id` (**NOT NULL**), then links the group via `customergroup.company_id`
|
5. Links creator as `companyuser` (`googleAuthId` → `company_id`)
|
||||||
6. Links creator as `companyuser` (`googleAuthId` → `company_id`)
|
|
||||||
|
|
||||||
`company.poscustomergroup_id` is required (POS pricing in `getTreeForCompany` / category articles). Circular FK with `customergroup.company_id`: the group is inserted first with `company_id` null, then linked after the company row exists.
|
`company.poscustomergroup_id` is required (POS pricing in `getTreeForCompany` / category articles). Circular FK with `customergroup.company_id`: the group is inserted first with `company_id` null, then linked after the company row exists.
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ Returns only companies where `googleAuthId` has a `companyuser` row.
|
|||||||
|
|
||||||
For `PATCH /api/company/:id`:
|
For `PATCH /api/company/:id`:
|
||||||
|
|
||||||
1. Fails if company missing / name taken by another company
|
1. Fails if company missing
|
||||||
2. Ensures lang + tax rates
|
2. Ensures lang + tax rates
|
||||||
3. Updates `name`, `default_language_id`, `taxrate`, `reducedtaxrate`
|
3. Updates `name`, `default_language_id`, `taxrate`, `reducedtaxrate`
|
||||||
(`taxfreerateId` / root category unchanged)
|
(`taxfreerateId` / root category unchanged)
|
||||||
@@ -169,7 +168,7 @@ Company-scoped key/value settings. Auth via selected company (`google_auth.selec
|
|||||||
- With `frontendOnly: true`, backend-only keys are omitted / `get` returns `undefined` (no leak via error type beyond missing).
|
- With `frontendOnly: true`, backend-only keys are omitted / `get` returns `undefined` (no leak via error type beyond missing).
|
||||||
- `writeonly: true` → usergui gets a `"***"` scrambled version when retrieving the key (in `list`, or in `get` when `maskWriteonly` is enabled / `frontendOnly: true`). The backend receives the full value.
|
- `writeonly: true` → usergui gets a `"***"` scrambled version when retrieving the key (in `list`, or in `get` when `maskWriteonly` is enabled / `frontendOnly: true`). The backend receives the full value.
|
||||||
- `keys.tavily`, `keys.openrouter`, and `keys.serper` default to `writeonly: true` on `set`.
|
- `keys.tavily`, `keys.openrouter`, and `keys.serper` default to `writeonly: true` on `set`.
|
||||||
- `set` only allows a fixed allow-list of keys: `ui.theme`, `keys.tavily`, `keys.openrouter`, `keys.serper`, `labelprinter.addr`, `labelprinter.type`.
|
- `set` only allows a fixed allow-list of keys: `ui.theme`, `keys.tavily`, `keys.openrouter`, `keys.serper`, `systemprompt-prefix`, `systemprompt-suffix`, `default-model`, `modelslug`, `modelslugflag`, `modelreasoning`, `modelslugimage`.
|
||||||
- No delete endpoint — settings are upsert / read only.
|
- No delete endpoint — settings are upsert / read only.
|
||||||
|
|
||||||
Errors: `No company selected`, `Company not found`, `Not authorized`, `Invalid setting`.
|
Errors: `No company selected`, `Company not found`, `Not authorized`, `Invalid setting`.
|
||||||
@@ -296,7 +295,8 @@ Maps articles to suppliers (`entity`) with purchase pricing, stock, packaging un
|
|||||||
| `id` | `INT` | Primary Key (`IDENTITY(1,1)`) |
|
| `id` | `INT` | Primary Key (`IDENTITY(1,1)`) |
|
||||||
| `article_id` | `INT` | FK → `article(id)` (`ON DELETE CASCADE`) |
|
| `article_id` | `INT` | FK → `article(id)` (`ON DELETE CASCADE`) |
|
||||||
| `entity_id` | `INT` | FK → `entity(id)` (`ON DELETE CASCADE`) |
|
| `entity_id` | `INT` | FK → `entity(id)` (`ON DELETE CASCADE`) |
|
||||||
| `price_id` | `INT` | FK → `price(id)` (`ON DELETE SET NULL`), nullable |
|
| `net` | `FLOAT` | Purchase net price, nullable |
|
||||||
|
| `tax_id` | `INT` | FK → `tax(id)` (`ON DELETE NO ACTION`), nullable |
|
||||||
| `sku` | `NVARCHAR(100)` | Supplier's SKU code, nullable |
|
| `sku` | `NVARCHAR(100)` | Supplier's SKU code, nullable |
|
||||||
| `stock` | `FLOAT` | Stock available at supplier, nullable |
|
| `stock` | `FLOAT` | Stock available at supplier, nullable |
|
||||||
| `pu` | `FLOAT` | Packaging unit (Verpackungseinheit), default `1` |
|
| `pu` | `FLOAT` | Packaging unit (Verpackungseinheit), default `1` |
|
||||||
@@ -322,13 +322,31 @@ Company-scoped goods receipts on `manifest` / `manifestarticle` with fixed `type
|
|||||||
|
|
||||||
| Method | Signature | Returns |
|
| Method | Signature | Returns |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `list` | `(googleAuthId)` | `DeliverySummary[]` |
|
| `list` | `(googleAuthId, opts?: DeliveryListOpts)` | `DeliverySummary[] | WindowedDeliveries` |
|
||||||
| `getById` | `(googleAuthId, id)` | `Delivery` |
|
| `getById` | `(googleAuthId, id)` | `Delivery` |
|
||||||
| `create` | `(googleAuthId, { date, supplierId?, externalNumber?, lines })` | `Delivery` |
|
| `create` | `(googleAuthId, { date, supplierId?, externalNumber?, lines })` | `Delivery` |
|
||||||
| `update` | `(googleAuthId, id, { number?, date?, supplierId?, externalNumber?, lines? })` | `Delivery` |
|
| `update` | `(googleAuthId, id, { number?, date?, supplierId?, externalNumber?, lines? })` | `Delivery` |
|
||||||
| `delete` | `(googleAuthId, id)` | `void` |
|
| `delete` | `(googleAuthId, id)` | `void` |
|
||||||
|
| `rebuildSummaries` | `(googleAuthId)` | `{ ok: boolean, companyId: number }` |
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
type DeliveryListOpts = {
|
||||||
|
offset?: number,
|
||||||
|
limit?: number,
|
||||||
|
search?: string,
|
||||||
|
onlyUnbooked?: boolean,
|
||||||
|
supplierId?: number,
|
||||||
|
seekMonths?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowedDeliveries = {
|
||||||
|
deliveries: DeliverySummary[],
|
||||||
|
total: number,
|
||||||
|
unfilteredTotal: number,
|
||||||
|
offset: number,
|
||||||
|
limit: number,
|
||||||
|
}
|
||||||
|
|
||||||
type DeliverySummary = {
|
type DeliverySummary = {
|
||||||
id: number,
|
id: number,
|
||||||
number: string,
|
number: string,
|
||||||
@@ -336,6 +354,31 @@ type DeliverySummary = {
|
|||||||
supplierId: number | null, // manifest.entity_id
|
supplierId: number | null, // manifest.entity_id
|
||||||
externalNumber: string | null, // manifest.external_number
|
externalNumber: string | null, // manifest.external_number
|
||||||
date: Date,
|
date: Date,
|
||||||
|
createdAt?: string,
|
||||||
|
supplierName?: string | null,
|
||||||
|
summary?: {
|
||||||
|
positions: number,
|
||||||
|
grossTotal: number,
|
||||||
|
netTotal: number,
|
||||||
|
totalQuantity: number,
|
||||||
|
stockPositions: number,
|
||||||
|
bookedPositions: number,
|
||||||
|
unbookedPositions: number,
|
||||||
|
stockQuantity: number,
|
||||||
|
bookedQuantity: number,
|
||||||
|
unbookedQuantity: number,
|
||||||
|
isBooked: boolean,
|
||||||
|
},
|
||||||
|
ledger?: {
|
||||||
|
totalPositions: number,
|
||||||
|
completelyOnLedgerPositions: number,
|
||||||
|
bookedPositions: number,
|
||||||
|
unbookedPositions: number,
|
||||||
|
totalQuantity: number,
|
||||||
|
bookedQuantity: number,
|
||||||
|
unbookedQuantity: number,
|
||||||
|
isBooked: boolean,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeliveryLine = {
|
type DeliveryLine = {
|
||||||
@@ -501,7 +544,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 }` |
|
||||||
|
|
||||||
@@ -520,7 +563,7 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c
|
|||||||
|
|
||||||
`move` moves a category to a new parent. Both `id` (source) and `parentId` (target) are validated via `resolveCategoryAccess` — the target must be a real category of the same authorized company, else `Parent not found`. Moving into itself or into a descendant (cycle) → `Invalid parent`. The moved category gets the next sibling `sort` under the new parent. Returns `{ id, parentId, sort }`. For `POST /api/category/:id/move`.
|
`move` moves a category to a new parent. Both `id` (source) and `parentId` (target) are validated via `resolveCategoryAccess` — the target must be a real category of the same authorized company, else `Parent not found`. Moving into itself or into a descendant (cycle) → `Invalid parent`. The moved category gets the next sibling `sort` under the new parent. Returns `{ id, parentId, sort }`. For `POST /api/category/:id/move`.
|
||||||
|
|
||||||
`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`pic.fullbinary`). Thumb always scaled to max 200×200 (`pic.thumbbinary`) via `sharp`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `categorypic`. Auth via `resolveCategoryAccess`. Errors: `Category not found`, `Not authorized`.
|
`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`storageDir/fullbinary`). Thumb always scaled to max 200×200 (`storageDir/thumbbinary`) via `wasm-image-optimization`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `categorypic`. Auth via `resolveCategoryAccess`. Errors: `Category not found`, `Not authorized`.
|
||||||
|
|
||||||
`addImage` returns:
|
`addImage` returns:
|
||||||
|
|
||||||
@@ -558,7 +601,7 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c
|
|||||||
| `orderImages` | `(googleAuthId: string, sku: string, { fullHashes: string[] })` | `{ fullHashes: string[] }` |
|
| `orderImages` | `(googleAuthId: string, sku: string, { fullHashes: string[] })` | `{ fullHashes: string[] }` |
|
||||||
| `delete` | `(googleAuthId: string, sku: string)` | `void` |
|
| `delete` | `(googleAuthId: string, sku: string)` | `void` |
|
||||||
|
|
||||||
`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`pic.fullbinary`). Thumb always scaled to max 200×200 (`pic.thumbbinary`) via `sharp`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `articlepic`. Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
|
`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`storageDir/fullbinary`). Thumb always scaled to max 200×200 (`storageDir/thumbbinary`) via `wasm-image-optimization`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `articlepic`. Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
|
||||||
|
|
||||||
`addImage` returns `ImageResult`:
|
`addImage` returns `ImageResult`:
|
||||||
|
|
||||||
@@ -643,8 +686,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 +724,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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -778,7 +821,8 @@ Company-scoped number sequences (`numbers` table). Auth via selected company (`g
|
|||||||
| `PATCH /api/article/:sku/supplier/:id` | `articlesupplier.update(googleAuthId, sku, id, patch)` |
|
| `PATCH /api/article/:sku/supplier/:id` | `articlesupplier.update(googleAuthId, sku, id, patch)` |
|
||||||
| `PUT /api/article/:sku/supplier/order` | `articlesupplier.order(googleAuthId, sku, { ids })` |
|
| `PUT /api/article/:sku/supplier/order` | `articlesupplier.order(googleAuthId, sku, { ids })` |
|
||||||
| `DELETE /api/article/:sku/supplier/:id` | `articlesupplier.delete(googleAuthId, sku, id)` |
|
| `DELETE /api/article/:sku/supplier/:id` | `articlesupplier.delete(googleAuthId, sku, id)` |
|
||||||
| `GET /api/delivery` | `delivery.list(googleAuthId)` |
|
| `GET /api/delivery` | `delivery.list(googleAuthId, opts)` |
|
||||||
|
| `POST /api/delivery/rebuild-summaries` | `delivery.rebuildSummaries(googleAuthId)` |
|
||||||
| `GET /api/delivery/:id` | `delivery.getById(googleAuthId, id)` |
|
| `GET /api/delivery/:id` | `delivery.getById(googleAuthId, id)` |
|
||||||
| `POST /api/delivery` | `delivery.create(googleAuthId, { number, date, supplierId?, lines })` |
|
| `POST /api/delivery` | `delivery.create(googleAuthId, { number, date, supplierId?, lines })` |
|
||||||
| `PATCH /api/delivery/:id` | `delivery.update(googleAuthId, id, { number?, date?, supplierId?, lines? })` |
|
| `PATCH /api/delivery/:id` | `delivery.update(googleAuthId, id, { number?, date?, supplierId?, lines? })` |
|
||||||
|
|||||||
67
TODO.md
67
TODO.md
@@ -133,3 +133,70 @@ Conversion order (bottom-up by dependency):
|
|||||||
(queries/schema stay mssql-specific, so likely keep it)
|
(queries/schema stay mssql-specific, so likely keep it)
|
||||||
- [ ] Decide export for `SCHEMA_SCRIPTS` (currently re-exported from `index.js`
|
- [ ] Decide export for `SCHEMA_SCRIPTS` (currently re-exported from `index.js`
|
||||||
from the moved schema file)
|
from the moved schema file)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
I'll check how auth is applied across the routes.
|
||||||
|
|
||||||
|
Viewed index.js:1-305
|
||||||
|
Searched for "app\.(post|put|patch|delete|get|use|all)\("
|
||||||
|
Viewed auth.js:1-132
|
||||||
|
|
||||||
|
The middleware skips auth on several path prefixes. Next I'll check which of those can write.
|
||||||
|
|
||||||
|
Searched for "\.(post|put|patch|delete)\("
|
||||||
|
Searched for "'/(api/)?(public|mcp|oauth|\.well-known)"
|
||||||
|
|
||||||
|
Next: the telemetry public route, the `/oauth/register` handler, and the v1 router.
|
||||||
|
|
||||||
|
Viewed telemetry.js:1-118
|
||||||
|
Viewed mcp.js:360-460
|
||||||
|
Searched for "(router|app)\.(post|put|patch|delete|use|all)\(|auth|token"
|
||||||
|
Viewed index.js:1-27
|
||||||
|
Searched for "registerClient|export (async )?function"
|
||||||
|
Viewed mcp.js:455-540
|
||||||
|
Searched for "db\.\w+|verifyIdToken|allow|jwt\.sign"
|
||||||
|
|
||||||
|
**No. With no JWT, an attacker can still write to the database.** The checks I did are static code review only, and I haven't run anything.
|
||||||
|
|
||||||
|
`authMiddleware` ([auth.js](file:///home/seb/src/dashback/src/api/middleware/auth.js#L49-L68)) lets several path groups through without a token. Most of them can't write to the SQL database, but two can.
|
||||||
|
|
||||||
|
### 1. Open OAuth registration: `POST /oauth/register`
|
||||||
|
- This route is public, and it calls `mcpDb.registerClient` ([mcp.js:368](file:///home/seb/src/dashback/src/api/routes/mcp.js#L368)) to insert a client row.
|
||||||
|
- Anyone can create unlimited rows with no auth and no rate limit.
|
||||||
|
- `client_name`, `redirect_uris`, `grant_types` and `scope` all come straight from the request body.
|
||||||
|
|
||||||
|
### 2. Auth bypass: `POST /oauth/authorize`
|
||||||
|
- The middleware calls `next()` on this route even with no token ([auth.js:70-79](file:///home/seb/src/dashback/src/api/middleware/auth.js#L70-L79)).
|
||||||
|
- The handler ([mcp.js:455-503](file:///home/seb/src/dashback/src/api/routes/mcp.js#L455-L503)) takes an `email` from the body when there's no `req.user`.
|
||||||
|
- If that email is on the allowlist, it writes an auth code (`createAuthCode`) for that user and redirects with it. **No password, Google token or JWT is checked.**
|
||||||
|
- An attacker can chain this with the previous hole:
|
||||||
|
1. Register a client with `/oauth/register`.
|
||||||
|
2. Post to `/oauth/authorize` with the victim's allowlisted email and their own PKCE `code_challenge`.
|
||||||
|
3. Exchange the code at `/oauth/token`, which I haven't read.
|
||||||
|
4. Call `/mcp` as that user.
|
||||||
|
- That would be a full impersonation if the MCP tools can write. Please confirm this by testing against a dev instance.
|
||||||
|
- Also, `new URL(redirect_uri)` runs with no try/catch and no check that `redirect_uri` matches the registered URIs. A bad value throws, and a valid one is an open redirect that sends the code to the attacker.
|
||||||
|
|
||||||
|
### Not writing to the SQL database
|
||||||
|
- `/api/public/telemetry/crash` ([telemetry.js](file:///home/seb/src/dashback/src/api/routes/telemetry.js#L87-L104)) is public and unauthenticated. It writes to a file on disk rather than SQL, but there's no size or rate limit beyond `bodyLimit`, so it can fill the disk.
|
||||||
|
- `/v1` and `/api/v1` skip the middleware, but the routes I saw are all `GET`. The ones with data use `posAuthMiddleware`, and `/client`, `/pimage` and `/cimage` have no auth.
|
||||||
|
- `/api/auth/login` verifies a Google ID token and checks the allowlist. If `IMPLICIT_CREATE=true`, any valid Google account gets added to the allowlist ([login.js:107](file:///home/seb/src/dashback/src/api/routes/auth/login.js#L107)). Make sure that's off in production.
|
||||||
|
|
||||||
|
### Suggested fixes
|
||||||
|
- Make `/oauth/authorize` POST require `req.user`, and remove the `email` fallback.
|
||||||
|
- Validate `redirect_uri` against the registered URIs for the client.
|
||||||
|
- Rate-limit `/oauth/register`, or gate it.
|
||||||
|
|
||||||
|
Want me to implement these? I haven't changed any code.
|
||||||
132
bench-graph.tmp.mjs
Normal file
132
bench-graph.tmp.mjs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { Graphviz } from '@hpcc-js/wasm-graphviz';
|
||||||
|
|
||||||
|
const src = readFileSync(
|
||||||
|
'/home/seb/src/dashdbschema/generate-schema-graph.js',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
const parser = src.slice(
|
||||||
|
src.indexOf('function parseSchema'),
|
||||||
|
src.indexOf('const EDGE_COLORS')
|
||||||
|
);
|
||||||
|
const edgeColorsConst = src.slice(
|
||||||
|
src.indexOf('const EDGE_COLORS'),
|
||||||
|
src.indexOf('function edgeKey')
|
||||||
|
);
|
||||||
|
const edgeKeyFn = src.slice(
|
||||||
|
src.indexOf('function edgeKey'),
|
||||||
|
src.indexOf('/**\n * Only a spanning forest')
|
||||||
|
);
|
||||||
|
const ranking = src.slice(
|
||||||
|
src.indexOf('function selectRankingEdgeKeys'),
|
||||||
|
src.indexOf('function assignEdgeColors')
|
||||||
|
);
|
||||||
|
const edgeColors = src.slice(
|
||||||
|
src.indexOf('function assignEdgeColors'),
|
||||||
|
src.indexOf('function escapeHtml')
|
||||||
|
);
|
||||||
|
const escape = src.slice(
|
||||||
|
src.indexOf('function escapeHtml'),
|
||||||
|
src.indexOf('/**\n * Domain grouping')
|
||||||
|
);
|
||||||
|
const clusters = src.slice(
|
||||||
|
src.indexOf('const CLUSTERS'),
|
||||||
|
src.indexOf('const COLORS =')
|
||||||
|
);
|
||||||
|
const colors = src.slice(
|
||||||
|
src.indexOf('const COLORS ='),
|
||||||
|
src.indexOf('const COLORS =') +
|
||||||
|
src.slice(src.indexOf('const COLORS =')).indexOf('};') +
|
||||||
|
2
|
||||||
|
);
|
||||||
|
const orderMembers = src.slice(
|
||||||
|
src.indexOf('function orderClusterMembers'),
|
||||||
|
src.indexOf('function buildTableLabel')
|
||||||
|
);
|
||||||
|
const plainPositions = src.slice(
|
||||||
|
src.indexOf('function parsePlainPositions'),
|
||||||
|
src.indexOf('function flipCompassLR')
|
||||||
|
);
|
||||||
|
const reorder = src.slice(
|
||||||
|
src.indexOf('function reorderColumnsForCrossingReduction'),
|
||||||
|
src.indexOf('function parsePlainPositions')
|
||||||
|
);
|
||||||
|
const builder = src.slice(
|
||||||
|
src.indexOf('function buildTableLabel'),
|
||||||
|
src.indexOf('function countSvgCrossings')
|
||||||
|
);
|
||||||
|
const crossingsFn = src.slice(
|
||||||
|
src.indexOf('function countSvgCrossings'),
|
||||||
|
src.indexOf('/**\n * Graphviz self-edges')
|
||||||
|
);
|
||||||
|
const mod = await import(
|
||||||
|
'data:text/javascript,' +
|
||||||
|
encodeURIComponent(
|
||||||
|
parser +
|
||||||
|
'\n' +
|
||||||
|
edgeColorsConst +
|
||||||
|
'\n' +
|
||||||
|
clusters +
|
||||||
|
'\n' +
|
||||||
|
colors +
|
||||||
|
'\n' +
|
||||||
|
orderMembers +
|
||||||
|
'\n' +
|
||||||
|
plainPositions +
|
||||||
|
'\n' +
|
||||||
|
reorder +
|
||||||
|
'\n' +
|
||||||
|
edgeKeyFn +
|
||||||
|
'\n' +
|
||||||
|
ranking +
|
||||||
|
'\n' +
|
||||||
|
edgeColors +
|
||||||
|
'\n' +
|
||||||
|
escape +
|
||||||
|
'\n' +
|
||||||
|
builder +
|
||||||
|
'\n' +
|
||||||
|
crossingsFn +
|
||||||
|
'\nexport { parseSchema, buildDot, assignEdgeAnchors, parsePlainPositions, reorderColumnsForCrossingReduction, orderClusterMembers, CLUSTERS, COLORS, countSvgCrossings };'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const schemaSource = readFileSync(
|
||||||
|
'/home/seb/src/dashdbschema/src/providers/mssql/schema.js',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
const { tables, edges } = mod.parseSchema(schemaSource);
|
||||||
|
const graphviz = await Graphviz.load();
|
||||||
|
|
||||||
|
let positions = null;
|
||||||
|
for (let pass = 0; pass < 3; pass++) {
|
||||||
|
const probeDot = mod.buildDot(tables, edges, positions);
|
||||||
|
const plain = graphviz.layout(probeDot, 'plain', 'dot');
|
||||||
|
positions = mod.parsePlainPositions(plain);
|
||||||
|
mod.reorderColumnsForCrossingReduction(tables, edges, positions);
|
||||||
|
}
|
||||||
|
const anchors = mod.assignEdgeAnchors(edges, positions);
|
||||||
|
const baseDot = mod.buildDot(tables, edges, positions, anchors);
|
||||||
|
|
||||||
|
const variants = [
|
||||||
|
['mclimit=4.0 splines=spline (current)', 'mclimit=4.0', 'splines=spline'],
|
||||||
|
['mclimit=1.0 splines=spline', 'mclimit=1.0', 'splines=spline'],
|
||||||
|
['mclimit=0.5 splines=spline', 'mclimit=0.5', 'splines=spline'],
|
||||||
|
['mclimit=1.0 splines=true', 'mclimit=1.0', 'splines=true'],
|
||||||
|
['mclimit=1.0 splines=polyline', 'mclimit=1.0', 'splines=polyline'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [label, mclimit, splines] of variants) {
|
||||||
|
const dot = baseDot
|
||||||
|
.replace('mclimit=4.0', mclimit)
|
||||||
|
.replace('splines=spline', splines);
|
||||||
|
const t = process.hrtime.bigint();
|
||||||
|
const svg = graphviz.layout(dot, 'svg', 'dot');
|
||||||
|
const ms = Number(process.hrtime.bigint() - t) / 1e6;
|
||||||
|
const t2 = process.hrtime.bigint();
|
||||||
|
const crossings = mod.countSvgCrossings(svg);
|
||||||
|
const cms = Number(process.hrtime.bigint() - t2) / 1e6;
|
||||||
|
console.log(
|
||||||
|
`${label}: layout=${ms.toFixed(0)}ms crossings=${crossings} (count=${cms.toFixed(0)}ms) total=${(ms + cms).toFixed(0)}ms`
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ const MODULE_ORDER = [
|
|||||||
'delivery',
|
'delivery',
|
||||||
'quote',
|
'quote',
|
||||||
'order',
|
'order',
|
||||||
|
'shipment',
|
||||||
|
'shipping',
|
||||||
'manifestattachment',
|
'manifestattachment',
|
||||||
'ledger',
|
'ledger',
|
||||||
'warehouse',
|
'warehouse',
|
||||||
@@ -42,6 +44,7 @@ const MODULE_ORDER = [
|
|||||||
'attributevalue',
|
'attributevalue',
|
||||||
'articleattributevalue',
|
'articleattributevalue',
|
||||||
'labeltemplate',
|
'labeltemplate',
|
||||||
|
'healthcheck',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Files that are internal helpers and should not appear as public API entries.
|
// Files that are internal helpers and should not appear as public API entries.
|
||||||
@@ -73,6 +76,8 @@ const INTERNAL_FILES = new Set([
|
|||||||
'shape.js',
|
'shape.js',
|
||||||
'createStore.js',
|
'createStore.js',
|
||||||
'manifestLines.js',
|
'manifestLines.js',
|
||||||
|
'runner.js',
|
||||||
|
'cron.js',
|
||||||
'index.js',
|
'index.js',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -319,28 +324,35 @@ 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)'],
|
['account', 'id PK, company_id->company, type:s(100), name:s(255)'],
|
||||||
|
['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, meta?:txt'],
|
||||||
['articlelist', 'id PK'],
|
['articlelist', 'id PK'],
|
||||||
['articlelistarticle', 'id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)'],
|
['articlelistarticle', 'id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)'],
|
||||||
['articlepic', 'id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)'],
|
['articlepic', 'id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)'],
|
||||||
['articleproperty', 'id PK, article_id->article, property_id->propertyname, value:txt, sort:i=0'],
|
['articleproperty', 'id PK, article_id->article, property_id->propertyname, value:txt, sort:i=0'],
|
||||||
['articlesupplier', 'id PK, article_id->article, entity_id->entity, price_id?->price, sku?:s(100), stock?:f, pu:f=1, sort:i=0, UQ(article_id, entity_id), UQ(article_id, sort)'],
|
['articlesupplier', 'id PK, article_id->article, entity_id->entity, net?:f, tax_id?->tax, sku?:s(100), stock?:f, pu:f=1, sort:i=0, UQ(article_id, entity_id), UQ(article_id, sort)'],
|
||||||
['articletext', 'id PK, article_id->article, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(article_id, language_id)'],
|
['articletext', 'id PK, article_id->article, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(article_id, language_id)'],
|
||||||
['attributename', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'],
|
['attributename', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'],
|
||||||
['attributevalue', 'id PK, attribute_id->attributename, value:s(255), sort:i=0, UQ(attribute_id, value)'],
|
['attributevalue', 'id PK, attribute_id->attributename, value:s(255), sort:i=0, UQ(attribute_id, value)'],
|
||||||
|
['attributevaluepic', 'id PK, attributevalue_id->attributevalue, pic_id->pic, sort:i=0, UQ(attributevalue_id, pic_id), UQ(attributevalue_id, sort)'],
|
||||||
['brand', 'id PK, company_id->company, name:s(255), gsprpinfo?:txt, UQ(company_id, name)'],
|
['brand', 'id PK, company_id->company, name:s(255), gsprpinfo?:txt, UQ(company_id, name)'],
|
||||||
['brandpic', 'id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)'],
|
['brandpic', 'id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)'],
|
||||||
['category', 'id PK, parent_id?->category, sort:i=0'],
|
['category', 'id PK, parent_id?->category, sort:i=0'],
|
||||||
['categoryarticle', 'id PK, category_id->category, article_id->article, UQ(category_id, article_id)'],
|
['categoryarticle', 'id PK, category_id->category, article_id->article, UQ(category_id, article_id)'],
|
||||||
|
['categoryattributevalue', 'id PK, category_id->category, attributevalue_id->attributevalue, sort:i=0, UQ(category_id, attributevalue_id)'],
|
||||||
['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)'],
|
||||||
|
['categoryproperty', 'id PK, category_id->category, property_id->propertyname, value:txt, sort:i=0'],
|
||||||
['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), 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'],
|
||||||
|
['articlewarehousesummary', 'id PK, article_id->article, warehouse_id->warehouse, stock:f=0, UQ(article_id, warehouse_id)'],
|
||||||
|
['deliverysummary', 'id PK, company_id->company, manifest_id->manifest UQ, delivery_number:s(100), external_number?:s(100), delivery_date:dt, created_at:dt, supplier_id?->entity, supplier_name?:s(255), positions:i=0, gross_total:f=0, net_total:f=0, total_quantity:f=0, stock_positions:i=0, booked_positions:i=0, unbooked_positions:i=0, stock_quantity:f=0, booked_quantity:f=0, unbooked_quantity:f=0, is_booked:b=0'],
|
||||||
['entity', 'id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)'],
|
['entity', 'id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)'],
|
||||||
['entityaddress', 'id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0'],
|
['entityaddress', 'id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0'],
|
||||||
['entitycontact', 'id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0'],
|
['entitycontact', 'id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0'],
|
||||||
@@ -353,17 +365,31 @@ const SCHEMA_TABLES = [
|
|||||||
['manifest', 'id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt'],
|
['manifest', 'id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt'],
|
||||||
['manifestarticle', 'id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0'],
|
['manifestarticle', 'id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0'],
|
||||||
['manifestattachment', 'id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)'],
|
['manifestattachment', 'id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)'],
|
||||||
|
['mcp_auth_code', 'code:s(128) PK, client_id:s(100), google_auth_id->google_auth, redirect_uri:s(2048), code_challenge:s(255), code_challenge_method:s(32), scope?:s(255), expires_at:dt, used:b=0, created_at:dt'],
|
||||||
|
['mcp_client', 'id PK, client_id:s(100) UQ, client_secret?:s(255), client_name:s(255), redirect_uris:txt, grant_types?:s(255), response_types?:s(255), scope?:s(255), created_at:dt'],
|
||||||
|
['mcp_token', 'id PK, refresh_token:s(128) UQ, client_id:s(100), google_auth_id->google_auth, scope?:s(255), expires_at:dt, revoked:b=0, created_at:dt'],
|
||||||
['meta', 'key:s(255) PK, value:txt'],
|
['meta', 'key:s(255) PK, value:txt'],
|
||||||
|
['modelconversation', 'id PK, companyuser_id->companyuser, provider:s(100), model:s(255), created_at:dt, updated_at:dt, state:s(20)=\'idle\', started_by?:i'],
|
||||||
|
['modelmessage', 'id PK, conversation_id->modelconversation, created_at:dt, role:s(20), message:txt'],
|
||||||
|
['modelvfs', 'id PK, company_id->company, name:s(255), mime:s(100), content:bin, meta?:txt, created_at:dt, updated_at:dt, UQ(company_id, name)'],
|
||||||
['numbers', 'id PK, company_id->company, type:s(100), prefix:s(50)=\'\', padding:i=0, next:i=1, UQ(company_id, type)'],
|
['numbers', 'id PK, company_id->company, type:s(100), prefix:s(50)=\'\', padding:i=0, next:i=1, UQ(company_id, type)'],
|
||||||
['optionslist', 'id PK'],
|
['optionslist', 'id PK'],
|
||||||
['optionslistarticle', 'id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)'],
|
['optionslistarticle', 'id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)'],
|
||||||
['pic', 'id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), source:txt, fullbinary:bin, thumbbinary:bin'],
|
['ordersummary', 'id PK, company_id->company, manifest_id->manifest UQ, order_number:s(100), order_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, paid_total:f=0, ordered_quantity:f=0, shipped_quantity:f=0, is_paid:b=0, is_shipped:b=0'],
|
||||||
['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin:s(50)'],
|
['package', 'id PK, shipping_id->shipping, entityaddress_id->entityaddress, number:s(100), type:s(100), state:s(100), details?:txt'],
|
||||||
|
['payment', 'id PK, account_id->account, manifest_id->manifest, type:s(100), amount:f'],
|
||||||
|
['pic', 'id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), smallthumbw:i, smallthumbh:i, smallthumbbinary:bin, source:txt'],
|
||||||
|
['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), maxknownobituaryid?:i, root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt'],
|
||||||
['posterminalcategory', 'id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)'],
|
['posterminalcategory', 'id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)'],
|
||||||
|
['posterminalobituary', 'id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i, created_at:dt'],
|
||||||
|
['posterminalsyncqueue', 'id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i'],
|
||||||
['price', 'id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f'],
|
['price', 'id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f'],
|
||||||
['printer', 'id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt'],
|
['printer', 'id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt'],
|
||||||
['propertyname', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'],
|
['propertyname', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'],
|
||||||
['syncpos', 'id PK, company_id->company, paircode:s(50) UQ, token?:s(255) UQ'],
|
['quotesummary', 'id PK, company_id->company, manifest_id->manifest UQ, quote_number:s(100), quote_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, total_quantity:f=0'],
|
||||||
|
['shipmentsummary', 'id PK, company_id->company, manifest_id->manifest UQ, shipment_number:s(100), shipment_date:dt, created_at:dt, order_id?:i, order_number?:s(100), entity_id?->entity, customer_name?:s(255), positions:i=0, total_quantity:f=0, package_count:i=0, carrier?:s(100), tracking_code?:s(255), state:s(50)=\'new\''],
|
||||||
|
['shipping', 'id PK, manifest_id->manifest, number:s(100), tracking_code?:s(255), carrier?:s(100), type:s(100), state:s(100)'],
|
||||||
|
['subagentschedule', 'id PK, company_id->company, desc?:txt, prompt:txt, skills?:txt, lastagentnote?:txt, max_itr:i=15, cron:s(100), created_at:dt, lastrun_at?:dt'],
|
||||||
['tax', 'id PK, rate:d(18,4) UQ'],
|
['tax', 'id PK, rate:d(18,4) UQ'],
|
||||||
['unit', 'id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)'],
|
['unit', 'id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)'],
|
||||||
['unittext', 'id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)'],
|
['unittext', 'id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)'],
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ function addInferredEdges(tables, edges) {
|
|||||||
const match = column.name.match(/^(.+)_id$/i);
|
const match = column.name.match(/^(.+)_id$/i);
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
|
|
||||||
|
// Obituary and sync queue tables store polymorphic entity_id references, not active foreign keys to entity.
|
||||||
|
if (tableName.includes('obituary') || tableName.includes('tombstone') || tableName.includes('syncqueue')) continue;
|
||||||
|
|
||||||
const candidate = match[1];
|
const candidate = match[1];
|
||||||
if (!tables.has(candidate) || candidate === tableName) continue;
|
if (!tables.has(candidate) || candidate === tableName) continue;
|
||||||
if (explicitlyConstrained.has(`${tableName}.${column.name}`)) continue;
|
if (explicitlyConstrained.has(`${tableName}.${column.name}`)) continue;
|
||||||
@@ -325,8 +328,8 @@ const CLUSTERS = [
|
|||||||
'meta',
|
'meta',
|
||||||
'google_auth',
|
'google_auth',
|
||||||
'companyuser',
|
'companyuser',
|
||||||
'syncpos',
|
|
||||||
'company',
|
'company',
|
||||||
|
'companyhistory',
|
||||||
'companysetting',
|
'companysetting',
|
||||||
'customergroup',
|
'customergroup',
|
||||||
'entity',
|
'entity',
|
||||||
@@ -361,20 +364,51 @@ const CLUSTERS = [
|
|||||||
{
|
{
|
||||||
id: 'media',
|
id: 'media',
|
||||||
label: 'Media',
|
label: 'Media',
|
||||||
tables: ['pic', 'articlepic', 'categorypic', 'brandpic', 'file'],
|
tables: ['pic', 'articlepic', 'categorypic', 'brandpic', 'attributevaluepic', 'file'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'manifests',
|
id: 'manifests',
|
||||||
label: 'Documents',
|
label: 'Documents & Shipping',
|
||||||
tables: ['manifest', 'manifestarticle', 'manifestattachment', 'ledger', 'warehouse'],
|
tables: [
|
||||||
|
'manifest',
|
||||||
|
'manifestarticle',
|
||||||
|
'manifestattachment',
|
||||||
|
'articlewarehousesummary',
|
||||||
|
'deliverysummary',
|
||||||
|
'ordersummary',
|
||||||
|
'quotesummary',
|
||||||
|
'shipmentsummary',
|
||||||
|
'ledger',
|
||||||
|
'warehouse',
|
||||||
|
'shipping',
|
||||||
|
'package',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'commerce',
|
id: 'commerce',
|
||||||
label: 'Pricing',
|
label: 'Pricing & Payments',
|
||||||
tables: ['tax', 'price'],
|
tables: ['tax', 'price', 'account', 'payment'],
|
||||||
},
|
},
|
||||||
{ id: 'units', label: 'Units', tables: ['unit'] },
|
{ id: 'units', label: 'Units', tables: ['unit'] },
|
||||||
{ id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'printer'] },
|
{ id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'posterminalobituary', 'posterminalsyncqueue', 'printer', 'labeltemplate'] },
|
||||||
|
{
|
||||||
|
id: 'ai',
|
||||||
|
label: 'AI & Models',
|
||||||
|
tables: ['modelconversation', 'modelmessage', 'modelvfs', 'subagentschedule'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'attributes',
|
||||||
|
label: 'Properties & Attributes',
|
||||||
|
tables: [
|
||||||
|
'propertyname',
|
||||||
|
'articleproperty',
|
||||||
|
'categoryproperty',
|
||||||
|
'attributename',
|
||||||
|
'attributevalue',
|
||||||
|
'articleattributevalue',
|
||||||
|
'categoryattributevalue',
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
@@ -478,14 +512,33 @@ function flipCompassNS(compass) {
|
|||||||
* Greedy exit/port tweaks against real Graphviz splines. Tries L/R flips,
|
* Greedy exit/port tweaks against real Graphviz splines. Tries L/R flips,
|
||||||
* N/S diagonal flips, and adjacent head-port swaps; keeps moves that cut
|
* N/S diagonal flips, and adjacent head-port swaps; keeps moves that cut
|
||||||
* SVG segment crossings.
|
* SVG segment crossings.
|
||||||
|
*
|
||||||
|
* Each trial is a full Graphviz layout, so the search is bounded by
|
||||||
|
* `maxLayouts` and reports progress — otherwise a large schema silently
|
||||||
|
* runs for many minutes with no output.
|
||||||
*/
|
*/
|
||||||
function optimizeAnchorsWithLayout(graphviz, tables, edges, positions, anchors) {
|
function optimizeAnchorsWithLayout(
|
||||||
|
graphviz,
|
||||||
|
tables,
|
||||||
|
edges,
|
||||||
|
positions,
|
||||||
|
anchors,
|
||||||
|
maxLayouts = 400
|
||||||
|
) {
|
||||||
const tailCompass = new Map(anchors.tailCompass);
|
const tailCompass = new Map(anchors.tailCompass);
|
||||||
const headPort = new Map(anchors.headPort);
|
const headPort = new Map(anchors.headPort);
|
||||||
const inboundCounts = anchors.inboundCounts;
|
const inboundCounts = anchors.inboundCounts;
|
||||||
let current = { headPort, tailCompass, inboundCounts };
|
let current = { headPort, tailCompass, inboundCounts };
|
||||||
|
|
||||||
|
let layouts = 0;
|
||||||
|
const started = Date.now();
|
||||||
const measure = () => {
|
const measure = () => {
|
||||||
|
layouts++;
|
||||||
|
if (layouts % 25 === 0) {
|
||||||
|
console.log(
|
||||||
|
` optimizing anchors: ${layouts} layouts, ${best} crossings, ${((Date.now() - started) / 1000).toFixed(0)}s`
|
||||||
|
);
|
||||||
|
}
|
||||||
const dot = buildDot(tables, edges, positions, current);
|
const dot = buildDot(tables, edges, positions, current);
|
||||||
const svg = fixSelfLoopSvg(graphviz.layout(dot, 'svg', 'dot'));
|
const svg = fixSelfLoopSvg(graphviz.layout(dot, 'svg', 'dot'));
|
||||||
return { svg, crossings: countSvgCrossings(svg), dot };
|
return { svg, crossings: countSvgCrossings(svg), dot };
|
||||||
@@ -503,6 +556,8 @@ function optimizeAnchorsWithLayout(graphviz, tables, edges, positions, anchors)
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const budgetLeft = () => layouts < maxLayouts;
|
||||||
|
|
||||||
const trySeed = (seedCompass) => {
|
const trySeed = (seedCompass) => {
|
||||||
const saved = new Map(tailCompass);
|
const saved = new Map(tailCompass);
|
||||||
for (const [k, v] of seedCompass) tailCompass.set(k, v);
|
for (const [k, v] of seedCompass) tailCompass.set(k, v);
|
||||||
@@ -529,9 +584,11 @@ function optimizeAnchorsWithLayout(graphviz, tables, edges, positions, anchors)
|
|||||||
let improved = true;
|
let improved = true;
|
||||||
for (let sweep = 0; sweep < 4 && improved; sweep++) {
|
for (let sweep = 0; sweep < 4 && improved; sweep++) {
|
||||||
improved = false;
|
improved = false;
|
||||||
|
if (!budgetLeft()) break;
|
||||||
|
|
||||||
for (const edge of edges) {
|
for (const edge of edges) {
|
||||||
if (edge.from === edge.to) continue;
|
if (edge.from === edge.to) continue;
|
||||||
|
if (!budgetLeft()) break;
|
||||||
const key = edgeKey(edge);
|
const key = edgeKey(edge);
|
||||||
const prev = tailCompass.get(key);
|
const prev = tailCompass.get(key);
|
||||||
if (!prev) continue;
|
if (!prev) continue;
|
||||||
@@ -552,6 +609,7 @@ function optimizeAnchorsWithLayout(graphviz, tables, edges, positions, anchors)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const group of byTarget.values()) {
|
for (const group of byTarget.values()) {
|
||||||
|
if (!budgetLeft()) break;
|
||||||
const keys = group
|
const keys = group
|
||||||
.map((e) => edgeKey(e))
|
.map((e) => edgeKey(e))
|
||||||
.sort(
|
.sort(
|
||||||
@@ -807,10 +865,12 @@ function buildDot(tables, edges, positions = null, anchors = null) {
|
|||||||
const lines = [
|
const lines = [
|
||||||
'digraph schema {',
|
'digraph schema {',
|
||||||
// TB = landscape for this schema (many tables per rank spread horizontally).
|
// TB = landscape for this schema (many tables per rank spread horizontally).
|
||||||
// mclimit bumps Graphviz's own crossing-minimization iterations.
|
// mclimit is Graphviz's crossing-minimization iteration budget. 4.0 (the
|
||||||
|
// old value) costs ~12x the layout time for only ~15% fewer crossings,
|
||||||
|
// which multiplied by the ~157 optimizer trials below. 1.0 is the default.
|
||||||
` graph [rankdir=TB, splines=spline, overlap=false, bgcolor="${COLORS.background}",`,
|
` graph [rankdir=TB, splines=spline, overlap=false, bgcolor="${COLORS.background}",`,
|
||||||
' nodesep=0.95, ranksep=1.0, pad=0.5, fontname="Helvetica",',
|
' nodesep=0.95, ranksep=1.0, pad=0.5, fontname="Helvetica",',
|
||||||
' compound=true, mclimit=4.0];',
|
' compound=true, mclimit=1.0];',
|
||||||
' node [shape=plaintext, fontname="Helvetica"];',
|
' node [shape=plaintext, fontname="Helvetica"];',
|
||||||
' edge [fontname="Helvetica", fontsize=9, penwidth=1.4, arrowsize=0.7];',
|
' edge [fontname="Helvetica", fontsize=9, penwidth=1.4, arrowsize=0.7];',
|
||||||
'',
|
'',
|
||||||
@@ -919,6 +979,27 @@ function countSvgCrossings(svg) {
|
|||||||
edgeSegs.push(segs);
|
edgeSegs.push(segs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Two splines can only intersect if their bounding boxes overlap, so the
|
||||||
|
// sweep below can skip almost every pair. Without this the check is
|
||||||
|
// O(edges^2 * segments^2) and dominates each optimizer trial.
|
||||||
|
const boxes = edgeSegs.map((segs) => {
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
for (const [a] of segs) {
|
||||||
|
if (a[0] < minX) minX = a[0];
|
||||||
|
if (a[0] > maxX) maxX = a[0];
|
||||||
|
if (a[1] < minY) minY = a[1];
|
||||||
|
if (a[1] > maxY) maxY = a[1];
|
||||||
|
}
|
||||||
|
return { minX, minY, maxX, maxY };
|
||||||
|
});
|
||||||
|
|
||||||
|
const order = boxes
|
||||||
|
.map((_, i) => i)
|
||||||
|
.sort((a, b) => boxes[a].minX - boxes[b].minX);
|
||||||
|
|
||||||
const orient = (a, b, c) =>
|
const orient = (a, b, c) =>
|
||||||
(b[1] - a[1]) * (c[0] - b[0]) - (b[0] - a[0]) * (c[1] - b[1]);
|
(b[1] - a[1]) * (c[0] - b[0]) - (b[0] - a[0]) * (c[1] - b[1]);
|
||||||
const crosses = (a, b, c, d) => {
|
const crosses = (a, b, c, d) => {
|
||||||
@@ -930,8 +1011,17 @@ function countSvgCrossings(svg) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let n = 0;
|
let n = 0;
|
||||||
for (let e1 = 0; e1 < edgeSegs.length; e1++) {
|
for (let ii = 0; ii < order.length; ii++) {
|
||||||
for (let e2 = e1 + 1; e2 < edgeSegs.length; e2++) {
|
const e1 = order[ii];
|
||||||
|
const b1 = boxes[e1];
|
||||||
|
for (let jj = ii + 1; jj < order.length; jj++) {
|
||||||
|
const e2 = order[jj];
|
||||||
|
const b2 = boxes[e2];
|
||||||
|
// Sorted by minX: once a box starts past b1's right edge, no later
|
||||||
|
// box can overlap it either.
|
||||||
|
if (b2.minX > b1.maxX) break;
|
||||||
|
if (b1.minX > b2.maxX || b1.minY > b2.maxY || b2.minY > b1.maxY) continue;
|
||||||
|
|
||||||
let pairCrosses = false;
|
let pairCrosses = false;
|
||||||
outer: for (const [a, b] of edgeSegs[e1]) {
|
outer: for (const [a, b] of edgeSegs[e1]) {
|
||||||
for (const [c, d] of edgeSegs[e2]) {
|
for (const [c, d] of edgeSegs[e2]) {
|
||||||
@@ -1032,20 +1122,25 @@ const graphviz = await Graphviz.load();
|
|||||||
// Layout passes: learn positions, reorder columns/cluster members (barycenter).
|
// Layout passes: learn positions, reorder columns/cluster members (barycenter).
|
||||||
let positions = null;
|
let positions = null;
|
||||||
for (let pass = 0; pass < 3; pass++) {
|
for (let pass = 0; pass < 3; pass++) {
|
||||||
|
console.log(`Layout pass ${pass + 1}/3…`);
|
||||||
const probeDot = buildDot(tables, edges, positions);
|
const probeDot = buildDot(tables, edges, positions);
|
||||||
const plain = graphviz.layout(probeDot, 'plain', 'dot');
|
const plain = graphviz.layout(probeDot, 'plain', 'dot');
|
||||||
positions = parsePlainPositions(plain);
|
positions = parsePlainPositions(plain);
|
||||||
reorderColumnsForCrossingReduction(tables, edges, positions);
|
reorderColumnsForCrossingReduction(tables, edges, positions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hill-climb L/R exits against real spline crossings.
|
// Hill-climb L/R exits against real spline crossings. Each trial is a full
|
||||||
|
// layout, so the budget keeps the worst case bounded; raise it via
|
||||||
|
// SCHEMA_GRAPH_MAX_LAYOUTS if you want a longer search.
|
||||||
|
const maxLayouts = Number(process.env.SCHEMA_GRAPH_MAX_LAYOUTS ?? 400);
|
||||||
const baseAnchors = assignEdgeAnchors(edges, positions);
|
const baseAnchors = assignEdgeAnchors(edges, positions);
|
||||||
const optimized = optimizeAnchorsWithLayout(
|
const optimized = optimizeAnchorsWithLayout(
|
||||||
graphviz,
|
graphviz,
|
||||||
tables,
|
tables,
|
||||||
edges,
|
edges,
|
||||||
positions,
|
positions,
|
||||||
baseAnchors
|
baseAnchors,
|
||||||
|
maxLayouts
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fill the whole canvas (including Graphviz's pad margin) so there is no
|
// Fill the whole canvas (including Graphviz's pad margin) so there is no
|
||||||
|
|||||||
809
package-lock.json
generated
809
package-lock.json
generated
@@ -10,16 +10,17 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"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.8",
|
||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"sharp": "^0.35.5",
|
"skelehash-wasm": "^0.1.1",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1",
|
||||||
|
"wasm-image-optimization": "^2.0.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"better-sqlite3": "^13.0.3",
|
"better-sqlite3": "^13.0.3",
|
||||||
"mssql": "^12.7.2"
|
"mssql": "^12.7.9"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"better-sqlite3": "^12.0.0",
|
"better-sqlite3": "^12.0.0",
|
||||||
@@ -183,9 +184,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/identity": {
|
"node_modules/@azure/identity": {
|
||||||
"version": "4.13.3",
|
"version": "4.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.14.0.tgz",
|
||||||
"integrity": "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==",
|
"integrity": "sha512-8utCLdTZn14CiAe0HW4JbtzUMsc0mdDwnjUvStFMrR4IxQP8S2eL9zVAdplpfORVJGYmidZElpeXRYjJQ4UyUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -199,7 +200,7 @@
|
|||||||
"@azure/logger": "^1.0.0",
|
"@azure/logger": "^1.0.0",
|
||||||
"@azure/msal-browser": "^5.5.0",
|
"@azure/msal-browser": "^5.5.0",
|
||||||
"@azure/msal-node": "^6.0.0",
|
"@azure/msal-node": "^6.0.0",
|
||||||
"open": "^10.1.0",
|
"open": "^11.0.0",
|
||||||
"tslib": "^2.2.0"
|
"tslib": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -264,22 +265,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/msal-browser": {
|
"node_modules/@azure/msal-browser": {
|
||||||
"version": "5.22.0",
|
"version": "5.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.26.0.tgz",
|
||||||
"integrity": "sha512-5kgu9xeEKgGc2JeidxAtU15NJTqiH/CMCRRQAJ4Rac56kB7KVg91vbNmn+z3RO1vNomPN69UvjKG9h1Pghx6dQ==",
|
"integrity": "sha512-GYKRquzDZmZggCdStWy2bNJKnPui1upssSuznJgP+/h6Mb0ldkUkaGvaY6uSGGmZOm0CGiBQ3b5BiGy43BpXSg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-common": "16.14.1"
|
"@azure/msal-common": "16.14.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/msal-common": {
|
"node_modules/@azure/msal-common": {
|
||||||
"version": "16.14.1",
|
"version": "16.14.3",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.3.tgz",
|
||||||
"integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==",
|
"integrity": "sha512-tz3GvMrR9V/plb2lDdW8qaO8CPpfZevdExBKjltzzbAtyhK0eB2UIvji1aU+gkyHj3fxQ2ie+LNHyMXdmo8wOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -300,6 +301,16 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@azure/msal-node/node_modules/@azure/msal-common": {
|
||||||
|
"version": "16.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz",
|
||||||
|
"integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@clack/core": {
|
"node_modules/@clack/core": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz",
|
||||||
@@ -328,574 +339,16 @@
|
|||||||
"node": ">= 20.12.0"
|
"node": ">= 20.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.11.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
|
||||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@hpcc-js/wasm-graphviz": {
|
"node_modules/@hpcc-js/wasm-graphviz": {
|
||||||
"version": "1.29.1",
|
"version": "1.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.30.0.tgz",
|
||||||
"integrity": "sha512-koAQL0wlryEMeBs4/cQC3GB5ziITv66gR9DU/SWYjjup+iQ+Nbjcqxeaef+sffN4F4cBYZ3P31B5z7+cbSABbw==",
|
"integrity": "sha512-T20IKosOZuex2qV049fQokVJy2MWv8w9Md5V/4mRKCXDf49fBCR50VD1yBlPYHaQlHPsFGRdqXN//r59vZliWQ==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@img/colour": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-darwin-arm64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-darwin-x64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-darwin-x64": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-freebsd-wasm32": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"@img/sharp-wasm32": "0.35.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-arm": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-arm": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-arm64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-arm64": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-ppc64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-ppc64": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-riscv64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-riscv64": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-s390x": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-s390x": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linux-x64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-x64": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-wasm32": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==",
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/runtime": "^1.11.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-webcontainers-wasm32": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==",
|
|
||||||
"cpu": [
|
|
||||||
"wasm32"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@img/sharp-wasm32": "0.35.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-win32-arm64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-win32-ia32": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@img/sharp-win32-x64": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@js-joda/core": {
|
"node_modules/@js-joda/core": {
|
||||||
"version": "6.1.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.3.0.tgz",
|
||||||
"integrity": "sha512-H8NTMRDJqad/leyv/D/A3kSOsf5/58Ydj4DJGDyaCWk9OU/zuZOLhndVffJgQjsgrn5GC0znHMHie7TfvPPG4w==",
|
"integrity": "sha512-mdmjc6lJkTCORsHlMxBT9XM1yLqgtsNZmv/Hwme2v/HIb15D2XcmklZKTnynqBgsSizA9SkFvzpuksfIXhDV/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
@@ -907,9 +360,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "26.6.2",
|
"version": "26.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
|
||||||
"integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==",
|
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -917,9 +370,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/readable-stream": {
|
"node_modules/@types/readable-stream": {
|
||||||
"version": "4.0.24",
|
"version": "4.0.25",
|
||||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz",
|
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.25.tgz",
|
||||||
"integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==",
|
"integrity": "sha512-7GsxL8Wd5X3jh73/qgW7WCnbBvGSdD8U3E4Jiuus3/8I+PN20DtlkJtwFhr/sImXfDDQ2Z03i/9wNiIF3TVSsw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1044,9 +497,9 @@
|
|||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/bundle-name": {
|
"node_modules/bundle-name": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz",
|
||||||
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
|
"integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1142,19 +595,10 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "18.0.4",
|
"version": "18.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.9.tgz",
|
||||||
"integrity": "sha512-jGt9uI5BIxpVO15kvy+iEFqNAwxYClKWnkptLjSNPnFLC003Imd0ofr3aEVsbpHdSBdSsbcApNIlz4nbyGMwdA==",
|
"integrity": "sha512-3fVOW4cB02R066PUSwn8O4TJyz9NckQl7F81blIbaq9jKBm2tSmOKky7ZHIXzGvkVPXCSn6jkpziBPZdGFs8Tw==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"bin": {
|
"bin": {
|
||||||
"dotenv": "dist/index.cjs"
|
"dotenv": "dist/index.cjs"
|
||||||
@@ -1309,6 +753,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-in-ssh": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-inside-container": {
|
"node_modules/is-inside-container": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||||
@@ -1454,9 +911,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mssql": {
|
"node_modules/mssql": {
|
||||||
"version": "12.7.2",
|
"version": "12.7.11",
|
||||||
"resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.11.tgz",
|
||||||
"integrity": "sha512-zhpPw+WXBWLw7d7J6Y0aWgQy0EV8HKDGNSvmkAjP8bSM4H41X0fweAeKWI+bg9Ri+JHCbsf/QScjmACBFcEo8A==",
|
"integrity": "sha512-O24UQA++JLvMUy5T6xL73kCHP4YjLMS6DBxKFeiWyc9kZW2mrVAZ6uqly2d3hGtBTXHRFRImfM+3EdCZVE5bWQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1491,19 +948,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/open": {
|
"node_modules/open": {
|
||||||
"version": "10.2.0",
|
"version": "11.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz",
|
||||||
"integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
|
"integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"default-browser": "^5.2.1",
|
"default-browser": "^5.5.1",
|
||||||
"define-lazy-prop": "^3.0.0",
|
"define-lazy-prop": "^3.0.0",
|
||||||
|
"is-in-ssh": "^1.0.0",
|
||||||
"is-inside-container": "^1.0.0",
|
"is-inside-container": "^1.0.0",
|
||||||
"wsl-utils": "^0.1.0"
|
"powershell-utils": "^0.2.1",
|
||||||
|
"wsl-utils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
@@ -1515,6 +974,19 @@
|
|||||||
"integrity": "sha512-FbEk+Grg0UST4kOPuN7OFfVsut1FvDOJNPS3pt4rzCS8VlFsFzD7ptg7YtFZjEOKgJ4MgqlfCgjK4RYaRBpT0Q==",
|
"integrity": "sha512-FbEk+Grg0UST4kOPuN7OFfVsut1FvDOJNPS3pt4rzCS8VlFsFzD7ptg7YtFZjEOKgJ4MgqlfCgjK4RYaRBpT0Q==",
|
||||||
"license": "GPLv3"
|
"license": "GPLv3"
|
||||||
},
|
},
|
||||||
|
"node_modules/powershell-utils": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process": {
|
"node_modules/process": {
|
||||||
"version": "0.11.10",
|
"version": "0.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
@@ -1526,9 +998,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "4.7.0",
|
"version": "4.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.8.0.tgz",
|
||||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
"integrity": "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1587,6 +1059,7 @@
|
|||||||
"version": "7.8.5",
|
"version": "7.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -1595,67 +1068,17 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sharp": {
|
|
||||||
"version": "0.35.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
|
|
||||||
"integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@img/colour": "^1.1.0",
|
|
||||||
"detect-libc": "^2.1.2",
|
|
||||||
"semver": "^7.8.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-darwin-arm64": "0.35.5",
|
|
||||||
"@img/sharp-darwin-x64": "0.35.5",
|
|
||||||
"@img/sharp-freebsd-wasm32": "0.35.5",
|
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.3.4",
|
|
||||||
"@img/sharp-libvips-darwin-x64": "1.3.4",
|
|
||||||
"@img/sharp-libvips-linux-arm": "1.3.4",
|
|
||||||
"@img/sharp-libvips-linux-arm64": "1.3.4",
|
|
||||||
"@img/sharp-libvips-linux-ppc64": "1.3.4",
|
|
||||||
"@img/sharp-libvips-linux-riscv64": "1.3.4",
|
|
||||||
"@img/sharp-libvips-linux-s390x": "1.3.4",
|
|
||||||
"@img/sharp-libvips-linux-x64": "1.3.4",
|
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4",
|
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.4",
|
|
||||||
"@img/sharp-linux-arm": "0.35.5",
|
|
||||||
"@img/sharp-linux-arm64": "0.35.5",
|
|
||||||
"@img/sharp-linux-ppc64": "0.35.5",
|
|
||||||
"@img/sharp-linux-riscv64": "0.35.5",
|
|
||||||
"@img/sharp-linux-s390x": "0.35.5",
|
|
||||||
"@img/sharp-linux-x64": "0.35.5",
|
|
||||||
"@img/sharp-linuxmusl-arm64": "0.35.5",
|
|
||||||
"@img/sharp-linuxmusl-x64": "0.35.5",
|
|
||||||
"@img/sharp-webcontainers-wasm32": "0.35.5",
|
|
||||||
"@img/sharp-win32-arm64": "0.35.5",
|
|
||||||
"@img/sharp-win32-ia32": "0.35.5",
|
|
||||||
"@img/sharp-win32-x64": "0.35.5"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/node": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sisteransi": {
|
"node_modules/sisteransi": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sprintf-js": {
|
"node_modules/skelehash-wasm": {
|
||||||
"version": "1.1.3",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/skelehash-wasm/-/skelehash-wasm-0.1.1.tgz",
|
||||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
"integrity": "sha512-5kC7dESMvQUdpLvRYfTc0oZ/Ff78bedUhqvYAGnVjs3agsS9PP8RJ/lGpol5sSKoYET/7KJbQ2UVSDTNpmDtTw==",
|
||||||
"dev": true,
|
"license": "0BSD"
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
@@ -1678,9 +1101,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tedious": {
|
"node_modules/tedious": {
|
||||||
"version": "20.0.0",
|
"version": "20.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/tedious/-/tedious-20.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tedious/-/tedious-20.3.8.tgz",
|
||||||
"integrity": "sha512-bTR0aou0Ghucf0ytvZUJjnKHGKDV8tT57jPYtEkSpfTWFe++4uR1wxJLQ4mh5wlSvAYXzmgBxbI0vaE56qigXw==",
|
"integrity": "sha512-ZrJiwyGn340Sa0V0g37Xqj5bvmJ1riYwvEtg7zdm/lpw70f2ZNIQ2fvHmntU5fEOorWDa48Jo1oKYAuUP+rh7Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1692,8 +1115,7 @@
|
|||||||
"bl": "^6.1.4",
|
"bl": "^6.1.4",
|
||||||
"iconv-lite": "^0.7.0",
|
"iconv-lite": "^0.7.0",
|
||||||
"js-md4": "^0.3.2",
|
"js-md4": "^0.3.2",
|
||||||
"native-duplexpair": "^1.0.0",
|
"native-duplexpair": "^1.0.0"
|
||||||
"sprintf-js": "^1.1.3"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
@@ -1709,7 +1131,7 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
@@ -1719,17 +1141,46 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/wasm-image-optimization": {
|
||||||
|
"version": "2.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/wasm-image-optimization/-/wasm-image-optimization-2.0.13.tgz",
|
||||||
|
"integrity": "sha512-MGaFZtdxMHZyaL3VUUcNkN8qccJcy33lcXoUIaegav/y1TiK6c2aALbKAjoe4bXHFI7nDl+5+TG0e9Y8j1/h9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "15.0.0",
|
||||||
|
"worker-lib": "2.2.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"wasm-image-optimization": "dist/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wasm-image-optimization/node_modules/commander": {
|
||||||
|
"version": "15.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
|
||||||
|
"integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-lib": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-lib/-/worker-lib-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-CauU4vz3vZUBG3A2CY93GY94KLhbH8JtPKtuvKu2w5G07cHkZ73QkTAJ/lEHDL6arT5WDu1qpU++T0+kGDgSCg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/wsl-utils": {
|
"node_modules/wsl-utils": {
|
||||||
"version": "0.1.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz",
|
||||||
"integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
|
"integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-wsl": "^3.1.0"
|
"is-wsl": "^3.1.0",
|
||||||
|
"powershell-utils": "^0.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -20,17 +20,20 @@
|
|||||||
"init-schema": "node scripts/init-schema.js",
|
"init-schema": "node scripts/init-schema.js",
|
||||||
"purge-db": "node scripts/purge-db.js",
|
"purge-db": "node scripts/purge-db.js",
|
||||||
"reset-db": "node scripts/reset-db.js",
|
"reset-db": "node scripts/reset-db.js",
|
||||||
|
"downgrade": "node scripts/downgrade.js",
|
||||||
|
"test": "node --test \"test/*.test.js\"",
|
||||||
"test:api": "node scripts/test-api.js",
|
"test:api": "node scripts/test-api.js",
|
||||||
"test:benchmark": "node scripts/benchmark.js"
|
"test:benchmark": "node scripts/benchmark.js"
|
||||||
},
|
},
|
||||||
"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.8",
|
||||||
"pdfconvert-wasm": "^0.3.0",
|
"pdfconvert-wasm": "^0.3.0",
|
||||||
"sharp": "^0.35.5",
|
"skelehash-wasm": "^0.1.1",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1",
|
||||||
|
"wasm-image-optimization": "^2.0.13"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"better-sqlite3": "^12.0.0",
|
"better-sqlite3": "^12.0.0",
|
||||||
@@ -43,6 +46,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"better-sqlite3": "^13.0.3",
|
"better-sqlite3": "^13.0.3",
|
||||||
"mssql": "^12.7.2"
|
"mssql": "^12.7.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
297
schema-graph.dot
297
schema-graph.dot
File diff suppressed because one or more lines are too long
5765
schema-graph.svg
5765
schema-graph.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 459 KiB |
171
scratch/test-category-subcategories.js
Normal file
171
scratch/test-category-subcategories.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
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 test() {
|
||||||
|
console.log('--- Testing setOn and setOff with subcategories ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-posterminal-subcats-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('subcat-tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'SubCat Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
const terminal = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'POS 1',
|
||||||
|
pairingpin: '1234',
|
||||||
|
});
|
||||||
|
await store.posterminal.pair(terminal.id, {
|
||||||
|
token: 'pos-token-subcat',
|
||||||
|
clientserial: 'SN-001',
|
||||||
|
pairedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Category Tree:
|
||||||
|
// Root -> Cat 4 -> SubCat 41 -> SubCat 411
|
||||||
|
// Root -> Cat 5
|
||||||
|
const cat4 = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Getränke',
|
||||||
|
});
|
||||||
|
const subCat41 = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat4.id,
|
||||||
|
name: 'Alkoholfrei',
|
||||||
|
});
|
||||||
|
const subCat411 = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: subCat41.id,
|
||||||
|
name: 'Cola',
|
||||||
|
});
|
||||||
|
const cat5 = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Snacks',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create articles in various categories
|
||||||
|
const artCola = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: subCat411.id,
|
||||||
|
name: 'Cola 0.5L',
|
||||||
|
});
|
||||||
|
|
||||||
|
const artSnack = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat5.id,
|
||||||
|
name: 'Chips',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set old timestamps on categories and articles to test updated_at touch
|
||||||
|
provider.db.prepare("UPDATE category SET updated_at = '2020-01-01T00:00:00.000Z'").run();
|
||||||
|
provider.db.prepare("UPDATE article SET updated_at = '2020-01-01T00:00:00.000Z'").run();
|
||||||
|
|
||||||
|
console.log('1. Testing setOn on parent category Cat 4 (should enable Cat 4, SubCat 41, and SubCat 411)...');
|
||||||
|
const onResult = await store.posterminal.setOn(googleAuthId, terminal.id, cat4.id);
|
||||||
|
console.log('Category IDs after setOn(cat4):', onResult.categoryIds);
|
||||||
|
|
||||||
|
assert(onResult.categoryIds.includes(cat4.id), 'Should include cat4');
|
||||||
|
assert(onResult.categoryIds.includes(subCat41.id), 'Should include subCat41');
|
||||||
|
assert(onResult.categoryIds.includes(subCat411.id), 'Should include subCat411');
|
||||||
|
assert(!onResult.categoryIds.includes(cat5.id), 'Should NOT include cat5');
|
||||||
|
|
||||||
|
// Check that categories in the subtree were touched
|
||||||
|
const catsAfterOn = provider.db.prepare('SELECT id, updated_at FROM category WHERE id IN (?, ?, ?)').all(cat4.id, subCat41.id, subCat411.id);
|
||||||
|
for (const c of catsAfterOn) {
|
||||||
|
assert.notEqual(c.updated_at, '2020-01-01T00:00:00.000Z', `Category ${c.id} updated_at should be touched on setOn`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that articles in the subtree were touched
|
||||||
|
const artAfterOn = provider.db.prepare('SELECT id, updated_at FROM article WHERE id = ?').get(artCola.id);
|
||||||
|
assert.notEqual(artAfterOn.updated_at, '2020-01-01T00:00:00.000Z', 'Article updated_at should be touched on setOn');
|
||||||
|
|
||||||
|
// Cat 5 article should NOT be touched
|
||||||
|
const artSnackAfterOn = provider.db.prepare('SELECT id, updated_at FROM article WHERE id = ?').get(artSnack.id);
|
||||||
|
assert.equal(artSnackAfterOn.updated_at, '2020-01-01T00:00:00.000Z', 'Unrelated article updated_at should not be touched');
|
||||||
|
|
||||||
|
console.log('2. Also enabling Cat 5...');
|
||||||
|
await store.posterminal.setOn(googleAuthId, terminal.id, cat5.id);
|
||||||
|
|
||||||
|
// Reset timestamps to test setOff touch
|
||||||
|
provider.db.prepare("UPDATE category SET updated_at = '2020-01-01T00:00:00.000Z'").run();
|
||||||
|
provider.db.prepare("UPDATE article SET updated_at = '2020-01-01T00:00:00.000Z'").run();
|
||||||
|
|
||||||
|
console.log('3. Testing setOff on parent category Cat 4 (should disable Cat 4, SubCat 41, and SubCat 411)...');
|
||||||
|
const offResult = await store.posterminal.setOff(googleAuthId, terminal.id, cat4.id);
|
||||||
|
console.log('Category IDs after setOff(cat4):', offResult.categoryIds);
|
||||||
|
|
||||||
|
assert(!offResult.categoryIds.includes(cat4.id), 'Should NOT include cat4');
|
||||||
|
assert(!offResult.categoryIds.includes(subCat41.id), 'Should NOT include subCat41');
|
||||||
|
assert(!offResult.categoryIds.includes(subCat411.id), 'Should NOT include subCat411');
|
||||||
|
assert(offResult.categoryIds.includes(cat5.id), 'Should still include cat5');
|
||||||
|
|
||||||
|
// Check that categories in the subtree were touched on setOff
|
||||||
|
const catsAfterOff = provider.db.prepare('SELECT id, updated_at FROM category WHERE id IN (?, ?, ?)').all(cat4.id, subCat41.id, subCat411.id);
|
||||||
|
for (const c of catsAfterOff) {
|
||||||
|
assert.notEqual(c.updated_at, '2020-01-01T00:00:00.000Z', `Category ${c.id} updated_at should be touched on setOff`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that article in the subtree was touched on setOff
|
||||||
|
const artAfterOff = provider.db.prepare('SELECT id, updated_at FROM article WHERE id = ?').get(artCola.id);
|
||||||
|
assert.notEqual(artAfterOff.updated_at, '2020-01-01T00:00:00.000Z', 'Article updated_at should be touched on setOff');
|
||||||
|
|
||||||
|
// Verify obituaries generated
|
||||||
|
const obituariesAfter = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ? ORDER BY id ASC')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Obituaries after setOff:', obituariesAfter);
|
||||||
|
|
||||||
|
const catObituaryIds = obituariesAfter.filter(o => o.entity_type === 'category').map(o => o.entity_id);
|
||||||
|
assert(catObituaryIds.includes(cat4.id), 'Should have obituary for cat4');
|
||||||
|
assert(catObituaryIds.includes(subCat41.id), 'Should have obituary for subCat41');
|
||||||
|
assert(catObituaryIds.includes(subCat411.id), 'Should have obituary for subCat411');
|
||||||
|
|
||||||
|
const artObituaryIds = obituariesAfter.filter(o => o.entity_type === 'article').map(o => o.entity_id);
|
||||||
|
assert(artObituaryIds.includes(artCola.id), 'Should have obituary for artCola');
|
||||||
|
assert(!artObituaryIds.includes(artSnack.id), 'Should NOT have obituary for artSnack');
|
||||||
|
|
||||||
|
console.log('4. Testing setOn on an intermediate subcategory (SubCat 41)...');
|
||||||
|
const subOnResult = await store.posterminal.setOn(googleAuthId, terminal.id, subCat41.id);
|
||||||
|
console.log('Category IDs after setOn(subCat41):', subOnResult.categoryIds);
|
||||||
|
assert(!subOnResult.categoryIds.includes(cat4.id), 'Parent cat4 should not be enabled when enabling subCat41');
|
||||||
|
assert(subOnResult.categoryIds.includes(subCat41.id), 'subCat41 should be enabled');
|
||||||
|
assert(subOnResult.categoryIds.includes(subCat411.id), 'Child subCat411 should be enabled');
|
||||||
|
|
||||||
|
console.log('✅ ALL SUBCATEGORY SET_ON / SET_OFF TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test()
|
||||||
|
.then(() => {
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
179
scratch/test-deposit-options.js
Normal file
179
scratch/test-deposit-options.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
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 deposit via mandated optionslist ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-deposit-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Deposit Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// 1. Create a category
|
||||||
|
const cat = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Getränke',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Create terminal and enable category
|
||||||
|
const terminal = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'POS 1',
|
||||||
|
pairingpin: '1234',
|
||||||
|
});
|
||||||
|
await store.posterminal.pair(terminal.id, {
|
||||||
|
token: 'pos-token-xyz',
|
||||||
|
clientserial: 'SN-001',
|
||||||
|
pairedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
await store.posterminal.setOn(googleAuthId, terminal.id, cat.id);
|
||||||
|
|
||||||
|
// 3. Create beverage article
|
||||||
|
const clubMate = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat.id,
|
||||||
|
name: 'Club Mate 0.5l',
|
||||||
|
sku: 'MATE-05',
|
||||||
|
});
|
||||||
|
await store.article.setPricesBySku(googleAuthId, clubMate.sku, [
|
||||||
|
{ net: 1.5, tax: 19 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4. Create deposit article
|
||||||
|
const pfandArt = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat.id,
|
||||||
|
name: 'Mehrwegpfand 0,08 €',
|
||||||
|
sku: 'PFAND-0.08',
|
||||||
|
track_stock: 0,
|
||||||
|
});
|
||||||
|
await store.article.setPricesBySku(googleAuthId, pfandArt.sku, [
|
||||||
|
{ gross: 0.08, tax: 19 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 5. Query products BEFORE linking mandated option
|
||||||
|
let prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||||
|
let mateProd = prods.find((p) => p.sku === 'MATE-05');
|
||||||
|
let pfandProd = prods.find((p) => p.sku === 'PFAND-0.08');
|
||||||
|
|
||||||
|
assert.ok(mateProd, 'Club Mate should be listed');
|
||||||
|
assert.equal(mateProd.deposit, '0');
|
||||||
|
assert.equal(mateProd.d_price, '0.0');
|
||||||
|
assert.equal(mateProd.deposit_name, '');
|
||||||
|
|
||||||
|
assert.ok(pfandProd, 'Pfand article should be listed');
|
||||||
|
assert.equal(pfandProd.deposit, '0');
|
||||||
|
|
||||||
|
// 6. Link Mehrwegpfand as mandated option to Club Mate
|
||||||
|
console.log('Linking Mehrwegpfand as mandated option...');
|
||||||
|
await store.optionslist.setOptionsByArticleSku(googleAuthId, clubMate.sku, [
|
||||||
|
{
|
||||||
|
articleId: pfandArt.id,
|
||||||
|
sku: pfandArt.sku,
|
||||||
|
count: 1,
|
||||||
|
sort: 0,
|
||||||
|
mandated: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 7. Query products AFTER linking mandated option
|
||||||
|
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||||
|
mateProd = prods.find((p) => p.sku === 'MATE-05');
|
||||||
|
|
||||||
|
console.log('Club Mate with deposit option:', {
|
||||||
|
sku: mateProd.sku,
|
||||||
|
deposit: mateProd.deposit,
|
||||||
|
d_price: mateProd.d_price,
|
||||||
|
deposit_name: mateProd.deposit_name,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(mateProd.deposit, '1');
|
||||||
|
assert.equal(mateProd.d_price, '0.08');
|
||||||
|
assert.equal(mateProd.deposit_name, 'Mehrwegpfand 0,08 €');
|
||||||
|
|
||||||
|
// 8. Test option name change touching parent article
|
||||||
|
console.log('Testing option name change touching parent article...');
|
||||||
|
const mateBeforeRename = (await provider.query('SELECT updated_at FROM article WHERE id = @id', { id: clubMate.id })).rows[0].updated_at;
|
||||||
|
await new Promise((r) => setTimeout(r, 1100)); // wait >1s for timestamp delta
|
||||||
|
|
||||||
|
await store.article.updateBySku(googleAuthId, pfandArt.sku, {
|
||||||
|
name: 'Glas-Mehrwegpfand 0,08 €',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mateAfterRename = (await provider.query('SELECT updated_at FROM article WHERE id = @id', { id: clubMate.id })).rows[0].updated_at;
|
||||||
|
assert.notEqual(
|
||||||
|
mateBeforeRename,
|
||||||
|
mateAfterRename,
|
||||||
|
'Club Mate updated_at must be bumped when its option article is renamed'
|
||||||
|
);
|
||||||
|
|
||||||
|
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||||
|
mateProd = prods.find((p) => p.sku === 'MATE-05');
|
||||||
|
assert.equal(mateProd.deposit_name, 'Glas-Mehrwegpfand 0,08 €');
|
||||||
|
console.log('Renamed deposit reflected on parent product:', mateProd.deposit_name);
|
||||||
|
|
||||||
|
// 9. Test option price change touching parent article
|
||||||
|
console.log('Testing option price change touching parent article...');
|
||||||
|
const mateBeforePrice = mateAfterRename;
|
||||||
|
await new Promise((r) => setTimeout(r, 1100));
|
||||||
|
|
||||||
|
await store.article.setPricesBySku(googleAuthId, pfandArt.sku, [
|
||||||
|
{ gross: 0.15, tax: 19 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const mateAfterPrice = (await provider.query('SELECT updated_at FROM article WHERE id = @id', { id: clubMate.id })).rows[0].updated_at;
|
||||||
|
assert.notEqual(
|
||||||
|
mateBeforePrice,
|
||||||
|
mateAfterPrice,
|
||||||
|
'Club Mate updated_at must be bumped when its option article price changes'
|
||||||
|
);
|
||||||
|
|
||||||
|
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||||
|
mateProd = prods.find((p) => p.sku === 'MATE-05');
|
||||||
|
assert.equal(mateProd.d_price, '0.15');
|
||||||
|
console.log('New deposit price reflected on parent product:', mateProd.d_price);
|
||||||
|
|
||||||
|
// 10. Test clearing options
|
||||||
|
console.log('Clearing options...');
|
||||||
|
await store.optionslist.setOptionsByArticleSku(googleAuthId, clubMate.sku, []);
|
||||||
|
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||||
|
mateProd = prods.find((p) => p.sku === 'MATE-05');
|
||||||
|
|
||||||
|
assert.equal(mateProd.deposit, '0');
|
||||||
|
assert.equal(mateProd.d_price, '0.0');
|
||||||
|
assert.equal(mateProd.deposit_name, '');
|
||||||
|
|
||||||
|
console.log('✅ ALL DEPOSIT OPTIONS TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
159
scratch/test-inventory-touch.js
Normal file
159
scratch/test-inventory-touch.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
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 main() {
|
||||||
|
console.log('--- Testing updated_at touch on all inventory movements ---');
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-touch-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Inventory Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// Create warehouse
|
||||||
|
const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
||||||
|
|
||||||
|
const cat = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Waren',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create article with past updated_at
|
||||||
|
const art = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat.id,
|
||||||
|
sku: 'TEST-SKU',
|
||||||
|
name: 'Test Article',
|
||||||
|
track_stock: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldTime = '2020-01-01T00:00:00.000Z';
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
|
||||||
|
|
||||||
|
const getUpdatedAt = async (articleId) => {
|
||||||
|
const res = await provider.query(`SELECT updated_at FROM article WHERE id = @articleId`, { articleId });
|
||||||
|
return res.rows[0]?.updated_at;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialUpdatedAt = await getUpdatedAt(art.id);
|
||||||
|
assert.equal(initialUpdatedAt, oldTime, 'Should start with old updated_at');
|
||||||
|
|
||||||
|
// 1. Test store.stock.adjust
|
||||||
|
console.log('1. Testing store.stock.adjust touches article.updated_at...');
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: 'TEST-SKU',
|
||||||
|
warehouseId: wh.id,
|
||||||
|
amount: 2,
|
||||||
|
// reason omitted so it defaults to "Manual stock addition (+2)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const afterAdjustUpdatedAt = await getUpdatedAt(art.id);
|
||||||
|
assert.notEqual(afterAdjustUpdatedAt, oldTime, 'stock.adjust should touch updated_at');
|
||||||
|
assert.ok(new Date(afterAdjustUpdatedAt).getTime() > new Date(oldTime).getTime());
|
||||||
|
|
||||||
|
// Check history was recorded with exact text
|
||||||
|
const historyRes = await provider.query(`SELECT text FROM articlehistory WHERE article_id = @id`, { id: art.id });
|
||||||
|
const adjustmentEntry = historyRes.rows.find((h) => h.text.includes('Bestandskorrektur +2 (Manual stock addition (+2))'));
|
||||||
|
assert.ok(adjustmentEntry, 'Should have exact article history entry');
|
||||||
|
|
||||||
|
// Reset updated_at to old time
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
|
||||||
|
|
||||||
|
// 2. Test Stückliste component touch
|
||||||
|
console.log('2. Testing Stückliste parent is touched when component has stock adjustment...');
|
||||||
|
const bundle = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat.id,
|
||||||
|
sku: 'BUNDLE-1',
|
||||||
|
name: 'Bundle 1',
|
||||||
|
track_stock: 0,
|
||||||
|
});
|
||||||
|
await store.articlelist.setItemsByArticleSku(googleAuthId, 'BUNDLE-1', [
|
||||||
|
{ sku: 'TEST-SKU', count: 1 },
|
||||||
|
]);
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: bundle.id });
|
||||||
|
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: 'TEST-SKU',
|
||||||
|
warehouseId: wh.id,
|
||||||
|
amount: 5,
|
||||||
|
text: 'Restock component',
|
||||||
|
});
|
||||||
|
|
||||||
|
const bundleUpdatedAt = await getUpdatedAt(bundle.id);
|
||||||
|
assert.notEqual(bundleUpdatedAt, oldTime, 'Stückliste bundle updated_at should be touched when component changes stock');
|
||||||
|
|
||||||
|
// 3. Test delivery acceptance touches article.updated_at
|
||||||
|
console.log('3. Testing delivery touches article.updated_at...');
|
||||||
|
const supplier = await store.supplier.create(googleAuthId, { name: 'Supplier Inc' });
|
||||||
|
const delivery = await store.delivery.create(googleAuthId, {
|
||||||
|
supplierId: supplier.id,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
lines: [{ sku: 'TEST-SKU', amount: 10, net: 5, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
|
||||||
|
|
||||||
|
await store.delivery.book(googleAuthId, { id: delivery.id, warehouseId: wh.id });
|
||||||
|
const deliveryUpdatedAt = await getUpdatedAt(art.id);
|
||||||
|
assert.notEqual(deliveryUpdatedAt, oldTime, 'delivery.book should touch article.updated_at');
|
||||||
|
|
||||||
|
// 4. Test customer shipment touches article.updated_at
|
||||||
|
console.log('4. Testing shipment touches article.updated_at...');
|
||||||
|
const groups = await store.customergroup.list(googleAuthId);
|
||||||
|
const customer = await store.entity.create(googleAuthId, { name: 'Customer A', customergroupId: groups[0].id });
|
||||||
|
const order = await store.order.create(googleAuthId, {
|
||||||
|
number: 'AUF-000001',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
entityId: customer.id,
|
||||||
|
lines: [{ sku: 'TEST-SKU', amount: 3, net: 10, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
const carrier = await store.carrier.create(googleAuthId, { name: 'DHL' });
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
|
||||||
|
|
||||||
|
await store.shipment.create(googleAuthId, {
|
||||||
|
orderId: order.id,
|
||||||
|
lines: [{ sku: 'TEST-SKU', amount: 3 }],
|
||||||
|
warehouseId: wh.id,
|
||||||
|
shipping: {
|
||||||
|
carrierId: carrier.id,
|
||||||
|
type: 'standard',
|
||||||
|
state: 'shipped',
|
||||||
|
warehouseId: wh.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const shipmentUpdatedAt = await getUpdatedAt(art.id);
|
||||||
|
assert.notEqual(shipmentUpdatedAt, oldTime, 'shipment.create should touch article.updated_at');
|
||||||
|
|
||||||
|
console.log('✅ ALL INVENTORY TOUCH TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
164
scratch/test-variants.js
Normal file
164
scratch/test-variants.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
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 POS father/child variants and options ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-variants-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Variant Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// 1. Create a category
|
||||||
|
const cat = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Kleidung',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Create terminal and enable category
|
||||||
|
const terminal = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'POS 1',
|
||||||
|
pairingpin: '1234',
|
||||||
|
});
|
||||||
|
await store.posterminal.pair(terminal.id, {
|
||||||
|
token: 'pos-token-xyz',
|
||||||
|
clientserial: 'SN-001',
|
||||||
|
pairedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
await store.posterminal.setOn(googleAuthId, terminal.id, cat.id);
|
||||||
|
|
||||||
|
// 3. Create attribute name "Farbe"
|
||||||
|
const attrName = await store.attributename.create(googleAuthId, {
|
||||||
|
name: 'Farbe',
|
||||||
|
sort: 1,
|
||||||
|
exclusive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Create base article and convert to parent
|
||||||
|
const tShirt = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat.id,
|
||||||
|
name: 'T-Shirt Basic',
|
||||||
|
sku: 'TSHIRT',
|
||||||
|
});
|
||||||
|
await store.article.toggleParent(googleAuthId, tShirt.sku, {
|
||||||
|
isParent: true,
|
||||||
|
attributeNameId: attrName.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Add children "Rot" and "Blau"
|
||||||
|
console.log('Adding children "Rot" and "Blau"...');
|
||||||
|
const childRot = await store.article.addChild(googleAuthId, tShirt.sku, {
|
||||||
|
value: 'Rot',
|
||||||
|
name: 'T-Shirt Basic - Rot',
|
||||||
|
});
|
||||||
|
const childBlau = await store.article.addChild(googleAuthId, tShirt.sku, {
|
||||||
|
value: 'Blau',
|
||||||
|
name: 'T-Shirt Basic - Blau',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Query POS products
|
||||||
|
let prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||||
|
let parentProd = prods.find((p) => p.sku === 'TSHIRT');
|
||||||
|
let rotProd = prods.find((p) => p.sku === childRot.article.sku);
|
||||||
|
let blauProd = prods.find((p) => p.sku === childBlau.article.sku);
|
||||||
|
|
||||||
|
console.log('Parent product:', {
|
||||||
|
sku: parentProd.sku,
|
||||||
|
is_parent: parentProd.is_parent,
|
||||||
|
parent: parentProd.parent,
|
||||||
|
variants: parentProd.variants,
|
||||||
|
options: parentProd.options,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(parentProd.is_parent, '1');
|
||||||
|
assert.equal(parentProd.parent, '0');
|
||||||
|
assert.equal(parentProd.variants, '');
|
||||||
|
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Blau' }]);
|
||||||
|
|
||||||
|
console.log('Child Rot:', {
|
||||||
|
sku: rotProd.sku,
|
||||||
|
is_parent: rotProd.is_parent,
|
||||||
|
parent: rotProd.parent,
|
||||||
|
variants: rotProd.variants,
|
||||||
|
sort: rotProd.sort,
|
||||||
|
options: rotProd.options,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(rotProd.is_parent, '0');
|
||||||
|
assert.equal(rotProd.parent, String(parentProd._id));
|
||||||
|
assert.equal(rotProd.variants, 'Rot');
|
||||||
|
assert.equal(rotProd.sort, '0');
|
||||||
|
assert.equal(rotProd.options, null);
|
||||||
|
|
||||||
|
console.log('Child Blau:', {
|
||||||
|
sku: blauProd.sku,
|
||||||
|
is_parent: blauProd.is_parent,
|
||||||
|
parent: blauProd.parent,
|
||||||
|
variants: blauProd.variants,
|
||||||
|
sort: blauProd.sort,
|
||||||
|
options: blauProd.options,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(blauProd.is_parent, '0');
|
||||||
|
assert.equal(blauProd.parent, String(parentProd._id));
|
||||||
|
assert.equal(blauProd.variants, 'Blau');
|
||||||
|
assert.equal(blauProd.sort, '1');
|
||||||
|
assert.equal(blauProd.options, null);
|
||||||
|
|
||||||
|
// 7. Add third child "Grün" and verify options list update
|
||||||
|
console.log('Adding third child "Grün"...');
|
||||||
|
const childGruen = await store.article.addChild(googleAuthId, tShirt.sku, {
|
||||||
|
value: 'Grün',
|
||||||
|
name: 'T-Shirt Basic - Grün',
|
||||||
|
});
|
||||||
|
|
||||||
|
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||||
|
parentProd = prods.find((p) => p.sku === 'TSHIRT');
|
||||||
|
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Blau,Grün' }]);
|
||||||
|
console.log('Updated parent options:', parentProd.options);
|
||||||
|
|
||||||
|
// 8. Remove child "Blau" and verify options list update
|
||||||
|
console.log('Removing child "Blau"...');
|
||||||
|
await store.article.removeChild(googleAuthId, tShirt.sku, childBlau.article.sku, { deleteArticle: true });
|
||||||
|
|
||||||
|
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
|
||||||
|
parentProd = prods.find((p) => p.sku === 'TSHIRT');
|
||||||
|
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Grün' }]);
|
||||||
|
console.log('Parent options after removal:', parentProd.options);
|
||||||
|
|
||||||
|
console.log('✅ ALL FATHER/CHILD VARIANT TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,35 +1,21 @@
|
|||||||
import sharp from 'sharp';
|
import { optimizeImage } from 'wasm-image-optimization';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates sample PNG image buffer.
|
* Creates sample PNG image buffer.
|
||||||
*/
|
*/
|
||||||
async function makeSampleImage(width = 200, height = 200, color = { r: 50, g: 120, b: 220 }) {
|
async function makeSampleImage(width = 200, height = 200, color = { r: 50, g: 120, b: 220 }) {
|
||||||
return sharp({
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="${width}" height="${height}" fill="rgb(${color.r},${color.g},${color.b})"/></svg>`;
|
||||||
create: {
|
const res = await optimizeImage({ image: svg, format: 'png' });
|
||||||
width,
|
return Buffer.from(res.data);
|
||||||
height,
|
|
||||||
channels: 4,
|
|
||||||
background: { ...color, alpha: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.png()
|
|
||||||
.toBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates sample AVIF thumbnail buffer.
|
* Creates sample AVIF thumbnail buffer.
|
||||||
*/
|
*/
|
||||||
async function makeSampleThumb(width = 64, height = 64) {
|
async function makeSampleThumb(width = 64, height = 64) {
|
||||||
return sharp({
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="${width}" height="${height}" fill="rgb(200,60,60)"/></svg>`;
|
||||||
create: {
|
const res = await optimizeImage({ image: svg, format: 'avif' });
|
||||||
width,
|
return Buffer.from(res.data);
|
||||||
height,
|
|
||||||
channels: 4,
|
|
||||||
background: { r: 200, g: 60, b: 60, alpha: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.avif()
|
|
||||||
.toBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +38,7 @@ export async function fillDatabase(store, options = {}) {
|
|||||||
secondaryCompany: null,
|
secondaryCompany: null,
|
||||||
categories: [],
|
categories: [],
|
||||||
articles: [],
|
articles: [],
|
||||||
fatherArticles: [],
|
parentArticles: [],
|
||||||
childArticles: [],
|
childArticles: [],
|
||||||
customergroups: [],
|
customergroups: [],
|
||||||
entities: [],
|
entities: [],
|
||||||
@@ -154,8 +140,8 @@ export async function fillDatabase(store, options = {}) {
|
|||||||
writeonly: false,
|
writeonly: false,
|
||||||
});
|
});
|
||||||
await store.companysetting.set(ctx.googleAuthId, {
|
await store.companysetting.set(ctx.googleAuthId, {
|
||||||
key: 'labelprinter.type',
|
key: 'default-model',
|
||||||
value: 'zebra',
|
value: 'openai/gpt-6-luna:floor',
|
||||||
frontend: false,
|
frontend: false,
|
||||||
writeonly: false,
|
writeonly: false,
|
||||||
});
|
});
|
||||||
@@ -326,7 +312,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 +351,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 +412,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({
|
||||||
|
|||||||
54
scripts/downgrade.js
Normal file
54
scripts/downgrade.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Downgrades the database schema to a target version.
|
||||||
|
*
|
||||||
|
* npm run downgrade -- <targetVersion>
|
||||||
|
*
|
||||||
|
* Provider from env: DB_PROVIDER=sqlite (+ SQLITE_PATH) or mssql (MSSQL_*, default).
|
||||||
|
* Runs src/providers/<provider>/downgrade/<from>_to_<to>.js one step at a time,
|
||||||
|
* each in its own transaction. Not part of the server — the server only upgrades.
|
||||||
|
* Note: starting a server whose code knows a higher version upgrades again.
|
||||||
|
*/
|
||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
const [rawTarget] = process.argv.slice(2);
|
||||||
|
const target = Number(rawTarget);
|
||||||
|
if (!rawTarget || !Number.isInteger(target) || target < 1) {
|
||||||
|
console.error('Usage: npm run downgrade -- <targetVersion>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerName = process.env.DB_PROVIDER || (process.env.SQLITE_PATH ? 'sqlite' : 'mssql');
|
||||||
|
let provider;
|
||||||
|
|
||||||
|
if (providerName === 'sqlite' || providerName === 'better-sqlite3') {
|
||||||
|
if (!process.env.SQLITE_PATH) {
|
||||||
|
console.error('SQLITE_PATH is required for DB_PROVIDER=sqlite');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const { BetterSqlite3Provider } = await import('../src/providers/sqlite3/index.js');
|
||||||
|
provider = new BetterSqlite3Provider({ filename: process.env.SQLITE_PATH, fileMustExist: true });
|
||||||
|
} else if (providerName === 'mssql') {
|
||||||
|
const { MssqlProvider } = await import('../src/providers/mssql/index.js');
|
||||||
|
provider = new MssqlProvider({
|
||||||
|
server: process.env.MSSQL_SERVER,
|
||||||
|
database: process.env.MSSQL_DATABASE,
|
||||||
|
user: process.env.MSSQL_USER,
|
||||||
|
password: process.env.MSSQL_PASSWORD,
|
||||||
|
options: { encrypt: false, trustServerCertificate: true },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error(`Unknown DB_PROVIDER: ${providerName}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const before = await provider.getSchemaVersion();
|
||||||
|
const steps = await provider.downgradeSchema(target);
|
||||||
|
console.log(`Schema ${providerName}: ${before} -> ${target} (${steps} step(s))`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Downgrade failed: ${err.message}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
} finally {
|
||||||
|
await provider.close();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import 'dotenv/config';
|
|
||||||
import Database from 'better-sqlite3';
|
|
||||||
import { MssqlProvider } from '../src/providers/mssql/index.js';
|
|
||||||
|
|
||||||
async function migrateSqlite(sqlitePath) {
|
|
||||||
console.log(`Migrating SQLite database at: ${sqlitePath}`);
|
|
||||||
const db = new Database(sqlitePath);
|
|
||||||
|
|
||||||
const columns = db.pragma('table_info("manifest")');
|
|
||||||
const hasCreatedAt = columns.some((c) => c.name === 'created_at');
|
|
||||||
if (hasCreatedAt) {
|
|
||||||
console.log('SQLite manifest already has created_at column.');
|
|
||||||
db.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.pragma('foreign_keys = OFF');
|
|
||||||
db.exec(`
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
CREATE TABLE manifest_new (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
company_id INTEGER NOT NULL,
|
|
||||||
"number" TEXT NOT NULL,
|
|
||||||
"type" TEXT NOT NULL,
|
|
||||||
entity_id INTEGER,
|
|
||||||
external_number TEXT,
|
|
||||||
"date" TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
||||||
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
|
|
||||||
FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
INSERT INTO manifest_new (id, company_id, "number", "type", entity_id, external_number, "date", created_at)
|
|
||||||
SELECT id, company_id, "number", "type", entity_id, external_number, "date", "date"
|
|
||||||
FROM manifest;
|
|
||||||
DROP TABLE manifest;
|
|
||||||
ALTER TABLE manifest_new RENAME TO manifest;
|
|
||||||
COMMIT;
|
|
||||||
`);
|
|
||||||
db.pragma('foreign_keys = ON');
|
|
||||||
|
|
||||||
const fkErrors = db.pragma('foreign_key_check');
|
|
||||||
if (fkErrors.length > 0) {
|
|
||||||
console.error('Foreign key check failed after SQLite migration:', fkErrors);
|
|
||||||
} else {
|
|
||||||
console.log('SQLite migration completed successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Updated SQLite manifest columns:', db.pragma('table_info("manifest")'));
|
|
||||||
console.log('Updated SQLite manifest sample row:', db.prepare('SELECT * FROM manifest LIMIT 1').get());
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateMssql() {
|
|
||||||
console.log('Migrating MSSQL database...');
|
|
||||||
const config = {
|
|
||||||
server: process.env.MSSQL_SERVER,
|
|
||||||
database: process.env.MSSQL_DATABASE,
|
|
||||||
user: process.env.MSSQL_USER,
|
|
||||||
password: process.env.MSSQL_PASSWORD,
|
|
||||||
options: {
|
|
||||||
encrypt: false,
|
|
||||||
trustServerCertificate: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const provider = new MssqlProvider(config);
|
|
||||||
try {
|
|
||||||
const pool = await provider.ensurePool();
|
|
||||||
const checkRes = await pool.request().query(`
|
|
||||||
SELECT * FROM sys.columns
|
|
||||||
WHERE object_id = OBJECT_ID(N'manifest') AND name = 'created_at'
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (checkRes.recordset.length > 0) {
|
|
||||||
console.log('MSSQL manifest already has created_at column.');
|
|
||||||
} else {
|
|
||||||
await pool.request().query(`
|
|
||||||
ALTER TABLE manifest ADD created_at DATETIME2 NOT NULL CONSTRAINT DF_manifest_created_at DEFAULT SYSUTCDATETIME();
|
|
||||||
`);
|
|
||||||
await pool.request().query(`
|
|
||||||
UPDATE manifest SET created_at = [date];
|
|
||||||
`);
|
|
||||||
console.log('MSSQL migration completed successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const colsRes = await pool.request().query(`
|
|
||||||
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_NAME = 'manifest'
|
|
||||||
`);
|
|
||||||
console.log('Updated MSSQL manifest columns:', colsRes.recordset);
|
|
||||||
const sampleRes = await pool.request().query(`
|
|
||||||
SELECT TOP 1 * FROM manifest
|
|
||||||
`);
|
|
||||||
console.log('Updated MSSQL manifest sample row:', sampleRes.recordset[0]);
|
|
||||||
await provider.close();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('MSSQL migration failed:', err);
|
|
||||||
await provider.close();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
await migrateSqlite('/home/seb/src/dashback/data/dash.db');
|
|
||||||
await migrateMssql();
|
|
||||||
}
|
|
||||||
|
|
||||||
run().catch((err) => {
|
|
||||||
console.error('Migration failed:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import 'dotenv/config';
|
|
||||||
import Database from 'better-sqlite3';
|
|
||||||
import { MssqlProvider } from '../src/providers/mssql/index.js';
|
|
||||||
|
|
||||||
async function migrateSqlite(sqlitePath) {
|
|
||||||
console.log(`Migrating SQLite database at: ${sqlitePath}`);
|
|
||||||
const db = new Database(sqlitePath);
|
|
||||||
|
|
||||||
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'printer'").get();
|
|
||||||
if (!row) {
|
|
||||||
console.log('Printer table does not exist in SQLite.');
|
|
||||||
db.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.sql.includes("'ipp'")) {
|
|
||||||
console.log("SQLite printer table already includes 'ipp' in CHECK constraint.");
|
|
||||||
db.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.pragma('foreign_keys = OFF');
|
|
||||||
db.exec(`
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
CREATE TABLE printer_new (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
company_id INTEGER NOT NULL,
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
type TEXT NOT NULL CHECK (type IN ('esc/pos', 'zpl', 'ipp')),
|
|
||||||
model TEXT,
|
|
||||||
serial TEXT,
|
|
||||||
sort INTEGER NOT NULL DEFAULT 0,
|
|
||||||
"desc" TEXT,
|
|
||||||
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
INSERT INTO printer_new (id, company_id, address, type, model, serial, sort, "desc")
|
|
||||||
SELECT id, company_id, address, type, model, serial, sort, "desc"
|
|
||||||
FROM printer;
|
|
||||||
DROP TABLE printer;
|
|
||||||
ALTER TABLE printer_new RENAME TO printer;
|
|
||||||
COMMIT;
|
|
||||||
`);
|
|
||||||
db.pragma('foreign_keys = ON');
|
|
||||||
|
|
||||||
const fkErrors = db.pragma('foreign_key_check');
|
|
||||||
if (fkErrors.length > 0) {
|
|
||||||
console.error('Foreign key check failed after SQLite migration:', fkErrors);
|
|
||||||
} else {
|
|
||||||
console.log('SQLite migration completed successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedRow = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'printer'").get();
|
|
||||||
console.log('Updated SQLite printer table SQL:', updatedRow?.sql);
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateMssql() {
|
|
||||||
console.log('Migrating MSSQL database...');
|
|
||||||
const config = {
|
|
||||||
server: process.env.MSSQL_SERVER,
|
|
||||||
database: process.env.MSSQL_DATABASE,
|
|
||||||
user: process.env.MSSQL_USER,
|
|
||||||
password: process.env.MSSQL_PASSWORD,
|
|
||||||
options: {
|
|
||||||
encrypt: false,
|
|
||||||
trustServerCertificate: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const provider = new MssqlProvider(config);
|
|
||||||
const pool = await provider.ensurePool();
|
|
||||||
const tableRes = await pool.request().query(`
|
|
||||||
SELECT * FROM sys.tables WHERE name = 'printer'
|
|
||||||
`);
|
|
||||||
if (tableRes.recordset.length === 0) {
|
|
||||||
console.log('Printer table does not exist in MSSQL.');
|
|
||||||
await provider.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop old CK_printer_type constraint if exists and add new one
|
|
||||||
await pool.request().query(`
|
|
||||||
IF EXISTS (SELECT * FROM sys.check_constraints WHERE name = 'CK_printer_type')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE printer DROP CONSTRAINT CK_printer_type;
|
|
||||||
END
|
|
||||||
ALTER TABLE printer ADD CONSTRAINT CK_printer_type CHECK (type IN ('esc/pos', 'zpl', 'ipp'));
|
|
||||||
`);
|
|
||||||
console.log('MSSQL migration completed successfully.');
|
|
||||||
await provider.close();
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('MSSQL migration skipped/failed (possibly server not running):', err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
await migrateSqlite('/home/seb/src/dashback/data/dash.db');
|
|
||||||
await migrateMssql();
|
|
||||||
}
|
|
||||||
|
|
||||||
run().catch((err) => {
|
|
||||||
console.error('Migration failed:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
108
scripts/test-attributevalue-pic.js
Normal file
108
scripts/test-attributevalue-pic.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { optimizeImage } from 'wasm-image-optimization';
|
||||||
|
import LibDashStore from '../src/index.js';
|
||||||
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('--- Testing attributevaluepic & attributevalue image operations ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-attrval-pic-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
const attr = await store.attributename.create(googleAuthId, {
|
||||||
|
name: 'Color',
|
||||||
|
exclusive: true,
|
||||||
|
});
|
||||||
|
const val = await store.attributevalue.create(googleAuthId, attr.id, {
|
||||||
|
value: 'Midnight Blue',
|
||||||
|
});
|
||||||
|
assert.ok(val.id > 0);
|
||||||
|
|
||||||
|
// Create a 100x100 test image
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="rgb(25,25,112)"/></svg>`;
|
||||||
|
const encoded = await optimizeImage({ image: svg, format: 'avif' });
|
||||||
|
const imageBuffer = Buffer.from(encoded.data);
|
||||||
|
|
||||||
|
console.log('1. Adding image to attribute value...');
|
||||||
|
const addResult = await store.attributevalue.addImage(googleAuthId, val.id, imageBuffer);
|
||||||
|
assert.equal(addResult.id, val.id);
|
||||||
|
assert.ok(addResult.fullHash);
|
||||||
|
assert.ok(addResult.thumbW > 0);
|
||||||
|
assert.ok(addResult.thumbH > 0);
|
||||||
|
assert.ok(addResult.thumbHash);
|
||||||
|
assert.equal(addResult.pics.length, 1);
|
||||||
|
assert.equal(addResult.pics[0].fullHash, addResult.fullHash);
|
||||||
|
|
||||||
|
// Verify row in attributevaluepic
|
||||||
|
const rows = await provider.query('SELECT * FROM attributevaluepic WHERE attributevalue_id = @id', {
|
||||||
|
id: val.id,
|
||||||
|
});
|
||||||
|
assert.equal(rows.rows.length, 1);
|
||||||
|
assert.equal(rows.rows[0].attributevalue_id, val.id);
|
||||||
|
|
||||||
|
// Add again to test replacement
|
||||||
|
console.log('2. Replacing image on attribute value...');
|
||||||
|
const replaceResult = await store.attributevalue.addImage(googleAuthId, val.id, imageBuffer);
|
||||||
|
assert.equal(replaceResult.id, val.id);
|
||||||
|
const rowsAfterReplace = await provider.query('SELECT * FROM attributevaluepic WHERE attributevalue_id = @id', {
|
||||||
|
id: val.id,
|
||||||
|
});
|
||||||
|
assert.equal(rowsAfterReplace.rows.length, 1);
|
||||||
|
|
||||||
|
// Delete image
|
||||||
|
console.log('3. Deleting image from attribute value...');
|
||||||
|
const deleteResult = await store.attributevalue.deleteImage(googleAuthId, val.id);
|
||||||
|
assert.equal(deleteResult.id, val.id);
|
||||||
|
assert.equal(deleteResult.thumbW, null);
|
||||||
|
assert.equal(deleteResult.thumbH, null);
|
||||||
|
assert.equal(deleteResult.thumbHash, null);
|
||||||
|
assert.deepEqual(deleteResult.pics, []);
|
||||||
|
|
||||||
|
const rowsAfterDelete = await provider.query('SELECT * FROM attributevaluepic WHERE attributevalue_id = @id', {
|
||||||
|
id: val.id,
|
||||||
|
});
|
||||||
|
assert.equal(rowsAfterDelete.rows.length, 0);
|
||||||
|
|
||||||
|
// Deleting again should fail with Image not found
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.attributevalue.deleteImage(googleAuthId, val.id);
|
||||||
|
},
|
||||||
|
/Image not found/
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('All attributevaluepic tests passed successfully!');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
142
scripts/test-carrier.js
Normal file
142
scripts/test-carrier.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
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 Carrier Store API ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-carrier-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('carrier-test@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Carrier Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// 1. Create carrier with defaults
|
||||||
|
const c1 = await store.carrier.create(googleAuthId, { name: 'DHL' });
|
||||||
|
assert.equal(c1.name, 'DHL');
|
||||||
|
assert.equal(c1.sort, 0);
|
||||||
|
assert.equal(c1.hasPackages, false);
|
||||||
|
assert.equal(c1.isReverse, false);
|
||||||
|
console.log('PASS: Carrier 1 created with defaults');
|
||||||
|
|
||||||
|
// 2. Create carrier with full flags
|
||||||
|
const c2 = await store.carrier.create(googleAuthId, {
|
||||||
|
name: 'UPS',
|
||||||
|
sort: 5,
|
||||||
|
hasPackages: true,
|
||||||
|
isReverse: true,
|
||||||
|
});
|
||||||
|
assert.equal(c2.name, 'UPS');
|
||||||
|
assert.equal(c2.sort, 5);
|
||||||
|
assert.equal(c2.hasPackages, true);
|
||||||
|
assert.equal(c2.isReverse, true);
|
||||||
|
console.log('PASS: Carrier 2 created with custom flags');
|
||||||
|
|
||||||
|
// 3. Auto-increment sort
|
||||||
|
const c3 = await store.carrier.create(googleAuthId, {
|
||||||
|
name: 'DPD',
|
||||||
|
hasPackages: true,
|
||||||
|
});
|
||||||
|
assert.equal(c3.name, 'DPD');
|
||||||
|
assert.equal(c3.sort, 6);
|
||||||
|
assert.equal(c3.hasPackages, true);
|
||||||
|
assert.equal(c3.isReverse, false);
|
||||||
|
console.log('PASS: Carrier 3 auto-incremented sort');
|
||||||
|
|
||||||
|
// 4. Duplicate name validation
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.carrier.create(googleAuthId, { name: 'DHL' }),
|
||||||
|
{ message: 'Carrier already exists' },
|
||||||
|
);
|
||||||
|
console.log('PASS: Duplicate carrier rejected');
|
||||||
|
|
||||||
|
// 5. Empty name validation
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.carrier.create(googleAuthId, { name: ' ' }),
|
||||||
|
{ message: 'Name required' },
|
||||||
|
);
|
||||||
|
console.log('PASS: Empty carrier name rejected');
|
||||||
|
|
||||||
|
// 6. getById
|
||||||
|
const fetched = await store.carrier.getById(googleAuthId, c2.id);
|
||||||
|
assert.deepEqual(fetched, c2);
|
||||||
|
const notFound = await store.carrier.getById(googleAuthId, 9999);
|
||||||
|
assert.equal(notFound, undefined);
|
||||||
|
console.log('PASS: getById fetched carrier correctly');
|
||||||
|
|
||||||
|
// 7. list
|
||||||
|
const list = await store.carrier.list(googleAuthId);
|
||||||
|
assert.equal(list.length, 3);
|
||||||
|
assert.equal(list[0].name, 'DHL');
|
||||||
|
assert.equal(list[1].name, 'UPS');
|
||||||
|
assert.equal(list[2].name, 'DPD');
|
||||||
|
console.log('PASS: list returned sorted carriers');
|
||||||
|
|
||||||
|
// 8. update
|
||||||
|
const updated = await store.carrier.update(googleAuthId, c1.id, {
|
||||||
|
name: 'DHL Express',
|
||||||
|
hasPackages: true,
|
||||||
|
isReverse: false,
|
||||||
|
});
|
||||||
|
assert.equal(updated.name, 'DHL Express');
|
||||||
|
assert.equal(updated.hasPackages, true);
|
||||||
|
console.log('PASS: update modified carrier');
|
||||||
|
|
||||||
|
// 9. reorder
|
||||||
|
const ordered = await store.carrier.order(googleAuthId, {
|
||||||
|
carrierIds: [c3.id, c2.id, c1.id],
|
||||||
|
});
|
||||||
|
assert.deepEqual(ordered.carrierIds, [c3.id, c2.id, c1.id]);
|
||||||
|
const listAfterOrder = await store.carrier.list(googleAuthId);
|
||||||
|
assert.equal(listAfterOrder[0].id, c3.id);
|
||||||
|
assert.equal(listAfterOrder[0].sort, 0);
|
||||||
|
assert.equal(listAfterOrder[1].id, c2.id);
|
||||||
|
assert.equal(listAfterOrder[1].sort, 1);
|
||||||
|
assert.equal(listAfterOrder[2].id, c1.id);
|
||||||
|
assert.equal(listAfterOrder[2].sort, 2);
|
||||||
|
console.log('PASS: order successfully reordered carriers');
|
||||||
|
|
||||||
|
// 10. delete
|
||||||
|
await store.carrier.delete(googleAuthId, c3.id);
|
||||||
|
const listAfterDelete = await store.carrier.list(googleAuthId);
|
||||||
|
assert.equal(listAfterDelete.length, 2);
|
||||||
|
console.log('PASS: delete removed carrier');
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.carrier.delete(googleAuthId, c3.id),
|
||||||
|
{ message: 'Carrier not found' },
|
||||||
|
);
|
||||||
|
console.log('PASS: delete non-existent carrier rejected');
|
||||||
|
|
||||||
|
console.log('--- ALL CARRIER TESTS PASSED! ---');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
219
scripts/test-category-delete-recursive.js
Normal file
219
scripts/test-category-delete-recursive.js
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
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 category recursive deletion ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-cat-del-rec-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Recursive Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
const rootId = company.rootCategoryId;
|
||||||
|
|
||||||
|
// 1. Refuse to delete root category, even with recursive: true
|
||||||
|
console.log('1. Testing refusal to delete root category...');
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.category.delete({ googleAuthId, id: rootId, recursive: true }),
|
||||||
|
/Cannot delete root category/
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Build category tree:
|
||||||
|
// Root
|
||||||
|
// ├── OutsideCat (for preserved articles)
|
||||||
|
// └── MainCat (to be recursively deleted)
|
||||||
|
// ├── SubCat1
|
||||||
|
// │ └── SubSubCat1
|
||||||
|
// └── SubCat2
|
||||||
|
console.log('2. Building category tree...');
|
||||||
|
const outsideCat = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: rootId,
|
||||||
|
name: 'Outside Category',
|
||||||
|
});
|
||||||
|
const mainCat = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: rootId,
|
||||||
|
name: 'Main Category',
|
||||||
|
});
|
||||||
|
const subCat1 = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: mainCat.id,
|
||||||
|
name: 'Sub Category 1',
|
||||||
|
});
|
||||||
|
const subSubCat1 = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: subCat1.id,
|
||||||
|
name: 'Sub Sub Category 1',
|
||||||
|
});
|
||||||
|
const subCat2 = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: mainCat.id,
|
||||||
|
name: 'Sub Category 2',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Test non-recursive delete guards
|
||||||
|
console.log('3. Testing non-recursive delete guards...');
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.category.delete({ googleAuthId, id: mainCat.id, recursive: false }),
|
||||||
|
/Category has subcategories/
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Create articles:
|
||||||
|
// Article 1: Only in SubSubCat1 -> Should be deleted
|
||||||
|
// Article 2: In SubCat1 AND OutsideCat -> Should survive, removed from SubCat1
|
||||||
|
// Article 3: Parent in SubCat2 with a variant child -> Both should be deleted
|
||||||
|
// Article 4: Outside article in OutsideCat -> Untouched
|
||||||
|
console.log('4. Creating articles in categories...');
|
||||||
|
const artOnlySub = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: subSubCat1.id,
|
||||||
|
name: 'Only SubSubCat Article',
|
||||||
|
});
|
||||||
|
|
||||||
|
const artMulti = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: subCat1.id,
|
||||||
|
name: 'Multi Category Article',
|
||||||
|
});
|
||||||
|
// Add outsideCat to artMulti
|
||||||
|
await store.article.setCategoryIdsBySku(googleAuthId, artMulti.sku, [subCat1.id, outsideCat.id]);
|
||||||
|
|
||||||
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Size' });
|
||||||
|
const artParent = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: subCat2.id,
|
||||||
|
name: 'Parent Article In Sub2',
|
||||||
|
});
|
||||||
|
await store.article.toggleParent(googleAuthId, artParent.sku, {
|
||||||
|
isParent: true,
|
||||||
|
attributeNameId: attrName.id,
|
||||||
|
});
|
||||||
|
const artChild = await store.article.addChild(googleAuthId, artParent.sku, {
|
||||||
|
value: 'M',
|
||||||
|
});
|
||||||
|
|
||||||
|
const artOutside = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: outsideCat.id,
|
||||||
|
name: 'Purely Outside Article',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Test POS terminal assignment unlinking on recursive delete
|
||||||
|
console.log('5. Setting up POS terminal assignment...');
|
||||||
|
const posTerminal = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'POS Register 1',
|
||||||
|
pairingpin: '1234',
|
||||||
|
});
|
||||||
|
await store.posterminal.pair(posTerminal.id, {
|
||||||
|
token: 'pos-token-1',
|
||||||
|
clientserial: 'SN1',
|
||||||
|
pairedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
await store.posterminal.setOn(googleAuthId, posTerminal.id, subCat2.id);
|
||||||
|
await store.posterminal.setOn(googleAuthId, posTerminal.id, outsideCat.id);
|
||||||
|
|
||||||
|
const posCatsBefore = provider.db.prepare(
|
||||||
|
'SELECT category_id FROM posterminalcategory WHERE posterminal_id = ?'
|
||||||
|
).all(posTerminal.id).map(r => r.category_id);
|
||||||
|
assert.ok(posCatsBefore.includes(subCat2.id));
|
||||||
|
assert.ok(posCatsBefore.includes(outsideCat.id));
|
||||||
|
|
||||||
|
// 6. Execute recursive deletion of mainCat
|
||||||
|
console.log('6. Executing recursive category delete on mainCat...');
|
||||||
|
await store.category.delete({
|
||||||
|
googleAuthId,
|
||||||
|
id: mainCat.id,
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Verify category tree state
|
||||||
|
console.log('7. Verifying category tree...');
|
||||||
|
const catsRemaining = provider.db.prepare('SELECT id FROM category').all().map(r => r.id);
|
||||||
|
assert.ok(catsRemaining.includes(rootId));
|
||||||
|
assert.ok(catsRemaining.includes(outsideCat.id));
|
||||||
|
assert.ok(!catsRemaining.includes(mainCat.id));
|
||||||
|
assert.ok(!catsRemaining.includes(subCat1.id));
|
||||||
|
assert.ok(!catsRemaining.includes(subSubCat1.id));
|
||||||
|
assert.ok(!catsRemaining.includes(subCat2.id));
|
||||||
|
|
||||||
|
// 8. Verify POS terminal assignment was unlinked for deleted category
|
||||||
|
console.log('8. Verifying POS terminal categories...');
|
||||||
|
const posCatsAfter = provider.db.prepare(
|
||||||
|
'SELECT category_id FROM posterminalcategory WHERE posterminal_id = ?'
|
||||||
|
).all(posTerminal.id).map(r => r.category_id);
|
||||||
|
assert.ok(!posCatsAfter.includes(subCat2.id));
|
||||||
|
assert.ok(posCatsAfter.includes(outsideCat.id));
|
||||||
|
|
||||||
|
// 9. Verify article states:
|
||||||
|
console.log('9. Verifying article states...');
|
||||||
|
// artOnlySub: deleted
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.article.getBySku(googleAuthId, artOnlySub.sku),
|
||||||
|
/Article not found/
|
||||||
|
);
|
||||||
|
// obituary for artOnlySub exists
|
||||||
|
const t1 = provider.db.prepare("SELECT * FROM posterminalobituary WHERE posterminal_id = ? AND entity_type = 'article' AND entity_id = ?").all(posTerminal.id, artOnlySub.id);
|
||||||
|
assert.equal(t1.length, 1);
|
||||||
|
|
||||||
|
// artParent and its child artChild: both deleted
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.article.getBySku(googleAuthId, artParent.sku),
|
||||||
|
/Article not found/
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.article.getBySku(googleAuthId, artChild.article.sku),
|
||||||
|
/Article not found/
|
||||||
|
);
|
||||||
|
const tParent = provider.db.prepare("SELECT * FROM posterminalobituary WHERE posterminal_id = ? AND entity_type = 'article' AND entity_id = ?").all(posTerminal.id, artParent.id);
|
||||||
|
const tChild = provider.db.prepare("SELECT * FROM posterminalobituary WHERE posterminal_id = ? AND entity_type = 'article' AND entity_id = ?").all(posTerminal.id, artChild.article.id);
|
||||||
|
assert.equal(tParent.length, 1);
|
||||||
|
assert.equal(tChild.length, 1);
|
||||||
|
|
||||||
|
// artMulti: continues to exist!
|
||||||
|
const artMultiDetail = await store.article.getBySku(googleAuthId, artMulti.sku);
|
||||||
|
assert.equal(artMultiDetail.article.sku, artMulti.sku);
|
||||||
|
// artMulti categoryarticle now ONLY contains outsideCat
|
||||||
|
const artMultiCats = provider.db.prepare(
|
||||||
|
'SELECT category_id FROM categoryarticle WHERE article_id = ?'
|
||||||
|
).all(artMultiDetail.article.id).map(r => r.category_id);
|
||||||
|
assert.deepEqual(artMultiCats, [outsideCat.id]);
|
||||||
|
|
||||||
|
// artOutside: completely untouched
|
||||||
|
const artOutsideDetail = await store.article.getBySku(googleAuthId, artOutside.sku);
|
||||||
|
assert.equal(artOutsideDetail.article.sku, artOutside.sku);
|
||||||
|
|
||||||
|
console.log('✅ ALL RECURSIVE CATEGORY DELETE TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
175
scripts/test-categoryproperty-and-attrvalue.js
Normal file
175
scripts/test-categoryproperty-and-attrvalue.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
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 CategoryProperty & CategoryAttributeValue ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-cat-prop-attr-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// Create a subcategory under root
|
||||||
|
const category = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Test Category',
|
||||||
|
});
|
||||||
|
assert.ok(category.id > 0);
|
||||||
|
|
||||||
|
// 1. Test CategoryProperty
|
||||||
|
console.log('1. Testing categoryproperty CRUD...');
|
||||||
|
const prop1 = await store.propertyname.create(googleAuthId, {
|
||||||
|
name: 'Material Care',
|
||||||
|
exclusive: true,
|
||||||
|
});
|
||||||
|
const prop2 = await store.propertyname.create(googleAuthId, {
|
||||||
|
name: 'Season',
|
||||||
|
exclusive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set properties
|
||||||
|
const setProps = await store.categoryproperty.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
{ propertyId: prop1.id, value: 'Dry Clean Only', sort: 10 },
|
||||||
|
{ propertyId: prop2.id, value: 'Summer 2026', sort: 20 },
|
||||||
|
]);
|
||||||
|
assert.equal(setProps.length, 2);
|
||||||
|
|
||||||
|
// List properties
|
||||||
|
const listedProps = await store.categoryproperty.listByCategoryId(googleAuthId, category.id);
|
||||||
|
assert.equal(listedProps.length, 2);
|
||||||
|
assert.equal(listedProps[0].name, 'Material Care');
|
||||||
|
assert.equal(listedProps[0].value, 'Dry Clean Only');
|
||||||
|
assert.equal(listedProps[0].exclusive, true);
|
||||||
|
assert.equal(listedProps[1].name, 'Season');
|
||||||
|
assert.equal(listedProps[1].value, 'Summer 2026');
|
||||||
|
assert.equal(listedProps[1].exclusive, false);
|
||||||
|
|
||||||
|
// Test exclusive violation on categoryproperty
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.categoryproperty.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
{ propertyId: prop1.id, value: 'Val 1' },
|
||||||
|
{ propertyId: prop1.id, value: 'Val 2' },
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
/exclusive/i,
|
||||||
|
'Should reject multiple values for exclusive property'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete single property
|
||||||
|
await store.categoryproperty.delete(googleAuthId, category.id, prop1.id);
|
||||||
|
const afterDeleteProps = await store.categoryproperty.listByCategoryId(googleAuthId, category.id);
|
||||||
|
assert.equal(afterDeleteProps.length, 1);
|
||||||
|
assert.equal(afterDeleteProps[0].propertyId, prop2.id);
|
||||||
|
|
||||||
|
console.log('CategoryProperty tests passed!');
|
||||||
|
|
||||||
|
// 2. Test CategoryAttributeValue
|
||||||
|
console.log('2. Testing categoryattributevalue CRUD...');
|
||||||
|
const attrExclusive = await store.attributename.create(googleAuthId, {
|
||||||
|
name: 'Department',
|
||||||
|
exclusive: true,
|
||||||
|
});
|
||||||
|
const valDeptMen = await store.attributevalue.create(googleAuthId, attrExclusive.id, {
|
||||||
|
value: 'Men',
|
||||||
|
});
|
||||||
|
const valDeptWomen = await store.attributevalue.create(googleAuthId, attrExclusive.id, {
|
||||||
|
value: 'Women',
|
||||||
|
});
|
||||||
|
|
||||||
|
const attrMulti = await store.attributename.create(googleAuthId, {
|
||||||
|
name: 'Tags',
|
||||||
|
exclusive: false,
|
||||||
|
});
|
||||||
|
const valTagSale = await store.attributevalue.create(googleAuthId, attrMulti.id, {
|
||||||
|
value: 'Sale',
|
||||||
|
});
|
||||||
|
const valTagNew = await store.attributevalue.create(googleAuthId, attrMulti.id, {
|
||||||
|
value: 'New',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bulk set with numeric array
|
||||||
|
const setAttrValues = await store.categoryattributevalue.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
valDeptMen.id,
|
||||||
|
valTagSale.id,
|
||||||
|
valTagNew.id,
|
||||||
|
]);
|
||||||
|
assert.equal(setAttrValues.length, 3);
|
||||||
|
|
||||||
|
// List attribute values
|
||||||
|
const listedAttrValues = await store.categoryattributevalue.listByCategoryId(googleAuthId, category.id);
|
||||||
|
assert.equal(listedAttrValues.length, 3);
|
||||||
|
const deptMatch = listedAttrValues.find((v) => v.attributeName === 'Department');
|
||||||
|
assert.ok(deptMatch);
|
||||||
|
assert.equal(deptMatch.value, 'Men');
|
||||||
|
assert.equal(deptMatch.exclusive, true);
|
||||||
|
|
||||||
|
// Test exclusive violation
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.categoryattributevalue.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
valDeptMen.id,
|
||||||
|
valDeptWomen.id,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
/exclusive/i,
|
||||||
|
'Should reject multiple values for exclusive attribute'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test flexible object shapes in setByCategoryId
|
||||||
|
const setWithObjs = await store.categoryattributevalue.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
{ attributeValueId: valDeptWomen.id, sort: 5 },
|
||||||
|
{ id: valTagSale.id, sort: 15 },
|
||||||
|
]);
|
||||||
|
assert.equal(setWithObjs.length, 2);
|
||||||
|
const listedAfterObjSet = await store.categoryattributevalue.listByCategoryId(googleAuthId, category.id);
|
||||||
|
assert.equal(listedAfterObjSet.length, 2);
|
||||||
|
assert.equal(listedAfterObjSet[0].value, 'Women');
|
||||||
|
assert.equal(listedAfterObjSet[0].sort, 5);
|
||||||
|
|
||||||
|
// 3. Test cascade deletion on category delete
|
||||||
|
console.log('3. Testing cascade deletion when category is deleted...');
|
||||||
|
await store.category.delete({ googleAuthId, id: category.id });
|
||||||
|
|
||||||
|
// Directly query database to ensure categoryproperty and categoryattributevalue rows were deleted by FK cascade
|
||||||
|
const remainingProps = await provider.query('SELECT * FROM categoryproperty WHERE category_id = @id', { id: category.id });
|
||||||
|
assert.equal(remainingProps.rows.length, 0);
|
||||||
|
|
||||||
|
const remainingAttrValues = await provider.query('SELECT * FROM categoryattributevalue WHERE category_id = @id', { id: category.id });
|
||||||
|
assert.equal(remainingAttrValues.rows.length, 0);
|
||||||
|
|
||||||
|
console.log('All CategoryProperty and CategoryAttributeValue tests passed successfully!');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
92
scripts/test-companyuser-roles.js
Normal file
92
scripts/test-companyuser-roles.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
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 open(tempDir) {
|
||||||
|
const provider = new BetterSqlite3Provider({ filename: join(tempDir, 'test.db'), storageDir: join(tempDir, 'storage') });
|
||||||
|
await provider.initialize();
|
||||||
|
return { provider, store: await new LibDashStore(provider) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-cu-roles-'));
|
||||||
|
try {
|
||||||
|
console.log('--- Testing companyuser roles + autoinvite ---');
|
||||||
|
let { provider, store } = await open(tempDir);
|
||||||
|
|
||||||
|
const adminId = await store.google_auth.add('admin@example.com');
|
||||||
|
const userId = await store.google_auth.add('user@example.com');
|
||||||
|
const company = await store.company.create({ name: 'Roles Corp', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId: adminId });
|
||||||
|
await store.google_auth.setSelectedCompany(adminId, company.id);
|
||||||
|
|
||||||
|
// creator is admin
|
||||||
|
let members = await store.companyuser.list(adminId);
|
||||||
|
assert.deepEqual(members.map((m) => [m.email, m.role]), [['admin@example.com', 'admin']]);
|
||||||
|
assert.equal((await store.company.list(adminId))[0].role, 'admin');
|
||||||
|
|
||||||
|
// non-member cannot select the company
|
||||||
|
await assert.rejects(store.google_auth.setSelectedCompany(userId, company.id), /Not authorized/);
|
||||||
|
|
||||||
|
// admin adds existing user
|
||||||
|
await assert.rejects(store.companyuser.addByEmail(adminId, { email: 'nobody@example.com' }), /User not found/);
|
||||||
|
await assert.rejects(store.companyuser.addByEmail(adminId, { email: 'user@example.com', role: 'root' }), /Invalid role/);
|
||||||
|
const added = await store.companyuser.addByEmail(adminId, { email: 'User@Example.com' });
|
||||||
|
assert.equal(added.role, 'user');
|
||||||
|
await assert.rejects(store.companyuser.addByEmail(adminId, { email: 'user@example.com' }), /already in company/);
|
||||||
|
|
||||||
|
// user can now select it, but is not admin
|
||||||
|
await store.google_auth.setSelectedCompany(userId, company.id);
|
||||||
|
await assert.rejects(store.companyuser.list(userId), /Not authorized/);
|
||||||
|
await assert.rejects(store.companyuser.addByEmail(userId, { email: 'admin@example.com' }), /Not authorized/);
|
||||||
|
await assert.rejects(store.companyuser.setRole(userId, added.id, 'admin'), /Not authorized/);
|
||||||
|
await assert.rejects(store.companyuser.remove(userId, added.id), /Not authorized/);
|
||||||
|
await assert.rejects(store.autoinvite.add(userId, { email: 'x@example.com' }), /Not authorized/);
|
||||||
|
|
||||||
|
// last admin guard
|
||||||
|
const adminCu = members[0].id;
|
||||||
|
await assert.rejects(store.companyuser.setRole(adminId, adminCu, 'user'), /last admin/);
|
||||||
|
await assert.rejects(store.companyuser.remove(adminId, adminCu), /last admin/);
|
||||||
|
await store.companyuser.setRole(adminId, added.id, 'admin');
|
||||||
|
await store.companyuser.setRole(adminId, adminCu, 'user');
|
||||||
|
await store.companyuser.setRole(userId, adminCu, 'admin');
|
||||||
|
await store.companyuser.setRole(userId, added.id, 'user');
|
||||||
|
await assert.rejects(store.companyuser.setRole(adminId, 99999, 'user'), /Company user not found/);
|
||||||
|
|
||||||
|
// autoinvite
|
||||||
|
await assert.rejects(store.autoinvite.add(adminId, { email: 'user@example.com' }), /already registered/);
|
||||||
|
await assert.rejects(store.autoinvite.add(adminId, { email: 'bad' }), /Invalid email/);
|
||||||
|
await store.autoinvite.add(adminId, { email: 'New@Example.com', role: 'admin' });
|
||||||
|
await assert.rejects(store.autoinvite.add(adminId, { email: 'new@example.com' }), /already exists/);
|
||||||
|
await store.autoinvite.add(adminId, { email: 'gone@example.com' });
|
||||||
|
assert.equal((await store.autoinvite.list(adminId)).length, 2);
|
||||||
|
await store.autoinvite.remove(adminId, (await store.autoinvite.list(adminId)).find((i) => i.email === 'gone@example.com').id);
|
||||||
|
await assert.rejects(store.autoinvite.remove(adminId, 99999), /Invitation not found/);
|
||||||
|
|
||||||
|
const newId = await store.google_auth.add('new@example.com');
|
||||||
|
assert.deepEqual(await store.autoinvite.list(adminId), []);
|
||||||
|
await store.google_auth.setSelectedCompany(newId, company.id);
|
||||||
|
members = await store.companyuser.list(adminId);
|
||||||
|
assert.equal(members.find((m) => m.email === 'new@example.com').role, 'admin');
|
||||||
|
|
||||||
|
// remove clears selection
|
||||||
|
const userCu = members.find((m) => m.email === 'user@example.com');
|
||||||
|
await store.companyuser.remove(adminId, userCu.id);
|
||||||
|
assert.equal((await store.google_auth.getById(userId)).selectedCompanyId, null);
|
||||||
|
|
||||||
|
// downgrade → upgrade through the migration engine
|
||||||
|
await provider.close?.();
|
||||||
|
const env = { ...process.env, DB_PROVIDER: 'sqlite', SQLITE_PATH: join(tempDir, 'test.db') };
|
||||||
|
execFileSync('node', ['scripts/downgrade.js', '5'], { env, stdio: 'inherit' });
|
||||||
|
({ provider, store } = await open(tempDir).catch((e) => { throw e; }));
|
||||||
|
// opening upgrades to latest again; roles reset to admin for all members
|
||||||
|
const rows = (await provider.query('SELECT role FROM companyuser')).rows;
|
||||||
|
assert.ok(rows.length > 0 && rows.every((r) => r.role === 'admin'));
|
||||||
|
assert.deepEqual((await provider.query('SELECT * FROM autoinvite')).rows, []);
|
||||||
|
await provider.close?.();
|
||||||
|
console.log('OK');
|
||||||
|
} finally {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
145
scripts/test-delivery-link-check.js
Normal file
145
scripts/test-delivery-link-check.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
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 Delivery Link Check & Sync in dashdbschema ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-delivery-link-check-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('link-check-test@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Link Check Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
||||||
|
|
||||||
|
const supplier = await store.supplier.create(googleAuthId, {
|
||||||
|
name: 'eks',
|
||||||
|
});
|
||||||
|
|
||||||
|
const art = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Club Mate 0,5l',
|
||||||
|
track_stock: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign supplier with default pu=1, without EK
|
||||||
|
await store.articlesupplier.create(googleAuthId, art.sku, {
|
||||||
|
entityId: supplier.id,
|
||||||
|
sku: 'mate',
|
||||||
|
pu: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let asup = await store.articlesupplier.listByArticleSku(googleAuthId, art.sku);
|
||||||
|
assert.equal(asup.length, 1);
|
||||||
|
assert.equal(asup[0].pu, 1);
|
||||||
|
assert.equal(asup[0].price, null);
|
||||||
|
|
||||||
|
// Create delivery with manifest line: sku: 'mate', pu: 20, net: 12
|
||||||
|
const delivery = await store.delivery.create(googleAuthId, {
|
||||||
|
date: '2026-10-25T10:00:00Z',
|
||||||
|
externalNumber: 'LS-000001',
|
||||||
|
supplierId: supplier.id,
|
||||||
|
lines: [
|
||||||
|
{ sku: 'mate', name: 'Club Mate 0,5l', pu: 20, amount: 2, net: 12, taxrate: 19 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(delivery.id > 0);
|
||||||
|
assert.equal(delivery.lines[0].pu, 20);
|
||||||
|
|
||||||
|
// Verify articlesupplier link was updated!
|
||||||
|
asup = await store.articlesupplier.listByArticleSku(googleAuthId, art.sku);
|
||||||
|
assert.equal(asup.length, 1);
|
||||||
|
assert.equal(asup[0].pu, 20, 'VPE in articlesupplier must be synced to 20');
|
||||||
|
assert.equal(asup[0].price.net, 12, 'Net price in articlesupplier must be synced to 12');
|
||||||
|
assert.equal(asup[0].price.taxRate, 19, 'Tax rate in articlesupplier must be synced to 19');
|
||||||
|
|
||||||
|
// Verify article history
|
||||||
|
const history = await store.article.getHistoryBySku(googleAuthId, art.sku);
|
||||||
|
const historyTexts = (Array.isArray(history) ? history : history.history).map((h) => h.text);
|
||||||
|
console.log('Article history entries:', historyTexts);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
historyTexts.some((t) => t.includes('Lieferant "eks" aktualisiert (VPE: 20, EK: € 12,00, MwSt.: 19%)')),
|
||||||
|
'Must record history entry for supplier update with VPE, EK, and MwSt'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now update delivery with new line pu: 24, net: 11.50, and taxrate: 7
|
||||||
|
await store.delivery.update(googleAuthId, delivery.id, {
|
||||||
|
lines: [
|
||||||
|
{ sku: 'mate', name: 'Club Mate 0,5l', pu: 24, amount: 3, net: 11.5, taxrate: 7 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
asup = await store.articlesupplier.listByArticleSku(googleAuthId, art.sku);
|
||||||
|
assert.equal(asup[0].pu, 24, 'VPE in articlesupplier must be synced to 24 on update');
|
||||||
|
assert.equal(asup[0].price.net, 11.5, 'Net price in articlesupplier must be synced to 11.5 on update');
|
||||||
|
assert.equal(asup[0].price.taxRate, 7, 'Tax rate in articlesupplier must be synced to 7 on update');
|
||||||
|
|
||||||
|
const historyAfterUpdate = await store.article.getHistoryBySku(googleAuthId, art.sku);
|
||||||
|
const historyAfterTexts = (Array.isArray(historyAfterUpdate) ? historyAfterUpdate : historyAfterUpdate.history).map((h) => h.text);
|
||||||
|
console.log('Article history entries after delivery update:', historyAfterTexts);
|
||||||
|
assert(
|
||||||
|
historyAfterTexts.some((t) => t.includes('Lieferant "eks" aktualisiert (VPE: 24, EK: € 11,50, MwSt.: 7%)')),
|
||||||
|
'Must record history entry for supplier update on delivery update'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now test booking the delivery with amount: 3, pu: 24
|
||||||
|
// Should add 3 * 24 = 72 to stock and record clear history!
|
||||||
|
await store.delivery.book(googleAuthId, delivery.id);
|
||||||
|
|
||||||
|
const artDetail = await store.article.getBySku(googleAuthId, art.sku);
|
||||||
|
const totalStock = (artDetail.article.warehouses || []).reduce((acc, w) => acc + (w.stock || 0), 0);
|
||||||
|
console.log('Article stock after booking delivery with amount 3, pu 24:', totalStock);
|
||||||
|
assert.equal(totalStock, 72, 'Physical warehouse stock must be amount * pu (3 * 24 = 72)');
|
||||||
|
|
||||||
|
const historyAfterBooking = await store.article.getHistoryBySku(googleAuthId, art.sku);
|
||||||
|
const bookingTexts = (Array.isArray(historyAfterBooking) ? historyAfterBooking : historyAfterBooking.history).map((h) => h.text);
|
||||||
|
console.log('Article history entries after delivery booking:', bookingTexts);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
bookingTexts.some((t) => t.includes('Lager: "Hauptlager"') && t.includes('Menge: 72 (3 x VPE 24)')),
|
||||||
|
'Warehouse history must clearly show Menge: 72 (3 x VPE 24)'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
bookingTexts.some((t) => t.includes('Lieferant: "eks"') && t.includes('Menge: 3, VPE: 24')),
|
||||||
|
'Acceptance history must clearly show Menge: 3, VPE: 24'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('✓ Delivery link check and sync passed successfully!');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
provider.close();
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
231
scripts/test-delivery-summary.js
Normal file
231
scripts/test-delivery-summary.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
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 Delivery Summary, Ledger Sync & Filtering in dashdbschema ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-delivery-summary-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('delivery-summary-test@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Delivery Summary Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
const warehouse = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
||||||
|
await store.unit.upsert(googleAuthId, {
|
||||||
|
si: 'stk',
|
||||||
|
basesi: 'stk',
|
||||||
|
basesifactor: 1,
|
||||||
|
symbol: 'Stk',
|
||||||
|
name: 'Stück',
|
||||||
|
});
|
||||||
|
|
||||||
|
const supplierAlpha = await store.supplier.create(googleAuthId, {
|
||||||
|
name: 'Alpha Supplier GmbH',
|
||||||
|
});
|
||||||
|
const supplierBeta = await store.supplier.create(googleAuthId, {
|
||||||
|
name: 'Beta Logistics Inc',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Create articles:
|
||||||
|
// Art1: track_stock = 1 (Stock item)
|
||||||
|
const artStock1 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Stock Item 1',
|
||||||
|
track_stock: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Art2: track_stock = 1 (Stock item)
|
||||||
|
const artStock2 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Stock Item 2',
|
||||||
|
track_stock: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Art3: track_stock = 0 (Service / Non-stock item)
|
||||||
|
const artService = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Shipping Freight Fee',
|
||||||
|
track_stock: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Create Delivery 1:
|
||||||
|
// Has 2 lines: artStock1 (qty 10), artService (qty 1)
|
||||||
|
const delivery1 = await store.delivery.create(googleAuthId, {
|
||||||
|
date: '2026-08-01T10:00:00Z',
|
||||||
|
externalNumber: 'DEL-ALPHA-1001',
|
||||||
|
supplierId: supplierAlpha.id,
|
||||||
|
lines: [
|
||||||
|
{ sku: artStock1.sku, name: 'Stock Item 1', amount: 10, net: 50, taxrate: 19 },
|
||||||
|
{ sku: artService.sku, name: 'Shipping Freight Fee', amount: 1, net: 10, taxrate: 19 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Delivery 1 created with id:', delivery1.id);
|
||||||
|
|
||||||
|
// Verify deliverysummary row for delivery 1:
|
||||||
|
// positions = 2, total_quantity = 11, stock_positions = 1, booked_positions = 0, unbooked_positions = 1, is_booked = 0
|
||||||
|
let sum1 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery1.id);
|
||||||
|
assert(sum1, 'deliverysummary row for delivery 1 must exist');
|
||||||
|
assert.equal(sum1.positions, 2, 'Total positions should be 2');
|
||||||
|
assert.equal(sum1.stock_positions, 1, 'Stock positions should be 1');
|
||||||
|
assert.equal(sum1.booked_positions, 0, 'Booked positions should initially be 0');
|
||||||
|
assert.equal(sum1.unbooked_positions, 1, 'Unbooked positions should initially be 1');
|
||||||
|
assert.equal(sum1.stock_quantity, 10, 'Stock quantity should be 10');
|
||||||
|
assert.equal(sum1.booked_quantity, 0, 'Booked quantity should initially be 0');
|
||||||
|
assert.equal(sum1.unbooked_quantity, 10, 'Unbooked quantity should initially be 10');
|
||||||
|
assert.equal(sum1.is_booked, 0, 'is_booked should be 0');
|
||||||
|
assert.equal(sum1.supplier_name, 'Alpha Supplier GmbH');
|
||||||
|
|
||||||
|
// 3. Create Delivery 2:
|
||||||
|
// Has 1 line: artStock2 (qty 5), Supplier: Beta
|
||||||
|
const delivery2 = await store.delivery.create(googleAuthId, {
|
||||||
|
date: '2026-10-01T10:00:00Z',
|
||||||
|
externalNumber: 'DEL-BETA-2002',
|
||||||
|
supplierId: supplierBeta.id,
|
||||||
|
lines: [
|
||||||
|
{ sku: artStock2.sku, name: 'Stock Item 2', amount: 5, net: 20, taxrate: 19 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let sum2 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery2.id);
|
||||||
|
assert(sum2, 'deliverysummary row for delivery 2 must exist');
|
||||||
|
assert.equal(sum2.stock_positions, 1);
|
||||||
|
assert.equal(sum2.unbooked_positions, 1);
|
||||||
|
assert.equal(sum2.is_booked, 0);
|
||||||
|
|
||||||
|
// 4. Book Delivery 1 partially:
|
||||||
|
// Book 4 units of artStock1 in warehouse
|
||||||
|
const lineStock1 = delivery1.lines.find((l) => l.sku === artStock1.sku);
|
||||||
|
assert(lineStock1, 'lineStock1 must exist');
|
||||||
|
|
||||||
|
await store.ledger.add(googleAuthId, {
|
||||||
|
manifestarticle_id: lineStock1.id,
|
||||||
|
warehouse_id: warehouse.id,
|
||||||
|
transaction_type: 'DELIVERY',
|
||||||
|
amount: 4,
|
||||||
|
transaction_text: 'Partial goods receipt',
|
||||||
|
});
|
||||||
|
|
||||||
|
sum1 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery1.id);
|
||||||
|
assert.equal(sum1.booked_positions, 0, 'Booked positions should still be 0 since 4 < 10');
|
||||||
|
assert.equal(sum1.unbooked_positions, 1);
|
||||||
|
assert.equal(sum1.booked_quantity, 4);
|
||||||
|
assert.equal(sum1.unbooked_quantity, 6);
|
||||||
|
assert.equal(sum1.is_booked, 0);
|
||||||
|
console.log('✓ Partial booking properly updated deliverysummary');
|
||||||
|
|
||||||
|
// 5. Book remaining 6 units of artStock1
|
||||||
|
await store.ledger.add(googleAuthId, {
|
||||||
|
manifestarticle_id: lineStock1.id,
|
||||||
|
warehouse_id: warehouse.id,
|
||||||
|
transaction_type: 'DELIVERY',
|
||||||
|
amount: 6,
|
||||||
|
transaction_text: 'Final goods receipt',
|
||||||
|
});
|
||||||
|
|
||||||
|
sum1 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery1.id);
|
||||||
|
assert.equal(sum1.booked_positions, 1, 'Booked positions should now be 1');
|
||||||
|
assert.equal(sum1.unbooked_positions, 0, 'Unbooked positions should now be 0');
|
||||||
|
assert.equal(sum1.booked_quantity, 10);
|
||||||
|
assert.equal(sum1.unbooked_quantity, 0);
|
||||||
|
assert.equal(sum1.is_booked, 1, 'Delivery 1 should now be fully booked (is_booked = 1)');
|
||||||
|
console.log('✓ Full booking properly updated deliverysummary (is_booked = 1)');
|
||||||
|
|
||||||
|
// 6. Test windowed / paged list:
|
||||||
|
// Both deliveries present
|
||||||
|
const pagedAll = await store.delivery.list(googleAuthId, { offset: 0, limit: 10 });
|
||||||
|
assert.equal(pagedAll.total, 2);
|
||||||
|
assert.equal(pagedAll.unfilteredTotal, 2);
|
||||||
|
assert.equal(pagedAll.deliveries.length, 2);
|
||||||
|
assert.equal(pagedAll.offset, 0);
|
||||||
|
assert.equal(pagedAll.limit, 10);
|
||||||
|
|
||||||
|
// Filter onlyUnbooked (Delivery 1 is fully booked, Delivery 2 is unbooked)
|
||||||
|
const pagedUnbooked = await store.delivery.list(googleAuthId, { onlyUnbooked: true });
|
||||||
|
assert.equal(pagedUnbooked.total, 1, 'Only 1 delivery is unbooked');
|
||||||
|
assert.equal(pagedUnbooked.unfilteredTotal, 2, 'Unfiltered total remains 2');
|
||||||
|
assert.equal(pagedUnbooked.deliveries[0].id, delivery2.id);
|
||||||
|
console.log('✓ onlyUnbooked filter returned only unbooked deliveries');
|
||||||
|
|
||||||
|
// Search filter: 'Beta'
|
||||||
|
const searchBeta = await store.delivery.list(googleAuthId, { search: 'Beta' });
|
||||||
|
assert.equal(searchBeta.total, 1);
|
||||||
|
assert.equal(searchBeta.unfilteredTotal, 2);
|
||||||
|
assert.equal(searchBeta.deliveries[0].id, delivery2.id);
|
||||||
|
|
||||||
|
// Search filter: 'ALPHA-1001'
|
||||||
|
const searchNum = await store.delivery.list(googleAuthId, { search: 'ALPHA-1001' });
|
||||||
|
assert.equal(searchNum.total, 1);
|
||||||
|
assert.equal(searchNum.deliveries[0].id, delivery1.id);
|
||||||
|
|
||||||
|
// Test unpaged backwards compatibility
|
||||||
|
const unpaged = await store.delivery.list(googleAuthId);
|
||||||
|
assert(Array.isArray(unpaged), 'Unpaged list should return Array');
|
||||||
|
assert.equal(unpaged.length, 2);
|
||||||
|
const d1 = unpaged.find((d) => d.id === delivery1.id);
|
||||||
|
assert(d1.ledger, 'd1 should contain ledger summary object');
|
||||||
|
assert.equal(d1.ledger.totalPositions, 1);
|
||||||
|
assert.equal(d1.ledger.bookedPositions, 1);
|
||||||
|
assert.equal(d1.ledger.unbookedPositions, 0);
|
||||||
|
assert.equal(d1.ledger.isBooked, true);
|
||||||
|
|
||||||
|
const d2 = unpaged.find((d) => d.id === delivery2.id);
|
||||||
|
assert.equal(d2.ledger.unbookedPositions, 1);
|
||||||
|
assert.equal(d2.ledger.isBooked, false);
|
||||||
|
console.log('✓ Unpaged list returns enriched deliveries with ledger summary');
|
||||||
|
|
||||||
|
// 7. Test rebuildSummaries
|
||||||
|
provider.db.prepare('DELETE FROM deliverysummary').run();
|
||||||
|
const countEmpty = provider.db.prepare('SELECT COUNT(*) AS cnt FROM deliverysummary').get().cnt;
|
||||||
|
assert.equal(countEmpty, 0);
|
||||||
|
|
||||||
|
await store.delivery.rebuildSummaries(googleAuthId);
|
||||||
|
const countRebuilt = provider.db.prepare('SELECT COUNT(*) AS cnt FROM deliverysummary').get().cnt;
|
||||||
|
assert.equal(countRebuilt, 2, 'Rebuilt deliverysummary should have 2 rows');
|
||||||
|
|
||||||
|
const rebuiltD1 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery1.id);
|
||||||
|
assert.equal(rebuiltD1.is_booked, 1);
|
||||||
|
assert.equal(rebuiltD1.booked_quantity, 10);
|
||||||
|
assert.equal(rebuiltD1.unbooked_quantity, 0);
|
||||||
|
|
||||||
|
const rebuiltD2 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery2.id);
|
||||||
|
assert.equal(rebuiltD2.is_booked, 0);
|
||||||
|
assert.equal(rebuiltD2.unbooked_positions, 1);
|
||||||
|
console.log('✓ rebuildSummaries restored all deliverysummary rows accurately');
|
||||||
|
|
||||||
|
console.log('--- ALL DELIVERY SUMMARY & FILTER TESTS PASSED ---');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
45
scripts/test-healthcheck.js
Normal file
45
scripts/test-healthcheck.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import assert from 'node:assert';
|
||||||
|
import LibDashStore from '../src/index.js';
|
||||||
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Testing healthcheck store method on in-memory SQLite...');
|
||||||
|
const provider = new BetterSqlite3Provider({ path: ':memory:' });
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
assert(store.healthcheck, 'store.healthcheck should exist');
|
||||||
|
assert.strictEqual(typeof store.healthcheck.check, 'function', 'store.healthcheck.check should be a function');
|
||||||
|
|
||||||
|
// First call: table is empty, should ensure row (0), select for update, increment to 1 and update
|
||||||
|
const res1 = await store.healthcheck.check();
|
||||||
|
console.log('Healthcheck call 1 result:', res1);
|
||||||
|
assert.strictEqual(res1.ok, true, 'res1.ok should be true');
|
||||||
|
assert.strictEqual(res1.sequence, 1, 'res1.sequence should be 1');
|
||||||
|
|
||||||
|
// Second call: row exists with 1, should increment to 2 and update
|
||||||
|
const res2 = await store.healthcheck.check();
|
||||||
|
console.log('Healthcheck call 2 result:', res2);
|
||||||
|
assert.strictEqual(res2.ok, true, 'res2.ok should be true');
|
||||||
|
assert.strictEqual(res2.sequence, 2, 'res2.sequence should be 2');
|
||||||
|
|
||||||
|
// Third call: row exists with 2, should increment to 3 and update
|
||||||
|
const res3 = await store.healthcheck.check();
|
||||||
|
console.log('Healthcheck call 3 result:', res3);
|
||||||
|
assert.strictEqual(res3.ok, true, 'res3.ok should be true');
|
||||||
|
assert.strictEqual(res3.sequence, 3, 'res3.sequence should be 3');
|
||||||
|
|
||||||
|
// Verify DB state directly
|
||||||
|
const rows = await provider.query('SELECT * FROM healthcheck');
|
||||||
|
console.log('Healthcheck DB rows:', rows.rows);
|
||||||
|
assert.strictEqual(rows.rows.length, 1, 'healthcheck should have exactly 1 row');
|
||||||
|
assert.strictEqual(Number(rows.rows[0].sequence), 3, 'sequence in DB should be 3');
|
||||||
|
|
||||||
|
await provider.close();
|
||||||
|
console.log('✓ All healthcheck store tests passed successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
121
scripts/test-history-and-attrvalue.js
Normal file
121
scripts/test-history-and-attrvalue.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
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 Parent History & AttributeValue Shapes ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-hist-attr-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
console.log('1. Testing setParentBySku history logging...');
|
||||||
|
const parent = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Parent Shirt',
|
||||||
|
});
|
||||||
|
const child = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Child Shirt Red',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check history before linking
|
||||||
|
let parentHist = await store.article.getHistoryBySku(googleAuthId, parent.sku);
|
||||||
|
assert.equal(parentHist.length, 1);
|
||||||
|
assert.match(parentHist[0].text, /angelegt/);
|
||||||
|
|
||||||
|
// Link child to parent
|
||||||
|
await store.article.setParentBySku(googleAuthId, child.sku, parent.sku);
|
||||||
|
|
||||||
|
// Parent history must now contain the conversion entry and the child entry!
|
||||||
|
parentHist = await store.article.getHistoryBySku(googleAuthId, parent.sku);
|
||||||
|
const hasConversion = parentHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
|
||||||
|
const hasVariantAdded = parentHist.some((h) => h.text.includes('hinzugefügt'));
|
||||||
|
assert.ok(hasConversion, `Parent history missing conversion row: ${JSON.stringify(parentHist)}`);
|
||||||
|
assert.ok(hasVariantAdded, `Parent history missing variant added row: ${JSON.stringify(parentHist)}`);
|
||||||
|
|
||||||
|
// Child history must contain the assignment entry
|
||||||
|
const childHist = await store.article.getHistoryBySku(googleAuthId, child.sku);
|
||||||
|
const hasAssigned = childHist.some((h) => h.text.includes('als Variante von'));
|
||||||
|
assert.ok(hasAssigned, `Child history missing variant assigned row: ${JSON.stringify(childHist)}`);
|
||||||
|
|
||||||
|
console.log('2. Testing importRows Pass 2 history logging...');
|
||||||
|
const importRes = await store.article.importRows({
|
||||||
|
googleAuthId,
|
||||||
|
rows: [
|
||||||
|
{ sku: 'IMP-PARENT', name: 'Imported Parent', parentId: company.rootCategoryId },
|
||||||
|
{ sku: 'IMP-CHILD', name: 'Imported Child', parentId: company.rootCategoryId, parentSku: 'IMP-PARENT' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(importRes.errors.length, 0);
|
||||||
|
|
||||||
|
const impParentHist = await store.article.getHistoryBySku(googleAuthId, 'IMP-PARENT');
|
||||||
|
const impHasConversion = impParentHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
|
||||||
|
assert.ok(impHasConversion, `Imported parent missing conversion row: ${JSON.stringify(impParentHist)}`);
|
||||||
|
|
||||||
|
console.log('3. Testing flexible attributevalue request shapes in setByArticleSku...');
|
||||||
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
|
||||||
|
const val1 = await store.attributevalue.create(googleAuthId, attrName.id, { value: 'Rot' });
|
||||||
|
const val2 = await store.attributevalue.create(googleAuthId, attrName.id, { value: 'Blau' });
|
||||||
|
|
||||||
|
// Test A: Array of IDs [val1.id, val2.id]
|
||||||
|
let res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [val1.id]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val1.id);
|
||||||
|
|
||||||
|
// Test B: Array of objects with .id [{ id: val1.id }]
|
||||||
|
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ id: val2.id }]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val2.id);
|
||||||
|
|
||||||
|
// Test C: Array of objects with .attributeValueId
|
||||||
|
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ attributeValueId: val1.id }]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val1.id);
|
||||||
|
|
||||||
|
// Test D: Array of objects with attributeId and value
|
||||||
|
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ attributeId: attrName.id, value: 'Blau' }]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val2.id);
|
||||||
|
|
||||||
|
// Test E: Array of objects with attributeName and value
|
||||||
|
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ attributeName: 'Farbe', value: 'Rot' }]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val1.id);
|
||||||
|
|
||||||
|
console.log('✅ ALL TESTS PASSED SUCCESSFULLY');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -5,12 +5,10 @@ import { join } from 'node:path';
|
|||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import LibDashStore from '../src/index.js';
|
import LibDashStore from '../src/index.js';
|
||||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
import { INIT_HASH, computePrevHash } from '../src/store/ledger/add.js';
|
import { computePrevHash, INIT_HASH } from '../src/store/ledger/add.js';
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
console.log('--- Testing store.ledger (add, created_at, list queries) ---');
|
const tempDir = mkdtempSync(join(tmpdir(), 'dash-ledger-test-'));
|
||||||
|
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), 'test-ledger-'));
|
|
||||||
const dbFile = join(tempDir, 'test.db');
|
const dbFile = join(tempDir, 'test.db');
|
||||||
const storageDir = join(tempDir, 'storage');
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
@@ -18,42 +16,85 @@ async function run() {
|
|||||||
filename: dbFile,
|
filename: dbFile,
|
||||||
storageDir,
|
storageDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
await provider.initialize();
|
await provider.initialize();
|
||||||
const store = await new LibDashStore(provider);
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Setup user and company
|
console.log('--- Testing store.ledger (acceptance, warehouse, shipment, guards) ---');
|
||||||
const googleAuthId = await store.google_auth.add('ledger-test@example.com');
|
const googleAuthId = await store.google_auth.add('ledger-tester@example.com');
|
||||||
|
|
||||||
const company = await store.company.create({
|
const company = await store.company.create({
|
||||||
name: 'Ledger Test Corp',
|
name: 'Ledger Test GmbH',
|
||||||
iso639: 'de',
|
iso639: 'de',
|
||||||
taxrate: 19,
|
taxrate: 19,
|
||||||
reducedtaxrate: 7,
|
reducedtaxrate: 7,
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
await store.unit.upsert(googleAuthId, {
|
const supplier = await store.supplier.create(googleAuthId, {
|
||||||
si: 'stk',
|
name: 'Supplier X',
|
||||||
basesi: 'stk',
|
|
||||||
basesifactor: 1,
|
|
||||||
symbol: 'Stk',
|
|
||||||
name: 'Stück',
|
|
||||||
});
|
});
|
||||||
const supplier = await store.supplier.create(googleAuthId, { name: 'Supplier X' });
|
|
||||||
|
// Create an article with track_stock = false initially
|
||||||
const article = await store.article.createUnderParent({
|
const article = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Ledger Linked Article',
|
name: 'Ledger Linked Article',
|
||||||
|
track_stock: false,
|
||||||
});
|
});
|
||||||
|
assert.equal(article.track_stock, false, 'Newly created article must have track_stock disabled');
|
||||||
|
|
||||||
await store.articlesupplier.create(googleAuthId, article.sku, {
|
await store.articlesupplier.create(googleAuthId, article.sku, {
|
||||||
entityId: supplier.id,
|
entityId: supplier.id,
|
||||||
sku: 'SKU-ABC',
|
sku: 'SKU-ABC',
|
||||||
pu: 1,
|
pu: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a delivery manifest with lines
|
const customer1 = await store.entity.create(googleAuthId, {
|
||||||
|
name: 'Customer Quote Test',
|
||||||
|
isCustomer: true,
|
||||||
|
customergroupId: company.rootCategoryId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a quote and verify ledger bookings are rejected
|
||||||
|
const quote = await store.quote.create(googleAuthId, {
|
||||||
|
number: 'Q-1',
|
||||||
|
entityId: customer1.id,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
lines: [{ sku: 'SKU-ABC', name: 'Quote item', pu: 1, amount: 2, net: 10, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.ledger.add(googleAuthId, {
|
||||||
|
manifestarticle_id: quote.lines[0].id,
|
||||||
|
transaction_type: 'ACCEPTANCE',
|
||||||
|
amount: 2,
|
||||||
|
transaction_text: 'Should fail for quote',
|
||||||
|
}),
|
||||||
|
/Cannot book ledger entries for quote/,
|
||||||
|
'Should reject booking ledger entries for quote manifest',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create an order and verify ledger bookings are rejected
|
||||||
|
const order = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-1',
|
||||||
|
entityId: customer1.id,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
lines: [{ sku: 'SKU-ABC', name: 'Order item', pu: 1, amount: 2, net: 10, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.ledger.add(googleAuthId, {
|
||||||
|
manifestarticle_id: order.lines[0].id,
|
||||||
|
transaction_type: 'ACCEPTANCE',
|
||||||
|
amount: 2,
|
||||||
|
transaction_text: 'Should fail for order',
|
||||||
|
}),
|
||||||
|
/Cannot book ledger entries for order/,
|
||||||
|
'Should reject booking ledger entries for order manifest',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. Create a delivery manifest with lines
|
||||||
const delivery = await store.delivery.create(googleAuthId, {
|
const delivery = await store.delivery.create(googleAuthId, {
|
||||||
supplierId: supplier.id,
|
supplierId: supplier.id,
|
||||||
externalNumber: 'DEL-1',
|
externalNumber: 'DEL-1',
|
||||||
@@ -70,7 +111,7 @@ async function run() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
sku: 'SKU-ABC',
|
sku: 'SKU-ABC',
|
||||||
name: 'Ledger Item 1 Batch 2',
|
name: 'Ledger Item 2',
|
||||||
pu: 1,
|
pu: 1,
|
||||||
amount: 10,
|
amount: 10,
|
||||||
net: 20,
|
net: 20,
|
||||||
@@ -82,243 +123,351 @@ async function run() {
|
|||||||
|
|
||||||
const manifestarticleId1 = delivery.lines[0].id;
|
const manifestarticleId1 = delivery.lines[0].id;
|
||||||
const manifestarticleId2 = delivery.lines[1].id;
|
const manifestarticleId2 = delivery.lines[1].id;
|
||||||
assert(manifestarticleId1, 'manifestarticle line 1 should have an id');
|
|
||||||
assert(manifestarticleId2, 'manifestarticle line 2 should have an id');
|
|
||||||
|
|
||||||
// 2. Test first row (genesis row)
|
// 2. Book line 1 for untracked article (acceptance only, warehouse_id is null)
|
||||||
console.log('1. Inserting genesis ledger row...');
|
console.log('1. Booking acceptance for untracked article line 1...');
|
||||||
const row1 = await store.ledger.add(googleAuthId, {
|
const row1 = await store.ledger.add(googleAuthId, {
|
||||||
manifestarticle_id: manifestarticleId1,
|
manifestarticle_id: manifestarticleId1,
|
||||||
transaction_type: 'GOODS_RECEIPT',
|
transaction_type: 'ACCEPTANCE',
|
||||||
amount: 5,
|
amount: 5,
|
||||||
transaction_text: 'Initial delivery movement',
|
transaction_text: `Lieferung ${delivery.number}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(row1.id, 1);
|
assert.equal(row1.id, 1);
|
||||||
assert.equal(row1.manifestarticle_id, manifestarticleId1);
|
assert.equal(row1.manifestarticle_id, manifestarticleId1);
|
||||||
assert.equal(row1.transaction_type, 'GOODS_RECEIPT');
|
assert.equal(row1.warehouse_id, null);
|
||||||
|
assert.equal(row1.transaction_type, 'ACCEPTANCE');
|
||||||
assert.equal(row1.amount, 5);
|
assert.equal(row1.amount, 5);
|
||||||
assert.equal(row1.transaction_text, 'Initial delivery movement');
|
|
||||||
assert.equal(row1.hash_version, 1);
|
assert.equal(row1.hash_version, 1);
|
||||||
assert.equal(row1.prev_hash, INIT_HASH);
|
assert.equal(row1.prev_hash, INIT_HASH);
|
||||||
assert(row1.created_at, 'created_at should exist');
|
assert(row1.created_at, 'created_at should exist');
|
||||||
assert(!Number.isNaN(new Date(row1.created_at).getTime()), 'created_at should be valid date string');
|
|
||||||
console.log('✓ Row 1 created_at:', row1.created_at);
|
|
||||||
|
|
||||||
// Verify manual hash computation of row 1 (including created_at)
|
// Attempting to book line 1 again must be rejected
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.ledger.add(googleAuthId, {
|
||||||
|
manifestarticle_id: manifestarticleId1,
|
||||||
|
transaction_type: 'ACCEPTANCE',
|
||||||
|
amount: 5,
|
||||||
|
transaction_text: 'Double booking attempt',
|
||||||
|
}),
|
||||||
|
/Delivery line already booked on ledger/,
|
||||||
|
'Should reject booking an already booked delivery line',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Create warehouse and enable tracking on the article
|
||||||
|
console.log('2. Creating warehouse and enabling stock tracking...');
|
||||||
|
const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
||||||
|
await store.article.setTrackStockBySku(googleAuthId, article.sku, true);
|
||||||
|
|
||||||
|
// Even though tracking is now enabled, line 1 is already accepted so it cannot be booked again
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.ledger.add(googleAuthId, {
|
||||||
|
manifestarticle_id: manifestarticleId1,
|
||||||
|
warehouse_id: wh1.id,
|
||||||
|
transaction_type: 'ACCEPTANCE',
|
||||||
|
amount: 5,
|
||||||
|
transaction_text: 'Late booking attempt after tracking enabled',
|
||||||
|
}),
|
||||||
|
/Delivery line already booked on ledger/,
|
||||||
|
'Cannot book a delivery line later once stock tracking is enabled if already accepted',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Book line 2 (now tracked): books ACCEPTANCE (row 2) + WAREHOUSE (row 3) in one transaction
|
||||||
|
console.log('3. Booking line 2 with tracking enabled (dual entries: ACCEPTANCE + WAREHOUSE)...');
|
||||||
const expectedHash2 = computePrevHash(row1);
|
const expectedHash2 = computePrevHash(row1);
|
||||||
const manualHash2 = createHash('sha512')
|
|
||||||
.update(
|
|
||||||
JSON.stringify({
|
|
||||||
id: 1,
|
|
||||||
manifestarticle_id: manifestarticleId1,
|
|
||||||
warehouse_id: null,
|
|
||||||
transaction_type: 'GOODS_RECEIPT',
|
|
||||||
amount: 5,
|
|
||||||
transaction_text: 'Initial delivery movement',
|
|
||||||
hash_version: 1,
|
|
||||||
prev_hash: INIT_HASH,
|
|
||||||
created_at: new Date(row1.created_at).toISOString(),
|
|
||||||
}),
|
|
||||||
'utf8',
|
|
||||||
)
|
|
||||||
.digest('hex');
|
|
||||||
assert.equal(expectedHash2, manualHash2);
|
|
||||||
assert.equal(manualHash2.length, 128);
|
|
||||||
|
|
||||||
// 3. Test second row (line 1, different transaction type)
|
|
||||||
console.log('2. Inserting second ledger row...');
|
|
||||||
const row2 = await store.ledger.add(googleAuthId, {
|
const row2 = await store.ledger.add(googleAuthId, {
|
||||||
manifestarticleId: manifestarticleId1,
|
manifestarticle_id: manifestarticleId2,
|
||||||
transactionType: 'INVENTORY_ADJUST',
|
warehouse_id: wh1.id,
|
||||||
amount: -1,
|
transaction_type: 'ACCEPTANCE',
|
||||||
transactionText: 'Damaged item written off',
|
amount: 10,
|
||||||
|
transaction_text: `Lieferung ${delivery.number}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(row2.id, 2);
|
assert.equal(row2.id, 2);
|
||||||
assert.equal(row2.hash_version, 1);
|
assert.equal(row2.manifestarticle_id, manifestarticleId2);
|
||||||
|
assert.equal(row2.warehouse_id, null);
|
||||||
|
assert.equal(row2.transaction_type, 'ACCEPTANCE');
|
||||||
|
assert.equal(row2.amount, 10);
|
||||||
assert.equal(row2.prev_hash, expectedHash2);
|
assert.equal(row2.prev_hash, expectedHash2);
|
||||||
assert(row2.created_at, 'row 2 created_at should exist');
|
|
||||||
console.log('✓ Row 2 prev_hash matches SHA-512 of Row 1 with created_at');
|
|
||||||
|
|
||||||
// 4. Test third row (line 2, same SKU SKU-ABC)
|
// Query ledger lines for manifestarticle 2 to verify row 3 (WAREHOUSE entry)
|
||||||
console.log('3. Inserting third ledger row on line 2...');
|
const line2Entries = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId2);
|
||||||
const expectedHash3 = computePrevHash(row2);
|
assert.equal(line2Entries.length, 2, 'Line 2 must have 2 ledger entries (ACCEPTANCE + WAREHOUSE)');
|
||||||
const row3 = await store.ledger.add(googleAuthId, {
|
assert.equal(line2Entries[0].transaction_type, 'ACCEPTANCE');
|
||||||
manifestarticle_id: manifestarticleId2,
|
assert.equal(line2Entries[0].warehouse_id, null);
|
||||||
transaction_type: 'GOODS_RECEIPT',
|
assert.equal(line2Entries[1].transaction_type, 'WAREHOUSE');
|
||||||
amount: 10,
|
assert.equal(line2Entries[1].warehouse_id, wh1.id);
|
||||||
transaction_text: 'Second line goods receipt',
|
assert.equal(line2Entries[1].amount, 10);
|
||||||
});
|
assert.equal(line2Entries[1].id, 3);
|
||||||
|
assert.equal(line2Entries[1].prev_hash, computePrevHash(line2Entries[0]));
|
||||||
|
|
||||||
assert.equal(row3.id, 3);
|
// 5. Check warehouse stock for article
|
||||||
assert.equal(row3.hash_version, 1);
|
console.log('4. Checking warehouse stock calculation...');
|
||||||
assert.equal(row3.prev_hash, expectedHash3);
|
const artDetail = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
|
const whStock = artDetail.article.warehouses.find((w) => w.id === wh1.id)?.stock;
|
||||||
|
assert.equal(whStock, 10, 'Warehouse stock must be 10 (from line 2 WAREHOUSE ledger entry only)');
|
||||||
|
|
||||||
// 5. Test query by manifestarticle_id
|
// 6. Check deliverysummary is_booked
|
||||||
console.log('4. Testing listByManifestarticleId...');
|
const updatedDelivery = await store.delivery.getById(googleAuthId, delivery.id);
|
||||||
const listLine1All = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId1);
|
assert.equal(updatedDelivery.ledger.isBooked, true, 'Delivery should be fully booked');
|
||||||
assert.equal(listLine1All.length, 2);
|
assert.equal(updatedDelivery.ledger.completelyOnLedgerPositions, 2);
|
||||||
assert.equal(listLine1All[0].id, 1);
|
|
||||||
assert.equal(listLine1All[1].id, 2);
|
|
||||||
assert.equal(listLine1All[0].sku, 'SKU-ABC');
|
|
||||||
|
|
||||||
const listLine1GoodsReceipt = await store.ledger.listByManifestarticleId(
|
// 7. Check article history
|
||||||
googleAuthId,
|
console.log('5. Verifying article history...');
|
||||||
manifestarticleId1,
|
|
||||||
'GOODS_RECEIPT',
|
|
||||||
);
|
|
||||||
assert.equal(listLine1GoodsReceipt.length, 1);
|
|
||||||
assert.equal(listLine1GoodsReceipt[0].id, 1);
|
|
||||||
|
|
||||||
const listLine1Adjust = await store.ledger.listByManifestarticleId(
|
|
||||||
googleAuthId,
|
|
||||||
manifestarticleId1,
|
|
||||||
{ transaction_type: 'INVENTORY_ADJUST' },
|
|
||||||
);
|
|
||||||
assert.equal(listLine1Adjust.length, 1);
|
|
||||||
assert.equal(listLine1Adjust[0].id, 2);
|
|
||||||
|
|
||||||
const listLine1None = await store.ledger.listByManifestarticleId(
|
|
||||||
googleAuthId,
|
|
||||||
manifestarticleId1,
|
|
||||||
'NON_EXISTENT',
|
|
||||||
);
|
|
||||||
assert.equal(listLine1None.length, 0);
|
|
||||||
|
|
||||||
// 6. Test query by SKU
|
|
||||||
console.log('5. Testing listBySku...');
|
|
||||||
const listSkuAll = await store.ledger.listBySku(googleAuthId, 'SKU-ABC');
|
|
||||||
assert.equal(listSkuAll.length, 3);
|
|
||||||
assert.equal(listSkuAll[0].id, 1);
|
|
||||||
assert.equal(listSkuAll[1].id, 2);
|
|
||||||
assert.equal(listSkuAll[2].id, 3);
|
|
||||||
|
|
||||||
const listSkuGoodsReceipt = await store.ledger.listBySku(
|
|
||||||
googleAuthId,
|
|
||||||
'SKU-ABC',
|
|
||||||
'GOODS_RECEIPT',
|
|
||||||
);
|
|
||||||
assert.equal(listSkuGoodsReceipt.length, 2);
|
|
||||||
assert.equal(listSkuGoodsReceipt[0].id, 1);
|
|
||||||
assert.equal(listSkuGoodsReceipt[1].id, 3);
|
|
||||||
|
|
||||||
const listSkuAdjust = await store.ledger.listBySku(
|
|
||||||
googleAuthId,
|
|
||||||
'SKU-ABC',
|
|
||||||
'INVENTORY_ADJUST',
|
|
||||||
);
|
|
||||||
assert.equal(listSkuAdjust.length, 1);
|
|
||||||
assert.equal(listSkuAdjust[0].id, 2);
|
|
||||||
|
|
||||||
const listUnknownSku = await store.ledger.listBySku(googleAuthId, 'UNKNOWN-SKU');
|
|
||||||
assert.equal(listUnknownSku.length, 0);
|
|
||||||
|
|
||||||
// 8. Test article history created from ledger
|
|
||||||
console.log('7. Testing article history entries from ledger rows...');
|
|
||||||
const history = await store.article.getHistoryBySku(googleAuthId, article.sku);
|
const history = await store.article.getHistoryBySku(googleAuthId, article.sku);
|
||||||
const historyTexts = history.map((h) => h.text);
|
const historyTexts = history.map((h) => h.text);
|
||||||
console.log('Article history texts:', historyTexts);
|
console.log('History entries:', historyTexts);
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
historyTexts.some((t) => t.includes('Initial delivery movement') && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 11.90') && t.includes('Menge: 5')),
|
historyTexts.some((t) => t.includes(`Lieferung ${delivery.number}`) && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 11.90') && t.includes('Menge: 5')),
|
||||||
'History should include initial delivery movement with supplier, EK brutto, and amount',
|
'History must include acceptance of line 1 with Lieferung number, supplier, EK brutto, amount',
|
||||||
);
|
);
|
||||||
assert(historyTexts.includes('Damaged item written off'), 'History should include undamaged adjustment memo as is');
|
|
||||||
assert(
|
assert(
|
||||||
historyTexts.some((t) => t.includes('Second line goods receipt') && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 23.80') && t.includes('Menge: 10')),
|
historyTexts.some((t) => t.includes(`Lieferung ${delivery.number}`) && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 23.80') && t.includes('Menge: 10')),
|
||||||
'History should include second line goods receipt with details',
|
'History must include acceptance of line 2',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
historyTexts.some((t) => t.includes(`Lieferung ${delivery.number}`) && t.includes('Lager: "Hauptlager"') && t.includes('Menge: 10')),
|
||||||
|
'History must include warehouse entry for tracked line 2',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 9. Test warehouse CRUD, sort, reorder, and linking with ledger
|
// 8. Test store.delivery.book on a fresh delivery
|
||||||
console.log('8. Testing warehouse CRUD, sort, reorder, and ledger association...');
|
console.log('6. Testing store.delivery.book batch booking...');
|
||||||
const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
const delivery2 = await store.delivery.create(googleAuthId, {
|
||||||
assert.equal(wh1.name, 'Hauptlager');
|
supplierId: supplier.id,
|
||||||
assert.equal(wh1.sort, 0);
|
externalNumber: 'DEL-2',
|
||||||
assert(wh1.id > 0);
|
date: new Date().toISOString(),
|
||||||
|
lines: [
|
||||||
const wh2 = await store.warehouse.create(googleAuthId, { name: 'Filiale Nord' });
|
{ sku: 'SKU-ABC', name: 'Batch Item 1', pu: 1, amount: 4, net: 15, taxrate: 19 },
|
||||||
assert.equal(wh2.name, 'Filiale Nord');
|
],
|
||||||
assert.equal(wh2.sort, 1);
|
|
||||||
|
|
||||||
const whList = await store.warehouse.list(googleAuthId);
|
|
||||||
assert.equal(whList.length, 2);
|
|
||||||
assert.equal(whList[0].id, wh1.id);
|
|
||||||
assert.equal(whList[0].sort, 0);
|
|
||||||
assert.equal(whList[1].id, wh2.id);
|
|
||||||
assert.equal(whList[1].sort, 1);
|
|
||||||
|
|
||||||
const whGet = await store.warehouse.getById(googleAuthId, wh1.id);
|
|
||||||
assert.equal(whGet.name, 'Hauptlager');
|
|
||||||
assert.equal(whGet.sort, 0);
|
|
||||||
|
|
||||||
const whUpdated = await store.warehouse.update(googleAuthId, wh2.id, { name: 'Filiale Süd', sort: 5 });
|
|
||||||
assert.equal(whUpdated.name, 'Filiale Süd');
|
|
||||||
assert.equal(whUpdated.sort, 5);
|
|
||||||
|
|
||||||
// Test warehouse order
|
|
||||||
await store.warehouse.order(googleAuthId, { warehouseIds: [wh2.id, wh1.id] });
|
|
||||||
const whListReordered = await store.warehouse.list(googleAuthId);
|
|
||||||
assert.equal(whListReordered[0].id, wh2.id);
|
|
||||||
assert.equal(whListReordered[0].sort, 0);
|
|
||||||
assert.equal(whListReordered[1].id, wh1.id);
|
|
||||||
assert.equal(whListReordered[1].sort, 1);
|
|
||||||
|
|
||||||
// Test invalid warehouse order
|
|
||||||
await assert.rejects(
|
|
||||||
async () => store.warehouse.order(googleAuthId, { warehouseIds: [wh1.id] }),
|
|
||||||
/Invalid warehouse order/,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify article detail includes warehouses with sort
|
|
||||||
const articleDetail = await store.article.getBySku(googleAuthId, article.sku);
|
|
||||||
assert(Array.isArray(articleDetail.article.warehouses), 'Article should have warehouses array');
|
|
||||||
assert.equal(articleDetail.article.warehouses.length, 2);
|
|
||||||
assert.equal(articleDetail.article.warehouses[0].id, wh2.id);
|
|
||||||
assert.equal(articleDetail.article.warehouses[0].sort, 0);
|
|
||||||
assert.equal(articleDetail.article.warehouses[1].id, wh1.id);
|
|
||||||
assert.equal(articleDetail.article.warehouses[1].sort, 1);
|
|
||||||
|
|
||||||
// Add ledger row with warehouse
|
|
||||||
const rowWithWh = await store.ledger.add(googleAuthId, {
|
|
||||||
manifestarticle_id: manifestarticleId1,
|
|
||||||
warehouse_id: wh1.id,
|
|
||||||
transaction_type: 'DELIVERY',
|
|
||||||
amount: 25,
|
|
||||||
transaction_text: 'Delivered directly to Hauptlager',
|
|
||||||
});
|
});
|
||||||
assert.equal(rowWithWh.warehouse_id, wh1.id);
|
const bookedDelivery2 = await store.delivery.book(googleAuthId, { id: delivery2.id, warehouseId: wh1.id });
|
||||||
assert.equal(rowWithWh.amount, 25);
|
assert.equal(bookedDelivery2.ledger.isBooked, true);
|
||||||
|
assert.equal(bookedDelivery2.ledger.completelyOnLedgerPositions, 1);
|
||||||
|
|
||||||
// Filter by warehouse in listBySku and listByManifestarticleId
|
// Double booking delivery2 must reject
|
||||||
const whRowsBySku = await store.ledger.listBySku(googleAuthId, 'SKU-ABC', { warehouse_id: wh1.id });
|
|
||||||
assert.equal(whRowsBySku.length, 1);
|
|
||||||
assert.equal(whRowsBySku[0].id, rowWithWh.id);
|
|
||||||
assert.equal(whRowsBySku[0].warehouse_id, wh1.id);
|
|
||||||
|
|
||||||
const whRowsByLine = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId1, { warehouse_id: wh1.id });
|
|
||||||
assert.equal(whRowsByLine.length, 1);
|
|
||||||
assert.equal(whRowsByLine[0].id, rowWithWh.id);
|
|
||||||
|
|
||||||
// Verify warehouse is recorded in article history
|
|
||||||
const historyWithWh = await store.article.getHistoryBySku(googleAuthId, article.sku);
|
|
||||||
assert(
|
|
||||||
historyWithWh.some((h) => h.text.includes('Lager: "Hauptlager"') && h.text.includes('Lieferant: "Supplier X"')),
|
|
||||||
'History should include warehouse name (Lager: "Hauptlager")',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Trying to delete wh1 (used in ledger) must fail
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
async () => store.warehouse.delete(googleAuthId, wh1.id),
|
async () => store.delivery.book(googleAuthId, { id: delivery2.id }),
|
||||||
/Warehouse in use in ledger/,
|
/Delivery line(s)? already (accepted|booked) on ledger/,
|
||||||
'Should reject deleting warehouse referenced in ledger',
|
'Should reject booking already booked delivery',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Deleting wh2 (not used in ledger) must succeed
|
// 9. Test shipment booking
|
||||||
await store.warehouse.delete(googleAuthId, wh2.id);
|
console.log('7. Testing shipment booking (SHIPMENT + WAREHOUSE dual entries)...');
|
||||||
const whListAfterDelete = await store.warehouse.list(googleAuthId);
|
const customer = await store.entity.create(googleAuthId, {
|
||||||
assert.equal(whListAfterDelete.length, 1);
|
name: 'Customer Y',
|
||||||
assert.equal(whListAfterDelete[0].id, wh1.id);
|
isCustomer: true,
|
||||||
console.log('✓ Warehouse CRUD and ledger enforcement passed');
|
customergroupId: company.rootCategoryId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const order2 = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-2',
|
||||||
|
entityId: customer.id,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
lines: [
|
||||||
|
{ sku: 'SKU-ABC', name: 'Ordered Item', pu: 1, amount: 3, net: 25, taxrate: 19 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const testCarrier = await store.carrier.create(googleAuthId, { name: 'Test Carrier', hasPackages: true });
|
||||||
|
const testShipping = { carrierId: testCarrier.id, type: 'parcel', state: 'draft' };
|
||||||
|
|
||||||
|
const shipment = await store.shipment.create(googleAuthId, {
|
||||||
|
orderId: order2.id,
|
||||||
|
shipping: testShipping,
|
||||||
|
lines: [{ sku: 'SKU-ABC', amount: 3 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const shipmentLineId = shipment.lines[0].id;
|
||||||
|
const shipRow = await store.ledger.add(googleAuthId, {
|
||||||
|
manifestarticle_id: shipmentLineId,
|
||||||
|
warehouse_id: wh1.id,
|
||||||
|
transaction_type: 'SHIPMENT',
|
||||||
|
amount: 3,
|
||||||
|
transaction_text: `Lieferschein ${shipment.number}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(shipRow.transaction_type, 'SHIPMENT');
|
||||||
|
assert.equal(shipRow.warehouse_id, null);
|
||||||
|
assert.equal(shipRow.amount, -3);
|
||||||
|
|
||||||
|
const shipEntries = await store.ledger.listByManifestarticleId(googleAuthId, shipmentLineId);
|
||||||
|
assert.equal(shipEntries.length, 2, 'Shipment line must have 2 entries (SHIPMENT + WAREHOUSE)');
|
||||||
|
assert.equal(shipEntries[0].transaction_type, 'SHIPMENT');
|
||||||
|
assert.equal(shipEntries[0].warehouse_id, null);
|
||||||
|
assert.equal(shipEntries[0].amount, -3);
|
||||||
|
assert.equal(shipEntries[1].transaction_type, 'WAREHOUSE');
|
||||||
|
assert.equal(shipEntries[1].warehouse_id, wh1.id);
|
||||||
|
assert.equal(shipEntries[1].amount, -3);
|
||||||
|
|
||||||
|
// Stock should now be: 10 (line 2) + 4 (delivery2) - 3 (shipment) = 11
|
||||||
|
const artDetailAfterShip = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
|
const whStockAfterShip = artDetailAfterShip.article.warehouses.find((w) => w.id === wh1.id)?.stock;
|
||||||
|
assert.equal(whStockAfterShip, 11, 'Warehouse stock must be 11 (10 + 4 - 3)');
|
||||||
|
|
||||||
|
// Verify shipment history
|
||||||
|
const historyAfterShip = await store.article.getHistoryBySku(googleAuthId, article.sku);
|
||||||
|
const shipHistoryTexts = historyAfterShip.map((h) => h.text);
|
||||||
|
assert(
|
||||||
|
shipHistoryTexts.some((t) => t.includes(`Lieferschein ${shipment.number}`) && t.includes('Menge: 3')),
|
||||||
|
'Article history should record outgoing shipment',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 8. Test store.stock.accept with deliverymanifestitem
|
||||||
|
console.log('8. Testing store.stock.accept (positive acceptance & negative return)...');
|
||||||
|
const delivery3 = await store.delivery.create(googleAuthId, {
|
||||||
|
supplierId: supplier.id,
|
||||||
|
externalNumber: 'DEL-3',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
lines: [
|
||||||
|
{ sku: 'SKU-ABC', name: 'Positive Delivery Line', pu: 1, amount: 6, net: 10, taxrate: 19 },
|
||||||
|
{ sku: 'SKU-ABC', name: 'Negative Delivery Line (Return)', pu: 1, amount: -2, net: 10, taxrate: 19 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const posDeliveryLineId = delivery3.lines[0].id;
|
||||||
|
const negDeliveryLineId = delivery3.lines[1].id;
|
||||||
|
|
||||||
|
const acceptPosResult = await store.stock.accept(googleAuthId, {
|
||||||
|
manifestarticleId: posDeliveryLineId,
|
||||||
|
warehouseId: wh1.id,
|
||||||
|
});
|
||||||
|
assert.equal(acceptPosResult.document_ledger.transaction_type, 'ACCEPTANCE');
|
||||||
|
assert.equal(acceptPosResult.document_ledger.amount, 6);
|
||||||
|
assert.equal(acceptPosResult.document_ledger.warehouse_id, null);
|
||||||
|
assert.equal(acceptPosResult.adjust_ledger.amount, 6);
|
||||||
|
assert.equal(acceptPosResult.adjust_ledger.warehouse_id, wh1.id);
|
||||||
|
|
||||||
|
// Negative delivery line: return of a delivery (outgoing)
|
||||||
|
const acceptNegResult = await store.stock.accept(googleAuthId, {
|
||||||
|
manifestarticleId: negDeliveryLineId,
|
||||||
|
warehouseId: wh1.id,
|
||||||
|
});
|
||||||
|
assert.equal(acceptNegResult.document_ledger.transaction_type, 'RETURN');
|
||||||
|
assert.equal(acceptNegResult.document_ledger.amount, -2);
|
||||||
|
assert.equal(acceptNegResult.document_ledger.warehouse_id, null);
|
||||||
|
assert.equal(acceptNegResult.adjust_ledger.amount, -2);
|
||||||
|
assert.equal(acceptNegResult.adjust_ledger.warehouse_id, wh1.id);
|
||||||
|
|
||||||
|
// 9. Test store.stock.ship with shipmentmanifestitem
|
||||||
|
console.log('9. Testing store.stock.ship (positive shipment & rejection of negative line)...');
|
||||||
|
const order3 = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-3',
|
||||||
|
entityId: customer.id,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
lines: [
|
||||||
|
{ sku: 'SKU-ABC', name: 'Shipment Item Normal', pu: 1, amount: 5, net: 10, taxrate: 19 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const shipment3 = await store.shipment.create(googleAuthId, {
|
||||||
|
orderId: order3.id,
|
||||||
|
shipping: testShipping,
|
||||||
|
lines: [
|
||||||
|
{ sku: 'SKU-ABC', amount: 4 },
|
||||||
|
{ sku: 'SKU-ABC', amount: -1 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const posShipLineId = shipment3.lines[0].id;
|
||||||
|
const negShipLineId = shipment3.lines[1].id;
|
||||||
|
|
||||||
|
const shipPosResult = await store.stock.ship(googleAuthId, {
|
||||||
|
manifestarticleId: posShipLineId,
|
||||||
|
warehouseId: wh1.id,
|
||||||
|
});
|
||||||
|
assert.equal(shipPosResult.document_ledger.transaction_type, 'SHIPMENT');
|
||||||
|
assert.equal(shipPosResult.document_ledger.amount, -4);
|
||||||
|
assert.equal(shipPosResult.adjust_ledger.amount, -4);
|
||||||
|
|
||||||
|
// Negative shipment line: shipping is rejected
|
||||||
|
await assert.rejects(
|
||||||
|
store.stock.ship(googleAuthId, {
|
||||||
|
manifestarticleId: negShipLineId,
|
||||||
|
warehouseId: wh1.id,
|
||||||
|
}),
|
||||||
|
/Amount must be positive/,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 10. Test store.stock.adjust
|
||||||
|
console.log('10. Testing store.stock.adjust failure on untracked article...');
|
||||||
|
const untrackedArt = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Pure Untracked Article',
|
||||||
|
track_stock: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.stock.adjust(googleAuthId, {
|
||||||
|
sku: untrackedArt.sku,
|
||||||
|
warehouseId: wh1.id,
|
||||||
|
amount: 5,
|
||||||
|
}),
|
||||||
|
/Stock tracking is disabled for article/,
|
||||||
|
'Adjust must fail when article has no stock tracking',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 10b. Test prevention of negative stock on shipment creation and stock adjust
|
||||||
|
console.log('10b. Testing prevention of negative stock on shipment creation and adjust...');
|
||||||
|
const guardOrder = await store.order.create(googleAuthId, {
|
||||||
|
number: 'ORD-GUARD-1',
|
||||||
|
date: '2026-10-20',
|
||||||
|
entityId: customer.id,
|
||||||
|
lines: [{ sku: article.sku, name: 'Order Line', pu: 1, amount: 9999, net: 10, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentArt = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
|
const curStock = currentArt.article.warehouses.find((w) => w.id === wh1.id)?.stock ?? 0;
|
||||||
|
|
||||||
|
// Explicit shipment requesting more than available stock must be rejected
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.shipment.create(googleAuthId, {
|
||||||
|
orderId: guardOrder.id,
|
||||||
|
shipping: testShipping,
|
||||||
|
warehouseId: wh1.id,
|
||||||
|
lines: [{ sku: article.sku, amount: curStock + 10 }],
|
||||||
|
}),
|
||||||
|
/Insufficient stock for article/,
|
||||||
|
'Shipment creation must fail when requested amount exceeds warehouse stock',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Manual stock adjust attempting to deduct more than current stock must be rejected
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.stock.adjust(googleAuthId, {
|
||||||
|
sku: article.sku,
|
||||||
|
warehouseId: wh1.id,
|
||||||
|
amount: -(curStock + 10),
|
||||||
|
}),
|
||||||
|
/Insufficient stock for article/,
|
||||||
|
'Stock adjust must fail when deduction exceeds available stock',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 11. Test before stock tracking is disabled, adjust to 0 has to be ledgerized
|
||||||
|
console.log('11. Testing zero-adjustment before stock tracking is disabled...');
|
||||||
|
const artBeforeDisable = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
|
const stockBeforeDisable = artBeforeDisable.article.warehouses.find((w) => w.id === wh1.id)?.stock;
|
||||||
|
console.log('Current stock before disabling tracking:', stockBeforeDisable);
|
||||||
|
assert(stockBeforeDisable > 0, 'Article must have non-zero stock before disabling');
|
||||||
|
|
||||||
|
// Disable stock tracking
|
||||||
|
await store.article.setTrackStockBySku(googleAuthId, article.sku, false);
|
||||||
|
|
||||||
|
const artAfterDisable = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
|
assert.equal(artAfterDisable.article.track_stock, false, 'Article track_stock must now be false');
|
||||||
|
const stockAfterDisable = artAfterDisable.article.warehouses.find((w) => w.id === wh1.id)?.stock;
|
||||||
|
assert.equal(stockAfterDisable, 0, 'Warehouse stock must be adjusted to 0 on the ledger');
|
||||||
|
|
||||||
|
// Calling adjust now must fail
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.stock.adjust(googleAuthId, {
|
||||||
|
sku: article.sku,
|
||||||
|
warehouseId: wh1.id,
|
||||||
|
amount: 1,
|
||||||
|
}),
|
||||||
|
/Stock tracking is disabled for article/,
|
||||||
|
'Subsequent adjust must fail once tracking is disabled',
|
||||||
|
);
|
||||||
|
|
||||||
console.log('✓ All ledger tests passed successfully!');
|
console.log('✓ All ledger tests passed successfully!');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ async function testProvider(name, provider) {
|
|||||||
});
|
});
|
||||||
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
const supplier = await store.supplier.create(googleAuthId, {
|
||||||
|
name: 'Test Supplier',
|
||||||
|
});
|
||||||
|
const groups = await store.customergroup.list(googleAuthId);
|
||||||
|
const customer = await store.entity.create(googleAuthId, {
|
||||||
|
name: 'Test Customer',
|
||||||
|
customergroupId: groups[0].id,
|
||||||
|
});
|
||||||
|
|
||||||
// 2. Test delivery with both dates
|
// 2. Test delivery with both dates
|
||||||
console.log('Testing delivery creation with date and createdAt...');
|
console.log('Testing delivery creation with date and createdAt...');
|
||||||
const deliveryDate = '2026-10-15T12:00:00.000Z';
|
const deliveryDate = '2026-10-15T12:00:00.000Z';
|
||||||
@@ -28,6 +37,7 @@ async function testProvider(name, provider) {
|
|||||||
|
|
||||||
const createdDelivery = await store.delivery.create(googleAuthId, {
|
const createdDelivery = await store.delivery.create(googleAuthId, {
|
||||||
externalNumber: 'DEL-2DATES-1',
|
externalNumber: 'DEL-2DATES-1',
|
||||||
|
supplierId: supplier.id,
|
||||||
date: deliveryDate,
|
date: deliveryDate,
|
||||||
createdAt: customCreatedAt,
|
createdAt: customCreatedAt,
|
||||||
lines: [
|
lines: [
|
||||||
@@ -98,6 +108,7 @@ async function testProvider(name, provider) {
|
|||||||
const order = await store.order.create(googleAuthId, {
|
const order = await store.order.create(googleAuthId, {
|
||||||
number: `ORD-${Date.now()}`,
|
number: `ORD-${Date.now()}`,
|
||||||
date: '2026-12-15T00:00:00.000Z',
|
date: '2026-12-15T00:00:00.000Z',
|
||||||
|
entityId: customer.id,
|
||||||
lines: [{ name: 'Order item', amount: 3, net: 25, taxrate: 19 }],
|
lines: [{ name: 'Order item', amount: 3, net: 25, taxrate: 19 }],
|
||||||
});
|
});
|
||||||
assert(order.createdAt, 'Order should have default createdAt populated');
|
assert(order.createdAt, 'Order should have default createdAt populated');
|
||||||
|
|||||||
94
scripts/test-mcp.js
Normal file
94
scripts/test-mcp.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import assert from 'node:assert';
|
||||||
|
import LibDashStore from '../src/index.js';
|
||||||
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Testing MCP store methods on in-memory SQLite...');
|
||||||
|
const provider = new BetterSqlite3Provider({ path: ':memory:' });
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
// 1. Create a google_auth user
|
||||||
|
const googleAuthId = await store.google_auth.add('mcp-tester@example.com');
|
||||||
|
assert(googleAuthId > 0, 'googleAuthId should be positive integer');
|
||||||
|
|
||||||
|
// 2. Register client
|
||||||
|
const client = await store.mcp.registerClient({
|
||||||
|
clientId: 'test-client-123',
|
||||||
|
clientSecret: 'secret-456',
|
||||||
|
clientName: 'Test Client',
|
||||||
|
redirectUris: ['http://localhost:3000/callback'],
|
||||||
|
grantTypes: ['authorization_code', 'refresh_token'],
|
||||||
|
responseTypes: ['code'],
|
||||||
|
scope: 'mcp:tools',
|
||||||
|
});
|
||||||
|
console.log('Registered client:', client);
|
||||||
|
assert.strictEqual(client.clientId, 'test-client-123');
|
||||||
|
assert.strictEqual(client.clientName, 'Test Client');
|
||||||
|
|
||||||
|
// 3. Get client
|
||||||
|
const fetched = await store.mcp.getClient('test-client-123');
|
||||||
|
assert(fetched, 'Should find client');
|
||||||
|
assert.strictEqual(fetched.clientId, 'test-client-123');
|
||||||
|
|
||||||
|
// 4. List clients
|
||||||
|
const clients = await store.mcp.listClients();
|
||||||
|
assert.strictEqual(clients.length, 1);
|
||||||
|
|
||||||
|
// 5. Create auth code
|
||||||
|
const code = 'auth-code-xyz';
|
||||||
|
await store.mcp.createAuthCode({
|
||||||
|
code,
|
||||||
|
clientId: 'test-client-123',
|
||||||
|
googleAuthId,
|
||||||
|
redirectUri: 'http://localhost:3000/callback',
|
||||||
|
codeChallenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
|
||||||
|
codeChallengeMethod: 'S256',
|
||||||
|
scope: 'mcp:tools',
|
||||||
|
expiresAt: new Date(Date.now() + 300_000),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Get auth code
|
||||||
|
const codeRow = await store.mcp.getAuthCode(code);
|
||||||
|
assert(codeRow, 'Should find auth code');
|
||||||
|
assert.strictEqual(codeRow.clientId, 'test-client-123');
|
||||||
|
assert.strictEqual(codeRow.used, 0);
|
||||||
|
|
||||||
|
// 7. Consume auth code
|
||||||
|
await store.mcp.consumeAuthCode(code);
|
||||||
|
const consumedRow = await store.mcp.getAuthCode(code);
|
||||||
|
assert.strictEqual(consumedRow.used, 1);
|
||||||
|
|
||||||
|
// 8. Create refresh token
|
||||||
|
const refreshToken = 'refresh-token-abc';
|
||||||
|
await store.mcp.createRefreshToken({
|
||||||
|
refreshToken,
|
||||||
|
clientId: 'test-client-123',
|
||||||
|
googleAuthId,
|
||||||
|
scope: 'mcp:tools',
|
||||||
|
expiresAt: new Date(Date.now() + 86400_000),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 9. Get refresh token
|
||||||
|
const tokenRow = await store.mcp.getRefreshToken(refreshToken);
|
||||||
|
assert(tokenRow, 'Should find refresh token');
|
||||||
|
assert.strictEqual(tokenRow.revoked, 0);
|
||||||
|
|
||||||
|
// 10. Revoke refresh token
|
||||||
|
await store.mcp.revokeRefreshToken(refreshToken);
|
||||||
|
const revokedRow = await store.mcp.getRefreshToken(refreshToken);
|
||||||
|
assert.strictEqual(revokedRow.revoked, 1);
|
||||||
|
|
||||||
|
// 11. Delete client
|
||||||
|
await store.mcp.deleteClient('test-client-123');
|
||||||
|
const deleted = await store.mcp.getClient('test-client-123');
|
||||||
|
assert.strictEqual(deleted, null);
|
||||||
|
|
||||||
|
await provider.close();
|
||||||
|
console.log('✓ All MCP store tests passed successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
117
scripts/test-modelconversation.js
Normal file
117
scripts/test-modelconversation.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
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 store.modelconversation / store.modelmessage ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-modelconv-'));
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: join(tempDir, 'test.db'),
|
||||||
|
storageDir: join(tempDir, 'storage'),
|
||||||
|
});
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userA = await store.google_auth.add('mc-a@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'MC Corp', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId: userA,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(userA, company.id);
|
||||||
|
|
||||||
|
const conv = await store.modelconversation.create(userA, {
|
||||||
|
provider: 'openrouter', model: 'test/model',
|
||||||
|
});
|
||||||
|
assert.equal(conv.state, 'idle');
|
||||||
|
assert.equal(conv.provider, 'openrouter');
|
||||||
|
await assert.rejects(store.modelconversation.create(userA, { model: 'x' }), /Provider required/);
|
||||||
|
await assert.rejects(store.modelconversation.create(userA, { provider: 'x' }), /Model required/);
|
||||||
|
|
||||||
|
const list = await store.modelconversation.list(userA);
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list[0].first_user_message, null);
|
||||||
|
|
||||||
|
assert.deepEqual(await store.modelmessage.list(userA, conv.id), []);
|
||||||
|
|
||||||
|
const m1 = await store.modelmessage.add(userA, conv.id, { message: 'hello' });
|
||||||
|
assert.equal(m1.role, 'user');
|
||||||
|
assert.equal(m1.conversation_id, conv.id);
|
||||||
|
await assert.rejects(store.modelmessage.add(userA, conv.id, { message: ' ' }), /Message required/);
|
||||||
|
await assert.rejects(store.modelmessage.add(userA, 9999, { message: 'x' }), /Conversation not found/);
|
||||||
|
|
||||||
|
// Non-idle conversation refuses user messages but allows assistant messages
|
||||||
|
const busyConv = await store.modelconversation.setState(userA, conv.id, 'busy');
|
||||||
|
assert.equal(busyConv.state, 'busy');
|
||||||
|
const fetchedBusy = await store.modelconversation.get(userA, conv.id);
|
||||||
|
assert.equal(fetchedBusy.state, 'busy');
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
store.modelmessage.add(userA, conv.id, { message: 'again' }),
|
||||||
|
/Conversation is not idle/,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assistant message can be added while busy
|
||||||
|
const mAssistant = await store.modelmessage.add(userA, conv.id, { message: 'echo: hello', role: 'assistant' });
|
||||||
|
assert.equal(mAssistant.role, 'assistant');
|
||||||
|
assert.equal(mAssistant.message, 'echo: hello');
|
||||||
|
|
||||||
|
// Back to idle
|
||||||
|
await store.modelconversation.setState(userA, conv.id, 'idle');
|
||||||
|
await store.modelmessage.add(userA, conv.id, { message: 'second' });
|
||||||
|
|
||||||
|
const msgs = await store.modelmessage.list(userA, conv.id);
|
||||||
|
assert.deepEqual(msgs.map((m) => m.message), ['hello', 'echo: hello', 'second']);
|
||||||
|
assert.deepEqual(msgs.map((m) => m.role), ['user', 'assistant', 'user']);
|
||||||
|
assert.equal((await store.modelconversation.list(userA)).find((c) => c.id === conv.id).first_user_message, 'hello');
|
||||||
|
assert.equal((await store.modelconversation.get(userA, conv.id)).first_user_message, 'hello');
|
||||||
|
|
||||||
|
// Other user cannot see or write
|
||||||
|
const userB = await store.google_auth.add('mc-b@example.com');
|
||||||
|
await store.companyuser.add(company.id, userB);
|
||||||
|
await store.google_auth.setSelectedCompany(userB, company.id);
|
||||||
|
assert.equal((await store.modelconversation.list(userB)).length, 0);
|
||||||
|
await assert.rejects(store.modelmessage.list(userB, conv.id), /Conversation not found/);
|
||||||
|
await assert.rejects(store.modelmessage.add(userB, conv.id, { message: 'x' }), /Conversation not found/);
|
||||||
|
|
||||||
|
// started_by: top-level is null, subagents link to their originator, list exposes it
|
||||||
|
assert.equal(conv.started_by, null);
|
||||||
|
const sub1 = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: conv.id });
|
||||||
|
assert.equal(sub1.started_by, conv.id);
|
||||||
|
const sub2 = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: sub1.id });
|
||||||
|
const sibling = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm' });
|
||||||
|
await assert.rejects(
|
||||||
|
store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: 0 }),
|
||||||
|
/Invalid started_by/,
|
||||||
|
);
|
||||||
|
const listed = await store.modelconversation.list(userA);
|
||||||
|
assert.equal(listed.find((c) => c.id === sub2.id).started_by, sub1.id);
|
||||||
|
assert.equal((await store.modelconversation.get(userA, sub1.id)).started_by, conv.id);
|
||||||
|
await store.modelmessage.add(userA, sub2.id, { message: 'deep' });
|
||||||
|
|
||||||
|
// Deleting a child removes only its own chain; deleting the root removes the whole chain
|
||||||
|
await store.modelconversation.delete(userA, sub2.id);
|
||||||
|
await assert.rejects(store.modelconversation.get(userA, sub2.id), /Conversation not found/);
|
||||||
|
await store.modelconversation.get(userA, sub1.id);
|
||||||
|
const sub3 = await store.modelconversation.create(userA, { provider: 'openrouter', model: 'm', started_by: sub1.id });
|
||||||
|
await store.modelconversation.delete(userA, conv.id);
|
||||||
|
for (const gone of [conv.id, sub1.id, sub3.id]) {
|
||||||
|
await assert.rejects(store.modelconversation.get(userA, gone), /Conversation not found/);
|
||||||
|
}
|
||||||
|
await store.modelconversation.get(userA, sibling.id);
|
||||||
|
await assert.rejects(store.modelconversation.delete(userA, conv.id), /Conversation not found/);
|
||||||
|
|
||||||
|
console.log('PASS');
|
||||||
|
} finally {
|
||||||
|
await provider.close?.();
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('FAIL', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
295
scripts/test-obituary.js
Normal file
295
scripts/test-obituary.js
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
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 posterminalobituary table and deletion flow ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-obituary-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Verify posterminalobituary table exists and has expected columns
|
||||||
|
console.log('1. Checking posterminalobituary schema...');
|
||||||
|
const tableInfo = provider.db.pragma('table_info("posterminalobituary")');
|
||||||
|
const colNames = tableInfo.map((c) => c.name);
|
||||||
|
console.log('posterminalobituary columns:', colNames);
|
||||||
|
assert.deepEqual(colNames, [
|
||||||
|
'id',
|
||||||
|
'posterminal_id',
|
||||||
|
'entity_type',
|
||||||
|
'entity_id',
|
||||||
|
'created_at',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Setup company first
|
||||||
|
console.log('2. Setting up base data...');
|
||||||
|
const googleAuthId = await store.google_auth.add('obituary-tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Obituary Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// Setup active paired POS terminal
|
||||||
|
const terminal = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'POS Terminal 1',
|
||||||
|
pairingpin: '1234',
|
||||||
|
});
|
||||||
|
await store.posterminal.pair(terminal.id, {
|
||||||
|
token: 'pos-token-123',
|
||||||
|
clientserial: 'SN-001',
|
||||||
|
pairedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Test article deletion flow
|
||||||
|
console.log('3. Testing article deletion obituary...');
|
||||||
|
const article = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Coffee Mug',
|
||||||
|
});
|
||||||
|
await store.article.delete(googleAuthId, article.sku);
|
||||||
|
|
||||||
|
// 4. Test category deletion flow
|
||||||
|
console.log('4. Testing category deletion obituary...');
|
||||||
|
const cat = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Snacks',
|
||||||
|
});
|
||||||
|
await store.category.delete({ googleAuthId, id: cat.id });
|
||||||
|
|
||||||
|
// 5. Test customer deletion flow
|
||||||
|
console.log('5. Testing customer deletion obituary...');
|
||||||
|
const customer = await store.entity.create(googleAuthId, {
|
||||||
|
name: 'Mustermann',
|
||||||
|
firstName: 'Max',
|
||||||
|
customergroupId: company.posCustomerGroupId,
|
||||||
|
});
|
||||||
|
await store.entity.delete(googleAuthId, customer.id);
|
||||||
|
|
||||||
|
// 6. Test customer group deletion flow
|
||||||
|
console.log('6. Testing customer group deletion obituary...');
|
||||||
|
const cg = await store.customergroup.create(googleAuthId, {
|
||||||
|
name: 'VIP Guests',
|
||||||
|
});
|
||||||
|
await store.customergroup.delete(googleAuthId, cg.id);
|
||||||
|
|
||||||
|
// 7. Verify all obituaries exist in posterminalobituary
|
||||||
|
console.log('7. Verifying obituaries in database...');
|
||||||
|
const allObituaries = provider.db
|
||||||
|
.prepare('SELECT * FROM posterminalobituary WHERE posterminal_id = ? ORDER BY id ASC')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Created obituaries:', allObituaries);
|
||||||
|
assert.equal(allObituaries.length, 4);
|
||||||
|
|
||||||
|
assert.equal(allObituaries[0].entity_type, 'article');
|
||||||
|
assert.equal(allObituaries[0].entity_id, article.id);
|
||||||
|
|
||||||
|
assert.equal(allObituaries[1].entity_type, 'category');
|
||||||
|
assert.equal(allObituaries[1].entity_id, cat.id);
|
||||||
|
|
||||||
|
assert.equal(allObituaries[2].entity_type, 'customer');
|
||||||
|
assert.equal(allObituaries[2].entity_id, customer.id);
|
||||||
|
|
||||||
|
assert.equal(allObituaries[3].entity_type, 'customergroup');
|
||||||
|
assert.equal(allObituaries[3].entity_id, cg.id);
|
||||||
|
|
||||||
|
// 8. Test getDeletedEntityCount & getDeletedEntityList APIs
|
||||||
|
console.log('8. Testing posterminal.getDeletedEntityCount & getDeletedEntityList...');
|
||||||
|
const initialCount = await store.posterminal.getDeletedEntityCount({
|
||||||
|
posterminalId: terminal.id,
|
||||||
|
cursor: 0,
|
||||||
|
});
|
||||||
|
assert.equal(initialCount, 4);
|
||||||
|
|
||||||
|
const list = await store.posterminal.getDeletedEntityList({
|
||||||
|
posterminalId: terminal.id,
|
||||||
|
cursor: 0,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
assert.equal(list.length, 4);
|
||||||
|
assert.equal(list[0].entityType, 'article');
|
||||||
|
assert.equal(list[0].entityId, article.id);
|
||||||
|
assert.equal(list[1].entityType, 'category');
|
||||||
|
assert.equal(list[2].entityType, 'customer');
|
||||||
|
assert.equal(list[3].entityType, 'customergroup');
|
||||||
|
|
||||||
|
// Test cursor pagination
|
||||||
|
const cursor = allObituaries[1].id;
|
||||||
|
const countAfterCursor = await store.posterminal.getDeletedEntityCount({
|
||||||
|
posterminalId: terminal.id,
|
||||||
|
cursor,
|
||||||
|
});
|
||||||
|
assert.equal(countAfterCursor, 2);
|
||||||
|
|
||||||
|
const listAfterCursor = await store.posterminal.getDeletedEntityList({
|
||||||
|
posterminalId: terminal.id,
|
||||||
|
cursor,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
assert.equal(listAfterCursor.length, 2);
|
||||||
|
assert.equal(listAfterCursor[0].id, allObituaries[2].id);
|
||||||
|
assert.equal(listAfterCursor[1].id, allObituaries[3].id);
|
||||||
|
|
||||||
|
// 9. Test maxknownobituaryid
|
||||||
|
console.log('9. Testing maxknownobituaryid...');
|
||||||
|
await store.posterminal.setMaxKnownObituaryId(terminal.id, allObituaries[3].id);
|
||||||
|
const updatedTerminal = await store.posterminal.findByToken('pos-token-123');
|
||||||
|
assert.equal(updatedTerminal.maxknownobituaryid, allObituaries[3].id);
|
||||||
|
|
||||||
|
// 10. Test setOff category & orphaned article obituary generation
|
||||||
|
console.log('10. Testing setOff with orphaned and non-orphaned articles...');
|
||||||
|
// Create Cat A and Cat B
|
||||||
|
const catA = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Category A',
|
||||||
|
});
|
||||||
|
const catB = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Category B',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Art A only in Cat A
|
||||||
|
const artA = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: catA.id,
|
||||||
|
name: 'Article A (only in Cat A)',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Art AB in both Cat A and Cat B
|
||||||
|
const artAB = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: catA.id,
|
||||||
|
name: 'Article AB (in Cat A and B)',
|
||||||
|
});
|
||||||
|
await store.article.setCategoryIdsBySku(googleAuthId, artAB.sku, [catA.id, catB.id]);
|
||||||
|
|
||||||
|
// Turn both categories on for the terminal
|
||||||
|
await store.posterminal.setOn(googleAuthId, terminal.id, catA.id);
|
||||||
|
await store.posterminal.setOn(googleAuthId, terminal.id, catB.id);
|
||||||
|
|
||||||
|
// Clear previous obituaries to test cleanly
|
||||||
|
provider.db.prepare('DELETE FROM posterminalobituary WHERE posterminal_id = ?').run(terminal.id);
|
||||||
|
|
||||||
|
// Turn off Cat A:
|
||||||
|
// - Cat A should get an obituary
|
||||||
|
// - Art A should get an obituary (orphaned on this terminal)
|
||||||
|
// - Art AB should NOT get an obituary (still has active Cat B on this terminal)
|
||||||
|
await store.posterminal.setOff(googleAuthId, terminal.id, catA.id);
|
||||||
|
|
||||||
|
const obituariesAfterSetOff = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ? ORDER BY id ASC')
|
||||||
|
.all(terminal.id);
|
||||||
|
|
||||||
|
console.log('Obituaries after setOff(Cat A):', obituariesAfterSetOff);
|
||||||
|
assert.equal(obituariesAfterSetOff.length, 2);
|
||||||
|
assert.deepEqual(obituariesAfterSetOff, [
|
||||||
|
{ entity_type: 'category', entity_id: catA.id },
|
||||||
|
{ entity_type: 'article', entity_id: artA.id },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 11. Test setOn category clears pending obituaries for that category and its articles
|
||||||
|
console.log('11. Testing setOn clears pending category and article obituaries...');
|
||||||
|
await store.posterminal.setOn(googleAuthId, terminal.id, catA.id);
|
||||||
|
const obituariesAfterSetOn = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Obituaries after setOn(Cat A):', obituariesAfterSetOn);
|
||||||
|
assert.equal(obituariesAfterSetOn.length, 0);
|
||||||
|
|
||||||
|
// 12. Test setOffAll generates obituaries for all active categories and all their articles
|
||||||
|
console.log('12. Testing setOffAll obituaries...');
|
||||||
|
await store.posterminal.setOffAll(googleAuthId, terminal.id);
|
||||||
|
const obituariesAfterSetOffAll = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ? ORDER BY id ASC')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Obituaries after setOffAll:', obituariesAfterSetOffAll);
|
||||||
|
// Cat A, Cat B, Art A, Art AB
|
||||||
|
const catObituaries = obituariesAfterSetOffAll.filter((t) => t.entity_type === 'category').map((t) => t.entity_id);
|
||||||
|
const artObituaries = obituariesAfterSetOffAll.filter((t) => t.entity_type === 'article').map((t) => t.entity_id);
|
||||||
|
assert(catObituaries.includes(catA.id));
|
||||||
|
assert(catObituaries.includes(catB.id));
|
||||||
|
assert(artObituaries.includes(artA.id));
|
||||||
|
assert(artObituaries.includes(artAB.id));
|
||||||
|
|
||||||
|
// 13. Test setOnAll clears category and article obituaries
|
||||||
|
console.log('13. Testing setOnAll clears all category & article obituaries...');
|
||||||
|
await store.posterminal.setOnAll(googleAuthId, terminal.id);
|
||||||
|
const obituariesAfterSetOnAll = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Obituaries after setOnAll:', obituariesAfterSetOnAll);
|
||||||
|
assert.equal(obituariesAfterSetOnAll.length, 0);
|
||||||
|
|
||||||
|
// 14. Test setCategoryIdsBySku generates obituary when removed from POS-enabled category
|
||||||
|
console.log('14. Testing setCategoryIdsBySku obituary generation and clearing...');
|
||||||
|
// Setup terminal: only has Cat A enabled
|
||||||
|
await store.posterminal.setOffAll(googleAuthId, terminal.id);
|
||||||
|
// Clear obituaries from setOffAll to start clean
|
||||||
|
provider.db.prepare('DELETE FROM posterminalobituary WHERE posterminal_id = ?').run(terminal.id);
|
||||||
|
await store.posterminal.setOn(googleAuthId, terminal.id, catA.id);
|
||||||
|
|
||||||
|
// Create article in Cat A
|
||||||
|
const artMoving = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: catA.id,
|
||||||
|
name: 'Moving Article',
|
||||||
|
});
|
||||||
|
// Should have 0 obituaries on terminal
|
||||||
|
let obits = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
|
||||||
|
.all(terminal.id);
|
||||||
|
assert.equal(obits.length, 0);
|
||||||
|
|
||||||
|
// Move article from Cat A to Cat B (which is not enabled on terminal)
|
||||||
|
console.log('Moving article to Cat B (not on terminal)...');
|
||||||
|
await store.article.setCategoryIdsBySku(googleAuthId, artMoving.sku, [catB.id]);
|
||||||
|
|
||||||
|
obits = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Obituaries after moving to Cat B:', obits);
|
||||||
|
assert.equal(obits.length, 1);
|
||||||
|
assert.deepEqual(obits[0], { entity_type: 'article', entity_id: artMoving.id });
|
||||||
|
|
||||||
|
// Move article back to Cat A (enabled on terminal)
|
||||||
|
console.log('Moving article back to Cat A (on terminal)...');
|
||||||
|
await store.article.setCategoryIdsBySku(googleAuthId, artMoving.sku, [catA.id]);
|
||||||
|
|
||||||
|
obits = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Obituaries after moving back to Cat A:', obits);
|
||||||
|
assert.equal(obits.length, 0);
|
||||||
|
|
||||||
|
console.log('✅ ALL POSTERMINALOBITUARY TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
151
scripts/test-order-create-from-quote.js
Normal file
151
scripts/test-order-create-from-quote.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
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 store.order.createFromQuote in dashdbschema ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-order-create-from-quote-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('order-from-quote-test@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Order Quote Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
const groups = await store.customergroup.list(googleAuthId);
|
||||||
|
const customer = await store.entity.create(googleAuthId, {
|
||||||
|
customergroupId: groups[0].id,
|
||||||
|
firstName: 'Alice',
|
||||||
|
name: 'Wonderland',
|
||||||
|
});
|
||||||
|
|
||||||
|
const art = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Gadget',
|
||||||
|
track_stock: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Create a quote
|
||||||
|
const quote = await store.quote.create(googleAuthId, {
|
||||||
|
number: 'Q-2026-001',
|
||||||
|
date: '2026-09-01T10:00:00Z',
|
||||||
|
entityId: customer.id,
|
||||||
|
externalNumber: 'EXT-QUOTE-123',
|
||||||
|
lines: [
|
||||||
|
{ sku: art.sku, name: 'Gadget', pu: 1, amount: 5, net: 20, rebate: 10, taxrate: 19, comment: 'test item' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(quote.number, 'Q-2026-001');
|
||||||
|
assert.equal(quote.lines.length, 1);
|
||||||
|
|
||||||
|
// 2. Create order from quote with default auto-allocated number & inherited fields
|
||||||
|
const order1 = await store.order.createFromQuote(googleAuthId, quote.id);
|
||||||
|
assert.ok(order1.id, 'Order must have an id');
|
||||||
|
assert.equal(order1.type, 'order');
|
||||||
|
assert.equal(order1.entityId, customer.id);
|
||||||
|
assert.equal(order1.externalNumber, quote.number);
|
||||||
|
assert.equal(order1.number, 'AUF-000001');
|
||||||
|
assert.equal(order1.lines.length, 1);
|
||||||
|
assert.equal(order1.lines[0].sku, art.sku);
|
||||||
|
assert.equal(order1.lines[0].amount, 5);
|
||||||
|
assert.equal(order1.lines[0].net, 20);
|
||||||
|
assert.equal(order1.lines[0].rebate, 10);
|
||||||
|
assert.equal(order1.lines[0].taxrate, 19);
|
||||||
|
assert.equal(order1.lines[0].comment, 'test item');
|
||||||
|
|
||||||
|
// Verify ordersummary row
|
||||||
|
const summaryRows = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').all(order1.id);
|
||||||
|
assert.equal(summaryRows.length, 1);
|
||||||
|
assert.equal(summaryRows[0].customer_name, 'Alice Wonderland');
|
||||||
|
assert.equal(summaryRows[0].positions, 1);
|
||||||
|
assert.equal(summaryRows[0].ordered_quantity, 5);
|
||||||
|
|
||||||
|
// 3. Create order from quote with custom overrides (number, externalNumber, date)
|
||||||
|
const order2 = await store.order.createFromQuote(googleAuthId, quote.id, {
|
||||||
|
number: 'ORD-CUSTOM-999',
|
||||||
|
date: '2026-09-15T12:00:00Z',
|
||||||
|
externalNumber: 'CUSTOM-EXT',
|
||||||
|
});
|
||||||
|
assert.equal(order2.number, 'ORD-CUSTOM-999');
|
||||||
|
assert.equal(order2.externalNumber, 'CUSTOM-EXT');
|
||||||
|
assert.equal(order2.lines.length, 1);
|
||||||
|
|
||||||
|
// 4. Non-existent quote should throw 'Quote not found'
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.order.createFromQuote(googleAuthId, 999999);
|
||||||
|
},
|
||||||
|
{ message: 'Quote not found' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Invalid quoteId should throw 'Quote not found'
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.order.createFromQuote(googleAuthId, 'invalid');
|
||||||
|
},
|
||||||
|
{ message: 'Quote not found' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. Quote without entity should throw 'No entity selected' if no entity passed
|
||||||
|
const quoteWithoutEntity = await store.quote.create(googleAuthId, {
|
||||||
|
number: 'Q-NO-ENT',
|
||||||
|
date: '2026-09-01T10:00:00Z',
|
||||||
|
lines: [{ sku: art.sku, name: 'Gadget', amount: 1, net: 10, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
assert.equal(quoteWithoutEntity.entityId, null);
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.order.createFromQuote(googleAuthId, quoteWithoutEntity.id);
|
||||||
|
},
|
||||||
|
{ message: 'No entity selected' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. Explicitly overriding with entityId: null should also throw 'No entity selected'
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.order.createFromQuote(googleAuthId, quote.id, { entityId: null });
|
||||||
|
},
|
||||||
|
{ message: 'No entity selected' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 8. Passing entityId for quote without entity should succeed
|
||||||
|
const orderFromNoEnt = await store.order.createFromQuote(googleAuthId, quoteWithoutEntity.id, {
|
||||||
|
entityId: customer.id,
|
||||||
|
});
|
||||||
|
assert.equal(orderFromNoEnt.entityId, customer.id);
|
||||||
|
|
||||||
|
console.log('PASS: All order.createFromQuote tests passed successfully!');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('FAIL: Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
207
scripts/test-order-filters.js
Normal file
207
scripts/test-order-filters.js
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
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 Order Filtering & Paging in dashdbschema ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-order-filters-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('filter-test@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Filter Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
const warehouse = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
||||||
|
await store.unit.upsert(googleAuthId, {
|
||||||
|
si: 'stk',
|
||||||
|
basesi: 'stk',
|
||||||
|
basesifactor: 1,
|
||||||
|
symbol: 'Stk',
|
||||||
|
name: 'Stück',
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups = await store.customergroup.list(googleAuthId);
|
||||||
|
const customerAlice = await store.entity.create(googleAuthId, {
|
||||||
|
customergroupId: groups[0].id,
|
||||||
|
firstName: 'Alice',
|
||||||
|
name: 'Wonderland',
|
||||||
|
});
|
||||||
|
const customerBob = await store.entity.create(googleAuthId, {
|
||||||
|
customergroupId: groups[0].id,
|
||||||
|
firstName: 'Bob',
|
||||||
|
name: 'Builder',
|
||||||
|
});
|
||||||
|
|
||||||
|
const art = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Widget',
|
||||||
|
track_stock: 1,
|
||||||
|
});
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: art.sku,
|
||||||
|
warehouseId: warehouse.id,
|
||||||
|
amount: 100,
|
||||||
|
text: 'Stock in',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Account for payments
|
||||||
|
const account = await store.account.create(googleAuthId, {
|
||||||
|
name: 'Bank',
|
||||||
|
type: 'bank',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create 3 orders
|
||||||
|
// Order 1: Alice, O-1001, €119 (1 * 100 + 19% tax), Date: 2 months ago (2026-08-01)
|
||||||
|
// Paid in full, Unshipped
|
||||||
|
const order1 = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-1001',
|
||||||
|
date: '2026-08-01T10:00:00Z',
|
||||||
|
entityId: customerAlice.id,
|
||||||
|
lines: [{ sku: art.sku, name: 'Widget', amount: 1, net: 100, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
await store.payment.create(googleAuthId, {
|
||||||
|
accountId: account.id,
|
||||||
|
manifestId: order1.id,
|
||||||
|
type: 'bank_transfer',
|
||||||
|
amount: 119,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Order 2: Bob, O-1002, €238 (2 * 100 + 19% tax), Date: 10 days ago (2026-10-03)
|
||||||
|
// Unpaid, Shipped in full with shipping 'shipped'
|
||||||
|
const order2 = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-1002',
|
||||||
|
date: '2026-10-03T10:00:00Z',
|
||||||
|
entityId: customerBob.id,
|
||||||
|
lines: [{ sku: art.sku, name: 'Widget', amount: 2, net: 100, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
const testCarrier = await store.carrier.create(googleAuthId, { name: 'Test Carrier', hasPackages: true });
|
||||||
|
const shipment2 = await store.shipment.create(googleAuthId, {
|
||||||
|
orderId: order2.id,
|
||||||
|
lines: [{ sku: art.sku, amount: 2 }],
|
||||||
|
shipping: { carrierId: testCarrier.id, type: 'parcel', state: 'shipped' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Order 3: Alice, O-1003, €59.50, Date: today (2026-10-13)
|
||||||
|
// Unpaid, Undelivered
|
||||||
|
const order3 = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-1003',
|
||||||
|
date: '2026-10-13T10:00:00Z',
|
||||||
|
entityId: customerAlice.id,
|
||||||
|
lines: [{ sku: art.sku, name: 'Widget', amount: 1, net: 50, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Order 4: Bob, O-1004, €119 (1 * 100 + 19% tax), Date: today (2026-10-13)
|
||||||
|
// Overpaid (paid €150), Delivered
|
||||||
|
const order4 = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-1004',
|
||||||
|
date: '2026-10-13T10:00:00Z',
|
||||||
|
entityId: customerBob.id,
|
||||||
|
lines: [{ sku: art.sku, name: 'Widget', amount: 1, net: 100, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
await store.payment.create(googleAuthId, {
|
||||||
|
accountId: account.id,
|
||||||
|
manifestId: order4.id,
|
||||||
|
type: 'bank_transfer',
|
||||||
|
amount: 150, // Overpaid!
|
||||||
|
});
|
||||||
|
await store.shipment.create(googleAuthId, {
|
||||||
|
orderId: order4.id,
|
||||||
|
lines: [{ sku: art.sku, amount: 1 }],
|
||||||
|
shipping: { carrierId: testCarrier.id, type: 'parcel', state: 'shipped' },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Orders created successfully.');
|
||||||
|
|
||||||
|
// 1. Search Alice
|
||||||
|
const resAlice = await store.order.list(googleAuthId, { search: 'Alice' });
|
||||||
|
assert.equal(resAlice.total, 2, 'Should find 2 orders for Alice');
|
||||||
|
assert.equal(resAlice.unfilteredTotal, 4, 'Unfiltered total should be 4');
|
||||||
|
assert.deepEqual(resAlice.orders.map((o) => o.number), ['O-1003', 'O-1001']);
|
||||||
|
|
||||||
|
// 2. Search Bob
|
||||||
|
const resBob = await store.order.list(googleAuthId, { search: 'Bob' });
|
||||||
|
assert.equal(resBob.total, 2, 'Should find 2 orders for Bob');
|
||||||
|
assert.equal(resBob.unfilteredTotal, 4, 'Unfiltered total should be 4');
|
||||||
|
assert.deepEqual(resBob.orders.map((o) => o.number), ['O-1004', 'O-1002']);
|
||||||
|
|
||||||
|
// 3. Search 1002
|
||||||
|
const res1002 = await store.order.list(googleAuthId, { search: '1002' });
|
||||||
|
assert.equal(res1002.total, 1, 'Should find 1 order for 1002');
|
||||||
|
assert.equal(res1002.unfilteredTotal, 4, 'Unfiltered total should be 4');
|
||||||
|
assert.equal(res1002.orders[0].number, 'O-1002');
|
||||||
|
|
||||||
|
// 4. onlyUnpaid (O-1004 is overpaid, so it also counts as unpaid!)
|
||||||
|
const resUnpaid = await store.order.list(googleAuthId, { onlyUnpaid: true });
|
||||||
|
assert.equal(resUnpaid.total, 3, 'O-1004 (overpaid), O-1003 (unpaid), O-1002 (unpaid) are unpaid');
|
||||||
|
assert.equal(resUnpaid.unfilteredTotal, 4, 'Unfiltered total should be 4');
|
||||||
|
assert.deepEqual(resUnpaid.orders.map((o) => o.number), ['O-1004', 'O-1003', 'O-1002']);
|
||||||
|
|
||||||
|
// 5. onlyUndelivered
|
||||||
|
const resUndelivered = await store.order.list(googleAuthId, { onlyUndelivered: true });
|
||||||
|
assert.equal(resUndelivered.total, 2, 'O-1001 and O-1003 are undelivered');
|
||||||
|
assert.equal(resUndelivered.unfilteredTotal, 4, 'Unfiltered total should be 4');
|
||||||
|
assert.deepEqual(resUndelivered.orders.map((o) => o.number), ['O-1003', 'O-1001']);
|
||||||
|
|
||||||
|
// 6. onlyUnpaid AND onlyUndelivered
|
||||||
|
const resBoth = await store.order.list(googleAuthId, { onlyUnpaid: true, onlyUndelivered: true });
|
||||||
|
assert.equal(resBoth.total, 1, 'Only O-1003 is both unpaid and undelivered');
|
||||||
|
assert.equal(resBoth.unfilteredTotal, 4, 'Unfiltered total should be 4');
|
||||||
|
assert.equal(resBoth.orders[0].number, 'O-1003');
|
||||||
|
|
||||||
|
// 7. seekMonths -1 (orders older than 1 month)
|
||||||
|
const resSeek = await store.order.list(googleAuthId, { seekMonths: -1 });
|
||||||
|
assert.equal(resSeek.total, 1, 'Only O-1001 is older than 1 month');
|
||||||
|
assert.equal(resSeek.unfilteredTotal, 4, 'Unfiltered total should be 4');
|
||||||
|
assert.equal(resSeek.orders[0].number, 'O-1001');
|
||||||
|
|
||||||
|
// 8. Paging
|
||||||
|
const resPaged = await store.order.list(googleAuthId, { offset: 0, limit: 1 });
|
||||||
|
assert.equal(resPaged.total, 4);
|
||||||
|
assert.equal(resPaged.unfilteredTotal, 4);
|
||||||
|
assert.equal(resPaged.orders.length, 1);
|
||||||
|
assert.equal(resPaged.orders[0].number, 'O-1004');
|
||||||
|
|
||||||
|
const resPaged2 = await store.order.list(googleAuthId, { offset: 1, limit: 1 });
|
||||||
|
assert.equal(resPaged2.total, 4);
|
||||||
|
assert.equal(resPaged2.unfilteredTotal, 4);
|
||||||
|
assert.equal(resPaged2.orders.length, 1);
|
||||||
|
assert.equal(resPaged2.orders[0].number, 'O-1003');
|
||||||
|
|
||||||
|
// 9. Unpaged backwards-compatibility
|
||||||
|
const resAll = await store.order.list(googleAuthId);
|
||||||
|
assert(Array.isArray(resAll));
|
||||||
|
assert.equal(resAll.length, 4);
|
||||||
|
|
||||||
|
console.log('✓ All filter and pagination tests passed via store.order.list!');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
287
scripts/test-order-shipping-ledger.js
Normal file
287
scripts/test-order-shipping-ledger.js
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
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 order shipping ledger & stock adjustment ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-order-shipping-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Setup company and user
|
||||||
|
const googleAuthId = await store.google_auth.add('shipping-test@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Shipping Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// Setup warehouse
|
||||||
|
const warehouse = await store.warehouse.create(googleAuthId, {
|
||||||
|
name: 'Hauptlager',
|
||||||
|
});
|
||||||
|
assert(warehouse.id, 'Warehouse should have id');
|
||||||
|
|
||||||
|
// Setup units
|
||||||
|
await store.unit.upsert(googleAuthId, {
|
||||||
|
si: 'stk',
|
||||||
|
basesi: 'stk',
|
||||||
|
basesifactor: 1,
|
||||||
|
symbol: 'Stk',
|
||||||
|
name: 'Stück',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup customer group & customer entity
|
||||||
|
const groups = await store.customergroup.list(googleAuthId);
|
||||||
|
const customer = await store.entity.create(googleAuthId, {
|
||||||
|
customergroupId: groups[0].id,
|
||||||
|
name: 'Max Mustermann',
|
||||||
|
});
|
||||||
|
|
||||||
|
const carrier = await store.carrier.create(googleAuthId, {
|
||||||
|
name: 'Standard Carrier',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Setup tracked article and untracked article
|
||||||
|
const trackedArt = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Tracked Article',
|
||||||
|
track_stock: 1,
|
||||||
|
});
|
||||||
|
const untrackedArt = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Untracked Service Item',
|
||||||
|
track_stock: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial stock for tracked article: +10
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: trackedArt.sku,
|
||||||
|
warehouseId: warehouse.id,
|
||||||
|
amount: 10,
|
||||||
|
text: 'Initial stock intake',
|
||||||
|
});
|
||||||
|
|
||||||
|
const artBefore = await store.article.getBySku(googleAuthId, trackedArt.sku);
|
||||||
|
const stockBefore = artBefore.article.warehouses.reduce((sum, w) => sum + w.stock, 0);
|
||||||
|
assert.equal(stockBefore, 10, 'Initial stock should be 10');
|
||||||
|
|
||||||
|
// 3. Create sales order with tracked and untracked items
|
||||||
|
const order = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-2026-001',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
entityId: customer.id,
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
sku: trackedArt.sku,
|
||||||
|
name: 'Tracked Article',
|
||||||
|
pu: 1,
|
||||||
|
amount: 3,
|
||||||
|
net: 50,
|
||||||
|
taxrate: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sku: untrackedArt.sku,
|
||||||
|
name: 'Untracked Service Item',
|
||||||
|
pu: 1,
|
||||||
|
amount: 2,
|
||||||
|
net: 20,
|
||||||
|
taxrate: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(order.lines.length, 2);
|
||||||
|
|
||||||
|
// 4. Create shipment and shipping with state 'open' (not shipped yet)
|
||||||
|
console.log('4. Creating shipment and shipping with state "open"...');
|
||||||
|
const shipment = await store.shipment.create(googleAuthId, {
|
||||||
|
orderId: order.id,
|
||||||
|
lines: [
|
||||||
|
{ sku: trackedArt.sku, amount: 3 },
|
||||||
|
{ sku: untrackedArt.sku, amount: 2 },
|
||||||
|
],
|
||||||
|
shipping: { carrierId: carrier.id, type: 'parcel', state: 'open' },
|
||||||
|
});
|
||||||
|
const shippingOpen = shipment.shippings[0];
|
||||||
|
assert.equal(shippingOpen.state, 'open');
|
||||||
|
|
||||||
|
// Assert stock is still 10
|
||||||
|
const artStill10 = await store.article.getBySku(googleAuthId, trackedArt.sku);
|
||||||
|
assert.equal(artStill10.article.warehouses.reduce((sum, w) => sum + w.stock, 0), 10, 'Stock should still be 10 when shipping is open');
|
||||||
|
|
||||||
|
// 5. Update shipping to state 'shipped'
|
||||||
|
console.log('5. Updating shipping to state "shipped"...');
|
||||||
|
const shippingShipped = await store.shipping.update(googleAuthId, shippingOpen.id, {
|
||||||
|
state: 'shipped',
|
||||||
|
warehouseId: warehouse.id,
|
||||||
|
});
|
||||||
|
assert.equal(shippingShipped.state, 'shipped');
|
||||||
|
|
||||||
|
// Assert stock is now 7 (10 - 3)
|
||||||
|
const artAfter = await store.article.getBySku(googleAuthId, trackedArt.sku);
|
||||||
|
assert.equal(artAfter.article.warehouses.reduce((sum, w) => sum + w.stock, 0), 7, 'Stock should be reduced to 7');
|
||||||
|
|
||||||
|
// Assert ledger entries
|
||||||
|
const ledgerRows = await store.ledger.listBySku(googleAuthId, trackedArt.sku);
|
||||||
|
assert.equal(ledgerRows.length, 3, 'Should have 3 ledger entries for tracked article (adjust + shipment doc + warehouse)');
|
||||||
|
const shipmentDocLedger = ledgerRows.find((l) => l.transaction_type === 'SHIPMENT' && l.warehouse_id == null);
|
||||||
|
assert(shipmentDocLedger, 'Shipment document ledger entry should exist with warehouse_id null');
|
||||||
|
assert.equal(shipmentDocLedger.amount, -3, 'Shipment document ledger entry should be -3');
|
||||||
|
const shipmentWhLedger = ledgerRows.find((l) => l.transaction_type === 'WAREHOUSE' && l.warehouse_id === warehouse.id);
|
||||||
|
assert(shipmentWhLedger, 'Shipment warehouse ledger entry should exist with warehouse_id set');
|
||||||
|
assert.equal(shipmentWhLedger.amount, -3, 'Shipment warehouse ledger entry should be -3');
|
||||||
|
|
||||||
|
// Assert untracked article has 1 document ledger entry and 0 warehouse ledger entries
|
||||||
|
const untrackedLedger = await store.ledger.listBySku(googleAuthId, untrackedArt.sku);
|
||||||
|
assert.equal(untrackedLedger.length, 1, 'Untracked article should have 1 shipment document ledger entry');
|
||||||
|
assert.equal(untrackedLedger[0].transaction_type, 'SHIPMENT');
|
||||||
|
assert.equal(untrackedLedger[0].warehouse_id, null, 'Untracked article should not have warehouse ledger entry');
|
||||||
|
assert.equal(untrackedLedger[0].amount, -2);
|
||||||
|
|
||||||
|
// Assert article history: order goes into history, and shipment produces history
|
||||||
|
const history = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku);
|
||||||
|
console.log('Tracked article history:', history.map((h) => h.text));
|
||||||
|
const orderHistory = history.find((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 3, € 59,50)');
|
||||||
|
assert(orderHistory, 'Article history should contain order creation entry with formatted gross price');
|
||||||
|
assert(orderHistory.meta, 'Article history entry should have meta');
|
||||||
|
const parsedMeta = JSON.parse(orderHistory.meta);
|
||||||
|
assert.equal(parsedMeta.type, 'order');
|
||||||
|
assert.equal(parsedMeta.orderId, order.id);
|
||||||
|
assert.equal(parsedMeta.gross, 59.5);
|
||||||
|
const shipmentHistory = history.find((h) => h.text.startsWith(`Lieferschein ${shipment.number}`) && h.text.includes('Kunde: "Max Mustermann"'));
|
||||||
|
assert(shipmentHistory, 'Article history should report the shipment document');
|
||||||
|
|
||||||
|
// Untracked article also has the order in history and shipment in history
|
||||||
|
const untrackedHistory = await store.article.getHistoryBySku(googleAuthId, untrackedArt.sku);
|
||||||
|
console.log('Untracked article history:', untrackedHistory.map((h) => h.text));
|
||||||
|
assert(
|
||||||
|
untrackedHistory.some((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 2, € 23,80)'),
|
||||||
|
'Untracked article history should contain order creation entry with formatted gross price',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
untrackedHistory.some((h) => h.text.startsWith(`Lieferschein ${shipment.number}`)),
|
||||||
|
'Untracked article history should report the shipment',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test order import with historical date preserves date in article history
|
||||||
|
const historicalDate = '2023-04-15T10:30:00.000Z';
|
||||||
|
await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-HIST-001',
|
||||||
|
date: historicalDate,
|
||||||
|
createdAt: historicalDate,
|
||||||
|
entityId: customer.id,
|
||||||
|
lines: [{ sku: trackedArt.sku, amount: 1, net: 10, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
const histWithOld = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku);
|
||||||
|
const oldEntry = histWithOld.find((h) => h.text.startsWith('Auftrag O-HIST-001'));
|
||||||
|
assert(oldEntry, 'Historical order entry should exist in article history');
|
||||||
|
assert.equal(new Date(oldEntry.date).toISOString(), historicalDate, 'History entry should preserve historical order date');
|
||||||
|
const oldMeta = JSON.parse(oldEntry.meta);
|
||||||
|
assert.equal(oldMeta.orderNumber, 'O-HIST-001');
|
||||||
|
|
||||||
|
// 6. Test immutability: once shipped, cannot be deleted
|
||||||
|
console.log('6. Testing cannot delete shipped shipping...');
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.shipping.delete(googleAuthId, shippingShipped.id);
|
||||||
|
},
|
||||||
|
{ message: 'Shipped shipping cannot be deleted' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. Test immutability: once shipped, cannot be undone
|
||||||
|
console.log('7. Testing cannot undo shipped shipping...');
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.shipping.update(googleAuthId, shippingShipped.id, { state: 'open' });
|
||||||
|
},
|
||||||
|
{ message: 'Shipped shipping cannot be undone' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 8. Test order manifest cannot be used directly for shipping
|
||||||
|
console.log('8. Testing order manifest cannot be used directly for shipping...');
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.shipping.create(googleAuthId, {
|
||||||
|
manifestId: order.id,
|
||||||
|
carrierId: carrier.id,
|
||||||
|
type: 'express',
|
||||||
|
state: 'shipped',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ message: 'Manifest is not a shipment' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 9. Test direct creation with state 'shipped' on shipment manifest
|
||||||
|
console.log('9. Testing direct creation with state "shipped"...');
|
||||||
|
const order2 = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-2026-002',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
entityId: customer.id,
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
sku: trackedArt.sku,
|
||||||
|
name: 'Tracked Article',
|
||||||
|
pu: 1,
|
||||||
|
amount: 2,
|
||||||
|
net: 50,
|
||||||
|
taxrate: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const shipment2 = await store.shipment.create(googleAuthId, {
|
||||||
|
orderId: order2.id,
|
||||||
|
lines: [{ sku: trackedArt.sku, amount: 2 }],
|
||||||
|
shipping: { carrierId: carrier.id, type: 'parcel', state: 'shipped' },
|
||||||
|
});
|
||||||
|
const directShipped = shipment2.shippings[0];
|
||||||
|
|
||||||
|
// 9b. Import-style: delivered shipment with shipped shipping created atomically
|
||||||
|
const order3 = await store.order.create(googleAuthId, {
|
||||||
|
number: 'O-2026-003',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
entityId: customer.id,
|
||||||
|
lines: [{ sku: trackedArt.sku, name: 'Tracked Article', pu: 1, amount: 1, net: 50, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
const shipment3 = await store.shipment.create(googleAuthId, {
|
||||||
|
orderId: order3.id,
|
||||||
|
state: 'delivered',
|
||||||
|
lines: [{ sku: trackedArt.sku, amount: 1 }],
|
||||||
|
shipping: { carrierId: carrier.id, type: 'parcel', state: 'shipped' },
|
||||||
|
});
|
||||||
|
assert.equal(shipment3.shippings[0].state, 'shipped');
|
||||||
|
assert.equal(directShipped.state, 'shipped');
|
||||||
|
|
||||||
|
const artFinal = await store.article.getBySku(googleAuthId, trackedArt.sku);
|
||||||
|
assert.equal(artFinal.article.warehouses.reduce((sum, w) => sum + w.stock, 0), 4, 'Stock should now be 4 (7 - 2 - 1)');
|
||||||
|
|
||||||
|
console.log('✓ All order shipping ledger tests passed successfully!');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
118
scripts/test-printeragent.js
Normal file
118
scripts/test-printeragent.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'dash-printeragent-test-'));
|
||||||
|
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('printeragent-tester@example.com');
|
||||||
|
const company = await store.company.create({ name: 'PA GmbH', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId });
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
assert.deepEqual(await store.printeragent.list(googleAuthId), []);
|
||||||
|
await assert.rejects(store.printeragent.create(googleAuthId, { name: ' ', pairingpin: '1' }), /Invalid printeragent/);
|
||||||
|
|
||||||
|
const a = await store.printeragent.create(googleAuthId, { name: ' Bar ', pairingpin: '123456', sort: 5 });
|
||||||
|
assert.equal(a.name, 'Bar');
|
||||||
|
assert.equal(a.pairingpin, '123456');
|
||||||
|
assert.equal(a.token, null);
|
||||||
|
assert.equal(a.sort, 5);
|
||||||
|
assert.equal(a.companyId, company.id);
|
||||||
|
assert.ok(a.updatedAt);
|
||||||
|
|
||||||
|
// findByPairingpin
|
||||||
|
const foundByPin = await store.printeragent.findByPairingpin('123456');
|
||||||
|
assert.equal(foundByPin.id, a.id);
|
||||||
|
assert.equal(await store.printeragent.findByPairingpin('999999'), null);
|
||||||
|
|
||||||
|
// pair
|
||||||
|
const paired = await store.printeragent.pair(a.id, {
|
||||||
|
token: 'test-token-123',
|
||||||
|
agentversion: '1.0.0',
|
||||||
|
});
|
||||||
|
assert.equal(paired.token, 'test-token-123');
|
||||||
|
assert.equal(paired.agentversion, '1.0.0');
|
||||||
|
assert.equal(paired.pairingpin, null);
|
||||||
|
assert.equal(paired.enabled, true);
|
||||||
|
assert.ok(paired.pairedAt);
|
||||||
|
|
||||||
|
// findByToken
|
||||||
|
const foundByToken = await store.printeragent.findByToken('test-token-123');
|
||||||
|
assert.equal(foundByToken.id, a.id);
|
||||||
|
assert.equal(foundByToken.enabled, true);
|
||||||
|
assert.equal(await store.printeragent.findByToken('invalid-token'), null);
|
||||||
|
|
||||||
|
// touchLastseen
|
||||||
|
await store.printeragent.touchLastseen(a.id, '1.0.1');
|
||||||
|
const afterTouch = await store.printeragent.findByToken('test-token-123');
|
||||||
|
assert.equal(afterTouch.agentversion, '1.0.1');
|
||||||
|
|
||||||
|
// update name, sort, and enabled
|
||||||
|
const b = await store.printeragent.update(googleAuthId, { id: a.id, name: 'Kitchen', sort: 1, enabled: false });
|
||||||
|
assert.equal(b.name, 'Kitchen');
|
||||||
|
assert.equal(b.sort, 1);
|
||||||
|
assert.equal(b.enabled, false);
|
||||||
|
|
||||||
|
// list
|
||||||
|
const list = await store.printeragent.list(googleAuthId);
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list[0].name, 'Kitchen');
|
||||||
|
assert.equal(list[0].sort, 1);
|
||||||
|
assert.equal(list[0].enabled, false);
|
||||||
|
|
||||||
|
// test agentprinter findOrCreate
|
||||||
|
const ap1 = await store.agentprinter.findOrCreate(a.id, { type: 'zpl', model: 'ZD410-200dpi', serial: '50J203002218' });
|
||||||
|
assert.equal(ap1.created, true);
|
||||||
|
assert.equal(ap1.agentprinter.type, 'zpl');
|
||||||
|
assert.equal(ap1.agentprinter.model, 'ZD410-200dpi');
|
||||||
|
assert.equal(ap1.agentprinter.serial, '50J203002218');
|
||||||
|
assert.equal(ap1.agentprinter.enabled, true);
|
||||||
|
|
||||||
|
// second call with same combo returns existing
|
||||||
|
const ap1Again = await store.agentprinter.findOrCreate(a.id, { type: 'zpl', model: 'ZD410-200dpi', serial: '50J203002218' });
|
||||||
|
assert.equal(ap1Again.created, false);
|
||||||
|
assert.equal(ap1Again.agentprinter.id, ap1.agentprinter.id);
|
||||||
|
|
||||||
|
// create second printer
|
||||||
|
const ap2 = await store.agentprinter.findOrCreate(a.id, { type: 'esc/pos', model: 'TM-T88VI', serial: 'SN999' });
|
||||||
|
assert.equal(ap2.created, true);
|
||||||
|
|
||||||
|
// list agentprinters
|
||||||
|
const apList = await store.agentprinter.list(googleAuthId, a.id);
|
||||||
|
assert.equal(apList.length, 2);
|
||||||
|
|
||||||
|
// getById
|
||||||
|
const apGet = await store.agentprinter.getById(googleAuthId, ap1.agentprinter.id);
|
||||||
|
assert.equal(apGet.serial, '50J203002218');
|
||||||
|
|
||||||
|
// update agentprinter
|
||||||
|
const apUpdated = await store.agentprinter.update(googleAuthId, { id: ap1.agentprinter.id, enabled: false, sort: 10 });
|
||||||
|
assert.equal(apUpdated.enabled, false);
|
||||||
|
assert.equal(apUpdated.sort, 10);
|
||||||
|
|
||||||
|
// order agentprinters
|
||||||
|
const ordered = await store.agentprinter.order(googleAuthId, {
|
||||||
|
printeragentId: a.id,
|
||||||
|
agentprinterIds: [ap2.agentprinter.id, ap1.agentprinter.id],
|
||||||
|
});
|
||||||
|
assert.deepEqual(ordered.agentprinterIds, [ap2.agentprinter.id, ap1.agentprinter.id]);
|
||||||
|
|
||||||
|
// delete agentprinter
|
||||||
|
await store.agentprinter.delete(googleAuthId, ap2.agentprinter.id);
|
||||||
|
assert.equal((await store.agentprinter.list(googleAuthId, a.id)).length, 1);
|
||||||
|
|
||||||
|
// delete
|
||||||
|
await store.printeragent.delete(googleAuthId, a.id);
|
||||||
|
assert.equal(await store.printeragent.findByToken('test-token-123'), null);
|
||||||
|
assert.deepEqual(await store.printeragent.list(googleAuthId), []);
|
||||||
|
console.log('printeragent full test OK');
|
||||||
|
} finally {
|
||||||
|
await provider.close?.();
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
142
scripts/test-quote-summary.js
Normal file
142
scripts/test-quote-summary.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
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 Quote Summary & Filtering in dashdbschema ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-quote-summary-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('quote-summary-test@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Quote Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
const groups = await store.customergroup.list(googleAuthId);
|
||||||
|
const customerAlice = await store.entity.create(googleAuthId, {
|
||||||
|
customergroupId: groups[0].id,
|
||||||
|
firstName: 'Alice',
|
||||||
|
name: 'Wonderland',
|
||||||
|
});
|
||||||
|
const customerBob = await store.entity.create(googleAuthId, {
|
||||||
|
customergroupId: groups[0].id,
|
||||||
|
firstName: 'Bob',
|
||||||
|
name: 'Builder',
|
||||||
|
});
|
||||||
|
|
||||||
|
const art = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Gadget',
|
||||||
|
track_stock: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Create quote 1: Alice, Q-1001, 2 items of 100 net @ 19% tax => 238 gross, 2 total quantity
|
||||||
|
const quote1 = await store.quote.create(googleAuthId, {
|
||||||
|
number: 'Q-1001',
|
||||||
|
date: '2026-08-01T10:00:00Z',
|
||||||
|
entityId: customerAlice.id,
|
||||||
|
lines: [{ sku: art.sku, name: 'Gadget', amount: 2, net: 100, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
assert.equal(quote1.number, 'Q-1001');
|
||||||
|
|
||||||
|
// Verify row in quotesummary table directly
|
||||||
|
const directRows1 = provider.db.prepare('SELECT * FROM quotesummary WHERE manifest_id = ?').all(quote1.id);
|
||||||
|
assert.equal(directRows1.length, 1);
|
||||||
|
assert.equal(directRows1[0].quote_number, 'Q-1001');
|
||||||
|
assert.equal(directRows1[0].customer_name, 'Alice Wonderland');
|
||||||
|
assert.equal(directRows1[0].positions, 1);
|
||||||
|
assert.equal(directRows1[0].gross_total, 238);
|
||||||
|
assert.equal(directRows1[0].total_quantity, 2);
|
||||||
|
|
||||||
|
// 2. Create quote 2: Bob, Q-1002, 3 items of 50 net @ 19% tax => 178.50 gross, 3 total quantity
|
||||||
|
const quote2 = await store.quote.create(googleAuthId, {
|
||||||
|
number: 'Q-1002',
|
||||||
|
date: '2026-10-01T10:00:00Z',
|
||||||
|
entityId: customerBob.id,
|
||||||
|
lines: [{ sku: art.sku, name: 'Gadget', amount: 3, net: 50, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test quote.list unpaged
|
||||||
|
const unpaged = await store.quote.list(googleAuthId);
|
||||||
|
assert.equal(unpaged.length, 2);
|
||||||
|
const q1 = unpaged.find((q) => q.id === quote1.id);
|
||||||
|
assert.equal(q1.positions, 1);
|
||||||
|
assert.equal(q1.grossTotal, 238);
|
||||||
|
assert.equal(q1.totalQuantity, 2);
|
||||||
|
assert.equal(q1.customerName, 'Alice Wonderland');
|
||||||
|
|
||||||
|
// Test quote.list paged with search
|
||||||
|
const searchAlice = await store.quote.list(googleAuthId, { search: 'Alice' });
|
||||||
|
assert.equal(searchAlice.total, 1);
|
||||||
|
assert.equal(searchAlice.unfilteredTotal, 2);
|
||||||
|
assert.equal(searchAlice.quotes[0].number, 'Q-1001');
|
||||||
|
|
||||||
|
const searchNumber = await store.quote.list(googleAuthId, { search: '1002' });
|
||||||
|
assert.equal(searchNumber.total, 1);
|
||||||
|
assert.equal(searchNumber.quotes[0].number, 'Q-1002');
|
||||||
|
|
||||||
|
// Test quote.update updates summary
|
||||||
|
await store.quote.update(googleAuthId, quote1.id, {
|
||||||
|
lines: [
|
||||||
|
{ sku: art.sku, name: 'Gadget', amount: 5, net: 100, taxrate: 19 },
|
||||||
|
{ sku: art.sku, name: 'Gadget 2', amount: 1, net: 10, taxrate: 19 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const updatedSummary = provider.db.prepare('SELECT * FROM quotesummary WHERE manifest_id = ?').get(quote1.id);
|
||||||
|
assert.equal(updatedSummary.positions, 2);
|
||||||
|
assert.equal(updatedSummary.total_quantity, 6);
|
||||||
|
// (5 * 100 * 1.19) + (1 * 10 * 1.19) = 595 + 11.9 = 606.90
|
||||||
|
assert.equal(updatedSummary.gross_total, 606.9);
|
||||||
|
|
||||||
|
// Test rebuildSummaries
|
||||||
|
provider.db.prepare('DELETE FROM quotesummary').run();
|
||||||
|
const countBeforeRebuild = provider.db.prepare('SELECT COUNT(*) as cnt FROM quotesummary').get().cnt;
|
||||||
|
assert.equal(countBeforeRebuild, 0);
|
||||||
|
|
||||||
|
const rebuildRes = await store.quote.rebuildSummaries(googleAuthId);
|
||||||
|
assert.equal(rebuildRes.ok, true);
|
||||||
|
const countAfterRebuild = provider.db.prepare('SELECT COUNT(*) as cnt FROM quotesummary').get().cnt;
|
||||||
|
assert.equal(countAfterRebuild, 2);
|
||||||
|
|
||||||
|
const rebuiltQ1 = provider.db.prepare('SELECT * FROM quotesummary WHERE manifest_id = ?').get(quote1.id);
|
||||||
|
assert.equal(rebuiltQ1.positions, 2);
|
||||||
|
assert.equal(rebuiltQ1.total_quantity, 6);
|
||||||
|
assert.equal(rebuiltQ1.gross_total, 606.9);
|
||||||
|
|
||||||
|
// Test delete cascade
|
||||||
|
await store.quote.delete(googleAuthId, quote1.id);
|
||||||
|
const countAfterDelete = provider.db.prepare('SELECT COUNT(*) as cnt FROM quotesummary WHERE manifest_id = ?').get(quote1.id);
|
||||||
|
assert.equal(countAfterDelete.cnt, 0);
|
||||||
|
|
||||||
|
console.log('--- ALL QUOTE SUMMARY TESTS PASSED ---');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
377
scripts/test-shipment.js
Normal file
377
scripts/test-shipment.js
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
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 Shipment Manifest & Multi-Package Shipping ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-shipment-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('shipment-test@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Shipment Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// Create a warehouse
|
||||||
|
const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
||||||
|
|
||||||
|
// Create customer entity
|
||||||
|
const groups = await store.customergroup.list(googleAuthId);
|
||||||
|
const customer = await store.entity.create(googleAuthId, {
|
||||||
|
customergroupId: groups[0].id,
|
||||||
|
name: 'Max Mustermann',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Item A and Item B
|
||||||
|
const itemA = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Item A',
|
||||||
|
sku: 'ITEM-A',
|
||||||
|
track_stock: true,
|
||||||
|
});
|
||||||
|
const itemB = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Item B',
|
||||||
|
sku: 'ITEM-B',
|
||||||
|
track_stock: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stock up Item A (20) and Item B (20)
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: 'ITEM-A',
|
||||||
|
warehouseId: wh.id,
|
||||||
|
amount: 20,
|
||||||
|
comment: 'Initial Stock',
|
||||||
|
});
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: 'ITEM-B',
|
||||||
|
warehouseId: wh.id,
|
||||||
|
amount: 20,
|
||||||
|
comment: 'Initial Stock',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Order with 10x Item A, 5x Item B
|
||||||
|
const order = await store.order.create(googleAuthId, {
|
||||||
|
number: 'AU-1001',
|
||||||
|
date: '2026-10-12',
|
||||||
|
entityId: customer.id,
|
||||||
|
lines: [
|
||||||
|
{ sku: 'ITEM-A', amount: 10, net: 100, taxrate: 19 },
|
||||||
|
{ sku: 'ITEM-B', amount: 5, net: 50, taxrate: 19 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(order.number, 'AU-1001');
|
||||||
|
console.log('Order created:', order.number);
|
||||||
|
|
||||||
|
const carrier = await store.carrier.create(googleAuthId, {
|
||||||
|
name: 'DHL',
|
||||||
|
hasPackages: true,
|
||||||
|
isReverse: false,
|
||||||
|
});
|
||||||
|
assert.equal(carrier.name, 'DHL');
|
||||||
|
assert.equal(carrier.hasPackages, true);
|
||||||
|
assert.equal(carrier.isReverse, false);
|
||||||
|
const shipping = { carrierId: carrier.id, type: 'parcel', state: 'draft' };
|
||||||
|
|
||||||
|
// 0. Shipment without shipping is rejected
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.shipment.create(googleAuthId, { orderId: order.id, lines: [{ sku: 'ITEM-A', amount: 1 }] }),
|
||||||
|
{ message: 'Shipping is required' },
|
||||||
|
);
|
||||||
|
console.log('PASS: Shipment without shipping rejected');
|
||||||
|
|
||||||
|
// 1. Create Shipment 1: 6x Item A, 3x Item B -> AU-1001-L-1
|
||||||
|
const shipment1 = await store.shipment.create(googleAuthId, {
|
||||||
|
shipping,
|
||||||
|
orderId: order.id,
|
||||||
|
lines: [
|
||||||
|
{ sku: 'ITEM-A', amount: 6 },
|
||||||
|
{ sku: 'ITEM-B', amount: 3 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(shipment1.number, 'AU-1001-L-1');
|
||||||
|
assert.equal(shipment1.lines.length, 2);
|
||||||
|
console.log('PASS: Shipment 1 created with number:', shipment1.number);
|
||||||
|
|
||||||
|
// 2. Create Shipment 2: 5x Item A -> Fails with ceiling violation (6 + 5 > 10)
|
||||||
|
let ceilingErrorThrown = false;
|
||||||
|
try {
|
||||||
|
await store.shipment.create(googleAuthId, {
|
||||||
|
shipping,
|
||||||
|
orderId: order.id,
|
||||||
|
lines: [{ sku: 'ITEM-A', amount: 5 }],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
ceilingErrorThrown = true;
|
||||||
|
assert.match(err.message, /Shipment amount exceeds ordered amount for article \(ITEM-A\)/);
|
||||||
|
console.log('PASS: Ceiling violation caught:', err.message);
|
||||||
|
}
|
||||||
|
assert.ok(ceilingErrorThrown, 'Expected ceiling violation error to be thrown');
|
||||||
|
|
||||||
|
// Also test foreign article rejection
|
||||||
|
let foreignArticleErrorThrown = false;
|
||||||
|
try {
|
||||||
|
await store.shipment.create(googleAuthId, {
|
||||||
|
shipping,
|
||||||
|
orderId: order.id,
|
||||||
|
lines: [{ sku: 'ITEM-UNKNOWN', amount: 1 }],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
foreignArticleErrorThrown = true;
|
||||||
|
assert.match(err.message, /Article \(ITEM-UNKNOWN\) is not part of order/);
|
||||||
|
console.log('PASS: Foreign article rejected:', err.message);
|
||||||
|
}
|
||||||
|
assert.ok(foreignArticleErrorThrown, 'Expected foreign article error to be thrown');
|
||||||
|
|
||||||
|
// 3. Create Shipment 2: 4x Item A, 2x Item B -> AU-1001-L-2
|
||||||
|
const shipment2 = await store.shipment.create(googleAuthId, {
|
||||||
|
shipping,
|
||||||
|
orderId: order.id,
|
||||||
|
lines: [
|
||||||
|
{ sku: 'ITEM-A', amount: 4 },
|
||||||
|
{ sku: 'ITEM-B', amount: 2 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(shipment2.number, 'AU-1001-L-2');
|
||||||
|
assert.equal(shipment2.lines.length, 2);
|
||||||
|
console.log('PASS: Shipment 2 created with number:', shipment2.number);
|
||||||
|
|
||||||
|
// 4. Try creating another shipment (now 10/10 of Item A and 5/5 of Item B are shipped)
|
||||||
|
let exhaustedErrorThrown = false;
|
||||||
|
try {
|
||||||
|
await store.shipment.create(googleAuthId, {
|
||||||
|
shipping,
|
||||||
|
orderId: order.id,
|
||||||
|
lines: [{ sku: 'ITEM-A', amount: 1 }],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
exhaustedErrorThrown = true;
|
||||||
|
assert.match(err.message, /Shipment amount exceeds ordered amount for article \(ITEM-A\)/);
|
||||||
|
console.log('PASS: Exhausted quantity violation caught:', err.message);
|
||||||
|
}
|
||||||
|
assert.ok(exhaustedErrorThrown, 'Expected exhausted error to be thrown');
|
||||||
|
|
||||||
|
// 5. Add further packages to Shipment 1 with tracking codes
|
||||||
|
const pkg1 = await store.shipping.create(googleAuthId, {
|
||||||
|
manifestId: shipment1.id,
|
||||||
|
carrierId: carrier.id,
|
||||||
|
type: 'parcel',
|
||||||
|
state: 'draft',
|
||||||
|
trackingCode: 'TRACK-111',
|
||||||
|
});
|
||||||
|
assert.equal(pkg1.trackingCode, 'TRACK-111');
|
||||||
|
assert.equal(pkg1.carrierId, carrier.id);
|
||||||
|
assert.equal(pkg1.carrier, 'DHL');
|
||||||
|
assert.equal(pkg1.state, 'draft');
|
||||||
|
console.log('PASS: Package 1 created for Shipment 1:', pkg1.number, pkg1.trackingCode);
|
||||||
|
|
||||||
|
const pkg2 = await store.shipping.create(googleAuthId, {
|
||||||
|
manifestId: shipment1.id,
|
||||||
|
carrierId: carrier.id,
|
||||||
|
type: 'parcel',
|
||||||
|
state: 'draft',
|
||||||
|
trackingCode: 'TRACK-222',
|
||||||
|
});
|
||||||
|
assert.equal(pkg2.trackingCode, 'TRACK-222');
|
||||||
|
assert.equal(pkg2.carrierId, carrier.id);
|
||||||
|
console.log('PASS: Package 2 created for Shipment 1:', pkg2.number, pkg2.trackingCode);
|
||||||
|
|
||||||
|
// Verify carrier in use prevents deletion
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.carrier.delete(googleAuthId, carrier.id),
|
||||||
|
{ message: 'Carrier in use' },
|
||||||
|
);
|
||||||
|
console.log('PASS: Carrier delete rejected while referenced in shipping');
|
||||||
|
|
||||||
|
// Check listByOrderId
|
||||||
|
const orderShipments = await store.shipment.listByOrderId(googleAuthId, order.id);
|
||||||
|
assert.equal(orderShipments.length, 2);
|
||||||
|
assert.equal(orderShipments[0].shippings.length, 3);
|
||||||
|
console.log('PASS: store.shipment.listByOrderId returned both shipments');
|
||||||
|
|
||||||
|
// Check getById
|
||||||
|
const fetchedShipment1 = await store.shipment.getById(googleAuthId, shipment1.id);
|
||||||
|
assert.equal(fetchedShipment1.id, shipment1.id);
|
||||||
|
assert.equal(fetchedShipment1.shippings.length, 3);
|
||||||
|
console.log('PASS: store.shipment.getById returned shipment with shippings');
|
||||||
|
|
||||||
|
// 6. Transition Package 1 to shipped (should deduct ledger stock for Shipment 1 lines: 6 of Item A, 3 of Item B)
|
||||||
|
const updatedPkg1 = await store.shipping.update(googleAuthId, pkg1.id, {
|
||||||
|
state: 'shipped',
|
||||||
|
});
|
||||||
|
assert.equal(updatedPkg1.state, 'shipped');
|
||||||
|
console.log('PASS: Package 1 transitioned to shipped');
|
||||||
|
|
||||||
|
// Transition Package 2 to shipped (stock already booked for shipment1, should not double-deduct)
|
||||||
|
const updatedPkg2 = await store.shipping.update(googleAuthId, pkg2.id, {
|
||||||
|
state: 'shipped',
|
||||||
|
});
|
||||||
|
assert.equal(updatedPkg2.state, 'shipped');
|
||||||
|
console.log('PASS: Package 2 transitioned to shipped');
|
||||||
|
|
||||||
|
// 7. Verify cannot delete shipped shipment
|
||||||
|
let deleteErrorThrown = false;
|
||||||
|
try {
|
||||||
|
await store.shipment.delete(googleAuthId, shipment1.id);
|
||||||
|
} catch (err) {
|
||||||
|
deleteErrorThrown = true;
|
||||||
|
assert.match(err.message, /Shipped shipment cannot be deleted/);
|
||||||
|
console.log('PASS: Shipped shipment delete rejected:', err.message);
|
||||||
|
}
|
||||||
|
assert.ok(deleteErrorThrown, 'Expected shipped shipment delete error');
|
||||||
|
|
||||||
|
// 8. Delete unshipped shipment 2
|
||||||
|
await store.shipment.delete(googleAuthId, shipment2.id);
|
||||||
|
const shipmentsAfterDel = await store.shipment.listByOrderId(googleAuthId, order.id);
|
||||||
|
assert.equal(shipmentsAfterDel.length, 1);
|
||||||
|
console.log('PASS: Unshipped shipment 2 successfully deleted');
|
||||||
|
|
||||||
|
// 9. Test order2: One item, some items, all items, and automatic ordersummary updates
|
||||||
|
const order2 = await store.order.create(googleAuthId, {
|
||||||
|
number: 'AU-1002',
|
||||||
|
date: '2026-10-15',
|
||||||
|
entityId: customer.id,
|
||||||
|
lines: [
|
||||||
|
{ sku: 'ITEM-A', amount: 10, net: 100, taxrate: 19 },
|
||||||
|
{ sku: 'ITEM-B', amount: 5, net: 50, taxrate: 19 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check initial ordersummary for order2
|
||||||
|
let sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id);
|
||||||
|
assert.equal(sum2.ordered_quantity, 15);
|
||||||
|
assert.equal(sum2.shipped_quantity, 0);
|
||||||
|
assert.equal(sum2.is_shipped, 0);
|
||||||
|
|
||||||
|
// 9a. Ship ONE item (3x ITEM-A) via store.order.createShipment
|
||||||
|
const shOne = await store.order.createShipment(googleAuthId, order2.id, {
|
||||||
|
shipping,
|
||||||
|
lines: [{ sku: 'ITEM-A', amount: 3 }],
|
||||||
|
});
|
||||||
|
assert.equal(shOne.number, 'AU-1002-L-1');
|
||||||
|
assert.equal(shOne.lines.length, 1);
|
||||||
|
assert.equal(shOne.lines[0].sku, 'ITEM-A');
|
||||||
|
assert.equal(shOne.lines[0].amount, 3);
|
||||||
|
|
||||||
|
// Verify ordersummary updated: shipped_quantity = 3, is_shipped = 0
|
||||||
|
sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id);
|
||||||
|
assert.equal(sum2.shipped_quantity, 3);
|
||||||
|
assert.equal(sum2.is_shipped, 0);
|
||||||
|
console.log('PASS: 9a. Shipped ONE item; ordersummary updated with shipped_quantity=3, is_shipped=0');
|
||||||
|
|
||||||
|
// 9b. Ship SOME items with omitted amount (e.g. ITEM-A with no amount -> defaults to remaining 7)
|
||||||
|
const shSome = await store.order.createShipment(googleAuthId, order2.id, {
|
||||||
|
shipping,
|
||||||
|
lines: [{ sku: 'ITEM-A' }],
|
||||||
|
});
|
||||||
|
assert.equal(shSome.number, 'AU-1002-L-2');
|
||||||
|
assert.equal(shSome.lines.length, 1);
|
||||||
|
assert.equal(shSome.lines[0].sku, 'ITEM-A');
|
||||||
|
assert.equal(shSome.lines[0].amount, 7);
|
||||||
|
|
||||||
|
// Verify ordersummary updated: shipped_quantity = 10 (3 + 7), is_shipped = 0
|
||||||
|
sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id);
|
||||||
|
assert.equal(sum2.shipped_quantity, 10);
|
||||||
|
assert.equal(sum2.is_shipped, 0);
|
||||||
|
console.log('PASS: 9b. Shipped SOME items with omitted amount; ordersummary updated with shipped_quantity=10, is_shipped=0');
|
||||||
|
|
||||||
|
// 9c. Ship remaining items with explicit lines
|
||||||
|
const shAll = await store.order.createShipment(googleAuthId, order2.id, { shipping, lines: [{ sku: 'ITEM-B', amount: 5 }] });
|
||||||
|
assert.equal(shAll.number, 'AU-1002-L-3');
|
||||||
|
assert.equal(shAll.lines.length, 1);
|
||||||
|
assert.equal(shAll.lines[0].sku, 'ITEM-B');
|
||||||
|
assert.equal(shAll.lines[0].amount, 5);
|
||||||
|
|
||||||
|
// Verify ordersummary updated: shipped_quantity = 15, is_shipped = 1 (order is fully shipped!)
|
||||||
|
sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id);
|
||||||
|
assert.equal(sum2.shipped_quantity, 15);
|
||||||
|
assert.equal(sum2.is_shipped, 1);
|
||||||
|
console.log('PASS: 9c. Shipped remaining items with explicit lines; ordersummary updated with shipped_quantity=15, is_shipped=1');
|
||||||
|
|
||||||
|
// 9d. Calling createShipment without lines is rejected with "Invalid lines"
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.order.createShipment(googleAuthId, order2.id);
|
||||||
|
},
|
||||||
|
{ message: 'Invalid lines' }
|
||||||
|
);
|
||||||
|
console.log('PASS: 9d. Creating shipment without lines rejected with "Invalid lines"');
|
||||||
|
|
||||||
|
// 10. Verify peek next number works for entity and other types without error
|
||||||
|
const nextEntity = await store.entity.peekNextNumber(googleAuthId);
|
||||||
|
assert.ok(typeof nextEntity === 'string' && nextEntity.length > 0);
|
||||||
|
console.log('PASS: store.entity.peekNextNumber succeeded:', nextEntity);
|
||||||
|
|
||||||
|
const numList = await store.numbers.list(googleAuthId);
|
||||||
|
assert.ok(Array.isArray(numList) && numList.length > 0);
|
||||||
|
const shipmentSeq = numList.find((n) => n.type === 'shipment');
|
||||||
|
assert.ok(shipmentSeq, 'Expected shipment sequence in numbers.list');
|
||||||
|
// 11. Test store.shipment.list, store.shipment.update, and rebuildSummaries
|
||||||
|
const listAll = await store.shipment.list(googleAuthId, { offset: 0, limit: 10 });
|
||||||
|
assert.ok(listAll.total >= 3, `Expected at least 3 shipments, got ${listAll.total}`);
|
||||||
|
assert.equal(listAll.limit, 10);
|
||||||
|
assert.equal(listAll.offset, 0);
|
||||||
|
|
||||||
|
const s1 = listAll.shipments.find((s) => s.id === shOne.id);
|
||||||
|
assert.ok(s1, 'Expected shOne in shipment list');
|
||||||
|
assert.equal(s1.orderNumber, 'AU-1002');
|
||||||
|
assert.equal(s1.orderId, order2.id);
|
||||||
|
assert.equal(s1.state, 'new');
|
||||||
|
assert.equal(s1.positions, 1);
|
||||||
|
assert.equal(s1.totalQuantity, 3);
|
||||||
|
console.log('PASS: store.shipment.list returned shipments with order info and state');
|
||||||
|
|
||||||
|
// 11a. Search by order number
|
||||||
|
const listSearchOrder = await store.shipment.list(googleAuthId, { search: 'AU-1002', offset: 0, limit: 10 });
|
||||||
|
assert.equal(listSearchOrder.total, 3);
|
||||||
|
console.log('PASS: store.shipment.list filtered by orderNumber search');
|
||||||
|
|
||||||
|
// 11b. Update shipment state to 'delivered'
|
||||||
|
const updatedSh = await store.shipment.update(googleAuthId, shOne.id, { state: 'delivered' });
|
||||||
|
assert.equal(updatedSh.state, 'delivered');
|
||||||
|
|
||||||
|
// 11c. Filter by state = 'delivered'
|
||||||
|
const listDelivered = await store.shipment.list(googleAuthId, { state: 'delivered', offset: 0, limit: 10 });
|
||||||
|
assert.equal(listDelivered.total, 1);
|
||||||
|
assert.equal(listDelivered.shipments[0].id, shOne.id);
|
||||||
|
console.log('PASS: store.shipment.update set state="delivered" and filtered correctly');
|
||||||
|
|
||||||
|
// 11d. Rebuild shipment summaries
|
||||||
|
await store.shipment.rebuildSummaries(googleAuthId);
|
||||||
|
const listAfterRebuild = await store.shipment.list(googleAuthId, { state: 'delivered', offset: 0, limit: 10 });
|
||||||
|
assert.equal(listAfterRebuild.total, 1);
|
||||||
|
console.log('PASS: store.shipment.rebuildSummaries preserved delivered state and rebuilt correctly');
|
||||||
|
|
||||||
|
console.log('--- ALL TESTS PASSED SUCCESSFULLY! ---');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('TEST FAILED:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import sharp from 'sharp';
|
import { optimizeImage } from 'wasm-image-optimization';
|
||||||
import LibDashStore from '../src/index.js';
|
import LibDashStore from '../src/index.js';
|
||||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ async function runTests() {
|
|||||||
assert(existsSync(join(storageDir, 'fullbinary')), 'fullbinary dir should exist');
|
assert(existsSync(join(storageDir, 'fullbinary')), 'fullbinary dir should exist');
|
||||||
assert(existsSync(join(storageDir, 'thumbbinary')), 'thumbbinary dir should exist');
|
assert(existsSync(join(storageDir, 'thumbbinary')), 'thumbbinary dir should exist');
|
||||||
|
|
||||||
// Verify sqlite tables do NOT contain blob columns
|
// Large file and image blobs stay on disk; the 32px small-thumb stays inline in pic.
|
||||||
const fileTableInfo = provider.db.pragma('table_info("file")');
|
const fileTableInfo = provider.db.pragma('table_info("file")');
|
||||||
const fileColNames = fileTableInfo.map((c) => c.name);
|
const fileColNames = fileTableInfo.map((c) => c.name);
|
||||||
console.log('file table columns:', fileColNames);
|
console.log('file table columns:', fileColNames);
|
||||||
@@ -40,6 +40,9 @@ async function runTests() {
|
|||||||
console.log('pic table columns:', picColNames);
|
console.log('pic table columns:', picColNames);
|
||||||
assert(!picColNames.includes('fullbinary'), 'pic table should NOT have fullbinary column');
|
assert(!picColNames.includes('fullbinary'), 'pic table should NOT have fullbinary column');
|
||||||
assert(!picColNames.includes('thumbbinary'), 'pic table should NOT have thumbbinary column');
|
assert(!picColNames.includes('thumbbinary'), 'pic table should NOT have thumbbinary column');
|
||||||
|
assert(picColNames.includes('smallthumbw'), 'pic table should have smallthumbw column');
|
||||||
|
assert(picColNames.includes('smallthumbh'), 'pic table should have smallthumbh column');
|
||||||
|
assert(picColNames.includes('smallthumbbinary'), 'pic table should have smallthumbbinary column');
|
||||||
|
|
||||||
// Setup base data: google_auth, company, unit, article, manifest
|
// Setup base data: google_auth, company, unit, article, manifest
|
||||||
const googleAuthId = await store.google_auth.add('test@example.com');
|
const googleAuthId = await store.google_auth.add('test@example.com');
|
||||||
@@ -69,16 +72,9 @@ async function runTests() {
|
|||||||
|
|
||||||
const manifestId = delivery.id;
|
const manifestId = delivery.id;
|
||||||
const testContent = Buffer.from('Hello SQLite Storage Engine! PDF or text content test.');
|
const testContent = Buffer.from('Hello SQLite Storage Engine! PDF or text content test.');
|
||||||
const testThumb = await sharp({
|
const thumbSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="rgb(255,0,0)"/></svg>`;
|
||||||
create: {
|
const testThumbRes = await optimizeImage({ image: thumbSvg, format: 'avif' });
|
||||||
width: 64,
|
const testThumb = Buffer.from(testThumbRes.data);
|
||||||
height: 64,
|
|
||||||
channels: 4,
|
|
||||||
background: { r: 255, g: 0, b: 0, alpha: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.avif()
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const attachment = await store.manifestattachment.add(googleAuthId, manifestId, {
|
const attachment = await store.manifestattachment.add(googleAuthId, manifestId, {
|
||||||
name: 'test-doc.txt',
|
name: 'test-doc.txt',
|
||||||
@@ -154,14 +150,27 @@ async function runTests() {
|
|||||||
parentId: cat.id,
|
parentId: cat.id,
|
||||||
name: 'Test Article',
|
name: 'Test Article',
|
||||||
});
|
});
|
||||||
assert.equal(article.track_stock, true, 'Default track_stock must be true');
|
assert.equal(article.track_stock, false, 'Default track_stock must be false');
|
||||||
|
|
||||||
const fetchedArt = await store.article.getBySku(googleAuthId, article.sku);
|
const fetchedArt = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
assert.equal(fetchedArt.article.track_stock, true, 'getBySku must return track_stock: true');
|
assert.equal(fetchedArt.article.track_stock, false, 'getBySku must return track_stock: false');
|
||||||
|
|
||||||
await store.article.setTrackStockBySku(googleAuthId, article.sku, false);
|
// Enabling track_stock without a warehouse must fail
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.article.setTrackStockBySku(googleAuthId, article.sku, true),
|
||||||
|
/At least one warehouse is required to enable stock tracking/,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create warehouse to allow enabling track_stock
|
||||||
|
await store.warehouse.create(googleAuthId, { name: 'Storage WH' });
|
||||||
|
|
||||||
|
await store.article.setTrackStockBySku(googleAuthId, article.sku, true);
|
||||||
const afterSet = await store.article.getBySku(googleAuthId, article.sku);
|
const afterSet = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
assert.equal(afterSet.article.track_stock, false, 'setTrackStockBySku false must update track_stock');
|
assert.equal(afterSet.article.track_stock, true, 'setTrackStockBySku true must update track_stock');
|
||||||
|
|
||||||
|
await store.article.updateBySku(googleAuthId, article.sku, { track_stock: false });
|
||||||
|
const afterDisable = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
|
assert.equal(afterDisable.article.track_stock, false, 'updateBySku track_stock: false must update track_stock');
|
||||||
|
|
||||||
await store.article.updateBySku(googleAuthId, article.sku, { track_stock: true });
|
await store.article.updateBySku(googleAuthId, article.sku, { track_stock: true });
|
||||||
const afterUpdate = await store.article.getBySku(googleAuthId, article.sku);
|
const afterUpdate = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
@@ -186,19 +195,48 @@ async function runTests() {
|
|||||||
|
|
||||||
console.log('Article track_stock tests passed!');
|
console.log('Article track_stock tests passed!');
|
||||||
|
|
||||||
const sampleImage = await sharp({
|
const sampleSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200"><rect width="300" height="200" fill="rgb(0,128,255)"/></svg>`;
|
||||||
create: {
|
const sampleImageRes = await optimizeImage({ image: sampleSvg, format: 'png' });
|
||||||
width: 300,
|
const sampleImage = Buffer.from(sampleImageRes.data);
|
||||||
height: 200,
|
|
||||||
channels: 4,
|
|
||||||
background: { r: 0, g: 128, b: 255, alpha: 1 },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.png()
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage);
|
const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage);
|
||||||
const fullHash = picResult.fullHash;
|
const fullHash = picResult.fullHash;
|
||||||
|
assert.equal(picResult.smallThumb.w, 32);
|
||||||
|
assert.equal(picResult.smallThumb.h, 21);
|
||||||
|
const smallThumbRow = provider.db.prepare(
|
||||||
|
'SELECT smallthumbw, smallthumbh, smallthumbbinary FROM pic WHERE hash = ?',
|
||||||
|
).get(fullHash);
|
||||||
|
assert.equal(smallThumbRow.smallthumbw, 32);
|
||||||
|
assert.equal(smallThumbRow.smallthumbh, 21);
|
||||||
|
assert(Buffer.isBuffer(smallThumbRow.smallthumbbinary), 'Small thumb AVIF must be stored in the pic row');
|
||||||
|
const smallThumbMetadata = await optimizeImage({ image: smallThumbRow.smallthumbbinary, format: 'none' });
|
||||||
|
assert.equal(smallThumbMetadata.originalFormat, 'avif');
|
||||||
|
assert.equal(smallThumbMetadata.originalWidth, 32);
|
||||||
|
assert.equal(smallThumbMetadata.originalHeight, 21);
|
||||||
|
|
||||||
|
const brand = await store.brand.create(googleAuthId, { name: 'Small Thumb Test Brand' });
|
||||||
|
await store.brand.addImage(googleAuthId, brand.id, sampleImage);
|
||||||
|
const listedBrands = await store.brand.list(googleAuthId, { offset: 0, limit: 100 });
|
||||||
|
const listedBrand = listedBrands.brands.find((item) => item.id === brand.id);
|
||||||
|
assert(listedBrand, 'Brand list should include the created brand');
|
||||||
|
assert.equal(listedBrand.pics.length, 1);
|
||||||
|
assert(!('smallThumbW' in listedBrand.pics[0]), 'Small-thumb fields should be omitted by default');
|
||||||
|
|
||||||
|
const listedBrandsWithSmallThumbs = await store.brand.list(googleAuthId, {
|
||||||
|
offset: 0,
|
||||||
|
limit: 100,
|
||||||
|
includeSmallThumbs: true,
|
||||||
|
});
|
||||||
|
const listedBrandWithSmallThumbs = listedBrandsWithSmallThumbs.brands.find((item) => item.id === brand.id);
|
||||||
|
assert(listedBrandWithSmallThumbs, 'Brand list with thumbnails should include the created brand');
|
||||||
|
assert.equal(listedBrandWithSmallThumbs.pics.length, 1);
|
||||||
|
const listedPic = listedBrandWithSmallThumbs.pics[0];
|
||||||
|
assert.equal(listedPic.smallThumbW, 32);
|
||||||
|
assert.equal(listedPic.smallThumbH, 21);
|
||||||
|
assert.equal(typeof listedPic.smallThumbBinary, 'string');
|
||||||
|
const listedSmallThumb = Buffer.from(listedPic.smallThumbBinary, 'base64');
|
||||||
|
assert.deepEqual(listedSmallThumb, smallThumbRow.smallthumbbinary);
|
||||||
|
|
||||||
console.log('Article pic added with hash:', fullHash);
|
console.log('Article pic added with hash:', fullHash);
|
||||||
|
|
||||||
const fullPath = join(storageDir, 'fullbinary', fullHash + '.avif');
|
const fullPath = join(storageDir, 'fullbinary', fullHash + '.avif');
|
||||||
|
|||||||
@@ -40,11 +40,13 @@ async function run() {
|
|||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Component Article 1',
|
name: 'Component Article 1',
|
||||||
|
track_stock: true,
|
||||||
});
|
});
|
||||||
const comp2 = await store.article.createUnderParent({
|
const comp2 = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Component Article 2',
|
name: 'Component Article 2',
|
||||||
|
track_stock: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a bundle article
|
// Create a bundle article
|
||||||
@@ -80,20 +82,29 @@ async function run() {
|
|||||||
assert.equal(wh1BundleStock, 5, 'Bundle stock in Hauptlager should be 5 (min(10/1, 10/2))');
|
assert.equal(wh1BundleStock, 5, 'Bundle stock in Hauptlager should be 5 (min(10/1, 10/2))');
|
||||||
assert.equal(wh2BundleStock, 0, 'Bundle stock in Filiale should be 0');
|
assert.equal(wh2BundleStock, 0, 'Bundle stock in Filiale should be 0');
|
||||||
|
|
||||||
// Book in 3x Stückliste bundle into Filiale
|
// Verify store.stock.adjust rejects adjustment on Stückliste
|
||||||
const adjustResult = await store.stock.adjust(googleAuthId, {
|
await assert.rejects(
|
||||||
sku: bundle.sku,
|
() =>
|
||||||
warehouseId: wh2.id,
|
store.stock.adjust(googleAuthId, {
|
||||||
amount: 3,
|
sku: bundle.sku,
|
||||||
text: 'Production bundle book-in',
|
warehouseId: wh2.id,
|
||||||
});
|
amount: 3,
|
||||||
|
text: 'Attempt to adjust Stückliste directly',
|
||||||
|
}),
|
||||||
|
(err) => {
|
||||||
|
assert.match(err.message, /A Stückliste cannot have manual stock adjustments/);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'Should reject stock adjust on Stückliste bundle'
|
||||||
|
);
|
||||||
|
|
||||||
// Verify adjust result lines
|
// Book components directly into Filiale: 3x comp1, 6x comp2
|
||||||
assert.equal(adjustResult.lines.length, 2, 'Should expand into 2 component lines');
|
await store.stock.adjust(googleAuthId, {
|
||||||
const comp1Line = adjustResult.lines.find((l) => l.sku === comp1.sku);
|
lines: [
|
||||||
const comp2Line = adjustResult.lines.find((l) => l.sku === comp2.sku);
|
{ sku: comp1.sku, warehouseId: wh2.id, amount: 3, text: 'Direct component 1 book-in' },
|
||||||
assert.equal(comp1Line.amount, 3, 'Component 1 should get 3 * 1 = 3');
|
{ sku: comp2.sku, warehouseId: wh2.id, amount: 6, text: 'Direct component 2 book-in' },
|
||||||
assert.equal(comp2Line.amount, 6, 'Component 2 should get 3 * 2 = 6');
|
],
|
||||||
|
});
|
||||||
|
|
||||||
// Verify bundle article itself has NO entries in the ledger
|
// Verify bundle article itself has NO entries in the ledger
|
||||||
const bundleLedger = await store.ledger.listBySku(googleAuthId, bundle.sku);
|
const bundleLedger = await store.ledger.listBySku(googleAuthId, bundle.sku);
|
||||||
@@ -112,17 +123,17 @@ async function run() {
|
|||||||
const wh2AfterBookIn = afterBundleBookIn.article.warehouses.find((w) => w.id === wh2.id).stock;
|
const wh2AfterBookIn = afterBundleBookIn.article.warehouses.find((w) => w.id === wh2.id).stock;
|
||||||
assert.equal(wh2AfterBookIn, 3, 'Bundle stock in Filiale should be 3');
|
assert.equal(wh2AfterBookIn, 3, 'Bundle stock in Filiale should be 3');
|
||||||
|
|
||||||
// Test booking out 1x bundle from Filiale
|
// Test booking out components from Filiale: -1x comp1, -2x comp2 (equivalent to 1 bundle sale)
|
||||||
await store.stock.adjust(googleAuthId, {
|
await store.stock.adjust(googleAuthId, {
|
||||||
sku: bundle.sku,
|
lines: [
|
||||||
warehouseId: wh2.id,
|
{ sku: comp1.sku, warehouseId: wh2.id, amount: -1, text: 'Component 1 book-out' },
|
||||||
amount: -1,
|
{ sku: comp2.sku, warehouseId: wh2.id, amount: -2, text: 'Component 2 book-out' },
|
||||||
text: 'Sale bundle book-out',
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const afterRemoval = await store.article.getBySku(googleAuthId, bundle.sku);
|
const afterRemoval = await store.article.getBySku(googleAuthId, bundle.sku);
|
||||||
const wh2AfterRemoval = afterRemoval.article.warehouses.find((w) => w.id === wh2.id).stock;
|
const wh2AfterRemoval = afterRemoval.article.warehouses.find((w) => w.id === wh2.id).stock;
|
||||||
assert.equal(wh2AfterRemoval, 2, 'Bundle stock in Filiale should be 2 after -1 removal');
|
assert.equal(wh2AfterRemoval, 2, 'Bundle stock in Filiale should be 2 after component removal');
|
||||||
|
|
||||||
// Verify components stock after removal in Filiale: comp1 = 2, comp2 = 4
|
// Verify components stock after removal in Filiale: comp1 = 2, comp2 = 4
|
||||||
const comp1After = await store.article.getBySku(googleAuthId, comp1.sku);
|
const comp1After = await store.article.getBySku(googleAuthId, comp1.sku);
|
||||||
|
|||||||
436
scripts/test-syncqueue.js
Normal file
436
scripts/test-syncqueue.js
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
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 posterminalsyncqueue table and sync queue flow ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-syncqueue-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
const clearQueue = () => {
|
||||||
|
provider.db.prepare('DELETE FROM posterminalsyncqueue').run();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQueue = () =>
|
||||||
|
provider.db
|
||||||
|
.prepare('SELECT posterminal_id, entity_type, entity_id FROM posterminalsyncqueue ORDER BY id ASC')
|
||||||
|
.all();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Verify posterminalsyncqueue table exists and has expected columns
|
||||||
|
console.log('1. Checking posterminalsyncqueue schema...');
|
||||||
|
const tableInfo = provider.db.pragma('table_info("posterminalsyncqueue")');
|
||||||
|
const colNames = tableInfo.map((c) => c.name);
|
||||||
|
console.log('posterminalsyncqueue columns:', colNames);
|
||||||
|
assert.deepEqual(colNames, [
|
||||||
|
'id',
|
||||||
|
'posterminal_id',
|
||||||
|
'entity_type',
|
||||||
|
'entity_id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Setup company & 2 POS terminals + another company with 1 terminal (to test company scoping)
|
||||||
|
console.log('2. Setting up base data with 2 terminals in company 1 and 1 terminal in company 2...');
|
||||||
|
const googleAuthId = await store.google_auth.add('syncqueue-tester@example.com');
|
||||||
|
const company1 = await store.company.create({
|
||||||
|
name: 'SyncQueue Corp 1',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
const company2 = await store.company.create({
|
||||||
|
name: 'SyncQueue Corp 2',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company2.id);
|
||||||
|
const otherTerminal = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'Other Corp Terminal',
|
||||||
|
pairingpin: '9999',
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company1.id);
|
||||||
|
const term1 = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'POS Terminal 1',
|
||||||
|
pairingpin: '1111',
|
||||||
|
});
|
||||||
|
const term2 = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'POS Terminal 2',
|
||||||
|
pairingpin: '2222',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to assert that both term1 and term2 received the expected entity_type + entity_id, and otherTerminal did not
|
||||||
|
const assertQueuedForBothTerminals = (entityType, entityId, label) => {
|
||||||
|
const rows = getQueue();
|
||||||
|
const matching = rows.filter((r) => r.entity_type === entityType && r.entity_id === entityId);
|
||||||
|
const termIds = matching.map((r) => r.posterminal_id).sort((a, b) => a - b);
|
||||||
|
assert.deepEqual(
|
||||||
|
termIds,
|
||||||
|
[term1.id, term2.id],
|
||||||
|
`Expected [${term1.id}, ${term2.id}] for ${label} (${entityType}:${entityId}), got ${JSON.stringify(rows)}`,
|
||||||
|
);
|
||||||
|
const otherRows = rows.filter((r) => r.posterminal_id === otherTerminal.id);
|
||||||
|
assert.equal(otherRows.length, 0, `Other company terminal should not receive rows (${label})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Test Category operations
|
||||||
|
console.log('3. Testing category operations...');
|
||||||
|
clearQueue();
|
||||||
|
const catA = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company1.rootCategoryId,
|
||||||
|
name: 'Category A',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('category', catA.id, 'category.createUnderParent');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.category.rename({
|
||||||
|
googleAuthId,
|
||||||
|
id: catA.id,
|
||||||
|
name: 'Category A Renamed',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('category', catA.id, 'category.rename');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
const catLeafId = await store.category.ensureByPath({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company1.rootCategoryId,
|
||||||
|
path: 'Category B | Subcategory B1',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('category', catLeafId, 'category.ensureByPath');
|
||||||
|
|
||||||
|
const catBRow = provider.db
|
||||||
|
.prepare('SELECT c.id FROM category c WHERE c.parent_id = ? AND c.id != ?')
|
||||||
|
.get(company1.rootCategoryId, catA.id);
|
||||||
|
clearQueue();
|
||||||
|
await store.category.order({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company1.rootCategoryId,
|
||||||
|
categoryIds: [catBRow.id, catA.id],
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('category', catA.id, 'category.order');
|
||||||
|
assertQueuedForBothTerminals('category', catBRow.id, 'category.order (sibling)');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.category.move({
|
||||||
|
googleAuthId,
|
||||||
|
id: catLeafId,
|
||||||
|
parentId: catA.id,
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('category', catLeafId, 'category.move');
|
||||||
|
|
||||||
|
const prop = await store.propertyname.create(googleAuthId, { name: 'Color', exclusive: false });
|
||||||
|
clearQueue();
|
||||||
|
await store.categoryproperty.setByCategoryId(googleAuthId, catA.id, [
|
||||||
|
{ propertyId: prop.id, value: 'Red' },
|
||||||
|
]);
|
||||||
|
assertQueuedForBothTerminals('category', catA.id, 'categoryproperty.setByCategoryId');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.categoryproperty.delete(googleAuthId, catA.id, prop.id);
|
||||||
|
assertQueuedForBothTerminals('category', catA.id, 'categoryproperty.delete');
|
||||||
|
|
||||||
|
const attr = await store.attributename.create(googleAuthId, { name: 'Size', exclusive: true });
|
||||||
|
const attrVal = await store.attributevalue.create(googleAuthId, attr.id, { value: 'XL' });
|
||||||
|
clearQueue();
|
||||||
|
await store.categoryattributevalue.setByCategoryId(googleAuthId, catA.id, [attrVal.id]);
|
||||||
|
assertQueuedForBothTerminals('category', catA.id, 'categoryattributevalue.setByCategoryId');
|
||||||
|
|
||||||
|
// 4. Test Article operations
|
||||||
|
console.log('4. Testing article operations...');
|
||||||
|
clearQueue();
|
||||||
|
const art1 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: catA.id,
|
||||||
|
name: 'Cola 0.33L',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'article.createUnderParent');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.article.updateBySku(googleAuthId, art1.sku, { name: 'Cola Classic 0.33L' });
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'article.updateBySku');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.article.setPricesBySku(googleAuthId, art1.sku, [{ net: 1.5, taxRate: 19 }]);
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'article.setPricesBySku');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.article.setEansBySku(googleAuthId, art1.sku, ['4001234567890']);
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'article.setEansBySku');
|
||||||
|
|
||||||
|
const brand = await store.brand.create(googleAuthId, { name: 'ColaBrand' });
|
||||||
|
clearQueue();
|
||||||
|
await store.article.setBrandIdBySku(googleAuthId, art1.sku, brand.id);
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'article.setBrandIdBySku');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.brand.update(googleAuthId, brand.id, { name: 'ColaBrand Updated' });
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'brand.update');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.article.setWeightBySku(googleAuthId, art1.sku, 0.35);
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'article.setWeightBySku');
|
||||||
|
|
||||||
|
const wh = await store.warehouse.create(googleAuthId, { name: 'Main Warehouse' });
|
||||||
|
clearQueue();
|
||||||
|
await store.article.setTrackStockBySku(googleAuthId, art1.sku, true);
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'article.setTrackStockBySku');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: art1.sku,
|
||||||
|
warehouseId: wh.id,
|
||||||
|
amount: 10,
|
||||||
|
text: 'Initial stock',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'stock.adjust');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.article.setCategoryIdsBySku(googleAuthId, art1.sku, [catA.id, catLeafId]);
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'article.setCategoryIdsBySku');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.articleproperty.setByArticleSku(googleAuthId, art1.sku, [
|
||||||
|
{ propertyId: prop.id, value: 'Dark' },
|
||||||
|
]);
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'articleproperty.setByArticleSku');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.articleproperty.delete(googleAuthId, art1.sku, prop.id);
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'articleproperty.delete');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.articleattributevalue.setByArticleSku(googleAuthId, art1.sku, [attrVal.id]);
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'articleattributevalue.setByArticleSku');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.attributevalue.update(googleAuthId, attrVal.id, { value: 'XXL' });
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'attributevalue.update');
|
||||||
|
|
||||||
|
// Optionslist & Stückliste
|
||||||
|
const pfand = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: catA.id,
|
||||||
|
name: 'Pfand 0.25',
|
||||||
|
});
|
||||||
|
clearQueue();
|
||||||
|
await store.optionslist.setOptionsByArticleSku(googleAuthId, art1.sku, [
|
||||||
|
{ sku: pfand.sku, count: 1, mandated: true },
|
||||||
|
]);
|
||||||
|
assertQueuedForBothTerminals('article', art1.sku ? art1.id : 0, 'optionslist.setOptionsByArticleSku');
|
||||||
|
|
||||||
|
// Updating option article price should also enqueue parent article!
|
||||||
|
clearQueue();
|
||||||
|
await store.article.setPricesBySku(googleAuthId, pfand.sku, [{ net: 0.25, taxRate: 19 }]);
|
||||||
|
assertQueuedForBothTerminals('article', pfand.id, 'setPricesBySku (option itself)');
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'setPricesBySku (option parent)');
|
||||||
|
|
||||||
|
const bundle = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: catA.id,
|
||||||
|
name: 'Cola Crate',
|
||||||
|
});
|
||||||
|
clearQueue();
|
||||||
|
await store.articlelist.setItemsByArticleSku(googleAuthId, bundle.sku, [
|
||||||
|
{ sku: art1.sku, count: 6 },
|
||||||
|
]);
|
||||||
|
assertQueuedForBothTerminals('article', bundle.id, 'articlelist.setItemsByArticleSku');
|
||||||
|
|
||||||
|
// Adjusting component stock should also enqueue Stückliste parent!
|
||||||
|
clearQueue();
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: art1.sku,
|
||||||
|
warehouseId: wh.id,
|
||||||
|
amount: 2,
|
||||||
|
text: 'Restock component',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('article', art1.id, 'stock.adjust (component)');
|
||||||
|
assertQueuedForBothTerminals('article', bundle.id, 'stock.adjust (Stückliste parent)');
|
||||||
|
|
||||||
|
// 5. Test Customer Group operations
|
||||||
|
console.log('5. Testing customergroup operations...');
|
||||||
|
clearQueue();
|
||||||
|
const cg1 = await store.customergroup.create(googleAuthId, { name: 'B2B' });
|
||||||
|
assertQueuedForBothTerminals('customergroup', cg1.id, 'customergroup.create');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.customergroup.update(googleAuthId, cg1.id, { name: 'B2B Gold' });
|
||||||
|
assertQueuedForBothTerminals('customergroup', cg1.id, 'customergroup.update');
|
||||||
|
|
||||||
|
const allGroups = await store.customergroup.list(googleAuthId);
|
||||||
|
clearQueue();
|
||||||
|
await store.customergroup.order(googleAuthId, {
|
||||||
|
customergroupIds: allGroups.map((g) => g.id).reverse(),
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('customergroup', cg1.id, 'customergroup.order');
|
||||||
|
|
||||||
|
// 6. Test Customer (Entity + Address + Contact) operations
|
||||||
|
console.log('6. Testing customer (entity, entityaddress, entitycontact) operations...');
|
||||||
|
clearQueue();
|
||||||
|
const cust = await store.entity.create(googleAuthId, {
|
||||||
|
customergroupId: cg1.id,
|
||||||
|
name: 'Doe',
|
||||||
|
firstName: 'John',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('customer', cust.id, 'entity.create');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.entity.update(googleAuthId, cust.id, { firstName: 'Johnny' });
|
||||||
|
assertQueuedForBothTerminals('customer', cust.id, 'entity.update');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
const addr = await store.entityaddress.create(googleAuthId, cust.id, {
|
||||||
|
addressType: 'billing',
|
||||||
|
street: 'Main St 1',
|
||||||
|
city: 'Berlin',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('customer', cust.id, 'entityaddress.create');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.entityaddress.update(googleAuthId, cust.id, addr.id, {
|
||||||
|
city: 'Munich',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('customer', cust.id, 'entityaddress.update');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.entityaddress.delete(googleAuthId, cust.id, addr.id);
|
||||||
|
assertQueuedForBothTerminals('customer', cust.id, 'entityaddress.delete');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
const contact = await store.entitycontact.create(googleAuthId, cust.id, {
|
||||||
|
contactType: 'email',
|
||||||
|
contact: 'john@example.com',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('customer', cust.id, 'entitycontact.create');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.entitycontact.update(googleAuthId, cust.id, contact.id, {
|
||||||
|
contact: 'johnny@example.com',
|
||||||
|
});
|
||||||
|
assertQueuedForBothTerminals('customer', cust.id, 'entitycontact.update');
|
||||||
|
|
||||||
|
clearQueue();
|
||||||
|
await store.entitycontact.delete(googleAuthId, cust.id, contact.id);
|
||||||
|
assertQueuedForBothTerminals('customer', cust.id, 'entitycontact.delete');
|
||||||
|
|
||||||
|
// 7. Test CHECK constraint on entity_type
|
||||||
|
console.log('7. Testing entity_type CHECK constraint...');
|
||||||
|
assert.throws(() => {
|
||||||
|
provider.db
|
||||||
|
.prepare('INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) VALUES (?, ?, ?)')
|
||||||
|
.run(term1.id, 'invalid_type', 1);
|
||||||
|
}, /CHECK constraint failed/);
|
||||||
|
|
||||||
|
// 8. Test POS Sync Cursor (posterminalsyncqueue.id) across counts and lists
|
||||||
|
console.log('8. Testing POS Sync Cursor (posterminalsyncqueue.id) for counts & lists...');
|
||||||
|
// Create a fresh terminal (term3) and enable all categories on it to verify full initial sync + delta cursor
|
||||||
|
const term3 = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'POS Terminal 3',
|
||||||
|
pairingpin: '3333',
|
||||||
|
});
|
||||||
|
await store.posterminal.setOnAll(googleAuthId, term3.id);
|
||||||
|
|
||||||
|
// Initial sync with cursor = 0
|
||||||
|
const initProductCount = await store.posterminal.getProductCount({ posterminalId: term3.id, cursor: 0 });
|
||||||
|
const initCategoryCount = await store.posterminal.getCategoryCount({ posterminalId: term3.id, cursor: 0 });
|
||||||
|
const initCustomerCount = await store.posterminal.getCustomerCount({ posterminalId: term3.id, cursor: 0 });
|
||||||
|
const initCustomerGroupCount = await store.posterminal.getCustomerGroupCount({ posterminalId: term3.id, cursor: 0 });
|
||||||
|
const initCompositeCount = await store.posterminal.getCompositeProductCount({ posterminalId: term3.id, cursor: 0 });
|
||||||
|
|
||||||
|
assert.ok(initProductCount > 0, `Expected initProductCount > 0, got ${initProductCount}`);
|
||||||
|
assert.ok(initCategoryCount > 0, `Expected initCategoryCount > 0, got ${initCategoryCount}`);
|
||||||
|
assert.ok(initCustomerCount > 0, `Expected initCustomerCount > 0, got ${initCustomerCount}`);
|
||||||
|
assert.ok(initCustomerGroupCount > 0, `Expected initCustomerGroupCount > 0, got ${initCustomerGroupCount}`);
|
||||||
|
assert.ok(initCompositeCount > 0, `Expected initCompositeCount > 0, got ${initCompositeCount}`);
|
||||||
|
|
||||||
|
const products = await store.posterminal.getProductList({ posterminalId: term3.id, cursor: 0, limit: 100 });
|
||||||
|
const categories = await store.posterminal.getCategoryList({ posterminalId: term3.id, cursor: 0, limit: 100 });
|
||||||
|
const customers = await store.posterminal.getCustomerList({ posterminalId: term3.id, cursor: 0, limit: 100 });
|
||||||
|
const customerGroups = await store.posterminal.getCustomerGroupList({ posterminalId: term3.id, cursor: 0, limit: 100 });
|
||||||
|
|
||||||
|
assert.equal(products.length, initProductCount);
|
||||||
|
assert.equal(categories.length, initCategoryCount);
|
||||||
|
assert.equal(customers.length, initCustomerCount);
|
||||||
|
assert.equal(customerGroups.length, initCustomerGroupCount);
|
||||||
|
|
||||||
|
// Advance cursors to max lastChanged
|
||||||
|
const maxProductCursor = Math.max(...products.map((p) => Number(p.lastChanged)));
|
||||||
|
const maxCategoryCursor = Math.max(...categories.map((c) => Number(c.lastChanged)));
|
||||||
|
const maxCustomerCursor = Math.max(...customers.map((c) => Number(c.lastChanged)));
|
||||||
|
const maxCustomerGroupCursor = Math.max(...customerGroups.map((cg) => Number(cg.lastChanged)));
|
||||||
|
|
||||||
|
// Verify all counts and lists are 0 at the current cursors
|
||||||
|
assert.equal(await store.posterminal.getProductCount({ posterminalId: term3.id, cursor: maxProductCursor }), 0);
|
||||||
|
assert.equal(await store.posterminal.getCategoryCount({ posterminalId: term3.id, cursor: maxCategoryCursor }), 0);
|
||||||
|
assert.equal(await store.posterminal.getCustomerCount({ posterminalId: term3.id, cursor: maxCustomerCursor }), 0);
|
||||||
|
assert.equal(await store.posterminal.getCustomerGroupCount({ posterminalId: term3.id, cursor: maxCustomerGroupCursor }), 0);
|
||||||
|
assert.equal(await store.posterminal.getCompositeProductCount({ posterminalId: term3.id, cursor: maxProductCursor }), 0);
|
||||||
|
|
||||||
|
assert.equal((await store.posterminal.getProductList({ posterminalId: term3.id, cursor: maxProductCursor })).length, 0);
|
||||||
|
assert.equal((await store.posterminal.getCategoryList({ posterminalId: term3.id, cursor: maxCategoryCursor })).length, 0);
|
||||||
|
assert.equal((await store.posterminal.getCustomerList({ posterminalId: term3.id, cursor: maxCustomerCursor })).length, 0);
|
||||||
|
assert.equal((await store.posterminal.getCustomerGroupList({ posterminalId: term3.id, cursor: maxCustomerGroupCursor })).length, 0);
|
||||||
|
|
||||||
|
// Now touch one article twice, one category, one customer, one customergroup
|
||||||
|
await store.article.updateBySku(googleAuthId, art1.sku, { name: 'Cola Classic v2' });
|
||||||
|
await store.article.updateBySku(googleAuthId, art1.sku, { name: 'Cola Classic v3' });
|
||||||
|
await store.category.rename({ googleAuthId, id: catA.id, name: 'Category A v2' });
|
||||||
|
await store.entity.update(googleAuthId, cust.id, { firstName: 'John v2' });
|
||||||
|
await store.customergroup.update(googleAuthId, cg1.id, { name: 'B2B Platinum' });
|
||||||
|
|
||||||
|
// Even though art1 was updated twice, count should be 1 (deduplicated by entity_id with MAX(id) > cursor)
|
||||||
|
assert.equal(await store.posterminal.getProductCount({ posterminalId: term3.id, cursor: maxProductCursor }), 1);
|
||||||
|
assert.equal(await store.posterminal.getCategoryCount({ posterminalId: term3.id, cursor: maxCategoryCursor }), 1);
|
||||||
|
assert.equal(await store.posterminal.getCustomerCount({ posterminalId: term3.id, cursor: maxCustomerCursor }), 1);
|
||||||
|
assert.equal(await store.posterminal.getCustomerGroupCount({ posterminalId: term3.id, cursor: maxCustomerGroupCursor }), 1);
|
||||||
|
|
||||||
|
const deltaProducts = await store.posterminal.getProductList({ posterminalId: term3.id, cursor: maxProductCursor });
|
||||||
|
const deltaCategories = await store.posterminal.getCategoryList({ posterminalId: term3.id, cursor: maxCategoryCursor });
|
||||||
|
const deltaCustomers = await store.posterminal.getCustomerList({ posterminalId: term3.id, cursor: maxCustomerCursor });
|
||||||
|
const deltaCustomerGroups = await store.posterminal.getCustomerGroupList({ posterminalId: term3.id, cursor: maxCustomerGroupCursor });
|
||||||
|
|
||||||
|
assert.equal(deltaProducts.length, 1);
|
||||||
|
assert.equal(deltaProducts[0]._id, String(art1.id));
|
||||||
|
assert.ok(Number(deltaProducts[0].lastChanged) > maxProductCursor);
|
||||||
|
|
||||||
|
assert.equal(deltaCategories.length, 1);
|
||||||
|
assert.equal(deltaCategories[0]._id, String(catA.id));
|
||||||
|
assert.ok(Number(deltaCategories[0].lastChanged) > maxCategoryCursor);
|
||||||
|
|
||||||
|
assert.equal(deltaCustomers.length, 1);
|
||||||
|
assert.equal(deltaCustomers[0].id, String(cust.id));
|
||||||
|
assert.ok(Number(deltaCustomers[0].lastChanged) > maxCustomerCursor);
|
||||||
|
|
||||||
|
assert.equal(deltaCustomerGroups.length, 1);
|
||||||
|
assert.equal(deltaCustomerGroups[0].customerGroupId, String(cg1.id));
|
||||||
|
assert.ok(Number(deltaCustomerGroups[0].lastChanged) > maxCustomerGroupCursor);
|
||||||
|
|
||||||
|
console.log('✅ ALL POSTERMINALSYNCQUEUE TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
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);
|
||||||
|
});
|
||||||
147
scripts/test-unique-slugs.js
Normal file
147
scripts/test-unique-slugs.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
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 Unique Slugs Disambiguation ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-slugs-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('slug-tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Slug Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
console.log('1. Testing createUnderParent duplicate slugs...');
|
||||||
|
const art1 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Root Parent Shirt',
|
||||||
|
});
|
||||||
|
const art1Res = await store.article.getBySku(googleAuthId, art1.sku);
|
||||||
|
assert.equal(art1Res.article.slug, 'root-parent-shirt', `Expected slug 'root-parent-shirt', got '${art1Res.article.slug}'`);
|
||||||
|
|
||||||
|
const art2 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Root Parent Shirt',
|
||||||
|
});
|
||||||
|
const art2Res = await store.article.getBySku(googleAuthId, art2.sku);
|
||||||
|
assert.equal(art2Res.article.slug, 'root-parent-shirt-2', `Expected slug 'root-parent-shirt-2', got '${art2Res.article.slug}'`);
|
||||||
|
|
||||||
|
const art3 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Root Parent Shirt',
|
||||||
|
});
|
||||||
|
const art3Res = await store.article.getBySku(googleAuthId, art3.sku);
|
||||||
|
assert.equal(art3Res.article.slug, 'root-parent-shirt-3', `Expected slug 'root-parent-shirt-3', got '${art3Res.article.slug}'`);
|
||||||
|
|
||||||
|
console.log('2. Testing updateBySku self-collision and renaming...');
|
||||||
|
// Update art2 without changing name
|
||||||
|
await store.article.updateBySku(googleAuthId, art2.sku, { unitamount: 2 });
|
||||||
|
let updated2 = await store.article.getBySku(googleAuthId, art2.sku);
|
||||||
|
assert.equal(updated2.article.slug, 'root-parent-shirt-2', `Expected unchanged slug, got '${updated2.article.slug}'`);
|
||||||
|
|
||||||
|
// Update art2 with same name
|
||||||
|
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Root Parent Shirt' });
|
||||||
|
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
|
||||||
|
assert.equal(updated2.article.slug, 'root-parent-shirt-2', `Expected slug to remain 'root-parent-shirt-2', got '${updated2.article.slug}'`);
|
||||||
|
|
||||||
|
// Rename art2 to something else
|
||||||
|
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Unique Shirt' });
|
||||||
|
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
|
||||||
|
assert.equal(updated2.article.slug, 'unique-shirt', `Expected slug 'unique-shirt', got '${updated2.article.slug}'`);
|
||||||
|
|
||||||
|
// Rename art3 to 'Root Parent Shirt' again: should take root-parent-shirt-2 now that it is free
|
||||||
|
await store.article.updateBySku(googleAuthId, art3.sku, { name: 'Root Parent Shirt' });
|
||||||
|
const updated3 = await store.article.getBySku(googleAuthId, art3.sku);
|
||||||
|
assert.equal(updated3.article.slug, 'root-parent-shirt-2', `Expected slug to fill gap 'root-parent-shirt-2', got '${updated3.article.slug}'`);
|
||||||
|
|
||||||
|
console.log('3. Testing addChild duplicate slugs...');
|
||||||
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
|
||||||
|
await store.article.toggleParent(googleAuthId, art1.sku, {
|
||||||
|
isParent: true,
|
||||||
|
attributeNameId: attrName.id,
|
||||||
|
});
|
||||||
|
const child1 = await store.article.addChild(googleAuthId, art1.sku, {
|
||||||
|
value: 'M',
|
||||||
|
});
|
||||||
|
assert.equal(child1.article.slug, 'root-parent-shirt-m', `Expected child slug 'root-parent-shirt-m', got '${child1.article.slug}'`);
|
||||||
|
|
||||||
|
// Add another child with name that yields same slug
|
||||||
|
const child2 = await store.article.addChild(googleAuthId, art1.sku, {
|
||||||
|
value: 'L',
|
||||||
|
name: 'Root Parent Shirt - M',
|
||||||
|
});
|
||||||
|
assert.equal(child2.article.slug, 'root-parent-shirt-m-2', `Expected child slug 'root-parent-shirt-m-2', got '${child2.article.slug}'`);
|
||||||
|
|
||||||
|
console.log('4. Testing importRows duplicate slugs in same batch...');
|
||||||
|
const importRes = await store.article.importRows({
|
||||||
|
googleAuthId,
|
||||||
|
rows: [
|
||||||
|
{ sku: 'IMP-1', name: 'Batch Shirt', parentId: company.rootCategoryId },
|
||||||
|
{ sku: 'IMP-2', name: 'Batch Shirt', parentId: company.rootCategoryId },
|
||||||
|
{ sku: 'IMP-3', name: 'Batch Shirt', parentId: company.rootCategoryId },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(importRes.errors.length, 0, `Import had errors: ${JSON.stringify(importRes.errors)}`);
|
||||||
|
assert.equal(importRes.imported.length, 3);
|
||||||
|
|
||||||
|
const imp1 = await store.article.getBySku(googleAuthId, 'IMP-1');
|
||||||
|
const imp2 = await store.article.getBySku(googleAuthId, 'IMP-2');
|
||||||
|
const imp3 = await store.article.getBySku(googleAuthId, 'IMP-3');
|
||||||
|
|
||||||
|
assert.equal(imp1.article.slug, 'batch-shirt');
|
||||||
|
assert.equal(imp2.article.slug, 'batch-shirt-2');
|
||||||
|
assert.equal(imp3.article.slug, 'batch-shirt-3');
|
||||||
|
|
||||||
|
console.log('5. Testing company isolation...');
|
||||||
|
const company2 = await store.company.create({
|
||||||
|
name: 'Other Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company2.id);
|
||||||
|
|
||||||
|
const artCompany2 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company2.rootCategoryId,
|
||||||
|
name: 'Root Parent Shirt',
|
||||||
|
});
|
||||||
|
const artCompany2Detail = await store.article.getBySku(googleAuthId, artCompany2.sku);
|
||||||
|
assert.equal(artCompany2Detail.article.slug, 'root-parent-shirt', `Expected 'root-parent-shirt' in company 2, got '${artCompany2Detail.article.slug}'`);
|
||||||
|
|
||||||
|
console.log('✅ ALL SLUG TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
160
scripts/test-variant-flow.js
Normal file
160
scripts/test-variant-flow.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
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 Variant Flow: Next-Free SKU, Parentsort Reclaim, and Duplicate Protection ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-variants-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('variant-tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Variant Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// Create parent article
|
||||||
|
const parent = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'T-Shirt',
|
||||||
|
});
|
||||||
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
|
||||||
|
await store.article.toggleParent(googleAuthId, parent.sku, {
|
||||||
|
isParent: true,
|
||||||
|
attributeNameId: attrName.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('1. Add initial children (Rot, Blau, Grün)...');
|
||||||
|
const child1 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Rot' });
|
||||||
|
const child2 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Blau' });
|
||||||
|
const child3 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Grün' });
|
||||||
|
|
||||||
|
assert.equal(child1.article.sku, `${parent.sku}-1`);
|
||||||
|
assert.equal(child2.article.sku, `${parent.sku}-2`);
|
||||||
|
assert.equal(child3.article.sku, `${parent.sku}-3`);
|
||||||
|
|
||||||
|
let list = await store.article.listChildren(googleAuthId, parent.sku);
|
||||||
|
assert.equal(list.length, 3);
|
||||||
|
assert.deepEqual(list.map((c) => c.parentsort), [0, 1, 2]);
|
||||||
|
|
||||||
|
console.log('2. Testing Duplicate Variant Protection (Defect 3)...');
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.article.addChild(googleAuthId, parent.sku, { value: 'Rot' });
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
assert.match(err.message, /Variant with this attribute value already exists/);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'Expected duplicate variant attribute value to be rejected'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('3. Unlink middle child (child2 / SKU-2) -> leaves orphan holding SKU-2 (Bug A & B)...');
|
||||||
|
await store.article.removeChild(googleAuthId, parent.sku, child2.article.sku, {
|
||||||
|
deleteArticle: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that remaining children's parentsort was recompacted
|
||||||
|
list = await store.article.listChildren(googleAuthId, parent.sku);
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
assert.deepEqual(list.map((c) => c.sku), [child1.article.sku, child3.article.sku]);
|
||||||
|
assert.deepEqual(list.map((c) => c.parentsort), [0, 1]);
|
||||||
|
|
||||||
|
// Check orphan still exists in DB
|
||||||
|
const orphan = await store.article.getBySku(googleAuthId, child2.article.sku);
|
||||||
|
assert.equal(orphan.article.parentArticleId, null);
|
||||||
|
assert.equal(orphan.article.sku, `${parent.sku}-2`);
|
||||||
|
|
||||||
|
console.log('4. Add new child (Gelb) -> SKU must NOT collide with orphan (Bug A: next free index)...');
|
||||||
|
const child4 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Gelb' });
|
||||||
|
// Since SKU-1, SKU-2 (orphan), SKU-3 exist, next free index is 4
|
||||||
|
assert.equal(child4.article.sku, `${parent.sku}-4`);
|
||||||
|
|
||||||
|
list = await store.article.listChildren(googleAuthId, parent.sku);
|
||||||
|
assert.equal(list.length, 3);
|
||||||
|
assert.deepEqual(list.map((c) => c.parentsort), [0, 1, 2]);
|
||||||
|
|
||||||
|
console.log('5. Testing gaps in parentsort (Evidence: parent with child at parentsort: 1)...');
|
||||||
|
// Unlink child3 and child4 so only child1 remains
|
||||||
|
await store.article.removeChild(googleAuthId, parent.sku, child3.article.sku, { deleteArticle: true });
|
||||||
|
await store.article.removeChild(googleAuthId, parent.sku, child4.article.sku, { deleteArticle: true });
|
||||||
|
|
||||||
|
// Manually force child1's parentsort: 1 to simulate pre-existing gap state (like parent 94)
|
||||||
|
await provider.query(
|
||||||
|
`UPDATE article SET parentsort = 1 WHERE sku = @sku`,
|
||||||
|
{ sku: child1.article.sku }
|
||||||
|
);
|
||||||
|
list = await store.article.listChildren(googleAuthId, parent.sku);
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list[0].parentsort, 1); // exactly like parent 94
|
||||||
|
|
||||||
|
// Now adding a variant MUST NOT hit UNIQUE constraint failed (409)
|
||||||
|
console.log('Adding variant to parent with child at parentsort: 1...');
|
||||||
|
const child5 = await store.article.addChild(googleAuthId, parent.sku, { value: 'Schwarz' });
|
||||||
|
assert.ok(child5);
|
||||||
|
list = await store.article.listChildren(googleAuthId, parent.sku);
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
// Parentsort should have picked 0 (next free non-negative integer)
|
||||||
|
assert.deepEqual(list.map((c) => c.parentsort), [0, 1]);
|
||||||
|
|
||||||
|
console.log('6. Testing rejecting sku when creating via addChild...');
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.article.addChild(googleAuthId, 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');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
334
src/index.js
334
src/index.js
@@ -1,10 +1,21 @@
|
|||||||
import companyuserGetByEmail from './store/companyuser/getByEmail.js';
|
import companyuserGetByEmail from './store/companyuser/getByEmail.js';
|
||||||
import companyuserAdd from './store/companyuser/add.js';
|
import companyuserAdd from './store/companyuser/add.js';
|
||||||
import companyuserDelete from './store/companyuser/delete.js';
|
import companyuserDelete from './store/companyuser/delete.js';
|
||||||
|
import companyuserList from './store/companyuser/list.js';
|
||||||
|
import companyuserAddByEmail from './store/companyuser/addByEmail.js';
|
||||||
|
import companyuserSetRole from './store/companyuser/setRole.js';
|
||||||
|
import companyuserRemove from './store/companyuser/remove.js';
|
||||||
|
import autoinviteAdd from './store/autoinvite/add.js';
|
||||||
|
import autoinviteList from './store/autoinvite/list.js';
|
||||||
|
import autoinviteRemove from './store/autoinvite/remove.js';
|
||||||
import companyCreate from './store/company/create.js';
|
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 companyGetById from './store/company/getById.js';
|
||||||
|
import companyGetByName from './store/company/getByName.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';
|
||||||
@@ -14,6 +25,7 @@ import categoryOrder from './store/category/order.js';
|
|||||||
import categoryDelete from './store/category/delete.js';
|
import categoryDelete from './store/category/delete.js';
|
||||||
import categoryAddImage from './store/category/addImage.js';
|
import categoryAddImage from './store/category/addImage.js';
|
||||||
import categoryDeleteImage from './store/category/deleteImage.js';
|
import categoryDeleteImage from './store/category/deleteImage.js';
|
||||||
|
import categoryGenerateImage from './store/category/generateImage.js';
|
||||||
import articleDelete from './store/article/delete.js';
|
import articleDelete from './store/article/delete.js';
|
||||||
import articleDeleteImage from './store/article/deleteImage.js';
|
import articleDeleteImage from './store/article/deleteImage.js';
|
||||||
import articleOrderImages from './store/article/orderImages.js';
|
import articleOrderImages from './store/article/orderImages.js';
|
||||||
@@ -21,6 +33,7 @@ import articleAddImage from './store/article/addImage.js';
|
|||||||
import articleCreateUnderParent from './store/article/createUnderParent.js';
|
import articleCreateUnderParent from './store/article/createUnderParent.js';
|
||||||
import articleGetBySku from './store/article/getBySku.js';
|
import articleGetBySku from './store/article/getBySku.js';
|
||||||
import articleGetHistoryBySku from './store/article/getHistoryBySku.js';
|
import articleGetHistoryBySku from './store/article/getHistoryBySku.js';
|
||||||
|
import articleListHistory from './store/article/listHistory.js';
|
||||||
import articleListSearchIndex from './store/article/listSearchIndex.js';
|
import articleListSearchIndex from './store/article/listSearchIndex.js';
|
||||||
import articleListExport from './store/article/listExport.js';
|
import articleListExport from './store/article/listExport.js';
|
||||||
import articleUpdateBySku from './store/article/updateBySku.js';
|
import articleUpdateBySku from './store/article/updateBySku.js';
|
||||||
@@ -30,9 +43,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';
|
||||||
@@ -44,6 +58,8 @@ import companysettingList from './store/companysetting/list.js';
|
|||||||
import companysettingGet from './store/companysetting/get.js';
|
import companysettingGet from './store/companysetting/get.js';
|
||||||
import companysettingSet from './store/companysetting/set.js';
|
import companysettingSet from './store/companysetting/set.js';
|
||||||
import brandList from './store/brand/list.js';
|
import brandList from './store/brand/list.js';
|
||||||
|
import brandCreate from './store/brand/create.js';
|
||||||
|
import brandUpdate from './store/brand/update.js';
|
||||||
import brandUpsert from './store/brand/upsert.js';
|
import brandUpsert from './store/brand/upsert.js';
|
||||||
import brandDelete from './store/brand/delete.js';
|
import brandDelete from './store/brand/delete.js';
|
||||||
import brandAddImage from './store/brand/addImage.js';
|
import brandAddImage from './store/brand/addImage.js';
|
||||||
@@ -83,16 +99,36 @@ import deliveryGetById from './store/delivery/getById.js';
|
|||||||
import deliveryCreate from './store/delivery/create.js';
|
import deliveryCreate from './store/delivery/create.js';
|
||||||
import deliveryUpdate from './store/delivery/update.js';
|
import deliveryUpdate from './store/delivery/update.js';
|
||||||
import deliveryDelete from './store/delivery/delete.js';
|
import deliveryDelete from './store/delivery/delete.js';
|
||||||
|
import deliveryBook from './store/delivery/book.js';
|
||||||
|
import deliveryRebuildSummaries from './store/delivery/rebuildSummaries.js';
|
||||||
import quoteList from './store/quote/list.js';
|
import quoteList from './store/quote/list.js';
|
||||||
import quoteGetById from './store/quote/getById.js';
|
import quoteGetById from './store/quote/getById.js';
|
||||||
import quoteCreate from './store/quote/create.js';
|
import quoteCreate from './store/quote/create.js';
|
||||||
import quoteUpdate from './store/quote/update.js';
|
import quoteUpdate from './store/quote/update.js';
|
||||||
import quoteDelete from './store/quote/delete.js';
|
import quoteDelete from './store/quote/delete.js';
|
||||||
|
import quoteRebuildSummaries from './store/quote/rebuildSummaries.js';
|
||||||
import orderList from './store/order/list.js';
|
import orderList from './store/order/list.js';
|
||||||
import orderGetById from './store/order/getById.js';
|
import orderGetById from './store/order/getById.js';
|
||||||
import orderCreate from './store/order/create.js';
|
import orderCreate from './store/order/create.js';
|
||||||
|
import orderCreateFromQuote from './store/order/createFromQuote.js';
|
||||||
|
import orderCreateShipment from './store/order/createShipment.js';
|
||||||
import orderUpdate from './store/order/update.js';
|
import orderUpdate from './store/order/update.js';
|
||||||
import orderDelete from './store/order/delete.js';
|
import orderDelete from './store/order/delete.js';
|
||||||
|
import orderRebuildSummaries from './store/order/rebuildSummaries.js';
|
||||||
|
import returnList from './store/return/list.js';
|
||||||
|
import returnGetById from './store/return/getById.js';
|
||||||
|
import returnCreate from './store/return/create.js';
|
||||||
|
import returnUpdate from './store/return/update.js';
|
||||||
|
import returnDelete from './store/return/delete.js';
|
||||||
|
import returnBook from './store/return/book.js';
|
||||||
|
import returnRebuildSummaries from './store/return/rebuildSummaries.js';
|
||||||
|
import retoureList from './store/retoure/list.js';
|
||||||
|
import retoureGetById from './store/retoure/getById.js';
|
||||||
|
import retoureCreate from './store/retoure/create.js';
|
||||||
|
import retoureCreateShipment from './store/retoure/createShipment.js';
|
||||||
|
import retoureUpdate from './store/retoure/update.js';
|
||||||
|
import retoureDelete from './store/retoure/delete.js';
|
||||||
|
import retoureRebuildSummaries from './store/retoure/rebuildSummaries.js';
|
||||||
import posterminalList from './store/posterminal/list.js';
|
import posterminalList from './store/posterminal/list.js';
|
||||||
import posterminalUpsert from './store/posterminal/upsert.js';
|
import posterminalUpsert from './store/posterminal/upsert.js';
|
||||||
import posterminalDelete from './store/posterminal/delete.js';
|
import posterminalDelete from './store/posterminal/delete.js';
|
||||||
@@ -107,12 +143,20 @@ import posterminalPair from './store/posterminal/pair.js';
|
|||||||
import posterminalGetProductCount from './store/posterminal/getProductCount.js';
|
import posterminalGetProductCount from './store/posterminal/getProductCount.js';
|
||||||
import posterminalGetCategoryCount from './store/posterminal/getCategoryCount.js';
|
import posterminalGetCategoryCount from './store/posterminal/getCategoryCount.js';
|
||||||
import posterminalGetCustomerCount from './store/posterminal/getCustomerCount.js';
|
import posterminalGetCustomerCount from './store/posterminal/getCustomerCount.js';
|
||||||
|
import posterminalGetCustomerList from './store/posterminal/getCustomerList.js';
|
||||||
import posterminalGetCustomerGroupCount from './store/posterminal/getCustomerGroupCount.js';
|
import posterminalGetCustomerGroupCount from './store/posterminal/getCustomerGroupCount.js';
|
||||||
|
import posterminalGetCustomerGroupList from './store/posterminal/getCustomerGroupList.js';
|
||||||
import posterminalGetCompositeProductCount from './store/posterminal/getCompositeProductCount.js';
|
import posterminalGetCompositeProductCount from './store/posterminal/getCompositeProductCount.js';
|
||||||
import posterminalGetDeletedEntityCount from './store/posterminal/getDeletedEntityCount.js';
|
import posterminalGetDeletedEntityCount from './store/posterminal/getDeletedEntityCount.js';
|
||||||
|
import posterminalGetDeletedEntityList from './store/posterminal/getDeletedEntityList.js';
|
||||||
|
import posterminalRecordObituary from './store/posterminal/recordObituary.js';
|
||||||
|
import posterminalRecordSyncQueue from './store/posterminal/recordSyncQueue.js';
|
||||||
import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js';
|
import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js';
|
||||||
import posterminalGetCategoryList from './store/posterminal/getCategoryList.js';
|
import posterminalGetCategoryList from './store/posterminal/getCategoryList.js';
|
||||||
import posterminalGetProductList from './store/posterminal/getProductList.js';
|
import posterminalGetProductList from './store/posterminal/getProductList.js';
|
||||||
|
import posterminalSetMaxKnownObituaryId from './store/posterminal/setMaxKnownObituaryId.js';
|
||||||
|
import posterminalTouchLastseen from './store/posterminal/touchLastseen.js';
|
||||||
|
import posterminalSetClientversion from './store/posterminal/setClientversion.js';
|
||||||
import langCreate from './store/lang/create.js';
|
import langCreate from './store/lang/create.js';
|
||||||
import langDelete from './store/lang/delete.js';
|
import langDelete from './store/lang/delete.js';
|
||||||
import langList from './store/lang/list.js';
|
import langList from './store/lang/list.js';
|
||||||
@@ -131,6 +175,7 @@ import googleAuthSetSelectedCompany from './store/google_auth/setSelectedCompany
|
|||||||
import googleAuthSetProfile from './store/google_auth/setProfile.js';
|
import googleAuthSetProfile from './store/google_auth/setProfile.js';
|
||||||
import picGetImageByHash from './store/pic/getImageByHash.js';
|
import picGetImageByHash from './store/pic/getImageByHash.js';
|
||||||
import picGetThumbByHash from './store/pic/getThumbByHash.js';
|
import picGetThumbByHash from './store/pic/getThumbByHash.js';
|
||||||
|
import picGetSmallThumbByHash from './store/pic/getSmallThumbByHash.js';
|
||||||
import numbersList from './store/numbers/list.js';
|
import numbersList from './store/numbers/list.js';
|
||||||
import numbersUpdate from './store/numbers/update.js';
|
import numbersUpdate from './store/numbers/update.js';
|
||||||
import storePurge from './store/purge.js';
|
import storePurge from './store/purge.js';
|
||||||
@@ -143,24 +188,92 @@ import manifestattachmentOrder from './store/manifestattachment/order.js';
|
|||||||
import ledgerAdd from './store/ledger/add.js';
|
import ledgerAdd from './store/ledger/add.js';
|
||||||
import ledgerListByManifestarticleId from './store/ledger/listByManifestarticleId.js';
|
import ledgerListByManifestarticleId from './store/ledger/listByManifestarticleId.js';
|
||||||
import ledgerListBySku from './store/ledger/listBySku.js';
|
import ledgerListBySku from './store/ledger/listBySku.js';
|
||||||
|
import ledgerListHistory from './store/ledger/listHistory.js';
|
||||||
import warehouseCreate from './store/warehouse/create.js';
|
import warehouseCreate from './store/warehouse/create.js';
|
||||||
import warehouseGetById from './store/warehouse/getById.js';
|
import warehouseGetById from './store/warehouse/getById.js';
|
||||||
import warehouseList from './store/warehouse/list.js';
|
import warehouseList from './store/warehouse/list.js';
|
||||||
import warehouseUpdate from './store/warehouse/update.js';
|
import warehouseUpdate from './store/warehouse/update.js';
|
||||||
import warehouseDelete from './store/warehouse/delete.js';
|
import warehouseDelete from './store/warehouse/delete.js';
|
||||||
import warehouseOrder from './store/warehouse/order.js';
|
import warehouseOrder from './store/warehouse/order.js';
|
||||||
|
import carrierCreate from './store/carrier/create.js';
|
||||||
|
import carrierGetById from './store/carrier/getById.js';
|
||||||
|
import carrierList from './store/carrier/list.js';
|
||||||
|
import carrierUpdate from './store/carrier/update.js';
|
||||||
|
import carrierDelete from './store/carrier/delete.js';
|
||||||
|
import carrierOrder from './store/carrier/order.js';
|
||||||
import printerCreate from './store/printer/create.js';
|
import printerCreate from './store/printer/create.js';
|
||||||
import printerGetById from './store/printer/getById.js';
|
import printerGetById from './store/printer/getById.js';
|
||||||
import printerList from './store/printer/list.js';
|
import printerList from './store/printer/list.js';
|
||||||
import printerUpdate from './store/printer/update.js';
|
import printerUpdate from './store/printer/update.js';
|
||||||
import printerDelete from './store/printer/delete.js';
|
import printerDelete from './store/printer/delete.js';
|
||||||
import printerOrder from './store/printer/order.js';
|
import printerOrder from './store/printer/order.js';
|
||||||
|
import printeragentList from './store/printeragent/list.js';
|
||||||
|
import printeragentCreate from './store/printeragent/create.js';
|
||||||
|
import printeragentUpdate from './store/printeragent/update.js';
|
||||||
|
import printeragentDelete from './store/printeragent/delete.js';
|
||||||
|
import printeragentFindByPairingpin from './store/printeragent/findByPairingpin.js';
|
||||||
|
import printeragentFindByToken from './store/printeragent/findByToken.js';
|
||||||
|
import printeragentPair from './store/printeragent/pair.js';
|
||||||
|
import printeragentUpdateState from './store/printeragent/updateState.js';
|
||||||
|
import printeragentTouchLastseen from './store/printeragent/touchLastseen.js';
|
||||||
|
import agentprinterList from './store/agentprinter/list.js';
|
||||||
|
import agentprinterGetById from './store/agentprinter/getById.js';
|
||||||
|
import agentprinterFindOrCreate from './store/agentprinter/findOrCreate.js';
|
||||||
|
import agentprinterUpdate from './store/agentprinter/update.js';
|
||||||
|
import agentprinterOrder from './store/agentprinter/order.js';
|
||||||
|
import agentprinterDelete from './store/agentprinter/delete.js';
|
||||||
|
import printerjobCreate from './store/printerjob/create.js';
|
||||||
|
import printerjobGetById from './store/printerjob/getById.js';
|
||||||
|
import printerjobList from './store/printerjob/list.js';
|
||||||
|
import printerjobUpdateState from './store/printerjob/updateState.js';
|
||||||
|
import printerjobDelete from './store/printerjob/delete.js';
|
||||||
|
import modelconversationCreate from './store/modelconversation/create.js';
|
||||||
|
import modelconversationGet from './store/modelconversation/get.js';
|
||||||
|
import modelconversationList from './store/modelconversation/list.js';
|
||||||
|
import modelconversationSetState from './store/modelconversation/setState.js';
|
||||||
|
import modelconversationDelete from './store/modelconversation/delete.js';
|
||||||
|
import modelmessageList from './store/modelmessage/list.js';
|
||||||
|
import modelmessageAdd from './store/modelmessage/add.js';
|
||||||
|
import modelvfsWrite from './store/modelvfs/write.js';
|
||||||
|
import modelvfsRead from './store/modelvfs/read.js';
|
||||||
|
import modelvfsList from './store/modelvfs/list.js';
|
||||||
|
import modelvfsDelete from './store/modelvfs/delete.js';
|
||||||
|
import modelvfsReset from './store/modelvfs/reset.js';
|
||||||
|
import subagentscheduleCreate from './store/subagentschedule/create.js';
|
||||||
|
import subagentscheduleGetById from './store/subagentschedule/getById.js';
|
||||||
|
import subagentscheduleList from './store/subagentschedule/list.js';
|
||||||
|
import subagentscheduleUpdate from './store/subagentschedule/update.js';
|
||||||
|
import subagentscheduleDelete from './store/subagentschedule/delete.js';
|
||||||
|
import accountCreate from './store/account/create.js';
|
||||||
|
import accountList from './store/account/list.js';
|
||||||
|
import accountDelete from './store/account/delete.js';
|
||||||
|
import paymentCreate from './store/payment/create.js';
|
||||||
|
import paymentListByManifestId from './store/payment/listByManifestId.js';
|
||||||
|
import paymentDelete from './store/payment/delete.js';
|
||||||
|
import shippingCreate from './store/shipping/create.js';
|
||||||
|
import shippingUpdate from './store/shipping/update.js';
|
||||||
|
import shippingListByManifestId from './store/shipping/listByManifestId.js';
|
||||||
|
import shippingDelete from './store/shipping/delete.js';
|
||||||
|
import shipmentCreate from './store/shipment/create.js';
|
||||||
|
import shipmentGetById from './store/shipment/getById.js';
|
||||||
|
import shipmentList from './store/shipment/list.js';
|
||||||
|
import shipmentListByOrderId from './store/shipment/listByOrderId.js';
|
||||||
|
import shipmentUpdate from './store/shipment/update.js';
|
||||||
|
import shipmentDelete from './store/shipment/delete.js';
|
||||||
|
import shipmentRebuildSummaries from './store/shipment/rebuildSummaries.js';
|
||||||
|
import packageCreate from './store/package/create.js';
|
||||||
|
import packageListByShippingId from './store/package/listByShippingId.js';
|
||||||
|
import packageDelete from './store/package/delete.js';
|
||||||
|
|
||||||
import labeltemplateCreate from './store/labeltemplate/create.js';
|
import labeltemplateCreate from './store/labeltemplate/create.js';
|
||||||
import labeltemplateGetById from './store/labeltemplate/getById.js';
|
import labeltemplateGetById from './store/labeltemplate/getById.js';
|
||||||
import labeltemplateList from './store/labeltemplate/list.js';
|
import labeltemplateList from './store/labeltemplate/list.js';
|
||||||
import labeltemplateUpdate from './store/labeltemplate/update.js';
|
import labeltemplateUpdate from './store/labeltemplate/update.js';
|
||||||
import labeltemplateDelete from './store/labeltemplate/delete.js';
|
import labeltemplateDelete from './store/labeltemplate/delete.js';
|
||||||
import stockAdjust from './store/stock/adjust.js';
|
import stockAdjust from './store/stock/adjust.js';
|
||||||
|
import stockAccept from './store/stock/accept.js';
|
||||||
|
import stockShip from './store/stock/ship.js';
|
||||||
|
import stockRebuildSummaries from './store/stock/rebuildSummaries.js';
|
||||||
import propertynameList from './store/propertyname/list.js';
|
import propertynameList from './store/propertyname/list.js';
|
||||||
import propertynameCreate from './store/propertyname/create.js';
|
import propertynameCreate from './store/propertyname/create.js';
|
||||||
import propertynameUpdate from './store/propertyname/update.js';
|
import propertynameUpdate from './store/propertyname/update.js';
|
||||||
@@ -179,12 +292,32 @@ import attributevalueCreate from './store/attributevalue/create.js';
|
|||||||
import attributevalueUpdate from './store/attributevalue/update.js';
|
import attributevalueUpdate from './store/attributevalue/update.js';
|
||||||
import attributevalueOrder from './store/attributevalue/order.js';
|
import attributevalueOrder from './store/attributevalue/order.js';
|
||||||
import attributevalueDelete from './store/attributevalue/delete.js';
|
import attributevalueDelete from './store/attributevalue/delete.js';
|
||||||
|
import attributevalueAddImage from './store/attributevalue/addImage.js';
|
||||||
|
import attributevalueDeleteImage from './store/attributevalue/deleteImage.js';
|
||||||
import articleattributevalueListByArticleSku from './store/articleattributevalue/listByArticleSku.js';
|
import articleattributevalueListByArticleSku from './store/articleattributevalue/listByArticleSku.js';
|
||||||
import articleattributevalueSetByArticleSku from './store/articleattributevalue/setByArticleSku.js';
|
import articleattributevalueSetByArticleSku from './store/articleattributevalue/setByArticleSku.js';
|
||||||
|
import categorypropertyListByCategoryId from './store/categoryproperty/listByCategoryId.js';
|
||||||
|
import categorypropertySetByCategoryId from './store/categoryproperty/setByCategoryId.js';
|
||||||
|
import categorypropertyDelete from './store/categoryproperty/delete.js';
|
||||||
|
import categoryattributevalueListByCategoryId from './store/categoryattributevalue/listByCategoryId.js';
|
||||||
|
import categoryattributevalueSetByCategoryId from './store/categoryattributevalue/setByCategoryId.js';
|
||||||
import articlelistGetItems from './store/articlelist/getItemsByArticleSku.js';
|
import articlelistGetItems from './store/articlelist/getItemsByArticleSku.js';
|
||||||
import articlelistSetItems from './store/articlelist/setItemsByArticleSku.js';
|
import articlelistSetItems from './store/articlelist/setItemsByArticleSku.js';
|
||||||
import optionslistGetOptions from './store/optionslist/getOptionsByArticleSku.js';
|
import optionslistGetOptions from './store/optionslist/getOptionsByArticleSku.js';
|
||||||
import optionslistSetOptions from './store/optionslist/setOptionsByArticleSku.js';
|
import optionslistSetOptions from './store/optionslist/setOptionsByArticleSku.js';
|
||||||
|
import mcpRegisterClient from './store/mcp/registerClient.js';
|
||||||
|
import mcpGetClient from './store/mcp/getClient.js';
|
||||||
|
import mcpListClients from './store/mcp/listClients.js';
|
||||||
|
import mcpDeleteClient from './store/mcp/deleteClient.js';
|
||||||
|
import mcpCreateAuthCode from './store/mcp/createAuthCode.js';
|
||||||
|
import mcpGetAuthCode from './store/mcp/getAuthCode.js';
|
||||||
|
import mcpConsumeAuthCode from './store/mcp/consumeAuthCode.js';
|
||||||
|
import mcpCreateRefreshToken from './store/mcp/createRefreshToken.js';
|
||||||
|
import mcpGetRefreshToken from './store/mcp/getRefreshToken.js';
|
||||||
|
import mcpRevokeRefreshToken from './store/mcp/revokeRefreshToken.js';
|
||||||
|
import mcpListAuthorizations from './store/mcp/listAuthorizations.js';
|
||||||
|
import mcpRevokeAuthorizations from './store/mcp/revokeAuthorizations.js';
|
||||||
|
import healthcheckCheck from './store/healthcheck/check.js';
|
||||||
|
|
||||||
export default class LibDashStore {
|
export default class LibDashStore {
|
||||||
/**
|
/**
|
||||||
@@ -208,6 +341,16 @@ export default class LibDashStore {
|
|||||||
getByEmail: companyuserGetByEmail(provider),
|
getByEmail: companyuserGetByEmail(provider),
|
||||||
add: companyuserAdd(provider),
|
add: companyuserAdd(provider),
|
||||||
delete: companyuserDelete(provider),
|
delete: companyuserDelete(provider),
|
||||||
|
list: companyuserList(provider),
|
||||||
|
addByEmail: companyuserAddByEmail(provider),
|
||||||
|
setRole: companyuserSetRole(provider),
|
||||||
|
remove: companyuserRemove(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.autoinvite = {
|
||||||
|
add: autoinviteAdd(provider),
|
||||||
|
list: autoinviteList(provider),
|
||||||
|
remove: autoinviteRemove(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.company = {
|
this.company = {
|
||||||
@@ -215,6 +358,10 @@ export default class LibDashStore {
|
|||||||
update: companyUpdate(provider),
|
update: companyUpdate(provider),
|
||||||
delete: companyDelete(provider),
|
delete: companyDelete(provider),
|
||||||
list: companyList(provider),
|
list: companyList(provider),
|
||||||
|
getById: companyGetById(provider),
|
||||||
|
getByName: companyGetByName(provider),
|
||||||
|
addHistory: companyAddHistory(provider),
|
||||||
|
getHistory: companyGetHistory(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.category = {
|
this.category = {
|
||||||
@@ -228,12 +375,14 @@ export default class LibDashStore {
|
|||||||
delete: categoryDelete(provider),
|
delete: categoryDelete(provider),
|
||||||
addImage: categoryAddImage(provider),
|
addImage: categoryAddImage(provider),
|
||||||
deleteImage: categoryDeleteImage(provider),
|
deleteImage: categoryDeleteImage(provider),
|
||||||
|
generateImage: categoryGenerateImage(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.article = {
|
this.article = {
|
||||||
createUnderParent: articleCreateUnderParent(provider),
|
createUnderParent: articleCreateUnderParent(provider),
|
||||||
getBySku: articleGetBySku(provider),
|
getBySku: articleGetBySku(provider),
|
||||||
getHistoryBySku: articleGetHistoryBySku(provider),
|
getHistoryBySku: articleGetHistoryBySku(provider),
|
||||||
|
listHistory: articleListHistory(provider),
|
||||||
listSearchIndex: articleListSearchIndex(provider),
|
listSearchIndex: articleListSearchIndex(provider),
|
||||||
listExport: articleListExport(provider),
|
listExport: articleListExport(provider),
|
||||||
updateBySku: articleUpdateBySku(provider),
|
updateBySku: articleUpdateBySku(provider),
|
||||||
@@ -243,9 +392,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),
|
||||||
@@ -288,6 +438,8 @@ export default class LibDashStore {
|
|||||||
|
|
||||||
this.brand = {
|
this.brand = {
|
||||||
list: brandList(provider),
|
list: brandList(provider),
|
||||||
|
create: brandCreate(provider),
|
||||||
|
update: brandUpdate(provider),
|
||||||
upsert: brandUpsert(provider),
|
upsert: brandUpsert(provider),
|
||||||
delete: brandDelete(provider),
|
delete: brandDelete(provider),
|
||||||
addImage: brandAddImage(provider),
|
addImage: brandAddImage(provider),
|
||||||
@@ -349,6 +501,8 @@ export default class LibDashStore {
|
|||||||
create: deliveryCreate(provider),
|
create: deliveryCreate(provider),
|
||||||
update: deliveryUpdate(provider),
|
update: deliveryUpdate(provider),
|
||||||
delete: deliveryDelete(provider),
|
delete: deliveryDelete(provider),
|
||||||
|
book: deliveryBook(provider),
|
||||||
|
rebuildSummaries: deliveryRebuildSummaries(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.quote = {
|
this.quote = {
|
||||||
@@ -357,14 +511,38 @@ export default class LibDashStore {
|
|||||||
create: quoteCreate(provider),
|
create: quoteCreate(provider),
|
||||||
update: quoteUpdate(provider),
|
update: quoteUpdate(provider),
|
||||||
delete: quoteDelete(provider),
|
delete: quoteDelete(provider),
|
||||||
|
rebuildSummaries: quoteRebuildSummaries(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.order = {
|
this.order = {
|
||||||
list: orderList(provider),
|
list: orderList(provider),
|
||||||
getById: orderGetById(provider),
|
getById: orderGetById(provider),
|
||||||
create: orderCreate(provider),
|
create: orderCreate(provider),
|
||||||
|
createFromQuote: orderCreateFromQuote(provider),
|
||||||
|
createShipment: orderCreateShipment(provider),
|
||||||
update: orderUpdate(provider),
|
update: orderUpdate(provider),
|
||||||
delete: orderDelete(provider),
|
delete: orderDelete(provider),
|
||||||
|
rebuildSummaries: orderRebuildSummaries(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.return = {
|
||||||
|
list: returnList(provider),
|
||||||
|
getById: returnGetById(provider),
|
||||||
|
create: returnCreate(provider),
|
||||||
|
update: returnUpdate(provider),
|
||||||
|
delete: returnDelete(provider),
|
||||||
|
book: returnBook(provider),
|
||||||
|
rebuildSummaries: returnRebuildSummaries(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.retoure = {
|
||||||
|
list: retoureList(provider),
|
||||||
|
getById: retoureGetById(provider),
|
||||||
|
create: retoureCreate(provider),
|
||||||
|
createShipment: retoureCreateShipment(provider),
|
||||||
|
update: retoureUpdate(provider),
|
||||||
|
delete: retoureDelete(provider),
|
||||||
|
rebuildSummaries: retoureRebuildSummaries(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.posterminal = {
|
this.posterminal = {
|
||||||
@@ -382,12 +560,55 @@ export default class LibDashStore {
|
|||||||
getProductCount: posterminalGetProductCount(provider),
|
getProductCount: posterminalGetProductCount(provider),
|
||||||
getCategoryCount: posterminalGetCategoryCount(provider),
|
getCategoryCount: posterminalGetCategoryCount(provider),
|
||||||
getCustomerCount: posterminalGetCustomerCount(provider),
|
getCustomerCount: posterminalGetCustomerCount(provider),
|
||||||
|
getCustomerList: posterminalGetCustomerList(provider),
|
||||||
getCustomerGroupCount: posterminalGetCustomerGroupCount(provider),
|
getCustomerGroupCount: posterminalGetCustomerGroupCount(provider),
|
||||||
|
getCustomerGroupList: posterminalGetCustomerGroupList(provider),
|
||||||
getCompositeProductCount: posterminalGetCompositeProductCount(provider),
|
getCompositeProductCount: posterminalGetCompositeProductCount(provider),
|
||||||
getDeletedEntityCount: posterminalGetDeletedEntityCount(provider),
|
getDeletedEntityCount: posterminalGetDeletedEntityCount(provider),
|
||||||
|
getDeletedEntityList: posterminalGetDeletedEntityList(provider),
|
||||||
|
recordObituary: posterminalRecordObituary(provider),
|
||||||
|
recordSyncQueue: posterminalRecordSyncQueue(provider),
|
||||||
getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider),
|
getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider),
|
||||||
getCategoryList: posterminalGetCategoryList(provider),
|
getCategoryList: posterminalGetCategoryList(provider),
|
||||||
getProductList: posterminalGetProductList(provider),
|
getProductList: posterminalGetProductList(provider),
|
||||||
|
setMaxKnownObituaryId: posterminalSetMaxKnownObituaryId(provider),
|
||||||
|
touchLastseen: posterminalTouchLastseen(provider),
|
||||||
|
setClientversion: posterminalSetClientversion(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.account = {
|
||||||
|
create: accountCreate(provider),
|
||||||
|
list: accountList(provider),
|
||||||
|
delete: accountDelete(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.payment = {
|
||||||
|
create: paymentCreate(provider),
|
||||||
|
listByManifestId: paymentListByManifestId(provider),
|
||||||
|
delete: paymentDelete(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.shipping = {
|
||||||
|
create: shippingCreate(provider),
|
||||||
|
update: shippingUpdate(provider),
|
||||||
|
listByManifestId: shippingListByManifestId(provider),
|
||||||
|
delete: shippingDelete(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.shipment = {
|
||||||
|
create: shipmentCreate(provider),
|
||||||
|
getById: shipmentGetById(provider),
|
||||||
|
list: shipmentList(provider),
|
||||||
|
listByOrderId: shipmentListByOrderId(provider),
|
||||||
|
update: shipmentUpdate(provider),
|
||||||
|
delete: shipmentDelete(provider),
|
||||||
|
rebuildSummaries: shipmentRebuildSummaries(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.package = {
|
||||||
|
create: packageCreate(provider),
|
||||||
|
listByShippingId: packageListByShippingId(provider),
|
||||||
|
delete: packageDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.google_auth = {
|
this.google_auth = {
|
||||||
@@ -403,6 +624,7 @@ export default class LibDashStore {
|
|||||||
this.pic = {
|
this.pic = {
|
||||||
getImageByHash: picGetImageByHash(provider),
|
getImageByHash: picGetImageByHash(provider),
|
||||||
getThumbByHash: picGetThumbByHash(provider),
|
getThumbByHash: picGetThumbByHash(provider),
|
||||||
|
getSmallThumbByHash: picGetSmallThumbByHash(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.numbers = {
|
this.numbers = {
|
||||||
@@ -422,6 +644,7 @@ export default class LibDashStore {
|
|||||||
add: ledgerAdd(provider),
|
add: ledgerAdd(provider),
|
||||||
listByManifestarticleId: ledgerListByManifestarticleId(provider),
|
listByManifestarticleId: ledgerListByManifestarticleId(provider),
|
||||||
listBySku: ledgerListBySku(provider),
|
listBySku: ledgerListBySku(provider),
|
||||||
|
listHistory: ledgerListHistory(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.warehouse = {
|
this.warehouse = {
|
||||||
@@ -433,6 +656,15 @@ export default class LibDashStore {
|
|||||||
order: warehouseOrder(provider),
|
order: warehouseOrder(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.carrier = {
|
||||||
|
create: carrierCreate(provider),
|
||||||
|
getById: carrierGetById(provider),
|
||||||
|
list: carrierList(provider),
|
||||||
|
update: carrierUpdate(provider),
|
||||||
|
delete: carrierDelete(provider),
|
||||||
|
order: carrierOrder(provider),
|
||||||
|
};
|
||||||
|
|
||||||
this.printer = {
|
this.printer = {
|
||||||
create: printerCreate(provider),
|
create: printerCreate(provider),
|
||||||
getById: printerGetById(provider),
|
getById: printerGetById(provider),
|
||||||
@@ -442,6 +674,64 @@ export default class LibDashStore {
|
|||||||
order: printerOrder(provider),
|
order: printerOrder(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.printeragent = {
|
||||||
|
list: printeragentList(provider),
|
||||||
|
create: printeragentCreate(provider),
|
||||||
|
update: printeragentUpdate(provider),
|
||||||
|
delete: printeragentDelete(provider),
|
||||||
|
findByPairingpin: printeragentFindByPairingpin(provider),
|
||||||
|
findByToken: printeragentFindByToken(provider),
|
||||||
|
pair: printeragentPair(provider),
|
||||||
|
updateState: printeragentUpdateState(provider),
|
||||||
|
touchLastseen: printeragentTouchLastseen(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.agentprinter = {
|
||||||
|
list: agentprinterList(provider),
|
||||||
|
getById: agentprinterGetById(provider),
|
||||||
|
findOrCreate: agentprinterFindOrCreate(provider),
|
||||||
|
update: agentprinterUpdate(provider),
|
||||||
|
order: agentprinterOrder(provider),
|
||||||
|
delete: agentprinterDelete(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.printerjob = {
|
||||||
|
create: printerjobCreate(provider),
|
||||||
|
getById: printerjobGetById(provider),
|
||||||
|
list: printerjobList(provider),
|
||||||
|
updateState: printerjobUpdateState(provider),
|
||||||
|
delete: printerjobDelete(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.modelconversation = {
|
||||||
|
create: modelconversationCreate(provider),
|
||||||
|
get: modelconversationGet(provider),
|
||||||
|
list: modelconversationList(provider),
|
||||||
|
setState: modelconversationSetState(provider),
|
||||||
|
delete: modelconversationDelete(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this.modelvfs = {
|
||||||
|
write: modelvfsWrite(provider),
|
||||||
|
read: modelvfsRead(provider),
|
||||||
|
list: modelvfsList(provider),
|
||||||
|
delete: modelvfsDelete(provider),
|
||||||
|
reset: modelvfsReset(provider),
|
||||||
|
};
|
||||||
|
this.modelmessage = {
|
||||||
|
list: modelmessageList(provider),
|
||||||
|
add: modelmessageAdd(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.subagentschedule = {
|
||||||
|
create: subagentscheduleCreate(provider),
|
||||||
|
getById: subagentscheduleGetById(provider),
|
||||||
|
list: subagentscheduleList(provider),
|
||||||
|
update: subagentscheduleUpdate(provider),
|
||||||
|
delete: subagentscheduleDelete(provider),
|
||||||
|
};
|
||||||
|
|
||||||
this.labeltemplate = {
|
this.labeltemplate = {
|
||||||
create: labeltemplateCreate(provider),
|
create: labeltemplateCreate(provider),
|
||||||
getById: labeltemplateGetById(provider),
|
getById: labeltemplateGetById(provider),
|
||||||
@@ -452,6 +742,9 @@ export default class LibDashStore {
|
|||||||
|
|
||||||
this.stock = {
|
this.stock = {
|
||||||
adjust: stockAdjust(provider),
|
adjust: stockAdjust(provider),
|
||||||
|
accept: stockAccept(provider),
|
||||||
|
ship: stockShip(provider),
|
||||||
|
rebuildSummaries: stockRebuildSummaries(provider),
|
||||||
};
|
};
|
||||||
this.adjustment = this.stock;
|
this.adjustment = this.stock;
|
||||||
|
|
||||||
@@ -483,6 +776,8 @@ export default class LibDashStore {
|
|||||||
update: attributevalueUpdate(provider),
|
update: attributevalueUpdate(provider),
|
||||||
order: attributevalueOrder(provider),
|
order: attributevalueOrder(provider),
|
||||||
delete: attributevalueDelete(provider),
|
delete: attributevalueDelete(provider),
|
||||||
|
addImage: attributevalueAddImage(provider),
|
||||||
|
deleteImage: attributevalueDeleteImage(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.articleattributevalue = {
|
this.articleattributevalue = {
|
||||||
@@ -490,6 +785,17 @@ export default class LibDashStore {
|
|||||||
setByArticleSku: articleattributevalueSetByArticleSku(provider),
|
setByArticleSku: articleattributevalueSetByArticleSku(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.categoryproperty = {
|
||||||
|
listByCategoryId: categorypropertyListByCategoryId(provider),
|
||||||
|
setByCategoryId: categorypropertySetByCategoryId(provider),
|
||||||
|
delete: categorypropertyDelete(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.categoryattributevalue = {
|
||||||
|
listByCategoryId: categoryattributevalueListByCategoryId(provider),
|
||||||
|
setByCategoryId: categoryattributevalueSetByCategoryId(provider),
|
||||||
|
};
|
||||||
|
|
||||||
this.articlelist = {
|
this.articlelist = {
|
||||||
getItemsByArticleSku: articlelistGetItems(provider),
|
getItemsByArticleSku: articlelistGetItems(provider),
|
||||||
setItemsByArticleSku: articlelistSetItems(provider),
|
setItemsByArticleSku: articlelistSetItems(provider),
|
||||||
@@ -500,8 +806,28 @@ export default class LibDashStore {
|
|||||||
setOptionsByArticleSku: optionslistSetOptions(provider),
|
setOptionsByArticleSku: optionslistSetOptions(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.mcp = {
|
||||||
|
registerClient: mcpRegisterClient(provider),
|
||||||
|
getClient: mcpGetClient(provider),
|
||||||
|
listClients: mcpListClients(provider),
|
||||||
|
deleteClient: mcpDeleteClient(provider),
|
||||||
|
createAuthCode: mcpCreateAuthCode(provider),
|
||||||
|
getAuthCode: mcpGetAuthCode(provider),
|
||||||
|
consumeAuthCode: mcpConsumeAuthCode(provider),
|
||||||
|
createRefreshToken: mcpCreateRefreshToken(provider),
|
||||||
|
getRefreshToken: mcpGetRefreshToken(provider),
|
||||||
|
revokeRefreshToken: mcpRevokeRefreshToken(provider),
|
||||||
|
listAuthorizations: mcpListAuthorizations(provider),
|
||||||
|
revokeAuthorizations: mcpRevokeAuthorizations(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.healthcheck = {
|
||||||
|
check: healthcheckCheck(provider),
|
||||||
|
};
|
||||||
|
|
||||||
this.purge = storePurge(provider);
|
this.purge = storePurge(provider);
|
||||||
this.initialize = storeInitialize(provider);
|
this.initialize = storeInitialize(provider);
|
||||||
|
this.transaction = (fn) => provider.transaction(fn);
|
||||||
|
|
||||||
return this.initialize().then(() => this);
|
return this.initialize().then(() => this);
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/providers/mssql/downgrade/2_to_1.js
Normal file
15
src/providers/mssql/downgrade/2_to_1.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Schema 2 → 1 (MSSQL): drops posterminal `sort`, `enabled`, `clientversion`.
|
||||||
|
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
|
||||||
|
*/
|
||||||
|
export default async function downgrade({ query, assertVersion, setVersion }) {
|
||||||
|
await assertVersion(2);
|
||||||
|
|
||||||
|
await query(`
|
||||||
|
ALTER TABLE posterminal DROP CONSTRAINT DF_posterminal_sort, DF_posterminal_enabled;
|
||||||
|
ALTER TABLE posterminal DROP COLUMN sort, enabled, clientversion;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await setVersion(1);
|
||||||
|
}
|
||||||
|
|
||||||
11
src/providers/mssql/downgrade/3_to_2.js
Normal file
11
src/providers/mssql/downgrade/3_to_2.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Schema 3 → 2 (MSSQL): drops the `printeragent` table.
|
||||||
|
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
|
||||||
|
*/
|
||||||
|
export default async function downgrade({ query, assertVersion, setVersion }) {
|
||||||
|
await assertVersion(3);
|
||||||
|
|
||||||
|
await query(`DROP TABLE IF EXISTS printeragent;`);
|
||||||
|
|
||||||
|
await setVersion(2);
|
||||||
|
}
|
||||||
38
src/providers/mssql/downgrade/4_to_3.js
Normal file
38
src/providers/mssql/downgrade/4_to_3.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Schema 4 → 3 (MSSQL):
|
||||||
|
* - Drops agentprinter table
|
||||||
|
* - Removes enabled from printeragent
|
||||||
|
* - Restores clientserial, clientmodel, clientstate in printeragent
|
||||||
|
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
|
||||||
|
*/
|
||||||
|
export default async function downgrade({ query, assertVersion, setVersion }) {
|
||||||
|
await assertVersion(4);
|
||||||
|
|
||||||
|
await query(`DROP TABLE IF EXISTS agentprinter;`);
|
||||||
|
|
||||||
|
await query(`
|
||||||
|
IF OBJECT_ID(N'dbo.printeragent', N'U') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
IF COL_LENGTH('printeragent', 'enabled') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DECLARE @defName NVARCHAR(255);
|
||||||
|
SELECT @defName = dc.name
|
||||||
|
FROM sys.default_constraints dc
|
||||||
|
JOIN sys.columns c ON c.default_object_id = dc.object_id
|
||||||
|
WHERE dc.parent_object_id = OBJECT_ID(N'dbo.printeragent') AND c.name = 'enabled';
|
||||||
|
IF @defName IS NOT NULL
|
||||||
|
EXEC('ALTER TABLE printeragent DROP CONSTRAINT ' + @defName);
|
||||||
|
ALTER TABLE printeragent DROP COLUMN enabled;
|
||||||
|
END
|
||||||
|
IF COL_LENGTH('printeragent', 'clientserial') IS NULL
|
||||||
|
ALTER TABLE printeragent ADD clientserial NVARCHAR(255) NULL;
|
||||||
|
IF COL_LENGTH('printeragent', 'clientmodel') IS NULL
|
||||||
|
ALTER TABLE printeragent ADD clientmodel NVARCHAR(255) NULL;
|
||||||
|
IF COL_LENGTH('printeragent', 'clientstate') IS NULL
|
||||||
|
ALTER TABLE printeragent ADD clientstate NVARCHAR(255) NULL;
|
||||||
|
END
|
||||||
|
`);
|
||||||
|
|
||||||
|
await setVersion(3);
|
||||||
|
}
|
||||||
|
|
||||||
13
src/providers/mssql/downgrade/5_to_4.js
Normal file
13
src/providers/mssql/downgrade/5_to_4.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Schema 5 → 4 (MSSQL):
|
||||||
|
* - Drops printerjob table
|
||||||
|
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
|
||||||
|
*/
|
||||||
|
export default async function downgrade({ query, assertVersion, setVersion }) {
|
||||||
|
await assertVersion(5);
|
||||||
|
|
||||||
|
await query(`DROP TABLE IF EXISTS printerjob;`);
|
||||||
|
|
||||||
|
await setVersion(4);
|
||||||
|
}
|
||||||
|
|
||||||
18
src/providers/mssql/downgrade/6_to_5.js
Normal file
18
src/providers/mssql/downgrade/6_to_5.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Schema 6 → 5 (MSSQL):
|
||||||
|
* - Drops autoinvite table
|
||||||
|
* - Drops companyuser.role
|
||||||
|
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
|
||||||
|
*/
|
||||||
|
export default async function downgrade({ query, assertVersion, setVersion }) {
|
||||||
|
await assertVersion(6);
|
||||||
|
|
||||||
|
await query(`DROP TABLE IF EXISTS autoinvite;`);
|
||||||
|
|
||||||
|
await query(`
|
||||||
|
ALTER TABLE companyuser DROP CONSTRAINT CK_companyuser_role, DF_companyuser_role;
|
||||||
|
`);
|
||||||
|
await query(`ALTER TABLE companyuser DROP COLUMN role;`);
|
||||||
|
|
||||||
|
await setVersion(5);
|
||||||
|
}
|
||||||
@@ -4,7 +4,12 @@
|
|||||||
* { rows, rowsAffected, insertId }
|
* { rows, rowsAffected, insertId }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { mkdirSync, promises as fsPromises } from 'node:fs';
|
||||||
import sql from 'mssql';
|
import sql from 'mssql';
|
||||||
|
import { logSlowQuery } from '../provider.js';
|
||||||
|
import { safeWrite, safeReadByHash, safeDeleteByHash } from '../storage.js';
|
||||||
|
import { upgradeSchema, downgradeSchema, readSchemaVersion } from './migrate.js';
|
||||||
|
|
||||||
function normalizeRows(result) {
|
function normalizeRows(result) {
|
||||||
const recordset = result?.recordset ?? [];
|
const recordset = result?.recordset ?? [];
|
||||||
@@ -15,20 +20,69 @@ function normalizeRows(result) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MssqlExecutor {
|
export class MssqlExecutor {
|
||||||
/** @param {() => import('mssql').Request} requestFactory */
|
/**
|
||||||
constructor(requestFactory) {
|
* @param {() => import('mssql').Request} requestFactory
|
||||||
|
* @param {string|null} [storageDir=null]
|
||||||
|
*/
|
||||||
|
constructor(requestFactory, storageDir = null) {
|
||||||
this.#requestFactory = requestFactory;
|
this.#requestFactory = requestFactory;
|
||||||
|
this.storageDir = storageDir;
|
||||||
|
this.dialect = 'mssql';
|
||||||
}
|
}
|
||||||
|
|
||||||
#requestFactory;
|
#requestFactory;
|
||||||
|
|
||||||
|
async writePicFull(hash, buffer) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeWrite(join(this.storageDir, 'fullbinary', hash + '.avif'), buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readPicFull(hash) {
|
||||||
|
if (!this.storageDir || !hash) return null;
|
||||||
|
return safeReadByHash(join(this.storageDir, 'fullbinary'), hash, '.avif');
|
||||||
|
}
|
||||||
|
|
||||||
|
async writePicThumb(hash, buffer) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeWrite(join(this.storageDir, 'thumbbinary', hash + '.avif'), buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readPicThumb(hash) {
|
||||||
|
if (!this.storageDir || !hash) return null;
|
||||||
|
return safeReadByHash(join(this.storageDir, 'thumbbinary'), hash, '.avif');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePic(hash) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeDeleteByHash(join(this.storageDir, 'fullbinary'), hash);
|
||||||
|
await safeDeleteByHash(join(this.storageDir, 'thumbbinary'), hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
async purgeFiles() {
|
||||||
|
if (!this.storageDir) return;
|
||||||
|
for (const sub of ['fullbinary', 'thumbbinary']) {
|
||||||
|
const dir = join(this.storageDir, sub);
|
||||||
|
try {
|
||||||
|
const files = await fsPromises.readdir(dir);
|
||||||
|
await Promise.all(files.map((f) => fsPromises.unlink(join(dir, f))));
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @returns {Promise<import('../provider.js').QueryResult>} */
|
/** @returns {Promise<import('../provider.js').QueryResult>} */
|
||||||
async query(sql, params = {}) {
|
async query(sql, params = {}) {
|
||||||
const req = this.#requestFactory();
|
const start = performance.now();
|
||||||
for (const [key, value] of Object.entries(params)) {
|
try {
|
||||||
req.input(key, value);
|
const req = this.#requestFactory();
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
req.input(key, value);
|
||||||
|
}
|
||||||
|
return normalizeRows(await req.query(sql));
|
||||||
|
} finally {
|
||||||
|
logSlowQuery(sql, performance.now() - start, params);
|
||||||
}
|
}
|
||||||
return normalizeRows(await req.query(sql));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,22 +91,27 @@ export class MssqlExecutor {
|
|||||||
* (`getnextnumberwithprefixandpadding`).
|
* (`getnextnumberwithprefixandpadding`).
|
||||||
*/
|
*/
|
||||||
async exec(procName, { inputs = {}, outputs = [] } = {}) {
|
async exec(procName, { inputs = {}, outputs = [] } = {}) {
|
||||||
const req = this.#requestFactory();
|
const start = performance.now();
|
||||||
for (const [key, value] of Object.entries(inputs)) {
|
try {
|
||||||
req.input(key, value);
|
const req = this.#requestFactory();
|
||||||
|
for (const [key, value] of Object.entries(inputs)) {
|
||||||
|
req.input(key, value);
|
||||||
|
}
|
||||||
|
for (const out of outputs) {
|
||||||
|
req.output(out);
|
||||||
|
}
|
||||||
|
const result = await req.execute(procName);
|
||||||
|
return {
|
||||||
|
rows: result.recordset ?? [],
|
||||||
|
rowsAffected: Array.isArray(result.rowsAffected)
|
||||||
|
? result.rowsAffected.reduce((a, b) => a + b, 0)
|
||||||
|
: 0,
|
||||||
|
insertId: null,
|
||||||
|
outputs: result.output ?? {},
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
logSlowQuery(`EXEC ${procName}`, performance.now() - start, inputs);
|
||||||
}
|
}
|
||||||
for (const out of outputs) {
|
|
||||||
req.output(out);
|
|
||||||
}
|
|
||||||
const result = await req.execute(procName);
|
|
||||||
return {
|
|
||||||
rows: result.recordset ?? [],
|
|
||||||
rowsAffected: Array.isArray(result.rowsAffected)
|
|
||||||
? result.rowsAffected.reduce((a, b) => a + b, 0)
|
|
||||||
: 0,
|
|
||||||
insertId: null,
|
|
||||||
outputs: result.output ?? {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +121,9 @@ export class MssqlProvider {
|
|||||||
* Either an already-connected mssql ConnectionPool, or a plain mssql
|
* Either an already-connected mssql ConnectionPool, or a plain mssql
|
||||||
* config object — in which case the pool is created lazily on first use
|
* config object — in which case the pool is created lazily on first use
|
||||||
* (and closed by {@link MssqlProvider#close}).
|
* (and closed by {@link MssqlProvider#close}).
|
||||||
|
* @param {{ storageDir?: string|null }} [options]
|
||||||
*/
|
*/
|
||||||
constructor(poolOrConfig) {
|
constructor(poolOrConfig, { storageDir = null } = {}) {
|
||||||
const isPool =
|
const isPool =
|
||||||
poolOrConfig && typeof poolOrConfig.request === 'function';
|
poolOrConfig && typeof poolOrConfig.request === 'function';
|
||||||
if (!poolOrConfig || (!isPool && !poolOrConfig.server)) {
|
if (!poolOrConfig || (!isPool && !poolOrConfig.server)) {
|
||||||
@@ -76,12 +136,50 @@ export class MssqlProvider {
|
|||||||
// a different object after a lazy connection would leave them with null.
|
// a different object after a lazy connection would leave them with null.
|
||||||
this.pool = isPool ? poolOrConfig : new sql.ConnectionPool(poolOrConfig);
|
this.pool = isPool ? poolOrConfig : new sql.ConnectionPool(poolOrConfig);
|
||||||
this.#config = isPool ? null : poolOrConfig;
|
this.#config = isPool ? null : poolOrConfig;
|
||||||
|
this.storageDir = storageDir || poolOrConfig?.storageDir || null;
|
||||||
this.dialect = 'mssql';
|
this.dialect = 'mssql';
|
||||||
}
|
}
|
||||||
|
|
||||||
#config;
|
#config;
|
||||||
#connecting;
|
#connecting;
|
||||||
|
|
||||||
|
async #tryCreateDatabaseIfMissing() {
|
||||||
|
if (!this.#config?.database) return false;
|
||||||
|
const targetDb = String(this.#config.database);
|
||||||
|
if (targetDb.toLowerCase() === 'master') return false;
|
||||||
|
|
||||||
|
const masterConfig = {
|
||||||
|
...this.#config,
|
||||||
|
database: 'master',
|
||||||
|
pool: { min: 0, max: 1, idleTimeoutMillis: 5000 },
|
||||||
|
};
|
||||||
|
|
||||||
|
let masterPool;
|
||||||
|
try {
|
||||||
|
masterPool = await new sql.ConnectionPool(masterConfig).connect();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const checkReq = masterPool.request();
|
||||||
|
checkReq.input('dbname', sql.NVarChar, targetDb);
|
||||||
|
const res = await checkReq.query('SELECT DB_ID(@dbname) AS dbid');
|
||||||
|
if (res.recordset[0]?.dbid != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createReq = masterPool.request();
|
||||||
|
createReq.input('dbname', sql.NVarChar, targetDb);
|
||||||
|
await createReq.query(
|
||||||
|
"DECLARE @sql NVARCHAR(MAX) = N'CREATE DATABASE ' + QUOTENAME(@dbname); EXEC sp_executesql @sql;"
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
await masterPool.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the underlying pool, creating and connecting it on first use
|
* Returns the underlying pool, creating and connecting it on first use
|
||||||
* when the provider was constructed from a config object.
|
* when the provider was constructed from a config object.
|
||||||
@@ -90,26 +188,37 @@ export class MssqlProvider {
|
|||||||
ensurePool() {
|
ensurePool() {
|
||||||
if (this.pool.connected) return Promise.resolve(this.pool);
|
if (this.pool.connected) return Promise.resolve(this.pool);
|
||||||
if (!this.#connecting) {
|
if (!this.#connecting) {
|
||||||
this.#connecting = this.pool
|
this.#connecting = (async () => {
|
||||||
.connect()
|
try {
|
||||||
.then((pool) => {
|
return await this.pool.connect();
|
||||||
return pool;
|
} catch (err) {
|
||||||
})
|
if (err.code === 'ELOGIN') {
|
||||||
.catch((err) => {
|
try {
|
||||||
this.#connecting = undefined;
|
const created = await this.#tryCreateDatabaseIfMissing();
|
||||||
|
if (created) {
|
||||||
|
return await this.pool.connect();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore creation error and fall through to rethrow original error
|
||||||
|
}
|
||||||
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
}
|
||||||
|
})().catch((err) => {
|
||||||
|
this.#connecting = undefined;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return this.#connecting;
|
return this.#connecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
makeExecutor(requestFactory = () => this.pool.request()) {
|
makeExecutor(requestFactory = () => this.pool.request()) {
|
||||||
return new MssqlExecutor(requestFactory);
|
return new MssqlExecutor(requestFactory, this.storageDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #withPool(fn) {
|
async #withPool(fn) {
|
||||||
const pool = await this.ensurePool();
|
const pool = await this.ensurePool();
|
||||||
return fn(new MssqlExecutor(() => pool.request()));
|
return fn(new MssqlExecutor(() => pool.request(), this.storageDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
async query(sql, params = {}) {
|
async query(sql, params = {}) {
|
||||||
@@ -120,6 +229,45 @@ export class MssqlProvider {
|
|||||||
return this.#withPool((executor) => executor.exec(procName, options));
|
return this.#withPool((executor) => executor.exec(procName, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async writePicFull(hash, buffer) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeWrite(join(this.storageDir, 'fullbinary', hash + '.avif'), buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readPicFull(hash) {
|
||||||
|
if (!this.storageDir || !hash) return null;
|
||||||
|
return safeReadByHash(join(this.storageDir, 'fullbinary'), hash, '.avif');
|
||||||
|
}
|
||||||
|
|
||||||
|
async writePicThumb(hash, buffer) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeWrite(join(this.storageDir, 'thumbbinary', hash + '.avif'), buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readPicThumb(hash) {
|
||||||
|
if (!this.storageDir || !hash) return null;
|
||||||
|
return safeReadByHash(join(this.storageDir, 'thumbbinary'), hash, '.avif');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePic(hash) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeDeleteByHash(join(this.storageDir, 'fullbinary'), hash);
|
||||||
|
await safeDeleteByHash(join(this.storageDir, 'thumbbinary'), hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
async purgeFiles() {
|
||||||
|
if (!this.storageDir) return;
|
||||||
|
for (const sub of ['fullbinary', 'thumbbinary']) {
|
||||||
|
const dir = join(this.storageDir, sub);
|
||||||
|
try {
|
||||||
|
const files = await fsPromises.readdir(dir);
|
||||||
|
await Promise.all(files.map((f) => fsPromises.unlink(join(dir, f))));
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs `fn(runner)` inside a transaction. Commit on success,
|
* Runs `fn(runner)` inside a transaction. Commit on success,
|
||||||
* rollback on error. `runner` has query/exec like the provider.
|
* rollback on error. `runner` has query/exec like the provider.
|
||||||
@@ -138,6 +286,12 @@ export class MssqlProvider {
|
|||||||
runner.query = runner;
|
runner.query = runner;
|
||||||
runner.exec = (proc, opts) => executor.exec(proc, opts);
|
runner.exec = (proc, opts) => executor.exec(proc, opts);
|
||||||
runner.dialect = 'mssql';
|
runner.dialect = 'mssql';
|
||||||
|
runner.writePicFull = executor.writePicFull.bind(executor);
|
||||||
|
runner.readPicFull = executor.readPicFull.bind(executor);
|
||||||
|
runner.writePicThumb = executor.writePicThumb.bind(executor);
|
||||||
|
runner.readPicThumb = executor.readPicThumb.bind(executor);
|
||||||
|
runner.deletePic = executor.deletePic.bind(executor);
|
||||||
|
runner.purgeFiles = executor.purgeFiles.bind(executor);
|
||||||
const result = await fn(runner, tx);
|
const result = await fn(runner, tx);
|
||||||
await tx.commit();
|
await tx.commit();
|
||||||
return result;
|
return result;
|
||||||
@@ -151,8 +305,17 @@ export class MssqlProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
/**
|
||||||
|
* Initializes the MSSQL schema.
|
||||||
|
* @param {((message: string) => void) | { log?: (message: string) => void }} [options]
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async initialize(options = {}) {
|
||||||
const pool = await this.ensurePool();
|
const pool = await this.ensurePool();
|
||||||
|
const log = typeof options === 'function' ? options : options?.log;
|
||||||
|
// Versioned upgrades first, so schema.js never meets an outdated table shape.
|
||||||
|
await upgradeSchema(this, log);
|
||||||
|
|
||||||
const { SCHEMA_SCRIPTS } = await import('./schema.js');
|
const { SCHEMA_SCRIPTS } = await import('./schema.js');
|
||||||
for (const script of SCHEMA_SCRIPTS) {
|
for (const script of SCHEMA_SCRIPTS) {
|
||||||
if (script && script.trim()) {
|
if (script && script.trim()) {
|
||||||
@@ -188,20 +351,57 @@ export class MssqlProvider {
|
|||||||
ALTER TABLE category ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_category_updated_at DEFAULT SYSUTCDATETIME();
|
ALTER TABLE category ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_category_updated_at DEFAULT SYSUTCDATETIME();
|
||||||
END
|
END
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.payment', N'U') IS NOT NULL AND COL_LENGTH('payment', 'created_at') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE payment ADD created_at DATETIME2 NOT NULL CONSTRAINT DF_payment_created_at DEFAULT SYSUTCDATETIME();
|
||||||
|
END
|
||||||
|
|
||||||
IF OBJECT_ID(N'dbo.customergroup', N'U') IS NOT NULL AND COL_LENGTH('customergroup', 'updated_at') IS NULL
|
IF OBJECT_ID(N'dbo.customergroup', N'U') IS NOT NULL AND COL_LENGTH('customergroup', 'updated_at') IS NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
ALTER TABLE customergroup ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_customergroup_updated_at DEFAULT SYSUTCDATETIME();
|
ALTER TABLE customergroup ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_customergroup_updated_at DEFAULT SYSUTCDATETIME();
|
||||||
END
|
END
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.printeragent', N'U') IS NOT NULL AND COL_LENGTH('printeragent', 'sort') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE printeragent ADD sort INT NOT NULL CONSTRAINT DF_printeragent_sort DEFAULT 0;
|
||||||
|
END
|
||||||
`);
|
`);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.storageDir) {
|
||||||
|
for (const sub of ['fullbinary', 'thumbbinary']) {
|
||||||
|
mkdirSync(join(this.storageDir, sub), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async purge() {
|
async purge() {
|
||||||
const pool = await this.ensurePool();
|
const pool = await this.ensurePool();
|
||||||
const { buildPurgeSql } = await import('./purgeSql.js');
|
const { buildPurgeSql } = await import('./purgeSql.js');
|
||||||
await pool.request().query(buildPurgeSql());
|
await pool.request().query(buildPurgeSql());
|
||||||
|
if (this.storageDir) {
|
||||||
|
await this.purgeFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current `meta.version`, or `null` if the database has none.
|
||||||
|
* @returns {Promise<number|null>}
|
||||||
|
*/
|
||||||
|
async getSchemaVersion() {
|
||||||
|
return readSchemaVersion((sql, params) => this.query(sql, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs downgrade steps down to `target`. CLI only (`npm run downgrade`).
|
||||||
|
* @param {number} target
|
||||||
|
* @param {(message: string) => void} [log]
|
||||||
|
* @returns {Promise<number>} Steps applied.
|
||||||
|
*/
|
||||||
|
async downgradeSchema(target, log) {
|
||||||
|
return downgradeSchema(this, target, log);
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
|||||||
126
src/providers/mssql/migrate.js
Normal file
126
src/providers/mssql/migrate.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* MSSQL schema versioning (see ../schemaMigrations.js).
|
||||||
|
*
|
||||||
|
* Step context: `{ query, getVersion, assertVersion, setVersion }` where
|
||||||
|
* `query(sql, params)` runs inside the step's transaction (same contract as
|
||||||
|
* `provider.query`). Note: a column added by `ALTER TABLE` is not visible to
|
||||||
|
* later statements of the SAME batch — use separate `query()` calls.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
BASE_SCHEMA_VERSION,
|
||||||
|
assertStepResult,
|
||||||
|
latestSchemaVersion,
|
||||||
|
makeVersionContext,
|
||||||
|
migrateSchema,
|
||||||
|
parseSchemaVersion,
|
||||||
|
} from '../schemaMigrations.js';
|
||||||
|
|
||||||
|
export const UPGRADE_DIR = new URL('./upgrade/', import.meta.url);
|
||||||
|
export const DOWNGRADE_DIR = new URL('./downgrade/', import.meta.url);
|
||||||
|
|
||||||
|
/** Schema version a fresh database is created at. */
|
||||||
|
export const SCHEMA_VERSION = latestSchemaVersion(UPGRADE_DIR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {(sql: string, params?: object) => Promise<{ rows: any[] }>} QueryFn
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads `meta.version`, or `null` when the meta table/row is missing.
|
||||||
|
* @param {QueryFn} query
|
||||||
|
* @returns {Promise<number|null>}
|
||||||
|
*/
|
||||||
|
export async function readSchemaVersion(query) {
|
||||||
|
const result = await query(`
|
||||||
|
IF OBJECT_ID(N'dbo.meta', N'U') IS NOT NULL
|
||||||
|
SELECT value FROM meta WHERE [key] = N'version'
|
||||||
|
ELSE
|
||||||
|
SELECT CAST(NULL AS NVARCHAR(MAX)) AS value WHERE 1 = 0
|
||||||
|
`);
|
||||||
|
const row = result.rows[0];
|
||||||
|
return row ? parseSchemaVersion(row.value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeSchemaVersion(query, version) {
|
||||||
|
await query(`
|
||||||
|
UPDATE meta SET value = @version WHERE [key] = N'version';
|
||||||
|
IF @@ROWCOUNT = 0 INSERT INTO meta ([key], value) VALUES (N'version', @version);
|
||||||
|
`, { version: String(version) });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasAnyTable(query) {
|
||||||
|
const result = await query(`SELECT TOP 1 1 AS found FROM sys.tables WHERE is_ms_shipped = 0`);
|
||||||
|
return result.rows.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('./index.js').MssqlProvider} provider
|
||||||
|
*/
|
||||||
|
function makeRunStep(provider) {
|
||||||
|
return async (step, fn) => {
|
||||||
|
await provider.transaction(async (runner) => {
|
||||||
|
const query = (sql, params) => runner.query(sql, params);
|
||||||
|
const ctx = {
|
||||||
|
query,
|
||||||
|
...makeVersionContext(step.file, () => readSchemaVersion(query), (v) => writeSchemaVersion(query, v)),
|
||||||
|
};
|
||||||
|
await fn(ctx);
|
||||||
|
assertStepResult(step, await readSchemaVersion(query));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrades an existing database to `SCHEMA_VERSION`. Must run BEFORE the
|
||||||
|
* `schema.js` scripts so they never meet an outdated table shape.
|
||||||
|
* Fresh (empty) databases are skipped — `INIT_META` stamps them.
|
||||||
|
* A non-empty database without a version row is treated as version 1.
|
||||||
|
* @param {import('./index.js').MssqlProvider} provider
|
||||||
|
* @param {(message: string) => void} [log]
|
||||||
|
*/
|
||||||
|
export async function upgradeSchema(provider, log) {
|
||||||
|
const query = (sql, params) => provider.query(sql, params);
|
||||||
|
if (!(await hasAnyTable(query))) {
|
||||||
|
log?.(`Fresh database detected; schema will initialize directly at version ${SCHEMA_VERSION}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let current = await readSchemaVersion(query);
|
||||||
|
if (current === null) {
|
||||||
|
log?.(`Existing database has no version row; setting baseline version ${BASE_SCHEMA_VERSION}`);
|
||||||
|
await query(`
|
||||||
|
IF OBJECT_ID(N'dbo.meta', N'U') IS NULL
|
||||||
|
CREATE TABLE meta ([key] NVARCHAR(255) NOT NULL, value NVARCHAR(MAX) NOT NULL, CONSTRAINT PK_meta PRIMARY KEY ([key]));
|
||||||
|
`);
|
||||||
|
await writeSchemaVersion(query, BASE_SCHEMA_VERSION);
|
||||||
|
current = BASE_SCHEMA_VERSION;
|
||||||
|
}
|
||||||
|
return migrateSchema({
|
||||||
|
direction: 'upgrade',
|
||||||
|
dirUrl: UPGRADE_DIR,
|
||||||
|
current,
|
||||||
|
target: SCHEMA_VERSION,
|
||||||
|
runStep: makeRunStep(provider),
|
||||||
|
log,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downgrades the database to `target` (CLI only, never at server start).
|
||||||
|
* @param {import('./index.js').MssqlProvider} provider
|
||||||
|
* @param {number} target
|
||||||
|
* @param {(message: string) => void} [log]
|
||||||
|
*/
|
||||||
|
export async function downgradeSchema(provider, target, log) {
|
||||||
|
const current = await readSchemaVersion((sql, params) => provider.query(sql, params));
|
||||||
|
if (current === null) throw new Error('Database has no schema version (meta table missing)');
|
||||||
|
return migrateSchema({
|
||||||
|
direction: 'downgrade',
|
||||||
|
dirUrl: DOWNGRADE_DIR,
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
runStep: makeRunStep(provider),
|
||||||
|
log,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user