@wavehouse/sdk 0.0.0-dev.h825a5d7fa405 → 0.0.0-dev.h9539004107f0
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 +47 -5
- package/dist/cli/codegen.js +202 -0
- package/dist/index.cjs +518 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +287 -11
- package/dist/index.d.ts +287 -11
- package/dist/index.global.js +12 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +518 -107
- package/dist/index.js.map +1 -1
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @wavehouse/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript client for [WaveHouse](https://github.com/Wave-RF/WaveHouse) — schema-aware real-time API gateway for ClickHouse. One runtime dependency (`eventsource-parser`, ~1.4 KB gzipped, itself dependency-free).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -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. Neither browsers nor Node need a polyfill: streaming runs on `fetch`, like the rest of the SDK — 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()`) runs on `fetch`, 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 a terminal `SSE_CONNECT_ERROR`), `.stream()` / `.liveQuery()` in a runtime with no global `fetch` and no `options.fetch`, and an `auth` callback that rejects — a token-refresh failure propagates out of the REST call, and on a stream is reported as a retryable `SSE_AUTH_ERROR`. A dropped stream *is* re-dialed by the SDK, on a jittered backoff; only non-retryable failures end it. One more exception can escape an SDK call, though it is yours rather than ours: a subscriber's `status` handler throwing on the first, synchronous `.subscribe()` / `.liveQuery()` call — see [Error Handling](https://wavehouse.dev/sdk/reference#error-handling).
|
|
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/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.
|
|
112
154
|
|
|
113
155
|
## Development & Testing
|
|
114
156
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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
|
+
|
|
22
|
+
// src/cli/codegen.ts
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const args = { url: "http://localhost:8080", out: "./wavehouse.d.ts" };
|
|
25
|
+
for (let i = 2; i < argv.length; i++) {
|
|
26
|
+
switch (argv[i]) {
|
|
27
|
+
case "--url":
|
|
28
|
+
case "-u":
|
|
29
|
+
args.url = argv[++i];
|
|
30
|
+
break;
|
|
31
|
+
case "--out":
|
|
32
|
+
case "-o":
|
|
33
|
+
args.out = argv[++i];
|
|
34
|
+
break;
|
|
35
|
+
case "--auth":
|
|
36
|
+
case "-a":
|
|
37
|
+
args.auth = argv[++i];
|
|
38
|
+
break;
|
|
39
|
+
case "--help":
|
|
40
|
+
case "-h":
|
|
41
|
+
console.log(`wavehouse codegen \u2014 Generate TypeScript types from WaveHouse schema
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--url, -u WaveHouse base URL (default: http://localhost:8080)
|
|
45
|
+
--out, -o Output file path (default: ./wavehouse.d.ts)
|
|
46
|
+
--auth, -a Bearer token for authenticated endpoints
|
|
47
|
+
--help, -h Show this help`);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return args;
|
|
52
|
+
}
|
|
53
|
+
function chTypeToTS(chType) {
|
|
54
|
+
if (chType.startsWith("Nullable(") && chType.endsWith(")")) {
|
|
55
|
+
const inner = chType.slice(9, -1);
|
|
56
|
+
return `${chTypeToTS(inner)} | null`;
|
|
57
|
+
}
|
|
58
|
+
if (chType.startsWith("LowCardinality(") && chType.endsWith(")")) {
|
|
59
|
+
return chTypeToTS(chType.slice(15, -1));
|
|
60
|
+
}
|
|
61
|
+
if (chType === "String" || chType.startsWith("FixedString(") || chType === "UUID" || chType.startsWith("DateTime") || chType.startsWith("Date") || chType.startsWith("Enum8(") || chType.startsWith("Enum16(") || chType === "IPv4" || chType === "IPv6") {
|
|
62
|
+
return "string";
|
|
63
|
+
}
|
|
64
|
+
if (chType === "Bool") return "boolean";
|
|
65
|
+
if (isNumeric(chType)) return "number";
|
|
66
|
+
if (chType.startsWith("Array(") && chType.endsWith(")")) {
|
|
67
|
+
const inner = chType.slice(6, -1);
|
|
68
|
+
return `${chTypeToTS(inner)}[]`;
|
|
69
|
+
}
|
|
70
|
+
if (chType.startsWith("Map(") && chType.endsWith(")")) {
|
|
71
|
+
const inner = chType.slice(4, -1);
|
|
72
|
+
const comma = findTopLevelComma(inner);
|
|
73
|
+
if (comma !== -1) {
|
|
74
|
+
const keyType = chTypeToTS(inner.slice(0, comma).trim());
|
|
75
|
+
const valType = chTypeToTS(inner.slice(comma + 1).trim());
|
|
76
|
+
return `Record<${keyType}, ${valType}>`;
|
|
77
|
+
}
|
|
78
|
+
return "Record<string, unknown>";
|
|
79
|
+
}
|
|
80
|
+
if (chType.startsWith("Tuple(")) {
|
|
81
|
+
return "unknown[]";
|
|
82
|
+
}
|
|
83
|
+
return "unknown";
|
|
84
|
+
}
|
|
85
|
+
function isNumeric(t) {
|
|
86
|
+
const numericPrefixes = [
|
|
87
|
+
"UInt8",
|
|
88
|
+
"UInt16",
|
|
89
|
+
"UInt32",
|
|
90
|
+
"UInt64",
|
|
91
|
+
"UInt128",
|
|
92
|
+
"UInt256",
|
|
93
|
+
"Int8",
|
|
94
|
+
"Int16",
|
|
95
|
+
"Int32",
|
|
96
|
+
"Int64",
|
|
97
|
+
"Int128",
|
|
98
|
+
"Int256",
|
|
99
|
+
"Float32",
|
|
100
|
+
"Float64",
|
|
101
|
+
"Decimal"
|
|
102
|
+
];
|
|
103
|
+
return numericPrefixes.some((p) => t === p || t.startsWith(`${p}(`));
|
|
104
|
+
}
|
|
105
|
+
function findTopLevelComma(s) {
|
|
106
|
+
let depth = 0;
|
|
107
|
+
for (let i = 0; i < s.length; i++) {
|
|
108
|
+
if (s[i] === "(") depth++;
|
|
109
|
+
else if (s[i] === ")") depth--;
|
|
110
|
+
else if (s[i] === "," && depth === 0) return i;
|
|
111
|
+
}
|
|
112
|
+
return -1;
|
|
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
|
+
}
|
|
124
|
+
async function fetchSchemas(url, auth) {
|
|
125
|
+
const headers = {};
|
|
126
|
+
if (auth) headers.Authorization = `Bearer ${auth}`;
|
|
127
|
+
const res = await fetch(resolveURL(url, "/v1/ops/schema").toString(), { headers });
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const text = await res.text();
|
|
130
|
+
throw new Error(`Schema fetch failed (${res.status}): ${text}`);
|
|
131
|
+
}
|
|
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;
|
|
137
|
+
}
|
|
138
|
+
function generateTypes(schemas) {
|
|
139
|
+
const lines = [
|
|
140
|
+
"// Auto-generated by @wavehouse/sdk codegen",
|
|
141
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
142
|
+
"// Do not edit manually \u2014 re-run: npm run codegen",
|
|
143
|
+
"",
|
|
144
|
+
"export interface Database {"
|
|
145
|
+
];
|
|
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;`);
|
|
149
|
+
}
|
|
150
|
+
lines.push("}");
|
|
151
|
+
lines.push("");
|
|
152
|
+
for (const table of tables) {
|
|
153
|
+
const rowType = `${pascalCase(table.name)}Row`;
|
|
154
|
+
lines.push(`export interface ${rowType} {`);
|
|
155
|
+
for (const col of table.columns) {
|
|
156
|
+
const tsType = chTypeToTS(col.type);
|
|
157
|
+
const optional = col.has_default ? "?" : "";
|
|
158
|
+
lines.push(` ${col.name}${optional}: ${tsType};`);
|
|
159
|
+
}
|
|
160
|
+
lines.push("}");
|
|
161
|
+
lines.push("");
|
|
162
|
+
}
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
function pascalCase(s) {
|
|
166
|
+
return s.split(/[_\-\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
167
|
+
}
|
|
168
|
+
async function main() {
|
|
169
|
+
const args = parseArgs(process.argv);
|
|
170
|
+
console.log(`Fetching schema from ${args.url}...`);
|
|
171
|
+
const schemas = await fetchSchemas(args.url, args.auth);
|
|
172
|
+
if (schemas.length === 0) {
|
|
173
|
+
console.warn("No tables found. Is WaveHouse running with tables in ClickHouse?");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
console.log(`Found ${schemas.length} table(s): ${schemas.map((t) => t.name).join(", ")}`);
|
|
177
|
+
const output = generateTypes(schemas);
|
|
178
|
+
const { writeFile } = await import("fs/promises");
|
|
179
|
+
const { resolve } = await import("path");
|
|
180
|
+
const outPath = resolve(args.out);
|
|
181
|
+
await writeFile(outPath, output, "utf-8");
|
|
182
|
+
console.log(`\u2713 Types written to ${outPath}`);
|
|
183
|
+
}
|
|
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
|
+
};
|