@loams/client 0.1.0 → 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.
Files changed (56) hide show
  1. package/README.md +104 -4
  2. package/dist/app.d.ts +149 -0
  3. package/dist/app.js +317 -0
  4. package/dist/builders/filter.d.ts +96 -0
  5. package/dist/builders/filter.js +190 -0
  6. package/dist/builders/schema.d.ts +50 -0
  7. package/dist/builders/schema.js +92 -0
  8. package/dist/builders/search.d.ts +159 -0
  9. package/dist/builders/search.js +307 -0
  10. package/dist/gen/facade.d.ts +267 -2
  11. package/dist/gen/facade.js +194 -0
  12. package/dist/handle.d.ts +67 -0
  13. package/dist/handle.js +107 -0
  14. package/dist/index.d.ts +13 -5
  15. package/dist/index.js +13 -3
  16. package/dist/loams.d.ts +78 -3
  17. package/dist/loams.js +84 -6
  18. package/dist/node.js +1 -1
  19. package/dist/runtime/batch.d.ts +29 -0
  20. package/dist/runtime/batch.js +95 -0
  21. package/dist/runtime/call.d.ts +63 -9
  22. package/dist/runtime/call.js +243 -41
  23. package/dist/runtime/consistency.d.ts +65 -11
  24. package/dist/runtime/consistency.js +212 -46
  25. package/dist/runtime/errors.d.ts +29 -2
  26. package/dist/runtime/errors.js +26 -0
  27. package/dist/runtime/machine-key.d.ts +33 -0
  28. package/dist/runtime/machine-key.js +174 -0
  29. package/dist/runtime/pagination.js +8 -2
  30. package/dist/runtime/subscribe.d.ts +37 -0
  31. package/dist/runtime/subscribe.js +128 -0
  32. package/dist/runtime/token-source.d.ts +1 -1
  33. package/dist/runtime/token-source.js +1 -1
  34. package/dist/runtime/transports.d.ts +22 -1
  35. package/dist/runtime/transports.js +28 -2
  36. package/dist/vendor/proto/collection-v1.d.ts +3 -0
  37. package/dist/vendor/proto/collection-v1.js +7 -0
  38. package/dist/vendor/proto/gen/loams/auth/v1/auth_pb.d.ts +7 -2
  39. package/dist/vendor/proto/gen/loams/auth/v1/auth_pb.js +5 -1
  40. package/dist/vendor/proto/gen/loams/collection/v1/collection_pb.d.ts +1610 -0
  41. package/dist/vendor/proto/gen/loams/collection/v1/collection_pb.js +399 -0
  42. package/dist/vendor/proto/gen/loams/collection/v1/document_pb.d.ts +1054 -0
  43. package/dist/vendor/proto/gen/loams/collection/v1/document_pb.js +327 -0
  44. package/dist/vendor/proto/gen/loams/collection/v1/query_pb.d.ts +2101 -0
  45. package/dist/vendor/proto/gen/loams/collection/v1/query_pb.js +608 -0
  46. package/dist/vendor/proto/gen/loams/graph/v1/graph_pb.d.ts +2299 -0
  47. package/dist/vendor/proto/gen/loams/graph/v1/graph_pb.js +596 -0
  48. package/dist/vendor/proto/gen/loams/instance/v1/instance_pb.d.ts +23 -7
  49. package/dist/vendor/proto/gen/loams/instance/v1/instance_pb.js +20 -5
  50. package/dist/vendor/proto/gen/loams/operations/v1/operations_pb.d.ts +399 -0
  51. package/dist/vendor/proto/gen/loams/operations/v1/operations_pb.js +118 -0
  52. package/dist/vendor/proto/gen/loams/sql/v1/sql_pb.d.ts +182 -0
  53. package/dist/vendor/proto/gen/loams/sql/v1/sql_pb.js +72 -0
  54. package/dist/vendor/proto/gen/loams/stream/v1/stream_pb.d.ts +688 -0
  55. package/dist/vendor/proto/gen/loams/stream/v1/stream_pb.js +180 -0
  56. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # The TypeScript SDK (`@loams/client`)
4
4
 
5
- > **Preview.** `0.1.0` tracks the Loams preview server and may change between previews; pin an exact version. Installed with `npm install @loams/client@preview`.
5
+ > **Preview.** `0.1.2` tracks the Loams preview server and may change between previews; pin an exact version. Installed with `npm install @loams/client@preview`.
6
6
 
7
7
  Design §44 §7 and §10.2: the SDK is a `Loams` object with namespaced modules,
8
8
  generated from the `loams.options.v1` annotations on the protos, over the
@@ -29,7 +29,7 @@ generator inputs stay outside the workspace, next to `proto/`.
29
29
  npm i @loams/client # and @loams/live for Live's stubs
30
30
  ```
31
31
 
32
- `0.1.0` tracks Loams 0.1.0; in 0.x the API may change between minor versions. <!-- verify after publish -->
32
+ `0.1.2` tracks Loams 0.1.2; in 0.x the API may change between minor versions.
33
33
 
34
34
  ## Using it
35
35
 
@@ -55,8 +55,13 @@ try {
55
55
  ```
56
56
 
57
57
  One port serves the Connect protocol, gRPC and gRPC-Web (D600), so the same
58
- object works in a browser, in Node, in Deno and in Bun. The default transport is
59
- Connect over `fetch`. For HTTP/2 gRPC on a server:
58
+ object works in a browser, in Node and in Bun. The default transport is
59
+ Connect over `fetch`.
60
+
61
+ Deno is not supported (design 79, D1790): Deno's team is ending the runtime's
62
+ development. The default `fetch` transport uses only standard Web APIs, so the
63
+ SDK may still run on Deno, but we don't test it there or promise it works.
64
+ Moving off Deno? See [Moving from Deno to Bun](https://github.com/ostrium-labs/loams/blob/dev/docs/guides/migrate-deno-to-bun.md). For HTTP/2 gRPC on a server:
60
65
 
61
66
  ```ts
62
67
  import { Loams, createNodeTransport } from '@loams/client/node';
@@ -67,6 +72,101 @@ The `/node` subpath exists so a bundler never follows `@connectrpc/connect-node`
67
72
  into a browser build it cannot satisfy; the conformance suite checks that
68
73
  statically.
69
74
 
75
+ ## Build an app
76
+
77
+ The app-building layer (SDKP) adds builders, a collection handle and the
78
+ runtime an app needs: a default namespace, deadlines, read-your-writes
79
+ consistency tokens, chunked bulk writes and stream subscriptions.
80
+
81
+ ```ts
82
+ import { Filter, Loams, Schema, Search, Sort, rrf } from '@loams/client';
83
+
84
+ const loams = Loams.fromEnv(); // LOAMS_ENDPOINT, LOAMS_API_KEY, LOAMS_NAMESPACE
85
+ // or: new Loams({ endpoint, auth: key, namespace: 'app', sessionConsistency: true })
86
+
87
+ await loams.collections.create({
88
+ name: 'notes',
89
+ schema: new Schema().text('title').text('body').keyword('tags').i64('n')
90
+ .vector('embedding', 4).toJson(),
91
+ });
92
+
93
+ const notes = loams.collection('notes'); // calls with `collection` filled in
94
+ await notes.upsert([
95
+ { id: 1, source: { title: 'Refunds', body: 'refund policy', tags: 'policy', n: 1 },
96
+ vectors: { embedding: [1, 0, 0, 0] } },
97
+ ]);
98
+
99
+ const hits = await notes.search(
100
+ notes.query()
101
+ .text('refund', { fields: ['body'] })
102
+ .vector('embedding', [1, 0, 0, 0], { k: 10 })
103
+ .fuse(rrf(60))
104
+ .filter(Filter.and(Filter.eq('tags', 'policy'), Filter.range('n', { gte: 1 })))
105
+ .highlight('body')
106
+ .select({ include: ['title'] })
107
+ .limit(5),
108
+ );
109
+
110
+ for await (const hit of loams.search.queryAll(
111
+ Search.on('notes').text('refund').sort(Sort.score(), Sort.pk()).limit(100),
112
+ )) { /* every hit, page by page */ }
113
+
114
+ await notes.writeBatch(rows); // 10 000 rows / 4 MiB a chunk, one key per chunk
115
+ ```
116
+
117
+ - **Default namespace (R11).** `namespace` fills every request that has a
118
+ `namespace` field and leaves it empty; an explicit one wins, and neither is
119
+ `InvalidArgumentError` before any network call.
120
+ - **Deadlines (R12).** Unary calls default to 30 s (`timeoutMs` on the client
121
+ or per call; `0` disables). Running out raises `DeadlineExceededError` with
122
+ reason `client_deadline_exceeded`, which is never retried. Streams have no
123
+ default deadline.
124
+ - **Consistency tokens (R4).** `ConsistencyToken.parse/merge/toString`; with
125
+ `sessionConsistency: true` every token a response carries merges into
126
+ `loams.consistency`, and every read with a `consistency` field is sent
127
+ `at_least` the session.
128
+ - **Bulk writes (R13).** `documents.writeBatch` sends one chunk at a time,
129
+ each with its own idempotency key, returns the merged token, and stops at
130
+ the first failed chunk with a `BatchError` (`chunksApplied`, `rowsApplied`).
131
+ - **Subscriptions (R14).** `streams.subscribe({ stream, partition, fromOffset })`
132
+ is an async iterable over `Fetch` that exposes `offset` for resuming.
133
+
134
+ The modules `loams.namespaces`, `loams.collections`, `loams.documents`,
135
+ `loams.search`, `loams.vector` and `loams.graph` are generated from the protos'
136
+ facade annotations, with the hand-written conveniences above added to them.
137
+ `loams.sql` is generated from `loams.sql.v1` the same way: `sql.query` answers
138
+ `columns` and `rows`, `sql.records(statement, [$1, …])` answers one object per
139
+ row, and `sqlRecords(answer)` reads an answer already in hand. `sql.queryArrow`
140
+ answers the result as Arrow IPC bytes (`arrowIpc`), which `apache-arrow`'s
141
+ `tableFromIPC` reads; the SDK does not depend on it. `loams.streams` is
142
+ generated from `loams.stream.v1`: `create`, `get`, `list` (and `listAll`),
143
+ `produce` (pass `producerId` and `producerSeq` so a retry cannot append
144
+ twice), `fetch`, `produceEvents` and `fetchEvents` (CloudEvents as a JSON
145
+ batch), plus `streams.subscribe(...)` (R14), an async iterable over `fetch`
146
+ whose `offset` is where to resume. `examples/notes/main.ts` is the whole app
147
+ scenario as a program, type-checked in CI. The session's token is used only for
148
+ collection and SQL reads: a graph's token names one graph (SDKP-17). Durable
149
+ workflows use the Resonate SDK pointed at `LOAMS_ENDPOINT/durable/` (SDKP-1).
150
+
151
+ ## Cloudflare Workers
152
+
153
+ `@loams/client` runs in Cloudflare Workers (workerd) with no `nodejs_compat` flag and no `fetch` override. The
154
+ default transport detects workerd (`navigator.userAgent === "Cloudflare-Workers"`) and sends
155
+ `redirect: "manual"`, because connect-web's default `redirect: "error"` is rejected by Workers. Loams calls never
156
+ redirect, so nothing changes for them. Other runtimes are untouched: in a browser `manual` would hide a redirect
157
+ as an opaque response.
158
+
159
+ ```ts
160
+ const loams = new Loams({ endpoint: env.LOAMS_URL, token: env.LOAMS_TOKEN });
161
+ ```
162
+
163
+ To control `fetch` yourself (a proxy, a service binding, a test double), pass it through the transport options;
164
+ it is used exactly as given, so set `redirect: "manual"` in it if you also run in workerd:
165
+
166
+ ```ts
167
+ new Loams({ endpoint, transportOptions: { fetch: env.GATEWAY.fetch.bind(env.GATEWAY) } });
168
+ ```
169
+
70
170
  ## What is generated and what is hand-written
71
171
 
72
172
  Design §44 §7.3 draws the line, and this SDK follows it exactly.
package/dist/app.d.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { type JsonObject, type JsonValue } from '@bufbuild/protobuf';
2
+ import { type ListValue } from '@bufbuild/protobuf/wkt';
3
+ import type { ModuleBinding } from './gen/facade.js';
4
+ import { type DocId, type FilterJson } from './builders/filter.js';
5
+ import { Search, type ConsistencyLevel, type FusionJson } from './builders/search.js';
6
+ import { type BatchOptions, type BatchResult } from './runtime/batch.js';
7
+ import type { CallOptions } from './runtime/call.js';
8
+ import { type OffsetRecord, type Subscription, type SubscribeRequest } from './runtime/subscribe.js';
9
+ /** A generated unary call, as the hand-written layer sees it. */
10
+ export type UnaryCall = (request: any, options?: CallOptions) => Promise<any>;
11
+ /** One document to write. */
12
+ export interface DocumentInput {
13
+ id: DocId;
14
+ /** The document's `_source`. */
15
+ source?: JsonObject;
16
+ /** Dense vectors by name. */
17
+ vectors?: Record<string, readonly number[]>;
18
+ /** Sparse vectors by name. */
19
+ sparseVectors?: Record<string, {
20
+ indices: number[];
21
+ values: number[];
22
+ }>;
23
+ }
24
+ /** Where a document call goes: the collection, and the namespace when it is
25
+ * not the client's default. */
26
+ export interface Target {
27
+ collection: string;
28
+ namespace?: string;
29
+ }
30
+ /** A document id as `DocumentId`'s proto3 JSON: `{"uint": "1"}`,
31
+ * `{"string": "a"}` or `{"uuid": "…"}`. */
32
+ export declare function documentIdJson(id: DocId): JsonObject;
33
+ /** The `WriteOp`s of the document conveniences, as proto3 JSON. */
34
+ export declare const WriteOps: {
35
+ readonly upsert: (doc: DocumentInput) => JsonObject;
36
+ readonly delete: (id: DocId) => JsonObject;
37
+ readonly patch: (id: DocId, change: PatchChange) => JsonObject;
38
+ };
39
+ /** What a `patch` changes. */
40
+ export interface PatchChange {
41
+ /** The keys to merge into `_source`. */
42
+ set?: JsonObject;
43
+ /** The top-level keys to remove. */
44
+ unset?: string[];
45
+ /** `merge_deep` (the default), `merge_top` or `replace`. */
46
+ mode?: 'merge_deep' | 'merge_top' | 'replace';
47
+ /** Vectors to set; `null` removes one. */
48
+ vectors?: Record<string, readonly number[] | null>;
49
+ /** The document to write when there is none to patch. */
50
+ upsert?: DocumentInput;
51
+ }
52
+ /** The scroll options of `scrollAll`. */
53
+ export interface ScrollOptions extends CallOptions {
54
+ /** A typed filter (`ScrollDocumentsRequest.where`, a `Query`'s proto3 JSON). */
55
+ where?: FilterJson;
56
+ /** Documents per page. */
57
+ limit?: number;
58
+ select?: JsonObject;
59
+ /** How fresh each page must be. */
60
+ read?: ConsistencyLevel;
61
+ }
62
+ /** The document conveniences over `documents.write` and `documents.scroll`. */
63
+ export declare function documentCalls(documents: {
64
+ write: UnaryCall;
65
+ scroll?: UnaryCall;
66
+ }): {
67
+ /** Writes (creates or replaces) documents in one call. Answers the write's
68
+ * response, whose `token` is its consistency token. */
69
+ upsert(collection: string | Target, docs: DocumentInput[], options?: CallOptions): Promise<any>;
70
+ /** Removes documents by id in one call. */
71
+ delete(collection: string | Target, ids: DocId[], options?: CallOptions): Promise<any>;
72
+ /** Changes part of one document. */
73
+ patch(collection: string | Target, id: DocId, change: PatchChange, options?: CallOptions): Promise<any>;
74
+ /** A bulk upsert, chunked, one chunk in flight (R13). */
75
+ writeBatch(collection: string | Target, docs: Iterable<DocumentInput> | AsyncIterable<DocumentInput>, options?: BatchOptions & CallOptions): Promise<BatchResult>;
76
+ /** Every document, page by page, following `next` until it is unset. */
77
+ scrollAll(collection: string | Target, options?: ScrollOptions): AsyncGenerator<unknown, void, unknown>;
78
+ };
79
+ /** The options of the search shorthands. */
80
+ export interface SearchShorthandOptions extends CallOptions {
81
+ namespace?: string;
82
+ filter?: FilterJson;
83
+ limit?: number;
84
+ }
85
+ /** The search conveniences over `search.query`. */
86
+ export declare function searchCalls(search: {
87
+ query: UnaryCall;
88
+ }): {
89
+ /** A text search: one field is a `match`, several a `multi_match`, none a
90
+ * `query_string`. */
91
+ text(collection: string, text: string, options?: SearchShorthandOptions & {
92
+ fields?: string[];
93
+ }): Promise<any>;
94
+ /** Text plus vector, fused (RRF by default). */
95
+ hybrid(collection: string, text: string, vector: readonly number[], options: SearchShorthandOptions & {
96
+ field: string;
97
+ fields?: string[];
98
+ fusion?: FusionJson;
99
+ k?: number;
100
+ }): Promise<any>;
101
+ /**
102
+ * Every hit, page after page, following `search_after` from the last hit's
103
+ * `sortValues` until a page comes back short. The sort must end in a
104
+ * primary-key tiebreaker (`Sort.pk()`), or two hits with equal sort values
105
+ * could straddle a page and be skipped or repeated; without one this
106
+ * refuses with `invalid_argument` before any call.
107
+ */
108
+ queryAll(builder: Search, options?: CallOptions): AsyncGenerator<{
109
+ sortValues?: unknown[];
110
+ }, void, unknown>;
111
+ };
112
+ /** The parts of a `sql.query` answer `sqlRecords` reads. */
113
+ export interface SqlAnswer {
114
+ columns: readonly {
115
+ name: string;
116
+ }[];
117
+ rows: readonly ListValue[];
118
+ }
119
+ /**
120
+ * A `sql.query` answer's rows as one object per row, keyed by column name
121
+ * (SDKP §2.5). A value is its JSON: a 64-bit integer past 2^53 is the decimal
122
+ * string the server sent, so it stays exact. A column name that repeats keeps
123
+ * its last value, as a JavaScript object must.
124
+ */
125
+ export declare function sqlRecords(answer: SqlAnswer): Record<string, JsonValue>[];
126
+ /** The options of `sql.records`. */
127
+ export interface SqlRecordsOptions extends CallOptions {
128
+ namespace?: string;
129
+ maxRows?: number;
130
+ }
131
+ /** The SQL conveniences over `sql.query`. */
132
+ export declare function sqlCalls(sql: {
133
+ query: UnaryCall;
134
+ }): {
135
+ /**
136
+ * One read-only statement with its `$1…$n` values, answered as records
137
+ * (`sqlRecords`). The values are JSON scalars; the server casts each to the
138
+ * type its placeholder was inferred as.
139
+ */
140
+ records(statement: string, params?: readonly JsonValue[], options?: SqlRecordsOptions): Promise<Record<string, JsonValue>[]>;
141
+ };
142
+ /** The stream conveniences over `streams.fetch`. */
143
+ export declare function streamCalls(streams: {
144
+ fetch: UnaryCall;
145
+ }): {
146
+ /** Every record from `fromOffset` on, as an async iterable (R14). */
147
+ subscribe<R extends OffsetRecord = OffsetRecord>(request: SubscribeRequest, options?: CallOptions): Subscription<R>;
148
+ };
149
+ export declare function attachAppCalls(modules: Record<string, unknown>, bindings: readonly ModuleBinding[]): void;
package/dist/app.js ADDED
@@ -0,0 +1,317 @@
1
+ // The hand-written half of the app modules (SDKP §2.3–§2.5, §4).
2
+ //
3
+ // The generated facade gives one function per proto method. A developer
4
+ // building an app wants a few more, each a thin loop or shorthand over those:
5
+ //
6
+ // - `documents.upsert`, `delete`, `patch` and `writeBatch` over `write`;
7
+ // - `documents.scrollAll` over `scroll` (its cursor is a `DocumentId`, not an
8
+ // AIP-158 string, so the generic `listAll` does not fit it);
9
+ // - `search.text`, `search.hybrid` and `search.queryAll` over `search.query`;
10
+ // - `sql.records` over `sql.query`, and `sqlRecords` for an answer in hand;
11
+ // - `streams.subscribe` over `streams.fetch`;
12
+ // - `<call>All` for every paged call (`namespaces.listAll`,
13
+ // `collections.listAll`), over the generic paging iterator.
14
+ //
15
+ // `attachAppCalls` adds them to the module objects the facade built, and only
16
+ // to modules that exist: an SDK whose facade has no `documents` module gets no
17
+ // `documents.upsert`. Nothing here names an RPC path or a retry class; those
18
+ // stay the generated bindings'.
19
+ import { fromJson, toJson } from '@bufbuild/protobuf';
20
+ import { ListValueSchema, ValueSchema } from '@bufbuild/protobuf/wkt';
21
+ import { Code } from '@connectrpc/connect';
22
+ import { WriteOpSchema } from './vendor/proto/gen/loams/collection/v1/document_pb.js';
23
+ import { idValue } from './builders/filter.js';
24
+ import { Search, consistencyJson, rrf } from './builders/search.js';
25
+ import { writeBatch } from './runtime/batch.js';
26
+ import { InvalidArgumentError } from './runtime/errors.js';
27
+ import { paginate } from './runtime/pagination.js';
28
+ import { subscribe } from './runtime/subscribe.js';
29
+ /** A document id as `DocumentId`'s proto3 JSON: `{"uint": "1"}`,
30
+ * `{"string": "a"}` or `{"uuid": "…"}`. */
31
+ export function documentIdJson(id) {
32
+ if (typeof id === 'string') {
33
+ return { string: id };
34
+ }
35
+ const value = idValue(id);
36
+ return typeof value === 'string' ? { string: value } : value;
37
+ }
38
+ function documentJson(doc) {
39
+ const out = { id: documentIdJson(doc.id) };
40
+ if (doc.source !== undefined) {
41
+ out.source = doc.source;
42
+ }
43
+ if (doc.vectors !== undefined) {
44
+ out.vectors = Object.fromEntries(Object.entries(doc.vectors).map(([name, values]) => [name, [...values]]));
45
+ }
46
+ if (doc.sparseVectors !== undefined) {
47
+ out.sparseVectors = doc.sparseVectors;
48
+ }
49
+ return out;
50
+ }
51
+ /** The `WriteOp`s of the document conveniences, as proto3 JSON. */
52
+ export const WriteOps = {
53
+ upsert(doc) {
54
+ return { upsert: documentJson(doc) };
55
+ },
56
+ delete(id) {
57
+ return { delete: { id: documentIdJson(id) } };
58
+ },
59
+ patch(id, change) {
60
+ const patch = { id: documentIdJson(id) };
61
+ if (change.mode !== undefined) {
62
+ patch.mode = change.mode;
63
+ }
64
+ if (change.set !== undefined) {
65
+ patch.source = change.set;
66
+ }
67
+ if (change.unset !== undefined && change.unset.length > 0) {
68
+ patch.deleteKeys = [...change.unset];
69
+ }
70
+ if (change.vectors !== undefined) {
71
+ patch.vectors = change.vectors;
72
+ }
73
+ if (change.upsert !== undefined) {
74
+ patch.upsert = documentJson(change.upsert);
75
+ }
76
+ return { patch };
77
+ },
78
+ };
79
+ function opsRequest(target, ops) {
80
+ return {
81
+ ...(target.namespace === undefined ? {} : { namespace: target.namespace }),
82
+ collection: target.collection,
83
+ ops: ops.map((op) => fromJson(WriteOpSchema, op)),
84
+ };
85
+ }
86
+ function targetOf(collection) {
87
+ return typeof collection === 'string' ? { collection } : collection;
88
+ }
89
+ /** The document conveniences over `documents.write` and `documents.scroll`. */
90
+ export function documentCalls(documents) {
91
+ return {
92
+ /** Writes (creates or replaces) documents in one call. Answers the write's
93
+ * response, whose `token` is its consistency token. */
94
+ upsert(collection, docs, options) {
95
+ return documents.write(opsRequest(targetOf(collection), docs.map(WriteOps.upsert)), options);
96
+ },
97
+ /** Removes documents by id in one call. */
98
+ delete(collection, ids, options) {
99
+ return documents.write(opsRequest(targetOf(collection), ids.map(WriteOps.delete)), options);
100
+ },
101
+ /** Changes part of one document. */
102
+ patch(collection, id, change, options) {
103
+ return documents.write(opsRequest(targetOf(collection), [WriteOps.patch(id, change)]), options);
104
+ },
105
+ /** A bulk upsert, chunked, one chunk in flight (R13). */
106
+ writeBatch(collection, docs, options = {}) {
107
+ const target = targetOf(collection);
108
+ return writeBatch((ops, callOptions) => documents.write(opsRequest(target, ops), callOptions), mapOps(docs), options);
109
+ },
110
+ /** Every document, page by page, following `next` until it is unset. */
111
+ async *scrollAll(collection, options = {}) {
112
+ if (documents.scroll === undefined) {
113
+ throw new InvalidArgumentError('this SDK build has no documents.scroll', {
114
+ code: Code.Unimplemented,
115
+ });
116
+ }
117
+ const { where, limit, select, read, ...call } = options;
118
+ const target = targetOf(collection);
119
+ let after;
120
+ for (;;) {
121
+ const page = await documents.scroll({
122
+ ...(target.namespace === undefined ? {} : { namespace: target.namespace }),
123
+ collection: target.collection,
124
+ // `where` is a `Query`'s proto3 JSON (SDKP-12), which is what a
125
+ // `Filter` already is.
126
+ ...(where === undefined ? {} : { where }),
127
+ ...(limit === undefined ? {} : { limit }),
128
+ ...(select === undefined ? {} : { select }),
129
+ ...(read === undefined ? {} : { consistency: consistencyJsonMessage(read) }),
130
+ ...(after === undefined ? {} : { after }),
131
+ }, call);
132
+ for (const doc of (page?.documents ?? [])) {
133
+ yield doc;
134
+ }
135
+ if (page?.next === undefined || page.next.id?.case === undefined) {
136
+ return;
137
+ }
138
+ after = page.next;
139
+ }
140
+ },
141
+ };
142
+ }
143
+ /** A `Consistency` init object for a call that takes one directly. */
144
+ function consistencyJsonMessage(level) {
145
+ const json = consistencyJson(level);
146
+ if ('atLeast' in json) {
147
+ return { atLeast: json.atLeast };
148
+ }
149
+ return { freshness: json.freshness === 'FRESHNESS_EVENTUAL' ? 2 : 1 };
150
+ }
151
+ async function* mapOps(docs) {
152
+ for await (const doc of docs) {
153
+ yield WriteOps.upsert(doc);
154
+ }
155
+ }
156
+ /** The search conveniences over `search.query`. */
157
+ export function searchCalls(search) {
158
+ const run = (builder, options) => search.query(builder.build(), options);
159
+ return {
160
+ /** A text search: one field is a `match`, several a `multi_match`, none a
161
+ * `query_string`. */
162
+ text(collection, text, options = {}) {
163
+ const { namespace, filter, limit, fields, ...call } = options;
164
+ let builder = Search.on(collection, { namespace }).text(text, { fields });
165
+ if (filter !== undefined)
166
+ builder = builder.filter(filter);
167
+ if (limit !== undefined)
168
+ builder = builder.limit(limit);
169
+ return run(builder, call);
170
+ },
171
+ /** Text plus vector, fused (RRF by default). */
172
+ hybrid(collection, text, vector, options) {
173
+ const { namespace, filter, limit, fields, field, fusion, k, ...call } = options;
174
+ let builder = Search.on(collection, { namespace })
175
+ .text(text, { fields, k })
176
+ .vector(field, vector, { k })
177
+ .fuse(fusion ?? rrf());
178
+ if (filter !== undefined)
179
+ builder = builder.filter(filter);
180
+ if (limit !== undefined)
181
+ builder = builder.limit(limit);
182
+ return run(builder, call);
183
+ },
184
+ /**
185
+ * Every hit, page after page, following `search_after` from the last hit's
186
+ * `sortValues` until a page comes back short. The sort must end in a
187
+ * primary-key tiebreaker (`Sort.pk()`), or two hits with equal sort values
188
+ * could straddle a page and be skipped or repeated; without one this
189
+ * refuses with `invalid_argument` before any call.
190
+ */
191
+ async *queryAll(builder, options = {}) {
192
+ if (!builder.hasTiebreak) {
193
+ throw new InvalidArgumentError('search.queryAll needs a sort that ends in the primary key, such as .sort(Sort.score(), Sort.pk())', { code: Code.InvalidArgument, reason: 'search_after_needs_tiebreak', metadata: { field: 'sort' } });
194
+ }
195
+ const limit = builder.toJson().limit ?? 10;
196
+ let page = builder;
197
+ for (;;) {
198
+ const answer = await run(page, options);
199
+ const hits = (answer?.hits ?? []);
200
+ for (const hit of hits) {
201
+ yield hit;
202
+ }
203
+ const last = hits[hits.length - 1];
204
+ if (hits.length < limit || last === undefined || last.sortValues === undefined) {
205
+ return;
206
+ }
207
+ page = builder.after(last.sortValues);
208
+ }
209
+ },
210
+ };
211
+ }
212
+ /**
213
+ * A `sql.query` answer's rows as one object per row, keyed by column name
214
+ * (SDKP §2.5). A value is its JSON: a 64-bit integer past 2^53 is the decimal
215
+ * string the server sent, so it stays exact. A column name that repeats keeps
216
+ * its last value, as a JavaScript object must.
217
+ */
218
+ export function sqlRecords(answer) {
219
+ const names = answer.columns.map((column) => column.name);
220
+ return answer.rows.map((row) => {
221
+ const values = toJson(ListValueSchema, row);
222
+ const record = {};
223
+ names.forEach((name, at) => {
224
+ record[name] = values[at] ?? null;
225
+ });
226
+ return record;
227
+ });
228
+ }
229
+ /** The SQL conveniences over `sql.query`. */
230
+ export function sqlCalls(sql) {
231
+ return {
232
+ /**
233
+ * One read-only statement with its `$1…$n` values, answered as records
234
+ * (`sqlRecords`). The values are JSON scalars; the server casts each to the
235
+ * type its placeholder was inferred as.
236
+ */
237
+ async records(statement, params = [], options = {}) {
238
+ const { namespace, maxRows, ...call } = options;
239
+ const answer = (await sql.query({
240
+ sql: statement,
241
+ params: params.map((value) => fromJson(ValueSchema, value)),
242
+ ...(namespace !== undefined ? { namespace } : {}),
243
+ ...(maxRows !== undefined ? { maxRows } : {}),
244
+ }, call));
245
+ return sqlRecords(answer);
246
+ },
247
+ };
248
+ }
249
+ /** The stream conveniences over `streams.fetch`. */
250
+ export function streamCalls(streams) {
251
+ return {
252
+ /** Every record from `fromOffset` on, as an async iterable (R14). */
253
+ subscribe(request, options = {}) {
254
+ return subscribe((fetchRequest, loopOptions) => streams.fetch(fetchRequest, { ...options, signal: loopOptions?.signal }), request, { signal: options.signal });
255
+ },
256
+ };
257
+ }
258
+ /**
259
+ * Adds the hand-written calls to the generated module objects, in place.
260
+ *
261
+ * Every paged call gets `<call>All`; `documents`, `search` and `streams` get
262
+ * their conveniences when the facade generated them. A name the facade
263
+ * already uses is never overwritten.
264
+ */
265
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
266
+ export function attachAppCalls(modules, bindings) {
267
+ const add = (module, calls) => {
268
+ // Module names come from the generated bindings, but an own-property
269
+ // check keeps a name such as `__proto__` from reaching Object.prototype.
270
+ if (!Object.hasOwn(modules, module) || UNSAFE_KEYS.has(module)) {
271
+ return;
272
+ }
273
+ const target = modules[module];
274
+ if (target === undefined) {
275
+ return;
276
+ }
277
+ for (const [name, fn] of Object.entries(calls)) {
278
+ if (!UNSAFE_KEYS.has(name) && !(name in target)) {
279
+ target[name] = fn;
280
+ }
281
+ }
282
+ };
283
+ for (const module of bindings) {
284
+ const target = modules[module.name];
285
+ if (target === undefined) {
286
+ continue;
287
+ }
288
+ for (const binding of module.calls) {
289
+ if (binding.pagination === null || binding.streaming !== 'unary') {
290
+ continue;
291
+ }
292
+ const call = target[binding.name];
293
+ if (typeof call !== 'function') {
294
+ continue;
295
+ }
296
+ add(module.name, {
297
+ [`${binding.name}All`]: (request = {}, options) => paginate(binding, call, request, options),
298
+ });
299
+ }
300
+ }
301
+ const documents = modules.documents;
302
+ if (typeof documents?.write === 'function') {
303
+ add('documents', documentCalls(documents));
304
+ }
305
+ const search = modules.search;
306
+ if (typeof search?.query === 'function') {
307
+ add('search', searchCalls(search));
308
+ }
309
+ const sql = modules.sql;
310
+ if (typeof sql?.query === 'function') {
311
+ add('sql', sqlCalls(sql));
312
+ }
313
+ const streams = modules.streams;
314
+ if (typeof streams?.fetch === 'function') {
315
+ add('streams', streamCalls(streams));
316
+ }
317
+ }