feat: add SQLite-backed products store
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013gwc3aCiyQxmCWQke8RvEz
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const SCHEMA = `
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
last_price REAL,
|
||||
last_checked_at TEXT,
|
||||
last_check_status TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)`;
|
||||
|
||||
function openDb(dbPath) {
|
||||
if (dbPath !== ':memory:') {
|
||||
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
||||
}
|
||||
const db = new Database(dbPath);
|
||||
db.exec(SCHEMA);
|
||||
return db;
|
||||
}
|
||||
|
||||
function listProducts(db) {
|
||||
return db.prepare('SELECT * FROM products ORDER BY created_at ASC').all();
|
||||
}
|
||||
|
||||
function getProduct(db, id) {
|
||||
return db.prepare('SELECT * FROM products WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
function addProduct(db, { name, url }) {
|
||||
const createdAt = new Date().toISOString();
|
||||
const info = db.prepare(
|
||||
'INSERT INTO products (name, url, created_at) VALUES (?, ?, ?)'
|
||||
).run(name, url, createdAt);
|
||||
return getProduct(db, info.lastInsertRowid);
|
||||
}
|
||||
|
||||
function deleteProduct(db, id) {
|
||||
db.prepare('DELETE FROM products WHERE id = ?').run(id);
|
||||
}
|
||||
|
||||
function recordCheckSuccess(db, id, { price, checkedAt }) {
|
||||
db.prepare(
|
||||
'UPDATE products SET last_price = ?, last_checked_at = ?, last_check_status = ? WHERE id = ?'
|
||||
).run(price, checkedAt, 'ok', id);
|
||||
}
|
||||
|
||||
function recordCheckFailure(db, id, status) {
|
||||
db.prepare('UPDATE products SET last_check_status = ? WHERE id = ?').run(status, id);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
openDb, listProducts, getProduct, addProduct, deleteProduct,
|
||||
recordCheckSuccess, recordCheckFailure,
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
openDb, listProducts, getProduct, addProduct, deleteProduct,
|
||||
recordCheckSuccess, recordCheckFailure,
|
||||
} = require('../src/db');
|
||||
|
||||
function freshDb() {
|
||||
return openDb(':memory:');
|
||||
}
|
||||
|
||||
test('addProduct inserts a row with defaults and returns it', () => {
|
||||
const db = freshDb();
|
||||
const product = addProduct(db, { name: 'Voorbeeld', url: 'https://example.com' });
|
||||
assert.equal(product.name, 'Voorbeeld');
|
||||
assert.equal(product.url, 'https://example.com');
|
||||
assert.equal(product.last_price, null);
|
||||
assert.equal(product.last_checked_at, null);
|
||||
assert.ok(product.created_at);
|
||||
assert.ok(product.id);
|
||||
});
|
||||
|
||||
test('listProducts returns all inserted products in insertion order', () => {
|
||||
const db = freshDb();
|
||||
addProduct(db, { name: 'A', url: 'https://a' });
|
||||
addProduct(db, { name: 'B', url: 'https://b' });
|
||||
const products = listProducts(db);
|
||||
assert.equal(products.length, 2);
|
||||
assert.equal(products[0].name, 'A');
|
||||
assert.equal(products[1].name, 'B');
|
||||
});
|
||||
|
||||
test('getProduct returns undefined for an unknown id', () => {
|
||||
const db = freshDb();
|
||||
assert.equal(getProduct(db, 999), undefined);
|
||||
});
|
||||
|
||||
test('deleteProduct removes the row', () => {
|
||||
const db = freshDb();
|
||||
const product = addProduct(db, { name: 'A', url: 'https://a' });
|
||||
deleteProduct(db, product.id);
|
||||
assert.equal(getProduct(db, product.id), undefined);
|
||||
});
|
||||
|
||||
test('recordCheckSuccess updates price, timestamp and status', () => {
|
||||
const db = freshDb();
|
||||
const product = addProduct(db, { name: 'A', url: 'https://a' });
|
||||
recordCheckSuccess(db, product.id, { price: 12.5, checkedAt: '2026-09-05T08:00:00.000Z' });
|
||||
const updated = getProduct(db, product.id);
|
||||
assert.equal(updated.last_price, 12.5);
|
||||
assert.equal(updated.last_checked_at, '2026-09-05T08:00:00.000Z');
|
||||
assert.equal(updated.last_check_status, 'ok');
|
||||
});
|
||||
|
||||
test('recordCheckFailure only updates the status, leaving price and timestamp untouched', () => {
|
||||
const db = freshDb();
|
||||
const product = addProduct(db, { name: 'A', url: 'https://a' });
|
||||
recordCheckSuccess(db, product.id, { price: 12.5, checkedAt: '2026-09-05T08:00:00.000Z' });
|
||||
recordCheckFailure(db, product.id, 'fetch_error');
|
||||
const updated = getProduct(db, product.id);
|
||||
assert.equal(updated.last_price, 12.5);
|
||||
assert.equal(updated.last_checked_at, '2026-09-05T08:00:00.000Z');
|
||||
assert.equal(updated.last_check_status, 'fetch_error');
|
||||
});
|
||||
Reference in New Issue
Block a user