Compare commits

...

3 Commits

Author SHA1 Message Date
seb
70e3ae7a87 u 2026-10-16 02:31:54 +02:00
seb
fcbcb243e3 u 2026-10-16 01:59:11 +02:00
seb
0d726c76f8 u 2026-10-15 22:49:14 +02:00
9 changed files with 47 additions and 8 deletions

View File

@@ -666,6 +666,7 @@ export default class LibDashStore {
this.purge = storePurge(provider); this.purge = storePurge(provider);
this.initialize = storeInitialize(provider); this.initialize = storeInitialize(provider);
this.transaction = (fn) => provider.transaction(fn);
return this.initialize().then(() => this); return this.initialize().then(() => this);
} }

View File

@@ -6,9 +6,12 @@
*/ */
import Database from 'better-sqlite3'; import Database from 'better-sqlite3';
import { AsyncLocalStorage } from 'node:async_hooks';
import { logSlowQuery } from '../provider.js'; import { logSlowQuery } from '../provider.js';
import { SCHEMA_SCRIPTS } from './schema.js'; import { SCHEMA_SCRIPTS } from './schema.js';
const asyncTxStorage = new AsyncLocalStorage();
function normalizeParamValue(value) { function normalizeParamValue(value) {
if (value === undefined || value === null) return null; if (value === undefined || value === null) return null;
if (value instanceof Date) { if (value instanceof Date) {
@@ -324,6 +327,7 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
this.pool = this; this.pool = this;
db.pragma('foreign_keys = ON'); db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL'); db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('busy_timeout = 5000'); db.pragma('busy_timeout = 5000');
} }
@@ -366,8 +370,9 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
return runner; return runner;
}; };
if (this.#transactionDepth > 0 || this.db.inTransaction) { const activeContext = asyncTxStorage.getStore();
const savepoint = `sp_${++this.#transactionDepth}`; if (activeContext) {
const savepoint = `sp_${++activeContext.depth}`;
this.db.exec(`SAVEPOINT ${savepoint}`); this.db.exec(`SAVEPOINT ${savepoint}`);
const executor = new BetterSqlite3Executor(this.db, this.storageDir); const executor = new BetterSqlite3Executor(this.db, this.storageDir);
const runner = bindRunner(executor); const runner = bindRunner(executor);
@@ -376,8 +381,12 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
.then(() => fn(runner, executor)) .then(() => fn(runner, executor))
.then( .then(
(result) => { (result) => {
try {
this.db.exec(`RELEASE SAVEPOINT ${savepoint}`); this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
this.#transactionDepth--; } catch {
// ignore if outer transaction already rolled back
}
activeContext.depth--;
return result; return result;
}, },
(err) => { (err) => {
@@ -387,7 +396,7 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
} catch { } catch {
// ignore // ignore
} }
this.#transactionDepth--; activeContext.depth--;
throw err; throw err;
}, },
); );
@@ -399,7 +408,7 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
try { try {
const executor = new BetterSqlite3Executor(this.db, this.storageDir); const executor = new BetterSqlite3Executor(this.db, this.storageDir);
const runner = bindRunner(executor); const runner = bindRunner(executor);
const result = await fn(runner, executor); const result = await asyncTxStorage.run({ depth: 0 }, () => fn(runner, executor));
this.db.exec('COMMIT'); this.db.exec('COMMIT');
return result; return result;
} catch (err) { } catch (err) {

View File

@@ -69,6 +69,9 @@ export default function create(provider) {
return shape(row); return shape(row);
}; };
if (tx) {
return run(makeRunner(provider, tx), tx);
}
if (typeof provider?.transaction === 'function') { if (typeof provider?.transaction === 'function') {
return provider.transaction((runner, innerTx) => run(runner, innerTx)); return provider.transaction((runner, innerTx) => run(runner, innerTx));
} }

View File

@@ -34,6 +34,7 @@ export default function create(provider) {
* @param {object} input - The shipment input. * @param {object} input - The shipment input.
* @param {number} input.orderId - The parent order manifest id. * @param {number} input.orderId - The parent order manifest id.
* @param {string|Date} [input.date] - Optional shipment date (defaults to current date). * @param {string|Date} [input.date] - Optional shipment date (defaults to current date).
* @param {'new'|'delivered'|'returned'} [input.state='new'] - Optional delivery state (defaults to 'new').
* @param {string} [input.externalNumber] - Optional external delivery note number (defaults to order.externalNumber or order.number). * @param {string} [input.externalNumber] - Optional external delivery note number (defaults to order.externalNumber or order.number).
* @param {Array<{sku: string, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>} [input.lines] - The shipment positions (optional). * @param {Array<{sku: string, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>} [input.lines] - The shipment positions (optional).
* @param {object} [tx] - Optional transaction. * @param {object} [tx] - Optional transaction.
@@ -232,12 +233,16 @@ export default function create(provider) {
// 9. Synchronize order summary row and shipment summary row // 9. Synchronize order summary row and shipment summary row
await syncOrderSummary(runner, { companyId, manifestId: orderId }); await syncOrderSummary(runner, { companyId, manifestId: orderId });
const validState = input.state && ['new', 'delivered', 'returned'].includes(String(input.state).trim().toLowerCase())
? String(input.state).trim().toLowerCase()
: 'new';
await syncShipmentSummary(runner, { await syncShipmentSummary(runner, {
companyId, companyId,
manifestId, manifestId,
orderId, orderId,
orderNumber: order.number, orderNumber: order.number,
state: 'new', state: validState,
}); });
return loadShipment(runner, q, companyId, manifestId); return loadShipment(runner, q, companyId, manifestId);

View File

@@ -39,9 +39,10 @@ export async function loadShipment(runner, q, companyId, id) {
const row = headerRes.rows[0]; const row = headerRes.rows[0];
if (!row) return null; if (!row) return null;
const [linesRes, shippingsRes] = await Promise.all([ const [linesRes, shippingsRes, summaryRes] = await Promise.all([
runner.query(q.GET_MANIFEST_LINES, { manifestId: id }), runner.query(q.GET_MANIFEST_LINES, { manifestId: id }),
runner.query(q.SHIPPING_LIST_BY_MANIFEST, { manifestId: id }), runner.query(q.SHIPPING_LIST_BY_MANIFEST, { manifestId: id }),
runner.query('SELECT state FROM shipmentsummary WHERE manifest_id = @manifestId', { manifestId: id }).catch(() => ({ rows: [] })),
]); ]);
const lines = linesRes.rows.map(shapeLine); const lines = linesRes.rows.map(shapeLine);
@@ -57,6 +58,7 @@ export async function loadShipment(runner, q, companyId, id) {
createdAt: row.createdAt ?? null, createdAt: row.createdAt ?? null,
lineCount: lines.length, lineCount: lines.length,
packageCount: shippings.length, packageCount: shippings.length,
state: summaryRes.rows[0]?.state || 'new',
lines, lines,
shippings, shippings,
}; };

View File

@@ -214,6 +214,14 @@ export async function syncShipmentSummary(runner, { companyId, manifestId, order
const existingState = existingRes.rows[0]?.state; const existingState = existingRes.rows[0]?.state;
if (existingState && VALID_STATES.has(existingState)) { if (existingState && VALID_STATES.has(existingState)) {
resolvedState = existingState; resolvedState = existingState;
} else {
const hasShippedPackage = await runner.query(
"SELECT 1 FROM shipping WHERE manifest_id = @manifestId AND LOWER(state) = 'shipped'",
{ manifestId },
);
if (hasShippedPackage.rows[0]) {
resolvedState = 'delivered';
}
} }
} }

View File

@@ -3,6 +3,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
import nextNumber from '../numbers/nextNumber.js'; import nextNumber from '../numbers/nextNumber.js';
import ledgerAdd from '../ledger/add.js'; import ledgerAdd from '../ledger/add.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
import { syncShipmentSummary } from '../shipment/syncSummary.js';
function shape(row) { function shape(row) {
return { return {
@@ -176,6 +177,8 @@ export default function create(provider) {
} }
} }
await syncShipmentSummary(runner, { companyId, manifestId });
return shape(row); return shape(row);
}; };

View File

@@ -1,6 +1,7 @@
import { queriesFor, requireId } from '../logistics/helpers.js'; import { queriesFor, requireId } from '../logistics/helpers.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
import { syncShipmentSummary } from '../shipment/syncSummary.js';
/** /**
* Deletes a shipping of the user's selected company. Its packages are removed by cascade. * Deletes a shipping of the user's selected company. Its packages are removed by cascade.
@@ -37,5 +38,9 @@ export default function del(provider) {
} }
await runner.query(q.SHIPPING_DELETE, { id: shippingId, companyId }); await runner.query(q.SHIPPING_DELETE, { id: shippingId, companyId });
await syncShipmentSummary(runner, {
companyId,
manifestId: shipping.manifestId ?? shipping.manifest_id,
});
}; };
} }

View File

@@ -2,6 +2,7 @@ import { queriesFor, requireId, requireText } from '../logistics/helpers.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import ledgerAdd from '../ledger/add.js'; import ledgerAdd from '../ledger/add.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
import { syncShipmentSummary } from '../shipment/syncSummary.js';
function shape(row) { function shape(row) {
return { return {
@@ -196,6 +197,8 @@ export default function update(provider) {
state: nextState, state: nextState,
}; };
await syncShipmentSummary(runner, { companyId, manifestId: current.manifestId });
return shape(updatedRow); return shape(updatedRow);
}; };