jsonbadger 0.5.0 → 0.6.1
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
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# Model API
|
|
2
|
+
|
|
3
|
+
Models are created through the owning connection:
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const connection = await JsonBadger.connect(uri, options);
|
|
7
|
+
const user_model = connection.model({name: 'User', schema: user_schema, table_name: 'users'});
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## TOC
|
|
11
|
+
|
|
12
|
+
- [Model API](#model-api)
|
|
13
|
+
- [Construction](#construction)
|
|
14
|
+
- [Ownership Boundary](#ownership-boundary)
|
|
15
|
+
- [Static Methods](#static-methods)
|
|
16
|
+
- [Model.from](#modelfrom)
|
|
17
|
+
- [Model.hydrate](#modelhydrate)
|
|
18
|
+
- [Model.cast](#modelcast)
|
|
19
|
+
- [Instance Methods](#instance-methods)
|
|
20
|
+
- [Related Docs](#related-docs)
|
|
21
|
+
|
|
22
|
+
## Construction
|
|
23
|
+
|
|
24
|
+
Build new documents with `Model.from(...)`:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const user_document = user_model.from({
|
|
28
|
+
name: 'john',
|
|
29
|
+
age: 30
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Hydrate persisted row-like data with `Model.hydrate(...)`:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const hydrated_user = user_model.hydrate({
|
|
37
|
+
id: '7',
|
|
38
|
+
data: {
|
|
39
|
+
name: 'maria'
|
|
40
|
+
},
|
|
41
|
+
created_at: '2026-03-03T08:00:00.000Z',
|
|
42
|
+
updated_at: '2026-03-03T09:00:00.000Z'
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> **Note:** Prefer `Model.from(...)` and `Model.hydrate(...)` over direct `new Model(...)` construction. Those entry points conform the document envelope, then apply schema defaults, casting, and validation before returning the document instance.
|
|
47
|
+
>
|
|
48
|
+
> Direct `new user_model(document)` construction remains available as a low-level path. It bypasses lifecycle until you call `doc.$conform_document(...)`, `doc.$apply_defaults(...)`, `doc.$cast(...)`, `doc.$validate(...)`, or `doc.$normalize(...)` manually.
|
|
49
|
+
|
|
50
|
+
## Ownership Boundary
|
|
51
|
+
|
|
52
|
+
`Model` owns how a schema is persisted and used at runtime.
|
|
53
|
+
|
|
54
|
+
It is the right place for:
|
|
55
|
+
|
|
56
|
+
1. `table_name`, `data_column`, and related storage options
|
|
57
|
+
2. Startup/bootstrap helpers such as `ensure_table()`, `ensure_indexes()`, and `ensure_model()`
|
|
58
|
+
3. Query and update entry points
|
|
59
|
+
4. Document construction and row hydration
|
|
60
|
+
|
|
61
|
+
`Model` consumes a `Schema`; it does not replace it.
|
|
62
|
+
|
|
63
|
+
## Static Methods
|
|
64
|
+
|
|
65
|
+
- `Model.create(document_or_list)`
|
|
66
|
+
- validates and saves one or more documents
|
|
67
|
+
- `Model.find(filter)`
|
|
68
|
+
- returns a query builder
|
|
69
|
+
- `Model.find_one(filter)`
|
|
70
|
+
- returns a query builder that resolves to one document or `null`
|
|
71
|
+
- `Model.find_by_id(id_value)`
|
|
72
|
+
- returns a query builder for one document by top-level `id`
|
|
73
|
+
- `Model.count_documents(filter)`
|
|
74
|
+
- returns a query builder that resolves to a count
|
|
75
|
+
- `Model.insert_one(input_data)`
|
|
76
|
+
- inserts one plain payload input and returns the persisted document
|
|
77
|
+
- `Model.update_one(filter, update_definition)`
|
|
78
|
+
- updates one matching row and returns the updated document or `null`
|
|
79
|
+
- `Model.delete_one(filter)`
|
|
80
|
+
- deletes one matching row and returns the deleted document or `null`
|
|
81
|
+
- `Model.ensure_table()`
|
|
82
|
+
- creates the backing table during startup/bootstrap setup
|
|
83
|
+
- `Model.ensure_indexes()`
|
|
84
|
+
- creates declared indexes during startup/bootstrap setup
|
|
85
|
+
- `Model.ensure_model()`
|
|
86
|
+
- runs table setup and, when `schema.auto_index` is enabled, declared indexes
|
|
87
|
+
- `Model.from(input_data, options?)`
|
|
88
|
+
- builds a new document from external payload input without marking it persisted
|
|
89
|
+
- `Model.hydrate(input_data, options?)`
|
|
90
|
+
- builds a persisted document from existing row-like data with no modified paths
|
|
91
|
+
- `Model.cast(document)`
|
|
92
|
+
- delegates to `schema.cast(document)`
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
const saved_user = await user_model.create({
|
|
98
|
+
name: 'maria',
|
|
99
|
+
age: 29
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const found_user = await user_model.find_one({name: 'maria'}).exec();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
> **Note:** `Model.insert_one(...)` accepts plain input only. If you already have a document instance, use `doc.insert()` or `doc.save()`.
|
|
106
|
+
|
|
107
|
+
## Model.from
|
|
108
|
+
|
|
109
|
+
Use `Model.from(...)` when the source is external payload input and you want a new document:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
const imported_user = user_model.from({
|
|
113
|
+
name: ' maria ',
|
|
114
|
+
age: '29',
|
|
115
|
+
created_at: '2026-03-03T08:00:00.000Z'
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Behavior:
|
|
120
|
+
- returns a new document instance
|
|
121
|
+
- keeps `is_new = true`
|
|
122
|
+
- leaves no paths marked as modified after lifecycle normalization
|
|
123
|
+
- conforms the document envelope, then applies schema defaults, casting, and validation before returning
|
|
124
|
+
- expands dotted payload keys at the input boundary
|
|
125
|
+
- routes root keys listed in `schema.options.slugs` out of the default slug
|
|
126
|
+
- keeps every other non-base root key inside the default slug
|
|
127
|
+
- does not interpret payload input as a persisted row envelope
|
|
128
|
+
|
|
129
|
+
Input flow:
|
|
130
|
+
1. check whether the input is object-like
|
|
131
|
+
2. serialize non-plain objects through `to_json`, `toJSON`, or `to_plain_object(...)`
|
|
132
|
+
3. expand dotted payload keys into nested objects
|
|
133
|
+
4. route registered extra slug roots out of the default slug
|
|
134
|
+
5. keep every remaining root key in the default slug
|
|
135
|
+
6. build the document instance
|
|
136
|
+
7. conform the document envelope, then apply schema defaults, casting, and validation
|
|
137
|
+
8. clear tracker changes so the normalized state becomes the baseline
|
|
138
|
+
|
|
139
|
+
Example with a custom default slug:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
const user_schema = new JsonBadger.Schema({
|
|
143
|
+
name: String,
|
|
144
|
+
settings: {
|
|
145
|
+
theme: String
|
|
146
|
+
}
|
|
147
|
+
}, {
|
|
148
|
+
default_slug: 'payload',
|
|
149
|
+
slugs: ['settings']
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const user_model = connection.model({
|
|
153
|
+
name: 'User',
|
|
154
|
+
schema: user_schema,
|
|
155
|
+
table_name: 'users'
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const imported_user = user_model.from({
|
|
159
|
+
id: '7',
|
|
160
|
+
created_at: '2026-03-03T08:00:00.000Z',
|
|
161
|
+
name: 'maria',
|
|
162
|
+
settings: {
|
|
163
|
+
theme: 'dark'
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Result:
|
|
169
|
+
- `imported_user.document.id === '7'`
|
|
170
|
+
- `imported_user.document.created_at instanceof Date`
|
|
171
|
+
- `imported_user.document.payload.name === 'maria'`
|
|
172
|
+
- `imported_user.document.settings.theme === 'dark'`
|
|
173
|
+
|
|
174
|
+
## Model.hydrate
|
|
175
|
+
|
|
176
|
+
Use `Model.hydrate(...)` when the source already represents a persisted row:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
const hydrated_user = user_model.hydrate({
|
|
180
|
+
id: '7',
|
|
181
|
+
payload: {
|
|
182
|
+
name: 'maria',
|
|
183
|
+
age: '29'
|
|
184
|
+
},
|
|
185
|
+
settings: {
|
|
186
|
+
theme: 'dark'
|
|
187
|
+
},
|
|
188
|
+
created_at: '2026-03-03T08:00:00.000Z',
|
|
189
|
+
updated_at: '2026-03-03T09:00:00.000Z'
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Behavior:
|
|
194
|
+
- returns a document instance
|
|
195
|
+
- sets `is_new = false`
|
|
196
|
+
- leaves no paths marked as modified initially
|
|
197
|
+
- treats the outer object as the persisted row envelope
|
|
198
|
+
- reads the payload from `input_data[default_slug]`
|
|
199
|
+
- preserves registered extra slug objects as sibling roots
|
|
200
|
+
- ignores unregistered extra root slugs
|
|
201
|
+
- applies schema defaults, casting, and validation before rebasing tracker state
|
|
202
|
+
- does not expand dotted payload keys during hydration
|
|
203
|
+
|
|
204
|
+
Input flow:
|
|
205
|
+
1. check whether the input is object-like
|
|
206
|
+
2. serialize non-plain objects through `to_json`, `toJSON`, or `to_plain_object(...)`
|
|
207
|
+
3. copy top-level base fields from the row
|
|
208
|
+
4. read `schema.get_slugs()` from the row and default missing slug roots to `{}`
|
|
209
|
+
5. build the document instance
|
|
210
|
+
6. conform the document envelope, then apply schema defaults, casting, and validation
|
|
211
|
+
7. clear tracker changes
|
|
212
|
+
8. mark the document persisted
|
|
213
|
+
|
|
214
|
+
> **Note:** `Model.hydrate(...)` is strict about row shape. It does not rebuild the default slug from unrelated root keys.
|
|
215
|
+
|
|
216
|
+
## Model.cast
|
|
217
|
+
|
|
218
|
+
Use `Model.cast(...)` when you want schema casting without building a document instance:
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
const casted_document = user_model.cast({
|
|
222
|
+
payload: {
|
|
223
|
+
age: '29'
|
|
224
|
+
},
|
|
225
|
+
settings: {
|
|
226
|
+
count: '7'
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Behavior:
|
|
232
|
+
- delegates to `schema.cast(...)`
|
|
233
|
+
- deep-clones the input document first
|
|
234
|
+
- casts existing base-field, default-slug, and extra-slug values
|
|
235
|
+
- does not validate
|
|
236
|
+
- does not conform
|
|
237
|
+
|
|
238
|
+
## Instance Methods
|
|
239
|
+
|
|
240
|
+
- `doc.save()`
|
|
241
|
+
- inserts a new row or updates an existing persisted document
|
|
242
|
+
- `doc.insert()`
|
|
243
|
+
- inserts the current document as a new row
|
|
244
|
+
- `doc.update()`
|
|
245
|
+
- updates the current persisted document by `id`
|
|
246
|
+
- `doc.get(path_name)`
|
|
247
|
+
- resolves aliases and reads the exact path from `doc.document`
|
|
248
|
+
- `doc.set(path_name, value)`
|
|
249
|
+
- resolves aliases, applies schema setter/cast logic, and writes the exact path into `doc.document`
|
|
250
|
+
- `doc.$apply_defaults(options?)`
|
|
251
|
+
- fills missing paths from schema defaults on the current tracked document
|
|
252
|
+
- `doc.$conform_document(options?)`
|
|
253
|
+
- removes unknown envelope keys from the current tracked document according to schema slugs and paths
|
|
254
|
+
- `doc.$cast(options?)`
|
|
255
|
+
- applies schema setter/cast logic across the current tracked document
|
|
256
|
+
- `doc.$validate(options?)`
|
|
257
|
+
- validates the current document envelope against the schema
|
|
258
|
+
- `doc.$normalize(options?)`
|
|
259
|
+
- runs `$conform_document(...)`, `$apply_defaults(...)`, `$cast(...)`, and `$validate(...)` in order
|
|
260
|
+
- `doc.bind_document(target)`
|
|
261
|
+
- binds live forwarding properties onto `target` from the current document instance
|
|
262
|
+
- `doc.rebase(reference)`
|
|
263
|
+
- replaces the current document state from another `Model` or `Document` and clears tracked changes
|
|
264
|
+
- `doc.id`
|
|
265
|
+
- `doc.created_at`
|
|
266
|
+
- `doc.updated_at`
|
|
267
|
+
- `doc.timestamps`
|
|
268
|
+
|
|
269
|
+
Example:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
const doc = user_model.from({
|
|
273
|
+
name: 'john',
|
|
274
|
+
profile: {city: 'Miami'}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const default_slug = user_model.schema.get_default_slug();
|
|
278
|
+
|
|
279
|
+
doc.set(default_slug + '.profile.city', 'Orlando');
|
|
280
|
+
const city = doc.get(default_slug + '.profile.city');
|
|
281
|
+
|
|
282
|
+
await doc.save();
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Low-level constructor path with manual lifecycle:
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
const doc = new user_model({
|
|
289
|
+
payload: {
|
|
290
|
+
age: '29'
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
doc.$normalize({mode: 'from'});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Behavior:
|
|
298
|
+
- direct `new user_model(document)` keeps the raw tracked state as given
|
|
299
|
+
- conform, defaults, casting, and validation do not run automatically on that low-level path
|
|
300
|
+
- manual lifecycle methods are available when you intentionally want that control
|
|
301
|
+
|
|
302
|
+
`doc.bind_document(target)` projects live document-backed fields onto another object, such as a domain entity wrapper.
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
const doc = user_model.hydrate({
|
|
306
|
+
id: '7',
|
|
307
|
+
payload: {
|
|
308
|
+
name: 'maria',
|
|
309
|
+
profile: {city: 'Miami'}
|
|
310
|
+
},
|
|
311
|
+
settings: {
|
|
312
|
+
theme: 'dark'
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const entity = {};
|
|
317
|
+
doc.bind_document(entity);
|
|
318
|
+
|
|
319
|
+
entity.name; // 'maria'
|
|
320
|
+
entity.profile.city = 'Orlando';
|
|
321
|
+
entity.settings.theme = 'light';
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Behavior:
|
|
325
|
+
- returns the same `target`
|
|
326
|
+
- flattens default-slug root fields onto the target root
|
|
327
|
+
- keeps extra slugs nested at their slug root
|
|
328
|
+
- returns live tracked objects, not snapshots
|
|
329
|
+
- throws if `target` already owns a conflicting property name
|
|
330
|
+
|
|
331
|
+
> **Note:** `bind_document(...)` does not create or require `target.model`. It binds directly to the current document instance. To rebind a different document later, clear or replace the old bound properties first, then call `next_doc.bind_document(target)` again.
|
|
332
|
+
|
|
333
|
+
`doc.rebase(reference)` is useful when you want to keep the same outer object but adopt a fresh persisted document state.
|
|
334
|
+
|
|
335
|
+
Scenario A: keep a bound entity wrapper and adopt the saved state.
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
const doc = user_model.from({
|
|
339
|
+
name: 'maria',
|
|
340
|
+
profile: {city: 'Miami'}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const entity = {};
|
|
344
|
+
doc.bind_document(entity);
|
|
345
|
+
|
|
346
|
+
entity.profile.city = 'Orlando';
|
|
347
|
+
|
|
348
|
+
const saved_doc = await doc.save();
|
|
349
|
+
doc.rebase(saved_doc);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Scenario B: refresh the current instance from a newer persisted copy.
|
|
353
|
+
|
|
354
|
+
```js
|
|
355
|
+
const current_doc = await user_model.find_one({id: user_id}).exec();
|
|
356
|
+
const fresh_doc = await user_model.find_one({id: user_id}).exec();
|
|
357
|
+
|
|
358
|
+
current_doc.rebase(fresh_doc);
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Scenario C: adopt a trusted `Document` returned by a custom persistence path.
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
const next_document = custom_save_flow(doc.document);
|
|
365
|
+
doc.rebase(next_document);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Behavior:
|
|
369
|
+
- accepts only a `Model` or `Document` reference
|
|
370
|
+
- sets `doc.is_new = false`
|
|
371
|
+
- replaces the current `doc.document` state
|
|
372
|
+
- clears pending tracker changes after rebasing
|
|
373
|
+
|
|
374
|
+
> **Note:** Use `Model.hydrate(...)` for plain row-like input. `rebase(...)` is for adopting an already-built `Model` or `Document`.
|
|
375
|
+
|
|
376
|
+
Direct document access:
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
const default_slug = user_model.schema.get_default_slug();
|
|
380
|
+
const payload = doc.document[default_slug];
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
> **Note:** Direct `doc.document[...]` mutation is still allowed, but it bypasses `doc.set(...)` and assignment-time casting.
|
|
384
|
+
|
|
385
|
+
## Related Docs
|
|
386
|
+
|
|
387
|
+
- [`connection.md`](connection.md)
|
|
388
|
+
- [`document.md`](document.md)
|
|
389
|
+
- [`schema.md`](schema.md)
|
|
390
|
+
- [`query-builder.md`](query-builder.md)
|
|
391
|
+
- [`../lifecycle.md`](../lifecycle.md)
|
|
392
|
+
- [`../examples.md`](../examples.md)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Query Builder API
|
|
2
|
+
|
|
3
|
+
## TOC
|
|
4
|
+
|
|
5
|
+
- [Query Builder API](#query-builder-api)
|
|
6
|
+
- [Builder Chain](#builder-chain)
|
|
7
|
+
- [Execution](#execution)
|
|
8
|
+
- [Supported Filter Families](#supported-filter-families)
|
|
9
|
+
- [Base-field Query Rules](#base-field-query-rules)
|
|
10
|
+
- [JSON Key Existence](#json-key-existence)
|
|
11
|
+
- [JSONPath](#jsonpath)
|
|
12
|
+
|
|
13
|
+
## Builder Chain
|
|
14
|
+
|
|
15
|
+
- `.where(filter)`
|
|
16
|
+
- `.sort(sort)`
|
|
17
|
+
- `.limit(limit)`
|
|
18
|
+
- `.skip(skip)`
|
|
19
|
+
- `.exec()`
|
|
20
|
+
|
|
21
|
+
## Execution
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
const result = await user_model.find({status: 'active'}).sort({created_at: -1}).limit(10).exec();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Filter objects must be plain JSON-like objects:
|
|
28
|
+
- use object literals for operator objects like `{age: {$gte: 18}}`
|
|
29
|
+
- use plain nested objects for JSON containment filters
|
|
30
|
+
- use plain objects instead of custom class instances for operator objects
|
|
31
|
+
|
|
32
|
+
> **Note:** Base-field operator objects and nested filter objects only accept plain objects with a default or null prototype.
|
|
33
|
+
|
|
34
|
+
Read/query execution:
|
|
35
|
+
- `find(...).exec()`
|
|
36
|
+
- `find_one(...).exec()`
|
|
37
|
+
- `find_by_id(...).exec()`
|
|
38
|
+
- `count_documents(...).exec()`
|
|
39
|
+
|
|
40
|
+
> **Note:** Query builders are not thenables. Call `.exec()`.
|
|
41
|
+
|
|
42
|
+
## Supported Filter Families
|
|
43
|
+
|
|
44
|
+
- scalar comparisons: direct equality, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`
|
|
45
|
+
- sets/arrays: `$in`, `$nin`, `$all`, `$size`, `$contains`, `$elem_match`
|
|
46
|
+
- regex: `$regex` and `$options`
|
|
47
|
+
- JSONB key existence: `$has_key`, `$has_any_keys`, `$has_all_keys`
|
|
48
|
+
- JSONPath: `$json_path_exists`, `$json_path_match`
|
|
49
|
+
|
|
50
|
+
See [`../query-translation.md`](../query-translation.md) for PostgreSQL operator/function mapping.
|
|
51
|
+
|
|
52
|
+
## Base-field Query Rules
|
|
53
|
+
|
|
54
|
+
Top-level base fields:
|
|
55
|
+
- `id`
|
|
56
|
+
- `created_at`
|
|
57
|
+
- `updated_at`
|
|
58
|
+
|
|
59
|
+
Rules:
|
|
60
|
+
- top-level base fields map to row columns
|
|
61
|
+
- dotted base-field paths are rejected
|
|
62
|
+
|
|
63
|
+
Operator matrix:
|
|
64
|
+
- `id`: direct equality, `$eq`, `$ne`, `$in`, `$nin`
|
|
65
|
+
- `created_at` / `updated_at`: direct equality, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`
|
|
66
|
+
- unsupported on base fields: `$regex`, `$contains`, `$all`, `$size`, `$has_key`, `$has_any_keys`, `$has_all_keys`, `$json_path_exists`, `$json_path_match`, `$elem_match`
|
|
67
|
+
|
|
68
|
+
## JSON Key Existence
|
|
69
|
+
|
|
70
|
+
- `$has_key` maps to PostgreSQL `?`
|
|
71
|
+
- `$has_any_keys` maps to PostgreSQL `?|`
|
|
72
|
+
- `$has_all_keys` maps to PostgreSQL `?&`
|
|
73
|
+
|
|
74
|
+
When used with a nested field path, JsonBadger extracts that nested JSONB value first, then applies the existence operator to the extracted value.
|
|
75
|
+
|
|
76
|
+
## JSONPath
|
|
77
|
+
|
|
78
|
+
- `$json_path_exists` maps to `@?`
|
|
79
|
+
- `$json_path_match` maps to `@@`
|
|
80
|
+
|
|
81
|
+
JsonBadger binds the JSONPath string as a `::jsonpath` parameter.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Schema API
|
|
2
|
+
|
|
3
|
+
## TOC
|
|
4
|
+
|
|
5
|
+
- [Schema API](#schema-api)
|
|
6
|
+
- [Constructor](#constructor)
|
|
7
|
+
- [Ownership Boundary](#ownership-boundary)
|
|
8
|
+
- [Core Methods](#core-methods)
|
|
9
|
+
- [Slug Access](#slug-access)
|
|
10
|
+
- [Casting and Validation](#casting-and-validation)
|
|
11
|
+
- [create_index](#create_index)
|
|
12
|
+
- [add_method](#add_method)
|
|
13
|
+
- [Path-level Index Sugar](#path-level-index-sugar)
|
|
14
|
+
|
|
15
|
+
## Constructor
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const user_schema = new JsonBadger.Schema(definition, options);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Ownership Boundary
|
|
22
|
+
|
|
23
|
+
`Schema` defines what a document looks like.
|
|
24
|
+
|
|
25
|
+
It is the right place for:
|
|
26
|
+
|
|
27
|
+
1. Field definitions
|
|
28
|
+
2. Validation and path introspection
|
|
29
|
+
3. Schema-declared indexes
|
|
30
|
+
4. Schema-level document methods
|
|
31
|
+
5. Slug ownership and validator domains
|
|
32
|
+
|
|
33
|
+
Use `Schema` to define document structure and behavior. Specify table names, connection details, and other storage options later when you register the model through `connection.model(...)`.
|
|
34
|
+
|
|
35
|
+
`definition` must be a plain JSON-like object:
|
|
36
|
+
- use object literals for nested schema branches
|
|
37
|
+
- use field definitions like `{type: String, required: true}`
|
|
38
|
+
- avoid custom class instances for schema definition objects
|
|
39
|
+
|
|
40
|
+
> **Note:** JsonBadger treats only default-prototype or null-prototype objects as plain schema objects. Custom instances are not parsed as nested schema branches.
|
|
41
|
+
|
|
42
|
+
## Core Methods
|
|
43
|
+
|
|
44
|
+
- `validate(document)`
|
|
45
|
+
- `validate_base_fields(document)`
|
|
46
|
+
- `cast(document)`
|
|
47
|
+
- `conform(document)`
|
|
48
|
+
- `get_path(path)`
|
|
49
|
+
- `get_path_type(path)`
|
|
50
|
+
- `is_array_root(path)`
|
|
51
|
+
- `create_index(index_definition)`
|
|
52
|
+
- `get_indexes()`
|
|
53
|
+
- `add_method(name, method_implementation)`
|
|
54
|
+
|
|
55
|
+
Base-field notes:
|
|
56
|
+
- `id`, `created_at`, and `updated_at` exist in schema/runtime introspection
|
|
57
|
+
- if user schema declares those base fields, user definitions are preserved
|
|
58
|
+
- `validate(document)` validates the full document envelope, not just the default slug
|
|
59
|
+
- when a schema registers extra root slugs through `schema.options.slugs`, validation fans out across those document roots
|
|
60
|
+
|
|
61
|
+
## Slug Access
|
|
62
|
+
|
|
63
|
+
Read slug ownership directly from the schema:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
const default_slug = schema.get_default_slug();
|
|
67
|
+
const extra_slugs = schema.get_extra_slugs();
|
|
68
|
+
const all_slugs = schema.get_slugs();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Rules:
|
|
72
|
+
- `get_default_slug()` returns the catch-all payload root
|
|
73
|
+
- `get_extra_slugs()` returns only explicitly registered sibling roots
|
|
74
|
+
- `get_slugs()` returns `[default_slug, ...extra_slugs]`
|
|
75
|
+
- duplicate extra slugs are collapsed during schema option normalization
|
|
76
|
+
|
|
77
|
+
## Casting and Validation
|
|
78
|
+
|
|
79
|
+
Validate a full document envelope:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
schema.validate({
|
|
83
|
+
id: '0194f028-579a-7b5b-8107-b9ad31395f43',
|
|
84
|
+
payload: {
|
|
85
|
+
name: 'maria'
|
|
86
|
+
},
|
|
87
|
+
settings: {
|
|
88
|
+
theme: 'dark'
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Validate only root base fields:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
schema.validate_base_fields({
|
|
97
|
+
id: '0194f028-579a-7b5b-8107-b9ad31395f43',
|
|
98
|
+
created_at: '2026-03-03T08:00:00.000Z',
|
|
99
|
+
updated_at: '2026-03-03T09:00:00.000Z'
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Cast a document envelope without validating or conforming:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const casted_document = schema.cast({
|
|
107
|
+
payload: {
|
|
108
|
+
age: '29'
|
|
109
|
+
},
|
|
110
|
+
settings: {
|
|
111
|
+
count: '7'
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Behavior:
|
|
117
|
+
- `schema.cast(...)` deep-clones the input first
|
|
118
|
+
- casts existing base-field, default-slug, and extra-slug values
|
|
119
|
+
- leaves missing paths untouched
|
|
120
|
+
- does not validate
|
|
121
|
+
- does not conform
|
|
122
|
+
|
|
123
|
+
Conform one document envelope to the schema-owned allowed tree:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
const document = {
|
|
127
|
+
payload: {
|
|
128
|
+
name: 'nell',
|
|
129
|
+
unknown_key: true
|
|
130
|
+
},
|
|
131
|
+
settings: {
|
|
132
|
+
theme: 'dark',
|
|
133
|
+
rogue: true
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
schema.conform(document);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
After conforming:
|
|
141
|
+
- known keys remain
|
|
142
|
+
- unknown keys are removed
|
|
143
|
+
|
|
144
|
+
## create_index
|
|
145
|
+
|
|
146
|
+
GIN single-path index:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
schema.create_index({using: 'gin', path: 'profile.city', name: 'idx_users_city_gin'});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
BTREE single-path index:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
schema.create_index({using: 'btree', path: 'age', order: -1, unique: true});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
BTREE compound index:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
schema.create_index({using: 'btree', paths: {name: 1, type: -1}, unique: true, name: 'idx_name_type'});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Rules:
|
|
165
|
+
- `using: 'gin'` requires `path`
|
|
166
|
+
- `using: 'gin'` ignores `order` and `unique`
|
|
167
|
+
- `using: 'btree'` requires `path` or `paths`
|
|
168
|
+
- `order` is only valid when using a single `path` with `using: 'btree'`
|
|
169
|
+
|
|
170
|
+
## add_method
|
|
171
|
+
|
|
172
|
+
Install a custom document-instance method onto models generated from the schema.
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
schema.add_method('full_name', function () {
|
|
176
|
+
const default_slug = this.constructor.schema.get_default_slug();
|
|
177
|
+
const payload = this.document[default_slug];
|
|
178
|
+
return payload.first_name + ' ' + payload.last_name;
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Rules:
|
|
183
|
+
- `name` must be a valid identifier
|
|
184
|
+
- `method_implementation` must be a function
|
|
185
|
+
- duplicate schema method names are rejected
|
|
186
|
+
- names that conflict with built-in document properties are rejected when the model is created
|
|
187
|
+
|
|
188
|
+
## Path-level Index Sugar
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
{
|
|
194
|
+
name: {type: String, index: true},
|
|
195
|
+
age: {type: Number, index: 1},
|
|
196
|
+
score: {type: Number, index: -1},
|
|
197
|
+
email: {type: String, index: {unique: true, name: 'idx_users_email_unique'}},
|
|
198
|
+
nick_name: {type: String, index: {using: 'gin'}}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Normalization notes:
|
|
203
|
+
- object-form `index` defaults to `btree` unless `using` says otherwise
|
|
204
|
+
- invalid or unsupported inline index values are ignored permissively
|