meadow 2.0.23 → 2.0.26
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/README.md +110 -141
- package/docs/README.md +34 -230
- package/docs/_cover.md +14 -0
- package/docs/_sidebar.md +44 -12
- package/docs/_topbar.md +5 -0
- package/docs/api/doCount.md +109 -0
- package/docs/api/doCreate.md +132 -0
- package/docs/api/doDelete.md +101 -0
- package/docs/api/doRead.md +122 -0
- package/docs/api/doReads.md +136 -0
- package/docs/api/doUndelete.md +98 -0
- package/docs/api/doUpdate.md +129 -0
- package/docs/api/getRoleName.md +84 -0
- package/docs/api/loadFromPackage.md +153 -0
- package/docs/api/marshalRecordFromSourceToObject.md +92 -0
- package/docs/api/query.md +133 -0
- package/docs/api/rawQueries.md +197 -0
- package/docs/api/reference.md +117 -0
- package/docs/api/setAuthorizer.md +103 -0
- package/docs/api/setDefault.md +90 -0
- package/docs/api/setDefaultIdentifier.md +84 -0
- package/docs/api/setDomain.md +56 -0
- package/docs/api/setIDUser.md +91 -0
- package/docs/api/setJsonSchema.md +92 -0
- package/docs/api/setProvider.md +87 -0
- package/docs/api/setSchema.md +107 -0
- package/docs/api/setScope.md +68 -0
- package/docs/api/validateObject.md +119 -0
- package/docs/architecture.md +316 -0
- package/docs/audit-tracking.md +226 -0
- package/docs/configuration.md +317 -0
- package/docs/providers/meadow-endpoints.md +306 -0
- package/docs/providers/mongodb.md +319 -0
- package/docs/providers/postgresql.md +312 -0
- package/docs/providers/rocksdb.md +297 -0
- package/docs/query-dsl.md +269 -0
- package/docs/quick-start.md +384 -0
- package/docs/raw-queries.md +193 -0
- package/docs/retold-catalog.json +61 -1
- package/docs/retold-keyword-index.json +15860 -4839
- package/docs/soft-deletes.md +224 -0
- package/package.json +36 -9
- package/scripts/bookstore-seed-postgresql.sql +135 -0
- package/scripts/dgraph-test-db.sh +144 -0
- package/scripts/meadow-test-cleanup.sh +5 -1
- package/scripts/mongodb-test-db.sh +98 -0
- package/scripts/postgresql-test-db.sh +124 -0
- package/scripts/solr-test-db.sh +135 -0
- package/source/Meadow.js +5 -0
- package/source/providers/Meadow-Provider-DGraph.js +679 -0
- package/source/providers/Meadow-Provider-MongoDB.js +527 -0
- package/source/providers/Meadow-Provider-PostgreSQL.js +361 -0
- package/source/providers/Meadow-Provider-RocksDB.js +1300 -0
- package/source/providers/Meadow-Provider-Solr.js +726 -0
- package/test/Meadow-Provider-DGraph_tests.js +741 -0
- package/test/Meadow-Provider-MongoDB_tests.js +661 -0
- package/test/Meadow-Provider-PostgreSQL_tests.js +787 -0
- package/test/Meadow-Provider-RocksDB_tests.js +887 -0
- package/test/Meadow-Provider-Solr_tests.js +679 -0
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
// ##### Part of the **[retold](https://stevenvelozo.github.io/retold/)** system
|
|
2
|
+
/**
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author <steven@velozo.com>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Meadow RocksDB Provider
|
|
9
|
+
*
|
|
10
|
+
* Implements Meadow's CRUD interface for RocksDB key-value storage.
|
|
11
|
+
* Processes FoxHound filter arrays directly with an in-memory filter engine.
|
|
12
|
+
* No FoxHound dialect dependency required.
|
|
13
|
+
*
|
|
14
|
+
* Key Design:
|
|
15
|
+
* GUID mode (default): M-E-{Scope}-{GUID}
|
|
16
|
+
* ID mode: M-EBI-{Scope}-{ID}
|
|
17
|
+
* Sequence counter: M-SEQ-{Scope}
|
|
18
|
+
*/
|
|
19
|
+
var MeadowProvider = function ()
|
|
20
|
+
{
|
|
21
|
+
function createNew(pFable)
|
|
22
|
+
{
|
|
23
|
+
// If a valid Fable object isn't passed in, return a constructor
|
|
24
|
+
if (typeof (pFable) !== 'object')
|
|
25
|
+
{
|
|
26
|
+
return { new: createNew };
|
|
27
|
+
}
|
|
28
|
+
var _Fable = pFable;
|
|
29
|
+
var _GlobalLogLevel = 0;
|
|
30
|
+
|
|
31
|
+
if (_Fable.settings.RocksDB)
|
|
32
|
+
{
|
|
33
|
+
_GlobalLogLevel = _Fable.settings.RocksDB.GlobalLogLevel || 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Key mode configuration: 'GUID' (default) or 'ID'
|
|
37
|
+
var _KeyMode = (_Fable.settings.RocksDB && _Fable.settings.RocksDB.KeyMode === 'ID') ? 'ID' : 'GUID';
|
|
38
|
+
|
|
39
|
+
// Schema state (set by Meadow via setSchema)
|
|
40
|
+
var _Scope = 'Unknown';
|
|
41
|
+
var _Schema = [];
|
|
42
|
+
var _DefaultIdentifier = 'ID';
|
|
43
|
+
var _DefaultGUIdentifier = 'GUID';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Set the schema information (called by Meadow.updateProviderState)
|
|
47
|
+
*/
|
|
48
|
+
var setSchema = function (pScope, pSchema, pDefaultIdentifier, pDefaultGUIdentifier)
|
|
49
|
+
{
|
|
50
|
+
_Scope = pScope || 'Unknown';
|
|
51
|
+
_Schema = Array.isArray(pSchema) ? pSchema : [];
|
|
52
|
+
_DefaultIdentifier = pDefaultIdentifier || ('ID' + _Scope);
|
|
53
|
+
_DefaultGUIdentifier = pDefaultGUIdentifier || ('GUID' + _Scope);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
// ============================================================
|
|
58
|
+
// Schema Helpers
|
|
59
|
+
// ============================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find a schema entry by column name.
|
|
63
|
+
*/
|
|
64
|
+
var findSchemaEntry = function (pColumnName)
|
|
65
|
+
{
|
|
66
|
+
for (var i = 0; i < _Schema.length; i++)
|
|
67
|
+
{
|
|
68
|
+
if (_Schema[i].Column === pColumnName)
|
|
69
|
+
{
|
|
70
|
+
return _Schema[i];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { Column: pColumnName, Type: 'Default' };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Strip table prefix from a column name (e.g., "FableTest.Name" -> "Name")
|
|
78
|
+
*/
|
|
79
|
+
var stripTablePrefix = function (pColumnName)
|
|
80
|
+
{
|
|
81
|
+
if (typeof pColumnName !== 'string') return pColumnName;
|
|
82
|
+
var tmpDotIndex = pColumnName.indexOf('.');
|
|
83
|
+
if (tmpDotIndex > -1)
|
|
84
|
+
{
|
|
85
|
+
return pColumnName.substring(tmpDotIndex + 1);
|
|
86
|
+
}
|
|
87
|
+
return pColumnName;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
// ============================================================
|
|
92
|
+
// Key Generation
|
|
93
|
+
// ============================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build the RocksDB key for a record
|
|
97
|
+
*/
|
|
98
|
+
var buildRecordKey = function (pScope, pRecord)
|
|
99
|
+
{
|
|
100
|
+
if (_KeyMode === 'GUID' && _DefaultGUIdentifier && pRecord[_DefaultGUIdentifier])
|
|
101
|
+
{
|
|
102
|
+
return 'M-E-' + pScope + '-' + pRecord[_DefaultGUIdentifier];
|
|
103
|
+
}
|
|
104
|
+
else
|
|
105
|
+
{
|
|
106
|
+
return 'M-EBI-' + pScope + '-' + pRecord[_DefaultIdentifier];
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build prefix for scanning all records of a scope
|
|
112
|
+
*/
|
|
113
|
+
var buildScanPrefix = function (pScope)
|
|
114
|
+
{
|
|
115
|
+
return (_KeyMode === 'GUID') ? 'M-E-' + pScope + '-' : 'M-EBI-' + pScope + '-';
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build the auto-increment counter key
|
|
120
|
+
*/
|
|
121
|
+
var buildSequenceKey = function (pScope)
|
|
122
|
+
{
|
|
123
|
+
return 'M-SEQ-' + pScope;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
// ============================================================
|
|
128
|
+
// Database Access
|
|
129
|
+
// ============================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the RocksDB database instance from the connection service.
|
|
133
|
+
*/
|
|
134
|
+
var getDB = function ()
|
|
135
|
+
{
|
|
136
|
+
if (typeof (_Fable.MeadowRocksDBProvider) == 'object' && _Fable.MeadowRocksDBProvider.connected)
|
|
137
|
+
{
|
|
138
|
+
return _Fable.MeadowRocksDBProvider.db;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
var getProvider = function ()
|
|
144
|
+
{
|
|
145
|
+
if (typeof (_Fable.MeadowRocksDBProvider) == 'object')
|
|
146
|
+
{
|
|
147
|
+
return _Fable.MeadowRocksDBProvider;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
// ============================================================
|
|
154
|
+
// Auto-Increment Sequence
|
|
155
|
+
// ============================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the next auto-increment sequence value for an entity scope.
|
|
159
|
+
*/
|
|
160
|
+
var getNextSequence = function (pDB, pScope, fCallback)
|
|
161
|
+
{
|
|
162
|
+
var tmpSeqKey = buildSequenceKey(pScope);
|
|
163
|
+
pDB.get(tmpSeqKey, function (pError, pValue)
|
|
164
|
+
{
|
|
165
|
+
var tmpNext = 1;
|
|
166
|
+
if (!pError && pValue)
|
|
167
|
+
{
|
|
168
|
+
tmpNext = parseInt(pValue.toString(), 10) + 1;
|
|
169
|
+
}
|
|
170
|
+
pDB.put(tmpSeqKey, String(tmpNext), function (pPutError)
|
|
171
|
+
{
|
|
172
|
+
return fCallback(pPutError, tmpNext);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
// ============================================================
|
|
179
|
+
// Document Building (replaces MongoDB dialect logic)
|
|
180
|
+
// ============================================================
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build the create document from the query's marshalled records.
|
|
184
|
+
* Handles AutoIdentity, AutoGUID, CreateDate, UpdateDate, etc.
|
|
185
|
+
*/
|
|
186
|
+
var buildCreateDocument = function (pQuery)
|
|
187
|
+
{
|
|
188
|
+
var tmpRecords = pQuery.parameters.query.records;
|
|
189
|
+
if (!Array.isArray(tmpRecords) || tmpRecords.length < 1)
|
|
190
|
+
{
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
var tmpDocument = {};
|
|
195
|
+
var tmpRecord = tmpRecords[0];
|
|
196
|
+
|
|
197
|
+
for (var tmpColumn in tmpRecord)
|
|
198
|
+
{
|
|
199
|
+
if (!tmpRecord.hasOwnProperty(tmpColumn))
|
|
200
|
+
{
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var tmpSchemaEntry = findSchemaEntry(tmpColumn);
|
|
205
|
+
|
|
206
|
+
// Skip delete columns on create (unless delete tracking is disabled)
|
|
207
|
+
if (!pQuery.parameters.query.disableDeleteTracking)
|
|
208
|
+
{
|
|
209
|
+
if (tmpSchemaEntry.Type === 'DeleteDate' ||
|
|
210
|
+
tmpSchemaEntry.Type === 'DeleteIDUser')
|
|
211
|
+
{
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
switch (tmpSchemaEntry.Type)
|
|
217
|
+
{
|
|
218
|
+
case 'AutoIdentity':
|
|
219
|
+
if (pQuery.parameters.query.disableAutoIdentity)
|
|
220
|
+
{
|
|
221
|
+
tmpDocument[tmpColumn] = tmpRecord[tmpColumn];
|
|
222
|
+
}
|
|
223
|
+
else
|
|
224
|
+
{
|
|
225
|
+
tmpDocument[tmpColumn] = '$$AUTOINCREMENT';
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
case 'AutoGUID':
|
|
229
|
+
if (pQuery.parameters.query.disableAutoIdentity)
|
|
230
|
+
{
|
|
231
|
+
tmpDocument[tmpColumn] = tmpRecord[tmpColumn];
|
|
232
|
+
}
|
|
233
|
+
else if (tmpRecord[tmpColumn] &&
|
|
234
|
+
tmpRecord[tmpColumn].length >= 5 &&
|
|
235
|
+
tmpRecord[tmpColumn] !== '0x0000000000000000')
|
|
236
|
+
{
|
|
237
|
+
tmpDocument[tmpColumn] = tmpRecord[tmpColumn];
|
|
238
|
+
}
|
|
239
|
+
else
|
|
240
|
+
{
|
|
241
|
+
tmpDocument[tmpColumn] = pQuery.parameters.query.UUID;
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
case 'UpdateDate':
|
|
245
|
+
case 'CreateDate':
|
|
246
|
+
if (pQuery.parameters.query.disableAutoDateStamp)
|
|
247
|
+
{
|
|
248
|
+
tmpDocument[tmpColumn] = tmpRecord[tmpColumn];
|
|
249
|
+
}
|
|
250
|
+
else
|
|
251
|
+
{
|
|
252
|
+
tmpDocument[tmpColumn] = new Date().toISOString();
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
case 'DeleteIDUser':
|
|
256
|
+
case 'UpdateIDUser':
|
|
257
|
+
case 'CreateIDUser':
|
|
258
|
+
if (pQuery.parameters.query.disableAutoUserStamp)
|
|
259
|
+
{
|
|
260
|
+
tmpDocument[tmpColumn] = tmpRecord[tmpColumn];
|
|
261
|
+
}
|
|
262
|
+
else
|
|
263
|
+
{
|
|
264
|
+
tmpDocument[tmpColumn] = pQuery.parameters.query.IDUser;
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
case 'Deleted':
|
|
268
|
+
if (pQuery.parameters.query.disableDeleteTracking)
|
|
269
|
+
{
|
|
270
|
+
tmpDocument[tmpColumn] = tmpRecord[tmpColumn];
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
{
|
|
274
|
+
tmpDocument[tmpColumn] = 0;
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
default:
|
|
278
|
+
tmpDocument[tmpColumn] = tmpRecord[tmpColumn];
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return tmpDocument;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Build the update $set fields from the query's marshalled records.
|
|
288
|
+
* Skips identity, create, and delete columns.
|
|
289
|
+
*/
|
|
290
|
+
var buildUpdateFields = function (pQuery)
|
|
291
|
+
{
|
|
292
|
+
var tmpRecords = pQuery.parameters.query.records;
|
|
293
|
+
if (!Array.isArray(tmpRecords) || tmpRecords.length < 1)
|
|
294
|
+
{
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
var tmpUpdateDoc = {};
|
|
299
|
+
var tmpRecord = tmpRecords[0];
|
|
300
|
+
|
|
301
|
+
for (var tmpColumn in tmpRecord)
|
|
302
|
+
{
|
|
303
|
+
if (!tmpRecord.hasOwnProperty(tmpColumn))
|
|
304
|
+
{
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
var tmpSchemaEntry = findSchemaEntry(tmpColumn);
|
|
309
|
+
|
|
310
|
+
// Skip columns that shouldn't be updated
|
|
311
|
+
switch (tmpSchemaEntry.Type)
|
|
312
|
+
{
|
|
313
|
+
case 'AutoIdentity':
|
|
314
|
+
case 'AutoGUID':
|
|
315
|
+
case 'CreateDate':
|
|
316
|
+
case 'CreateIDUser':
|
|
317
|
+
case 'DeleteDate':
|
|
318
|
+
case 'DeleteIDUser':
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
switch (tmpSchemaEntry.Type)
|
|
323
|
+
{
|
|
324
|
+
case 'UpdateDate':
|
|
325
|
+
if (!pQuery.parameters.query.disableAutoDateStamp)
|
|
326
|
+
{
|
|
327
|
+
tmpUpdateDoc[tmpColumn] = new Date().toISOString();
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
case 'UpdateIDUser':
|
|
331
|
+
if (!pQuery.parameters.query.disableAutoUserStamp)
|
|
332
|
+
{
|
|
333
|
+
tmpUpdateDoc[tmpColumn] = pQuery.parameters.query.IDUser;
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
default:
|
|
337
|
+
tmpUpdateDoc[tmpColumn] = tmpRecord[tmpColumn];
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return tmpUpdateDoc;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Build the soft-delete field updates.
|
|
347
|
+
*/
|
|
348
|
+
var buildDeleteSetters = function (pQuery)
|
|
349
|
+
{
|
|
350
|
+
if (pQuery.parameters.query.disableDeleteTracking)
|
|
351
|
+
{
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
var tmpHasDeletedField = false;
|
|
356
|
+
var tmpSetters = {};
|
|
357
|
+
|
|
358
|
+
for (var i = 0; i < _Schema.length; i++)
|
|
359
|
+
{
|
|
360
|
+
var tmpSchemaEntry = _Schema[i];
|
|
361
|
+
switch (tmpSchemaEntry.Type)
|
|
362
|
+
{
|
|
363
|
+
case 'Deleted':
|
|
364
|
+
tmpSetters[tmpSchemaEntry.Column] = 1;
|
|
365
|
+
tmpHasDeletedField = true;
|
|
366
|
+
break;
|
|
367
|
+
case 'DeleteDate':
|
|
368
|
+
tmpSetters[tmpSchemaEntry.Column] = new Date().toISOString();
|
|
369
|
+
break;
|
|
370
|
+
case 'UpdateDate':
|
|
371
|
+
tmpSetters[tmpSchemaEntry.Column] = new Date().toISOString();
|
|
372
|
+
break;
|
|
373
|
+
case 'DeleteIDUser':
|
|
374
|
+
tmpSetters[tmpSchemaEntry.Column] = pQuery.parameters.query.IDUser;
|
|
375
|
+
break;
|
|
376
|
+
default:
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!tmpHasDeletedField || Object.keys(tmpSetters).length === 0)
|
|
382
|
+
{
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return tmpSetters;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Build the undelete field updates.
|
|
391
|
+
*/
|
|
392
|
+
var buildUndeleteSetters = function (pQuery)
|
|
393
|
+
{
|
|
394
|
+
var tmpHasDeletedField = false;
|
|
395
|
+
var tmpSetters = {};
|
|
396
|
+
|
|
397
|
+
for (var i = 0; i < _Schema.length; i++)
|
|
398
|
+
{
|
|
399
|
+
var tmpSchemaEntry = _Schema[i];
|
|
400
|
+
switch (tmpSchemaEntry.Type)
|
|
401
|
+
{
|
|
402
|
+
case 'Deleted':
|
|
403
|
+
tmpSetters[tmpSchemaEntry.Column] = 0;
|
|
404
|
+
tmpHasDeletedField = true;
|
|
405
|
+
break;
|
|
406
|
+
case 'UpdateDate':
|
|
407
|
+
tmpSetters[tmpSchemaEntry.Column] = new Date().toISOString();
|
|
408
|
+
break;
|
|
409
|
+
case 'UpdateIDUser':
|
|
410
|
+
tmpSetters[tmpSchemaEntry.Column] = pQuery.parameters.query.IDUser;
|
|
411
|
+
break;
|
|
412
|
+
default:
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!tmpHasDeletedField || Object.keys(tmpSetters).length === 0)
|
|
418
|
+
{
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return tmpSetters;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
// ============================================================
|
|
427
|
+
// In-Memory Filter Engine
|
|
428
|
+
// ============================================================
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Build the filter array from query parameters, adding Deleted=0 if needed.
|
|
432
|
+
*/
|
|
433
|
+
var buildFilterArray = function (pQuery, pDisableDeleteTracking)
|
|
434
|
+
{
|
|
435
|
+
var tmpFilter = Array.isArray(pQuery.parameters.filter) ? pQuery.parameters.filter.slice() : [];
|
|
436
|
+
|
|
437
|
+
// Auto-add Deleted filter if applicable
|
|
438
|
+
var tmpDisableDeleteTracking = pDisableDeleteTracking || pQuery.parameters.query.disableDeleteTracking;
|
|
439
|
+
if (!tmpDisableDeleteTracking)
|
|
440
|
+
{
|
|
441
|
+
for (var i = 0; i < _Schema.length; i++)
|
|
442
|
+
{
|
|
443
|
+
if (_Schema[i].Type === 'Deleted')
|
|
444
|
+
{
|
|
445
|
+
// Check if a Deleted filter already exists
|
|
446
|
+
var tmpHasDeletedParam = false;
|
|
447
|
+
for (var x = 0; x < tmpFilter.length; x++)
|
|
448
|
+
{
|
|
449
|
+
if (stripTablePrefix(tmpFilter[x].Column) === _Schema[i].Column)
|
|
450
|
+
{
|
|
451
|
+
tmpHasDeletedParam = true;
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (!tmpHasDeletedParam)
|
|
456
|
+
{
|
|
457
|
+
tmpFilter.push({
|
|
458
|
+
Column: _Schema[i].Column,
|
|
459
|
+
Operator: '=',
|
|
460
|
+
Value: 0,
|
|
461
|
+
Connector: 'AND',
|
|
462
|
+
Parameter: 'Deleted'
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return tmpFilter;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Evaluate a single filter entry against a record.
|
|
475
|
+
*/
|
|
476
|
+
var evaluateFilterEntry = function (pEntry, pRecord)
|
|
477
|
+
{
|
|
478
|
+
var tmpColumn = stripTablePrefix(pEntry.Column);
|
|
479
|
+
var tmpValue = pRecord[tmpColumn];
|
|
480
|
+
var tmpExpected = pEntry.Value;
|
|
481
|
+
|
|
482
|
+
switch (pEntry.Operator)
|
|
483
|
+
{
|
|
484
|
+
case '=':
|
|
485
|
+
// eslint-disable-next-line eqeqeq
|
|
486
|
+
return tmpValue == tmpExpected;
|
|
487
|
+
case '!=':
|
|
488
|
+
// eslint-disable-next-line eqeqeq
|
|
489
|
+
return tmpValue != tmpExpected;
|
|
490
|
+
case '>':
|
|
491
|
+
return tmpValue > tmpExpected;
|
|
492
|
+
case '>=':
|
|
493
|
+
return tmpValue >= tmpExpected;
|
|
494
|
+
case '<':
|
|
495
|
+
return tmpValue < tmpExpected;
|
|
496
|
+
case '<=':
|
|
497
|
+
return tmpValue <= tmpExpected;
|
|
498
|
+
case 'LIKE':
|
|
499
|
+
// Convert SQL LIKE pattern to regex: % -> .*, _ -> .
|
|
500
|
+
var tmpPattern = String(tmpExpected).replace(/%/g, '.*').replace(/_/g, '.');
|
|
501
|
+
try
|
|
502
|
+
{
|
|
503
|
+
var tmpRegex = new RegExp('^' + tmpPattern + '$', 'i');
|
|
504
|
+
return tmpRegex.test(String(tmpValue || ''));
|
|
505
|
+
}
|
|
506
|
+
catch (pError)
|
|
507
|
+
{
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
case 'IN':
|
|
511
|
+
if (!Array.isArray(tmpExpected))
|
|
512
|
+
{
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
for (var i = 0; i < tmpExpected.length; i++)
|
|
516
|
+
{
|
|
517
|
+
// eslint-disable-next-line eqeqeq
|
|
518
|
+
if (tmpValue == tmpExpected[i])
|
|
519
|
+
{
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return false;
|
|
524
|
+
case 'NOT IN':
|
|
525
|
+
if (!Array.isArray(tmpExpected))
|
|
526
|
+
{
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
for (var n = 0; n < tmpExpected.length; n++)
|
|
530
|
+
{
|
|
531
|
+
// eslint-disable-next-line eqeqeq
|
|
532
|
+
if (tmpValue == tmpExpected[n])
|
|
533
|
+
{
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return true;
|
|
538
|
+
case 'IS NULL':
|
|
539
|
+
return (tmpValue === null || tmpValue === undefined);
|
|
540
|
+
case 'IS NOT NULL':
|
|
541
|
+
return (tmpValue !== null && tmpValue !== undefined);
|
|
542
|
+
case '(':
|
|
543
|
+
case ')':
|
|
544
|
+
// Parenthetical grouping handled by evaluateFilterArray
|
|
545
|
+
return true;
|
|
546
|
+
default:
|
|
547
|
+
// Unknown operator, treat as equality
|
|
548
|
+
// eslint-disable-next-line eqeqeq
|
|
549
|
+
return tmpValue == tmpExpected;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Evaluate a FoxHound filter array against a record.
|
|
555
|
+
* Supports AND/OR connectors and parenthetical grouping.
|
|
556
|
+
*/
|
|
557
|
+
var evaluateFilterArray = function (pFilterArray, pRecord)
|
|
558
|
+
{
|
|
559
|
+
if (!pFilterArray || pFilterArray.length === 0)
|
|
560
|
+
{
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Stack-based processing for parenthetical groups
|
|
565
|
+
var tmpStack = [[]]; // Stack of condition result arrays
|
|
566
|
+
|
|
567
|
+
for (var i = 0; i < pFilterArray.length; i++)
|
|
568
|
+
{
|
|
569
|
+
var tmpEntry = pFilterArray[i];
|
|
570
|
+
|
|
571
|
+
if (tmpEntry.Operator === '(')
|
|
572
|
+
{
|
|
573
|
+
tmpStack.push([]);
|
|
574
|
+
}
|
|
575
|
+
else if (tmpEntry.Operator === ')')
|
|
576
|
+
{
|
|
577
|
+
var tmpGroupResults = tmpStack.pop();
|
|
578
|
+
var tmpGroupResult = resolveConditionGroup(tmpGroupResults);
|
|
579
|
+
tmpStack[tmpStack.length - 1].push({
|
|
580
|
+
result: tmpGroupResult,
|
|
581
|
+
connector: tmpEntry.Connector || 'AND'
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
else
|
|
585
|
+
{
|
|
586
|
+
var tmpResult = evaluateFilterEntry(tmpEntry, pRecord);
|
|
587
|
+
tmpStack[tmpStack.length - 1].push({
|
|
588
|
+
result: tmpResult,
|
|
589
|
+
connector: tmpEntry.Connector || 'AND'
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return resolveConditionGroup(tmpStack[0]);
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Resolve a group of condition results using AND/OR logic.
|
|
599
|
+
*/
|
|
600
|
+
var resolveConditionGroup = function (pConditions)
|
|
601
|
+
{
|
|
602
|
+
if (!pConditions || pConditions.length === 0)
|
|
603
|
+
{
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Start with the first condition's result
|
|
608
|
+
var tmpResult = pConditions[0].result;
|
|
609
|
+
|
|
610
|
+
for (var i = 1; i < pConditions.length; i++)
|
|
611
|
+
{
|
|
612
|
+
var tmpConnector = pConditions[i].connector || 'AND';
|
|
613
|
+
if (tmpConnector === 'OR')
|
|
614
|
+
{
|
|
615
|
+
tmpResult = tmpResult || pConditions[i].result;
|
|
616
|
+
}
|
|
617
|
+
else
|
|
618
|
+
{
|
|
619
|
+
tmpResult = tmpResult && pConditions[i].result;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return tmpResult;
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
// ============================================================
|
|
628
|
+
// Prefix Scanning
|
|
629
|
+
// ============================================================
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Scan all records with a given prefix and collect them.
|
|
633
|
+
*/
|
|
634
|
+
var scanPrefix = function (pDB, pPrefix, fCallback)
|
|
635
|
+
{
|
|
636
|
+
var tmpResults = [];
|
|
637
|
+
var tmpIterator = pDB.iterator({
|
|
638
|
+
gte: pPrefix,
|
|
639
|
+
lt: pPrefix + '\uffff',
|
|
640
|
+
keyAsBuffer: false,
|
|
641
|
+
valueAsBuffer: false
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
function readNext()
|
|
645
|
+
{
|
|
646
|
+
tmpIterator.next(function (pError, pKey, pValue)
|
|
647
|
+
{
|
|
648
|
+
if (pError)
|
|
649
|
+
{
|
|
650
|
+
return tmpIterator.end(function ()
|
|
651
|
+
{
|
|
652
|
+
return fCallback(pError, tmpResults);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (pKey === undefined)
|
|
656
|
+
{
|
|
657
|
+
// End of iteration
|
|
658
|
+
return tmpIterator.end(function ()
|
|
659
|
+
{
|
|
660
|
+
return fCallback(null, tmpResults);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
try
|
|
665
|
+
{
|
|
666
|
+
var tmpRecord = JSON.parse(pValue.toString());
|
|
667
|
+
tmpRecord._rocksdb_key = pKey.toString();
|
|
668
|
+
tmpResults.push(tmpRecord);
|
|
669
|
+
}
|
|
670
|
+
catch (pParseError)
|
|
671
|
+
{
|
|
672
|
+
_Fable.log.error('RocksDB: Failed to parse record at key [' + pKey + ']: ' + pParseError);
|
|
673
|
+
}
|
|
674
|
+
readNext();
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
readNext();
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
// ============================================================
|
|
682
|
+
// Sorting and Pagination
|
|
683
|
+
// ============================================================
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Sort records in-memory using the FoxHound sort array.
|
|
687
|
+
*/
|
|
688
|
+
var applySort = function (pRecords, pSortArray)
|
|
689
|
+
{
|
|
690
|
+
if (!Array.isArray(pSortArray) || pSortArray.length === 0)
|
|
691
|
+
{
|
|
692
|
+
return pRecords;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
pRecords.sort(function (pA, pB)
|
|
696
|
+
{
|
|
697
|
+
for (var s = 0; s < pSortArray.length; s++)
|
|
698
|
+
{
|
|
699
|
+
var tmpCol = stripTablePrefix(pSortArray[s].Column);
|
|
700
|
+
var tmpDir = (pSortArray[s].Direction === 'Descending') ? -1 : 1;
|
|
701
|
+
var tmpValA = pA[tmpCol];
|
|
702
|
+
var tmpValB = pB[tmpCol];
|
|
703
|
+
|
|
704
|
+
if (tmpValA === undefined || tmpValA === null) tmpValA = '';
|
|
705
|
+
if (tmpValB === undefined || tmpValB === null) tmpValB = '';
|
|
706
|
+
|
|
707
|
+
if (tmpValA < tmpValB) return -1 * tmpDir;
|
|
708
|
+
if (tmpValA > tmpValB) return 1 * tmpDir;
|
|
709
|
+
}
|
|
710
|
+
return 0;
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
return pRecords;
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Apply pagination (skip + limit).
|
|
718
|
+
*/
|
|
719
|
+
var applyPagination = function (pRecords, pBegin, pCap)
|
|
720
|
+
{
|
|
721
|
+
var tmpStart = (pBegin !== false && pBegin > 0) ? pBegin : 0;
|
|
722
|
+
if (pCap)
|
|
723
|
+
{
|
|
724
|
+
return pRecords.slice(tmpStart, tmpStart + pCap);
|
|
725
|
+
}
|
|
726
|
+
if (tmpStart > 0)
|
|
727
|
+
{
|
|
728
|
+
return pRecords.slice(tmpStart);
|
|
729
|
+
}
|
|
730
|
+
return pRecords;
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
// ============================================================
|
|
735
|
+
// Read Records Helper (scan + filter + sort + paginate)
|
|
736
|
+
// ============================================================
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Read matching records for a Read or Count operation.
|
|
740
|
+
*/
|
|
741
|
+
var readMatchingRecords = function (pDB, pScope, pFilterArray, pSortArray, pBegin, pCap, fCallback)
|
|
742
|
+
{
|
|
743
|
+
var tmpPrefix = buildScanPrefix(pScope);
|
|
744
|
+
scanPrefix(pDB, tmpPrefix, function (pScanError, pRecords)
|
|
745
|
+
{
|
|
746
|
+
if (pScanError)
|
|
747
|
+
{
|
|
748
|
+
return fCallback(pScanError, []);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Apply filter
|
|
752
|
+
var tmpFiltered = [];
|
|
753
|
+
for (var i = 0; i < pRecords.length; i++)
|
|
754
|
+
{
|
|
755
|
+
if (evaluateFilterArray(pFilterArray, pRecords[i]))
|
|
756
|
+
{
|
|
757
|
+
tmpFiltered.push(pRecords[i]);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Apply sort
|
|
762
|
+
tmpFiltered = applySort(tmpFiltered, pSortArray);
|
|
763
|
+
|
|
764
|
+
// Apply pagination
|
|
765
|
+
tmpFiltered = applyPagination(tmpFiltered, pBegin, pCap);
|
|
766
|
+
|
|
767
|
+
return fCallback(null, tmpFiltered);
|
|
768
|
+
});
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
// ============================================================
|
|
773
|
+
// Marshal
|
|
774
|
+
// ============================================================
|
|
775
|
+
|
|
776
|
+
var marshalRecordFromSourceToObject = function (pObject, pRecord)
|
|
777
|
+
{
|
|
778
|
+
for (var tmpColumn in pRecord)
|
|
779
|
+
{
|
|
780
|
+
// Skip internal RocksDB key tracking field
|
|
781
|
+
if (tmpColumn === '_rocksdb_key')
|
|
782
|
+
{
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
pObject[tmpColumn] = pRecord[tmpColumn];
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
// ============================================================
|
|
791
|
+
// CRUD Operations
|
|
792
|
+
// ============================================================
|
|
793
|
+
|
|
794
|
+
var Create = function (pQuery, fCallback)
|
|
795
|
+
{
|
|
796
|
+
var tmpResult = pQuery.parameters.result;
|
|
797
|
+
|
|
798
|
+
if (pQuery.logLevel > 0 || _GlobalLogLevel > 0)
|
|
799
|
+
{
|
|
800
|
+
_Fable.log.trace('RocksDB Create', { scope: _Scope });
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
var tmpDB = getDB();
|
|
804
|
+
if (!tmpDB)
|
|
805
|
+
{
|
|
806
|
+
tmpResult.error = new Error('No RocksDB connection available.');
|
|
807
|
+
tmpResult.executed = true;
|
|
808
|
+
return fCallback();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
var tmpDocument = buildCreateDocument(pQuery);
|
|
812
|
+
if (!tmpDocument)
|
|
813
|
+
{
|
|
814
|
+
tmpResult.error = new Error('No RocksDB document generated for Create.');
|
|
815
|
+
tmpResult.executed = true;
|
|
816
|
+
return fCallback();
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Check for $$AUTOINCREMENT sentinel
|
|
820
|
+
var tmpAutoIncrementColumn = false;
|
|
821
|
+
for (var tmpKey in tmpDocument)
|
|
822
|
+
{
|
|
823
|
+
if (tmpDocument[tmpKey] === '$$AUTOINCREMENT')
|
|
824
|
+
{
|
|
825
|
+
tmpAutoIncrementColumn = tmpKey;
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
var doInsert = function (pAutoIncrementValue)
|
|
831
|
+
{
|
|
832
|
+
if (tmpAutoIncrementColumn && pAutoIncrementValue)
|
|
833
|
+
{
|
|
834
|
+
tmpDocument[tmpAutoIncrementColumn] = pAutoIncrementValue;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
var tmpRecordKey = buildRecordKey(_Scope, tmpDocument);
|
|
838
|
+
var tmpRecordValue = JSON.stringify(tmpDocument);
|
|
839
|
+
|
|
840
|
+
tmpDB.put(tmpRecordKey, tmpRecordValue, function (pPutError)
|
|
841
|
+
{
|
|
842
|
+
if (pPutError)
|
|
843
|
+
{
|
|
844
|
+
tmpResult.error = pPutError;
|
|
845
|
+
tmpResult.value = false;
|
|
846
|
+
tmpResult.executed = true;
|
|
847
|
+
return fCallback();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
tmpResult.error = null;
|
|
851
|
+
tmpResult.value = pAutoIncrementValue || tmpDocument[_DefaultIdentifier];
|
|
852
|
+
tmpResult.executed = true;
|
|
853
|
+
return fCallback();
|
|
854
|
+
});
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
if (tmpAutoIncrementColumn)
|
|
858
|
+
{
|
|
859
|
+
getNextSequence(tmpDB, _Scope, function (pSeqError, pSeqValue)
|
|
860
|
+
{
|
|
861
|
+
if (pSeqError)
|
|
862
|
+
{
|
|
863
|
+
tmpResult.error = pSeqError;
|
|
864
|
+
tmpResult.value = false;
|
|
865
|
+
tmpResult.executed = true;
|
|
866
|
+
return fCallback();
|
|
867
|
+
}
|
|
868
|
+
doInsert(pSeqValue);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
else
|
|
872
|
+
{
|
|
873
|
+
doInsert();
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
var Read = function (pQuery, fCallback)
|
|
879
|
+
{
|
|
880
|
+
var tmpResult = pQuery.parameters.result;
|
|
881
|
+
|
|
882
|
+
if (pQuery.logLevel > 0 || _GlobalLogLevel > 0)
|
|
883
|
+
{
|
|
884
|
+
_Fable.log.trace('RocksDB Read', { scope: _Scope, filter: pQuery.parameters.filter });
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
var tmpDB = getDB();
|
|
888
|
+
if (!tmpDB)
|
|
889
|
+
{
|
|
890
|
+
tmpResult.error = new Error('No RocksDB connection available.');
|
|
891
|
+
tmpResult.executed = true;
|
|
892
|
+
return fCallback();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
var tmpFilterArray = buildFilterArray(pQuery);
|
|
896
|
+
var tmpSortArray = pQuery.parameters.sort || false;
|
|
897
|
+
var tmpBegin = pQuery.parameters.begin;
|
|
898
|
+
var tmpCap = pQuery.parameters.cap;
|
|
899
|
+
|
|
900
|
+
readMatchingRecords(tmpDB, _Scope, tmpFilterArray, tmpSortArray, tmpBegin, tmpCap, function (pReadError, pRecords)
|
|
901
|
+
{
|
|
902
|
+
if (pReadError)
|
|
903
|
+
{
|
|
904
|
+
tmpResult.error = pReadError;
|
|
905
|
+
tmpResult.value = false;
|
|
906
|
+
tmpResult.executed = true;
|
|
907
|
+
return fCallback();
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Strip internal keys before returning
|
|
911
|
+
for (var i = 0; i < pRecords.length; i++)
|
|
912
|
+
{
|
|
913
|
+
delete pRecords[i]._rocksdb_key;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
tmpResult.error = null;
|
|
917
|
+
tmpResult.value = pRecords;
|
|
918
|
+
tmpResult.executed = true;
|
|
919
|
+
return fCallback();
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
var Update = function (pQuery, fCallback)
|
|
925
|
+
{
|
|
926
|
+
var tmpResult = pQuery.parameters.result;
|
|
927
|
+
|
|
928
|
+
if (pQuery.logLevel > 0 || _GlobalLogLevel > 0)
|
|
929
|
+
{
|
|
930
|
+
_Fable.log.trace('RocksDB Update', { scope: _Scope, filter: pQuery.parameters.filter });
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
var tmpDB = getDB();
|
|
934
|
+
if (!tmpDB)
|
|
935
|
+
{
|
|
936
|
+
tmpResult.error = new Error('No RocksDB connection available.');
|
|
937
|
+
tmpResult.executed = true;
|
|
938
|
+
return fCallback();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
var tmpUpdateFields = buildUpdateFields(pQuery);
|
|
942
|
+
if (!tmpUpdateFields || Object.keys(tmpUpdateFields).length === 0)
|
|
943
|
+
{
|
|
944
|
+
tmpResult.error = null;
|
|
945
|
+
tmpResult.value = 0;
|
|
946
|
+
tmpResult.executed = true;
|
|
947
|
+
return fCallback();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
var tmpFilterArray = buildFilterArray(pQuery);
|
|
951
|
+
|
|
952
|
+
// Read all matching records (no sort/pagination needed for update)
|
|
953
|
+
readMatchingRecords(tmpDB, _Scope, tmpFilterArray, false, false, false, function (pReadError, pRecords)
|
|
954
|
+
{
|
|
955
|
+
if (pReadError)
|
|
956
|
+
{
|
|
957
|
+
tmpResult.error = pReadError;
|
|
958
|
+
tmpResult.value = false;
|
|
959
|
+
tmpResult.executed = true;
|
|
960
|
+
return fCallback();
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (pRecords.length === 0)
|
|
964
|
+
{
|
|
965
|
+
tmpResult.error = null;
|
|
966
|
+
tmpResult.value = 0;
|
|
967
|
+
tmpResult.executed = true;
|
|
968
|
+
return fCallback();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Build batch of updates
|
|
972
|
+
var tmpBatchOps = [];
|
|
973
|
+
for (var i = 0; i < pRecords.length; i++)
|
|
974
|
+
{
|
|
975
|
+
var tmpRecord = pRecords[i];
|
|
976
|
+
var tmpRecordKey = tmpRecord._rocksdb_key;
|
|
977
|
+
|
|
978
|
+
// Apply update fields
|
|
979
|
+
for (var tmpCol in tmpUpdateFields)
|
|
980
|
+
{
|
|
981
|
+
if (tmpUpdateFields.hasOwnProperty(tmpCol))
|
|
982
|
+
{
|
|
983
|
+
tmpRecord[tmpCol] = tmpUpdateFields[tmpCol];
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Remove internal key before storing
|
|
988
|
+
delete tmpRecord._rocksdb_key;
|
|
989
|
+
|
|
990
|
+
tmpBatchOps.push({
|
|
991
|
+
type: 'put',
|
|
992
|
+
key: tmpRecordKey,
|
|
993
|
+
value: JSON.stringify(tmpRecord)
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
tmpDB.batch(tmpBatchOps, function (pBatchError)
|
|
998
|
+
{
|
|
999
|
+
if (pBatchError)
|
|
1000
|
+
{
|
|
1001
|
+
tmpResult.error = pBatchError;
|
|
1002
|
+
tmpResult.value = false;
|
|
1003
|
+
tmpResult.executed = true;
|
|
1004
|
+
return fCallback();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
tmpResult.error = null;
|
|
1008
|
+
// Return as an object (Meadow-Update behavior checks typeof === 'object')
|
|
1009
|
+
tmpResult.value = { changes: pRecords.length };
|
|
1010
|
+
tmpResult.executed = true;
|
|
1011
|
+
return fCallback();
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
var Delete = function (pQuery, fCallback)
|
|
1018
|
+
{
|
|
1019
|
+
var tmpResult = pQuery.parameters.result;
|
|
1020
|
+
|
|
1021
|
+
if (pQuery.logLevel > 0 || _GlobalLogLevel > 0)
|
|
1022
|
+
{
|
|
1023
|
+
_Fable.log.trace('RocksDB Delete', { scope: _Scope, filter: pQuery.parameters.filter });
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
var tmpDB = getDB();
|
|
1027
|
+
if (!tmpDB)
|
|
1028
|
+
{
|
|
1029
|
+
tmpResult.error = new Error('No RocksDB connection available.');
|
|
1030
|
+
tmpResult.executed = true;
|
|
1031
|
+
return fCallback();
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
var tmpDeleteSetters = buildDeleteSetters(pQuery);
|
|
1035
|
+
var tmpFilterArray = buildFilterArray(pQuery);
|
|
1036
|
+
|
|
1037
|
+
if (tmpDeleteSetters)
|
|
1038
|
+
{
|
|
1039
|
+
// Soft delete — update matched records with delete fields
|
|
1040
|
+
readMatchingRecords(tmpDB, _Scope, tmpFilterArray, false, false, false, function (pReadError, pRecords)
|
|
1041
|
+
{
|
|
1042
|
+
if (pReadError)
|
|
1043
|
+
{
|
|
1044
|
+
tmpResult.error = pReadError;
|
|
1045
|
+
tmpResult.value = false;
|
|
1046
|
+
tmpResult.executed = true;
|
|
1047
|
+
return fCallback();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (pRecords.length === 0)
|
|
1051
|
+
{
|
|
1052
|
+
tmpResult.error = null;
|
|
1053
|
+
tmpResult.value = 0;
|
|
1054
|
+
tmpResult.executed = true;
|
|
1055
|
+
return fCallback();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
var tmpBatchOps = [];
|
|
1059
|
+
for (var i = 0; i < pRecords.length; i++)
|
|
1060
|
+
{
|
|
1061
|
+
var tmpRecord = pRecords[i];
|
|
1062
|
+
var tmpRecordKey = tmpRecord._rocksdb_key;
|
|
1063
|
+
for (var tmpCol in tmpDeleteSetters)
|
|
1064
|
+
{
|
|
1065
|
+
if (tmpDeleteSetters.hasOwnProperty(tmpCol))
|
|
1066
|
+
{
|
|
1067
|
+
tmpRecord[tmpCol] = tmpDeleteSetters[tmpCol];
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
delete tmpRecord._rocksdb_key;
|
|
1071
|
+
tmpBatchOps.push({
|
|
1072
|
+
type: 'put',
|
|
1073
|
+
key: tmpRecordKey,
|
|
1074
|
+
value: JSON.stringify(tmpRecord)
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
tmpDB.batch(tmpBatchOps, function (pBatchError)
|
|
1079
|
+
{
|
|
1080
|
+
if (pBatchError)
|
|
1081
|
+
{
|
|
1082
|
+
tmpResult.error = pBatchError;
|
|
1083
|
+
tmpResult.value = false;
|
|
1084
|
+
tmpResult.executed = true;
|
|
1085
|
+
return fCallback();
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
tmpResult.error = null;
|
|
1089
|
+
tmpResult.value = pRecords.length;
|
|
1090
|
+
tmpResult.executed = true;
|
|
1091
|
+
return fCallback();
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
else
|
|
1096
|
+
{
|
|
1097
|
+
// Hard delete — remove matched records from RocksDB
|
|
1098
|
+
readMatchingRecords(tmpDB, _Scope, tmpFilterArray, false, false, false, function (pReadError, pRecords)
|
|
1099
|
+
{
|
|
1100
|
+
if (pReadError)
|
|
1101
|
+
{
|
|
1102
|
+
tmpResult.error = pReadError;
|
|
1103
|
+
tmpResult.value = false;
|
|
1104
|
+
tmpResult.executed = true;
|
|
1105
|
+
return fCallback();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (pRecords.length === 0)
|
|
1109
|
+
{
|
|
1110
|
+
tmpResult.error = null;
|
|
1111
|
+
tmpResult.value = 0;
|
|
1112
|
+
tmpResult.executed = true;
|
|
1113
|
+
return fCallback();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
var tmpBatchOps = [];
|
|
1117
|
+
for (var i = 0; i < pRecords.length; i++)
|
|
1118
|
+
{
|
|
1119
|
+
tmpBatchOps.push({
|
|
1120
|
+
type: 'del',
|
|
1121
|
+
key: pRecords[i]._rocksdb_key
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
tmpDB.batch(tmpBatchOps, function (pBatchError)
|
|
1126
|
+
{
|
|
1127
|
+
if (pBatchError)
|
|
1128
|
+
{
|
|
1129
|
+
tmpResult.error = pBatchError;
|
|
1130
|
+
tmpResult.value = false;
|
|
1131
|
+
tmpResult.executed = true;
|
|
1132
|
+
return fCallback();
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
tmpResult.error = null;
|
|
1136
|
+
tmpResult.value = pRecords.length;
|
|
1137
|
+
tmpResult.executed = true;
|
|
1138
|
+
return fCallback();
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
var Undelete = function (pQuery, fCallback)
|
|
1146
|
+
{
|
|
1147
|
+
var tmpResult = pQuery.parameters.result;
|
|
1148
|
+
|
|
1149
|
+
if (pQuery.logLevel > 0 || _GlobalLogLevel > 0)
|
|
1150
|
+
{
|
|
1151
|
+
_Fable.log.trace('RocksDB Undelete', { scope: _Scope, filter: pQuery.parameters.filter });
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
var tmpDB = getDB();
|
|
1155
|
+
if (!tmpDB)
|
|
1156
|
+
{
|
|
1157
|
+
tmpResult.error = new Error('No RocksDB connection available.');
|
|
1158
|
+
tmpResult.executed = true;
|
|
1159
|
+
return fCallback();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
var tmpUndeleteSetters = buildUndeleteSetters(pQuery);
|
|
1163
|
+
if (!tmpUndeleteSetters)
|
|
1164
|
+
{
|
|
1165
|
+
// No Deleted column in schema — nothing to undelete
|
|
1166
|
+
tmpResult.error = null;
|
|
1167
|
+
tmpResult.value = 0;
|
|
1168
|
+
tmpResult.executed = true;
|
|
1169
|
+
return fCallback();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Build filter with delete tracking disabled so we can find Deleted=1 records
|
|
1173
|
+
var tmpFilterArray = buildFilterArray(pQuery, true);
|
|
1174
|
+
|
|
1175
|
+
readMatchingRecords(tmpDB, _Scope, tmpFilterArray, false, false, false, function (pReadError, pRecords)
|
|
1176
|
+
{
|
|
1177
|
+
if (pReadError)
|
|
1178
|
+
{
|
|
1179
|
+
tmpResult.error = pReadError;
|
|
1180
|
+
tmpResult.value = false;
|
|
1181
|
+
tmpResult.executed = true;
|
|
1182
|
+
return fCallback();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (pRecords.length === 0)
|
|
1186
|
+
{
|
|
1187
|
+
tmpResult.error = null;
|
|
1188
|
+
tmpResult.value = 0;
|
|
1189
|
+
tmpResult.executed = true;
|
|
1190
|
+
return fCallback();
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
var tmpBatchOps = [];
|
|
1194
|
+
for (var i = 0; i < pRecords.length; i++)
|
|
1195
|
+
{
|
|
1196
|
+
var tmpRecord = pRecords[i];
|
|
1197
|
+
var tmpRecordKey = tmpRecord._rocksdb_key;
|
|
1198
|
+
for (var tmpCol in tmpUndeleteSetters)
|
|
1199
|
+
{
|
|
1200
|
+
if (tmpUndeleteSetters.hasOwnProperty(tmpCol))
|
|
1201
|
+
{
|
|
1202
|
+
tmpRecord[tmpCol] = tmpUndeleteSetters[tmpCol];
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
delete tmpRecord._rocksdb_key;
|
|
1206
|
+
tmpBatchOps.push({
|
|
1207
|
+
type: 'put',
|
|
1208
|
+
key: tmpRecordKey,
|
|
1209
|
+
value: JSON.stringify(tmpRecord)
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
tmpDB.batch(tmpBatchOps, function (pBatchError)
|
|
1214
|
+
{
|
|
1215
|
+
if (pBatchError)
|
|
1216
|
+
{
|
|
1217
|
+
tmpResult.error = pBatchError;
|
|
1218
|
+
tmpResult.value = false;
|
|
1219
|
+
tmpResult.executed = true;
|
|
1220
|
+
return fCallback();
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
tmpResult.error = null;
|
|
1224
|
+
tmpResult.value = pRecords.length;
|
|
1225
|
+
tmpResult.executed = true;
|
|
1226
|
+
return fCallback();
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
var Count = function (pQuery, fCallback)
|
|
1233
|
+
{
|
|
1234
|
+
var tmpResult = pQuery.parameters.result;
|
|
1235
|
+
|
|
1236
|
+
if (pQuery.logLevel > 0 || _GlobalLogLevel > 0)
|
|
1237
|
+
{
|
|
1238
|
+
_Fable.log.trace('RocksDB Count', { scope: _Scope, filter: pQuery.parameters.filter });
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
var tmpDB = getDB();
|
|
1242
|
+
if (!tmpDB)
|
|
1243
|
+
{
|
|
1244
|
+
tmpResult.error = new Error('No RocksDB connection available.');
|
|
1245
|
+
tmpResult.executed = true;
|
|
1246
|
+
return fCallback();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
var tmpFilterArray = buildFilterArray(pQuery);
|
|
1250
|
+
|
|
1251
|
+
// Scan and count (no sort/pagination needed for count)
|
|
1252
|
+
readMatchingRecords(tmpDB, _Scope, tmpFilterArray, false, false, false, function (pReadError, pRecords)
|
|
1253
|
+
{
|
|
1254
|
+
if (pReadError)
|
|
1255
|
+
{
|
|
1256
|
+
tmpResult.error = pReadError;
|
|
1257
|
+
tmpResult.value = false;
|
|
1258
|
+
tmpResult.executed = true;
|
|
1259
|
+
return fCallback();
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
tmpResult.error = null;
|
|
1263
|
+
tmpResult.value = pRecords.length;
|
|
1264
|
+
tmpResult.executed = true;
|
|
1265
|
+
return fCallback();
|
|
1266
|
+
});
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
// ============================================================
|
|
1271
|
+
// Provider Export
|
|
1272
|
+
// ============================================================
|
|
1273
|
+
|
|
1274
|
+
var tmpNewProvider = (
|
|
1275
|
+
{
|
|
1276
|
+
marshalRecordFromSourceToObject: marshalRecordFromSourceToObject,
|
|
1277
|
+
|
|
1278
|
+
Create: Create,
|
|
1279
|
+
Read: Read,
|
|
1280
|
+
Update: Update,
|
|
1281
|
+
Delete: Delete,
|
|
1282
|
+
Undelete: Undelete,
|
|
1283
|
+
Count: Count,
|
|
1284
|
+
|
|
1285
|
+
setSchema: setSchema,
|
|
1286
|
+
|
|
1287
|
+
getProvider: getProvider,
|
|
1288
|
+
providerCreatesSupported: true,
|
|
1289
|
+
|
|
1290
|
+
new: createNew
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
return tmpNewProvider;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
return createNew();
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
module.exports = new MeadowProvider();
|