jsonbadger 0.5.0 → 0.6.0

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 (123) hide show
  1. package/README.md +36 -18
  2. package/docs/api/connection.md +144 -0
  3. package/docs/api/delta-tracker.md +106 -0
  4. package/docs/api/document.md +77 -0
  5. package/docs/api/field-types.md +329 -0
  6. package/docs/api/index.md +35 -0
  7. package/docs/api/model.md +392 -0
  8. package/docs/api/query-builder.md +81 -0
  9. package/docs/api/schema.md +204 -0
  10. package/docs/architecture-flow.md +397 -0
  11. package/docs/examples.md +495 -218
  12. package/docs/jsonb-ops.md +171 -0
  13. package/docs/lifecycle/model-compilation.md +111 -0
  14. package/docs/lifecycle.md +146 -0
  15. package/docs/query-translation.md +11 -10
  16. package/package.json +10 -3
  17. package/src/connection/connect.js +12 -17
  18. package/src/connection/connection.js +128 -0
  19. package/src/connection/server-capabilities.js +60 -59
  20. package/src/constants/defaults.js +32 -19
  21. package/src/constants/{id-strategies.js → id-strategy.js} +28 -29
  22. package/src/constants/intake-mode.js +8 -0
  23. package/src/debug/debug-logger.js +17 -15
  24. package/src/errors/model-overwrite-error.js +25 -0
  25. package/src/errors/query-error.js +25 -23
  26. package/src/errors/validation-error.js +25 -23
  27. package/src/field-types/base-field-type.js +137 -140
  28. package/src/field-types/builtins/advanced.js +365 -365
  29. package/src/field-types/builtins/index.js +579 -585
  30. package/src/field-types/field-type-namespace.js +9 -0
  31. package/src/field-types/registry.js +149 -122
  32. package/src/index.js +26 -36
  33. package/src/migration/ensure-index.js +157 -154
  34. package/src/migration/ensure-schema.js +27 -15
  35. package/src/migration/ensure-table.js +44 -31
  36. package/src/migration/schema-indexes-resolver.js +8 -6
  37. package/src/model/document-instance.js +29 -540
  38. package/src/model/document.js +60 -0
  39. package/src/model/factory/constants.js +36 -0
  40. package/src/model/factory/index.js +58 -0
  41. package/src/model/model.js +875 -0
  42. package/src/model/operations/delete-one.js +39 -0
  43. package/src/model/operations/insert-one.js +35 -0
  44. package/src/model/operations/query-builder.js +132 -0
  45. package/src/model/operations/update-one.js +333 -0
  46. package/src/model/state.js +34 -0
  47. package/src/schema/field-definition-parser.js +213 -218
  48. package/src/schema/path-introspection.js +87 -82
  49. package/src/schema/schema-compiler.js +126 -212
  50. package/src/schema/schema.js +621 -138
  51. package/src/sql/index.js +17 -0
  52. package/src/sql/jsonb/ops.js +153 -0
  53. package/src/{query → sql/jsonb}/path-parser.js +54 -43
  54. package/src/sql/jsonb/read/elem-match.js +133 -0
  55. package/src/{query → sql/jsonb/read}/operators/contains.js +13 -7
  56. package/src/sql/jsonb/read/operators/elem-match.js +9 -0
  57. package/src/{query → sql/jsonb/read}/operators/has-all-keys.js +17 -11
  58. package/src/{query → sql/jsonb/read}/operators/has-any-keys.js +18 -11
  59. package/src/sql/jsonb/read/operators/has-key.js +12 -0
  60. package/src/{query → sql/jsonb/read}/operators/jsonpath-exists.js +22 -15
  61. package/src/{query → sql/jsonb/read}/operators/jsonpath-match.js +22 -15
  62. package/src/{query → sql/jsonb/read}/operators/size.js +23 -16
  63. package/src/sql/parameter-binder.js +18 -13
  64. package/src/sql/read/build-count-query.js +12 -0
  65. package/src/sql/read/build-find-query.js +25 -0
  66. package/src/sql/read/limit-skip.js +21 -0
  67. package/src/sql/read/sort.js +85 -0
  68. package/src/sql/read/where/base-fields.js +310 -0
  69. package/src/sql/read/where/casting.js +90 -0
  70. package/src/sql/read/where/context.js +79 -0
  71. package/src/sql/read/where/field-clause.js +58 -0
  72. package/src/sql/read/where/index.js +38 -0
  73. package/src/sql/read/where/operator-entries.js +29 -0
  74. package/src/{query → sql/read/where}/operators/all.js +16 -10
  75. package/src/sql/read/where/operators/eq.js +12 -0
  76. package/src/{query → sql/read/where}/operators/gt.js +23 -16
  77. package/src/{query → sql/read/where}/operators/gte.js +23 -16
  78. package/src/{query → sql/read/where}/operators/in.js +18 -12
  79. package/src/sql/read/where/operators/index.js +40 -0
  80. package/src/{query → sql/read/where}/operators/lt.js +23 -16
  81. package/src/{query → sql/read/where}/operators/lte.js +23 -16
  82. package/src/sql/read/where/operators/ne.js +12 -0
  83. package/src/{query → sql/read/where}/operators/nin.js +18 -12
  84. package/src/{query → sql/read/where}/operators/regex.js +14 -8
  85. package/src/sql/read/where/operators.js +126 -0
  86. package/src/sql/read/where/text-operators.js +83 -0
  87. package/src/sql/run.js +46 -0
  88. package/src/sql/write/build-delete-query.js +33 -0
  89. package/src/sql/write/build-insert-query.js +42 -0
  90. package/src/sql/write/build-update-query.js +65 -0
  91. package/src/utils/assert.js +34 -27
  92. package/src/utils/delta-tracker/.archive/1 tracker-redesign-codex-v2.md +250 -0
  93. package/src/utils/delta-tracker/.archive/1 tracker-redesign-gemini.md +101 -0
  94. package/src/utils/delta-tracker/.archive/2 evaluation by gemini.txt +65 -0
  95. package/src/utils/delta-tracker/.archive/2 evaluation by grok.txt +39 -0
  96. package/src/utils/delta-tracker/.archive/3 gemini evaluate grok.txt +37 -0
  97. package/src/utils/delta-tracker/.archive/3 grok evaluate gemini.txt +63 -0
  98. package/src/utils/delta-tracker/.archive/4 gemini veredict.txt +16 -0
  99. package/src/utils/delta-tracker/.archive/index.1.js +587 -0
  100. package/src/utils/delta-tracker/.archive/index.2.js +612 -0
  101. package/src/utils/delta-tracker/index.js +592 -0
  102. package/src/utils/dirty-tracker/inline.js +335 -0
  103. package/src/utils/dirty-tracker/instance.js +414 -0
  104. package/src/utils/dirty-tracker/static.js +343 -0
  105. package/src/utils/json-safe.js +13 -9
  106. package/src/utils/object-path.js +227 -33
  107. package/src/utils/object.js +408 -168
  108. package/src/utils/string.js +55 -0
  109. package/src/utils/value.js +169 -30
  110. package/docs/api.md +0 -152
  111. package/src/connection/disconnect.js +0 -16
  112. package/src/connection/pool-store.js +0 -46
  113. package/src/model/model-factory.js +0 -555
  114. package/src/query/limit-skip-compiler.js +0 -31
  115. package/src/query/operators/elem-match.js +0 -3
  116. package/src/query/operators/eq.js +0 -6
  117. package/src/query/operators/has-key.js +0 -6
  118. package/src/query/operators/index.js +0 -60
  119. package/src/query/operators/ne.js +0 -6
  120. package/src/query/query-builder.js +0 -93
  121. package/src/query/sort-compiler.js +0 -30
  122. package/src/query/where-compiler.js +0 -477
  123. package/src/sql/sql-runner.js +0 -31
@@ -0,0 +1,414 @@
1
+ import {to_array} from '#src/utils/array.js';
2
+ import {deep_clone, is_function, is_not_object, is_object} from '#src/utils/value.js';
3
+
4
+ // dirty-tracker: instance
5
+
6
+ /*
7
+ * MAIN API
8
+ */
9
+
10
+ /**
11
+ * Create a new state store instance that proxies a root object,
12
+ * optionally restricting tracking to specific child branches.
13
+ *
14
+ * @param {object} target
15
+ * @param {object} [options]
16
+ * @param {string[]} [options.track] - Array of top-level keys to track (e.g., ['data', 'fields'])
17
+ * @param {object} [options.watch]
18
+ * @param {function} [options.intercept_set]
19
+ * @returns {Proxy}
20
+ */
21
+ function DirtyTracker(target, options = {}) {
22
+ const track_list = get_track_list(options);
23
+ const base_state = clone_tracked_state(target, track_list);
24
+
25
+ const store = {
26
+ base_state,
27
+ dirty_keys: new Set(),
28
+ watchers: [],
29
+ // Reuse nested proxies so repeated reads preserve referential equality.
30
+ proxy_cache: new WeakMap()
31
+ };
32
+
33
+ let root_proxy;
34
+ const get_root = () => root_proxy;
35
+
36
+ // Build the intercepting proxy
37
+ root_proxy = build_proxy(target, target, store, get_root, options);
38
+ // Seed the cache with the root object so nested traversals can share proxy identity.
39
+ store.proxy_cache.set(target, root_proxy);
40
+
41
+ // Parse and bind Watchers
42
+ if(is_object(options.watch)) {
43
+ const watch_keys = Object.keys(options.watch);
44
+ let key_index = 0;
45
+
46
+ while(key_index < watch_keys.length) {
47
+ const watch_path = watch_keys[key_index];
48
+ add_watcher(store, target, root_proxy, watch_path, options.watch[watch_path]);
49
+ key_index += 1;
50
+ }
51
+ }
52
+
53
+ return root_proxy;
54
+ }
55
+
56
+ /*
57
+ * LAZY PROXY BUILDER
58
+ */
59
+
60
+ function build_proxy(target_object, root_object, store, get_root, options = {}) {
61
+ const track_list = get_track_list(options);
62
+ const base_path = options.base_path || '';
63
+ let intercept_set = (path, next_value) => next_value;
64
+
65
+ if(is_function(options.intercept_set)) {
66
+ intercept_set = options.intercept_set;
67
+ }
68
+
69
+ const internal_methods = Object.assign(Object.create(null), {
70
+ $has_dirty_fields: () => {
71
+ return has_dirty_fields(store);
72
+ },
73
+ $get_dirty_fields: () => {
74
+ return get_dirty_fields(store);
75
+ },
76
+ $reset_dirty_fields: () => {
77
+ return reset_dirty_fields(store, get_root());
78
+ },
79
+ $rebase_dirty_fields: () => {
80
+ return rebase_dirty_fields(store, root_object, options);
81
+ },
82
+ $watch: (path, watch_options) => {
83
+ return add_watcher(store, root_object, get_root(), path, watch_options);
84
+ }
85
+ });
86
+
87
+ return new Proxy(target_object, {
88
+ get(target, prop) {
89
+ // Do not proxy internal JS symbols
90
+ if(typeof prop === 'symbol') {
91
+ return target[prop];
92
+ }
93
+
94
+ // Root level: allow access to tracker methods and pass through untracked branches
95
+ if(base_path === '') {
96
+ // The instance variant keeps helper methods only at the root so tracked payload branches
97
+ // remain pure data and cannot collide with `$has_dirty_fields`-style names.
98
+ const internal_method = internal_methods[prop];
99
+
100
+ if(internal_method) {
101
+ return internal_method;
102
+ }
103
+
104
+ if(prop in target && is_function(target[prop])) {
105
+ return target[prop];
106
+ }
107
+
108
+ // Keep untracked root branches raw so only the declared tracked surface participates
109
+ // in dirty-state bookkeeping and helper-method interception.
110
+ if(track_list && !track_list.includes(prop)) {
111
+ return target[prop];
112
+ }
113
+ }
114
+
115
+ const value = target[prop];
116
+
117
+ // Lazy-proxy nested objects on access
118
+ if(is_object(value)) {
119
+ const cached_proxy = store.proxy_cache.get(value);
120
+
121
+ if(cached_proxy) {
122
+ // Reuse the same proxy instance so `tracker.user === tracker.user` stays true.
123
+ return cached_proxy;
124
+ }
125
+
126
+ const next_path = base_path === '' ? prop : `${base_path}.${prop}`;
127
+ const next_options = {...options, base_path: next_path};
128
+ const nested_proxy = build_proxy(value, root_object, store, get_root, next_options);
129
+
130
+ store.proxy_cache.set(value, nested_proxy);
131
+
132
+ return nested_proxy;
133
+ }
134
+
135
+ return value;
136
+ },
137
+
138
+ set(target, prop, value) {
139
+ if(typeof prop === 'symbol') {
140
+ target[prop] = value;
141
+ return true;
142
+ }
143
+
144
+ // Do not track mutations to functions/methods
145
+ if(base_path === '' && is_function(target[prop])) {
146
+ target[prop] = value;
147
+ return true;
148
+ }
149
+
150
+ // Bypass tracking for untracked top-level keys
151
+ if(base_path === '' && track_list && !track_list.includes(prop)) {
152
+ target[prop] = value;
153
+ return true;
154
+ }
155
+
156
+ const full_path = base_path === '' ? prop : `${base_path}.${prop}`;
157
+ const next_value = intercept_set(full_path, value);
158
+
159
+ const old_value = target[prop];
160
+ // Dirty state is snapshot-based: compare the current write against the rebased baseline
161
+ // rather than trying to replay or diff the full mutation history.
162
+ const original_value = read_path(store.base_state, full_path);
163
+
164
+ // Apply the mutation
165
+ target[prop] = next_value;
166
+
167
+ // Track Dirty State
168
+ if(original_value !== next_value) {
169
+ store.dirty_keys.add(full_path);
170
+ } else {
171
+ store.dirty_keys.delete(full_path);
172
+ }
173
+
174
+ // Trigger Watchers
175
+ if(old_value !== next_value) {
176
+ check_watchers(store, full_path, old_value, root_object, get_root());
177
+ }
178
+
179
+ return true;
180
+ }
181
+ });
182
+ }
183
+
184
+ /*
185
+ * STATE MANAGEMENT HELPERS
186
+ */
187
+
188
+ /**
189
+ * Normalize tracker options into a stable track list.
190
+ * If 'track' is omitted, it stays null (tracks everything).
191
+ * If 'track' is provided (string, array, or []), it converts to a clean array.
192
+ */
193
+ function get_track_list(options) {
194
+ return options.track === undefined ? null : to_array(options.track);
195
+ }
196
+
197
+ /**
198
+ * Clone either the full source object or only the tracked root keys.
199
+ */
200
+ function clone_tracked_state(source_object, track_list) {
201
+ if(!track_list) {
202
+ // Full tracking can reuse the deep clone directly instead of copying into another object.
203
+ return deep_clone(source_object);
204
+ }
205
+
206
+ const cloned_state = {};
207
+
208
+ let key_index = 0;
209
+
210
+ // This loop is on the tracked-state setup path, so `while` is kept deliberately for the
211
+ // lowest-overhead iteration style in this file's hot paths.
212
+ while(key_index < track_list.length) {
213
+ const key = track_list[key_index];
214
+
215
+ if(key in source_object) {
216
+ cloned_state[key] = deep_clone(source_object[key]);
217
+ }
218
+
219
+ key_index += 1;
220
+ }
221
+
222
+ return cloned_state;
223
+ }
224
+
225
+ function has_dirty_fields(store) {
226
+ return store ? store.dirty_keys.size > 0 : false;
227
+ }
228
+
229
+ function get_dirty_fields(store) {
230
+ return store ? Array.from(store.dirty_keys) : [];
231
+ }
232
+
233
+ function reset_dirty_fields(store, proxy) {
234
+ const original = store.base_state;
235
+ const original_keys = Object.keys(original);
236
+ let key_index = 0;
237
+
238
+ // Resetting through the proxy triggers setters naturally.
239
+ // Because `base_state` only holds tracked branches, untracked fields remain untouched.
240
+ while(key_index < original_keys.length) {
241
+ const key = original_keys[key_index];
242
+
243
+ if(!is_function(original[key])) {
244
+ proxy[key] = deep_clone(original[key]);
245
+ }
246
+
247
+ key_index += 1;
248
+ }
249
+
250
+ store.dirty_keys.clear();
251
+ }
252
+
253
+ function rebase_dirty_fields(store, root_object, options) {
254
+ const track_list = get_track_list(options);
255
+ store.base_state = clone_tracked_state(root_object, track_list);
256
+ store.dirty_keys.clear();
257
+ }
258
+
259
+ /*
260
+ * WATCHER HELPERS
261
+ */
262
+
263
+ function add_watcher(store, root_object, proxy, path, options) {
264
+ const handler = is_function(options) ? options : () => {};
265
+ const config = is_object(options) ? options : {handler};
266
+
267
+ const watcher = {
268
+ path,
269
+ handler: config.handler,
270
+ deep: config.deep === true,
271
+ once: config.once === true,
272
+ active: true
273
+ };
274
+
275
+ store.watchers.push(watcher);
276
+
277
+ if(config.immediate) {
278
+ const initial_value = read_path(root_object, path);
279
+ watcher.handler.call(proxy, initial_value, undefined);
280
+
281
+ if(watcher.once) {
282
+ watcher.active = false;
283
+ }
284
+ }
285
+
286
+ // Return the closure to unwatch
287
+ return () => {
288
+ watcher.active = false;
289
+ const index = store.watchers.indexOf(watcher);
290
+
291
+ if(index !== -1) {
292
+ store.watchers.splice(index, 1);
293
+ }
294
+ };
295
+ }
296
+
297
+ const pending_watchers = new Map();
298
+ let is_flushing = false;
299
+
300
+ function check_watchers(store, mutated_path, old_value, root_object, root_proxy) {
301
+ const watchers = store.watchers;
302
+ let watcher_index = 0;
303
+
304
+ while(watcher_index < watchers.length) {
305
+ const watcher = watchers[watcher_index];
306
+ if(!watcher.active) {
307
+ watcher_index += 1;
308
+ continue;
309
+ }
310
+
311
+ let should_trigger = false;
312
+ let handler_new_value = undefined;
313
+ let handler_old_value = old_value;
314
+
315
+ // Exact path match
316
+ if(watcher.path === mutated_path) {
317
+ should_trigger = true;
318
+ handler_new_value = read_path(root_object, mutated_path);
319
+ }
320
+ // Deep mutation (e.g., watching 'user', mutated 'user.name')
321
+ else if(watcher.deep && mutated_path.startsWith(watcher.path + '.')) {
322
+ should_trigger = true;
323
+ // Deep child writes mutate the same parent object in place, so there is no separate
324
+ // pre-mutation parent snapshot to pass through here.
325
+ handler_new_value = read_path(root_object, watcher.path);
326
+ handler_old_value = handler_new_value;
327
+ }
328
+ // Parent replacement (e.g., watching 'user.name', mutated 'user')
329
+ else if(watcher.path.startsWith(mutated_path + '.')) {
330
+ should_trigger = true;
331
+ handler_new_value = read_path(root_object, watcher.path);
332
+
333
+ // Attempt to extract the old nested value from the replaced parent object
334
+ const nested_path = watcher.path.substring(mutated_path.length + 1);
335
+ handler_old_value = read_path(old_value, nested_path);
336
+ }
337
+
338
+ if(should_trigger) {
339
+ queue_watcher(watcher, handler_new_value, handler_old_value, root_proxy);
340
+ }
341
+
342
+ watcher_index += 1;
343
+ }
344
+ }
345
+
346
+ function queue_watcher(watcher, new_value, old_value, context) {
347
+ // Deduplicate watchers in the same tick.
348
+ // If it exists, we update to the latest new_value, but keep the initial old_value of this tick.
349
+ if(pending_watchers.has(watcher)) {
350
+ pending_watchers.get(watcher).new_value = new_value;
351
+ } else {
352
+ pending_watchers.set(watcher, {new_value, old_value, context});
353
+ }
354
+
355
+ if(is_flushing) {
356
+ return;
357
+ }
358
+
359
+ is_flushing = true;
360
+
361
+ // Batch watcher delivery into one microtask so multiple synchronous writes collapse into a
362
+ // single callback pass with the latest value and the first old value from that tick.
363
+ Promise.resolve().then(() => {
364
+ const jobs = Array.from(pending_watchers.entries());
365
+ pending_watchers.clear();
366
+ is_flushing = false;
367
+
368
+ let job_index = 0;
369
+ while(job_index < jobs.length) {
370
+ const job_watcher = jobs[job_index][0];
371
+ const job_args = jobs[job_index][1];
372
+
373
+ if(job_watcher.active) {
374
+ job_watcher.handler.call(
375
+ job_args.context,
376
+ job_args.new_value,
377
+ job_args.old_value
378
+ );
379
+
380
+ if(job_watcher.once) {
381
+ job_watcher.active = false; // Mark inactive after first run
382
+ }
383
+ }
384
+ job_index += 1;
385
+ }
386
+ });
387
+ }
388
+
389
+ /*
390
+ * PATH HELPERS
391
+ */
392
+
393
+ function read_path(root_object, dot_path) {
394
+ if(!dot_path) {
395
+ return root_object;
396
+ }
397
+
398
+ const segments = dot_path.split('.');
399
+ let current_value = root_object;
400
+ let segment_index = 0;
401
+
402
+ while(segment_index < segments.length) {
403
+ if(is_not_object(current_value)) {
404
+ return undefined;
405
+ }
406
+
407
+ current_value = current_value[segments[segment_index]];
408
+ segment_index += 1;
409
+ }
410
+
411
+ return current_value;
412
+ }
413
+
414
+ export default DirtyTracker;