@tinybirdco/sdk 0.0.19 → 0.0.21

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.
Files changed (55) hide show
  1. package/README.md +47 -0
  2. package/dist/api/api.d.ts +89 -0
  3. package/dist/api/api.d.ts.map +1 -0
  4. package/dist/api/api.js +218 -0
  5. package/dist/api/api.js.map +1 -0
  6. package/dist/api/api.test.d.ts +2 -0
  7. package/dist/api/api.test.d.ts.map +1 -0
  8. package/dist/api/api.test.js +226 -0
  9. package/dist/api/api.test.js.map +1 -0
  10. package/dist/api/dashboard.d.ts +74 -0
  11. package/dist/api/dashboard.d.ts.map +1 -0
  12. package/dist/api/dashboard.js +102 -0
  13. package/dist/api/dashboard.js.map +1 -0
  14. package/dist/api/dashboard.test.d.ts +2 -0
  15. package/dist/api/dashboard.test.d.ts.map +1 -0
  16. package/dist/api/dashboard.test.js +91 -0
  17. package/dist/api/dashboard.test.js.map +1 -0
  18. package/dist/cli/commands/branch.d.ts +2 -0
  19. package/dist/cli/commands/branch.d.ts.map +1 -1
  20. package/dist/cli/commands/branch.js +12 -1
  21. package/dist/cli/commands/branch.js.map +1 -1
  22. package/dist/cli/commands/build.d.ts +17 -0
  23. package/dist/cli/commands/build.d.ts.map +1 -1
  24. package/dist/cli/commands/build.js +25 -0
  25. package/dist/cli/commands/build.js.map +1 -1
  26. package/dist/cli/commands/dev.d.ts +2 -0
  27. package/dist/cli/commands/dev.d.ts.map +1 -1
  28. package/dist/cli/commands/dev.js +19 -3
  29. package/dist/cli/commands/dev.js.map +1 -1
  30. package/dist/cli/index.js +23 -24
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/cli/output.d.ts +20 -0
  33. package/dist/cli/output.d.ts.map +1 -1
  34. package/dist/cli/output.js +37 -0
  35. package/dist/cli/output.js.map +1 -1
  36. package/dist/client/base.d.ts +3 -17
  37. package/dist/client/base.d.ts.map +1 -1
  38. package/dist/client/base.js +31 -153
  39. package/dist/client/base.js.map +1 -1
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +4 -0
  43. package/dist/index.js.map +1 -1
  44. package/package.json +7 -2
  45. package/src/api/api.test.ts +295 -0
  46. package/src/api/api.ts +345 -0
  47. package/src/api/dashboard.test.ts +115 -0
  48. package/src/api/dashboard.ts +121 -0
  49. package/src/cli/commands/branch.ts +15 -1
  50. package/src/cli/commands/build.ts +46 -0
  51. package/src/cli/commands/dev.ts +47 -7
  52. package/src/cli/index.ts +25 -27
  53. package/src/cli/output.ts +54 -0
  54. package/src/client/base.ts +34 -181
  55. package/src/index.ts +23 -0
@@ -12,6 +12,8 @@ import {
12
12
  getLocalWorkspaceName,
13
13
  LocalNotRunningError,
14
14
  } from "../../api/local.js";
15
+ import { getWorkspace } from "../../api/workspaces.js";
16
+ import { getBranchDashboardUrl, getLocalDashboardUrl } from "../../api/dashboard.js";
15
17
 
16
18
  /**
17
19
  * Build command options
@@ -27,6 +29,22 @@ export interface BuildCommandOptions {
27
29
  devModeOverride?: DevMode;
28
30
  }
29
31
 
32
+ /**
33
+ * Branch info included in build result
34
+ */
35
+ export interface BuildBranchInfo {
36
+ /** Git branch name */
37
+ gitBranch: string | null;
38
+ /** Tinybird branch name */
39
+ tinybirdBranch: string | null;
40
+ /** Whether the branch was newly created */
41
+ wasCreated: boolean;
42
+ /** Dashboard URL for the branch */
43
+ dashboardUrl?: string;
44
+ /** Whether using local mode */
45
+ isLocal: boolean;
46
+ }
47
+
30
48
  /**
31
49
  * Build command result
32
50
  */
@@ -37,6 +55,8 @@ export interface BuildCommandResult {
37
55
  build?: BuildFromIncludeResult;
38
56
  /** Build API result (if not dry run) */
39
57
  deploy?: BuildApiResult;
58
+ /** Branch info (when building to a branch) */
59
+ branchInfo?: BuildBranchInfo;
40
60
  /** Error message if failed */
41
61
  error?: string;
42
62
  /** Duration in milliseconds */
@@ -101,6 +121,7 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise<Build
101
121
  }
102
122
 
103
123
  let deployResult: BuildApiResult;
124
+ let branchInfo: BuildBranchInfo | undefined;
104
125
 
105
126
  // Handle local mode
106
127
  if (devMode === "local") {
@@ -123,6 +144,14 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise<Build
123
144
  console.log(`[debug] Workspace ${wasCreated ? "created" : "found"}: ${workspace.name}`);
124
145
  }
125
146
 
147
+ branchInfo = {
148
+ gitBranch: config.gitBranch,
149
+ tinybirdBranch: workspaceName,
150
+ wasCreated,
151
+ dashboardUrl: getLocalDashboardUrl(workspaceName),
152
+ isLocal: true,
153
+ };
154
+
126
155
  // Always use /v1/build for local (no deploy endpoint)
127
156
  deployResult = await buildToTinybird(
128
157
  {
@@ -197,6 +226,21 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise<Build
197
226
  if (debug) {
198
227
  console.log(`[debug] Using branch token for branch: ${config.tinybirdBranch}`);
199
228
  }
229
+
230
+ // Get workspace name for dashboard URL
231
+ const workspace = await getWorkspace({
232
+ baseUrl: config.baseUrl,
233
+ token: config.token,
234
+ });
235
+ const dashboardUrl = getBranchDashboardUrl(config.baseUrl, workspace.name, config.tinybirdBranch!) ?? undefined;
236
+
237
+ branchInfo = {
238
+ gitBranch: config.gitBranch,
239
+ tinybirdBranch: config.tinybirdBranch,
240
+ wasCreated: tinybirdBranch.wasCreated ?? false,
241
+ dashboardUrl,
242
+ isLocal: false,
243
+ };
200
244
  } catch (error) {
201
245
  return {
202
246
  success: false,
@@ -231,6 +275,7 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise<Build
231
275
  success: false,
232
276
  build: buildResult,
233
277
  deploy: deployResult,
278
+ branchInfo,
234
279
  error: deployResult.error,
235
280
  durationMs: Date.now() - startTime,
236
281
  };
@@ -240,6 +285,7 @@ export async function runBuild(options: BuildCommandOptions = {}): Promise<Build
240
285
  success: true,
241
286
  build: buildResult,
242
287
  deploy: deployResult,
288
+ branchInfo,
243
289
  durationMs: Date.now() - startTime,
244
290
  };
245
291
  }
@@ -4,7 +4,16 @@
4
4
 
5
5
  import * as path from "path";
6
6
  import { watch } from "chokidar";
7
- import { loadConfig, configExists, findConfigFile, hasValidToken, updateConfig, LOCAL_BASE_URL, type ResolvedConfig, type DevMode } from "../config.js";
7
+ import {
8
+ loadConfig,
9
+ configExists,
10
+ findConfigFile,
11
+ hasValidToken,
12
+ updateConfig,
13
+ LOCAL_BASE_URL,
14
+ type ResolvedConfig,
15
+ type DevMode,
16
+ } from "../config.js";
8
17
  import { runBuild, type BuildCommandResult } from "./build.js";
9
18
  import { getOrCreateBranch, type TinybirdBranch } from "../../api/branches.js";
10
19
  import { browserLogin } from "../auth.js";
@@ -19,6 +28,8 @@ import {
19
28
  getLocalWorkspaceName,
20
29
  type LocalWorkspace,
21
30
  } from "../../api/local.js";
31
+ import { getWorkspace } from "../../api/workspaces.js";
32
+ import { getBranchDashboardUrl, getLocalDashboardUrl } from "../../api/dashboard.js";
22
33
 
23
34
  /**
24
35
  * Login result info
@@ -70,6 +81,8 @@ export interface BranchReadyInfo {
70
81
  isLocal?: boolean;
71
82
  /** Local workspace info (only in local mode) */
72
83
  localWorkspace?: LocalWorkspace;
84
+ /** Dashboard URL for the branch (only in branch mode) */
85
+ dashboardUrl?: string;
73
86
  }
74
87
 
75
88
  /**
@@ -99,7 +112,9 @@ export interface DevController {
99
112
  * @param options - Dev options
100
113
  * @returns Dev controller
101
114
  */
102
- export async function runDev(options: DevCommandOptions = {}): Promise<DevController> {
115
+ export async function runDev(
116
+ options: DevCommandOptions = {}
117
+ ): Promise<DevController> {
103
118
  const cwd = options.cwd ?? process.cwd();
104
119
  const debounceMs = options.debounce ?? 100;
105
120
 
@@ -129,7 +144,8 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
129
144
 
130
145
  if (!authResult.success || !authResult.token) {
131
146
  throw new Error(
132
- authResult.error ?? "Login failed. Run 'npx tinybird login' to authenticate."
147
+ authResult.error ??
148
+ "Login failed. Run 'npx tinybird login' to authenticate."
133
149
  );
134
150
  }
135
151
 
@@ -173,8 +189,14 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
173
189
  if (devMode === "local") {
174
190
  // Local mode: get tokens from local container and set up workspace
175
191
  const localTokens = await getLocalTokens();
176
- const workspaceName = getLocalWorkspaceName(config.tinybirdBranch, config.cwd);
177
- const { workspace, wasCreated } = await getOrCreateLocalWorkspace(localTokens, workspaceName);
192
+ const workspaceName = getLocalWorkspaceName(
193
+ config.tinybirdBranch,
194
+ config.cwd
195
+ );
196
+ const { workspace, wasCreated } = await getOrCreateLocalWorkspace(
197
+ localTokens,
198
+ workspaceName
199
+ );
178
200
 
179
201
  effectiveToken = workspace.token;
180
202
  effectiveBaseUrl = LOCAL_BASE_URL;
@@ -184,6 +206,7 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
184
206
  isLocal: true,
185
207
  localWorkspace: workspace,
186
208
  wasCreated,
209
+ dashboardUrl: getLocalDashboardUrl(workspace.name),
187
210
  };
188
211
  } else {
189
212
  // Branch mode: use Tinybird cloud with branches
@@ -216,11 +239,22 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
216
239
  }
217
240
 
218
241
  effectiveToken = tinybirdBranch.token;
242
+
243
+ // Get workspace name for dashboard URL
244
+ const workspace = await getWorkspace({
245
+ baseUrl: config.baseUrl,
246
+ token: config.token,
247
+ });
248
+ const dashboardUrl =
249
+ getBranchDashboardUrl(config.baseUrl, workspace.name, branchName) ??
250
+ undefined;
251
+
219
252
  branchInfo = {
220
253
  gitBranch: config.gitBranch, // Original git branch name for display
221
254
  isMainBranch: false,
222
255
  tinybirdBranch,
223
256
  wasCreated: tinybirdBranch.wasCreated ?? false,
257
+ dashboardUrl,
224
258
  };
225
259
  }
226
260
  }
@@ -246,7 +280,11 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
246
280
  async function doBuild(): Promise<BuildCommandResult> {
247
281
  if (isBuilding) {
248
282
  pendingBuild = true;
249
- return { success: false, error: "Build already in progress", durationMs: 0 };
283
+ return {
284
+ success: false,
285
+ error: "Build already in progress",
286
+ durationMs: 0,
287
+ };
250
288
  }
251
289
 
252
290
  isBuilding = true;
@@ -357,7 +395,9 @@ export async function runDev(options: DevCommandOptions = {}): Promise<DevContro
357
395
  });
358
396
 
359
397
  watcher.on("error", (error: unknown) => {
360
- options.onError?.(error instanceof Error ? error : new Error(String(error)));
398
+ options.onError?.(
399
+ error instanceof Error ? error : new Error(String(error))
400
+ );
361
401
  });
362
402
 
363
403
  // Do initial build
package/src/cli/index.ts CHANGED
@@ -188,15 +188,19 @@ function createCli(): Command {
188
188
  devModeOverride = "local";
189
189
  }
190
190
 
191
- const modeLabel = devModeOverride === "local" ? " (local)" : "";
192
- output.highlight(`Building${modeLabel}...`);
193
-
194
191
  const result = await runBuild({
195
192
  dryRun: options.dryRun,
196
193
  devModeOverride,
197
194
  });
198
195
 
199
- const { build, deploy } = result;
196
+ const { build, deploy, branchInfo } = result;
197
+
198
+ // Show branch info
199
+ if (branchInfo) {
200
+ output.showBranchInfo(branchInfo);
201
+ }
202
+
203
+ output.highlight("Building...");
200
204
 
201
205
  if (!result.success) {
202
206
  // Show detailed errors if available
@@ -486,32 +490,23 @@ function createCli(): Command {
486
490
  },
487
491
  onBranchReady: (info) => {
488
492
  if (info.isLocal) {
489
- // Local mode
490
- const workspaceName = info.localWorkspace?.name ?? "unknown";
491
- if (info.wasCreated) {
492
- console.log(`Using local Tinybird container`);
493
- console.log(`Creating local workspace '${workspaceName}'...`);
494
- console.log("Workspace created.\n");
495
- } else {
496
- console.log(`Using local Tinybird container`);
497
- console.log(
498
- `Using existing local workspace '${workspaceName}'\n`
499
- );
500
- }
493
+ output.showBranchInfo({
494
+ gitBranch: info.gitBranch,
495
+ tinybirdBranch: info.localWorkspace?.name ?? null,
496
+ wasCreated: info.wasCreated ?? false,
497
+ dashboardUrl: info.dashboardUrl,
498
+ isLocal: true,
499
+ });
501
500
  } else if (info.isMainBranch) {
502
501
  console.log("On main branch - deploying to workspace\n");
503
502
  } else if (info.gitBranch) {
504
- const tinybirdName = info.tinybirdBranch?.name ?? info.gitBranch;
505
- if (info.wasCreated) {
506
- console.log(`Detected git branch: ${info.gitBranch}`);
507
- console.log(`Creating Tinybird branch '${tinybirdName}'...`);
508
- console.log("Branch created and token cached.\n");
509
- } else {
510
- console.log(`Detected git branch: ${info.gitBranch}`);
511
- console.log(
512
- `Using existing Tinybird branch '${tinybirdName}'\n`
513
- );
514
- }
503
+ output.showBranchInfo({
504
+ gitBranch: info.gitBranch,
505
+ tinybirdBranch: info.tinybirdBranch?.name ?? null,
506
+ wasCreated: info.wasCreated ?? false,
507
+ dashboardUrl: info.dashboardUrl,
508
+ isLocal: false,
509
+ });
515
510
  } else {
516
511
  console.log("Not in a git repository - deploying to workspace\n");
517
512
  }
@@ -659,6 +654,9 @@ function createCli(): Command {
659
654
  console.log(` Tinybird branch: ${result.tinybirdBranch.name}`);
660
655
  console.log(` Branch ID: ${result.tinybirdBranch.id}`);
661
656
  console.log(` Created: ${result.tinybirdBranch.created_at}`);
657
+ if (result.dashboardUrl) {
658
+ console.log(` Dashboard: ${result.dashboardUrl}`);
659
+ }
662
660
  } else if (!result.isMainBranch && result.tinybirdBranchName) {
663
661
  console.log(" Tinybird branch: not created yet");
664
662
  console.log(" (Run 'npx tinybird dev' to create it)");
package/src/cli/output.ts CHANGED
@@ -239,6 +239,59 @@ export function showDeployFailure(): void {
239
239
  error(`\n✗ Deploy failed`);
240
240
  }
241
241
 
242
+ /**
243
+ * Branch info for display
244
+ */
245
+ export interface BranchDisplayInfo {
246
+ /** Git branch name */
247
+ gitBranch: string | null;
248
+ /** Tinybird branch name */
249
+ tinybirdBranch: string | null;
250
+ /** Whether the branch was newly created */
251
+ wasCreated: boolean;
252
+ /** Dashboard URL for the branch */
253
+ dashboardUrl?: string;
254
+ /** Whether using local mode */
255
+ isLocal?: boolean;
256
+ }
257
+
258
+ /**
259
+ * Show branch information in a compact, styled format
260
+ */
261
+ export function showBranchInfo(info: BranchDisplayInfo): void {
262
+ const status = info.wasCreated
263
+ ? colorize("✓ created", "green")
264
+ : colorize("existing", "gray");
265
+
266
+ if (info.isLocal) {
267
+ // Show git branch
268
+ if (info.gitBranch) {
269
+ console.log(`» Git branch: ${info.gitBranch}`);
270
+ }
271
+ // Show local workspace
272
+ const name = info.tinybirdBranch ?? "unknown";
273
+ console.log(`» Local workspace: ${name} ${status}`);
274
+ // Show dashboard URL
275
+ if (info.dashboardUrl) {
276
+ console.log(colorize(` ↳ ${info.dashboardUrl}`, "gray"));
277
+ }
278
+ } else {
279
+ // Show git branch
280
+ if (info.gitBranch) {
281
+ console.log(`» Git branch: ${info.gitBranch}`);
282
+ }
283
+ // Show Tinybird branch
284
+ if (info.tinybirdBranch) {
285
+ console.log(`» Tinybird branch: ${info.tinybirdBranch} ${status}`);
286
+ }
287
+ // Show dashboard URL
288
+ if (info.dashboardUrl) {
289
+ console.log(colorize(` ↳ ${info.dashboardUrl}`, "gray"));
290
+ }
291
+ }
292
+ console.log("");
293
+ }
294
+
242
295
  /**
243
296
  * Output object containing all output functions
244
297
  */
@@ -265,4 +318,5 @@ export const output = {
265
318
  showValidatingDeployment,
266
319
  showDeploySuccess,
267
320
  showDeployFailure,
321
+ showBranchInfo,
268
322
  };
@@ -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,
@@ -221,3 +235,12 @@ export {
221
235
  resolveToken,
222
236
  clearTokenCache,
223
237
  } from "./client/preview.js";
238
+
239
+ // ============ Dashboard URL Utilities ============
240
+ export {
241
+ parseApiUrl,
242
+ getDashboardUrl,
243
+ getBranchDashboardUrl,
244
+ getLocalDashboardUrl,
245
+ } from "./api/dashboard.js";
246
+ export type { RegionInfo } from "./api/dashboard.js";