screenshots for admin MFA PR (#738)
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* documentSequences — atomic gap-free sequence generator for CRM
|
||||
* document numbers (invoices, quotes, contracts, future doc kinds).
|
||||
*
|
||||
* **Contract**: `claimNextSequence(kind, year, [trx])` returns the
|
||||
* next integer in the (kind, year) series. Atomic against concurrent
|
||||
* callers: two simultaneous claims for the same row return strictly
|
||||
* increasing values, no collisions, no gaps.
|
||||
*
|
||||
* **How the atomicity works**
|
||||
*
|
||||
* Postgres: a single `UPDATE … SET current_value = current_value + 1
|
||||
* WHERE kind = ? AND year = ? RETURNING current_value` holds a row
|
||||
* lock for the duration of the statement; concurrent callers
|
||||
* serialize on the lock.
|
||||
*
|
||||
* SQLite: knex does not expose `BEGIN IMMEDIATE` declaratively, but
|
||||
* SQLite's default journal mode (or WAL) gives us per-row serialization
|
||||
* via the transaction. We wrap the UPDATE + re-SELECT in a transaction
|
||||
* which acquires the write lock; concurrent transactions queue.
|
||||
*
|
||||
* **First-claim path** (no row yet for the (kind, year))
|
||||
*
|
||||
* Migration 132 seeded rows for every existing year via MAX(...)
|
||||
* backfill. New years need an INSERT on first use. We do an
|
||||
* INSERT-OR-IGNORE then UPDATE … RETURNING. Both steps are inside
|
||||
* the same transaction so the year row is guaranteed to exist when
|
||||
* the UPDATE fires.
|
||||
*/
|
||||
|
||||
const { db } = require('../database/db');
|
||||
const { AppError } = require('./errors');
|
||||
|
||||
/**
|
||||
* Claim the next sequence value for (kind, year). Returns the new
|
||||
* integer. Throws AppError on DB failure; caller composes the
|
||||
* formatted document number from this integer via formatNumberInTemplate.
|
||||
*
|
||||
* @param {string} kind 'invoice' | 'quote' | 'contract' | ...
|
||||
* @param {number} year 4-digit year
|
||||
* @param {object} [trx] optional knex transaction. When supplied the
|
||||
* claim joins the caller's transaction so the
|
||||
* sequence increment and the row INSERT can
|
||||
* commit-or-roll-back together. Otherwise we
|
||||
* run our own micro-transaction.
|
||||
*/
|
||||
async function claimNextSequence(kind, year, trx) {
|
||||
if (!kind || typeof kind !== 'string') {
|
||||
throw new AppError('claimNextSequence: kind required', 500);
|
||||
}
|
||||
const yr = parseInt(year, 10);
|
||||
if (!Number.isFinite(yr)) {
|
||||
throw new AppError('claimNextSequence: invalid year', 500);
|
||||
}
|
||||
|
||||
const exec = async (q) => {
|
||||
// Step 1: ensure the (kind, year) row exists. INSERT...ON CONFLICT
|
||||
// DO NOTHING is the Postgres-native form; SQLite supports the same
|
||||
// syntax (3.24+). knex's `onConflict('...').ignore()` paves over
|
||||
// the differences.
|
||||
await q('document_sequences')
|
||||
.insert({
|
||||
kind, year: yr, current_value: 0,
|
||||
created_at: new Date(), updated_at: new Date(),
|
||||
})
|
||||
.onConflict(['kind', 'year']).ignore();
|
||||
|
||||
// Step 2: atomic claim. We do UPDATE … (no RETURNING because
|
||||
// knex's returning() is uneven across drivers) then re-SELECT.
|
||||
// The transaction wrapper (or the caller's trx) keeps the two
|
||||
// statements on the same row lock, so concurrent claimers
|
||||
// serialize.
|
||||
await q('document_sequences')
|
||||
.where({ kind, year: yr })
|
||||
.increment('current_value', 1)
|
||||
.update({ updated_at: new Date() });
|
||||
const row = await q('document_sequences')
|
||||
.where({ kind, year: yr })
|
||||
.select('current_value')
|
||||
.first();
|
||||
if (!row) {
|
||||
throw new AppError(`claimNextSequence: row vanished for ${kind}/${yr}`, 500);
|
||||
}
|
||||
return row.current_value;
|
||||
};
|
||||
|
||||
if (trx) {
|
||||
return await exec(trx);
|
||||
}
|
||||
return await db.transaction(async (innerTrx) => exec(innerTrx));
|
||||
}
|
||||
|
||||
module.exports = { claimNextSequence };
|
||||
Reference in New Issue
Block a user