@thadeu/ndb 0.1.1 → 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 CHANGED
@@ -86,30 +86,28 @@ const results = await calls.insert([
86
86
  // Find by id. Returns null when missing.
87
87
  const row = await calls.find('c1')
88
88
 
89
- // Update with optimistic locking.
90
- await calls.update('c1', { status: 'done' }, { ifVersion: row!._v })
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)
91
93
 
92
- // Remove a field.
93
- await calls.update('c1', {}, { unset: ['duration'] })
94
+ // A guarded decrement: exactly one of two racing calls wins.
95
+ await stock.update({ sku: 'a', qty: { $gte: 1 } }, { $inc: { qty: -1 } })
94
96
 
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`:
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
100
 
101
- ```ts
102
- import { VersionConflictError } from '@thadeu/ndb'
103
-
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
- }
101
+ // Delete by filter: { deleted }.
102
+ await calls.delete({ call_id: 'c2' })
103
+ await calls.delete({ status: 'failed' }, { many: true })
111
104
  ```
112
105
 
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
+
113
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`.
114
112
 
115
113
  ### Expiry (TTL like Redis)
@@ -122,7 +120,7 @@ const sessions = db.model('sessions', {
122
120
  })
123
121
 
124
122
  await sessions.insert({ sid: 's1' }, { expireIn: '1h' }) // or 3600; all docs of the call
125
- await sessions.update('s1', { user: 'u' }, { expireIn: '30m' })
123
+ await sessions.update({ sid: '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))
@@ -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,6 +273,7 @@ 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
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,7 +33,6 @@ __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
38
  makeError: () => makeError
@@ -54,8 +54,6 @@ var UnauthorizedError = class extends NdbError {
54
54
  };
55
55
  var NotFoundError = class extends NdbError {
56
56
  };
57
- var VersionConflictError = class extends NdbError {
58
- };
59
57
  var DuplicateIdError = class extends NdbError {
60
58
  };
61
59
  var UniqueViolationError = class extends NdbError {
@@ -68,13 +66,14 @@ var WatchLaggedError = class extends NdbError {
68
66
  };
69
67
  var HistoryTruncatedError = class extends NdbError {
70
68
  };
69
+ var InvalidUpdateError = class extends NdbError {
70
+ };
71
71
  var TtlError = class extends NdbError {
72
72
  };
73
73
  var subclasses = {
74
74
  unauthorized: UnauthorizedError,
75
75
  not_found: NotFoundError,
76
76
  unknown_collection: NotFoundError,
77
- version_conflict: VersionConflictError,
78
77
  duplicate_id: DuplicateIdError,
79
78
  unique_violation: UniqueViolationError,
80
79
  invalid_query: InvalidQueryError,
@@ -85,7 +84,13 @@ var subclasses = {
85
84
  watch_lagged: WatchLaggedError,
86
85
  history_truncated: HistoryTruncatedError,
87
86
  ttl_not_enabled: TtlError,
88
- ttl_derived: 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
89
94
  };
90
95
  function makeError(code, message, status = 0) {
91
96
  const Cls = subclasses[code] ?? NdbError;
@@ -291,23 +296,35 @@ var Model = class {
291
296
  create(docs, opts = {}) {
292
297
  return this.insert(docs, opts);
293
298
  }
294
- async update(id, set, opts = {}) {
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 = {}) {
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 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) };
298
313
  }
299
- async delete(id, opts = {}) {
314
+ // Deletes the documents that the filter matches: one, or every match with
315
+ // many (same rule as update).
316
+ async delete(filter, opts = {}) {
300
317
  await this.ready();
301
- await this.db.transport.request("POST", this.path("delete"), { delete: { id, if_version: opts.ifVersion } });
318
+ return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
302
319
  }
303
320
  // Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
304
321
  // NotFoundError for a missing or expired document, and TtlError when the
305
322
  // schema has no expires field.
306
- expire(id, duration, opts = {}) {
307
- return this.setExpiry({ id, in: duration, if_version: opts.ifVersion });
323
+ expire(id, duration) {
324
+ return this.setExpiry({ id, in: duration });
308
325
  }
309
- expireAt(id, at, opts = {}) {
310
- return this.setExpiry({ id, at: at.toISOString(), if_version: opts.ifVersion });
326
+ expireAt(id, at) {
327
+ return this.setExpiry({ id, at: at.toISOString() });
311
328
  }
312
329
  // Removes the expiry of a document (PERSIST in Redis).
313
330
  async persist(id) {
@@ -419,6 +436,24 @@ var Model = class {
419
436
  };
420
437
  }
421
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
+ }
422
457
 
423
458
  // src/transport.ts
424
459
  var import_undici = require("undici");
@@ -624,6 +659,7 @@ function connect(addr, opts) {
624
659
  DuplicateIdError,
625
660
  HistoryTruncatedError,
626
661
  InvalidQueryError,
662
+ InvalidUpdateError,
627
663
  Model,
628
664
  Ndb,
629
665
  NdbError,
@@ -632,7 +668,6 @@ function connect(addr, opts) {
632
668
  TtlError,
633
669
  UnauthorizedError,
634
670
  UniqueViolationError,
635
- VersionConflictError,
636
671
  WatchLaggedError,
637
672
  connect,
638
673
  makeError
package/dist/index.d.cts CHANGED
@@ -66,14 +66,38 @@ interface WriteResult {
66
66
  id: string;
67
67
  v: number;
68
68
  }
69
- interface WriteOptions {
70
- ifVersion?: number;
71
- }
72
69
  type Duration = number | string;
73
- interface UpdateOptions extends WriteOptions {
74
- unset?: 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;
75
88
  expireIn?: Duration;
76
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;
100
+ }
77
101
  interface InsertOptions {
78
102
  expireIn?: Duration;
79
103
  }
@@ -150,10 +174,10 @@ declare class Model<F extends Fields> {
150
174
  ready(): Promise<void>;
151
175
  insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
152
176
  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>;
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>;
157
181
  persist(id: string): Promise<WriteResult>;
158
182
  ttl(id: string): Promise<number | null>;
159
183
  private setExpiry;
@@ -215,7 +239,7 @@ declare class Ndb {
215
239
  }
216
240
  declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
217
241
 
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 & {});
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 & {});
219
243
  declare class NdbError extends Error {
220
244
  readonly code: ErrorCode;
221
245
  readonly status: number;
@@ -225,8 +249,6 @@ declare class UnauthorizedError extends NdbError {
225
249
  }
226
250
  declare class NotFoundError extends NdbError {
227
251
  }
228
- declare class VersionConflictError extends NdbError {
229
- }
230
252
  declare class DuplicateIdError extends NdbError {
231
253
  }
232
254
  declare class UniqueViolationError extends NdbError {
@@ -239,9 +261,11 @@ declare class WatchLaggedError extends NdbError {
239
261
  }
240
262
  declare class HistoryTruncatedError extends NdbError {
241
263
  }
264
+ declare class InvalidUpdateError extends NdbError {
265
+ }
242
266
  declare class TtlError extends NdbError {
243
267
  }
244
268
  declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
245
269
 
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 };
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
@@ -66,14 +66,38 @@ interface WriteResult {
66
66
  id: string;
67
67
  v: number;
68
68
  }
69
- interface WriteOptions {
70
- ifVersion?: number;
71
- }
72
69
  type Duration = number | string;
73
- interface UpdateOptions extends WriteOptions {
74
- unset?: 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;
75
88
  expireIn?: Duration;
76
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;
100
+ }
77
101
  interface InsertOptions {
78
102
  expireIn?: Duration;
79
103
  }
@@ -150,10 +174,10 @@ declare class Model<F extends Fields> {
150
174
  ready(): Promise<void>;
151
175
  insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
152
176
  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>;
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>;
157
181
  persist(id: string): Promise<WriteResult>;
158
182
  ttl(id: string): Promise<number | null>;
159
183
  private setExpiry;
@@ -215,7 +239,7 @@ declare class Ndb {
215
239
  }
216
240
  declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
217
241
 
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 & {});
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 & {});
219
243
  declare class NdbError extends Error {
220
244
  readonly code: ErrorCode;
221
245
  readonly status: number;
@@ -225,8 +249,6 @@ declare class UnauthorizedError extends NdbError {
225
249
  }
226
250
  declare class NotFoundError extends NdbError {
227
251
  }
228
- declare class VersionConflictError extends NdbError {
229
- }
230
252
  declare class DuplicateIdError extends NdbError {
231
253
  }
232
254
  declare class UniqueViolationError extends NdbError {
@@ -239,9 +261,11 @@ declare class WatchLaggedError extends NdbError {
239
261
  }
240
262
  declare class HistoryTruncatedError extends NdbError {
241
263
  }
264
+ declare class InvalidUpdateError extends NdbError {
265
+ }
242
266
  declare class TtlError extends NdbError {
243
267
  }
244
268
  declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
245
269
 
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 };
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,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;
@@ -250,23 +255,35 @@ var Model = class {
250
255
  create(docs, opts = {}) {
251
256
  return this.insert(docs, opts);
252
257
  }
253
- async update(id, set, opts = {}) {
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 = {}) {
254
263
  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 });
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) };
257
272
  }
258
- async delete(id, opts = {}) {
273
+ // Deletes the documents that the filter matches: one, or every match with
274
+ // many (same rule as update).
275
+ async delete(filter, opts = {}) {
259
276
  await this.ready();
260
- await this.db.transport.request("POST", this.path("delete"), { delete: { id, if_version: opts.ifVersion } });
277
+ return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
261
278
  }
262
279
  // Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
263
280
  // NotFoundError for a missing or expired document, and TtlError when the
264
281
  // schema has no expires field.
265
- expire(id, duration, opts = {}) {
266
- return this.setExpiry({ id, in: duration, if_version: opts.ifVersion });
282
+ expire(id, duration) {
283
+ return this.setExpiry({ id, in: duration });
267
284
  }
268
- expireAt(id, at, opts = {}) {
269
- return this.setExpiry({ id, at: at.toISOString(), if_version: opts.ifVersion });
285
+ expireAt(id, at) {
286
+ return this.setExpiry({ id, at: at.toISOString() });
270
287
  }
271
288
  // Removes the expiry of a document (PERSIST in Redis).
272
289
  async persist(id) {
@@ -378,6 +395,24 @@ var Model = class {
378
395
  };
379
396
  }
380
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
+ }
381
416
 
382
417
  // src/transport.ts
383
418
  import { Pool } from "undici";
@@ -582,6 +617,7 @@ export {
582
617
  DuplicateIdError,
583
618
  HistoryTruncatedError,
584
619
  InvalidQueryError,
620
+ InvalidUpdateError,
585
621
  Model,
586
622
  Ndb,
587
623
  NdbError,
@@ -590,7 +626,6 @@ export {
590
626
  TtlError,
591
627
  UnauthorizedError,
592
628
  UniqueViolationError,
593
- VersionConflictError,
594
629
  WatchLaggedError,
595
630
  connect,
596
631
  makeError
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thadeu/ndb",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "TypeScript client for the ndb daemon",
5
5
  "keywords": [
6
6
  "ndb",