masterrecord 0.2.34 → 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.16
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;
@@ -348,7 +349,14 @@ class context {
348
349
 
349
350
  dbset(model, name){
350
351
  var validModel = modelBuilder.create(model);
351
- 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;
352
360
  this.__entities.push(validModel); // model object
353
361
  var buildMod = tools.createNewInstance(validModel, query, this);
354
362
  this.__builderEntities.push(buildMod); // query builder entites
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.2.34",
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 ===');