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.
- package/.claude/settings.local.json +25 -1
- package/Entity/entityModel.js +6 -0
- package/Entity/entityTrackerModel.js +20 -3
- package/Entity/fieldTransformer.js +266 -0
- package/Migrations/migrationMySQLQuery.js +145 -1
- package/Migrations/migrationPostgresQuery.js +402 -0
- package/Migrations/migrationSQLiteQuery.js +145 -1
- package/Migrations/schema.js +131 -28
- package/QueryLanguage/queryMethods.js +193 -15
- package/QueryLanguage/queryParameters.js +136 -0
- package/QueryLanguage/queryScript.js +14 -5
- package/SQLLiteEngine.js +309 -19
- package/context.js +57 -12
- package/docs/INCLUDES_CLARIFICATION.md +202 -0
- package/docs/METHODS_REFERENCE.md +184 -0
- package/docs/MIGRATIONS_GUIDE.md +699 -0
- package/docs/POSTGRESQL_SETUP.md +415 -0
- package/examples/jsonArrayTransformer.js +215 -0
- package/mySQLEngine.js +249 -17
- package/package.json +6 -6
- package/postgresEngine.js +434 -491
- package/postgresSyncConnect.js +209 -0
- package/readme.md +1121 -265
- package/test/anyCommaStringTest.js +237 -0
- package/test/anyMethodTest.js +176 -0
- package/test/findByIdTest.js +227 -0
- package/test/includesFeatureTest.js +183 -0
- package/test/includesTransformTest.js +110 -0
- package/test/newMethodTest.js +330 -0
- package/test/newMethodUnitTest.js +320 -0
- package/test/parameterizedPlaceholderTest.js +159 -0
- package/test/postgresEngineTest.js +463 -0
- package/test/postgresIntegrationTest.js +381 -0
- package/test/securityTest.js +268 -0
- package/test/singleDollarPlaceholderTest.js +238 -0
- package/test/tablePrefixTest.js +100 -0
- package/test/transformerTest.js +287 -0
- package/test/verifyFindById.js +169 -0
- package/test/verifyNewMethod.js +191 -0
- package/test/whereChainingTest.js +88 -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");
|