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

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.198",
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.200",
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.200"
60
60
  }
61
61
  }
@@ -50,6 +50,21 @@ Knex discovers package-owned migrations from the installed dependency graph.
50
50
  entrypoint for managed sessions and deployments. `example/.env.example` names
51
51
  the five connection values without a secret.
52
52
 
53
+ `example/tests/browser/database.js` is the application-owned browser preparation
54
+ helper. It requires the exact `TEST_DB_NAME`, rejects the ordinary database,
55
+ retains the schema and migration ledger, applies pending migrations, then clears
56
+ test rows and calls one explicit fixture seed transaction. The seed must restore
57
+ any migration-owned baseline rows the app needs. Call it once before starting
58
+ the isolated test server, never in `beforeEach`. It closes its connections without
59
+ dropping the database. The launcher selects that database only for its own server
60
+ and identity process, disables external effects, and proves actual server identity
61
+ before the suite writes data. It must not edit the normal environment.
62
+
63
+ Keep fixtures small for the selected test scope. Run related cases in one suite
64
+ invocation. Use the development launcher for ordinary browser checks; test the
65
+ production build separately when needed. Fresh-schema migration proofs are
66
+ separate from browser startup.
67
+
53
68
  ## Variation points
54
69
 
55
70
  Change scripts, migration location, connection values, and secret injection to
@@ -66,6 +81,11 @@ it as `seed` to `prepareDatabaseFromApp()`.
66
81
  - Run `npm run db:migrate:status` and the application verification command.
67
82
  - Exercise one transaction and one negative connection case.
68
83
  - When a seed exists, run `db:prepare` twice and require the second run to be safe.
84
+ - For browser preparation, prove two consecutive starts: the second applies no
85
+ old migrations, stale fixture data is removed, baseline rows are restored,
86
+ foreign-key checks remain enabled, and the normal database is unchanged.
87
+ Also prove that a new pending migration is applied. Keep this as a regression
88
+ test when adapting the example; reject a harness that recreates the schema.
69
89
 
70
90
  `example/.github/workflows/verify.yml` is a normal app-owned CI workflow with
71
91
  an explicit MariaDB service. Adapt it as source rather than generating it from
@@ -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.198"
12
12
  }
13
13
  }
@@ -0,0 +1,49 @@
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
+ if (environment.NODE_ENV === "production") throw new Error("Browser tests cannot prepare a production runtime.");
6
+ if (typeof seed !== "function") throw new TypeError("An explicit browser fixture seed is required.");
7
+ if (knexConfig?.client !== "mysql2" || !knexConfig.connection || typeof knexConfig.connection !== "object") {
8
+ throw new Error("Browser preparation requires a resolved MySQL Knex configuration.");
9
+ }
10
+ const database = String(environment.TEST_DB_NAME || "").trim();
11
+ if (!/^[a-zA-Z0-9_]{1,64}$/.test(database)) throw new Error("TEST_DB_NAME must name the exact disposable database.");
12
+ const protectedNames = [knexConfig.connection.database, environment.DB_NAME];
13
+ if (environment.DATABASE_URL) {
14
+ try { protectedNames.push(decodeURIComponent(new URL(environment.DATABASE_URL).pathname.slice(1))); }
15
+ catch { throw new Error("DATABASE_URL must be a valid database URL."); }
16
+ }
17
+ if (!knexConfig.connection.database || protectedNames.some(name => String(name || "").trim().toLowerCase() === database.toLowerCase())) {
18
+ throw new Error("TEST_DB_NAME must differ from every configured application database.");
19
+ }
20
+ const adminConnection = { ...knexConfig.connection };
21
+ delete adminConnection.database;
22
+ const admin = knex({ ...knexConfig, connection: adminConnection, pool: { min: 0, max: 1 } });
23
+ try { await admin.raw("CREATE DATABASE IF NOT EXISTS ??", [database]); }
24
+ finally { await admin.destroy(); }
25
+ const db = knex({ ...knexConfig, connection: { ...knexConfig.connection, database } });
26
+ try {
27
+ const [, migrations] = await db.migrate.latest();
28
+ const connection = await db.client.acquireConnection();
29
+ try {
30
+ const [[actual]] = await db.raw("SELECT DATABASE() AS databaseName").connection(connection);
31
+ if (actual.databaseName !== database) throw new Error("Browser reset requires the exact TEST_DB_NAME connection.");
32
+ const [tables] = await db.raw(
33
+ "SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'", [database]
34
+ ).connection(connection);
35
+ const ledger = knexConfig.migrations?.tableName || "knex_migrations";
36
+ await db.raw("SET FOREIGN_KEY_CHECKS = 0").connection(connection);
37
+ try {
38
+ for (const { name } of tables) {
39
+ if (name === ledger || name === `${ledger}_lock`) continue;
40
+ await db.raw("TRUNCATE TABLE ??", [name]).connection(connection);
41
+ }
42
+ } finally { await db.raw("SET FOREIGN_KEY_CHECKS = 1").connection(connection); }
43
+ } finally { await db.client.releaseConnection(connection); }
44
+ await db.transaction(seed);
45
+ return { database, migrations };
46
+ } finally { await db.destroy(); }
47
+ }
48
+
49
+ export { prepareBrowserTestDatabase };
@@ -0,0 +1,74 @@
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
+ {}, { TEST_DB_NAME: "application" }, { TEST_DB_NAME: "APPLICATION" },
12
+ { TEST_DB_NAME: "other", DB_NAME: "other" },
13
+ { TEST_DB_NAME: "other", DATABASE_URL: "mysql://localhost/other" },
14
+ { TEST_DB_NAME: "tests", NODE_ENV: "production" }
15
+ ]) {
16
+ await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment, seed }), /TEST_DB_NAME|production/u);
17
+ }
18
+ await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment: { TEST_DB_NAME: "tests" } }), /seed/u);
19
+ });
20
+
21
+ test("native browser preparation retains schema, clears data and applies only new migrations", {
22
+ skip: !process.env.TEST_DB_NAME || !process.env.DB_HOST
23
+ }, async () => {
24
+ const environment = process.env;
25
+ const connection = { host: environment.DB_HOST, port: Number(environment.DB_PORT), user: environment.DB_USER,
26
+ password: environment.DB_PASSWORD, database: environment.DB_NAME };
27
+ const admin = knex({ client: "mysql2", connection: { ...connection, database: undefined } });
28
+ const [[existing]] = await admin.raw("SELECT COUNT(*) AS n FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?", [environment.TEST_DB_NAME]);
29
+ if (Number(existing.n)) {
30
+ await admin.destroy();
31
+ throw new Error("The native pattern regression requires a fresh, explicitly supplied TEST_DB_NAME.");
32
+ }
33
+ let db;
34
+ let applied = 0;
35
+ const migrations = [{ name: "001_fixture.js", async up(db) {
36
+ applied += 1;
37
+ await db.schema.createTable("fixtures", table => { table.increments("id"); table.string("label"); });
38
+ await db.schema.createTable("children", table => { table.increments("id"); table.integer("parent_id").unsigned().references("fixtures.id"); });
39
+ }, async down() {} }];
40
+ const knexConfig = { client: "mysql2", connection, migrations: { migrationSource: {
41
+ getMigrations: () => migrations, getMigrationName: migration => migration.name, getMigration: migration => migration
42
+ } } };
43
+ try {
44
+ const [normalBefore] = await admin.raw("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?", [environment.DB_NAME]);
45
+ const first = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
46
+ assert.deepEqual(first.migrations, ["001_fixture.js"]);
47
+ db = knex({ client: "mysql2", connection: { ...connection, database: environment.TEST_DB_NAME } });
48
+ const ledger = await db("knex_migrations").select();
49
+ await db("fixtures").insert({ id: 2, label: "stale" });
50
+ await db("children").insert({ parent_id: 2 });
51
+ const second = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
52
+ assert.deepEqual(second.migrations, []);
53
+ assert.equal(applied, 1);
54
+ assert.deepEqual(await db("knex_migrations").select(), ledger);
55
+ assert.deepEqual(await db("fixtures").select(), [{ id: 1, label: "baseline" }]);
56
+ assert.deepEqual(await db("children").select(), []);
57
+ // The preparation connection has foreign-key checks restored before seeding.
58
+ await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment,
59
+ seed: async db => db("children").insert({ parent_id: 99 }) }), /foreign key constraint/iu);
60
+ migrations.push({ name: "002_pending.js", async up(db) {
61
+ await db.schema.alterTable("fixtures", table => table.string("extra"));
62
+ }, async down() {} });
63
+ const third = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
64
+ assert.deepEqual(third.migrations, ["002_pending.js"]);
65
+ assert.equal(await db.schema.hasColumn("fixtures", "extra"), true);
66
+ assert.equal(applied, 1);
67
+ const [normalAfter] = await admin.raw("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?", [environment.DB_NAME]);
68
+ assert.deepEqual(normalAfter, normalBefore);
69
+ } finally {
70
+ await db?.destroy();
71
+ await admin.raw("DROP DATABASE IF EXISTS ??", [environment.TEST_DB_NAME]);
72
+ await admin.destroy();
73
+ }
74
+ });