@jskit-ai/database-runtime-mysql 0.1.198 → 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 +3 -3
- package/patterns/mysql-application/PATTERN.md +11 -4
- package/patterns/mysql-application/example/.env.example +2 -0
- package/patterns/mysql-application/example/package.json +1 -1
- package/patterns/mysql-application/example/tests/browser/database.js +26 -7
- package/test/browserDatabasePattern.test.js +17 -9
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.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.
|
|
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.
|
|
59
|
+
"@jskit-ai/kernel": "0.1.201"
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -48,17 +48,24 @@ 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
|
|
51
|
+
the connection values and separate test databases without a secret.
|
|
52
52
|
|
|
53
53
|
`example/tests/browser/database.js` is the application-owned browser preparation
|
|
54
|
-
helper. It requires the exact `
|
|
54
|
+
helper. It requires the exact `BROWSER_TEST_DB_NAME`, rejects the ordinary and
|
|
55
|
+
disposable `TEST_DB_NAME` databases,
|
|
55
56
|
retains the schema and migration ledger, applies pending migrations, then clears
|
|
56
57
|
test rows and calls one explicit fixture seed transaction. The seed must restore
|
|
57
58
|
any migration-owned baseline rows the app needs. Call it once before starting
|
|
58
59
|
the isolated test server, never in `beforeEach`. It closes its connections without
|
|
59
60
|
dropping the database. The launcher selects that database only for its own server
|
|
60
61
|
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
|
+
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.
|
|
62
69
|
|
|
63
70
|
Keep fixtures small for the selected test scope. Run related cases in one suite
|
|
64
71
|
invocation. Use the development launcher for ordinary browser checks; test the
|
|
@@ -81,7 +88,7 @@ it as `seed` to `prepareDatabaseFromApp()`.
|
|
|
81
88
|
- Run `npm run db:migrate:status` and the application verification command.
|
|
82
89
|
- Exercise one transaction and one negative connection case.
|
|
83
90
|
- When a seed exists, run `db:prepare` twice and require the second run to be safe.
|
|
84
|
-
- For browser preparation,
|
|
91
|
+
- For browser preparation, run a disposable test between two starts: the second applies no
|
|
85
92
|
old migrations, stale fixture data is removed, baseline rows are restored,
|
|
86
93
|
foreign-key checks remain enabled, and the normal database is unchanged.
|
|
87
94
|
Also prove that a new pending migration is applied. Keep this as a regression
|
|
@@ -2,33 +2,45 @@ import knex from "knex";
|
|
|
2
2
|
|
|
3
3
|
// Application-owned test setup. Schema migration tests use a separate fresh database.
|
|
4
4
|
async function prepareBrowserTestDatabase({ knexConfig, environment = process.env, seed }) {
|
|
5
|
+
const started = performance.now();
|
|
5
6
|
if (environment.NODE_ENV === "production") throw new Error("Browser tests cannot prepare a production runtime.");
|
|
6
7
|
if (typeof seed !== "function") throw new TypeError("An explicit browser fixture seed is required.");
|
|
7
8
|
if (knexConfig?.client !== "mysql2" || !knexConfig.connection || typeof knexConfig.connection !== "object") {
|
|
8
9
|
throw new Error("Browser preparation requires a resolved MySQL Knex configuration.");
|
|
9
10
|
}
|
|
10
|
-
const database = String(environment.
|
|
11
|
-
if (!/^[a-zA-Z0-9_]{1,64}$/.test(database)) throw new Error("
|
|
12
|
-
const protectedNames = [knexConfig.connection.database, environment.DB_NAME];
|
|
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];
|
|
13
14
|
if (environment.DATABASE_URL) {
|
|
14
15
|
try { protectedNames.push(decodeURIComponent(new URL(environment.DATABASE_URL).pathname.slice(1))); }
|
|
15
16
|
catch { throw new Error("DATABASE_URL must be a valid database URL."); }
|
|
16
17
|
}
|
|
17
18
|
if (!knexConfig.connection.database || protectedNames.some(name => String(name || "").trim().toLowerCase() === database.toLowerCase())) {
|
|
18
|
-
throw new Error("
|
|
19
|
+
throw new Error("BROWSER_TEST_DB_NAME must differ from the application and disposable TEST_DB_NAME databases.");
|
|
19
20
|
}
|
|
20
21
|
const adminConnection = { ...knexConfig.connection };
|
|
21
22
|
delete adminConnection.database;
|
|
22
23
|
const admin = knex({ ...knexConfig, connection: adminConnection, pool: { min: 0, max: 1 } });
|
|
23
|
-
|
|
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
|
+
}
|
|
24
30
|
finally { await admin.destroy(); }
|
|
31
|
+
console.info(`Browser database ${database}: ${schemaExisted ? "existing schema" : "new schema"}.`);
|
|
25
32
|
const db = knex({ ...knexConfig, connection: { ...knexConfig.connection, database } });
|
|
26
33
|
try {
|
|
34
|
+
const migrationStarted = performance.now();
|
|
27
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;
|
|
28
40
|
const connection = await db.client.acquireConnection();
|
|
29
41
|
try {
|
|
30
42
|
const [[actual]] = await db.raw("SELECT DATABASE() AS databaseName").connection(connection);
|
|
31
|
-
if (actual.databaseName !== database) throw new Error("Browser reset requires the exact
|
|
43
|
+
if (actual.databaseName !== database) throw new Error("Browser reset requires the exact BROWSER_TEST_DB_NAME connection.");
|
|
32
44
|
const [tables] = await db.raw(
|
|
33
45
|
"SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'", [database]
|
|
34
46
|
).connection(connection);
|
|
@@ -38,11 +50,18 @@ async function prepareBrowserTestDatabase({ knexConfig, environment = process.en
|
|
|
38
50
|
for (const { name } of tables) {
|
|
39
51
|
if (name === ledger || name === `${ledger}_lock`) continue;
|
|
40
52
|
await db.raw("TRUNCATE TABLE ??", [name]).connection(connection);
|
|
53
|
+
resetTables += 1;
|
|
41
54
|
}
|
|
42
55
|
} finally { await db.raw("SET FOREIGN_KEY_CHECKS = 1").connection(connection); }
|
|
43
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();
|
|
44
60
|
await db.transaction(seed);
|
|
45
|
-
|
|
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 } };
|
|
46
65
|
} finally { await db.destroy(); }
|
|
47
66
|
}
|
|
48
67
|
|
|
@@ -8,27 +8,28 @@ const seed = async (db) => db("fixtures").insert({ id: 1, label: "baseline" });
|
|
|
8
8
|
test("browser preparation rejects missing, ordinary and production databases before connecting", async () => {
|
|
9
9
|
const knexConfig = { client: "mysql2", connection: { database: "application" } };
|
|
10
10
|
for (const environment of [
|
|
11
|
-
{}, {
|
|
12
|
-
{
|
|
13
|
-
{
|
|
14
|
-
{
|
|
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" }
|
|
15
16
|
]) {
|
|
16
17
|
await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment, seed }), /TEST_DB_NAME|production/u);
|
|
17
18
|
}
|
|
18
|
-
await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment: {
|
|
19
|
+
await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment: { BROWSER_TEST_DB_NAME: "tests" } }), /seed/u);
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
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
|
+
skip: !process.env.BROWSER_TEST_DB_NAME || !process.env.TEST_DB_NAME || !process.env.DB_HOST
|
|
23
24
|
}, async () => {
|
|
24
25
|
const environment = process.env;
|
|
25
26
|
const connection = { host: environment.DB_HOST, port: Number(environment.DB_PORT), user: environment.DB_USER,
|
|
26
27
|
password: environment.DB_PASSWORD, database: environment.DB_NAME };
|
|
27
28
|
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
|
|
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]);
|
|
29
30
|
if (Number(existing.n)) {
|
|
30
31
|
await admin.destroy();
|
|
31
|
-
throw new Error("The native pattern regression requires
|
|
32
|
+
throw new Error("The native pattern regression requires fresh, explicitly supplied test database names.");
|
|
32
33
|
}
|
|
33
34
|
let db;
|
|
34
35
|
let applied = 0;
|
|
@@ -44,11 +45,17 @@ test("native browser preparation retains schema, clears data and applies only ne
|
|
|
44
45
|
const [normalBefore] = await admin.raw("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?", [environment.DB_NAME]);
|
|
45
46
|
const first = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
|
|
46
47
|
assert.deepEqual(first.migrations, ["001_fixture.js"]);
|
|
47
|
-
|
|
48
|
+
assert.equal(first.schemaExisted, false);
|
|
49
|
+
db = knex({ client: "mysql2", connection: { ...connection, database: environment.BROWSER_TEST_DB_NAME } });
|
|
48
50
|
const ledger = await db("knex_migrations").select();
|
|
49
51
|
await db("fixtures").insert({ id: 2, label: "stale" });
|
|
50
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]);
|
|
51
57
|
const second = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
|
|
58
|
+
assert.equal(second.schemaExisted, true);
|
|
52
59
|
assert.deepEqual(second.migrations, []);
|
|
53
60
|
assert.equal(applied, 1);
|
|
54
61
|
assert.deepEqual(await db("knex_migrations").select(), ledger);
|
|
@@ -68,6 +75,7 @@ test("native browser preparation retains schema, clears data and applies only ne
|
|
|
68
75
|
assert.deepEqual(normalAfter, normalBefore);
|
|
69
76
|
} finally {
|
|
70
77
|
await db?.destroy();
|
|
78
|
+
await admin.raw("DROP DATABASE IF EXISTS ??", [environment.BROWSER_TEST_DB_NAME]);
|
|
71
79
|
await admin.raw("DROP DATABASE IF EXISTS ??", [environment.TEST_DB_NAME]);
|
|
72
80
|
await admin.destroy();
|
|
73
81
|
}
|