This commit is contained in:
seb
2026-10-18 20:48:47 +02:00
parent 8cc7d0eb0b
commit 9ed7e4ee3b
11 changed files with 201 additions and 1 deletions

26
API.healthcheck.md Normal file
View File

@@ -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 }
```

2
API.md
View File

@@ -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<Array<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>>`
- `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<void>`

View File

@@ -44,6 +44,7 @@ const MODULE_ORDER = [
'attributevalue',
'articleattributevalue',
'labeltemplate',
'healthcheck',
];
// Files that are internal helpers and should not appear as public API entries.

View File

@@ -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);
});

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
/** Drop order = reverse of schema create dependency order. */
export const TABLES = [
'healthcheck',
'articlewarehousesummary',
'deliverysummary',
'shipmentsummary',

View File

@@ -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;
`;

View File

@@ -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,

View File

@@ -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;
`;

View File

@@ -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,

View File

@@ -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));
};
}