From 9ed7e4ee3b01463e2a4ff394951cd44be032a826 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 18 Oct 2026 20:48:47 +0200 Subject: [PATCH] u --- API.healthcheck.md | 26 +++++++++ API.md | 2 + generate-api-doc.js | 1 + scripts/test-healthcheck.js | 45 ++++++++++++++++ src/index.js | 5 ++ src/providers/mssql/purgeSql.js | 2 +- src/providers/mssql/queries/healthcheck.js | 19 +++++++ src/providers/mssql/schema.js | 12 +++++ src/providers/sqlite3/queries/healthcheck.js | 24 +++++++++ src/providers/sqlite3/schema.js | 9 ++++ src/store/healthcheck/check.js | 57 ++++++++++++++++++++ 11 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 API.healthcheck.md create mode 100644 scripts/test-healthcheck.js create mode 100644 src/providers/mssql/queries/healthcheck.js create mode 100644 src/providers/sqlite3/queries/healthcheck.js create mode 100644 src/store/healthcheck/check.js diff --git a/API.healthcheck.md b/API.healthcheck.md new file mode 100644 index 0000000..2bb869f --- /dev/null +++ b/API.healthcheck.md @@ -0,0 +1,26 @@ +# healthcheck + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `healthcheck.check` + +*Source: `src/store/healthcheck/check.js`* + +Executes a database health check: 1. Ensures the healthcheck table has one row (creates with sequence 0 if not). 2. Selects that row for update with minimal, reasonable locking within a transaction. 3. Updates the sequence by 1 in a separate statement within the transaction. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{ok: boolean, sequence: number}>` — The health check result. + +**Example** + +```js +const result = await store.healthcheck.check(); +// => { ok: true, sequence: 1 } +``` diff --git a/API.md b/API.md index 098115b..3607991 100644 --- a/API.md +++ b/API.md @@ -330,6 +330,8 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>` - `list(googleAuthId: number, tx?: object): Promise>` - `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>` +- [healthcheck](API.healthcheck.md) + - `check(tx?: object): Promise<{ok: boolean, sequence: number}>` - [account](API.account.md) - `create(googleAuthId: number, input: {type: string, name: string}, tx?: object): Promise<{id: number, companyId: number, type: string, name: string}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` diff --git a/generate-api-doc.js b/generate-api-doc.js index 727a362..2d86d38 100644 --- a/generate-api-doc.js +++ b/generate-api-doc.js @@ -44,6 +44,7 @@ const MODULE_ORDER = [ 'attributevalue', 'articleattributevalue', 'labeltemplate', + 'healthcheck', ]; // Files that are internal helpers and should not appear as public API entries. diff --git a/scripts/test-healthcheck.js b/scripts/test-healthcheck.js new file mode 100644 index 0000000..a1b6659 --- /dev/null +++ b/scripts/test-healthcheck.js @@ -0,0 +1,45 @@ +import assert from 'node:assert'; +import LibDashStore from '../src/index.js'; +import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; + +async function main() { + console.log('Testing healthcheck store method on in-memory SQLite...'); + const provider = new BetterSqlite3Provider({ path: ':memory:' }); + await provider.initialize(); + const store = await new LibDashStore(provider); + + assert(store.healthcheck, 'store.healthcheck should exist'); + assert.strictEqual(typeof store.healthcheck.check, 'function', 'store.healthcheck.check should be a function'); + + // First call: table is empty, should ensure row (0), select for update, increment to 1 and update + const res1 = await store.healthcheck.check(); + console.log('Healthcheck call 1 result:', res1); + assert.strictEqual(res1.ok, true, 'res1.ok should be true'); + assert.strictEqual(res1.sequence, 1, 'res1.sequence should be 1'); + + // Second call: row exists with 1, should increment to 2 and update + const res2 = await store.healthcheck.check(); + console.log('Healthcheck call 2 result:', res2); + assert.strictEqual(res2.ok, true, 'res2.ok should be true'); + assert.strictEqual(res2.sequence, 2, 'res2.sequence should be 2'); + + // Third call: row exists with 2, should increment to 3 and update + const res3 = await store.healthcheck.check(); + console.log('Healthcheck call 3 result:', res3); + assert.strictEqual(res3.ok, true, 'res3.ok should be true'); + assert.strictEqual(res3.sequence, 3, 'res3.sequence should be 3'); + + // Verify DB state directly + const rows = await provider.query('SELECT * FROM healthcheck'); + console.log('Healthcheck DB rows:', rows.rows); + assert.strictEqual(rows.rows.length, 1, 'healthcheck should have exactly 1 row'); + assert.strictEqual(Number(rows.rows[0].sequence), 3, 'sequence in DB should be 3'); + + await provider.close(); + console.log('✓ All healthcheck store tests passed successfully!'); +} + +main().catch((err) => { + console.error('Test failed:', err); + process.exit(1); +}); diff --git a/src/index.js b/src/index.js index f411429..7f772a9 100644 --- a/src/index.js +++ b/src/index.js @@ -269,6 +269,7 @@ import mcpGetRefreshToken from './store/mcp/getRefreshToken.js'; import mcpRevokeRefreshToken from './store/mcp/revokeRefreshToken.js'; import mcpListAuthorizations from './store/mcp/listAuthorizations.js'; import mcpRevokeAuthorizations from './store/mcp/revokeAuthorizations.js'; +import healthcheckCheck from './store/healthcheck/check.js'; export default class LibDashStore { /** @@ -706,6 +707,10 @@ export default class LibDashStore { revokeAuthorizations: mcpRevokeAuthorizations(provider), }; + this.healthcheck = { + check: healthcheckCheck(provider), + }; + this.purge = storePurge(provider); this.initialize = storeInitialize(provider); this.transaction = (fn) => provider.transaction(fn); diff --git a/src/providers/mssql/purgeSql.js b/src/providers/mssql/purgeSql.js index 9b10cf6..03f5cfb 100644 --- a/src/providers/mssql/purgeSql.js +++ b/src/providers/mssql/purgeSql.js @@ -1,6 +1,6 @@ /** Drop order = reverse of schema create dependency order. */ export const TABLES = [ - + 'healthcheck', 'articlewarehousesummary', 'deliverysummary', 'shipmentsummary', diff --git a/src/providers/mssql/queries/healthcheck.js b/src/providers/mssql/queries/healthcheck.js new file mode 100644 index 0000000..9b8d975 --- /dev/null +++ b/src/providers/mssql/queries/healthcheck.js @@ -0,0 +1,19 @@ +export const ENSURE_ROW = ` +IF NOT EXISTS (SELECT 1 FROM healthcheck) +BEGIN + INSERT INTO healthcheck ([sequence]) VALUES (0); +END +`; + +export const SELECT_FOR_UPDATE = ` +SELECT TOP 1 id, [sequence] +FROM healthcheck WITH (UPDLOCK, ROWLOCK) +ORDER BY id ASC; +`; + +export const UPDATE_SEQUENCE = ` +UPDATE healthcheck +SET [sequence] = @sequence, + updated_at = SYSUTCDATETIME() +WHERE id = @id; +`; diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index 93f46cf..8c2d47e 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -1437,8 +1437,20 @@ BEGIN END `; +export const INIT_HEALTHCHECK = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'healthcheck') +BEGIN + CREATE TABLE healthcheck ( + id INT IDENTITY(1,1) PRIMARY KEY, + [sequence] BIGINT NOT NULL CONSTRAINT DF_healthcheck_sequence DEFAULT 0, + updated_at DATETIME2 NOT NULL CONSTRAINT DF_healthcheck_updated_at DEFAULT SYSUTCDATETIME() + ); +END +`; + /** DDL scripts in dependency order for schema init. */ export const SCHEMA_SCRIPTS = [ + INIT_HEALTHCHECK, INIT_META, INIT_LANGUAGE, INIT_TAX, diff --git a/src/providers/sqlite3/queries/healthcheck.js b/src/providers/sqlite3/queries/healthcheck.js new file mode 100644 index 0000000..017a586 --- /dev/null +++ b/src/providers/sqlite3/queries/healthcheck.js @@ -0,0 +1,24 @@ +import * as source from '../../mssql/queries/healthcheck.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'healthcheck'); + +export const ENSURE_ROW = ` +INSERT INTO healthcheck (id, "sequence") +SELECT 1, 0 +WHERE NOT EXISTS (SELECT 1 FROM healthcheck); +`; + +export const SELECT_FOR_UPDATE = ` +SELECT id, "sequence" +FROM healthcheck +ORDER BY id ASC +LIMIT 1; +`; + +export const UPDATE_SEQUENCE = ` +UPDATE healthcheck +SET "sequence" = @sequence, + updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') +WHERE id = @id; +`; diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index ea01691..3ac8755 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -1058,8 +1058,17 @@ CREATE TABLE IF NOT EXISTS subagentschedule ( CREATE INDEX IF NOT EXISTS IX_subagentschedule_company ON subagentschedule (company_id); `; +export const INIT_HEALTHCHECK = ` +CREATE TABLE IF NOT EXISTS healthcheck ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + "sequence" INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}) +); +`; + /** DDL scripts in dependency order for SQLite schema initialization. */ export const SCHEMA_SCRIPTS = [ + INIT_HEALTHCHECK, INIT_META, INIT_LANGUAGE, INIT_TAX, diff --git a/src/store/healthcheck/check.js b/src/store/healthcheck/check.js new file mode 100644 index 0000000..487f2a2 --- /dev/null +++ b/src/store/healthcheck/check.js @@ -0,0 +1,57 @@ +import * as mssqlQueries from '../../providers/mssql/queries/healthcheck.js'; +import * as sqliteQueries from '../../providers/sqlite3/queries/healthcheck.js'; +import makeRunner from '../runner.js'; + +export default function check(provider) { + const isSqlite = provider?.dialect === 'sqlite3'; + const queries = isSqlite ? sqliteQueries : mssqlQueries; + + /** + * Executes a database health check: + * 1. Ensures the healthcheck table has one row (creates with sequence 0 if not). + * 2. Selects that row for update with minimal, reasonable locking within a transaction. + * 3. Updates the sequence by 1 in a separate statement within the transaction. + * + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{ok: boolean, sequence: number}>} The health check result. + * + * @example + * const result = await store.healthcheck.check(); + * // => { ok: true, sequence: 1 } + */ + return async (tx) => { + const runTx = async (runner) => { + // 1. Ensure table has one row, if not create that row with number 0 + await runner.query(queries.ENSURE_ROW); + + // 2. Select for update on that row with minimal, reasonable locking + const selected = await runner.query(queries.SELECT_FOR_UPDATE); + const row = selected.rows[0]; + if (!row) { + throw new Error('Healthcheck row not found'); + } + + // 3. Updating the number by 1 and do an update (not in one statement, but in a tx) + const currentSeq = Number(row.sequence ?? 0); + const nextSeq = currentSeq + 1; + + await runner.query(queries.UPDATE_SEQUENCE, { + id: row.id, + sequence: nextSeq, + }); + + return { + ok: true, + sequence: nextSeq, + }; + }; + + if (tx) { + return runTx(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction((runner) => runTx(makeRunner(provider, runner))); + } + return runTx(makeRunner(provider)); + }; +}