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,343 @@
1
+ import {deep_clone, is_function, is_not_object, is_object} from '#src/utils/value.js';
2
+
3
+ // dirty-tracker: static
4
+
5
+ /*
6
+ * MAIN API
7
+ */
8
+
9
+ /**
10
+ * Wrap this object with dirty tracking and optional set interception.
11
+ *
12
+ * @param {object} target
13
+ * @param {object} [options]
14
+ * @param {object} [options.watch]
15
+ * @param {function} [options.intercept_set]
16
+ * @returns {Proxy}
17
+ */
18
+ function track_changes(target, options = {}) {
19
+ const store = {
20
+ root_object: target,
21
+ base_state: deep_clone(target),
22
+ dirty_keys: new Set(),
23
+ watchers: []
24
+ };
25
+
26
+ let root_proxy;
27
+ const get_root = () => root_proxy;
28
+
29
+ // Build the intercepting proxy
30
+ root_proxy = build_proxy(target, target, store, get_root, options);
31
+
32
+ // Parse and bind Watchers
33
+ if(is_object(options.watch)) {
34
+ const watch_keys = Object.keys(options.watch);
35
+ let key_index = 0;
36
+
37
+ while(key_index < watch_keys.length) {
38
+ const watch_path = watch_keys[key_index];
39
+ add_watcher(root_proxy, watch_path, options.watch[watch_path]);
40
+ key_index += 1;
41
+ }
42
+ }
43
+
44
+ // Register the root proxy for static utility lookups
45
+ tracker_registry.set(root_proxy, store);
46
+
47
+ return root_proxy;
48
+ }
49
+
50
+ /*
51
+ * LAZY PROXY BUILDER
52
+ */
53
+
54
+ function build_proxy(target_object, root_object, store, get_root, options = {}) {
55
+ const base_path = options.base_path || '';
56
+ let intercept_set = (path, next_value) => next_value;
57
+
58
+ if(is_function(options.intercept_set)) {
59
+ intercept_set = options.intercept_set;
60
+ }
61
+
62
+ return new Proxy(target_object, {
63
+ get(target, prop) {
64
+ // Do not proxy internal JS symbols
65
+ if(typeof prop === 'symbol') {
66
+ return target[prop];
67
+ }
68
+
69
+ // Root level: allow access to tracker methods
70
+ if(base_path === '') {
71
+ if(prop in target && is_function(target[prop])) {
72
+ return target[prop];
73
+ }
74
+ }
75
+
76
+ const value = target[prop];
77
+
78
+ // Lazy-proxy nested objects on access
79
+ if(is_object(value)) {
80
+ const next_path = base_path === '' ? prop : `${base_path}.${prop}`;
81
+ const next_options = {...options, base_path: next_path};
82
+ return build_proxy(value, root_object, store, get_root, next_options);
83
+ }
84
+
85
+ return value;
86
+ },
87
+
88
+ set(target, prop, value) {
89
+ if(typeof prop === 'symbol') {
90
+ target[prop] = value;
91
+ return true;
92
+ }
93
+
94
+ // Do not track mutations to functions/methods
95
+ if(base_path === '' && is_function(target[prop])) {
96
+ target[prop] = value;
97
+ return true;
98
+ }
99
+
100
+ const full_path = base_path === '' ? prop : `${base_path}.${prop}`;
101
+ const next_value = intercept_set(full_path, value);
102
+
103
+ const old_value = target[prop];
104
+ const original_value = read_path(store.base_state, full_path);
105
+
106
+ // Apply the mutation
107
+ target[prop] = next_value;
108
+
109
+ // Track Dirty State
110
+ if(original_value !== next_value) {
111
+ store.dirty_keys.add(full_path);
112
+ } else {
113
+ store.dirty_keys.delete(full_path);
114
+ }
115
+
116
+ // Trigger Watchers
117
+ if(old_value !== next_value) {
118
+ check_watchers(store, full_path, old_value, root_object, get_root());
119
+ }
120
+
121
+ return true;
122
+ }
123
+ });
124
+ }
125
+
126
+ /*
127
+ * STATE MANAGEMENT HELPERS
128
+ */
129
+
130
+ const tracker_registry = new WeakMap();
131
+
132
+ function has_dirty_fields(proxy) {
133
+ const store = tracker_registry.get(proxy);
134
+ return store ? store.dirty_keys.size > 0 : false;
135
+ }
136
+
137
+ function get_dirty_fields(proxy) {
138
+ const store = tracker_registry.get(proxy);
139
+ return store ? Array.from(store.dirty_keys) : [];
140
+ }
141
+
142
+ function reset_dirty_fields(proxy) {
143
+ const store = tracker_registry.get(proxy);
144
+
145
+ if(!store) {
146
+ return;
147
+ }
148
+
149
+ const original = store.base_state;
150
+ const original_keys = Object.keys(original);
151
+ let key_index = 0;
152
+
153
+ // Resetting through the proxy triggers setters naturally
154
+ while(key_index < original_keys.length) {
155
+ const key = original_keys[key_index];
156
+
157
+ if(!is_function(original[key])) {
158
+ proxy[key] = deep_clone(original[key]);
159
+ }
160
+
161
+ key_index += 1;
162
+ }
163
+
164
+ store.dirty_keys.clear();
165
+ }
166
+
167
+ function rebase_dirty_fields(proxy) {
168
+ const store = tracker_registry.get(proxy);
169
+
170
+ if(!store) {
171
+ return;
172
+ }
173
+
174
+ store.base_state = deep_clone(store.root_object);
175
+ store.dirty_keys.clear();
176
+ }
177
+
178
+ /*
179
+ * WATCHER HELPERS
180
+ */
181
+
182
+ function add_watcher(proxy, path, options) {
183
+ const store = tracker_registry.get(proxy);
184
+
185
+ if(!store) {
186
+ throw new Error('Cannot add watcher: Object is not actively tracked.');
187
+ }
188
+
189
+ const handler = is_function(options) ? options : () => {};
190
+ const config = is_object(options) ? options : {handler};
191
+
192
+ const watcher = {
193
+ path,
194
+ handler: config.handler,
195
+ deep: config.deep === true,
196
+ once: config.once === true,
197
+ active: true
198
+ };
199
+
200
+ store.watchers.push(watcher);
201
+
202
+ if(config.immediate) {
203
+ const initial_value = read_path(store.root_object, path);
204
+ watcher.handler.call(proxy, initial_value, undefined);
205
+
206
+ if(watcher.once) {
207
+ watcher.active = false;
208
+ }
209
+ }
210
+
211
+ // Return the closure to unwatch
212
+ return () => {
213
+ watcher.active = false;
214
+ const index = store.watchers.indexOf(watcher);
215
+
216
+ if(index !== -1) {
217
+ store.watchers.splice(index, 1);
218
+ }
219
+ };
220
+ }
221
+
222
+ const pending_watchers = new Map();
223
+ let is_flushing = false;
224
+
225
+ function check_watchers(store, mutated_path, old_value, root_object, root_proxy) {
226
+ const watchers = store.watchers;
227
+ let watcher_index = 0;
228
+
229
+ while(watcher_index < watchers.length) {
230
+ const watcher = watchers[watcher_index];
231
+ if(!watcher.active) {
232
+ watcher_index += 1;
233
+ continue;
234
+ }
235
+
236
+ let should_trigger = false;
237
+ let handler_new_value = undefined;
238
+ let handler_old_value = old_value;
239
+
240
+ // Exact path match
241
+ if(watcher.path === mutated_path) {
242
+ should_trigger = true;
243
+ handler_new_value = read_path(root_object, mutated_path);
244
+ }
245
+ // Deep mutation (e.g., watching 'user', mutated 'user.name')
246
+ else if(watcher.deep && mutated_path.startsWith(watcher.path + '.')) {
247
+ should_trigger = true;
248
+ // In deep mutations, new and old values are identical references to the same parent object
249
+ handler_new_value = read_path(root_object, watcher.path);
250
+ handler_old_value = handler_new_value;
251
+ }
252
+ // Parent replacement (e.g., watching 'user.name', mutated 'user')
253
+ else if(watcher.path.startsWith(mutated_path + '.')) {
254
+ should_trigger = true;
255
+ handler_new_value = read_path(root_object, watcher.path);
256
+
257
+ // Attempt to extract the old nested value from the replaced parent object
258
+ const nested_path = watcher.path.substring(mutated_path.length + 1);
259
+ handler_old_value = read_path(old_value, nested_path);
260
+ }
261
+
262
+ if(should_trigger) {
263
+ queue_watcher(watcher, handler_new_value, handler_old_value, root_proxy);
264
+ }
265
+
266
+ watcher_index += 1;
267
+ }
268
+ }
269
+
270
+ function queue_watcher(watcher, new_value, old_value, context) {
271
+ // Deduplicate watchers in the same tick.
272
+ // If it exists, we update to the latest new_value, but keep the initial old_value of this tick.
273
+ if(pending_watchers.has(watcher)) {
274
+ pending_watchers.get(watcher).new_value = new_value;
275
+ } else {
276
+ pending_watchers.set(watcher, {new_value, old_value, context});
277
+ }
278
+
279
+ if(is_flushing) {
280
+ return;
281
+ }
282
+
283
+ is_flushing = true;
284
+
285
+ // Flush asynchronously on the next microtask (after synchronous code finishes)
286
+ Promise.resolve().then(() => {
287
+ const jobs = Array.from(pending_watchers.entries());
288
+ pending_watchers.clear();
289
+ is_flushing = false;
290
+
291
+ let job_index = 0;
292
+ while(job_index < jobs.length) {
293
+ const job_watcher = jobs[job_index][0];
294
+ const job_args = jobs[job_index][1];
295
+
296
+ if(job_watcher.active) {
297
+ job_watcher.handler.call(
298
+ job_args.context,
299
+ job_args.new_value,
300
+ job_args.old_value
301
+ );
302
+
303
+ if(job_watcher.once) {
304
+ job_watcher.active = false; // Mark inactive after first run
305
+ }
306
+ }
307
+ job_index += 1;
308
+ }
309
+ });
310
+ }
311
+
312
+ /*
313
+ * PATH HELPERS
314
+ */
315
+
316
+ function read_path(root_object, dot_path) {
317
+ if(!dot_path) {
318
+ return root_object;
319
+ }
320
+
321
+ const segments = dot_path.split('.');
322
+ let current_value = root_object;
323
+ let segment_index = 0;
324
+
325
+ while(segment_index < segments.length) {
326
+ if(is_not_object(current_value)) {
327
+ return undefined;
328
+ }
329
+ current_value = current_value[segments[segment_index]];
330
+ segment_index += 1;
331
+ }
332
+
333
+ return current_value;
334
+ }
335
+
336
+ export {
337
+ track_changes,
338
+ has_dirty_fields,
339
+ get_dirty_fields,
340
+ reset_dirty_fields,
341
+ rebase_dirty_fields,
342
+ add_watcher
343
+ };
@@ -1,9 +1,13 @@
1
- export function safe_json_stringify(value) {
2
- try {
3
- return JSON.stringify(value);
4
- } catch(error) {
5
- return '"[unserializable]"';
6
- }
7
- }
8
-
9
- export default safe_json_stringify;
1
+ function safe_json_stringify(value) {
2
+ try {
3
+ return JSON.stringify(value);
4
+ } catch(error) {
5
+ return '"[unserializable]"';
6
+ }
7
+ }
8
+
9
+ export default safe_json_stringify;
10
+
11
+ export {
12
+ safe_json_stringify
13
+ };
@@ -1,33 +1,227 @@
1
- import {assert_path} from '#src/utils/assert.js';
2
-
3
- export function split_dot_path(path_value) {
4
- assert_path(path_value, 'path');
5
- return path_value.split('.');
6
- }
7
-
8
- export function build_path_literal(path_segments) {
9
- return '{' + path_segments.join(',') + '}';
10
- }
11
-
12
- export function build_nested_object(path_value, leaf_value) {
13
- const path_segments = split_dot_path(path_value);
14
- const root_object = {};
15
- let current_object = root_object;
16
- let path_index = 0;
17
-
18
- while(path_index < path_segments.length) {
19
- const path_segment = path_segments[path_index];
20
- const is_leaf = path_index === path_segments.length - 1;
21
-
22
- if(is_leaf) {
23
- current_object[path_segment] = leaf_value;
24
- break;
25
- }
26
-
27
- current_object[path_segment] = {};
28
- current_object = current_object[path_segment];
29
- path_index += 1;
30
- }
31
-
32
- return root_object;
33
- }
1
+ import {assert_path} from '#src/utils/assert.js';
2
+ import {is_array} from '#src/utils/array.js';
3
+ import {has_own} from '#src/utils/object.js';
4
+ import {is_not_object, is_plain_object} from '#src/utils/value.js';
5
+
6
+ // --- PUBLIC API ---
7
+
8
+ /**
9
+ * Encodes an array of path segments into a PostgreSQL text array literal {a,b,c}.
10
+ *
11
+ * @param {string[]} path_segments
12
+ * @returns {string}
13
+ */
14
+ function build_path_literal(path_segments) {
15
+ const escaped_segments = [];
16
+
17
+ for(const segment of path_segments) {
18
+ // Escape backslashes and double quotes with a backslash, then wrap in double quotes
19
+ escaped_segments.push('"' + String(segment).replace(/(["\\])/g, '\\$1') + '"');
20
+ }
21
+
22
+ return '{' + escaped_segments.join(',') + '}';
23
+ }
24
+
25
+ /**
26
+ * Recursively builds a nested object structure from a dot-path and a leaf value.
27
+ * Used for generating JSONB containment payloads.
28
+ *
29
+ * @param {string} path_value
30
+ * @param {*} leaf_value
31
+ * @returns {object}
32
+ * @throws {Error} If the path contains restricted prototype keys.
33
+ */
34
+ function build_nested_object(path_value, leaf_value) {
35
+ const path_segments = split_dot_path(path_value);
36
+ const root_object = {};
37
+ let current_object = root_object;
38
+
39
+ const depth = path_segments.length - 1;
40
+ const leaf_segment = path_segments[depth];
41
+
42
+ for(let i = 0; i < depth; i++) {
43
+ const path_segment = path_segments[i];
44
+ current_object[path_segment] = {};
45
+ current_object = current_object[path_segment];
46
+ }
47
+
48
+ current_object[leaf_segment] = leaf_value;
49
+
50
+ return root_object;
51
+ }
52
+
53
+ /**
54
+ * Expands dotted object keys into nested object structures at the input boundary.
55
+ *
56
+ * @param {*} source_value
57
+ * @returns {*}
58
+ * @throws {Error} If any nested path contains restricted prototype keys.
59
+ */
60
+ function expand_dot_paths(source_value) {
61
+ if(is_array(source_value)) {
62
+ const next_array = [];
63
+
64
+ for(const item of source_value) {
65
+ next_array.push(expand_dot_paths(item));
66
+ }
67
+
68
+ return next_array;
69
+ }
70
+
71
+ if(!is_plain_object(source_value)) {
72
+ return source_value;
73
+ }
74
+
75
+ const expanded_object = {};
76
+
77
+ for(const [path_name, next_value] of Object.entries(source_value)) {
78
+ const expanded_value = expand_dot_paths(next_value);
79
+
80
+ if(path_name.includes('.')) {
81
+ assign_nested_value(expanded_object, split_dot_path(path_name), expanded_value);
82
+ } else if(is_plain_object(expanded_value) && is_plain_object(expanded_object[path_name])) {
83
+ expanded_object[path_name] = merge_plain_objects(expanded_object[path_name], expanded_value);
84
+ } else {
85
+ expanded_object[path_name] = expanded_value;
86
+ }
87
+ }
88
+
89
+ return expanded_object;
90
+ }
91
+
92
+ /**
93
+ * Normalizes a dot-path by collapsing multiple consecutive dots and splitting into segments.
94
+ *
95
+ * @param {string} path_value
96
+ * @returns {string[]}
97
+ * @throws {Error} If the path contains restricted prototype keys.
98
+ */
99
+ function split_dot_path(path_value) {
100
+ // Clean state: collapse multiple dots into one to prevent empty segment errors.
101
+ const clean_path = String(path_value).replace(/\.+/g, '.');
102
+
103
+ assert_path(clean_path, 'path');
104
+ const segments = clean_path.split('.');
105
+
106
+ for(const segment of segments) {
107
+ // Prevent Prototype Pollution by strictly blocking internal object keys
108
+ if(segment === '__proto__' || segment === 'constructor' || segment === 'prototype') {
109
+ throw new Error('Invalid path: restricted key name "' + segment + '"');
110
+ }
111
+ }
112
+
113
+ return segments;
114
+ }
115
+
116
+ /**
117
+ * Reads one nested value from an object path.
118
+ *
119
+ * @param {object} root_object
120
+ * @param {string[]} path_segments
121
+ * @returns {{exists: boolean, value: *}}
122
+ */
123
+ function read_nested_path(root_object, path_segments) {
124
+ let current = root_object;
125
+
126
+ for(const segment of path_segments) {
127
+ if(is_not_object(current) || !has_own(current, segment)) {
128
+ return {
129
+ exists: false,
130
+ value: undefined
131
+ };
132
+ }
133
+
134
+ current = current[segment];
135
+ }
136
+
137
+ return {
138
+ exists: true,
139
+ value: current
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Writes one normalized value into a nested object path.
145
+ *
146
+ * @param {object} root_object
147
+ * @param {string[]} path_segments
148
+ * @param {*} next_value
149
+ * @returns {void}
150
+ */
151
+ function write_nested_path(root_object, path_segments, next_value) {
152
+ let current_object = root_object;
153
+ const depth = path_segments.length - 1;
154
+
155
+ for(let i = 0; i < depth; i++) {
156
+ const path_segment = path_segments[i];
157
+
158
+ if(is_not_object(current_object[path_segment])) {
159
+ current_object[path_segment] = {};
160
+ }
161
+
162
+ current_object = current_object[path_segment];
163
+ }
164
+
165
+ current_object[path_segments[depth]] = next_value;
166
+ }
167
+
168
+ // --- LOCAL HELPERS ---
169
+
170
+ /**
171
+ * Writes one normalized value into a nested object path.
172
+ *
173
+ * @param {object} root_object
174
+ * @param {string[]} path_segments
175
+ * @param {*} next_value
176
+ * @returns {void}
177
+ */
178
+ function assign_nested_value(root_object, path_segments, next_value) {
179
+ let current_object = root_object;
180
+ const depth = path_segments.length - 1;
181
+ const leaf_segment = path_segments[depth];
182
+
183
+ for(let i = 0; i < depth; i++) {
184
+ const path_segment = path_segments[i];
185
+ if(!is_plain_object(current_object[path_segment])) {
186
+ current_object[path_segment] = {};
187
+ }
188
+
189
+ current_object = current_object[path_segment];
190
+ }
191
+
192
+ if(is_plain_object(next_value) && is_plain_object(current_object[leaf_segment])) {
193
+ current_object[leaf_segment] = merge_plain_objects(current_object[leaf_segment], next_value);
194
+ } else {
195
+ current_object[leaf_segment] = next_value;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Deep-merges plain objects while letting later values win on scalar collisions.
201
+ *
202
+ * @param {object} base_object
203
+ * @param {object} patch_object
204
+ * @returns {object}
205
+ */
206
+ function merge_plain_objects(base_object, patch_object) {
207
+ const merged_object = {...base_object};
208
+
209
+ for(const [key, value] of Object.entries(patch_object)) {
210
+ if(is_plain_object(value) && is_plain_object(merged_object[key])) {
211
+ merged_object[key] = merge_plain_objects(merged_object[key], value);
212
+ } else {
213
+ merged_object[key] = value;
214
+ }
215
+ }
216
+
217
+ return merged_object;
218
+ }
219
+
220
+ export {
221
+ split_dot_path,
222
+ read_nested_path,
223
+ write_nested_path,
224
+ build_path_literal,
225
+ build_nested_object,
226
+ expand_dot_paths
227
+ };