masterrecord 0.2.36 → 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 (38) hide show
  1. package/.claude/settings.local.json +19 -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 +13 -4
  12. package/SQLLiteEngine.js +309 -19
  13. package/context.js +47 -10
  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 +3 -3
  21. package/postgresEngine.js +434 -491
  22. package/postgresSyncConnect.js +209 -0
  23. package/readme.md +1046 -416
  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/transformerTest.js +287 -0
  37. package/test/verifyFindById.js +169 -0
  38. package/test/verifyNewMethod.js +191 -0
package/readme.md CHANGED
@@ -1,600 +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
19
+
20
+ ## Database Support
4
21
 
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.
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 |
6
27
 
7
- - Supported databases: MySQL, SQLite
8
- - Synchronous API by default (no await needed)
9
- - Built-in CLI for migrations and seeding
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
27
65
 
28
- ```bash
29
- masterrecord enable-migrations AppContext
30
- ```
66
+ ```javascript
67
+ // app/models/context.js
68
+ const context = require('masterrecord/context');
69
+ const User = require('./User');
70
+ const Post = require('./Post');
31
71
 
32
- 2) Make or change your entities, then create a migration file:
72
+ class AppContext extends context {
73
+ constructor() {
74
+ super();
33
75
 
34
- ```bash
35
- masterrecord add-migration Init AppContext
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;
36
93
  ```
37
94
 
38
- 3) Apply the migration to your database:
95
+ ### 2. Define Entities
39
96
 
40
- ```bash
41
- master=development masterrecord update-database AppContext
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;
42
110
  ```
43
111
 
44
- ### Enable migrations (one-time per Context)
112
+ ### 3. Run Migrations
45
113
 
46
- - Run from the project root where your Context file lives. Use the Context file name (without extension) as the argument.
47
114
  ```bash
48
- master=development masterrecord enable-migrations AppContext
49
- ```
50
- This creates `db/migrations/<context>_contextSnapShot.json` and the `db/migrations` directory.
115
+ # Enable migrations (one-time setup)
116
+ masterrecord enable-migrations AppContext
51
117
 
52
- ### Create a migration
118
+ # Create initial migration
119
+ masterrecord add-migration InitialCreate AppContext
53
120
 
54
- - After you change your entity models, generate a migration file:
55
- ```bash
56
- master=development masterrecord add-migration <MigrationName> AppContext
121
+ # Apply migrations
122
+ masterrecord migrate AppContext
57
123
  ```
58
- This writes a new file to `db/migrations/<timestamp>_<MigrationName>_migration.js`.
59
124
 
60
- ### Apply migrations to the database
125
+ ### 4. Query Your Data
61
126
 
62
- - Apply only the latest pending migration:
63
- ```bash
64
- master=development masterrecord update-database AppContext
65
- ```
66
- - Apply all migrations from the beginning (useful for a clean DB):
67
- ```bash
68
- master=development masterrecord update-database-restart AppContext
69
- ```
70
- - List migration files (debug/inspection):
71
- ```bash
72
- master=development masterrecord get-migrations 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();
73
150
  ```
74
151
 
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.
152
+ ## Database Configuration
78
153
 
79
- ### Updating the running server
154
+ ### PostgreSQL (Async)
80
155
 
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
- }
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
+ }
168
175
  }
169
- module.exports = AddSettings;
176
+
177
+ // Usage requires await
178
+ const db = new AppContext();
179
+ await db.saveChanges(); // PostgreSQL is async
170
180
  ```
171
181
 
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.
182
+ ### MySQL (Synchronous)
182
183
 
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.
184
+ ```javascript
185
+ class AppContext extends context {
186
+ constructor() {
187
+ super();
187
188
 
188
- ## Type Validation
189
+ this.env({
190
+ type: 'mysql',
191
+ host: 'localhost',
192
+ port: 3306,
193
+ database: 'myapp',
194
+ user: 'root',
195
+ password: 'password'
196
+ });
189
197
 
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.
198
+ this.dbset(User);
199
+ }
200
+ }
191
201
 
192
- ### How it works
202
+ // Usage is synchronous
203
+ const db = new AppContext();
204
+ db.saveChanges(); // No await needed
205
+ ```
193
206
 
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
207
+ ### SQLite (Synchronous)
198
208
 
199
- ### Type conversion rules
209
+ ```javascript
210
+ class AppContext extends context {
211
+ constructor() {
212
+ super();
200
213
 
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"`)
214
+ this.env({
215
+ type: 'sqlite',
216
+ connection: './data/myapp.db' // File path
217
+ });
208
218
 
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
219
+ this.dbset(User);
220
+ }
221
+ }
222
+ ```
215
223
 
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
224
+ ### Environment Files
222
225
 
223
- #### Time fields (`db.time()`, timestamps)
224
- - ✅ **Accepts**: strings or numbers
225
- - ❌ **Throws error**: objects, booleans
226
+ Store configurations in JSON files:
226
227
 
227
- ### Example warnings and errors
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
+ }
238
+ ```
228
239
 
229
- **Auto-conversion warning (non-breaking):**
230
240
  ```javascript
231
- const chunk = new DocumentChunk();
232
- chunk.document_id = "4"; // string assigned to integer field
233
- context.DocumentChunk.add(chunk);
234
- context.saveChanges();
235
- ```
236
- Console output:
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
+ }
237
249
  ```
238
- ⚠️ Field DocumentChunk.document_id: Auto-converting string "4" to integer 4
250
+
251
+ ```bash
252
+ # Set environment
253
+ export NODE_ENV=development
254
+ node app.js
239
255
  ```
240
256
 
241
- **Type mismatch error (breaks execution):**
257
+ ## Entity Definitions
258
+
259
+ ### Basic Entity
260
+
242
261
  ```javascript
243
- const chunk = new DocumentChunk();
244
- chunk.document_id = "invalid"; // non-numeric string
245
- context.DocumentChunk.add(chunk);
246
- context.saveChanges(); // throws error
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
+ }
247
297
  ```
248
- Error thrown:
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
+ }
249
350
  ```
250
- INSERT failed: Type mismatch for DocumentChunk.document_id: Expected integer, got string "invalid" which cannot be converted to a number
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
+ }
374
+ }
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!
251
383
  ```
252
384
 
253
- ### Migration from older versions
385
+ ## Querying
254
386
 
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
387
+ ### Basic Queries
259
388
 
260
- **Recommendation**: Review warnings and fix type mismatches in your code for cleaner, more predictable behavior.
389
+ ```javascript
390
+ // Find all
391
+ const users = db.User.all();
261
392
 
262
- ### Benefits
393
+ // Find by primary key
394
+ const user = db.User.findById(123);
263
395
 
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.
396
+ // Find single with where clause
397
+ const alice = db.User
398
+ .where(u => u.email == $$, 'alice@example.com')
399
+ .single();
265
400
 
266
- 2. **Clear error messages**: Errors include entity name, field name, expected type, actual type, and the problematic value.
401
+ // Find multiple with conditions
402
+ const adults = db.User
403
+ .where(u => u.age >= $$, 18)
404
+ .toList();
405
+ ```
267
406
 
268
- 3. **Predictable behavior**: Auto-conversions match common patterns (e.g., database IDs returned as strings from some drivers are converted to integers).
407
+ ### Parameterized Queries
269
408
 
270
- 4. **Better debugging**: Type issues are caught at save time, not when you query the data later.
409
+ **Always use `$$` placeholders** for SQL injection protection:
271
410
 
272
- ## Multi-context (multi-database) projects
411
+ ```javascript
412
+ // Single parameter
413
+ const user = db.User.where(u => u.id == $$, 123).single();
273
414
 
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.
415
+ // Multiple parameters
416
+ const results = db.User
417
+ .where(u => u.age > $$ && u.status == $$, 25, 'active')
418
+ .toList();
275
419
 
276
- ### New bulk commands
420
+ // Single $ for OR conditions
421
+ const results = db.User
422
+ .where(u => u.status == $ || u.status == null, 'active')
423
+ .toList();
424
+ ```
277
425
 
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`.
426
+ ### IN Clauses
280
427
 
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.
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();
283
434
 
284
- - update-database-all (alias: uda)
285
- - Applies the latest migration for every detected context with migrations.
435
+ // Generated SQL: WHERE id IN ($1, $2, $3, $4, $5)
436
+ // PostgreSQL parameters: [1, 2, 3, 4, 5]
286
437
 
287
- - update-database-down <ContextName> (alias: udd)
288
- - Runs the latest migration’s `down()` for the specified context.
438
+ // Alternative .any() syntax
439
+ const users = db.User
440
+ .where(u => u.id.any($$), [1, 2, 3])
441
+ .toList();
289
442
 
290
- - update-database-target <migrationFileName> (alias: udt)
291
- - Rolls back migrations newer than the given migration file within that context’s migrations folder.
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
+ ```
292
448
 
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.
449
+ ### Query Chaining
295
450
 
296
- ### Portable snapshots (no hardcoded absolute paths)
451
+ ```javascript
452
+ let query = db.User;
297
453
 
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
454
+ // Build query dynamically
455
+ if (searchTerm) {
456
+ query = query.where(u => u.name.like($$), `%${searchTerm}%`);
457
+ }
302
458
 
303
- ### Typical flow for multiple contexts
459
+ if (minAge) {
460
+ query = query.where(u => u.age >= $$, minAge);
461
+ }
304
462
 
305
- 1) Enable migrations everywhere:
306
- ```bash
307
- # macOS/Linux
308
- master=development masterrecord enable-migrations-all
463
+ // Add sorting and pagination
464
+ const users = query
465
+ .orderBy(u => u.created_at)
466
+ .skip(offset)
467
+ .take(limit)
468
+ .toList();
469
+ ```
470
+
471
+ ### Ordering
472
+
473
+ ```javascript
474
+ // Ascending
475
+ const users = db.User
476
+ .orderBy(u => u.name)
477
+ .toList();
309
478
 
310
- # Windows PowerShell
311
- $env:master = 'development'
312
- masterrecord enable-migrations-all
479
+ // Descending
480
+ const users = db.User
481
+ .orderByDescending(u => u.created_at)
482
+ .toList();
313
483
  ```
314
484
 
315
- 2) Create an initial migration for all contexts:
316
- ```bash
317
- # macOS/Linux
318
- master=development masterrecord add-migration-all Init
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();
319
494
 
320
- # Windows PowerShell
321
- $env:master = 'development'
322
- masterrecord add-migration-all Init
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();
323
502
  ```
324
503
 
325
- 3) Apply migrations everywhere:
326
- ```bash
327
- # macOS/Linux
328
- master=development masterrecord update-database-all
504
+ ### Counting
329
505
 
330
- # Windows PowerShell
331
- $env:master = 'development'
332
- masterrecord update-database-all
506
+ ```javascript
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();
333
514
  ```
334
515
 
335
- 4) Inspect migrations for a specific context:
336
- ```bash
337
- # macOS/Linux
338
- master=development masterrecord get-migrations userContext
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();
339
524
 
340
- # Windows PowerShell
341
- $env:master = 'development'
342
- masterrecord get-migrations userContext
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();
343
534
  ```
344
535
 
345
- 5) Roll back latest for a specific context:
536
+ ## Migrations
537
+
538
+ ### CLI Commands
539
+
346
540
  ```bash
347
- # macOS/Linux
348
- master=development masterrecord update-database-down userContext
541
+ # Enable migrations (one-time per context)
542
+ masterrecord enable-migrations AppContext
349
543
 
350
- # Windows PowerShell
351
- $env:master = 'development'
352
- masterrecord update-database-down userContext
353
- ```
544
+ # Create a migration
545
+ masterrecord add-migration MigrationName AppContext
354
546
 
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
361
- ```
362
- - Windows cmd.exe:
363
- ```cmd
364
- set master=development && masterrecord update-database-all
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
365
560
  ```
366
561
 
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>`.
562
+ ### Migration File Structure
373
563
 
374
- ## Table Prefixes
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
+ ```
375
585
 
376
- MasterRecord supports automatic table prefixing for both MySQL and SQLite databases. This is useful for:
377
- - Multi-tenant applications sharing a single database
378
- - Plugin systems where each plugin needs isolated tables
379
- - Avoiding table name conflicts in shared database environments
586
+ ### Migration Operations
380
587
 
381
- ### Using tablePrefix
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
+ ```
382
634
 
383
- Set the `tablePrefix` property in your Context constructor before calling `dbset()`:
635
+ ### Seed Data
384
636
 
385
637
  ```javascript
386
- var masterrecord = require('masterrecord');
387
- const User = require('./models/User');
388
- const Post = require('./models/Post');
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
+ ```
389
661
 
390
- class AppContext extends masterrecord.context {
391
- constructor() {
392
- super();
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`
393
666
 
394
- // Set table prefix
395
- this.tablePrefix = 'myapp_';
667
+ ## Advanced Features
396
668
 
397
- // Configure environment
398
- this.env('config/environments');
669
+ ### Type Validation
399
670
 
400
- // Register models - prefix will be automatically applied
401
- this.dbset(User); // Creates table: myapp_User
402
- this.dbset(Post); // Creates table: myapp_Post
403
- }
404
- }
671
+ MasterRecord validates and coerces field types at runtime:
405
672
 
406
- module.exports = AppContext;
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"
407
682
  ```
408
683
 
409
- ### How it works
684
+ ### Field Transformers (Advanced)
410
685
 
411
- When `tablePrefix` is set:
412
- 1. The prefix is automatically prepended to all table names during `dbset()` registration
413
- 2. Works with both the default table name (model class name) and custom names
414
- 3. Applies to all database operations: queries, inserts, updates, deletes, and migrations
415
- 4. Supports both MySQL and SQLite databases
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
+ ```
711
+
712
+ ### Table Prefixes
416
713
 
417
- ### Example with custom table names
714
+ Useful for multi-tenant applications or plugin systems:
418
715
 
419
716
  ```javascript
420
- class AppContext extends masterrecord.context {
717
+ class AppContext extends context {
421
718
  constructor() {
422
719
  super();
423
- this.tablePrefix = 'myapp_';
720
+
721
+ this.tablePrefix = 'myapp_'; // Set before dbset()
424
722
  this.env('config/environments');
425
723
 
426
- // Custom table name + prefix
427
- this.dbset(User, 'users'); // Creates table: myapp_users
428
- this.dbset(Post, 'blog_posts'); // Creates table: myapp_blog_posts
724
+ this.dbset(User); // Creates table: myapp_User
725
+ this.dbset(Post); // Creates table: myapp_Post
429
726
  }
430
727
  }
431
728
  ```
432
729
 
433
- ### Plugin example
730
+ ### Transactions (PostgreSQL)
731
+
732
+ ```javascript
733
+ const { PostgresSyncConnect } = require('masterrecord/postgresSyncConnect');
734
+
735
+ const connection = new PostgresSyncConnect();
736
+ await connection.connect(config);
737
+
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
434
758
 
435
- Perfect for plugin systems where each plugin needs isolated tables:
759
+ Manage multiple databases in one application:
436
760
 
437
761
  ```javascript
438
- // RAG Plugin Context
439
- class RagContext extends masterrecord.context {
762
+ // contexts/userContext.js
763
+ class UserContext extends context {
440
764
  constructor() {
441
765
  super();
766
+ this.env({ type: 'postgres', database: 'users_db', ... });
767
+ this.dbset(User);
768
+ this.dbset(Profile);
769
+ }
770
+ }
442
771
 
443
- // Prefix all RAG plugin tables
444
- this.tablePrefix = 'rag_';
445
-
446
- this.env(path.join(__dirname, '../../config/environments'));
447
-
448
- this.dbset(Document); // Creates table: rag_Document
449
- this.dbset(DocumentChunk); // Creates table: rag_DocumentChunk
450
- this.dbset(Settings); // Creates table: rag_Settings
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);
451
779
  }
452
780
  }
453
- ```
454
781
 
455
- ### Migrations with table prefixes
782
+ // Usage
783
+ const userDb = new UserContext();
784
+ const analyticsDb = new AnalyticsContext();
456
785
 
457
- Table prefixes work seamlessly with migrations:
786
+ const user = userDb.User.findById(123);
787
+ analyticsDb.Event.new().log('user_login', user.id);
788
+ await analyticsDb.saveChanges();
789
+ ```
458
790
 
459
791
  ```bash
460
- # Enable migrations (prefix is read from your Context)
461
- master=development masterrecord enable-migrations AppContext
792
+ # Migrate all contexts at once
793
+ masterrecord migrate-all
794
+ ```
795
+
796
+ ### Raw SQL Queries
462
797
 
463
- # Create migration (tables will have prefix in migration file)
464
- master=development masterrecord add-migration Init AppContext
798
+ When you need full control:
465
799
 
466
- # Apply migration (creates prefixed tables)
467
- master=development masterrecord update-database AppContext
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
+ );
468
812
  ```
469
813
 
470
- The generated migration files will reference the prefixed table names, so you don't need to manually add prefixes in your migration code.
814
+ ## API Reference
471
815
 
472
- ### Notes
473
- - The prefix is applied during Context construction, so it must be set before `dbset()` calls
474
- - The prefix is stored in migration snapshots, ensuring consistency across migration operations
475
- - Empty strings or non-string values are ignored (no prefix applied)
476
- - Both MySQL and SQLite fully support table prefixes with no special configuration needed
816
+ ### Context Methods
477
817
 
478
- ## Query Method Chaining
818
+ ```javascript
819
+ // Entity registration
820
+ context.dbset(EntityClass)
821
+ context.dbset(EntityClass, 'custom_table_name')
479
822
 
480
- MasterRecord supports fluent query chaining for building complex queries. You can chain multiple `where()`, `orderBy()`, `skip()`, `take()`, and other methods together to build your query dynamically.
823
+ // Save changes
824
+ await context.saveChanges() // PostgreSQL (async)
825
+ context.saveChanges() // MySQL/SQLite (sync)
481
826
 
482
- ### Chaining Multiple where() Clauses
827
+ // Add/Remove entities
828
+ context.EntityName.add(entity)
829
+ context.remove(entity)
830
+ ```
483
831
 
484
- Multiple `where()` calls are automatically combined with AND logic:
832
+ ### Query Methods
485
833
 
486
834
  ```javascript
487
- // Build query dynamically
488
- let query = context.QaTask;
489
-
490
- // Add first condition
491
- query = query.where(t => t.assigned_worker_id == $$, currentUser.id);
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
855
+ ```
492
856
 
493
- // Add second condition (combines with AND)
494
- query = query.where(t => t.status == $$, 'pending');
857
+ ### Migration Methods
495
858
 
496
- // Add ordering and execute
497
- let tasks = query.orderBy(t => t.created_at).toList();
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)
498
869
  ```
499
870
 
500
- **Generated SQL:**
501
- ```sql
502
- SELECT * FROM QaTask AS t
503
- WHERE t.assigned_worker_id = 123
504
- AND t.status = 'pending'
505
- ORDER BY t.created_at ASC
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();
506
907
  ```
507
908
 
508
- ### Dynamic Query Building
909
+ ### Pagination Example
509
910
 
510
- This is especially useful for building queries based on conditional logic:
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
511
937
 
512
938
  ```javascript
513
- let query = context.User;
939
+ async function searchUsers(filters) {
940
+ const db = new AppContext();
941
+ let query = db.User;
514
942
 
515
- // Always apply base filter
516
- query = query.where(u => u.is_active == true);
943
+ // Apply filters dynamically
944
+ if (filters.name) {
945
+ query = query.where(u => u.name.like($$), `%${filters.name}%`);
946
+ }
517
947
 
518
- // Conditionally add filters
519
- if (searchTerm) {
520
- query = query.where(u => u.name.like($$), `%${searchTerm}%`);
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();
521
974
  }
975
+ ```
976
+
977
+ ### Relationship Example
522
978
 
523
- if (roleFilter) {
524
- query = query.where(u => u.role == $$, roleFilter);
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
+ }
525
987
  }
526
988
 
527
- // Add pagination
528
- query = query
529
- .orderBy(u => u.created_at)
530
- .skip(offset)
531
- .take(limit);
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();
532
1006
 
533
- // Execute query
534
- let users = query.toList();
1007
+ console.log(`${author.name} has ${posts.length} posts`);
535
1008
  ```
536
1009
 
537
- ### Chainable Query Methods
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
+ ```
538
1029
 
539
- All of these methods return the query builder and can be chained:
1030
+ ### 2. Use Indexes
540
1031
 
541
- - **`where(query, ...args)`** - Add WHERE condition (multiple calls combine with AND)
542
- - **`and(query, ...args)`** - Explicitly add AND condition (alternative to chaining where)
543
- - **`orderBy(query, ...args)`** - Sort ascending
544
- - **`orderByDescending(query, ...args)`** - Sort descending
545
- - **`skip(number)`** - Skip N records (pagination offset)
546
- - **`take(number)`** - Limit to N records (pagination limit)
547
- - **`select(query, ...args)`** - Select specific fields
548
- - **`include(query, ...args)`** - Eager load relationships
1032
+ ```javascript
1033
+ class User {
1034
+ constructor() {
1035
+ this.email = {
1036
+ type: 'string',
1037
+ unique: true // Automatically creates index
1038
+ };
1039
+ }
1040
+ }
549
1041
 
550
- ### Combining with OR Logic
1042
+ // For complex queries, add database indexes manually
1043
+ // CREATE INDEX idx_user_status ON User(status);
1044
+ ```
551
1045
 
552
- For OR conditions within a single where clause, use the `||` operator:
1046
+ ### 3. Limit Result Sets
553
1047
 
554
1048
  ```javascript
555
- // Single where with OR
556
- let tasks = context.Task
557
- .where(t => t.status == 'pending' || t.status == 'in_progress')
1049
+ // GOOD: Limit results
1050
+ const recentUsers = db.User
1051
+ .orderByDescending(u => u.created_at)
1052
+ .take(100)
558
1053
  .toList();
1054
+
1055
+ // ❌ BAD: Load everything
1056
+ const allUsers = db.User.all();
559
1057
  ```
560
1058
 
561
- **Generated SQL:**
562
- ```sql
563
- SELECT * FROM Task AS t
564
- WHERE (t.status = 'pending' OR t.status = 'in_progress')
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
+ });
565
1068
  ```
566
1069
 
567
- ### Complex Example
1070
+ ## Security
1071
+
1072
+ ### SQL Injection Protection
1073
+
1074
+ MasterRecord uses **parameterized queries throughout** to prevent SQL injection:
568
1075
 
569
1076
  ```javascript
570
- // Complex query with multiple conditions
571
- let query = context.Order;
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:
572
1095
 
573
- // Base filters
574
- query = query.where(o => o.customer_id == $$, customerId);
575
- query = query.where(o => o.status == $$ || o.status == $$, 'pending', 'processing');
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
+ }
576
1102
 
577
- // Date range filter
578
- if (startDate) {
579
- query = query.where(o => o.created_at >= $$, startDate);
1103
+ return db.User.findById(userId);
580
1104
  }
581
- if (endDate) {
582
- query = query.where(o => o.created_at <= $$, endDate);
1105
+ ```
1106
+
1107
+ ## Troubleshooting
1108
+
1109
+ ### PostgreSQL Connection Issues
1110
+
1111
+ ```bash
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
1117
+
1118
+ # Error: Database does not exist
1119
+ createdb myapp
1120
+
1121
+ # Error: Authentication failed
1122
+ # Check pg_hba.conf and user permissions
1123
+ ```
1124
+
1125
+ ### MySQL Connection Issues
1126
+
1127
+ ```bash
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';
1131
+
1132
+ # Error: ER_ACCESS_DENIED_ERROR
1133
+ # Check user permissions
1134
+ GRANT ALL PRIVILEGES ON myapp.* TO 'user'@'localhost';
1135
+ ```
1136
+
1137
+ ### Migration Issues
1138
+
1139
+ ```bash
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/
1148
+
1149
+ # Type errors in migration
1150
+ # Check entity definitions match database types
1151
+ ```
1152
+
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();
583
1166
  }
584
1167
 
585
- // Sorting and pagination
586
- let orders = query
587
- .orderByDescending(o => o.created_at)
588
- .skip(page * pageSize)
589
- .take(pageSize)
590
- .toList();
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 }
1171
+ ```
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
591
1213
  ```
592
1214
 
593
- ### Important Notes
1215
+ ## License
1216
+
1217
+ MIT License - see [LICENSE](LICENSE) file for details.
1218
+
1219
+ ## Credits
1220
+
1221
+ Created by Alexander Rich
1222
+
1223
+ ## Support
594
1224
 
595
- - Each `where()` call adds an AND condition to the existing WHERE clause
596
- - Conditions are combined in the order they're added
597
- - The query is only executed when you call a terminal method: `toList()`, `single()`, `count()`
598
- - Query builders are reusable - calling `toList()` resets the builder for the next query
1225
+ - GitHub Issues: [Report bugs or request features](https://github.com/Tailor/MasterRecord/issues)
1226
+ - npm: [masterrecord](https://www.npmjs.com/package/masterrecord)
599
1227
 
1228
+ ---
600
1229
 
1230
+ **MasterRecord** - Code-first ORM for Node.js with multi-database support