u
This commit is contained in:
26
API.healthcheck.md
Normal file
26
API.healthcheck.md
Normal 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
2
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>`
|
- `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}>>`
|
- `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}>`
|
- `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)
|
- [account](API.account.md)
|
||||||
- `create(googleAuthId: number, input: {type: string, name: string}, tx?: object): Promise<{id: number, companyId: number, type: string, name: string}>`
|
- `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>`
|
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const MODULE_ORDER = [
|
|||||||
'attributevalue',
|
'attributevalue',
|
||||||
'articleattributevalue',
|
'articleattributevalue',
|
||||||
'labeltemplate',
|
'labeltemplate',
|
||||||
|
'healthcheck',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Files that are internal helpers and should not appear as public API entries.
|
// Files that are internal helpers and should not appear as public API entries.
|
||||||
|
|||||||
45
scripts/test-healthcheck.js
Normal file
45
scripts/test-healthcheck.js
Normal 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);
|
||||||
|
});
|
||||||
@@ -269,6 +269,7 @@ import mcpGetRefreshToken from './store/mcp/getRefreshToken.js';
|
|||||||
import mcpRevokeRefreshToken from './store/mcp/revokeRefreshToken.js';
|
import mcpRevokeRefreshToken from './store/mcp/revokeRefreshToken.js';
|
||||||
import mcpListAuthorizations from './store/mcp/listAuthorizations.js';
|
import mcpListAuthorizations from './store/mcp/listAuthorizations.js';
|
||||||
import mcpRevokeAuthorizations from './store/mcp/revokeAuthorizations.js';
|
import mcpRevokeAuthorizations from './store/mcp/revokeAuthorizations.js';
|
||||||
|
import healthcheckCheck from './store/healthcheck/check.js';
|
||||||
|
|
||||||
export default class LibDashStore {
|
export default class LibDashStore {
|
||||||
/**
|
/**
|
||||||
@@ -706,6 +707,10 @@ export default class LibDashStore {
|
|||||||
revokeAuthorizations: mcpRevokeAuthorizations(provider),
|
revokeAuthorizations: mcpRevokeAuthorizations(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.healthcheck = {
|
||||||
|
check: healthcheckCheck(provider),
|
||||||
|
};
|
||||||
|
|
||||||
this.purge = storePurge(provider);
|
this.purge = storePurge(provider);
|
||||||
this.initialize = storeInitialize(provider);
|
this.initialize = storeInitialize(provider);
|
||||||
this.transaction = (fn) => provider.transaction(fn);
|
this.transaction = (fn) => provider.transaction(fn);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** Drop order = reverse of schema create dependency order. */
|
/** Drop order = reverse of schema create dependency order. */
|
||||||
export const TABLES = [
|
export const TABLES = [
|
||||||
|
'healthcheck',
|
||||||
'articlewarehousesummary',
|
'articlewarehousesummary',
|
||||||
'deliverysummary',
|
'deliverysummary',
|
||||||
'shipmentsummary',
|
'shipmentsummary',
|
||||||
|
|||||||
19
src/providers/mssql/queries/healthcheck.js
Normal file
19
src/providers/mssql/queries/healthcheck.js
Normal 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;
|
||||||
|
`;
|
||||||
@@ -1437,8 +1437,20 @@ BEGIN
|
|||||||
END
|
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. */
|
/** DDL scripts in dependency order for schema init. */
|
||||||
export const SCHEMA_SCRIPTS = [
|
export const SCHEMA_SCRIPTS = [
|
||||||
|
INIT_HEALTHCHECK,
|
||||||
INIT_META,
|
INIT_META,
|
||||||
INIT_LANGUAGE,
|
INIT_LANGUAGE,
|
||||||
INIT_TAX,
|
INIT_TAX,
|
||||||
|
|||||||
24
src/providers/sqlite3/queries/healthcheck.js
Normal file
24
src/providers/sqlite3/queries/healthcheck.js
Normal 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;
|
||||||
|
`;
|
||||||
@@ -1058,8 +1058,17 @@ CREATE TABLE IF NOT EXISTS subagentschedule (
|
|||||||
CREATE INDEX IF NOT EXISTS IX_subagentschedule_company ON subagentschedule (company_id);
|
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. */
|
/** DDL scripts in dependency order for SQLite schema initialization. */
|
||||||
export const SCHEMA_SCRIPTS = [
|
export const SCHEMA_SCRIPTS = [
|
||||||
|
INIT_HEALTHCHECK,
|
||||||
INIT_META,
|
INIT_META,
|
||||||
INIT_LANGUAGE,
|
INIT_LANGUAGE,
|
||||||
INIT_TAX,
|
INIT_TAX,
|
||||||
|
|||||||
57
src/store/healthcheck/check.js
Normal file
57
src/store/healthcheck/check.js
Normal 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));
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user