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,39 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Execute the model-layer delete-one persistence path.
4
+ */
5
+ import sql from '#src/sql/index.js';
6
+ import where_compiler from '#src/sql/read/where/index.js';
7
+ import {quote_identifier} from '#src/utils/assert.js';
8
+
9
+ /**
10
+ * Execute Model.delete_one() for a model constructor.
11
+ *
12
+ * @param {Function} model Model constructor.
13
+ * @param {object|undefined} query_filter Query filter.
14
+ * @returns {Promise<object|null>}
15
+ */
16
+ async function exec_delete_one(model, query_filter) {
17
+ const schema = model.schema;
18
+ const id_strategy = schema.id_strategy;
19
+
20
+ const model_options = model.options;
21
+ const data_column = model_options.data_column;
22
+ const table_name = model_options.table_name;
23
+
24
+ const table_identifier = quote_identifier(table_name);
25
+ const data_identifier = quote_identifier(data_column);
26
+ const where_result = where_compiler(query_filter, {schema, data_column, id_strategy});
27
+ const query_context = {table_identifier, data_identifier, where_result};
28
+
29
+ const delete_query = sql.build_delete_query(query_context);
30
+ const query_result = await sql.run(delete_query.sql_text, delete_query.sql_params, model.connection);
31
+ const rows = query_result.rows;
32
+ const row = rows.length ? rows[0] : null;
33
+
34
+ return row;
35
+ }
36
+
37
+ export {
38
+ exec_delete_one
39
+ };
@@ -0,0 +1,35 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Execute the model-layer insert-one persistence path.
4
+ */
5
+ import sql from '#src/sql/index.js';
6
+ import {quote_identifier} from '#src/utils/assert.js';
7
+
8
+ /**
9
+ * Execute one insert against prepared SQL-facing insert state.
10
+ *
11
+ * @param {Function} model Model constructor.
12
+ * @param {object} data Prepared insert state.
13
+ * @param {object} data.payload JSONB payload for the data column.
14
+ * @param {object} data.base_fields Row-level fields to persist outside the data column.
15
+ * @returns {Promise<object|null>}
16
+ */
17
+ async function exec_insert_one(model, data) {
18
+ const model_options = model.options;
19
+ const table_name = model_options.table_name;
20
+ const data_column = model_options.data_column;
21
+ const table_identifier = quote_identifier(table_name);
22
+ const data_identifier = quote_identifier(data_column);
23
+ const payload = data.payload;
24
+ const base_fields = data.base_fields;
25
+
26
+ const insert_query = sql.build_insert_query({table_identifier, data_identifier, payload, base_fields});
27
+ const query_result = await sql.run(insert_query.sql_text, insert_query.sql_params, model.connection);
28
+ const saved_row = query_result.rows[0];
29
+
30
+ return saved_row;
31
+ }
32
+
33
+ export {
34
+ exec_insert_one
35
+ };
@@ -0,0 +1,132 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Own model-facing query state, execution dispatch, and row hydration for read operations.
4
+ */
5
+ import build_count_query from '#src/sql/read/build-count-query.js';
6
+ import build_find_query from '#src/sql/read/build-find-query.js';
7
+ import where_compiler from '#src/sql/read/where/index.js';
8
+
9
+ import run from '#src/sql/run.js';
10
+
11
+ import {assert_condition, quote_identifier} from '#src/utils/assert.js';
12
+ import {is_object, to_iso_timestamp} from '#src/utils/value.js';
13
+
14
+ function QueryBuilder(model, operation, query_filter) {
15
+ assert_condition(model, 'QueryBuilder requires model');
16
+ assert_condition(model.schema, 'QueryBuilder requires model.schema');
17
+ assert_condition(model.options, 'QueryBuilder requires model.options');
18
+
19
+ this.connection = model.connection || null;
20
+ this.model = model;
21
+ this.operation = operation;
22
+ this.base_filter = query_filter || {};
23
+ this.where_filter = {};
24
+ this.sort_filter = null;
25
+ this.limit_count = null;
26
+ this.skip_count = null;
27
+ }
28
+
29
+ QueryBuilder.prototype.where = function (extra_filter) {
30
+ this.where_filter = Object.assign({}, this.where_filter, extra_filter || {});
31
+ return this;
32
+ };
33
+
34
+ QueryBuilder.prototype.sort = function (sort_filter) {
35
+ this.sort_filter = sort_filter || null;
36
+ return this;
37
+ };
38
+
39
+ QueryBuilder.prototype.limit = function (limit_value) {
40
+ this.limit_count = limit_value;
41
+ return this;
42
+ };
43
+
44
+ QueryBuilder.prototype.skip = function (skip_value) {
45
+ this.skip_count = skip_value;
46
+ return this;
47
+ };
48
+
49
+ QueryBuilder.prototype.exec = async function () {
50
+ // Query execution is the queried/hydrated lifecycle boundary for read methods:
51
+ // rows become document instances through `model.hydrate(...)`.
52
+ const schema = this.model.schema;
53
+ const id_strategy = schema.id_strategy;
54
+ const model_options = this.model.options;
55
+
56
+ const table_name = model_options.table_name;
57
+ const data_column = model_options.data_column;
58
+
59
+ const table_identifier = quote_identifier(table_name);
60
+ const data_identifier = quote_identifier(data_column);
61
+ const limit_count = this.operation === 'find_one' ? 1 : this.limit_count;
62
+
63
+ const merged_filter = Object.assign({}, this.base_filter, this.where_filter);
64
+ const where_result = where_compiler(merged_filter, {schema, data_column, id_strategy});
65
+ const query_context = {
66
+ connection: this.connection,
67
+ table_identifier,
68
+ data_identifier,
69
+ data_column,
70
+ where_result,
71
+ sort: this.sort_filter,
72
+ limit_count,
73
+ skip_count: this.skip_count
74
+ };
75
+
76
+ const model = this.model;
77
+
78
+ switch(this.operation) {
79
+ case 'count_documents': {
80
+ return exec_count_documents(query_context);
81
+ }
82
+
83
+ case 'find_one': {
84
+ const rows = await exec_find_query(query_context);
85
+ return rows.length ? model.hydrate(row_to_document(rows[0])) : null;
86
+ }
87
+
88
+ case 'find': {
89
+ const rows = await exec_find_query(query_context);
90
+ const documents = [];
91
+
92
+ for(const row of rows) {
93
+ documents.push(model.hydrate(row_to_document(row)));
94
+ }
95
+
96
+ return documents;
97
+ }
98
+
99
+ default: {
100
+ throw new Error('Unsupported query operation: ' + this.operation);
101
+ }
102
+ }
103
+ };
104
+
105
+ function row_to_document(row) {
106
+ return {
107
+ id: row.id != null ? String(row.id) : null,
108
+ data: is_object(row.data) ? row.data : {},
109
+ created_at: to_iso_timestamp(row.created_at),
110
+ updated_at: to_iso_timestamp(row.updated_at)
111
+ };
112
+ }
113
+
114
+ async function exec_count_documents(query_context) {
115
+ const count_query = build_count_query(query_context);
116
+ const count_result = await run(count_query.sql_text, count_query.sql_params, query_context.connection);
117
+ const rows = count_result.rows;
118
+
119
+ if(rows.length === 0) {
120
+ return 0;
121
+ }
122
+
123
+ return Number(rows[0].total_count);
124
+ }
125
+
126
+ async function exec_find_query(query_context) {
127
+ const find_query = build_find_query(query_context);
128
+ const query_result = await run(find_query.sql_text, find_query.sql_params, query_context.connection);
129
+ return query_result.rows;
130
+ }
131
+
132
+ export default QueryBuilder;
@@ -0,0 +1,333 @@
1
+ /*
2
+ * MODULE RESPONSIBILITY
3
+ * Bridge model-layer update input into the SQL update context.
4
+ */
5
+ import sql from '#src/sql/index.js';
6
+ import where_compiler from '#src/sql/read/where/index.js';
7
+ import {create_parameter_state} from '#src/sql/parameter-binder.js';
8
+
9
+ import {JsonbOps} from '#src/sql/jsonb/ops.js';
10
+ import {timestamp_fields} from '#src/model/factory/constants.js';
11
+ import {is_array} from '#src/utils/array.js';
12
+ import {quote_identifier} from '#src/utils/assert.js';
13
+ import {expand_dot_paths} from '#src/utils/object-path.js';
14
+ import {has_own} from '#src/utils/object.js';
15
+ import {is_not_object, is_object, is_plain_object} from '#src/utils/value.js';
16
+
17
+ // --- PUBLIC API ---
18
+
19
+ /**
20
+ * Execute Model.update_one() by coordinating syntax parsing and SQL generation.
21
+ *
22
+ * @param {Function} model
23
+ * @param {object} query_filter
24
+ * @param {object} update_definition
25
+ * @returns {Promise<object|null>}
26
+ */
27
+ async function exec_update_one(model, query_filter, update_definition) {
28
+ if(is_not_object(update_definition) || Object.keys(update_definition).length === 0) {
29
+ return null;
30
+ }
31
+
32
+ const model_options = model.options;
33
+ const data_column = model_options.data_column;
34
+
35
+ const gateway_definition = normalize_update_gateway(update_definition);
36
+ const {timestamp_set, payload} = normalize_update_definition(gateway_definition, data_column);
37
+
38
+ const data_identifier = quote_identifier(data_column);
39
+ const jsonb_ops = JsonbOps.from(payload, {
40
+ column_name: data_identifier,
41
+ coalesce: true
42
+ });
43
+
44
+ const where_result = where_compiler(query_filter, {
45
+ schema: model.schema,
46
+ data_column,
47
+ id_strategy: model.schema.id_strategy
48
+ });
49
+
50
+ const parameter_state = create_parameter_state(where_result.next_index);
51
+
52
+ const query_context = {
53
+ model,
54
+ table_identifier: quote_identifier(model_options.table_name),
55
+ data_identifier,
56
+ where_result,
57
+ parameter_state,
58
+ update_expression: {
59
+ jsonb_ops,
60
+ timestamp_set
61
+ }
62
+ };
63
+
64
+ const update_query = sql.build_update_query(query_context);
65
+ const query_result = await sql.run(update_query.sql_text, update_query.sql_params, model.connection);
66
+ const row = query_result.rows.length ? query_result.rows[0] : null;
67
+
68
+ return row;
69
+ }
70
+
71
+ // --- LOCAL HELPER FUNCTIONS ---
72
+
73
+ /**
74
+ * Expand public dotted update input before the existing router consumes it.
75
+ *
76
+ * @param {object} update_definition
77
+ * @returns {object}
78
+ * @throws {Error} If update input includes invalid or restricted dotted paths.
79
+ */
80
+ function normalize_update_gateway(update_definition) {
81
+ if(is_tracker_delta_update(update_definition)) {
82
+ return update_definition;
83
+ }
84
+
85
+ const implicit_definition = {};
86
+ const normalized_definition = {};
87
+
88
+ for(const [key, value] of Object.entries(update_definition)) {
89
+ if(key.startsWith('$')) {
90
+ normalized_definition[key] = normalize_update_operator_value(key, value);
91
+ continue;
92
+ }
93
+
94
+ implicit_definition[key] = value;
95
+ }
96
+
97
+ return Object.assign({}, normalized_definition, expand_dot_paths(implicit_definition));
98
+ }
99
+
100
+ /**
101
+ * Detect the internal tracker delta shape so public gateway expansion stays out of that path.
102
+ *
103
+ * @param {object} update_definition
104
+ * @returns {boolean}
105
+ */
106
+ function is_tracker_delta_update(update_definition) {
107
+ return is_object(update_definition) && (
108
+ has_own(update_definition, 'set') ||
109
+ has_own(update_definition, 'unset') ||
110
+ has_own(update_definition, 'replace_roots')
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Normalize one operator payload before the router turns it into SQL-oriented state.
116
+ *
117
+ * @param {string} operator_name
118
+ * @param {*} operator_value
119
+ * @returns {*}
120
+ * @throws {Error} If operator input includes invalid or restricted dotted paths.
121
+ */
122
+ function normalize_update_operator_value(operator_name, operator_value) {
123
+ if((operator_name === '$set' || operator_name === '$replace_roots') && is_object(operator_value)) {
124
+ return expand_dot_paths(operator_value);
125
+ }
126
+
127
+ return operator_value;
128
+ }
129
+
130
+ /**
131
+ * Normalize one update definition into row timestamps plus operator-style JSONB payload.
132
+ *
133
+ * @param {object} update_definition
134
+ * @param {string} data_column
135
+ * @returns {{timestamp_set: object, payload: object}}
136
+ */
137
+ function normalize_update_definition(update_definition, data_column) {
138
+ const timestamp_set = {};
139
+ const payload = pull_timestamp_fields(update_definition, timestamp_set);
140
+
141
+ merge_tracked_set(payload, data_column, timestamp_set);
142
+ merge_tracked_unset(payload, data_column);
143
+ merge_tracked_replace_roots(payload, data_column);
144
+ move_implicit_set_values(payload);
145
+
146
+ return {timestamp_set, payload};
147
+ }
148
+
149
+ /**
150
+ * Safely map tracker-emitted "set" objects to the explicit "$set" bucket.
151
+ *
152
+ * @param {object} payload
153
+ * @param {string} data_column
154
+ * @param {object} timestamp_set
155
+ * @returns {void}
156
+ */
157
+ function merge_tracked_set(payload, data_column, timestamp_set) {
158
+ if(is_object(payload.$set)) {
159
+ payload.$set = flatten_update_paths(pull_timestamp_fields(payload.$set, timestamp_set));
160
+ }
161
+
162
+ if(!is_object(payload.set)) {
163
+ return;
164
+ }
165
+
166
+ const tracked_set = pull_timestamp_fields(payload.set, timestamp_set);
167
+ const set_payload = is_object(payload.$set) ? payload.$set : {};
168
+
169
+ delete payload.set;
170
+ payload.$set = set_payload;
171
+
172
+ for(const [path_name, value] of Object.entries(tracked_set)) {
173
+ const next_path = strip_tracked_root(path_name, data_column);
174
+
175
+ if(next_path !== '') {
176
+ set_payload[next_path] = value;
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Safely map tracker-emitted "unset" arrays to the explicit "$unset" bucket.
183
+ *
184
+ * @param {object} payload
185
+ * @param {string} data_column
186
+ * @returns {void}
187
+ */
188
+ function merge_tracked_unset(payload, data_column) {
189
+ if(!is_array(payload.unset)) {
190
+ return;
191
+ }
192
+
193
+ const tracked_unset = payload.unset;
194
+ const unset_payload = is_array(payload.$unset) ? payload.$unset : [];
195
+
196
+ delete payload.unset;
197
+ payload.$unset = unset_payload;
198
+
199
+ for(const path_name of tracked_unset) {
200
+ const next_path = strip_tracked_root(path_name, data_column);
201
+
202
+ if(next_path === '') {
203
+ payload.$replace_roots = {};
204
+ } else {
205
+ unset_payload.push(next_path);
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Safely map tracker-emitted "replace_roots" objects to the explicit "$replace_roots" bucket.
212
+ *
213
+ * @param {object} payload
214
+ * @param {string} data_column
215
+ * @returns {void}
216
+ */
217
+ function merge_tracked_replace_roots(payload, data_column) {
218
+ if(!is_object(payload.replace_roots)) {
219
+ return;
220
+ }
221
+
222
+ const next_root_payload = payload.replace_roots[data_column];
223
+ delete payload.replace_roots;
224
+
225
+ if(next_root_payload !== undefined) {
226
+ payload.$replace_roots = next_root_payload;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Extract supported row timestamp fields from one payload object.
232
+ *
233
+ * @param {object} source_object
234
+ * @param {object} timestamp_set
235
+ * @returns {object}
236
+ */
237
+ function pull_timestamp_fields(source_object, timestamp_set) {
238
+ const next_object = {...source_object};
239
+
240
+ for(const key of timestamp_fields) {
241
+ if(has_own(next_object, key)) {
242
+ timestamp_set[key] = next_object[key];
243
+ delete next_object[key];
244
+ }
245
+ }
246
+
247
+ return next_object;
248
+ }
249
+
250
+ /**
251
+ * Move implicit object keys into the explicit `$set` bucket as leaf dot paths.
252
+ *
253
+ * @param {object} payload
254
+ * @returns {void}
255
+ */
256
+ function move_implicit_set_values(payload) {
257
+ const implicit_source = {};
258
+
259
+ for(const [key, value] of Object.entries(payload)) {
260
+ if(key.startsWith('$')) {
261
+ continue;
262
+ }
263
+
264
+ implicit_source[key] = value;
265
+ delete payload[key];
266
+ }
267
+
268
+ if(Object.keys(implicit_source).length === 0) {
269
+ return;
270
+ }
271
+
272
+ const implicit_set = flatten_update_paths(implicit_source);
273
+ const set_payload = is_object(payload.$set) ? payload.$set : {};
274
+
275
+ payload.$set = set_payload;
276
+ Object.assign(set_payload, implicit_set);
277
+ }
278
+
279
+ /**
280
+ * Flatten nested object updates into leaf dot-path assignments for JSONB mutation.
281
+ *
282
+ * @param {object} source_object
283
+ * @param {string} [parent_path]
284
+ * @returns {object}
285
+ */
286
+ function flatten_update_paths(source_object, parent_path = '') {
287
+ const path_map = {};
288
+
289
+ for(const [key, value] of Object.entries(source_object)) {
290
+ const next_path = parent_path ? `${parent_path}.${key}` : key;
291
+
292
+ if(is_plain_object(value)) {
293
+ const nested_keys = Object.keys(value);
294
+
295
+ if(nested_keys.length === 0) {
296
+ path_map[next_path] = {};
297
+ continue;
298
+ }
299
+
300
+ Object.assign(path_map, flatten_update_paths(value, next_path));
301
+ continue;
302
+ }
303
+
304
+ path_map[next_path] = value;
305
+ }
306
+
307
+ return path_map;
308
+ }
309
+
310
+ /**
311
+ * Strip the tracked root segment from one tracker-emitted path.
312
+ *
313
+ * @param {string} path_name
314
+ * @param {string} data_column
315
+ * @returns {string}
316
+ */
317
+ function strip_tracked_root(path_name, data_column) {
318
+ if(path_name === data_column) {
319
+ return '';
320
+ }
321
+
322
+ const path_prefix = `${data_column}.`;
323
+
324
+ if(path_name.startsWith(path_prefix)) {
325
+ return path_name.substring(path_prefix.length);
326
+ }
327
+
328
+ return path_name;
329
+ }
330
+
331
+ export {
332
+ exec_update_one
333
+ };
@@ -0,0 +1,34 @@
1
+ const runtime_store = new WeakMap();
2
+
3
+ /**
4
+ * Create and store one compiled-model runtime object by model constructor.
5
+ *
6
+ * @param {Function} key
7
+ * @param {object} init_data
8
+ * @returns {object}
9
+ */
10
+ function create_runtime_store(key, init_data) {
11
+ const runtime_data = Object.assign({}, init_data);
12
+
13
+ runtime_store.set(key, runtime_data);
14
+ return runtime_data;
15
+ }
16
+
17
+ /**
18
+ * Read one compiled-model runtime object by model constructor.
19
+ *
20
+ * @param {Function} key
21
+ * @returns {object|null}
22
+ */
23
+ function get_runtime_store(key) {
24
+ if(runtime_store.has(key)) {
25
+ return runtime_store.get(key);
26
+ }
27
+
28
+ return null;
29
+ }
30
+
31
+ export {
32
+ create_runtime_store,
33
+ get_runtime_store
34
+ };