@thadeu/ndb 0.3.1 → 0.4.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
@@ -111,6 +111,16 @@ await calls.delete({ status: 'failed' }, { many: true })
111
111
 
112
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`.
113
113
 
114
+ ### Truncate and drop
115
+
116
+ ```ts
117
+ await calls.truncate() // every document goes; the definition and indexes stay
118
+ await calls.drop() // the collection, its definition and its watch consumers go
119
+ ```
120
+
121
+ - Both take the same time for 10 or 10 million documents. Watch consumers get one event with `op: 'truncate'` (`_id` is `''`), then the new changes.
122
+ - After `drop()`, the next call on the model declares the collection again. A collection declared in `ndb.yml` comes back, empty, when the daemon restarts.
123
+
114
124
  ### Expiry (TTL like Redis)
115
125
 
116
126
  ```ts
@@ -236,7 +246,7 @@ const controller = new AbortController()
236
246
 
237
247
  try {
238
248
  for await (const ev of calls.watch('relay', { where: { status: 'answered' }, signal: controller.signal })) {
239
- // ev: { consumer, pos, op: 'put' | 'del', _id, v, doc }
249
+ // ev: { consumer, pos, op: 'put' | 'del' | 'truncate', _id, v, doc }
240
250
  await handle(ev)
241
251
 
242
252
  await calls.ack(ev)
package/dist/index.cjs CHANGED
@@ -160,6 +160,12 @@ var Query = class _Query {
160
160
  where(w) {
161
161
  return this.with({ clauses: [...this.state.clauses, toWhere(w)] });
162
162
  }
163
+ // A where object built elsewhere, such as from a search form. It is not
164
+ // type-checked and is sent as is; the daemon validates it. It is ANDed
165
+ // with the other clauses.
166
+ whereRaw(w) {
167
+ return this.with({ clauses: [...this.state.clauses, w] });
168
+ }
163
169
  not(w) {
164
170
  return this.with({ clauses: [...this.state.clauses, { $not: toWhere(w) }] });
165
171
  }
@@ -256,7 +262,7 @@ async function* watchEvents(lines2, consumer) {
256
262
  if (line.error) {
257
263
  throw makeError(line.error.code, line.error.message);
258
264
  }
259
- yield { consumer, pos: line.pos, op: line.op, _id: line._id, v: line.v, doc: line.doc };
265
+ yield { consumer, pos: line.pos, op: line.op, _id: line._id ?? "", v: line.v, doc: line.doc };
260
266
  }
261
267
  }
262
268
 
@@ -275,7 +281,13 @@ var Model = class {
275
281
  const base = `/v1/c/${encodeURIComponent(this.name)}`;
276
282
  return op ? `${base}/${op}` : base;
277
283
  }
284
+ // A collection pattern ("logs-2026-*") reads many collections and declares
285
+ // none: its writes fail with pattern_read_only.
286
+ get isPattern() {
287
+ return this.name.includes("*");
288
+ }
278
289
  ready() {
290
+ if (this.isPattern) return Promise.resolve();
279
291
  this.ensured ??= this.db.transport.request("PUT", this.path(), this.catalog()).catch((err) => {
280
292
  this.ensured = void 0;
281
293
  throw err;
@@ -323,6 +335,22 @@ var Model = class {
323
335
  const filter = idOrFilter;
324
336
  return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
325
337
  }
338
+ // Removes every document of the collection and keeps its definition and
339
+ // indexes. The cost does not depend on the number of documents. Watch
340
+ // consumers get one event with op "truncate".
341
+ async truncate() {
342
+ await this.ready();
343
+ await this.db.transport.request("POST", this.path("truncate") + this.confirm());
344
+ }
345
+ // Removes the collection: its documents, its definition and the positions
346
+ // of its watch consumers. The next call on the model declares it again.
347
+ async drop() {
348
+ await this.db.transport.request("DELETE", this.path() + this.confirm());
349
+ this.ensured = void 0;
350
+ }
351
+ confirm() {
352
+ return `?confirm=${encodeURIComponent(this.name)}`;
353
+ }
326
354
  // Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
327
355
  // NotFoundError for a missing or expired document, and TtlError when the
328
356
  // schema has no expires field.
@@ -375,6 +403,9 @@ var Model = class {
375
403
  where(w) {
376
404
  return this.query().where(w);
377
405
  }
406
+ whereRaw(w) {
407
+ return this.query().whereRaw(w);
408
+ }
378
409
  not(w) {
379
410
  return this.query().not(w);
380
411
  }
@@ -411,6 +442,18 @@ var Model = class {
411
442
  iterate() {
412
443
  return this.query().iterate();
413
444
  }
445
+ // Runs a find with a whole body built elsewhere, such as from a search
446
+ // form: { where, sort, limit, after, count }. The body is sent as is (the
447
+ // daemon validates it); rows are decoded like the rows of where().
448
+ async find(body) {
449
+ await this.ready();
450
+ const res = await this.db.transport.stream("POST", this.path("find"), { ...body, versions: true });
451
+ const rows = [];
452
+ for await (const raw of res.rows()) {
453
+ rows.push(this.decode(raw));
454
+ }
455
+ return { rows, next: res.header("x-ndb-next") ?? "", total: Number(res.header("x-ndb-total") ?? 0) };
456
+ }
414
457
  async aggregate(pipeline, opts = {}) {
415
458
  await this.ready();
416
459
  const res = await this.db.transport.stream("POST", this.path("aggregate"), { pipeline, ...opts });
package/dist/index.d.cts CHANGED
@@ -39,6 +39,7 @@ type Input<F extends Fields> = {
39
39
  type Row<F extends Fields> = Doc<F> & {
40
40
  _id: string;
41
41
  _v: number;
42
+ _coll?: string;
42
43
  };
43
44
  interface Ops<V> {
44
45
  $eq?: V;
@@ -52,6 +53,7 @@ interface Ops<V> {
52
53
  $exists?: boolean;
53
54
  $regex?: string | RegExp;
54
55
  $options?: string;
56
+ $elemMatch?: Record<string, unknown>;
55
57
  }
56
58
  type Operand<V> = V extends Date ? Date | string : V;
57
59
  type RegexOperand<V> = V extends string ? RegExp : never;
@@ -115,6 +117,20 @@ interface AggregateOptions {
115
117
  after?: string;
116
118
  count?: boolean;
117
119
  }
120
+ interface FindBody {
121
+ where?: Record<string, unknown>;
122
+ fields?: string[];
123
+ sort?: Record<string, 1 | -1>;
124
+ limit?: number;
125
+ skip?: number;
126
+ after?: string;
127
+ count?: boolean;
128
+ }
129
+ interface FindResult<R> {
130
+ rows: R[];
131
+ next: string;
132
+ total: number;
133
+ }
118
134
  interface AggregateResult<R = Record<string, unknown>> {
119
135
  rows: R[];
120
136
  next: string;
@@ -129,7 +145,7 @@ interface WatchOptions<F extends Fields = Fields> {
129
145
  interface WatchEvent<R> {
130
146
  consumer: string;
131
147
  pos: string;
132
- op: 'put' | 'del';
148
+ op: 'put' | 'del' | 'truncate';
133
149
  _id: string;
134
150
  v: number;
135
151
  doc: R | undefined;
@@ -149,6 +165,7 @@ declare class Query<F extends Fields, R = Row<F>> implements PromiseLike<R[]> {
149
165
  nextCursor: string;
150
166
  constructor(model: Model<F>, state?: State);
151
167
  where(w: Where<F>): Query<F, R>;
168
+ whereRaw(w: Record<string, unknown>): Query<F, R>;
152
169
  not(w: Where<F>): Query<F, R>;
153
170
  or(...ws: Where<F>[]): Query<F, R>;
154
171
  sort(spec: SortSpec<F>): Query<F, R>;
@@ -175,11 +192,15 @@ declare class Model<F extends Fields> {
175
192
  private ensured?;
176
193
  constructor(db: Ndb, name: string, schema: Schema<F>);
177
194
  path(op?: string): string;
195
+ get isPattern(): boolean;
178
196
  ready(): Promise<void>;
179
197
  insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
180
198
  create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
181
199
  update(idOrFilter: string | Filter<F>, ops: UpdateOps<F>, opts?: UpdateOptions): Promise<UpdateResult<F>>;
182
200
  delete(idOrFilter: string | Filter<F>, opts?: DeleteOptions): Promise<DeleteResult>;
201
+ truncate(): Promise<void>;
202
+ drop(): Promise<void>;
203
+ private confirm;
183
204
  expire(id: string, duration: Duration): Promise<ExpireResult>;
184
205
  expireAt(id: string, at: Date): Promise<ExpireResult>;
185
206
  persist(id: string): Promise<WriteResult>;
@@ -188,6 +209,7 @@ declare class Model<F extends Fields> {
188
209
  get(id: string): Promise<Row<F> | null>;
189
210
  query(): Query<F>;
190
211
  where(w: Where<F>): Query<F>;
212
+ whereRaw(w: Record<string, unknown>): Query<F>;
191
213
  not(w: Where<F>): Query<F>;
192
214
  or(...ws: Where<F>[]): Query<F>;
193
215
  sort(spec: SortSpec<F>): Query<F>;
@@ -200,6 +222,7 @@ declare class Model<F extends Fields> {
200
222
  count(): Promise<number>;
201
223
  first(): Promise<Row<F> | null>;
202
224
  iterate(): AsyncGenerator<Row<F>>;
225
+ find(body: FindBody): Promise<FindResult<Row<F>>>;
203
226
  aggregate<R = Record<string, unknown>>(pipeline: Record<string, unknown>[], opts?: AggregateOptions): Promise<AggregateResult<R>>;
204
227
  watch(consumer: string, opts?: WatchOptions<F>): AsyncGenerator<WatchEvent<Row<F>>>;
205
228
  ack(event: Pick<WatchEvent<unknown>, 'consumer' | 'pos'>): Promise<void>;
@@ -245,7 +268,7 @@ declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
245
268
 
246
269
  declare function newId(): string;
247
270
 
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 & {});
271
+ 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' | 'confirm_required' | 'invalid_update' | 'http_status' | (string & {});
249
272
  declare class NdbError extends Error {
250
273
  readonly code: ErrorCode;
251
274
  readonly status: number;
@@ -274,4 +297,4 @@ declare class TtlError extends NdbError {
274
297
  declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
275
298
 
276
299
  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 };
300
+ export type { AggregateOptions, AggregateResult, ConnectOptions, DeleteOptions, DeleteResult, Doc, Duration, ErrorCode, ExpireResult, FieldDef, FieldType, Fields, Filter, FindBody, FindResult, Input, InsertOptions, Ops, Row, Schema, SortSpec, TypeMap, UpdateOps, UpdateOptions, UpdateResult, WatchEvent, WatchOptions, Where, WriteResult };
package/dist/index.d.ts CHANGED
@@ -39,6 +39,7 @@ type Input<F extends Fields> = {
39
39
  type Row<F extends Fields> = Doc<F> & {
40
40
  _id: string;
41
41
  _v: number;
42
+ _coll?: string;
42
43
  };
43
44
  interface Ops<V> {
44
45
  $eq?: V;
@@ -52,6 +53,7 @@ interface Ops<V> {
52
53
  $exists?: boolean;
53
54
  $regex?: string | RegExp;
54
55
  $options?: string;
56
+ $elemMatch?: Record<string, unknown>;
55
57
  }
56
58
  type Operand<V> = V extends Date ? Date | string : V;
57
59
  type RegexOperand<V> = V extends string ? RegExp : never;
@@ -115,6 +117,20 @@ interface AggregateOptions {
115
117
  after?: string;
116
118
  count?: boolean;
117
119
  }
120
+ interface FindBody {
121
+ where?: Record<string, unknown>;
122
+ fields?: string[];
123
+ sort?: Record<string, 1 | -1>;
124
+ limit?: number;
125
+ skip?: number;
126
+ after?: string;
127
+ count?: boolean;
128
+ }
129
+ interface FindResult<R> {
130
+ rows: R[];
131
+ next: string;
132
+ total: number;
133
+ }
118
134
  interface AggregateResult<R = Record<string, unknown>> {
119
135
  rows: R[];
120
136
  next: string;
@@ -129,7 +145,7 @@ interface WatchOptions<F extends Fields = Fields> {
129
145
  interface WatchEvent<R> {
130
146
  consumer: string;
131
147
  pos: string;
132
- op: 'put' | 'del';
148
+ op: 'put' | 'del' | 'truncate';
133
149
  _id: string;
134
150
  v: number;
135
151
  doc: R | undefined;
@@ -149,6 +165,7 @@ declare class Query<F extends Fields, R = Row<F>> implements PromiseLike<R[]> {
149
165
  nextCursor: string;
150
166
  constructor(model: Model<F>, state?: State);
151
167
  where(w: Where<F>): Query<F, R>;
168
+ whereRaw(w: Record<string, unknown>): Query<F, R>;
152
169
  not(w: Where<F>): Query<F, R>;
153
170
  or(...ws: Where<F>[]): Query<F, R>;
154
171
  sort(spec: SortSpec<F>): Query<F, R>;
@@ -175,11 +192,15 @@ declare class Model<F extends Fields> {
175
192
  private ensured?;
176
193
  constructor(db: Ndb, name: string, schema: Schema<F>);
177
194
  path(op?: string): string;
195
+ get isPattern(): boolean;
178
196
  ready(): Promise<void>;
179
197
  insert(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
180
198
  create(docs: Input<F> | Input<F>[], opts?: InsertOptions): Promise<WriteResult[]>;
181
199
  update(idOrFilter: string | Filter<F>, ops: UpdateOps<F>, opts?: UpdateOptions): Promise<UpdateResult<F>>;
182
200
  delete(idOrFilter: string | Filter<F>, opts?: DeleteOptions): Promise<DeleteResult>;
201
+ truncate(): Promise<void>;
202
+ drop(): Promise<void>;
203
+ private confirm;
183
204
  expire(id: string, duration: Duration): Promise<ExpireResult>;
184
205
  expireAt(id: string, at: Date): Promise<ExpireResult>;
185
206
  persist(id: string): Promise<WriteResult>;
@@ -188,6 +209,7 @@ declare class Model<F extends Fields> {
188
209
  get(id: string): Promise<Row<F> | null>;
189
210
  query(): Query<F>;
190
211
  where(w: Where<F>): Query<F>;
212
+ whereRaw(w: Record<string, unknown>): Query<F>;
191
213
  not(w: Where<F>): Query<F>;
192
214
  or(...ws: Where<F>[]): Query<F>;
193
215
  sort(spec: SortSpec<F>): Query<F>;
@@ -200,6 +222,7 @@ declare class Model<F extends Fields> {
200
222
  count(): Promise<number>;
201
223
  first(): Promise<Row<F> | null>;
202
224
  iterate(): AsyncGenerator<Row<F>>;
225
+ find(body: FindBody): Promise<FindResult<Row<F>>>;
203
226
  aggregate<R = Record<string, unknown>>(pipeline: Record<string, unknown>[], opts?: AggregateOptions): Promise<AggregateResult<R>>;
204
227
  watch(consumer: string, opts?: WatchOptions<F>): AsyncGenerator<WatchEvent<Row<F>>>;
205
228
  ack(event: Pick<WatchEvent<unknown>, 'consumer' | 'pos'>): Promise<void>;
@@ -245,7 +268,7 @@ declare function connect(addr?: string, opts?: ConnectOptions): Ndb;
245
268
 
246
269
  declare function newId(): string;
247
270
 
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 & {});
271
+ 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' | 'confirm_required' | 'invalid_update' | 'http_status' | (string & {});
249
272
  declare class NdbError extends Error {
250
273
  readonly code: ErrorCode;
251
274
  readonly status: number;
@@ -274,4 +297,4 @@ declare class TtlError extends NdbError {
274
297
  declare function makeError(code: ErrorCode, message: string, status?: number): NdbError;
275
298
 
276
299
  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 };
300
+ export type { AggregateOptions, AggregateResult, ConnectOptions, DeleteOptions, DeleteResult, Doc, Duration, ErrorCode, ExpireResult, FieldDef, FieldType, Fields, Filter, FindBody, FindResult, Input, InsertOptions, Ops, Row, Schema, SortSpec, TypeMap, UpdateOps, UpdateOptions, UpdateResult, WatchEvent, WatchOptions, Where, WriteResult };
package/dist/index.js CHANGED
@@ -118,6 +118,12 @@ var Query = class _Query {
118
118
  where(w) {
119
119
  return this.with({ clauses: [...this.state.clauses, toWhere(w)] });
120
120
  }
121
+ // A where object built elsewhere, such as from a search form. It is not
122
+ // type-checked and is sent as is; the daemon validates it. It is ANDed
123
+ // with the other clauses.
124
+ whereRaw(w) {
125
+ return this.with({ clauses: [...this.state.clauses, w] });
126
+ }
121
127
  not(w) {
122
128
  return this.with({ clauses: [...this.state.clauses, { $not: toWhere(w) }] });
123
129
  }
@@ -214,7 +220,7 @@ async function* watchEvents(lines2, consumer) {
214
220
  if (line.error) {
215
221
  throw makeError(line.error.code, line.error.message);
216
222
  }
217
- yield { consumer, pos: line.pos, op: line.op, _id: line._id, v: line.v, doc: line.doc };
223
+ yield { consumer, pos: line.pos, op: line.op, _id: line._id ?? "", v: line.v, doc: line.doc };
218
224
  }
219
225
  }
220
226
 
@@ -233,7 +239,13 @@ var Model = class {
233
239
  const base = `/v1/c/${encodeURIComponent(this.name)}`;
234
240
  return op ? `${base}/${op}` : base;
235
241
  }
242
+ // A collection pattern ("logs-2026-*") reads many collections and declares
243
+ // none: its writes fail with pattern_read_only.
244
+ get isPattern() {
245
+ return this.name.includes("*");
246
+ }
236
247
  ready() {
248
+ if (this.isPattern) return Promise.resolve();
237
249
  this.ensured ??= this.db.transport.request("PUT", this.path(), this.catalog()).catch((err) => {
238
250
  this.ensured = void 0;
239
251
  throw err;
@@ -281,6 +293,22 @@ var Model = class {
281
293
  const filter = idOrFilter;
282
294
  return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
283
295
  }
296
+ // Removes every document of the collection and keeps its definition and
297
+ // indexes. The cost does not depend on the number of documents. Watch
298
+ // consumers get one event with op "truncate".
299
+ async truncate() {
300
+ await this.ready();
301
+ await this.db.transport.request("POST", this.path("truncate") + this.confirm());
302
+ }
303
+ // Removes the collection: its documents, its definition and the positions
304
+ // of its watch consumers. The next call on the model declares it again.
305
+ async drop() {
306
+ await this.db.transport.request("DELETE", this.path() + this.confirm());
307
+ this.ensured = void 0;
308
+ }
309
+ confirm() {
310
+ return `?confirm=${encodeURIComponent(this.name)}`;
311
+ }
284
312
  // Makes a document expire, like EXPIRE in Redis: seconds or "30m". It throws
285
313
  // NotFoundError for a missing or expired document, and TtlError when the
286
314
  // schema has no expires field.
@@ -333,6 +361,9 @@ var Model = class {
333
361
  where(w) {
334
362
  return this.query().where(w);
335
363
  }
364
+ whereRaw(w) {
365
+ return this.query().whereRaw(w);
366
+ }
336
367
  not(w) {
337
368
  return this.query().not(w);
338
369
  }
@@ -369,6 +400,18 @@ var Model = class {
369
400
  iterate() {
370
401
  return this.query().iterate();
371
402
  }
403
+ // Runs a find with a whole body built elsewhere, such as from a search
404
+ // form: { where, sort, limit, after, count }. The body is sent as is (the
405
+ // daemon validates it); rows are decoded like the rows of where().
406
+ async find(body) {
407
+ await this.ready();
408
+ const res = await this.db.transport.stream("POST", this.path("find"), { ...body, versions: true });
409
+ const rows = [];
410
+ for await (const raw of res.rows()) {
411
+ rows.push(this.decode(raw));
412
+ }
413
+ return { rows, next: res.header("x-ndb-next") ?? "", total: Number(res.header("x-ndb-total") ?? 0) };
414
+ }
372
415
  async aggregate(pipeline, opts = {}) {
373
416
  await this.ready();
374
417
  const res = await this.db.transport.stream("POST", this.path("aggregate"), { pipeline, ...opts });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thadeu/ndb",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript client for the ndb daemon",
5
5
  "keywords": [
6
6
  "ndb",