masterrecord 0.3.43 → 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/schema.js +132 -36
- package/context.js +6 -0
- package/package.json +1 -1
- package/readme.md +12 -0
package/Migrations/schema.js
CHANGED
|
@@ -11,23 +11,148 @@ class schema{
|
|
|
11
11
|
* The context constructor fires off an async pool init that may not have
|
|
12
12
|
* finished by the time migration methods run. This must be awaited before
|
|
13
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.
|
|
14
17
|
*/
|
|
15
18
|
async _ensureReady(){
|
|
16
19
|
if(this.context && this.context._initPromise){
|
|
17
|
-
|
|
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
|
+
}
|
|
95
|
+
|
|
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);
|
|
18
131
|
}
|
|
19
132
|
}
|
|
20
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
|
+
|
|
21
150
|
async init(table){
|
|
22
151
|
// Wait for async DB init (MySQL/PostgreSQL) before any operations
|
|
23
152
|
await this._ensureReady();
|
|
24
153
|
if(table){
|
|
25
154
|
this.fullTable = table.___table;
|
|
26
155
|
}
|
|
27
|
-
// Ensure backing database exists for MySQL before running any DDL
|
|
28
|
-
if(this.context && this.context.isMySQL && this._dbEnsured !== true){
|
|
29
|
-
try{ await this.createDatabase(); }catch(_){ /* best-effort */ }
|
|
30
|
-
}
|
|
31
156
|
}
|
|
32
157
|
|
|
33
158
|
// create obj to convert into create sql
|
|
@@ -340,38 +465,9 @@ class schema{
|
|
|
340
465
|
}
|
|
341
466
|
|
|
342
467
|
// EnsureCreated equivalent for MySQL: create DB if missing
|
|
468
|
+
// Delegates to _createDatabaseFromConfig which uses stored config
|
|
343
469
|
async createDatabase(){
|
|
344
|
-
|
|
345
|
-
if(!(this.context && this.context.isMySQL)){ return; }
|
|
346
|
-
const MySQLAsyncClient = require('masterrecord/mySQLConnect');
|
|
347
|
-
const client = this.context.db; // main client (may not be connected yet)
|
|
348
|
-
if(!client || !client.config || !client.config.database){ return; }
|
|
349
|
-
const dbName = client.config.database;
|
|
350
|
-
// Build server-level connection (no database)
|
|
351
|
-
const baseConfig = { ...client.config };
|
|
352
|
-
delete baseConfig.database;
|
|
353
|
-
const admin = new MySQLAsyncClient(baseConfig);
|
|
354
|
-
await admin.connect();
|
|
355
|
-
const pool = admin.getPool();
|
|
356
|
-
if(!pool){ return; }
|
|
357
|
-
|
|
358
|
-
// Use parameterized query for checking database existence
|
|
359
|
-
const [rows] = await pool.execute(`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, [dbName]);
|
|
360
|
-
const exists = Array.isArray(rows) && rows.length > 0;
|
|
361
|
-
if(!exists){
|
|
362
|
-
// Validate database name (alphanumeric, underscore, hyphen only)
|
|
363
|
-
if(!/^[a-zA-Z0-9_-]+$/.test(dbName)){
|
|
364
|
-
throw new Error(`Invalid database name: ${dbName}. Only alphanumeric characters, underscores, and hyphens are allowed.`);
|
|
365
|
-
}
|
|
366
|
-
// CREATE DATABASE doesn't support placeholders, but we've validated the name
|
|
367
|
-
await pool.execute(`CREATE DATABASE \`${dbName}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
|
368
|
-
}
|
|
369
|
-
await admin.close();
|
|
370
|
-
this._dbEnsured = true;
|
|
371
|
-
}catch(err){
|
|
372
|
-
// Non-fatal: migrations may still proceed if DB already exists or permissions blocked
|
|
373
|
-
try{ console.error(err); }catch(_){ }
|
|
374
|
-
}
|
|
470
|
+
return this._createDatabaseFromConfig();
|
|
375
471
|
}
|
|
376
472
|
|
|
377
473
|
// Alias for consistency with user expectation
|
package/context.js
CHANGED
|
@@ -711,6 +711,9 @@ 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
|
|
716
719
|
this._initPromise = (async () => {
|
|
@@ -727,6 +730,9 @@ class context {
|
|
|
727
730
|
this.isMySQL = false;
|
|
728
731
|
this.isSQLite = false;
|
|
729
732
|
|
|
733
|
+
// Store config so migration schema can create the database if it doesn't exist
|
|
734
|
+
this._dbConfig = options;
|
|
735
|
+
|
|
730
736
|
// PostgreSQL is async - caller must await env()
|
|
731
737
|
// Store promise so migration schema can await it
|
|
732
738
|
this._initPromise = (async () => {
|
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,6 +3369,18 @@ user.name = null; // Error if name is { nullable: false }
|
|
|
3369
3369
|
|
|
3370
3370
|
## Changelog
|
|
3371
3371
|
|
|
3372
|
+
### Version 0.3.44 (2026-02-20) - FIX: MySQL/PostgreSQL Auto-Create Database During Migrations
|
|
3373
|
+
|
|
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
|
+
|
|
3372
3384
|
### Version 0.3.42 (2026-02-20) - FIX: Migration System + add-migration Improvements
|
|
3373
3385
|
|
|
3374
3386
|
#### Bug Fixed: `update-database` failing for MySQL/PostgreSQL with "Cannot read properties of null"
|