@jskit-ai/database-runtime-mysql 0.1.197 → 0.1.199

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/database-runtime-mysql",
3
- "version": "0.1.197",
3
+ "version": "0.1.199",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -12,7 +12,7 @@
12
12
  "./shared/dialect": "./src/shared/dialect.js"
13
13
  },
14
14
  "dependencies": {
15
- "@jskit-ai/database-runtime": "0.1.199",
15
+ "@jskit-ai/database-runtime": "0.1.201",
16
16
  "mysql2": "^3.11.2"
17
17
  },
18
18
  "jskit": {
@@ -56,6 +56,6 @@
56
56
  }
57
57
  },
58
58
  "peerDependencies": {
59
- "@jskit-ai/kernel": "0.1.199"
59
+ "@jskit-ai/kernel": "0.1.201"
60
60
  }
61
61
  }
@@ -48,7 +48,29 @@ Knex discovers package-owned migrations from the installed dependency graph.
48
48
  `example/knexfile.js` loads an optional local `.env` and fixes the MySQL dialect.
49
49
  `example/scripts/prepare-database.js` is the portable migrate-then-seed
50
50
  entrypoint for managed sessions and deployments. `example/.env.example` names
51
- the five connection values without a secret.
51
+ the connection values and separate test databases without a secret.
52
+
53
+ `example/tests/browser/database.js` is the application-owned browser preparation
54
+ helper. It requires the exact `BROWSER_TEST_DB_NAME`, rejects the ordinary and
55
+ disposable `TEST_DB_NAME` databases,
56
+ retains the schema and migration ledger, applies pending migrations, then clears
57
+ test rows and calls one explicit fixture seed transaction. The seed must restore
58
+ any migration-owned baseline rows the app needs. Call it once before starting
59
+ the isolated test server, never in `beforeEach`. It closes its connections without
60
+ dropping the database. The launcher selects that database only for its own server
61
+ and identity process, disables external effects, and proves actual server identity
62
+ before the suite writes data. It must not edit the normal environment. Provision
63
+ both test databases with exact grants for the application account. A disposable
64
+ integration or migration test must never drop the browser database.
65
+
66
+ Startup reports whether the schema exists, the number of migrations applied,
67
+ and the migration, fixture-reset and seed durations. Use those measurements to
68
+ diagnose a slow start before rerunning a browser case.
69
+
70
+ Keep fixtures small for the selected test scope. Run related cases in one suite
71
+ invocation. Use the development launcher for ordinary browser checks; test the
72
+ production build separately when needed. Fresh-schema migration proofs are
73
+ separate from browser startup.
52
74
 
53
75
  ## Variation points
54
76
 
@@ -66,6 +88,11 @@ it as `seed` to `prepareDatabaseFromApp()`.
66
88
  - Run `npm run db:migrate:status` and the application verification command.
67
89
  - Exercise one transaction and one negative connection case.
68
90
  - When a seed exists, run `db:prepare` twice and require the second run to be safe.
91
+ - For browser preparation, run a disposable test between two starts: the second applies no
92
+ old migrations, stale fixture data is removed, baseline rows are restored,
93
+ foreign-key checks remain enabled, and the normal database is unchanged.
94
+ Also prove that a new pending migration is applied. Keep this as a regression
95
+ test when adapting the example; reject a harness that recreates the schema.
69
96
 
70
97
  `example/.github/workflows/verify.yml` is a normal app-owned CI workflow with
71
98
  an explicit MariaDB service. Adapt it as source rather than generating it from
@@ -3,3 +3,5 @@ DB_PORT=3306
3
3
  DB_NAME=application
4
4
  DB_USER=application
5
5
  DB_PASSWORD=replace-me
6
+ TEST_DB_NAME=application_test
7
+ BROWSER_TEST_DB_NAME=application_browser_test
@@ -8,6 +8,6 @@
8
8
  "db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/database-runtime-mysql": "0.1.197"
11
+ "@jskit-ai/database-runtime-mysql": "0.1.199"
12
12
  }
13
13
  }
@@ -0,0 +1,68 @@
1
+ import knex from "knex";
2
+
3
+ // Application-owned test setup. Schema migration tests use a separate fresh database.
4
+ async function prepareBrowserTestDatabase({ knexConfig, environment = process.env, seed }) {
5
+ const started = performance.now();
6
+ if (environment.NODE_ENV === "production") throw new Error("Browser tests cannot prepare a production runtime.");
7
+ if (typeof seed !== "function") throw new TypeError("An explicit browser fixture seed is required.");
8
+ if (knexConfig?.client !== "mysql2" || !knexConfig.connection || typeof knexConfig.connection !== "object") {
9
+ throw new Error("Browser preparation requires a resolved MySQL Knex configuration.");
10
+ }
11
+ const database = String(environment.BROWSER_TEST_DB_NAME || "").trim();
12
+ if (!/^[a-zA-Z0-9_]{1,64}$/.test(database)) throw new Error("BROWSER_TEST_DB_NAME must name the exact browser database.");
13
+ const protectedNames = [knexConfig.connection.database, environment.DB_NAME, environment.TEST_DB_NAME];
14
+ if (environment.DATABASE_URL) {
15
+ try { protectedNames.push(decodeURIComponent(new URL(environment.DATABASE_URL).pathname.slice(1))); }
16
+ catch { throw new Error("DATABASE_URL must be a valid database URL."); }
17
+ }
18
+ if (!knexConfig.connection.database || protectedNames.some(name => String(name || "").trim().toLowerCase() === database.toLowerCase())) {
19
+ throw new Error("BROWSER_TEST_DB_NAME must differ from the application and disposable TEST_DB_NAME databases.");
20
+ }
21
+ const adminConnection = { ...knexConfig.connection };
22
+ delete adminConnection.database;
23
+ const admin = knex({ ...knexConfig, connection: adminConnection, pool: { min: 0, max: 1 } });
24
+ let schemaExisted;
25
+ try {
26
+ const [[row]] = await admin.raw("SELECT COUNT(*) AS count FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?", [database]);
27
+ schemaExisted = Number(row.count) === 1;
28
+ await admin.raw("CREATE DATABASE IF NOT EXISTS ??", [database]);
29
+ }
30
+ finally { await admin.destroy(); }
31
+ console.info(`Browser database ${database}: ${schemaExisted ? "existing schema" : "new schema"}.`);
32
+ const db = knex({ ...knexConfig, connection: { ...knexConfig.connection, database } });
33
+ try {
34
+ const migrationStarted = performance.now();
35
+ const [, migrations] = await db.migrate.latest();
36
+ const migrationMs = Math.round(performance.now() - migrationStarted);
37
+ console.info(`Browser migrations: ${migrations.length} applied in ${migrationMs}ms.`);
38
+ const resetStarted = performance.now();
39
+ let resetTables = 0;
40
+ const connection = await db.client.acquireConnection();
41
+ try {
42
+ const [[actual]] = await db.raw("SELECT DATABASE() AS databaseName").connection(connection);
43
+ if (actual.databaseName !== database) throw new Error("Browser reset requires the exact BROWSER_TEST_DB_NAME connection.");
44
+ const [tables] = await db.raw(
45
+ "SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'", [database]
46
+ ).connection(connection);
47
+ const ledger = knexConfig.migrations?.tableName || "knex_migrations";
48
+ await db.raw("SET FOREIGN_KEY_CHECKS = 0").connection(connection);
49
+ try {
50
+ for (const { name } of tables) {
51
+ if (name === ledger || name === `${ledger}_lock`) continue;
52
+ await db.raw("TRUNCATE TABLE ??", [name]).connection(connection);
53
+ resetTables += 1;
54
+ }
55
+ } finally { await db.raw("SET FOREIGN_KEY_CHECKS = 1").connection(connection); }
56
+ } finally { await db.client.releaseConnection(connection); }
57
+ const resetMs = Math.round(performance.now() - resetStarted);
58
+ console.info(`Browser fixture reset: ${resetTables} tables in ${resetMs}ms.`);
59
+ const seedStarted = performance.now();
60
+ await db.transaction(seed);
61
+ const seedMs = Math.round(performance.now() - seedStarted);
62
+ const totalMs = Math.round(performance.now() - started);
63
+ console.info(`Browser fixture seed: ${seedMs}ms; database preparation total: ${totalMs}ms.`);
64
+ return { database, migrations, schemaExisted, timings: { migrationMs, resetMs, seedMs, totalMs } };
65
+ } finally { await db.destroy(); }
66
+ }
67
+
68
+ export { prepareBrowserTestDatabase };
@@ -0,0 +1,82 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import knex from "knex";
4
+ import { prepareBrowserTestDatabase } from "../patterns/mysql-application/example/tests/browser/database.js";
5
+
6
+ const seed = async (db) => db("fixtures").insert({ id: 1, label: "baseline" });
7
+
8
+ test("browser preparation rejects missing, ordinary and production databases before connecting", async () => {
9
+ const knexConfig = { client: "mysql2", connection: { database: "application" } };
10
+ for (const environment of [
11
+ {}, { BROWSER_TEST_DB_NAME: "application" }, { BROWSER_TEST_DB_NAME: "APPLICATION" },
12
+ { BROWSER_TEST_DB_NAME: "other", DB_NAME: "other" },
13
+ { BROWSER_TEST_DB_NAME: "other", DATABASE_URL: "mysql://localhost/other" },
14
+ { BROWSER_TEST_DB_NAME: "tests", TEST_DB_NAME: "TESTS" },
15
+ { BROWSER_TEST_DB_NAME: "tests", NODE_ENV: "production" }
16
+ ]) {
17
+ await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment, seed }), /TEST_DB_NAME|production/u);
18
+ }
19
+ await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment: { BROWSER_TEST_DB_NAME: "tests" } }), /seed/u);
20
+ });
21
+
22
+ test("native browser preparation retains schema, clears data and applies only new migrations", {
23
+ skip: !process.env.BROWSER_TEST_DB_NAME || !process.env.TEST_DB_NAME || !process.env.DB_HOST
24
+ }, async () => {
25
+ const environment = process.env;
26
+ const connection = { host: environment.DB_HOST, port: Number(environment.DB_PORT), user: environment.DB_USER,
27
+ password: environment.DB_PASSWORD, database: environment.DB_NAME };
28
+ const admin = knex({ client: "mysql2", connection: { ...connection, database: undefined } });
29
+ const [[existing]] = await admin.raw("SELECT COUNT(*) AS n FROM information_schema.SCHEMATA WHERE SCHEMA_NAME IN (?, ?)", [environment.BROWSER_TEST_DB_NAME, environment.TEST_DB_NAME]);
30
+ if (Number(existing.n)) {
31
+ await admin.destroy();
32
+ throw new Error("The native pattern regression requires fresh, explicitly supplied test database names.");
33
+ }
34
+ let db;
35
+ let applied = 0;
36
+ const migrations = [{ name: "001_fixture.js", async up(db) {
37
+ applied += 1;
38
+ await db.schema.createTable("fixtures", table => { table.increments("id"); table.string("label"); });
39
+ await db.schema.createTable("children", table => { table.increments("id"); table.integer("parent_id").unsigned().references("fixtures.id"); });
40
+ }, async down() {} }];
41
+ const knexConfig = { client: "mysql2", connection, migrations: { migrationSource: {
42
+ getMigrations: () => migrations, getMigrationName: migration => migration.name, getMigration: migration => migration
43
+ } } };
44
+ try {
45
+ const [normalBefore] = await admin.raw("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?", [environment.DB_NAME]);
46
+ const first = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
47
+ assert.deepEqual(first.migrations, ["001_fixture.js"]);
48
+ assert.equal(first.schemaExisted, false);
49
+ db = knex({ client: "mysql2", connection: { ...connection, database: environment.BROWSER_TEST_DB_NAME } });
50
+ const ledger = await db("knex_migrations").select();
51
+ await db("fixtures").insert({ id: 2, label: "stale" });
52
+ await db("children").insert({ parent_id: 2 });
53
+ // A disposable integration test between browser starts must not erase its schema.
54
+ await admin.raw("CREATE DATABASE ??", [environment.TEST_DB_NAME]);
55
+ await admin.raw("CREATE TABLE ??.proof (id INT PRIMARY KEY)", [environment.TEST_DB_NAME]);
56
+ await admin.raw("DROP DATABASE ??", [environment.TEST_DB_NAME]);
57
+ const second = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
58
+ assert.equal(second.schemaExisted, true);
59
+ assert.deepEqual(second.migrations, []);
60
+ assert.equal(applied, 1);
61
+ assert.deepEqual(await db("knex_migrations").select(), ledger);
62
+ assert.deepEqual(await db("fixtures").select(), [{ id: 1, label: "baseline" }]);
63
+ assert.deepEqual(await db("children").select(), []);
64
+ // The preparation connection has foreign-key checks restored before seeding.
65
+ await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment,
66
+ seed: async db => db("children").insert({ parent_id: 99 }) }), /foreign key constraint/iu);
67
+ migrations.push({ name: "002_pending.js", async up(db) {
68
+ await db.schema.alterTable("fixtures", table => table.string("extra"));
69
+ }, async down() {} });
70
+ const third = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
71
+ assert.deepEqual(third.migrations, ["002_pending.js"]);
72
+ assert.equal(await db.schema.hasColumn("fixtures", "extra"), true);
73
+ assert.equal(applied, 1);
74
+ const [normalAfter] = await admin.raw("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?", [environment.DB_NAME]);
75
+ assert.deepEqual(normalAfter, normalBefore);
76
+ } finally {
77
+ await db?.destroy();
78
+ await admin.raw("DROP DATABASE IF EXISTS ??", [environment.BROWSER_TEST_DB_NAME]);
79
+ await admin.raw("DROP DATABASE IF EXISTS ??", [environment.TEST_DB_NAME]);
80
+ await admin.destroy();
81
+ }
82
+ });