@tinybirdco/sdk 0.0.36 → 0.0.38

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 (59) hide show
  1. package/README.md +50 -1
  2. package/dist/api/api.d.ts +66 -1
  3. package/dist/api/api.d.ts.map +1 -1
  4. package/dist/api/api.js +114 -0
  5. package/dist/api/api.js.map +1 -1
  6. package/dist/api/api.test.js +192 -0
  7. package/dist/api/api.test.js.map +1 -1
  8. package/dist/api/tokens.d.ts +79 -0
  9. package/dist/api/tokens.d.ts.map +1 -0
  10. package/dist/api/tokens.js +80 -0
  11. package/dist/api/tokens.js.map +1 -0
  12. package/dist/api/tokens.test.d.ts +2 -0
  13. package/dist/api/tokens.test.d.ts.map +1 -0
  14. package/dist/api/tokens.test.js +209 -0
  15. package/dist/api/tokens.test.js.map +1 -0
  16. package/dist/cli/commands/init.d.ts +2 -0
  17. package/dist/cli/commands/init.d.ts.map +1 -1
  18. package/dist/cli/commands/init.js +2 -3
  19. package/dist/cli/commands/init.js.map +1 -1
  20. package/dist/client/base.d.ts +29 -1
  21. package/dist/client/base.d.ts.map +1 -1
  22. package/dist/client/base.js +44 -0
  23. package/dist/client/base.js.map +1 -1
  24. package/dist/client/base.test.js +25 -0
  25. package/dist/client/base.test.js.map +1 -1
  26. package/dist/client/tokens.d.ts +42 -0
  27. package/dist/client/tokens.d.ts.map +1 -0
  28. package/dist/client/tokens.js +67 -0
  29. package/dist/client/tokens.js.map +1 -0
  30. package/dist/client/tokens.test.d.ts +2 -0
  31. package/dist/client/tokens.test.d.ts.map +1 -0
  32. package/dist/client/tokens.test.js +79 -0
  33. package/dist/client/tokens.test.js.map +1 -0
  34. package/dist/client/types.d.ts +49 -0
  35. package/dist/client/types.d.ts.map +1 -1
  36. package/dist/index.d.ts +4 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/schema/project.d.ts +40 -12
  41. package/dist/schema/project.d.ts.map +1 -1
  42. package/dist/schema/project.js +67 -13
  43. package/dist/schema/project.js.map +1 -1
  44. package/dist/schema/project.test.js +63 -10
  45. package/dist/schema/project.test.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/api/api.test.ts +265 -0
  48. package/src/api/api.ts +196 -0
  49. package/src/api/tokens.test.ts +253 -0
  50. package/src/api/tokens.ts +169 -0
  51. package/src/cli/commands/init.ts +5 -3
  52. package/src/client/base.test.ts +32 -0
  53. package/src/client/base.ts +60 -0
  54. package/src/client/tokens.test.ts +103 -0
  55. package/src/client/tokens.ts +69 -0
  56. package/src/client/types.ts +54 -0
  57. package/src/index.ts +20 -0
  58. package/src/schema/project.test.ts +75 -10
  59. package/src/schema/project.ts +134 -27
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { TinybirdClient, createClient } from "./base.js";
3
+ import type { DatasourcesNamespace } from "./types.js";
3
4
 
4
5
  describe("TinybirdClient", () => {
5
6
  describe("constructor", () => {
@@ -137,4 +138,35 @@ describe("TinybirdClient", () => {
137
138
  expect(context.baseUrl).toBe("https://api.tinybird.co");
138
139
  });
139
140
  });
141
+
142
+ describe("datasources", () => {
143
+ it("exposes datasources namespace", () => {
144
+ const client = createClient({
145
+ baseUrl: "https://api.tinybird.co",
146
+ token: "test-token",
147
+ });
148
+
149
+ expect(client.datasources).toBeDefined();
150
+ });
151
+
152
+ it("datasources namespace has append method", () => {
153
+ const client = createClient({
154
+ baseUrl: "https://api.tinybird.co",
155
+ token: "test-token",
156
+ });
157
+
158
+ expect(typeof client.datasources.append).toBe("function");
159
+ });
160
+
161
+ it("datasources conforms to DatasourcesNamespace interface", () => {
162
+ const client = createClient({
163
+ baseUrl: "https://api.tinybird.co",
164
+ token: "test-token",
165
+ });
166
+
167
+ const datasources: DatasourcesNamespace = client.datasources;
168
+ expect(datasources).toBeDefined();
169
+ expect(typeof datasources.append).toBe("function");
170
+ });
171
+ });
140
172
  });
@@ -3,8 +3,11 @@
3
3
  */
4
4
 
5
5
  import type {
6
+ AppendOptions,
7
+ AppendResult,
6
8
  ClientConfig,
7
9
  ClientContext,
10
+ DatasourcesNamespace,
8
11
  QueryResult,
9
12
  IngestResult,
10
13
  QueryOptions,
@@ -12,6 +15,7 @@ import type {
12
15
  } from "./types.js";
13
16
  import { TinybirdError } from "./types.js";
14
17
  import { TinybirdApi, TinybirdApiError } from "../api/api.js";
18
+ import { TokensNamespace } from "./tokens.js";
15
19
 
16
20
  /**
17
21
  * Resolved token info from dev mode
@@ -57,6 +61,14 @@ export class TinybirdClient {
57
61
  private contextPromise: Promise<ClientContext> | null = null;
58
62
  private resolvedContext: ClientContext | null = null;
59
63
 
64
+ /**
65
+ * Datasources namespace for import operations
66
+ */
67
+ readonly datasources: DatasourcesNamespace;
68
+
69
+ /** Token operations (JWT creation, etc.) */
70
+ public readonly tokens: TokensNamespace;
71
+
60
72
  constructor(config: ClientConfig) {
61
73
  // Validate required config
62
74
  if (!config.baseUrl) {
@@ -71,6 +83,54 @@ export class TinybirdClient {
71
83
  ...config,
72
84
  baseUrl: config.baseUrl.replace(/\/$/, ""),
73
85
  };
86
+
87
+ // Initialize datasources namespace
88
+ this.datasources = {
89
+ append: (datasourceName: string, options: AppendOptions): Promise<AppendResult> => {
90
+ return this.appendDatasource(datasourceName, options);
91
+ },
92
+ };
93
+
94
+ // Initialize tokens namespace
95
+ this.tokens = new TokensNamespace(
96
+ () => this.getToken(),
97
+ this.config.baseUrl,
98
+ this.config.fetch,
99
+ this.config.timeout
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Append data to a datasource from a URL or local file
105
+ *
106
+ * @param datasourceName - Name of the datasource
107
+ * @param options - Append options including url or file source
108
+ * @returns Append result
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Append from URL
113
+ * await client.datasources.append('events', {
114
+ * url: 'https://example.com/data.csv',
115
+ * });
116
+ *
117
+ * // Append from local file
118
+ * await client.datasources.append('events', {
119
+ * file: './data/events.ndjson',
120
+ * });
121
+ * ```
122
+ */
123
+ private async appendDatasource(
124
+ datasourceName: string,
125
+ options: AppendOptions
126
+ ): Promise<AppendResult> {
127
+ const token = await this.getToken();
128
+
129
+ try {
130
+ return await this.getApi(token).appendDatasource(datasourceName, options);
131
+ } catch (error) {
132
+ this.rethrowApiError(error);
133
+ }
74
134
  }
75
135
 
76
136
  /**
@@ -0,0 +1,103 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { TinybirdClient } from "./base.js";
3
+
4
+ const mockFetch = vi.fn();
5
+ global.fetch = mockFetch;
6
+
7
+ describe("TokensNamespace", () => {
8
+ beforeEach(() => {
9
+ mockFetch.mockReset();
10
+ });
11
+
12
+ it("uses client custom fetch for createJWT", async () => {
13
+ const customFetch = vi.fn().mockResolvedValueOnce({
14
+ ok: true,
15
+ json: () => Promise.resolve({ token: "jwt-token" }),
16
+ });
17
+
18
+ const client = new TinybirdClient({
19
+ baseUrl: "https://api.tinybird.co",
20
+ token: "p.admin-token",
21
+ fetch: customFetch as typeof fetch,
22
+ });
23
+
24
+ const result = await client.tokens.createJWT({
25
+ name: "user_token",
26
+ expiresAt: 1700000000,
27
+ scopes: [{ type: "PIPES:READ", resource: "analytics_pipe" }],
28
+ });
29
+
30
+ expect(result).toEqual({ token: "jwt-token" });
31
+ expect(customFetch).toHaveBeenCalledTimes(1);
32
+ expect(mockFetch).not.toHaveBeenCalled();
33
+
34
+ const [url, init] = customFetch.mock.calls[0] as [string, RequestInit];
35
+ const parsed = new URL(url);
36
+ expect(parsed.pathname).toBe("/v0/tokens/");
37
+ expect(parsed.searchParams.get("expiration_time")).toBe("1700000000");
38
+ expect(parsed.searchParams.get("from")).toBe("ts-sdk");
39
+ expect(init.method).toBe("POST");
40
+ const headers = new Headers(init.headers);
41
+ expect(headers.get("Authorization")).toBe("Bearer p.admin-token");
42
+ expect(headers.get("Content-Type")).toBe("application/json");
43
+
44
+ const body = JSON.parse(String(init.body)) as {
45
+ name: string;
46
+ scopes: Array<{ type: string; resource: string }>;
47
+ limits?: { rps?: number };
48
+ };
49
+ expect(body).toEqual({
50
+ name: "user_token",
51
+ scopes: [{ type: "PIPES:READ", resource: "analytics_pipe" }],
52
+ });
53
+ });
54
+
55
+ it("sends full JWT payload when limits and scope fields are provided", async () => {
56
+ const customFetch = vi.fn().mockResolvedValueOnce({
57
+ ok: true,
58
+ json: () => Promise.resolve({ token: "jwt-token" }),
59
+ });
60
+
61
+ const client = new TinybirdClient({
62
+ baseUrl: "https://api.tinybird.co",
63
+ token: "p.admin-token",
64
+ fetch: customFetch as typeof fetch,
65
+ });
66
+
67
+ await client.tokens.createJWT({
68
+ name: "tenant_token",
69
+ expiresAt: 1700000000,
70
+ scopes: [
71
+ {
72
+ type: "PIPES:READ",
73
+ resource: "analytics_pipe",
74
+ fixed_params: { tenant_id: 123 },
75
+ },
76
+ ],
77
+ limits: { rps: 10 },
78
+ });
79
+
80
+ const [, init] = customFetch.mock.calls[0] as [string, RequestInit];
81
+ const body = JSON.parse(String(init.body)) as {
82
+ name: string;
83
+ scopes: Array<{
84
+ type: string;
85
+ resource: string;
86
+ fixed_params?: Record<string, unknown>;
87
+ }>;
88
+ limits?: { rps?: number };
89
+ };
90
+
91
+ expect(body).toEqual({
92
+ name: "tenant_token",
93
+ scopes: [
94
+ {
95
+ type: "PIPES:READ",
96
+ resource: "analytics_pipe",
97
+ fixed_params: { tenant_id: 123 },
98
+ },
99
+ ],
100
+ limits: { rps: 10 },
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Token operations namespace for TinybirdClient
3
+ */
4
+
5
+ import type { CreateJWTOptions, CreateJWTResult } from "../api/tokens.js";
6
+ import { createJWT as apiCreateJWT, TokenApiError } from "../api/tokens.js";
7
+ import { TinybirdError } from "./types.js";
8
+
9
+ /**
10
+ * Token operations namespace for TinybirdClient
11
+ */
12
+ export class TokensNamespace {
13
+ constructor(
14
+ private readonly getToken: () => Promise<string>,
15
+ private readonly baseUrl: string,
16
+ private readonly fetchFn?: typeof globalThis.fetch,
17
+ private readonly timeout?: number
18
+ ) {}
19
+
20
+ /**
21
+ * Create a JWT token
22
+ *
23
+ * Creates a short-lived JWT token with specific scopes for secure,
24
+ * time-limited access to pipes and datasources.
25
+ *
26
+ * @param options - JWT creation options
27
+ * @returns The created JWT token
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const result = await client.tokens.createJWT({
32
+ * name: "user_123_token",
33
+ * expiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour
34
+ * scopes: [
35
+ * {
36
+ * type: "PIPES:READ",
37
+ * resource: "user_analytics",
38
+ * fixed_params: { user_id: 123 },
39
+ * },
40
+ * ],
41
+ * });
42
+ *
43
+ * console.log(result.token); // "eyJ..."
44
+ * ```
45
+ */
46
+ async createJWT(options: CreateJWTOptions): Promise<CreateJWTResult> {
47
+ const token = await this.getToken();
48
+
49
+ try {
50
+ return await apiCreateJWT(
51
+ {
52
+ baseUrl: this.baseUrl,
53
+ token,
54
+ fetch: this.fetchFn,
55
+ timeout: this.timeout,
56
+ },
57
+ options
58
+ );
59
+ } catch (error) {
60
+ if (error instanceof TokenApiError) {
61
+ throw new TinybirdError(error.message, error.status, {
62
+ error: error.message,
63
+ status: error.status,
64
+ });
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ }
@@ -189,3 +189,57 @@ export interface TypedDatasourceIngest<TRow> {
189
189
  /** Send multiple events in a batch */
190
190
  sendBatch(events: TRow[], options?: IngestOptions): Promise<IngestResult>;
191
191
  }
192
+
193
+ /**
194
+ * Data format for append operations
195
+ */
196
+ export type AppendFormat = "csv" | "ndjson" | "parquet";
197
+
198
+ /**
199
+ * CSV dialect options for customizing CSV parsing
200
+ */
201
+ export interface CsvDialectOptions {
202
+ /** Field delimiter character (default: ',') */
203
+ delimiter?: string;
204
+ /** New line character(s) (default: '\n') */
205
+ newLine?: string;
206
+ /** Escape character for special characters */
207
+ escapeChar?: string;
208
+ }
209
+
210
+ /**
211
+ * Options for append operation
212
+ * Exactly one of `url` or `file` must be provided
213
+ */
214
+ export interface AppendOptions {
215
+ /** Remote URL to import data from */
216
+ url?: string;
217
+ /** Local file path to import data from */
218
+ file?: string;
219
+ /** CSV dialect options (only applicable for csv format) */
220
+ csvDialect?: CsvDialectOptions;
221
+ /** Request timeout in milliseconds */
222
+ timeout?: number;
223
+ /** AbortController signal for cancellation */
224
+ signal?: AbortSignal;
225
+ }
226
+
227
+ /**
228
+ * Result of an append operation
229
+ */
230
+ export interface AppendResult {
231
+ /** Number of rows successfully appended */
232
+ successful_rows: number;
233
+ /** Number of rows that failed to append (quarantined) */
234
+ quarantined_rows: number;
235
+ /** Import ID for tracking */
236
+ import_id?: string;
237
+ }
238
+
239
+ /**
240
+ * Datasources namespace interface for raw client
241
+ */
242
+ export interface DatasourcesNamespace {
243
+ /** Append data to a datasource from a URL or file */
244
+ append(datasourceName: string, options: AppendOptions): Promise<AppendResult>;
245
+ }
package/src/index.ts CHANGED
@@ -202,8 +202,12 @@ export type {
202
202
  export { TinybirdClient, createClient } from "./client/base.js";
203
203
  export { TinybirdError } from "./client/types.js";
204
204
  export type {
205
+ AppendOptions,
206
+ AppendResult,
205
207
  ClientConfig,
206
208
  ClientContext,
209
+ CsvDialectOptions,
210
+ DatasourcesNamespace,
207
211
  QueryResult,
208
212
  IngestResult,
209
213
  QueryOptions,
@@ -226,7 +230,12 @@ export type {
226
230
  TinybirdApiConfig,
227
231
  TinybirdApiQueryOptions,
228
232
  TinybirdApiIngestOptions,
233
+ TinybirdApiAppendOptions,
229
234
  TinybirdApiRequestInit,
235
+ TinybirdApiTokenScope,
236
+ TinybirdApiCreateTokenRequest,
237
+ TinybirdApiCreateTokenOptions,
238
+ TinybirdApiCreateTokenResult,
230
239
  } from "./api/api.js";
231
240
 
232
241
  // ============ Preview Environment ============
@@ -246,6 +255,17 @@ export {
246
255
  } from "./api/dashboard.js";
247
256
  export type { RegionInfo } from "./api/dashboard.js";
248
257
 
258
+ // ============ Token API ============
259
+ export { createJWT, TokenApiError } from "./api/tokens.js";
260
+ export type {
261
+ TokenApiConfig,
262
+ JWTScope,
263
+ JWTScopeType,
264
+ JWTLimits,
265
+ CreateJWTOptions,
266
+ CreateJWTResult,
267
+ } from "./api/tokens.js";
268
+
249
269
  // ============ Config Types ============
250
270
  // Import from config-types.ts to avoid bundling esbuild in client code
251
271
  export type { TinybirdConfig, DevMode } from "./cli/config-types.js";
@@ -70,7 +70,7 @@ describe("Project Schema", () => {
70
70
  expect(project.pipes.topEvents).toBe(topEvents);
71
71
  });
72
72
 
73
- it("creates tinybird client with query and ingest methods", () => {
73
+ it("creates tinybird client with pipe accessors and ingest methods", () => {
74
74
  const events = defineDatasource("events", {
75
75
  schema: { id: t.string() },
76
76
  });
@@ -86,11 +86,43 @@ describe("Project Schema", () => {
86
86
  pipes: { topEvents },
87
87
  });
88
88
 
89
- expect(project.tinybird.query).toBeDefined();
90
89
  expect(project.tinybird.ingest).toBeDefined();
91
- expect(typeof project.tinybird.query.topEvents).toBe("function");
90
+ expect(typeof project.tinybird.topEvents.query).toBe("function");
92
91
  expect(typeof project.tinybird.ingest.events).toBe("function");
93
92
  expect(typeof project.tinybird.ingest.eventsBatch).toBe("function");
93
+ expect((project.tinybird as unknown as Record<string, unknown>).query).toBeUndefined();
94
+ expect((project.tinybird as unknown as Record<string, unknown>).pipes).toBeUndefined();
95
+ });
96
+
97
+ it("creates datasource accessors with append method", () => {
98
+ const events = defineDatasource("events", {
99
+ schema: { timestamp: t.dateTime() },
100
+ });
101
+
102
+ const project = defineProject({
103
+ datasources: { events },
104
+ });
105
+
106
+ expect(project.tinybird.events).toBeDefined();
107
+ expect(typeof project.tinybird.events.append).toBe("function");
108
+ });
109
+
110
+ it("creates multiple datasource accessors", () => {
111
+ const events = defineDatasource("events", {
112
+ schema: { timestamp: t.dateTime() },
113
+ });
114
+ const pageViews = defineDatasource("page_views", {
115
+ schema: { pathname: t.string() },
116
+ });
117
+
118
+ const project = defineProject({
119
+ datasources: { events, pageViews },
120
+ });
121
+
122
+ expect(project.tinybird.events).toBeDefined();
123
+ expect(project.tinybird.pageViews).toBeDefined();
124
+ expect(typeof project.tinybird.events.append).toBe("function");
125
+ expect(typeof project.tinybird.pageViews.append).toBe("function");
94
126
  });
95
127
 
96
128
  it("throws error when accessing client before initialization", () => {
@@ -113,7 +145,7 @@ describe("Project Schema", () => {
113
145
  });
114
146
 
115
147
  // Cast to any since the type system expects params but stub throws regardless
116
- const queryFn = project.tinybird.query.internalPipe as () => Promise<unknown>;
148
+ const queryFn = project.tinybird.internalPipe.query as () => Promise<unknown>;
117
149
  await expect(queryFn()).rejects.toThrow(
118
150
  'Pipe "internalPipe" is not exposed as an endpoint'
119
151
  );
@@ -246,7 +278,7 @@ describe("Project Schema", () => {
246
278
  process.env.NODE_ENV = originalNodeEnv;
247
279
  });
248
280
 
249
- it("creates a client with query and ingest methods", () => {
281
+ it("creates a client with pipe accessors and ingest methods", () => {
250
282
  const events = defineDatasource("events", {
251
283
  schema: { id: t.string() },
252
284
  });
@@ -262,11 +294,47 @@ describe("Project Schema", () => {
262
294
  pipes: { topEvents },
263
295
  });
264
296
 
265
- expect(client.query).toBeDefined();
266
297
  expect(client.ingest).toBeDefined();
267
- expect(typeof client.query.topEvents).toBe("function");
298
+ expect(client.sql).toBeDefined();
299
+ expect(typeof client.topEvents.query).toBe("function");
268
300
  expect(typeof client.ingest.events).toBe("function");
269
301
  expect(typeof client.ingest.eventsBatch).toBe("function");
302
+ expect(typeof client.sql).toBe("function");
303
+ expect((client as unknown as Record<string, unknown>).query).toBeUndefined();
304
+ expect((client as unknown as Record<string, unknown>).pipes).toBeUndefined();
305
+ });
306
+
307
+ it("creates datasource accessors with append method", () => {
308
+ const events = defineDatasource("events", {
309
+ schema: { id: t.string() },
310
+ });
311
+
312
+ const client = createTinybirdClient({
313
+ datasources: { events },
314
+ pipes: {},
315
+ });
316
+
317
+ expect(client.events).toBeDefined();
318
+ expect(typeof client.events.append).toBe("function");
319
+ });
320
+
321
+ it("creates multiple datasource accessors", () => {
322
+ const events = defineDatasource("events", {
323
+ schema: { id: t.string() },
324
+ });
325
+ const pageViews = defineDatasource("page_views", {
326
+ schema: { pathname: t.string() },
327
+ });
328
+
329
+ const client = createTinybirdClient({
330
+ datasources: { events, pageViews },
331
+ pipes: {},
332
+ });
333
+
334
+ expect(client.events).toBeDefined();
335
+ expect(client.pageViews).toBeDefined();
336
+ expect(typeof client.events.append).toBe("function");
337
+ expect(typeof client.pageViews.append).toBe("function");
270
338
  });
271
339
 
272
340
  it("accepts devMode option", () => {
@@ -281,7 +349,6 @@ describe("Project Schema", () => {
281
349
  devMode: true,
282
350
  });
283
351
 
284
- expect(clientWithDevMode.query).toBeDefined();
285
352
  expect(clientWithDevMode.ingest).toBeDefined();
286
353
 
287
354
  const clientWithoutDevMode = createTinybirdClient({
@@ -290,7 +357,6 @@ describe("Project Schema", () => {
290
357
  devMode: false,
291
358
  });
292
359
 
293
- expect(clientWithoutDevMode.query).toBeDefined();
294
360
  expect(clientWithoutDevMode.ingest).toBeDefined();
295
361
  });
296
362
 
@@ -309,7 +375,6 @@ describe("Project Schema", () => {
309
375
  devMode: true,
310
376
  });
311
377
 
312
- expect(client.query).toBeDefined();
313
378
  expect(client.ingest).toBeDefined();
314
379
  });
315
380