@tinybirdco/sdk 0.0.19 → 0.0.20
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 -0
- package/dist/api/api.d.ts +89 -0
- package/dist/api/api.d.ts.map +1 -0
- package/dist/api/api.js +218 -0
- package/dist/api/api.js.map +1 -0
- package/dist/api/api.test.d.ts +2 -0
- package/dist/api/api.test.d.ts.map +1 -0
- package/dist/api/api.test.js +226 -0
- package/dist/api/api.test.js.map +1 -0
- package/dist/client/base.d.ts +3 -17
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +31 -153
- package/dist/client/base.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- package/src/api/api.test.ts +295 -0
- package/src/api/api.ts +345 -0
- package/src/client/base.ts +34 -181
- package/src/index.ts +14 -0
package/src/api/api.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { createTinybirdFetcher, type TinybirdFetch } from "./fetcher.js";
|
|
2
|
+
import type {
|
|
3
|
+
IngestOptions,
|
|
4
|
+
IngestResult,
|
|
5
|
+
QueryOptions,
|
|
6
|
+
QueryResult,
|
|
7
|
+
TinybirdErrorResponse,
|
|
8
|
+
} from "../client/types.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Public, decoupled Tinybird API wrapper configuration
|
|
14
|
+
*/
|
|
15
|
+
export interface TinybirdApiConfig {
|
|
16
|
+
/** Tinybird API base URL (e.g. https://api.tinybird.co) */
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
/** Tinybird token used for Authorization bearer header */
|
|
19
|
+
token: string;
|
|
20
|
+
/** Custom fetch implementation (optional) */
|
|
21
|
+
fetch?: typeof fetch;
|
|
22
|
+
/** Default timeout in milliseconds (optional) */
|
|
23
|
+
timeout?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Request options for the API layer
|
|
28
|
+
*/
|
|
29
|
+
export interface TinybirdApiRequestInit extends RequestInit {
|
|
30
|
+
/** Optional token override for a specific request */
|
|
31
|
+
token?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TinybirdApiQueryOptions extends QueryOptions {
|
|
35
|
+
/** Optional token override for this request */
|
|
36
|
+
token?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TinybirdApiIngestOptions extends IngestOptions {
|
|
40
|
+
/** Optional token override for this request */
|
|
41
|
+
token?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Error thrown by TinybirdApi when a response is not OK
|
|
46
|
+
*/
|
|
47
|
+
export class TinybirdApiError extends Error {
|
|
48
|
+
readonly statusCode: number;
|
|
49
|
+
readonly responseBody?: string;
|
|
50
|
+
readonly response?: TinybirdErrorResponse;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
message: string,
|
|
54
|
+
statusCode: number,
|
|
55
|
+
responseBody?: string,
|
|
56
|
+
response?: TinybirdErrorResponse
|
|
57
|
+
) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "TinybirdApiError";
|
|
60
|
+
this.statusCode = statusCode;
|
|
61
|
+
this.responseBody = responseBody;
|
|
62
|
+
this.response = response;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Low-level Tinybird API wrapper.
|
|
68
|
+
*
|
|
69
|
+
* This layer is intentionally decoupled from the typed TinybirdClient layer
|
|
70
|
+
* so it can be used standalone with just baseUrl + token.
|
|
71
|
+
*/
|
|
72
|
+
export class TinybirdApi {
|
|
73
|
+
private readonly config: TinybirdApiConfig;
|
|
74
|
+
private readonly baseUrl: string;
|
|
75
|
+
private readonly defaultToken: string;
|
|
76
|
+
private readonly fetchFn: TinybirdFetch;
|
|
77
|
+
|
|
78
|
+
constructor(config: TinybirdApiConfig) {
|
|
79
|
+
if (!config.baseUrl) {
|
|
80
|
+
throw new Error("baseUrl is required");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!config.token) {
|
|
84
|
+
throw new Error("token is required");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.config = config;
|
|
88
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
89
|
+
this.defaultToken = config.token;
|
|
90
|
+
this.fetchFn = createTinybirdFetcher(config.fetch ?? globalThis.fetch);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Execute a request against Tinybird API.
|
|
95
|
+
*/
|
|
96
|
+
request(path: string, init: TinybirdApiRequestInit = {}): Promise<Response> {
|
|
97
|
+
const { token, headers, ...requestInit } = init;
|
|
98
|
+
const authToken = token ?? this.defaultToken;
|
|
99
|
+
const requestHeaders = new Headers(headers);
|
|
100
|
+
|
|
101
|
+
if (!requestHeaders.has("Authorization")) {
|
|
102
|
+
requestHeaders.set("Authorization", `Bearer ${authToken}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return this.fetchFn(this.resolveUrl(path), {
|
|
106
|
+
...requestInit,
|
|
107
|
+
headers: requestHeaders,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Execute a request and parse JSON response.
|
|
113
|
+
*/
|
|
114
|
+
async requestJson<T = unknown>(
|
|
115
|
+
path: string,
|
|
116
|
+
init: TinybirdApiRequestInit = {}
|
|
117
|
+
): Promise<T> {
|
|
118
|
+
const response = await this.request(path, init);
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
await this.handleErrorResponse(response);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (await response.json()) as T;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Query a Tinybird endpoint
|
|
129
|
+
*/
|
|
130
|
+
async query<T = unknown, P extends Record<string, unknown> = Record<string, unknown>>(
|
|
131
|
+
endpointName: string,
|
|
132
|
+
params: P = {} as P,
|
|
133
|
+
options: TinybirdApiQueryOptions = {}
|
|
134
|
+
): Promise<QueryResult<T>> {
|
|
135
|
+
const url = new URL(`/v0/pipes/${endpointName}.json`, `${this.baseUrl}/`);
|
|
136
|
+
|
|
137
|
+
for (const [key, value] of Object.entries(params)) {
|
|
138
|
+
if (value === undefined || value === null) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
for (const item of value) {
|
|
144
|
+
url.searchParams.append(key, String(item));
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (value instanceof Date) {
|
|
150
|
+
url.searchParams.set(key, value.toISOString());
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
url.searchParams.set(key, String(value));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const response = await this.request(url.toString(), {
|
|
158
|
+
method: "GET",
|
|
159
|
+
token: options.token,
|
|
160
|
+
signal: this.createAbortSignal(options.timeout, options.signal),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
await this.handleErrorResponse(response);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (await response.json()) as QueryResult<T>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Ingest a single row into a datasource
|
|
172
|
+
*/
|
|
173
|
+
async ingest<T extends Record<string, unknown>>(
|
|
174
|
+
datasourceName: string,
|
|
175
|
+
event: T,
|
|
176
|
+
options: TinybirdApiIngestOptions = {}
|
|
177
|
+
): Promise<IngestResult> {
|
|
178
|
+
return this.ingestBatch(datasourceName, [event], options);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Ingest a batch of rows into a datasource
|
|
183
|
+
*/
|
|
184
|
+
async ingestBatch<T extends Record<string, unknown>>(
|
|
185
|
+
datasourceName: string,
|
|
186
|
+
events: T[],
|
|
187
|
+
options: TinybirdApiIngestOptions = {}
|
|
188
|
+
): Promise<IngestResult> {
|
|
189
|
+
if (events.length === 0) {
|
|
190
|
+
return { successful_rows: 0, quarantined_rows: 0 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const url = new URL("/v0/events", `${this.baseUrl}/`);
|
|
194
|
+
url.searchParams.set("name", datasourceName);
|
|
195
|
+
|
|
196
|
+
if (options.wait !== false) {
|
|
197
|
+
url.searchParams.set("wait", "true");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const ndjson = events
|
|
201
|
+
.map((event) => JSON.stringify(this.serializeEvent(event)))
|
|
202
|
+
.join("\n");
|
|
203
|
+
|
|
204
|
+
const response = await this.request(url.toString(), {
|
|
205
|
+
method: "POST",
|
|
206
|
+
token: options.token,
|
|
207
|
+
headers: {
|
|
208
|
+
"Content-Type": "application/x-ndjson",
|
|
209
|
+
},
|
|
210
|
+
body: ndjson,
|
|
211
|
+
signal: this.createAbortSignal(options.timeout, options.signal),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
await this.handleErrorResponse(response);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return (await response.json()) as IngestResult;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Execute raw SQL against Tinybird
|
|
223
|
+
*/
|
|
224
|
+
async sql<T = unknown>(
|
|
225
|
+
sql: string,
|
|
226
|
+
options: TinybirdApiQueryOptions = {}
|
|
227
|
+
): Promise<QueryResult<T>> {
|
|
228
|
+
const response = await this.request("/v0/sql", {
|
|
229
|
+
method: "POST",
|
|
230
|
+
token: options.token,
|
|
231
|
+
headers: {
|
|
232
|
+
"Content-Type": "text/plain",
|
|
233
|
+
},
|
|
234
|
+
body: sql,
|
|
235
|
+
signal: this.createAbortSignal(options.timeout, options.signal),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
await this.handleErrorResponse(response);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (await response.json()) as QueryResult<T>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private createAbortSignal(
|
|
246
|
+
timeout?: number,
|
|
247
|
+
existingSignal?: AbortSignal
|
|
248
|
+
): AbortSignal | undefined {
|
|
249
|
+
const timeoutMs = timeout ?? this.config.timeout ?? DEFAULT_TIMEOUT;
|
|
250
|
+
|
|
251
|
+
if (!timeoutMs && !existingSignal) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!timeoutMs && existingSignal) {
|
|
256
|
+
return existingSignal;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
260
|
+
|
|
261
|
+
if (!existingSignal) {
|
|
262
|
+
return timeoutSignal;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return AbortSignal.any([timeoutSignal, existingSignal]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private serializeEvent(
|
|
269
|
+
event: Record<string, unknown>
|
|
270
|
+
): Record<string, unknown> {
|
|
271
|
+
const serialized: Record<string, unknown> = {};
|
|
272
|
+
|
|
273
|
+
for (const [key, value] of Object.entries(event)) {
|
|
274
|
+
serialized[key] = this.serializeValue(value);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return serialized;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private serializeValue(value: unknown): unknown {
|
|
281
|
+
if (value instanceof Date) {
|
|
282
|
+
return value.toISOString();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (value instanceof Map) {
|
|
286
|
+
return Object.fromEntries(value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (typeof value === "bigint") {
|
|
290
|
+
return value.toString();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (Array.isArray(value)) {
|
|
294
|
+
return value.map((item) => this.serializeValue(item));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (typeof value === "object" && value !== null) {
|
|
298
|
+
return this.serializeEvent(value as Record<string, unknown>);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return value;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private async handleErrorResponse(response: Response): Promise<never> {
|
|
305
|
+
const rawBody = await response.text();
|
|
306
|
+
let errorResponse: TinybirdErrorResponse | undefined;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
errorResponse = JSON.parse(rawBody) as TinybirdErrorResponse;
|
|
310
|
+
} catch {
|
|
311
|
+
// ignore parse error and keep raw body
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const message =
|
|
315
|
+
errorResponse?.error ??
|
|
316
|
+
(rawBody
|
|
317
|
+
? `Request failed with status ${response.status}: ${rawBody}`
|
|
318
|
+
: `Request failed with status ${response.status}`);
|
|
319
|
+
|
|
320
|
+
throw new TinybirdApiError(
|
|
321
|
+
message,
|
|
322
|
+
response.status,
|
|
323
|
+
rawBody || undefined,
|
|
324
|
+
errorResponse
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private resolveUrl(path: string): string {
|
|
329
|
+
return new URL(path, `${this.baseUrl}/`).toString();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Create a decoupled Tinybird API wrapper.
|
|
335
|
+
*/
|
|
336
|
+
export function createTinybirdApi(
|
|
337
|
+
config: TinybirdApiConfig
|
|
338
|
+
): TinybirdApi {
|
|
339
|
+
return new TinybirdApi(config);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Alias for teams that prefer "wrapper" naming.
|
|
344
|
+
*/
|
|
345
|
+
export const createTinybirdApiWrapper = createTinybirdApi;
|
package/src/client/base.ts
CHANGED
|
@@ -8,15 +8,9 @@ import type {
|
|
|
8
8
|
IngestResult,
|
|
9
9
|
QueryOptions,
|
|
10
10
|
IngestOptions,
|
|
11
|
-
TinybirdErrorResponse,
|
|
12
11
|
} from "./types.js";
|
|
13
12
|
import { TinybirdError } from "./types.js";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Default timeout for requests (30 seconds)
|
|
18
|
-
*/
|
|
19
|
-
const DEFAULT_TIMEOUT = 30000;
|
|
13
|
+
import { TinybirdApi, TinybirdApiError } from "../api/api.js";
|
|
20
14
|
|
|
21
15
|
/**
|
|
22
16
|
* Resolved token info from dev mode
|
|
@@ -57,7 +51,7 @@ interface ResolvedTokenInfo {
|
|
|
57
51
|
*/
|
|
58
52
|
export class TinybirdClient {
|
|
59
53
|
private readonly config: ClientConfig;
|
|
60
|
-
private readonly
|
|
54
|
+
private readonly apisByToken = new Map<string, TinybirdApi>();
|
|
61
55
|
private tokenPromise: Promise<ResolvedTokenInfo> | null = null;
|
|
62
56
|
private resolvedToken: string | null = null;
|
|
63
57
|
|
|
@@ -75,8 +69,6 @@ export class TinybirdClient {
|
|
|
75
69
|
...config,
|
|
76
70
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
77
71
|
};
|
|
78
|
-
|
|
79
|
-
this.fetchFn = createTinybirdFetcher(config.fetch ?? globalThis.fetch);
|
|
80
72
|
}
|
|
81
73
|
|
|
82
74
|
/**
|
|
@@ -159,39 +151,12 @@ export class TinybirdClient {
|
|
|
159
151
|
options: QueryOptions = {}
|
|
160
152
|
): Promise<QueryResult<T>> {
|
|
161
153
|
const token = await this.getToken();
|
|
162
|
-
const url = new URL(`/v0/pipes/${pipeName}.json`, this.config.baseUrl);
|
|
163
|
-
|
|
164
|
-
// Add parameters to query string
|
|
165
|
-
for (const [key, value] of Object.entries(params)) {
|
|
166
|
-
if (value !== undefined && value !== null) {
|
|
167
|
-
if (Array.isArray(value)) {
|
|
168
|
-
// Handle array parameters
|
|
169
|
-
for (const item of value) {
|
|
170
|
-
url.searchParams.append(key, String(item));
|
|
171
|
-
}
|
|
172
|
-
} else if (value instanceof Date) {
|
|
173
|
-
// Handle Date objects
|
|
174
|
-
url.searchParams.set(key, value.toISOString());
|
|
175
|
-
} else {
|
|
176
|
-
url.searchParams.set(key, String(value));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
154
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
},
|
|
186
|
-
signal: this.createAbortSignal(options.timeout, options.signal),
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
if (!response.ok) {
|
|
190
|
-
await this.handleErrorResponse(response);
|
|
155
|
+
try {
|
|
156
|
+
return await this.getApi(token).query<T>(pipeName, params, options);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
this.rethrowApiError(error);
|
|
191
159
|
}
|
|
192
|
-
|
|
193
|
-
const result = (await response.json()) as QueryResult<T>;
|
|
194
|
-
return result;
|
|
195
160
|
}
|
|
196
161
|
|
|
197
162
|
/**
|
|
@@ -223,39 +188,13 @@ export class TinybirdClient {
|
|
|
223
188
|
events: T[],
|
|
224
189
|
options: IngestOptions = {}
|
|
225
190
|
): Promise<IngestResult> {
|
|
226
|
-
if (events.length === 0) {
|
|
227
|
-
return { successful_rows: 0, quarantined_rows: 0 };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
191
|
const token = await this.getToken();
|
|
231
|
-
const url = new URL("/v0/events", this.config.baseUrl);
|
|
232
|
-
url.searchParams.set("name", datasourceName);
|
|
233
192
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Convert events to NDJSON format
|
|
239
|
-
const ndjson = events
|
|
240
|
-
.map((event) => JSON.stringify(this.serializeEvent(event)))
|
|
241
|
-
.join("\n");
|
|
242
|
-
|
|
243
|
-
const response = await this.fetch(url.toString(), {
|
|
244
|
-
method: "POST",
|
|
245
|
-
headers: {
|
|
246
|
-
Authorization: `Bearer ${token}`,
|
|
247
|
-
"Content-Type": "application/x-ndjson",
|
|
248
|
-
},
|
|
249
|
-
body: ndjson,
|
|
250
|
-
signal: this.createAbortSignal(options.timeout, options.signal),
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
if (!response.ok) {
|
|
254
|
-
await this.handleErrorResponse(response);
|
|
193
|
+
try {
|
|
194
|
+
return await this.getApi(token).ingestBatch(datasourceName, events, options);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.rethrowApiError(error);
|
|
255
197
|
}
|
|
256
|
-
|
|
257
|
-
const result = (await response.json()) as IngestResult;
|
|
258
|
-
return result;
|
|
259
198
|
}
|
|
260
199
|
|
|
261
200
|
/**
|
|
@@ -270,127 +209,41 @@ export class TinybirdClient {
|
|
|
270
209
|
options: QueryOptions = {}
|
|
271
210
|
): Promise<QueryResult<T>> {
|
|
272
211
|
const token = await this.getToken();
|
|
273
|
-
const url = new URL("/v0/sql", this.config.baseUrl);
|
|
274
|
-
|
|
275
|
-
const response = await this.fetch(url.toString(), {
|
|
276
|
-
method: "POST",
|
|
277
|
-
headers: {
|
|
278
|
-
Authorization: `Bearer ${token}`,
|
|
279
|
-
"Content-Type": "text/plain",
|
|
280
|
-
},
|
|
281
|
-
body: sql,
|
|
282
|
-
signal: this.createAbortSignal(options.timeout, options.signal),
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
if (!response.ok) {
|
|
286
|
-
await this.handleErrorResponse(response);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const result = (await response.json()) as QueryResult<T>;
|
|
290
|
-
return result;
|
|
291
|
-
}
|
|
292
212
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
event: T
|
|
298
|
-
): Record<string, unknown> {
|
|
299
|
-
const serialized: Record<string, unknown> = {};
|
|
300
|
-
|
|
301
|
-
for (const [key, value] of Object.entries(event)) {
|
|
302
|
-
if (value instanceof Date) {
|
|
303
|
-
// Convert Date to ISO string
|
|
304
|
-
serialized[key] = value.toISOString();
|
|
305
|
-
} else if (value instanceof Map) {
|
|
306
|
-
// Convert Map to object
|
|
307
|
-
serialized[key] = Object.fromEntries(value);
|
|
308
|
-
} else if (typeof value === "bigint") {
|
|
309
|
-
// Convert BigInt to string (ClickHouse will parse it)
|
|
310
|
-
serialized[key] = value.toString();
|
|
311
|
-
} else if (Array.isArray(value)) {
|
|
312
|
-
// Recursively serialize array elements
|
|
313
|
-
serialized[key] = value.map((item) =>
|
|
314
|
-
typeof item === "object" && item !== null
|
|
315
|
-
? this.serializeEvent(item as Record<string, unknown>)
|
|
316
|
-
: item instanceof Date
|
|
317
|
-
? item.toISOString()
|
|
318
|
-
: item
|
|
319
|
-
);
|
|
320
|
-
} else if (typeof value === "object" && value !== null) {
|
|
321
|
-
// Recursively serialize nested objects
|
|
322
|
-
serialized[key] = this.serializeEvent(value as Record<string, unknown>);
|
|
323
|
-
} else {
|
|
324
|
-
serialized[key] = value;
|
|
325
|
-
}
|
|
213
|
+
try {
|
|
214
|
+
return await this.getApi(token).sql<T>(sql, options);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.rethrowApiError(error);
|
|
326
217
|
}
|
|
327
|
-
|
|
328
|
-
return serialized;
|
|
329
218
|
}
|
|
330
219
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
timeout?: number,
|
|
336
|
-
existingSignal?: AbortSignal
|
|
337
|
-
): AbortSignal | undefined {
|
|
338
|
-
const timeoutMs = timeout ?? this.config.timeout ?? DEFAULT_TIMEOUT;
|
|
339
|
-
|
|
340
|
-
// If no timeout and no existing signal, return undefined
|
|
341
|
-
if (!timeoutMs && !existingSignal) {
|
|
342
|
-
return undefined;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// If only existing signal, return it
|
|
346
|
-
if (!timeoutMs && existingSignal) {
|
|
347
|
-
return existingSignal;
|
|
220
|
+
private getApi(token: string): TinybirdApi {
|
|
221
|
+
const existing = this.apisByToken.get(token);
|
|
222
|
+
if (existing) {
|
|
223
|
+
return existing;
|
|
348
224
|
}
|
|
349
225
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
226
|
+
const api = new TinybirdApi({
|
|
227
|
+
baseUrl: this.config.baseUrl,
|
|
228
|
+
token,
|
|
229
|
+
fetch: this.config.fetch,
|
|
230
|
+
timeout: this.config.timeout,
|
|
231
|
+
});
|
|
357
232
|
|
|
358
|
-
|
|
359
|
-
return
|
|
233
|
+
this.apisByToken.set(token, api);
|
|
234
|
+
return api;
|
|
360
235
|
}
|
|
361
236
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
try {
|
|
370
|
-
rawBody = await response.text();
|
|
371
|
-
errorResponse = JSON.parse(rawBody) as TinybirdErrorResponse;
|
|
372
|
-
} catch {
|
|
373
|
-
// Failed to parse error response - include raw body in message
|
|
374
|
-
if (rawBody) {
|
|
375
|
-
throw new TinybirdError(
|
|
376
|
-
`Request failed with status ${response.status}: ${rawBody}`,
|
|
377
|
-
response.status,
|
|
378
|
-
undefined
|
|
379
|
-
);
|
|
380
|
-
}
|
|
237
|
+
private rethrowApiError(error: unknown): never {
|
|
238
|
+
if (error instanceof TinybirdApiError) {
|
|
239
|
+
throw new TinybirdError(
|
|
240
|
+
error.message,
|
|
241
|
+
error.statusCode,
|
|
242
|
+
error.response
|
|
243
|
+
);
|
|
381
244
|
}
|
|
382
245
|
|
|
383
|
-
|
|
384
|
-
errorResponse?.error ?? `Request failed with status ${response.status}`;
|
|
385
|
-
|
|
386
|
-
throw new TinybirdError(message, response.status, errorResponse);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Internal fetch wrapper
|
|
391
|
-
*/
|
|
392
|
-
private fetch(url: string, init?: RequestInit): Promise<Response> {
|
|
393
|
-
return this.fetchFn(url, init);
|
|
246
|
+
throw error;
|
|
394
247
|
}
|
|
395
248
|
}
|
|
396
249
|
|
package/src/index.ts
CHANGED
|
@@ -214,6 +214,20 @@ export type {
|
|
|
214
214
|
TypedDatasourceIngest,
|
|
215
215
|
} from "./client/types.js";
|
|
216
216
|
|
|
217
|
+
// ============ Public Tinybird API ============
|
|
218
|
+
export {
|
|
219
|
+
TinybirdApi,
|
|
220
|
+
TinybirdApiError,
|
|
221
|
+
createTinybirdApi,
|
|
222
|
+
createTinybirdApiWrapper,
|
|
223
|
+
} from "./api/api.js";
|
|
224
|
+
export type {
|
|
225
|
+
TinybirdApiConfig,
|
|
226
|
+
TinybirdApiQueryOptions,
|
|
227
|
+
TinybirdApiIngestOptions,
|
|
228
|
+
TinybirdApiRequestInit,
|
|
229
|
+
} from "./api/api.js";
|
|
230
|
+
|
|
217
231
|
// ============ Preview Environment ============
|
|
218
232
|
export {
|
|
219
233
|
isPreviewEnvironment,
|