masterrecord 0.3.42 → 0.3.44
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/Migrations/migrationTemplate.js +2 -2
- package/Migrations/schema.js +148 -36
- package/context.js +12 -2
- package/package.json +1 -1
- package/readme.md +27 -9
|
@@ -24,12 +24,12 @@ class ${this.name} extends masterrecord.schema {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async up(table){
|
|
27
|
-
this.init(table);
|
|
27
|
+
await this.init(table);
|
|
28
28
|
${this.#up}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async down(table){
|
|
32
|
-
this.init(table);
|
|
32
|
+
await this.init(table);
|
|
33
33
|
${this.#down}
|
|
34
34
|
}
|
|
35
35
|
}
|
package/Migrations/schema.js
CHANGED
|
@@ -6,15 +6,153 @@ class schema{
|
|
|
6
6
|
this._dbEnsured = false;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Wait for async database initialization (MySQL/PostgreSQL) to complete.
|
|
11
|
+
* The context constructor fires off an async pool init that may not have
|
|
12
|
+
* finished by the time migration methods run. This must be awaited before
|
|
13
|
+
* accessing this.context._SQLEngine or this.context.db.
|
|
14
|
+
*
|
|
15
|
+
* For MySQL: if the init fails because the database doesn't exist yet,
|
|
16
|
+
* create the database first, then retry the connection.
|
|
17
|
+
*/
|
|
18
|
+
async _ensureReady(){
|
|
19
|
+
if(this.context && this.context._initPromise){
|
|
20
|
+
try{
|
|
21
|
+
await this.context._initPromise;
|
|
22
|
+
}catch(err){
|
|
23
|
+
const msg = err && (err.message || (err.context && err.context.originalError) || '');
|
|
24
|
+
const msgStr = typeof msg === 'string' ? msg : '';
|
|
25
|
+
// MySQL: "Unknown database 'X'"
|
|
26
|
+
if(this.context.isMySQL && msgStr.includes('Unknown database')){
|
|
27
|
+
await this._createDatabaseFromConfig();
|
|
28
|
+
await this._retryMySQLInit();
|
|
29
|
+
// PostgreSQL: 'database "X" does not exist'
|
|
30
|
+
}else if(this.context.isPostgres && msgStr.includes('does not exist')){
|
|
31
|
+
await this._createPostgresDatabaseFromConfig();
|
|
32
|
+
await this._retryPostgresInit();
|
|
33
|
+
}else{
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create MySQL database using stored config (no existing connection needed).
|
|
42
|
+
* Used when the initial connection fails because the database doesn't exist.
|
|
43
|
+
*/
|
|
44
|
+
async _createDatabaseFromConfig(){
|
|
45
|
+
try{
|
|
46
|
+
const config = this.context._dbConfig;
|
|
47
|
+
if(!config || !config.database){ return; }
|
|
48
|
+
const dbName = config.database;
|
|
49
|
+
// Validate database name
|
|
50
|
+
if(!/^[a-zA-Z0-9_-]+$/.test(dbName)){
|
|
51
|
+
throw new Error(`Invalid database name: ${dbName}. Only alphanumeric characters, underscores, and hyphens are allowed.`);
|
|
52
|
+
}
|
|
53
|
+
const MySQLAsyncClient = require('masterrecord/mySQLConnect');
|
|
54
|
+
// Connect without specifying database
|
|
55
|
+
const adminConfig = { ...config };
|
|
56
|
+
delete adminConfig.database;
|
|
57
|
+
delete adminConfig.type;
|
|
58
|
+
const admin = new MySQLAsyncClient(adminConfig);
|
|
59
|
+
await admin.connect();
|
|
60
|
+
const pool = admin.getPool();
|
|
61
|
+
if(!pool){ return; }
|
|
62
|
+
// Check and create
|
|
63
|
+
const [rows] = await pool.execute(`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, [dbName]);
|
|
64
|
+
const exists = Array.isArray(rows) && rows.length > 0;
|
|
65
|
+
if(!exists){
|
|
66
|
+
await pool.execute(`CREATE DATABASE \`${dbName}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
|
67
|
+
console.log(`[MySQL] Created database '${dbName}'`);
|
|
68
|
+
}
|
|
69
|
+
await admin.close();
|
|
70
|
+
this._dbEnsured = true;
|
|
71
|
+
}catch(err){
|
|
72
|
+
console.error('[MySQL] Failed to create database:', err.message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Retry MySQL init after database creation.
|
|
78
|
+
* Re-runs __mysqlInit and stores the new promise.
|
|
79
|
+
*/
|
|
80
|
+
async _retryMySQLInit(){
|
|
81
|
+
const config = this.context._dbConfig;
|
|
82
|
+
if(!config){ throw new Error('No MySQL config available for retry'); }
|
|
83
|
+
const MySQLEngine = require('masterrecord/mySQLEngine');
|
|
84
|
+
const MySQLAsyncClient = require('masterrecord/mySQLConnect');
|
|
85
|
+
console.log('[MySQL] Retrying connection after database creation...');
|
|
86
|
+
const client = new MySQLAsyncClient(config);
|
|
87
|
+
await client.connect();
|
|
88
|
+
const pool = client.getPool();
|
|
89
|
+
this.context._SQLEngine = new MySQLEngine();
|
|
90
|
+
this.context._SQLEngine.setDB(pool);
|
|
91
|
+
this.context._SQLEngine.__name = 'mysql2';
|
|
92
|
+
this.context.db = client;
|
|
93
|
+
console.log('[MySQL] Connection pool ready');
|
|
94
|
+
}
|
|
9
95
|
|
|
10
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Create PostgreSQL database using stored config (no existing connection needed).
|
|
98
|
+
* Used when the initial connection fails because the database doesn't exist.
|
|
99
|
+
*/
|
|
100
|
+
async _createPostgresDatabaseFromConfig(){
|
|
101
|
+
try{
|
|
102
|
+
const config = this.context._dbConfig;
|
|
103
|
+
if(!config || !config.database){ return; }
|
|
104
|
+
const dbName = config.database;
|
|
105
|
+
// Validate database name
|
|
106
|
+
if(!/^[a-zA-Z0-9_-]+$/.test(dbName)){
|
|
107
|
+
throw new Error(`Invalid database name: ${dbName}. Only alphanumeric characters, underscores, and hyphens are allowed.`);
|
|
108
|
+
}
|
|
109
|
+
const { Pool } = require('pg');
|
|
110
|
+
// Connect to default 'postgres' database to run CREATE DATABASE
|
|
111
|
+
const adminConfig = {
|
|
112
|
+
host: config.host || 'localhost',
|
|
113
|
+
port: config.port || 5432,
|
|
114
|
+
user: config.user,
|
|
115
|
+
password: config.password,
|
|
116
|
+
database: 'postgres',
|
|
117
|
+
ssl: config.ssl || false
|
|
118
|
+
};
|
|
119
|
+
const adminPool = new Pool(adminConfig);
|
|
120
|
+
// Check if database exists
|
|
121
|
+
const result = await adminPool.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [dbName]);
|
|
122
|
+
if(result.rows.length === 0){
|
|
123
|
+
// CREATE DATABASE cannot use parameterized queries, but name is validated above
|
|
124
|
+
await adminPool.query(`CREATE DATABASE "${dbName}" ENCODING 'UTF8'`);
|
|
125
|
+
console.log(`[PostgreSQL] Created database '${dbName}'`);
|
|
126
|
+
}
|
|
127
|
+
await adminPool.end();
|
|
128
|
+
this._dbEnsured = true;
|
|
129
|
+
}catch(err){
|
|
130
|
+
console.error('[PostgreSQL] Failed to create database:', err.message);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Retry PostgreSQL init after database creation.
|
|
136
|
+
*/
|
|
137
|
+
async _retryPostgresInit(){
|
|
138
|
+
const config = this.context._dbConfig;
|
|
139
|
+
if(!config){ throw new Error('No PostgreSQL config available for retry'); }
|
|
140
|
+
const PostgresClient = require('masterrecord/postgresSyncConnect');
|
|
141
|
+
console.log('[PostgreSQL] Retrying connection after database creation...');
|
|
142
|
+
const connection = new PostgresClient();
|
|
143
|
+
await connection.connect(config);
|
|
144
|
+
this.context._SQLEngine = connection.getEngine();
|
|
145
|
+
this.context._SQLEngine.__name = 'pg';
|
|
146
|
+
this.context.db = connection.getPool();
|
|
147
|
+
console.log('[PostgreSQL] Connection pool ready');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async init(table){
|
|
151
|
+
// Wait for async DB init (MySQL/PostgreSQL) before any operations
|
|
152
|
+
await this._ensureReady();
|
|
11
153
|
if(table){
|
|
12
154
|
this.fullTable = table.___table;
|
|
13
155
|
}
|
|
14
|
-
// Ensure backing database exists for MySQL before running any DDL
|
|
15
|
-
if(this.context && this.context.isMySQL && this._dbEnsured !== true){
|
|
16
|
-
try{ this.createDatabase(); }catch(_){ /* best-effort */ }
|
|
17
|
-
}
|
|
18
156
|
}
|
|
19
157
|
|
|
20
158
|
// create obj to convert into create sql
|
|
@@ -83,6 +221,9 @@ class schema{
|
|
|
83
221
|
}
|
|
84
222
|
|
|
85
223
|
async createTable(table){
|
|
224
|
+
// Ensure async DB init is complete (safety net for older migrations
|
|
225
|
+
// that call this.init(table) without await)
|
|
226
|
+
await this._ensureReady();
|
|
86
227
|
|
|
87
228
|
if(table){
|
|
88
229
|
// If table exists, run sync instead of blind create
|
|
@@ -324,38 +465,9 @@ class schema{
|
|
|
324
465
|
}
|
|
325
466
|
|
|
326
467
|
// EnsureCreated equivalent for MySQL: create DB if missing
|
|
468
|
+
// Delegates to _createDatabaseFromConfig which uses stored config
|
|
327
469
|
async createDatabase(){
|
|
328
|
-
|
|
329
|
-
if(!(this.context && this.context.isMySQL)){ return; }
|
|
330
|
-
const MySQLAsyncClient = require('masterrecord/mySQLConnect');
|
|
331
|
-
const client = this.context.db; // main client (may not be connected yet)
|
|
332
|
-
if(!client || !client.config || !client.config.database){ return; }
|
|
333
|
-
const dbName = client.config.database;
|
|
334
|
-
// Build server-level connection (no database)
|
|
335
|
-
const baseConfig = { ...client.config };
|
|
336
|
-
delete baseConfig.database;
|
|
337
|
-
const admin = new MySQLAsyncClient(baseConfig);
|
|
338
|
-
await admin.connect();
|
|
339
|
-
const pool = admin.getPool();
|
|
340
|
-
if(!pool){ return; }
|
|
341
|
-
|
|
342
|
-
// Use parameterized query for checking database existence
|
|
343
|
-
const [rows] = await pool.execute(`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, [dbName]);
|
|
344
|
-
const exists = Array.isArray(rows) && rows.length > 0;
|
|
345
|
-
if(!exists){
|
|
346
|
-
// Validate database name (alphanumeric, underscore, hyphen only)
|
|
347
|
-
if(!/^[a-zA-Z0-9_-]+$/.test(dbName)){
|
|
348
|
-
throw new Error(`Invalid database name: ${dbName}. Only alphanumeric characters, underscores, and hyphens are allowed.`);
|
|
349
|
-
}
|
|
350
|
-
// CREATE DATABASE doesn't support placeholders, but we've validated the name
|
|
351
|
-
await pool.execute(`CREATE DATABASE \`${dbName}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
|
352
|
-
}
|
|
353
|
-
await admin.close();
|
|
354
|
-
this._dbEnsured = true;
|
|
355
|
-
}catch(err){
|
|
356
|
-
// Non-fatal: migrations may still proceed if DB already exists or permissions blocked
|
|
357
|
-
try{ console.error(err); }catch(_){ }
|
|
358
|
-
}
|
|
470
|
+
return this._createDatabaseFromConfig();
|
|
359
471
|
}
|
|
360
472
|
|
|
361
473
|
// Alias for consistency with user expectation
|
package/context.js
CHANGED
|
@@ -711,12 +711,17 @@ class context {
|
|
|
711
711
|
this.isSQLite = false;
|
|
712
712
|
this.isPostgres = false;
|
|
713
713
|
|
|
714
|
+
// Store config so migration schema can create the database if it doesn't exist
|
|
715
|
+
this._dbConfig = options;
|
|
716
|
+
|
|
714
717
|
// MySQL is async - caller must await env()
|
|
715
|
-
|
|
718
|
+
// Store promise so migration schema can await it
|
|
719
|
+
this._initPromise = (async () => {
|
|
716
720
|
this.db = await this.__mysqlInit(options, 'mysql2');
|
|
717
721
|
// Note: engine is already set in __mysqlInit
|
|
718
722
|
return this;
|
|
719
723
|
})();
|
|
724
|
+
return this._initPromise;
|
|
720
725
|
}
|
|
721
726
|
|
|
722
727
|
// PostgreSQL initialization (async)
|
|
@@ -725,12 +730,17 @@ class context {
|
|
|
725
730
|
this.isMySQL = false;
|
|
726
731
|
this.isSQLite = false;
|
|
727
732
|
|
|
733
|
+
// Store config so migration schema can create the database if it doesn't exist
|
|
734
|
+
this._dbConfig = options;
|
|
735
|
+
|
|
728
736
|
// PostgreSQL is async - caller must await env()
|
|
729
|
-
|
|
737
|
+
// Store promise so migration schema can await it
|
|
738
|
+
this._initPromise = (async () => {
|
|
730
739
|
this.db = await this.__postgresInit(options, 'pg');
|
|
731
740
|
// Note: engine is already set in __postgresInit
|
|
732
741
|
return this;
|
|
733
742
|
})();
|
|
743
|
+
return this._initPromise;
|
|
734
744
|
}
|
|
735
745
|
|
|
736
746
|
throw new ConfigurationError(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.44",
|
|
4
4
|
"description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
|
|
5
5
|
"main": "MasterRecord.js",
|
|
6
6
|
"bin": {
|
package/readme.md
CHANGED
|
@@ -3369,19 +3369,37 @@ user.name = null; // Error if name is { nullable: false }
|
|
|
3369
3369
|
|
|
3370
3370
|
## Changelog
|
|
3371
3371
|
|
|
3372
|
-
### Version 0.3.
|
|
3372
|
+
### Version 0.3.44 (2026-02-20) - FIX: MySQL/PostgreSQL Auto-Create Database During Migrations
|
|
3373
3373
|
|
|
3374
|
-
#### Bug Fixed: `
|
|
3374
|
+
#### Bug Fixed: `update-database` failing when database doesn't exist yet (MySQL & PostgreSQL)
|
|
3375
|
+
- **FIXED**: Running `masterrecord update-database` on a fresh MySQL or PostgreSQL server would fail because `__mysqlInit`/`__postgresInit` tried to connect to the database before it was created
|
|
3376
|
+
- **Root Cause**: The `createDatabase()` method depended on `this.context.db` (the connected client) to get config, but the client connection failed because the database didn't exist — a chicken-and-egg problem
|
|
3377
|
+
- **Solution**: Store config as `this._dbConfig` on the context before async init. `_ensureReady()` now catches database-not-found errors (MySQL: "Unknown database", PostgreSQL: "does not exist"), creates the database using a server-level connection, then retries the full init
|
|
3378
|
+
- **Impact**: `update-database` now automatically creates the database if it doesn't exist for all three engines (SQLite, MySQL, PostgreSQL)
|
|
3379
|
+
|
|
3380
|
+
#### Files Modified
|
|
3381
|
+
1. **`context.js`** - Store `_dbConfig` before async MySQL and PostgreSQL init
|
|
3382
|
+
2. **`Migrations/schema.js`** - Added `_createDatabaseFromConfig()`, `_retryMySQLInit()`, `_createPostgresDatabaseFromConfig()`, `_retryPostgresInit()`, updated `_ensureReady()` to handle missing database for both engines
|
|
3383
|
+
|
|
3384
|
+
### Version 0.3.42 (2026-02-20) - FIX: Migration System + add-migration Improvements
|
|
3385
|
+
|
|
3386
|
+
#### Bug Fixed: `update-database` failing for MySQL/PostgreSQL with "Cannot read properties of null"
|
|
3387
|
+
- **FIXED**: Running `masterrecord update-database` with MySQL or PostgreSQL would crash with `Cannot read properties of null (reading 'tableExists')`
|
|
3388
|
+
- **Root Cause**: The migration `schema.js` constructor creates a new context instance, but MySQL/PostgreSQL database initialization is async. The `_SQLEngine` property was still `null` when `createTable` tried to use it because the async pool init hadn't completed yet.
|
|
3389
|
+
- **Solution**: Store the async init promise as `_initPromise` on the context instance. Added `_ensureReady()` method to `schema.js` that awaits it. Called in both `init()` and `createTable()` (safety net for existing migrations).
|
|
3390
|
+
- **Also fixed**: Typo in `schema.js` — `require('masterrecord/mySQLAsyncConnect')` corrected to `require('masterrecord/mySQLConnect')`
|
|
3391
|
+
- **Also fixed**: `init()` now properly `await`s `createDatabase()` instead of fire-and-forget
|
|
3392
|
+
|
|
3393
|
+
#### Bug Fixed: `add-migration` creating unnecessary database connections
|
|
3375
3394
|
- **FIXED**: Running `masterrecord add-migration` would open a real database connection (creating SQLite files on disk, or connecting to MySQL/PostgreSQL) even though it only needs entity schemas and seed data to generate migration files
|
|
3376
|
-
- **
|
|
3377
|
-
- **Root Cause**: The context constructor calls `env()` which initializes a full database connection; `add-migration` only needs entity metadata
|
|
3378
|
-
- **Solution**: Added schema-only mode (`MASTERRECORD_SCHEMA_ONLY` env var) that sets database type flags for correct entity registration but skips all database initialization (no file creation, no connections)
|
|
3379
|
-
- **Impact**: `add-migration` is now faster, creates no side effects, and works safely on production servers
|
|
3395
|
+
- **Solution**: Added schema-only mode (`MASTERRECORD_SCHEMA_ONLY` env var) that sets database type flags but skips all DB initialization
|
|
3380
3396
|
|
|
3381
3397
|
#### Files Modified
|
|
3382
|
-
1. **`Migrations/
|
|
3383
|
-
2. **`
|
|
3384
|
-
3. **`
|
|
3398
|
+
1. **`Migrations/schema.js`** - Added `_ensureReady()`, made `init()` async with proper awaits, fixed `mySQLConnect` require
|
|
3399
|
+
2. **`Migrations/migrationTemplate.js`** - New migrations now `await this.init(table)`
|
|
3400
|
+
3. **`Migrations/cli.js`** - Schema-only mode for `add-migration`
|
|
3401
|
+
4. **`context.js`** - Store `_initPromise` for MySQL/PostgreSQL async init, schema-only mode in `env()`
|
|
3402
|
+
5. **`package.json`** - Updated to v0.3.42
|
|
3385
3403
|
|
|
3386
3404
|
### Version 0.3.39 (2026-02-09) - CRITICAL BUG FIX: Foreign Key String Values
|
|
3387
3405
|
|