@thadeu/ndb 0.1.2 → 0.2.1
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 +30 -30
- package/dist/index.cjs +72 -24
- package/dist/index.d.cts +15 -9
- package/dist/index.d.ts +15 -9
- package/dist/index.js +70 -23
- 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,49 +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
87
|
// Atomic update by filter: filter first, then the write operators.
|
|
90
88
|
// _v in the filter is an optimistic lock.
|
|
91
|
-
const res = await calls.update({
|
|
89
|
+
const res = await calls.update({ _id: 'c1', _v: row!._v }, { $set: { status: 'done' }, $unset: { duration: true } })
|
|
92
90
|
// res: { matched, modified, doc } (doc is the new row with _v; absent for many)
|
|
93
91
|
|
|
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
|
// A guarded decrement: exactly one of two racing calls wins.
|
|
95
|
-
await stock.update({
|
|
96
|
+
await stock.update({ _id: 'a', qty: { $gte: 1 } }, { $inc: { qty: -1 } })
|
|
96
97
|
|
|
97
|
-
// Every match (the filter needs
|
|
98
|
+
// Every match (the filter needs _id or an indexed field), or create it.
|
|
98
99
|
await calls.update({ status: 'failed' }, { $set: { status: 'retry' } }, { many: true })
|
|
99
|
-
await calls.update(
|
|
100
|
+
await calls.update('c9', { $inc: { duration: 1 } }, { upsert: true })
|
|
100
101
|
|
|
101
|
-
// Delete by filter: { deleted }.
|
|
102
|
-
await calls.delete(
|
|
102
|
+
// Delete by _id or by filter: { deleted }.
|
|
103
|
+
await calls.delete('c2')
|
|
103
104
|
await calls.delete({ status: 'failed' }, { many: true })
|
|
104
105
|
```
|
|
105
106
|
|
|
106
|
-
- Operators: `$set`, `$unset`, `$inc`, `$push` (with `{ $each: [...] }`), `$pull`, `$min`, `$max`. Field names can be dot paths.
|
|
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.
|
|
107
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.
|
|
108
|
-
- `many` needs
|
|
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`).
|
|
109
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.
|
|
110
111
|
|
|
111
|
-
An insert with a duplicate
|
|
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`.
|
|
112
113
|
|
|
113
114
|
### Expiry (TTL like Redis)
|
|
114
115
|
|
|
115
116
|
```ts
|
|
116
117
|
const sessions = db.model('sessions', {
|
|
117
|
-
id: 'sid',
|
|
118
118
|
expires: 'exp',
|
|
119
|
-
fields: {
|
|
119
|
+
fields: { exp: { type: 'date', optional: true } },
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
-
await sessions.insert({
|
|
123
|
-
await sessions.update(
|
|
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' })
|
|
124
124
|
|
|
125
125
|
const { v, expiresAt } = await sessions.expire('s1', '30m') // EXPIRE
|
|
126
126
|
await sessions.expireAt('s1', new Date(Date.now() + 3_600_000))
|
|
@@ -142,8 +142,8 @@ const rows = await calls
|
|
|
142
142
|
.where({ caller: { $in: ['a', 'b'] } })
|
|
143
143
|
.not({ caller: 'zzz' })
|
|
144
144
|
.or({ duration: { $lt: 5 } }, { 'meta.agent.id': 7 })
|
|
145
|
-
.sort({ at: -1,
|
|
146
|
-
.fields('
|
|
145
|
+
.sort({ at: -1, _id: 1 })
|
|
146
|
+
.fields('_id', 'status')
|
|
147
147
|
.limit(50)
|
|
148
148
|
.skip(100)
|
|
149
149
|
```
|
|
@@ -168,11 +168,11 @@ const total = await calls.count()
|
|
|
168
168
|
Offset pages get slower as `skip` grows. Use a cursor for deep pages.
|
|
169
169
|
|
|
170
170
|
```ts
|
|
171
|
-
const q = calls.where({ status: 'answered' }).sort({
|
|
171
|
+
const q = calls.where({ status: 'answered' }).sort({ _id: 1 }).limit(100)
|
|
172
172
|
const page = await q
|
|
173
173
|
const token = q.nextCursor // '' on the last page
|
|
174
174
|
|
|
175
|
-
const next = await calls.where({ status: 'answered' }).sort({
|
|
175
|
+
const next = await calls.where({ status: 'answered' }).sort({ _id: 1 }).limit(100).after(token)
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
A token works only with the same `where` and `sort`. Another query shape throws `CursorError`.
|
|
@@ -180,8 +180,8 @@ A token works only with the same `where` and `sort`. Another query shape throws
|
|
|
180
180
|
To walk every row, use `iterate()`. It follows the cursor, with `limit` as the batch size:
|
|
181
181
|
|
|
182
182
|
```ts
|
|
183
|
-
for await (const row of calls.where({ status: 'answered' }).sort({
|
|
184
|
-
console.log(row.
|
|
183
|
+
for await (const row of calls.where({ status: 'answered' }).sort({ _id: 1 }).limit(500).iterate()) {
|
|
184
|
+
console.log(row._id)
|
|
185
185
|
}
|
|
186
186
|
```
|
|
187
187
|
|
|
@@ -236,7 +236,7 @@ const controller = new AbortController()
|
|
|
236
236
|
|
|
237
237
|
try {
|
|
238
238
|
for await (const ev of calls.watch('relay', { where: { status: 'answered' }, signal: controller.signal })) {
|
|
239
|
-
// ev: { consumer, pos, op: 'put' | 'del',
|
|
239
|
+
// ev: { consumer, pos, op: 'put' | 'del', _id, v, doc }
|
|
240
240
|
await handle(ev)
|
|
241
241
|
|
|
242
242
|
await calls.ack(ev)
|
|
@@ -280,7 +280,7 @@ Daemon errors are `NdbError` or a subclass. They have `code` (the daemon code) a
|
|
|
280
280
|
import { NdbError } from '@thadeu/ndb'
|
|
281
281
|
|
|
282
282
|
try {
|
|
283
|
-
await calls.
|
|
283
|
+
await calls.get('c1')
|
|
284
284
|
} catch (err) {
|
|
285
285
|
if (err instanceof NdbError && err.code === 'not_ready') {
|
|
286
286
|
// The daemon is still recovering, and readyTimeout ran out.
|
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,8 @@ __export(index_exports, {
|
|
|
35
35
|
UniqueViolationError: () => UniqueViolationError,
|
|
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
|
|
|
@@ -255,7 +256,7 @@ async function* watchEvents(lines2, consumer) {
|
|
|
255
256
|
if (line.error) {
|
|
256
257
|
throw makeError(line.error.code, line.error.message);
|
|
257
258
|
}
|
|
258
|
-
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 };
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
|
|
@@ -270,9 +271,6 @@ var Model = class {
|
|
|
270
271
|
name;
|
|
271
272
|
schema;
|
|
272
273
|
ensured;
|
|
273
|
-
get idField() {
|
|
274
|
-
return this.schema.id ?? "_id";
|
|
275
|
-
}
|
|
276
274
|
path(op) {
|
|
277
275
|
const base = `/v1/c/${encodeURIComponent(this.name)}`;
|
|
278
276
|
return op ? `${base}/${op}` : base;
|
|
@@ -296,47 +294,55 @@ var Model = class {
|
|
|
296
294
|
create(docs, opts = {}) {
|
|
297
295
|
return this.insert(docs, opts);
|
|
298
296
|
}
|
|
299
|
-
// Atomic update
|
|
300
|
-
//
|
|
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
|
|
301
300
|
// concurrent updates never interleave. A version guard is a filter:
|
|
302
|
-
// {
|
|
303
|
-
|
|
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 = {}) {
|
|
304
304
|
await this.ready();
|
|
305
|
-
const
|
|
306
|
-
const
|
|
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";
|
|
307
309
|
const out = await this.db.transport.request(
|
|
308
310
|
"POST",
|
|
309
|
-
this.path(
|
|
311
|
+
this.path(op) + query,
|
|
310
312
|
body
|
|
311
313
|
);
|
|
312
314
|
return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
|
|
313
315
|
}
|
|
314
|
-
// Deletes the documents that the filter matches: one,
|
|
315
|
-
// many (same rule as update).
|
|
316
|
-
async delete(
|
|
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 = {}) {
|
|
317
319
|
await this.ready();
|
|
320
|
+
if (typeof idOrFilter === "string") {
|
|
321
|
+
return this.db.transport.request("DELETE", this.path(encodeURIComponent(idOrFilter)));
|
|
322
|
+
}
|
|
323
|
+
const filter = idOrFilter;
|
|
318
324
|
return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
|
|
319
325
|
}
|
|
320
326
|
// Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
|
|
321
327
|
// NotFoundError for a missing or expired document, and TtlError when the
|
|
322
328
|
// schema has no expires field.
|
|
323
329
|
expire(id, duration) {
|
|
324
|
-
return this.setExpiry({ id, in: duration });
|
|
330
|
+
return this.setExpiry({ _id: id, in: duration });
|
|
325
331
|
}
|
|
326
332
|
expireAt(id, at) {
|
|
327
|
-
return this.setExpiry({ id, at: at.toISOString() });
|
|
333
|
+
return this.setExpiry({ _id: id, at: at.toISOString() });
|
|
328
334
|
}
|
|
329
335
|
// Removes the expiry of a document (PERSIST in Redis).
|
|
330
336
|
async persist(id) {
|
|
331
337
|
await this.ready();
|
|
332
|
-
return this.db.transport.request("POST", this.path("persist"), { id });
|
|
338
|
+
return this.db.transport.request("POST", this.path("persist"), { _id: id });
|
|
333
339
|
}
|
|
334
340
|
// The seconds left (rounded up), -1 when the document never expires, null
|
|
335
341
|
// when it is missing or expired.
|
|
336
342
|
async ttl(id) {
|
|
337
343
|
await this.ready();
|
|
338
344
|
try {
|
|
339
|
-
const out = await this.db.transport.request("GET",
|
|
345
|
+
const out = await this.db.transport.request("GET", this.path(`${encodeURIComponent(id)}/ttl`));
|
|
340
346
|
return out.ttl;
|
|
341
347
|
} catch (err) {
|
|
342
348
|
if (err instanceof NotFoundError) {
|
|
@@ -348,10 +354,20 @@ var Model = class {
|
|
|
348
354
|
async setExpiry(body) {
|
|
349
355
|
await this.ready();
|
|
350
356
|
const out = await this.db.transport.request("POST", this.path("expire"), body);
|
|
351
|
-
return {
|
|
357
|
+
return { _id: out._id, v: out.v, expiresAt: new Date(out.expires_at) };
|
|
352
358
|
}
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
}
|
|
355
371
|
}
|
|
356
372
|
query() {
|
|
357
373
|
return new Query(this);
|
|
@@ -428,7 +444,6 @@ var Model = class {
|
|
|
428
444
|
catalog() {
|
|
429
445
|
const entries = Object.entries(this.schema.fields);
|
|
430
446
|
return {
|
|
431
|
-
id: this.schema.id,
|
|
432
447
|
time: this.schema.time,
|
|
433
448
|
expires_field: this.schema.expires,
|
|
434
449
|
indexes: entries.filter(([, d]) => d.index).map(([n]) => n),
|
|
@@ -653,6 +668,38 @@ var Ndb = class {
|
|
|
653
668
|
function connect(addr, opts) {
|
|
654
669
|
return new Ndb(addr, opts);
|
|
655
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
|
+
}
|
|
656
703
|
// Annotate the CommonJS export names for ESM import in node:
|
|
657
704
|
0 && (module.exports = {
|
|
658
705
|
CursorError,
|
|
@@ -670,5 +717,6 @@ function connect(addr, opts) {
|
|
|
670
717
|
UniqueViolationError,
|
|
671
718
|
WatchLaggedError,
|
|
672
719
|
connect,
|
|
673
|
-
makeError
|
|
720
|
+
makeError,
|
|
721
|
+
newId
|
|
674
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,12 +60,14 @@ 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
73
|
type Duration = number | string;
|
|
@@ -75,6 +79,7 @@ type Filter<F extends Fields> = Where<F> & {
|
|
|
75
79
|
};
|
|
76
80
|
interface UpdateOps<F extends Fields = Fields> {
|
|
77
81
|
$set?: Input<F> & Record<string, unknown>;
|
|
82
|
+
$setOnInsert?: Input<F> & Record<string, unknown>;
|
|
78
83
|
$unset?: Record<string, true | ''>;
|
|
79
84
|
$inc?: Record<string, number>;
|
|
80
85
|
$push?: Record<string, unknown>;
|
|
@@ -125,7 +130,7 @@ interface WatchEvent<R> {
|
|
|
125
130
|
consumer: string;
|
|
126
131
|
pos: string;
|
|
127
132
|
op: 'put' | 'del';
|
|
128
|
-
|
|
133
|
+
_id: string;
|
|
129
134
|
v: number;
|
|
130
135
|
doc: R | undefined;
|
|
131
136
|
}
|
|
@@ -169,19 +174,18 @@ declare class Model<F extends Fields> {
|
|
|
169
174
|
readonly schema: Schema<F>;
|
|
170
175
|
private ensured?;
|
|
171
176
|
constructor(db: Ndb, name: string, schema: Schema<F>);
|
|
172
|
-
get idField(): string;
|
|
173
177
|
path(op?: string): string;
|
|
174
178
|
ready(): Promise<void>;
|
|
175
179
|
insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
176
180
|
create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
177
|
-
update(
|
|
178
|
-
delete(
|
|
181
|
+
update(idOrFilter: string | Filter<F>, ops: UpdateOps<F>, opts?: UpdateOptions): Promise<UpdateResult<F>>;
|
|
182
|
+
delete(idOrFilter: string | Filter<F>, opts?: DeleteOptions): Promise<DeleteResult>;
|
|
179
183
|
expire(id: string, duration: Duration): Promise<ExpireResult>;
|
|
180
184
|
expireAt(id: string, at: Date): Promise<ExpireResult>;
|
|
181
185
|
persist(id: string): Promise<WriteResult>;
|
|
182
186
|
ttl(id: string): Promise<number | null>;
|
|
183
187
|
private setExpiry;
|
|
184
|
-
|
|
188
|
+
get(id: string): Promise<Row<F> | null>;
|
|
185
189
|
query(): Query<F>;
|
|
186
190
|
where(w: Where<F>): Query<F>;
|
|
187
191
|
not(w: Where<F>): Query<F>;
|
|
@@ -239,6 +243,8 @@ declare class Ndb {
|
|
|
239
243
|
}
|
|
240
244
|
declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
|
|
241
245
|
|
|
246
|
+
declare function newId(): string;
|
|
247
|
+
|
|
242
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 & {});
|
|
243
249
|
declare class NdbError extends Error {
|
|
244
250
|
readonly code: ErrorCode;
|
|
@@ -267,5 +273,5 @@ declare class TtlError extends NdbError {
|
|
|
267
273
|
}
|
|
268
274
|
declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
|
|
269
275
|
|
|
270
|
-
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, InvalidUpdateError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError, WatchLaggedError, connect, makeError };
|
|
276
|
+
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, InvalidUpdateError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError, WatchLaggedError, connect, makeError, newId };
|
|
271
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,12 +60,14 @@ 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
73
|
type Duration = number | string;
|
|
@@ -75,6 +79,7 @@ type Filter<F extends Fields> = Where<F> & {
|
|
|
75
79
|
};
|
|
76
80
|
interface UpdateOps<F extends Fields = Fields> {
|
|
77
81
|
$set?: Input<F> & Record<string, unknown>;
|
|
82
|
+
$setOnInsert?: Input<F> & Record<string, unknown>;
|
|
78
83
|
$unset?: Record<string, true | ''>;
|
|
79
84
|
$inc?: Record<string, number>;
|
|
80
85
|
$push?: Record<string, unknown>;
|
|
@@ -125,7 +130,7 @@ interface WatchEvent<R> {
|
|
|
125
130
|
consumer: string;
|
|
126
131
|
pos: string;
|
|
127
132
|
op: 'put' | 'del';
|
|
128
|
-
|
|
133
|
+
_id: string;
|
|
129
134
|
v: number;
|
|
130
135
|
doc: R | undefined;
|
|
131
136
|
}
|
|
@@ -169,19 +174,18 @@ declare class Model<F extends Fields> {
|
|
|
169
174
|
readonly schema: Schema<F>;
|
|
170
175
|
private ensured?;
|
|
171
176
|
constructor(db: Ndb, name: string, schema: Schema<F>);
|
|
172
|
-
get idField(): string;
|
|
173
177
|
path(op?: string): string;
|
|
174
178
|
ready(): Promise<void>;
|
|
175
179
|
insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
176
180
|
create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
177
|
-
update(
|
|
178
|
-
delete(
|
|
181
|
+
update(idOrFilter: string | Filter<F>, ops: UpdateOps<F>, opts?: UpdateOptions): Promise<UpdateResult<F>>;
|
|
182
|
+
delete(idOrFilter: string | Filter<F>, opts?: DeleteOptions): Promise<DeleteResult>;
|
|
179
183
|
expire(id: string, duration: Duration): Promise<ExpireResult>;
|
|
180
184
|
expireAt(id: string, at: Date): Promise<ExpireResult>;
|
|
181
185
|
persist(id: string): Promise<WriteResult>;
|
|
182
186
|
ttl(id: string): Promise<number | null>;
|
|
183
187
|
private setExpiry;
|
|
184
|
-
|
|
188
|
+
get(id: string): Promise<Row<F> | null>;
|
|
185
189
|
query(): Query<F>;
|
|
186
190
|
where(w: Where<F>): Query<F>;
|
|
187
191
|
not(w: Where<F>): Query<F>;
|
|
@@ -239,6 +243,8 @@ declare class Ndb {
|
|
|
239
243
|
}
|
|
240
244
|
declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
|
|
241
245
|
|
|
246
|
+
declare function newId(): string;
|
|
247
|
+
|
|
242
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 & {});
|
|
243
249
|
declare class NdbError extends Error {
|
|
244
250
|
readonly code: ErrorCode;
|
|
@@ -267,5 +273,5 @@ declare class TtlError extends NdbError {
|
|
|
267
273
|
}
|
|
268
274
|
declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
|
|
269
275
|
|
|
270
|
-
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, InvalidUpdateError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError, WatchLaggedError, connect, makeError };
|
|
276
|
+
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, InvalidUpdateError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError, WatchLaggedError, connect, makeError, newId };
|
|
271
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
|
@@ -214,7 +214,7 @@ async function* watchEvents(lines2, consumer) {
|
|
|
214
214
|
if (line.error) {
|
|
215
215
|
throw makeError(line.error.code, line.error.message);
|
|
216
216
|
}
|
|
217
|
-
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 };
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
@@ -229,9 +229,6 @@ var Model = class {
|
|
|
229
229
|
name;
|
|
230
230
|
schema;
|
|
231
231
|
ensured;
|
|
232
|
-
get idField() {
|
|
233
|
-
return this.schema.id ?? "_id";
|
|
234
|
-
}
|
|
235
232
|
path(op) {
|
|
236
233
|
const base = `/v1/c/${encodeURIComponent(this.name)}`;
|
|
237
234
|
return op ? `${base}/${op}` : base;
|
|
@@ -255,47 +252,55 @@ var Model = class {
|
|
|
255
252
|
create(docs, opts = {}) {
|
|
256
253
|
return this.insert(docs, opts);
|
|
257
254
|
}
|
|
258
|
-
// Atomic update
|
|
259
|
-
//
|
|
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
|
|
260
258
|
// concurrent updates never interleave. A version guard is a filter:
|
|
261
|
-
// {
|
|
262
|
-
|
|
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 = {}) {
|
|
263
262
|
await this.ready();
|
|
264
|
-
const
|
|
265
|
-
const
|
|
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";
|
|
266
267
|
const out = await this.db.transport.request(
|
|
267
268
|
"POST",
|
|
268
|
-
this.path(
|
|
269
|
+
this.path(op) + query,
|
|
269
270
|
body
|
|
270
271
|
);
|
|
271
272
|
return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
|
|
272
273
|
}
|
|
273
|
-
// Deletes the documents that the filter matches: one,
|
|
274
|
-
// many (same rule as update).
|
|
275
|
-
async delete(
|
|
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 = {}) {
|
|
276
277
|
await this.ready();
|
|
278
|
+
if (typeof idOrFilter === "string") {
|
|
279
|
+
return this.db.transport.request("DELETE", this.path(encodeURIComponent(idOrFilter)));
|
|
280
|
+
}
|
|
281
|
+
const filter = idOrFilter;
|
|
277
282
|
return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
|
|
278
283
|
}
|
|
279
284
|
// Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
|
|
280
285
|
// NotFoundError for a missing or expired document, and TtlError when the
|
|
281
286
|
// schema has no expires field.
|
|
282
287
|
expire(id, duration) {
|
|
283
|
-
return this.setExpiry({ id, in: duration });
|
|
288
|
+
return this.setExpiry({ _id: id, in: duration });
|
|
284
289
|
}
|
|
285
290
|
expireAt(id, at) {
|
|
286
|
-
return this.setExpiry({ id, at: at.toISOString() });
|
|
291
|
+
return this.setExpiry({ _id: id, at: at.toISOString() });
|
|
287
292
|
}
|
|
288
293
|
// Removes the expiry of a document (PERSIST in Redis).
|
|
289
294
|
async persist(id) {
|
|
290
295
|
await this.ready();
|
|
291
|
-
return this.db.transport.request("POST", this.path("persist"), { id });
|
|
296
|
+
return this.db.transport.request("POST", this.path("persist"), { _id: id });
|
|
292
297
|
}
|
|
293
298
|
// The seconds left (rounded up), -1 when the document never expires, null
|
|
294
299
|
// when it is missing or expired.
|
|
295
300
|
async ttl(id) {
|
|
296
301
|
await this.ready();
|
|
297
302
|
try {
|
|
298
|
-
const out = await this.db.transport.request("GET",
|
|
303
|
+
const out = await this.db.transport.request("GET", this.path(`${encodeURIComponent(id)}/ttl`));
|
|
299
304
|
return out.ttl;
|
|
300
305
|
} catch (err) {
|
|
301
306
|
if (err instanceof NotFoundError) {
|
|
@@ -307,10 +312,20 @@ var Model = class {
|
|
|
307
312
|
async setExpiry(body) {
|
|
308
313
|
await this.ready();
|
|
309
314
|
const out = await this.db.transport.request("POST", this.path("expire"), body);
|
|
310
|
-
return {
|
|
315
|
+
return { _id: out._id, v: out.v, expiresAt: new Date(out.expires_at) };
|
|
311
316
|
}
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
}
|
|
314
329
|
}
|
|
315
330
|
query() {
|
|
316
331
|
return new Query(this);
|
|
@@ -387,7 +402,6 @@ var Model = class {
|
|
|
387
402
|
catalog() {
|
|
388
403
|
const entries = Object.entries(this.schema.fields);
|
|
389
404
|
return {
|
|
390
|
-
id: this.schema.id,
|
|
391
405
|
time: this.schema.time,
|
|
392
406
|
expires_field: this.schema.expires,
|
|
393
407
|
indexes: entries.filter(([, d]) => d.index).map(([n]) => n),
|
|
@@ -612,6 +626,38 @@ var Ndb = class {
|
|
|
612
626
|
function connect(addr, opts) {
|
|
613
627
|
return new Ndb(addr, opts);
|
|
614
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
|
+
}
|
|
615
661
|
export {
|
|
616
662
|
CursorError,
|
|
617
663
|
DuplicateIdError,
|
|
@@ -628,5 +674,6 @@ export {
|
|
|
628
674
|
UniqueViolationError,
|
|
629
675
|
WatchLaggedError,
|
|
630
676
|
connect,
|
|
631
|
-
makeError
|
|
677
|
+
makeError,
|
|
678
|
+
newId
|
|
632
679
|
};
|