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.
- package/README.md +36 -18
- package/docs/api/connection.md +144 -0
- package/docs/api/delta-tracker.md +106 -0
- package/docs/api/document.md +77 -0
- package/docs/api/field-types.md +329 -0
- package/docs/api/index.md +35 -0
- package/docs/api/model.md +392 -0
- package/docs/api/query-builder.md +81 -0
- package/docs/api/schema.md +204 -0
- package/docs/architecture-flow.md +397 -0
- package/docs/examples.md +495 -218
- package/docs/jsonb-ops.md +171 -0
- package/docs/lifecycle/model-compilation.md +111 -0
- package/docs/lifecycle.md +146 -0
- package/docs/query-translation.md +11 -10
- package/package.json +10 -3
- package/src/connection/connect.js +12 -17
- package/src/connection/connection.js +128 -0
- package/src/connection/server-capabilities.js +60 -59
- package/src/constants/defaults.js +32 -19
- package/src/constants/{id-strategies.js → id-strategy.js} +28 -29
- package/src/constants/intake-mode.js +8 -0
- package/src/debug/debug-logger.js +17 -15
- package/src/errors/model-overwrite-error.js +25 -0
- package/src/errors/query-error.js +25 -23
- package/src/errors/validation-error.js +25 -23
- package/src/field-types/base-field-type.js +137 -140
- package/src/field-types/builtins/advanced.js +365 -365
- package/src/field-types/builtins/index.js +579 -585
- package/src/field-types/field-type-namespace.js +9 -0
- package/src/field-types/registry.js +149 -122
- package/src/index.js +26 -36
- package/src/migration/ensure-index.js +157 -154
- package/src/migration/ensure-schema.js +27 -15
- package/src/migration/ensure-table.js +44 -31
- package/src/migration/schema-indexes-resolver.js +8 -6
- package/src/model/document-instance.js +29 -540
- package/src/model/document.js +60 -0
- package/src/model/factory/constants.js +36 -0
- package/src/model/factory/index.js +58 -0
- package/src/model/model.js +875 -0
- package/src/model/operations/delete-one.js +39 -0
- package/src/model/operations/insert-one.js +35 -0
- package/src/model/operations/query-builder.js +132 -0
- package/src/model/operations/update-one.js +333 -0
- package/src/model/state.js +34 -0
- package/src/schema/field-definition-parser.js +213 -218
- package/src/schema/path-introspection.js +87 -82
- package/src/schema/schema-compiler.js +126 -212
- package/src/schema/schema.js +621 -138
- package/src/sql/index.js +17 -0
- package/src/sql/jsonb/ops.js +153 -0
- package/src/{query → sql/jsonb}/path-parser.js +54 -43
- package/src/sql/jsonb/read/elem-match.js +133 -0
- package/src/{query → sql/jsonb/read}/operators/contains.js +13 -7
- package/src/sql/jsonb/read/operators/elem-match.js +9 -0
- package/src/{query → sql/jsonb/read}/operators/has-all-keys.js +17 -11
- package/src/{query → sql/jsonb/read}/operators/has-any-keys.js +18 -11
- package/src/sql/jsonb/read/operators/has-key.js +12 -0
- package/src/{query → sql/jsonb/read}/operators/jsonpath-exists.js +22 -15
- package/src/{query → sql/jsonb/read}/operators/jsonpath-match.js +22 -15
- package/src/{query → sql/jsonb/read}/operators/size.js +23 -16
- package/src/sql/parameter-binder.js +18 -13
- package/src/sql/read/build-count-query.js +12 -0
- package/src/sql/read/build-find-query.js +25 -0
- package/src/sql/read/limit-skip.js +21 -0
- package/src/sql/read/sort.js +85 -0
- package/src/sql/read/where/base-fields.js +310 -0
- package/src/sql/read/where/casting.js +90 -0
- package/src/sql/read/where/context.js +79 -0
- package/src/sql/read/where/field-clause.js +58 -0
- package/src/sql/read/where/index.js +38 -0
- package/src/sql/read/where/operator-entries.js +29 -0
- package/src/{query → sql/read/where}/operators/all.js +16 -10
- package/src/sql/read/where/operators/eq.js +12 -0
- package/src/{query → sql/read/where}/operators/gt.js +23 -16
- package/src/{query → sql/read/where}/operators/gte.js +23 -16
- package/src/{query → sql/read/where}/operators/in.js +18 -12
- package/src/sql/read/where/operators/index.js +40 -0
- package/src/{query → sql/read/where}/operators/lt.js +23 -16
- package/src/{query → sql/read/where}/operators/lte.js +23 -16
- package/src/sql/read/where/operators/ne.js +12 -0
- package/src/{query → sql/read/where}/operators/nin.js +18 -12
- package/src/{query → sql/read/where}/operators/regex.js +14 -8
- package/src/sql/read/where/operators.js +126 -0
- package/src/sql/read/where/text-operators.js +83 -0
- package/src/sql/run.js +46 -0
- package/src/sql/write/build-delete-query.js +33 -0
- package/src/sql/write/build-insert-query.js +42 -0
- package/src/sql/write/build-update-query.js +65 -0
- package/src/utils/assert.js +34 -27
- package/src/utils/delta-tracker/.archive/1 tracker-redesign-codex-v2.md +250 -0
- package/src/utils/delta-tracker/.archive/1 tracker-redesign-gemini.md +101 -0
- package/src/utils/delta-tracker/.archive/2 evaluation by gemini.txt +65 -0
- package/src/utils/delta-tracker/.archive/2 evaluation by grok.txt +39 -0
- package/src/utils/delta-tracker/.archive/3 gemini evaluate grok.txt +37 -0
- package/src/utils/delta-tracker/.archive/3 grok evaluate gemini.txt +63 -0
- package/src/utils/delta-tracker/.archive/4 gemini veredict.txt +16 -0
- package/src/utils/delta-tracker/.archive/index.1.js +587 -0
- package/src/utils/delta-tracker/.archive/index.2.js +612 -0
- package/src/utils/delta-tracker/index.js +592 -0
- package/src/utils/dirty-tracker/inline.js +335 -0
- package/src/utils/dirty-tracker/instance.js +414 -0
- package/src/utils/dirty-tracker/static.js +343 -0
- package/src/utils/json-safe.js +13 -9
- package/src/utils/object-path.js +227 -33
- package/src/utils/object.js +408 -168
- package/src/utils/string.js +55 -0
- package/src/utils/value.js +169 -30
- package/docs/api.md +0 -152
- package/src/connection/disconnect.js +0 -16
- package/src/connection/pool-store.js +0 -46
- package/src/model/model-factory.js +0 -555
- package/src/query/limit-skip-compiler.js +0 -31
- package/src/query/operators/elem-match.js +0 -3
- package/src/query/operators/eq.js +0 -6
- package/src/query/operators/has-key.js +0 -6
- package/src/query/operators/index.js +0 -60
- package/src/query/operators/ne.js +0 -6
- package/src/query/query-builder.js +0 -93
- package/src/query/sort-compiler.js +0 -30
- package/src/query/where-compiler.js +0 -477
- package/src/sql/sql-runner.js +0 -31
package/src/schema/schema.js
CHANGED
|
@@ -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 {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
93
|
+
return this;
|
|
29
94
|
};
|
|
30
95
|
|
|
31
|
-
Schema.prototype.
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
127
|
+
return {
|
|
128
|
+
valid: true,
|
|
129
|
+
errors: null
|
|
130
|
+
};
|
|
37
131
|
};
|
|
38
132
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
147
|
+
return validation_result;
|
|
45
148
|
};
|
|
46
149
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
69
|
-
let index_position = 0;
|
|
311
|
+
const cloned_indexes = [];
|
|
70
312
|
|
|
71
|
-
|
|
72
|
-
const
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
323
|
+
return cloned_indexes;
|
|
84
324
|
};
|
|
85
325
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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
|
-
|
|
118
|
-
return true;
|
|
416
|
+
aliases[alias_value] = {path: path_name};
|
|
119
417
|
}
|
|
120
418
|
|
|
121
|
-
return
|
|
419
|
+
return aliases;
|
|
122
420
|
}
|
|
123
421
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
}, index_options);
|
|
148
|
-
return;
|
|
483
|
+
seen_slug_keys.add(slug_key);
|
|
484
|
+
next_slug_keys.push(slug_key);
|
|
149
485
|
}
|
|
150
486
|
|
|
151
|
-
|
|
487
|
+
next_options.slugs = next_slug_keys;
|
|
488
|
+
return next_options;
|
|
152
489
|
}
|
|
153
490
|
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
550
|
+
const next_branch = branch[key];
|
|
551
|
+
const next_data = document[key];
|
|
171
552
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
559
|
+
return document;
|
|
560
|
+
}
|
|
179
561
|
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
|
204
|
-
if(
|
|
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
|
-
|
|
616
|
+
// Ensure input is an object for further processing
|
|
617
|
+
if(!is_object(index_input)) {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
209
620
|
|
|
210
|
-
const
|
|
621
|
+
const raw_definition = Object.assign({}, index_input);
|
|
211
622
|
|
|
212
|
-
if
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
}
|
|
643
|
+
// Build normalized definition based on type
|
|
644
|
+
const normalized_definition = {using: index_type};
|
|
222
645
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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;
|