@wavehouse/sdk 0.0.0-dev.h514be5522d89 → 0.0.0-dev.h70dbf5d8f5ed
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 +10 -2
- package/dist/cli/codegen.js +62 -18
- package/dist/index.cjs +65 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +143 -9
- package/dist/index.d.ts +143 -9
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +65 -30
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -8,8 +8,12 @@ Zero-dependency TypeScript client for [WaveHouse](https://github.com/Wave-RF/Wav
|
|
|
8
8
|
npm install @wavehouse/sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Requires Node 22 or newer — the only line this SDK is tested against; Node 18 and 20 are past upstream end-of-life. Browsers need no polyfill, and Node needs an `EventSource` polyfill only for streaming — see [Runtime support](https://wavehouse.dev/sdk/#runtime-support).
|
|
12
|
+
|
|
11
13
|
This works in any framework that uses a bundler — React, Vue, Svelte, Angular, Astro, SolidJS, or plain Vite — with `import { createClient } from '@wavehouse/sdk'`.
|
|
12
14
|
|
|
15
|
+
The package is published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements) — npm shows a verified provenance badge on the [package page](https://www.npmjs.com/package/@wavehouse/sdk), and `npm audit signatures` checks the attestation against this repository's signed build.
|
|
16
|
+
|
|
13
17
|
## Use without a build step (CDN)
|
|
14
18
|
|
|
15
19
|
No bundler, no `npm`, no framework required — drop the SDK straight into an HTML file you deploy over FTP, object storage, or any static host.
|
|
@@ -53,6 +57,8 @@ const wh = createClient({
|
|
|
53
57
|
});
|
|
54
58
|
```
|
|
55
59
|
|
|
60
|
+
`baseURL` may include a path prefix (`https://app.example.com/api/warehouse`) when WaveHouse is served under one — see [Serving under a path prefix](https://wavehouse.dev/sdk#serving-under-a-path-prefix).
|
|
61
|
+
|
|
56
62
|
### Query Data
|
|
57
63
|
|
|
58
64
|
```ts
|
|
@@ -123,7 +129,9 @@ const { data } = await wh.from('clicks').select('page').limit(10);
|
|
|
123
129
|
|
|
124
130
|
## Error Handling
|
|
125
131
|
|
|
126
|
-
|
|
132
|
+
Async request methods return a `Result<T>` object — destructure it as `{ data, error }` — and never throw for anything the server returns. `.stream()` and `.liveQuery()` return controllers instead, reporting failures through the subscriber's `error` callback.
|
|
133
|
+
|
|
134
|
+
The SDK does throw on caller and environment errors: a non-absolute `baseURL` (REST calls reject with a `TypeError`; streams report `SSE_CONNECT_ERROR`, and despite that error's `retryable: true` the SDK never re-dials the stream itself), `.stream()` / `.liveQuery()` in a runtime with no `EventSource`, and an `auth` callback that rejects — a token-refresh failure propagates out of the REST call.
|
|
127
135
|
|
|
128
136
|
```ts
|
|
129
137
|
const { data, error } = await wh.from('clicks').fetch();
|
|
@@ -142,7 +150,7 @@ Generate TypeScript types from your live WaveHouse schema. The package ships a `
|
|
|
142
150
|
npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
|
|
143
151
|
```
|
|
144
152
|
|
|
145
|
-
This introspects `/v1/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`. `/v1/schema` is **admin-only** — pass an admin-role token with `--auth <jwt>` (or `-a`) against any non-dev policy.
|
|
153
|
+
This introspects `/v1/ops/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`. `/v1/ops/schema` is **admin-only** — pass an admin-role token with `--auth <jwt>` (or `-a`) against any non-dev policy.
|
|
146
154
|
|
|
147
155
|
## Development & Testing
|
|
148
156
|
|
package/dist/cli/codegen.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli/codegen.ts
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
|
|
7
|
+
// src/url.ts
|
|
8
|
+
function resolveURL(base, path, params) {
|
|
9
|
+
const root = new URL(base);
|
|
10
|
+
root.search = "";
|
|
11
|
+
root.hash = "";
|
|
12
|
+
if (!root.pathname.endsWith("/")) root.pathname += "/";
|
|
13
|
+
const url = new URL(path.replace(/^\/+/, ""), root);
|
|
14
|
+
if (params) {
|
|
15
|
+
for (const [k, v] of Object.entries(params)) {
|
|
16
|
+
url.searchParams.set(k, v);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
// src/cli/codegen.ts
|
|
4
23
|
function parseArgs(argv) {
|
|
5
24
|
const args = { url: "http://localhost:8080", out: "./wavehouse.d.ts" };
|
|
@@ -92,15 +111,29 @@ function findTopLevelComma(s) {
|
|
|
92
111
|
}
|
|
93
112
|
return -1;
|
|
94
113
|
}
|
|
114
|
+
function isColumn(value) {
|
|
115
|
+
if (typeof value !== "object" || value === null) return false;
|
|
116
|
+
const col = value;
|
|
117
|
+
return typeof col.name === "string" && typeof col.type === "string";
|
|
118
|
+
}
|
|
119
|
+
function isTableSchema(value) {
|
|
120
|
+
if (typeof value !== "object" || value === null) return false;
|
|
121
|
+
const table = value;
|
|
122
|
+
return typeof table.name === "string" && Array.isArray(table.columns) && table.columns.every(isColumn);
|
|
123
|
+
}
|
|
95
124
|
async function fetchSchemas(url, auth) {
|
|
96
125
|
const headers = {};
|
|
97
126
|
if (auth) headers.Authorization = `Bearer ${auth}`;
|
|
98
|
-
const res = await fetch(
|
|
127
|
+
const res = await fetch(resolveURL(url, "/v1/ops/schema").toString(), { headers });
|
|
99
128
|
if (!res.ok) {
|
|
100
129
|
const text = await res.text();
|
|
101
130
|
throw new Error(`Schema fetch failed (${res.status}): ${text}`);
|
|
102
131
|
}
|
|
103
|
-
|
|
132
|
+
const body = await res.json();
|
|
133
|
+
if (!Array.isArray(body) || !body.every(isTableSchema)) {
|
|
134
|
+
throw new Error("Unexpected /v1/ops/schema response: expected a JSON array of table schemas");
|
|
135
|
+
}
|
|
136
|
+
return body;
|
|
104
137
|
}
|
|
105
138
|
function generateTypes(schemas) {
|
|
106
139
|
const lines = [
|
|
@@ -110,19 +143,16 @@ function generateTypes(schemas) {
|
|
|
110
143
|
"",
|
|
111
144
|
"export interface Database {"
|
|
112
145
|
];
|
|
113
|
-
const
|
|
114
|
-
for (const
|
|
115
|
-
|
|
116
|
-
const rowType = `${pascalCase(tableName)}Row`;
|
|
117
|
-
lines.push(` ${tableName}: ${rowType};`);
|
|
146
|
+
const tables = [...schemas].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
147
|
+
for (const table of tables) {
|
|
148
|
+
lines.push(` ${table.name}: ${pascalCase(table.name)}Row;`);
|
|
118
149
|
}
|
|
119
150
|
lines.push("}");
|
|
120
151
|
lines.push("");
|
|
121
|
-
for (const
|
|
122
|
-
const
|
|
123
|
-
const rowType = `${pascalCase(tableName)}Row`;
|
|
152
|
+
for (const table of tables) {
|
|
153
|
+
const rowType = `${pascalCase(table.name)}Row`;
|
|
124
154
|
lines.push(`export interface ${rowType} {`);
|
|
125
|
-
for (const col of
|
|
155
|
+
for (const col of table.columns) {
|
|
126
156
|
const tsType = chTypeToTS(col.type);
|
|
127
157
|
const optional = col.has_default ? "?" : "";
|
|
128
158
|
lines.push(` ${col.name}${optional}: ${tsType};`);
|
|
@@ -139,12 +169,11 @@ async function main() {
|
|
|
139
169
|
const args = parseArgs(process.argv);
|
|
140
170
|
console.log(`Fetching schema from ${args.url}...`);
|
|
141
171
|
const schemas = await fetchSchemas(args.url, args.auth);
|
|
142
|
-
|
|
143
|
-
if (tableCount === 0) {
|
|
172
|
+
if (schemas.length === 0) {
|
|
144
173
|
console.warn("No tables found. Is WaveHouse running with tables in ClickHouse?");
|
|
145
174
|
process.exit(1);
|
|
146
175
|
}
|
|
147
|
-
console.log(`Found ${
|
|
176
|
+
console.log(`Found ${schemas.length} table(s): ${schemas.map((t) => t.name).join(", ")}`);
|
|
148
177
|
const output = generateTypes(schemas);
|
|
149
178
|
const { writeFile } = await import("fs/promises");
|
|
150
179
|
const { resolve } = await import("path");
|
|
@@ -152,7 +181,22 @@ async function main() {
|
|
|
152
181
|
await writeFile(outPath, output, "utf-8");
|
|
153
182
|
console.log(`\u2713 Types written to ${outPath}`);
|
|
154
183
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
function isDirectInvocation() {
|
|
185
|
+
if (!process.argv[1]) return false;
|
|
186
|
+
try {
|
|
187
|
+
return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (isDirectInvocation()) {
|
|
193
|
+
main().catch((err) => {
|
|
194
|
+
console.error("Codegen failed:", err.message);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
chTypeToTS,
|
|
200
|
+
fetchSchemas,
|
|
201
|
+
generateTypes
|
|
202
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -72,9 +72,39 @@ function err(error) {
|
|
|
72
72
|
return { ok: false, data: null, error };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// src/url.ts
|
|
76
|
+
function resolveURL(base, path, params) {
|
|
77
|
+
const root = new URL(base);
|
|
78
|
+
root.search = "";
|
|
79
|
+
root.hash = "";
|
|
80
|
+
if (!root.pathname.endsWith("/")) root.pathname += "/";
|
|
81
|
+
const url = new URL(path.replace(/^\/+/, ""), root);
|
|
82
|
+
if (params) {
|
|
83
|
+
for (const [k, v] of Object.entries(params)) {
|
|
84
|
+
url.searchParams.set(k, v);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return url;
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
// src/http.ts
|
|
91
|
+
function mergeHeaders(base, extra) {
|
|
92
|
+
if (!extra) return base;
|
|
93
|
+
const sdkNames = new Set(Object.keys(base).map((k) => k.toLowerCase()));
|
|
94
|
+
const merged = { ...base };
|
|
95
|
+
const configured = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const [name, value] of Object.entries(extra)) {
|
|
97
|
+
const lower = name.toLowerCase();
|
|
98
|
+
if (sdkNames.has(lower)) continue;
|
|
99
|
+
const prior = configured.get(lower);
|
|
100
|
+
if (prior !== void 0) delete merged[prior];
|
|
101
|
+
merged[name] = value;
|
|
102
|
+
configured.set(lower, name);
|
|
103
|
+
}
|
|
104
|
+
return merged;
|
|
105
|
+
}
|
|
76
106
|
async function request(ctx, opts) {
|
|
77
|
-
const url =
|
|
107
|
+
const url = resolveURL(ctx.baseURL, opts.path, opts.params).toString();
|
|
78
108
|
const headers = {
|
|
79
109
|
"Content-Type": opts.contentType ?? "application/json",
|
|
80
110
|
Accept: "application/json"
|
|
@@ -86,16 +116,20 @@ async function request(ctx, opts) {
|
|
|
86
116
|
headers.Authorization = `Bearer ${token}`;
|
|
87
117
|
}
|
|
88
118
|
}
|
|
119
|
+
const finalHeaders = mergeHeaders(headers, ctx.options.headers);
|
|
89
120
|
let lastError = null;
|
|
90
121
|
const maxAttempts = ctx.options.maxRetries + 1;
|
|
91
122
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
92
123
|
try {
|
|
93
|
-
const
|
|
124
|
+
const init = {
|
|
125
|
+
...ctx.options.fetchOptions,
|
|
94
126
|
method: opts.method,
|
|
95
|
-
headers,
|
|
127
|
+
headers: finalHeaders,
|
|
96
128
|
body: requestBody,
|
|
97
129
|
signal: opts.signal
|
|
98
|
-
}
|
|
130
|
+
};
|
|
131
|
+
const doFetch = ctx.options.fetch;
|
|
132
|
+
const res = doFetch ? await doFetch(url, init) : await fetch(url, init);
|
|
99
133
|
if (res.ok) {
|
|
100
134
|
const text = await res.text();
|
|
101
135
|
const data = text ? JSON.parse(text) : void 0;
|
|
@@ -133,15 +167,6 @@ async function request(ctx, opts) {
|
|
|
133
167
|
}
|
|
134
168
|
return { data: null, error: lastError, headers: new Headers() };
|
|
135
169
|
}
|
|
136
|
-
function buildURL(base, path, params) {
|
|
137
|
-
const url = new URL(path, base.endsWith("/") ? base : `${base}/`);
|
|
138
|
-
if (params) {
|
|
139
|
-
for (const [k, v] of Object.entries(params)) {
|
|
140
|
-
url.searchParams.set(k, v);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return url.toString();
|
|
144
|
-
}
|
|
145
170
|
function backoff(attempt) {
|
|
146
171
|
return Math.min(1e3 * 2 ** attempt, 3e4);
|
|
147
172
|
}
|
|
@@ -175,7 +200,7 @@ var DLQNamespace = class {
|
|
|
175
200
|
async list(opts) {
|
|
176
201
|
const { data, error } = await request(this._ctx, {
|
|
177
202
|
method: "GET",
|
|
178
|
-
path: "/v1/dlq/stats",
|
|
203
|
+
path: "/v1/ops/dlq/stats",
|
|
179
204
|
signal: opts?.signal
|
|
180
205
|
});
|
|
181
206
|
if (error) return err(error);
|
|
@@ -185,7 +210,7 @@ var DLQNamespace = class {
|
|
|
185
210
|
async table(name, opts) {
|
|
186
211
|
const { data, error } = await request(this._ctx, {
|
|
187
212
|
method: "GET",
|
|
188
|
-
path: "/v1/dlq/stats",
|
|
213
|
+
path: "/v1/ops/dlq/stats",
|
|
189
214
|
params: { table: name },
|
|
190
215
|
signal: opts?.signal
|
|
191
216
|
});
|
|
@@ -210,7 +235,14 @@ var PipeRef = class {
|
|
|
210
235
|
this._params = params;
|
|
211
236
|
this._createStream = createStream;
|
|
212
237
|
}
|
|
213
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* Execute the pipe and return results.
|
|
240
|
+
*
|
|
241
|
+
* Takes only `signal` — deliberately narrower than the `RequestOptions` the
|
|
242
|
+
* query builder accepts. The pipes endpoint binds the body as the pipe's
|
|
243
|
+
* parameters, so there is no row cap to forward; give the pipe a `{{limit}}`
|
|
244
|
+
* parameter in its SQL and pass it via `wh.pipe(name, { limit })`.
|
|
245
|
+
*/
|
|
214
246
|
async fetch(opts) {
|
|
215
247
|
const { data, error } = await request(this._ctx, {
|
|
216
248
|
method: "POST",
|
|
@@ -238,7 +270,7 @@ var PipesNamespace = class {
|
|
|
238
270
|
async list(opts) {
|
|
239
271
|
const { data, error } = await request(this._ctx, {
|
|
240
272
|
method: "GET",
|
|
241
|
-
path: "/v1/
|
|
273
|
+
path: "/v1/ops/pipes",
|
|
242
274
|
signal: opts?.signal
|
|
243
275
|
});
|
|
244
276
|
if (error) return err(error);
|
|
@@ -248,7 +280,7 @@ var PipesNamespace = class {
|
|
|
248
280
|
async get(name, opts) {
|
|
249
281
|
const { data, error } = await request(this._ctx, {
|
|
250
282
|
method: "GET",
|
|
251
|
-
path: `/v1/
|
|
283
|
+
path: `/v1/ops/pipes/${encodeURIComponent(name)}`,
|
|
252
284
|
signal: opts?.signal
|
|
253
285
|
});
|
|
254
286
|
if (error) return err(error);
|
|
@@ -258,7 +290,7 @@ var PipesNamespace = class {
|
|
|
258
290
|
async set(name, def, opts) {
|
|
259
291
|
const { error } = await request(this._ctx, {
|
|
260
292
|
method: "PUT",
|
|
261
|
-
path: `/v1/
|
|
293
|
+
path: `/v1/ops/pipes/${encodeURIComponent(name)}`,
|
|
262
294
|
body: def,
|
|
263
295
|
signal: opts?.signal
|
|
264
296
|
});
|
|
@@ -269,7 +301,7 @@ var PipesNamespace = class {
|
|
|
269
301
|
async delete(name, opts) {
|
|
270
302
|
const { error } = await request(this._ctx, {
|
|
271
303
|
method: "DELETE",
|
|
272
|
-
path: `/v1/
|
|
304
|
+
path: `/v1/ops/pipes/${encodeURIComponent(name)}`,
|
|
273
305
|
signal: opts?.signal
|
|
274
306
|
});
|
|
275
307
|
if (error) return err(error);
|
|
@@ -287,7 +319,7 @@ var PolicyNamespace = class {
|
|
|
287
319
|
async get(opts) {
|
|
288
320
|
const { data, error } = await request(this._ctx, {
|
|
289
321
|
method: "GET",
|
|
290
|
-
path: "/v1/
|
|
322
|
+
path: "/v1/ops/policy",
|
|
291
323
|
signal: opts?.signal
|
|
292
324
|
});
|
|
293
325
|
if (error) return err(error);
|
|
@@ -297,7 +329,7 @@ var PolicyNamespace = class {
|
|
|
297
329
|
async set(policy, opts) {
|
|
298
330
|
const { error } = await request(this._ctx, {
|
|
299
331
|
method: "PUT",
|
|
300
|
-
path: "/v1/
|
|
332
|
+
path: "/v1/ops/policy",
|
|
301
333
|
body: policy,
|
|
302
334
|
signal: opts?.signal
|
|
303
335
|
});
|
|
@@ -308,7 +340,7 @@ var PolicyNamespace = class {
|
|
|
308
340
|
async validate(policy, opts) {
|
|
309
341
|
const { data, error } = await request(this._ctx, {
|
|
310
342
|
method: "POST",
|
|
311
|
-
path: "/v1/
|
|
343
|
+
path: "/v1/ops/policy/validate",
|
|
312
344
|
body: policy,
|
|
313
345
|
signal: opts?.signal
|
|
314
346
|
});
|
|
@@ -327,7 +359,7 @@ var SchemaNamespace = class {
|
|
|
327
359
|
async list(opts) {
|
|
328
360
|
const { data, error } = await request(this._ctx, {
|
|
329
361
|
method: "GET",
|
|
330
|
-
path: "/v1/schema",
|
|
362
|
+
path: "/v1/ops/schema",
|
|
331
363
|
signal: opts?.signal
|
|
332
364
|
});
|
|
333
365
|
if (error) return err(error);
|
|
@@ -348,7 +380,7 @@ var SchemaNamespace = class {
|
|
|
348
380
|
async refresh(opts) {
|
|
349
381
|
const { error } = await request(this._ctx, {
|
|
350
382
|
method: "POST",
|
|
351
|
-
path: "/v1/schema/refresh",
|
|
383
|
+
path: "/v1/ops/schema/refresh",
|
|
352
384
|
signal: opts?.signal
|
|
353
385
|
});
|
|
354
386
|
if (error) return err(error);
|
|
@@ -360,7 +392,7 @@ var SchemaNamespace = class {
|
|
|
360
392
|
async function sql(ctx, query, opts) {
|
|
361
393
|
const { data, error } = await request(ctx, {
|
|
362
394
|
method: "POST",
|
|
363
|
-
path: "/v1/
|
|
395
|
+
path: "/v1/ops/query",
|
|
364
396
|
body: { sql: query },
|
|
365
397
|
signal: opts?.signal
|
|
366
398
|
});
|
|
@@ -558,7 +590,7 @@ var SSETransport = class {
|
|
|
558
590
|
this.onStatus?.("closed");
|
|
559
591
|
}
|
|
560
592
|
async _doConnect() {
|
|
561
|
-
const url =
|
|
593
|
+
const url = resolveURL(this._opts.baseURL, "/v1/stream");
|
|
562
594
|
url.searchParams.set("table", this._opts.table);
|
|
563
595
|
if (this._opts.since) {
|
|
564
596
|
url.searchParams.set("since", this._opts.since);
|
|
@@ -1072,7 +1104,7 @@ var TableRef = class {
|
|
|
1072
1104
|
async schema(opts) {
|
|
1073
1105
|
const { data, error } = await request(this._ctx, {
|
|
1074
1106
|
method: "GET",
|
|
1075
|
-
path: `/v1/schema?table=${encodeURIComponent(this._table)}`,
|
|
1107
|
+
path: `/v1/ops/schema?table=${encodeURIComponent(this._table)}`,
|
|
1076
1108
|
signal: opts?.signal
|
|
1077
1109
|
});
|
|
1078
1110
|
if (error) return err(error);
|
|
@@ -1103,7 +1135,10 @@ var WaveHouseClient = class {
|
|
|
1103
1135
|
baseURL: config.baseURL.replace(/\/+$/, ""),
|
|
1104
1136
|
auth: config.auth,
|
|
1105
1137
|
options: {
|
|
1106
|
-
maxRetries: config.options?.maxRetries ?? 2
|
|
1138
|
+
maxRetries: config.options?.maxRetries ?? 2,
|
|
1139
|
+
fetch: config.options?.fetch,
|
|
1140
|
+
headers: config.options?.headers,
|
|
1141
|
+
fetchOptions: config.options?.fetchOptions
|
|
1107
1142
|
}
|
|
1108
1143
|
};
|
|
1109
1144
|
this.schema = new SchemaNamespace(this._ctx);
|
|
@@ -1135,7 +1170,7 @@ var WaveHouseClient = class {
|
|
|
1135
1170
|
sql(query, opts) {
|
|
1136
1171
|
if (Array.isArray(opts)) {
|
|
1137
1172
|
throw new Error(
|
|
1138
|
-
"[WaveHouse SDK] client.sql(sql, params) was removed. The /v1/
|
|
1173
|
+
"[WaveHouse SDK] client.sql(sql, params) was removed. The /v1/ops/query endpoint does not accept positional `?` params. Inline literals into the SQL, or use the structured query builder (wh.from(table)\u2026) for safe binding from user input."
|
|
1139
1174
|
);
|
|
1140
1175
|
}
|
|
1141
1176
|
return sql(this._ctx, query, opts);
|