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.
@@ -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
  }
@@ -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
- init(table){
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
- try{
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
- return (async () => {
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
- return (async () => {
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.42",
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.41 (2026-02-20) - FIX: add-migration No Longer Creates Unnecessary Database Connections
3372
+ ### Version 0.3.44 (2026-02-20) - FIX: MySQL/PostgreSQL Auto-Create Database During Migrations
3373
3373
 
3374
- #### Bug Fixed: `add-migration` creating real database connections and files
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
- - **Problem**: On production servers, `add-migration` created unwanted `.sqlite3` files and printed misleading "SQLite database closed" messages
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/cli.js`** - Set `MASTERRECORD_SCHEMA_ONLY=1` before context construction, removed unnecessary `close()` calls
3383
- 2. **`context.js`** - Added schema-only mode check in `env()` that returns early after setting type flags
3384
- 3. **`package.json`** - Updated to v0.3.41
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