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
@@ -0,0 +1,415 @@
1
+ # PostgreSQL Setup for MasterRecord
2
+
3
+ Complete guide for using PostgreSQL with MasterRecord ORM.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install pg@^8.16.3
9
+ ```
10
+
11
+ ## Basic Setup
12
+
13
+ ### 1. Initialize PostgreSQL Connection
14
+
15
+ ```javascript
16
+ const context = require('masterrecord/context');
17
+
18
+ // Create a new context
19
+ const db = new context();
20
+
21
+ // Configure PostgreSQL connection
22
+ await db.env({
23
+ type: 'postgres', // or 'postgresql'
24
+ host: 'localhost',
25
+ port: 5432,
26
+ database: 'your_database',
27
+ user: 'your_user',
28
+ password: 'your_password',
29
+ max: 20, // Maximum pool size
30
+ idleTimeoutMillis: 30000, // Idle connection timeout
31
+ connectionTimeoutMillis: 2000 // Connection timeout
32
+ });
33
+ ```
34
+
35
+ ### 2. Define Entities
36
+
37
+ ```javascript
38
+ const User = db.dbset('User')
39
+ .key('id').auto()
40
+ .field('name').string().notNull()
41
+ .field('email').string().notNull()
42
+ .field('age').integer().nullable()
43
+ .field('created_at').datetime()
44
+ .create();
45
+ ```
46
+
47
+ ### 3. Query with Parameterized Placeholders
48
+
49
+ MasterRecord automatically handles PostgreSQL's `$1, $2, $3...` placeholder format:
50
+
51
+ ```javascript
52
+ // Single parameter
53
+ const user = db.User
54
+ .where(u => u.email == $$, 'john@example.com')
55
+ .single();
56
+
57
+ // Multiple parameters
58
+ const users = db.User
59
+ .where(u => u.age > $$ && u.status == $$, 25, 'active')
60
+ .all();
61
+
62
+ // OR conditions with single $ placeholder
63
+ const results = db.User
64
+ .where(u => u.status == $ || u.status == null, 'active')
65
+ .all();
66
+ ```
67
+
68
+ ### 4. Insert Records
69
+
70
+ ```javascript
71
+ const newUser = db.User.new();
72
+ newUser.name = 'Jane Smith';
73
+ newUser.email = 'jane@example.com';
74
+ newUser.age = 28;
75
+ newUser.created_at = new Date();
76
+
77
+ // Save to database
78
+ await db.saveChanges();
79
+
80
+ // ID is available after saveChanges()
81
+ console.log(newUser.id); // PostgreSQL auto-increment ID
82
+ ```
83
+
84
+ ### 5. Update Records
85
+
86
+ ```javascript
87
+ const user = db.User
88
+ .where(u => u.id == $$, 123)
89
+ .single();
90
+
91
+ user.age = 30;
92
+ await db.saveChanges();
93
+ ```
94
+
95
+ ### 6. Delete Records
96
+
97
+ ```javascript
98
+ const user = db.User.findById(123);
99
+ db.remove(user);
100
+ await db.saveChanges();
101
+ ```
102
+
103
+ ## Advanced Features
104
+
105
+ ### Transactions
106
+
107
+ ```javascript
108
+ const PostgresSyncConnect = require('masterrecord/postgresSyncConnect');
109
+
110
+ const connection = new PostgresSyncConnect();
111
+ await connection.connect(config);
112
+
113
+ // Execute in transaction
114
+ const result = await connection.transaction(async (client) => {
115
+ // Insert
116
+ const userResult = await client.query(
117
+ 'INSERT INTO User (name, email) VALUES ($1, $2) RETURNING id',
118
+ ['Bob', 'bob@example.com']
119
+ );
120
+
121
+ // Update
122
+ await client.query(
123
+ 'UPDATE User SET verified = $1 WHERE id = $2',
124
+ [true, userResult.rows[0].id]
125
+ );
126
+
127
+ return userResult.rows[0].id;
128
+ });
129
+ ```
130
+
131
+ ### IN Clauses with .any()
132
+
133
+ ```javascript
134
+ // Array parameter
135
+ const users = db.User
136
+ .where(u => u.id.any($$), [1, 2, 3, 4, 5])
137
+ .all();
138
+
139
+ // Comma-separated string (auto-splits)
140
+ const ids = '10,20,30,40';
141
+ const users = db.User
142
+ .where(u => u.id.any($$), ids)
143
+ .all();
144
+ ```
145
+
146
+ ### Array Filtering with .includes()
147
+
148
+ ```javascript
149
+ const tags = ['javascript', 'node', 'postgres'];
150
+ const posts = db.Post
151
+ .where(p => $$.includes(p.category), tags)
152
+ .all();
153
+ ```
154
+
155
+ ### Find by Primary Key
156
+
157
+ ```javascript
158
+ // Convenience method - auto-detects primary key
159
+ const user = db.User.findById(123);
160
+
161
+ // Equivalent to:
162
+ const user = db.User
163
+ .where(u => u.id == $$, 123)
164
+ .single();
165
+ ```
166
+
167
+ ### Pagination
168
+
169
+ ```javascript
170
+ // Skip 20, take 10
171
+ const users = db.User
172
+ .orderBy('created_at')
173
+ .skip(20)
174
+ .take(10)
175
+ .all();
176
+
177
+ // PostgreSQL generates: LIMIT 10 OFFSET 20
178
+ ```
179
+
180
+ ### Joins
181
+
182
+ ```javascript
183
+ const userPosts = db.User
184
+ .join('Post', (u, p) => u.id == p.user_id)
185
+ .where(u => u.id == $$, 123)
186
+ .all();
187
+ ```
188
+
189
+ ### NULL Handling
190
+
191
+ ```javascript
192
+ // Find users with no email
193
+ const users = db.User
194
+ .where(u => u.email == null)
195
+ .all();
196
+
197
+ // Find users with email OR age is null
198
+ const users = db.User
199
+ .where(u => u.email != null)
200
+ .and(u => u.age == null)
201
+ .all();
202
+ ```
203
+
204
+ ## Connection Management
205
+
206
+ ### Health Check
207
+
208
+ ```javascript
209
+ const health = await connection.healthCheck();
210
+
211
+ if (health.healthy) {
212
+ console.log('Server time:', health.serverTime);
213
+ console.log('PostgreSQL version:', health.version);
214
+ console.log('Pool size:', health.poolSize);
215
+ console.log('Idle connections:', health.idleCount);
216
+ }
217
+ ```
218
+
219
+ ### Connection Info
220
+
221
+ ```javascript
222
+ const info = connection.getConnectionInfo();
223
+ console.log(`Connected to ${info.database} at ${info.host}:${info.port}`);
224
+ console.log(`Max connections: ${info.maxConnections}`);
225
+ ```
226
+
227
+ ### Close Connection
228
+
229
+ ```javascript
230
+ await connection.close();
231
+ ```
232
+
233
+ ## Placeholder Syntax Reference
234
+
235
+ MasterRecord uses double dollar signs (`$$`) for placeholders that get converted to PostgreSQL format:
236
+
237
+ | MasterRecord Syntax | PostgreSQL SQL | Parameters |
238
+ |---------------------|----------------|------------|
239
+ | `.where(u => u.id == $$, 5)` | `WHERE id = $1` | `[5]` |
240
+ | `.where(u => u.age > $$ && u.status == $$, 25, 'active')` | `WHERE age > $1 AND status = $2` | `[25, 'active']` |
241
+ | `.where(u => u.id.any($$), [1,2,3])` | `WHERE id IN ($1, $2, $3)` | `[1, 2, 3]` |
242
+
243
+ **Single `$` for OR conditions:**
244
+ ```javascript
245
+ .where(u => u.status == $ || u.status == null, 'active')
246
+ // Generates: WHERE status = $1 OR status IS NULL
247
+ ```
248
+
249
+ ## Field Transformers
250
+
251
+ Custom serialization for complex types:
252
+
253
+ ```javascript
254
+ const Post = db.dbset('Post')
255
+ .key('id').auto()
256
+ .field('title').string()
257
+ .field('tags').string().transform({
258
+ toDatabase: (value) => {
259
+ // Array → JSON string
260
+ return Array.isArray(value) ? JSON.stringify(value) : value;
261
+ },
262
+ fromDatabase: (value) => {
263
+ // JSON string → Array
264
+ return typeof value === 'string' ? JSON.parse(value) : value;
265
+ }
266
+ })
267
+ .create();
268
+
269
+ // Use as array in code
270
+ const post = db.Post.new();
271
+ post.tags = ['javascript', 'postgres', 'node'];
272
+ await db.saveChanges();
273
+
274
+ // Stored as: '["javascript","postgres","node"]'
275
+ // Retrieved as: ['javascript', 'postgres', 'node']
276
+ ```
277
+
278
+ ## Performance Tips
279
+
280
+ 1. **Use Connection Pooling**: Adjust `max` pool size based on your needs
281
+ 2. **Parameterized Queries**: Always use `$$` placeholders (automatic SQL injection protection)
282
+ 3. **Indexes**: Create indexes on frequently queried columns
283
+ 4. **Pagination**: Use `.skip()` and `.take()` for large result sets
284
+ 5. **Transactions**: Group related operations in transactions
285
+
286
+ ## Common Issues
287
+
288
+ ### Issue: "Cannot find module 'pg'"
289
+ **Solution**: Install pg library: `npm install pg@^8.16.3`
290
+
291
+ ### Issue: "Connection refused"
292
+ **Solution**: Ensure PostgreSQL is running on the specified host/port
293
+
294
+ ### Issue: "database does not exist"
295
+ **Solution**: Create the database first:
296
+ ```sql
297
+ CREATE DATABASE your_database;
298
+ ```
299
+
300
+ ### Issue: "password authentication failed"
301
+ **Solution**: Check your credentials and pg_hba.conf settings
302
+
303
+ ### Issue: "too many clients"
304
+ **Solution**: Reduce `max` pool size or increase PostgreSQL's max_connections
305
+
306
+ ## Migration from MySQL/SQLite
307
+
308
+ Key differences when migrating to PostgreSQL:
309
+
310
+ 1. **Placeholder Format**:
311
+ - MySQL/SQLite: `?`
312
+ - PostgreSQL: `$1, $2, $3...`
313
+ - MasterRecord handles this automatically with `$$`
314
+
315
+ 2. **Auto-increment**:
316
+ - MySQL: `AUTO_INCREMENT`
317
+ - PostgreSQL: `SERIAL` or `BIGSERIAL`
318
+ - MasterRecord uses `.auto()` for both
319
+
320
+ 3. **Boolean Type**:
321
+ - SQLite: 0/1
322
+ - PostgreSQL: true/false
323
+ - MasterRecord handles type coercion
324
+
325
+ 4. **Date/Time**:
326
+ - Both use Date objects in JavaScript
327
+ - PostgreSQL has more precise timestamp handling
328
+
329
+ 5. **RETURNING Clause**:
330
+ - PostgreSQL requires `RETURNING id` for INSERT
331
+ - MasterRecord adds this automatically
332
+
333
+ ## Testing
334
+
335
+ Run PostgreSQL tests:
336
+
337
+ ```bash
338
+ # Unit tests (no database required)
339
+ node test/postgresEngineTest.js
340
+
341
+ # Integration tests (requires PostgreSQL running)
342
+ node test/postgresIntegrationTest.js
343
+ ```
344
+
345
+ ## Version Compatibility
346
+
347
+ - **MasterRecord**: 0.3.0+
348
+ - **pg (node-postgres)**: 8.16.3+
349
+ - **PostgreSQL Server**: 9.6+ (tested with 12+, 13+, 14+)
350
+ - **Node.js**: 14+ (async/await support required)
351
+
352
+ ## Complete Example
353
+
354
+ ```javascript
355
+ const context = require('masterrecord/context');
356
+
357
+ async function main() {
358
+ // Initialize
359
+ const db = new context();
360
+ await db.env({
361
+ type: 'postgres',
362
+ host: 'localhost',
363
+ port: 5432,
364
+ database: 'myapp',
365
+ user: 'postgres',
366
+ password: 'password',
367
+ max: 20
368
+ });
369
+
370
+ // Define entity
371
+ const User = db.dbset('User')
372
+ .key('id').auto()
373
+ .field('name').string().notNull()
374
+ .field('email').string().notNull()
375
+ .field('age').integer()
376
+ .create();
377
+
378
+ // Create
379
+ const newUser = db.User.new();
380
+ newUser.name = 'Alice';
381
+ newUser.email = 'alice@example.com';
382
+ newUser.age = 25;
383
+ await db.saveChanges();
384
+
385
+ // Read
386
+ const user = db.User.findById(newUser.id);
387
+ console.log(user.name); // "Alice"
388
+
389
+ // Update
390
+ user.age = 26;
391
+ await db.saveChanges();
392
+
393
+ // Query
394
+ const adults = db.User
395
+ .where(u => u.age >= $$, 18)
396
+ .orderBy('name')
397
+ .all();
398
+
399
+ // Delete
400
+ db.remove(user);
401
+ await db.saveChanges();
402
+ }
403
+
404
+ main();
405
+ ```
406
+
407
+ ## Support
408
+
409
+ For issues or questions:
410
+ - GitHub: [MasterRecord Issues](https://github.com/yourusername/MasterRecord/issues)
411
+ - Documentation: [docs/](../docs/)
412
+
413
+ ## License
414
+
415
+ MIT License - see LICENSE file for details
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Real-World Example: Storing JavaScript Arrays as JSON Strings
3
+ *
4
+ * This example demonstrates how to use field transformers to store
5
+ * JavaScript arrays in database string columns, solving the common
6
+ * problem of "Type validation blocking array-to-JSON transformation"
7
+ *
8
+ * BEFORE (using raw SQL - not ideal):
9
+ * - Some fields saved through ORM
10
+ * - Array fields saved via raw SQL to bypass validation
11
+ * - Inconsistent, error-prone, loses ORM benefits
12
+ *
13
+ * AFTER (using transformers - production-ready):
14
+ * - All fields saved through ORM consistently
15
+ * - Arrays automatically transformed to/from JSON
16
+ * - Type-safe, maintainable, elegant
17
+ */
18
+
19
+ const masterrecord = require('masterrecord');
20
+
21
+ // ============================================================================
22
+ // 1. Define Entity with Transformers
23
+ // ============================================================================
24
+
25
+ class User {
26
+ constructor() {
27
+ // Regular fields - no transformation needed
28
+ this.id = { type: "integer", primary: true, auto: true };
29
+ this.name = { type: "string" };
30
+ this.email = { type: "string" };
31
+ this.role = { type: "string" };
32
+
33
+ // 🔥 ARRAY FIELDS WITH TRANSFORMERS
34
+ // These fields store arrays as JSON strings in the database
35
+ this.certified_models = {
36
+ type: "string", // Database column type
37
+ nullable: true,
38
+ transform: {
39
+ // Transform JavaScript array → JSON string for database
40
+ toDatabase: (value) => {
41
+ if (value === null || value === undefined) return null;
42
+ if (Array.isArray(value)) return JSON.stringify(value);
43
+ // Already a string (maybe from edit scenario)
44
+ return value;
45
+ },
46
+
47
+ // Transform JSON string → JavaScript array from database
48
+ fromDatabase: (value) => {
49
+ if (!value) return [];
50
+ if (Array.isArray(value)) return value; // Already parsed
51
+ try {
52
+ return JSON.parse(value);
53
+ } catch {
54
+ console.warn(`Failed to parse certified_models: ${value}`);
55
+ return [];
56
+ }
57
+ }
58
+ }
59
+ };
60
+
61
+ this.certified_agent_types = {
62
+ type: "string",
63
+ nullable: true,
64
+ transform: {
65
+ toDatabase: (value) => {
66
+ if (value === null || value === undefined) return null;
67
+ if (Array.isArray(value)) return JSON.stringify(value);
68
+ return value;
69
+ },
70
+ fromDatabase: (value) => {
71
+ if (!value) return [];
72
+ if (Array.isArray(value)) return value;
73
+ try {
74
+ return JSON.parse(value);
75
+ } catch {
76
+ console.warn(`Failed to parse certified_agent_types: ${value}`);
77
+ return [];
78
+ }
79
+ }
80
+ }
81
+ };
82
+
83
+ // Regular numeric field
84
+ this.calibration_score = { type: "integer", nullable: true };
85
+ }
86
+ }
87
+
88
+ // ============================================================================
89
+ // 2. Create Context
90
+ // ============================================================================
91
+
92
+ class AppContext extends masterrecord.context {
93
+ constructor(config) {
94
+ super(config);
95
+ }
96
+
97
+ onConfig(db) {
98
+ this.User = this.dbset(User, "User");
99
+ }
100
+ }
101
+
102
+ // ============================================================================
103
+ // 3. Usage Example - Creating a User with Arrays
104
+ // ============================================================================
105
+
106
+ console.log("╔════════════════════════════════════════════════════════════════╗");
107
+ console.log("║ JSON Array Transformer - Real-World Example ║");
108
+ console.log("╚════════════════════════════════════════════════════════════════╝\n");
109
+
110
+ console.log("📝 Scenario: User certification management system");
111
+ console.log(" - Users can be certified for multiple AI models");
112
+ console.log(" - Users can handle multiple agent types");
113
+ console.log(" - Arrays stored as JSON strings in database\n");
114
+
115
+ // Simulated context (in real app, this would connect to actual database)
116
+ console.log("1️⃣ Creating new user with array fields");
117
+ console.log("──────────────────────────────────────────────────");
118
+
119
+ const user = new User();
120
+ user.name = "Alex Rich";
121
+ user.email = "alex@example.com";
122
+ user.role = "calibrator";
123
+
124
+ // ✨ Arrays assigned naturally - NO raw SQL needed!
125
+ user.certified_models = [1, 2, 5, 8]; // Array of model IDs
126
+ user.certified_agent_types = [10, 20, 30]; // Array of agent type IDs
127
+ user.calibration_score = 95;
128
+
129
+ console.log(` Name: ${user.name}`);
130
+ console.log(` Certified Models (array): [${user.certified_models.join(', ')}]`);
131
+ console.log(` Certified Agent Types (array): [${user.certified_agent_types.join(', ')}]`);
132
+ console.log(` Calibration Score: ${user.calibration_score}\n`);
133
+
134
+ // When saved, transformers automatically convert:
135
+ // [1, 2, 5, 8] → "[1,2,5,8]" (stored in DB)
136
+ console.log("2️⃣ What happens when saving");
137
+ console.log("──────────────────────────────────────────────────");
138
+ console.log(" User provides: [1, 2, 5, 8]");
139
+ console.log(" ↓");
140
+ console.log(" Transformer (toDatabase): [1, 2, 5, 8] → '[1,2,5,8]'");
141
+ console.log(" ↓");
142
+ console.log(" Type Validation: string '[1,2,5,8]' ✓ matches type: 'string'");
143
+ console.log(" ↓");
144
+ console.log(" Database Stores: '[1,2,5,8]' (as string column)\n");
145
+
146
+ // Standard save - no raw SQL required!
147
+ // context.User.add(user);
148
+ // context.saveChanges();
149
+
150
+ console.log("3️⃣ What happens when loading");
151
+ console.log("──────────────────────────────────────────────────");
152
+ console.log(" Database Returns: '[1,2,5,8]' (string)");
153
+ console.log(" ↓");
154
+ console.log(" Transformer (fromDatabase): '[1,2,5,8]' → [1, 2, 5, 8]");
155
+ console.log(" ↓");
156
+ console.log(" Application Receives: [1, 2, 5, 8] (JavaScript array)");
157
+ console.log(" ↓");
158
+ console.log(" Code: user.certified_models.includes(2) → true ✓\n");
159
+
160
+ // When loaded from DB, transformers automatically convert back:
161
+ // "[1,2,5,8]" → [1, 2, 5, 8] (JavaScript array)
162
+ // const users = context.User.where(u => u.id == $$, userId).toList();
163
+ // console.log(users[0].certified_models); // [1, 2, 5, 8]
164
+
165
+ console.log("4️⃣ Updating existing user");
166
+ console.log("──────────────────────────────────────────────────");
167
+ console.log(" const user = context.User.where(u => u.id == $$, 14).single();");
168
+ console.log(" user.certified_models = [1, 2, 5, 8, 12]; // Add model 12");
169
+ console.log(" context.saveChanges(); // ✓ Works perfectly!\n");
170
+
171
+ console.log("5️⃣ Benefits over raw SQL approach");
172
+ console.log("──────────────────────────────────────────────────");
173
+ console.log(" ✅ Consistent ORM usage (no raw SQL needed)");
174
+ console.log(" ✅ Automatic transformation (transparent to application code)");
175
+ console.log(" ✅ Type-safe (validation happens after transformation)");
176
+ console.log(" ✅ Maintainable (transformation logic in one place)");
177
+ console.log(" ✅ Testable (transformers are pure functions)");
178
+ console.log(" ✅ Works with all ORM features (tracking, relationships, etc.)\n");
179
+
180
+ console.log("6️⃣ Common Patterns");
181
+ console.log("──────────────────────────────────────────────────");
182
+
183
+ console.log("\n Pattern A: Simple Arrays");
184
+ console.log(" ─────────────────────────");
185
+ console.log(" certified_models: [1, 2, 3] → '[1,2,3]'");
186
+
187
+ console.log("\n Pattern B: String Arrays");
188
+ console.log(" ─────────────────────────");
189
+ console.log(" tags: ['urgent', 'review'] → '[\"urgent\",\"review\"]'");
190
+
191
+ console.log("\n Pattern C: Complex Objects");
192
+ console.log(" ─────────────────────────");
193
+ console.log(" metadata: {key: 'value'} → '{\"key\":\"value\"}'");
194
+ console.log(" transform: { toDatabase: JSON.stringify, fromDatabase: JSON.parse }");
195
+
196
+ console.log("\n Pattern D: Defaults for Null");
197
+ console.log(" ─────────────────────────");
198
+ console.log(" fromDatabase: (v) => v ? JSON.parse(v) : []");
199
+
200
+ console.log("\n\n╔════════════════════════════════════════════════════════════════╗");
201
+ console.log("║ Summary ║");
202
+ console.log("╚════════════════════════════════════════════════════════════════╝\n");
203
+
204
+ console.log("✨ PROBLEM SOLVED!");
205
+ console.log("\nBefore: Had to use raw SQL to bypass type validation");
206
+ console.log(" const sql = `UPDATE User SET certified_models = ? WHERE id = ?`;");
207
+ console.log(" context.User.raw(sql, [jsonString, userId]);");
208
+ console.log("\nAfter: Use ORM naturally with automatic transformation");
209
+ console.log(" user.certified_models = [1, 2, 3];");
210
+ console.log(" context.saveChanges();");
211
+
212
+ console.log("\n🎯 Use Case: This example solves the exact problem from the");
213
+ console.log(" BookBag calibration system where arrays needed to bypass ORM.\n");
214
+
215
+ console.log("📖 See readme.md 'Field Transformers' section for full documentation.\n");