@wavehouse/sdk 0.0.0-dev.0f8826c → 0.0.0-dev.h514be5522d89
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 -6
- package/dist/cli/codegen.js +158 -0
- package/dist/index.cjs +101 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +94 -3
- package/dist/index.d.ts +94 -3
- package/dist/index.global.js +3 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +101 -29
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -8,6 +8,40 @@ 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
|
+
## Use without a build step (CDN)
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
**ES module (recommended).** Modern browsers run `type="module"` natively:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<script type="module">
|
|
21
|
+
import { createClient } from 'https://esm.sh/@wavehouse/sdk';
|
|
22
|
+
|
|
23
|
+
const wh = createClient({ baseURL: 'https://your-wavehouse.example.com' });
|
|
24
|
+
const { data, error } = await wh.from('clicks').select('page').limit(10);
|
|
25
|
+
console.log(data ?? error);
|
|
26
|
+
</script>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
**Classic global (`<script src>`).** For pages that can't use ES modules, the bundled IIFE build attaches everything to a `WaveHouse` global:
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<script src="https://cdn.jsdelivr.net/npm/@wavehouse/sdk"></script>
|
|
35
|
+
<script>
|
|
36
|
+
const wh = WaveHouse.createClient({ baseURL: 'https://your-wavehouse.example.com' });
|
|
37
|
+
wh.from('clicks').select('page').limit(10).then(({ data }) => console.log(data));
|
|
38
|
+
</script>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**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.
|
|
42
|
+
|
|
43
|
+
Streaming (`.stream()`) uses the browser's native `EventSource`, so it works in both forms with no polyfill.
|
|
44
|
+
|
|
11
45
|
## Quick Start
|
|
12
46
|
|
|
13
47
|
```ts
|
|
@@ -102,13 +136,13 @@ if (error) {
|
|
|
102
136
|
|
|
103
137
|
## Codegen
|
|
104
138
|
|
|
105
|
-
Generate TypeScript types from your live WaveHouse schema
|
|
139
|
+
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
140
|
|
|
107
141
|
```bash
|
|
108
|
-
|
|
142
|
+
npx wavehouse-codegen --url http://localhost:8080 --out ./src/db.d.ts
|
|
109
143
|
```
|
|
110
144
|
|
|
111
|
-
This introspects `/v1/schema`, maps ClickHouse column types to TypeScript, and outputs a `Database` interface you can pass to `createClient<Database>()`.
|
|
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.
|
|
112
146
|
|
|
113
147
|
## Development & Testing
|
|
114
148
|
|
|
@@ -124,13 +158,12 @@ npm run test:coverage # coverage report
|
|
|
124
158
|
|
|
125
159
|
### E2E Integration Tests
|
|
126
160
|
|
|
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](
|
|
161
|
+
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
162
|
|
|
129
163
|
## API Reference
|
|
130
164
|
|
|
131
|
-
See the full [SDK API Reference](
|
|
165
|
+
See the full [SDK API Reference](https://wavehouse.dev/sdk) for detailed documentation of every method, type, and option.
|
|
132
166
|
|
|
133
167
|
## License
|
|
134
168
|
|
|
135
169
|
Apache-2.0 © Wave RF — see [LICENSE](./LICENSE).
|
|
136
|
-
|
|
@@ -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;
|
|
@@ -782,11 +792,11 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
782
792
|
if (error) return err(error);
|
|
783
793
|
const rows = data;
|
|
784
794
|
const hasMore = effectiveLimit != null && rows.length >= effectiveLimit;
|
|
785
|
-
if (hasMore) {
|
|
795
|
+
if (hasMore && this._state.orderBy.length > 0) {
|
|
786
796
|
const nextFn = () => this._fetchNext(rows, effectiveLimit, opts);
|
|
787
797
|
return okPage(rows, true, nextFn);
|
|
788
798
|
}
|
|
789
|
-
return okPage(rows,
|
|
799
|
+
return okPage(rows, hasMore);
|
|
790
800
|
}
|
|
791
801
|
stream(opts) {
|
|
792
802
|
const raw = this._createStream(this._state.table, opts);
|
|
@@ -825,31 +835,35 @@ 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
|
-
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
|
-
}
|
|
850
|
+
if (this._state.orderBy.length > 0) ast.order_by = this._state.orderBy;
|
|
837
851
|
if (effectiveLimit != null) ast.limit = effectiveLimit;
|
|
838
852
|
if (this._state.timeRange) ast.time_range = this._state.timeRange;
|
|
839
853
|
return ast;
|
|
840
854
|
}
|
|
841
855
|
async _fetchNext(prevRows, _limit, opts) {
|
|
842
|
-
const
|
|
843
|
-
|
|
856
|
+
const cursor = this._state.orderBy[0];
|
|
857
|
+
if (cursor == null) return okPage([], false);
|
|
858
|
+
const { column: orderCol, dir: orderDir } = cursor;
|
|
844
859
|
const lastRow = prevRows[prevRows.length - 1];
|
|
845
860
|
const lastValue = lastRow?.[orderCol];
|
|
846
861
|
if (lastValue === void 0) return okPage([], false);
|
|
847
862
|
const cursorOp = orderDir === "desc" ? "lt" : "gt";
|
|
848
863
|
const cursorFilter = { column: orderCol, op: cursorOp, value: lastValue };
|
|
849
|
-
const orderBy = this._state.orderBy.length > 0 ? this._state.orderBy : [{ column: orderCol, dir: orderDir }];
|
|
850
864
|
const nextBuilder = this._clone({
|
|
851
865
|
filters: [...this._state.filters, cursorFilter],
|
|
852
|
-
orderBy
|
|
866
|
+
orderBy: this._state.orderBy
|
|
853
867
|
});
|
|
854
868
|
return nextBuilder.fetch(opts);
|
|
855
869
|
}
|
|
@@ -940,6 +954,12 @@ function projectColumns(row, columns) {
|
|
|
940
954
|
}
|
|
941
955
|
|
|
942
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
|
+
}
|
|
943
963
|
var TableRef = class {
|
|
944
964
|
_ctx;
|
|
945
965
|
_table;
|
|
@@ -968,22 +988,34 @@ var TableRef = class {
|
|
|
968
988
|
this._createStream
|
|
969
989
|
);
|
|
970
990
|
}
|
|
971
|
-
/**
|
|
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
|
+
*/
|
|
972
1012
|
async insert(data, opts) {
|
|
973
1013
|
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);
|
|
1014
|
+
if (data.length === 0) {
|
|
1015
|
+
return ok({ ok: true, total: 0, succeeded: 0, failed: 0, duplicates: 0, results: [] });
|
|
985
1016
|
}
|
|
986
|
-
|
|
1017
|
+
const ndjson = data.map((row) => JSON.stringify(row)).join("\n");
|
|
1018
|
+
return this._sendNDJSON(ndjson, opts);
|
|
987
1019
|
}
|
|
988
1020
|
const { data: res, error } = await request(this._ctx, {
|
|
989
1021
|
method: "POST",
|
|
@@ -996,6 +1028,46 @@ var TableRef = class {
|
|
|
996
1028
|
if (res?.duplicate != null) result.duplicate = res.duplicate;
|
|
997
1029
|
return ok(result);
|
|
998
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
|
+
}
|
|
999
1071
|
/** Fetch the schema for this table. */
|
|
1000
1072
|
async schema(opts) {
|
|
1001
1073
|
const { data, error } = await request(this._ctx, {
|