@jskit-ai/database-runtime 0.1.159 → 0.1.161
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 +18 -71
- package/src/server/databaseSetup.js +69 -0
- package/src/server/knexFactory.js +23 -0
- package/src/server/knexMigrationConfig.js +108 -0
- package/src/server/providers/DatabaseProvider.js +64 -0
- package/src/shared/databaseConnection.js +2 -1
- package/src/shared/index.js +2 -2
- package/src/shared/repositoryOptions.js +38 -1
- package/test/databaseConnection.test.js +2 -0
- package/test/databaseSetup.test.js +64 -0
- package/test/entrypoints.boundary.test.js +7 -7
- package/test/knexMigrationConfig.test.js +80 -0
- package/test/packageMetadata.test.js +11 -46
- package/test/providerRuntime.test.js +103 -161
- package/test/repositoryOptions.test.js +47 -0
- package/test/runtimeCore.test.js +1 -54
- package/src/server/providers/DatabaseRuntimeServiceProvider.js +0 -212
- package/src/shared/runtime.js +0 -58
- package/templates/knexfile.js +0 -36
- package/templates/migrations/.gitkeep +0 -1
- package/templates/migrations/constraints/.gitkeep +0 -1
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/database-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.161",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
10
|
-
"./server/providers/
|
|
10
|
+
"./server/providers/DatabaseProvider": "./src/server/providers/DatabaseProvider.js",
|
|
11
|
+
"./server/databaseSetup": "./src/server/databaseSetup.js",
|
|
12
|
+
"./server/knexMigrationConfig": "./src/server/knexMigrationConfig.js",
|
|
11
13
|
"./shared": "./src/shared/index.js",
|
|
12
14
|
"./shared/dateUtils": "./src/shared/dateUtils.js",
|
|
13
15
|
"./shared/databaseClient": "./src/shared/databaseClient.js",
|
|
@@ -25,7 +27,8 @@
|
|
|
25
27
|
"./shared/transactions": "./src/shared/transactions.js"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
|
-
"@jskit-ai/kernel": "0.1.
|
|
30
|
+
"@jskit-ai/kernel": "0.1.161",
|
|
31
|
+
"knex": "^3.1.0"
|
|
29
32
|
},
|
|
30
33
|
"jskit": {
|
|
31
34
|
"kind": "runtime",
|
|
@@ -39,11 +42,10 @@
|
|
|
39
42
|
},
|
|
40
43
|
"runtime": {
|
|
41
44
|
"server": {
|
|
42
|
-
"providerEntrypoint": "src/server/providers/DatabaseRuntimeServiceProvider.js",
|
|
43
45
|
"providers": [
|
|
44
46
|
{
|
|
45
|
-
"entrypoint": "src/server/providers/
|
|
46
|
-
"export": "
|
|
47
|
+
"entrypoint": "src/server/providers/DatabaseProvider.js",
|
|
48
|
+
"export": "DatabaseProvider"
|
|
47
49
|
}
|
|
48
50
|
]
|
|
49
51
|
},
|
|
@@ -56,7 +58,15 @@
|
|
|
56
58
|
"surfaces": [
|
|
57
59
|
{
|
|
58
60
|
"subpath": "./server",
|
|
59
|
-
"summary": "
|
|
61
|
+
"summary": "Provides the cohesive runtime.database capability with Knex, transactions, the selected driver, and repository utilities."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"subpath": "./server/databaseSetup",
|
|
65
|
+
"summary": "Migrates the installed package graph and runs one explicit app-owned seed operation without a CLI or persistent setup record."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"subpath": "./server/knexMigrationConfig",
|
|
69
|
+
"summary": "Builds fixed-dialect Knex migration configuration and discovers immutable migrations directly from installed JSKIT packages."
|
|
60
70
|
},
|
|
61
71
|
{
|
|
62
72
|
"subpath": "./shared",
|
|
@@ -66,71 +76,8 @@
|
|
|
66
76
|
"subpath": "./client",
|
|
67
77
|
"summary": "Exports no runtime API today (reserved client entrypoint)."
|
|
68
78
|
}
|
|
69
|
-
]
|
|
70
|
-
"containerTokens": {
|
|
71
|
-
"server": [
|
|
72
|
-
"runtime.database",
|
|
73
|
-
"runtime.database.driver",
|
|
74
|
-
"jskit.database.knex",
|
|
75
|
-
"jskit.database.transactionManager"
|
|
76
|
-
],
|
|
77
|
-
"client": []
|
|
78
|
-
}
|
|
79
|
+
]
|
|
79
80
|
}
|
|
80
|
-
},
|
|
81
|
-
"ci": {
|
|
82
|
-
"environment": {},
|
|
83
|
-
"services": [],
|
|
84
|
-
"steps": [
|
|
85
|
-
{
|
|
86
|
-
"id": "database-migrations",
|
|
87
|
-
"phase": "before-verify",
|
|
88
|
-
"label": "Apply database migrations",
|
|
89
|
-
"command": "npm run db:migrate"
|
|
90
|
-
}
|
|
91
|
-
]
|
|
92
|
-
},
|
|
93
|
-
"mutations": {
|
|
94
|
-
"dependencies": {
|
|
95
|
-
"runtime": {
|
|
96
|
-
"@jskit-ai/kernel": "0.1.159",
|
|
97
|
-
"dotenv": "^16.4.5",
|
|
98
|
-
"knex": "^3.1.0"
|
|
99
|
-
},
|
|
100
|
-
"dev": {}
|
|
101
|
-
},
|
|
102
|
-
"packageJson": {
|
|
103
|
-
"scripts": {
|
|
104
|
-
"db:migrations:sync": "jskit migrations sync",
|
|
105
|
-
"db:migrate": "npm run db:migrations:sync && knex --knexfile ./knexfile.js migrate:latest",
|
|
106
|
-
"db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
|
|
107
|
-
"db:migrate:status": "npm run db:migrations:sync && knex --knexfile ./knexfile.js migrate:list"
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
"procfile": {},
|
|
111
|
-
"files": [
|
|
112
|
-
{
|
|
113
|
-
"from": "templates/knexfile.js",
|
|
114
|
-
"to": "knexfile.js",
|
|
115
|
-
"reason": "Install root Knex configuration so app scripts can run migrations through Knex CLI.",
|
|
116
|
-
"category": "database-runtime",
|
|
117
|
-
"id": "database-runtime-knexfile"
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
"from": "templates/migrations/.gitkeep",
|
|
121
|
-
"to": "migrations/.gitkeep",
|
|
122
|
-
"reason": "Ensure migrations directory exists so Knex migration commands can run before any module installs migrations.",
|
|
123
|
-
"category": "database-runtime",
|
|
124
|
-
"id": "database-runtime-migrations-dir"
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
"from": "templates/migrations/constraints/.gitkeep",
|
|
128
|
-
"to": "migrations/constraints/.gitkeep",
|
|
129
|
-
"reason": "Ensure the ordered deferred-constraint migration directory exists.",
|
|
130
|
-
"category": "database-runtime",
|
|
131
|
-
"id": "database-runtime-constraint-migrations-dir"
|
|
132
|
-
}
|
|
133
|
-
]
|
|
134
81
|
}
|
|
135
82
|
}
|
|
136
83
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createKnexMigrationConfigFromApp } from "./knexMigrationConfig.js";
|
|
3
|
+
import { loadKnexFactory } from "./knexFactory.js";
|
|
4
|
+
|
|
5
|
+
function normalizeMigrationResult(result) {
|
|
6
|
+
const [batchValue, migrationValues] = Array.isArray(result) ? result : [];
|
|
7
|
+
const batch = Number.isSafeInteger(batchValue) ? batchValue : 0;
|
|
8
|
+
const migrations = Array.isArray(migrationValues)
|
|
9
|
+
? migrationValues.map((value) => String(value || "").trim()).filter(Boolean)
|
|
10
|
+
: [];
|
|
11
|
+
return Object.freeze({ batch, migrations: Object.freeze(migrations) });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function runDatabaseSetup({ knex, seed, appRoot = process.cwd(), environment = process.env } = {}) {
|
|
15
|
+
if (!knex || typeof knex !== "function" || typeof knex.migrate?.latest !== "function") {
|
|
16
|
+
throw new TypeError("runDatabaseSetup requires a Knex instance.");
|
|
17
|
+
}
|
|
18
|
+
if (typeof seed !== "undefined" && seed !== null && typeof seed !== "function") {
|
|
19
|
+
throw new TypeError("runDatabaseSetup seed must be a function when supplied.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const migrationResult = normalizeMigrationResult(await knex.migrate.latest());
|
|
23
|
+
if (typeof seed !== "function") {
|
|
24
|
+
return Object.freeze({ ...migrationResult, seeded: false });
|
|
25
|
+
}
|
|
26
|
+
if (typeof knex.transaction !== "function") {
|
|
27
|
+
throw new TypeError("runDatabaseSetup requires Knex transaction support for seeding.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await knex.transaction(async (transaction) => {
|
|
31
|
+
await seed(Object.freeze({
|
|
32
|
+
knex: transaction,
|
|
33
|
+
appRoot: path.resolve(appRoot),
|
|
34
|
+
environment
|
|
35
|
+
}));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return Object.freeze({ ...migrationResult, seeded: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function prepareDatabaseFromApp({
|
|
42
|
+
client,
|
|
43
|
+
appRoot = process.cwd(),
|
|
44
|
+
environment = process.env,
|
|
45
|
+
migrationsDirectory = "migrations",
|
|
46
|
+
seed
|
|
47
|
+
} = {}) {
|
|
48
|
+
const normalizedAppRoot = path.resolve(appRoot);
|
|
49
|
+
const config = await createKnexMigrationConfigFromApp({
|
|
50
|
+
client,
|
|
51
|
+
appRoot: normalizedAppRoot,
|
|
52
|
+
environment,
|
|
53
|
+
migrationsDirectory
|
|
54
|
+
});
|
|
55
|
+
const knex = loadKnexFactory()(config);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await runDatabaseSetup({
|
|
59
|
+
knex,
|
|
60
|
+
seed,
|
|
61
|
+
appRoot: normalizedAppRoot,
|
|
62
|
+
environment
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
await knex.destroy();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { prepareDatabaseFromApp, runDatabaseSetup };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { symlinkSafeRequire } from "@jskit-ai/kernel/server/support";
|
|
2
|
+
|
|
3
|
+
function loadKnexFactory() {
|
|
4
|
+
let moduleValue;
|
|
5
|
+
try {
|
|
6
|
+
moduleValue = symlinkSafeRequire("knex");
|
|
7
|
+
} catch {
|
|
8
|
+
throw new Error("Knex package is not installed. Install the selected JSKIT database runtime package with npm.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const knexFactory =
|
|
12
|
+
typeof moduleValue === "function"
|
|
13
|
+
? moduleValue
|
|
14
|
+
: typeof moduleValue?.default === "function"
|
|
15
|
+
? moduleValue.default
|
|
16
|
+
: null;
|
|
17
|
+
if (!knexFactory) {
|
|
18
|
+
throw new Error("Knex package resolved but did not expose a callable factory.");
|
|
19
|
+
}
|
|
20
|
+
return knexFactory;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { loadKnexFactory };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { discoverInstalledPackages } from "@jskit-ai/kernel/server/support";
|
|
4
|
+
import {
|
|
5
|
+
normalizeDatabaseClient,
|
|
6
|
+
toKnexClientId
|
|
7
|
+
} from "../shared/databaseClient.js";
|
|
8
|
+
import { resolveKnexConnectionFromEnvironment } from "../shared/databaseConnection.js";
|
|
9
|
+
|
|
10
|
+
function createKnexMigrationConfig({
|
|
11
|
+
client,
|
|
12
|
+
environment = process.env,
|
|
13
|
+
appRoot = process.cwd(),
|
|
14
|
+
migrationsDirectory = "migrations",
|
|
15
|
+
packageMigrationDirectories = []
|
|
16
|
+
} = {}) {
|
|
17
|
+
const dialectId = normalizeDatabaseClient(client);
|
|
18
|
+
const resolvedMigrationsDirectory = path.resolve(appRoot, migrationsDirectory);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
client: toKnexClientId(dialectId),
|
|
22
|
+
connection: resolveKnexConnectionFromEnvironment(environment, {
|
|
23
|
+
client: dialectId,
|
|
24
|
+
defaultPort: dialectId === "pg" ? 5432 : 3306,
|
|
25
|
+
context: "knex migrations"
|
|
26
|
+
}),
|
|
27
|
+
migrations: {
|
|
28
|
+
directory: [
|
|
29
|
+
resolvedMigrationsDirectory,
|
|
30
|
+
...packageMigrationDirectories.map((directory) => path.resolve(directory)),
|
|
31
|
+
path.join(resolvedMigrationsDirectory, "constraints")
|
|
32
|
+
],
|
|
33
|
+
extension: "cjs",
|
|
34
|
+
sortDirsSeparately: true
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function directoryExists(directory) {
|
|
40
|
+
try {
|
|
41
|
+
await access(directory);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function discoverPackageMigrationDirectories({ appRoot = process.cwd() } = {}) {
|
|
49
|
+
const normalizedAppRoot = path.resolve(appRoot);
|
|
50
|
+
const directories = [];
|
|
51
|
+
|
|
52
|
+
for (const installedPackage of await discoverInstalledPackages({ appRoot: normalizedAppRoot })) {
|
|
53
|
+
const declaredDirectories = installedPackage.packageMetadata?.migrations?.directories;
|
|
54
|
+
if (typeof declaredDirectories === "undefined") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(declaredDirectories) || declaredDirectories.length < 1) {
|
|
58
|
+
throw new TypeError(
|
|
59
|
+
`${installedPackage.packageId} package.json#jskit.migrations.directories must be a non-empty array.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const declaredDirectory of declaredDirectories) {
|
|
64
|
+
const relativeDirectory = String(declaredDirectory || "").trim();
|
|
65
|
+
if (
|
|
66
|
+
!relativeDirectory ||
|
|
67
|
+
path.isAbsolute(relativeDirectory) ||
|
|
68
|
+
relativeDirectory === ".." ||
|
|
69
|
+
relativeDirectory.startsWith(`..${path.sep}`)
|
|
70
|
+
) {
|
|
71
|
+
throw new TypeError(
|
|
72
|
+
`${installedPackage.packageId} declares an invalid package migration directory: ${relativeDirectory || "<empty>"}.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const absoluteDirectory = path.resolve(installedPackage.packageRoot, relativeDirectory);
|
|
76
|
+
const relativeToPackage = path.relative(installedPackage.packageRoot, absoluteDirectory);
|
|
77
|
+
if (relativeToPackage.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToPackage)) {
|
|
78
|
+
throw new TypeError(
|
|
79
|
+
`${installedPackage.packageId} package migration directory escapes its package root: ${relativeDirectory}.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (!(await directoryExists(absoluteDirectory))) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`${installedPackage.packageId} declares a missing package migration directory: ${relativeDirectory}.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
directories.push(absoluteDirectory);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Object.freeze([...new Set(directories)].sort((left, right) => left.localeCompare(right)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function createKnexMigrationConfigFromApp(options = {}) {
|
|
95
|
+
const appRoot = path.resolve(options.appRoot || process.cwd());
|
|
96
|
+
const packageMigrationDirectories = await discoverPackageMigrationDirectories({ appRoot });
|
|
97
|
+
return createKnexMigrationConfig({
|
|
98
|
+
...options,
|
|
99
|
+
appRoot,
|
|
100
|
+
packageMigrationDirectories
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
createKnexMigrationConfig,
|
|
106
|
+
createKnexMigrationConfigFromApp,
|
|
107
|
+
discoverPackageMigrationDirectories
|
|
108
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
2
|
+
import * as databaseUtilities from "../../shared/index.js";
|
|
3
|
+
import { normalizeDatabaseClient, toKnexClientId } from "../../shared/databaseClient.js";
|
|
4
|
+
import { resolveKnexConnectionFromEnvironment } from "../../shared/databaseConnection.js";
|
|
5
|
+
import { createTransactionManager } from "../../shared/transactionManager.js";
|
|
6
|
+
import { loadKnexFactory } from "../knexFactory.js";
|
|
7
|
+
|
|
8
|
+
function resolveDriverDialectId(driver) {
|
|
9
|
+
const dialectId = normalizeDatabaseClient(
|
|
10
|
+
driver?.DIALECT_ID || driver?.dialectId || driver?.dialect || driver?.getDialectId?.(),
|
|
11
|
+
{ allowEmpty: true }
|
|
12
|
+
);
|
|
13
|
+
if (!dialectId) {
|
|
14
|
+
throw new Error("The installed database driver did not expose a valid dialect id.");
|
|
15
|
+
}
|
|
16
|
+
return dialectId;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createDatabase({ driver, env }) {
|
|
20
|
+
const dialectId = resolveDriverDialectId(driver);
|
|
21
|
+
const configuredClient = normalizeDatabaseClient(env.DB_CLIENT, { allowEmpty: true });
|
|
22
|
+
if (configuredClient && configuredClient !== dialectId) {
|
|
23
|
+
throw new Error(`DB_CLIENT="${configuredClient}" does not match installed database driver "${dialectId}".`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const knex = loadKnexFactory()({
|
|
27
|
+
client: toKnexClientId(dialectId),
|
|
28
|
+
connection: resolveKnexConnectionFromEnvironment(env, {
|
|
29
|
+
client: dialectId,
|
|
30
|
+
defaultPort: dialectId === "pg" ? 5432 : 3306,
|
|
31
|
+
context: "database runtime"
|
|
32
|
+
})
|
|
33
|
+
});
|
|
34
|
+
const transactionManager = createTransactionManager({ knex });
|
|
35
|
+
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
...databaseUtilities,
|
|
38
|
+
driver,
|
|
39
|
+
knex,
|
|
40
|
+
transactionManager
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DatabaseProvider = defineProvider({
|
|
45
|
+
id: "runtime.database",
|
|
46
|
+
requires: {
|
|
47
|
+
env: "runtime.env",
|
|
48
|
+
driver: "runtime.database.driver"
|
|
49
|
+
},
|
|
50
|
+
provides: {
|
|
51
|
+
database: "runtime.database"
|
|
52
|
+
},
|
|
53
|
+
setup({ driver, env }) {
|
|
54
|
+
return { database: createDatabase({ driver, env }) };
|
|
55
|
+
},
|
|
56
|
+
async shutdown(_dependencies, { outputs }) {
|
|
57
|
+
const knex = outputs.database?.knex;
|
|
58
|
+
if (knex && typeof knex.destroy === "function") {
|
|
59
|
+
await knex.destroy();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export { DatabaseProvider };
|
package/src/shared/index.js
CHANGED
|
@@ -5,7 +5,6 @@ export {
|
|
|
5
5
|
} from "./runtimeErrors.js";
|
|
6
6
|
export { TransactionManager, createTransactionManager } from "./transactionManager.js";
|
|
7
7
|
export { BaseRepository, buildPaginationMeta } from "./repository.js";
|
|
8
|
-
export { registerDatabaseRuntime } from "./runtime.js";
|
|
9
8
|
export {
|
|
10
9
|
normalizeDateInput,
|
|
11
10
|
toIsoString,
|
|
@@ -46,7 +45,8 @@ export {
|
|
|
46
45
|
parseJsonValue,
|
|
47
46
|
toDbJson,
|
|
48
47
|
runInTransaction,
|
|
49
|
-
createWithTransaction
|
|
48
|
+
createWithTransaction,
|
|
49
|
+
lockScopedRecordId
|
|
50
50
|
} from "./repositoryOptions.js";
|
|
51
51
|
export {
|
|
52
52
|
normalizeBatchSize,
|
|
@@ -242,6 +242,42 @@ function createWithTransaction(knex) {
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
function requireSqlIdentifier(value, label) {
|
|
246
|
+
const identifier = String(value || "").trim();
|
|
247
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(identifier)) {
|
|
248
|
+
throw new TypeError(`${label} must be a safe SQL identifier.`);
|
|
249
|
+
}
|
|
250
|
+
return identifier;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function lockScopedRecordId({
|
|
254
|
+
trx,
|
|
255
|
+
tableName,
|
|
256
|
+
recordId,
|
|
257
|
+
scopeId,
|
|
258
|
+
idColumn = "id",
|
|
259
|
+
scopeColumn = "workspace_id"
|
|
260
|
+
} = {}) {
|
|
261
|
+
if (typeof trx !== "function") {
|
|
262
|
+
throw new TypeError("lockScopedRecordId requires a transaction.");
|
|
263
|
+
}
|
|
264
|
+
const table = requireSqlIdentifier(tableName, "lockScopedRecordId tableName");
|
|
265
|
+
const recordKey = requireSqlIdentifier(idColumn, "lockScopedRecordId idColumn");
|
|
266
|
+
const scopeKey = requireSqlIdentifier(scopeColumn, "lockScopedRecordId scopeColumn");
|
|
267
|
+
const normalizedRecordId = normalizeDbRecordId(recordId);
|
|
268
|
+
const normalizedScopeId = normalizeDbRecordId(scopeId);
|
|
269
|
+
if (!normalizedRecordId || !normalizedScopeId) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const row = await trx(table)
|
|
273
|
+
.select(recordKey)
|
|
274
|
+
.where(recordKey, normalizedRecordId)
|
|
275
|
+
.andWhere(scopeKey, normalizedScopeId)
|
|
276
|
+
.forUpdate()
|
|
277
|
+
.first();
|
|
278
|
+
return normalizeDbRecordId(row?.[recordKey]);
|
|
279
|
+
}
|
|
280
|
+
|
|
245
281
|
export {
|
|
246
282
|
resolveQueryOptions,
|
|
247
283
|
resolveRepoClient,
|
|
@@ -260,5 +296,6 @@ export {
|
|
|
260
296
|
parseJsonValue,
|
|
261
297
|
toDbJson,
|
|
262
298
|
runInTransaction,
|
|
263
|
-
createWithTransaction
|
|
299
|
+
createWithTransaction,
|
|
300
|
+
lockScopedRecordId
|
|
264
301
|
};
|
|
@@ -87,6 +87,7 @@ test("resolveKnexConnectionFromEnvironment keeps only MySQL DATE columns as stri
|
|
|
87
87
|
assert.deepEqual(mysqlConnection.dateStrings, ["DATE"]);
|
|
88
88
|
assert.equal(mysqlConnection.supportBigNumbers, true);
|
|
89
89
|
assert.equal(mysqlConnection.bigNumberStrings, true);
|
|
90
|
+
assert.equal(mysqlConnection.timezone, "Z");
|
|
90
91
|
|
|
91
92
|
const postgresConnection = resolveKnexConnectionFromEnvironment({
|
|
92
93
|
DB_CLIENT: "pg",
|
|
@@ -100,4 +101,5 @@ test("resolveKnexConnectionFromEnvironment keeps only MySQL DATE columns as stri
|
|
|
100
101
|
});
|
|
101
102
|
|
|
102
103
|
assert.equal(postgresConnection.dateStrings, undefined);
|
|
104
|
+
assert.equal(postgresConnection.timezone, undefined);
|
|
103
105
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { runDatabaseSetup } from "../src/server/databaseSetup.js";
|
|
4
|
+
|
|
5
|
+
function fakeKnex({ migrationResult = [3, ["users.cjs"]] } = {}) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const transaction = Object.freeze({ id: "transaction" });
|
|
8
|
+
const knex = function knex() {};
|
|
9
|
+
knex.migrate = {
|
|
10
|
+
async latest() {
|
|
11
|
+
calls.push("migrate");
|
|
12
|
+
return migrationResult;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
knex.transaction = async (callback) => {
|
|
16
|
+
calls.push("transaction:start");
|
|
17
|
+
await callback(transaction);
|
|
18
|
+
calls.push("transaction:commit");
|
|
19
|
+
};
|
|
20
|
+
return { calls, knex, transaction };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("runDatabaseSetup migrates before one explicit app seed transaction", async () => {
|
|
24
|
+
const { calls, knex, transaction } = fakeKnex();
|
|
25
|
+
const environment = { DB_NAME: "session_database" };
|
|
26
|
+
|
|
27
|
+
const result = await runDatabaseSetup({
|
|
28
|
+
knex,
|
|
29
|
+
appRoot: "/srv/session/source",
|
|
30
|
+
environment,
|
|
31
|
+
async seed(context) {
|
|
32
|
+
calls.push("seed");
|
|
33
|
+
assert.equal(context.knex, transaction);
|
|
34
|
+
assert.equal(context.appRoot, "/srv/session/source");
|
|
35
|
+
assert.equal(context.environment, environment);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(calls, ["migrate", "transaction:start", "seed", "transaction:commit"]);
|
|
40
|
+
assert.deepEqual(result, {
|
|
41
|
+
batch: 3,
|
|
42
|
+
migrations: ["users.cjs"],
|
|
43
|
+
seeded: true
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("runDatabaseSetup supports applications with no seed operation", async () => {
|
|
48
|
+
const { calls, knex } = fakeKnex({ migrationResult: [0, []] });
|
|
49
|
+
|
|
50
|
+
assert.deepEqual(await runDatabaseSetup({ knex }), {
|
|
51
|
+
batch: 0,
|
|
52
|
+
migrations: [],
|
|
53
|
+
seeded: false
|
|
54
|
+
});
|
|
55
|
+
assert.deepEqual(calls, ["migrate"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("runDatabaseSetup rejects a non-function seed", async () => {
|
|
59
|
+
const { knex } = fakeKnex();
|
|
60
|
+
await assert.rejects(
|
|
61
|
+
() => runDatabaseSetup({ knex, seed: "seed" }),
|
|
62
|
+
/seed must be a function/u
|
|
63
|
+
);
|
|
64
|
+
});
|
|
@@ -4,15 +4,15 @@ import test from "node:test";
|
|
|
4
4
|
|
|
5
5
|
import * as clientApi from "../src/client/index.js";
|
|
6
6
|
import * as sharedApi from "../src/shared/index.js";
|
|
7
|
-
import {
|
|
7
|
+
import { DatabaseProvider } from "../src/server/providers/DatabaseProvider.js";
|
|
8
8
|
|
|
9
9
|
test("package exports include explicit shared and provider entrypoints", async () => {
|
|
10
10
|
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
11
|
const exportsMap = packageJson && typeof packageJson === "object" ? packageJson.exports : {};
|
|
12
12
|
assert.equal(exportsMap["./server"], undefined);
|
|
13
13
|
assert.equal(
|
|
14
|
-
exportsMap["./server/providers/
|
|
15
|
-
"./src/server/providers/
|
|
14
|
+
exportsMap["./server/providers/DatabaseProvider"],
|
|
15
|
+
"./src/server/providers/DatabaseProvider.js"
|
|
16
16
|
);
|
|
17
17
|
assert.equal(exportsMap["./shared"], "./src/shared/index.js");
|
|
18
18
|
});
|
|
@@ -21,14 +21,14 @@ test("client entrypoint exports no database runtime api", () => {
|
|
|
21
21
|
assert.deepEqual(Object.keys(clientApi), []);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
test("server provider module exports
|
|
25
|
-
assert.equal(
|
|
24
|
+
test("server provider module exports one declarative provider", () => {
|
|
25
|
+
assert.equal(DatabaseProvider.id, "runtime.database");
|
|
26
|
+
assert.deepEqual(DatabaseProvider.provides, { database: "runtime.database" });
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
test("shared entrypoint exports shared database utilities only", () => {
|
|
29
|
-
assert.equal(typeof sharedApi.registerDatabaseRuntime, "function");
|
|
30
30
|
assert.equal(typeof sharedApi.createTransactionManager, "function");
|
|
31
31
|
assert.equal(typeof sharedApi.isDuplicateEntryError, "function");
|
|
32
32
|
assert.equal(typeof sharedApi.createRepositoryScope, "function");
|
|
33
|
-
assert.equal(typeof sharedApi.
|
|
33
|
+
assert.equal(typeof sharedApi.DatabaseProvider, "undefined");
|
|
34
34
|
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
createKnexMigrationConfig,
|
|
8
|
+
createKnexMigrationConfigFromApp,
|
|
9
|
+
discoverPackageMigrationDirectories
|
|
10
|
+
} from "../src/server/knexMigrationConfig.js";
|
|
11
|
+
|
|
12
|
+
const connectionEnvironment = Object.freeze({
|
|
13
|
+
DB_HOST: "127.0.0.1",
|
|
14
|
+
DB_NAME: "catalogue",
|
|
15
|
+
DB_USER: "catalogue_user",
|
|
16
|
+
DB_PASSWORD: "secret"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("createKnexMigrationConfig fixes MySQL without requiring DB_CLIENT", () => {
|
|
20
|
+
const config = createKnexMigrationConfig({
|
|
21
|
+
client: "mysql2",
|
|
22
|
+
environment: connectionEnvironment,
|
|
23
|
+
appRoot: "/srv/catalogue"
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
assert.equal(config.client, "mysql2");
|
|
27
|
+
assert.equal(config.connection.port, 3306);
|
|
28
|
+
assert.deepEqual(config.migrations.directory, [
|
|
29
|
+
"/srv/catalogue/migrations",
|
|
30
|
+
"/srv/catalogue/migrations/constraints"
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("createKnexMigrationConfig fixes PostgreSQL without requiring DB_CLIENT", () => {
|
|
35
|
+
const config = createKnexMigrationConfig({
|
|
36
|
+
client: "pg",
|
|
37
|
+
environment: connectionEnvironment,
|
|
38
|
+
appRoot: "/srv/catalogue"
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
assert.equal(config.client, "pg");
|
|
42
|
+
assert.equal(config.connection.port, 5432);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("installed package migrations run from their owning packages without copied projections", async (context) => {
|
|
46
|
+
const appRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-package-migrations-"));
|
|
47
|
+
context.after(async () => {
|
|
48
|
+
await import("node:fs/promises").then(({ rm }) => rm(appRoot, { recursive: true, force: true }));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const packageRoot = path.join(appRoot, "node_modules", "@example", "accounts-core");
|
|
52
|
+
await mkdir(path.join(packageRoot, "migrations"), { recursive: true });
|
|
53
|
+
await writeFile(path.join(appRoot, "package.json"), JSON.stringify({
|
|
54
|
+
dependencies: { "@example/accounts-core": "1.0.0" }
|
|
55
|
+
}));
|
|
56
|
+
await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({
|
|
57
|
+
name: "@example/accounts-core",
|
|
58
|
+
version: "1.0.0",
|
|
59
|
+
jskit: {
|
|
60
|
+
kind: "runtime",
|
|
61
|
+
runtime: { server: { providers: [] }, client: { providers: [] } },
|
|
62
|
+
migrations: { directories: ["migrations"] }
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
await writeFile(path.join(packageRoot, "migrations", "accounts_initial.cjs"), "exports.up = async () => {};\n");
|
|
66
|
+
|
|
67
|
+
const directories = await discoverPackageMigrationDirectories({ appRoot });
|
|
68
|
+
assert.deepEqual(directories, [path.join(packageRoot, "migrations")]);
|
|
69
|
+
|
|
70
|
+
const config = await createKnexMigrationConfigFromApp({
|
|
71
|
+
client: "mysql2",
|
|
72
|
+
environment: connectionEnvironment,
|
|
73
|
+
appRoot
|
|
74
|
+
});
|
|
75
|
+
assert.deepEqual(config.migrations.directory, [
|
|
76
|
+
path.join(appRoot, "migrations"),
|
|
77
|
+
path.join(packageRoot, "migrations"),
|
|
78
|
+
path.join(appRoot, "migrations", "constraints")
|
|
79
|
+
]);
|
|
80
|
+
});
|