@tinybirdco/sdk 0.0.18 → 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.
@@ -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 { createTinybirdFetcher, type TinybirdFetch } from "../api/fetcher.js";
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 fetchFn: TinybirdFetch;
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
- const response = await this.fetch(url.toString(), {
182
- method: "GET",
183
- headers: {
184
- Authorization: `Bearer ${token}`,
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
- if (options.wait !== false) {
235
- url.searchParams.set("wait", "true");
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
- * Serialize an event for ingestion, handling Date objects and other special types
295
- */
296
- private serializeEvent<T extends Record<string, unknown>>(
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
- * Create an AbortSignal with timeout
333
- */
334
- private createAbortSignal(
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
- // Create timeout signal
351
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
352
-
353
- // If only timeout, return timeout signal
354
- if (!existingSignal) {
355
- return timeoutSignal;
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
- // Combine both signals
359
- return AbortSignal.any([timeoutSignal, existingSignal]);
233
+ this.apisByToken.set(token, api);
234
+ return api;
360
235
  }
361
236
 
362
- /**
363
- * Handle error responses from the API
364
- */
365
- private async handleErrorResponse(response: Response): Promise<never> {
366
- let errorResponse: TinybirdErrorResponse | undefined;
367
- let rawBody: string | undefined;
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
- const message =
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,