couchset 0.2.7 → 0.4.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/CHANGELOG.md +41 -0
- package/README.md +292 -152
- package/dist/connection/connection.d.ts +68 -0
- package/dist/connection/connection.js +495 -0
- package/dist/connection/connection.js.map +1 -0
- package/dist/connection/env.d.ts +2 -0
- package/dist/connection/env.js +16 -0
- package/dist/connection/env.js.map +1 -0
- package/dist/connection/index.d.ts +3 -0
- package/dist/connection/index.js +25 -0
- package/dist/connection/index.js.map +1 -0
- package/dist/connection/types.d.ts +25 -0
- package/dist/connection/types.js +3 -0
- package/dist/connection/types.js.map +1 -0
- package/dist/database.d.ts +9 -0
- package/dist/database.js +196 -0
- package/dist/database.js.map +1 -0
- package/dist/index.d.ts +20 -6
- package/dist/index.js +55 -8
- package/dist/index.js.map +1 -1
- package/dist/legacy/connection.d.ts +2 -0
- package/dist/legacy/connection.js +24 -0
- package/dist/legacy/connection.js.map +1 -0
- package/dist/legacy/index.d.ts +10 -0
- package/dist/legacy/index.js +93 -0
- package/dist/legacy/index.js.map +1 -0
- package/dist/legacy/model.d.ts +55 -0
- package/dist/legacy/model.js +282 -0
- package/dist/legacy/model.js.map +1 -0
- package/dist/legacy/pagination/index.d.ts +1 -0
- package/dist/legacy/pagination/index.js +18 -0
- package/dist/legacy/pagination/index.js.map +1 -0
- package/dist/legacy/pagination/pagination.d.ts +12 -0
- package/dist/legacy/pagination/pagination.js +85 -0
- package/dist/legacy/pagination/pagination.js.map +1 -0
- package/dist/legacy/search/customQuery.d.ts +13 -0
- package/dist/legacy/search/customQuery.js +73 -0
- package/dist/legacy/search/customQuery.js.map +1 -0
- package/dist/legacy/search/index.d.ts +1 -0
- package/dist/legacy/search/index.js +18 -0
- package/dist/legacy/search/index.js.map +1 -0
- package/dist/model/default-scope.d.ts +10 -0
- package/dist/model/default-scope.js +47 -0
- package/dist/model/default-scope.js.map +1 -0
- package/dist/model/hydrated-document.d.ts +30 -0
- package/dist/model/hydrated-document.js +118 -0
- package/dist/model/hydrated-document.js.map +1 -0
- package/dist/model/include.d.ts +33 -0
- package/dist/model/include.js +250 -0
- package/dist/model/include.js.map +1 -0
- package/dist/model/index.d.ts +192 -52
- package/dist/model/index.js +646 -172
- package/dist/model/index.js.map +1 -1
- package/dist/model/indexes.d.ts +18 -0
- package/dist/model/indexes.js +155 -0
- package/dist/model/indexes.js.map +1 -0
- package/dist/model/keyspace.d.ts +9 -0
- package/dist/model/keyspace.js +28 -0
- package/dist/model/keyspace.js.map +1 -0
- package/dist/model/read-helpers.d.ts +36 -0
- package/dist/model/read-helpers.js +275 -0
- package/dist/model/read-helpers.js.map +1 -0
- package/dist/model/safe-query.d.ts +23 -0
- package/dist/model/safe-query.js +138 -0
- package/dist/model/safe-query.js.map +1 -0
- package/dist/model/ttl.d.ts +9 -0
- package/dist/model/ttl.js +73 -0
- package/dist/model/ttl.js.map +1 -0
- package/dist/model/validation.d.ts +5 -0
- package/dist/model/validation.js +104 -0
- package/dist/model/validation.js.map +1 -0
- package/dist/model/write-helpers.d.ts +39 -0
- package/dist/model/write-helpers.js +245 -0
- package/dist/model/write-helpers.js.map +1 -0
- package/dist/next.d.ts +204 -0
- package/dist/next.js +1190 -0
- package/dist/next.js.map +1 -0
- package/dist/next.types.d.ts +1 -0
- package/dist/next.types.js +38 -0
- package/dist/next.types.js.map +1 -0
- package/dist/pagination/index.d.ts +1 -0
- package/dist/pagination/index.js +1 -0
- package/dist/pagination/index.js.map +1 -1
- package/dist/pagination/pagination.d.ts +1 -8
- package/dist/pagination/pagination.js +16 -29
- package/dist/pagination/pagination.js.map +1 -1
- package/dist/pagination/safe-pagination.d.ts +15 -0
- package/dist/pagination/safe-pagination.js +221 -0
- package/dist/pagination/safe-pagination.js.map +1 -0
- package/dist/pagination/types.d.ts +11 -0
- package/dist/pagination/types.js +3 -0
- package/dist/pagination/types.js.map +1 -0
- package/dist/search/customQuery.d.ts +10 -3
- package/dist/search/customQuery.js +17 -18
- package/dist/search/customQuery.js.map +1 -1
- package/dist/shared/common.model.js +2 -2
- package/dist/shared/common.model.js.map +1 -1
- package/dist/timeseries/index.d.ts +1 -0
- package/dist/timeseries/index.js +18 -0
- package/dist/timeseries/index.js.map +1 -0
- package/dist/timeseries/time-series-model.d.ts +60 -0
- package/dist/timeseries/time-series-model.js +267 -0
- package/dist/timeseries/time-series-model.js.map +1 -0
- package/dist/utils/awaitTo.d.ts +1 -1
- package/dist/utils/awaitTo.js +1 -1
- package/dist/utils/awaitTo.js.map +1 -1
- package/docs/beta-migration.md +175 -0
- package/docs/document-modeling.md +133 -0
- package/docs/next-primitives.md +81 -0
- package/legacy/index.js +1 -0
- package/legacy/package.json +4 -0
- package/next/index.js +1 -0
- package/next/package.json +4 -0
- package/package.json +18 -10
- package/dist/connection.d.ts +0 -42
- package/dist/connection.js +0 -156
- package/dist/connection.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 - 2026-05-18
|
|
4
|
+
|
|
5
|
+
### Compatibility
|
|
6
|
+
|
|
7
|
+
- Kept the default `couchset` entrypoint on the legacy API so existing projects can upgrade without rewriting model imports.
|
|
8
|
+
- Kept `couchset/legacy` as an explicit legacy alias.
|
|
9
|
+
- Added `couchset/next` for the modern API.
|
|
10
|
+
- Shared one Couchbase connection singleton across legacy and modern models, allowing both APIs to run side by side during gradual migrations.
|
|
11
|
+
|
|
12
|
+
### Modern API
|
|
13
|
+
|
|
14
|
+
- Added strict modern model helpers: `insert`, `upsert`, `getById`, `replaceById`, `patchById`, `deleteById`, `findMany`, `findOne`, `page`, `exists`, and `count`.
|
|
15
|
+
- Removed old model method names from the modern model surface so the new API can evolve independently.
|
|
16
|
+
- Added hydrated document helpers for reload/save/patch/delete workflows.
|
|
17
|
+
- Added soft-delete/default-scope helpers, TTL helpers, validation/parse hooks, scoped collection support, include helpers, and declarative index helpers.
|
|
18
|
+
- Added safe SQL++ query helpers with named/positional parameters: `queryRows`, `queryOne`, and `queryPage`.
|
|
19
|
+
- Added safer keyspace helpers: `Model.bucket()` and `Model.from(alias?)`.
|
|
20
|
+
|
|
21
|
+
### Connection Lifecycle
|
|
22
|
+
|
|
23
|
+
- Added lazy model binding so models can be constructed before `couchset()` connects.
|
|
24
|
+
- Added lifecycle helpers: `ready`, `ping`, `health`, `shutdown`, and `ensureIndexes`.
|
|
25
|
+
- Added reconnect support that is enabled by default, with `COUCHSET_RECONNECT` and `COUCHSET_RECONNECT_INTERVAL_MS` environment controls.
|
|
26
|
+
- Added retry/rebind behavior for reconnectable operation failures.
|
|
27
|
+
|
|
28
|
+
### Starters
|
|
29
|
+
|
|
30
|
+
- Added modern env-based starter helpers on `couchset/next`: `getConnectionOptions`, `connectionOptions`, `startCouchbase`, and `startCouchbaseServerless`.
|
|
31
|
+
- Starter helpers read `COUCHBASE_URL`, `COUCHBASE_BUCKET`, `COUCHBASE_USERNAME`, `COUCHBASE_PASSWORD`, and `COUCHBASE_PROXY`.
|
|
32
|
+
|
|
33
|
+
### Documentation
|
|
34
|
+
|
|
35
|
+
- Updated the README for the legacy-default and modern-next import paths.
|
|
36
|
+
- Added beta migration notes for running legacy and modern models together.
|
|
37
|
+
|
|
38
|
+
### Maintenance
|
|
39
|
+
|
|
40
|
+
- Cleared high and critical `npm audit` findings.
|
|
41
|
+
- Updated the publish workflow to the two-job version bump and npm publish flow.
|
package/README.md
CHANGED
|
@@ -1,236 +1,376 @@
|
|
|
1
|
-
|
|
2
1
|
<p align="center">
|
|
3
|
-
<h1 align="center">
|
|
2
|
+
<h1 align="center">CouchSet</h1>
|
|
4
3
|
</p>
|
|
5
4
|
|
|
6
|
-
|
|
7
5
|
<div align="center">
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<img alt="NPM" src="https://img.shields.io/npm/dt/couchset.svg"></img>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<img width="500px" src="./docs/couchset.png"></img>
|
|
6
|
+
<img alt="NPM" src="https://img.shields.io/npm/dt/couchset.svg"></img>
|
|
7
|
+
<br />
|
|
8
|
+
<img width="500px" src="./docs/couchset.png"></img>
|
|
19
9
|
</div>
|
|
20
10
|
|
|
11
|
+
CouchSet is a Couchbase model layer for TypeScript and Node.js. The default `couchset` entrypoint keeps the legacy API for safe upgrades; the modern API is available from `couchset/next`.
|
|
21
12
|
|
|
22
|
-
|
|
13
|
+
The additive client-owned primitives—typed definitions, explicit provisioning, transaction-bound models, CAS helpers, and safe index plans—are documented in [Next primitives](./docs/next-primitives.md).
|
|
23
14
|
|
|
24
15
|
- [Install](#install)
|
|
16
|
+
- [Legacy Default](#legacy-default)
|
|
17
|
+
- [Modern API](#modern-api)
|
|
18
|
+
- [Next primitives](./docs/next-primitives.md)
|
|
19
|
+
- [Connection Lifecycle](#connection-lifecycle)
|
|
20
|
+
- [Models](#models)
|
|
21
|
+
- [Reads](#reads)
|
|
22
|
+
- [Document Modeling](./docs/document-modeling.md)
|
|
23
|
+
- [Writes](#writes)
|
|
24
|
+
- [Queries](#queries)
|
|
25
|
+
- [Gradual Migration](#gradual-migration)
|
|
26
|
+
- [Migration Notes](./docs/beta-migration.md)
|
|
27
|
+
- [Changelog](./CHANGELOG.md)
|
|
25
28
|
- [License](#license)
|
|
26
29
|
|
|
27
|
-
##
|
|
30
|
+
## Install
|
|
31
|
+
|
|
28
32
|
```bash
|
|
29
33
|
npm i couchset --save
|
|
30
34
|
```
|
|
31
35
|
|
|
32
|
-
##
|
|
36
|
+
## Legacy Default
|
|
37
|
+
|
|
38
|
+
Existing projects can keep importing from `couchset` and continue using the old model methods while gradually migrating.
|
|
39
|
+
|
|
33
40
|
```ts
|
|
34
|
-
import {
|
|
41
|
+
import {couchset, Model} from 'couchset';
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
connectionString: 'couchbase://localhost',
|
|
38
|
-
username: 'admin',
|
|
39
|
-
password: '
|
|
40
|
-
bucketName: '
|
|
41
|
-
|
|
43
|
+
await couchset({
|
|
44
|
+
connectionString: process.env.COUCHBASE_URL || 'couchbase://localhost',
|
|
45
|
+
username: process.env.COUCHBASE_USERNAME || 'admin',
|
|
46
|
+
password: process.env.COUCHBASE_PASSWORD || '1234',
|
|
47
|
+
bucketName: process.env.COUCHBASE_BUCKET || 'dev',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const users = new Model('User', {schema: {createdAt: 'date'}});
|
|
51
|
+
|
|
52
|
+
const created = await users.create({
|
|
53
|
+
userId: 'ceddy',
|
|
54
|
+
email: 'ceddy@example.com',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const found = await users.findById(created.id);
|
|
58
|
+
await users.updateById(created.id, {...found, email: 'new@example.com'});
|
|
59
|
+
await users.delete(created.id);
|
|
42
60
|
```
|
|
43
61
|
|
|
44
|
-
##
|
|
62
|
+
## Modern API
|
|
63
|
+
|
|
64
|
+
New code can opt into the modern API with `couchset/next`.
|
|
45
65
|
|
|
46
66
|
```ts
|
|
47
|
-
import { Model
|
|
67
|
+
import {couchset, Model} from 'couchset/next';
|
|
68
|
+
|
|
69
|
+
type User = {
|
|
70
|
+
userId: string;
|
|
71
|
+
email?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const users = new Model('User', {
|
|
75
|
+
schema: {
|
|
76
|
+
createdAt: 'date',
|
|
77
|
+
updatedAt: 'date',
|
|
78
|
+
},
|
|
79
|
+
indexes: [
|
|
80
|
+
{
|
|
81
|
+
name: 'idx_user_userId',
|
|
82
|
+
fields: ['userId'],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
48
86
|
|
|
49
|
-
|
|
87
|
+
await couchset({
|
|
88
|
+
connectionString: process.env.COUCHBASE_URL || 'couchbase://localhost',
|
|
89
|
+
username: process.env.COUCHBASE_USERNAME || 'admin',
|
|
90
|
+
password: process.env.COUCHBASE_PASSWORD || '1234',
|
|
91
|
+
bucketName: process.env.COUCHBASE_BUCKET || 'dev',
|
|
92
|
+
});
|
|
50
93
|
|
|
94
|
+
await couchset.ready();
|
|
51
95
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
password: 'love couchbase',
|
|
96
|
+
const created = await users.insert<User>({
|
|
97
|
+
userId: 'ceddy',
|
|
98
|
+
email: 'ceddy@example.com',
|
|
56
99
|
});
|
|
57
100
|
|
|
58
|
-
|
|
59
|
-
const foundData = await userModel.findById(created.id);
|
|
101
|
+
const found = await users.getById<User>(created.id);
|
|
60
102
|
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
const patched = await users.patchById<User>(created.id, {
|
|
104
|
+
$set: {email: 'new@example.com'},
|
|
105
|
+
});
|
|
63
106
|
|
|
64
|
-
|
|
65
|
-
|
|
107
|
+
const page = await users.page<User>({
|
|
108
|
+
where: {userId: {$eq: 'ceddy'}},
|
|
109
|
+
limit: 25,
|
|
110
|
+
page: 0,
|
|
111
|
+
});
|
|
66
112
|
|
|
113
|
+
await users.deleteById(created.id, {hard: true});
|
|
67
114
|
```
|
|
68
115
|
|
|
69
|
-
##
|
|
116
|
+
## Connection Lifecycle
|
|
117
|
+
|
|
118
|
+
Models can be declared before connecting. Model operations wait for the shared connection before binding to the Couchbase bucket and collection.
|
|
70
119
|
|
|
71
|
-
All models come with a method for automatic pagination
|
|
72
120
|
```ts
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
121
|
+
import {couchset, health, ping, ready, shutdown} from 'couchset/next';
|
|
122
|
+
|
|
123
|
+
await couchset({
|
|
124
|
+
connectionString: 'couchbase://localhost',
|
|
125
|
+
username: 'admin',
|
|
126
|
+
password: '1234',
|
|
127
|
+
bucketName: 'dev',
|
|
128
|
+
autoReconnect: true,
|
|
129
|
+
reconnectIntervalMs: 5000,
|
|
81
130
|
});
|
|
131
|
+
|
|
132
|
+
await ready();
|
|
133
|
+
await ping();
|
|
134
|
+
console.log(health());
|
|
135
|
+
|
|
136
|
+
await shutdown();
|
|
82
137
|
```
|
|
83
138
|
|
|
84
|
-
|
|
139
|
+
Reconnect is enabled by default. Environment flags:
|
|
140
|
+
|
|
141
|
+
- `COUCHSET_RECONNECT`: use `false`, `0`, or `no` to disable reconnect.
|
|
142
|
+
- `COUCHSET_RECONNECT_INTERVAL_MS`: reconnect and health-check interval in milliseconds. Default is `5000`.
|
|
143
|
+
|
|
144
|
+
The modern entrypoint also exports app starter helpers that read Couchbase credentials from env:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import {startCouchbase, startCouchbaseServerless} from 'couchset/next';
|
|
85
148
|
|
|
86
|
-
|
|
87
|
-
|
|
149
|
+
await startCouchbase();
|
|
150
|
+
await startCouchbaseServerless();
|
|
88
151
|
```
|
|
89
152
|
|
|
153
|
+
The starters read `COUCHBASE_URL`, `COUCHBASE_BUCKET`, `COUCHBASE_USERNAME`, `COUCHBASE_PASSWORD`, and `COUCHBASE_PROXY`. You can pass any `CouchsetArgs` field as an override.
|
|
90
154
|
|
|
91
|
-
|
|
155
|
+
## Models
|
|
92
156
|
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
id: '1392e4f6-ae1e-4e01-b7d5-103bdd0e843f',
|
|
106
|
-
updatedAt: 2021-01-26T01:51:29.591Z,
|
|
107
|
-
createdAt: 2021-01-26T01:51:29.583Z,
|
|
108
|
-
_type: 'User',
|
|
109
|
-
userId: 'ceddy',
|
|
110
|
-
password: '...',
|
|
111
|
-
someValue: 'x'
|
|
112
|
-
}
|
|
113
|
-
]
|
|
157
|
+
```ts
|
|
158
|
+
const auditEvents = new Model('AuditEvent', {
|
|
159
|
+
scope: 'app',
|
|
160
|
+
collection: 'events',
|
|
161
|
+
softDelete: true,
|
|
162
|
+
defaultWhere: {tenantId: {$eq: 'tenant-1'}},
|
|
163
|
+
dateFields: ['profile.createdAt'],
|
|
164
|
+
validateCreate: (doc) => doc,
|
|
165
|
+
validateReplace: (doc) => doc,
|
|
166
|
+
parse: (doc) => doc,
|
|
167
|
+
});
|
|
114
168
|
```
|
|
115
169
|
|
|
170
|
+
Useful model helpers:
|
|
116
171
|
|
|
117
|
-
|
|
118
|
-
|
|
172
|
+
```ts
|
|
173
|
+
users.bucket(); // `dev`
|
|
174
|
+
users.keyspace(); // `dev` or default:`dev`.`scope`.`collection`
|
|
175
|
+
users.from('u'); // `dev` AS u
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
For large objects, keep list metadata and heavy payloads in separate models.
|
|
179
|
+
See [Document Modeling](./docs/document-modeling.md) for the recommended read
|
|
180
|
+
shape.
|
|
181
|
+
|
|
182
|
+
## Reads
|
|
119
183
|
|
|
120
184
|
```ts
|
|
121
|
-
|
|
185
|
+
await users.getById<User>('user::1');
|
|
186
|
+
await users.findByIdWithMeta<User>('user::1');
|
|
187
|
+
|
|
188
|
+
await users.findMany<User>({
|
|
189
|
+
select: ['id', 'userId', 'email'],
|
|
190
|
+
where: {userId: {$eq: 'ceddy'}},
|
|
191
|
+
orderBy: {createdAt: 'DESC'},
|
|
192
|
+
limit: 10,
|
|
193
|
+
});
|
|
122
194
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
$count: {
|
|
127
|
-
$field: {
|
|
128
|
-
name: 'type',
|
|
129
|
-
},
|
|
130
|
-
as: 'odm',
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
let: [
|
|
135
|
-
{ key: 'amount_val', value: 10 },
|
|
136
|
-
{ key: 'size_val', value: 20 },
|
|
137
|
-
],
|
|
138
|
-
where: {
|
|
139
|
-
$or: [{ price: { $gt: 'amount_val', $isNotNull: true } }, { auto: { $gt: 10 } }, { amount: 10 }],
|
|
140
|
-
$and: [
|
|
141
|
-
{ price2: { $gt: 1.99, $isNotNull: true } },
|
|
142
|
-
{ $or: [{ price3: { $gt: 1.99, $isNotNull: true } }, { id: '20' }] },
|
|
143
|
-
],
|
|
144
|
-
$any: {
|
|
145
|
-
$expr: [{ $in: { search_expr: 'search', target_expr: 'address' } }],
|
|
146
|
-
$satisfied: { address: '10' },
|
|
147
|
-
},
|
|
148
|
-
$in: { search_expr: 'search', target_expr: ['address'] },
|
|
149
|
-
},
|
|
150
|
-
groupBy: [{ expr: 'type', as: 'sch' }],
|
|
151
|
-
letting: [
|
|
152
|
-
{ key: 'amount_v2', value: 10 },
|
|
153
|
-
{ key: 'size_v2', value: 20 },
|
|
154
|
-
],
|
|
155
|
-
having: { type: { $like: '%hotel%' } },
|
|
156
|
-
orderBy: { type: 'DESC' },
|
|
157
|
-
limit: 10,
|
|
158
|
-
offset: 1,
|
|
159
|
-
use: ['airlineR_8093', 'airlineR_8094'],
|
|
160
|
-
};
|
|
195
|
+
await users.findOne<User>({where: {email: {$eq: 'ceddy@example.com'}}});
|
|
196
|
+
await users.exists({where: {userId: {$eq: 'ceddy'}}});
|
|
197
|
+
await users.count({where: {userId: {$eq: 'ceddy'}}});
|
|
161
198
|
|
|
162
|
-
const
|
|
163
|
-
|
|
199
|
+
const result = await users.page<User>({
|
|
200
|
+
where: {userId: {$eq: 'ceddy'}},
|
|
201
|
+
limit: 10,
|
|
202
|
+
page: 0,
|
|
203
|
+
});
|
|
164
204
|
|
|
205
|
+
result.items;
|
|
206
|
+
result.hasNext;
|
|
207
|
+
result.pageInfo.nextPage;
|
|
165
208
|
```
|
|
166
209
|
|
|
167
|
-
|
|
210
|
+
Hydrated documents:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const doc = await users.findDocById<User & {id: string}>('user::1');
|
|
168
214
|
|
|
169
|
-
|
|
170
|
-
|
|
215
|
+
doc.email = 'updated@example.com';
|
|
216
|
+
await doc.save();
|
|
217
|
+
await doc.patch({$set: {verified: true}});
|
|
218
|
+
await doc.reload();
|
|
219
|
+
await doc.delete({hard: true});
|
|
171
220
|
```
|
|
172
221
|
|
|
222
|
+
Soft delete scopes:
|
|
173
223
|
|
|
224
|
+
```ts
|
|
225
|
+
await users.softDeleteById<User>('user::1');
|
|
226
|
+
await users.restoreById<User>('user::1');
|
|
227
|
+
|
|
228
|
+
await users.withDeleted().findMany<User>();
|
|
229
|
+
await users.onlyDeleted().findMany<User>();
|
|
230
|
+
await users.withoutDefaultWhere().findMany<User>();
|
|
231
|
+
```
|
|
174
232
|
|
|
175
|
-
|
|
233
|
+
## Writes
|
|
176
234
|
|
|
177
235
|
```ts
|
|
178
|
-
|
|
236
|
+
await users.insert<User>({id: 'user::1', userId: 'ceddy'});
|
|
237
|
+
await users.upsert<User>({id: 'user::1', userId: 'ceddy'});
|
|
238
|
+
await users.replaceById<User>('user::1', {userId: 'ceddy', email: 'new@example.com'});
|
|
239
|
+
await users.patchById<User>('user::1', {
|
|
240
|
+
$set: {email: 'new@example.com'},
|
|
241
|
+
$inc: {loginCount: 1},
|
|
242
|
+
$unset: ['temporaryCode'],
|
|
243
|
+
});
|
|
244
|
+
await users.incrementById<User>('user::1', 'loginCount', 1);
|
|
245
|
+
await users.deleteById('user::1', {hard: true});
|
|
246
|
+
```
|
|
179
247
|
|
|
180
|
-
|
|
181
|
-
// queryresults = { rows: object[], meta: any}
|
|
248
|
+
TTL helpers:
|
|
182
249
|
|
|
250
|
+
```ts
|
|
251
|
+
await users.insert<User>({id: 'user::1', userId: 'ceddy'}, {ttl: '2h'});
|
|
252
|
+
await users.upsert<User>({id: 'user::2', userId: 'ceddy'}, {ttlSeconds: 300});
|
|
183
253
|
```
|
|
184
254
|
|
|
185
|
-
##
|
|
255
|
+
## Queries
|
|
186
256
|
|
|
187
|
-
|
|
257
|
+
Modern query helpers pass SDK parameters correctly and throw on failures by default.
|
|
188
258
|
|
|
189
259
|
```ts
|
|
190
|
-
const
|
|
260
|
+
const rows = await users.queryRows<User>(
|
|
261
|
+
`SELECT u.* FROM ${users.from('u')} WHERE u.userId=$userId LIMIT $limit`,
|
|
262
|
+
{userId: 'ceddy', limit: 10}
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const first = await users.queryOne<User>(
|
|
266
|
+
`SELECT u.* FROM ${users.from('u')} WHERE u.email=$email LIMIT 1`,
|
|
267
|
+
{email: 'ceddy@example.com'}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const page = await users.queryPage<User>(
|
|
271
|
+
`SELECT u.* FROM ${users.from('u')} WHERE u.userId=$userId LIMIT $limit`,
|
|
272
|
+
{userId: 'ceddy', limit: 10}
|
|
273
|
+
);
|
|
191
274
|
```
|
|
192
275
|
|
|
193
|
-
|
|
276
|
+
Model read helpers also throw by default. Use `throwOnError: false` only when an empty fallback is intentional.
|
|
194
277
|
|
|
278
|
+
```ts
|
|
279
|
+
const rows = await users.findMany<User>({
|
|
280
|
+
where: {userId: {$eq: 'ceddy'}},
|
|
281
|
+
throwOnError: false,
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Indexes
|
|
195
286
|
|
|
196
287
|
```ts
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
288
|
+
const users = new Model('User', {
|
|
289
|
+
indexes: [
|
|
290
|
+
{
|
|
291
|
+
name: 'idx_user_email',
|
|
292
|
+
fields: ['email'],
|
|
293
|
+
where: {deleted: {$isNotValued: true}},
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
});
|
|
203
297
|
|
|
298
|
+
await users.ensureIndexes();
|
|
299
|
+
await couchset.ensureIndexes();
|
|
204
300
|
```
|
|
205
301
|
|
|
206
|
-
##
|
|
207
|
-
TODO
|
|
302
|
+
## Includes
|
|
208
303
|
|
|
304
|
+
```ts
|
|
305
|
+
const posts = new Model('Post');
|
|
306
|
+
|
|
307
|
+
const rows = await posts.findMany({
|
|
308
|
+
where: {published: {$eq: true}},
|
|
309
|
+
include: [
|
|
310
|
+
{
|
|
311
|
+
as: 'author',
|
|
312
|
+
model: users,
|
|
313
|
+
key: 'authorId',
|
|
314
|
+
type: 'leftJoin',
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
});
|
|
318
|
+
```
|
|
209
319
|
|
|
320
|
+
## Time Series
|
|
210
321
|
|
|
322
|
+
```ts
|
|
323
|
+
import {TimeSeriesModel} from 'couchset/next';
|
|
211
324
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
325
|
+
const metrics = new TimeSeriesModel('Metric', {
|
|
326
|
+
keyField: 'deviceId',
|
|
327
|
+
timeField: 'timestamp',
|
|
328
|
+
values: [{field: 'temperature'}],
|
|
329
|
+
interval: '1m',
|
|
330
|
+
});
|
|
215
331
|
|
|
216
|
-
|
|
217
|
-
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
- Automated indexes (only couchbase enterprise) ✅
|
|
221
|
-
- Geospatial queries ✅
|
|
222
|
-
- FTS queries ✅
|
|
332
|
+
await metrics.appendChunk('device-1', [
|
|
333
|
+
{deviceId: 'device-1', timestamp: Date.now(), temperature: 21.5},
|
|
334
|
+
]);
|
|
335
|
+
```
|
|
223
336
|
|
|
337
|
+
## Gradual Migration
|
|
224
338
|
|
|
339
|
+
Use `couchset` for old code and `couchset/next` for new code. Both model APIs share the same connection singleton, reconnect loop, and health state, so you can migrate one model or file at a time without opening a second Couchbase cluster connection.
|
|
225
340
|
|
|
341
|
+
```ts
|
|
342
|
+
import {couchset, Model} from 'couchset';
|
|
343
|
+
import {Model as NextModel} from 'couchset/next';
|
|
226
344
|
|
|
227
|
-
|
|
228
|
-
<img height="300px" src="./docs/couch.png"></img>
|
|
229
|
-
</div>
|
|
345
|
+
await couchset(args);
|
|
230
346
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
347
|
+
const legacyUsers = new Model('User');
|
|
348
|
+
const nextUsers = new NextModel('User');
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`couchset/legacy` remains available as an explicit alias for the default legacy API.
|
|
352
|
+
|
|
353
|
+
Modern replacements:
|
|
354
|
+
|
|
355
|
+
| Old method | Modern method |
|
|
356
|
+
| --- | --- |
|
|
357
|
+
| `create()` | `insert()` or `upsert()` |
|
|
358
|
+
| `findById()` | `getById()` |
|
|
359
|
+
| `updateById()` / `save()` | `replaceById()` or `patchById()` |
|
|
360
|
+
| `delete()` | `deleteById()` |
|
|
361
|
+
| `pagination()` | `findMany()` or `page()` |
|
|
362
|
+
| `customQuery()` | `queryRows()`, `queryOne()`, or `queryPage()` |
|
|
363
|
+
|
|
364
|
+
## Local Tests
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
npm run build
|
|
368
|
+
npm test
|
|
369
|
+
npm run test:serverless
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Set `COUCHBASE_URL`, `COUCHBASE_BUCKET`, `COUCHBASE_USERNAME`, and `COUCHBASE_PASSWORD` to point the integration tests at a local Couchbase instance.
|
|
373
|
+
|
|
374
|
+
## License
|
|
234
375
|
|
|
235
|
-
|
|
236
|
-
Couchset is [MIT licensed](./LICENSE).
|
|
376
|
+
CouchSet is [MIT licensed](./LICENSE).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Bucket, Cluster, Collection } from 'couchbase';
|
|
2
|
+
import { ConnectionHealth, ConnectionState, CouchsetArgs } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* CouchbaseConnection class
|
|
5
|
+
* Only one CouchbaseConnection can exist that's why it's a singleton
|
|
6
|
+
*/
|
|
7
|
+
export declare class CouchbaseConnection implements CouchsetArgs {
|
|
8
|
+
private static _instance;
|
|
9
|
+
bucket: Bucket;
|
|
10
|
+
cluster: Cluster;
|
|
11
|
+
private connectionPromise?;
|
|
12
|
+
private connectionSettings?;
|
|
13
|
+
private reconnectPromise?;
|
|
14
|
+
private reconnectReject?;
|
|
15
|
+
private reconnectTimer?;
|
|
16
|
+
private healthTimer?;
|
|
17
|
+
private connectionState;
|
|
18
|
+
private autoReconnectEnabled;
|
|
19
|
+
private reconnectDelayMs;
|
|
20
|
+
private manuallyClosed;
|
|
21
|
+
private lastConnectionError?;
|
|
22
|
+
connectionString: string;
|
|
23
|
+
bucketName: string;
|
|
24
|
+
username: string;
|
|
25
|
+
password: string;
|
|
26
|
+
static get Instance(): CouchbaseConnection;
|
|
27
|
+
private constructor();
|
|
28
|
+
private configureReconnect;
|
|
29
|
+
private normalizeArgs;
|
|
30
|
+
private sameSettings;
|
|
31
|
+
private assignSettings;
|
|
32
|
+
private settingsError;
|
|
33
|
+
private connectionOptions;
|
|
34
|
+
private clearReconnectTimer;
|
|
35
|
+
private clearHealthTimer;
|
|
36
|
+
private scheduleTimer;
|
|
37
|
+
private rawPing;
|
|
38
|
+
private openCluster;
|
|
39
|
+
private scheduleHealthCheck;
|
|
40
|
+
private connectWithSettings;
|
|
41
|
+
private startReconnect;
|
|
42
|
+
/**
|
|
43
|
+
* start
|
|
44
|
+
*/
|
|
45
|
+
init: (args: CouchsetArgs) => Promise<CouchbaseConnection>;
|
|
46
|
+
/**
|
|
47
|
+
* start serverless start
|
|
48
|
+
*/
|
|
49
|
+
initServerless: (args: CouchsetArgs) => Promise<CouchbaseConnection>;
|
|
50
|
+
/**
|
|
51
|
+
* getCollection
|
|
52
|
+
*/
|
|
53
|
+
getCollection: (scopeName?: string, collectionName?: string) => Collection;
|
|
54
|
+
getCluster: () => Cluster;
|
|
55
|
+
getBucket: () => string;
|
|
56
|
+
isConnected: () => boolean;
|
|
57
|
+
ready: () => Promise<CouchbaseConnection>;
|
|
58
|
+
ping: () => Promise<any>;
|
|
59
|
+
state: () => ConnectionState;
|
|
60
|
+
health: () => ConnectionHealth;
|
|
61
|
+
shouldReconnect: (error: unknown) => boolean;
|
|
62
|
+
markDisconnected: (error?: unknown) => void;
|
|
63
|
+
/**
|
|
64
|
+
* shutdown cluster
|
|
65
|
+
*/
|
|
66
|
+
shutdown: () => Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
export default CouchbaseConnection;
|