@thadeu/ndb 0.1.0 → 0.1.2
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 +40 -18
- package/dist/index.cjs +91 -15
- package/dist/index.d.cts +53 -13
- package/dist/index.d.ts +53 -13
- package/dist/index.js +89 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,7 +70,8 @@ const calls = ndb.model('calls', {
|
|
|
70
70
|
- `time` is the collection time field. Used by zone maps and `$dateTrunc`.
|
|
71
71
|
- The model declares its collection on first use (`ensure_collection`). You can call `await calls.ready()` to do it at once.
|
|
72
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.
|
|
73
|
-
-
|
|
73
|
+
- `expires` is the `date` field that holds when a document expires. It turns on `expireIn`, `expire`, `persist` and `ttl` (below).
|
|
74
|
+
- The client has no `ttl` catalog option and no blob API. Use the HTTP API for those.
|
|
74
75
|
|
|
75
76
|
## Insert, find, update, delete
|
|
76
77
|
|
|
@@ -85,31 +86,51 @@ const results = await calls.insert([
|
|
|
85
86
|
// Find by id. Returns null when missing.
|
|
86
87
|
const row = await calls.find('c1')
|
|
87
88
|
|
|
88
|
-
//
|
|
89
|
-
|
|
89
|
+
// Atomic update by filter: filter first, then the write operators.
|
|
90
|
+
// _v in the filter is an optimistic lock.
|
|
91
|
+
const res = await calls.update({ call_id: 'c1', _v: row!._v }, { $set: { status: 'done' }, $unset: { duration: true } })
|
|
92
|
+
// res: { matched, modified, doc } (doc is the new row with _v; absent for many)
|
|
90
93
|
|
|
91
|
-
//
|
|
92
|
-
await
|
|
94
|
+
// A guarded decrement: exactly one of two racing calls wins.
|
|
95
|
+
await stock.update({ sku: 'a', qty: { $gte: 1 } }, { $inc: { qty: -1 } })
|
|
93
96
|
|
|
94
|
-
//
|
|
95
|
-
await calls.
|
|
97
|
+
// Every match (the filter needs the id or an indexed field), or create it.
|
|
98
|
+
await calls.update({ status: 'failed' }, { $set: { status: 'retry' } }, { many: true })
|
|
99
|
+
await calls.update({ call_id: 'c9' }, { $inc: { duration: 1 } }, { upsert: true })
|
|
100
|
+
|
|
101
|
+
// Delete by filter: { deleted }.
|
|
102
|
+
await calls.delete({ call_id: 'c2' })
|
|
103
|
+
await calls.delete({ status: 'failed' }, { many: true })
|
|
96
104
|
```
|
|
97
105
|
|
|
98
|
-
`
|
|
106
|
+
- Operators: `$set`, `$unset`, `$inc`, `$push` (with `{ $each: [...] }`), `$pull`, `$min`, `$max`. Field names can be dot paths.
|
|
107
|
+
- 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 the id or an indexed field in the filter. `upsert` creates the document from the equality fields of the filter.
|
|
109
|
+
- 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
|
+
An insert with a duplicate id throws `DuplicateIdError`. A duplicate `unique` value throws `UniqueViolationError`. An insert without the id when the schema names one fails with code `missing_id`.
|
|
112
|
+
|
|
113
|
+
### Expiry (TTL like Redis)
|
|
99
114
|
|
|
100
115
|
```ts
|
|
101
|
-
|
|
116
|
+
const sessions = db.model('sessions', {
|
|
117
|
+
id: 'sid',
|
|
118
|
+
expires: 'exp',
|
|
119
|
+
fields: { sid: { type: 'string' }, exp: { type: 'date', optional: true } },
|
|
120
|
+
})
|
|
102
121
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
await sessions.insert({ sid: 's1' }, { expireIn: '1h' }) // or 3600; all docs of the call
|
|
123
|
+
await sessions.update({ sid: 's1' }, { $set: { user: 'u' } }, { expireIn: '30m' })
|
|
124
|
+
|
|
125
|
+
const { v, expiresAt } = await sessions.expire('s1', '30m') // EXPIRE
|
|
126
|
+
await sessions.expireAt('s1', new Date(Date.now() + 3_600_000))
|
|
127
|
+
await sessions.ttl('s1') // seconds left, -1 = never expires, null = missing or expired
|
|
128
|
+
await sessions.persist('s1') // PERSIST
|
|
110
129
|
```
|
|
111
130
|
|
|
112
|
-
|
|
131
|
+
- A duration is seconds or a string such as `"90m"`, `"1h30m"`, `"1d"`. `expireIn` wins over a value of the `expires` field in the same call.
|
|
132
|
+
- `expire` and `persist` throw `NotFoundError` for a missing or expired document. `TtlError` (`ttl_not_enabled`, `ttl_derived`) means the schema has no `expires` field, or the expiry comes from `ttl` + `time`.
|
|
133
|
+
- Disk space of expired documents returns after compaction, and fully only with `wal_retention`; see [Freeing disk](https://github.com/thadeu/ndb/blob/main/docs/dev/configuration.md#freeing-disk).
|
|
113
134
|
|
|
114
135
|
## Query
|
|
115
136
|
|
|
@@ -245,13 +266,14 @@ Daemon errors are `NdbError` or a subclass. They have `code` (the daemon code) a
|
|
|
245
266
|
|---|---|
|
|
246
267
|
| `UnauthorizedError` | `unauthorized` |
|
|
247
268
|
| `NotFoundError` | `not_found`, `unknown_collection` |
|
|
248
|
-
| `VersionConflictError` | `version_conflict` |
|
|
249
269
|
| `DuplicateIdError` | `duplicate_id` |
|
|
250
270
|
| `UniqueViolationError` | `unique_violation` |
|
|
251
271
|
| `InvalidQueryError` | `invalid_query`, `invalid_pipeline` |
|
|
252
272
|
| `CursorError` | `cursor_invalid`, `cursor_mismatch`, `cursor_expired` |
|
|
253
273
|
| `WatchLaggedError` | `watch_lagged` |
|
|
254
274
|
| `HistoryTruncatedError` | `history_truncated` |
|
|
275
|
+
| `TtlError` | `ttl_not_enabled`, `ttl_derived` |
|
|
276
|
+
| `InvalidUpdateError` | `unknown_operator`, `no_update`, `conflicting_update`, `invalid_field`, `unindexed_many`, `empty_filter`, `invalid_update` |
|
|
255
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`, ... |
|
|
256
278
|
|
|
257
279
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -24,14 +24,15 @@ __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,
|
|
30
31
|
NotFoundError: () => NotFoundError,
|
|
31
32
|
Query: () => Query,
|
|
33
|
+
TtlError: () => TtlError,
|
|
32
34
|
UnauthorizedError: () => UnauthorizedError,
|
|
33
35
|
UniqueViolationError: () => UniqueViolationError,
|
|
34
|
-
VersionConflictError: () => VersionConflictError,
|
|
35
36
|
WatchLaggedError: () => WatchLaggedError,
|
|
36
37
|
connect: () => connect,
|
|
37
38
|
makeError: () => makeError
|
|
@@ -53,8 +54,6 @@ var UnauthorizedError = class extends NdbError {
|
|
|
53
54
|
};
|
|
54
55
|
var NotFoundError = class extends NdbError {
|
|
55
56
|
};
|
|
56
|
-
var VersionConflictError = class extends NdbError {
|
|
57
|
-
};
|
|
58
57
|
var DuplicateIdError = class extends NdbError {
|
|
59
58
|
};
|
|
60
59
|
var UniqueViolationError = class extends NdbError {
|
|
@@ -67,11 +66,14 @@ var WatchLaggedError = class extends NdbError {
|
|
|
67
66
|
};
|
|
68
67
|
var HistoryTruncatedError = class extends NdbError {
|
|
69
68
|
};
|
|
69
|
+
var InvalidUpdateError = class extends NdbError {
|
|
70
|
+
};
|
|
71
|
+
var TtlError = class extends NdbError {
|
|
72
|
+
};
|
|
70
73
|
var subclasses = {
|
|
71
74
|
unauthorized: UnauthorizedError,
|
|
72
75
|
not_found: NotFoundError,
|
|
73
76
|
unknown_collection: NotFoundError,
|
|
74
|
-
version_conflict: VersionConflictError,
|
|
75
77
|
duplicate_id: DuplicateIdError,
|
|
76
78
|
unique_violation: UniqueViolationError,
|
|
77
79
|
invalid_query: InvalidQueryError,
|
|
@@ -80,7 +82,15 @@ var subclasses = {
|
|
|
80
82
|
cursor_mismatch: CursorError,
|
|
81
83
|
cursor_expired: CursorError,
|
|
82
84
|
watch_lagged: WatchLaggedError,
|
|
83
|
-
history_truncated: HistoryTruncatedError
|
|
85
|
+
history_truncated: HistoryTruncatedError,
|
|
86
|
+
ttl_not_enabled: TtlError,
|
|
87
|
+
ttl_derived: TtlError,
|
|
88
|
+
unknown_operator: InvalidUpdateError,
|
|
89
|
+
no_update: InvalidUpdateError,
|
|
90
|
+
conflicting_update: InvalidUpdateError,
|
|
91
|
+
invalid_field: InvalidUpdateError,
|
|
92
|
+
unindexed_many: InvalidUpdateError,
|
|
93
|
+
empty_filter: InvalidUpdateError
|
|
84
94
|
};
|
|
85
95
|
function makeError(code, message, status = 0) {
|
|
86
96
|
const Cls = subclasses[code] ?? NdbError;
|
|
@@ -274,25 +284,71 @@ var Model = class {
|
|
|
274
284
|
});
|
|
275
285
|
return this.ensured;
|
|
276
286
|
}
|
|
277
|
-
async insert(docs) {
|
|
287
|
+
async insert(docs, opts = {}) {
|
|
278
288
|
await this.ready();
|
|
279
289
|
const list = Array.isArray(docs) ? docs : [docs];
|
|
280
290
|
const out = await this.db.transport.request("POST", this.path("insert"), {
|
|
281
|
-
insert: list
|
|
291
|
+
insert: list,
|
|
292
|
+
expire_in: opts.expireIn
|
|
282
293
|
});
|
|
283
294
|
return out.results;
|
|
284
295
|
}
|
|
285
|
-
create(docs) {
|
|
286
|
-
return this.insert(docs);
|
|
296
|
+
create(docs, opts = {}) {
|
|
297
|
+
return this.insert(docs, opts);
|
|
298
|
+
}
|
|
299
|
+
// Atomic update by filter. The filter and the operators are sent as one
|
|
300
|
+
// body; the daemon matches and writes in one step on the newest state, so
|
|
301
|
+
// concurrent updates never interleave. A version guard is a filter:
|
|
302
|
+
// { call_id: 'c1', _v: 7 }. Matching nothing is not an error: matched is 0.
|
|
303
|
+
async update(filter, ops, opts = {}) {
|
|
304
|
+
await this.ready();
|
|
305
|
+
const body = mergeUpdate(filter, ops);
|
|
306
|
+
const query = writeQuery({ many: opts.many, upsert: opts.upsert, expire_in: opts.expireIn });
|
|
307
|
+
const out = await this.db.transport.request(
|
|
308
|
+
"POST",
|
|
309
|
+
this.path("update") + query,
|
|
310
|
+
body
|
|
311
|
+
);
|
|
312
|
+
return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
|
|
287
313
|
}
|
|
288
|
-
|
|
314
|
+
// Deletes the documents that the filter matches: one, or every match with
|
|
315
|
+
// many (same rule as update).
|
|
316
|
+
async delete(filter, opts = {}) {
|
|
289
317
|
await this.ready();
|
|
290
|
-
|
|
291
|
-
return this.db.transport.request("POST", this.path("update"), { update });
|
|
318
|
+
return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
|
|
292
319
|
}
|
|
293
|
-
|
|
320
|
+
// Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
|
|
321
|
+
// NotFoundError for a missing or expired document, and TtlError when the
|
|
322
|
+
// schema has no expires field.
|
|
323
|
+
expire(id, duration) {
|
|
324
|
+
return this.setExpiry({ id, in: duration });
|
|
325
|
+
}
|
|
326
|
+
expireAt(id, at) {
|
|
327
|
+
return this.setExpiry({ id, at: at.toISOString() });
|
|
328
|
+
}
|
|
329
|
+
// Removes the expiry of a document (PERSIST in Redis).
|
|
330
|
+
async persist(id) {
|
|
294
331
|
await this.ready();
|
|
295
|
-
|
|
332
|
+
return this.db.transport.request("POST", this.path("persist"), { id });
|
|
333
|
+
}
|
|
334
|
+
// The seconds left (rounded up), -1 when the document never expires, null
|
|
335
|
+
// when it is missing or expired.
|
|
336
|
+
async ttl(id) {
|
|
337
|
+
await this.ready();
|
|
338
|
+
try {
|
|
339
|
+
const out = await this.db.transport.request("GET", `${this.path("ttl")}/${encodeURIComponent(id)}`);
|
|
340
|
+
return out.ttl;
|
|
341
|
+
} catch (err) {
|
|
342
|
+
if (err instanceof NotFoundError) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async setExpiry(body) {
|
|
349
|
+
await this.ready();
|
|
350
|
+
const out = await this.db.transport.request("POST", this.path("expire"), body);
|
|
351
|
+
return { id: out.id, v: out.v, expiresAt: new Date(out.expires_at) };
|
|
296
352
|
}
|
|
297
353
|
find(id) {
|
|
298
354
|
return this.query().where({ [this.idField]: id }).first();
|
|
@@ -374,11 +430,30 @@ var Model = class {
|
|
|
374
430
|
return {
|
|
375
431
|
id: this.schema.id,
|
|
376
432
|
time: this.schema.time,
|
|
433
|
+
expires_field: this.schema.expires,
|
|
377
434
|
indexes: entries.filter(([, d]) => d.index).map(([n]) => n),
|
|
378
435
|
unique: entries.filter(([, d]) => d.unique).map(([n]) => n)
|
|
379
436
|
};
|
|
380
437
|
}
|
|
381
438
|
};
|
|
439
|
+
function writeQuery(params) {
|
|
440
|
+
const q = new URLSearchParams();
|
|
441
|
+
for (const [name, value] of Object.entries(params)) {
|
|
442
|
+
if (value !== void 0 && value !== false) {
|
|
443
|
+
q.set(name, String(value));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const text = q.toString();
|
|
447
|
+
return text === "" ? "" : `?${text}`;
|
|
448
|
+
}
|
|
449
|
+
function mergeUpdate(filter, ops) {
|
|
450
|
+
for (const key of Object.keys(ops)) {
|
|
451
|
+
if (!key.startsWith("$")) {
|
|
452
|
+
throw new InvalidUpdateError("invalid_update", `update operator "${key}" must start with $ (for example "$set")`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return { ...toWhere(filter), ...ops };
|
|
456
|
+
}
|
|
382
457
|
|
|
383
458
|
// src/transport.ts
|
|
384
459
|
var import_undici = require("undici");
|
|
@@ -584,14 +659,15 @@ function connect(addr, opts) {
|
|
|
584
659
|
DuplicateIdError,
|
|
585
660
|
HistoryTruncatedError,
|
|
586
661
|
InvalidQueryError,
|
|
662
|
+
InvalidUpdateError,
|
|
587
663
|
Model,
|
|
588
664
|
Ndb,
|
|
589
665
|
NdbError,
|
|
590
666
|
NotFoundError,
|
|
591
667
|
Query,
|
|
668
|
+
TtlError,
|
|
592
669
|
UnauthorizedError,
|
|
593
670
|
UniqueViolationError,
|
|
594
|
-
VersionConflictError,
|
|
595
671
|
WatchLaggedError,
|
|
596
672
|
connect,
|
|
597
673
|
makeError
|
package/dist/index.d.cts
CHANGED
|
@@ -18,6 +18,7 @@ type Fields = Record<string, FieldDef>;
|
|
|
18
18
|
interface Schema<F extends Fields> {
|
|
19
19
|
id?: Extract<keyof F, string>;
|
|
20
20
|
time?: Extract<keyof F, string>;
|
|
21
|
+
expires?: Extract<keyof F, string>;
|
|
21
22
|
fields: F;
|
|
22
23
|
}
|
|
23
24
|
type ValueOf<D extends FieldDef> = TypeMap[D['type']];
|
|
@@ -65,11 +66,43 @@ interface WriteResult {
|
|
|
65
66
|
id: string;
|
|
66
67
|
v: number;
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
type Duration = number | string;
|
|
70
|
+
type Filter<F extends Fields> = Where<F> & {
|
|
71
|
+
_v?: number | Ops<number>;
|
|
72
|
+
$and?: Filter<F>[];
|
|
73
|
+
$or?: Filter<F>[];
|
|
74
|
+
$nor?: Filter<F>[];
|
|
75
|
+
};
|
|
76
|
+
interface UpdateOps<F extends Fields = Fields> {
|
|
77
|
+
$set?: Input<F> & Record<string, unknown>;
|
|
78
|
+
$unset?: Record<string, true | ''>;
|
|
79
|
+
$inc?: Record<string, number>;
|
|
80
|
+
$push?: Record<string, unknown>;
|
|
81
|
+
$pull?: Record<string, unknown>;
|
|
82
|
+
$min?: Record<string, number | string>;
|
|
83
|
+
$max?: Record<string, number | string>;
|
|
84
|
+
}
|
|
85
|
+
interface UpdateOptions {
|
|
86
|
+
many?: boolean;
|
|
87
|
+
upsert?: boolean;
|
|
88
|
+
expireIn?: Duration;
|
|
89
|
+
}
|
|
90
|
+
interface DeleteOptions {
|
|
91
|
+
many?: boolean;
|
|
92
|
+
}
|
|
93
|
+
interface UpdateResult<F extends Fields = Fields> {
|
|
94
|
+
matched: number;
|
|
95
|
+
modified: number;
|
|
96
|
+
doc?: Row<F>;
|
|
97
|
+
}
|
|
98
|
+
interface DeleteResult {
|
|
99
|
+
deleted: number;
|
|
70
100
|
}
|
|
71
|
-
interface
|
|
72
|
-
|
|
101
|
+
interface InsertOptions {
|
|
102
|
+
expireIn?: Duration;
|
|
103
|
+
}
|
|
104
|
+
interface ExpireResult extends WriteResult {
|
|
105
|
+
expiresAt: Date;
|
|
73
106
|
}
|
|
74
107
|
interface AggregateOptions {
|
|
75
108
|
limit?: number;
|
|
@@ -139,10 +172,15 @@ declare class Model<F extends Fields> {
|
|
|
139
172
|
get idField(): string;
|
|
140
173
|
path(op?: string): string;
|
|
141
174
|
ready(): Promise<void>;
|
|
142
|
-
insert(docs: Input<F> | Input<F>[]): Promise<WriteResult[]>;
|
|
143
|
-
create(docs: Input<F> | Input<F>[]): Promise<WriteResult[]>;
|
|
144
|
-
update(
|
|
145
|
-
delete(
|
|
175
|
+
insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
176
|
+
create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
177
|
+
update(filter: Filter<F>, ops: UpdateOps<F>, opts?: UpdateOptions): Promise<UpdateResult<F>>;
|
|
178
|
+
delete(filter: Filter<F>, opts?: DeleteOptions): Promise<DeleteResult>;
|
|
179
|
+
expire(id: string, duration: Duration): Promise<ExpireResult>;
|
|
180
|
+
expireAt(id: string, at: Date): Promise<ExpireResult>;
|
|
181
|
+
persist(id: string): Promise<WriteResult>;
|
|
182
|
+
ttl(id: string): Promise<number | null>;
|
|
183
|
+
private setExpiry;
|
|
146
184
|
find(id: string): Promise<Row<F> | null>;
|
|
147
185
|
query(): Query<F>;
|
|
148
186
|
where(w: Where<F>): Query<F>;
|
|
@@ -201,7 +239,7 @@ declare class Ndb {
|
|
|
201
239
|
}
|
|
202
240
|
declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
|
|
203
241
|
|
|
204
|
-
type ErrorCode = 'unauthorized' | 'not_found' | '
|
|
242
|
+
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 & {});
|
|
205
243
|
declare class NdbError extends Error {
|
|
206
244
|
readonly code: ErrorCode;
|
|
207
245
|
readonly status: number;
|
|
@@ -211,8 +249,6 @@ declare class UnauthorizedError extends NdbError {
|
|
|
211
249
|
}
|
|
212
250
|
declare class NotFoundError extends NdbError {
|
|
213
251
|
}
|
|
214
|
-
declare class VersionConflictError extends NdbError {
|
|
215
|
-
}
|
|
216
252
|
declare class DuplicateIdError extends NdbError {
|
|
217
253
|
}
|
|
218
254
|
declare class UniqueViolationError extends NdbError {
|
|
@@ -225,7 +261,11 @@ declare class WatchLaggedError extends NdbError {
|
|
|
225
261
|
}
|
|
226
262
|
declare class HistoryTruncatedError extends NdbError {
|
|
227
263
|
}
|
|
264
|
+
declare class InvalidUpdateError extends NdbError {
|
|
265
|
+
}
|
|
266
|
+
declare class TtlError extends NdbError {
|
|
267
|
+
}
|
|
228
268
|
declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
|
|
229
269
|
|
|
230
|
-
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, Model, Ndb, NdbError, NotFoundError, Query, UnauthorizedError, UniqueViolationError,
|
|
231
|
-
export type { AggregateOptions, AggregateResult, ConnectOptions, Doc, ErrorCode, FieldDef, FieldType, Fields, Input, Ops, Row, Schema, SortSpec, TypeMap, UpdateOptions, WatchEvent, WatchOptions, Where,
|
|
270
|
+
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, InvalidUpdateError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError, WatchLaggedError, connect, makeError };
|
|
271
|
+
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
|
@@ -18,6 +18,7 @@ type Fields = Record<string, FieldDef>;
|
|
|
18
18
|
interface Schema<F extends Fields> {
|
|
19
19
|
id?: Extract<keyof F, string>;
|
|
20
20
|
time?: Extract<keyof F, string>;
|
|
21
|
+
expires?: Extract<keyof F, string>;
|
|
21
22
|
fields: F;
|
|
22
23
|
}
|
|
23
24
|
type ValueOf<D extends FieldDef> = TypeMap[D['type']];
|
|
@@ -65,11 +66,43 @@ interface WriteResult {
|
|
|
65
66
|
id: string;
|
|
66
67
|
v: number;
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
type Duration = number | string;
|
|
70
|
+
type Filter<F extends Fields> = Where<F> & {
|
|
71
|
+
_v?: number | Ops<number>;
|
|
72
|
+
$and?: Filter<F>[];
|
|
73
|
+
$or?: Filter<F>[];
|
|
74
|
+
$nor?: Filter<F>[];
|
|
75
|
+
};
|
|
76
|
+
interface UpdateOps<F extends Fields = Fields> {
|
|
77
|
+
$set?: Input<F> & Record<string, unknown>;
|
|
78
|
+
$unset?: Record<string, true | ''>;
|
|
79
|
+
$inc?: Record<string, number>;
|
|
80
|
+
$push?: Record<string, unknown>;
|
|
81
|
+
$pull?: Record<string, unknown>;
|
|
82
|
+
$min?: Record<string, number | string>;
|
|
83
|
+
$max?: Record<string, number | string>;
|
|
84
|
+
}
|
|
85
|
+
interface UpdateOptions {
|
|
86
|
+
many?: boolean;
|
|
87
|
+
upsert?: boolean;
|
|
88
|
+
expireIn?: Duration;
|
|
89
|
+
}
|
|
90
|
+
interface DeleteOptions {
|
|
91
|
+
many?: boolean;
|
|
92
|
+
}
|
|
93
|
+
interface UpdateResult<F extends Fields = Fields> {
|
|
94
|
+
matched: number;
|
|
95
|
+
modified: number;
|
|
96
|
+
doc?: Row<F>;
|
|
97
|
+
}
|
|
98
|
+
interface DeleteResult {
|
|
99
|
+
deleted: number;
|
|
70
100
|
}
|
|
71
|
-
interface
|
|
72
|
-
|
|
101
|
+
interface InsertOptions {
|
|
102
|
+
expireIn?: Duration;
|
|
103
|
+
}
|
|
104
|
+
interface ExpireResult extends WriteResult {
|
|
105
|
+
expiresAt: Date;
|
|
73
106
|
}
|
|
74
107
|
interface AggregateOptions {
|
|
75
108
|
limit?: number;
|
|
@@ -139,10 +172,15 @@ declare class Model<F extends Fields> {
|
|
|
139
172
|
get idField(): string;
|
|
140
173
|
path(op?: string): string;
|
|
141
174
|
ready(): Promise<void>;
|
|
142
|
-
insert(docs: Input<F> | Input<F>[]): Promise<WriteResult[]>;
|
|
143
|
-
create(docs: Input<F> | Input<F>[]): Promise<WriteResult[]>;
|
|
144
|
-
update(
|
|
145
|
-
delete(
|
|
175
|
+
insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
176
|
+
create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
|
|
177
|
+
update(filter: Filter<F>, ops: UpdateOps<F>, opts?: UpdateOptions): Promise<UpdateResult<F>>;
|
|
178
|
+
delete(filter: Filter<F>, opts?: DeleteOptions): Promise<DeleteResult>;
|
|
179
|
+
expire(id: string, duration: Duration): Promise<ExpireResult>;
|
|
180
|
+
expireAt(id: string, at: Date): Promise<ExpireResult>;
|
|
181
|
+
persist(id: string): Promise<WriteResult>;
|
|
182
|
+
ttl(id: string): Promise<number | null>;
|
|
183
|
+
private setExpiry;
|
|
146
184
|
find(id: string): Promise<Row<F> | null>;
|
|
147
185
|
query(): Query<F>;
|
|
148
186
|
where(w: Where<F>): Query<F>;
|
|
@@ -201,7 +239,7 @@ declare class Ndb {
|
|
|
201
239
|
}
|
|
202
240
|
declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
|
|
203
241
|
|
|
204
|
-
type ErrorCode = 'unauthorized' | 'not_found' | '
|
|
242
|
+
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 & {});
|
|
205
243
|
declare class NdbError extends Error {
|
|
206
244
|
readonly code: ErrorCode;
|
|
207
245
|
readonly status: number;
|
|
@@ -211,8 +249,6 @@ declare class UnauthorizedError extends NdbError {
|
|
|
211
249
|
}
|
|
212
250
|
declare class NotFoundError extends NdbError {
|
|
213
251
|
}
|
|
214
|
-
declare class VersionConflictError extends NdbError {
|
|
215
|
-
}
|
|
216
252
|
declare class DuplicateIdError extends NdbError {
|
|
217
253
|
}
|
|
218
254
|
declare class UniqueViolationError extends NdbError {
|
|
@@ -225,7 +261,11 @@ declare class WatchLaggedError extends NdbError {
|
|
|
225
261
|
}
|
|
226
262
|
declare class HistoryTruncatedError extends NdbError {
|
|
227
263
|
}
|
|
264
|
+
declare class InvalidUpdateError extends NdbError {
|
|
265
|
+
}
|
|
266
|
+
declare class TtlError extends NdbError {
|
|
267
|
+
}
|
|
228
268
|
declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
|
|
229
269
|
|
|
230
|
-
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, Model, Ndb, NdbError, NotFoundError, Query, UnauthorizedError, UniqueViolationError,
|
|
231
|
-
export type { AggregateOptions, AggregateResult, ConnectOptions, Doc, ErrorCode, FieldDef, FieldType, Fields, Input, Ops, Row, Schema, SortSpec, TypeMap, UpdateOptions, WatchEvent, WatchOptions, Where,
|
|
270
|
+
export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, InvalidUpdateError, Model, Ndb, NdbError, NotFoundError, Query, TtlError, UnauthorizedError, UniqueViolationError, WatchLaggedError, connect, makeError };
|
|
271
|
+
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,11 +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
|
+
var TtlError = class extends NdbError {
|
|
31
|
+
};
|
|
30
32
|
var subclasses = {
|
|
31
33
|
unauthorized: UnauthorizedError,
|
|
32
34
|
not_found: NotFoundError,
|
|
33
35
|
unknown_collection: NotFoundError,
|
|
34
|
-
version_conflict: VersionConflictError,
|
|
35
36
|
duplicate_id: DuplicateIdError,
|
|
36
37
|
unique_violation: UniqueViolationError,
|
|
37
38
|
invalid_query: InvalidQueryError,
|
|
@@ -40,7 +41,15 @@ var subclasses = {
|
|
|
40
41
|
cursor_mismatch: CursorError,
|
|
41
42
|
cursor_expired: CursorError,
|
|
42
43
|
watch_lagged: WatchLaggedError,
|
|
43
|
-
history_truncated: HistoryTruncatedError
|
|
44
|
+
history_truncated: HistoryTruncatedError,
|
|
45
|
+
ttl_not_enabled: 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
|
|
44
53
|
};
|
|
45
54
|
function makeError(code, message, status = 0) {
|
|
46
55
|
const Cls = subclasses[code] ?? NdbError;
|
|
@@ -234,25 +243,71 @@ var Model = class {
|
|
|
234
243
|
});
|
|
235
244
|
return this.ensured;
|
|
236
245
|
}
|
|
237
|
-
async insert(docs) {
|
|
246
|
+
async insert(docs, opts = {}) {
|
|
238
247
|
await this.ready();
|
|
239
248
|
const list = Array.isArray(docs) ? docs : [docs];
|
|
240
249
|
const out = await this.db.transport.request("POST", this.path("insert"), {
|
|
241
|
-
insert: list
|
|
250
|
+
insert: list,
|
|
251
|
+
expire_in: opts.expireIn
|
|
242
252
|
});
|
|
243
253
|
return out.results;
|
|
244
254
|
}
|
|
245
|
-
create(docs) {
|
|
246
|
-
return this.insert(docs);
|
|
255
|
+
create(docs, opts = {}) {
|
|
256
|
+
return this.insert(docs, opts);
|
|
257
|
+
}
|
|
258
|
+
// Atomic update by filter. The filter and the operators are sent as one
|
|
259
|
+
// body; the daemon matches and writes in one step on the newest state, so
|
|
260
|
+
// concurrent updates never interleave. A version guard is a filter:
|
|
261
|
+
// { call_id: 'c1', _v: 7 }. Matching nothing is not an error: matched is 0.
|
|
262
|
+
async update(filter, ops, opts = {}) {
|
|
263
|
+
await this.ready();
|
|
264
|
+
const body = mergeUpdate(filter, ops);
|
|
265
|
+
const query = writeQuery({ many: opts.many, upsert: opts.upsert, expire_in: opts.expireIn });
|
|
266
|
+
const out = await this.db.transport.request(
|
|
267
|
+
"POST",
|
|
268
|
+
this.path("update") + query,
|
|
269
|
+
body
|
|
270
|
+
);
|
|
271
|
+
return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
|
|
247
272
|
}
|
|
248
|
-
|
|
273
|
+
// Deletes the documents that the filter matches: one, or every match with
|
|
274
|
+
// many (same rule as update).
|
|
275
|
+
async delete(filter, opts = {}) {
|
|
249
276
|
await this.ready();
|
|
250
|
-
|
|
251
|
-
return this.db.transport.request("POST", this.path("update"), { update });
|
|
277
|
+
return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
|
|
252
278
|
}
|
|
253
|
-
|
|
279
|
+
// Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
|
|
280
|
+
// NotFoundError for a missing or expired document, and TtlError when the
|
|
281
|
+
// schema has no expires field.
|
|
282
|
+
expire(id, duration) {
|
|
283
|
+
return this.setExpiry({ id, in: duration });
|
|
284
|
+
}
|
|
285
|
+
expireAt(id, at) {
|
|
286
|
+
return this.setExpiry({ id, at: at.toISOString() });
|
|
287
|
+
}
|
|
288
|
+
// Removes the expiry of a document (PERSIST in Redis).
|
|
289
|
+
async persist(id) {
|
|
254
290
|
await this.ready();
|
|
255
|
-
|
|
291
|
+
return this.db.transport.request("POST", this.path("persist"), { id });
|
|
292
|
+
}
|
|
293
|
+
// The seconds left (rounded up), -1 when the document never expires, null
|
|
294
|
+
// when it is missing or expired.
|
|
295
|
+
async ttl(id) {
|
|
296
|
+
await this.ready();
|
|
297
|
+
try {
|
|
298
|
+
const out = await this.db.transport.request("GET", `${this.path("ttl")}/${encodeURIComponent(id)}`);
|
|
299
|
+
return out.ttl;
|
|
300
|
+
} catch (err) {
|
|
301
|
+
if (err instanceof NotFoundError) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async setExpiry(body) {
|
|
308
|
+
await this.ready();
|
|
309
|
+
const out = await this.db.transport.request("POST", this.path("expire"), body);
|
|
310
|
+
return { id: out.id, v: out.v, expiresAt: new Date(out.expires_at) };
|
|
256
311
|
}
|
|
257
312
|
find(id) {
|
|
258
313
|
return this.query().where({ [this.idField]: id }).first();
|
|
@@ -334,11 +389,30 @@ var Model = class {
|
|
|
334
389
|
return {
|
|
335
390
|
id: this.schema.id,
|
|
336
391
|
time: this.schema.time,
|
|
392
|
+
expires_field: this.schema.expires,
|
|
337
393
|
indexes: entries.filter(([, d]) => d.index).map(([n]) => n),
|
|
338
394
|
unique: entries.filter(([, d]) => d.unique).map(([n]) => n)
|
|
339
395
|
};
|
|
340
396
|
}
|
|
341
397
|
};
|
|
398
|
+
function writeQuery(params) {
|
|
399
|
+
const q = new URLSearchParams();
|
|
400
|
+
for (const [name, value] of Object.entries(params)) {
|
|
401
|
+
if (value !== void 0 && value !== false) {
|
|
402
|
+
q.set(name, String(value));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const text = q.toString();
|
|
406
|
+
return text === "" ? "" : `?${text}`;
|
|
407
|
+
}
|
|
408
|
+
function mergeUpdate(filter, ops) {
|
|
409
|
+
for (const key of Object.keys(ops)) {
|
|
410
|
+
if (!key.startsWith("$")) {
|
|
411
|
+
throw new InvalidUpdateError("invalid_update", `update operator "${key}" must start with $ (for example "$set")`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { ...toWhere(filter), ...ops };
|
|
415
|
+
}
|
|
342
416
|
|
|
343
417
|
// src/transport.ts
|
|
344
418
|
import { Pool } from "undici";
|
|
@@ -543,14 +617,15 @@ export {
|
|
|
543
617
|
DuplicateIdError,
|
|
544
618
|
HistoryTruncatedError,
|
|
545
619
|
InvalidQueryError,
|
|
620
|
+
InvalidUpdateError,
|
|
546
621
|
Model,
|
|
547
622
|
Ndb,
|
|
548
623
|
NdbError,
|
|
549
624
|
NotFoundError,
|
|
550
625
|
Query,
|
|
626
|
+
TtlError,
|
|
551
627
|
UnauthorizedError,
|
|
552
628
|
UniqueViolationError,
|
|
553
|
-
VersionConflictError,
|
|
554
629
|
WatchLaggedError,
|
|
555
630
|
connect,
|
|
556
631
|
makeError
|