masterrecord 0.3.23 → 0.3.25
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.
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"Bash(done)",
|
|
36
36
|
"WebSearch",
|
|
37
37
|
"WebFetch(domain:github.com)",
|
|
38
|
-
"Bash(master=development masterrecord update-database qaContext)"
|
|
38
|
+
"Bash(master=development masterrecord update-database qaContext)",
|
|
39
|
+
"Bash(chmod:*)",
|
|
40
|
+
"Bash(npm install:*)"
|
|
39
41
|
],
|
|
40
42
|
"deny": [],
|
|
41
43
|
"ask": []
|
|
@@ -66,6 +66,9 @@ class EntityTrackerModel {
|
|
|
66
66
|
}else{
|
|
67
67
|
return this["__proto__"]["_" + modelField];
|
|
68
68
|
}
|
|
69
|
+
}else{
|
|
70
|
+
// If skipGetFunction is true, return the raw value
|
|
71
|
+
return this["__proto__"]["_" + modelField];
|
|
69
72
|
}
|
|
70
73
|
}else{
|
|
71
74
|
return this["__proto__"]["_" + modelField];
|
|
@@ -186,6 +186,14 @@ class queryMethods{
|
|
|
186
186
|
this.__reset();
|
|
187
187
|
return val;
|
|
188
188
|
}
|
|
189
|
+
|
|
190
|
+
if(this.__context.isPostgres){
|
|
191
|
+
// trying to match string select and relace with select Count(*);
|
|
192
|
+
var entityValue = await this.__context._SQLEngine.getCount(this.__queryObject, this.__entity, this.__context);
|
|
193
|
+
var val = entityValue[Object.keys(entityValue)[0]];
|
|
194
|
+
this.__reset();
|
|
195
|
+
return val;
|
|
196
|
+
}
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
/**
|
|
@@ -276,7 +284,7 @@ class queryMethods{
|
|
|
276
284
|
const placeholders = this.__queryObject.parameters.addParams(itemArray, dbType);
|
|
277
285
|
// Replace $$ first (preferred), then $ (backwards compatibility)
|
|
278
286
|
if(str.includes('$$')){
|
|
279
|
-
str = str.replace(
|
|
287
|
+
str = str.replace(/\$\$/g, placeholders);
|
|
280
288
|
} else {
|
|
281
289
|
// Replace single $ but not $N (postgres placeholders)
|
|
282
290
|
str = str.replace(/\$(?!\d)/, placeholders);
|
|
@@ -297,7 +305,7 @@ class queryMethods{
|
|
|
297
305
|
const placeholder = this.__queryObject.parameters.addParam(item, dbType);
|
|
298
306
|
// Replace $$ first (preferred), then $ (backwards compatibility)
|
|
299
307
|
if(str.includes('$$')){
|
|
300
|
-
str = str.replace(
|
|
308
|
+
str = str.replace(/\$\$/g, placeholder);
|
|
301
309
|
} else {
|
|
302
310
|
// Replace single $ but not $N (postgres placeholders)
|
|
303
311
|
str = str.replace(/\$(?!\d)/, placeholder);
|
|
@@ -366,6 +374,11 @@ class queryMethods{
|
|
|
366
374
|
result = this.__singleEntityBuilder(entityValue[0]);
|
|
367
375
|
}
|
|
368
376
|
|
|
377
|
+
if(this.__context.isPostgres){
|
|
378
|
+
var entityValue = await this.__context._SQLEngine.get(this.__queryObject.script, this.__entity, this.__context);
|
|
379
|
+
result = this.__singleEntityBuilder(entityValue[0]);
|
|
380
|
+
}
|
|
381
|
+
|
|
369
382
|
// Store in cache
|
|
370
383
|
if (this.__useCache && result) {
|
|
371
384
|
this.__context._queryCache.set(cacheKey, result, tableName);
|
|
@@ -410,6 +423,11 @@ class queryMethods{
|
|
|
410
423
|
result = this.__multipleEntityBuilder(entityValue);
|
|
411
424
|
}
|
|
412
425
|
|
|
426
|
+
if(this.__context.isPostgres){
|
|
427
|
+
var entityValue = await this.__context._SQLEngine.all(this.__queryObject.script, this.__entity, this.__context);
|
|
428
|
+
result = this.__multipleEntityBuilder(entityValue);
|
|
429
|
+
}
|
|
430
|
+
|
|
413
431
|
// Store in cache
|
|
414
432
|
if (this.__useCache && result) {
|
|
415
433
|
this.__context._queryCache.set(cacheKey, result, tableName);
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# MasterRecord Troubleshooting Guide
|
|
2
|
+
|
|
3
|
+
This guide covers common issues and potential pitfalls when using MasterRecord.
|
|
4
|
+
|
|
5
|
+
## Navigation Properties vs Regular Properties
|
|
6
|
+
|
|
7
|
+
### Understanding the Difference
|
|
8
|
+
|
|
9
|
+
MasterRecord provides **two different ways** to access related entities, and mixing them causes bugs.
|
|
10
|
+
|
|
11
|
+
#### 1. Navigation Properties (Lazy Loading)
|
|
12
|
+
|
|
13
|
+
When you access a related entity using a **capital letter that matches the entity name**, MasterRecord treats it as a navigation property with lazy loading:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// Accessing with capital letter triggers lazy loading
|
|
17
|
+
const auth = await user.Auth; // Executes a database query
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**How it works:**
|
|
21
|
+
- MasterRecord looks up the entity definition in your context
|
|
22
|
+
- Executes a SQL query to load the related record
|
|
23
|
+
- Returns a **Promise** that must be awaited
|
|
24
|
+
- Defined in your entity's relationship configuration (hasOne, hasMany, belongsTo)
|
|
25
|
+
|
|
26
|
+
#### 2. Regular Properties (Direct Access)
|
|
27
|
+
|
|
28
|
+
When you manually attach an object to an entity as a regular property, you access it directly:
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// Attach as regular property (any case except navigation property name)
|
|
32
|
+
user.auth = someAuthObject; // lowercase
|
|
33
|
+
|
|
34
|
+
// Access directly - no query, just returns the object
|
|
35
|
+
console.log(user.auth.password_hash); // Immediate access
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Common Bug: Mixing Both Approaches
|
|
39
|
+
|
|
40
|
+
**❌ THIS CAUSES BUGS:**
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// In authService.js - attaching as regular property (lowercase)
|
|
44
|
+
async findAuthByEmail(email, context) {
|
|
45
|
+
var user = await context.User
|
|
46
|
+
.where(r => r.email.toLowerCase() == $$, email.toLowerCase())
|
|
47
|
+
.single();
|
|
48
|
+
|
|
49
|
+
var auth = await context.Auth
|
|
50
|
+
.where(a => a.user_id == $$, user.id)
|
|
51
|
+
.single();
|
|
52
|
+
|
|
53
|
+
// Attaching as lowercase regular property
|
|
54
|
+
user.auth = auth;
|
|
55
|
+
return user;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// In credentialsController.js - accessing as navigation property (capital)
|
|
59
|
+
async login(req) {
|
|
60
|
+
var authObj = await req.authService.authenticate(email, password, req.userContext);
|
|
61
|
+
|
|
62
|
+
// ❌ BUG: Accessing as capital triggers navigation property!
|
|
63
|
+
authObj.user.Auth.temp_access_token = refreshToken; // Returns Promise, not auth object!
|
|
64
|
+
req.userContext.saveChanges(); // Doesn't save - Promise isn't resolved
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**What happens:**
|
|
69
|
+
1. `findAuthByEmail()` attaches auth as `user.auth` (lowercase regular property)
|
|
70
|
+
2. `credentialsController` tries to access `user.Auth` (capital navigation property)
|
|
71
|
+
3. MasterRecord sees the capital letter and thinks "navigation property!"
|
|
72
|
+
4. It executes a lazy loading query and returns a Promise
|
|
73
|
+
5. The Promise isn't awaited, so you get `undefined` or the Promise object itself
|
|
74
|
+
6. Your data doesn't save, properties return undefined, bcrypt fails with "Illegal arguments"
|
|
75
|
+
|
|
76
|
+
### Solutions
|
|
77
|
+
|
|
78
|
+
#### Option 1: Use Regular Properties (Recommended when entity already loaded)
|
|
79
|
+
|
|
80
|
+
**✅ CORRECT:**
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
// Attach as lowercase regular property
|
|
84
|
+
user.auth = auth;
|
|
85
|
+
|
|
86
|
+
// Access as lowercase regular property
|
|
87
|
+
authObj.user.auth.temp_access_token = refreshToken;
|
|
88
|
+
authObj.user.auth.login_counter = authObj.user.auth.login_counter + 1;
|
|
89
|
+
req.userContext.saveChanges();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Benefits:**
|
|
93
|
+
- ✅ No extra database queries
|
|
94
|
+
- ✅ Synchronous access
|
|
95
|
+
- ✅ More performant
|
|
96
|
+
|
|
97
|
+
**Use when:**
|
|
98
|
+
- You've already loaded the related entity with an explicit query
|
|
99
|
+
- You want direct access without database overhead
|
|
100
|
+
- You need synchronous property access
|
|
101
|
+
|
|
102
|
+
#### Option 2: Use Navigation Properties with Await (When you want lazy loading)
|
|
103
|
+
|
|
104
|
+
**✅ CORRECT:**
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
// Access navigation property with await
|
|
108
|
+
const auth = await authObj.user.Auth; // Must await!
|
|
109
|
+
auth.temp_access_token = refreshToken;
|
|
110
|
+
auth.login_counter = auth.login_counter + 1;
|
|
111
|
+
req.userContext.saveChanges();
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Benefits:**
|
|
115
|
+
- ✅ Automatic loading of related entities
|
|
116
|
+
- ✅ Works without manual queries
|
|
117
|
+
|
|
118
|
+
**Drawbacks:**
|
|
119
|
+
- ❌ Extra database query (even if you already loaded it)
|
|
120
|
+
- ❌ Must remember to await
|
|
121
|
+
- ❌ Async only
|
|
122
|
+
|
|
123
|
+
**Use when:**
|
|
124
|
+
- You want lazy loading behavior
|
|
125
|
+
- You haven't already loaded the related entity
|
|
126
|
+
- You're okay with the extra database query
|
|
127
|
+
|
|
128
|
+
### Best Practice: Be Consistent
|
|
129
|
+
|
|
130
|
+
The key is **consistency** - pick one approach per relationship and stick with it:
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
// Good: Explicit loading + regular properties
|
|
134
|
+
async findAuthByEmail(email, context) {
|
|
135
|
+
const user = await context.User.where(r => r.email == $$, email).single();
|
|
136
|
+
const auth = await context.Auth.where(a => a.user_id == $$, user.id).single();
|
|
137
|
+
user.auth = auth; // lowercase
|
|
138
|
+
return user;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Access consistently with lowercase
|
|
142
|
+
authObj.user.auth.temp_access_token = refreshToken; // lowercase
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
OR
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
// Good: Let navigation properties do the work
|
|
149
|
+
async findAuthByEmail(email, context) {
|
|
150
|
+
const user = await context.User.where(r => r.email == $$, email).single();
|
|
151
|
+
// Don't manually load auth - let navigation property handle it
|
|
152
|
+
return user;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Access with await
|
|
156
|
+
const auth = await authObj.user.Auth; // capital + await
|
|
157
|
+
auth.temp_access_token = refreshToken;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Real-World Example: Authentication Bug
|
|
161
|
+
|
|
162
|
+
This bug was found in a production authentication system where login succeeded but immediately redirected back to the login page.
|
|
163
|
+
|
|
164
|
+
**The Problem:**
|
|
165
|
+
```javascript
|
|
166
|
+
// authService.js - Line 306
|
|
167
|
+
user.auth = auth; // Attaching as lowercase
|
|
168
|
+
|
|
169
|
+
// credentialsController.js - Line 287-288
|
|
170
|
+
authObj.user.Auth.temp_access_token = refreshToken; // ❌ Accessing as capital!
|
|
171
|
+
req.userContext.saveChanges();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**What happened:**
|
|
175
|
+
1. JWT token was generated correctly
|
|
176
|
+
2. `authObj.user.Auth.temp_access_token = refreshToken` didn't actually save the token
|
|
177
|
+
3. It set the property on a Promise object instead of the auth entity
|
|
178
|
+
4. Database query `WHERE temp_access_token = ?` returned no results
|
|
179
|
+
5. `currentUser()` check failed
|
|
180
|
+
6. User was redirected back to login
|
|
181
|
+
|
|
182
|
+
**The Fix:**
|
|
183
|
+
```javascript
|
|
184
|
+
// credentialsController.js - Line 287-288
|
|
185
|
+
authObj.user.auth.temp_access_token = refreshToken; // ✅ lowercase matches attachment
|
|
186
|
+
authObj.user.auth.login_counter = authObj.user.auth.login_counter + 1;
|
|
187
|
+
req.userContext.saveChanges(); // Now it actually saves!
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Debugging Tips
|
|
191
|
+
|
|
192
|
+
If you're seeing any of these symptoms:
|
|
193
|
+
- Properties returning `undefined` when you know they exist in the database
|
|
194
|
+
- `bcrypt.compareSync()` failing with "Illegal arguments: string, undefined"
|
|
195
|
+
- Data not saving when you call `saveChanges()`
|
|
196
|
+
- `[object Promise]` appearing in logs instead of actual values
|
|
197
|
+
- Queries executing successfully but entities seem empty
|
|
198
|
+
|
|
199
|
+
**Check for navigation property mismatches:**
|
|
200
|
+
|
|
201
|
+
1. Add debug logging:
|
|
202
|
+
```javascript
|
|
203
|
+
console.log('Type of user.Auth:', typeof user.Auth);
|
|
204
|
+
console.log('Is Promise?', user.Auth instanceof Promise);
|
|
205
|
+
console.log('Value:', user.Auth);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
2. Look for:
|
|
209
|
+
- Capital letter access (`user.Auth`, `auth.User`) without `await`
|
|
210
|
+
- Mixing lowercase attachment with capital access
|
|
211
|
+
- Accessing navigation properties in non-async contexts
|
|
212
|
+
|
|
213
|
+
3. Fix by:
|
|
214
|
+
- Making access match attachment (both lowercase)
|
|
215
|
+
- OR using `await` with capital navigation properties
|
|
216
|
+
- OR explicitly loading entities instead of relying on lazy loading
|
|
217
|
+
|
|
218
|
+
### Additional Examples
|
|
219
|
+
|
|
220
|
+
#### Creating New Entities with Relationships
|
|
221
|
+
|
|
222
|
+
When creating new entities, you can use capital letters during construction:
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
// ✅ This is fine - entity not yet persisted
|
|
226
|
+
var user = new userEntity();
|
|
227
|
+
var auth = new authEntity();
|
|
228
|
+
user.Auth = auth; // Capital is OK here
|
|
229
|
+
auth.password_hash = bcrypt.hashSync(password, salt);
|
|
230
|
+
context.User.add(user);
|
|
231
|
+
await context.saveChanges();
|
|
232
|
+
|
|
233
|
+
// ❌ After saveChanges, use regular property or reload
|
|
234
|
+
// Store reference to avoid navigation property
|
|
235
|
+
auth.temp_access_token = refreshToken; // Use stored reference
|
|
236
|
+
await context.saveChanges();
|
|
237
|
+
|
|
238
|
+
// ❌ DON'T DO THIS after saveChanges:
|
|
239
|
+
user.Auth.temp_access_token = refreshToken; // Triggers navigation property!
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### Working with Loaded Entities
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
// Load user from database
|
|
246
|
+
const user = await context.User.where(r => r.id == $$, userId).single();
|
|
247
|
+
|
|
248
|
+
// ✅ Option 1: Explicitly load and attach
|
|
249
|
+
const auth = await context.Auth.where(a => a.user_id == $$, user.id).single();
|
|
250
|
+
user.auth = auth; // lowercase
|
|
251
|
+
console.log(user.auth.password_hash); // Direct access
|
|
252
|
+
|
|
253
|
+
// ✅ Option 2: Use navigation property with await
|
|
254
|
+
const auth = await user.Auth; // capital + await
|
|
255
|
+
console.log(auth.password_hash);
|
|
256
|
+
|
|
257
|
+
// ❌ DON'T MIX:
|
|
258
|
+
user.auth = await context.Auth.where(a => a.user_id == $$, user.id).single();
|
|
259
|
+
console.log(user.Auth.password_hash); // Wrong! This is navigation property
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Summary
|
|
263
|
+
|
|
264
|
+
- **Navigation properties** (capital letter) = lazy loading = returns Promise = must await
|
|
265
|
+
- **Regular properties** (lowercase or any non-entity name) = direct access = immediate value
|
|
266
|
+
- **Never mix them** - be consistent in how you attach and access related entities
|
|
267
|
+
- **When in doubt**, explicitly load entities and use lowercase regular properties
|
|
268
|
+
- **Add debug logging** to catch Promise objects being treated as entities
|
|
269
|
+
|
|
270
|
+
This pattern is consistent throughout MasterRecord and applies to all relationship types: `hasOne`, `hasMany`, `belongsTo`, and `hasManyThrough`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.25",
|
|
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": {
|