masterrecord 0.2.34 โ†’ 0.3.0

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.
Files changed (40) hide show
  1. package/.claude/settings.local.json +25 -1
  2. package/Entity/entityModel.js +6 -0
  3. package/Entity/entityTrackerModel.js +20 -3
  4. package/Entity/fieldTransformer.js +266 -0
  5. package/Migrations/migrationMySQLQuery.js +145 -1
  6. package/Migrations/migrationPostgresQuery.js +402 -0
  7. package/Migrations/migrationSQLiteQuery.js +145 -1
  8. package/Migrations/schema.js +131 -28
  9. package/QueryLanguage/queryMethods.js +193 -15
  10. package/QueryLanguage/queryParameters.js +136 -0
  11. package/QueryLanguage/queryScript.js +14 -5
  12. package/SQLLiteEngine.js +309 -19
  13. package/context.js +57 -12
  14. package/docs/INCLUDES_CLARIFICATION.md +202 -0
  15. package/docs/METHODS_REFERENCE.md +184 -0
  16. package/docs/MIGRATIONS_GUIDE.md +699 -0
  17. package/docs/POSTGRESQL_SETUP.md +415 -0
  18. package/examples/jsonArrayTransformer.js +215 -0
  19. package/mySQLEngine.js +249 -17
  20. package/package.json +6 -6
  21. package/postgresEngine.js +434 -491
  22. package/postgresSyncConnect.js +209 -0
  23. package/readme.md +1121 -265
  24. package/test/anyCommaStringTest.js +237 -0
  25. package/test/anyMethodTest.js +176 -0
  26. package/test/findByIdTest.js +227 -0
  27. package/test/includesFeatureTest.js +183 -0
  28. package/test/includesTransformTest.js +110 -0
  29. package/test/newMethodTest.js +330 -0
  30. package/test/newMethodUnitTest.js +320 -0
  31. package/test/parameterizedPlaceholderTest.js +159 -0
  32. package/test/postgresEngineTest.js +463 -0
  33. package/test/postgresIntegrationTest.js +381 -0
  34. package/test/securityTest.js +268 -0
  35. package/test/singleDollarPlaceholderTest.js +238 -0
  36. package/test/tablePrefixTest.js +100 -0
  37. package/test/transformerTest.js +287 -0
  38. package/test/verifyFindById.js +169 -0
  39. package/test/verifyNewMethod.js +191 -0
  40. package/test/whereChainingTest.js +88 -0
package/readme.md CHANGED
@@ -1,374 +1,1230 @@
1
+ # MasterRecord
1
2
 
3
+ [![npm version](https://img.shields.io/npm/v/masterrecord.svg)](https://www.npmjs.com/package/masterrecord)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
5
 
3
- # MasterRecord
6
+ **MasterRecord** is a lightweight, code-first ORM for Node.js with a fluent query API, comprehensive migrations, and multi-database support. Build type-safe queries with lambda expressions, manage schema changes with CLI-driven migrations, and work seamlessly across MySQL, PostgreSQL, and SQLite.
7
+
8
+ ## Key Features
9
+
10
+ ๐Ÿ”น **Multi-Database Support** - MySQL, PostgreSQL, SQLite with consistent API
11
+ ๐Ÿ”น **Code-First Design** - Define entities in JavaScript, generate schema automatically
12
+ ๐Ÿ”น **Fluent Query API** - Lambda-based queries with parameterized placeholders
13
+ ๐Ÿ”น **Migration System** - CLI-driven migrations with rollback support
14
+ ๐Ÿ”น **SQL Injection Protection** - Automatic parameterized queries throughout
15
+ ๐Ÿ”น **Field Transformers** - Custom serialization/deserialization for complex types
16
+ ๐Ÿ”น **Type Validation** - Runtime type checking and coercion
17
+ ๐Ÿ”น **Relationship Mapping** - One-to-many, many-to-one, many-to-many support
18
+ ๐Ÿ”น **Seed Data** - Built-in seeding with idempotent operations
4
19
 
5
- MasterRecord is a lightweight, code-first ORM and migration tool for Node.js with a fluent query API. It lets you define entities in JavaScript, generate migrations, and query with expressive syntax.
20
+ ## Database Support
6
21
 
7
- - Supported databases: MySQL, SQLite
8
- - Synchronous API by default (no await needed)
9
- - Built-in CLI for migrations and seeding
22
+ | Database | Version | Features |
23
+ |------------|--------------|---------------------------------------------------|
24
+ | PostgreSQL | 9.6+ (12+) | JSONB, UUID, async/await, connection pooling |
25
+ | MySQL | 5.7+ (8.0+) | JSON, transactions, AUTO_INCREMENT |
26
+ | SQLite | 3.x | Embedded, zero-config, file-based |
27
+
28
+ ## Table of Contents
29
+
30
+ - [Installation](#installation)
31
+ - [Quick Start](#quick-start)
32
+ - [Database Configuration](#database-configuration)
33
+ - [Entity Definitions](#entity-definitions)
34
+ - [Querying](#querying)
35
+ - [Migrations](#migrations)
36
+ - [Advanced Features](#advanced-features)
37
+ - [API Reference](#api-reference)
38
+ - [Examples](#examples)
10
39
 
11
40
  ## Installation
12
41
 
13
42
  ```bash
14
- # npm
43
+ # Global installation (recommended for CLI)
15
44
  npm install -g masterrecord
16
45
 
17
- # pnpm
18
- pnpm add -g masterrecord
46
+ # Local installation
47
+ npm install masterrecord
19
48
 
20
- # yarn
21
- yarn global add masterrecord
49
+ # With specific database drivers
50
+ npm install masterrecord pg # PostgreSQL
51
+ npm install masterrecord mysql2 # MySQL
52
+ npm install masterrecord better-sqlite3 # SQLite
22
53
  ```
23
54
 
55
+ ### Dependencies
56
+
57
+ MasterRecord includes the following database drivers by default:
58
+ - `pg@^8.16.3` - PostgreSQL
59
+ - `sync-mysql2@^1.0.8` - MySQL
60
+ - `better-sqlite3@^12.6.0` - SQLite
61
+
24
62
  ## Quick Start
25
63
 
26
- 1) Create an environment config file (see Environment below), then enable migrations:
64
+ ### 1. Create a Context
65
+
66
+ ```javascript
67
+ // app/models/context.js
68
+ const context = require('masterrecord/context');
69
+ const User = require('./User');
70
+ const Post = require('./Post');
71
+
72
+ class AppContext extends context {
73
+ constructor() {
74
+ super();
75
+
76
+ // Configure database connection
77
+ this.env({
78
+ type: 'postgres', // or 'mysql', 'sqlite'
79
+ host: 'localhost',
80
+ port: 5432,
81
+ database: 'myapp',
82
+ user: 'postgres',
83
+ password: 'password'
84
+ });
85
+
86
+ // Register entities
87
+ this.dbset(User);
88
+ this.dbset(Post);
89
+ }
90
+ }
91
+
92
+ module.exports = AppContext;
93
+ ```
94
+
95
+ ### 2. Define Entities
96
+
97
+ ```javascript
98
+ // app/models/User.js
99
+ class User {
100
+ constructor() {
101
+ this.id = { type: 'integer', primary: true, auto: true };
102
+ this.name = { type: 'string', nullable: false };
103
+ this.email = { type: 'string', nullable: false, unique: true };
104
+ this.age = { type: 'integer', nullable: true };
105
+ this.created_at = { type: 'timestamp', default: 'CURRENT_TIMESTAMP' };
106
+ }
107
+ }
108
+
109
+ module.exports = User;
110
+ ```
111
+
112
+ ### 3. Run Migrations
27
113
 
28
114
  ```bash
115
+ # Enable migrations (one-time setup)
29
116
  masterrecord enable-migrations AppContext
117
+
118
+ # Create initial migration
119
+ masterrecord add-migration InitialCreate AppContext
120
+
121
+ # Apply migrations
122
+ masterrecord migrate AppContext
30
123
  ```
31
124
 
32
- 2) Make or change your entities, then create a migration file:
125
+ ### 4. Query Your Data
33
126
 
34
- ```bash
35
- masterrecord add-migration Init AppContext
127
+ ```javascript
128
+ const AppContext = require('./app/models/context');
129
+ const db = new AppContext();
130
+
131
+ // Create
132
+ const user = db.User.new();
133
+ user.name = 'Alice';
134
+ user.email = 'alice@example.com';
135
+ user.age = 28;
136
+ await db.saveChanges();
137
+
138
+ // Read with parameterized query
139
+ const alice = db.User
140
+ .where(u => u.email == $$, 'alice@example.com')
141
+ .single();
142
+
143
+ // Update
144
+ alice.age = 29;
145
+ await db.saveChanges();
146
+
147
+ // Delete
148
+ db.remove(alice);
149
+ await db.saveChanges();
36
150
  ```
37
151
 
38
- 3) Apply the migration to your database:
152
+ ## Database Configuration
39
153
 
40
- ```bash
41
- master=development masterrecord update-database AppContext
154
+ ### PostgreSQL (Async)
155
+
156
+ ```javascript
157
+ class AppContext extends context {
158
+ constructor() {
159
+ super();
160
+
161
+ this.env({
162
+ type: 'postgres',
163
+ host: 'localhost',
164
+ port: 5432,
165
+ database: 'myapp',
166
+ user: 'postgres',
167
+ password: 'password',
168
+ max: 20, // Connection pool size
169
+ idleTimeoutMillis: 30000,
170
+ connectionTimeoutMillis: 2000
171
+ });
172
+
173
+ this.dbset(User);
174
+ }
175
+ }
176
+
177
+ // Usage requires await
178
+ const db = new AppContext();
179
+ await db.saveChanges(); // PostgreSQL is async
42
180
  ```
43
181
 
44
- ### Enable migrations (one-time per Context)
182
+ ### MySQL (Synchronous)
45
183
 
46
- - Run from the project root where your Context file lives. Use the Context file name (without extension) as the argument.
47
- ```bash
48
- master=development masterrecord enable-migrations AppContext
184
+ ```javascript
185
+ class AppContext extends context {
186
+ constructor() {
187
+ super();
188
+
189
+ this.env({
190
+ type: 'mysql',
191
+ host: 'localhost',
192
+ port: 3306,
193
+ database: 'myapp',
194
+ user: 'root',
195
+ password: 'password'
196
+ });
197
+
198
+ this.dbset(User);
199
+ }
200
+ }
201
+
202
+ // Usage is synchronous
203
+ const db = new AppContext();
204
+ db.saveChanges(); // No await needed
49
205
  ```
50
- This creates `db/migrations/<context>_contextSnapShot.json` and the `db/migrations` directory.
51
206
 
52
- ### Create a migration
207
+ ### SQLite (Synchronous)
53
208
 
54
- - After you change your entity models, generate a migration file:
55
- ```bash
56
- master=development masterrecord add-migration <MigrationName> AppContext
209
+ ```javascript
210
+ class AppContext extends context {
211
+ constructor() {
212
+ super();
213
+
214
+ this.env({
215
+ type: 'sqlite',
216
+ connection: './data/myapp.db' // File path
217
+ });
218
+
219
+ this.dbset(User);
220
+ }
221
+ }
57
222
  ```
58
- This writes a new file to `db/migrations/<timestamp>_<MigrationName>_migration.js`.
59
223
 
60
- ### Apply migrations to the database
224
+ ### Environment Files
61
225
 
62
- - Apply only the latest pending migration:
63
- ```bash
64
- master=development masterrecord update-database AppContext
226
+ Store configurations in JSON files:
227
+
228
+ ```json
229
+ // config/environments/env.development.json
230
+ {
231
+ "type": "postgres",
232
+ "host": "localhost",
233
+ "port": 5432,
234
+ "database": "myapp_dev",
235
+ "user": "postgres",
236
+ "password": "dev_password"
237
+ }
65
238
  ```
66
- - Apply all migrations from the beginning (useful for a clean DB):
67
- ```bash
68
- master=development masterrecord update-database-restart AppContext
239
+
240
+ ```javascript
241
+ // Load environment file
242
+ class AppContext extends context {
243
+ constructor() {
244
+ super();
245
+ this.env('config/environments'); // Loads env.<NODE_ENV>.json
246
+ this.dbset(User);
247
+ }
248
+ }
69
249
  ```
70
- - List migration files (debug/inspection):
250
+
71
251
  ```bash
72
- master=development masterrecord get-migrations AppContext
252
+ # Set environment
253
+ export NODE_ENV=development
254
+ node app.js
73
255
  ```
74
256
 
75
- Notes:
76
- - The CLI searches for `<context>_contextSnapShot.json` under `db/migrations` relative to your current working directory.
77
- - For MySQL, ensure your credentials allow DDL. For SQLite, the data directory is created if missing.
257
+ ## Entity Definitions
78
258
 
79
- ### Updating the running server
259
+ ### Basic Entity
80
260
 
81
- General flow to roll out schema changes:
82
- - Stop the server or put it into maintenance mode (optional but recommended for non-backward-compatible changes).
83
- - Pull the latest code (containing updated models and generated migration files).
84
- - Run migrations against the target environment:
85
- ```bash
86
- master=production masterrecord update-database AppContext
87
- ```
88
- - Restart your server/process manager (e.g., `pm2 restart <app>`, `docker compose up -d`, or your platformโ€™s restart command).
89
-
90
- Backward-compatible rollout tip:
91
- - If possible, deploy additive changes first (new tables/columns), release app code that begins using them, then later clean up/removal migrations.
92
-
93
- ### Troubleshooting
94
-
95
- - Cannot find Context file: ensure you run commands from the app root and pass the correct Context file name used when defining your class (case-insensitive in the snapshot, but supply the same name you used).
96
- - Cannot connect to DB: confirm `master=<env>` is set and `env.<env>.json` exists with correct credentials and paths.
97
- - MySQL type mismatches: the migration engine maps MasterRecord types to SQL types; verify your entity field `type` values are correct.
98
-
99
- ### Recent improvements (2025-10)
100
-
101
- - **Type validation and coercion (Entity Framework-style)**:
102
- - INSERT and UPDATE operations now validate field types against entity definitions.
103
- - Auto-converts compatible types with warnings (e.g., string "4" โ†’ integer 4).
104
- - Throws clear errors for incompatible types with detailed context.
105
- - Prevents silent failures where fields were skipped due to type mismatches.
106
- - See [Type Validation](#type-validation) section below for details.
107
- - Query language and SQL engines:
108
- - Correct parsing of multi-char operators (>=, <=, ===, !==) and spaced logical operators.
109
- - Support for grouped OR conditions rendered as parenthesized OR in WHERE across SQLite/MySQL.
110
- - Resilient fallback for partially parsed expressions.
111
- - Relationships:
112
- - `hasManyThrough` supported in insert and delete cascades.
113
- - Environment file discovery:
114
- - Context now walks up directories to find `config/environments/env.<env>.json`; fixed error throwing.
115
- - Migrations (DDL generation):
116
- - Default values emitted for SQLite/MySQL (including boolean coercion).
117
- - `CREATE TABLE IF NOT EXISTS` to avoid failures when rerunning.
118
- - Table introspection added; existing tables are synced: missing columns are added, MySQL applies `ALTER ... MODIFY` for NULL/DEFAULT changes, SQLite rebuilds table when necessary.
119
- - Migration API additions in `schema.js`:
120
- - `renameColumn(table)` implemented for SQLite/MySQL.
121
- - `seed(tableName, rows)` implemented for bulk/single inserts with safe quoting.
122
-
123
- ### Using renameColumn and seed in migrations
124
-
125
- Basic migration skeleton (generated by CLI):
126
- ```js
127
- var masterrecord = require('masterrecord');
128
-
129
- class AddSettings extends masterrecord.schema {
130
- constructor(context){ super(context); }
131
-
132
- up(table){
133
- this.init(table);
134
- // Add a new table
135
- this.createTable(table.MailSettings);
136
-
137
- // Rename a column on an existing table
138
- this.renameColumn({ tableName: 'MailSettings', name: 'from_email', newName: 'reply_to' });
139
-
140
- // Seed initial data (single row)
141
- this.seed('MailSettings', {
142
- from_name: 'System',
143
- reply_to: 'no-reply@example.com',
144
- return_path_matches_from: 0,
145
- weekly_summary_enabled: 0,
146
- created_at: Date.now(),
147
- updated_at: Date.now()
148
- });
149
-
150
- // Seed multiple rows
151
- this.seed('MailSettings', [
152
- { from_name: 'Support', reply_to: 'support@example.com', created_at: Date.now(), updated_at: Date.now() },
153
- { from_name: 'Marketing', reply_to: 'marketing@example.com', created_at: Date.now(), updated_at: Date.now() }
154
- ]);
155
- }
156
-
157
- down(table){
158
- this.init(table);
159
- // Revert the rename
160
- this.renameColumn({ tableName: 'MailSettings', name: 'reply_to', newName: 'from_email' });
161
-
162
- // Optionally clean up seeded rows
163
- // this.context._execute("DELETE FROM MailSettings WHERE reply_to IN ('no-reply@example.com','support@example.com','marketing@example.com')");
164
-
165
- // Drop table if that was part of up
166
- // this.dropTable(table.MailSettings);
167
- }
261
+ ```javascript
262
+ class User {
263
+ constructor() {
264
+ // Primary key with auto-increment
265
+ this.id = {
266
+ type: 'integer',
267
+ primary: true,
268
+ auto: true
269
+ };
270
+
271
+ // Required string field
272
+ this.name = {
273
+ type: 'string',
274
+ nullable: false
275
+ };
276
+
277
+ // Optional field with default
278
+ this.status = {
279
+ type: 'string',
280
+ nullable: true,
281
+ default: 'active'
282
+ };
283
+
284
+ // Unique constraint
285
+ this.email = {
286
+ type: 'string',
287
+ unique: true
288
+ };
289
+
290
+ // Timestamp
291
+ this.created_at = {
292
+ type: 'timestamp',
293
+ default: 'CURRENT_TIMESTAMP'
294
+ };
295
+ }
296
+ }
297
+ ```
298
+
299
+ ### Field Types
300
+
301
+ | MasterRecord Type | PostgreSQL | MySQL | SQLite |
302
+ |-------------------|---------------|---------------|-----------|
303
+ | `integer` | INTEGER | INT | INTEGER |
304
+ | `bigint` | BIGINT | BIGINT | INTEGER |
305
+ | `string` | VARCHAR(255) | VARCHAR(255) | TEXT |
306
+ | `text` | TEXT | TEXT | TEXT |
307
+ | `float` | REAL | FLOAT | REAL |
308
+ | `decimal` | DECIMAL | DECIMAL | REAL |
309
+ | `boolean` | BOOLEAN | TINYINT | INTEGER |
310
+ | `date` | DATE | DATE | TEXT |
311
+ | `time` | TIME | TIME | TEXT |
312
+ | `datetime` | TIMESTAMP | DATETIME | TEXT |
313
+ | `timestamp` | TIMESTAMP | TIMESTAMP | TEXT |
314
+ | `json` | JSON | JSON | TEXT |
315
+ | `jsonb` | JSONB | JSON | TEXT |
316
+ | `uuid` | UUID | VARCHAR(36) | TEXT |
317
+ | `binary` | BYTEA | BLOB | BLOB |
318
+
319
+ ### Relationships
320
+
321
+ ```javascript
322
+ class User {
323
+ constructor() {
324
+ this.id = { type: 'integer', primary: true, auto: true };
325
+ this.name = { type: 'string' };
326
+
327
+ // One-to-many: User has many Posts
328
+ this.Posts = {
329
+ type: 'hasMany',
330
+ model: 'Post',
331
+ foreignKey: 'user_id'
332
+ };
333
+ }
334
+ }
335
+
336
+ class Post {
337
+ constructor() {
338
+ this.id = { type: 'integer', primary: true, auto: true };
339
+ this.title = { type: 'string' };
340
+ this.user_id = { type: 'integer' };
341
+
342
+ // Many-to-one: Post belongs to User
343
+ this.User = {
344
+ type: 'belongsTo',
345
+ model: 'User',
346
+ foreignKey: 'user_id'
347
+ };
348
+ }
349
+ }
350
+ ```
351
+
352
+ ### Field Transformers
353
+
354
+ Store complex JavaScript types in simple database columns:
355
+
356
+ ```javascript
357
+ class User {
358
+ constructor() {
359
+ this.id = { type: 'integer', primary: true, auto: true };
360
+
361
+ // Store arrays as JSON strings
362
+ this.tags = {
363
+ type: 'string',
364
+ transform: {
365
+ toDatabase: (value) => {
366
+ return Array.isArray(value) ? JSON.stringify(value) : value;
367
+ },
368
+ fromDatabase: (value) => {
369
+ return value ? JSON.parse(value) : [];
370
+ }
371
+ }
372
+ };
373
+ }
168
374
  }
169
- module.exports = AddSettings;
375
+
376
+ // Usage is natural
377
+ const user = db.User.new();
378
+ user.tags = ['admin', 'moderator']; // Assign array
379
+ await db.saveChanges(); // Stored as '["admin","moderator"]'
380
+
381
+ const loaded = db.User.findById(user.id);
382
+ console.log(loaded.tags); // ['admin', 'moderator'] - JavaScript array!
170
383
  ```
171
384
 
172
- Notes:
173
- - `renameColumn` expects an object: `{ tableName, name, newName }` and works in both SQLite and MySQL.
174
- - `seed(tableName, rows)` accepts:
175
- - a single object: `{ col: value, ... }`
176
- - or an array of objects: `[{...}, {...}]`
177
- Values are auto-quoted; booleans become 1/0.
178
- - When a table already exists, `update-database` will sync schema:
179
- - Add missing columns.
180
- - MySQL: adjust default/nullability via `ALTER ... MODIFY`.
181
- - SQLite: rebuilds the table when nullability/default/type changes require it.
385
+ ## Querying
182
386
 
183
- ### Tips
184
- - Prefer additive changes (add columns) before destructive changes (drops/renames) to minimize downtime.
185
- - For large SQLite tables, a rebuild copies data; consider maintenance windows.
186
- - Use `master=development masterrecord get-migrations AppContext` to inspect migration order.
387
+ ### Basic Queries
187
388
 
188
- ## Type Validation
389
+ ```javascript
390
+ // Find all
391
+ const users = db.User.all();
189
392
 
190
- MasterRecord now validates and coerces field types during INSERT and UPDATE operations, similar to Entity Framework. This prevents silent failures where fields were skipped due to type mismatches.
393
+ // Find by primary key
394
+ const user = db.User.findById(123);
191
395
 
192
- ### How it works
396
+ // Find single with where clause
397
+ const alice = db.User
398
+ .where(u => u.email == $$, 'alice@example.com')
399
+ .single();
193
400
 
194
- When you assign a value to an entity field, MasterRecord:
195
- 1. **Validates** the value against the field's type definition
196
- 2. **Auto-converts** compatible types with console warnings
197
- 3. **Throws clear errors** for incompatible types
401
+ // Find multiple with conditions
402
+ const adults = db.User
403
+ .where(u => u.age >= $$, 18)
404
+ .toList();
405
+ ```
198
406
 
199
- ### Type conversion rules
407
+ ### Parameterized Queries
200
408
 
201
- #### Integer fields (`db.integer()`)
202
- - โœ… **Accepts**: integer numbers
203
- - โš ๏ธ **Auto-converts with warning**:
204
- - Float โ†’ integer (rounds: `3.7` โ†’ `4`)
205
- - Valid string โ†’ integer (`"42"` โ†’ `42`)
206
- - Boolean โ†’ integer (`true` โ†’ `1`, `false` โ†’ `0`)
207
- - โŒ **Throws error**: invalid strings (`"abc"`)
409
+ **Always use `$$` placeholders** for SQL injection protection:
208
410
 
209
- #### String fields (`db.string()`)
210
- - โœ… **Accepts**: strings
211
- - โš ๏ธ **Auto-converts with warning**:
212
- - Number โ†’ string (`42` โ†’ `"42"`)
213
- - Boolean โ†’ string (`true` โ†’ `"true"`)
214
- - โŒ **Throws error**: objects, arrays
411
+ ```javascript
412
+ // Single parameter
413
+ const user = db.User.where(u => u.id == $$, 123).single();
414
+
415
+ // Multiple parameters
416
+ const results = db.User
417
+ .where(u => u.age > $$ && u.status == $$, 25, 'active')
418
+ .toList();
419
+
420
+ // Single $ for OR conditions
421
+ const results = db.User
422
+ .where(u => u.status == $ || u.status == null, 'active')
423
+ .toList();
424
+ ```
215
425
 
216
- #### Boolean fields (`db.boolean()`)
217
- - โœ… **Accepts**: booleans
218
- - โš ๏ธ **Auto-converts with warning**:
219
- - Number โ†’ boolean (`0` โ†’ `false`, others โ†’ `true`)
220
- - String โ†’ boolean (`"true"/"1"/"yes"` โ†’ `true`, `"false"/"0"/"no"/""`โ†’ `false`)
221
- - โŒ **Throws error**: invalid strings, objects
426
+ ### IN Clauses
222
427
 
223
- #### Time fields (`db.time()`, timestamps)
224
- - โœ… **Accepts**: strings or numbers
225
- - โŒ **Throws error**: objects, booleans
428
+ ```javascript
429
+ // Array parameter with .includes()
430
+ const ids = [1, 2, 3, 4, 5];
431
+ const users = db.User
432
+ .where(u => $$.includes(u.id), ids)
433
+ .toList();
434
+
435
+ // Generated SQL: WHERE id IN ($1, $2, $3, $4, $5)
436
+ // PostgreSQL parameters: [1, 2, 3, 4, 5]
437
+
438
+ // Alternative .any() syntax
439
+ const users = db.User
440
+ .where(u => u.id.any($$), [1, 2, 3])
441
+ .toList();
442
+
443
+ // Comma-separated strings (auto-splits)
444
+ const users = db.User
445
+ .where(u => u.id.any($$), "1,2,3,4,5")
446
+ .toList();
447
+ ```
226
448
 
227
- ### Example warnings and errors
449
+ ### Query Chaining
228
450
 
229
- **Auto-conversion warning (non-breaking):**
230
451
  ```javascript
231
- const chunk = new DocumentChunk();
232
- chunk.document_id = "4"; // string assigned to integer field
233
- context.DocumentChunk.add(chunk);
234
- context.saveChanges();
452
+ let query = db.User;
453
+
454
+ // Build query dynamically
455
+ if (searchTerm) {
456
+ query = query.where(u => u.name.like($$), `%${searchTerm}%`);
457
+ }
458
+
459
+ if (minAge) {
460
+ query = query.where(u => u.age >= $$, minAge);
461
+ }
462
+
463
+ // Add sorting and pagination
464
+ const users = query
465
+ .orderBy(u => u.created_at)
466
+ .skip(offset)
467
+ .take(limit)
468
+ .toList();
235
469
  ```
236
- Console output:
470
+
471
+ ### Ordering
472
+
473
+ ```javascript
474
+ // Ascending
475
+ const users = db.User
476
+ .orderBy(u => u.name)
477
+ .toList();
478
+
479
+ // Descending
480
+ const users = db.User
481
+ .orderByDescending(u => u.created_at)
482
+ .toList();
237
483
  ```
238
- โš ๏ธ Field DocumentChunk.document_id: Auto-converting string "4" to integer 4
484
+
485
+ ### Pagination
486
+
487
+ ```javascript
488
+ // Skip 20, take 10
489
+ const users = db.User
490
+ .orderBy(u => u.id)
491
+ .skip(20)
492
+ .take(10)
493
+ .toList();
494
+
495
+ // Page-based pagination
496
+ const page = 2;
497
+ const pageSize = 10;
498
+ const users = db.User
499
+ .skip(page * pageSize)
500
+ .take(pageSize)
501
+ .toList();
239
502
  ```
240
503
 
241
- **Type mismatch error (breaks execution):**
504
+ ### Counting
505
+
242
506
  ```javascript
243
- const chunk = new DocumentChunk();
244
- chunk.document_id = "invalid"; // non-numeric string
245
- context.DocumentChunk.add(chunk);
246
- context.saveChanges(); // throws error
507
+ // Count all
508
+ const total = db.User.count();
509
+
510
+ // Count with conditions
511
+ const activeCount = db.User
512
+ .where(u => u.status == $$, 'active')
513
+ .count();
247
514
  ```
248
- Error thrown:
515
+
516
+ ### Complex Queries
517
+
518
+ ```javascript
519
+ // Multiple conditions with OR
520
+ const results = db.User
521
+ .where(u => (u.status == 'active' || u.status == 'pending') && u.age >= $$, 18)
522
+ .orderBy(u => u.name)
523
+ .toList();
524
+
525
+ // Nullable checks
526
+ const usersWithoutEmail = db.User
527
+ .where(u => u.email == null)
528
+ .toList();
529
+
530
+ // LIKE queries
531
+ const matching = db.User
532
+ .where(u => u.name.like($$), '%john%')
533
+ .toList();
249
534
  ```
250
- INSERT failed: Type mismatch for DocumentChunk.document_id: Expected integer, got string "invalid" which cannot be converted to a number
535
+
536
+ ## Migrations
537
+
538
+ ### CLI Commands
539
+
540
+ ```bash
541
+ # Enable migrations (one-time per context)
542
+ masterrecord enable-migrations AppContext
543
+
544
+ # Create a migration
545
+ masterrecord add-migration MigrationName AppContext
546
+
547
+ # Apply migrations
548
+ masterrecord migrate AppContext
549
+
550
+ # Apply all migrations from scratch
551
+ masterrecord migrate-restart AppContext
552
+
553
+ # List migrations
554
+ masterrecord get-migrations AppContext
555
+
556
+ # Multi-context commands
557
+ masterrecord enable-migrations-all # Enable for all contexts
558
+ masterrecord add-migration-all Init # Create migration for all
559
+ masterrecord migrate-all # Apply all pending migrations
251
560
  ```
252
561
 
253
- ### Migration from older versions
562
+ ### Migration File Structure
254
563
 
255
- If your code relies on implicit type coercion that was previously silent:
256
- - **No breaking changes**: Compatible types are still auto-converted
257
- - **New warnings**: You'll see console warnings for auto-conversions
258
- - **New errors**: Incompatible types that were silently skipped now throw errors
564
+ ```javascript
565
+ // db/migrations/20250111_143052_CreateUser.js
566
+ module.exports = {
567
+ up: function(table, schema) {
568
+ // Create table
569
+ schema.createTable(table.User);
570
+
571
+ // Seed initial data
572
+ schema.seed('User', {
573
+ name: 'Admin',
574
+ email: 'admin@example.com',
575
+ role: 'admin'
576
+ });
577
+ },
578
+
579
+ down: function(table, schema) {
580
+ // Rollback
581
+ schema.dropTable(table.User);
582
+ }
583
+ };
584
+ ```
259
585
 
260
- **Recommendation**: Review warnings and fix type mismatches in your code for cleaner, more predictable behavior.
586
+ ### Migration Operations
261
587
 
262
- ### Benefits
588
+ ```javascript
589
+ module.exports = {
590
+ up: function(table, schema) {
591
+ // Create table
592
+ schema.createTable(table.User);
593
+
594
+ // Add column
595
+ schema.addColumn({
596
+ tableName: 'User',
597
+ name: 'phone',
598
+ type: 'string'
599
+ });
600
+
601
+ // Alter column
602
+ schema.alterColumn({
603
+ tableName: 'User',
604
+ table: {
605
+ name: 'age',
606
+ type: 'integer',
607
+ nullable: false,
608
+ default: 0
609
+ }
610
+ });
611
+
612
+ // Rename column
613
+ schema.renameColumn({
614
+ tableName: 'User',
615
+ name: 'old_name',
616
+ newName: 'new_name'
617
+ });
618
+
619
+ // Drop column
620
+ schema.dropColumn({
621
+ tableName: 'User',
622
+ name: 'deprecated_field'
623
+ });
624
+
625
+ // Drop table
626
+ schema.dropTable(table.OldTable);
627
+ },
628
+
629
+ down: function(table, schema) {
630
+ // Reverse operations
631
+ }
632
+ };
633
+ ```
263
634
 
264
- 1. **No more silent field skipping**: Previously, if you assigned a string to an integer field, the ORM would silently skip it in the INSERT/UPDATE statement. Now you get immediate feedback.
635
+ ### Seed Data
265
636
 
266
- 2. **Clear error messages**: Errors include entity name, field name, expected type, actual type, and the problematic value.
637
+ ```javascript
638
+ module.exports = {
639
+ up: function(table, schema) {
640
+ schema.createTable(table.User);
641
+
642
+ // Single record
643
+ schema.seed('User', {
644
+ name: 'Admin',
645
+ email: 'admin@example.com'
646
+ });
647
+
648
+ // Multiple records (efficient bulk insert)
649
+ schema.bulkSeed('User', [
650
+ { name: 'Alice', email: 'alice@example.com', age: 25 },
651
+ { name: 'Bob', email: 'bob@example.com', age: 30 },
652
+ { name: 'Charlie', email: 'charlie@example.com', age: 35 }
653
+ ]);
654
+ },
655
+
656
+ down: function(table, schema) {
657
+ schema.dropTable(table.User);
658
+ }
659
+ };
660
+ ```
267
661
 
268
- 3. **Predictable behavior**: Auto-conversions match common patterns (e.g., database IDs returned as strings from some drivers are converted to integers).
662
+ **Seed data is idempotent** - re-running migrations won't create duplicates:
663
+ - SQLite: `INSERT OR IGNORE`
664
+ - MySQL: `INSERT IGNORE`
665
+ - PostgreSQL: `INSERT ... ON CONFLICT DO NOTHING`
269
666
 
270
- 4. **Better debugging**: Type issues are caught at save time, not when you query the data later.
667
+ ## Advanced Features
271
668
 
272
- ## Multi-context (multi-database) projects
669
+ ### Type Validation
273
670
 
274
- When your project defines multiple Context files (e.g., `userContext.js`, `modelContext.js`, `mailContext.js`, `chatContext.js`) across different packages or feature directories, MasterRecord can auto-detect and operate on all of them.
671
+ MasterRecord validates and coerces field types at runtime:
275
672
 
276
- ### New bulk commands
673
+ ```javascript
674
+ const user = db.User.new();
675
+ user.age = "25"; // String assigned to integer field
676
+ await db.saveChanges();
677
+ // โš ๏ธ Console: Auto-converting string "25" to integer 25
678
+
679
+ user.age = "invalid";
680
+ await db.saveChanges();
681
+ // โŒ Error: Field User.age must be an integer, got string "invalid"
682
+ ```
277
683
 
278
- - enable-migrations-all (alias: ema)
279
- - Scans the project for MasterRecord Context files (heuristic) and enables migrations for each by writing a portable snapshot next to the context at `<ContextDir>/db/migrations/<context>_contextSnapShot.json`.
684
+ ### Field Transformers (Advanced)
280
685
 
281
- - add-migration-all <Name> (alias: ama)
282
- - Creates a migration named `<Name>` (e.g., `Init`) for every detected context that has a snapshot. Migrations are written into each contextโ€™s own migrations folder.
686
+ ```javascript
687
+ class Post {
688
+ constructor() {
689
+ this.id = { type: 'integer', primary: true, auto: true };
690
+
691
+ // Store array as JSON
692
+ this.tags = {
693
+ type: 'string',
694
+ transform: {
695
+ toDatabase: (v) => Array.isArray(v) ? JSON.stringify(v) : v,
696
+ fromDatabase: (v) => v ? JSON.parse(v) : []
697
+ }
698
+ };
699
+
700
+ // PostgreSQL JSONB (native JSON support)
701
+ this.metadata = {
702
+ type: 'jsonb', // PostgreSQL only
703
+ transform: {
704
+ toDatabase: (v) => JSON.stringify(v || {}),
705
+ fromDatabase: (v) => typeof v === 'string' ? JSON.parse(v) : v
706
+ }
707
+ };
708
+ }
709
+ }
710
+ ```
283
711
 
284
- - update-database-all (alias: uda)
285
- - Applies the latest migration for every detected context with migrations.
712
+ ### Table Prefixes
286
713
 
287
- - update-database-down <ContextName> (alias: udd)
288
- - Runs the latest migrationโ€™s `down()` for the specified context.
714
+ Useful for multi-tenant applications or plugin systems:
289
715
 
290
- - update-database-target <migrationFileName> (alias: udt)
291
- - Rolls back migrations newer than the given migration file within that contextโ€™s migrations folder.
716
+ ```javascript
717
+ class AppContext extends context {
718
+ constructor() {
719
+ super();
292
720
 
293
- - ensure-database <ContextName> (alias: ed)
294
- - For MySQL contexts, ensures the database exists (like EFโ€™s `Database.EnsureCreated`). Auto-detects connection info from your Context env settings.
721
+ this.tablePrefix = 'myapp_'; // Set before dbset()
722
+ this.env('config/environments');
295
723
 
296
- ### Portable snapshots (no hardcoded absolute paths)
724
+ this.dbset(User); // Creates table: myapp_User
725
+ this.dbset(Post); // Creates table: myapp_Post
726
+ }
727
+ }
728
+ ```
297
729
 
298
- Snapshots are written with relative paths, so moving/renaming the project root does not break CLI resolution:
299
- - `contextLocation`: path from the migrations folder to the Context file
300
- - `migrationFolder`: `.` (the snapshot resides in the migrations folder)
301
- - `snapShotLocation`: the snapshot filename
730
+ ### Transactions (PostgreSQL)
302
731
 
303
- ### Typical flow for multiple contexts
732
+ ```javascript
733
+ const { PostgresSyncConnect } = require('masterrecord/postgresSyncConnect');
304
734
 
305
- 1) Enable migrations everywhere:
306
- ```bash
307
- # macOS/Linux
308
- master=development masterrecord enable-migrations-all
735
+ const connection = new PostgresSyncConnect();
736
+ await connection.connect(config);
309
737
 
310
- # Windows PowerShell
311
- $env:master = 'development'
312
- masterrecord enable-migrations-all
738
+ const result = await connection.transaction(async (client) => {
739
+ // Insert user
740
+ const userResult = await client.query(
741
+ 'INSERT INTO User (name, email) VALUES ($1, $2) RETURNING id',
742
+ ['Alice', 'alice@example.com']
743
+ );
744
+
745
+ // Insert related record
746
+ await client.query(
747
+ 'INSERT INTO Profile (user_id, bio) VALUES ($1, $2)',
748
+ [userResult.rows[0].id, 'Software Engineer']
749
+ );
750
+
751
+ return userResult.rows[0].id;
752
+ });
753
+
754
+ // Automatically commits on success, rolls back on error
755
+ ```
756
+
757
+ ### Multi-Context Applications
758
+
759
+ Manage multiple databases in one application:
760
+
761
+ ```javascript
762
+ // contexts/userContext.js
763
+ class UserContext extends context {
764
+ constructor() {
765
+ super();
766
+ this.env({ type: 'postgres', database: 'users_db', ... });
767
+ this.dbset(User);
768
+ this.dbset(Profile);
769
+ }
770
+ }
771
+
772
+ // contexts/analyticsContext.js
773
+ class AnalyticsContext extends context {
774
+ constructor() {
775
+ super();
776
+ this.env({ type: 'postgres', database: 'analytics_db', ... });
777
+ this.dbset(Event);
778
+ this.dbset(Metric);
779
+ }
780
+ }
781
+
782
+ // Usage
783
+ const userDb = new UserContext();
784
+ const analyticsDb = new AnalyticsContext();
785
+
786
+ const user = userDb.User.findById(123);
787
+ analyticsDb.Event.new().log('user_login', user.id);
788
+ await analyticsDb.saveChanges();
313
789
  ```
314
790
 
315
- 2) Create an initial migration for all contexts:
316
791
  ```bash
317
- # macOS/Linux
318
- master=development masterrecord add-migration-all Init
792
+ # Migrate all contexts at once
793
+ masterrecord migrate-all
794
+ ```
795
+
796
+ ### Raw SQL Queries
797
+
798
+ When you need full control:
799
+
800
+ ```javascript
801
+ // PostgreSQL parameterized query
802
+ const users = await db._SQLEngine.exec(
803
+ 'SELECT * FROM "User" WHERE age > $1 AND status = $2',
804
+ [25, 'active']
805
+ );
806
+
807
+ // MySQL parameterized query
808
+ const users = db._SQLEngine.exec(
809
+ 'SELECT * FROM User WHERE age > ? AND status = ?',
810
+ [25, 'active']
811
+ );
812
+ ```
813
+
814
+ ## API Reference
815
+
816
+ ### Context Methods
817
+
818
+ ```javascript
819
+ // Entity registration
820
+ context.dbset(EntityClass)
821
+ context.dbset(EntityClass, 'custom_table_name')
822
+
823
+ // Save changes
824
+ await context.saveChanges() // PostgreSQL (async)
825
+ context.saveChanges() // MySQL/SQLite (sync)
826
+
827
+ // Add/Remove entities
828
+ context.EntityName.add(entity)
829
+ context.remove(entity)
830
+ ```
319
831
 
320
- # Windows PowerShell
321
- $env:master = 'development'
322
- masterrecord add-migration-all Init
832
+ ### Query Methods
833
+
834
+ ```javascript
835
+ // Chainable query builders
836
+ .where(query, ...params) // Add WHERE condition
837
+ .and(query, ...params) // Add AND condition
838
+ .orderBy(field) // Sort ascending
839
+ .orderByDescending(field) // Sort descending
840
+ .skip(number) // Skip N records
841
+ .take(number) // Limit to N records
842
+ .include(relationship) // Eager load
843
+
844
+ // Terminal methods (execute query)
845
+ .toList() // Return array
846
+ .single() // Return one or null
847
+ .first() // Return first or null
848
+ .count() // Return count
849
+ .any() // Return boolean
850
+ .all() // Return all records
851
+
852
+ // Convenience methods
853
+ .findById(id) // Find by primary key
854
+ .new() // Create new entity instance
323
855
  ```
324
856
 
325
- 3) Apply migrations everywhere:
857
+ ### Migration Methods
858
+
859
+ ```javascript
860
+ // In migration up/down functions
861
+ schema.createTable(table.EntityName)
862
+ schema.dropTable(table.EntityName)
863
+ schema.addColumn({ tableName, name, type })
864
+ schema.dropColumn({ tableName, name })
865
+ schema.alterColumn({ tableName, table: { name, type, nullable, default } })
866
+ schema.renameColumn({ tableName, name, newName })
867
+ schema.seed(tableName, data)
868
+ schema.bulkSeed(tableName, dataArray)
869
+ ```
870
+
871
+ ## Examples
872
+
873
+ ### Complete CRUD Example
874
+
875
+ ```javascript
876
+ const AppContext = require('./app/models/context');
877
+
878
+ async function demo() {
879
+ const db = new AppContext();
880
+
881
+ // CREATE
882
+ const user = db.User.new();
883
+ user.name = 'Alice';
884
+ user.email = 'alice@example.com';
885
+ user.age = 28;
886
+ await db.saveChanges();
887
+ console.log('Created user:', user.id);
888
+
889
+ // READ
890
+ const alice = db.User
891
+ .where(u => u.email == $$, 'alice@example.com')
892
+ .single();
893
+ console.log('Found user:', alice.name);
894
+
895
+ // UPDATE
896
+ alice.age = 29;
897
+ await db.saveChanges();
898
+ console.log('Updated age to:', alice.age);
899
+
900
+ // DELETE
901
+ db.remove(alice);
902
+ await db.saveChanges();
903
+ console.log('User deleted');
904
+ }
905
+
906
+ demo();
907
+ ```
908
+
909
+ ### Pagination Example
910
+
911
+ ```javascript
912
+ async function getUsers(page = 0, pageSize = 10) {
913
+ const db = new AppContext();
914
+
915
+ const users = db.User
916
+ .where(u => u.status == $$, 'active')
917
+ .orderBy(u => u.created_at)
918
+ .skip(page * pageSize)
919
+ .take(pageSize)
920
+ .toList();
921
+
922
+ const total = db.User
923
+ .where(u => u.status == $$, 'active')
924
+ .count();
925
+
926
+ return {
927
+ users,
928
+ page,
929
+ pageSize,
930
+ total,
931
+ totalPages: Math.ceil(total / pageSize)
932
+ };
933
+ }
934
+ ```
935
+
936
+ ### Search with Filters
937
+
938
+ ```javascript
939
+ async function searchUsers(filters) {
940
+ const db = new AppContext();
941
+ let query = db.User;
942
+
943
+ // Apply filters dynamically
944
+ if (filters.name) {
945
+ query = query.where(u => u.name.like($$), `%${filters.name}%`);
946
+ }
947
+
948
+ if (filters.minAge) {
949
+ query = query.where(u => u.age >= $$, filters.minAge);
950
+ }
951
+
952
+ if (filters.status) {
953
+ query = query.where(u => u.status == $$, filters.status);
954
+ }
955
+
956
+ // Add sorting
957
+ const sortField = filters.sortBy || 'created_at';
958
+ const sortOrder = filters.sortOrder || 'desc';
959
+
960
+ if (sortOrder === 'asc') {
961
+ query = query.orderBy(sortField);
962
+ } else {
963
+ query = query.orderByDescending(sortField);
964
+ }
965
+
966
+ // Add pagination
967
+ if (filters.page && filters.pageSize) {
968
+ query = query
969
+ .skip(filters.page * filters.pageSize)
970
+ .take(filters.pageSize);
971
+ }
972
+
973
+ return query.toList();
974
+ }
975
+ ```
976
+
977
+ ### Relationship Example
978
+
979
+ ```javascript
980
+ class BlogContext extends context {
981
+ constructor() {
982
+ super();
983
+ this.env('config/environments');
984
+ this.dbset(Author);
985
+ this.dbset(Post);
986
+ }
987
+ }
988
+
989
+ // Create author with posts
990
+ const db = new BlogContext();
991
+
992
+ const author = db.Author.new();
993
+ author.name = 'John Doe';
994
+ await db.saveChanges();
995
+
996
+ const post = db.Post.new();
997
+ post.title = 'My First Post';
998
+ post.content = 'Hello World!';
999
+ post.author_id = author.id;
1000
+ await db.saveChanges();
1001
+
1002
+ // Query with relationships
1003
+ const posts = db.Post
1004
+ .where(p => p.author_id == $$, author.id)
1005
+ .toList();
1006
+
1007
+ console.log(`${author.name} has ${posts.length} posts`);
1008
+ ```
1009
+
1010
+ ## Performance Tips
1011
+
1012
+ ### 1. Use Bulk Operations
1013
+
1014
+ ```javascript
1015
+ // โŒ BAD: Multiple inserts
1016
+ for (const item of items) {
1017
+ const entity = db.Entity.new();
1018
+ entity.data = item;
1019
+ await db.saveChanges();
1020
+ }
1021
+
1022
+ // โœ… GOOD: Single bulk insert
1023
+ for (const item of items) {
1024
+ const entity = db.Entity.new();
1025
+ entity.data = item;
1026
+ }
1027
+ await db.saveChanges(); // Batch insert
1028
+ ```
1029
+
1030
+ ### 2. Use Indexes
1031
+
1032
+ ```javascript
1033
+ class User {
1034
+ constructor() {
1035
+ this.email = {
1036
+ type: 'string',
1037
+ unique: true // Automatically creates index
1038
+ };
1039
+ }
1040
+ }
1041
+
1042
+ // For complex queries, add database indexes manually
1043
+ // CREATE INDEX idx_user_status ON User(status);
1044
+ ```
1045
+
1046
+ ### 3. Limit Result Sets
1047
+
1048
+ ```javascript
1049
+ // โœ… GOOD: Limit results
1050
+ const recentUsers = db.User
1051
+ .orderByDescending(u => u.created_at)
1052
+ .take(100)
1053
+ .toList();
1054
+
1055
+ // โŒ BAD: Load everything
1056
+ const allUsers = db.User.all();
1057
+ ```
1058
+
1059
+ ### 4. Use Connection Pooling (PostgreSQL)
1060
+
1061
+ ```javascript
1062
+ this.env({
1063
+ type: 'postgres',
1064
+ max: 20, // Pool size
1065
+ idleTimeoutMillis: 30000,
1066
+ connectionTimeoutMillis: 2000
1067
+ });
1068
+ ```
1069
+
1070
+ ## Security
1071
+
1072
+ ### SQL Injection Protection
1073
+
1074
+ MasterRecord uses **parameterized queries throughout** to prevent SQL injection:
1075
+
1076
+ ```javascript
1077
+ // โœ… SAFE: Parameterized
1078
+ const user = db.User.where(u => u.name == $$, userInput).single();
1079
+
1080
+ // โŒ UNSAFE: Never do this
1081
+ // const query = `SELECT * FROM User WHERE name = '${userInput}'`;
1082
+ ```
1083
+
1084
+ All operations use parameterized queries:
1085
+ - SELECT queries
1086
+ - INSERT operations
1087
+ - UPDATE operations
1088
+ - DELETE operations
1089
+ - IN clauses
1090
+ - LIKE patterns
1091
+
1092
+ ### Input Validation
1093
+
1094
+ While SQL injection is prevented, always validate business logic:
1095
+
1096
+ ```javascript
1097
+ // Validate input before querying
1098
+ function getUser(userId) {
1099
+ if (!Number.isInteger(userId) || userId <= 0) {
1100
+ throw new Error('Invalid user ID');
1101
+ }
1102
+
1103
+ return db.User.findById(userId);
1104
+ }
1105
+ ```
1106
+
1107
+ ## Troubleshooting
1108
+
1109
+ ### PostgreSQL Connection Issues
1110
+
326
1111
  ```bash
327
- # macOS/Linux
328
- master=development masterrecord update-database-all
1112
+ # Error: Cannot find module 'pg'
1113
+ npm install pg@^8.16.3
1114
+
1115
+ # Error: Connection refused
1116
+ # Check PostgreSQL is running: sudo service postgresql status
329
1117
 
330
- # Windows PowerShell
331
- $env:master = 'development'
332
- masterrecord update-database-all
1118
+ # Error: Database does not exist
1119
+ createdb myapp
1120
+
1121
+ # Error: Authentication failed
1122
+ # Check pg_hba.conf and user permissions
333
1123
  ```
334
1124
 
335
- 4) Inspect migrations for a specific context:
1125
+ ### MySQL Connection Issues
1126
+
336
1127
  ```bash
337
- # macOS/Linux
338
- master=development masterrecord get-migrations userContext
1128
+ # Error: ER_NOT_SUPPORTED_AUTH_MODE
1129
+ # Use mysql_native_password for MySQL 8.0+
1130
+ ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
339
1131
 
340
- # Windows PowerShell
341
- $env:master = 'development'
342
- masterrecord get-migrations userContext
1132
+ # Error: ER_ACCESS_DENIED_ERROR
1133
+ # Check user permissions
1134
+ GRANT ALL PRIVILEGES ON myapp.* TO 'user'@'localhost';
343
1135
  ```
344
1136
 
345
- 5) Roll back latest for a specific context:
1137
+ ### Migration Issues
1138
+
346
1139
  ```bash
347
- # macOS/Linux
348
- master=development masterrecord update-database-down userContext
1140
+ # Cannot find context file
1141
+ # Ensure you're running from project root
1142
+ cd /path/to/project
1143
+ masterrecord migrate AppContext
1144
+
1145
+ # No migrations found
1146
+ # Check migrations directory exists
1147
+ ls app/models/db/migrations/
349
1148
 
350
- # Windows PowerShell
351
- $env:master = 'development'
352
- masterrecord update-database-down userContext
1149
+ # Type errors in migration
1150
+ # Check entity definitions match database types
353
1151
  ```
354
1152
 
355
- ### Environment selection (cross-platform)
356
- - macOS/Linux prefix: `master=development ...` or `NODE_ENV=development ...`
357
- - Windows PowerShell:
358
- ```powershell
359
- $env:master = 'development'
360
- masterrecord update-database-all
1153
+ ### Common Errors
1154
+
1155
+ ```javascript
1156
+ // Error: "expected N value(s) for '$', but received M"
1157
+ // Solution: Match placeholder count with parameters
1158
+ db.User.where(u => u.age > $$ && u.status == $$, 25, 'active');
1159
+ // Two $$ placeholders โ†‘ โ†‘ Two parameters
1160
+
1161
+ // Error: "Cannot create IN clause with empty array"
1162
+ // Solution: Check array has values before querying
1163
+ const ids = [1, 2, 3];
1164
+ if (ids.length > 0) {
1165
+ db.User.where(u => $$.includes(u.id), ids).toList();
1166
+ }
1167
+
1168
+ // Error: "Field X cannot be null"
1169
+ // Solution: Entity defines field as non-nullable
1170
+ user.name = null; // Error if name is { nullable: false }
361
1171
  ```
362
- - Windows cmd.exe:
363
- ```cmd
364
- set master=development && masterrecord update-database-all
1172
+
1173
+ ## Version Compatibility
1174
+
1175
+ | Component | Version | Notes |
1176
+ |---------------|---------------|------------------------------------------|
1177
+ | MasterRecord | 0.3.0+ | Current version with PostgreSQL support |
1178
+ | Node.js | 14+ | Async/await support required |
1179
+ | PostgreSQL | 9.6+ (12+) | Tested with 12, 13, 14, 15, 16 |
1180
+ | MySQL | 5.7+ (8.0+) | Tested with 8.0+ |
1181
+ | SQLite | 3.x | Any recent version |
1182
+ | pg | 8.16.3+ | PostgreSQL driver |
1183
+ | sync-mysql2 | 1.0.8+ | MySQL driver |
1184
+ | better-sqlite3| 12.6.0+ | SQLite driver |
1185
+
1186
+ ## Documentation
1187
+
1188
+ - [PostgreSQL Setup Guide](./docs/POSTGRESQL_SETUP.md) - Complete PostgreSQL configuration
1189
+ - [Migrations Guide](./docs/MIGRATIONS_GUIDE.md) - Detailed migration tutorial
1190
+ - [Methods Reference](./docs/METHODS_REFERENCE.md) - Complete API reference
1191
+ - [Field Transformers](./docs/FIELD_TRANSFORMERS.md) - Custom type handling
1192
+
1193
+ ## Contributing
1194
+
1195
+ Contributions are welcome! Please:
1196
+
1197
+ 1. Fork the repository
1198
+ 2. Create a feature branch
1199
+ 3. Make your changes with tests
1200
+ 4. Submit a pull request
1201
+
1202
+ ### Running Tests
1203
+
1204
+ ```bash
1205
+ # PostgreSQL engine tests
1206
+ node test/postgresEngineTest.js
1207
+
1208
+ # Integration tests (requires database)
1209
+ node test/postgresIntegrationTest.js
1210
+
1211
+ # All tests
1212
+ npm test
365
1213
  ```
366
1214
 
367
- ### Notes and tips
368
- - Each Context should define its own env settings and tables; `update-database-all` operates context-by-context so separate databases are handled cleanly.
369
- - For SQLite contexts, the `connection` path will be created if the directory does not exist.
370
- - For MySQL contexts, `ensure-database <ContextName>` can create the DB (permissions required) before migrations run.
371
- - If you rename/move the project root, re-run `enable-migrations-all` or any single-context command once; snapshots use relative paths and will continue working.
372
- - If `update-database-all` reports โ€œno migration files foundโ€ for a context, run `get-migrations <ContextName>`. If empty, create a migration with `add-migration <Name> <ContextName>` or use `add-migration-all <Name>`.
1215
+ ## License
1216
+
1217
+ MIT License - see [LICENSE](LICENSE) file for details.
1218
+
1219
+ ## Credits
1220
+
1221
+ Created by Alexander Rich
1222
+
1223
+ ## Support
1224
+
1225
+ - GitHub Issues: [Report bugs or request features](https://github.com/Tailor/MasterRecord/issues)
1226
+ - npm: [masterrecord](https://www.npmjs.com/package/masterrecord)
373
1227
 
1228
+ ---
374
1229
 
1230
+ **MasterRecord** - Code-first ORM for Node.js with multi-database support