@wavehouse/sdk 0.0.0-dev.h825a5d7fa405 → 0.0.0-dev.h8ea15a023aa2
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 +39 -3
- package/dist/cli/codegen.js +158 -0
- package/dist/index.cjs +94 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -4
- package/dist/index.d.ts +109 -4
- package/dist/index.global.js +3 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +94 -18
- 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
|
|
@@ -102,13 +138,13 @@ if (error) {
|
|
|
102
138
|
|
|
103
139
|
## Codegen
|
|
104
140
|
|
|
105
|
-
Generate TypeScript types from your live WaveHouse schema
|
|
141
|
+
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
142
|
|
|
107
143
|
```bash
|
|
108
|
-
|
|
144
|
+
npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
|
|
109
145
|
```
|
|
110
146
|
|
|
111
|
-
This introspects `/v1/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`.
|
|
147
|
+
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
148
|
|
|
113
149
|
## Development & Testing
|
|
114
150
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/codegen.ts
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
const args = { url: "http://localhost:8080", out: "./wavehouse.d.ts" };
|
|
6
|
+
for (let i = 2; i < argv.length; i++) {
|
|
7
|
+
switch (argv[i]) {
|
|
8
|
+
case "--url":
|
|
9
|
+
case "-u":
|
|
10
|
+
args.url = argv[++i];
|
|
11
|
+
break;
|
|
12
|
+
case "--out":
|
|
13
|
+
case "-o":
|
|
14
|
+
args.out = argv[++i];
|
|
15
|
+
break;
|
|
16
|
+
case "--auth":
|
|
17
|
+
case "-a":
|
|
18
|
+
args.auth = argv[++i];
|
|
19
|
+
break;
|
|
20
|
+
case "--help":
|
|
21
|
+
case "-h":
|
|
22
|
+
console.log(`wavehouse codegen \u2014 Generate TypeScript types from WaveHouse schema
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--url, -u WaveHouse base URL (default: http://localhost:8080)
|
|
26
|
+
--out, -o Output file path (default: ./wavehouse.d.ts)
|
|
27
|
+
--auth, -a Bearer token for authenticated endpoints
|
|
28
|
+
--help, -h Show this help`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return args;
|
|
33
|
+
}
|
|
34
|
+
function chTypeToTS(chType) {
|
|
35
|
+
if (chType.startsWith("Nullable(") && chType.endsWith(")")) {
|
|
36
|
+
const inner = chType.slice(9, -1);
|
|
37
|
+
return `${chTypeToTS(inner)} | null`;
|
|
38
|
+
}
|
|
39
|
+
if (chType.startsWith("LowCardinality(") && chType.endsWith(")")) {
|
|
40
|
+
return chTypeToTS(chType.slice(15, -1));
|
|
41
|
+
}
|
|
42
|
+
if (chType === "String" || chType.startsWith("FixedString(") || chType === "UUID" || chType.startsWith("DateTime") || chType.startsWith("Date") || chType.startsWith("Enum8(") || chType.startsWith("Enum16(") || chType === "IPv4" || chType === "IPv6") {
|
|
43
|
+
return "string";
|
|
44
|
+
}
|
|
45
|
+
if (chType === "Bool") return "boolean";
|
|
46
|
+
if (isNumeric(chType)) return "number";
|
|
47
|
+
if (chType.startsWith("Array(") && chType.endsWith(")")) {
|
|
48
|
+
const inner = chType.slice(6, -1);
|
|
49
|
+
return `${chTypeToTS(inner)}[]`;
|
|
50
|
+
}
|
|
51
|
+
if (chType.startsWith("Map(") && chType.endsWith(")")) {
|
|
52
|
+
const inner = chType.slice(4, -1);
|
|
53
|
+
const comma = findTopLevelComma(inner);
|
|
54
|
+
if (comma !== -1) {
|
|
55
|
+
const keyType = chTypeToTS(inner.slice(0, comma).trim());
|
|
56
|
+
const valType = chTypeToTS(inner.slice(comma + 1).trim());
|
|
57
|
+
return `Record<${keyType}, ${valType}>`;
|
|
58
|
+
}
|
|
59
|
+
return "Record<string, unknown>";
|
|
60
|
+
}
|
|
61
|
+
if (chType.startsWith("Tuple(")) {
|
|
62
|
+
return "unknown[]";
|
|
63
|
+
}
|
|
64
|
+
return "unknown";
|
|
65
|
+
}
|
|
66
|
+
function isNumeric(t) {
|
|
67
|
+
const numericPrefixes = [
|
|
68
|
+
"UInt8",
|
|
69
|
+
"UInt16",
|
|
70
|
+
"UInt32",
|
|
71
|
+
"UInt64",
|
|
72
|
+
"UInt128",
|
|
73
|
+
"UInt256",
|
|
74
|
+
"Int8",
|
|
75
|
+
"Int16",
|
|
76
|
+
"Int32",
|
|
77
|
+
"Int64",
|
|
78
|
+
"Int128",
|
|
79
|
+
"Int256",
|
|
80
|
+
"Float32",
|
|
81
|
+
"Float64",
|
|
82
|
+
"Decimal"
|
|
83
|
+
];
|
|
84
|
+
return numericPrefixes.some((p) => t === p || t.startsWith(`${p}(`));
|
|
85
|
+
}
|
|
86
|
+
function findTopLevelComma(s) {
|
|
87
|
+
let depth = 0;
|
|
88
|
+
for (let i = 0; i < s.length; i++) {
|
|
89
|
+
if (s[i] === "(") depth++;
|
|
90
|
+
else if (s[i] === ")") depth--;
|
|
91
|
+
else if (s[i] === "," && depth === 0) return i;
|
|
92
|
+
}
|
|
93
|
+
return -1;
|
|
94
|
+
}
|
|
95
|
+
async function fetchSchemas(url, auth) {
|
|
96
|
+
const headers = {};
|
|
97
|
+
if (auth) headers.Authorization = `Bearer ${auth}`;
|
|
98
|
+
const res = await fetch(`${url.replace(/\/+$/, "")}/v1/schema`, { headers });
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const text = await res.text();
|
|
101
|
+
throw new Error(`Schema fetch failed (${res.status}): ${text}`);
|
|
102
|
+
}
|
|
103
|
+
return await res.json();
|
|
104
|
+
}
|
|
105
|
+
function generateTypes(schemas) {
|
|
106
|
+
const lines = [
|
|
107
|
+
"// Auto-generated by @wavehouse/sdk codegen",
|
|
108
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
109
|
+
"// Do not edit manually \u2014 re-run: npm run codegen",
|
|
110
|
+
"",
|
|
111
|
+
"export interface Database {"
|
|
112
|
+
];
|
|
113
|
+
const tableNames = Object.keys(schemas).sort();
|
|
114
|
+
for (const tableName of tableNames) {
|
|
115
|
+
const _schema = schemas[tableName];
|
|
116
|
+
const rowType = `${pascalCase(tableName)}Row`;
|
|
117
|
+
lines.push(` ${tableName}: ${rowType};`);
|
|
118
|
+
}
|
|
119
|
+
lines.push("}");
|
|
120
|
+
lines.push("");
|
|
121
|
+
for (const tableName of tableNames) {
|
|
122
|
+
const schema = schemas[tableName];
|
|
123
|
+
const rowType = `${pascalCase(tableName)}Row`;
|
|
124
|
+
lines.push(`export interface ${rowType} {`);
|
|
125
|
+
for (const col of schema.columns) {
|
|
126
|
+
const tsType = chTypeToTS(col.type);
|
|
127
|
+
const optional = col.has_default ? "?" : "";
|
|
128
|
+
lines.push(` ${col.name}${optional}: ${tsType};`);
|
|
129
|
+
}
|
|
130
|
+
lines.push("}");
|
|
131
|
+
lines.push("");
|
|
132
|
+
}
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
function pascalCase(s) {
|
|
136
|
+
return s.split(/[_\-\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
137
|
+
}
|
|
138
|
+
async function main() {
|
|
139
|
+
const args = parseArgs(process.argv);
|
|
140
|
+
console.log(`Fetching schema from ${args.url}...`);
|
|
141
|
+
const schemas = await fetchSchemas(args.url, args.auth);
|
|
142
|
+
const tableCount = Object.keys(schemas).length;
|
|
143
|
+
if (tableCount === 0) {
|
|
144
|
+
console.warn("No tables found. Is WaveHouse running with tables in ClickHouse?");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
console.log(`Found ${tableCount} table(s): ${Object.keys(schemas).join(", ")}`);
|
|
148
|
+
const output = generateTypes(schemas);
|
|
149
|
+
const { writeFile } = await import("fs/promises");
|
|
150
|
+
const { resolve } = await import("path");
|
|
151
|
+
const outPath = resolve(args.out);
|
|
152
|
+
await writeFile(outPath, output, "utf-8");
|
|
153
|
+
console.log(`\u2713 Types written to ${outPath}`);
|
|
154
|
+
}
|
|
155
|
+
main().catch((err) => {
|
|
156
|
+
console.error("Codegen failed:", err.message);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|
package/dist/index.cjs
CHANGED
|
@@ -76,9 +76,10 @@ function err(error) {
|
|
|
76
76
|
async function request(ctx, opts) {
|
|
77
77
|
const url = buildURL(ctx.baseURL, opts.path, opts.params);
|
|
78
78
|
const headers = {
|
|
79
|
-
"Content-Type": "application/json",
|
|
79
|
+
"Content-Type": opts.contentType ?? "application/json",
|
|
80
80
|
Accept: "application/json"
|
|
81
81
|
};
|
|
82
|
+
const requestBody = opts.rawBody !== void 0 ? opts.rawBody : opts.body !== void 0 ? JSON.stringify(opts.body) : void 0;
|
|
82
83
|
if (ctx.auth) {
|
|
83
84
|
const token = await ctx.auth();
|
|
84
85
|
if (token) {
|
|
@@ -92,7 +93,7 @@ async function request(ctx, opts) {
|
|
|
92
93
|
const res = await fetch(url, {
|
|
93
94
|
method: opts.method,
|
|
94
95
|
headers,
|
|
95
|
-
body:
|
|
96
|
+
body: requestBody,
|
|
96
97
|
signal: opts.signal
|
|
97
98
|
});
|
|
98
99
|
if (res.ok) {
|
|
@@ -727,6 +728,15 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
727
728
|
select(...columns) {
|
|
728
729
|
return this._clone({ columns: [...this._state.columns, ...columns] });
|
|
729
730
|
}
|
|
731
|
+
/**
|
|
732
|
+
* Select every column the caller's role is allowed to read (the all-columns
|
|
733
|
+
* wildcard). Use this instead of `.select(...)` when you want a full-row read;
|
|
734
|
+
* a bare `.fetch()` with no `.select()` does this implicitly. Mutually
|
|
735
|
+
* exclusive with `.select(...)`.
|
|
736
|
+
*/
|
|
737
|
+
selectAll() {
|
|
738
|
+
return this._clone({ selectAll: true });
|
|
739
|
+
}
|
|
730
740
|
where(column, op, value) {
|
|
731
741
|
const filter = { column, op: OP_MAP[op], value };
|
|
732
742
|
return this._clone({ filters: [...this._state.filters, filter] });
|
|
@@ -768,7 +778,7 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
768
778
|
return this._clone({ cacheTTL: seconds });
|
|
769
779
|
}
|
|
770
780
|
// --- Execution ---
|
|
771
|
-
/** Default row limit when none is specified
|
|
781
|
+
/** Default row limit when none is specified — deliberately tighter than the backend's DefaultMaxRows (10000) safety cap. */
|
|
772
782
|
static DEFAULT_LIMIT = 1e3;
|
|
773
783
|
async fetch(opts) {
|
|
774
784
|
const effectiveLimit = opts?.limit ?? this._state.limit ?? _QueryBuilder.DEFAULT_LIMIT;
|
|
@@ -825,8 +835,16 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
825
835
|
}
|
|
826
836
|
_buildAST(effectiveLimit) {
|
|
827
837
|
const ast = {};
|
|
828
|
-
|
|
829
|
-
|
|
838
|
+
const hasColumns = this._state.columns.length > 0;
|
|
839
|
+
const hasAggs = this._state.aggregations.length > 0;
|
|
840
|
+
if (this._state.selectAll) {
|
|
841
|
+
ast.select_all = true;
|
|
842
|
+
} else if (hasColumns) {
|
|
843
|
+
ast.columns = this._state.columns;
|
|
844
|
+
} else if (!hasAggs) {
|
|
845
|
+
ast.select_all = true;
|
|
846
|
+
}
|
|
847
|
+
if (hasAggs) ast.aggregations = this._state.aggregations;
|
|
830
848
|
if (this._state.filters.length > 0) ast.filters = this._state.filters;
|
|
831
849
|
if (this._state.groupBy.length > 0) ast.group_by = this._state.groupBy;
|
|
832
850
|
if (this._state.orderBy.length > 0) ast.order_by = this._state.orderBy;
|
|
@@ -936,6 +954,12 @@ function projectColumns(row, columns) {
|
|
|
936
954
|
}
|
|
937
955
|
|
|
938
956
|
// src/table.ts
|
|
957
|
+
var NDJSON_CONTENT_TYPE = "application/x-ndjson";
|
|
958
|
+
async function ndjsonSourceToString(source) {
|
|
959
|
+
if (typeof source === "string") return source;
|
|
960
|
+
if (source instanceof Uint8Array) return new TextDecoder().decode(source);
|
|
961
|
+
return new Response(source).text();
|
|
962
|
+
}
|
|
939
963
|
var TableRef = class {
|
|
940
964
|
_ctx;
|
|
941
965
|
_table;
|
|
@@ -964,22 +988,34 @@ var TableRef = class {
|
|
|
964
988
|
this._createStream
|
|
965
989
|
);
|
|
966
990
|
}
|
|
967
|
-
/**
|
|
991
|
+
/**
|
|
992
|
+
* Start a query that selects every column the caller's role is allowed to read
|
|
993
|
+
* (the all-columns wildcard). Use instead of `.select(...)` for an explicit
|
|
994
|
+
* full-row read; `.fetch()` does this implicitly.
|
|
995
|
+
*/
|
|
996
|
+
selectAll() {
|
|
997
|
+
return this.select().selectAll();
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Insert one or more rows into this table.
|
|
1001
|
+
*
|
|
1002
|
+
* A single object is sent as a JSON `POST /v1/ingest`. An **array** is
|
|
1003
|
+
* serialized to NDJSON (one record per line) and sent as a single
|
|
1004
|
+
* `application/x-ndjson` request: a bad record no longer fails or hides the
|
|
1005
|
+
* rest of the batch — per-record outcomes come back in the result
|
|
1006
|
+
* (`failed` / `results`), and `ok` is true only when every record succeeded.
|
|
1007
|
+
*
|
|
1008
|
+
* The array path sends one request regardless of size; bounded-concurrency
|
|
1009
|
+
* chunking of very large arrays is tracked separately (#196). For NDJSON you
|
|
1010
|
+
* already have (a file or stream), use {@link insertNDJSON}.
|
|
1011
|
+
*/
|
|
968
1012
|
async insert(data, opts) {
|
|
969
1013
|
if (Array.isArray(data)) {
|
|
970
|
-
|
|
971
|
-
(
|
|
972
|
-
method: "POST",
|
|
973
|
-
path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
|
|
974
|
-
body: row,
|
|
975
|
-
signal: opts?.signal
|
|
976
|
-
})
|
|
977
|
-
);
|
|
978
|
-
const results = await Promise.all(promises);
|
|
979
|
-
for (const res2 of results) {
|
|
980
|
-
if (res2.error) return err(res2.error);
|
|
1014
|
+
if (data.length === 0) {
|
|
1015
|
+
return ok({ ok: true, total: 0, succeeded: 0, failed: 0, duplicates: 0, results: [] });
|
|
981
1016
|
}
|
|
982
|
-
|
|
1017
|
+
const ndjson = data.map((row) => JSON.stringify(row)).join("\n");
|
|
1018
|
+
return this._sendNDJSON(ndjson, opts);
|
|
983
1019
|
}
|
|
984
1020
|
const { data: res, error } = await request(this._ctx, {
|
|
985
1021
|
method: "POST",
|
|
@@ -992,6 +1028,46 @@ var TableRef = class {
|
|
|
992
1028
|
if (res?.duplicate != null) result.duplicate = res.duplicate;
|
|
993
1029
|
return ok(result);
|
|
994
1030
|
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Insert pre-formatted NDJSON (newline-delimited JSON, one record per line)
|
|
1033
|
+
* from a string, raw bytes, a Blob/File, or a byte stream — e.g. a `.ndjson`
|
|
1034
|
+
* file or a stream produced by another system. For in-memory rows, prefer
|
|
1035
|
+
* {@link insert}.
|
|
1036
|
+
*
|
|
1037
|
+
* Non-string sources are read fully into memory before sending (the server
|
|
1038
|
+
* streams the parse). Returns the same per-record batch summary as an array
|
|
1039
|
+
* `insert`.
|
|
1040
|
+
*/
|
|
1041
|
+
async insertNDJSON(source, opts) {
|
|
1042
|
+
const ndjson = await ndjsonSourceToString(source);
|
|
1043
|
+
return this._sendNDJSON(ndjson, opts);
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* @internal Send an NDJSON body and map the server's batch response onto an
|
|
1047
|
+
* {@link InsertResult}. A transport / whole-request failure surfaces as the
|
|
1048
|
+
* Result error arm; a processed batch (even with rejected records) surfaces
|
|
1049
|
+
* as the success arm with `ok = failed === 0`.
|
|
1050
|
+
*/
|
|
1051
|
+
async _sendNDJSON(ndjson, opts) {
|
|
1052
|
+
const { data, error } = await request(this._ctx, {
|
|
1053
|
+
method: "POST",
|
|
1054
|
+
path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
|
|
1055
|
+
rawBody: ndjson,
|
|
1056
|
+
contentType: NDJSON_CONTENT_TYPE,
|
|
1057
|
+
signal: opts?.signal
|
|
1058
|
+
});
|
|
1059
|
+
if (error) return err(error);
|
|
1060
|
+
const r = data ?? { total: 0, succeeded: 0, failed: 0, duplicates: 0 };
|
|
1061
|
+
const result = {
|
|
1062
|
+
ok: r.failed === 0,
|
|
1063
|
+
total: r.total,
|
|
1064
|
+
succeeded: r.succeeded,
|
|
1065
|
+
failed: r.failed,
|
|
1066
|
+
duplicates: r.duplicates
|
|
1067
|
+
};
|
|
1068
|
+
if (r.results && r.results.length > 0) result.results = r.results;
|
|
1069
|
+
return ok(result);
|
|
1070
|
+
}
|
|
995
1071
|
/** Fetch the schema for this table. */
|
|
996
1072
|
async schema(opts) {
|
|
997
1073
|
const { data, error } = await request(this._ctx, {
|