@wavehouse/sdk 0.0.0-dev.h97a11a9f5a0e → 0.0.0-dev.hb0c3a577b2f8
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 +44 -4
- package/dist/cli/codegen.js +173 -0
- package/dist/index.cjs +45 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -4
- package/dist/index.d.ts +58 -4
- package/dist/index.global.js +3 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +45 -14
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -8,6 +8,42 @@ Zero-dependency TypeScript client for [WaveHouse](https://github.com/Wave-RF/Wav
|
|
|
8
8
|
npm install @wavehouse/sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
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
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Use without a build step (CDN)
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
**ES module (recommended).** Modern browsers run `type="module"` natively:
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<script type="module">
|
|
23
|
+
import { createClient } from 'https://esm.sh/@wavehouse/sdk';
|
|
24
|
+
|
|
25
|
+
const wh = createClient({ baseURL: 'https://your-wavehouse.example.com' });
|
|
26
|
+
const { data, error } = await wh.from('clicks').select('page').limit(10);
|
|
27
|
+
console.log(data ?? error);
|
|
28
|
+
</script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
**Classic global (`<script src>`).** For pages that can't use ES modules, the bundled IIFE build attaches everything to a `WaveHouse` global:
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<script src="https://cdn.jsdelivr.net/npm/@wavehouse/sdk"></script>
|
|
37
|
+
<script>
|
|
38
|
+
const wh = WaveHouse.createClient({ baseURL: 'https://your-wavehouse.example.com' });
|
|
39
|
+
wh.from('clicks').select('page').limit(10).then(({ data }) => console.log(data));
|
|
40
|
+
</script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**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.
|
|
44
|
+
|
|
45
|
+
Streaming (`.stream()`) uses the browser's native `EventSource`, so it works in both forms with no polyfill.
|
|
46
|
+
|
|
11
47
|
## Quick Start
|
|
12
48
|
|
|
13
49
|
```ts
|
|
@@ -19,6 +55,8 @@ const wh = createClient({
|
|
|
19
55
|
});
|
|
20
56
|
```
|
|
21
57
|
|
|
58
|
+
`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).
|
|
59
|
+
|
|
22
60
|
### Query Data
|
|
23
61
|
|
|
24
62
|
```ts
|
|
@@ -89,7 +127,9 @@ const { data } = await wh.from('clicks').select('page').limit(10);
|
|
|
89
127
|
|
|
90
128
|
## Error Handling
|
|
91
129
|
|
|
92
|
-
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
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
133
|
|
|
94
134
|
```ts
|
|
95
135
|
const { data, error } = await wh.from('clicks').fetch();
|
|
@@ -102,13 +142,13 @@ if (error) {
|
|
|
102
142
|
|
|
103
143
|
## Codegen
|
|
104
144
|
|
|
105
|
-
Generate TypeScript types from your live WaveHouse schema
|
|
145
|
+
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
146
|
|
|
107
147
|
```bash
|
|
108
|
-
|
|
148
|
+
npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
|
|
109
149
|
```
|
|
110
150
|
|
|
111
|
-
This introspects `/v1/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`.
|
|
151
|
+
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
152
|
|
|
113
153
|
## Development & Testing
|
|
114
154
|
|
|
@@ -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,9 +72,24 @@ 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
|
|
76
91
|
async function request(ctx, opts) {
|
|
77
|
-
const url =
|
|
92
|
+
const url = resolveURL(ctx.baseURL, opts.path, opts.params).toString();
|
|
78
93
|
const headers = {
|
|
79
94
|
"Content-Type": opts.contentType ?? "application/json",
|
|
80
95
|
Accept: "application/json"
|
|
@@ -133,15 +148,6 @@ async function request(ctx, opts) {
|
|
|
133
148
|
}
|
|
134
149
|
return { data: null, error: lastError, headers: new Headers() };
|
|
135
150
|
}
|
|
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
151
|
function backoff(attempt) {
|
|
146
152
|
return Math.min(1e3 * 2 ** attempt, 3e4);
|
|
147
153
|
}
|
|
@@ -558,7 +564,7 @@ var SSETransport = class {
|
|
|
558
564
|
this.onStatus?.("closed");
|
|
559
565
|
}
|
|
560
566
|
async _doConnect() {
|
|
561
|
-
const url =
|
|
567
|
+
const url = resolveURL(this._opts.baseURL, "/v1/stream");
|
|
562
568
|
url.searchParams.set("table", this._opts.table);
|
|
563
569
|
if (this._opts.since) {
|
|
564
570
|
url.searchParams.set("since", this._opts.since);
|
|
@@ -728,6 +734,15 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
728
734
|
select(...columns) {
|
|
729
735
|
return this._clone({ columns: [...this._state.columns, ...columns] });
|
|
730
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* Select every column the caller's role is allowed to read (the all-columns
|
|
739
|
+
* wildcard). Use this instead of `.select(...)` when you want a full-row read;
|
|
740
|
+
* a bare `.fetch()` with no `.select()` does this implicitly. Mutually
|
|
741
|
+
* exclusive with `.select(...)`.
|
|
742
|
+
*/
|
|
743
|
+
selectAll() {
|
|
744
|
+
return this._clone({ selectAll: true });
|
|
745
|
+
}
|
|
731
746
|
where(column, op, value) {
|
|
732
747
|
const filter = { column, op: OP_MAP[op], value };
|
|
733
748
|
return this._clone({ filters: [...this._state.filters, filter] });
|
|
@@ -769,7 +784,7 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
769
784
|
return this._clone({ cacheTTL: seconds });
|
|
770
785
|
}
|
|
771
786
|
// --- Execution ---
|
|
772
|
-
/** Default row limit when none is specified
|
|
787
|
+
/** Default row limit when none is specified — deliberately tighter than the backend's DefaultMaxRows (10000) safety cap. */
|
|
773
788
|
static DEFAULT_LIMIT = 1e3;
|
|
774
789
|
async fetch(opts) {
|
|
775
790
|
const effectiveLimit = opts?.limit ?? this._state.limit ?? _QueryBuilder.DEFAULT_LIMIT;
|
|
@@ -826,8 +841,16 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
826
841
|
}
|
|
827
842
|
_buildAST(effectiveLimit) {
|
|
828
843
|
const ast = {};
|
|
829
|
-
|
|
830
|
-
|
|
844
|
+
const hasColumns = this._state.columns.length > 0;
|
|
845
|
+
const hasAggs = this._state.aggregations.length > 0;
|
|
846
|
+
if (this._state.selectAll) {
|
|
847
|
+
ast.select_all = true;
|
|
848
|
+
} else if (hasColumns) {
|
|
849
|
+
ast.columns = this._state.columns;
|
|
850
|
+
} else if (!hasAggs) {
|
|
851
|
+
ast.select_all = true;
|
|
852
|
+
}
|
|
853
|
+
if (hasAggs) ast.aggregations = this._state.aggregations;
|
|
831
854
|
if (this._state.filters.length > 0) ast.filters = this._state.filters;
|
|
832
855
|
if (this._state.groupBy.length > 0) ast.group_by = this._state.groupBy;
|
|
833
856
|
if (this._state.orderBy.length > 0) ast.order_by = this._state.orderBy;
|
|
@@ -971,6 +994,14 @@ var TableRef = class {
|
|
|
971
994
|
this._createStream
|
|
972
995
|
);
|
|
973
996
|
}
|
|
997
|
+
/**
|
|
998
|
+
* Start a query that selects every column the caller's role is allowed to read
|
|
999
|
+
* (the all-columns wildcard). Use instead of `.select(...)` for an explicit
|
|
1000
|
+
* full-row read; `.fetch()` does this implicitly.
|
|
1001
|
+
*/
|
|
1002
|
+
selectAll() {
|
|
1003
|
+
return this.select().selectAll();
|
|
1004
|
+
}
|
|
974
1005
|
/**
|
|
975
1006
|
* Insert one or more rows into this table.
|
|
976
1007
|
*
|