u
This commit is contained in:
6
API.md
6
API.md
@@ -19,7 +19,6 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- **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, 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)`
|
- **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)`
|
||||||
- **articletombstone**: `id PK, company_id->company, article_id?:i, sku?:s(100), name?:s(255), taxrate?:d(18,4), net?:f, deleted_at:dt`
|
|
||||||
- **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)`
|
- **attributevaluepic**: `id PK, attributevalue_id->attributevalue, pic_id->pic, sort:i=0, UQ(attributevalue_id, pic_id), UQ(attributevalue_id, sort)`
|
||||||
@@ -66,6 +65,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- **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`
|
- **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`
|
||||||
- **posterminal**: `id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt`
|
- **posterminal**: `id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), 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)`
|
||||||
|
- **posterminaltombstone**: `id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i, created_at:dt`
|
||||||
- **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)`
|
||||||
@@ -263,12 +263,14 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `getCustomerGroupCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
- `getCustomerGroupCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
|
||||||
- `getCustomerGroupList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<{customerGroupId: string, name: string, standard: string, discountPercent: string, lastChanged: string}>>`
|
- `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>>`
|
- `getCustomerList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
|
||||||
- `getDeletedEntityCount(input?: {posterminalId?: number, cursor?: number}): Promise<number>`
|
- `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|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknowntombstoneid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}>>`
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknowntombstoneid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}>>`
|
||||||
- `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}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>`
|
||||||
|
- `recordTombstone(input: {companyId: number, entityType: string, entityId: number}, tx?: object): Promise<void>`
|
||||||
- `setMaxKnownTombstoneId(id: number, maxknowntombstoneid: number|null, tx?: object): Promise<{id: number, maxknowntombstoneid: number|null}>`
|
- `setMaxKnownTombstoneId(id: number, maxknowntombstoneid: number|null, tx?: object): Promise<{id: number, maxknowntombstoneid: 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>}>`
|
||||||
|
|||||||
@@ -204,19 +204,40 @@ Lists customers for a POS terminal modified after the cursor, excluding 'Liefera
|
|||||||
|
|
||||||
*Source: `src/store/posterminal/getDeletedEntityCount.js`*
|
*Source: `src/store/posterminal/getDeletedEntityCount.js`*
|
||||||
|
|
||||||
Counts deleted entities after the given cursor (currently always 0).
|
Counts deleted entities for a POS terminal after the given cursor ID.
|
||||||
|
|
||||||
**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` | The highest synced tombstone ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
|
|
||||||
`Promise<number>` — Always 0.
|
`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 tombstone 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`
|
||||||
|
|
||||||
@@ -320,6 +341,26 @@ Completes POS device pairing for a terminal.
|
|||||||
|
|
||||||
`Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>`
|
`Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>`
|
||||||
|
|
||||||
|
## `posterminal.recordTombstone`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/recordTombstone.js`*
|
||||||
|
|
||||||
|
Records a tombstone 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.setMaxKnownTombstoneId`
|
## `posterminal.setMaxKnownTombstoneId`
|
||||||
|
|
||||||
*Source: `src/store/posterminal/setMaxKnownTombstoneId.js`*
|
*Source: `src/store/posterminal/setMaxKnownTombstoneId.js`*
|
||||||
|
|||||||
@@ -335,7 +335,6 @@ const SCHEMA_TABLES = [
|
|||||||
['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, 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)'],
|
['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)'],
|
||||||
['articletombstone', 'id PK, company_id->company, article_id?:i, sku?:s(100), name?:s(255), taxrate?:d(18,4), net?:f, deleted_at:dt'],
|
|
||||||
['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)'],
|
['attributevaluepic', 'id PK, attributevalue_id->attributevalue, pic_id->pic, sort:i=0, UQ(attributevalue_id, pic_id), UQ(attributevalue_id, sort)'],
|
||||||
@@ -382,6 +381,7 @@ const SCHEMA_TABLES = [
|
|||||||
['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'],
|
['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'],
|
||||||
['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt'],
|
['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), 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)'],
|
||||||
|
['posterminaltombstone', 'id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i, created_at:dt'],
|
||||||
['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)'],
|
||||||
|
|||||||
@@ -354,7 +354,6 @@ const CLUSTERS = [
|
|||||||
'article',
|
'article',
|
||||||
'articleean',
|
'articleean',
|
||||||
'articlehistory',
|
'articlehistory',
|
||||||
'articletombstone',
|
|
||||||
'articlelist',
|
'articlelist',
|
||||||
'articlelistarticle',
|
'articlelistarticle',
|
||||||
'optionslist',
|
'optionslist',
|
||||||
@@ -391,7 +390,7 @@ const CLUSTERS = [
|
|||||||
tables: ['tax', 'price', 'account', 'payment'],
|
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', 'labeltemplate'] },
|
{ id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'posterminaltombstone', 'printer', 'labeltemplate'] },
|
||||||
{
|
{
|
||||||
id: 'ai',
|
id: 'ai',
|
||||||
label: 'AI & Models',
|
label: 'AI & Models',
|
||||||
|
|||||||
@@ -127,6 +127,11 @@ async function run() {
|
|||||||
name: 'POS Register 1',
|
name: 'POS Register 1',
|
||||||
pairingpin: '1234',
|
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, subCat2.id);
|
||||||
await store.posterminal.setOn(googleAuthId, posTerminal.id, outsideCat.id);
|
await store.posterminal.setOn(googleAuthId, posTerminal.id, outsideCat.id);
|
||||||
|
|
||||||
@@ -170,7 +175,7 @@ async function run() {
|
|||||||
/Article not found/
|
/Article not found/
|
||||||
);
|
);
|
||||||
// tombstone for artOnlySub exists
|
// tombstone for artOnlySub exists
|
||||||
const t1 = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(artOnlySub.sku);
|
const t1 = provider.db.prepare("SELECT * FROM posterminaltombstone WHERE posterminal_id = ? AND entity_type = 'article' AND entity_id = ?").all(posTerminal.id, artOnlySub.id);
|
||||||
assert.equal(t1.length, 1);
|
assert.equal(t1.length, 1);
|
||||||
|
|
||||||
// artParent and its child artChild: both deleted
|
// artParent and its child artChild: both deleted
|
||||||
@@ -182,8 +187,8 @@ async function run() {
|
|||||||
() => store.article.getBySku(googleAuthId, artChild.article.sku),
|
() => store.article.getBySku(googleAuthId, artChild.article.sku),
|
||||||
/Article not found/
|
/Article not found/
|
||||||
);
|
);
|
||||||
const tParent = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(artParent.sku);
|
const tParent = provider.db.prepare("SELECT * FROM posterminaltombstone WHERE posterminal_id = ? AND entity_type = 'article' AND entity_id = ?").all(posTerminal.id, artParent.id);
|
||||||
const tChild = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(artChild.article.sku);
|
const tChild = provider.db.prepare("SELECT * FROM posterminaltombstone WHERE posterminal_id = ? AND entity_type = 'article' AND entity_id = ?").all(posTerminal.id, artChild.article.id);
|
||||||
assert.equal(tParent.length, 1);
|
assert.equal(tParent.length, 1);
|
||||||
assert.equal(tChild.length, 1);
|
assert.equal(tChild.length, 1);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import LibDashStore from '../src/index.js';
|
|||||||
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
console.log('--- Testing articletombstone table and deletion flow ---');
|
console.log('--- Testing posterminaltombstone table and deletion flow ---');
|
||||||
|
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), 'test-tombstone-'));
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-tombstone-'));
|
||||||
const dbFile = join(tempDir, 'test.db');
|
const dbFile = join(tempDir, 'test.db');
|
||||||
@@ -21,20 +21,17 @@ async function run() {
|
|||||||
const store = await new LibDashStore(provider);
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Verify table exists and has expected columns
|
// 1. Verify posterminaltombstone table exists and has expected columns
|
||||||
console.log('1. Checking articletombstone schema...');
|
console.log('1. Checking posterminaltombstone schema...');
|
||||||
const tableInfo = provider.db.pragma('table_info("articletombstone")');
|
const tableInfo = provider.db.pragma('table_info("posterminaltombstone")');
|
||||||
const colNames = tableInfo.map((c) => c.name);
|
const colNames = tableInfo.map((c) => c.name);
|
||||||
console.log('articletombstone columns:', colNames);
|
console.log('posterminaltombstone columns:', colNames);
|
||||||
assert.deepEqual(colNames, [
|
assert.deepEqual(colNames, [
|
||||||
'id',
|
'id',
|
||||||
'company_id',
|
'posterminal_id',
|
||||||
'article_id',
|
'entity_type',
|
||||||
'sku',
|
'entity_id',
|
||||||
'name',
|
'created_at',
|
||||||
'taxrate',
|
|
||||||
'net',
|
|
||||||
'deleted_at',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Setup company first
|
// Setup company first
|
||||||
@@ -49,154 +46,201 @@ async function run() {
|
|||||||
});
|
});
|
||||||
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
// 3. Test CHECK constraint (article_id IS NOT NULL OR sku IS NOT NULL)
|
// Setup active paired POS terminal
|
||||||
console.log('3. Testing CHECK constraint...');
|
const terminal = await store.posterminal.upsert(googleAuthId, {
|
||||||
// Setting both article_id and sku to NULL should fail
|
name: 'POS Terminal 1',
|
||||||
assert.throws(() => {
|
pairingpin: '1234',
|
||||||
provider.db.prepare(`
|
});
|
||||||
INSERT INTO articletombstone (company_id, article_id, sku, name)
|
await store.posterminal.pair(terminal.id, {
|
||||||
VALUES (?, NULL, NULL, 'Test')
|
token: 'pos-token-123',
|
||||||
`).run(company.id);
|
clientserial: 'SN-001',
|
||||||
}, /CHECK constraint failed/);
|
pairedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
// article_id present, sku null -> should succeed
|
|
||||||
const r1 = provider.db.prepare(`
|
|
||||||
INSERT INTO articletombstone (company_id, article_id, sku, name)
|
|
||||||
VALUES (?, 999, NULL, 'Only ID')
|
|
||||||
`).run(company.id);
|
|
||||||
assert.ok(r1.lastInsertRowid);
|
|
||||||
|
|
||||||
// sku present, article_id null -> should succeed
|
|
||||||
const r2 = provider.db.prepare(`
|
|
||||||
INSERT INTO articletombstone (company_id, article_id, sku, name)
|
|
||||||
VALUES (?, NULL, 'SKU-ONLY', 'Only SKU')
|
|
||||||
`).run(company.id);
|
|
||||||
assert.ok(r2.lastInsertRowid);
|
|
||||||
|
|
||||||
// Clean up manual test entries
|
|
||||||
provider.db.prepare('DELETE FROM articletombstone').run();
|
|
||||||
|
|
||||||
// 4. Test article deletion flow
|
|
||||||
console.log('4. Creating test article...');
|
|
||||||
|
|
||||||
|
// 3. Test article deletion flow
|
||||||
|
console.log('3. Testing article deletion tombstone...');
|
||||||
const article = await store.article.createUnderParent({
|
const article = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Coffee Mug',
|
name: 'Coffee Mug',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set prices for the article
|
|
||||||
await store.article.setPricesBySku(googleAuthId, article.sku, [
|
|
||||||
{ price: 12.50, tax: 19, minmultiplier: 1 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Verify article exists
|
|
||||||
const detailBefore = await store.article.getBySku(googleAuthId, article.sku);
|
|
||||||
assert.equal(detailBefore.article.name, 'Coffee Mug');
|
|
||||||
assert.equal(detailBefore.article.prices[0].net, 12.50);
|
|
||||||
assert.equal(detailBefore.article.prices[0].taxRate, 19);
|
|
||||||
|
|
||||||
// Delete the article
|
|
||||||
console.log('4. Deleting article and checking tombstone creation...');
|
|
||||||
await store.article.delete(googleAuthId, article.sku);
|
await store.article.delete(googleAuthId, article.sku);
|
||||||
|
|
||||||
// Verify article is deleted
|
// 4. Test category deletion flow
|
||||||
await assert.rejects(
|
console.log('4. Testing category deletion tombstone...');
|
||||||
() => store.article.getBySku(googleAuthId, article.sku),
|
const cat = await store.category.createUnderParent({
|
||||||
/Article not found/
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify tombstone record
|
|
||||||
const tombstones = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(article.sku);
|
|
||||||
assert.equal(tombstones.length, 1);
|
|
||||||
const tomb = tombstones[0];
|
|
||||||
console.log('Created tombstone:', tomb);
|
|
||||||
assert.equal(tomb.company_id, company.id);
|
|
||||||
assert.equal(tomb.article_id, article.id);
|
|
||||||
assert.equal(tomb.sku, article.sku);
|
|
||||||
assert.equal(tomb.name, 'Coffee Mug');
|
|
||||||
assert.equal(tomb.net, 12.50);
|
|
||||||
assert.equal(tomb.taxrate, 19);
|
|
||||||
assert.ok(tomb.deleted_at);
|
|
||||||
|
|
||||||
// 5. Test cascading deletion with parent and variants
|
|
||||||
console.log('5. Testing parent with variant child deletion...');
|
|
||||||
const parent = await store.article.createUnderParent({
|
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Pants',
|
name: 'Snacks',
|
||||||
});
|
});
|
||||||
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
|
await store.category.delete({ googleAuthId, id: cat.id });
|
||||||
await store.article.toggleParent(googleAuthId, parent.sku, {
|
|
||||||
isParent: true,
|
// 5. Test customer deletion flow
|
||||||
attributeNameId: attrName.id,
|
console.log('5. Testing customer deletion tombstone...');
|
||||||
|
const customer = await store.entity.create(googleAuthId, {
|
||||||
|
name: 'Mustermann',
|
||||||
|
firstName: 'Max',
|
||||||
|
customergroupId: company.posCustomerGroupId,
|
||||||
});
|
});
|
||||||
const child = await store.article.addChild(googleAuthId, parent.sku, { value: 'M' });
|
await store.entity.delete(googleAuthId, customer.id);
|
||||||
await store.article.setPricesBySku(googleAuthId, child.article.sku, [
|
|
||||||
{ price: 49.99, tax: 19, minmultiplier: 1 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Delete child via removeChild with deleteArticle
|
// 6. Test customer group deletion flow
|
||||||
console.log('Deleting child via removeChild({ deleteArticle: true })...');
|
console.log('6. Testing customer group deletion tombstone...');
|
||||||
await store.article.removeChild(googleAuthId, parent.sku, child.article.sku, { deleteArticle: true });
|
const cg = await store.customergroup.create(googleAuthId, {
|
||||||
|
name: 'VIP Guests',
|
||||||
|
});
|
||||||
|
await store.customergroup.delete(googleAuthId, cg.id);
|
||||||
|
|
||||||
// Add another child and delete parent with cascade: true
|
// 7. Verify all tombstones exist in posterminaltombstone
|
||||||
const child2 = await store.article.addChild(googleAuthId, parent.sku, { value: 'L' });
|
console.log('7. Verifying tombstones in database...');
|
||||||
console.log('Deleting parent with cascade: true...');
|
const allTombstones = provider.db
|
||||||
await store.article.delete(googleAuthId, parent.sku, { cascade: true });
|
.prepare('SELECT * FROM posterminaltombstone WHERE posterminal_id = ? ORDER BY id ASC')
|
||||||
|
.all(terminal.id);
|
||||||
const allTombstones = provider.db.prepare('SELECT * FROM articletombstone ORDER BY id ASC').all();
|
console.log('Created tombstones:', allTombstones);
|
||||||
assert.equal(allTombstones.length, 4);
|
assert.equal(allTombstones.length, 4);
|
||||||
|
|
||||||
// 1st tombstone: Coffee Mug
|
assert.equal(allTombstones[0].entity_type, 'article');
|
||||||
assert.equal(allTombstones[0].name, 'Coffee Mug');
|
assert.equal(allTombstones[0].entity_id, article.id);
|
||||||
assert.equal(allTombstones[0].sku, '1');
|
|
||||||
assert.equal(allTombstones[0].net, 12.5);
|
|
||||||
assert.equal(allTombstones[0].taxrate, 19);
|
|
||||||
|
|
||||||
// 2nd tombstone: Pants - M (deleted via removeChild)
|
assert.equal(allTombstones[1].entity_type, 'category');
|
||||||
assert.equal(allTombstones[1].name, 'Pants - M');
|
assert.equal(allTombstones[1].entity_id, cat.id);
|
||||||
assert.equal(allTombstones[1].sku, child.article.sku);
|
|
||||||
assert.equal(allTombstones[1].net, 49.99);
|
|
||||||
assert.equal(allTombstones[1].taxrate, 19);
|
|
||||||
|
|
||||||
// 3rd tombstone: Pants - L (child deleted in cascade)
|
assert.equal(allTombstones[2].entity_type, 'customer');
|
||||||
assert.equal(allTombstones[2].name, 'Pants - L');
|
assert.equal(allTombstones[2].entity_id, customer.id);
|
||||||
assert.equal(allTombstones[2].sku, child2.article.sku);
|
|
||||||
|
|
||||||
// 4th tombstone: Pants (parent deleted in cascade)
|
assert.equal(allTombstones[3].entity_type, 'customergroup');
|
||||||
assert.equal(allTombstones[3].name, 'Pants');
|
assert.equal(allTombstones[3].entity_id, cg.id);
|
||||||
assert.equal(allTombstones[3].sku, parent.sku);
|
|
||||||
|
|
||||||
// 6. Test posterminal maxknowntombstoneid column and updates
|
// 8. Test getDeletedEntityCount & getDeletedEntityList APIs
|
||||||
console.log('6. Testing posterminal maxknowntombstoneid column...');
|
console.log('8. Testing posterminal.getDeletedEntityCount & getDeletedEntityList...');
|
||||||
const posterminalCols = provider.db.pragma('table_info("posterminal")').map((c) => c.name);
|
const initialCount = await store.posterminal.getDeletedEntityCount({
|
||||||
assert.ok(posterminalCols.includes('maxknowntombstoneid'), 'posterminal should have maxknowntombstoneid column');
|
posterminalId: terminal.id,
|
||||||
|
cursor: 0,
|
||||||
const terminal = await store.posterminal.upsert(googleAuthId, {
|
|
||||||
name: 'POS Terminal 1',
|
|
||||||
pairingpin: '1234',
|
|
||||||
});
|
});
|
||||||
assert.equal(terminal.maxknowntombstoneid, null);
|
assert.equal(initialCount, 4);
|
||||||
|
|
||||||
// Pair terminal
|
const list = await store.posterminal.getDeletedEntityList({
|
||||||
await store.posterminal.pair(terminal.id, {
|
posterminalId: terminal.id,
|
||||||
token: 'tok-123',
|
cursor: 0,
|
||||||
clientserial: 'SN-001',
|
limit: 10,
|
||||||
pairedAt: new Date().toISOString(),
|
});
|
||||||
|
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 = allTombstones[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, allTombstones[2].id);
|
||||||
|
assert.equal(listAfterCursor[1].id, allTombstones[3].id);
|
||||||
|
|
||||||
|
// 9. Test maxknowntombstoneid
|
||||||
|
console.log('9. Testing maxknowntombstoneid...');
|
||||||
|
await store.posterminal.setMaxKnownTombstoneId(terminal.id, allTombstones[3].id);
|
||||||
|
const updatedTerminal = await store.posterminal.findByToken('pos-token-123');
|
||||||
|
assert.equal(updatedTerminal.maxknowntombstoneid, allTombstones[3].id);
|
||||||
|
|
||||||
|
// 10. Test setOff category & orphaned article tombstone 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',
|
||||||
});
|
});
|
||||||
|
|
||||||
const foundByToken = await store.posterminal.findByToken('tok-123');
|
// Art A only in Cat A
|
||||||
assert.equal(foundByToken.id, terminal.id);
|
const artA = await store.article.createUnderParent({
|
||||||
assert.equal(foundByToken.maxknowntombstoneid, null);
|
googleAuthId,
|
||||||
|
parentId: catA.id,
|
||||||
|
name: 'Article A (only in Cat A)',
|
||||||
|
});
|
||||||
|
|
||||||
// Update maxknowntombstoneid
|
// Art AB in both Cat A and Cat B
|
||||||
await store.posterminal.setMaxKnownTombstoneId(terminal.id, 4);
|
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]);
|
||||||
|
|
||||||
const foundAfterUpdate = await store.posterminal.findByToken('tok-123');
|
// Turn both categories on for the terminal
|
||||||
assert.equal(foundAfterUpdate.maxknowntombstoneid, 4);
|
await store.posterminal.setOn(googleAuthId, terminal.id, catA.id);
|
||||||
|
await store.posterminal.setOn(googleAuthId, terminal.id, catB.id);
|
||||||
|
|
||||||
console.log('✅ ALL TOMBSTONE TESTS PASSED');
|
// Clear previous tombstones to test cleanly
|
||||||
|
provider.db.prepare('DELETE FROM posterminaltombstone WHERE posterminal_id = ?').run(terminal.id);
|
||||||
|
|
||||||
|
// Turn off Cat A:
|
||||||
|
// - Cat A should get a tombstone
|
||||||
|
// - Art A should get a tombstone (orphaned on this terminal)
|
||||||
|
// - Art AB should NOT get a tombstone (still has active Cat B on this terminal)
|
||||||
|
await store.posterminal.setOff(googleAuthId, terminal.id, catA.id);
|
||||||
|
|
||||||
|
const tombstonesAfterSetOff = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminaltombstone WHERE posterminal_id = ? ORDER BY id ASC')
|
||||||
|
.all(terminal.id);
|
||||||
|
|
||||||
|
console.log('Tombstones after setOff(Cat A):', tombstonesAfterSetOff);
|
||||||
|
assert.equal(tombstonesAfterSetOff.length, 2);
|
||||||
|
assert.deepEqual(tombstonesAfterSetOff, [
|
||||||
|
{ entity_type: 'category', entity_id: catA.id },
|
||||||
|
{ entity_type: 'article', entity_id: artA.id },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 11. Test setOn category clears pending tombstones for that category and its articles
|
||||||
|
console.log('11. Testing setOn clears pending category and article tombstones...');
|
||||||
|
await store.posterminal.setOn(googleAuthId, terminal.id, catA.id);
|
||||||
|
const tombstonesAfterSetOn = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminaltombstone WHERE posterminal_id = ?')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Tombstones after setOn(Cat A):', tombstonesAfterSetOn);
|
||||||
|
assert.equal(tombstonesAfterSetOn.length, 0);
|
||||||
|
|
||||||
|
// 12. Test setOffAll generates tombstones for all active categories and all their articles
|
||||||
|
console.log('12. Testing setOffAll tombstones...');
|
||||||
|
await store.posterminal.setOffAll(googleAuthId, terminal.id);
|
||||||
|
const tombstonesAfterSetOffAll = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminaltombstone WHERE posterminal_id = ? ORDER BY id ASC')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Tombstones after setOffAll:', tombstonesAfterSetOffAll);
|
||||||
|
// Cat A, Cat B, Art A, Art AB
|
||||||
|
const catTombstones = tombstonesAfterSetOffAll.filter((t) => t.entity_type === 'category').map((t) => t.entity_id);
|
||||||
|
const artTombstones = tombstonesAfterSetOffAll.filter((t) => t.entity_type === 'article').map((t) => t.entity_id);
|
||||||
|
assert(catTombstones.includes(catA.id));
|
||||||
|
assert(catTombstones.includes(catB.id));
|
||||||
|
assert(artTombstones.includes(artA.id));
|
||||||
|
assert(artTombstones.includes(artAB.id));
|
||||||
|
|
||||||
|
// 13. Test setOnAll clears category and article tombstones
|
||||||
|
console.log('13. Testing setOnAll clears all category & article tombstones...');
|
||||||
|
await store.posterminal.setOnAll(googleAuthId, terminal.id);
|
||||||
|
const tombstonesAfterSetOnAll = provider.db
|
||||||
|
.prepare('SELECT entity_type, entity_id FROM posterminaltombstone WHERE posterminal_id = ?')
|
||||||
|
.all(terminal.id);
|
||||||
|
console.log('Tombstones after setOnAll:', tombstonesAfterSetOnAll);
|
||||||
|
assert.equal(tombstonesAfterSetOnAll.length, 0);
|
||||||
|
|
||||||
|
console.log('✅ ALL POSTERMINALTOMBSTONE TESTS PASSED');
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
rmSync(tempDir, { recursive: true, force: true });
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ import posterminalGetCustomerGroupCount from './store/posterminal/getCustomerGro
|
|||||||
import posterminalGetCustomerGroupList from './store/posterminal/getCustomerGroupList.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 posterminalRecordTombstone from './store/posterminal/recordTombstone.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';
|
||||||
@@ -488,6 +490,8 @@ export default class LibDashStore {
|
|||||||
getCustomerGroupList: posterminalGetCustomerGroupList(provider),
|
getCustomerGroupList: posterminalGetCustomerGroupList(provider),
|
||||||
getCompositeProductCount: posterminalGetCompositeProductCount(provider),
|
getCompositeProductCount: posterminalGetCompositeProductCount(provider),
|
||||||
getDeletedEntityCount: posterminalGetDeletedEntityCount(provider),
|
getDeletedEntityCount: posterminalGetDeletedEntityCount(provider),
|
||||||
|
getDeletedEntityList: posterminalGetDeletedEntityList(provider),
|
||||||
|
recordTombstone: posterminalRecordTombstone(provider),
|
||||||
getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider),
|
getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider),
|
||||||
getCategoryList: posterminalGetCategoryList(provider),
|
getCategoryList: posterminalGetCategoryList(provider),
|
||||||
getProductList: posterminalGetProductList(provider),
|
getProductList: posterminalGetProductList(provider),
|
||||||
|
|||||||
@@ -17,29 +17,6 @@ WHERE id = @articleId
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
export const CREATE_TOMBSTONE = `
|
|
||||||
INSERT INTO articletombstone (company_id, article_id, sku, name, taxrate, net)
|
|
||||||
SELECT
|
|
||||||
a.company_id,
|
|
||||||
a.id AS article_id,
|
|
||||||
a.sku,
|
|
||||||
COALESCE(
|
|
||||||
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
|
|
||||||
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id ORDER BY at.id ASC)
|
|
||||||
) AS name,
|
|
||||||
COALESCE(
|
|
||||||
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
|
|
||||||
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.id ASC)
|
|
||||||
) AS taxrate,
|
|
||||||
COALESCE(
|
|
||||||
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
|
|
||||||
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id ORDER BY pr.id ASC)
|
|
||||||
) AS net
|
|
||||||
FROM article a
|
|
||||||
INNER JOIN company co ON co.id = a.company_id
|
|
||||||
WHERE a.id = @articleId
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DELETE_PICS_BY_ARTICLE = `
|
export const DELETE_PICS_BY_ARTICLE = `
|
||||||
DELETE p
|
DELETE p
|
||||||
FROM pic p
|
FROM pic p
|
||||||
|
|||||||
@@ -482,3 +482,27 @@ INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.compan
|
|||||||
WHERE pr.minmultiplier = 1
|
WHERE pr.minmultiplier = 1
|
||||||
GROUP BY pr.article_id, pr.customergroup_id
|
GROUP BY pr.article_id, pr.customergroup_id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const RECORD_TOMBSTONE = `
|
||||||
|
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
|
||||||
|
SELECT pt.id, @entityType, @entityId
|
||||||
|
FROM posterminal pt
|
||||||
|
WHERE pt.company_id = @companyId AND pt.token IS NOT NULL
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const COUNT_DELETED_ENTITIES = `
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM posterminaltombstone
|
||||||
|
WHERE posterminal_id = @posterminalId AND id > @cursor
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LIST_DELETED_ENTITIES = `
|
||||||
|
SELECT TOP (@limit)
|
||||||
|
id,
|
||||||
|
entity_type AS entityType,
|
||||||
|
entity_id AS entityId
|
||||||
|
FROM posterminaltombstone
|
||||||
|
WHERE posterminal_id = @posterminalId AND id > @cursor
|
||||||
|
ORDER BY id ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -392,27 +392,24 @@ BEGIN
|
|||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const INIT_ARTICLETOMBSTONE = `
|
export const INIT_POSTERMINALTOMBSTONE = `
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articletombstone')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'posterminaltombstone')
|
||||||
BEGIN
|
BEGIN
|
||||||
CREATE TABLE articletombstone (
|
CREATE TABLE posterminaltombstone (
|
||||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
company_id INT NOT NULL,
|
posterminal_id INT NOT NULL,
|
||||||
article_id INT NULL,
|
entity_type NVARCHAR(50) NOT NULL,
|
||||||
sku NVARCHAR(100) NULL,
|
entity_id INT NOT NULL,
|
||||||
name NVARCHAR(255) NULL,
|
created_at DATETIME2 NOT NULL CONSTRAINT DF_posterminaltombstone_created_at DEFAULT SYSUTCDATETIME(),
|
||||||
taxrate DECIMAL(18,4) NULL,
|
CONSTRAINT FK_posterminaltombstone_posterminal FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE
|
||||||
net FLOAT NULL,
|
|
||||||
deleted_at DATETIME2 NOT NULL CONSTRAINT DF_articletombstone_deleted_at DEFAULT SYSUTCDATETIME(),
|
|
||||||
CONSTRAINT CK_articletombstone_article_or_sku CHECK (article_id IS NOT NULL OR sku IS NOT NULL),
|
|
||||||
CONSTRAINT FK_articletombstone_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IX_articletombstone_company_deleted
|
CREATE INDEX IX_posterminaltombstone_posterminal_id
|
||||||
ON articletombstone (company_id, deleted_at);
|
ON posterminaltombstone (posterminal_id, id);
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
export const INIT_ARTICLEEAN = `
|
export const INIT_ARTICLEEAN = `
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -1501,12 +1498,12 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_MODELMESSAGE,
|
INIT_MODELMESSAGE,
|
||||||
INIT_MODELVFS,
|
INIT_MODELVFS,
|
||||||
INIT_ARTICLEHISTORY,
|
INIT_ARTICLEHISTORY,
|
||||||
INIT_ARTICLETOMBSTONE,
|
|
||||||
INIT_ENTITYHISTORY,
|
INIT_ENTITYHISTORY,
|
||||||
INIT_COMPANYHISTORY,
|
INIT_COMPANYHISTORY,
|
||||||
INIT_COMPANYSETTING,
|
INIT_COMPANYSETTING,
|
||||||
INIT_POSTERMINAL,
|
INIT_POSTERMINAL,
|
||||||
INIT_POSTERMINALCATEGORY,
|
INIT_POSTERMINALCATEGORY,
|
||||||
|
INIT_POSTERMINALTOMBSTONE,
|
||||||
INIT_PRINTER,
|
INIT_PRINTER,
|
||||||
INIT_LABELTEMPLATE,
|
INIT_LABELTEMPLATE,
|
||||||
INIT_FILE,
|
INIT_FILE,
|
||||||
|
|||||||
@@ -7,29 +7,6 @@ export const {
|
|||||||
GET_CHILDREN_IDS, GET_LIST_IDS,
|
GET_CHILDREN_IDS, GET_LIST_IDS,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
export const CREATE_TOMBSTONE = `
|
|
||||||
INSERT INTO articletombstone (company_id, article_id, sku, name, taxrate, net)
|
|
||||||
SELECT
|
|
||||||
a.company_id,
|
|
||||||
a.id AS article_id,
|
|
||||||
a.sku,
|
|
||||||
COALESCE(
|
|
||||||
(SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
|
|
||||||
(SELECT at.name FROM articletext at WHERE at.article_id = a.id ORDER BY at.id ASC LIMIT 1)
|
|
||||||
) AS name,
|
|
||||||
COALESCE(
|
|
||||||
(SELECT t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
|
|
||||||
(SELECT t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.id ASC LIMIT 1)
|
|
||||||
) AS taxrate,
|
|
||||||
COALESCE(
|
|
||||||
(SELECT pr.net FROM price pr WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
|
|
||||||
(SELECT pr.net FROM price pr WHERE pr.article_id = a.id ORDER BY pr.id ASC LIMIT 1)
|
|
||||||
) AS net
|
|
||||||
FROM article a
|
|
||||||
INNER JOIN company co ON co.id = a.company_id
|
|
||||||
WHERE a.id = @articleId
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DELETE_PICS_BY_ARTICLE = `
|
export const DELETE_PICS_BY_ARTICLE = `
|
||||||
DELETE FROM pic
|
DELETE FROM pic
|
||||||
WHERE id IN (SELECT pic_id FROM articlepic WHERE article_id = @articleId)
|
WHERE id IN (SELECT pic_id FROM articlepic WHERE article_id = @articleId)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const {
|
|||||||
FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS,
|
FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS,
|
||||||
COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID,
|
COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID,
|
||||||
LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES,
|
LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES,
|
||||||
TOUCH_LASTSEEN,
|
TOUCH_LASTSEEN, RECORD_TOMBSTONE, COUNT_DELETED_ENTITIES,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
export const SET_ON = `
|
export const SET_ON = `
|
||||||
@@ -263,3 +263,15 @@ AND CAST(strftime('%s', a.updated_at) AS INTEGER) > @cursor
|
|||||||
ORDER BY lastChanged ASC, a.id ASC
|
ORDER BY lastChanged ASC, a.id ASC
|
||||||
LIMIT @limit
|
LIMIT @limit
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const LIST_DELETED_ENTITIES = `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
entity_type AS entityType,
|
||||||
|
entity_id AS entityId
|
||||||
|
FROM posterminaltombstone
|
||||||
|
WHERE posterminal_id = @posterminalId AND id > @cursor
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT @limit
|
||||||
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -573,22 +573,6 @@ CREATE TABLE IF NOT EXISTS companyhistory (
|
|||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const INIT_ARTICLETOMBSTONE = `
|
|
||||||
CREATE TABLE IF NOT EXISTS articletombstone (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
company_id INTEGER NOT NULL,
|
|
||||||
article_id INTEGER,
|
|
||||||
sku TEXT COLLATE NOCASE,
|
|
||||||
name TEXT,
|
|
||||||
taxrate NUMERIC,
|
|
||||||
net REAL,
|
|
||||||
deleted_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
|
||||||
CHECK (article_id IS NOT NULL OR sku IS NOT NULL),
|
|
||||||
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS IX_articletombstone_company_deleted
|
|
||||||
ON articletombstone (company_id, deleted_at);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const INIT_ENTITYHISTORY = `
|
export const INIT_ENTITYHISTORY = `
|
||||||
CREATE TABLE IF NOT EXISTS entityhistory (
|
CREATE TABLE IF NOT EXISTS entityhistory (
|
||||||
@@ -647,6 +631,19 @@ CREATE TABLE IF NOT EXISTS posterminalcategory (
|
|||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const INIT_POSTERMINALTOMBSTONE = `
|
||||||
|
CREATE TABLE IF NOT EXISTS posterminaltombstone (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
posterminal_id INTEGER NOT NULL,
|
||||||
|
entity_type TEXT NOT NULL,
|
||||||
|
entity_id INTEGER NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||||
|
FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_posterminaltombstone_posterminal_id
|
||||||
|
ON posterminaltombstone (posterminal_id, id);
|
||||||
|
`;
|
||||||
|
|
||||||
export const INIT_PRINTER = `
|
export const INIT_PRINTER = `
|
||||||
CREATE TABLE IF NOT EXISTS printer (
|
CREATE TABLE IF NOT EXISTS printer (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -1117,12 +1114,12 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_MODELMESSAGE,
|
INIT_MODELMESSAGE,
|
||||||
INIT_MODELVFS,
|
INIT_MODELVFS,
|
||||||
INIT_ARTICLEHISTORY,
|
INIT_ARTICLEHISTORY,
|
||||||
INIT_ARTICLETOMBSTONE,
|
|
||||||
INIT_ENTITYHISTORY,
|
INIT_ENTITYHISTORY,
|
||||||
INIT_COMPANYHISTORY,
|
INIT_COMPANYHISTORY,
|
||||||
INIT_COMPANYSETTING,
|
INIT_COMPANYSETTING,
|
||||||
INIT_POSTERMINAL,
|
INIT_POSTERMINAL,
|
||||||
INIT_POSTERMINALCATEGORY,
|
INIT_POSTERMINALCATEGORY,
|
||||||
|
INIT_POSTERMINALTOMBSTONE,
|
||||||
INIT_PRINTER,
|
INIT_PRINTER,
|
||||||
INIT_LABELTEMPLATE,
|
INIT_LABELTEMPLATE,
|
||||||
INIT_FILE,
|
INIT_FILE,
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import * as sqliteQueries from '../../providers/sqlite3/queries/articleDelete.js
|
|||||||
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||||
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import recordTombstone from '../posterminal/recordTombstone.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(provider) {
|
export default function del(provider) {
|
||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const tombstone = recordTombstone(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
@@ -37,13 +39,13 @@ export default function del(provider) {
|
|||||||
const cascade = Boolean(options.cascade);
|
const cascade = Boolean(options.cascade);
|
||||||
|
|
||||||
const runDelete = async (runner) => {
|
const runDelete = async (runner) => {
|
||||||
const { articleId } = await resolve(googleAuthId, sku, runner);
|
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const deleteArticleAndLists = async (id) => {
|
const deleteArticleAndLists = async (id) => {
|
||||||
const listIds = await runner.query(queries.GET_LIST_IDS, { articleId: id });
|
const listIds = await runner.query(queries.GET_LIST_IDS, { articleId: id });
|
||||||
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
|
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
|
||||||
|
|
||||||
await runner.query(queries.CREATE_TOMBSTONE, { articleId: id });
|
await tombstone({ companyId, entityType: 'article', entityId: id }, runner);
|
||||||
|
|
||||||
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id });
|
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id });
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export default function addImage(provider) {
|
|||||||
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
|
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
|
||||||
const createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC;
|
const createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC;
|
||||||
const addCategoryPicSql = isSqlite ? SQLITE_ADD_CATEGORYPIC : MSSQL_ADD_CATEGORYPIC;
|
const addCategoryPicSql = isSqlite ? SQLITE_ADD_CATEGORYPIC : MSSQL_ADD_CATEGORYPIC;
|
||||||
|
const touchSql = isSqlite
|
||||||
|
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
|
||||||
|
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an image to a category, processing full, thumb, and 32px-edge small-thumb AVIF variants.
|
* Adds an image to a category, processing full, thumb, and 32px-edge small-thumb AVIF variants.
|
||||||
@@ -95,6 +98,8 @@ export default function addImage(provider) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await runner.query(touchSql, { categoryId });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: categoryId,
|
id: categoryId,
|
||||||
fullHash,
|
fullHash,
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import * as sqliteArticleDelete from '../../providers/sqlite3/queries/articleDel
|
|||||||
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||||
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
|
import recordTombstone from '../posterminal/recordTombstone.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(provider) {
|
export default function del(provider) {
|
||||||
const access = resolveCategoryAccess(provider);
|
const access = resolveCategoryAccess(provider);
|
||||||
|
const tombstone = recordTombstone(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
const sql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
const sql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
const articleDeleteQueries = isSqlite ? sqliteArticleDelete : mssqlArticleDelete;
|
const articleDeleteQueries = isSqlite ? sqliteArticleDelete : mssqlArticleDelete;
|
||||||
@@ -32,7 +34,7 @@ export default function del(provider) {
|
|||||||
*/
|
*/
|
||||||
return async ({ googleAuthId, id, recursive = false }, tx) => {
|
return async ({ googleAuthId, id, recursive = false }, tx) => {
|
||||||
const run = async (runner) => {
|
const run = async (runner) => {
|
||||||
const { rootCategoryId } = await access(googleAuthId, id, runner);
|
const { rootCategoryId, companyId } = await access(googleAuthId, id, runner);
|
||||||
|
|
||||||
if (id === rootCategoryId) {
|
if (id === rootCategoryId) {
|
||||||
throw new Error('Cannot delete root category');
|
throw new Error('Cannot delete root category');
|
||||||
@@ -65,6 +67,7 @@ export default function del(provider) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await runner.query(sql, { id });
|
await runner.query(sql, { id });
|
||||||
|
await tombstone({ companyId, entityType: 'category', entityId: id }, runner);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (
|
if (
|
||||||
err.message?.includes('FOREIGN KEY constraint failed') ||
|
err.message?.includes('FOREIGN KEY constraint failed') ||
|
||||||
@@ -146,7 +149,7 @@ export default function del(provider) {
|
|||||||
const listIds = await runner.query(articleDeleteQueries.GET_LIST_IDS, { articleId: artId });
|
const listIds = await runner.query(articleDeleteQueries.GET_LIST_IDS, { articleId: artId });
|
||||||
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
|
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
|
||||||
|
|
||||||
await runner.query(articleDeleteQueries.CREATE_TOMBSTONE, { articleId: artId });
|
await tombstone({ companyId, entityType: 'article', entityId: artId }, runner);
|
||||||
await runner.query(articleDeleteQueries.DELETE_PICS_BY_ARTICLE, { articleId: artId });
|
await runner.query(articleDeleteQueries.DELETE_PICS_BY_ARTICLE, { articleId: artId });
|
||||||
|
|
||||||
const deleted = await runner.query(articleDeleteQueries.DELETE_BY_ID, { articleId: artId });
|
const deleted = await runner.query(articleDeleteQueries.DELETE_BY_ID, { articleId: artId });
|
||||||
@@ -215,6 +218,7 @@ export default function del(provider) {
|
|||||||
const reversedIds = [...subtreeIds].reverse();
|
const reversedIds = [...subtreeIds].reverse();
|
||||||
for (const catId of reversedIds) {
|
for (const catId of reversedIds) {
|
||||||
await runner.query(sql, { id: catId });
|
await runner.query(sql, { id: catId });
|
||||||
|
await tombstone({ companyId, entityType: 'category', entityId: catId }, runner);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import makeRunner from '../runner.js';
|
|||||||
export default function deleteImage(provider) {
|
export default function deleteImage(provider) {
|
||||||
const access = resolveCategoryAccess(provider);
|
const access = resolveCategoryAccess(provider);
|
||||||
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const touchSql = isSqlite
|
||||||
|
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
|
||||||
|
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the image of a category.
|
* Deletes the image of a category.
|
||||||
@@ -30,6 +34,8 @@ export default function deleteImage(provider) {
|
|||||||
throw new Error('Image not found');
|
throw new Error('Image not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await runner.query(touchSql, { categoryId });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: categoryId,
|
id: categoryId,
|
||||||
thumbW: null,
|
thumbW: null,
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import makeRunner from '../runner.js';
|
|||||||
export default function rename(provider) {
|
export default function rename(provider) {
|
||||||
const access = resolveCategoryAccess(provider);
|
const access = resolveCategoryAccess(provider);
|
||||||
const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPDATE : MSSQL_UPDATE;
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPDATE : MSSQL_UPDATE;
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const touchSql = isSqlite
|
||||||
|
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
|
||||||
|
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames a category and updates its slug.
|
* Renames a category and updates its slug.
|
||||||
@@ -33,7 +37,8 @@ export default function rename(provider) {
|
|||||||
|
|
||||||
const slug = slugify(cleaned);
|
const slug = slugify(cleaned);
|
||||||
|
|
||||||
const result = await makeRunner(provider, tx).query(sql, {
|
const runner = makeRunner(provider, tx);
|
||||||
|
const result = await runner.query(sql, {
|
||||||
categoryId: id,
|
categoryId: id,
|
||||||
languageId,
|
languageId,
|
||||||
name: cleaned,
|
name: cleaned,
|
||||||
@@ -44,6 +49,8 @@ export default function rename(provider) {
|
|||||||
throw new Error('Category not found');
|
throw new Error('Category not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await runner.query(touchSql, { categoryId: id });
|
||||||
|
|
||||||
return { id, name: cleaned, slug };
|
return { id, name: cleaned, slug };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE_BY_COMPANY_AND_ID } from '../.
|
|||||||
import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE_BY_COMPANY_AND_ID } from '../../providers/sqlite3/queries/customergroup.js';
|
import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE_BY_COMPANY_AND_ID } from '../../providers/sqlite3/queries/customergroup.js';
|
||||||
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import recordTombstone from '../posterminal/recordTombstone.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(provider) {
|
export default function del(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(provider);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const tombstone = recordTombstone(provider);
|
||||||
const sql =
|
const sql =
|
||||||
provider?.dialect === 'sqlite3'
|
provider?.dialect === 'sqlite3'
|
||||||
? SQLITE_DELETE_BY_COMPANY_AND_ID
|
? SQLITE_DELETE_BY_COMPANY_AND_ID
|
||||||
@@ -31,7 +33,8 @@ export default function del(provider) {
|
|||||||
const { companyId } = await access(googleAuthId, tx);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await makeRunner(provider, tx).query(sql, {
|
const runner = makeRunner(provider, tx);
|
||||||
|
const result = await runner.query(sql, {
|
||||||
id,
|
id,
|
||||||
companyId,
|
companyId,
|
||||||
});
|
});
|
||||||
@@ -39,6 +42,8 @@ export default function del(provider) {
|
|||||||
if (!result.rowsAffected) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Customer group not found');
|
throw new Error('Customer group not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await tombstone({ companyId, entityType: 'customergroup', entityId: id }, runner);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isForeignKeyConstraintError(err)) {
|
if (isForeignKeyConstraintError(err)) {
|
||||||
throw new Error('Customer group in use');
|
throw new Error('Customer group in use');
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import {
|
|||||||
GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID,
|
GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID,
|
||||||
} from '../../providers/sqlite3/queries/entity.js';
|
} from '../../providers/sqlite3/queries/entity.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import recordTombstone from '../posterminal/recordTombstone.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(provider) {
|
export default function del(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(provider);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const tombstone = recordTombstone(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
const sql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
const sql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
const checkInUseSql = isSqlite ? SQLITE_CHECK_IN_USE : MSSQL_CHECK_IN_USE;
|
const checkInUseSql = isSqlite ? SQLITE_CHECK_IN_USE : MSSQL_CHECK_IN_USE;
|
||||||
@@ -63,6 +65,8 @@ export default function del(provider) {
|
|||||||
if (!result.rowsAffected) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await tombstone({ companyId, entityType: 'customer', entityId: numericId }, runner);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
export default function getDeletedEntityCount() {
|
import { COUNT_DELETED_ENTITIES as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js';
|
||||||
|
import { COUNT_DELETED_ENTITIES as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function getDeletedEntityCount(provider) {
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts deleted entities after the given cursor (currently always 0).
|
* Counts deleted entities for a POS terminal after the given cursor ID.
|
||||||
*
|
*
|
||||||
* @param {object} [input]
|
* @param {object} input
|
||||||
* @param {number} [input.posterminalId] - The POS terminal id.
|
* @param {number} input.posterminalId - The POS terminal id.
|
||||||
* @param {number} [input.cursor=0] - Unix timestamp cursor.
|
* @param {number} [input.cursor=0] - The highest synced tombstone ID.
|
||||||
* @returns {Promise<number>} Always 0.
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<number>} Count of deleted entities.
|
||||||
*/
|
*/
|
||||||
return async () => 0;
|
return async ({ posterminalId, cursor = 0 } = {}, tx) => {
|
||||||
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
|
posterminalId: Number(posterminalId),
|
||||||
|
cursor: Number(cursor) || 0,
|
||||||
|
});
|
||||||
|
return Number(result.rows[0]?.count ?? 0);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/store/posterminal/getDeletedEntityList.js
Normal file
32
src/store/posterminal/getDeletedEntityList.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { LIST_DELETED_ENTITIES as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js';
|
||||||
|
import { LIST_DELETED_ENTITIES as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function getDeletedEntityList(provider) {
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists deleted entities for a POS terminal after the given cursor ID.
|
||||||
|
*
|
||||||
|
* @param {object} input
|
||||||
|
* @param {number} input.posterminalId - The POS terminal id.
|
||||||
|
* @param {number} [input.cursor=0] - The highest synced tombstone ID.
|
||||||
|
* @param {number} [input.limit=200] - Max items to return.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<Array<{id: number, entityType: string, entityId: number}>>}
|
||||||
|
*/
|
||||||
|
return async ({ posterminalId, cursor = 0, limit = 200 } = {}, tx) => {
|
||||||
|
const safeLimit = Number.isInteger(limit) && limit > 0 ? limit : 200;
|
||||||
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
|
posterminalId: Number(posterminalId),
|
||||||
|
cursor: Number(cursor) || 0,
|
||||||
|
limit: safeLimit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.rows.map((row) => ({
|
||||||
|
id: Number(row.id),
|
||||||
|
entityType: String(row.entityType),
|
||||||
|
entityId: Number(row.entityId),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
27
src/store/posterminal/recordTombstone.js
Normal file
27
src/store/posterminal/recordTombstone.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { RECORD_TOMBSTONE as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js';
|
||||||
|
import { RECORD_TOMBSTONE as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function recordTombstone(provider) {
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a tombstone for all active paired POS terminals of a company.
|
||||||
|
*
|
||||||
|
* @param {object} input
|
||||||
|
* @param {number} input.companyId - The company id.
|
||||||
|
* @param {string} input.entityType - 'category' | 'article' | 'customer' | 'customergroup'
|
||||||
|
* @param {number} input.entityId - The deleted entity's id.
|
||||||
|
* @param {object} [tx] - Optional transaction/runner.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
return async ({ companyId, entityType, entityId }, tx) => {
|
||||||
|
if (!companyId || !entityType || !entityId) return;
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
await runner.query(sql, {
|
||||||
|
companyId: Number(companyId),
|
||||||
|
entityType: String(entityType).toLowerCase().trim(),
|
||||||
|
entityId: Number(entityId),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -21,6 +21,26 @@ export default function setOff(provider) {
|
|||||||
? SQLITE_LIST_CATEGORY_IDS
|
? SQLITE_LIST_CATEGORY_IDS
|
||||||
: MSSQL_LIST_CATEGORY_IDS;
|
: MSSQL_LIST_CATEGORY_IDS;
|
||||||
|
|
||||||
|
const addCategoryTombstoneSql = `
|
||||||
|
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
|
||||||
|
VALUES (@posterminalId, 'category', @categoryId)
|
||||||
|
`;
|
||||||
|
|
||||||
|
const addArticleTombstonesSql = `
|
||||||
|
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
|
||||||
|
SELECT DISTINCT @posterminalId, 'article', ca.article_id
|
||||||
|
FROM categoryarticle ca
|
||||||
|
WHERE ca.category_id = @categoryId
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM categoryarticle ca2
|
||||||
|
INNER JOIN posterminalcategory pc ON pc.category_id = ca2.category_id
|
||||||
|
WHERE ca2.article_id = ca.article_id
|
||||||
|
AND pc.posterminal_id = @posterminalId
|
||||||
|
AND pc.category_id <> @categoryId
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a category assignment from a posterminal (turns it off).
|
* Removes a category assignment from a posterminal (turns it off).
|
||||||
*
|
*
|
||||||
@@ -49,6 +69,16 @@ export default function setOff(provider) {
|
|||||||
throw new Error('Posterminal not found');
|
throw new Error('Posterminal not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await runner.query(addCategoryTombstoneSql, {
|
||||||
|
posterminalId,
|
||||||
|
categoryId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runner.query(addArticleTombstonesSql, {
|
||||||
|
posterminalId,
|
||||||
|
categoryId,
|
||||||
|
});
|
||||||
|
|
||||||
await runner.query(setOffSql, {
|
await runner.query(setOffSql, {
|
||||||
posterminalId,
|
posterminalId,
|
||||||
categoryId,
|
categoryId,
|
||||||
|
|||||||
@@ -21,6 +21,21 @@ export default function setOffAll(provider) {
|
|||||||
? SQLITE_LIST_CATEGORY_IDS
|
? SQLITE_LIST_CATEGORY_IDS
|
||||||
: MSSQL_LIST_CATEGORY_IDS;
|
: MSSQL_LIST_CATEGORY_IDS;
|
||||||
|
|
||||||
|
const addCategoryTombstonesSql = `
|
||||||
|
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
|
||||||
|
SELECT posterminal_id, 'category', category_id
|
||||||
|
FROM posterminalcategory
|
||||||
|
WHERE posterminal_id = @posterminalId
|
||||||
|
`;
|
||||||
|
|
||||||
|
const addArticleTombstonesSql = `
|
||||||
|
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
|
||||||
|
SELECT DISTINCT pc.posterminal_id, 'article', ca.article_id
|
||||||
|
FROM posterminalcategory pc
|
||||||
|
INNER JOIN categoryarticle ca ON ca.category_id = pc.category_id
|
||||||
|
WHERE pc.posterminal_id = @posterminalId
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all category assignments from a posterminal.
|
* Removes all category assignments from a posterminal.
|
||||||
*
|
*
|
||||||
@@ -49,6 +64,9 @@ export default function setOffAll(provider) {
|
|||||||
throw new Error('Posterminal not found');
|
throw new Error('Posterminal not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await runner.query(addCategoryTombstonesSql, { posterminalId });
|
||||||
|
await runner.query(addArticleTombstonesSql, { posterminalId });
|
||||||
|
|
||||||
await runner.query(setOffAllSql, { posterminalId });
|
await runner.query(setOffAllSql, { posterminalId });
|
||||||
|
|
||||||
const result = await runner.query(listCatIdsSql, { posterminalId });
|
const result = await runner.query(listCatIdsSql, { posterminalId });
|
||||||
|
|||||||
@@ -20,6 +20,31 @@ export default function setOn(provider) {
|
|||||||
const listCatIdsSql = isSqlite
|
const listCatIdsSql = isSqlite
|
||||||
? SQLITE_LIST_CATEGORY_IDS
|
? SQLITE_LIST_CATEGORY_IDS
|
||||||
: MSSQL_LIST_CATEGORY_IDS;
|
: MSSQL_LIST_CATEGORY_IDS;
|
||||||
|
const touchSql = isSqlite
|
||||||
|
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
|
||||||
|
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
|
||||||
|
const touchArticlesSql = isSqlite
|
||||||
|
? `UPDATE article
|
||||||
|
SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id = @categoryId
|
||||||
|
)`
|
||||||
|
: `UPDATE article
|
||||||
|
SET updated_at = SYSUTCDATETIME()
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id = @categoryId
|
||||||
|
)`;
|
||||||
|
|
||||||
|
const clearTombstonesSql = `
|
||||||
|
DELETE FROM posterminaltombstone
|
||||||
|
WHERE posterminal_id = @posterminalId
|
||||||
|
AND (
|
||||||
|
(entity_type = 'category' AND entity_id = @categoryId)
|
||||||
|
OR (entity_type = 'article' AND entity_id IN (
|
||||||
|
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id = @categoryId
|
||||||
|
))
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigns a category to a posterminal (turns it on for that category).
|
* Assigns a category to a posterminal (turns it on for that category).
|
||||||
@@ -54,6 +79,14 @@ export default function setOn(provider) {
|
|||||||
categoryId,
|
categoryId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await runner.query(clearTombstonesSql, {
|
||||||
|
posterminalId,
|
||||||
|
categoryId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runner.query(touchSql, { categoryId });
|
||||||
|
await runner.query(touchArticlesSql, { categoryId });
|
||||||
|
|
||||||
const result = await runner.query(listCatIdsSql, { posterminalId });
|
const result = await runner.query(listCatIdsSql, { posterminalId });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -23,6 +23,35 @@ export default function setOnAll(provider) {
|
|||||||
? SQLITE_LIST_CATEGORY_IDS
|
? SQLITE_LIST_CATEGORY_IDS
|
||||||
: MSSQL_LIST_CATEGORY_IDS;
|
: MSSQL_LIST_CATEGORY_IDS;
|
||||||
const rootSql = isSqlite ? SQLITE_ROOT : MSSQL_ROOT;
|
const rootSql = isSqlite ? SQLITE_ROOT : MSSQL_ROOT;
|
||||||
|
const touchAllSql = isSqlite
|
||||||
|
? `WITH RECURSIVE cats AS (
|
||||||
|
SELECT id FROM category WHERE id = @rootCategoryId
|
||||||
|
UNION ALL
|
||||||
|
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
|
||||||
|
)
|
||||||
|
UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||||
|
WHERE id IN (SELECT id FROM cats)`
|
||||||
|
: `;WITH cats AS (
|
||||||
|
SELECT id FROM category WHERE id = @rootCategoryId
|
||||||
|
UNION ALL
|
||||||
|
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
|
||||||
|
)
|
||||||
|
UPDATE category SET updated_at = SYSUTCDATETIME()
|
||||||
|
WHERE id IN (SELECT id FROM cats)`;
|
||||||
|
|
||||||
|
const touchAllArticlesSql = isSqlite
|
||||||
|
? `UPDATE article
|
||||||
|
SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
||||||
|
WHERE company_id = @companyId`
|
||||||
|
: `UPDATE article
|
||||||
|
SET updated_at = SYSUTCDATETIME()
|
||||||
|
WHERE company_id = @companyId`;
|
||||||
|
|
||||||
|
const clearTombstonesSql = `
|
||||||
|
DELETE FROM posterminaltombstone
|
||||||
|
WHERE posterminal_id = @posterminalId
|
||||||
|
AND entity_type IN ('category', 'article')
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigns all categories of the user's selected company to a posterminal.
|
* Assigns all categories of the user's selected company to a posterminal.
|
||||||
@@ -65,6 +94,13 @@ export default function setOnAll(provider) {
|
|||||||
rootCategoryId,
|
rootCategoryId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await runner.query(clearTombstonesSql, {
|
||||||
|
posterminalId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runner.query(touchAllSql, { rootCategoryId });
|
||||||
|
await runner.query(touchAllArticlesSql, { companyId });
|
||||||
|
|
||||||
const result = await runner.query(listCatIdsSql, { posterminalId });
|
const result = await runner.query(listCatIdsSql, { posterminalId });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user