jsonbadger 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +36 -18
  2. package/docs/api/connection.md +144 -0
  3. package/docs/api/delta-tracker.md +106 -0
  4. package/docs/api/document.md +77 -0
  5. package/docs/api/field-types.md +329 -0
  6. package/docs/api/index.md +35 -0
  7. package/docs/api/model.md +392 -0
  8. package/docs/api/query-builder.md +81 -0
  9. package/docs/api/schema.md +204 -0
  10. package/docs/architecture-flow.md +397 -0
  11. package/docs/examples.md +495 -218
  12. package/docs/jsonb-ops.md +171 -0
  13. package/docs/lifecycle/model-compilation.md +111 -0
  14. package/docs/lifecycle.md +146 -0
  15. package/docs/query-translation.md +11 -10
  16. package/package.json +10 -3
  17. package/src/connection/connect.js +12 -17
  18. package/src/connection/connection.js +128 -0
  19. package/src/connection/server-capabilities.js +60 -59
  20. package/src/constants/defaults.js +32 -19
  21. package/src/constants/{id-strategies.js → id-strategy.js} +28 -29
  22. package/src/constants/intake-mode.js +8 -0
  23. package/src/debug/debug-logger.js +17 -15
  24. package/src/errors/model-overwrite-error.js +25 -0
  25. package/src/errors/query-error.js +25 -23
  26. package/src/errors/validation-error.js +25 -23
  27. package/src/field-types/base-field-type.js +137 -140
  28. package/src/field-types/builtins/advanced.js +365 -365
  29. package/src/field-types/builtins/index.js +579 -585
  30. package/src/field-types/field-type-namespace.js +9 -0
  31. package/src/field-types/registry.js +149 -122
  32. package/src/index.js +26 -36
  33. package/src/migration/ensure-index.js +157 -154
  34. package/src/migration/ensure-schema.js +27 -15
  35. package/src/migration/ensure-table.js +44 -31
  36. package/src/migration/schema-indexes-resolver.js +8 -6
  37. package/src/model/document-instance.js +29 -540
  38. package/src/model/document.js +60 -0
  39. package/src/model/factory/constants.js +36 -0
  40. package/src/model/factory/index.js +58 -0
  41. package/src/model/model.js +875 -0
  42. package/src/model/operations/delete-one.js +39 -0
  43. package/src/model/operations/insert-one.js +35 -0
  44. package/src/model/operations/query-builder.js +132 -0
  45. package/src/model/operations/update-one.js +333 -0
  46. package/src/model/state.js +34 -0
  47. package/src/schema/field-definition-parser.js +213 -218
  48. package/src/schema/path-introspection.js +87 -82
  49. package/src/schema/schema-compiler.js +126 -212
  50. package/src/schema/schema.js +621 -138
  51. package/src/sql/index.js +17 -0
  52. package/src/sql/jsonb/ops.js +153 -0
  53. package/src/{query → sql/jsonb}/path-parser.js +54 -43
  54. package/src/sql/jsonb/read/elem-match.js +133 -0
  55. package/src/{query → sql/jsonb/read}/operators/contains.js +13 -7
  56. package/src/sql/jsonb/read/operators/elem-match.js +9 -0
  57. package/src/{query → sql/jsonb/read}/operators/has-all-keys.js +17 -11
  58. package/src/{query → sql/jsonb/read}/operators/has-any-keys.js +18 -11
  59. package/src/sql/jsonb/read/operators/has-key.js +12 -0
  60. package/src/{query → sql/jsonb/read}/operators/jsonpath-exists.js +22 -15
  61. package/src/{query → sql/jsonb/read}/operators/jsonpath-match.js +22 -15
  62. package/src/{query → sql/jsonb/read}/operators/size.js +23 -16
  63. package/src/sql/parameter-binder.js +18 -13
  64. package/src/sql/read/build-count-query.js +12 -0
  65. package/src/sql/read/build-find-query.js +25 -0
  66. package/src/sql/read/limit-skip.js +21 -0
  67. package/src/sql/read/sort.js +85 -0
  68. package/src/sql/read/where/base-fields.js +310 -0
  69. package/src/sql/read/where/casting.js +90 -0
  70. package/src/sql/read/where/context.js +79 -0
  71. package/src/sql/read/where/field-clause.js +58 -0
  72. package/src/sql/read/where/index.js +38 -0
  73. package/src/sql/read/where/operator-entries.js +29 -0
  74. package/src/{query → sql/read/where}/operators/all.js +16 -10
  75. package/src/sql/read/where/operators/eq.js +12 -0
  76. package/src/{query → sql/read/where}/operators/gt.js +23 -16
  77. package/src/{query → sql/read/where}/operators/gte.js +23 -16
  78. package/src/{query → sql/read/where}/operators/in.js +18 -12
  79. package/src/sql/read/where/operators/index.js +40 -0
  80. package/src/{query → sql/read/where}/operators/lt.js +23 -16
  81. package/src/{query → sql/read/where}/operators/lte.js +23 -16
  82. package/src/sql/read/where/operators/ne.js +12 -0
  83. package/src/{query → sql/read/where}/operators/nin.js +18 -12
  84. package/src/{query → sql/read/where}/operators/regex.js +14 -8
  85. package/src/sql/read/where/operators.js +126 -0
  86. package/src/sql/read/where/text-operators.js +83 -0
  87. package/src/sql/run.js +46 -0
  88. package/src/sql/write/build-delete-query.js +33 -0
  89. package/src/sql/write/build-insert-query.js +42 -0
  90. package/src/sql/write/build-update-query.js +65 -0
  91. package/src/utils/assert.js +34 -27
  92. package/src/utils/delta-tracker/.archive/1 tracker-redesign-codex-v2.md +250 -0
  93. package/src/utils/delta-tracker/.archive/1 tracker-redesign-gemini.md +101 -0
  94. package/src/utils/delta-tracker/.archive/2 evaluation by gemini.txt +65 -0
  95. package/src/utils/delta-tracker/.archive/2 evaluation by grok.txt +39 -0
  96. package/src/utils/delta-tracker/.archive/3 gemini evaluate grok.txt +37 -0
  97. package/src/utils/delta-tracker/.archive/3 grok evaluate gemini.txt +63 -0
  98. package/src/utils/delta-tracker/.archive/4 gemini veredict.txt +16 -0
  99. package/src/utils/delta-tracker/.archive/index.1.js +587 -0
  100. package/src/utils/delta-tracker/.archive/index.2.js +612 -0
  101. package/src/utils/delta-tracker/index.js +592 -0
  102. package/src/utils/dirty-tracker/inline.js +335 -0
  103. package/src/utils/dirty-tracker/instance.js +414 -0
  104. package/src/utils/dirty-tracker/static.js +343 -0
  105. package/src/utils/json-safe.js +13 -9
  106. package/src/utils/object-path.js +227 -33
  107. package/src/utils/object.js +408 -168
  108. package/src/utils/string.js +55 -0
  109. package/src/utils/value.js +169 -30
  110. package/docs/api.md +0 -152
  111. package/src/connection/disconnect.js +0 -16
  112. package/src/connection/pool-store.js +0 -46
  113. package/src/model/model-factory.js +0 -555
  114. package/src/query/limit-skip-compiler.js +0 -31
  115. package/src/query/operators/elem-match.js +0 -3
  116. package/src/query/operators/eq.js +0 -6
  117. package/src/query/operators/has-key.js +0 -6
  118. package/src/query/operators/index.js +0 -60
  119. package/src/query/operators/ne.js +0 -6
  120. package/src/query/query-builder.js +0 -93
  121. package/src/query/sort-compiler.js +0 -30
  122. package/src/query/where-compiler.js +0 -477
  123. package/src/sql/sql-runner.js +0 -31
package/docs/examples.md CHANGED
@@ -1,110 +1,201 @@
1
- # jsonbadger examples cheat sheet
1
+ # JsonBadger examples cheat sheet
2
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.
3
+ JsonBadger is a PostgreSQL-backed document mapper for working with JSONB data through a model/document API.
4
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.
5
+ 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.
6
+
7
+ Use this page for syntax and working shapes. Use [`docs/api/index.md`](api/index.md) for the module API map and [`docs/query-translation.md`](query-translation.md) for PostgreSQL operator/function semantics.
8
+ Use [`docs/lifecycle.md`](lifecycle.md) when you need the document phase map instead of operator syntax.
6
9
 
7
10
  ## How to Read This Page
8
11
 
9
12
  - Examples are ordered from setup -> model usage -> queries -> updates -> runtime behavior.
10
13
  - 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)
14
+ - Query operators use the current `$` + `snake_case` naming (for example `$elem_match`, `$has_key`, `$json_path_exists`).
15
+ - This page only includes currently implemented behavior.
16
+ - `Assumes:` notes tell you what earlier setup/data a snippet depends on.
17
+ - Important mechanics: queries run when you call `.exec()` (for example `await User.find({}).exec()`), and direct in-place mutations on `doc.document[...]` bypass `doc.set(...)` and assignment-time casting.
18
+ - Operation scope: this page covers `create`, `save`, `find`, `find_one`, `find_by_id`, `count_documents`, `update_one`, and `delete_one`. Bulk helpers (`insert_many`, `update_many`, `delete_many`) are not part of the current API surface.
19
+
20
+ Quick jump:
21
+
22
+ Setup
23
+ - [How to Read This Page](#how-to-read-this-page)
24
+ - [Shared Fixture and Assumptions](#shared-fixture-and-assumptions)
25
+ - [Reserved Base Fields](#reserved-base-fields)
26
+ - [Setup and Connect](#setup-and-connect)
27
+ - [ID Strategy Examples](#id-strategy-examples)
28
+ - [Schema and Model Definition](#schema-and-model-definition)
29
+ - [Built-in FieldType Examples](#built-in-fieldtype-examples)
30
+ - [Create and Save Documents](#create-and-save-documents)
31
+
32
+ Queries
33
+ - [Query Basics (find, find_one, count_documents)](#query-basics-find-find_one-count_documents)
34
+ - [Direct Equality and Scalar Comparisons](#direct-equality-and-scalar-comparisons)
35
+ - [Regex Operators](#regex-operators)
36
+ - [Array and JSON Query Operators](#array-and-json-query-operators)
37
+ - [JSONB Key Existence Operators](#jsonb-key-existence-operators)
38
+ - [JSONPath Operators](#jsonpath-operators)
39
+
40
+ Mutations
41
+ - [Update Operators (`update_one`)](#update-operators-update_one)
42
+ - [Delete Operations](#delete-operations)
43
+
44
+ Advanced
45
+ - [Runtime Document Methods (get/set, dirty tracking, serialization)](#runtime-document-methods-getset-dirty-tracking-serialization)
46
+ - [Lifecycle Quick Reference](#lifecycle-quick-reference)
47
+ - [Optional Alias Path Example](#optional-alias-path-example)
48
+ - [Complete Operator Checklist (Query and Update)](#complete-operator-checklist-query-and-update)
49
+ - [Related Docs](#related-docs)
36
50
 
37
51
  ## Shared Fixture and Assumptions
38
52
 
39
53
  Most snippets below are intentionally short and not fully standalone. Unless a section says otherwise, examples assume:
40
54
 
41
- - You already connected with `jsonbadger.connect(...)`.
55
+ - You are running ESM (`import ...`) with top-level `await` enabled.
56
+ - You already connected with `JsonBadger.connect(...)`.
42
57
  - 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.
58
+ - You already ran startup/bootstrap setup from the migration section below.
44
59
  - You seeded at least one `User` document using the `Create and Save Documents` example (including `tags`, `orders`, and `payload`).
45
60
 
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.
61
+ When a snippet uses a different value (for example `name: 'jane'`), either seed a matching row first or replace the filter value with one that exists in your local data.
62
+
63
+
64
+ ## Reserved Base Fields
65
+
66
+ - `id`, `created_at`, and `updated_at` are top-level base fields returned in data-returning operations.
67
+ - These base fields exist in schema/runtime introspection even when omitted from user schema input.
68
+ - If user schema declares any of these paths, the user definition is preserved.
69
+ - Query/sort support for base fields is top-level only (no dotted base-field paths like `created_at.value`).
70
+
71
+ `id` behavior:
72
+ - Create with `IdStrategies.uuidv7`:
73
+ - caller-provided `id` is used.
74
+ - if `id` is omitted, PostgreSQL default `uuidv7()` generates it.
75
+ - Create with `IdStrategies.bigserial`:
76
+ - caller-provided `id` is ignored silently.
77
+ - PostgreSQL sequence generates it.
78
+ - Update paths cannot mutate `id`.
47
79
 
80
+ Timestamp helper behavior:
81
+ - Create:
82
+ - provided `created_at` / `updated_at` values are kept.
83
+ - omitted values are auto-filled.
84
+ - Update:
85
+ - provided `updated_at` is kept.
86
+ - omitted `updated_at` is auto-set.
87
+ - `created_at` is not auto-updated.
48
88
 
49
89
  ## Setup and Connect
50
90
 
91
+ > Important: `IdStrategies.uuidv7` requires native PostgreSQL `uuidv7()` support (PostgreSQL 18+).
92
+
51
93
  ```js
52
- import jsonbadger from 'jsonbadger';
94
+ import JsonBadger from 'jsonbadger';
53
95
 
54
- await jsonbadger.connect(process.env.APP_POSTGRES_URI, {
96
+ const db_uri = 'postgresql://user:pass@localhost:5432/dbname';
97
+ const options = {
55
98
  debug: false,
56
99
  max: 10,
57
100
  ssl: false,
58
101
  auto_index: true,
59
- id_strategy: jsonbadger.IdStrategies.bigserial
60
- });
102
+ id_strategy: JsonBadger.IdStrategies.bigserial
103
+ };
104
+
105
+ const connection = await JsonBadger.connect(db_uri, options);
61
106
  ```
62
107
 
63
108
  UUIDv7 server default (PostgreSQL 18+ native `uuidv7()` required):
64
109
 
65
110
  ```js
66
- await jsonbadger.connect(process.env.APP_POSTGRES_URI, {
67
- id_strategy: jsonbadger.IdStrategies.uuidv7
68
- });
111
+ const db_uri = 'postgresql://user:pass@localhost:5432/dbname';
112
+ const options = {
113
+ id_strategy: JsonBadger.IdStrategies.uuidv7
114
+ };
115
+
116
+ const connection = await JsonBadger.connect(db_uri, options);
69
117
  ```
70
118
 
71
119
  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.
120
+ - JsonBadger checks native `uuidv7()` support automatically during `connect(...)` and caches the capability result.
121
+ - Run `ensure_table()` / `ensure_indexes()` during startup/bootstrap before normal runtime operations, or use `ensure_model()` when you want the combined path.
74
122
 
75
- ## ID Strategy Examples
123
+ Teardown example:
76
124
 
77
- Server-wide default (`connect`) plus model override (`model(...)`):
125
+ ```js
126
+ await connection.disconnect();
127
+ ```
128
+
129
+ Connection failure pattern (before a pool is established in the process):
78
130
 
79
131
  ```js
80
- await jsonbadger.connect(process.env.APP_POSTGRES_URI, {
81
- id_strategy: jsonbadger.IdStrategies.uuidv7 // PostgreSQL 18+ native uuidv7() required
82
- });
132
+ try {
133
+ await JsonBadger.connect('postgresql://wrong_user:wrong_pass@localhost:5432/dbname');
134
+ } catch(error) {
135
+ // connection/auth/network failures bubble directly from `pg`
136
+ console.error(error.message);
137
+ }
138
+ ```
139
+
140
+ ## ID Strategy Examples
83
141
 
84
- const AuditLog = jsonbadger.model(new jsonbadger.Schema({
85
- event_name: String
86
- }), {
87
- table_name: 'audit_logs',
88
- // inherits server id_strategy: uuidv7
142
+ Schema-level `id_strategy` selection:
143
+
144
+ ```js
145
+ const db_uri = 'postgresql://user:pass@localhost:5432/dbname';
146
+ const connection = await JsonBadger.connect(db_uri);
147
+
148
+ const AuditLog = connection.model({
149
+ name: 'AuditLog',
150
+ schema: new JsonBadger.Schema({
151
+ event_name: String
152
+ }, {
153
+ id_strategy: JsonBadger.IdStrategies.uuidv7 // PostgreSQL 18+ native uuidv7() required
154
+ }),
155
+ table_name: 'audit_logs'
89
156
  });
90
157
 
91
- const Counter = jsonbadger.model(new jsonbadger.Schema({
92
- label: String
93
- }), {
94
- table_name: 'counters',
95
- id_strategy: jsonbadger.IdStrategies.bigserial // model override
158
+ const Counter = connection.model({
159
+ name: 'Counter',
160
+ schema: new JsonBadger.Schema({
161
+ label: String
162
+ }, {
163
+ id_strategy: JsonBadger.IdStrategies.bigserial // schema override
164
+ }),
165
+ table_name: 'counters'
96
166
  });
97
167
  ```
98
168
 
99
169
  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.
170
+ - `id_strategy` lives on the schema. If omitted, the library default is `bigserial`.
171
+ - `IdStrategies.uuidv7` uses database-generated IDs (`DEFAULT uuidv7()`) and JsonBadger validates support internally.
172
+
173
+ Create-time `id` behavior example:
174
+
175
+ ```js
176
+ // uuidv7 model: pass-through when provided
177
+ const uuid_user = await AuditLog.create({
178
+ id: '0194f028-579a-7b5b-8107-b9ad31395f43',
179
+ event_name: 'login'
180
+ });
181
+
182
+ // uuidv7 model: DB generates when omitted
183
+ const generated_uuid_user = await AuditLog.create({
184
+ event_name: 'logout'
185
+ });
186
+
187
+ // bigserial model: caller id is ignored, DB generates numeric id
188
+ const serial_counter = await Counter.create({
189
+ id: 999,
190
+ label: 'requests'
191
+ });
192
+ ```
102
193
 
103
194
  ## Schema and Model Definition
104
195
 
105
196
  ```js
106
- const user_schema = new jsonbadger.Schema({
107
- user_name: {type: String, required: true, trim: true, index: true, get: (value) => value && value.toUpperCase()},
197
+ const user_schema = new JsonBadger.Schema({
198
+ name: {type: String, required: true, trim: true, index: true, get: (value) => value && value.toUpperCase()},
108
199
  age: {type: Number, min: 0, index: -1},
109
200
  status: {type: String, immutable: true},
110
201
  tags: [String],
@@ -112,36 +203,40 @@ const user_schema = new jsonbadger.Schema({
112
203
  city: String,
113
204
  country: String
114
205
  },
115
- orders: [{sku: String, qty: Number, price: Number}],
116
- payload: {}
117
- });
206
+ orders: [{sku: String, qty: Number, price: Number}],
207
+ payload: {}
208
+ });
118
209
 
119
210
  // Schema-level indexes (single path and compound)
120
- user_schema.create_index('profile.city');
121
- user_schema.create_index({user_name: 1, age: -1});
211
+ user_schema.create_index({using: 'gin', path: 'profile.city'});
212
+ user_schema.create_index({using: 'btree', paths: {name: 1, age: -1}});
122
213
 
123
- const User = jsonbadger.model(user_schema, {
214
+ const connection = await JsonBadger.connect(db_uri, options);
215
+ const User = connection.model({
216
+ name: 'User',
217
+ schema: user_schema,
124
218
  table_name: 'users',
125
- data_column: 'data',
126
219
  auto_index: true,
127
- id_strategy: jsonbadger.IdStrategies.bigserial
220
+ id_strategy: JsonBadger.IdStrategies.bigserial
128
221
  });
129
222
  ```
130
223
 
131
224
  Migration helpers (manual/explicit path):
132
225
 
133
226
  ```js
227
+ // Option A: run table and indexes explicitly
134
228
  await User.ensure_table();
135
- await User.ensure_index();
136
- // or: ensure_schema() does both (table + declared schema indexes)
137
- await User.ensure_schema();
229
+ await User.ensure_indexes();
230
+
231
+ // Option B: run the combined model bootstrap path
232
+ await User.ensure_model();
138
233
  ```
139
234
 
140
235
  ## Built-in FieldType Examples
141
236
 
142
237
  ```js
143
- const account_schema = new jsonbadger.Schema({
144
- user_name: { type: String, trim: true, required: true },
238
+ const account_schema = new JsonBadger.Schema({
239
+ name: { type: String, trim: true, required: true },
145
240
  age: { type: Number, min: 0 },
146
241
  is_active: Boolean,
147
242
  joined_at: Date,
@@ -158,22 +253,38 @@ const account_schema = new jsonbadger.Schema({
158
253
  });
159
254
  ```
160
255
 
256
+ Quick FieldType reference:
257
+
258
+ | Field | FieldType | Example |
259
+ | --- | --- | --- |
260
+ | `name` | `String` | `{ type: String, trim: true, required: true }` |
261
+ | `age` | `Number` | `{ type: Number, min: 0 }` |
262
+ | `is_active` | `Boolean` | `is_active: Boolean` |
263
+ | `joined_at` | `Date` | `joined_at: Date` |
264
+ | `owner_id` | `UUIDv7` | `{ type: 'UUIDv7' }` |
265
+ | `avatar` | `Buffer` | `avatar: Buffer` |
266
+ | `payload` | `Mixed` | `payload: {}` |
267
+ | `tags` | `Array<String>` | `tags: [String]` |
268
+ | `handles` | `Map<String>` | `{ type: Map, of: String }` |
269
+ | `amount` | `Decimal128` | `{ type: 'Decimal128' }` |
270
+ | `visits_64` | `BigInt` | `{ type: 'BigInt' }` |
271
+ | `ratio` | `Double` | `{ type: 'Double' }` |
272
+ | `score_32` | `Int32` | `{ type: 'Int32' }` |
273
+ | `code_or_label` | `Union<String, Number>` | `{ type: 'Union', of: [String, Number] }` |
274
+
161
275
  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.
276
+ - In-place changes to nested values under `doc.document[...]` bypass `doc.set(...)`; prefer `doc.set(...)` when you want assignment-time casting and setter logic.
163
277
  - Arrays default to `[]`; set `default: undefined` to disable the implicit empty-array default.
164
278
  - 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
279
 
167
280
  ## Create and Save Documents
168
281
 
169
- First write can create the table if it does not exist yet.
170
-
171
282
  Assumes:
172
283
  - `User` is defined from the schema/model section above.
173
284
 
174
285
  ```js
175
- const user_doc = new User({
176
- user_name: 'john',
286
+ const user_doc = User.from({
287
+ name: 'john',
177
288
  age: 30,
178
289
  status: 'active',
179
290
  tags: ['vip', 'beta'],
@@ -191,47 +302,159 @@ const user_doc = new User({
191
302
  }
192
303
  });
193
304
 
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
- ```
305
+ const saved_user = await user_doc.save();
306
+ ```
307
+
308
+ Returns: `saved_user` is a `User` document instance.
309
+ Raw document shape: `saved_user.document` -> `{ id, data, created_at, updated_at }` when the default slug is still `data`.
310
+ Runtime note: read the default slug through `saved_user.document[User.schema.get_default_slug()]`.
311
+
312
+ Static create and id lookup:
313
+
314
+ ```js
315
+ const created_user = await User.create({
316
+ name: 'maria',
317
+ age: 29
318
+ });
319
+
320
+ const same_user = await User.find_by_id(created_user.id).exec();
321
+ ```
322
+
323
+ Build a new document from external input without marking it persisted:
324
+
325
+ ```js
326
+ const imported_user = User.from({
327
+ name: ' maria ',
328
+ age: '29',
329
+ created_at: '2026-03-03T08:00:00.000Z'
330
+ });
331
+ ```
332
+
333
+ `User.from(...)` extracts root `id`, `created_at`, and `updated_at` into base fields and treats every other non-base root key as default-slug payload unless that root key is registered in `schema.options.slugs`. It does not interpret payload input as a persisted row envelope.
334
+
335
+ Set base fields after construction when needed:
336
+
337
+ ```js
338
+ const imported_user = User.from({
339
+ name: 'maria'
340
+ });
341
+
342
+ imported_user.id = '7';
343
+ imported_user.created_at = '2026-03-03T08:00:00.000Z';
344
+ imported_user.updated_at = '2026-03-03T09:00:00.000Z';
345
+ ```
346
+
347
+ Hydrate a persisted document from raw row-like data:
348
+
349
+ ```js
350
+ const hydrated_user = User.hydrate({
351
+ id: '7',
352
+ data: {
353
+ name: 'maria',
354
+ age: '29'
355
+ },
356
+ created_at: '2026-03-03T08:00:00.000Z',
357
+ updated_at: '2026-03-03T09:00:00.000Z'
358
+ });
359
+ ```
360
+
361
+ `User.hydrate(...)` is the row-like path. It reads the payload from the configured default slug key on the outer object and ignores unregistered extra root slugs.
362
+
363
+ Timestamp helper examples on create:
364
+
365
+ ```js
366
+ // Caller-provided timestamp values are kept
367
+ const user_with_timestamps = await User.create({
368
+ name: 'timed-user',
369
+ created_at: '2026-03-03T08:00:00.000Z',
370
+ updated_at: '2026-03-03T09:00:00.000Z'
371
+ });
372
+
373
+ // Omitted timestamps are auto-filled
374
+ const user_with_auto_timestamps = await User.create({
375
+ name: 'auto-timestamp-user'
376
+ });
377
+ ```
378
+
379
+ Validation failure pattern (`validation_error`):
380
+
381
+ ```js
382
+ try {
383
+ const invalid_user = User.from({
384
+ name: 'bad_input',
385
+ age: -1 // violates `min: 0` from the schema example
386
+ });
387
+
388
+ await invalid_user.save();
389
+ } catch(error) {
390
+ if(error.name === 'validation_error') {
391
+ const error_payload = error.to_json();
392
+ // shape: { success: false, error: { type: 'validation_error', message: '...', details: ... } }
393
+ // `error.details` is the same validation detail payload included in `error_payload.error.details`
394
+ }
395
+ }
396
+ ```
397
+
398
+ Constraint/query failure pattern (`query_error`):
399
+
400
+ Assumes:
401
+ - You created a unique index and applied it (for example via `ensure_table()` + `ensure_indexes()` or `ensure_model()`).
402
+
403
+ ```js
404
+ const unique_user_schema = new JsonBadger.Schema({
405
+ email: {type: String, required: true}
406
+ });
407
+ unique_user_schema.create_index({
408
+ using: 'btree',
409
+ path: 'email',
410
+ unique: true,
411
+ name: 'idx_unique_users_email'
412
+ });
413
+
414
+ const UniqueUser = connection.model({
415
+ name: 'UniqueUser',
416
+ schema: unique_user_schema,
417
+ table_name: 'unique_users'
418
+ });
419
+ await UniqueUser.ensure_table();
420
+ await UniqueUser.ensure_indexes();
421
+
422
+ await new UniqueUser({email: 'john@example.com'}).save();
423
+
424
+ try {
425
+ await new UniqueUser({email: 'john@example.com'}).save();
426
+ } catch(error) {
427
+ if(error.name === 'query_error') {
428
+ const error_payload = error.to_json();
429
+ const cause_message = error_payload.error.details?.cause;
430
+ // usually includes: duplicate key value violates unique constraint
431
+ }
432
+ }
433
+ ```
434
+
435
+ ## Query Basics (find, find_one, count_documents)
436
+
437
+ Assumes (for the query sections below):
438
+ - `User` is defined and at least one document was saved (see `## Create and Save Documents`).
439
+ - Examples that filter by `name: 'john'`, `tags`, `orders`, or `payload` assume those fields/values exist in seeded data.
440
+
441
+
442
+ ```js
443
+ const all_users = await User.find({}).exec();
444
+
445
+ const found_user = await User.find_one({name: 'john'}).exec();
446
+
447
+ const by_id_user = await User.find_by_id('1').exec();
448
+
449
+ const adult_count = await User.count_documents({age: {$gte: 18}}).exec();
450
+ ```
451
+
452
+ Returns:
453
+ - `all_users`: `User[]` (document instances)
454
+ - `found_user`: `User | null`
455
+ - `by_id_user`: `User | null`
456
+ - `adult_count`: `number`
457
+ - Snapshot example: `all_users[0]?.document` -> `{ id, data, created_at, updated_at }` when the default slug is still `data`
235
458
 
236
459
  Query builder chaining:
237
460
 
@@ -244,16 +467,13 @@ const page_of_users = await User.find({status: 'active'})
244
467
  .exec();
245
468
  ```
246
469
 
247
- Read behavior note:
248
- - `find(...).exec()`, `find_one(...).exec()`, and `count_documents(...).exec()` do not call `ensure_table()`.
249
-
250
470
  ## Direct Equality and Scalar Comparisons
251
471
 
252
472
  Direct equality and explicit `$eq` both work:
253
473
 
254
474
  ```js
255
- await User.find({user_name: 'john'}).exec();
256
- await User.find({user_name: {$eq: 'john'}}).exec();
475
+ await User.find({name: 'john'}).exec();
476
+ await User.find({name: {$eq: 'john'}}).exec();
257
477
  ```
258
478
 
259
479
  Scalar comparison operators:
@@ -273,19 +493,27 @@ await User.find({status: {$in: ['active', 'pending']}}).exec();
273
493
  await User.find({status: {$nin: ['disabled', 'banned']}}).exec();
274
494
  ```
275
495
 
496
+ Reserved base-field filters (top-level only):
497
+
498
+ ```js
499
+ await User.find({id: {$in: [1, 2, 3]}}).exec(); // bigserial default
500
+ await User.find({created_at: {$gte: '2026-01-01T00:00:00.000Z'}}).exec();
501
+ await User.find({updated_at: {$lt: '2026-12-31T23:59:59.999Z'}}).sort({updated_at: -1}).exec();
502
+ ```
503
+
276
504
  ## Regex Operators
277
505
 
278
506
  Regex literal shorthand:
279
507
 
280
508
  ```js
281
- await User.find({user_name: /jo/i}).exec();
509
+ await User.find({name: /jo/i}).exec();
282
510
  ```
283
511
 
284
512
  `$regex` with `$options`:
285
513
 
286
514
  ```js
287
515
  await User.find({
288
- user_name: {
516
+ name: {
289
517
  $regex: '^jo',
290
518
  $options: 'i'
291
519
  }
@@ -355,7 +583,7 @@ await User.find({payload: {$has_any_keys: ['profile', 'flags']}}).exec();
355
583
  await User.find({payload: {$has_all_keys: ['profile', 'score']}}).exec();
356
584
  ```
357
585
 
358
- Nested-scope existence (jsonbadger extracts the nested JSONB value first, then applies top-level existence there):
586
+ Nested-scope existence (JsonBadger extracts the nested JSONB value first, then applies top-level existence there):
359
587
 
360
588
  ```js
361
589
  await User.find({'payload.profile': {$has_key: 'city'}}).exec();
@@ -368,14 +596,14 @@ await User.find({payload: {$has_key: 'profile.city'}}).exec();
368
596
 
369
597
  `$json_path_exists` (`@?`) and `$json_path_match` (`@@`):
370
598
 
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({
599
+ Assumes:
600
+ - Your seeded `payload` includes shapes like `items[*].qty` and `score` (as shown in `## Create and Save Documents`).
601
+
602
+ Target shape reminder:
603
+ - `payload` is a JSON object with keys like `score` and `items`, where `items` is an array of objects (for example `{qty: 2}`).
604
+
605
+ ```js
606
+ await User.find({
379
607
  payload: {$json_path_exists: '$.items[*] ? (@.qty > 1)'}
380
608
  }).exec();
381
609
 
@@ -392,39 +620,41 @@ Expected behavior:
392
620
 
393
621
  `update_one(...)` supports `$set`, `$insert`, and `$set_lax`.
394
622
 
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(...)`):
623
+ Assumes (for update and delete sections below):
624
+ - A matching row exists (examples use `name: 'john'` and `name: 'missing'`).
625
+ - The seeded row includes `tags` and `payload` fields from the create/save example.
626
+
627
+ Target shape reminder:
628
+ - `tags` is an array, `profile` is an object, and `payload.profile` / `payload.score` are nested JSON values in the seeded row.
629
+
630
+ Basic `$set` (maps to `jsonb_set(...)`):
403
631
 
404
632
  ```js
405
633
  const updated_user = await User.update_one(
406
- {user_name: 'john'},
407
- {
408
- $set: {
409
- age: 31,
634
+ {name: 'john'},
635
+ {
636
+ $set: {
637
+ age: 31,
410
638
  'profile.city': 'Orlando',
411
639
  'payload.profile.state': 'FL'
412
- }
413
- }
414
- );
415
- // returns: updated data object (plain object) or null
416
- ```
640
+ }
641
+ }
642
+ );
643
+ ```
644
+
645
+ Returns: `updated_user` is `User | null`.
646
+ Snapshot shape: `updated_user?.document` -> `{ id, data, created_at, updated_at }` when the default slug is still `data`.
417
647
 
418
648
  `$insert` (maps to `jsonb_insert(...)`) with numeric array index paths:
419
649
 
420
650
  ```js
421
- await User.update_one({user_name: 'john'}, {
651
+ await User.update_one({name: 'john'}, {
422
652
  $insert: {
423
653
  'tags.0': 'first_tag'
424
654
  }
425
655
  });
426
656
 
427
- await User.update_one({user_name: 'john'}, {
657
+ await User.update_one({name: 'john'}, {
428
658
  $insert: {
429
659
  'tags.0': {
430
660
  value: 'after_first',
@@ -434,13 +664,13 @@ await User.update_one({user_name: 'john'}, {
434
664
  });
435
665
  ```
436
666
 
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'}, {
667
+ `$set_lax` (maps to `jsonb_set_lax(...)`) object form:
668
+
669
+ Target shape reminder for `$set_lax`:
670
+ - `payload` is a JSON object; these examples update or delete nested keys inside `payload`.
671
+
672
+ ```js
673
+ await User.update_one({name: 'john'}, {
444
674
  $set_lax: {
445
675
  'payload.cleanup_flag': {
446
676
  value: null,
@@ -453,7 +683,7 @@ await User.update_one({user_name: 'john'}, {
453
683
  `$set_lax` options example (`create_if_missing`, `null_value_treatment`):
454
684
 
455
685
  ```js
456
- await User.update_one({user_name: 'john'}, {
686
+ await User.update_one({name: 'john'}, {
457
687
  $set_lax: {
458
688
  'payload.archived_at': {
459
689
  value: null,
@@ -467,7 +697,7 @@ await User.update_one({user_name: 'john'}, {
467
697
  Multiple update operators in one call (application order is `$set` -> `$insert` -> `$set_lax`):
468
698
 
469
699
  ```js
470
- await User.update_one({user_name: 'john'}, {
700
+ await User.update_one({name: 'john'}, {
471
701
  $set: {
472
702
  'payload.score': 50
473
703
  },
@@ -486,7 +716,7 @@ await User.update_one({user_name: 'john'}, {
486
716
  Conflict rule (rejected before SQL):
487
717
 
488
718
  ```js
489
- await User.update_one({user_name: 'john'}, {
719
+ await User.update_one({name: 'john'}, {
490
720
  $set: {
491
721
  payload: {nested: true}
492
722
  },
@@ -497,116 +727,163 @@ await User.update_one({user_name: 'john'}, {
497
727
  // throws: conflicting update paths (same path or parent/child overlap)
498
728
  ```
499
729
 
730
+ Timestamp helper examples on update:
731
+
732
+ ```js
733
+ // Caller-provided updated_at is kept
734
+ await User.update_one({name: 'john'}, {
735
+ $set: {
736
+ age: 32,
737
+ updated_at: '2026-03-03T10:00:00.000Z'
738
+ }
739
+ });
740
+
741
+ // Omitted updated_at is auto-refreshed
742
+ await User.update_one({name: 'john'}, {
743
+ $set: {
744
+ age: 33
745
+ }
746
+ });
747
+ ```
748
+
500
749
  ## Delete Operations
501
750
 
502
- `delete_one(...)` deletes one matching row and returns the deleted document data.
751
+ `delete_one(...)` deletes one matching row and returns the deleted document instance.
503
752
 
504
- ```js
505
- const deleted_user = await User.delete_one({user_name: 'john'});
506
- // returns: deleted data object (plain object) or null
507
- ```
753
+ ```js
754
+ const deleted_user = await User.delete_one({name: 'john'});
755
+ ```
756
+
757
+ Returns: `deleted_user` is `User | null`.
758
+ Snapshot shape: `deleted_user?.document` -> `{ id, data, created_at, updated_at }` when the default slug is still `data`.
508
759
 
509
760
  No match (or missing table) returns `null`:
510
761
 
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
- }
762
+ ```js
763
+ const maybe_deleted = await User.delete_one({name: 'missing'});
764
+ if(maybe_deleted === null) {
765
+ // no matching row, or table does not exist yet
766
+ }
517
767
  ```
518
768
 
519
- ## Runtime Document Methods (get/set, dirty tracking, serialization)
769
+ Returns: `null` when no row matches (or when the table does not exist yet).
770
+
771
+ ## Runtime Document Methods (get/set and direct document access)
520
772
 
521
- Runtime getters/setters and dirty tracking:
773
+ Runtime getters/setters:
522
774
 
523
775
  Assumes:
524
- - `User` is defined from the schema/model example and includes the `user_name` getter configuration shown there.
776
+ - `User` is defined from the schema/model example and includes the `name` getter configuration shown there.
525
777
  - This snippet demonstrates in-memory runtime behavior; no database read is required until `save()`.
526
778
 
527
779
 
528
780
  ```js
529
- const doc = new User({
530
- user_name: 'jane',
781
+ const doc = User.from({
782
+ name: 'jane',
531
783
  status: 'active',
532
784
  payload: {count: 1},
533
785
  profile: {city: 'Miami'}
534
786
  });
535
787
 
536
- // normal path set/get
788
+ const default_slug = User.schema.get_default_slug();
537
789
 
538
- doc.set('user_name', ' jane_doe ');
539
- const upper_name = doc.get('user_name');
790
+ doc.set(default_slug + '.name', ' jane_doe ');
791
+ const user_name = doc.get(default_slug + '.name');
540
792
 
541
- doc.set('profile.city', 'Orlando');
542
- const city = doc.get('profile.city');
793
+ doc.set(default_slug + '.profile.city', 'Orlando');
794
+ const city = doc.get(default_slug + '.profile.city');
543
795
 
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');
796
+ // raw document access still works
797
+ doc.document[default_slug].payload.count += 1;
798
+ const pending_delta = doc.document.$get_delta();
799
+ ```
800
+
801
+ Bind live document fields onto another object:
802
+
803
+ ```js
804
+ const user_entity = {};
805
+ doc.bind_document(user_entity);
551
806
 
552
- doc.clear_modified();
807
+ user_entity.name; // reads from the default slug
808
+ user_entity.profile.city = 'Tampa'; // live nested default-slug object
553
809
  ```
554
810
 
811
+ > **Note:** `bind_document(...)` flattens default-slug root fields onto the target root, but keeps extra slugs nested. It also throws if the target already owns one of the field names it needs to bind.
812
+
555
813
  ### Optional Alias Path Example
556
814
 
557
815
  Use aliases only when you need an alternate path name (for example, a short path shortcut for a nested field).
558
816
 
559
817
  ```js
560
- const alias_schema = new jsonbadger.Schema({
818
+ const alias_schema = new JsonBadger.Schema({
561
819
  profile: {
562
820
  city: {type: String, alias: 'city'}
563
821
  }
564
822
  });
565
823
 
566
- const AliasUser = jsonbadger.model('alias_users', alias_schema);
567
- const alias_doc = new AliasUser({profile: {city: 'Miami'}});
824
+ const AliasUser = connection.model({
825
+ name: 'AliasUser',
826
+ schema: alias_schema,
827
+ table_name: 'alias_users'
828
+ });
829
+ const alias_doc = AliasUser.from({profile: {city: 'Miami'}});
568
830
 
569
831
  // alias path -> underlying path
570
832
  alias_doc.get('city'); // 'Miami'
571
833
  alias_doc.set('city', 'Orlando');
572
- alias_doc.get('profile.city'); // 'Orlando'
834
+ alias_doc.get('data.profile.city'); // 'Orlando' when the default slug is still `data`
573
835
  ```
574
836
 
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`).
837
+ Aliases are schema-defined alternate path names for `doc.get(...)` and `doc.set(...)`. They are collected when the schema is created, while canonical schema paths remain the persisted keys.
576
838
 
577
- Serialization helpers (`to_object`, `to_json`) apply getters by default:
839
+ Immutable behavior (`immutable: true`) after first persist:
578
840
 
579
841
  ```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();
842
+ await doc.save();
843
+
844
+ // after successful save, immutable fields reject updates
845
+ // doc.set('status', 'disabled'); // throws
583
846
  ```
584
847
 
585
- Immutable behavior (`immutable: true`) after first persist:
848
+ ## Lifecycle Quick Reference
586
849
 
587
850
  ```js
851
+ const doc = User.from({name: 'john'});
852
+ doc.set('data.profile.city', 'Miami');
588
853
  await doc.save();
589
854
 
590
- // after successful save, immutable fields reject updates
591
- // doc.set('status', 'disabled'); // throws
855
+ const found = await User.find_by_id(doc.id).exec();
856
+ const snapshot = found.document;
592
857
  ```
593
858
 
859
+ Phase summary:
860
+ - `User.from(...)` -> constructed
861
+ - first runtime interaction (`set/get/save/delete`) -> runtime-ready
862
+ - `save()` on new doc -> persisted
863
+ - `find_one(...).exec()` / `find_by_id(...).exec()` -> queried/hydrated
864
+
865
+ For the full lifecycle contract, see [`docs/lifecycle.md`](lifecycle.md).
866
+
594
867
  ## Complete Operator Checklist (Query and Update)
595
868
 
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
869
+ | Family | Supported operators/methods |
870
+ | --- | --- |
871
+ | Scalar equality/comparison | direct equality, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte` |
872
+ | Set membership | `$in`, `$nin` |
873
+ | Regex | RegExp literal shorthand, `$regex`, `$options` |
874
+ | Array operators | `$all`, `$size`, `$elem_match` |
875
+ | JSON containment | `$contains` |
876
+ | JSONB key existence | `$has_key`, `$has_any_keys`, `$has_all_keys` |
877
+ | JSONPath | `$json_path_exists`, `$json_path_match` |
878
+ | Updates (`update_one`) | `$set`, `$insert`, `$set_lax` |
879
+
880
+ ## Related Docs
608
881
 
609
882
  - [`README.md`](../README.md) (quick start + selected examples)
610
- - [`docs/api.md`](api.md) (API surface and behavior notes)
883
+ - [`docs/api/index.md`](api/index.md) (module API map)
884
+ - [`docs/api/model.md`](api/model.md) (model construction, queries, persistence, and document methods)
885
+ - [`docs/api/schema.md`](api/schema.md) (schema API and index declaration rules)
886
+ - [`docs/api/query-builder.md`](api/query-builder.md) (query builder chain and filter families)
887
+ - [`docs/lifecycle.md`](lifecycle.md) (document phases, hydration/save flow, dirty tracking, and serialization)
611
888
  - [`docs/query-translation.md`](query-translation.md) (PostgreSQL operator/function mapping)
612
889
  - [`docs/local-integration-testing.md`](local-integration-testing.md) (local integration test setup)