This commit is contained in:
seb
2026-10-22 12:30:06 +02:00
parent bb880e3c84
commit 9495ad51cc
27 changed files with 539 additions and 238 deletions

View File

@@ -127,6 +127,11 @@ async function run() {
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);
@@ -170,7 +175,7 @@ async function run() {
/Article not found/
);
// 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);
// artParent and its child artChild: both deleted
@@ -182,8 +187,8 @@ async function run() {
() => store.article.getBySku(googleAuthId, artChild.article.sku),
/Article not found/
);
const tParent = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(artParent.sku);
const tChild = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(artChild.article.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 posterminaltombstone 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);

View File

@@ -6,7 +6,7 @@ import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing articletombstone table and deletion flow ---');
console.log('--- Testing posterminaltombstone table and deletion flow ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-tombstone-'));
const dbFile = join(tempDir, 'test.db');
@@ -21,20 +21,17 @@ async function run() {
const store = await new LibDashStore(provider);
try {
// 1. Verify table exists and has expected columns
console.log('1. Checking articletombstone schema...');
const tableInfo = provider.db.pragma('table_info("articletombstone")');
// 1. Verify posterminaltombstone table exists and has expected columns
console.log('1. Checking posterminaltombstone schema...');
const tableInfo = provider.db.pragma('table_info("posterminaltombstone")');
const colNames = tableInfo.map((c) => c.name);
console.log('articletombstone columns:', colNames);
console.log('posterminaltombstone columns:', colNames);
assert.deepEqual(colNames, [
'id',
'company_id',
'article_id',
'sku',
'name',
'taxrate',
'net',
'deleted_at',
'posterminal_id',
'entity_type',
'entity_id',
'created_at',
]);
// Setup company first
@@ -49,154 +46,201 @@ async function run() {
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// 3. Test CHECK constraint (article_id IS NOT NULL OR sku IS NOT NULL)
console.log('3. Testing CHECK constraint...');
// Setting both article_id and sku to NULL should fail
assert.throws(() => {
provider.db.prepare(`
INSERT INTO articletombstone (company_id, article_id, sku, name)
VALUES (?, NULL, NULL, 'Test')
`).run(company.id);
}, /CHECK constraint failed/);
// 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...');
// 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 tombstone...');
const article = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
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);
// Verify article is deleted
await assert.rejects(
() => store.article.getBySku(googleAuthId, article.sku),
/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({
// 4. Test category deletion flow
console.log('4. Testing category deletion tombstone...');
const cat = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Pants',
name: 'Snacks',
});
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
await store.article.toggleParent(googleAuthId, parent.sku, {
isParent: true,
attributeNameId: attrName.id,
await store.category.delete({ googleAuthId, id: cat.id });
// 5. Test customer deletion flow
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.article.setPricesBySku(googleAuthId, child.article.sku, [
{ price: 49.99, tax: 19, minmultiplier: 1 },
]);
await store.entity.delete(googleAuthId, customer.id);
// Delete child via removeChild with deleteArticle
console.log('Deleting child via removeChild({ deleteArticle: true })...');
await store.article.removeChild(googleAuthId, parent.sku, child.article.sku, { deleteArticle: true });
// 6. Test customer group deletion flow
console.log('6. Testing customer group deletion tombstone...');
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
const child2 = await store.article.addChild(googleAuthId, parent.sku, { value: 'L' });
console.log('Deleting parent with cascade: true...');
await store.article.delete(googleAuthId, parent.sku, { cascade: true });
const allTombstones = provider.db.prepare('SELECT * FROM articletombstone ORDER BY id ASC').all();
// 7. Verify all tombstones exist in posterminaltombstone
console.log('7. Verifying tombstones in database...');
const allTombstones = provider.db
.prepare('SELECT * FROM posterminaltombstone WHERE posterminal_id = ? ORDER BY id ASC')
.all(terminal.id);
console.log('Created tombstones:', allTombstones);
assert.equal(allTombstones.length, 4);
// 1st tombstone: Coffee Mug
assert.equal(allTombstones[0].name, 'Coffee Mug');
assert.equal(allTombstones[0].sku, '1');
assert.equal(allTombstones[0].net, 12.5);
assert.equal(allTombstones[0].taxrate, 19);
assert.equal(allTombstones[0].entity_type, 'article');
assert.equal(allTombstones[0].entity_id, article.id);
// 2nd tombstone: Pants - M (deleted via removeChild)
assert.equal(allTombstones[1].name, 'Pants - M');
assert.equal(allTombstones[1].sku, child.article.sku);
assert.equal(allTombstones[1].net, 49.99);
assert.equal(allTombstones[1].taxrate, 19);
assert.equal(allTombstones[1].entity_type, 'category');
assert.equal(allTombstones[1].entity_id, cat.id);
// 3rd tombstone: Pants - L (child deleted in cascade)
assert.equal(allTombstones[2].name, 'Pants - L');
assert.equal(allTombstones[2].sku, child2.article.sku);
assert.equal(allTombstones[2].entity_type, 'customer');
assert.equal(allTombstones[2].entity_id, customer.id);
// 4th tombstone: Pants (parent deleted in cascade)
assert.equal(allTombstones[3].name, 'Pants');
assert.equal(allTombstones[3].sku, parent.sku);
assert.equal(allTombstones[3].entity_type, 'customergroup');
assert.equal(allTombstones[3].entity_id, cg.id);
// 6. Test posterminal maxknowntombstoneid column and updates
console.log('6. Testing posterminal maxknowntombstoneid column...');
const posterminalCols = provider.db.pragma('table_info("posterminal")').map((c) => c.name);
assert.ok(posterminalCols.includes('maxknowntombstoneid'), 'posterminal should have maxknowntombstoneid column');
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS Terminal 1',
pairingpin: '1234',
// 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(terminal.maxknowntombstoneid, null);
assert.equal(initialCount, 4);
// Pair terminal
await store.posterminal.pair(terminal.id, {
token: 'tok-123',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
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 = 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');
assert.equal(foundByToken.id, terminal.id);
assert.equal(foundByToken.maxknowntombstoneid, null);
// Art A only in Cat A
const artA = await store.article.createUnderParent({
googleAuthId,
parentId: catA.id,
name: 'Article A (only in Cat A)',
});
// Update maxknowntombstoneid
await store.posterminal.setMaxKnownTombstoneId(terminal.id, 4);
// 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]);
const foundAfterUpdate = await store.posterminal.findByToken('tok-123');
assert.equal(foundAfterUpdate.maxknowntombstoneid, 4);
// 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);
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 {
try {
rmSync(tempDir, { recursive: true, force: true });