masterrecord 0.3.43 → 0.3.45

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.
@@ -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
- await 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
+ }
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
- try{
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 () => {
@@ -718,6 +721,8 @@ class context {
718
721
  // Note: engine is already set in __mysqlInit
719
722
  return this;
720
723
  })();
724
+ // Prevent unhandled rejection crash — _ensureReady() will handle errors
725
+ this._initPromise.catch(() => {});
721
726
  return this._initPromise;
722
727
  }
723
728
 
@@ -727,6 +732,9 @@ class context {
727
732
  this.isMySQL = false;
728
733
  this.isSQLite = false;
729
734
 
735
+ // Store config so migration schema can create the database if it doesn't exist
736
+ this._dbConfig = options;
737
+
730
738
  // PostgreSQL is async - caller must await env()
731
739
  // Store promise so migration schema can await it
732
740
  this._initPromise = (async () => {
@@ -734,6 +742,8 @@ class context {
734
742
  // Note: engine is already set in __postgresInit
735
743
  return this;
736
744
  })();
745
+ // Prevent unhandled rejection crash — _ensureReady() will handle errors
746
+ this._initPromise.catch(() => {});
737
747
  return this._initPromise;
738
748
  }
739
749
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.43",
3
+ "version": "0.3.45",
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"