masterrecord 0.2.33 → 0.2.36

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.
@@ -3,7 +3,13 @@
3
3
  "allow": [
4
4
  "Read(//Users/alexanderrich/Documents/development/bookbaghq/bookbag-ce/components/models/app/models/**)",
5
5
  "Read(//Users/alexanderrich/Documents/development/bookbaghq/bookbag-ce/components/models/app/controllers/api/**)",
6
- "Read(//Users/alexanderrich/Documents/development/bookbaghq/bookbag-ce/config/environments/**)"
6
+ "Read(//Users/alexanderrich/Documents/development/bookbaghq/bookbag-ce/config/environments/**)",
7
+ "Bash(node test/tablePrefixTest.js:*)",
8
+ "Bash(node test/whereChainingTest.js:*)",
9
+ "Bash(npm whoami:*)",
10
+ "Bash(npm pkg fix:*)",
11
+ "Bash(~/.npmrc)",
12
+ "Bash(cat:*)"
7
13
  ],
8
14
  "deny": [],
9
15
  "ask": []
@@ -140,7 +140,7 @@ class queryScript{
140
140
  else if(type === "where"){
141
141
  // If where already exists, merge new expressions into existing where so multiple
142
142
  // chained where(...) calls combine into a single WHERE clause (joined by AND).
143
- if(obj.where && obj[entityName] && cachedExpr[entityName]){
143
+ if(obj.where && obj.where[entityName] && cachedExpr[entityName]){
144
144
  const existingQuery = obj.where[entityName].query || {};
145
145
  const incomingQuery = cachedExpr[entityName].query || {};
146
146
  const existingExprs = existingQuery.expressions || [];
package/context.js CHANGED
@@ -1,4 +1,4 @@
1
- // Version 0.0.15
1
+ // Version 0.0.17
2
2
 
3
3
  var modelBuilder = require('./Entity/entityModelBuilder');
4
4
  var query = require('masterrecord/QueryLanguage/queryMethods');
@@ -24,6 +24,7 @@ class context {
24
24
  __relationshipModels = [];
25
25
  __environment = "";
26
26
  __name = "";
27
+ tablePrefix = "";
27
28
  isSQLite = false;
28
29
  isMySQL = false;
29
30
  isPostgres = false;
@@ -146,7 +147,23 @@ class context {
146
147
  const envFileB = path.join(directFolder, `${envType}.json`);
147
148
  const picked = fs.existsSync(envFileA) ? envFileA : (fs.existsSync(envFileB) ? envFileB : null);
148
149
  if(picked){
149
- file = { file: picked, rootFolder: path.dirname(path.dirname(picked)) };
150
+ // Smart root folder detection for plugin paths
151
+ // If the env file is in a bb-plugins/<plugin-name>/config/environments/ structure,
152
+ // we should set rootFolder to the project root, not the plugin's config folder
153
+ let detectedRoot = path.dirname(path.dirname(picked));
154
+
155
+ // Check if we're in a bb-plugins structure
156
+ const pickedParts = picked.split(path.sep);
157
+ const pluginsIndex = pickedParts.findIndex(part => part === 'bb-plugins');
158
+
159
+ if(pluginsIndex !== -1 && pluginsIndex + 3 < pickedParts.length) {
160
+ // We're in bb-plugins/<plugin-name>/config/environments/...
161
+ // Set rootFolder to the project root (parent of bb-plugins)
162
+ const projectRootParts = pickedParts.slice(0, pluginsIndex);
163
+ detectedRoot = projectRootParts.join(path.sep) || path.sep;
164
+ }
165
+
166
+ file = { file: picked, rootFolder: detectedRoot };
150
167
  }
151
168
  }
152
169
  if(!file){
@@ -332,7 +349,14 @@ class context {
332
349
 
333
350
  dbset(model, name){
334
351
  var validModel = modelBuilder.create(model);
335
- validModel.__name = name === undefined ? model.name : name;
352
+ var tableName = name === undefined ? model.name : name;
353
+
354
+ // Apply tablePrefix if set
355
+ if(this.tablePrefix && typeof this.tablePrefix === 'string' && this.tablePrefix.length > 0){
356
+ tableName = this.tablePrefix + tableName;
357
+ }
358
+
359
+ validModel.__name = tableName;
336
360
  this.__entities.push(validModel); // model object
337
361
  var buildMod = tools.createNewInstance(validModel, query, this);
338
362
  this.__builderEntities.push(buildMod); // query builder entites
@@ -0,0 +1,190 @@
1
+ # belongsTo Relationships in MasterRecord
2
+
3
+ ## Overview
4
+
5
+ When you define a `belongsTo` relationship in MasterRecord, the ORM creates **two separate properties** on your model instances:
6
+
7
+ 1. **The foreign key field** (e.g., `document_id`) - the actual database column value
8
+ 2. **The relationship property** (e.g., `Document`) - for accessing/setting the related object or ID
9
+
10
+ ## How belongsTo Works Internally
11
+
12
+ ### Entity Definition
13
+
14
+ ```javascript
15
+ // DocumentChunk entity
16
+ DocumentChunk(db) {
17
+ db.integer("id").primary().notNull().autoIncrement();
18
+ db.string("content");
19
+ db.belongsTo("Document", "document_id"); // Creates both Document and document_id properties
20
+ }
21
+ ```
22
+
23
+ The `belongsTo("Document", "document_id")` call:
24
+ - **Does NOT** create a separate `document_id` field definition
25
+ - Tells MasterRecord that `document_id` is a foreign key to the `Document` entity
26
+ - Creates a relationship property named `Document`
27
+
28
+ ### Model Instance Properties
29
+
30
+ When you query records or create new instances, **both properties are available**:
31
+
32
+ ```javascript
33
+ const chunk = context.DocumentChunk.where("r => r.id == 1").single();
34
+
35
+ // Both of these work:
36
+ console.log(chunk.document_id); // Accesses the foreign key value (e.g., 5)
37
+ console.log(chunk.Document); // Accesses the relationship (object or ID)
38
+ ```
39
+
40
+ ## Usage Patterns
41
+
42
+ ### Pattern 1: INSERT Operations (Setting Foreign Keys)
43
+
44
+ When **creating new records**, use the **relationship property name**:
45
+
46
+ ```javascript
47
+ const chunk = new DocumentChunk();
48
+ chunk.content = "Sample content";
49
+ chunk.Document = documentId; // ✅ CORRECT - Use relationship property for INSERT
50
+
51
+ context.DocumentChunk.add(chunk);
52
+ context.saveChanges();
53
+ ```
54
+
55
+ **Why?** The `belongsTo` setter (entityTrackerModel.js:98-105) triggers dirty field tracking and marks the model as modified when you set the relationship property.
56
+
57
+ ### Pattern 2: READ Operations (Accessing Foreign Keys)
58
+
59
+ When **reading or filtering records**, use the **foreign key field name**:
60
+
61
+ ```javascript
62
+ // ✅ CORRECT - Use foreign key for filtering
63
+ const chunks = context.DocumentChunk
64
+ .where(`r => r.document_id == ${docId}`)
65
+ .toList();
66
+
67
+ // ✅ CORRECT - Use foreign key in JavaScript filters
68
+ const filtered = allChunks.filter(c => documentIds.includes(c.document_id));
69
+
70
+ // ✅ CORRECT - Access foreign key value directly
71
+ if (chunk.document_id === targetId) {
72
+ // ...
73
+ }
74
+ ```
75
+
76
+ **Why?** When records are loaded from the database, the raw column data includes `document_id`. The `build` method (entityTrackerModel.js:21-59) creates getters/setters for all non-relationship fields from the database result.
77
+
78
+ ### Pattern 3: UPDATE Operations
79
+
80
+ For updates, you can use either property:
81
+
82
+ ```javascript
83
+ // Using relationship property (preferred for consistency)
84
+ chunk.Document = newDocumentId;
85
+
86
+ // Using foreign key directly (also works)
87
+ chunk.document_id = newDocumentId;
88
+ ```
89
+
90
+ Both will work because:
91
+ - Setting `chunk.Document` triggers the relationship setter (line 98-105)
92
+ - Setting `chunk.document_id` directly modifies the underlying value
93
+
94
+ ## Code References
95
+
96
+ The behavior is implemented in `/Entity/entityTrackerModel.js`:
97
+
98
+ ### Creating Non-Relationship Properties (lines 21-59)
99
+
100
+ ```javascript
101
+ for (const [modelField, modelFieldValue] of modelFields) {
102
+ if(!$that._isRelationship(currentEntity[modelField])){
103
+ // Creates getter/setter for document_id (from database column)
104
+ modelClass["__proto__"]["_" + modelField] = modelFieldValue;
105
+ Object.defineProperty(modelClass, modelField, {
106
+ set: function(value) {
107
+ modelClass.__state = "modified";
108
+ modelClass.__dirtyFields.push(modelField);
109
+ this["__proto__"]["_" + modelField] = value;
110
+ },
111
+ get: function() {
112
+ return this["__proto__"]["_" + modelField];
113
+ }
114
+ });
115
+ }
116
+ }
117
+ ```
118
+
119
+ This loop processes fields from the database result (like `document_id`) and makes them accessible as properties.
120
+
121
+ ### Creating Relationship Properties (lines 89-149)
122
+
123
+ ```javascript
124
+ if($that._isRelationship(currentEntity[entityField])){
125
+ // Creates getter/setter for Document (relationship property)
126
+ Object.defineProperty(modelClass, entityField, {
127
+ set: function(value) {
128
+ if(typeof value === "string" || typeof value === "number" ||
129
+ typeof value === "boolean" || typeof value === "bigint") {
130
+ modelClass.__state = "modified";
131
+ modelClass.__dirtyFields.push(entityField);
132
+ modelClass.__context.__track(modelClass);
133
+ }
134
+ this["__proto__"]["_" + entityField] = value;
135
+ },
136
+ get: function() {
137
+ // Complex getter logic for lazy loading, etc.
138
+ return this["__proto__"]["_" + entityField];
139
+ }
140
+ });
141
+
142
+ if(currentEntity[entityField].relationshipType === "belongsTo"){
143
+ // Initialize relationship value from database result
144
+ if(currentModel[entityField]){
145
+ modelClass[entityField] = currentModel[entityField];
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
151
+ This creates the `Document` property for relationship access.
152
+
153
+ ## Real-World Example from bookbag-ce
154
+
155
+ In `bookbag-ce`, the `UserChat` entity demonstrates this pattern:
156
+
157
+ ```javascript
158
+ // Entity definition
159
+ UserChat(db) {
160
+ db.integer("id").primary().notNull().autoIncrement();
161
+ db.belongsTo("Chat", "chat_id"); // Only defines relationship, not chat_id field
162
+ }
163
+
164
+ // Controller usage (chatController.js:66)
165
+ const messages = context.UserChat
166
+ .where(`r => r.chat_id == ${chatId}`) // ✅ Uses foreign key for filtering
167
+ .toList();
168
+ ```
169
+
170
+ Even though `chat_id` is never explicitly defined as a field, it's accessible because:
171
+ 1. The database returns `chat_id` as a column
172
+ 2. `entityTrackerModel.build()` creates properties for all columns from the result
173
+ 3. The `belongsTo` relationship doesn't prevent the foreign key from being accessible
174
+
175
+ ## Summary
176
+
177
+ | Operation | Property to Use | Example |
178
+ |-----------|----------------|---------|
179
+ | **INSERT** (setting FK) | Relationship property | `chunk.Document = docId` |
180
+ | **READ** (accessing FK) | Foreign key field | `chunk.document_id` |
181
+ | **FILTER** (where clauses) | Foreign key field | `r.document_id == 5` |
182
+ | **UPDATE** (changing FK) | Either (prefer relationship) | `chunk.Document = newId` |
183
+
184
+ ## Key Takeaways
185
+
186
+ 1. **belongsTo creates TWO properties**, not one
187
+ 2. The **foreign key column** (`document_id`) is automatically available from database results
188
+ 3. The **relationship property** (`Document`) is created by the ORM for setting relationships
189
+ 4. **Use the relationship property for INSERT**, foreign key field for READ/FILTER
190
+ 5. Both properties reference the same underlying foreign key value in the database
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.2.33",
3
+ "version": "0.2.36",
4
4
  "description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
5
5
  "main": "MasterRecord.js",
6
6
  "bin": {
7
- "masterrecord": "./Migrations/cli.js"
7
+ "masterrecord": "Migrations/cli.js"
8
8
  },
9
9
  "scripts": {
10
10
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -27,12 +27,12 @@
27
27
  "masterrecord"
28
28
  ],
29
29
  "dependencies": {
30
- "commander": "^14.0.1",
31
- "glob": "^11.0.3",
30
+ "commander": "^14.0.2",
31
+ "glob": "^13.0.0",
32
32
  "deep-object-diff": "^1.1.9",
33
33
  "pg": "^8.16.3",
34
34
  "sync-mysql2": "^1.0.7",
35
35
  "app-root-path": "^3.1.0",
36
- "better-sqlite3": "^12.4.1"
36
+ "better-sqlite3": "^12.5.0"
37
37
  }
38
38
  }
package/readme.md CHANGED
@@ -369,6 +369,232 @@ set master=development && masterrecord update-database-all
369
369
  - For SQLite contexts, the `connection` path will be created if the directory does not exist.
370
370
  - For MySQL contexts, `ensure-database <ContextName>` can create the DB (permissions required) before migrations run.
371
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>`.
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>`.
373
+
374
+ ## Table Prefixes
375
+
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
380
+
381
+ ### Using tablePrefix
382
+
383
+ Set the `tablePrefix` property in your Context constructor before calling `dbset()`:
384
+
385
+ ```javascript
386
+ var masterrecord = require('masterrecord');
387
+ const User = require('./models/User');
388
+ const Post = require('./models/Post');
389
+
390
+ class AppContext extends masterrecord.context {
391
+ constructor() {
392
+ super();
393
+
394
+ // Set table prefix
395
+ this.tablePrefix = 'myapp_';
396
+
397
+ // Configure environment
398
+ this.env('config/environments');
399
+
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
+ }
405
+
406
+ module.exports = AppContext;
407
+ ```
408
+
409
+ ### How it works
410
+
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
416
+
417
+ ### Example with custom table names
418
+
419
+ ```javascript
420
+ class AppContext extends masterrecord.context {
421
+ constructor() {
422
+ super();
423
+ this.tablePrefix = 'myapp_';
424
+ this.env('config/environments');
425
+
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
429
+ }
430
+ }
431
+ ```
432
+
433
+ ### Plugin example
434
+
435
+ Perfect for plugin systems where each plugin needs isolated tables:
436
+
437
+ ```javascript
438
+ // RAG Plugin Context
439
+ class RagContext extends masterrecord.context {
440
+ constructor() {
441
+ super();
442
+
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
451
+ }
452
+ }
453
+ ```
454
+
455
+ ### Migrations with table prefixes
456
+
457
+ Table prefixes work seamlessly with migrations:
458
+
459
+ ```bash
460
+ # Enable migrations (prefix is read from your Context)
461
+ master=development masterrecord enable-migrations AppContext
462
+
463
+ # Create migration (tables will have prefix in migration file)
464
+ master=development masterrecord add-migration Init AppContext
465
+
466
+ # Apply migration (creates prefixed tables)
467
+ master=development masterrecord update-database AppContext
468
+ ```
469
+
470
+ The generated migration files will reference the prefixed table names, so you don't need to manually add prefixes in your migration code.
471
+
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
477
+
478
+ ## Query Method Chaining
479
+
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.
481
+
482
+ ### Chaining Multiple where() Clauses
483
+
484
+ Multiple `where()` calls are automatically combined with AND logic:
485
+
486
+ ```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);
492
+
493
+ // Add second condition (combines with AND)
494
+ query = query.where(t => t.status == $$, 'pending');
495
+
496
+ // Add ordering and execute
497
+ let tasks = query.orderBy(t => t.created_at).toList();
498
+ ```
499
+
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
506
+ ```
507
+
508
+ ### Dynamic Query Building
509
+
510
+ This is especially useful for building queries based on conditional logic:
511
+
512
+ ```javascript
513
+ let query = context.User;
514
+
515
+ // Always apply base filter
516
+ query = query.where(u => u.is_active == true);
517
+
518
+ // Conditionally add filters
519
+ if (searchTerm) {
520
+ query = query.where(u => u.name.like($$), `%${searchTerm}%`);
521
+ }
522
+
523
+ if (roleFilter) {
524
+ query = query.where(u => u.role == $$, roleFilter);
525
+ }
526
+
527
+ // Add pagination
528
+ query = query
529
+ .orderBy(u => u.created_at)
530
+ .skip(offset)
531
+ .take(limit);
532
+
533
+ // Execute query
534
+ let users = query.toList();
535
+ ```
536
+
537
+ ### Chainable Query Methods
538
+
539
+ All of these methods return the query builder and can be chained:
540
+
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
549
+
550
+ ### Combining with OR Logic
551
+
552
+ For OR conditions within a single where clause, use the `||` operator:
553
+
554
+ ```javascript
555
+ // Single where with OR
556
+ let tasks = context.Task
557
+ .where(t => t.status == 'pending' || t.status == 'in_progress')
558
+ .toList();
559
+ ```
560
+
561
+ **Generated SQL:**
562
+ ```sql
563
+ SELECT * FROM Task AS t
564
+ WHERE (t.status = 'pending' OR t.status = 'in_progress')
565
+ ```
566
+
567
+ ### Complex Example
568
+
569
+ ```javascript
570
+ // Complex query with multiple conditions
571
+ let query = context.Order;
572
+
573
+ // Base filters
574
+ query = query.where(o => o.customer_id == $$, customerId);
575
+ query = query.where(o => o.status == $$ || o.status == $$, 'pending', 'processing');
576
+
577
+ // Date range filter
578
+ if (startDate) {
579
+ query = query.where(o => o.created_at >= $$, startDate);
580
+ }
581
+ if (endDate) {
582
+ query = query.where(o => o.created_at <= $$, endDate);
583
+ }
584
+
585
+ // Sorting and pagination
586
+ let orders = query
587
+ .orderByDescending(o => o.created_at)
588
+ .skip(page * pageSize)
589
+ .take(pageSize)
590
+ .toList();
591
+ ```
592
+
593
+ ### Important Notes
594
+
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
373
599
 
374
600
 
@@ -0,0 +1,100 @@
1
+ // Test for tablePrefix functionality
2
+ // Run with: node test/tablePrefixTest.js
3
+
4
+ var masterrecord = require('../MasterRecord');
5
+
6
+ // Define a simple test model
7
+ class TestUser extends masterrecord.model {
8
+ id(db) {
9
+ db.integer().primaryKey().autoIncrement();
10
+ }
11
+
12
+ name(db) {
13
+ db.string();
14
+ }
15
+
16
+ email(db) {
17
+ db.string();
18
+ }
19
+ }
20
+
21
+ class TestPost extends masterrecord.model {
22
+ id(db) {
23
+ db.integer().primaryKey().autoIncrement();
24
+ }
25
+
26
+ title(db) {
27
+ db.string();
28
+ }
29
+ }
30
+
31
+ // Test 1: Context WITHOUT prefix
32
+ class TestContextNoPrefix extends masterrecord.context {
33
+ constructor() {
34
+ super();
35
+ this.dbset(TestUser, 'User');
36
+ this.dbset(TestPost, 'Post');
37
+ }
38
+ }
39
+
40
+ // Test 2: Context WITH prefix
41
+ class TestContextWithPrefix extends masterrecord.context {
42
+ constructor() {
43
+ super();
44
+ this.tablePrefix = 'myapp_';
45
+ this.dbset(TestUser, 'User');
46
+ this.dbset(TestPost, 'Post');
47
+ }
48
+ }
49
+
50
+ // Test 3: Context WITH prefix using default names
51
+ class TestContextWithPrefixDefault extends masterrecord.context {
52
+ constructor() {
53
+ super();
54
+ this.tablePrefix = 'test_';
55
+ this.dbset(TestUser);
56
+ this.dbset(TestPost);
57
+ }
58
+ }
59
+
60
+ // Run tests
61
+ console.log('=== MasterRecord tablePrefix Tests ===\n');
62
+
63
+ // Test 1: No prefix
64
+ console.log('Test 1: Context without prefix');
65
+ const ctx1 = new TestContextNoPrefix();
66
+ const user1TableName = ctx1.__entities[0].__name;
67
+ const post1TableName = ctx1.__entities[1].__name;
68
+ console.log(` User table name: ${user1TableName}`);
69
+ console.log(` Post table name: ${post1TableName}`);
70
+ console.log(` Expected: User, Post`);
71
+ console.log(` Result: ${user1TableName === 'User' && post1TableName === 'Post' ? '✓ PASS' : '✗ FAIL'}\n`);
72
+
73
+ // Test 2: With prefix and custom names
74
+ console.log('Test 2: Context with prefix "myapp_" and custom table names');
75
+ const ctx2 = new TestContextWithPrefix();
76
+ const user2TableName = ctx2.__entities[0].__name;
77
+ const post2TableName = ctx2.__entities[1].__name;
78
+ console.log(` User table name: ${user2TableName}`);
79
+ console.log(` Post table name: ${post2TableName}`);
80
+ console.log(` Expected: myapp_User, myapp_Post`);
81
+ console.log(` Result: ${user2TableName === 'myapp_User' && post2TableName === 'myapp_Post' ? '✓ PASS' : '✗ FAIL'}\n`);
82
+
83
+ // Test 3: With prefix using default names
84
+ console.log('Test 3: Context with prefix "test_" and default table names');
85
+ const ctx3 = new TestContextWithPrefixDefault();
86
+ const user3TableName = ctx3.__entities[0].__name;
87
+ const post3TableName = ctx3.__entities[1].__name;
88
+ console.log(` TestUser table name: ${user3TableName}`);
89
+ console.log(` TestPost table name: ${post3TableName}`);
90
+ console.log(` Expected: test_TestUser, test_TestPost`);
91
+ console.log(` Result: ${user3TableName === 'test_TestUser' && post3TableName === 'test_TestPost' ? '✓ PASS' : '✗ FAIL'}\n`);
92
+
93
+ // Test 4: Verify query builder has correct table name
94
+ console.log('Test 4: Query builder references');
95
+ console.log(` ctx2.myapp_User exists: ${ctx2.myapp_User !== undefined ? '✓ PASS' : '✗ FAIL'}`);
96
+ console.log(` ctx2.myapp_Post exists: ${ctx2.myapp_Post !== undefined ? '✓ PASS' : '✗ FAIL'}`);
97
+ console.log(` ctx3.test_TestUser exists: ${ctx3.test_TestUser !== undefined ? '✓ PASS' : '✗ FAIL'}`);
98
+ console.log(` ctx3.test_TestPost exists: ${ctx3.test_TestPost !== undefined ? '✓ PASS' : '✗ FAIL'}\n`);
99
+
100
+ console.log('=== Tests Complete ===');
@@ -0,0 +1,88 @@
1
+ // Test for where() chaining functionality
2
+ // This test verifies that multiple .where() calls combine properly with AND
3
+
4
+ var queryScript = require('../QueryLanguage/queryScript');
5
+
6
+ console.log('=== MasterRecord where() Chaining Test ===\n');
7
+
8
+ // Simulate what happens in the query builder
9
+ const qs = new queryScript();
10
+
11
+ // Test Case: Two chained where() calls (like the user's example)
12
+ // let query = this._qaContext.QaTask;
13
+ // query = query.where(t => t.assigned_worker_id == $$, this._currentUser.id);
14
+ // query = query.where(t => t.status == $$, status);
15
+
16
+ console.log('Test: Chaining two where() calls');
17
+ console.log(' First: where(t => t.assigned_worker_id == 123)');
18
+ console.log(' Second: where(t => t.status == "pending")');
19
+ console.log();
20
+
21
+ // First where call
22
+ qs.where('t => t.assigned_worker_id == 123', 'QaTask');
23
+
24
+ console.log('After first where():');
25
+ console.log(' script.where exists:', qs.script.where !== false);
26
+ console.log(' script.where.QaTask exists:', qs.script.where && qs.script.where.QaTask !== undefined);
27
+ if (qs.script.where && qs.script.where.QaTask && qs.script.where.QaTask.query) {
28
+ const exprs1 = qs.script.where.QaTask.query.expressions || [];
29
+ console.log(' Expressions count:', exprs1.length);
30
+ console.log(' Expression 1:', JSON.stringify(exprs1[0]));
31
+ }
32
+ console.log();
33
+
34
+ // Second where call (this should MERGE, not overwrite)
35
+ qs.where('t => t.status == "pending"', 'QaTask');
36
+
37
+ console.log('After second where():');
38
+ console.log(' script.where exists:', qs.script.where !== false);
39
+ console.log(' script.where.QaTask exists:', qs.script.where && qs.script.where.QaTask !== undefined);
40
+
41
+ if (qs.script.where && qs.script.where.QaTask && qs.script.where.QaTask.query) {
42
+ const exprs2 = qs.script.where.QaTask.query.expressions || [];
43
+ console.log(' Expressions count:', exprs2.length);
44
+ console.log(' Expression 1:', JSON.stringify(exprs2[0]));
45
+ console.log(' Expression 2:', JSON.stringify(exprs2[1]));
46
+ console.log();
47
+
48
+ // Verify results
49
+ console.log('=== Test Results ===');
50
+ if (exprs2.length === 2) {
51
+ console.log('✓ PASS: Both where conditions are present');
52
+ console.log(' - First condition: assigned_worker_id == 123');
53
+ console.log(' - Second condition: status == "pending"');
54
+ console.log(' - These should be combined with AND in the SQL');
55
+ } else {
56
+ console.log('✗ FAIL: Expected 2 expressions, got', exprs2.length);
57
+ if (exprs2.length === 1) {
58
+ console.log(' - Only the last where() was applied (bug not fixed)');
59
+ }
60
+ }
61
+ } else {
62
+ console.log('✗ FAIL: script.where structure is invalid');
63
+ }
64
+
65
+ console.log();
66
+ console.log('=== Additional Test: Three where() calls ===');
67
+
68
+ // Test with three chained where calls
69
+ const qs2 = new queryScript();
70
+ qs2.where('t => t.user_id == 1', 'Task');
71
+ qs2.where('t => t.status == "active"', 'Task');
72
+ qs2.where('t => t.priority == "high"', 'Task');
73
+
74
+ if (qs2.script.where && qs2.script.where.Task && qs2.script.where.Task.query) {
75
+ const exprs3 = qs2.script.where.Task.query.expressions || [];
76
+ console.log('Expressions count:', exprs3.length);
77
+ if (exprs3.length === 3) {
78
+ console.log('✓ PASS: All three where conditions are present');
79
+ exprs3.forEach((expr, idx) => {
80
+ console.log(` ${idx + 1}. ${expr.field} ${expr.func} ${expr.arg}`);
81
+ });
82
+ } else {
83
+ console.log('✗ FAIL: Expected 3 expressions, got', exprs3.length);
84
+ }
85
+ }
86
+
87
+ console.log();
88
+ console.log('=== Test Complete ===');