jsonbadger 0.5.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/LICENSE +21 -0
- package/README.md +114 -0
- package/docs/api.md +152 -0
- package/docs/examples.md +612 -0
- package/docs/local-integration-testing.md +17 -0
- package/docs/query-translation.md +98 -0
- package/index.js +2 -0
- package/package.json +58 -0
- package/src/connection/connect.js +56 -0
- package/src/connection/disconnect.js +16 -0
- package/src/connection/pool-store.js +46 -0
- package/src/connection/server-capabilities.js +59 -0
- package/src/constants/defaults.js +20 -0
- package/src/constants/id-strategies.js +29 -0
- package/src/debug/debug-logger.js +15 -0
- package/src/errors/query-error.js +23 -0
- package/src/errors/validation-error.js +23 -0
- package/src/field-types/base-field-type.js +140 -0
- package/src/field-types/builtins/advanced.js +365 -0
- package/src/field-types/builtins/index.js +585 -0
- package/src/field-types/registry.js +122 -0
- package/src/index.js +36 -0
- package/src/migration/ensure-index.js +155 -0
- package/src/migration/ensure-schema.js +16 -0
- package/src/migration/ensure-table.js +31 -0
- package/src/migration/schema-indexes-resolver.js +6 -0
- package/src/model/document-instance.js +540 -0
- package/src/model/model-factory.js +555 -0
- package/src/query/limit-skip-compiler.js +31 -0
- package/src/query/operators/all.js +10 -0
- package/src/query/operators/contains.js +7 -0
- package/src/query/operators/elem-match.js +3 -0
- package/src/query/operators/eq.js +6 -0
- package/src/query/operators/gt.js +16 -0
- package/src/query/operators/gte.js +16 -0
- package/src/query/operators/has-all-keys.js +11 -0
- package/src/query/operators/has-any-keys.js +11 -0
- package/src/query/operators/has-key.js +6 -0
- package/src/query/operators/in.js +12 -0
- package/src/query/operators/index.js +60 -0
- package/src/query/operators/jsonpath-exists.js +15 -0
- package/src/query/operators/jsonpath-match.js +15 -0
- package/src/query/operators/lt.js +16 -0
- package/src/query/operators/lte.js +16 -0
- package/src/query/operators/ne.js +6 -0
- package/src/query/operators/nin.js +12 -0
- package/src/query/operators/regex.js +8 -0
- package/src/query/operators/size.js +16 -0
- package/src/query/path-parser.js +43 -0
- package/src/query/query-builder.js +93 -0
- package/src/query/sort-compiler.js +30 -0
- package/src/query/where-compiler.js +477 -0
- package/src/schema/field-definition-parser.js +218 -0
- package/src/schema/path-introspection.js +82 -0
- package/src/schema/schema-compiler.js +212 -0
- package/src/schema/schema.js +234 -0
- package/src/sql/parameter-binder.js +13 -0
- package/src/sql/sql-runner.js +31 -0
- package/src/utils/array.js +31 -0
- package/src/utils/assert.js +27 -0
- package/src/utils/json-safe.js +9 -0
- package/src/utils/json.js +21 -0
- package/src/utils/object-path.js +33 -0
- package/src/utils/object.js +168 -0
- package/src/utils/value.js +30 -0
package/docs/examples.md
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
# jsonbadger examples cheat sheet
|
|
2
|
+
|
|
3
|
+
This page is an example-first cheat sheet for users and AI agents. It shows copy-pasteable jsonbadger usage in increasing complexity and covers the currently implemented query and update operator surface.
|
|
4
|
+
|
|
5
|
+
Use this page for syntax and working shapes. Use [`docs/api.md`](api.md) for API reference details and [`docs/query-translation.md`](query-translation.md) for PostgreSQL operator/function semantics.
|
|
6
|
+
|
|
7
|
+
## How to Read This Page
|
|
8
|
+
|
|
9
|
+
- Examples are ordered from setup -> model usage -> queries -> updates -> runtime behavior.
|
|
10
|
+
- Snippets reuse a `User` model shape where possible.
|
|
11
|
+
- Query operators use the current `$` + `snake_case` naming (for example `$elem_match`, `$has_key`, `$json_path_exists`).
|
|
12
|
+
- This page only includes currently implemented behavior.
|
|
13
|
+
- `Assumes:` notes tell you what earlier setup/data a snippet depends on.
|
|
14
|
+
- Important mechanics: queries run when you call `.exec()` (for example `await User.find({}).exec()`), and direct in-place mutations on nested `doc.data` values may require `doc.mark_modified('path')` after mutating.
|
|
15
|
+
|
|
16
|
+
Quick jump:
|
|
17
|
+
- [How to Read This Page](#how-to-read-this-page)
|
|
18
|
+
- [Shared Fixture and Assumptions](#shared-fixture-and-assumptions)
|
|
19
|
+
- [Setup and Connect](#setup-and-connect)
|
|
20
|
+
- [ID Strategy Examples](#id-strategy-examples)
|
|
21
|
+
- [Schema and Model Definition](#schema-and-model-definition)
|
|
22
|
+
- [Built-in FieldType Examples](#built-in-fieldtype-examples)
|
|
23
|
+
- [Create and Save Documents](#create-and-save-documents)
|
|
24
|
+
- [Query Basics (find, find_one, count)](#query-basics-find-find_one-count)
|
|
25
|
+
- [Direct Equality and Scalar Comparisons](#direct-equality-and-scalar-comparisons)
|
|
26
|
+
- [Regex Operators](#regex-operators)
|
|
27
|
+
- [Array and JSON Query Operators](#array-and-json-query-operators)
|
|
28
|
+
- [JSONB Key Existence Operators](#jsonb-key-existence-operators)
|
|
29
|
+
- [JSONPath Operators](#jsonpath-operators)
|
|
30
|
+
- [Update Operators (`update_one`)](#update-operators-update_one)
|
|
31
|
+
- [Delete Operations](#delete-operations)
|
|
32
|
+
- [Runtime Document Methods (get/set, dirty tracking, serialization)](#runtime-document-methods-getset-dirty-tracking-serialization)
|
|
33
|
+
- [Optional Alias Path Example](#optional-alias-path-example)
|
|
34
|
+
- [Complete Operator Checklist (Query and Update)](#complete-operator-checklist-query-and-update)
|
|
35
|
+
- [Related Docs](#related-docs)
|
|
36
|
+
|
|
37
|
+
## Shared Fixture and Assumptions
|
|
38
|
+
|
|
39
|
+
Most snippets below are intentionally short and not fully standalone. Unless a section says otherwise, examples assume:
|
|
40
|
+
|
|
41
|
+
- You already connected with `jsonbadger.connect(...)`.
|
|
42
|
+
- You defined the `User` model from `Schema and Model Definition`.
|
|
43
|
+
- You either ran manual migrations (`ensure_table()` / `ensure_schema()`) or allowed a first write to create the table.
|
|
44
|
+
- You seeded at least one `User` document using the `Create and Save Documents` example (including `tags`, `orders`, and `payload`).
|
|
45
|
+
|
|
46
|
+
When a snippet uses a different value (for example `user_name: 'jane'`), either seed a matching row first or replace the filter value with one that exists in your local data.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Setup and Connect
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import jsonbadger from 'jsonbadger';
|
|
53
|
+
|
|
54
|
+
await jsonbadger.connect(process.env.APP_POSTGRES_URI, {
|
|
55
|
+
debug: false,
|
|
56
|
+
max: 10,
|
|
57
|
+
ssl: false,
|
|
58
|
+
auto_index: true,
|
|
59
|
+
id_strategy: jsonbadger.IdStrategies.bigserial
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
UUIDv7 server default (PostgreSQL 18+ native `uuidv7()` required):
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
await jsonbadger.connect(process.env.APP_POSTGRES_URI, {
|
|
67
|
+
id_strategy: jsonbadger.IdStrategies.uuidv7
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Notes:
|
|
72
|
+
- jsonbadger checks native `uuidv7()` support automatically during `connect(...)` and caches the capability result.
|
|
73
|
+
- Reads do not auto-create tables. A first write (`save()` / `update_one(...)`) can create the table, or you can call `ensure_table()` / `ensure_schema()` explicitly.
|
|
74
|
+
|
|
75
|
+
## ID Strategy Examples
|
|
76
|
+
|
|
77
|
+
Server-wide default (`connect`) plus model override (`model(...)`):
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
await jsonbadger.connect(process.env.APP_POSTGRES_URI, {
|
|
81
|
+
id_strategy: jsonbadger.IdStrategies.uuidv7 // PostgreSQL 18+ native uuidv7() required
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const AuditLog = jsonbadger.model(new jsonbadger.Schema({
|
|
85
|
+
event_name: String
|
|
86
|
+
}), {
|
|
87
|
+
table_name: 'audit_logs',
|
|
88
|
+
// inherits server id_strategy: uuidv7
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const Counter = jsonbadger.model(new jsonbadger.Schema({
|
|
92
|
+
label: String
|
|
93
|
+
}), {
|
|
94
|
+
table_name: 'counters',
|
|
95
|
+
id_strategy: jsonbadger.IdStrategies.bigserial // model override
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Notes:
|
|
100
|
+
- `id_strategy` precedence is: model option -> connection option -> library default (`bigserial`).
|
|
101
|
+
- `IdStrategies.uuidv7` uses database-generated IDs (`DEFAULT uuidv7()`) and jsonbadger validates support internally.
|
|
102
|
+
|
|
103
|
+
## Schema and Model Definition
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const user_schema = new jsonbadger.Schema({
|
|
107
|
+
user_name: {type: String, required: true, trim: true, index: true, get: (value) => value && value.toUpperCase()},
|
|
108
|
+
age: {type: Number, min: 0, index: -1},
|
|
109
|
+
status: {type: String, immutable: true},
|
|
110
|
+
tags: [String],
|
|
111
|
+
profile: {
|
|
112
|
+
city: String,
|
|
113
|
+
country: String
|
|
114
|
+
},
|
|
115
|
+
orders: [{sku: String, qty: Number, price: Number}],
|
|
116
|
+
payload: {}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Schema-level indexes (single path and compound)
|
|
120
|
+
user_schema.create_index('profile.city');
|
|
121
|
+
user_schema.create_index({user_name: 1, age: -1});
|
|
122
|
+
|
|
123
|
+
const User = jsonbadger.model(user_schema, {
|
|
124
|
+
table_name: 'users',
|
|
125
|
+
data_column: 'data',
|
|
126
|
+
auto_index: true,
|
|
127
|
+
id_strategy: jsonbadger.IdStrategies.bigserial
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Migration helpers (manual/explicit path):
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
await User.ensure_table();
|
|
135
|
+
await User.ensure_index();
|
|
136
|
+
// or: ensure_schema() does both (table + declared schema indexes)
|
|
137
|
+
await User.ensure_schema();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Built-in FieldType Examples
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
const account_schema = new jsonbadger.Schema({
|
|
144
|
+
user_name: { type: String, trim: true, required: true },
|
|
145
|
+
age: { type: Number, min: 0 },
|
|
146
|
+
is_active: Boolean,
|
|
147
|
+
joined_at: Date,
|
|
148
|
+
owner_id: { type: 'UUIDv7' },
|
|
149
|
+
avatar: Buffer,
|
|
150
|
+
payload: {}, // Mixed
|
|
151
|
+
tags: [String],
|
|
152
|
+
handles: { type: Map, of: String },
|
|
153
|
+
amount: { type: 'Decimal128' },
|
|
154
|
+
visits_64: { type: 'BigInt' },
|
|
155
|
+
ratio: { type: 'Double' },
|
|
156
|
+
score_32: { type: 'Int32' },
|
|
157
|
+
code_or_label: { type: 'Union', of: [String, Number] }
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Edge cases and practical notes:
|
|
162
|
+
- In-place changes to `Mixed` (`{}`) values and `Date` objects do not mark the path dirty automatically; call `doc.mark_modified('path')` after mutating them.
|
|
163
|
+
- Arrays default to `[]`; set `default: undefined` to disable the implicit empty-array default.
|
|
164
|
+
- For `Map`-like paths, prefer `document.set('handles.github', 'name')` so casting and dirty tracking run.
|
|
165
|
+
- Serialization applies getters by default; use `doc.to_object({ getters: false })` or `doc.to_json({ getters: false })` to bypass them.
|
|
166
|
+
|
|
167
|
+
## Create and Save Documents
|
|
168
|
+
|
|
169
|
+
First write can create the table if it does not exist yet.
|
|
170
|
+
|
|
171
|
+
Assumes:
|
|
172
|
+
- `User` is defined from the schema/model section above.
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
const user_doc = new User({
|
|
176
|
+
user_name: 'john',
|
|
177
|
+
age: 30,
|
|
178
|
+
status: 'active',
|
|
179
|
+
tags: ['vip', 'beta'],
|
|
180
|
+
profile: {city: 'Miami', country: 'US'},
|
|
181
|
+
orders: [
|
|
182
|
+
{sku: 'A1', qty: 2, price: 20},
|
|
183
|
+
{sku: 'B2', qty: 1, price: 5}
|
|
184
|
+
],
|
|
185
|
+
payload: {
|
|
186
|
+
flags: ['vip', 'beta'],
|
|
187
|
+
score: 42,
|
|
188
|
+
profile: {city: 'Miami', country: 'US'},
|
|
189
|
+
items: [{sku: 'A1', qty: 2}, {sku: 'B2', qty: 0}],
|
|
190
|
+
text_value: 'abc'
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await user_doc.validate();
|
|
195
|
+
const saved_user = await user_doc.save();
|
|
196
|
+
// returns: saved data object (plain object); `user_doc.data` is updated to the saved payload
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Validation failure pattern (`validation_error`):
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
try {
|
|
203
|
+
const invalid_user = new User({
|
|
204
|
+
user_name: 'bad_input',
|
|
205
|
+
age: -1 // violates `min: 0` from the schema example
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await invalid_user.save();
|
|
209
|
+
} catch(error) {
|
|
210
|
+
if(error.name === 'validation_error') {
|
|
211
|
+
const error_payload = error.to_json();
|
|
212
|
+
// shape: { success: false, error: { type: 'validation_error', message: '...', details: ... } }
|
|
213
|
+
// `error.details` is the same validation detail payload included in `error_payload.error.details`
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Query Basics (find, find_one, count)
|
|
219
|
+
|
|
220
|
+
Assumes (for the query sections below):
|
|
221
|
+
- `User` is defined and at least one document was saved (see `## Create and Save Documents`).
|
|
222
|
+
- Examples that filter by `user_name: 'john'`, `tags`, `orders`, or `payload` assume those fields/values exist in seeded data.
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
const all_users = await User.find({}).exec();
|
|
227
|
+
// returns: [{ user_name: 'john', ... }, ...] (array of plain data objects)
|
|
228
|
+
|
|
229
|
+
const found_user = await User.find_one({user_name: 'john'}).exec();
|
|
230
|
+
// returns: { user_name: 'john', ... } or null (plain data object)
|
|
231
|
+
|
|
232
|
+
const adult_count = await User.count_documents({age: {$gte: 18}}).exec();
|
|
233
|
+
// returns: number
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Query builder chaining:
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
const page_of_users = await User.find({status: 'active'})
|
|
240
|
+
.where({'profile.country': 'US'})
|
|
241
|
+
.sort({age: -1})
|
|
242
|
+
.limit(20)
|
|
243
|
+
.skip(0)
|
|
244
|
+
.exec();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Read behavior note:
|
|
248
|
+
- `find(...).exec()`, `find_one(...).exec()`, and `count_documents(...).exec()` do not call `ensure_table()`.
|
|
249
|
+
|
|
250
|
+
## Direct Equality and Scalar Comparisons
|
|
251
|
+
|
|
252
|
+
Direct equality and explicit `$eq` both work:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
await User.find({user_name: 'john'}).exec();
|
|
256
|
+
await User.find({user_name: {$eq: 'john'}}).exec();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Scalar comparison operators:
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
await User.find({age: {$ne: 21}}).exec();
|
|
263
|
+
await User.find({age: {$gt: 18}}).exec();
|
|
264
|
+
await User.find({age: {$gte: 18}}).exec();
|
|
265
|
+
await User.find({age: {$lt: 65}}).exec();
|
|
266
|
+
await User.find({age: {$lte: 65}}).exec();
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Set membership operators:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
await User.find({status: {$in: ['active', 'pending']}}).exec();
|
|
273
|
+
await User.find({status: {$nin: ['disabled', 'banned']}}).exec();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Regex Operators
|
|
277
|
+
|
|
278
|
+
Regex literal shorthand:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
await User.find({user_name: /jo/i}).exec();
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`$regex` with `$options`:
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
await User.find({
|
|
288
|
+
user_name: {
|
|
289
|
+
$regex: '^jo',
|
|
290
|
+
$options: 'i'
|
|
291
|
+
}
|
|
292
|
+
}).exec();
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Array and JSON Query Operators
|
|
296
|
+
|
|
297
|
+
Array-root shorthand (for array fields) uses containment behavior:
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
await User.find({tags: 'vip'}).exec();
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`$all` and `$size`:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
await User.find({tags: {$all: ['vip', 'beta']}}).exec();
|
|
307
|
+
await User.find({tags: {$size: 2}}).exec();
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`$elem_match` on scalar arrays:
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
await User.find({
|
|
314
|
+
tags: {
|
|
315
|
+
$elem_match: {
|
|
316
|
+
$regex: '^v',
|
|
317
|
+
$options: 'i'
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}).exec();
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`$elem_match` on arrays of objects:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
await User.find({
|
|
327
|
+
orders: {
|
|
328
|
+
$elem_match: {
|
|
329
|
+
qty: {$gt: 1},
|
|
330
|
+
sku: {$regex: '^A'}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}).exec();
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
JSON containment with `$contains` (`@>` semantics):
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
await User.find({
|
|
340
|
+
payload: {$contains: {profile: {city: 'Miami'}}}
|
|
341
|
+
}).exec();
|
|
342
|
+
|
|
343
|
+
await User.find({
|
|
344
|
+
payload: {$contains: {flags: ['vip']}}
|
|
345
|
+
}).exec();
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## JSONB Key Existence Operators
|
|
349
|
+
|
|
350
|
+
Top-level existence on the selected JSONB value:
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
await User.find({payload: {$has_key: 'profile'}}).exec();
|
|
354
|
+
await User.find({payload: {$has_any_keys: ['profile', 'flags']}}).exec();
|
|
355
|
+
await User.find({payload: {$has_all_keys: ['profile', 'score']}}).exec();
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Nested-scope existence (jsonbadger extracts the nested JSONB value first, then applies top-level existence there):
|
|
359
|
+
|
|
360
|
+
```js
|
|
361
|
+
await User.find({'payload.profile': {$has_key: 'city'}}).exec();
|
|
362
|
+
|
|
363
|
+
// This checks for a literal key name "profile.city" at the top level of payload (usually false)
|
|
364
|
+
await User.find({payload: {$has_key: 'profile.city'}}).exec();
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## JSONPath Operators
|
|
368
|
+
|
|
369
|
+
`$json_path_exists` (`@?`) and `$json_path_match` (`@@`):
|
|
370
|
+
|
|
371
|
+
Assumes:
|
|
372
|
+
- Your seeded `payload` includes shapes like `items[*].qty` and `score` (as shown in `## Create and Save Documents`).
|
|
373
|
+
|
|
374
|
+
Target shape reminder:
|
|
375
|
+
- `payload` is a JSON object with keys like `score` and `items`, where `items` is an array of objects (for example `{qty: 2}`).
|
|
376
|
+
|
|
377
|
+
```js
|
|
378
|
+
await User.find({
|
|
379
|
+
payload: {$json_path_exists: '$.items[*] ? (@.qty > 1)'}
|
|
380
|
+
}).exec();
|
|
381
|
+
|
|
382
|
+
await User.find({
|
|
383
|
+
payload: {$json_path_match: '$.score > 10'}
|
|
384
|
+
}).exec();
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
Expected behavior:
|
|
389
|
+
- If your seeded payload does not include those paths/types, these examples return `[]` instead of failing.
|
|
390
|
+
|
|
391
|
+
## Update Operators (`update_one`)
|
|
392
|
+
|
|
393
|
+
`update_one(...)` supports `$set`, `$insert`, and `$set_lax`.
|
|
394
|
+
|
|
395
|
+
Assumes (for update and delete sections below):
|
|
396
|
+
- A matching row exists (examples use `user_name: 'john'` and `user_name: 'missing'`).
|
|
397
|
+
- The seeded row includes `tags` and `payload` fields from the create/save example.
|
|
398
|
+
|
|
399
|
+
Target shape reminder:
|
|
400
|
+
- `tags` is an array, `profile` is an object, and `payload.profile` / `payload.score` are nested JSON values in the seeded row.
|
|
401
|
+
|
|
402
|
+
Basic `$set` (maps to `jsonb_set(...)`):
|
|
403
|
+
|
|
404
|
+
```js
|
|
405
|
+
const updated_user = await User.update_one(
|
|
406
|
+
{user_name: 'john'},
|
|
407
|
+
{
|
|
408
|
+
$set: {
|
|
409
|
+
age: 31,
|
|
410
|
+
'profile.city': 'Orlando',
|
|
411
|
+
'payload.profile.state': 'FL'
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
// returns: updated data object (plain object) or null
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`$insert` (maps to `jsonb_insert(...)`) with numeric array index paths:
|
|
419
|
+
|
|
420
|
+
```js
|
|
421
|
+
await User.update_one({user_name: 'john'}, {
|
|
422
|
+
$insert: {
|
|
423
|
+
'tags.0': 'first_tag'
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await User.update_one({user_name: 'john'}, {
|
|
428
|
+
$insert: {
|
|
429
|
+
'tags.0': {
|
|
430
|
+
value: 'after_first',
|
|
431
|
+
insert_after: true
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`$set_lax` (maps to `jsonb_set_lax(...)`) object form:
|
|
438
|
+
|
|
439
|
+
Target shape reminder for `$set_lax`:
|
|
440
|
+
- `payload` is a JSON object; these examples update or delete nested keys inside `payload`.
|
|
441
|
+
|
|
442
|
+
```js
|
|
443
|
+
await User.update_one({user_name: 'john'}, {
|
|
444
|
+
$set_lax: {
|
|
445
|
+
'payload.cleanup_flag': {
|
|
446
|
+
value: null,
|
|
447
|
+
null_value_treatment: 'delete_key'
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`$set_lax` options example (`create_if_missing`, `null_value_treatment`):
|
|
454
|
+
|
|
455
|
+
```js
|
|
456
|
+
await User.update_one({user_name: 'john'}, {
|
|
457
|
+
$set_lax: {
|
|
458
|
+
'payload.archived_at': {
|
|
459
|
+
value: null,
|
|
460
|
+
create_if_missing: false,
|
|
461
|
+
null_value_treatment: 'return_target'
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Multiple update operators in one call (application order is `$set` -> `$insert` -> `$set_lax`):
|
|
468
|
+
|
|
469
|
+
```js
|
|
470
|
+
await User.update_one({user_name: 'john'}, {
|
|
471
|
+
$set: {
|
|
472
|
+
'payload.score': 50
|
|
473
|
+
},
|
|
474
|
+
$insert: {
|
|
475
|
+
'tags.0': {value: 'vip', insert_after: true}
|
|
476
|
+
},
|
|
477
|
+
$set_lax: {
|
|
478
|
+
'payload.cleanup_flag': {
|
|
479
|
+
value: null,
|
|
480
|
+
null_value_treatment: 'delete_key'
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Conflict rule (rejected before SQL):
|
|
487
|
+
|
|
488
|
+
```js
|
|
489
|
+
await User.update_one({user_name: 'john'}, {
|
|
490
|
+
$set: {
|
|
491
|
+
payload: {nested: true}
|
|
492
|
+
},
|
|
493
|
+
$set_lax: {
|
|
494
|
+
'payload.value': 'ok'
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
// throws: conflicting update paths (same path or parent/child overlap)
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Delete Operations
|
|
501
|
+
|
|
502
|
+
`delete_one(...)` deletes one matching row and returns the deleted document data.
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
const deleted_user = await User.delete_one({user_name: 'john'});
|
|
506
|
+
// returns: deleted data object (plain object) or null
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
No match (or missing table) returns `null`:
|
|
510
|
+
|
|
511
|
+
```js
|
|
512
|
+
const maybe_deleted = await User.delete_one({user_name: 'missing'});
|
|
513
|
+
// returns: null when no row matches (or when the table does not exist yet)
|
|
514
|
+
if(maybe_deleted === null) {
|
|
515
|
+
// no matching row, or table does not exist yet
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Runtime Document Methods (get/set, dirty tracking, serialization)
|
|
520
|
+
|
|
521
|
+
Runtime getters/setters and dirty tracking:
|
|
522
|
+
|
|
523
|
+
Assumes:
|
|
524
|
+
- `User` is defined from the schema/model example and includes the `user_name` getter configuration shown there.
|
|
525
|
+
- This snippet demonstrates in-memory runtime behavior; no database read is required until `save()`.
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
```js
|
|
529
|
+
const doc = new User({
|
|
530
|
+
user_name: 'jane',
|
|
531
|
+
status: 'active',
|
|
532
|
+
payload: {count: 1},
|
|
533
|
+
profile: {city: 'Miami'}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// normal path set/get
|
|
537
|
+
|
|
538
|
+
doc.set('user_name', ' jane_doe ');
|
|
539
|
+
const upper_name = doc.get('user_name');
|
|
540
|
+
|
|
541
|
+
doc.set('profile.city', 'Orlando');
|
|
542
|
+
const city = doc.get('profile.city');
|
|
543
|
+
|
|
544
|
+
// dirty tracking
|
|
545
|
+
const before_dirty = doc.is_modified('payload');
|
|
546
|
+
doc.data.payload.count += 1;
|
|
547
|
+
if(!doc.is_modified('payload')) {
|
|
548
|
+
doc.mark_modified('payload');
|
|
549
|
+
}
|
|
550
|
+
const after_dirty = doc.is_modified('payload');
|
|
551
|
+
|
|
552
|
+
doc.clear_modified();
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Optional Alias Path Example
|
|
556
|
+
|
|
557
|
+
Use aliases only when you need an alternate path name (for example, a short path shortcut for a nested field).
|
|
558
|
+
|
|
559
|
+
```js
|
|
560
|
+
const alias_schema = new jsonbadger.Schema({
|
|
561
|
+
profile: {
|
|
562
|
+
city: {type: String, alias: 'city'}
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const AliasUser = jsonbadger.model('alias_users', alias_schema);
|
|
567
|
+
const alias_doc = new AliasUser({profile: {city: 'Miami'}});
|
|
568
|
+
|
|
569
|
+
// alias path -> underlying path
|
|
570
|
+
alias_doc.get('city'); // 'Miami'
|
|
571
|
+
alias_doc.set('city', 'Orlando');
|
|
572
|
+
alias_doc.get('profile.city'); // 'Orlando'
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Aliases are path aliases for `doc.get(...)`, `doc.set(...)`, and `doc.mark_modified(...)`. They do not create direct properties (for example, `alias_doc.city`).
|
|
576
|
+
|
|
577
|
+
Serialization helpers (`to_object`, `to_json`) apply getters by default:
|
|
578
|
+
|
|
579
|
+
```js
|
|
580
|
+
const as_object = doc.to_object();
|
|
581
|
+
const as_object_without_getters = doc.to_object({getters: false});
|
|
582
|
+
const as_json_ready = doc.to_json();
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Immutable behavior (`immutable: true`) after first persist:
|
|
586
|
+
|
|
587
|
+
```js
|
|
588
|
+
await doc.save();
|
|
589
|
+
|
|
590
|
+
// after successful save, immutable fields reject updates
|
|
591
|
+
// doc.set('status', 'disabled'); // throws
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## Complete Operator Checklist (Query and Update)
|
|
595
|
+
|
|
596
|
+
This page includes examples for:
|
|
597
|
+
- direct equality and `$eq`
|
|
598
|
+
- `$ne`, `$gt`, `$gte`, `$lt`, `$lte`
|
|
599
|
+
- `$in`, `$nin`
|
|
600
|
+
- `$regex` + `$options` (and RegExp literal shorthand)
|
|
601
|
+
- `$all`, `$size`, `$elem_match`
|
|
602
|
+
- `$contains`
|
|
603
|
+
- `$has_key`, `$has_any_keys`, `$has_all_keys`
|
|
604
|
+
- `$json_path_exists`, `$json_path_match`
|
|
605
|
+
- `update_one.$set`, `update_one.$insert`, `update_one.$set_lax`
|
|
606
|
+
|
|
607
|
+
## Related Docs
|
|
608
|
+
|
|
609
|
+
- [`README.md`](../README.md) (quick start + selected examples)
|
|
610
|
+
- [`docs/api.md`](api.md) (API surface and behavior notes)
|
|
611
|
+
- [`docs/query-translation.md`](query-translation.md) (PostgreSQL operator/function mapping)
|
|
612
|
+
- [`docs/local-integration-testing.md`](local-integration-testing.md) (local integration test setup)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# local postgres testing
|
|
2
|
+
|
|
3
|
+
1. copy `.env.example` to `.env`.
|
|
4
|
+
2. update `.env` values for your local PostgreSQL user/database.
|
|
5
|
+
3. run `npm run test-integration`.
|
|
6
|
+
4. run `npm run test-all` to run unit + local integration suites.
|
|
7
|
+
|
|
8
|
+
environment variables
|
|
9
|
+
- `APP_POSTGRES_URI`: required full PostgreSQL connection URI.
|
|
10
|
+
- `APP_POSTGRES_SSL`: `true` or `false`.
|
|
11
|
+
- `APP_DEBUG`: `true` or `false`.
|
|
12
|
+
- `APP_POOL_MAX`: max pool size.
|
|
13
|
+
|
|
14
|
+
notes
|
|
15
|
+
- the local integration test generates a unique table name per run and drops it afterward.
|
|
16
|
+
- this keeps table creation checks in-scope and avoids static table-name config in `.env`.
|
|
17
|
+
- `npm test` only runs unit tests.
|