masterrecord 0.3.58 → 0.3.60

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.
@@ -613,6 +613,14 @@ class queryMethods{
613
613
  if(!this.__dirtyFields.includes(fname)){
614
614
  this.__dirtyFields.push(fname);
615
615
  }
616
+ // After INSERT (state transitions to "track"), behave like
617
+ // query-loaded entities: mark modified and re-register with tracker
618
+ if (this.__state === 'track') {
619
+ this.__state = 'modified';
620
+ }
621
+ if (this.__context && typeof this.__context.__track === 'function') {
622
+ this.__context.__track(this);
623
+ }
616
624
  },
617
625
  get: function(){
618
626
  // Apply get function if defined
@@ -4,6 +4,12 @@ const LOG_OPERATORS_REGEX = /(\|\|)|(&&)/;
4
4
  var tools = require('../Tools');
5
5
  const QueryParameters = require('./queryParameters');
6
6
 
7
+ // Escape special regex characters so user-supplied names can be safely
8
+ // interpolated into RegExp constructors (prevents "Unmatched ')'" etc.)
9
+ function escapeRegExp(str) {
10
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11
+ }
12
+
7
13
  class queryScript{
8
14
 
9
15
  constructor(){
@@ -216,12 +222,12 @@ class queryScript{
216
222
  }
217
223
 
218
224
  MATCH_ENTITY_REGEXP(entityName) {
219
- return new RegExp("(^|[^\\w\\d])" + entityName + "[ \\.\\)]");
225
+ return new RegExp("(^|[^\\w\\d])" + escapeRegExp(entityName) + "[ \\.\\)]");
220
226
  }
221
227
 
222
228
  OPERATORS_REGEX(entityName){
223
229
  // Prefer longest operators first to avoid partially matching '>' in '>=' and leaving '=' in the argument
224
- return new RegExp("(?:^|[^\\w\\d])" + entityName
230
+ return new RegExp("(?:^|[^\\w\\d])" + escapeRegExp(entityName)
225
231
  + "\\.((?:\\.?[\\w\\d_\\$]+)+)(?:\\((.*?)\\))?(?:\\s*((?:===)|(?:!==)|(?:<=)|(?:>=)|(?:==)|(?:!=)|(?:in)|>|<|(?:=))\\s*(.*))?")
226
232
  }
227
233
 
package/context.js CHANGED
@@ -1614,6 +1614,13 @@ class context {
1614
1614
  await entity.afterSave.call(entity);
1615
1615
  }
1616
1616
  }
1617
+
1618
+ // Transition inserted entities to tracked state so subsequent
1619
+ // property changes trigger UPDATE instead of a second INSERT
1620
+ for (const entity of entities) {
1621
+ entity.__state = 'track';
1622
+ entity.__dirtyFields = [];
1623
+ }
1617
1624
  }
1618
1625
 
1619
1626
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.58",
3
+ "version": "0.3.60",
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": {
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Test: Post-INSERT entity tracking
3
+ *
4
+ * Verifies that after saveChanges() INSERTs a new entity, modifying
5
+ * properties on that same in-memory object correctly transitions to
6
+ * "modified" state and re-registers with the change tracker, so that
7
+ * a subsequent saveChanges() issues an UPDATE.
8
+ */
9
+
10
+ // Alias 'masterrecord' to local root
11
+ const path = require('path');
12
+ const Module = require('module');
13
+ const __MASTERRECORD_ROOT__ = path.join(__dirname, '..');
14
+ const __ORIGINAL_REQUIRE__ = Module.prototype.require;
15
+ Module.prototype.require = function(request) {
16
+ if (request === 'masterrecord' || request.startsWith('masterrecord/')) {
17
+ const resolved = request === 'masterrecord'
18
+ ? __MASTERRECORD_ROOT__
19
+ : path.join(__MASTERRECORD_ROOT__, request.slice('masterrecord/'.length));
20
+ return __ORIGINAL_REQUIRE__.call(this, resolved);
21
+ }
22
+ return __ORIGINAL_REQUIRE__.call(this, request);
23
+ };
24
+
25
+ const queryMethods = require('../QueryLanguage/queryMethods');
26
+
27
+ console.log("╔════════════════════════════════════════════════════════════════╗");
28
+ console.log("║ Post-INSERT Entity Tracking Test ║");
29
+ console.log("╚════════════════════════════════════════════════════════════════╝\n");
30
+
31
+ let passed = 0;
32
+ let failed = 0;
33
+
34
+ function assert(condition, label) {
35
+ if (condition) {
36
+ console.log(` ✓ ${label}`);
37
+ passed++;
38
+ } else {
39
+ console.log(` ✗ ${label}`);
40
+ failed++;
41
+ }
42
+ }
43
+
44
+ // Minimal simulated context with tracking
45
+ function makeContext(entity) {
46
+ const ctx = {
47
+ __trackedEntities: [],
48
+ __trackedEntitiesMap: new Map(),
49
+ __entities: [entity],
50
+ __track(model) {
51
+ if (!this.__trackedEntitiesMap.has(model.__ID)) {
52
+ this.__trackedEntities.push(model);
53
+ this.__trackedEntitiesMap.set(model.__ID, model);
54
+ }
55
+ },
56
+ __clearTracked() {
57
+ this.__trackedEntities = [];
58
+ this.__trackedEntitiesMap.clear();
59
+ },
60
+ };
61
+ return ctx;
62
+ }
63
+
64
+ // Entity definition
65
+ const userEntity = {
66
+ __name: 'User',
67
+ id: { type: 'integer', primary: true, auto: true, nullable: true },
68
+ status: { type: 'string', nullable: true },
69
+ name: { type: 'string', nullable: true },
70
+ };
71
+
72
+ // Build a queryMethods-style dbset so we can call .new()
73
+ function makeDbSet(ctx, entity) {
74
+ const qs = new queryMethods();
75
+ qs.__entity = entity;
76
+ qs.__context = ctx;
77
+ return qs;
78
+ }
79
+
80
+ // -----------------------------------------------------------
81
+ // Test 1: .new() entity starts in "insert" state
82
+ // -----------------------------------------------------------
83
+ console.log("Test 1: .new() entity starts in insert state");
84
+ console.log("──────────────────────────────────────────────────");
85
+ {
86
+ const ctx = makeContext(userEntity);
87
+ const dbset = makeDbSet(ctx, userEntity);
88
+ const entity = dbset.new();
89
+
90
+ assert(entity.__state === 'insert', 'State is "insert"');
91
+ assert(Array.isArray(entity.__dirtyFields), 'Has dirtyFields array');
92
+ assert(ctx.__trackedEntitiesMap.has(entity.__ID), 'Entity is tracked');
93
+ }
94
+
95
+ // -----------------------------------------------------------
96
+ // Test 2: Setting properties during "insert" state keeps state
97
+ // -----------------------------------------------------------
98
+ console.log("\nTest 2: Properties set during insert state keep insert state");
99
+ console.log("──────────────────────────────────────────────────");
100
+ {
101
+ const ctx = makeContext(userEntity);
102
+ const dbset = makeDbSet(ctx, userEntity);
103
+ const entity = dbset.new();
104
+
105
+ entity.status = 'queued';
106
+ entity.name = 'Test';
107
+
108
+ assert(entity.__state === 'insert', 'State stays "insert"');
109
+ assert(entity.__dirtyFields.includes('status'), 'status in dirtyFields');
110
+ assert(entity.__dirtyFields.includes('name'), 'name in dirtyFields');
111
+ }
112
+
113
+ // -----------------------------------------------------------
114
+ // Test 3: Simulating post-INSERT transition
115
+ // -----------------------------------------------------------
116
+ console.log("\nTest 3: After INSERT, entity transitions to track state");
117
+ console.log("──────────────────────────────────────────────────");
118
+ {
119
+ const ctx = makeContext(userEntity);
120
+ const dbset = makeDbSet(ctx, userEntity);
121
+ const entity = dbset.new();
122
+ entity.status = 'queued';
123
+
124
+ // Simulate what _processBatchInserts now does after INSERT
125
+ entity.__state = 'track';
126
+ entity.__dirtyFields = [];
127
+
128
+ // Simulate saveChanges clearing tracked entities
129
+ ctx.__clearTracked();
130
+
131
+ assert(entity.__state === 'track', 'State is "track" after INSERT');
132
+ assert(entity.__dirtyFields.length === 0, 'dirtyFields cleared');
133
+ assert(!ctx.__trackedEntitiesMap.has(entity.__ID), 'Entity removed from tracker');
134
+ }
135
+
136
+ // -----------------------------------------------------------
137
+ // Test 4: Modifying after INSERT transitions to "modified" and re-tracks
138
+ // -----------------------------------------------------------
139
+ console.log("\nTest 4: Modification after INSERT transitions to modified + re-tracks");
140
+ console.log("──────────────────────────────────────────────────");
141
+ {
142
+ const ctx = makeContext(userEntity);
143
+ const dbset = makeDbSet(ctx, userEntity);
144
+ const entity = dbset.new();
145
+ entity.status = 'queued';
146
+
147
+ // Simulate post-INSERT transition + clear
148
+ entity.__state = 'track';
149
+ entity.__dirtyFields = [];
150
+ ctx.__clearTracked();
151
+
152
+ // Now modify — this is the bug scenario
153
+ entity.status = 'completed';
154
+
155
+ assert(entity.__state === 'modified', 'State transitions to "modified"');
156
+ assert(entity.__dirtyFields.includes('status'), 'status in dirtyFields');
157
+ assert(entity.__dirtyFields.length === 1, 'Only modified field is dirty');
158
+ assert(ctx.__trackedEntitiesMap.has(entity.__ID), 'Entity re-registered with tracker');
159
+ assert(ctx.__trackedEntities.length === 1, 'Exactly one tracked entity');
160
+ }
161
+
162
+ // -----------------------------------------------------------
163
+ // Test 5: Multiple modifications after INSERT
164
+ // -----------------------------------------------------------
165
+ console.log("\nTest 5: Multiple modifications after INSERT");
166
+ console.log("──────────────────────────────────────────────────");
167
+ {
168
+ const ctx = makeContext(userEntity);
169
+ const dbset = makeDbSet(ctx, userEntity);
170
+ const entity = dbset.new();
171
+ entity.status = 'queued';
172
+ entity.name = 'Job1';
173
+
174
+ // Simulate post-INSERT + clear
175
+ entity.__state = 'track';
176
+ entity.__dirtyFields = [];
177
+ ctx.__clearTracked();
178
+
179
+ // Multiple modifications
180
+ entity.status = 'completed';
181
+ entity.name = 'Job1-done';
182
+
183
+ assert(entity.__state === 'modified', 'State is "modified"');
184
+ assert(entity.__dirtyFields.includes('status'), 'status tracked');
185
+ assert(entity.__dirtyFields.includes('name'), 'name tracked');
186
+ assert(ctx.__trackedEntities.length === 1, 'Entity tracked only once (idempotent)');
187
+ }
188
+
189
+ // -----------------------------------------------------------
190
+ // Test 6: Entity value is correct after post-INSERT modification
191
+ // -----------------------------------------------------------
192
+ console.log("\nTest 6: Values are correct through full lifecycle");
193
+ console.log("──────────────────────────────────────────────────");
194
+ {
195
+ const ctx = makeContext(userEntity);
196
+ const dbset = makeDbSet(ctx, userEntity);
197
+ const entity = dbset.new();
198
+ entity.status = 'queued';
199
+
200
+ assert(entity.status === 'queued', 'Initial value correct');
201
+
202
+ // Post-INSERT
203
+ entity.__state = 'track';
204
+ entity.__dirtyFields = [];
205
+ ctx.__clearTracked();
206
+
207
+ assert(entity.status === 'queued', 'Value preserved after INSERT transition');
208
+
209
+ entity.status = 'completed';
210
+ assert(entity.status === 'completed', 'Updated value correct');
211
+ }
212
+
213
+ // -----------------------------------------------------------
214
+ // Test 7: _processTrackedEntities would route correctly
215
+ // -----------------------------------------------------------
216
+ console.log("\nTest 7: Simulated _processTrackedEntities routes to UPDATE");
217
+ console.log("──────────────────────────────────────────────────");
218
+ {
219
+ const ctx = makeContext(userEntity);
220
+ const dbset = makeDbSet(ctx, userEntity);
221
+ const entity = dbset.new();
222
+ entity.status = 'queued';
223
+
224
+ // Post-INSERT
225
+ entity.__state = 'track';
226
+ entity.__dirtyFields = [];
227
+ ctx.__clearTracked();
228
+
229
+ // Modify
230
+ entity.status = 'completed';
231
+
232
+ // Simulate _processTrackedEntities grouping
233
+ const toInsert = [];
234
+ const toUpdate = [];
235
+ for (const tracked of ctx.__trackedEntities) {
236
+ switch (tracked.__state) {
237
+ case 'insert': toInsert.push(tracked); break;
238
+ case 'modified':
239
+ if (tracked.__dirtyFields && tracked.__dirtyFields.length > 0) {
240
+ toUpdate.push(tracked);
241
+ }
242
+ break;
243
+ }
244
+ }
245
+
246
+ assert(toInsert.length === 0, 'No entities routed to INSERT');
247
+ assert(toUpdate.length === 1, 'One entity routed to UPDATE');
248
+ assert(toUpdate[0] === entity, 'Correct entity routed to UPDATE');
249
+ }
250
+
251
+ // Summary
252
+ console.log("\n" + "=".repeat(64));
253
+ console.log(`Test Results: ${passed} passed, ${failed} failed`);
254
+ console.log("=".repeat(64));
255
+
256
+ if (failed > 0) {
257
+ process.exit(1);
258
+ }