@jskit-ai/database-runtime-mysql 0.1.196 → 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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
+
});
|