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.
Files changed (59) hide show
  1. package/README.md +110 -141
  2. package/docs/README.md +34 -230
  3. package/docs/_cover.md +14 -0
  4. package/docs/_sidebar.md +44 -12
  5. package/docs/_topbar.md +5 -0
  6. package/docs/api/doCount.md +109 -0
  7. package/docs/api/doCreate.md +132 -0
  8. package/docs/api/doDelete.md +101 -0
  9. package/docs/api/doRead.md +122 -0
  10. package/docs/api/doReads.md +136 -0
  11. package/docs/api/doUndelete.md +98 -0
  12. package/docs/api/doUpdate.md +129 -0
  13. package/docs/api/getRoleName.md +84 -0
  14. package/docs/api/loadFromPackage.md +153 -0
  15. package/docs/api/marshalRecordFromSourceToObject.md +92 -0
  16. package/docs/api/query.md +133 -0
  17. package/docs/api/rawQueries.md +197 -0
  18. package/docs/api/reference.md +117 -0
  19. package/docs/api/setAuthorizer.md +103 -0
  20. package/docs/api/setDefault.md +90 -0
  21. package/docs/api/setDefaultIdentifier.md +84 -0
  22. package/docs/api/setDomain.md +56 -0
  23. package/docs/api/setIDUser.md +91 -0
  24. package/docs/api/setJsonSchema.md +92 -0
  25. package/docs/api/setProvider.md +87 -0
  26. package/docs/api/setSchema.md +107 -0
  27. package/docs/api/setScope.md +68 -0
  28. package/docs/api/validateObject.md +119 -0
  29. package/docs/architecture.md +316 -0
  30. package/docs/audit-tracking.md +226 -0
  31. package/docs/configuration.md +317 -0
  32. package/docs/providers/meadow-endpoints.md +306 -0
  33. package/docs/providers/mongodb.md +319 -0
  34. package/docs/providers/postgresql.md +312 -0
  35. package/docs/providers/rocksdb.md +297 -0
  36. package/docs/query-dsl.md +269 -0
  37. package/docs/quick-start.md +384 -0
  38. package/docs/raw-queries.md +193 -0
  39. package/docs/retold-catalog.json +61 -1
  40. package/docs/retold-keyword-index.json +15860 -4839
  41. package/docs/soft-deletes.md +224 -0
  42. package/package.json +36 -9
  43. package/scripts/bookstore-seed-postgresql.sql +135 -0
  44. package/scripts/dgraph-test-db.sh +144 -0
  45. package/scripts/meadow-test-cleanup.sh +5 -1
  46. package/scripts/mongodb-test-db.sh +98 -0
  47. package/scripts/postgresql-test-db.sh +124 -0
  48. package/scripts/solr-test-db.sh +135 -0
  49. package/source/Meadow.js +5 -0
  50. package/source/providers/Meadow-Provider-DGraph.js +679 -0
  51. package/source/providers/Meadow-Provider-MongoDB.js +527 -0
  52. package/source/providers/Meadow-Provider-PostgreSQL.js +361 -0
  53. package/source/providers/Meadow-Provider-RocksDB.js +1300 -0
  54. package/source/providers/Meadow-Provider-Solr.js +726 -0
  55. package/test/Meadow-Provider-DGraph_tests.js +741 -0
  56. package/test/Meadow-Provider-MongoDB_tests.js +661 -0
  57. package/test/Meadow-Provider-PostgreSQL_tests.js +787 -0
  58. package/test/Meadow-Provider-RocksDB_tests.js +887 -0
  59. 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();