openvibers/openvibe.sdk Commits
0.18.1. db.prepare: a statement with one parameter takes a single array argument as its value (WHERE id = ANY(?)); only statements with several parameters read one array as the list of values. Found converting OpenVibe.Wiki. openvibe-sdk/testing gains createTestDb({ migrations, store, service }): a migrated PGlite database, or on the containers roles and a schema of its own, shaped as the host's add-service.sh makes them. Setup and teardown take an advisory lock, so parallel runs never race on the catalog ("tuple concurrently updated", found in Wiki's tests). Also createTestValkey. Services drop their copied helpers. 40/40 test files with the containers.
Changes
@@@ -6,6 +6,17 @@ release may change an API and says so here.++++## 0.18.1 (2026-09-28)++++- **`db.prepare`:** a statement with one parameter takes a single array argument as that parameter's value (`WHERE id = ANY(?)`). Only statements with several parameters read a single array as the list of values (better-sqlite3's array form). Found converting OpenVibe.Wiki.++- **`openvibe-sdk/testing`:**++ - `createTestDb({ migrations, store, service })` gives a migrated database for one test run. It is PGlite by default. With `store: 'pg'` (or `OV_TEST_STORE=pg`) it runs on the containers, with roles and a schema of its own shaped as the host's `add-service.sh` makes them.++ - Setup and teardown take an advisory lock, so parallel runs never race on the catalog ("tuple concurrently updated").++ - `open()` gives a second pooled handle, and `close()` ends the roles' backends and drops them.++ - `createTestValkey({ prefix })` gives the containers' Valkey under a prefix of its own.++ - Every service's copied test helper is replaced by these.++## 0.18.0 (2026-09-28)This release makes the move from better-sqlite3 mostly mechanical: call sites keep their shape and gain `await`.@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.18.0",++ "version": "0.18.1","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag",@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.18.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.18.1, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces// (auth, registry, modules, realtime, media, community, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */@@@ -41,7 +41,7 @@ const { paginate, offsetPager } = __load(7);const semver = __load(6);/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.18.0';++const SDK_VERSION = '0.18.1';module.exports = {createClient,@@@ -2,6 +2,7 @@import cjs from '../src/testing/index.js';export const {-- createMockPlatform, DEFAULT_ORIGINS, DEFAULT_TOOLS_SATELLITES, DEFAULT_APP_CATALOG,++ createMockPlatform, DEFAULT_ORIGINS, DEFAULT_TOOLS_SATELLITES, DEFAULT_APP_CATALOG, createTestDb, createTestValkey,++ pgAvailable, valkeyAvailable,} = cjs;export default cjs;@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.18.0",++ "version": "0.18.1","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.18.0",++ "version": "0.18.1","license": "MIT","devDependencies": {"@electric-sql/pglite": "^0.5.8",@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.18.0",++ "version": "0.18.1","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');const semver = require('./semver');/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.18.0';++const SDK_VERSION = '0.18.1';module.exports = {createClient,@@@ -3,7 +3,7 @@* db.prepare(text): async statements shaped like better-sqlite3's, so a service moving from SQLite changes its* call sites by adding `await` (the SQL itself still has to be PostgreSQL). Parameters are written as SQLite code* wrote them and compiled once to PostgreSQL's $n:-- * ? positional, in order++ * ? positional, in order (a statement with one parameter takes an array as its value: ANY(?))* @name :name named, bound from one plain object; a name used twice is one parameter* Question marks, @ and : inside '…' strings, "…" identifiers, dollar-quoted bodies and comments are left alone,* and so are :: casts.@@@ -71,7 +71,9 @@ function bind(stmt, args) {return o[k] === undefined ? null : o[k];});}-- const flat = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;++ // One parameter: the one argument is its value, an array included (col = ANY(?)). Several: a single array++ // argument is the list of values (better-sqlite3's array form), else the arguments are.++ const flat = stmt.count !== 1 && args.length === 1 && Array.isArray(args[0]) ? args[0] : args;if (flat.length !== stmt.count) throw new TypeError(`openvibe-sdk/db prepare: expected ${stmt.count} parameter(s), got ${flat.length}`);return flat.map((v) => (v === undefined ? null : v));}nnew file mode 100644@@@ -0,0 +1,88 @@++'use strict';++/**++ * A migrated database for one test run (ADR-035): what every service's test helper needs, in one place.++ *++ * const { createTestDb } = require('openvibe-sdk/testing');++ * const { db, close } = await createTestDb({ migrations: path.join(__dirname, '..', 'migrations') });++ *++ * store 'pglite' (default): real PostgreSQL in-process, migrated.++ * store 'pg' (OV_TEST_STORE=pg, or a service's own variable passed as `store`): the production-shaped containers++ * (scripts/test-services.sh up: OV_TEST_PG_URL through PgBouncer, OV_TEST_PG_DIRECT_URL). The run gets roles and++ * a schema of its own, shaped as OpenVibe.Host's roles/data/add-service.sh makes them: an owner that migrates on++ * the direct connection, and a runtime role (DML only, statement_timeout 15 s, lock_timeout 5 s) serving through++ * PgBouncer. Role setup and teardown take an advisory lock, so parallel runs never race on the catalog ("tuple++ * concurrently updated"). close() ends the roles' backends (PgBouncer keeps idle server connections) and drops them.++ * open(): another pooled handle on the same database (a second process).++ *++ * createTestValkey({ prefix }): the containers' Valkey (OV_TEST_VALKEY_URL) under a prefix no other run uses, or null.++ */++const crypto = require('crypto');++++const quiet = { log() {}, warn() {}, error: (...a) => console.error(...a) };++const SETUP_LOCK = 735_1_2026;++const pgAvailable = () => !!(process.env.OV_TEST_PG_URL && process.env.OV_TEST_PG_DIRECT_URL);++const valkeyAvailable = () => !!process.env.OV_TEST_VALKEY_URL;++++async function createTestDb({ migrations, store = process.env.OV_TEST_STORE || 'pglite', service = 'test', max = 4, log = quiet } = {}) {++ const { createDb } = require('../db');++ if (store !== 'pg') {++ const db = createDb({ pglite: true, service: `${service}-test`, log });++ if (migrations) await db.migrate({ dir: migrations, log });++ return { db, store: 'pglite', open: null, close: () => db.close().catch(() => {}) };++ }++ if (!pgAvailable()) throw new Error('store pg needs OV_TEST_PG_URL and OV_TEST_PG_DIRECT_URL (openvibe-sdk scripts/test-services.sh up)');++ const safe = String(service).replace(/[^a-z0-9]/g, '').slice(0, 12) || 'svc';++ const name = `${safe}_t${process.pid}_${crypto.randomBytes(4).toString('hex')}`;++ const owner = `${name}_owner`;++ const pw = crypto.randomBytes(16).toString('hex');++ const su = createDb({ url: process.env.OV_TEST_PG_DIRECT_URL, service: `${service}-test-admin`, max: 1, log });++ const database = await su.value('SELECT current_database()');++ await su.tx(async (t) => {++ await t.query('SELECT pg_advisory_xact_lock($1)', [SETUP_LOCK]);++ for (const stmt of [++ `CREATE ROLE ${owner} LOGIN PASSWORD '${pw}'`,++ `CREATE ROLE ${name} LOGIN PASSWORD '${pw}'`,++ `GRANT CONNECT ON DATABASE ${database} TO ${owner}, ${name}`,++ `CREATE SCHEMA ${name} AUTHORIZATION ${owner}`,++ `ALTER ROLE ${owner} SET search_path = ${name}`,++ `ALTER ROLE ${name} SET search_path = ${name}`,++ `ALTER ROLE ${name} SET statement_timeout = '15s'`,++ `ALTER ROLE ${name} SET lock_timeout = '5s'`,++ `GRANT USAGE ON SCHEMA ${name} TO ${name}`,++ `ALTER DEFAULT PRIVILEGES FOR ROLE ${owner} IN SCHEMA ${name} GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${name}`,++ `ALTER DEFAULT PRIVILEGES FOR ROLE ${owner} IN SCHEMA ${name} GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO ${name}`,++ ]) await t.query(stmt);++ });++ const as = (url, user) => { const u = new URL(url); u.username = user; u.password = pw; return u.toString(); };++ async function drop() {++ try {++ await su.query('SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = ANY($1)', [[name, owner]]);++ await su.tx(async (t) => {++ await t.query('SELECT pg_advisory_xact_lock($1)', [SETUP_LOCK]);++ await t.query(`DROP SCHEMA IF EXISTS ${name} CASCADE`);++ for (const r of [name, owner]) { await t.query(`DROP OWNED BY ${r}`); await t.query(`DROP ROLE ${r}`); }++ });++ } finally { await su.close(); }++ }++ let db;++ try {++ if (migrations) {++ const ownerDb = createDb({ url: as(process.env.OV_TEST_PG_DIRECT_URL, owner), service: `${service}-test-migrate`, max: 1, log });++ try { await ownerDb.migrate({ dir: migrations, log }); } finally { await ownerDb.close(); }++ }++ db = createDb({ url: as(process.env.OV_TEST_PG_URL, name), service: `${service}-test`, max, log });++ } catch (e) { await drop().catch(() => {}); throw e; } // a failed setup leaves nothing behind++ return {++ db, store: 'postgresql', schema: name,++ open: (o = {}) => createDb({ url: as(process.env.OV_TEST_PG_URL, name), service: `${service}-test`, max, log, ...o }),++ async close() { await db.close().catch(() => {}); await drop(); },++ };++}++++function createTestValkey({ prefix = 'test' } = {}) {++ if (!valkeyAvailable()) return null;++ const { createValkey } = require('../valkey');++ return createValkey({ url: process.env.OV_TEST_VALKEY_URL, prefix: `ov:${prefix}-test:${crypto.randomBytes(4).toString('hex')}:`, log: quiet });++}++++module.exports = { createTestDb, createTestValkey, pgAvailable, valkeyAvailable };@@@ -872,4 +872,11 @@ function createMockPlatform(opts = {}) {};}--module.exports = { createMockPlatform, DEFAULT_ORIGINS, DEFAULT_TOOLS_SATELLITES, DEFAULT_APP_CATALOG };++// Test databases (PGlite, or the PostgreSQL + PgBouncer containers) and Valkey: loaded on first use.++const lazyDb = (name) => (...args) => require('./db')[name](...args);++++module.exports = {++ createMockPlatform, DEFAULT_ORIGINS, DEFAULT_TOOLS_SATELLITES, DEFAULT_APP_CATALOG,++ createTestDb: lazyDb('createTestDb'), createTestValkey: lazyDb('createTestValkey'),++ pgAvailable: lazyDb('pgAvailable'), valkeyAvailable: lazyDb('valkeyAvailable'),++};@@@ -69,6 +69,7 @@ function cases(label, open) {assert.deepEqual((await q.all({ name: 'x', min: 0 })).map((r) => r.name), ['x']);assert.equal(await db.prepare('SELECT name FROM items WHERE qty = ?').get(99), undefined, 'no row is undefined, as in better-sqlite3');assert.equal((await db.prepare('UPDATE items SET qty = qty + 1 WHERE qty > ?').run(0)).changes, 2);++ assert.deepEqual(await db.prepare('SELECT name FROM items WHERE name = ANY(?) ORDER BY name').pluck().all(['x', 'y', 'zz']), ['x', 'y'], 'one parameter: an array is its value');await assert.rejects(q.all({ name: 'x' }), /missing parameter @min/);await assert.rejects(db.prepare('SELECT ? AS a').get(), /expected 1 parameter/);} finally { await done(); }nnew file mode 100644@@@ -0,0 +1,34 @@++'use strict';++/** openvibe-sdk/testing createTestDb: a migrated PGlite database; on the containers, parallel setups do not race. */++const assert = require('node:assert/strict');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { run } = require('./helpers');++const { createTestDb, pgAvailable } = require('../src/testing');++++const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-testdb-'));++fs.writeFileSync(path.join(dir, '0001_initial.sql'), '-- phase: expand\nCREATE TABLE notes (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, body text NOT NULL);\n');++++const tests = [++ ['pglite: migrated and usable', async () => {++ const t = await createTestDb({ migrations: dir, service: 'sdk' });++ try {++ assert.equal(t.store, 'pglite');++ await t.db.prepare('INSERT INTO notes (body) VALUES (?)').run('hi');++ assert.equal(await t.db.value('SELECT count(*)::int FROM notes'), 1);++ } finally { await t.close(); }++ }],++];++if (pgAvailable()) {++ tests.push(['postgresql+pgbouncer: four setups at once each get their own schema, and close() drops them', async () => {++ const all = await Promise.all([1, 2, 3, 4].map(() => createTestDb({ migrations: dir, store: 'pg', service: 'sdk' })));++ try {++ assert.equal(new Set(all.map((t) => t.schema)).size, 4);++ await Promise.all(all.map((t, i) => t.db.prepare('INSERT INTO notes (body) VALUES (?)').run(`n${i}`)));++ for (const t of all) assert.equal(await t.db.value('SELECT count(*)::int FROM notes'), 1, 'isolated');++ } finally { await Promise.all(all.map((t) => t.close())); }++ }]);++} else console.log('createTestDb on the containers: skipped (OV_TEST_PG_URL not set; scripts/test-services.sh up)');++++run(tests);@@@ -147,3 +147,19 @@ export declare const DEFAULT_ORIGINS: Record<Origin, string>;export declare const DEFAULT_TOOLS_SATELLITES: string[];/** Capability ids a developer app can be granted in openvibe-contracts v0.28.0 (public + active). */export declare const DEFAULT_APP_CATALOG: string[];++++import type { Db } from './db';++import type { Valkey } from './valkey';++export interface TestDb {++ db: Db;++ store: 'pglite' | 'postgresql';++ schema?: string;++ /** Another pooled handle on the same database (a second process); null on PGlite. */++ open: ((o?: object) => Db) | null;++ close(): Promise<void>;++}++/** A migrated database for one test run: PGlite, or (store 'pg') roles and a schema of its own on the containers. */++export declare function createTestDb(opts?: { migrations?: string; store?: 'pglite' | 'pg' | string; service?: string; max?: number; log?: object }): Promise<TestDb>;++export declare function createTestValkey(opts?: { prefix?: string }): Valkey | null;++export declare function pgAvailable(): boolean;++export declare function valkeyAvailable(): boolean;
@@ -6,6 +6,17 @@ release may change an API and says so here.
@@ -6,6 +6,17 @@ release may change an API and says so here.
+
+## 0.18.1 (2026-09-28)
+
+- **`db.prepare`:** a statement with one parameter takes a single array argument as that parameter's value (`WHERE id = ANY(?)`). Only statements with several parameters read a single array as the list of values (better-sqlite3's array form). Found converting OpenVibe.Wiki.
+- **`openvibe-sdk/testing`:**
+ - `createTestDb({ migrations, store, service })` gives a migrated database for one test run. It is PGlite by default. With `store: 'pg'` (or `OV_TEST_STORE=pg`) it runs on the containers, with roles and a schema of its own shaped as the host's `add-service.sh` makes them.
+ - Setup and teardown take an advisory lock, so parallel runs never race on the catalog ("tuple concurrently updated").
+ - `open()` gives a second pooled handle, and `close()` ends the roles' backends and drops them.
+ - `createTestValkey({ prefix })` gives the containers' Valkey under a prefix of its own.
+ - Every service's copied test helper is replaced by these.
+
## 0.18.0 (2026-09-28)
## 0.18.0 (2026-09-28)
This release makes the move from better-sqlite3 mostly mechanical: call sites keep their shape and gain `await`.
This release makes the move from better-sqlite3 mostly mechanical: call sites keep their shape and gain `await`.
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.18.0",
+ "version": "0.18.1",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.18.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.18.1, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, registry, modules, realtime, media, community, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
// (auth, registry, modules, realtime, media, community, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
@@ -41,7 +41,7 @@ const { paginate, offsetPager } = __load(7);
@@ -41,7 +41,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.18.0';
+const SDK_VERSION = '0.18.1';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -2,6 +2,7 @@
@@ -2,6 +2,7 @@
import cjs from '../src/testing/index.js';
import cjs from '../src/testing/index.js';
export const {
export const {
- createMockPlatform, DEFAULT_ORIGINS, DEFAULT_TOOLS_SATELLITES, DEFAULT_APP_CATALOG,
+ createMockPlatform, DEFAULT_ORIGINS, DEFAULT_TOOLS_SATELLITES, DEFAULT_APP_CATALOG, createTestDb, createTestValkey,
+ pgAvailable, valkeyAvailable,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.18.0",
+ "version": "0.18.1",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.18.0",
+ "version": "0.18.1",
"license": "MIT",
"license": "MIT",
"devDependencies": {
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.18.0",
+ "version": "0.18.1",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.18.0';
+const SDK_VERSION = '0.18.1';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
* db.prepare(text): async statements shaped like better-sqlite3's, so a service moving from SQLite changes its
* db.prepare(text): async statements shaped like better-sqlite3's, so a service moving from SQLite changes its
* call sites by adding `await` (the SQL itself still has to be PostgreSQL). Parameters are written as SQLite code
* call sites by adding `await` (the SQL itself still has to be PostgreSQL). Parameters are written as SQLite code
* wrote them and compiled once to PostgreSQL's $n:
* wrote them and compiled once to PostgreSQL's $n:
- * ? positional, in order
+ * ? positional, in order (a statement with one parameter takes an array as its value: ANY(?))
* @name :name named, bound from one plain object; a name used twice is one parameter
* @name :name named, bound from one plain object; a name used twice is one parameter
* Question marks, @ and : inside '…' strings, "…" identifiers, dollar-quoted bodies and comments are left alone,
* Question marks, @ and : inside '…' strings, "…" identifiers, dollar-quoted bodies and comments are left alone,
* and so are :: casts.
* and so are :: casts.
@@ -71,7 +71,9 @@ function bind(stmt, args) {
@@ -71,7 +71,9 @@ function bind(stmt, args) {
return o[k] === undefined ? null : o[k];
return o[k] === undefined ? null : o[k];
});
});
}
}
- const flat = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
+ // One parameter: the one argument is its value, an array included (col = ANY(?)). Several: a single array
+ // argument is the list of values (better-sqlite3's array form), else the arguments are.
+ const flat = stmt.count !== 1 && args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
if (flat.length !== stmt.count) throw new TypeError(`openvibe-sdk/db prepare: expected ${stmt.count} parameter(s), got ${flat.length}`);
if (flat.length !== stmt.count) throw new TypeError(`openvibe-sdk/db prepare: expected ${stmt.count} parameter(s), got ${flat.length}`);
return flat.map((v) => (v === undefined ? null : v));
return flat.map((v) => (v === undefined ? null : v));
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
+'use strict';
+/**
+ * A migrated database for one test run (ADR-035): what every service's test helper needs, in one place.
+ *
+ * const { createTestDb } = require('openvibe-sdk/testing');
+ * const { db, close } = await createTestDb({ migrations: path.join(__dirname, '..', 'migrations') });
+ *
+ * store 'pglite' (default): real PostgreSQL in-process, migrated.
+ * store 'pg' (OV_TEST_STORE=pg, or a service's own variable passed as `store`): the production-shaped containers
+ * (scripts/test-services.sh up: OV_TEST_PG_URL through PgBouncer, OV_TEST_PG_DIRECT_URL). The run gets roles and
+ * a schema of its own, shaped as OpenVibe.Host's roles/data/add-service.sh makes them: an owner that migrates on
+ * the direct connection, and a runtime role (DML only, statement_timeout 15 s, lock_timeout 5 s) serving through
+ * PgBouncer. Role setup and teardown take an advisory lock, so parallel runs never race on the catalog ("tuple
+ * concurrently updated"). close() ends the roles' backends (PgBouncer keeps idle server connections) and drops them.
+ * open(): another pooled handle on the same database (a second process).
+ *
+ * createTestValkey({ prefix }): the containers' Valkey (OV_TEST_VALKEY_URL) under a prefix no other run uses, or null.
+ */
+const crypto = require('crypto');
+
+const quiet = { log() {}, warn() {}, error: (...a) => console.error(...a) };
+const SETUP_LOCK = 735_1_2026;
+const pgAvailable = () => !!(process.env.OV_TEST_PG_URL && process.env.OV_TEST_PG_DIRECT_URL);
+const valkeyAvailable = () => !!process.env.OV_TEST_VALKEY_URL;
+
+async function createTestDb({ migrations, store = process.env.OV_TEST_STORE || 'pglite', service = 'test', max = 4, log = quiet } = {}) {
+ const { createDb } = require('../db');
+ if (store !== 'pg') {
+ const db = createDb({ pglite: true, service: `${service}-test`, log });
+ if (migrations) await db.migrate({ dir: migrations, log });
+ return { db, store: 'pglite', open: null, close: () => db.close().catch(() => {}) };
+ }
+ if (!pgAvailable()) throw new Error('store pg needs OV_TEST_PG_URL and OV_TEST_PG_DIRECT_URL (openvibe-sdk scripts/test-services.sh up)');
+ const safe = String(service).replace(/[^a-z0-9]/g, '').slice(0, 12) || 'svc';
+ const name = `${safe}_t${process.pid}_${crypto.randomBytes(4).toString('hex')}`;
+ const owner = `${name}_owner`;
+ const pw = crypto.randomBytes(16).toString('hex');
+ const su = createDb({ url: process.env.OV_TEST_PG_DIRECT_URL, service: `${service}-test-admin`, max: 1, log });
+ const database = await su.value('SELECT current_database()');
+ await su.tx(async (t) => {
+ await t.query('SELECT pg_advisory_xact_lock($1)', [SETUP_LOCK]);
+ for (const stmt of [
+ `CREATE ROLE ${owner} LOGIN PASSWORD '${pw}'`,
+ `CREATE ROLE ${name} LOGIN PASSWORD '${pw}'`,
+ `GRANT CONNECT ON DATABASE ${database} TO ${owner}, ${name}`,
+ `CREATE SCHEMA ${name} AUTHORIZATION ${owner}`,
+ `ALTER ROLE ${owner} SET search_path = ${name}`,
+ `ALTER ROLE ${name} SET search_path = ${name}`,
+ `ALTER ROLE ${name} SET statement_timeout = '15s'`,
+ `ALTER ROLE ${name} SET lock_timeout = '5s'`,
+ `GRANT USAGE ON SCHEMA ${name} TO ${name}`,
+ `ALTER DEFAULT PRIVILEGES FOR ROLE ${owner} IN SCHEMA ${name} GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${name}`,
+ `ALTER DEFAULT PRIVILEGES FOR ROLE ${owner} IN SCHEMA ${name} GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO ${name}`,
+ ]) await t.query(stmt);
+ });
+ const as = (url, user) => { const u = new URL(url); u.username = user; u.password = pw; return u.toString(); };
+ async function drop() {
+ try {
+ await su.query('SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = ANY($1)', [[name, owner]]);
+ await su.tx(async (t) => {
+ await t.query('SELECT pg_advisory_xact_lock($1)', [SETUP_LOCK]);
+ await t.query(`DROP SCHEMA IF EXISTS ${name} CASCADE`);
+ for (const r of [name, owner]) { await t.query(`DROP OWNED BY ${r}`); await t.query(`DROP ROLE ${r}`); }
+ });
+ } finally { await su.close(); }
+ }
+ let db;
+ try {
+ if (migrations) {
+ const ownerDb = createDb({ url: as(process.env.OV_TEST_PG_DIRECT_URL, owner), service: `${service}-test-migrate`, max: 1, log });
+ try { await ownerDb.migrate({ dir: migrations, log }); } finally { await ownerDb.close(); }
+ }
+ db = createDb({ url: as(process.env.OV_TEST_PG_URL, name), service: `${service}-test`, max, log });
+ } catch (e) { await drop().catch(() => {}); throw e; } // a failed setup leaves nothing behind
+ return {
+ db, store: 'postgresql', schema: name,
+ open: (o = {}) => createDb({ url: as(process.env.OV_TEST_PG_URL, name), service: `${service}-test`, max, log, ...o }),
+ async close() { await db.close().catch(() => {}); await drop(); },
+ };
+}
+
+function createTestValkey({ prefix = 'test' } = {}) {
+ if (!valkeyAvailable()) return null;
+ const { createValkey } = require('../valkey');
+ return createValkey({ url: process.env.OV_TEST_VALKEY_URL, prefix: `ov:${prefix}-test:${crypto.randomBytes(4).toString('hex')}:`, log: quiet });
+}
+
+module.exports = { createTestDb, createTestValkey, pgAvailable, valkeyAvailable };
@@ -872,4 +872,11 @@ function createMockPlatform(opts = {}) {
@@ -872,4 +872,11 @@ function createMockPlatform(opts = {}) {
};
};
}
}
-module.exports = { createMockPlatform, DEFAULT_ORIGINS, DEFAULT_TOOLS_SATELLITES, DEFAULT_APP_CATALOG };
+// Test databases (PGlite, or the PostgreSQL + PgBouncer containers) and Valkey: loaded on first use.
+const lazyDb = (name) => (...args) => require('./db')[name](...args);
+
+module.exports = {
+ createMockPlatform, DEFAULT_ORIGINS, DEFAULT_TOOLS_SATELLITES, DEFAULT_APP_CATALOG,
+ createTestDb: lazyDb('createTestDb'), createTestValkey: lazyDb('createTestValkey'),
+ pgAvailable: lazyDb('pgAvailable'), valkeyAvailable: lazyDb('valkeyAvailable'),
+};
@@ -69,6 +69,7 @@ function cases(label, open) {
@@ -69,6 +69,7 @@ function cases(label, open) {
assert.deepEqual((await q.all({ name: 'x', min: 0 })).map((r) => r.name), ['x']);
assert.deepEqual((await q.all({ name: 'x', min: 0 })).map((r) => r.name), ['x']);
assert.equal(await db.prepare('SELECT name FROM items WHERE qty = ?').get(99), undefined, 'no row is undefined, as in better-sqlite3');
assert.equal(await db.prepare('SELECT name FROM items WHERE qty = ?').get(99), undefined, 'no row is undefined, as in better-sqlite3');
assert.equal((await db.prepare('UPDATE items SET qty = qty + 1 WHERE qty > ?').run(0)).changes, 2);
assert.equal((await db.prepare('UPDATE items SET qty = qty + 1 WHERE qty > ?').run(0)).changes, 2);
+ assert.deepEqual(await db.prepare('SELECT name FROM items WHERE name = ANY(?) ORDER BY name').pluck().all(['x', 'y', 'zz']), ['x', 'y'], 'one parameter: an array is its value');
await assert.rejects(q.all({ name: 'x' }), /missing parameter @min/);
await assert.rejects(q.all({ name: 'x' }), /missing parameter @min/);
await assert.rejects(db.prepare('SELECT ? AS a').get(), /expected 1 parameter/);
await assert.rejects(db.prepare('SELECT ? AS a').get(), /expected 1 parameter/);
} finally { await done(); }
} finally { await done(); }
new file mode 100644
new file mode 100644
@@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
+'use strict';
+/** openvibe-sdk/testing createTestDb: a migrated PGlite database; on the containers, parallel setups do not race. */
+const assert = require('node:assert/strict');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { run } = require('./helpers');
+const { createTestDb, pgAvailable } = require('../src/testing');
+
+const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-testdb-'));
+fs.writeFileSync(path.join(dir, '0001_initial.sql'), '-- phase: expand\nCREATE TABLE notes (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, body text NOT NULL);\n');
+
+const tests = [
+ ['pglite: migrated and usable', async () => {
+ const t = await createTestDb({ migrations: dir, service: 'sdk' });
+ try {
+ assert.equal(t.store, 'pglite');
+ await t.db.prepare('INSERT INTO notes (body) VALUES (?)').run('hi');
+ assert.equal(await t.db.value('SELECT count(*)::int FROM notes'), 1);
+ } finally { await t.close(); }
+ }],
+];
+if (pgAvailable()) {
+ tests.push(['postgresql+pgbouncer: four setups at once each get their own schema, and close() drops them', async () => {
+ const all = await Promise.all([1, 2, 3, 4].map(() => createTestDb({ migrations: dir, store: 'pg', service: 'sdk' })));
+ try {
+ assert.equal(new Set(all.map((t) => t.schema)).size, 4);
+ await Promise.all(all.map((t, i) => t.db.prepare('INSERT INTO notes (body) VALUES (?)').run(`n${i}`)));
+ for (const t of all) assert.equal(await t.db.value('SELECT count(*)::int FROM notes'), 1, 'isolated');
+ } finally { await Promise.all(all.map((t) => t.close())); }
+ }]);
+} else console.log('createTestDb on the containers: skipped (OV_TEST_PG_URL not set; scripts/test-services.sh up)');
+
+run(tests);
@@ -147,3 +147,19 @@ export declare const DEFAULT_ORIGINS: Record<Origin, string>;
@@ -147,3 +147,19 @@ export declare const DEFAULT_ORIGINS: Record<Origin, string>;
export declare const DEFAULT_TOOLS_SATELLITES: string[];
export declare const DEFAULT_TOOLS_SATELLITES: string[];
/** Capability ids a developer app can be granted in openvibe-contracts v0.28.0 (public + active). */
/** Capability ids a developer app can be granted in openvibe-contracts v0.28.0 (public + active). */
export declare const DEFAULT_APP_CATALOG: string[];
export declare const DEFAULT_APP_CATALOG: string[];
+
+import type { Db } from './db';
+import type { Valkey } from './valkey';
+export interface TestDb {
+ db: Db;
+ store: 'pglite' | 'postgresql';
+ schema?: string;
+ /** Another pooled handle on the same database (a second process); null on PGlite. */
+ open: ((o?: object) => Db) | null;
+ close(): Promise<void>;
+}
+/** A migrated database for one test run: PGlite, or (store 'pg') roles and a schema of its own on the containers. */
+export declare function createTestDb(opts?: { migrations?: string; store?: 'pglite' | 'pg' | string; service?: string; max?: number; log?: object }): Promise<TestDb>;
+export declare function createTestValkey(opts?: { prefix?: string }): Valkey | null;
+export declare function pgAvailable(): boolean;
+export declare function valkeyAvailable(): boolean;