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.
- package/.claude/settings.local.json +7 -1
- package/QueryLanguage/queryScript.js +1 -1
- package/context.js +27 -3
- package/docs/belongsTo-relationships.md +190 -0
- package/package.json +5 -5
- package/readme.md +227 -1
- package/test/tablePrefixTest.js +100 -0
- package/test/whereChainingTest.js +88 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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.
|
|
31
|
-
"glob": "^
|
|
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.
|
|
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
|
|
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 ===');
|