@wavehouse/sdk 0.0.0-dev.0f8826c → 0.0.0-dev.h04a85f20bb96
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 +48 -7
- package/dist/cli/codegen.js +173 -0
- package/dist/index.cjs +152 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +236 -11
- package/dist/index.d.ts +236 -11
- package/dist/index.global.js +3 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +152 -45
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -8,6 +8,44 @@ 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
|
+
|
|
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'`.
|
|
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
|
+
|
|
17
|
+
## Use without a build step (CDN)
|
|
18
|
+
|
|
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.
|
|
20
|
+
|
|
21
|
+
**ES module (recommended).** Modern browsers run `type="module"` natively:
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<script type="module">
|
|
25
|
+
import { createClient } from 'https://esm.sh/@wavehouse/sdk';
|
|
26
|
+
|
|
27
|
+
const wh = createClient({ baseURL: 'https://your-wavehouse.example.com' });
|
|
28
|
+
const { data, error } = await wh.from('clicks').select('page').limit(10);
|
|
29
|
+
console.log(data ?? error);
|
|
30
|
+
</script>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pin a version for production (`https://esm.sh/@wavehouse/sdk@0.1.0`). jsDelivr (`https://cdn.jsdelivr.net/npm/@wavehouse/sdk/+esm`) and unpkg (`https://unpkg.com/@wavehouse/sdk?module`) serve the same ES module.
|
|
34
|
+
|
|
35
|
+
**Classic global (`<script src>`).** For pages that can't use ES modules, the bundled IIFE build attaches everything to a `WaveHouse` global:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<script src="https://cdn.jsdelivr.net/npm/@wavehouse/sdk"></script>
|
|
39
|
+
<script>
|
|
40
|
+
const wh = WaveHouse.createClient({ baseURL: 'https://your-wavehouse.example.com' });
|
|
41
|
+
wh.from('clicks').select('page').limit(10).then(({ data }) => console.log(data));
|
|
42
|
+
</script>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Versioning.** A bare CDN URL serves the latest published **release**; pin for production (`@wavehouse/sdk@0.1.0`) or float on a range (`@0` for the newest 0.x, `@0.1` for 0.1.x). Builds from `main` are published under the `dev` tag — `@wavehouse/sdk@dev` — for trying unreleased changes.
|
|
46
|
+
|
|
47
|
+
Streaming (`.stream()`) uses the browser's native `EventSource`, so it works in both forms with no polyfill.
|
|
48
|
+
|
|
11
49
|
## Quick Start
|
|
12
50
|
|
|
13
51
|
```ts
|
|
@@ -19,6 +57,8 @@ const wh = createClient({
|
|
|
19
57
|
});
|
|
20
58
|
```
|
|
21
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
|
+
|
|
22
62
|
### Query Data
|
|
23
63
|
|
|
24
64
|
```ts
|
|
@@ -89,7 +129,9 @@ const { data } = await wh.from('clicks').select('page').limit(10);
|
|
|
89
129
|
|
|
90
130
|
## Error Handling
|
|
91
131
|
|
|
92
|
-
|
|
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.
|
|
93
135
|
|
|
94
136
|
```ts
|
|
95
137
|
const { data, error } = await wh.from('clicks').fetch();
|
|
@@ -102,13 +144,13 @@ if (error) {
|
|
|
102
144
|
|
|
103
145
|
## Codegen
|
|
104
146
|
|
|
105
|
-
Generate TypeScript types from your live WaveHouse schema
|
|
147
|
+
Generate TypeScript types from your live WaveHouse schema. The package ships a `wavehouse-codegen` bin, so after installing you can run it with `npx`:
|
|
106
148
|
|
|
107
149
|
```bash
|
|
108
|
-
|
|
150
|
+
npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
|
|
109
151
|
```
|
|
110
152
|
|
|
111
|
-
This introspects `/v1/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`.
|
|
153
|
+
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.
|
|
112
154
|
|
|
113
155
|
## Development & Testing
|
|
114
156
|
|
|
@@ -124,13 +166,12 @@ npm run test:coverage # coverage report
|
|
|
124
166
|
|
|
125
167
|
### E2E Integration Tests
|
|
126
168
|
|
|
127
|
-
E2E tests live in `tests/sdk/` (repo root) and exercise the full pipeline through the SDK. See `make test-e2e` in the [Development Guide](
|
|
169
|
+
E2E tests live in `tests/e2e/sdk/` (repo root) and exercise the full pipeline through the SDK. See `make test-e2e` in the [Development Guide](https://wavehouse.dev/development#e2e-tests-via-sdk).
|
|
128
170
|
|
|
129
171
|
## API Reference
|
|
130
172
|
|
|
131
|
-
See the full [SDK API Reference](
|
|
173
|
+
See the full [SDK API Reference](https://wavehouse.dev/sdk) for detailed documentation of every method, type, and option.
|
|
132
174
|
|
|
133
175
|
## License
|
|
134
176
|
|
|
135
177
|
Apache-2.0 © Wave RF — see [LICENSE](./LICENSE).
|
|
136
|
-
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/url.ts
|
|
4
|
+
function resolveURL(base, path, params) {
|
|
5
|
+
const root = new URL(base);
|
|
6
|
+
root.search = "";
|
|
7
|
+
root.hash = "";
|
|
8
|
+
if (!root.pathname.endsWith("/")) root.pathname += "/";
|
|
9
|
+
const url = new URL(path.replace(/^\/+/, ""), root);
|
|
10
|
+
if (params) {
|
|
11
|
+
for (const [k, v] of Object.entries(params)) {
|
|
12
|
+
url.searchParams.set(k, v);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return url;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/cli/codegen.ts
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = { url: "http://localhost:8080", out: "./wavehouse.d.ts" };
|
|
21
|
+
for (let i = 2; i < argv.length; i++) {
|
|
22
|
+
switch (argv[i]) {
|
|
23
|
+
case "--url":
|
|
24
|
+
case "-u":
|
|
25
|
+
args.url = argv[++i];
|
|
26
|
+
break;
|
|
27
|
+
case "--out":
|
|
28
|
+
case "-o":
|
|
29
|
+
args.out = argv[++i];
|
|
30
|
+
break;
|
|
31
|
+
case "--auth":
|
|
32
|
+
case "-a":
|
|
33
|
+
args.auth = argv[++i];
|
|
34
|
+
break;
|
|
35
|
+
case "--help":
|
|
36
|
+
case "-h":
|
|
37
|
+
console.log(`wavehouse codegen \u2014 Generate TypeScript types from WaveHouse schema
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--url, -u WaveHouse base URL (default: http://localhost:8080)
|
|
41
|
+
--out, -o Output file path (default: ./wavehouse.d.ts)
|
|
42
|
+
--auth, -a Bearer token for authenticated endpoints
|
|
43
|
+
--help, -h Show this help`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return args;
|
|
48
|
+
}
|
|
49
|
+
function chTypeToTS(chType) {
|
|
50
|
+
if (chType.startsWith("Nullable(") && chType.endsWith(")")) {
|
|
51
|
+
const inner = chType.slice(9, -1);
|
|
52
|
+
return `${chTypeToTS(inner)} | null`;
|
|
53
|
+
}
|
|
54
|
+
if (chType.startsWith("LowCardinality(") && chType.endsWith(")")) {
|
|
55
|
+
return chTypeToTS(chType.slice(15, -1));
|
|
56
|
+
}
|
|
57
|
+
if (chType === "String" || chType.startsWith("FixedString(") || chType === "UUID" || chType.startsWith("DateTime") || chType.startsWith("Date") || chType.startsWith("Enum8(") || chType.startsWith("Enum16(") || chType === "IPv4" || chType === "IPv6") {
|
|
58
|
+
return "string";
|
|
59
|
+
}
|
|
60
|
+
if (chType === "Bool") return "boolean";
|
|
61
|
+
if (isNumeric(chType)) return "number";
|
|
62
|
+
if (chType.startsWith("Array(") && chType.endsWith(")")) {
|
|
63
|
+
const inner = chType.slice(6, -1);
|
|
64
|
+
return `${chTypeToTS(inner)}[]`;
|
|
65
|
+
}
|
|
66
|
+
if (chType.startsWith("Map(") && chType.endsWith(")")) {
|
|
67
|
+
const inner = chType.slice(4, -1);
|
|
68
|
+
const comma = findTopLevelComma(inner);
|
|
69
|
+
if (comma !== -1) {
|
|
70
|
+
const keyType = chTypeToTS(inner.slice(0, comma).trim());
|
|
71
|
+
const valType = chTypeToTS(inner.slice(comma + 1).trim());
|
|
72
|
+
return `Record<${keyType}, ${valType}>`;
|
|
73
|
+
}
|
|
74
|
+
return "Record<string, unknown>";
|
|
75
|
+
}
|
|
76
|
+
if (chType.startsWith("Tuple(")) {
|
|
77
|
+
return "unknown[]";
|
|
78
|
+
}
|
|
79
|
+
return "unknown";
|
|
80
|
+
}
|
|
81
|
+
function isNumeric(t) {
|
|
82
|
+
const numericPrefixes = [
|
|
83
|
+
"UInt8",
|
|
84
|
+
"UInt16",
|
|
85
|
+
"UInt32",
|
|
86
|
+
"UInt64",
|
|
87
|
+
"UInt128",
|
|
88
|
+
"UInt256",
|
|
89
|
+
"Int8",
|
|
90
|
+
"Int16",
|
|
91
|
+
"Int32",
|
|
92
|
+
"Int64",
|
|
93
|
+
"Int128",
|
|
94
|
+
"Int256",
|
|
95
|
+
"Float32",
|
|
96
|
+
"Float64",
|
|
97
|
+
"Decimal"
|
|
98
|
+
];
|
|
99
|
+
return numericPrefixes.some((p) => t === p || t.startsWith(`${p}(`));
|
|
100
|
+
}
|
|
101
|
+
function findTopLevelComma(s) {
|
|
102
|
+
let depth = 0;
|
|
103
|
+
for (let i = 0; i < s.length; i++) {
|
|
104
|
+
if (s[i] === "(") depth++;
|
|
105
|
+
else if (s[i] === ")") depth--;
|
|
106
|
+
else if (s[i] === "," && depth === 0) return i;
|
|
107
|
+
}
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
async function fetchSchemas(url, auth) {
|
|
111
|
+
const headers = {};
|
|
112
|
+
if (auth) headers.Authorization = `Bearer ${auth}`;
|
|
113
|
+
const res = await fetch(resolveURL(url, "/v1/schema").toString(), { headers });
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const text = await res.text();
|
|
116
|
+
throw new Error(`Schema fetch failed (${res.status}): ${text}`);
|
|
117
|
+
}
|
|
118
|
+
return await res.json();
|
|
119
|
+
}
|
|
120
|
+
function generateTypes(schemas) {
|
|
121
|
+
const lines = [
|
|
122
|
+
"// Auto-generated by @wavehouse/sdk codegen",
|
|
123
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
124
|
+
"// Do not edit manually \u2014 re-run: npm run codegen",
|
|
125
|
+
"",
|
|
126
|
+
"export interface Database {"
|
|
127
|
+
];
|
|
128
|
+
const tableNames = Object.keys(schemas).sort();
|
|
129
|
+
for (const tableName of tableNames) {
|
|
130
|
+
const _schema = schemas[tableName];
|
|
131
|
+
const rowType = `${pascalCase(tableName)}Row`;
|
|
132
|
+
lines.push(` ${tableName}: ${rowType};`);
|
|
133
|
+
}
|
|
134
|
+
lines.push("}");
|
|
135
|
+
lines.push("");
|
|
136
|
+
for (const tableName of tableNames) {
|
|
137
|
+
const schema = schemas[tableName];
|
|
138
|
+
const rowType = `${pascalCase(tableName)}Row`;
|
|
139
|
+
lines.push(`export interface ${rowType} {`);
|
|
140
|
+
for (const col of schema.columns) {
|
|
141
|
+
const tsType = chTypeToTS(col.type);
|
|
142
|
+
const optional = col.has_default ? "?" : "";
|
|
143
|
+
lines.push(` ${col.name}${optional}: ${tsType};`);
|
|
144
|
+
}
|
|
145
|
+
lines.push("}");
|
|
146
|
+
lines.push("");
|
|
147
|
+
}
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
function pascalCase(s) {
|
|
151
|
+
return s.split(/[_\-\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
152
|
+
}
|
|
153
|
+
async function main() {
|
|
154
|
+
const args = parseArgs(process.argv);
|
|
155
|
+
console.log(`Fetching schema from ${args.url}...`);
|
|
156
|
+
const schemas = await fetchSchemas(args.url, args.auth);
|
|
157
|
+
const tableCount = Object.keys(schemas).length;
|
|
158
|
+
if (tableCount === 0) {
|
|
159
|
+
console.warn("No tables found. Is WaveHouse running with tables in ClickHouse?");
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
console.log(`Found ${tableCount} table(s): ${Object.keys(schemas).join(", ")}`);
|
|
163
|
+
const output = generateTypes(schemas);
|
|
164
|
+
const { writeFile } = await import("fs/promises");
|
|
165
|
+
const { resolve } = await import("path");
|
|
166
|
+
const outPath = resolve(args.out);
|
|
167
|
+
await writeFile(outPath, output, "utf-8");
|
|
168
|
+
console.log(`\u2713 Types written to ${outPath}`);
|
|
169
|
+
}
|
|
170
|
+
main().catch((err) => {
|
|
171
|
+
console.error("Codegen failed:", err.message);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
package/dist/index.cjs
CHANGED
|
@@ -72,29 +72,64 @@ 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
|
-
"Content-Type": "application/json",
|
|
109
|
+
"Content-Type": opts.contentType ?? "application/json",
|
|
80
110
|
Accept: "application/json"
|
|
81
111
|
};
|
|
112
|
+
const requestBody = opts.rawBody !== void 0 ? opts.rawBody : opts.body !== void 0 ? JSON.stringify(opts.body) : void 0;
|
|
82
113
|
if (ctx.auth) {
|
|
83
114
|
const token = await ctx.auth();
|
|
84
115
|
if (token) {
|
|
85
116
|
headers.Authorization = `Bearer ${token}`;
|
|
86
117
|
}
|
|
87
118
|
}
|
|
119
|
+
const finalHeaders = mergeHeaders(headers, ctx.options.headers);
|
|
88
120
|
let lastError = null;
|
|
89
121
|
const maxAttempts = ctx.options.maxRetries + 1;
|
|
90
122
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
91
123
|
try {
|
|
92
|
-
const
|
|
124
|
+
const init = {
|
|
125
|
+
...ctx.options.fetchOptions,
|
|
93
126
|
method: opts.method,
|
|
94
|
-
headers,
|
|
95
|
-
body:
|
|
127
|
+
headers: finalHeaders,
|
|
128
|
+
body: requestBody,
|
|
96
129
|
signal: opts.signal
|
|
97
|
-
}
|
|
130
|
+
};
|
|
131
|
+
const doFetch = ctx.options.fetch;
|
|
132
|
+
const res = doFetch ? await doFetch(url, init) : await fetch(url, init);
|
|
98
133
|
if (res.ok) {
|
|
99
134
|
const text = await res.text();
|
|
100
135
|
const data = text ? JSON.parse(text) : void 0;
|
|
@@ -132,15 +167,6 @@ async function request(ctx, opts) {
|
|
|
132
167
|
}
|
|
133
168
|
return { data: null, error: lastError, headers: new Headers() };
|
|
134
169
|
}
|
|
135
|
-
function buildURL(base, path, params) {
|
|
136
|
-
const url = new URL(path, base.endsWith("/") ? base : `${base}/`);
|
|
137
|
-
if (params) {
|
|
138
|
-
for (const [k, v] of Object.entries(params)) {
|
|
139
|
-
url.searchParams.set(k, v);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return url.toString();
|
|
143
|
-
}
|
|
144
170
|
function backoff(attempt) {
|
|
145
171
|
return Math.min(1e3 * 2 ** attempt, 3e4);
|
|
146
172
|
}
|
|
@@ -209,7 +235,14 @@ var PipeRef = class {
|
|
|
209
235
|
this._params = params;
|
|
210
236
|
this._createStream = createStream;
|
|
211
237
|
}
|
|
212
|
-
/**
|
|
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
|
+
*/
|
|
213
246
|
async fetch(opts) {
|
|
214
247
|
const { data, error } = await request(this._ctx, {
|
|
215
248
|
method: "POST",
|
|
@@ -557,7 +590,7 @@ var SSETransport = class {
|
|
|
557
590
|
this.onStatus?.("closed");
|
|
558
591
|
}
|
|
559
592
|
async _doConnect() {
|
|
560
|
-
const url =
|
|
593
|
+
const url = resolveURL(this._opts.baseURL, "/v1/stream");
|
|
561
594
|
url.searchParams.set("table", this._opts.table);
|
|
562
595
|
if (this._opts.since) {
|
|
563
596
|
url.searchParams.set("since", this._opts.since);
|
|
@@ -727,6 +760,15 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
727
760
|
select(...columns) {
|
|
728
761
|
return this._clone({ columns: [...this._state.columns, ...columns] });
|
|
729
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* Select every column the caller's role is allowed to read (the all-columns
|
|
765
|
+
* wildcard). Use this instead of `.select(...)` when you want a full-row read;
|
|
766
|
+
* a bare `.fetch()` with no `.select()` does this implicitly. Mutually
|
|
767
|
+
* exclusive with `.select(...)`.
|
|
768
|
+
*/
|
|
769
|
+
selectAll() {
|
|
770
|
+
return this._clone({ selectAll: true });
|
|
771
|
+
}
|
|
730
772
|
where(column, op, value) {
|
|
731
773
|
const filter = { column, op: OP_MAP[op], value };
|
|
732
774
|
return this._clone({ filters: [...this._state.filters, filter] });
|
|
@@ -768,7 +810,7 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
768
810
|
return this._clone({ cacheTTL: seconds });
|
|
769
811
|
}
|
|
770
812
|
// --- Execution ---
|
|
771
|
-
/** Default row limit when none is specified
|
|
813
|
+
/** Default row limit when none is specified — deliberately tighter than the backend's DefaultMaxRows (10000) safety cap. */
|
|
772
814
|
static DEFAULT_LIMIT = 1e3;
|
|
773
815
|
async fetch(opts) {
|
|
774
816
|
const effectiveLimit = opts?.limit ?? this._state.limit ?? _QueryBuilder.DEFAULT_LIMIT;
|
|
@@ -782,11 +824,11 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
782
824
|
if (error) return err(error);
|
|
783
825
|
const rows = data;
|
|
784
826
|
const hasMore = effectiveLimit != null && rows.length >= effectiveLimit;
|
|
785
|
-
if (hasMore) {
|
|
827
|
+
if (hasMore && this._state.orderBy.length > 0) {
|
|
786
828
|
const nextFn = () => this._fetchNext(rows, effectiveLimit, opts);
|
|
787
829
|
return okPage(rows, true, nextFn);
|
|
788
830
|
}
|
|
789
|
-
return okPage(rows,
|
|
831
|
+
return okPage(rows, hasMore);
|
|
790
832
|
}
|
|
791
833
|
stream(opts) {
|
|
792
834
|
const raw = this._createStream(this._state.table, opts);
|
|
@@ -825,31 +867,35 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
825
867
|
}
|
|
826
868
|
_buildAST(effectiveLimit) {
|
|
827
869
|
const ast = {};
|
|
828
|
-
|
|
829
|
-
|
|
870
|
+
const hasColumns = this._state.columns.length > 0;
|
|
871
|
+
const hasAggs = this._state.aggregations.length > 0;
|
|
872
|
+
if (this._state.selectAll) {
|
|
873
|
+
ast.select_all = true;
|
|
874
|
+
} else if (hasColumns) {
|
|
875
|
+
ast.columns = this._state.columns;
|
|
876
|
+
} else if (!hasAggs) {
|
|
877
|
+
ast.select_all = true;
|
|
878
|
+
}
|
|
879
|
+
if (hasAggs) ast.aggregations = this._state.aggregations;
|
|
830
880
|
if (this._state.filters.length > 0) ast.filters = this._state.filters;
|
|
831
881
|
if (this._state.groupBy.length > 0) ast.group_by = this._state.groupBy;
|
|
832
|
-
if (this._state.orderBy.length > 0)
|
|
833
|
-
ast.order_by = this._state.orderBy;
|
|
834
|
-
} else if (effectiveLimit != null && this._state.aggregations.length === 0) {
|
|
835
|
-
ast.order_by = [{ column: "received_timestamp", dir: "desc" }];
|
|
836
|
-
}
|
|
882
|
+
if (this._state.orderBy.length > 0) ast.order_by = this._state.orderBy;
|
|
837
883
|
if (effectiveLimit != null) ast.limit = effectiveLimit;
|
|
838
884
|
if (this._state.timeRange) ast.time_range = this._state.timeRange;
|
|
839
885
|
return ast;
|
|
840
886
|
}
|
|
841
887
|
async _fetchNext(prevRows, _limit, opts) {
|
|
842
|
-
const
|
|
843
|
-
|
|
888
|
+
const cursor = this._state.orderBy[0];
|
|
889
|
+
if (cursor == null) return okPage([], false);
|
|
890
|
+
const { column: orderCol, dir: orderDir } = cursor;
|
|
844
891
|
const lastRow = prevRows[prevRows.length - 1];
|
|
845
892
|
const lastValue = lastRow?.[orderCol];
|
|
846
893
|
if (lastValue === void 0) return okPage([], false);
|
|
847
894
|
const cursorOp = orderDir === "desc" ? "lt" : "gt";
|
|
848
895
|
const cursorFilter = { column: orderCol, op: cursorOp, value: lastValue };
|
|
849
|
-
const orderBy = this._state.orderBy.length > 0 ? this._state.orderBy : [{ column: orderCol, dir: orderDir }];
|
|
850
896
|
const nextBuilder = this._clone({
|
|
851
897
|
filters: [...this._state.filters, cursorFilter],
|
|
852
|
-
orderBy
|
|
898
|
+
orderBy: this._state.orderBy
|
|
853
899
|
});
|
|
854
900
|
return nextBuilder.fetch(opts);
|
|
855
901
|
}
|
|
@@ -940,6 +986,12 @@ function projectColumns(row, columns) {
|
|
|
940
986
|
}
|
|
941
987
|
|
|
942
988
|
// src/table.ts
|
|
989
|
+
var NDJSON_CONTENT_TYPE = "application/x-ndjson";
|
|
990
|
+
async function ndjsonSourceToString(source) {
|
|
991
|
+
if (typeof source === "string") return source;
|
|
992
|
+
if (source instanceof Uint8Array) return new TextDecoder().decode(source);
|
|
993
|
+
return new Response(source).text();
|
|
994
|
+
}
|
|
943
995
|
var TableRef = class {
|
|
944
996
|
_ctx;
|
|
945
997
|
_table;
|
|
@@ -968,22 +1020,34 @@ var TableRef = class {
|
|
|
968
1020
|
this._createStream
|
|
969
1021
|
);
|
|
970
1022
|
}
|
|
971
|
-
/**
|
|
1023
|
+
/**
|
|
1024
|
+
* Start a query that selects every column the caller's role is allowed to read
|
|
1025
|
+
* (the all-columns wildcard). Use instead of `.select(...)` for an explicit
|
|
1026
|
+
* full-row read; `.fetch()` does this implicitly.
|
|
1027
|
+
*/
|
|
1028
|
+
selectAll() {
|
|
1029
|
+
return this.select().selectAll();
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Insert one or more rows into this table.
|
|
1033
|
+
*
|
|
1034
|
+
* A single object is sent as a JSON `POST /v1/ingest`. An **array** is
|
|
1035
|
+
* serialized to NDJSON (one record per line) and sent as a single
|
|
1036
|
+
* `application/x-ndjson` request: a bad record no longer fails or hides the
|
|
1037
|
+
* rest of the batch — per-record outcomes come back in the result
|
|
1038
|
+
* (`failed` / `results`), and `ok` is true only when every record succeeded.
|
|
1039
|
+
*
|
|
1040
|
+
* The array path sends one request regardless of size; bounded-concurrency
|
|
1041
|
+
* chunking of very large arrays is tracked separately (#196). For NDJSON you
|
|
1042
|
+
* already have (a file or stream), use {@link insertNDJSON}.
|
|
1043
|
+
*/
|
|
972
1044
|
async insert(data, opts) {
|
|
973
1045
|
if (Array.isArray(data)) {
|
|
974
|
-
|
|
975
|
-
(
|
|
976
|
-
method: "POST",
|
|
977
|
-
path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
|
|
978
|
-
body: row,
|
|
979
|
-
signal: opts?.signal
|
|
980
|
-
})
|
|
981
|
-
);
|
|
982
|
-
const results = await Promise.all(promises);
|
|
983
|
-
for (const res2 of results) {
|
|
984
|
-
if (res2.error) return err(res2.error);
|
|
1046
|
+
if (data.length === 0) {
|
|
1047
|
+
return ok({ ok: true, total: 0, succeeded: 0, failed: 0, duplicates: 0, results: [] });
|
|
985
1048
|
}
|
|
986
|
-
|
|
1049
|
+
const ndjson = data.map((row) => JSON.stringify(row)).join("\n");
|
|
1050
|
+
return this._sendNDJSON(ndjson, opts);
|
|
987
1051
|
}
|
|
988
1052
|
const { data: res, error } = await request(this._ctx, {
|
|
989
1053
|
method: "POST",
|
|
@@ -996,6 +1060,46 @@ var TableRef = class {
|
|
|
996
1060
|
if (res?.duplicate != null) result.duplicate = res.duplicate;
|
|
997
1061
|
return ok(result);
|
|
998
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Insert pre-formatted NDJSON (newline-delimited JSON, one record per line)
|
|
1065
|
+
* from a string, raw bytes, a Blob/File, or a byte stream — e.g. a `.ndjson`
|
|
1066
|
+
* file or a stream produced by another system. For in-memory rows, prefer
|
|
1067
|
+
* {@link insert}.
|
|
1068
|
+
*
|
|
1069
|
+
* Non-string sources are read fully into memory before sending (the server
|
|
1070
|
+
* streams the parse). Returns the same per-record batch summary as an array
|
|
1071
|
+
* `insert`.
|
|
1072
|
+
*/
|
|
1073
|
+
async insertNDJSON(source, opts) {
|
|
1074
|
+
const ndjson = await ndjsonSourceToString(source);
|
|
1075
|
+
return this._sendNDJSON(ndjson, opts);
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* @internal Send an NDJSON body and map the server's batch response onto an
|
|
1079
|
+
* {@link InsertResult}. A transport / whole-request failure surfaces as the
|
|
1080
|
+
* Result error arm; a processed batch (even with rejected records) surfaces
|
|
1081
|
+
* as the success arm with `ok = failed === 0`.
|
|
1082
|
+
*/
|
|
1083
|
+
async _sendNDJSON(ndjson, opts) {
|
|
1084
|
+
const { data, error } = await request(this._ctx, {
|
|
1085
|
+
method: "POST",
|
|
1086
|
+
path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
|
|
1087
|
+
rawBody: ndjson,
|
|
1088
|
+
contentType: NDJSON_CONTENT_TYPE,
|
|
1089
|
+
signal: opts?.signal
|
|
1090
|
+
});
|
|
1091
|
+
if (error) return err(error);
|
|
1092
|
+
const r = data ?? { total: 0, succeeded: 0, failed: 0, duplicates: 0 };
|
|
1093
|
+
const result = {
|
|
1094
|
+
ok: r.failed === 0,
|
|
1095
|
+
total: r.total,
|
|
1096
|
+
succeeded: r.succeeded,
|
|
1097
|
+
failed: r.failed,
|
|
1098
|
+
duplicates: r.duplicates
|
|
1099
|
+
};
|
|
1100
|
+
if (r.results && r.results.length > 0) result.results = r.results;
|
|
1101
|
+
return ok(result);
|
|
1102
|
+
}
|
|
999
1103
|
/** Fetch the schema for this table. */
|
|
1000
1104
|
async schema(opts) {
|
|
1001
1105
|
const { data, error } = await request(this._ctx, {
|
|
@@ -1031,7 +1135,10 @@ var WaveHouseClient = class {
|
|
|
1031
1135
|
baseURL: config.baseURL.replace(/\/+$/, ""),
|
|
1032
1136
|
auth: config.auth,
|
|
1033
1137
|
options: {
|
|
1034
|
-
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
|
|
1035
1142
|
}
|
|
1036
1143
|
};
|
|
1037
1144
|
this.schema = new SchemaNamespace(this._ctx);
|