@thadeu/ndb 0.1.1 → 0.2.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/README.md +39 -41
- package/dist/index.cjs +110 -27
- package/dist/index.d.cts +50 -20
- package/dist/index.d.ts +50 -20
- package/dist/index.js +107 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,10 +51,8 @@ await ndb.close()
|
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
53
|
const calls = ndb.model('calls', {
|
|
54
|
-
id: 'call_id',
|
|
55
54
|
time: 'at',
|
|
56
55
|
fields: {
|
|
57
|
-
call_id: { type: 'string' },
|
|
58
56
|
status: { type: 'string', index: true },
|
|
59
57
|
caller: { type: 'string', unique: true },
|
|
60
58
|
duration: { type: 'number', optional: true },
|
|
@@ -66,10 +64,10 @@ const calls = ndb.model('calls', {
|
|
|
66
64
|
|
|
67
65
|
- Field types: `string`, `number`, `boolean`, `date` (a `Date`), `json` (any value).
|
|
68
66
|
- Field options: `index`, `unique`, `optional`.
|
|
69
|
-
- `
|
|
67
|
+
- `_id` is the primary key of every document. It is not a schema field: leave it out on insert and the daemon makes a UUIDv7 (or use `newId()`). For a natural key (email, SKU), keep `_id` and mark that field `unique`.
|
|
70
68
|
- `time` is the collection time field. Used by zone maps and `$dateTrunc`.
|
|
71
69
|
- The model declares its collection on first use (`ensure_collection`). You can call `await calls.ready()` to do it at once.
|
|
72
|
-
- Types come from the schema. Rows have the schema fields and `_v`, the document version. A `date` field is a `Date` in rows. Input takes `Date` values and ISO strings in filters.
|
|
70
|
+
- Types come from the schema. Rows have the schema fields, `_id` and `_v`, the document version. A `date` field is a `Date` in rows. Input takes `Date` values and ISO strings in filters.
|
|
73
71
|
- `expires` is the `date` field that holds when a document expires. It turns on `expireIn`, `expire`, `persist` and `ttl` (below).
|
|
74
72
|
- The client has no `ttl` catalog option and no blob API. Use the HTTP API for those.
|
|
75
73
|
|
|
@@ -78,51 +76,51 @@ const calls = ndb.model('calls', {
|
|
|
78
76
|
```ts
|
|
79
77
|
// Insert one or many. All documents go in one transaction.
|
|
80
78
|
const results = await calls.insert([
|
|
81
|
-
{
|
|
82
|
-
{
|
|
79
|
+
{ _id: 'c1', status: 'answered', caller: 'a', duration: 30, at: new Date() },
|
|
80
|
+
{ _id: 'c2', status: 'failed', caller: 'b', at: new Date() },
|
|
83
81
|
])
|
|
84
|
-
// results: [{
|
|
82
|
+
// results: [{ _id: 'c1', v: 1 }, ...]
|
|
85
83
|
|
|
86
|
-
//
|
|
87
|
-
const row = await calls.
|
|
84
|
+
// Get by _id (GET /v1/c/calls/c1). Returns null when missing.
|
|
85
|
+
const row = await calls.get('c1')
|
|
88
86
|
|
|
89
|
-
//
|
|
90
|
-
|
|
87
|
+
// Atomic update by filter: filter first, then the write operators.
|
|
88
|
+
// _v in the filter is an optimistic lock.
|
|
89
|
+
const res = await calls.update({ _id: 'c1', _v: row!._v }, { $set: { status: 'done' }, $unset: { duration: true } })
|
|
90
|
+
// res: { matched, modified, doc } (doc is the new row with _v; absent for many)
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
await calls.update('c1', {
|
|
92
|
+
// A string is an _id (POST /{_id}/update). The same guard rules apply.
|
|
93
|
+
await calls.update('c1', { $inc: { duration: 1 } })
|
|
94
94
|
|
|
95
|
-
//
|
|
96
|
-
await
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
`update` returns `{ id, v }` with the new version. A stale `ifVersion` throws `VersionConflictError`:
|
|
95
|
+
// A guarded decrement: exactly one of two racing calls wins.
|
|
96
|
+
await stock.update({ _id: 'a', qty: { $gte: 1 } }, { $inc: { qty: -1 } })
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
// Every match (the filter needs _id or an indexed field), or create it.
|
|
99
|
+
await calls.update({ status: 'failed' }, { $set: { status: 'retry' } }, { many: true })
|
|
100
|
+
await calls.update('c9', { $inc: { duration: 1 } }, { upsert: true })
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
if (err instanceof VersionConflictError) {
|
|
108
|
-
// Someone else wrote first. Read again and retry.
|
|
109
|
-
}
|
|
110
|
-
}
|
|
102
|
+
// Delete by _id or by filter: { deleted }.
|
|
103
|
+
await calls.delete('c2')
|
|
104
|
+
await calls.delete({ status: 'failed' }, { many: true })
|
|
111
105
|
```
|
|
112
106
|
|
|
113
|
-
|
|
107
|
+
- Operators: `$set`, `$setOnInsert` (like `$set`, but only when an upsert creates the document), `$unset`, `$inc`, `$push` (with `{ $each: [...] }`), `$pull`, `$min`, `$max`. Field names can be dot paths.
|
|
108
|
+
- Nothing matched is not an error: `matched` is 0 (for example when `_v` is stale, so read again and retry). `modified` is 0 when the operators leave the document as it is.
|
|
109
|
+
- `many` needs `_id` or an indexed field in the filter, and does not apply to an `_id` string. `upsert` creates the document from the equality fields of the filter (or with that `_id`).
|
|
110
|
+
- A key of the operators without `$` throws `InvalidUpdateError` before anything is sent. The daemon contract errors (`unknown_operator`, `no_update`, `conflicting_update`, `invalid_field`, `unindexed_many`, `empty_filter`) are `InvalidUpdateError` too.
|
|
111
|
+
|
|
112
|
+
An insert with a duplicate `_id` throws `DuplicateIdError`. A duplicate `unique` value throws `UniqueViolationError`. An empty or non-string `_id` fails with code `invalid_id`.
|
|
114
113
|
|
|
115
114
|
### Expiry (TTL like Redis)
|
|
116
115
|
|
|
117
116
|
```ts
|
|
118
117
|
const sessions = db.model('sessions', {
|
|
119
|
-
id: 'sid',
|
|
120
118
|
expires: 'exp',
|
|
121
|
-
fields: {
|
|
119
|
+
fields: { exp: { type: 'date', optional: true } },
|
|
122
120
|
})
|
|
123
121
|
|
|
124
|
-
await sessions.insert({
|
|
125
|
-
await sessions.update('s1', { user: 'u' }, { expireIn: '30m' })
|
|
122
|
+
await sessions.insert({ _id: 's1' }, { expireIn: '1h' }) // or 3600; all docs of the call
|
|
123
|
+
await sessions.update('s1', { $set: { user: 'u' } }, { expireIn: '30m' })
|
|
126
124
|
|
|
127
125
|
const { v, expiresAt } = await sessions.expire('s1', '30m') // EXPIRE
|
|
128
126
|
await sessions.expireAt('s1', new Date(Date.now() + 3_600_000))
|
|
@@ -144,8 +142,8 @@ const rows = await calls
|
|
|
144
142
|
.where({ caller: { $in: ['a', 'b'] } })
|
|
145
143
|
.not({ caller: 'zzz' })
|
|
146
144
|
.or({ duration: { $lt: 5 } }, { 'meta.agent.id': 7 })
|
|
147
|
-
.sort({ at: -1,
|
|
148
|
-
.fields('
|
|
145
|
+
.sort({ at: -1, _id: 1 })
|
|
146
|
+
.fields('_id', 'status')
|
|
149
147
|
.limit(50)
|
|
150
148
|
.skip(100)
|
|
151
149
|
```
|
|
@@ -170,11 +168,11 @@ const total = await calls.count()
|
|
|
170
168
|
Offset pages get slower as `skip` grows. Use a cursor for deep pages.
|
|
171
169
|
|
|
172
170
|
```ts
|
|
173
|
-
const q = calls.where({ status: 'answered' }).sort({
|
|
171
|
+
const q = calls.where({ status: 'answered' }).sort({ _id: 1 }).limit(100)
|
|
174
172
|
const page = await q
|
|
175
173
|
const token = q.nextCursor // '' on the last page
|
|
176
174
|
|
|
177
|
-
const next = await calls.where({ status: 'answered' }).sort({
|
|
175
|
+
const next = await calls.where({ status: 'answered' }).sort({ _id: 1 }).limit(100).after(token)
|
|
178
176
|
```
|
|
179
177
|
|
|
180
178
|
A token works only with the same `where` and `sort`. Another query shape throws `CursorError`.
|
|
@@ -182,8 +180,8 @@ A token works only with the same `where` and `sort`. Another query shape throws
|
|
|
182
180
|
To walk every row, use `iterate()`. It follows the cursor, with `limit` as the batch size:
|
|
183
181
|
|
|
184
182
|
```ts
|
|
185
|
-
for await (const row of calls.where({ status: 'answered' }).sort({
|
|
186
|
-
console.log(row.
|
|
183
|
+
for await (const row of calls.where({ status: 'answered' }).sort({ _id: 1 }).limit(500).iterate()) {
|
|
184
|
+
console.log(row._id)
|
|
187
185
|
}
|
|
188
186
|
```
|
|
189
187
|
|
|
@@ -238,7 +236,7 @@ const controller = new AbortController()
|
|
|
238
236
|
|
|
239
237
|
try {
|
|
240
238
|
for await (const ev of calls.watch('relay', { where: { status: 'answered' }, signal: controller.signal })) {
|
|
241
|
-
// ev: { consumer, pos, op: 'put' | 'del',
|
|
239
|
+
// ev: { consumer, pos, op: 'put' | 'del', _id, v, doc }
|
|
242
240
|
await handle(ev)
|
|
243
241
|
|
|
244
242
|
await calls.ack(ev)
|
|
@@ -268,7 +266,6 @@ Daemon errors are `NdbError` or a subclass. They have `code` (the daemon code) a
|
|
|
268
266
|
|---|---|
|
|
269
267
|
| `UnauthorizedError` | `unauthorized` |
|
|
270
268
|
| `NotFoundError` | `not_found`, `unknown_collection` |
|
|
271
|
-
| `VersionConflictError` | `version_conflict` |
|
|
272
269
|
| `DuplicateIdError` | `duplicate_id` |
|
|
273
270
|
| `UniqueViolationError` | `unique_violation` |
|
|
274
271
|
| `InvalidQueryError` | `invalid_query`, `invalid_pipeline` |
|
|
@@ -276,13 +273,14 @@ Daemon errors are `NdbError` or a subclass. They have `code` (the daemon code) a
|
|
|
276
273
|
| `WatchLaggedError` | `watch_lagged` |
|
|
277
274
|
| `HistoryTruncatedError` | `history_truncated` |
|
|
278
275
|
| `TtlError` | `ttl_not_enabled`, `ttl_derived` |
|
|
276
|
+
| `InvalidUpdateError` | `unknown_operator`, `no_update`, `conflicting_update`, `invalid_field`, `unindexed_many`, `empty_filter`, `invalid_update` |
|
|
279
277
|
| `NdbError` | every other code: `not_ready`, `catalog_conflict`, `catalog_invalid`, `direct_collection`, `invalid_request`, `group_limit_exceeded`, `request_too_large`, `line_too_large`, `closed`, `invalid_position`, `http_status`, ... |
|
|
280
278
|
|
|
281
279
|
```ts
|
|
282
280
|
import { NdbError } from '@thadeu/ndb'
|
|
283
281
|
|
|
284
282
|
try {
|
|
285
|
-
await calls.
|
|
283
|
+
await calls.get('c1')
|
|
286
284
|
} catch (err) {
|
|
287
285
|
if (err instanceof NdbError && err.code === 'not_ready') {
|
|
288
286
|
// The daemon is still recovering, and readyTimeout ran out.
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
DuplicateIdError: () => DuplicateIdError,
|
|
25
25
|
HistoryTruncatedError: () => HistoryTruncatedError,
|
|
26
26
|
InvalidQueryError: () => InvalidQueryError,
|
|
27
|
+
InvalidUpdateError: () => InvalidUpdateError,
|
|
27
28
|
Model: () => Model,
|
|
28
29
|
Ndb: () => Ndb,
|
|
29
30
|
NdbError: () => NdbError,
|
|
@@ -32,10 +33,10 @@ __export(index_exports, {
|
|
|
32
33
|
TtlError: () => TtlError,
|
|
33
34
|
UnauthorizedError: () => UnauthorizedError,
|
|
34
35
|
UniqueViolationError: () => UniqueViolationError,
|
|
35
|
-
VersionConflictError: () => VersionConflictError,
|
|
36
36
|
WatchLaggedError: () => WatchLaggedError,
|
|
37
37
|
connect: () => connect,
|
|
38
|
-
makeError: () => makeError
|
|
38
|
+
makeError: () => makeError,
|
|
39
|
+
newId: () => newId
|
|
39
40
|
});
|
|
40
41
|
module.exports = __toCommonJS(index_exports);
|
|
41
42
|
|
|
@@ -54,8 +55,6 @@ var UnauthorizedError = class extends NdbError {
|
|
|
54
55
|
};
|
|
55
56
|
var NotFoundError = class extends NdbError {
|
|
56
57
|
};
|
|
57
|
-
var VersionConflictError = class extends NdbError {
|
|
58
|
-
};
|
|
59
58
|
var DuplicateIdError = class extends NdbError {
|
|
60
59
|
};
|
|
61
60
|
var UniqueViolationError = class extends NdbError {
|
|
@@ -68,13 +67,14 @@ var WatchLaggedError = class extends NdbError {
|
|
|
68
67
|
};
|
|
69
68
|
var HistoryTruncatedError = class extends NdbError {
|
|
70
69
|
};
|
|
70
|
+
var InvalidUpdateError = class extends NdbError {
|
|
71
|
+
};
|
|
71
72
|
var TtlError = class extends NdbError {
|
|
72
73
|
};
|
|
73
74
|
var subclasses = {
|
|
74
75
|
unauthorized: UnauthorizedError,
|
|
75
76
|
not_found: NotFoundError,
|
|
76
77
|
unknown_collection: NotFoundError,
|
|
77
|
-
version_conflict: VersionConflictError,
|
|
78
78
|
duplicate_id: DuplicateIdError,
|
|
79
79
|
unique_violation: UniqueViolationError,
|
|
80
80
|
invalid_query: InvalidQueryError,
|
|
@@ -85,7 +85,13 @@ var subclasses = {
|
|
|
85
85
|
watch_lagged: WatchLaggedError,
|
|
86
86
|
history_truncated: HistoryTruncatedError,
|
|
87
87
|
ttl_not_enabled: TtlError,
|
|
88
|
-
ttl_derived: TtlError
|
|
88
|
+
ttl_derived: TtlError,
|
|
89
|
+
unknown_operator: InvalidUpdateError,
|
|
90
|
+
no_update: InvalidUpdateError,
|
|
91
|
+
conflicting_update: InvalidUpdateError,
|
|
92
|
+
invalid_field: InvalidUpdateError,
|
|
93
|
+
unindexed_many: InvalidUpdateError,
|
|
94
|
+
empty_filter: InvalidUpdateError
|
|
89
95
|
};
|
|
90
96
|
function makeError(code, message, status = 0) {
|
|
91
97
|
const Cls = subclasses[code] ?? NdbError;
|
|
@@ -250,7 +256,7 @@ async function* watchEvents(lines2, consumer) {
|
|
|
250
256
|
if (line.error) {
|
|
251
257
|
throw makeError(line.error.code, line.error.message);
|
|
252
258
|
}
|
|
253
|
-
yield { consumer, pos: line.pos, op: line.op,
|
|
259
|
+
yield { consumer, pos: line.pos, op: line.op, _id: line._id, v: line.v, doc: line.doc };
|
|
254
260
|
}
|
|
255
261
|
}
|
|
256
262
|
|
|
@@ -265,9 +271,6 @@ var Model = class {
|
|
|
265
271
|
name;
|
|
266
272
|
schema;
|
|
267
273
|
ensured;
|
|
268
|
-
get idField() {
|
|
269
|
-
return this.schema.id ?? "_id";
|
|
270
|
-
}
|
|
271
274
|
path(op) {
|
|
272
275
|
const base = `/v1/c/${encodeURIComponent(this.name)}`;
|
|
273
276
|
return op ? `${base}/${op}` : base;
|
|
@@ -291,35 +294,55 @@ var Model = class {
|
|
|
291
294
|
create(docs, opts = {}) {
|
|
292
295
|
return this.insert(docs, opts);
|
|
293
296
|
}
|
|
294
|
-
|
|
297
|
+
// Atomic update. A string is an _id (POST /{_id}/update); an object is a
|
|
298
|
+
// filter (POST /update). The filter and the operators are sent as one body;
|
|
299
|
+
// the daemon matches and writes in one step on the newest state, so
|
|
300
|
+
// concurrent updates never interleave. A version guard is a filter:
|
|
301
|
+
// { _id: 'c1', _v: 7 }. Matching nothing is not an error: matched is 0.
|
|
302
|
+
// many does not apply to an _id.
|
|
303
|
+
async update(idOrFilter, ops, opts = {}) {
|
|
295
304
|
await this.ready();
|
|
296
|
-
const
|
|
297
|
-
|
|
305
|
+
const byId = typeof idOrFilter === "string";
|
|
306
|
+
const body = mergeUpdate(byId ? {} : idOrFilter, ops);
|
|
307
|
+
const query = writeQuery({ many: byId ? void 0 : opts.many, upsert: opts.upsert, expire_in: opts.expireIn });
|
|
308
|
+
const op = byId ? `${encodeURIComponent(idOrFilter)}/update` : "update";
|
|
309
|
+
const out = await this.db.transport.request(
|
|
310
|
+
"POST",
|
|
311
|
+
this.path(op) + query,
|
|
312
|
+
body
|
|
313
|
+
);
|
|
314
|
+
return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
|
|
298
315
|
}
|
|
299
|
-
|
|
316
|
+
// Deletes by _id (a string), or the documents that the filter matches: one,
|
|
317
|
+
// or every match with many (same rule as update).
|
|
318
|
+
async delete(idOrFilter, opts = {}) {
|
|
300
319
|
await this.ready();
|
|
301
|
-
|
|
320
|
+
if (typeof idOrFilter === "string") {
|
|
321
|
+
return this.db.transport.request("DELETE", this.path(encodeURIComponent(idOrFilter)));
|
|
322
|
+
}
|
|
323
|
+
const filter = idOrFilter;
|
|
324
|
+
return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
|
|
302
325
|
}
|
|
303
326
|
// Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
|
|
304
327
|
// NotFoundError for a missing or expired document, and TtlError when the
|
|
305
328
|
// schema has no expires field.
|
|
306
|
-
expire(id, duration
|
|
307
|
-
return this.setExpiry({ id, in: duration
|
|
329
|
+
expire(id, duration) {
|
|
330
|
+
return this.setExpiry({ _id: id, in: duration });
|
|
308
331
|
}
|
|
309
|
-
expireAt(id, at
|
|
310
|
-
return this.setExpiry({ id, at: at.toISOString()
|
|
332
|
+
expireAt(id, at) {
|
|
333
|
+
return this.setExpiry({ _id: id, at: at.toISOString() });
|
|
311
334
|
}
|
|
312
335
|
// Removes the expiry of a document (PERSIST in Redis).
|
|
313
336
|
async persist(id) {
|
|
314
337
|
await this.ready();
|
|
315
|
-
return this.db.transport.request("POST", this.path("persist"), { id });
|
|
338
|
+
return this.db.transport.request("POST", this.path("persist"), { _id: id });
|
|
316
339
|
}
|
|
317
340
|
// The seconds left (rounded up), -1 when the document never expires, null
|
|
318
341
|
// when it is missing or expired.
|
|
319
342
|
async ttl(id) {
|
|
320
343
|
await this.ready();
|
|
321
344
|
try {
|
|
322
|
-
const out = await this.db.transport.request("GET",
|
|
345
|
+
const out = await this.db.transport.request("GET", this.path(`${encodeURIComponent(id)}/ttl`));
|
|
323
346
|
return out.ttl;
|
|
324
347
|
} catch (err) {
|
|
325
348
|
if (err instanceof NotFoundError) {
|
|
@@ -331,10 +354,20 @@ var Model = class {
|
|
|
331
354
|
async setExpiry(body) {
|
|
332
355
|
await this.ready();
|
|
333
356
|
const out = await this.db.transport.request("POST", this.path("expire"), body);
|
|
334
|
-
return {
|
|
357
|
+
return { _id: out._id, v: out.v, expiresAt: new Date(out.expires_at) };
|
|
335
358
|
}
|
|
336
|
-
|
|
337
|
-
|
|
359
|
+
// The document with this _id, or null.
|
|
360
|
+
async get(id) {
|
|
361
|
+
await this.ready();
|
|
362
|
+
try {
|
|
363
|
+
const doc = await this.db.transport.request("GET", this.path(encodeURIComponent(id)));
|
|
364
|
+
return this.decode(doc);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (err instanceof NotFoundError) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
338
371
|
}
|
|
339
372
|
query() {
|
|
340
373
|
return new Query(this);
|
|
@@ -411,7 +444,6 @@ var Model = class {
|
|
|
411
444
|
catalog() {
|
|
412
445
|
const entries = Object.entries(this.schema.fields);
|
|
413
446
|
return {
|
|
414
|
-
id: this.schema.id,
|
|
415
447
|
time: this.schema.time,
|
|
416
448
|
expires_field: this.schema.expires,
|
|
417
449
|
indexes: entries.filter(([, d]) => d.index).map(([n]) => n),
|
|
@@ -419,6 +451,24 @@ var Model = class {
|
|
|
419
451
|
};
|
|
420
452
|
}
|
|
421
453
|
};
|
|
454
|
+
function writeQuery(params) {
|
|
455
|
+
const q = new URLSearchParams();
|
|
456
|
+
for (const [name, value] of Object.entries(params)) {
|
|
457
|
+
if (value !== void 0 && value !== false) {
|
|
458
|
+
q.set(name, String(value));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const text = q.toString();
|
|
462
|
+
return text === "" ? "" : `?${text}`;
|
|
463
|
+
}
|
|
464
|
+
function mergeUpdate(filter, ops) {
|
|
465
|
+
for (const key of Object.keys(ops)) {
|
|
466
|
+
if (!key.startsWith("$")) {
|
|
467
|
+
throw new InvalidUpdateError("invalid_update", `update operator "${key}" must start with $ (for example "$set")`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return { ...toWhere(filter), ...ops };
|
|
471
|
+
}
|
|
422
472
|
|
|
423
473
|
// src/transport.ts
|
|
424
474
|
var import_undici = require("undici");
|
|
@@ -618,12 +668,45 @@ var Ndb = class {
|
|
|
618
668
|
function connect(addr, opts) {
|
|
619
669
|
return new Ndb(addr, opts);
|
|
620
670
|
}
|
|
671
|
+
|
|
672
|
+
// src/id.ts
|
|
673
|
+
var lastMs = 0;
|
|
674
|
+
var counter = 0;
|
|
675
|
+
function newId() {
|
|
676
|
+
const bytes = new Uint8Array(16);
|
|
677
|
+
crypto.getRandomValues(bytes);
|
|
678
|
+
const now = Date.now();
|
|
679
|
+
if (now > lastMs) {
|
|
680
|
+
lastMs = now;
|
|
681
|
+
counter = seedCounter();
|
|
682
|
+
} else if (counter === 4095) {
|
|
683
|
+
lastMs += 1;
|
|
684
|
+
counter = seedCounter();
|
|
685
|
+
} else {
|
|
686
|
+
counter += 1;
|
|
687
|
+
}
|
|
688
|
+
const ms = BigInt(lastMs);
|
|
689
|
+
for (let i = 0; i < 6; i++) {
|
|
690
|
+
bytes[i] = Number(ms >> BigInt(8 * (5 - i)) & 0xffn);
|
|
691
|
+
}
|
|
692
|
+
bytes[6] = 112 | counter >> 8;
|
|
693
|
+
bytes[7] = counter & 255;
|
|
694
|
+
bytes[8] = 128 | bytes[8] & 63;
|
|
695
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
696
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
697
|
+
}
|
|
698
|
+
function seedCounter() {
|
|
699
|
+
const r = new Uint16Array(1);
|
|
700
|
+
crypto.getRandomValues(r);
|
|
701
|
+
return r[0] & 2047;
|
|
702
|
+
}
|
|
621
703
|
// Annotate the CommonJS export names for ESM import in node:
|
|
622
704
|
0 && (module.exports = {
|
|
623
705
|
CursorError,
|
|
624
706
|
DuplicateIdError,
|
|
625
707
|
HistoryTruncatedError,
|
|
626
708
|
InvalidQueryError,
|
|
709
|
+
InvalidUpdateError,
|
|
627
710
|
Model,
|
|
628
711
|
Ndb,
|
|
629
712
|
NdbError,
|
|
@@ -632,8 +715,8 @@ function connect(addr, opts) {
|
|
|
632
715
|
TtlError,
|
|
633
716
|
UnauthorizedError,
|
|
634
717
|
UniqueViolationError,
|
|
635
|
-
VersionConflictError,
|
|
636
718
|
WatchLaggedError,
|
|
637
719
|
connect,
|
|
638
|
-
makeError
|
|
720
|
+
makeError,
|
|
721
|
+
newId
|
|
639
722
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -16,7 +16,6 @@ interface FieldDef {
|
|
|
16
16
|
}
|
|
17
17
|
type Fields = Record<string, FieldDef>;
|
|
18
18
|
interface Schema<F extends Fields> {
|
|
19
|
-
id?: Extract<keyof F, string>;
|
|
20
19
|
time?: Extract<keyof F, string>;
|
|
21
20
|
expires?: Extract<keyof F, string>;
|
|
22
21
|
fields: F;
|
|
@@ -34,8 +33,11 @@ type Doc<F extends Fields> = {
|
|
|
34
33
|
};
|
|
35
34
|
type Input<F extends Fields> = {
|
|
36
35
|
[K in keyof F]?: ValueOf<F[K]>;
|
|
36
|
+
} & {
|
|
37
|
+
_id?: string;
|
|
37
38
|
};
|
|
38
39
|
type Row<F extends Fields> = Doc<F> & {
|
|
40
|
+
_id: string;
|
|
39
41
|
_v: number;
|
|
40
42
|
};
|
|
41
43
|
interface Ops<V> {
|
|
@@ -58,22 +60,49 @@ type JsonKeys<F extends Fields> = Extract<{
|
|
|
58
60
|
}[keyof F], string>;
|
|
59
61
|
type Where<F extends Fields> = {
|
|
60
62
|
[K in keyof F]?: Operand<ValueOf<F[K]>> | Ops<Operand<ValueOf<F[K]>>> | RegexOperand<ValueOf<F[K]>>;
|
|
63
|
+
} & {
|
|
64
|
+
_id?: string | Ops<string>;
|
|
61
65
|
} & {
|
|
62
66
|
[P in `${JsonKeys<F>}.${string}`]?: unknown;
|
|
63
67
|
};
|
|
64
|
-
type SortSpec<F extends Fields> = Partial<Record<Extract<keyof F, string
|
|
68
|
+
type SortSpec<F extends Fields> = Partial<Record<Extract<keyof F, string> | '_id', 1 | -1>>;
|
|
65
69
|
interface WriteResult {
|
|
66
|
-
|
|
70
|
+
_id: string;
|
|
67
71
|
v: number;
|
|
68
72
|
}
|
|
69
|
-
interface WriteOptions {
|
|
70
|
-
ifVersion?: number;
|
|
71
|
-
}
|
|
72
73
|
type Duration = number | string;
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
type Filter<F extends Fields> = Where<F> & {
|
|
75
|
+
_v?: number | Ops<number>;
|
|
76
|
+
$and?: Filter<F>[];
|
|
77
|
+
$or?: Filter<F>[];
|
|
78
|
+
$nor?: Filter<F>[];
|
|
79
|
+
};
|
|
80
|
+
interface UpdateOps<F extends Fields = Fields> {
|
|
81
|
+
$set?: Input<F> & Record<string, unknown>;
|
|
82
|
+
$setOnInsert?: Input<F> & Record<string, unknown>;
|
|
83
|
+
$unset?: Record<string, true | ''>;
|
|
84
|
+
$inc?: Record<string, number>;
|
|
85
|
+
$push?: Record<string, unknown>;
|
|
86
|
+
$pull?: Record<string, unknown>;
|
|
87
|
+
$min?: Record<string, number | string>;
|
|
88
|
+
$max?: Record<string, number | string>;
|
|
89
|
+
}
|
|
90
|
+
interface UpdateOptions {
|
|
91
|
+
many?: boolean;
|
|
92
|
+
upsert?: boolean;
|
|
75
93
|
expireIn?: Duration;
|
|
76
94
|
}
|
|
95
|
+
interface DeleteOptions {
|
|
96
|
+
many?: boolean;
|
|
97
|
+
}
|
|
98
|
+
interface UpdateResult<F extends Fields = Fields> {
|
|
99
|
+
matched: number;
|
|
100
|
+
modified: number;
|
|
101
|
+
doc?: Row<F>;
|
|
102
|
+
}
|
|
103
|
+
interface DeleteResult {
|
|
104
|
+
deleted: number;
|
|
105
|
+
}
|
|
77
106
|
interface InsertOptions {
|
|
78
107
|
expireIn?: Duration;
|
|
79
108
|
}
|
|
@@ -101,7 +130,7 @@ interface WatchEvent<R> {
|
|
|
101
130
|
consumer: string;
|
|
102
131
|
pos: string;
|
|
103
132
|
op: 'put' | 'del';
|
|
104
|
-
|
|
133
|
+
_id: string;
|
|
105
134
|
v: number;
|
|
106
135
|
doc: R | undefined;
|
|
107
136
|
}
|
|
@@ -145,19 +174,18 @@ declare class Model<F extends Fields> {
|
|
|
145
174
|
readonly schema: Schema<F>;
|
|
146
175
|
private ensured?;
|
|
147
176
|
constructor(db: Ndb, name: string, schema: Schema<F>);
|
|
148
|
-
get idField(): string;
|
|
149
177
|
path(op?: string): string;
|
|
150
178
|
ready(): Promise<void>;
|
|
151
179
|
insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
152
180
|
create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
153
|
-
update(
|
|
154
|
-
delete(
|
|
155
|
-
expire(id: string, duration: Duration
|
|
156
|
-
expireAt(id: string, at: Date
|
|
181
|
+
update(idOrFilter: string | Filter<F>, ops: UpdateOps<F>, opts?: UpdateOptions): Promise<UpdateResult<F>>;
|
|
182
|
+
delete(idOrFilter: string | Filter<F>, opts?: DeleteOptions): Promise<DeleteResult>;
|
|
183
|
+
expire(id: string, duration: Duration): Promise<ExpireResult>;
|
|
184
|
+
expireAt(id: string, at: Date): Promise<ExpireResult>;
|
|
157
185
|
persist(id: string): Promise<WriteResult>;
|
|
158
186
|
ttl(id: string): Promise<number | null>;
|
|
159
187
|
private setExpiry;
|
|
160
|
-
|
|
188
|
+
get(id: string): Promise<Row<F> | null>;
|
|
161
189
|
query(): Query<F>;
|
|
162
190
|
where(w: Where<F>): Query<F>;
|
|
163
191
|
not(w: Where<F>): Query<F>;
|
|
@@ -215,7 +243,9 @@ declare class Ndb {
|
|
|
215
243
|
}
|
|
216
244
|
declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
|
|
217
245
|
|
|
218
|
-
|
|
246
|
+
declare function newId(): string;
|
|
247
|
+
|
|
248
|
+
type ErrorCode = 'unauthorized' | 'not_found' | 'duplicate_id' | 'unique_violation' | 'catalog_conflict' | 'catalog_invalid' | 'unknown_collection' | 'direct_collection' | 'invalid_request' | 'invalid_query' | 'invalid_pipeline' | 'group_limit_exceeded' | 'cursor_invalid' | 'cursor_mismatch' | 'cursor_expired' | 'request_too_large' | 'line_too_large' | 'not_ready' | 'closed' | 'watch_lagged' | 'invalid_position' | 'history_truncated' | 'ttl_not_enabled' | 'ttl_derived' | 'unknown_operator' | 'no_update' | 'conflicting_update' | 'invalid_field' | 'unindexed_many' | 'empty_filter' | 'invalid_update' | 'http_status' | (string & {});
|
|
219
249
|
declare class NdbError extends Error {
|
|
220
250
|
readonly code: ErrorCode;
|
|
221
251
|
readonly status: number;
|
|
@@ -225,8 +255,6 @@ declare class UnauthorizedError extends NdbError {
|
|
|
225
255
|
}
|
|
226
256
|
declare class NotFoundError extends NdbError {
|
|
227
257
|
}
|
|
228
|
-
declare class VersionConflictError extends NdbError {
|
|
229
|
-
}
|
|
230
258
|
declare class DuplicateIdError extends NdbError {
|
|
231
259
|
}
|
|
232
260
|
declare class UniqueViolationError extends NdbError {
|
|
@@ -239,9 +267,11 @@ declare class WatchLaggedError extends NdbError {
|
|
|
239
267
|
}
|
|
240
268
|
declare class HistoryTruncatedError extends NdbError {
|
|
241
269
|
}
|
|
270
|
+
declare class InvalidUpdateError extends NdbError {
|
|
271
|
+
}
|
|
242
272
|
declare class TtlError extends NdbError {
|
|
243
273
|
}
|
|
244
274
|
declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
|
|
245
275
|
|
|
246
|
-
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError,
|
|
247
|
-
export type { AggregateOptions, AggregateResult, ConnectOptions, Doc, Duration, ErrorCode, ExpireResult, FieldDef, FieldType, Fields, Input, InsertOptions, Ops, Row, Schema, SortSpec, TypeMap, UpdateOptions, WatchEvent, WatchOptions, Where,
|
|
276
|
+
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, InvalidUpdateError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError, WatchLaggedError, connect, makeError, newId };
|
|
277
|
+
export type { AggregateOptions, AggregateResult, ConnectOptions, DeleteOptions, DeleteResult, Doc, Duration, ErrorCode, ExpireResult, FieldDef, FieldType, Fields, Filter, Input, InsertOptions, Ops, Row, Schema, SortSpec, TypeMap, UpdateOps, UpdateOptions, UpdateResult, WatchEvent, WatchOptions, Where, WriteResult };
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,6 @@ interface FieldDef {
|
|
|
16
16
|
}
|
|
17
17
|
type Fields = Record<string, FieldDef>;
|
|
18
18
|
interface Schema<F extends Fields> {
|
|
19
|
-
id?: Extract<keyof F, string>;
|
|
20
19
|
time?: Extract<keyof F, string>;
|
|
21
20
|
expires?: Extract<keyof F, string>;
|
|
22
21
|
fields: F;
|
|
@@ -34,8 +33,11 @@ type Doc<F extends Fields> = {
|
|
|
34
33
|
};
|
|
35
34
|
type Input<F extends Fields> = {
|
|
36
35
|
[K in keyof F]?: ValueOf<F[K]>;
|
|
36
|
+
} & {
|
|
37
|
+
_id?: string;
|
|
37
38
|
};
|
|
38
39
|
type Row<F extends Fields> = Doc<F> & {
|
|
40
|
+
_id: string;
|
|
39
41
|
_v: number;
|
|
40
42
|
};
|
|
41
43
|
interface Ops<V> {
|
|
@@ -58,22 +60,49 @@ type JsonKeys<F extends Fields> = Extract<{
|
|
|
58
60
|
}[keyof F], string>;
|
|
59
61
|
type Where<F extends Fields> = {
|
|
60
62
|
[K in keyof F]?: Operand<ValueOf<F[K]>> | Ops<Operand<ValueOf<F[K]>>> | RegexOperand<ValueOf<F[K]>>;
|
|
63
|
+
} & {
|
|
64
|
+
_id?: string | Ops<string>;
|
|
61
65
|
} & {
|
|
62
66
|
[P in `${JsonKeys<F>}.${string}`]?: unknown;
|
|
63
67
|
};
|
|
64
|
-
type SortSpec<F extends Fields> = Partial<Record<Extract<keyof F, string
|
|
68
|
+
type SortSpec<F extends Fields> = Partial<Record<Extract<keyof F, string> | '_id', 1 | -1>>;
|
|
65
69
|
interface WriteResult {
|
|
66
|
-
|
|
70
|
+
_id: string;
|
|
67
71
|
v: number;
|
|
68
72
|
}
|
|
69
|
-
interface WriteOptions {
|
|
70
|
-
ifVersion?: number;
|
|
71
|
-
}
|
|
72
73
|
type Duration = number | string;
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
type Filter<F extends Fields> = Where<F> & {
|
|
75
|
+
_v?: number | Ops<number>;
|
|
76
|
+
$and?: Filter<F>[];
|
|
77
|
+
$or?: Filter<F>[];
|
|
78
|
+
$nor?: Filter<F>[];
|
|
79
|
+
};
|
|
80
|
+
interface UpdateOps<F extends Fields = Fields> {
|
|
81
|
+
$set?: Input<F> & Record<string, unknown>;
|
|
82
|
+
$setOnInsert?: Input<F> & Record<string, unknown>;
|
|
83
|
+
$unset?: Record<string, true | ''>;
|
|
84
|
+
$inc?: Record<string, number>;
|
|
85
|
+
$push?: Record<string, unknown>;
|
|
86
|
+
$pull?: Record<string, unknown>;
|
|
87
|
+
$min?: Record<string, number | string>;
|
|
88
|
+
$max?: Record<string, number | string>;
|
|
89
|
+
}
|
|
90
|
+
interface UpdateOptions {
|
|
91
|
+
many?: boolean;
|
|
92
|
+
upsert?: boolean;
|
|
75
93
|
expireIn?: Duration;
|
|
76
94
|
}
|
|
95
|
+
interface DeleteOptions {
|
|
96
|
+
many?: boolean;
|
|
97
|
+
}
|
|
98
|
+
interface UpdateResult<F extends Fields = Fields> {
|
|
99
|
+
matched: number;
|
|
100
|
+
modified: number;
|
|
101
|
+
doc?: Row<F>;
|
|
102
|
+
}
|
|
103
|
+
interface DeleteResult {
|
|
104
|
+
deleted: number;
|
|
105
|
+
}
|
|
77
106
|
interface InsertOptions {
|
|
78
107
|
expireIn?: Duration;
|
|
79
108
|
}
|
|
@@ -101,7 +130,7 @@ interface WatchEvent<R> {
|
|
|
101
130
|
consumer: string;
|
|
102
131
|
pos: string;
|
|
103
132
|
op: 'put' | 'del';
|
|
104
|
-
|
|
133
|
+
_id: string;
|
|
105
134
|
v: number;
|
|
106
135
|
doc: R | undefined;
|
|
107
136
|
}
|
|
@@ -145,19 +174,18 @@ declare class Model<F extends Fields> {
|
|
|
145
174
|
readonly schema: Schema<F>;
|
|
146
175
|
private ensured?;
|
|
147
176
|
constructor(db: Ndb, name: string, schema: Schema<F>);
|
|
148
|
-
get idField(): string;
|
|
149
177
|
path(op?: string): string;
|
|
150
178
|
ready(): Promise<void>;
|
|
151
179
|
insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
152
180
|
create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
153
|
-
update(
|
|
154
|
-
delete(
|
|
155
|
-
expire(id: string, duration: Duration
|
|
156
|
-
expireAt(id: string, at: Date
|
|
181
|
+
update(idOrFilter: string | Filter<F>, ops: UpdateOps<F>, opts?: UpdateOptions): Promise<UpdateResult<F>>;
|
|
182
|
+
delete(idOrFilter: string | Filter<F>, opts?: DeleteOptions): Promise<DeleteResult>;
|
|
183
|
+
expire(id: string, duration: Duration): Promise<ExpireResult>;
|
|
184
|
+
expireAt(id: string, at: Date): Promise<ExpireResult>;
|
|
157
185
|
persist(id: string): Promise<WriteResult>;
|
|
158
186
|
ttl(id: string): Promise<number | null>;
|
|
159
187
|
private setExpiry;
|
|
160
|
-
|
|
188
|
+
get(id: string): Promise<Row<F> | null>;
|
|
161
189
|
query(): Query<F>;
|
|
162
190
|
where(w: Where<F>): Query<F>;
|
|
163
191
|
not(w: Where<F>): Query<F>;
|
|
@@ -215,7 +243,9 @@ declare class Ndb {
|
|
|
215
243
|
}
|
|
216
244
|
declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
|
|
217
245
|
|
|
218
|
-
|
|
246
|
+
declare function newId(): string;
|
|
247
|
+
|
|
248
|
+
type ErrorCode = 'unauthorized' | 'not_found' | 'duplicate_id' | 'unique_violation' | 'catalog_conflict' | 'catalog_invalid' | 'unknown_collection' | 'direct_collection' | 'invalid_request' | 'invalid_query' | 'invalid_pipeline' | 'group_limit_exceeded' | 'cursor_invalid' | 'cursor_mismatch' | 'cursor_expired' | 'request_too_large' | 'line_too_large' | 'not_ready' | 'closed' | 'watch_lagged' | 'invalid_position' | 'history_truncated' | 'ttl_not_enabled' | 'ttl_derived' | 'unknown_operator' | 'no_update' | 'conflicting_update' | 'invalid_field' | 'unindexed_many' | 'empty_filter' | 'invalid_update' | 'http_status' | (string & {});
|
|
219
249
|
declare class NdbError extends Error {
|
|
220
250
|
readonly code: ErrorCode;
|
|
221
251
|
readonly status: number;
|
|
@@ -225,8 +255,6 @@ declare class UnauthorizedError extends NdbError {
|
|
|
225
255
|
}
|
|
226
256
|
declare class NotFoundError extends NdbError {
|
|
227
257
|
}
|
|
228
|
-
declare class VersionConflictError extends NdbError {
|
|
229
|
-
}
|
|
230
258
|
declare class DuplicateIdError extends NdbError {
|
|
231
259
|
}
|
|
232
260
|
declare class UniqueViolationError extends NdbError {
|
|
@@ -239,9 +267,11 @@ declare class WatchLaggedError extends NdbError {
|
|
|
239
267
|
}
|
|
240
268
|
declare class HistoryTruncatedError extends NdbError {
|
|
241
269
|
}
|
|
270
|
+
declare class InvalidUpdateError extends NdbError {
|
|
271
|
+
}
|
|
242
272
|
declare class TtlError extends NdbError {
|
|
243
273
|
}
|
|
244
274
|
declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
|
|
245
275
|
|
|
246
|
-
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError,
|
|
247
|
-
export type { AggregateOptions, AggregateResult, ConnectOptions, Doc, Duration, ErrorCode, ExpireResult, FieldDef, FieldType, Fields, Input, InsertOptions, Ops, Row, Schema, SortSpec, TypeMap, UpdateOptions, WatchEvent, WatchOptions, Where,
|
|
276
|
+
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, InvalidUpdateError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError, WatchLaggedError, connect, makeError, newId };
|
|
277
|
+
export type { AggregateOptions, AggregateResult, ConnectOptions, DeleteOptions, DeleteResult, Doc, Duration, ErrorCode, ExpireResult, FieldDef, FieldType, Fields, Filter, Input, InsertOptions, Ops, Row, Schema, SortSpec, TypeMap, UpdateOps, UpdateOptions, UpdateResult, WatchEvent, WatchOptions, Where, WriteResult };
|
package/dist/index.js
CHANGED
|
@@ -13,8 +13,6 @@ var UnauthorizedError = class extends NdbError {
|
|
|
13
13
|
};
|
|
14
14
|
var NotFoundError = class extends NdbError {
|
|
15
15
|
};
|
|
16
|
-
var VersionConflictError = class extends NdbError {
|
|
17
|
-
};
|
|
18
16
|
var DuplicateIdError = class extends NdbError {
|
|
19
17
|
};
|
|
20
18
|
var UniqueViolationError = class extends NdbError {
|
|
@@ -27,13 +25,14 @@ var WatchLaggedError = class extends NdbError {
|
|
|
27
25
|
};
|
|
28
26
|
var HistoryTruncatedError = class extends NdbError {
|
|
29
27
|
};
|
|
28
|
+
var InvalidUpdateError = class extends NdbError {
|
|
29
|
+
};
|
|
30
30
|
var TtlError = class extends NdbError {
|
|
31
31
|
};
|
|
32
32
|
var subclasses = {
|
|
33
33
|
unauthorized: UnauthorizedError,
|
|
34
34
|
not_found: NotFoundError,
|
|
35
35
|
unknown_collection: NotFoundError,
|
|
36
|
-
version_conflict: VersionConflictError,
|
|
37
36
|
duplicate_id: DuplicateIdError,
|
|
38
37
|
unique_violation: UniqueViolationError,
|
|
39
38
|
invalid_query: InvalidQueryError,
|
|
@@ -44,7 +43,13 @@ var subclasses = {
|
|
|
44
43
|
watch_lagged: WatchLaggedError,
|
|
45
44
|
history_truncated: HistoryTruncatedError,
|
|
46
45
|
ttl_not_enabled: TtlError,
|
|
47
|
-
ttl_derived: TtlError
|
|
46
|
+
ttl_derived: TtlError,
|
|
47
|
+
unknown_operator: InvalidUpdateError,
|
|
48
|
+
no_update: InvalidUpdateError,
|
|
49
|
+
conflicting_update: InvalidUpdateError,
|
|
50
|
+
invalid_field: InvalidUpdateError,
|
|
51
|
+
unindexed_many: InvalidUpdateError,
|
|
52
|
+
empty_filter: InvalidUpdateError
|
|
48
53
|
};
|
|
49
54
|
function makeError(code, message, status = 0) {
|
|
50
55
|
const Cls = subclasses[code] ?? NdbError;
|
|
@@ -209,7 +214,7 @@ async function* watchEvents(lines2, consumer) {
|
|
|
209
214
|
if (line.error) {
|
|
210
215
|
throw makeError(line.error.code, line.error.message);
|
|
211
216
|
}
|
|
212
|
-
yield { consumer, pos: line.pos, op: line.op,
|
|
217
|
+
yield { consumer, pos: line.pos, op: line.op, _id: line._id, v: line.v, doc: line.doc };
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
|
|
@@ -224,9 +229,6 @@ var Model = class {
|
|
|
224
229
|
name;
|
|
225
230
|
schema;
|
|
226
231
|
ensured;
|
|
227
|
-
get idField() {
|
|
228
|
-
return this.schema.id ?? "_id";
|
|
229
|
-
}
|
|
230
232
|
path(op) {
|
|
231
233
|
const base = `/v1/c/${encodeURIComponent(this.name)}`;
|
|
232
234
|
return op ? `${base}/${op}` : base;
|
|
@@ -250,35 +252,55 @@ var Model = class {
|
|
|
250
252
|
create(docs, opts = {}) {
|
|
251
253
|
return this.insert(docs, opts);
|
|
252
254
|
}
|
|
253
|
-
|
|
255
|
+
// Atomic update. A string is an _id (POST /{_id}/update); an object is a
|
|
256
|
+
// filter (POST /update). The filter and the operators are sent as one body;
|
|
257
|
+
// the daemon matches and writes in one step on the newest state, so
|
|
258
|
+
// concurrent updates never interleave. A version guard is a filter:
|
|
259
|
+
// { _id: 'c1', _v: 7 }. Matching nothing is not an error: matched is 0.
|
|
260
|
+
// many does not apply to an _id.
|
|
261
|
+
async update(idOrFilter, ops, opts = {}) {
|
|
254
262
|
await this.ready();
|
|
255
|
-
const
|
|
256
|
-
|
|
263
|
+
const byId = typeof idOrFilter === "string";
|
|
264
|
+
const body = mergeUpdate(byId ? {} : idOrFilter, ops);
|
|
265
|
+
const query = writeQuery({ many: byId ? void 0 : opts.many, upsert: opts.upsert, expire_in: opts.expireIn });
|
|
266
|
+
const op = byId ? `${encodeURIComponent(idOrFilter)}/update` : "update";
|
|
267
|
+
const out = await this.db.transport.request(
|
|
268
|
+
"POST",
|
|
269
|
+
this.path(op) + query,
|
|
270
|
+
body
|
|
271
|
+
);
|
|
272
|
+
return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
|
|
257
273
|
}
|
|
258
|
-
|
|
274
|
+
// Deletes by _id (a string), or the documents that the filter matches: one,
|
|
275
|
+
// or every match with many (same rule as update).
|
|
276
|
+
async delete(idOrFilter, opts = {}) {
|
|
259
277
|
await this.ready();
|
|
260
|
-
|
|
278
|
+
if (typeof idOrFilter === "string") {
|
|
279
|
+
return this.db.transport.request("DELETE", this.path(encodeURIComponent(idOrFilter)));
|
|
280
|
+
}
|
|
281
|
+
const filter = idOrFilter;
|
|
282
|
+
return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
|
|
261
283
|
}
|
|
262
284
|
// Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
|
|
263
285
|
// NotFoundError for a missing or expired document, and TtlError when the
|
|
264
286
|
// schema has no expires field.
|
|
265
|
-
expire(id, duration
|
|
266
|
-
return this.setExpiry({ id, in: duration
|
|
287
|
+
expire(id, duration) {
|
|
288
|
+
return this.setExpiry({ _id: id, in: duration });
|
|
267
289
|
}
|
|
268
|
-
expireAt(id, at
|
|
269
|
-
return this.setExpiry({ id, at: at.toISOString()
|
|
290
|
+
expireAt(id, at) {
|
|
291
|
+
return this.setExpiry({ _id: id, at: at.toISOString() });
|
|
270
292
|
}
|
|
271
293
|
// Removes the expiry of a document (PERSIST in Redis).
|
|
272
294
|
async persist(id) {
|
|
273
295
|
await this.ready();
|
|
274
|
-
return this.db.transport.request("POST", this.path("persist"), { id });
|
|
296
|
+
return this.db.transport.request("POST", this.path("persist"), { _id: id });
|
|
275
297
|
}
|
|
276
298
|
// The seconds left (rounded up), -1 when the document never expires, null
|
|
277
299
|
// when it is missing or expired.
|
|
278
300
|
async ttl(id) {
|
|
279
301
|
await this.ready();
|
|
280
302
|
try {
|
|
281
|
-
const out = await this.db.transport.request("GET",
|
|
303
|
+
const out = await this.db.transport.request("GET", this.path(`${encodeURIComponent(id)}/ttl`));
|
|
282
304
|
return out.ttl;
|
|
283
305
|
} catch (err) {
|
|
284
306
|
if (err instanceof NotFoundError) {
|
|
@@ -290,10 +312,20 @@ var Model = class {
|
|
|
290
312
|
async setExpiry(body) {
|
|
291
313
|
await this.ready();
|
|
292
314
|
const out = await this.db.transport.request("POST", this.path("expire"), body);
|
|
293
|
-
return {
|
|
315
|
+
return { _id: out._id, v: out.v, expiresAt: new Date(out.expires_at) };
|
|
294
316
|
}
|
|
295
|
-
|
|
296
|
-
|
|
317
|
+
// The document with this _id, or null.
|
|
318
|
+
async get(id) {
|
|
319
|
+
await this.ready();
|
|
320
|
+
try {
|
|
321
|
+
const doc = await this.db.transport.request("GET", this.path(encodeURIComponent(id)));
|
|
322
|
+
return this.decode(doc);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (err instanceof NotFoundError) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
297
329
|
}
|
|
298
330
|
query() {
|
|
299
331
|
return new Query(this);
|
|
@@ -370,7 +402,6 @@ var Model = class {
|
|
|
370
402
|
catalog() {
|
|
371
403
|
const entries = Object.entries(this.schema.fields);
|
|
372
404
|
return {
|
|
373
|
-
id: this.schema.id,
|
|
374
405
|
time: this.schema.time,
|
|
375
406
|
expires_field: this.schema.expires,
|
|
376
407
|
indexes: entries.filter(([, d]) => d.index).map(([n]) => n),
|
|
@@ -378,6 +409,24 @@ var Model = class {
|
|
|
378
409
|
};
|
|
379
410
|
}
|
|
380
411
|
};
|
|
412
|
+
function writeQuery(params) {
|
|
413
|
+
const q = new URLSearchParams();
|
|
414
|
+
for (const [name, value] of Object.entries(params)) {
|
|
415
|
+
if (value !== void 0 && value !== false) {
|
|
416
|
+
q.set(name, String(value));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const text = q.toString();
|
|
420
|
+
return text === "" ? "" : `?${text}`;
|
|
421
|
+
}
|
|
422
|
+
function mergeUpdate(filter, ops) {
|
|
423
|
+
for (const key of Object.keys(ops)) {
|
|
424
|
+
if (!key.startsWith("$")) {
|
|
425
|
+
throw new InvalidUpdateError("invalid_update", `update operator "${key}" must start with $ (for example "$set")`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { ...toWhere(filter), ...ops };
|
|
429
|
+
}
|
|
381
430
|
|
|
382
431
|
// src/transport.ts
|
|
383
432
|
import { Pool } from "undici";
|
|
@@ -577,11 +626,44 @@ var Ndb = class {
|
|
|
577
626
|
function connect(addr, opts) {
|
|
578
627
|
return new Ndb(addr, opts);
|
|
579
628
|
}
|
|
629
|
+
|
|
630
|
+
// src/id.ts
|
|
631
|
+
var lastMs = 0;
|
|
632
|
+
var counter = 0;
|
|
633
|
+
function newId() {
|
|
634
|
+
const bytes = new Uint8Array(16);
|
|
635
|
+
crypto.getRandomValues(bytes);
|
|
636
|
+
const now = Date.now();
|
|
637
|
+
if (now > lastMs) {
|
|
638
|
+
lastMs = now;
|
|
639
|
+
counter = seedCounter();
|
|
640
|
+
} else if (counter === 4095) {
|
|
641
|
+
lastMs += 1;
|
|
642
|
+
counter = seedCounter();
|
|
643
|
+
} else {
|
|
644
|
+
counter += 1;
|
|
645
|
+
}
|
|
646
|
+
const ms = BigInt(lastMs);
|
|
647
|
+
for (let i = 0; i < 6; i++) {
|
|
648
|
+
bytes[i] = Number(ms >> BigInt(8 * (5 - i)) & 0xffn);
|
|
649
|
+
}
|
|
650
|
+
bytes[6] = 112 | counter >> 8;
|
|
651
|
+
bytes[7] = counter & 255;
|
|
652
|
+
bytes[8] = 128 | bytes[8] & 63;
|
|
653
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
654
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
655
|
+
}
|
|
656
|
+
function seedCounter() {
|
|
657
|
+
const r = new Uint16Array(1);
|
|
658
|
+
crypto.getRandomValues(r);
|
|
659
|
+
return r[0] & 2047;
|
|
660
|
+
}
|
|
580
661
|
export {
|
|
581
662
|
CursorError,
|
|
582
663
|
DuplicateIdError,
|
|
583
664
|
HistoryTruncatedError,
|
|
584
665
|
InvalidQueryError,
|
|
666
|
+
InvalidUpdateError,
|
|
585
667
|
Model,
|
|
586
668
|
Ndb,
|
|
587
669
|
NdbError,
|
|
@@ -590,8 +672,8 @@ export {
|
|
|
590
672
|
TtlError,
|
|
591
673
|
UnauthorizedError,
|
|
592
674
|
UniqueViolationError,
|
|
593
|
-
VersionConflictError,
|
|
594
675
|
WatchLaggedError,
|
|
595
676
|
connect,
|
|
596
|
-
makeError
|
|
677
|
+
makeError,
|
|
678
|
+
newId
|
|
597
679
|
};
|