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
@@ -1,234 +1,717 @@
1
1
  import defaults from '#src/constants/defaults.js';
2
+ import ID_STRATEGY from '#src/constants/id-strategy.js';
2
3
 
3
4
  import ValidationError from '#src/errors/validation-error.js';
4
- import schema_compiler from '#src/schema/schema-compiler.js';
5
5
 
6
6
  import {default_field_type_registry} from '#src/field-types/registry.js';
7
+
8
+ import {create_path_validator, prepare_schema_state} from '#src/schema/schema-compiler.js';
9
+ import {get_path_type as resolve_path_type, is_array_root as resolve_is_array_root} from '#src/schema/path-introspection.js';
10
+
7
11
  import {assert_condition, assert_identifier, assert_path} from '#src/utils/assert.js';
8
- import {is_object, is_string} from '#src/utils/value.js';
9
- import {has_own} from '#src/utils/object.js';
10
-
11
- export default function Schema(schema_def, options) {
12
- this.schema_def = schema_def || {};
13
- this.options = Object.assign({}, defaults.schema_options, options || {});
14
- this.compiled_schema = schema_compiler(this.schema_def);
15
- this.schema_description = this.compiled_schema.describe();
16
- this.indexes = [];
12
+ import {is_array, is_not_array} from '#src/utils/array.js';
13
+ import {read_nested_path, write_nested_path} from '#src/utils/object-path.js';
14
+ import {deep_clone, has_own} from '#src/utils/object.js';
15
+ import {is_function, is_object, is_plain_object, is_string, is_uuid_v7, is_valid_timestamp} from '#src/utils/value.js';
16
+
17
+ const base_fields = Object.freeze({
18
+ id: Object.freeze({type: 'Mixed'}),
19
+ created_at: Object.freeze({type: Date}),
20
+ updated_at: Object.freeze({type: Date})
21
+ });
22
+
23
+ const base_fields_keys = new Set(Object.keys(base_fields));
24
+
25
+ function Schema(schema_definition = {}, schema_options = {}) {
26
+ const $schema = apply_base_fields(schema_definition);
27
+ const $options = build_schema_options(schema_options);
28
+ const {path_introspection, field_types} = prepare_schema_state($schema);
29
+ const default_slug = $options.default_slug;
30
+ const extra_slug_keys = $options.slugs;
31
+
32
+ this.$field_registry = default_field_type_registry;
33
+ this.$path_introspection = path_introspection;
34
+ this.$field_types = field_types;
17
35
 
18
- register_path_level_indexes(this, this.schema_def, '', default_field_type_registry);
36
+ this.indexes = [];
37
+ this.options = $options;
38
+ this.id_strategy = $options.id_strategy;
39
+ this.auto_index = $options.auto_index;
40
+ this.methods = Object.create(null);
41
+ this.validators = Object.create(null);
42
+ this.aliases = collect_aliases(field_types);
43
+ this.$conform_tree = build_conform_tree(field_types, default_slug, extra_slug_keys);
44
+
45
+ assert_condition(typeof this.auto_index === 'boolean', 'auto_index must be a boolean');
46
+
47
+ this.configure_validators();
48
+ this.register_field_indexes($schema);
19
49
  }
20
50
 
21
- Schema.prototype.validate = function (payload) {
22
- const validation_result = this.compiled_schema.validate(payload);
51
+ // --- PROTOTYPE: LIFECYCLE ---
52
+
53
+ /**
54
+ * Configure document-slice validators for the default slug and registered root slugs.
55
+ *
56
+ * @returns {Schema}
57
+ */
58
+ Schema.prototype.configure_validators = function () {
59
+ const default_slug = this.get_default_slug();
60
+ const registered_slug_keys = this.get_extra_slugs();
61
+ const id_strategy = this.id_strategy;
62
+ const validators = Object.create(null);
63
+
64
+ // Root document validator
65
+ validators.base_fields = (document) => {
66
+ return validate_base_field_values(document, id_strategy);
67
+ };
68
+
69
+ // Get all definitions in one pass
70
+ const slug_definitions = build_slug_field_maps(this.$field_types, default_slug, registered_slug_keys);
71
+
72
+ // Wire up the validators dynamically
73
+ for(const [slug_key, field_types] of Object.entries(slug_definitions)) {
74
+ const validate_slice = create_path_validator(field_types);
75
+
76
+ validators[slug_key] = (document) => {
77
+ const slice = (is_object(document) && has_own(document, slug_key)) ? document[slug_key] : {};
78
+ return validate_slice(slice);
79
+ };
80
+ }
23
81
 
24
- if(validation_result.error) {
25
- throw new ValidationError('Schema validation failed', validation_result.error.details);
82
+ this.validators = validators;
83
+ return this;
84
+ };
85
+
86
+ Schema.prototype.register_field_indexes = function (schema_definition) {
87
+ const field_defined_indexes = this.collect_field_defined_indexes(schema_definition);
88
+
89
+ for(const index_definition of field_defined_indexes) {
90
+ this.create_index(index_definition);
26
91
  }
27
92
 
28
- return validation_result.value;
93
+ return this;
29
94
  };
30
95
 
31
- Schema.prototype.path = function (path_name) {
32
- if(!this.compiled_schema || typeof this.compiled_schema.path !== 'function') {
33
- return null;
96
+ Schema.prototype.collect_field_defined_indexes = function (schema_definition) {
97
+ const index_definitions = [];
98
+ collect_schema_indexes(schema_definition, '', this.$field_registry, index_definitions);
99
+ return index_definitions;
100
+ };
101
+
102
+
103
+ // --- PROTOTYPE: PRIMARY PUBLIC ---
104
+
105
+ /**
106
+ * Validate one full document envelope across base fields and configured slug slices.
107
+ *
108
+ * @param {object} document
109
+ * @returns {{valid: boolean, errors: object[]|null}}
110
+ * @throws {ValidationError}
111
+ */
112
+ Schema.prototype.validate = function (document) {
113
+ const error_details = [];
114
+
115
+ for(const run_validator of Object.values(this.validators)) {
116
+ const validation_result = run_validator(document);
117
+
118
+ if(validation_result.valid === false && is_array(validation_result.errors)) {
119
+ error_details.push(...validation_result.errors);
120
+ }
121
+ }
122
+
123
+ if(error_details.length > 0) {
124
+ throw new ValidationError('Schema validation failed', error_details);
34
125
  }
35
126
 
36
- return this.compiled_schema.path(path_name);
127
+ return {
128
+ valid: true,
129
+ errors: null
130
+ };
37
131
  };
38
132
 
39
- Schema.prototype.get_path_type = function (path_name) {
40
- if(!this.compiled_schema || typeof this.compiled_schema.get_path_type !== 'function') {
41
- return null;
133
+ /**
134
+ * Validate root base fields directly against schema-level timestamp and id rules.
135
+ *
136
+ * @param {object} document
137
+ * @returns {{valid: boolean, errors: object[]|null}}
138
+ * @throws {ValidationError}
139
+ */
140
+ Schema.prototype.validate_base_fields = function (document) {
141
+ const validation_result = validate_base_field_values(document, this.id_strategy);
142
+
143
+ if(!validation_result.valid) {
144
+ throw new ValidationError('Schema validation failed', validation_result.errors);
42
145
  }
43
146
 
44
- return this.compiled_schema.get_path_type(path_name);
147
+ return validation_result;
45
148
  };
46
149
 
47
- Schema.prototype.is_array_root = function (path_name) {
48
- if(!this.compiled_schema || typeof this.compiled_schema.is_array_root !== 'function') {
49
- return false;
150
+ /**
151
+ * Read the configured default slug name.
152
+ *
153
+ * @returns {string}
154
+ */
155
+ Schema.prototype.get_default_slug = function () {
156
+ return this.options.default_slug;
157
+ };
158
+
159
+ /**
160
+ * Read the configured extra slug names.
161
+ *
162
+ * @returns {string[]}
163
+ */
164
+ Schema.prototype.get_extra_slugs = function () {
165
+ const slug_keys = this.options.slugs;
166
+
167
+ if(is_not_array(slug_keys)) {
168
+ return [];
50
169
  }
51
170
 
52
- return this.compiled_schema.is_array_root(path_name);
171
+ return [...slug_keys];
172
+ };
173
+
174
+ /**
175
+ * Read the full configured slug list, including the default slug.
176
+ *
177
+ * @returns {string[]}
178
+ */
179
+ Schema.prototype.get_slugs = function () {
180
+ const default_slug = this.get_default_slug();
181
+ const extra_slugs = this.get_extra_slugs();
182
+
183
+ return [default_slug, ...extra_slugs];
53
184
  };
54
185
 
55
- Schema.prototype.create_index = function (index_spec, index_options) {
56
- const normalized_index_spec = normalize_index_spec(index_spec);
57
- const normalized_index_options = normalize_index_options(index_options);
186
+ /**
187
+ * Remove keys that are not allowed by the compiled schema tree.
188
+ *
189
+ * @param {object} document
190
+ * @returns {object|undefined}
191
+ */
192
+ Schema.prototype.conform = function (document) {
193
+ prune_document_shape(document, this.$conform_tree);
194
+ return document;
195
+ };
196
+
197
+ /**
198
+ * Cast one full document envelope according to compiled schema paths.
199
+ *
200
+ * @param {object} document
201
+ * @returns {object}
202
+ */
203
+ Schema.prototype.cast = function (document) {
204
+ if(!is_plain_object(document)) {
205
+ return document;
206
+ }
207
+
208
+ const next_document = deep_clone(document);
209
+ const default_slug = this.get_default_slug();
210
+ const extra_slugs = new Set(this.get_extra_slugs());
211
+
212
+ for(const [path_name, field_type] of Object.entries(this.$field_types)) {
213
+ const path_segments = path_name.split('.');
214
+ const root_key = path_segments[0];
215
+ let target_root = next_document;
216
+ let target_segments = path_segments;
217
+
218
+ if(!base_fields_keys.has(root_key)) {
219
+ if(extra_slugs.has(root_key)) {
220
+ target_root = next_document[root_key];
221
+ target_segments = path_segments.slice(1);
222
+ } else {
223
+ target_root = next_document[default_slug];
224
+ }
225
+ }
226
+
227
+ const path_state = read_nested_path(target_root, target_segments);
228
+
229
+ if(!path_state.exists) {
230
+ continue;
231
+ }
232
+
233
+ let casted_value = path_state.value;
234
+ const cast_context = {
235
+ path: path_name,
236
+ mode: 'cast'
237
+ };
58
238
 
59
- this.indexes.push({
60
- index_spec: normalized_index_spec,
61
- index_options: normalized_index_options
62
- });
239
+ if(is_function(field_type.apply_set)) {
240
+ casted_value = field_type.apply_set(casted_value, cast_context);
241
+ }
242
+
243
+ if(is_function(field_type.cast)) {
244
+ casted_value = field_type.cast(casted_value, cast_context);
245
+ }
63
246
 
247
+ write_nested_path(target_root, target_segments, casted_value);
248
+ }
249
+
250
+ return next_document;
251
+ };
252
+
253
+ Schema.prototype.add_method = function (method_name, method_implementation) {
254
+ assert_identifier(method_name, 'method_name');
255
+ assert_condition(is_function(method_implementation), 'method_implementation must be a function');
256
+ assert_condition(has_own(this.methods, method_name) === false, 'Schema method "' + method_name + '" already exists');
257
+
258
+ this.methods[method_name] = method_implementation;
64
259
  return this;
65
260
  };
66
261
 
262
+ Schema.prototype.clone = function () {
263
+ const cloned_schema = Object.create(Schema.prototype);
264
+
265
+ cloned_schema.$field_registry = this.$field_registry;
266
+ cloned_schema.$path_introspection = deep_clone(this.$path_introspection);
267
+ cloned_schema.$field_types = deep_clone(this.$field_types);
268
+
269
+ cloned_schema.indexes = this.get_indexes();
270
+ cloned_schema.options = deep_clone(this.options);
271
+ cloned_schema.id_strategy = this.id_strategy;
272
+ cloned_schema.auto_index = this.auto_index;
273
+ cloned_schema.methods = Object.assign(Object.create(null), this.methods);
274
+ cloned_schema.validators = Object.create(null);
275
+ cloned_schema.aliases = deep_clone(this.aliases);
276
+ cloned_schema.$conform_tree = deep_clone(this.$conform_tree);
277
+
278
+ return cloned_schema;
279
+ };
280
+
281
+ Schema.prototype.create_index = function (index_definition) {
282
+ const normalized_index_definition = normalize_index_definition(index_definition);
283
+
284
+ if(normalized_index_definition) {
285
+ this.indexes.push(normalized_index_definition);
286
+ }
287
+
288
+ return this; // Allows chaining
289
+ };
290
+
291
+
292
+ // --- PROTOTYPE: SECONDARY / READ HELPERS ---
293
+
294
+ Schema.prototype.get_path = function (path_name) {
295
+ if(has_own(this.$field_types, path_name)) {
296
+ return this.$field_types[path_name];
297
+ }
298
+
299
+ return null;
300
+ };
301
+
302
+ Schema.prototype.get_path_type = function (path_name) {
303
+ return resolve_path_type(this.$path_introspection, path_name);
304
+ };
305
+
306
+ Schema.prototype.is_array_root = function (path_name) {
307
+ return resolve_is_array_root(this.$path_introspection, path_name);
308
+ };
309
+
67
310
  Schema.prototype.get_indexes = function () {
68
- const index_definitions = [];
69
- let index_position = 0;
311
+ const cloned_indexes = [];
70
312
 
71
- while(index_position < this.indexes.length) {
72
- const index_definition = this.indexes[index_position];
73
- const index_spec = clone_index_spec(index_definition.index_spec);
74
- const index_options = Object.assign({}, index_definition.index_options);
313
+ for(const index_definition of this.indexes) {
314
+ const cloned_definition = Object.assign({}, index_definition);
75
315
 
76
- index_definitions.push({
77
- index_spec: index_spec,
78
- index_options: index_options
79
- });
80
- index_position += 1;
316
+ if(is_object(index_definition.paths)) {
317
+ cloned_definition.paths = Object.assign({}, index_definition.paths);
318
+ }
319
+
320
+ cloned_indexes.push(cloned_definition);
81
321
  }
82
322
 
83
- return index_definitions;
323
+ return cloned_indexes;
84
324
  };
85
325
 
86
- function register_path_level_indexes(schema_instance, schema_definition, parent_path, field_registry) {
87
- const schema_entries = Object.entries(schema_definition);
88
- let entry_position = 0;
326
+ // --- LOCAL HELPER FUNCTIONS ---
327
+
328
+ function apply_base_fields(schema_def) {
329
+ const next_schema = Object.assign({}, schema_def);
330
+
331
+ for(const [field_name, field_config] of Object.entries(base_fields)) {
332
+ if(!has_own(next_schema, field_name)) {
333
+ next_schema[field_name] = Object.assign({}, field_config);
334
+ }
335
+ }
336
+
337
+ return next_schema;
338
+ }
339
+
340
+ function build_conform_tree(paths, default_slug, slug_keys) {
341
+ const allowed_tree = {};
342
+ const extra_slug_set = new Set(slug_keys || []);
89
343
 
90
- while(entry_position < schema_entries.length) {
91
- const schema_entry = schema_entries[entry_position];
92
- const path_segment = schema_entry[0];
93
- const field_definition = schema_entry[1];
94
- const path_value = parent_path ? parent_path + '.' + path_segment : path_segment;
344
+ for(const base_field_key of base_fields_keys) {
345
+ allowed_tree[base_field_key] = true;
346
+ }
347
+
348
+ for(const path_name of Object.keys(paths)) {
349
+ const path_segments = path_name.split('.');
350
+ const root_segment = path_segments[0];
351
+ let current_branch = allowed_tree;
352
+ let target_segments = path_segments;
95
353
 
96
- if(is_explicit_field_definition(field_definition, field_registry)) {
97
- register_index_option(schema_instance, path_value, field_definition.index);
98
- entry_position += 1;
354
+ if(base_fields_keys.has(root_segment)) {
99
355
  continue;
100
356
  }
101
357
 
102
- if(is_plain_object(field_definition)) {
103
- register_path_level_indexes(schema_instance, field_definition, path_value, field_registry);
358
+ // Unprefixed schema paths belong to the default slug, so wrap them under that slug root here.
359
+ if(!extra_slug_set.has(root_segment)) {
360
+ target_segments = [default_slug, ...path_segments];
104
361
  }
105
362
 
106
- entry_position += 1;
363
+ const depth = target_segments.length - 1;
364
+
365
+ for(let i = 0; i < depth; i++) {
366
+ const segment_value = target_segments[i];
367
+
368
+ if(!is_plain_object(current_branch[segment_value])) {
369
+ current_branch[segment_value] = {};
370
+ }
371
+
372
+ current_branch = current_branch[segment_value];
373
+ }
374
+
375
+ current_branch[target_segments[depth]] = true;
107
376
  }
377
+
378
+ return allowed_tree;
108
379
  }
109
380
 
110
- function is_explicit_field_definition(field_definition, field_registry) {
111
- if(!is_plain_object(field_definition) || !has_own(field_definition, 'type')) {
112
- return false;
113
- }
381
+ /**
382
+ * Build a normalized alias registry from compiled schema paths.
383
+ *
384
+ * @param {object} paths
385
+ * @returns {object}
386
+ * @throws {Error}
387
+ */
388
+ function collect_aliases(paths) {
389
+ const aliases = Object.create(null);
114
390
 
115
- const type_key = field_definition.type;
391
+ for(const [path_name, field_type] of Object.entries(paths)) {
392
+ const alias_value = field_type?.options?.alias;
393
+
394
+ if(!is_string(alias_value) || alias_value.length === 0) {
395
+ continue;
396
+ }
397
+
398
+ assert_identifier(alias_value, 'alias');
399
+
400
+ if(alias_value === path_name) {
401
+ continue;
402
+ }
403
+
404
+ if(base_fields_keys.has(alias_value)) {
405
+ throw new Error('Alias "' + alias_value + '" conflicts with reserved base field "' + alias_value + '"');
406
+ }
407
+
408
+ if(has_own(paths, alias_value) && alias_value !== path_name) {
409
+ throw new Error('Alias "' + alias_value + '" conflicts with existing schema path "' + alias_value + '"');
410
+ }
411
+
412
+ if(has_own(aliases, alias_value) && aliases[alias_value].path !== path_name) {
413
+ throw new Error('Duplicate alias "' + alias_value + '" for paths "' + aliases[alias_value].path + '" and "' + path_name + '"');
414
+ }
116
415
 
117
- if(Array.isArray(type_key)) {
118
- return true;
416
+ aliases[alias_value] = {path: path_name};
119
417
  }
120
418
 
121
- return field_registry.has_field_type(type_key);
419
+ return aliases;
122
420
  }
123
421
 
124
- function register_index_option(schema_instance, path_value, index_option) {
125
- if(index_option === undefined || index_option === false) {
126
- return;
422
+ /**
423
+ * Extract field-type entries for all slugs in a single pass.
424
+ *
425
+ * @param {object} field_types
426
+ * @param {string} default_slug
427
+ * @param {string[]} slug_keys
428
+ * @returns {object}
429
+ * @throws {Error} If a registered slug is defined as a primitive instead of an object.
430
+ */
431
+ function build_slug_field_maps(field_types, default_slug, slug_keys) {
432
+ const definitions = {
433
+ [default_slug]: {}
434
+ };
435
+
436
+ // Pre-initialize objects for explicit slugs
437
+ for(const slug_key of slug_keys) {
438
+ if(is_string(slug_key) && !base_fields_keys.has(slug_key) && slug_key !== default_slug) {
439
+ definitions[slug_key] = {};
440
+ }
127
441
  }
128
442
 
129
- if(index_option === true) {
130
- schema_instance.create_index(path_value);
131
- return;
443
+ for(const [path_name, field_type] of Object.entries(field_types)) {
444
+ const dot_index = path_name.indexOf('.');
445
+ const root_key = dot_index > -1 ? path_name.substring(0, dot_index) : path_name;
446
+
447
+ if(base_fields_keys.has(root_key)) {
448
+ continue;
449
+ }
450
+
451
+ // Explicit secondary slugs (strip the root prefix)
452
+ if(has_own(definitions, root_key) && root_key !== default_slug) {
453
+ // Fail fast: Registered slugs must be objects, not flat primitives
454
+ assert_condition(dot_index > -1, 'Registered slug "' + root_key + '" must be defined as a root object in schema');
455
+
456
+ const relative_path = path_name.substring(dot_index + 1);
457
+ definitions[root_key][relative_path] = field_type;
458
+ } else {
459
+ // Catch-all default slug (keep the full path)
460
+ definitions[default_slug][path_name] = field_type;
461
+ }
132
462
  }
133
463
 
134
- if(index_option === 1 || index_option === -1) {
135
- schema_instance.create_index({
136
- [path_value]: index_option
137
- });
138
- return;
464
+ return definitions;
465
+ }
466
+
467
+ function build_schema_options(schema_options = {}) {
468
+ const next_options = Object.assign({}, defaults.schema_options, schema_options);
469
+ const default_slug = next_options.default_slug;
470
+ const next_slug_keys = [];
471
+ const seen_slug_keys = new Set();
472
+
473
+ if(!is_array(next_options.slugs)) {
474
+ next_options.slugs = [];
475
+ return next_options;
139
476
  }
140
477
 
141
- if(is_plain_object(index_option)) {
142
- const index_options = Object.assign({}, index_option);
143
- const direction_value = resolve_path_level_index_direction(path_value, index_options);
478
+ for(const slug_key of next_options.slugs) {
479
+ if(!is_string(slug_key) || slug_key === default_slug || seen_slug_keys.has(slug_key)) {
480
+ continue;
481
+ }
144
482
 
145
- schema_instance.create_index({
146
- [path_value]: direction_value
147
- }, index_options);
148
- return;
483
+ seen_slug_keys.add(slug_key);
484
+ next_slug_keys.push(slug_key);
149
485
  }
150
486
 
151
- throw new Error('index option at path "' + path_value + '" must be true, false, 1, -1, or an options object');
487
+ next_options.slugs = next_slug_keys;
488
+ return next_options;
152
489
  }
153
490
 
154
- function resolve_path_level_index_direction(path_value, index_options) {
155
- let direction_value = 1;
491
+ /**
492
+ * Validate base-field values and collect deterministic error details.
493
+ *
494
+ * @param {object} document
495
+ * @param {string} id_strategy
496
+ * @returns {{valid: boolean, errors: object[]|null}}
497
+ */
498
+ function validate_base_field_values(document, id_strategy) {
499
+ const error_details = [];
500
+
501
+ if(id_strategy === ID_STRATEGY.uuidv7 && document.id != null && !is_uuid_v7(document.id)) {
502
+ error_details.push({
503
+ path: 'id',
504
+ code: 'validator_error',
505
+ message: 'Path "id" must be a valid UUIDv7',
506
+ type: 'validator_error',
507
+ value: document.id
508
+ });
509
+ }
156
510
 
157
- if(has_own(index_options, 'direction')) {
158
- direction_value = index_options.direction;
159
- delete index_options.direction;
511
+ for(const key of ['created_at', 'updated_at']) {
512
+ const value = document[key];
513
+
514
+ if(value != null && !is_valid_timestamp(value)) {
515
+ error_details.push({
516
+ path: key,
517
+ code: 'validator_error',
518
+ message: 'Path "' + key + '" must be a valid timestamp',
519
+ type: 'validator_error',
520
+ value
521
+ });
522
+ }
160
523
  }
161
524
 
162
- if(has_own(index_options, 'order')) {
163
- direction_value = index_options.order;
164
- delete index_options.order;
525
+ return {
526
+ valid: error_details.length === 0,
527
+ errors: error_details.length > 0 ? error_details : null
528
+ };
529
+ }
530
+
531
+ /**
532
+ * Remove keys that are not allowed by one compiled schema branch and recurse into nested objects.
533
+ *
534
+ * @param {object} document
535
+ * @param {object} branch
536
+ * @returns {void}
537
+ */
538
+ function prune_document_shape(document, branch) {
539
+ if(!is_object(document) || !is_object(branch)) {
540
+ return;
165
541
  }
166
542
 
167
- assert_condition(direction_value === 1 || direction_value === -1, 'index direction for path "' + path_value + '" must be 1 or -1');
543
+ for(const key of Object.keys(document)) {
544
+ // If key is not in schema branch, delete it
545
+ if(!has_own(branch, key)) {
546
+ delete document[key];
547
+ continue;
548
+ }
168
549
 
169
- return direction_value;
170
- }
550
+ const next_branch = branch[key];
551
+ const next_data = document[key];
171
552
 
172
- function normalize_index_spec(index_spec) {
173
- if(is_string(index_spec)) {
174
- assert_path(index_spec, 'index path');
175
- return index_spec;
553
+ // If branch continues and data is an object, keep walking
554
+ if(next_branch !== true && is_object(next_data)) {
555
+ prune_document_shape(next_data, next_branch);
556
+ }
176
557
  }
177
558
 
178
- assert_condition(is_object(index_spec), 'index_spec must be a path string or an object map of paths to sort directions');
559
+ return document;
560
+ }
179
561
 
180
- const index_entries = Object.entries(index_spec);
181
- assert_condition(index_entries.length > 0, 'index_spec object must define at least one indexed path');
562
+ /**
563
+ * Recursively collect inline index definitions from one schema definition tree.
564
+ *
565
+ * @param {object} current_definition
566
+ * @param {string} parent_path
567
+ * @param {object} field_registry
568
+ * @param {object[]} index_definitions
569
+ * @returns {void}
570
+ */
571
+ function collect_schema_indexes(current_definition, parent_path, field_registry, index_definitions) {
572
+ for(const [path_segment, field_definition] of Object.entries(current_definition)) {
573
+ const full_path = parent_path ? `${parent_path}.${path_segment}` : path_segment;
574
+
575
+ if(!parent_path && base_fields_keys.has(path_segment)) {
576
+ continue;
577
+ }
182
578
 
183
- const normalized_spec = {};
184
- let entry_position = 0;
579
+ if(is_explicit_field(field_definition, field_registry)) {
580
+ const inline_index_definition = normalize_index_definition(field_definition.index, full_path);
185
581
 
186
- while(entry_position < index_entries.length) {
187
- const index_entry = index_entries[entry_position];
188
- const path_value = index_entry[0];
189
- const direction_value = index_entry[1];
582
+ if(inline_index_definition) {
583
+ index_definitions.push(inline_index_definition);
584
+ }
585
+ } else if(is_object(field_definition)) {
586
+ collect_schema_indexes(field_definition, full_path, field_registry, index_definitions);
587
+ }
588
+ }
589
+ }
190
590
 
191
- assert_path(path_value, 'index path');
192
- assert_condition(
193
- direction_value === 1 || direction_value === -1,
194
- 'index direction for path "' + path_value + '" must be 1 or -1'
195
- );
196
- normalized_spec[path_value] = direction_value;
197
- entry_position += 1;
591
+ function is_explicit_field(field_definition, registry) {
592
+ if(!is_object(field_definition) || !has_own(field_definition, 'type')) {
593
+ return false;
198
594
  }
199
595
 
200
- return normalized_spec;
596
+ const type_key = field_definition.type;
597
+ return Array.isArray(type_key) || registry.has_field_type(type_key);
201
598
  }
202
599
 
203
- function normalize_index_options(index_options) {
204
- if(index_options === undefined) {
205
- return {};
600
+ function normalize_index_definition(index_input, path_name) {
601
+ if(index_input === undefined || index_input === false || index_input === null) {
602
+ return null;
603
+ }
604
+
605
+ // Handle shorthand primitives when path_name is provided
606
+ if(path_name !== undefined) {
607
+ if(index_input === true) {
608
+ return {using: 'gin', path: path_name};
609
+ }
610
+
611
+ if(index_input === 1 || index_input === -1) {
612
+ return {using: 'btree', path: path_name, order: index_input};
613
+ }
206
614
  }
207
615
 
208
- assert_condition(is_object(index_options), 'index_options must be an object');
616
+ // Ensure input is an object for further processing
617
+ if(!is_object(index_input)) {
618
+ return null;
619
+ }
209
620
 
210
- const normalized_options = Object.assign({}, index_options);
621
+ const raw_definition = Object.assign({}, index_input);
211
622
 
212
- if(normalized_options.unique !== undefined) {
213
- assert_condition(typeof normalized_options.unique === 'boolean', 'index_options.unique must be a boolean');
623
+ // Auto-inject path_name if provided and missing
624
+ if(path_name !== undefined && !has_own(raw_definition, 'path')) {
625
+ raw_definition.path = path_name;
214
626
  }
215
627
 
216
- if(normalized_options.name !== undefined) {
217
- assert_identifier(normalized_options.name, 'index_options.name');
628
+ // Determine index type
629
+ let index_type = raw_definition.using;
630
+
631
+ if(index_type !== 'gin' && index_type !== 'btree') {
632
+ if(raw_definition.using !== undefined) {
633
+ index_type = 'btree';
634
+ } else if(raw_definition.paths !== undefined || raw_definition.order !== undefined || raw_definition.unique !== undefined) {
635
+ index_type = 'btree';
636
+ } else if(raw_definition.path !== undefined) {
637
+ index_type = 'gin';
638
+ } else {
639
+ return null; // Cannot infer index type
640
+ }
218
641
  }
219
642
 
220
- return normalized_options;
221
- }
643
+ // Build normalized definition based on type
644
+ const normalized_definition = {using: index_type};
222
645
 
223
- function clone_index_spec(index_spec) {
224
- if(is_string(index_spec)) {
225
- return index_spec;
646
+ if(is_string(raw_definition.name)) {
647
+ try {
648
+ assert_identifier(raw_definition.name, 'index_definition.name');
649
+ normalized_definition.name = raw_definition.name;
650
+ } catch(error) {
651
+ // Intentionally ignore invalid names
652
+ }
226
653
  }
227
654
 
228
- return Object.assign({}, index_spec);
229
- }
655
+ if(index_type === 'gin') {
656
+ // GIN: requires a single valid path, rejects order and unique
657
+ if(!is_string(raw_definition.path)) {
658
+ return null;
659
+ }
230
660
 
231
- function is_plain_object(value) {
232
- return value !== null && typeof value === 'object' && !Array.isArray(value);
661
+ try {
662
+ assert_path(raw_definition.path, 'index path');
663
+ normalized_definition.path = raw_definition.path;
664
+ return normalized_definition;
665
+ } catch(error) {
666
+ return null;
667
+ }
668
+ }
669
+
670
+ if(index_type === 'btree') {
671
+ // BTREE: allows unique, order, single path, or multiple paths
672
+ let has_valid_path = false;
673
+
674
+ if(is_string(raw_definition.path)) {
675
+ try {
676
+ assert_path(raw_definition.path, 'index path');
677
+ normalized_definition.path = raw_definition.path;
678
+ normalized_definition.order = (raw_definition.order === 1 || raw_definition.order === -1) ? raw_definition.order : 1;
679
+ has_valid_path = true;
680
+ } catch(error) {
681
+ }
682
+ }
683
+
684
+ if(!has_valid_path && is_object(raw_definition.paths)) {
685
+ const valid_paths = {};
686
+
687
+ for(const [p_value, p_order] of Object.entries(raw_definition.paths)) {
688
+ if(!is_string(p_value) || (p_order !== 1 && p_order !== -1)) {
689
+ continue;
690
+ }
691
+
692
+ try {
693
+ assert_path(p_value, 'index path');
694
+ valid_paths[p_value] = p_order;
695
+ } catch(error) {
696
+ }
697
+ }
698
+
699
+ if(Object.keys(valid_paths).length > 0) {
700
+ normalized_definition.paths = valid_paths;
701
+ has_valid_path = true;
702
+ }
703
+ }
704
+
705
+ if(!has_valid_path) {
706
+ return null;
707
+ }
708
+
709
+ if(typeof raw_definition.unique === 'boolean') {
710
+ normalized_definition.unique = raw_definition.unique;
711
+ }
712
+
713
+ return normalized_definition;
714
+ }
233
715
  }
234
716
 
717
+ export default Schema;