@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 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
- - `id` is the id field. Without it the id is `_id`, made by the daemon.
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
- { call_id: 'c1', status: 'answered', caller: 'a', duration: 30, at: new Date() },
82
- { call_id: 'c2', status: 'failed', caller: 'b', at: new Date() },
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: [{ id: 'c1', v: 1 }, ...]
82
+ // results: [{ _id: 'c1', v: 1 }, ...]
85
83
 
86
- // Find by id. Returns null when missing.
87
- const row = await calls.find('c1')
84
+ // Get by _id (GET /v1/c/calls/c1). Returns null when missing.
85
+ const row = await calls.get('c1')
88
86
 
89
- // Update with optimistic locking.
90
- await calls.update('c1', { status: 'done' }, { ifVersion: row!._v })
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
- // Remove a field.
93
- await calls.update('c1', {}, { unset: ['duration'] })
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
- // Delete. ifVersion is optional here too.
96
- await calls.delete('c1', { ifVersion: 3 })
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
- ```ts
102
- import { VersionConflictError } from '@thadeu/ndb'
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
- try {
105
- await calls.update('c1', { status: 'x' }, { ifVersion: row!._v })
106
- } catch (err) {
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
- 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`.
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: { sid: { type: 'string' }, exp: { type: 'date', optional: true } },
119
+ fields: { exp: { type: 'date', optional: true } },
122
120
  })
123
121
 
124
- await sessions.insert({ sid: 's1' }, { expireIn: '1h' }) // or 3600; all docs of the call
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, call_id: 1 })
148
- .fields('call_id', 'status')
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({ call_id: 1 }).limit(100)
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({ call_id: 1 }).limit(100).after(token)
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({ call_id: 1 }).limit(500).iterate()) {
186
- console.log(row.call_id)
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', id, v, doc }
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.find('c1')
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, id: line.id, v: line.v, doc: line.doc };
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
- async update(id, set, opts = {}) {
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 update = { id, set, unset: opts.unset, if_version: opts.ifVersion, expire_in: opts.expireIn };
297
- return this.db.transport.request("POST", this.path("update"), { update });
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
- async delete(id, opts = {}) {
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
- await this.db.transport.request("POST", this.path("delete"), { delete: { id, if_version: opts.ifVersion } });
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, opts = {}) {
307
- return this.setExpiry({ id, in: duration, if_version: opts.ifVersion });
329
+ expire(id, duration) {
330
+ return this.setExpiry({ _id: id, in: duration });
308
331
  }
309
- expireAt(id, at, opts = {}) {
310
- return this.setExpiry({ id, at: at.toISOString(), if_version: opts.ifVersion });
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", `${this.path("ttl")}/${encodeURIComponent(id)}`);
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 { id: out.id, v: out.v, expiresAt: new Date(out.expires_at) };
357
+ return { _id: out._id, v: out.v, expiresAt: new Date(out.expires_at) };
335
358
  }
336
- find(id) {
337
- return this.query().where({ [this.idField]: id }).first();
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>, 1 | -1>>;
68
+ type SortSpec<F extends Fields> = Partial<Record<Extract<keyof F, string> | '_id', 1 | -1>>;
65
69
  interface WriteResult {
66
- id: string;
70
+ _id: string;
67
71
  v: number;
68
72
  }
69
- interface WriteOptions {
70
- ifVersion?: number;
71
- }
72
73
  type Duration = number | string;
73
- interface UpdateOptions extends WriteOptions {
74
- unset?: string[];
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
- id: string;
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(id: string, set: Input<F>, opts?: UpdateOptions): Promise<WriteResult>;
154
- delete(id: string, opts?: WriteOptions): Promise<void>;
155
- expire(id: string, duration: Duration, opts?: WriteOptions): Promise<ExpireResult>;
156
- expireAt(id: string, at: Date, opts?: WriteOptions): Promise<ExpireResult>;
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
- find(id: string): Promise<Row<F> | null>;
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
- type ErrorCode = 'unauthorized' | 'not_found' | 'version_conflict' | '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' | 'http_status' | (string & {});
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, VersionConflictError, WatchLaggedError, connect, makeError };
247
- export type { AggregateOptions, AggregateResult, ConnectOptions, Doc, Duration, ErrorCode, ExpireResult, FieldDef, FieldType, Fields, Input, InsertOptions, Ops, Row, Schema, SortSpec, TypeMap, UpdateOptions, WatchEvent, WatchOptions, Where, WriteOptions, WriteResult };
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>, 1 | -1>>;
68
+ type SortSpec<F extends Fields> = Partial<Record<Extract<keyof F, string> | '_id', 1 | -1>>;
65
69
  interface WriteResult {
66
- id: string;
70
+ _id: string;
67
71
  v: number;
68
72
  }
69
- interface WriteOptions {
70
- ifVersion?: number;
71
- }
72
73
  type Duration = number | string;
73
- interface UpdateOptions extends WriteOptions {
74
- unset?: string[];
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
- id: string;
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(id: string, set: Input<F>, opts?: UpdateOptions): Promise<WriteResult>;
154
- delete(id: string, opts?: WriteOptions): Promise<void>;
155
- expire(id: string, duration: Duration, opts?: WriteOptions): Promise<ExpireResult>;
156
- expireAt(id: string, at: Date, opts?: WriteOptions): Promise<ExpireResult>;
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
- find(id: string): Promise<Row<F> | null>;
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
- type ErrorCode = 'unauthorized' | 'not_found' | 'version_conflict' | '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' | 'http_status' | (string & {});
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, VersionConflictError, WatchLaggedError, connect, makeError };
247
- export type { AggregateOptions, AggregateResult, ConnectOptions, Doc, Duration, ErrorCode, ExpireResult, FieldDef, FieldType, Fields, Input, InsertOptions, Ops, Row, Schema, SortSpec, TypeMap, UpdateOptions, WatchEvent, WatchOptions, Where, WriteOptions, WriteResult };
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, id: line.id, v: line.v, doc: line.doc };
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
- async update(id, set, opts = {}) {
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 update = { id, set, unset: opts.unset, if_version: opts.ifVersion, expire_in: opts.expireIn };
256
- return this.db.transport.request("POST", this.path("update"), { update });
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
- async delete(id, opts = {}) {
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
- await this.db.transport.request("POST", this.path("delete"), { delete: { id, if_version: opts.ifVersion } });
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, opts = {}) {
266
- return this.setExpiry({ id, in: duration, if_version: opts.ifVersion });
287
+ expire(id, duration) {
288
+ return this.setExpiry({ _id: id, in: duration });
267
289
  }
268
- expireAt(id, at, opts = {}) {
269
- return this.setExpiry({ id, at: at.toISOString(), if_version: opts.ifVersion });
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", `${this.path("ttl")}/${encodeURIComponent(id)}`);
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 { id: out.id, v: out.v, expiresAt: new Date(out.expires_at) };
315
+ return { _id: out._id, v: out.v, expiresAt: new Date(out.expires_at) };
294
316
  }
295
- find(id) {
296
- return this.query().where({ [this.idField]: id }).first();
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thadeu/ndb",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript client for the ndb daemon",
5
5
  "keywords": [
6
6
  "ndb",