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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carlos López
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# JsonBadger
|
|
2
|
+
|
|
3
|
+
JsonBadger is a Node.js library that simplifies working with complex JSON data in PostgreSQL. Instead of writing raw, complex SQL to query nested `jsonb` columns, JsonBadger lets you define schemas and interact with your data using a clean, object-based API and simple query operators.
|
|
4
|
+
|
|
5
|
+
## Why did I create this utility?
|
|
6
|
+
|
|
7
|
+
I originally built my personal systems and small business client projects using MongoDB and Mongoose. Over time, that setup was no longer viable or cost-effective on shared hosting. I needed a solution that was more contained and budget-friendly. Since most shared hosting providers offer free, robust PostgreSQL databases, I created this utility to leverage PostgreSQL's `jsonb` fields as an alternative for document-based storage.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Install](#install)
|
|
12
|
+
- [Examples (Quick Start)](#examples-quick-start)
|
|
13
|
+
- [Examples Cheat Sheet](#examples-cheat-sheet)
|
|
14
|
+
- [Core Features](#core-features)
|
|
15
|
+
- [Documentation](#documentation)
|
|
16
|
+
- [Author](#author)
|
|
17
|
+
- [License](#license)
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Requirements:
|
|
22
|
+
- Node.js >=20
|
|
23
|
+
- PostgreSQL
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install jsonbadger
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Examples (Quick Start)
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import jsonbadger from 'jsonbadger';
|
|
33
|
+
|
|
34
|
+
// 1. Connect to database
|
|
35
|
+
await jsonbadger.connect('postgresql://user:pass@localhost:5432/dbname', {
|
|
36
|
+
debug: false,
|
|
37
|
+
max: 10,
|
|
38
|
+
ssl: false,
|
|
39
|
+
auto_index: true,
|
|
40
|
+
id_strategy: jsonbadger.IdStrategies.bigserial // server default: bigserial | uuidv7 (native PostgreSQL uuidv7() required)
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 2. Define schema (FieldType format)
|
|
44
|
+
const user_schema = new jsonbadger.Schema({
|
|
45
|
+
user_name: {type: String, required: true, index: true},
|
|
46
|
+
age: {type: Number, min: 0, index: -1},
|
|
47
|
+
type: {type: String},
|
|
48
|
+
email: {type: String, index: {unique: true, name: 'idx_users_email_unique'}}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 3. Declare indexes on schema (path-level + schema-level compound index)
|
|
52
|
+
// Object-form create_index(...) is how you define a compound index.
|
|
53
|
+
user_schema.create_index({user_name: 1, type: -1});
|
|
54
|
+
|
|
55
|
+
// 4. Create model
|
|
56
|
+
const User = jsonbadger.model(user_schema, {
|
|
57
|
+
table_name: 'users',
|
|
58
|
+
data_column: 'data',
|
|
59
|
+
auto_index: false, // optional model-level override
|
|
60
|
+
id_strategy: jsonbadger.IdStrategies.bigserial // optional model-level override (uuidv7 uses DB-generated ids)
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 5. Run migrations manually (optional if you rely on first-write auto table creation via save()/update_one())
|
|
64
|
+
await User.ensure_table();
|
|
65
|
+
await User.ensure_index();
|
|
66
|
+
|
|
67
|
+
// 6. Save a document
|
|
68
|
+
const saved_user = await new User({
|
|
69
|
+
user_name: 'john',
|
|
70
|
+
age: 30,
|
|
71
|
+
type: 'admin'
|
|
72
|
+
}).save();
|
|
73
|
+
|
|
74
|
+
// 7. Find a document
|
|
75
|
+
const found_user = await User.find_one({
|
|
76
|
+
user_name: 'john'
|
|
77
|
+
}).exec();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
When using `IdStrategies.uuidv7`, jsonbadger checks PostgreSQL native `uuidv7()` support automatically during `connect(...)` and reuses cached capability metadata for later model-level `uuidv7` overrides. You do not need to run manual version checks; `SELECT version();` and `SHOW server_version;` are optional troubleshooting commands only.
|
|
81
|
+
|
|
82
|
+
## Examples Cheat Sheet
|
|
83
|
+
|
|
84
|
+
For a complete copy-paste operator and runtime cheat sheet (queries, updates, and document methods), see [`docs/examples.md`](docs/examples.md).
|
|
85
|
+
|
|
86
|
+
## Core Features
|
|
87
|
+
|
|
88
|
+
* **JSONB-first**: Model API designed specifically for PostgreSQL JSONB.
|
|
89
|
+
* **Validation**: FieldType-based schema validation built in.
|
|
90
|
+
* **Querying**: Mongo-style query operators (e.g., `$gt`, `$regex`) compiled into SQL.
|
|
91
|
+
* **Runtime document APIs**: `get`, `set`, alias virtuals, serialization helpers, `mark_modified`/dirty tracking, and immutable enforcement after save.
|
|
92
|
+
* **JSON update operators**: PostgreSQL-native `jsonb_set`, `jsonb_insert`, and `jsonb_set_lax` support in `update_one(...)`.
|
|
93
|
+
* **Migrations**: Helpers for `ensure_table` and `ensure_index`.
|
|
94
|
+
* **Configurable row IDs**: use `IdStrategies.bigserial` and `IdStrategies.uuidv7` (`uuidv7` is generated by PostgreSQL via native `uuidv7()` support).
|
|
95
|
+
* **Configurable index lifecycle**: `auto_index` can be set at connection and overridden per model.
|
|
96
|
+
* **UUIDv7 compatibility gating**: jsonbadger checks support automatically and fails early with server version/capability context when unavailable.
|
|
97
|
+
* **Modern**: Native ESM package.
|
|
98
|
+
|
|
99
|
+
## Documentation
|
|
100
|
+
|
|
101
|
+
* [`docs/api.md`](docs/api.md)
|
|
102
|
+
* [`docs/examples.md`](docs/examples.md) (complete example cheat sheet for queries, updates, and runtime document methods)
|
|
103
|
+
* [`docs/query-translation.md`](docs/query-translation.md) (includes PostgreSQL capability map for query/update operators and indexability expectations)
|
|
104
|
+
* [`docs/local-integration-testing.md`](docs/local-integration-testing.md)
|
|
105
|
+
* [`CHANGELOG.md`](CHANGELOG.md) for release notes and version history
|
|
106
|
+
|
|
107
|
+
## Author
|
|
108
|
+
|
|
109
|
+
**Carlos López**
|
|
110
|
+
* GitHub: [@carlosjln](https://github.com/carlosjln)
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT. See `LICENSE`.
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# jsonbadger API
|
|
2
|
+
|
|
3
|
+
## Top-level exports
|
|
4
|
+
|
|
5
|
+
Named exports:
|
|
6
|
+
- `connect(connection_uri, connection_options)`
|
|
7
|
+
- `disconnect()`
|
|
8
|
+
- `Schema`
|
|
9
|
+
- `IdStrategies`
|
|
10
|
+
- `model(schema_instance, model_options)`
|
|
11
|
+
- `create_field_type(path_value, type_reference, options?)`
|
|
12
|
+
- `register_field_type(type_name, type_constructor, references?)`
|
|
13
|
+
- `resolve_field_type(type_reference)`
|
|
14
|
+
|
|
15
|
+
Default export (`jsonbadger`) includes the same plus:
|
|
16
|
+
- `field_types.UUIDv7`
|
|
17
|
+
- `field_types.boolean_convert_to_true`
|
|
18
|
+
- `field_types.boolean_convert_to_false`
|
|
19
|
+
|
|
20
|
+
## connect
|
|
21
|
+
|
|
22
|
+
`connect(connection_uri, connection_options)` opens (or reuses) a shared PostgreSQL pool.
|
|
23
|
+
|
|
24
|
+
`connection_options`:
|
|
25
|
+
- `max`: pool size (default `10`)
|
|
26
|
+
- `debug`: logs SQL/debug events when `true`
|
|
27
|
+
- `auto_index`: server-wide default for automatic index creation when `Model.ensure_table()` is called (default `true`)
|
|
28
|
+
- `id_strategy`: server-wide default row-id strategy (`IdStrategies.bigserial` or `IdStrategies.uuidv7`, default `IdStrategies.bigserial`)
|
|
29
|
+
- any `pg` pool options (`ssl`, `host`, `port`, `user`, `password`, `database`)
|
|
30
|
+
|
|
31
|
+
UUIDv7 compatibility behavior:
|
|
32
|
+
- `connect(...)` performs an internal PostgreSQL capability scan (server version + native `uuidv7()` function presence) and caches the result for later model/runtime checks.
|
|
33
|
+
- If the connection-level `id_strategy` resolves to `IdStrategies.uuidv7` and native support is unavailable, `connect(...)` fails fast.
|
|
34
|
+
- Model-level `uuidv7` overrides defined after `connect(...)` are validated on use with the cached capability metadata.
|
|
35
|
+
- Optional troubleshooting commands only: `SELECT version();` and `SHOW server_version;`. Runtime checks use machine-readable checks internally.
|
|
36
|
+
|
|
37
|
+
## schema
|
|
38
|
+
|
|
39
|
+
`new Schema(schema_def, options?)`
|
|
40
|
+
|
|
41
|
+
`Schema` is responsible for document structure and index declarations.
|
|
42
|
+
|
|
43
|
+
methods:
|
|
44
|
+
- `validate(payload)`
|
|
45
|
+
- `path(path_name)`
|
|
46
|
+
- `get_path_type(path_name)`
|
|
47
|
+
- `is_array_root(path_name)`
|
|
48
|
+
- `create_index(index_spec, index_options?)`
|
|
49
|
+
- `get_indexes()`
|
|
50
|
+
|
|
51
|
+
`create_index` supports:
|
|
52
|
+
- single-path index: `schema.create_index('profile.city')`
|
|
53
|
+
- compound index: `schema.create_index({name: 1, type: -1})`
|
|
54
|
+
- optional options: `unique` and `name`
|
|
55
|
+
|
|
56
|
+
Notes:
|
|
57
|
+
- Single-path string specs (`'profile.city'`) create GIN indexes and do not support `unique`.
|
|
58
|
+
- Object specs (`{field: 1}` / compound) create BTREE indexes and support `unique`.
|
|
59
|
+
|
|
60
|
+
Path-level sugar in schema definitions is also supported:
|
|
61
|
+
- `{name: {type: String, index: true}}` -> single-path index
|
|
62
|
+
- `{age: {type: Number, index: 1}}` -> ascending single-path spec
|
|
63
|
+
- `{age: {type: Number, index: -1}}` -> descending single-path spec
|
|
64
|
+
- `{email: {type: String, index: {unique: true, name: 'idx_users_email_unique'}}}` -> path-level index options
|
|
65
|
+
- optional direction in object form: `direction` or `order` (`1` or `-1`)
|
|
66
|
+
|
|
67
|
+
## model
|
|
68
|
+
|
|
69
|
+
`const User = model(user_schema, { table_name, data_column?, id_strategy?, auto_index? })`
|
|
70
|
+
|
|
71
|
+
- `table_name` is required.
|
|
72
|
+
- `data_column` defaults to `data`.
|
|
73
|
+
- `id_strategy` defaults to connection-level `id_strategy`, then library default `IdStrategies.bigserial`.
|
|
74
|
+
- `auto_index` defaults to model option if set, otherwise connection-level `auto_index`.
|
|
75
|
+
|
|
76
|
+
UUIDv7 notes:
|
|
77
|
+
- `IdStrategies.uuidv7` uses database-generated row IDs via table DDL (`id UUID PRIMARY KEY DEFAULT uuidv7()`).
|
|
78
|
+
- jsonbadger validates `uuidv7` support before uuidv7-backed table/schema/write paths using cached server capability metadata.
|
|
79
|
+
|
|
80
|
+
static methods:
|
|
81
|
+
- `ensure_table()`: manual migration call that ensures the table exists (for `IdStrategies.uuidv7`, table DDL uses `DEFAULT uuidv7()`); when `auto_index` is enabled, applies declared schema indexes once per model instance. First-write methods (`save()`, `update_one(...)`) can also create the table automatically.
|
|
82
|
+
- `ensure_index()`: ensures table exists and applies declared schema indexes immediately.
|
|
83
|
+
- `ensure_schema()`: ensures table and applies declared schema indexes.
|
|
84
|
+
- `find(query_filter, projection_value?)`
|
|
85
|
+
- `find_one(query_filter)`
|
|
86
|
+
- `count_documents(query_filter)`
|
|
87
|
+
- `update_one(query_filter, update_definition)`
|
|
88
|
+
- supported operators: `$set`, `$insert`, `$set_lax`
|
|
89
|
+
- operator application order is deterministic and nested: `$set` -> `$insert` -> `$set_lax`
|
|
90
|
+
- `$insert` maps to PostgreSQL `jsonb_insert(...)`
|
|
91
|
+
- `$set_lax` maps to PostgreSQL `jsonb_set_lax(...)` and supports object form `{ value, create_if_missing?, null_value_treatment? }`
|
|
92
|
+
- update paths allow numeric nested segments for JSON array indexes (for example, `tags.0`)
|
|
93
|
+
- conflicting update paths (same path or parent/child overlap across operators) are rejected before SQL execution
|
|
94
|
+
- `delete_one(query_filter)`
|
|
95
|
+
- deletes one matching row and returns the deleted document data
|
|
96
|
+
- returns `null` when no row matches
|
|
97
|
+
- does not implicitly call `ensure_table()`; if the table does not exist yet, returns `null`
|
|
98
|
+
|
|
99
|
+
instance methods:
|
|
100
|
+
- `validate()`
|
|
101
|
+
- `get(path_name, runtime_options?)`
|
|
102
|
+
- `set(path_name, value, runtime_options?)`
|
|
103
|
+
- `mark_modified(path_name)`
|
|
104
|
+
- `is_modified(path_name?)`
|
|
105
|
+
- `clear_modified()`
|
|
106
|
+
- `to_object(serialization_options?)`
|
|
107
|
+
- `to_json(serialization_options?)`
|
|
108
|
+
- `save()`
|
|
109
|
+
|
|
110
|
+
Runtime behavior notes:
|
|
111
|
+
- Read/query execution (`find(...).exec()`, `find_one(...).exec()`, `count_documents(...).exec()`) does not implicitly call `ensure_table()`.
|
|
112
|
+
- Use `ensure_table()` / `ensure_schema()` explicitly, or let a first write (`save()` / `update_one(...)`) create the table.
|
|
113
|
+
- `set(...)` applies field setters + casting by default and marks the path as modified.
|
|
114
|
+
- `mark_modified(path_name)` is required after in-place `Mixed`/`Date` mutations (for example mutating nested objects or calling `Date` mutator methods directly).
|
|
115
|
+
- `is_modified(path_name?)` and `clear_modified()` are available runtime helpers for dirty-path inspection/reset.
|
|
116
|
+
- `immutable: true` fields are writable before first persist and become protected after a successful `save()`.
|
|
117
|
+
- `to_object(...)` / `to_json(...)` apply getters by default; pass `{ getters: false }` to bypass getter transforms during serialization.
|
|
118
|
+
|
|
119
|
+
For built-in FieldType declaration examples and edge-case notes, see [`docs/examples.md`](examples.md).
|
|
120
|
+
|
|
121
|
+
For copy-paste usage examples (including all current query/update operators), see [`docs/examples.md`](examples.md). For PostgreSQL translation semantics, see [`docs/query-translation.md`](query-translation.md).
|
|
122
|
+
|
|
123
|
+
## query filters
|
|
124
|
+
|
|
125
|
+
Supported filter/operator families (current implemented set):
|
|
126
|
+
- scalar comparisons: direct equality, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`
|
|
127
|
+
- sets/arrays: `$in`, `$nin`, `$all`, `$size`, `$contains`, `$elem_match`
|
|
128
|
+
- regex: `$regex` (+ `$options`)
|
|
129
|
+
- JSONB key existence: `$has_key`, `$has_any_keys`, `$has_all_keys`
|
|
130
|
+
- JSONPath: `$json_path_exists`, `$json_path_match`
|
|
131
|
+
|
|
132
|
+
Existence semantics:
|
|
133
|
+
- `$has_key` / `$has_any_keys` / `$has_all_keys` map directly to PostgreSQL `?`, `?|`, and `?&`.
|
|
134
|
+
- These operators are top-level for the JSONB value on the left side of the operator.
|
|
135
|
+
- When used with a nested field path, jsonbadger extracts that nested JSONB value first, then applies the existence operator to that extracted value.
|
|
136
|
+
|
|
137
|
+
JSONPath notes:
|
|
138
|
+
- `$json_path_exists` maps to `@?` and binds the JSONPath string as a `::jsonpath` parameter.
|
|
139
|
+
- `$json_path_match` maps to `@@` and binds the JSONPath string as a `::jsonpath` parameter.
|
|
140
|
+
|
|
141
|
+
## query builder
|
|
142
|
+
|
|
143
|
+
builder chain:
|
|
144
|
+
- `.where(filter)`
|
|
145
|
+
- `.sort(sort_definition)`
|
|
146
|
+
- `.limit(limit_value)`
|
|
147
|
+
- `.skip(skip_value)`
|
|
148
|
+
- `.exec()`
|
|
149
|
+
|
|
150
|
+
## PostgreSQL capability map (summary)
|
|
151
|
+
|
|
152
|
+
See [`docs/query-translation.md`](query-translation.md) for the dedicated operator/function capability map, including update operators (`jsonb_set`, `jsonb_insert`, `jsonb_set_lax`) and indexability expectations for query operators over JSONB.
|