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
@@ -0,0 +1,171 @@
1
+ # JSONB Update Pipeline
2
+
3
+ This document describes the current JSONB update flow.
4
+
5
+ ## Example
6
+
7
+ This page describes the internal JSONB operator layer after `Model.update_one(...)` input has already been normalized.
8
+
9
+ ```js
10
+ const payload = {
11
+ $unset: ['profile.address'],
12
+ $set: {
13
+ 'profile.city': 'Santiago',
14
+ 'profile.tags': ['vip']
15
+ }
16
+ };
17
+
18
+ const jsonb_ops = JsonbOps.from(payload, {
19
+ column_name: '"data"',
20
+ coalesce: true
21
+ });
22
+ ```
23
+
24
+ > **Note:** `updated_at` is optional in this payload. `Model.prototype.update()` sets it automatically, and direct code paths can pass it explicitly when they want to control the value. The SQL builder only binds the row-level timestamps it receives. A dedicated `model_options.timestamps` flag is a follow-up and is called out by the TODO in `src/constants/defaults.js`.
25
+
26
+ Runtime flow:
27
+ 1. `exec_update_one(...)` splits row-level fields (`created_at`, `updated_at`) from the JSONB payload.
28
+ 2. `JsonbOps.from(payload, {column_name, coalesce: true})` returns a `JsonbOps` instance with `target` and ordered `operations`.
29
+ 3. `jsonb_ops.compile(parameter_state)` folds `operations` into one SQL RHS expression and binds JSON values into the shared parameter state.
30
+ 4. `build_update_query(...)` assembles `UPDATE ... SET ...` and binds only the row-level timestamp parameters it receives after the JSONB expression is finalized.
31
+ 5. `exec_update_one(...)` returns the raw updated row to the public model boundary, which can then hydrate or rebase as needed.
32
+
33
+ > **Note:** `jsonb_set(NULL, ...)` returns `NULL`. Partial JSONB updates must start from `COALESCE(column, '{}'::jsonb)`.
34
+
35
+ ## Current Contract
36
+
37
+ `JsonbOps.from(...)` accepts the internal operator-style JSONB contract:
38
+
39
+ ```js
40
+ const jsonb_ops = JsonbOps.from(payload, {column_name: '"data"', coalesce: true});
41
+
42
+ jsonb_ops.target;
43
+ // -> 'COALESCE("data", \'{}\'::jsonb)'
44
+
45
+ jsonb_ops.operations;
46
+ // -> [
47
+ // {op: '$replace_roots', value: '{"profile":{"city":"Miami"}}'},
48
+ // {op: '#-', path: '{"profile","address"}'},
49
+ // {op: 'jsonb_set', path: '{"profile","tags"}', value: '["vip"]'}
50
+ // ]
51
+
52
+ const rhs = jsonb_ops.compile(parameter_state);
53
+ ```
54
+
55
+ Rules:
56
+ 1. `operations` is ordered for PostgreSQL execution: replace roots -> unsets -> sets.
57
+ 2. `from(...)` never receives `parameter_state`.
58
+ 3. `compile(...)` is the only JSONB layer that calls `bind_parameter(...)`.
59
+
60
+ ## Update Shapes
61
+
62
+ The internal JSONB contract is operator-style:
63
+ 1. `$replace_roots`: plain object payload replacement.
64
+ 2. `$unset`: array of dot paths.
65
+ 3. `$set`: map of dot path -> value.
66
+ 4. Any top-level non-`$` key is treated as implicit `$set`.
67
+
68
+ The public `Model.update_one(...)` API is different:
69
+ 1. `$set`
70
+ 2. `$insert`
71
+ 3. `$set_lax`
72
+ 4. row-level `updated_at` at the root when provided explicitly
73
+
74
+ `src/model/operations/update-one.js` is the boundary that adapts the public update API and tracker deltas into the internal JSONB operator contract above.
75
+
76
+ Tracked document updates come from `DeltaTracker` as:
77
+
78
+ ```js
79
+ {replace_roots: {...}, set: {...}, unset: [...]}
80
+ ```
81
+
82
+ When a model tracks slug roots, tracked paths are prefixed with that slug root. For example, when the default slug is still `data`, tracked paths look like `data.profile.city`. The orchestrator boundary in `src/model/operations/update-one.js` strips the tracked root and maps the delta into operator-style input before calling `JsonbOps.from(...)`.
83
+
84
+ ## Interaction Model
85
+
86
+ The update path is easiest to reason about as three stages:
87
+ 1. change set from `DeltaTracker`,
88
+ 2. ordered JSONB operations from `JsonbOps`,
89
+ 3. final SQL assembly in the query builder.
90
+
91
+ ### 1. Change Set
92
+
93
+ Tracked document updates start as pure delta data:
94
+
95
+ ```js
96
+ const tracker_delta = document.$get_delta();
97
+
98
+ // Example shape:
99
+ // {
100
+ // replace_roots: {},
101
+ // set: {
102
+ // 'data.profile.name': 'John Doe',
103
+ // 'data.profile.age': 35
104
+ // },
105
+ // unset: ['data.profile.address']
106
+ // }
107
+ ```
108
+
109
+ This shape is still tracker-oriented. It may include the tracked root prefix and it does not use SQL-facing operator names.
110
+
111
+ ### 2. JSONB Operations
112
+
113
+ The orchestrator adapts the tracker delta into operator-style input and builds one `JsonbOps` instance:
114
+
115
+ ```js
116
+ const jsonb_ops = JsonbOps.from(payload, {
117
+ column_name: '"data"',
118
+ coalesce: true
119
+ });
120
+ ```
121
+
122
+ That instance stores:
123
+ 1. `target`: the starting SQL expression,
124
+ 2. `operations`: the ordered JSONB mutations to apply.
125
+
126
+ ```js
127
+ jsonb_ops.operations;
128
+ // -> [
129
+ // {op: '#-', path: '{"profile","address"}'},
130
+ // {op: 'jsonb_set', path: '{"profile","name"}', value: '"John Doe"'},
131
+ // {op: 'jsonb_set', path: '{"profile","age"}', value: '35'}
132
+ // ]
133
+ ```
134
+
135
+ > **Note:** `JsonbOps.from(...)` does not bind SQL parameters. It only parses and orders JSONB mutations.
136
+
137
+ ### 3. SQL Assembly
138
+
139
+ The SQL builder compiles those operations against the shared parameter state:
140
+
141
+ ```js
142
+ const compiled_data_expression = jsonb_ops.compile(parameter_state);
143
+ ```
144
+
145
+ That compile step folds the ordered operations into one RHS expression and pushes JSON values into the global parameter list in the same pass.
146
+
147
+ ```js
148
+ // Example result:
149
+ // jsonb_set(
150
+ // jsonb_set(
151
+ // COALESCE("data", '{}'::jsonb) #- '{"profile","address"}',
152
+ // '{"profile","name"}',
153
+ // $2::jsonb,
154
+ // true
155
+ // ),
156
+ // '{"profile","age"}',
157
+ // $3::jsonb,
158
+ // true
159
+ // )
160
+ ```
161
+
162
+ ## Module Responsibilities
163
+
164
+ 1. `src/sql/jsonb/ops.js`: parse operator-style JSONB updates and compile them into the RHS SQL expression.
165
+ 2. `src/model/operations/update-one.js`: split row timestamps, adapt tracked deltas when needed, and build the query context.
166
+ 3. `src/sql/write/build-update-query.js`: compile `jsonb_ops`, then bind row-level timestamps and assemble the final `UPDATE`.
167
+ 4. `src/model/model.js`: keep `updated_at` and `created_at` at the row boundary instead of pushing them into JSONB `$set`.
168
+
169
+ ## Pending Note
170
+
171
+ The current boundary adapter assumes one JSONB slug per document update path. Multi-slug document routing is a follow-up and is called out by the TODO in `src/model/operations/update-one.js`.
@@ -0,0 +1,111 @@
1
+ # Model Compilation
2
+
3
+ This note explains what the model factory produces and why JsonBadger creates one concrete `Model` constructor per model definition.
4
+
5
+ ## Quick Example
6
+
7
+ ```js
8
+ const connection = await JsonBadger.connect(uri, options);
9
+
10
+ const User = connection.model({
11
+ name: 'User',
12
+ schema: user_schema,
13
+ table_name: 'users'
14
+ });
15
+ ```
16
+
17
+ That `User` value is not the shared base `Model`. It is a compiled constructor created for that specific schema, connection, and model configuration.
18
+
19
+ ## Why Compilation Exists
20
+
21
+ The base `Model` in `src/model/model.js` provides shared behavior such as query helpers, document methods, validation flow, and persistence entry points.
22
+
23
+ The factory step in `src/model/factory/index.js` turns that shared behavior into a concrete runtime constructor that is bound to one definition. Without that step, the runtime would have nowhere to attach per-model state such as schema metadata, resolved options, and connection ownership.
24
+
25
+ ## Current Flow
26
+
27
+ ```js
28
+ const User = connection.model({
29
+ name: 'User',
30
+ schema: user_schema,
31
+ table_name: 'users'
32
+ });
33
+ ```
34
+
35
+ The current compilation flow is:
36
+
37
+ 1. `Connection.model(...)`
38
+ 1. validates the registration input
39
+ 2. resolves model options such as `table_name`
40
+ 3. delegates compilation to `src/model/factory/index.js`
41
+ 2. `model_factory(...)`
42
+ 1. creates a fresh concrete constructor for that definition
43
+ 2. wires inheritance against the base `Model`
44
+ 3. binds schema, options, runtime state, and connection onto the compiled constructor
45
+ 3. `Connection.model(...)`
46
+ 1. registers the compiled constructor
47
+ 2. returns it to user code
48
+
49
+ ## What The Factory Produces
50
+
51
+ The model factory creates a fresh constructor function and then links it to the shared base `Model` chain.
52
+
53
+ At a high level, the compiled constructor gets:
54
+
55
+ 1. Shared static behavior through `Object.setPrototypeOf(model, Model)`.
56
+ 2. Shared instance behavior through `Object.setPrototypeOf(model.prototype, Model.prototype)`.
57
+ 3. Definition-specific static state such as:
58
+ 1. `model.schema`
59
+ 2. `model.options`
60
+ 3. `model.state`
61
+ 4. `model.connection`
62
+ 4. Definition-specific instance access through `model.prototype.connection`.
63
+
64
+ This is why each model definition needs its own constructor instance.
65
+
66
+ ## Base Model vs Compiled Model
67
+
68
+ ### Base `Model`
69
+
70
+ The base `Model` is the shared behavior template.
71
+
72
+ It defines the common runtime surface used by all compiled models, including:
73
+
74
+ 1. document construction
75
+ 2. instance reads and writes
76
+ 3. validation flow
77
+ 4. `save`, `insert`, `update`, and `delete` entry points
78
+ 5. static query and migration helpers
79
+
80
+ ### Compiled Model
81
+
82
+ A compiled model is the concrete constructor returned by `connection.model(...)`.
83
+
84
+ It carries the definition-specific runtime context needed for real work:
85
+
86
+ 1. one schema instance
87
+ 2. one resolved model-options object
88
+ 3. one owning connection
89
+ 4. one model-level runtime state bucket
90
+
91
+ ## Why A Detached Constructor Helper Is Not Enough
92
+
93
+ The important architectural point is not just "create a constructor." The important point is where that happens.
94
+
95
+ Model construction belongs inside the compiler/factory step because that is the place where JsonBadger already has the full definition context:
96
+
97
+ 1. schema
98
+ 2. resolved model options
99
+ 3. owning connection
100
+
101
+ That keeps the lifecycle cohesive. A detached helper that only returns a constructor would still need the compiler step to finish wiring the actual runtime state, so it would split one responsibility across two places without simplifying the design.
102
+
103
+ ## Mental Model
104
+
105
+ Use this distinction when reading the codebase:
106
+
107
+ 1. `src/model/model.js` defines the shared model behavior.
108
+ 2. `src/model/factory/index.js` compiles one concrete model constructor.
109
+ 3. `src/connection/connection.js` owns registration and returns compiled models to user code.
110
+
111
+ So when application code holds `User`, `Post`, or `Invoice`, it is holding a compiled model constructor, not the shared base `Model` itself.
@@ -0,0 +1,146 @@
1
+ # JsonBadger Document Lifecycle
2
+
3
+ This page explains what happens to a document instance from construction to persistence and hydration.
4
+
5
+ Use this page when you need to answer:
6
+ - when `is_new` changes
7
+ - when base fields are populated
8
+ - how `from(...)` and `hydrate(...)` differ
9
+ - what `get(...)`, `set(...)`, and direct `document` mutation actually do
10
+
11
+ ## Lifecycle Overview
12
+
13
+ ```js
14
+ const doc = User.from({
15
+ name: 'john'
16
+ });
17
+
18
+ doc.set('data.profile.city', 'Miami');
19
+ await doc.save();
20
+
21
+ const found = await User.find_one({id: doc.id}).exec();
22
+ const snapshot = found.document;
23
+ ```
24
+
25
+ > **Note:** Query builders are not thenables. Run them with `.exec()`.
26
+
27
+ Phases:
28
+ - `constructed`: `Model.from(...)`
29
+ - `hydrated`: `Model.hydrate(...)` or document-returning query results
30
+ - `persisted`: successful `doc.save()` on a new document
31
+ - `mutated`: `set(...)` or direct `document` mutation
32
+
33
+ ## Transition Table
34
+
35
+ | Trigger | From | To | What changes |
36
+ | --- | --- | --- | --- |
37
+ | `Model.from(input)` | none | constructed | builds a new validated document envelope |
38
+ | `Model.hydrate(row)` | none | hydrated | builds a persisted validated document envelope from row-like input |
39
+ | `doc.save()` on new document | constructed | persisted | inserts one row, sets `is_new = false`, and rebases tracked changes |
40
+ | `doc.save()` on existing document | mutated/persisted | persisted | validates current state, updates one row, refreshes `updated_at`, and rebases tracked changes |
41
+ | `find(...).exec()` / `find_one(...).exec()` / `find_by_id(...).exec()` | none | hydrated | row results become hydrated document instances |
42
+ | `update_one(...)` / `delete_one(...)` | none | hydrated | returned row becomes a hydrated document instance, or `null` |
43
+
44
+ ## Constructed
45
+
46
+ ```js
47
+ const doc = User.from({
48
+ name: 'john',
49
+ created_at: '2026-03-03T08:00:00.000Z'
50
+ });
51
+ ```
52
+
53
+ At construction time:
54
+ - `doc.is_new === true`
55
+ - base fields are kept at the document root
56
+ - payload values are stored under the schema-owned default slug
57
+ - registered extra slugs stay as sibling document roots
58
+
59
+ ## Hydrated
60
+
61
+ ```js
62
+ const doc = User.hydrate({
63
+ id: '7',
64
+ data: {
65
+ name: 'john'
66
+ },
67
+ created_at: '2026-03-03T08:00:00.000Z',
68
+ updated_at: '2026-03-03T09:00:00.000Z'
69
+ });
70
+ ```
71
+
72
+ Hydration rules:
73
+ - `doc.is_new === false`
74
+ - the outer object is treated as the persisted row envelope
75
+ - the payload is read from the configured default slug key
76
+ - registered extra slugs are read from sibling roots
77
+ - unregistered extra root slugs are ignored
78
+ - dotted payload keys are not expanded during hydration
79
+
80
+ ## Persisted
81
+
82
+ Create path:
83
+
84
+ ```js
85
+ const created = await User.from({name: 'john'}).save();
86
+ ```
87
+
88
+ Update path:
89
+
90
+ ```js
91
+ const found = await User.find_one({name: 'john'}).exec();
92
+
93
+ if(found) {
94
+ found.set('data.profile.city', 'Orlando');
95
+ await found.save();
96
+ }
97
+ ```
98
+
99
+ After a successful save:
100
+ - `doc.is_new === false`
101
+ - `doc.id`, `doc.created_at`, and `doc.updated_at` reflect persisted row values
102
+ - tracked changes are rebased
103
+
104
+ Timestamp behavior:
105
+ - inserts keep caller-provided `created_at`, otherwise fill it
106
+ - inserts always refresh `updated_at`
107
+ - updates keep caller-provided `updated_at`, otherwise refresh it
108
+ - updates do not auto-change `created_at`
109
+
110
+ ## Mutated
111
+
112
+ ```js
113
+ const doc = User.from({
114
+ name: 'john'
115
+ });
116
+
117
+ doc.set('data.profile.city', 'Miami');
118
+ doc.document.data.profile.city = 'Orlando';
119
+
120
+ const pending_delta = doc.document.$get_delta();
121
+ ```
122
+
123
+ Mutation rules:
124
+ - `set(...)` resolves aliases, applies schema setter/cast logic, and writes the exact path
125
+ - direct `doc.document[...]` mutation stays allowed
126
+ - direct mutation bypasses `set(...)` and assignment-time casting
127
+ - later `schema.validate(...)` and persistence still enforce the contract
128
+
129
+ Base-field write rules:
130
+ - `id` is read-only through `set(...)`
131
+ - `created_at` and `updated_at` only allow top-level assignment through `set(...)`
132
+ - dotted timestamp writes are rejected
133
+
134
+ ## FAQ
135
+
136
+ ### Why is `id` not inside the default slug?
137
+
138
+ Because `id`, `created_at`, and `updated_at` are row-level base fields, not payload fields.
139
+
140
+ ### Why do I need `.exec()` on `find_one()` and `find_by_id()`?
141
+
142
+ Because those methods return a query builder, not a promise.
143
+
144
+ ### Why did a direct nested mutation skip casting?
145
+
146
+ Because direct `doc.document[...]` mutation is the lower-level escape hatch. Use `doc.set(...)` when you want schema-aware setter and cast behavior.
@@ -1,4 +1,4 @@
1
- # query translation
1
+ # query translation
2
2
 
3
3
  This project compiles Mongo-like filters into PostgreSQL SQL over JSONB data.
4
4
 
@@ -35,9 +35,9 @@ This project compiles Mongo-like filters into PostgreSQL SQL over JSONB data.
35
35
 
36
36
  Top-level semantics note:
37
37
  - PostgreSQL key-existence operators only check keys at the top level of the left-hand JSONB value.
38
- - jsonbadger preserves that behavior exactly.
39
- - If the filter path is nested (for example `profile.city`), jsonbadger first extracts that nested JSONB value (`#>`), then applies the existence operator to the extracted value's top level.
40
- - jsonbadger does not emulate recursive/deep key search for these operators.
38
+ - JsonBadger preserves that behavior exactly.
39
+ - If the filter path is nested (for example `profile.city`), JsonBadger first extracts that nested JSONB value (`#>`), then applies the existence operator to the extracted value's top level.
40
+ - JsonBadger does not emulate recursive/deep key search for these operators.
41
41
 
42
42
  ## JSONPath operators
43
43
 
@@ -72,7 +72,7 @@ Update path behavior:
72
72
 
73
73
  This table is the implementation-facing capability map for currently supported query/update operators over JSONB.
74
74
 
75
- | jsonbadger feature | PostgreSQL operator/function | Notes | Indexability expectation |
75
+ | JsonBadger feature | PostgreSQL operator/function | Notes | Indexability expectation |
76
76
  | --- | --- | --- | --- |
77
77
  | scalar equality (`{ path: value }`, `$ne`) | JSON extraction (`->>`, `#>>`) + `=` / `!=` | Compares extracted text/scalar representation | Manual expression indexes required for fast path filtering/sorting; current schema index helpers do not auto-create these expression indexes |
78
78
  | numeric comparisons (`$gt`, `$gte`, `$lt`, `$lte`) | JSON extraction + `::numeric` + comparison | Invalid numeric inputs fail before SQL execution | Manual expression indexes may be used; no automatic expression-index creation |
@@ -81,7 +81,7 @@ This table is the implementation-facing capability map for currently supported q
81
81
  | `$all` | `@>` (JSON array containment) | Encodes requested list as JSON array | GIN on JSONB value is typically the relevant index strategy |
82
82
  | `$size` | `jsonb_array_length(...)` | Array length equality | Usually expression-based optimization if needed; no auto-created support |
83
83
  | `$elem_match` | `jsonb_array_elements(...)` + nested predicates | Uses set-returning expansion | Often less index-friendly than direct containment; depends on query shape |
84
- | `$regex` | `~` / `~*` on extracted text | PostgreSQL regex semantics | Manual text/expression indexing strategy required; jsonbadger does not auto-create regex-specific indexes |
84
+ | `$regex` | `~` / `~*` on extracted text | PostgreSQL regex semantics | Manual text/expression indexing strategy required; JsonBadger does not auto-create regex-specific indexes |
85
85
  | `$has_key` | `?` | Top-level key existence on the left JSONB value | GIN on JSONB value is the expected index family |
86
86
  | `$has_any_keys` | `?|` | Any-key existence | GIN on JSONB value is the expected index family |
87
87
  | `$has_all_keys` | `?&` | All-keys existence | GIN on JSONB value is the expected index family |
@@ -91,8 +91,9 @@ This table is the implementation-facing capability map for currently supported q
91
91
  | `update_one.$insert` | `jsonb_insert(...)` | Supports `insert_after` and numeric array-index path segments | N/A (write-path function) |
92
92
  | `update_one.$set_lax` | `jsonb_set_lax(...)` | Supports `create_if_missing` + `null_value_treatment` | N/A (write-path function) |
93
93
 
94
- Index helper behavior (current implementation):
95
- - `schema.create_index('path')` creates a GIN index on the extracted JSONB path expression.
96
- - `schema.create_index({ path: 1 })` (and compound object specs) create BTREE indexes on extracted text path expressions.
97
- - Single-path string GIN indexes do not support `unique` in jsonbadger; object specs support `unique` because they compile to BTREE indexes.
94
+ Index helper behavior:
95
+ - `schema.create_index({using: 'gin', path: 'profile.city'})` creates a GIN index on the extracted JSONB path expression.
96
+ - `schema.create_index({using: 'btree', path: 'age', order: -1})` creates a BTREE index on extracted text path expressions.
97
+ - `schema.create_index({using: 'btree', paths: {name: 1, age: -1}})` creates compound BTREE indexes.
98
+ - GIN descriptors reject `unique`; BTREE descriptors support `unique`.
98
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsonbadger",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "JSONB query/model library for PostgreSQL",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -53,6 +53,13 @@
53
53
  "test-integration": "npm run jest-cli -- --runInBand test/integration/postgres",
54
54
  "test-all": "npm run test-unit && npm run test-integration",
55
55
  "test-output": "npm run jest-cli -- --runInBand test/unit --json --outputFile=test/test-output.json",
56
- "test-coverage": "npm run jest-cli -- --runInBand test/unit --coverage"
56
+ "test-coverage": "npm run jest-cli -- --runInBand test/unit --coverage",
57
+ "test-coverage-watch": "npm run jest-cli -- --watch test/unit --coverage",
58
+ "bump-major": "npm version major",
59
+ "bump-minor": "npm version minor",
60
+ "bump-patch": "npm version patch",
61
+ "bump-major-local": "npm version major --no-git-tag-version",
62
+ "bump-minor-local": "npm version minor --no-git-tag-version",
63
+ "bump-patch-local": "npm version patch --no-git-tag-version"
57
64
  }
58
- }
65
+ }
@@ -1,25 +1,18 @@
1
1
  import {Pool} from 'pg';
2
2
 
3
3
  import defaults from '#src/constants/defaults.js';
4
+ import Connection from '#src/connection/connection.js';
4
5
  import debug_logger from '#src/debug/debug-logger.js';
6
+
5
7
  import {assert_condition} from '#src/utils/assert.js';
6
- import {assert_valid_id_strategy} from '#src/constants/id-strategies.js';
7
- import {scan_server_capabilities, assert_id_strategy_capability} from '#src/connection/server-capabilities.js';
8
- import {is_string} from '#src/utils/value.js';
9
- import {get_pool, has_pool, set_pool} from '#src/connection/pool-store.js';
8
+ import {scan_server_capabilities} from '#src/connection/server-capabilities.js';
9
+ import {is_function, is_string} from '#src/utils/value.js';
10
10
 
11
- export default async function connect(uri, options) {
11
+ async function connect(uri, options) {
12
12
  assert_condition(is_string(uri) && uri.length > 0, 'connection_uri is required');
13
13
 
14
- if(has_pool()) {
15
- return get_pool();
16
- }
17
-
18
14
  const final_options = Object.assign({}, defaults.connection_options, options);
19
- const {debug, id_strategy, auto_index, ...pool_options} = final_options;
20
-
21
- assert_valid_id_strategy(id_strategy);
22
- assert_condition(typeof auto_index === 'boolean', 'auto_index must be a boolean');
15
+ const {debug, ...pool_options} = final_options;
23
16
 
24
17
  const pool_instance = new Pool(Object.assign({}, pool_options, {connectionString: uri}));
25
18
  let server_capabilities;
@@ -27,24 +20,24 @@ export default async function connect(uri, options) {
27
20
  try {
28
21
  await pool_instance.query('SELECT 1');
29
22
  server_capabilities = await scan_server_capabilities(pool_instance);
30
- assert_id_strategy_capability(id_strategy, server_capabilities);
31
- set_pool(pool_instance, final_options, server_capabilities);
32
23
  } catch(error) {
33
24
  await close_pool_quietly(pool_instance);
34
25
  throw error;
35
26
  }
36
27
 
28
+ const connection_instance = new Connection(pool_instance, final_options, server_capabilities);
29
+
37
30
  debug_logger(debug, 'connection_ready', {
38
31
  max: pool_options.max,
39
32
  server_version: server_capabilities.server_version,
40
33
  supports_uuidv7: server_capabilities.supports_uuidv7
41
34
  });
42
35
 
43
- return pool_instance;
36
+ return connection_instance;
44
37
  }
45
38
 
46
39
  async function close_pool_quietly(pool_instance) {
47
- if(!pool_instance || typeof pool_instance.end !== 'function') {
40
+ if(!pool_instance || !is_function(pool_instance.end)) {
48
41
  return;
49
42
  }
50
43
 
@@ -54,3 +47,5 @@ async function close_pool_quietly(pool_instance) {
54
47
  // Ignore cleanup failures so the original connection/capability error is preserved.
55
48
  }
56
49
  }
50
+
51
+ export default connect;