@thadeu/ndb 0.4.0 → 1.0.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/dist/index.cjs CHANGED
@@ -20,12 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ Collection: () => Collection,
23
24
  CursorError: () => CursorError,
24
25
  DuplicateIdError: () => DuplicateIdError,
25
26
  HistoryTruncatedError: () => HistoryTruncatedError,
26
27
  InvalidQueryError: () => InvalidQueryError,
27
28
  InvalidUpdateError: () => InvalidUpdateError,
28
- Model: () => Model,
29
29
  Ndb: () => Ndb,
30
30
  NdbError: () => NdbError,
31
31
  NotFoundError: () => NotFoundError,
@@ -150,20 +150,21 @@ function toWhere(v) {
150
150
 
151
151
  // src/query.ts
152
152
  var Query = class _Query {
153
- constructor(model, state = { clauses: [] }) {
154
- this.model = model;
153
+ constructor(coll, state = { clauses: [] }) {
154
+ this.coll = coll;
155
155
  this.state = state;
156
156
  }
157
- model;
157
+ coll;
158
158
  state;
159
159
  nextCursor = "";
160
- where(w) {
160
+ // Adds conditions, ANDed with the ones already there.
161
+ query(w) {
161
162
  return this.with({ clauses: [...this.state.clauses, toWhere(w)] });
162
163
  }
163
- // A where object built elsewhere, such as from a search form. It is not
164
+ // A query built elsewhere, such as from a search form. It is not
164
165
  // type-checked and is sent as is; the daemon validates it. It is ANDed
165
166
  // with the other clauses.
166
- whereRaw(w) {
167
+ rawQuery(w) {
167
168
  return this.with({ clauses: [...this.state.clauses, w] });
168
169
  }
169
170
  not(w) {
@@ -206,14 +207,15 @@ var Query = class _Query {
206
207
  }
207
208
  return null;
208
209
  }
210
+ // How many documents match (POST /{coll}/_count). limit, skip and sort do
211
+ // not change it.
209
212
  async count() {
210
- await this.model.ready();
211
- const body = { where: this.whereDoc(), count: true, limit: 1 };
212
- const res = await this.model.db.transport.stream("POST", this.model.path("find"), body);
213
- await res.body.dump();
214
- return Number(res.header("x-ndb-total") ?? 0);
213
+ await this.coll.ready();
214
+ const out = await this.coll.db.transport.request("POST", this.coll.path("_count"), { query: this.whereDoc() });
215
+ return out.count;
215
216
  }
216
- async *iterate() {
217
+ // Every row, following the cursor page by page (limit is the page size).
218
+ async *each() {
217
219
  let body = this.body();
218
220
  for (; ; ) {
219
221
  yield* this.rows(body);
@@ -224,16 +226,16 @@ var Query = class _Query {
224
226
  }
225
227
  }
226
228
  async *rows(body) {
227
- await this.model.ready();
228
- const res = await this.model.db.transport.stream("POST", this.model.path("find"), body);
229
+ await this.coll.ready();
230
+ const res = await this.coll.db.transport.stream("POST", this.coll.path("_search"), body);
229
231
  this.nextCursor = res.header("x-ndb-next") ?? "";
230
232
  for await (const raw of res.rows()) {
231
- yield this.model.decode(raw);
233
+ yield this.coll.decode(raw);
232
234
  }
233
235
  }
234
236
  body() {
235
237
  const { fields, sort, limit, skip, after } = this.state;
236
- return { where: this.whereDoc(), fields, sort, limit, skip, after, versions: true };
238
+ return { query: this.whereDoc(), fields, sort, limit, skip, after, versions: true };
237
239
  }
238
240
  whereDoc() {
239
241
  const merged = {};
@@ -248,7 +250,7 @@ var Query = class _Query {
248
250
  return merged;
249
251
  }
250
252
  with(patch) {
251
- return new _Query(this.model, { ...this.state, ...patch });
253
+ return new _Query(this.coll, { ...this.state, ...patch });
252
254
  }
253
255
  };
254
256
 
@@ -262,12 +264,12 @@ async function* watchEvents(lines2, consumer) {
262
264
  if (line.error) {
263
265
  throw makeError(line.error.code, line.error.message);
264
266
  }
265
- yield { consumer, pos: line.pos, op: line.op, _id: line._id ?? "", v: line.v, doc: line.doc };
267
+ yield { consumer, pos: line.pos, op: line.op, _id: line._id ?? "", _v: line._v, doc: line.doc };
266
268
  }
267
269
  }
268
270
 
269
- // src/model.ts
270
- var Model = class {
271
+ // src/collection.ts
272
+ var Collection = class {
271
273
  constructor(db, name, schema) {
272
274
  this.db = db;
273
275
  this.name = name;
@@ -277,12 +279,11 @@ var Model = class {
277
279
  name;
278
280
  schema;
279
281
  ensured;
280
- path(op) {
281
- const base = `/v1/c/${encodeURIComponent(this.name)}`;
282
- return op ? `${base}/${op}` : base;
282
+ // The path of the collection, or of one of its actions ("_search").
283
+ path(action) {
284
+ const base = `/${encodeURIComponent(this.name)}`;
285
+ return action ? `${base}/${action}` : base;
283
286
  }
284
- // A collection pattern ("logs-2026-*") reads many collections and declares
285
- // none: its writes fail with pattern_read_only.
286
287
  get isPattern() {
287
288
  return this.name.includes("*");
288
289
  }
@@ -295,58 +296,72 @@ var Model = class {
295
296
  return this.ensured;
296
297
  }
297
298
  async insert(docs, opts = {}) {
298
- await this.ready();
299
+ if (!Array.isArray(docs) && opts.expireIn === void 0) {
300
+ return this.send("POST", "_doc", docs);
301
+ }
299
302
  const list = Array.isArray(docs) ? docs : [docs];
300
- const out = await this.db.transport.request("POST", this.path("insert"), {
301
- insert: list,
302
- expire_in: opts.expireIn
303
- });
303
+ const out = await this.send("POST", "_bulk", { docs: list, expire_in: opts.expireIn });
304
+ return Array.isArray(docs) ? out.results : out.results[0];
305
+ }
306
+ // Writes a document by its _id: creates it when the _id is free, and
307
+ // merges its fields into the stored document when it exists (the other
308
+ // fields stay), like doc_as_upsert in Elasticsearch. No _id is an insert.
309
+ async upsert(doc, opts = {}) {
310
+ return (await this.upsertAll([doc], opts))[0];
311
+ }
312
+ // upsert for many documents in one transaction, all or nothing
313
+ // (POST /{coll}/_bulk with upsert).
314
+ async upsertAll(docs, opts = {}) {
315
+ const out = await this.send("POST", "_bulk", { docs, expire_in: opts.expireIn, upsert: true });
304
316
  return out.results;
305
317
  }
306
- create(docs, opts = {}) {
307
- return this.insert(docs, opts);
318
+ // Applies ops to the document with this _id, in one atomic step
319
+ // (POST /{coll}/_update/{id}). guard adds conditions on the same document,
320
+ // such as { _v: 7 }: matched is 0 when it fails. Matching nothing is not an
321
+ // error.
322
+ async update(id, ops, opts = {}) {
323
+ const out = await this.send("POST", docPath("_update", id), {
324
+ query: opts.guard === void 0 ? void 0 : toWhere(opts.guard),
325
+ update: checkOps(ops),
326
+ upsert: opts.upsert,
327
+ expire_in: opts.expireIn
328
+ });
329
+ return this.updated(out);
330
+ }
331
+ // The atomic update by query (POST /{coll}/_update_by_query): the daemon
332
+ // matches and writes in one step on the newest state, so concurrent updates
333
+ // never interleave. Without many it updates the first match; many updates
334
+ // every match and needs _id or an indexed field in the query.
335
+ async updateBy(query, ops, opts = {}) {
336
+ const out = await this.send("POST", "_update_by_query", {
337
+ query: toWhere(query),
338
+ update: checkOps(ops),
339
+ many: opts.many,
340
+ upsert: opts.upsert,
341
+ expire_in: opts.expireIn
342
+ });
343
+ return this.updated(out);
308
344
  }
309
- // Atomic update. A string is an _id (POST /{_id}/update); an object is a
310
- // filter (POST /update). The filter and the operators are sent as one body;
311
- // the daemon matches and writes in one step on the newest state, so
312
- // concurrent updates never interleave. A version guard is a filter:
313
- // { _id: 'c1', _v: 7 }. Matching nothing is not an error: matched is 0.
314
- // many does not apply to an _id.
315
- async update(idOrFilter, ops, opts = {}) {
316
- await this.ready();
317
- const byId = typeof idOrFilter === "string";
318
- const body = mergeUpdate(byId ? {} : idOrFilter, ops);
319
- const query = writeQuery({ many: byId ? void 0 : opts.many, upsert: opts.upsert, expire_in: opts.expireIn });
320
- const op = byId ? `${encodeURIComponent(idOrFilter)}/update` : "update";
321
- const out = await this.db.transport.request(
322
- "POST",
323
- this.path(op) + query,
324
- body
325
- );
326
- return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
345
+ // Deletes the document with this _id (DELETE /{coll}/_doc/{id}).
346
+ delete(id) {
347
+ return this.send("DELETE", docPath("_doc", id));
327
348
  }
328
- // Deletes by _id (a string), or the documents that the filter matches: one,
329
- // or every match with many (same rule as update).
330
- async delete(idOrFilter, opts = {}) {
331
- await this.ready();
332
- if (typeof idOrFilter === "string") {
333
- return this.db.transport.request("DELETE", this.path(encodeURIComponent(idOrFilter)));
334
- }
335
- const filter = idOrFilter;
336
- return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
349
+ // Deletes the first document the query matches, or every match with many
350
+ // (POST /{coll}/_delete_by_query, same rule as updateBy).
351
+ deleteBy(query, opts = {}) {
352
+ return this.send("POST", "_delete_by_query", { query: toWhere(query), many: opts.many });
337
353
  }
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".
354
+ // Removes every document and keeps the definition and indexes. The cost
355
+ // does not depend on the number of documents. Watch consumers get one event
356
+ // with op "truncate".
341
357
  async truncate() {
342
- await this.ready();
343
- await this.db.transport.request("POST", this.path("truncate") + this.confirm());
358
+ await this.db.transport.request("POST", this.path("_truncate") + this.confirm());
344
359
  }
345
360
  // Removes the collection: its documents, its definition and the positions
346
- // of its watch consumers. The next call on the model declares it again.
361
+ // of its watch consumers. The next call declares it again.
347
362
  async drop() {
348
- await this.db.transport.request("DELETE", this.path() + this.confirm());
349
363
  this.ensured = void 0;
364
+ await this.db.transport.request("DELETE", this.path() + this.confirm());
350
365
  }
351
366
  confirm() {
352
367
  return `?confirm=${encodeURIComponent(this.name)}`;
@@ -355,99 +370,84 @@ var Model = class {
355
370
  // NotFoundError for a missing or expired document, and TtlError when the
356
371
  // schema has no expires field.
357
372
  expire(id, duration) {
358
- return this.setExpiry({ _id: id, in: duration });
373
+ return this.setExpiry(id, { in: duration });
359
374
  }
360
375
  expireAt(id, at) {
361
- return this.setExpiry({ _id: id, at: at.toISOString() });
376
+ return this.setExpiry(id, { at: at.toISOString() });
362
377
  }
363
378
  // Removes the expiry of a document (PERSIST in Redis).
364
- async persist(id) {
365
- await this.ready();
366
- return this.db.transport.request("POST", this.path("persist"), { _id: id });
379
+ persist(id) {
380
+ return this.send("POST", docPath("_persist", id));
367
381
  }
368
382
  // The seconds left (rounded up), -1 when the document never expires, null
369
383
  // when it is missing or expired.
370
384
  async ttl(id) {
371
- await this.ready();
372
- try {
373
- const out = await this.db.transport.request("GET", this.path(`${encodeURIComponent(id)}/ttl`));
374
- return out.ttl;
375
- } catch (err) {
376
- if (err instanceof NotFoundError) {
377
- return null;
378
- }
379
- throw err;
380
- }
385
+ const out = await orNull(this.send("GET", docPath("_ttl", id)));
386
+ return out === null ? null : out.ttl;
381
387
  }
382
- async setExpiry(body) {
383
- await this.ready();
384
- const out = await this.db.transport.request("POST", this.path("expire"), body);
385
- return { _id: out._id, v: out.v, expiresAt: new Date(out.expires_at) };
388
+ async setExpiry(id, body) {
389
+ const out = await this.send("POST", docPath("_expire", id), body);
390
+ return { _id: out._id, _v: out._v, expiresAt: new Date(out.expires_at) };
386
391
  }
387
392
  // The document with this _id, or null.
388
393
  async get(id) {
389
- await this.ready();
390
- try {
391
- const doc = await this.db.transport.request("GET", this.path(encodeURIComponent(id)));
392
- return this.decode(doc);
393
- } catch (err) {
394
- if (err instanceof NotFoundError) {
395
- return null;
396
- }
397
- throw err;
398
- }
399
- }
400
- query() {
401
- return new Query(this);
394
+ const doc = await orNull(this.send("GET", docPath("_doc", id)));
395
+ return doc === null ? null : this.decode(doc);
402
396
  }
403
- where(w) {
404
- return this.query().where(w);
397
+ // A search (POST /{coll}/_search). No query matches every document.
398
+ query(w) {
399
+ const q = new Query(this);
400
+ return w === void 0 ? q : q.query(w);
405
401
  }
406
- whereRaw(w) {
407
- return this.query().whereRaw(w);
402
+ // A query built elsewhere, such as from a search form: it is not
403
+ // type-checked and is sent as is; the daemon validates it.
404
+ rawQuery(w) {
405
+ return new Query(this).rawQuery(w);
408
406
  }
409
407
  not(w) {
410
- return this.query().not(w);
408
+ return new Query(this).not(w);
411
409
  }
412
410
  or(...ws) {
413
- return this.query().or(...ws);
411
+ return new Query(this).or(...ws);
414
412
  }
415
413
  sort(spec) {
416
- return this.query().sort(spec);
414
+ return new Query(this).sort(spec);
417
415
  }
418
416
  limit(n) {
419
- return this.query().limit(n);
417
+ return new Query(this).limit(n);
420
418
  }
421
419
  skip(n) {
422
- return this.query().skip(n);
420
+ return new Query(this).skip(n);
423
421
  }
424
422
  page(n, size) {
425
- return this.query().page(n, size);
423
+ return new Query(this).page(n, size);
426
424
  }
427
425
  after(token) {
428
- return this.query().after(token);
426
+ return new Query(this).after(token);
429
427
  }
430
428
  fields(...names) {
431
- return this.query().fields(...names);
429
+ return new Query(this).fields(...names);
432
430
  }
433
431
  all() {
434
- return this.query().all();
432
+ return new Query(this).all();
435
433
  }
436
- count() {
437
- return this.query().count();
434
+ // How many documents match (POST /{coll}/_count).
435
+ count(w) {
436
+ return this.query(w).count();
438
437
  }
439
438
  first() {
440
- return this.query().first();
439
+ return new Query(this).first();
441
440
  }
442
- iterate() {
443
- return this.query().iterate();
441
+ // Every row, page by page.
442
+ each() {
443
+ return new Query(this).each();
444
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) {
445
+ // Runs a whole search body built elsewhere, such as from a search form:
446
+ // { query, sort, limit, after, count }. The body is sent as is (the daemon
447
+ // validates it); rows are decoded like the rows of query().
448
+ async search(body) {
449
449
  await this.ready();
450
- const res = await this.db.transport.stream("POST", this.path("find"), { ...body, versions: true });
450
+ const res = await this.db.transport.stream("POST", this.path("_search"), { ...body, versions: true });
451
451
  const rows = [];
452
452
  for await (const raw of res.rows()) {
453
453
  rows.push(this.decode(raw));
@@ -456,7 +456,7 @@ var Model = class {
456
456
  }
457
457
  async aggregate(pipeline, opts = {}) {
458
458
  await this.ready();
459
- const res = await this.db.transport.stream("POST", this.path("aggregate"), { pipeline, ...opts });
459
+ const res = await this.db.transport.stream("POST", this.path("_aggregate"), { pipeline, ...opts });
460
460
  const rows = [];
461
461
  for await (const row of res.rows()) {
462
462
  rows.push(row);
@@ -465,14 +465,30 @@ var Model = class {
465
465
  }
466
466
  async *watch(consumer, opts = {}) {
467
467
  await this.ready();
468
- const body = { consumer, from: opts.from, limit: opts.limit, where: opts.where === void 0 ? void 0 : toWhere(opts.where) };
469
- const res = await this.db.transport.stream("POST", this.path("watch"), body, opts.signal);
468
+ const body = { consumer, from: opts.from, limit: opts.limit, query: opts.query === void 0 ? void 0 : toWhere(opts.query) };
469
+ const res = await this.db.transport.stream("POST", this.path("_watch"), body, opts.signal);
470
470
  for await (const ev of watchEvents(res.rows(), consumer)) {
471
471
  yield { ...ev, doc: ev.doc && this.decode(ev.doc) };
472
472
  }
473
473
  }
474
474
  async ack(event) {
475
- await this.db.transport.request("POST", this.path("ack"), { consumer: event.consumer, pos: event.pos });
475
+ await this.send("POST", "_ack", { consumer: event.consumer, pos: event.pos });
476
+ }
477
+ // Rows, indexes, files and bytes (GET /{coll}/_stats).
478
+ stats() {
479
+ return this.send("GET", "_stats");
480
+ }
481
+ // Seals the memtable and merges every segment now (POST /{coll}/_compact).
482
+ compact() {
483
+ return this.send("POST", "_compact");
484
+ }
485
+ // Adds a secondary index and builds it in the background. Returns the
486
+ // indexed fields.
487
+ async ensureIndex(field) {
488
+ return (await this.send("PUT", docPath("_indexes", field))).indexes;
489
+ }
490
+ async dropIndex(field) {
491
+ return (await this.send("DELETE", docPath("_indexes", field))).indexes;
476
492
  }
477
493
  decode(raw) {
478
494
  const out = { ...raw };
@@ -484,6 +500,15 @@ var Model = class {
484
500
  }
485
501
  return out;
486
502
  }
503
+ // Declares the collection on first use, then sends one request to
504
+ // /{coll}/{action}.
505
+ async send(method, action, body) {
506
+ await this.ready();
507
+ return this.db.transport.request(method, this.path(action), body);
508
+ }
509
+ updated(out) {
510
+ return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
511
+ }
487
512
  catalog() {
488
513
  const entries = Object.entries(this.schema.fields);
489
514
  return {
@@ -494,23 +519,26 @@ var Model = class {
494
519
  };
495
520
  }
496
521
  };
497
- function writeQuery(params) {
498
- const q = new URLSearchParams();
499
- for (const [name, value] of Object.entries(params)) {
500
- if (value !== void 0 && value !== false) {
501
- q.set(name, String(value));
502
- }
503
- }
504
- const text = q.toString();
505
- return text === "" ? "" : `?${text}`;
522
+ function docPath(action, id) {
523
+ return `${action}/${encodeURIComponent(id)}`;
506
524
  }
507
- function mergeUpdate(filter, ops) {
525
+ function checkOps(ops) {
508
526
  for (const key of Object.keys(ops)) {
509
527
  if (!key.startsWith("$")) {
510
528
  throw new InvalidUpdateError("invalid_update", `update operator "${key}" must start with $ (for example "$set")`);
511
529
  }
512
530
  }
513
- return { ...toWhere(filter), ...ops };
531
+ return ops;
532
+ }
533
+ async function orNull(p) {
534
+ try {
535
+ return await p;
536
+ } catch (err) {
537
+ if (err instanceof NotFoundError) {
538
+ return null;
539
+ }
540
+ throw err;
541
+ }
514
542
  }
515
543
 
516
544
  // src/transport.ts
@@ -548,7 +576,7 @@ var notConnected = /* @__PURE__ */ new Set(["ECONNREFUSED", "ENOENT"]);
548
576
  var retryBaseMs = 50;
549
577
  var retryCapMs = 1e3;
550
578
  var defaultReadyTimeoutMs = 1e4;
551
- var readOps = /\/(find|count|aggregate|get|stats)$/;
579
+ var readOps = /\/(_search|_count|_aggregate)$/;
552
580
  var Transport = class {
553
581
  client;
554
582
  token;
@@ -567,9 +595,9 @@ var Transport = class {
567
595
  throw new TypeError(`ndb: address "${target}" must be unix:///path or tcp://host:port`);
568
596
  }
569
597
  }
570
- async stream(method, path, body, signal) {
598
+ async stream(method, path, body, signal, contentType) {
571
599
  const res = await this.withReadyRetry(method, path, signal, async () => {
572
- const r = await this.send(method, path, body, signal);
600
+ const r = await this.send(method, path, body, signal, contentType);
573
601
  if (r.statusCode >= 300) {
574
602
  throw await apiError(r);
575
603
  }
@@ -583,8 +611,9 @@ var Transport = class {
583
611
  header: (name) => headerOf(res.headers, name)
584
612
  };
585
613
  }
586
- async request(method, path, body) {
587
- const res = await this.stream(method, path, body);
614
+ // contentType sends body as raw bytes (blobs) instead of JSON.
615
+ async request(method, path, body, contentType) {
616
+ const res = await this.stream(method, path, body, void 0, contentType);
588
617
  const text = await res.body.setEncoding("utf8").toArray();
589
618
  return text.length > 0 ? JSON.parse(text.join("")) : void 0;
590
619
  }
@@ -612,20 +641,22 @@ var Transport = class {
612
641
  }
613
642
  }
614
643
  }
615
- headers() {
616
- const headers = { "content-type": "application/json" };
644
+ // Rows come as NDJSON: the SDK reads them one by one (jsonRows).
645
+ headers(contentType = "application/json") {
646
+ const headers = { "content-type": contentType, accept: "application/x-ndjson" };
617
647
  if (this.token) {
618
648
  headers.authorization = `Bearer ${this.token}`;
619
649
  }
620
650
  return headers;
621
651
  }
622
- async send(method, path, body, signal) {
652
+ async send(method, path, body, signal, contentType) {
653
+ const raw = contentType !== void 0;
623
654
  const opts = {
624
655
  method,
625
656
  path,
626
657
  signal,
627
- headers: this.headers(),
628
- body: body === void 0 ? void 0 : JSON.stringify(body)
658
+ headers: this.headers(contentType),
659
+ body: body === void 0 ? void 0 : raw ? body : JSON.stringify(body)
629
660
  };
630
661
  try {
631
662
  return await this.client.request(opts);
@@ -692,11 +723,37 @@ var Ndb = class {
692
723
  constructor(addr, opts) {
693
724
  this.transport = new Transport(addr, opts);
694
725
  }
695
- model(name, schema) {
696
- return new Model(this, name, schema);
726
+ // A collection of the daemon; see Collection. Nothing is sent until its
727
+ // first call.
728
+ collection(name, schema) {
729
+ return new Collection(this, name, schema);
697
730
  }
731
+ // Resolves when the daemon is ready (GET /_health).
698
732
  async health() {
699
- await this.transport.request("GET", "/health");
733
+ await this.transport.request("GET", "/_health");
734
+ }
735
+ // The catalog: one definition per collection (GET /_collections).
736
+ async collections() {
737
+ return (await this.transport.request("GET", "/_collections")).collections;
738
+ }
739
+ // Every collection, totals, version and uptime (GET /_stats).
740
+ stats() {
741
+ return this.transport.request("GET", "/_stats");
742
+ }
743
+ // Disk use per collection and per part (GET /_size).
744
+ size() {
745
+ return this.transport.request("GET", "/_size");
746
+ }
747
+ // Stores bytes in the blob store (POST /_blobs) and returns the reference
748
+ // to put in a document field.
749
+ putBlob(data, contentType = "application/octet-stream") {
750
+ return this.transport.request("POST", "/_blobs", data, contentType);
751
+ }
752
+ // The bytes of a blob (GET /_blobs/{ref}).
753
+ async getBlob(blob) {
754
+ const ref = typeof blob === "string" ? blob : blob.$blob;
755
+ const res = await this.transport.stream("GET", `/_blobs/${encodeURIComponent(ref)}`);
756
+ return new Uint8Array(await res.body.arrayBuffer());
700
757
  }
701
758
  request(method, path, body) {
702
759
  return this.transport.request(method, path, body);
@@ -745,12 +802,12 @@ function seedCounter() {
745
802
  }
746
803
  // Annotate the CommonJS export names for ESM import in node:
747
804
  0 && (module.exports = {
805
+ Collection,
748
806
  CursorError,
749
807
  DuplicateIdError,
750
808
  HistoryTruncatedError,
751
809
  InvalidQueryError,
752
810
  InvalidUpdateError,
753
- Model,
754
811
  Ndb,
755
812
  NdbError,
756
813
  NotFoundError,