@thadeu/ndb 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
- - The client has no `expires` or `ttl` option and no blob API. Use the HTTP API for those.
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
 
@@ -111,6 +112,28 @@ try {
111
112
 
112
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`.
113
114
 
115
+ ### Expiry (TTL like Redis)
116
+
117
+ ```ts
118
+ const sessions = db.model('sessions', {
119
+ id: 'sid',
120
+ expires: 'exp',
121
+ fields: { sid: { type: 'string' }, exp: { type: 'date', optional: true } },
122
+ })
123
+
124
+ await sessions.insert({ sid: 's1' }, { expireIn: '1h' }) // or 3600; all docs of the call
125
+ await sessions.update('s1', { user: 'u' }, { expireIn: '30m' })
126
+
127
+ const { v, expiresAt } = await sessions.expire('s1', '30m') // EXPIRE
128
+ await sessions.expireAt('s1', new Date(Date.now() + 3_600_000))
129
+ await sessions.ttl('s1') // seconds left, -1 = never expires, null = missing or expired
130
+ await sessions.persist('s1') // PERSIST
131
+ ```
132
+
133
+ - 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.
134
+ - `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`.
135
+ - 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).
136
+
114
137
  ## Query
115
138
 
116
139
  A query is lazy and immutable. It is also thenable: `await` runs it.
@@ -252,6 +275,7 @@ Daemon errors are `NdbError` or a subclass. They have `code` (the daemon code) a
252
275
  | `CursorError` | `cursor_invalid`, `cursor_mismatch`, `cursor_expired` |
253
276
  | `WatchLaggedError` | `watch_lagged` |
254
277
  | `HistoryTruncatedError` | `history_truncated` |
278
+ | `TtlError` | `ttl_not_enabled`, `ttl_derived` |
255
279
  | `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
280
 
257
281
  ```ts
package/dist/index.cjs CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  NdbError: () => NdbError,
30
30
  NotFoundError: () => NotFoundError,
31
31
  Query: () => Query,
32
+ TtlError: () => TtlError,
32
33
  UnauthorizedError: () => UnauthorizedError,
33
34
  UniqueViolationError: () => UniqueViolationError,
34
35
  VersionConflictError: () => VersionConflictError,
@@ -67,6 +68,8 @@ var WatchLaggedError = class extends NdbError {
67
68
  };
68
69
  var HistoryTruncatedError = class extends NdbError {
69
70
  };
71
+ var TtlError = class extends NdbError {
72
+ };
70
73
  var subclasses = {
71
74
  unauthorized: UnauthorizedError,
72
75
  not_found: NotFoundError,
@@ -80,7 +83,9 @@ var subclasses = {
80
83
  cursor_mismatch: CursorError,
81
84
  cursor_expired: CursorError,
82
85
  watch_lagged: WatchLaggedError,
83
- history_truncated: HistoryTruncatedError
86
+ history_truncated: HistoryTruncatedError,
87
+ ttl_not_enabled: TtlError,
88
+ ttl_derived: TtlError
84
89
  };
85
90
  function makeError(code, message, status = 0) {
86
91
  const Cls = subclasses[code] ?? NdbError;
@@ -274,26 +279,60 @@ var Model = class {
274
279
  });
275
280
  return this.ensured;
276
281
  }
277
- async insert(docs) {
282
+ async insert(docs, opts = {}) {
278
283
  await this.ready();
279
284
  const list = Array.isArray(docs) ? docs : [docs];
280
285
  const out = await this.db.transport.request("POST", this.path("insert"), {
281
- insert: list
286
+ insert: list,
287
+ expire_in: opts.expireIn
282
288
  });
283
289
  return out.results;
284
290
  }
285
- create(docs) {
286
- return this.insert(docs);
291
+ create(docs, opts = {}) {
292
+ return this.insert(docs, opts);
287
293
  }
288
294
  async update(id, set, opts = {}) {
289
295
  await this.ready();
290
- const update = { id, set, unset: opts.unset, if_version: opts.ifVersion };
296
+ const update = { id, set, unset: opts.unset, if_version: opts.ifVersion, expire_in: opts.expireIn };
291
297
  return this.db.transport.request("POST", this.path("update"), { update });
292
298
  }
293
299
  async delete(id, opts = {}) {
294
300
  await this.ready();
295
301
  await this.db.transport.request("POST", this.path("delete"), { delete: { id, if_version: opts.ifVersion } });
296
302
  }
303
+ // Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
304
+ // NotFoundError for a missing or expired document, and TtlError when the
305
+ // schema has no expires field.
306
+ expire(id, duration, opts = {}) {
307
+ return this.setExpiry({ id, in: duration, if_version: opts.ifVersion });
308
+ }
309
+ expireAt(id, at, opts = {}) {
310
+ return this.setExpiry({ id, at: at.toISOString(), if_version: opts.ifVersion });
311
+ }
312
+ // Removes the expiry of a document (PERSIST in Redis).
313
+ async persist(id) {
314
+ await this.ready();
315
+ return this.db.transport.request("POST", this.path("persist"), { id });
316
+ }
317
+ // The seconds left (rounded up), -1 when the document never expires, null
318
+ // when it is missing or expired.
319
+ async ttl(id) {
320
+ await this.ready();
321
+ try {
322
+ const out = await this.db.transport.request("GET", `${this.path("ttl")}/${encodeURIComponent(id)}`);
323
+ return out.ttl;
324
+ } catch (err) {
325
+ if (err instanceof NotFoundError) {
326
+ return null;
327
+ }
328
+ throw err;
329
+ }
330
+ }
331
+ async setExpiry(body) {
332
+ await this.ready();
333
+ 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) };
335
+ }
297
336
  find(id) {
298
337
  return this.query().where({ [this.idField]: id }).first();
299
338
  }
@@ -374,6 +413,7 @@ var Model = class {
374
413
  return {
375
414
  id: this.schema.id,
376
415
  time: this.schema.time,
416
+ expires_field: this.schema.expires,
377
417
  indexes: entries.filter(([, d]) => d.index).map(([n]) => n),
378
418
  unique: entries.filter(([, d]) => d.unique).map(([n]) => n)
379
419
  };
@@ -589,6 +629,7 @@ function connect(addr, opts) {
589
629
  NdbError,
590
630
  NotFoundError,
591
631
  Query,
632
+ TtlError,
592
633
  UnauthorizedError,
593
634
  UniqueViolationError,
594
635
  VersionConflictError,
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']];
@@ -68,8 +69,16 @@ interface WriteResult {
68
69
  interface WriteOptions {
69
70
  ifVersion?: number;
70
71
  }
72
+ type Duration = number | string;
71
73
  interface UpdateOptions extends WriteOptions {
72
74
  unset?: string[];
75
+ expireIn?: Duration;
76
+ }
77
+ interface InsertOptions {
78
+ expireIn?: Duration;
79
+ }
80
+ interface ExpireResult extends WriteResult {
81
+ expiresAt: Date;
73
82
  }
74
83
  interface AggregateOptions {
75
84
  limit?: number;
@@ -139,10 +148,15 @@ declare class Model<F extends Fields> {
139
148
  get idField(): string;
140
149
  path(op?: string): string;
141
150
  ready(): Promise<void>;
142
- insert(docs: Input<F> | Input<F>[]): Promise<WriteResult[]>;
143
- create(docs: Input<F> | Input<F>[]): Promise<WriteResult[]>;
151
+ insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
152
+ create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
144
153
  update(id: string, set: Input<F>, opts?: UpdateOptions): Promise<WriteResult>;
145
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>;
157
+ persist(id: string): Promise<WriteResult>;
158
+ ttl(id: string): Promise<number | null>;
159
+ private setExpiry;
146
160
  find(id: string): Promise<Row<F> | null>;
147
161
  query(): Query<F>;
148
162
  where(w: Where<F>): Query<F>;
@@ -201,7 +215,7 @@ declare class Ndb {
201
215
  }
202
216
  declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
203
217
 
204
- 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' | 'http_status' | (string & {});
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 & {});
205
219
  declare class NdbError extends Error {
206
220
  readonly code: ErrorCode;
207
221
  readonly status: number;
@@ -225,7 +239,9 @@ declare class WatchLaggedError extends NdbError {
225
239
  }
226
240
  declare class HistoryTruncatedError extends NdbError {
227
241
  }
242
+ declare class TtlError extends NdbError {
243
+ }
228
244
  declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
229
245
 
230
- export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, Model, Ndb, NdbError, NotFoundError, Query, UnauthorizedError, UniqueViolationError, VersionConflictError, WatchLaggedError, connect, makeError };
231
- export type { AggregateOptions, AggregateResult, ConnectOptions, Doc, ErrorCode, FieldDef, FieldType, Fields, Input, Ops, Row, Schema, SortSpec, TypeMap, UpdateOptions, WatchEvent, WatchOptions, Where, WriteOptions, WriteResult };
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 };
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']];
@@ -68,8 +69,16 @@ interface WriteResult {
68
69
  interface WriteOptions {
69
70
  ifVersion?: number;
70
71
  }
72
+ type Duration = number | string;
71
73
  interface UpdateOptions extends WriteOptions {
72
74
  unset?: string[];
75
+ expireIn?: Duration;
76
+ }
77
+ interface InsertOptions {
78
+ expireIn?: Duration;
79
+ }
80
+ interface ExpireResult extends WriteResult {
81
+ expiresAt: Date;
73
82
  }
74
83
  interface AggregateOptions {
75
84
  limit?: number;
@@ -139,10 +148,15 @@ declare class Model<F extends Fields> {
139
148
  get idField(): string;
140
149
  path(op?: string): string;
141
150
  ready(): Promise<void>;
142
- insert(docs: Input<F> | Input<F>[]): Promise<WriteResult[]>;
143
- create(docs: Input<F> | Input<F>[]): Promise<WriteResult[]>;
151
+ insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
152
+ create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
144
153
  update(id: string, set: Input<F>, opts?: UpdateOptions): Promise<WriteResult>;
145
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>;
157
+ persist(id: string): Promise<WriteResult>;
158
+ ttl(id: string): Promise<number | null>;
159
+ private setExpiry;
146
160
  find(id: string): Promise<Row<F> | null>;
147
161
  query(): Query<F>;
148
162
  where(w: Where<F>): Query<F>;
@@ -201,7 +215,7 @@ declare class Ndb {
201
215
  }
202
216
  declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
203
217
 
204
- 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' | 'http_status' | (string & {});
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 & {});
205
219
  declare class NdbError extends Error {
206
220
  readonly code: ErrorCode;
207
221
  readonly status: number;
@@ -225,7 +239,9 @@ declare class WatchLaggedError extends NdbError {
225
239
  }
226
240
  declare class HistoryTruncatedError extends NdbError {
227
241
  }
242
+ declare class TtlError extends NdbError {
243
+ }
228
244
  declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
229
245
 
230
- export { CursorError, DuplicateIdError, HistoryTruncatedError, InvalidQueryError, Model, Ndb, NdbError, NotFoundError, Query, UnauthorizedError, UniqueViolationError, VersionConflictError, WatchLaggedError, connect, makeError };
231
- export type { AggregateOptions, AggregateResult, ConnectOptions, Doc, ErrorCode, FieldDef, FieldType, Fields, Input, Ops, Row, Schema, SortSpec, TypeMap, UpdateOptions, WatchEvent, WatchOptions, Where, WriteOptions, WriteResult };
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 };
package/dist/index.js CHANGED
@@ -27,6 +27,8 @@ var WatchLaggedError = class extends NdbError {
27
27
  };
28
28
  var HistoryTruncatedError = class extends NdbError {
29
29
  };
30
+ var TtlError = class extends NdbError {
31
+ };
30
32
  var subclasses = {
31
33
  unauthorized: UnauthorizedError,
32
34
  not_found: NotFoundError,
@@ -40,7 +42,9 @@ var subclasses = {
40
42
  cursor_mismatch: CursorError,
41
43
  cursor_expired: CursorError,
42
44
  watch_lagged: WatchLaggedError,
43
- history_truncated: HistoryTruncatedError
45
+ history_truncated: HistoryTruncatedError,
46
+ ttl_not_enabled: TtlError,
47
+ ttl_derived: TtlError
44
48
  };
45
49
  function makeError(code, message, status = 0) {
46
50
  const Cls = subclasses[code] ?? NdbError;
@@ -234,26 +238,60 @@ var Model = class {
234
238
  });
235
239
  return this.ensured;
236
240
  }
237
- async insert(docs) {
241
+ async insert(docs, opts = {}) {
238
242
  await this.ready();
239
243
  const list = Array.isArray(docs) ? docs : [docs];
240
244
  const out = await this.db.transport.request("POST", this.path("insert"), {
241
- insert: list
245
+ insert: list,
246
+ expire_in: opts.expireIn
242
247
  });
243
248
  return out.results;
244
249
  }
245
- create(docs) {
246
- return this.insert(docs);
250
+ create(docs, opts = {}) {
251
+ return this.insert(docs, opts);
247
252
  }
248
253
  async update(id, set, opts = {}) {
249
254
  await this.ready();
250
- const update = { id, set, unset: opts.unset, if_version: opts.ifVersion };
255
+ const update = { id, set, unset: opts.unset, if_version: opts.ifVersion, expire_in: opts.expireIn };
251
256
  return this.db.transport.request("POST", this.path("update"), { update });
252
257
  }
253
258
  async delete(id, opts = {}) {
254
259
  await this.ready();
255
260
  await this.db.transport.request("POST", this.path("delete"), { delete: { id, if_version: opts.ifVersion } });
256
261
  }
262
+ // Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
263
+ // NotFoundError for a missing or expired document, and TtlError when the
264
+ // schema has no expires field.
265
+ expire(id, duration, opts = {}) {
266
+ return this.setExpiry({ id, in: duration, if_version: opts.ifVersion });
267
+ }
268
+ expireAt(id, at, opts = {}) {
269
+ return this.setExpiry({ id, at: at.toISOString(), if_version: opts.ifVersion });
270
+ }
271
+ // Removes the expiry of a document (PERSIST in Redis).
272
+ async persist(id) {
273
+ await this.ready();
274
+ return this.db.transport.request("POST", this.path("persist"), { id });
275
+ }
276
+ // The seconds left (rounded up), -1 when the document never expires, null
277
+ // when it is missing or expired.
278
+ async ttl(id) {
279
+ await this.ready();
280
+ try {
281
+ const out = await this.db.transport.request("GET", `${this.path("ttl")}/${encodeURIComponent(id)}`);
282
+ return out.ttl;
283
+ } catch (err) {
284
+ if (err instanceof NotFoundError) {
285
+ return null;
286
+ }
287
+ throw err;
288
+ }
289
+ }
290
+ async setExpiry(body) {
291
+ await this.ready();
292
+ 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) };
294
+ }
257
295
  find(id) {
258
296
  return this.query().where({ [this.idField]: id }).first();
259
297
  }
@@ -334,6 +372,7 @@ var Model = class {
334
372
  return {
335
373
  id: this.schema.id,
336
374
  time: this.schema.time,
375
+ expires_field: this.schema.expires,
337
376
  indexes: entries.filter(([, d]) => d.index).map(([n]) => n),
338
377
  unique: entries.filter(([, d]) => d.unique).map(([n]) => n)
339
378
  };
@@ -548,6 +587,7 @@ export {
548
587
  NdbError,
549
588
  NotFoundError,
550
589
  Query,
590
+ TtlError,
551
591
  UnauthorizedError,
552
592
  UniqueViolationError,
553
593
  VersionConflictError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thadeu/ndb",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "TypeScript client for the ndb daemon",
5
5
  "keywords": [
6
6
  "ndb",