@tinybirdco/sdk 0.0.40 → 0.0.42

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 (100) hide show
  1. package/README.md +42 -1
  2. package/dist/api/api.d.ts +2 -0
  3. package/dist/api/api.d.ts.map +1 -1
  4. package/dist/api/api.js +1 -1
  5. package/dist/api/api.js.map +1 -1
  6. package/dist/api/api.test.js +14 -0
  7. package/dist/api/api.test.js.map +1 -1
  8. package/dist/api/resources.d.ts +72 -1
  9. package/dist/api/resources.d.ts.map +1 -1
  10. package/dist/api/resources.js +197 -1
  11. package/dist/api/resources.js.map +1 -1
  12. package/dist/api/resources.test.js +82 -1
  13. package/dist/api/resources.test.js.map +1 -1
  14. package/dist/cli/commands/migrate.d.ts +11 -0
  15. package/dist/cli/commands/migrate.d.ts.map +1 -0
  16. package/dist/cli/commands/migrate.js +196 -0
  17. package/dist/cli/commands/migrate.js.map +1 -0
  18. package/dist/cli/commands/migrate.test.d.ts +2 -0
  19. package/dist/cli/commands/migrate.test.d.ts.map +1 -0
  20. package/dist/cli/commands/migrate.test.js +473 -0
  21. package/dist/cli/commands/migrate.test.js.map +1 -0
  22. package/dist/cli/commands/pull.d.ts +59 -0
  23. package/dist/cli/commands/pull.d.ts.map +1 -0
  24. package/dist/cli/commands/pull.js +104 -0
  25. package/dist/cli/commands/pull.js.map +1 -0
  26. package/dist/cli/commands/pull.test.d.ts +2 -0
  27. package/dist/cli/commands/pull.test.d.ts.map +1 -0
  28. package/dist/cli/commands/pull.test.js +140 -0
  29. package/dist/cli/commands/pull.test.js.map +1 -0
  30. package/dist/cli/index.js +77 -0
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/client/base.d.ts +18 -1
  33. package/dist/client/base.d.ts.map +1 -1
  34. package/dist/client/base.js +43 -2
  35. package/dist/client/base.js.map +1 -1
  36. package/dist/client/base.test.js +5 -1
  37. package/dist/client/base.test.js.map +1 -1
  38. package/dist/client/types.d.ts +4 -0
  39. package/dist/client/types.d.ts.map +1 -1
  40. package/dist/migrate/discovery.d.ts +7 -0
  41. package/dist/migrate/discovery.d.ts.map +1 -0
  42. package/dist/migrate/discovery.js +125 -0
  43. package/dist/migrate/discovery.js.map +1 -0
  44. package/dist/migrate/emit-ts.d.ts +4 -0
  45. package/dist/migrate/emit-ts.d.ts.map +1 -0
  46. package/dist/migrate/emit-ts.js +387 -0
  47. package/dist/migrate/emit-ts.js.map +1 -0
  48. package/dist/migrate/parse-connection.d.ts +3 -0
  49. package/dist/migrate/parse-connection.d.ts.map +1 -0
  50. package/dist/migrate/parse-connection.js +74 -0
  51. package/dist/migrate/parse-connection.js.map +1 -0
  52. package/dist/migrate/parse-datasource.d.ts +3 -0
  53. package/dist/migrate/parse-datasource.d.ts.map +1 -0
  54. package/dist/migrate/parse-datasource.js +324 -0
  55. package/dist/migrate/parse-datasource.js.map +1 -0
  56. package/dist/migrate/parse-pipe.d.ts +3 -0
  57. package/dist/migrate/parse-pipe.d.ts.map +1 -0
  58. package/dist/migrate/parse-pipe.js +332 -0
  59. package/dist/migrate/parse-pipe.js.map +1 -0
  60. package/dist/migrate/parse.d.ts +3 -0
  61. package/dist/migrate/parse.d.ts.map +1 -0
  62. package/dist/migrate/parse.js +18 -0
  63. package/dist/migrate/parse.js.map +1 -0
  64. package/dist/migrate/parser-utils.d.ts +20 -0
  65. package/dist/migrate/parser-utils.d.ts.map +1 -0
  66. package/dist/migrate/parser-utils.js +130 -0
  67. package/dist/migrate/parser-utils.js.map +1 -0
  68. package/dist/migrate/types.d.ts +110 -0
  69. package/dist/migrate/types.d.ts.map +1 -0
  70. package/dist/migrate/types.js +2 -0
  71. package/dist/migrate/types.js.map +1 -0
  72. package/dist/schema/project.d.ts +13 -27
  73. package/dist/schema/project.d.ts.map +1 -1
  74. package/dist/schema/project.js +14 -24
  75. package/dist/schema/project.js.map +1 -1
  76. package/dist/schema/project.test.js +25 -13
  77. package/dist/schema/project.test.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/api/api.test.ts +25 -0
  80. package/src/api/api.ts +3 -1
  81. package/src/api/resources.test.ts +121 -0
  82. package/src/api/resources.ts +292 -1
  83. package/src/cli/commands/migrate.test.ts +564 -0
  84. package/src/cli/commands/migrate.ts +240 -0
  85. package/src/cli/commands/pull.test.ts +173 -0
  86. package/src/cli/commands/pull.ts +177 -0
  87. package/src/cli/index.ts +112 -0
  88. package/src/client/base.test.ts +5 -1
  89. package/src/client/base.ts +56 -2
  90. package/src/client/types.ts +8 -0
  91. package/src/migrate/discovery.ts +151 -0
  92. package/src/migrate/emit-ts.ts +469 -0
  93. package/src/migrate/parse-connection.ts +128 -0
  94. package/src/migrate/parse-datasource.ts +453 -0
  95. package/src/migrate/parse-pipe.ts +518 -0
  96. package/src/migrate/parse.ts +20 -0
  97. package/src/migrate/parser-utils.ts +160 -0
  98. package/src/migrate/types.ts +125 -0
  99. package/src/schema/project.test.ts +25 -13
  100. package/src/schema/project.ts +33 -57
@@ -5,7 +5,13 @@ import {
5
5
  listDatasources,
6
6
  getDatasource,
7
7
  listPipes,
8
+ listPipesV1,
8
9
  getPipe,
10
+ listConnectors,
11
+ getDatasourceFile,
12
+ getPipeFile,
13
+ getConnectorFile,
14
+ pullAllResourceFiles,
9
15
  fetchAllResources,
10
16
  hasResources,
11
17
  ResourceApiError,
@@ -66,6 +72,13 @@ const handlers = [
66
72
  });
67
73
  }),
68
74
 
75
+ // List pipes v1
76
+ http.get(`${BASE_URL}/v1/pipes`, () => {
77
+ return HttpResponse.json({
78
+ pipes: [{ name: "top_events" }, { name: "daily_stats_mv" }],
79
+ });
80
+ }),
81
+
69
82
  // Get pipe detail - endpoint
70
83
  http.get(`${BASE_URL}/v0/pipes/top_events`, () => {
71
84
  return HttpResponse.json({
@@ -119,6 +132,45 @@ const handlers = [
119
132
  ],
120
133
  });
121
134
  }),
135
+
136
+ // List connectors
137
+ http.get(`${BASE_URL}/v0/connectors`, () => {
138
+ return HttpResponse.json({
139
+ connectors: [{ name: "main_kafka" }],
140
+ });
141
+ }),
142
+
143
+ // Raw datasource datafile
144
+ http.get(`${BASE_URL}/v0/datasources/events.datasource`, () => {
145
+ return new HttpResponse(
146
+ "SCHEMA >\n timestamp DateTime,\n event_name String",
147
+ { headers: { "Content-Type": "text/plain" } }
148
+ );
149
+ }),
150
+ http.get(`${BASE_URL}/v0/datasources/users.datasource`, () => {
151
+ return new HttpResponse("SCHEMA >\n user_id String", {
152
+ headers: { "Content-Type": "text/plain" },
153
+ });
154
+ }),
155
+
156
+ // Raw pipe datafiles
157
+ http.get(`${BASE_URL}/v1/pipes/top_events.pipe`, () => {
158
+ return new HttpResponse("NODE endpoint\nSQL >\n SELECT 1", {
159
+ headers: { "Content-Type": "text/plain" },
160
+ });
161
+ }),
162
+ http.get(`${BASE_URL}/v1/pipes/daily_stats_mv.pipe`, () => {
163
+ return new HttpResponse("TYPE MATERIALIZED\nDATASOURCE daily_stats", {
164
+ headers: { "Content-Type": "text/plain" },
165
+ });
166
+ }),
167
+
168
+ // Raw connector datafile
169
+ http.get(`${BASE_URL}/v0/connectors/main_kafka.connection`, () => {
170
+ return new HttpResponse("TYPE kafka\nKAFKA_BOOTSTRAP_SERVERS localhost:9092", {
171
+ headers: { "Content-Type": "text/plain" },
172
+ });
173
+ }),
122
174
  ];
123
175
 
124
176
  const server = setupServer(...handlers);
@@ -207,6 +259,26 @@ describe("listPipes", () => {
207
259
  });
208
260
  });
209
261
 
262
+ describe("listPipesV1", () => {
263
+ it("returns array of pipe names from /v1/pipes", async () => {
264
+ const result = await listPipesV1({ baseUrl: BASE_URL, token: TOKEN });
265
+
266
+ expect(result).toEqual(["top_events", "daily_stats_mv"]);
267
+ });
268
+
269
+ it("falls back to /v0/pipes when /v1/pipes is unavailable", async () => {
270
+ server.use(
271
+ http.get(`${BASE_URL}/v1/pipes`, () => {
272
+ return new HttpResponse(null, { status: 404 });
273
+ })
274
+ );
275
+
276
+ const result = await listPipesV1({ baseUrl: BASE_URL, token: TOKEN });
277
+
278
+ expect(result).toEqual(["top_events", "daily_stats_mv"]);
279
+ });
280
+ });
281
+
210
282
  describe("getPipe", () => {
211
283
  it("returns endpoint pipe info", async () => {
212
284
  const result = await getPipe({ baseUrl: BASE_URL, token: TOKEN }, "top_events");
@@ -264,6 +336,55 @@ describe("fetchAllResources", () => {
264
336
  });
265
337
  });
266
338
 
339
+ describe("raw datafile APIs", () => {
340
+ it("lists connectors", async () => {
341
+ const result = await listConnectors({ baseUrl: BASE_URL, token: TOKEN });
342
+ expect(result).toEqual(["main_kafka"]);
343
+ });
344
+
345
+ it("gets datasource .datasource content", async () => {
346
+ const result = await getDatasourceFile(
347
+ { baseUrl: BASE_URL, token: TOKEN },
348
+ "events"
349
+ );
350
+
351
+ expect(result).toContain("SCHEMA >");
352
+ expect(result).toContain("timestamp DateTime");
353
+ });
354
+
355
+ it("gets pipe .pipe content", async () => {
356
+ const result = await getPipeFile(
357
+ { baseUrl: BASE_URL, token: TOKEN },
358
+ "top_events"
359
+ );
360
+
361
+ expect(result).toContain("NODE endpoint");
362
+ });
363
+
364
+ it("gets connector .connection content", async () => {
365
+ const result = await getConnectorFile(
366
+ { baseUrl: BASE_URL, token: TOKEN },
367
+ "main_kafka"
368
+ );
369
+
370
+ expect(result).toContain("TYPE kafka");
371
+ });
372
+ });
373
+
374
+ describe("pullAllResourceFiles", () => {
375
+ it("pulls datasources, pipes, and connectors as raw datafiles", async () => {
376
+ const result = await pullAllResourceFiles({ baseUrl: BASE_URL, token: TOKEN });
377
+
378
+ expect(result.datasources).toHaveLength(2);
379
+ expect(result.pipes).toHaveLength(2);
380
+ expect(result.connections).toHaveLength(1);
381
+
382
+ expect(result.datasources[0]?.filename).toMatch(/\.datasource$/);
383
+ expect(result.pipes[0]?.filename).toMatch(/\.pipe$/);
384
+ expect(result.connections[0]?.filename).toMatch(/\.connection$/);
385
+ });
386
+ });
387
+
267
388
  describe("hasResources", () => {
268
389
  it("returns true when workspace has resources", async () => {
269
390
  const result = await hasResources({ baseUrl: BASE_URL, token: TOKEN });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Tinybird Resources API client
3
- * Functions to list and fetch datasources and pipes from a workspace
3
+ * Functions to list and fetch resources from a workspace
4
4
  */
5
5
 
6
6
  import type { WorkspaceApiConfig } from "./workspaces.js";
@@ -248,6 +248,36 @@ interface PipeDetailResponse {
248
248
  materialized_datasource?: string;
249
249
  }
250
250
 
251
+ // ============ Connector/Datafile Types ============
252
+
253
+ /**
254
+ * Resource file type returned by pull operations
255
+ */
256
+ export type ResourceFileType = "datasource" | "pipe" | "connection";
257
+
258
+ /**
259
+ * Raw Tinybird datafile pulled from API
260
+ */
261
+ export interface ResourceFile {
262
+ /** Resource name (without extension) */
263
+ name: string;
264
+ /** Resource kind */
265
+ type: ResourceFileType;
266
+ /** Filename with extension */
267
+ filename: string;
268
+ /** Raw datafile content */
269
+ content: string;
270
+ }
271
+
272
+ /**
273
+ * Grouped resource files returned by pull operations
274
+ */
275
+ export interface PulledResourceFiles {
276
+ datasources: ResourceFile[];
277
+ pipes: ResourceFile[];
278
+ connections: ResourceFile[];
279
+ }
280
+
251
281
  // ============ API Helper ============
252
282
 
253
283
  /**
@@ -290,6 +320,107 @@ async function handleApiResponse<T>(
290
320
  return response.json() as Promise<T>;
291
321
  }
292
322
 
323
+ /**
324
+ * Handle API text response and throw appropriate errors
325
+ */
326
+ async function handleApiTextResponse(
327
+ response: Response,
328
+ endpoint: string
329
+ ): Promise<string> {
330
+ if (response.status === 401) {
331
+ throw new ResourceApiError("Invalid or expired token", 401, endpoint);
332
+ }
333
+ if (response.status === 403) {
334
+ throw new ResourceApiError(
335
+ "Insufficient permissions to access resources",
336
+ 403,
337
+ endpoint
338
+ );
339
+ }
340
+ if (response.status === 404) {
341
+ throw new ResourceApiError("Resource not found", 404, endpoint);
342
+ }
343
+ if (!response.ok) {
344
+ const body = await response.text();
345
+ throw new ResourceApiError(
346
+ `API request failed: ${response.status} ${response.statusText}`,
347
+ response.status,
348
+ endpoint,
349
+ body
350
+ );
351
+ }
352
+ return response.text();
353
+ }
354
+
355
+ /**
356
+ * Extract resource names from API response arrays
357
+ */
358
+ function extractNames(
359
+ data: Record<string, unknown>,
360
+ keys: string[]
361
+ ): string[] {
362
+ for (const key of keys) {
363
+ const value = data[key];
364
+ if (!Array.isArray(value)) {
365
+ continue;
366
+ }
367
+
368
+ const names = value
369
+ .map((item) => {
370
+ if (typeof item === "string") {
371
+ return item;
372
+ }
373
+ if (
374
+ typeof item === "object" &&
375
+ item !== null &&
376
+ "name" in item &&
377
+ typeof (item as { name: unknown }).name === "string"
378
+ ) {
379
+ return (item as { name: string }).name;
380
+ }
381
+ return null;
382
+ })
383
+ .filter((name): name is string => name !== null);
384
+
385
+ return names;
386
+ }
387
+
388
+ return [];
389
+ }
390
+
391
+ /**
392
+ * Fetch text resource from the first successful endpoint.
393
+ * Falls back on 404 responses.
394
+ */
395
+ async function fetchTextFromAnyEndpoint(
396
+ config: WorkspaceApiConfig,
397
+ endpoints: string[]
398
+ ): Promise<string> {
399
+ let lastNotFound: ResourceApiError | null = null;
400
+
401
+ for (const endpoint of endpoints) {
402
+ const url = new URL(endpoint, config.baseUrl);
403
+ const response = await tinybirdFetch(url.toString(), {
404
+ method: "GET",
405
+ headers: {
406
+ Authorization: `Bearer ${config.token}`,
407
+ },
408
+ });
409
+
410
+ if (response.status === 404) {
411
+ lastNotFound = new ResourceApiError("Resource not found", 404, endpoint);
412
+ continue;
413
+ }
414
+
415
+ return handleApiTextResponse(response, endpoint);
416
+ }
417
+
418
+ throw (
419
+ lastNotFound ??
420
+ new ResourceApiError("Resource not found", 404, endpoints[0] ?? "unknown")
421
+ );
422
+ }
423
+
293
424
  // ============ Datasource API ============
294
425
 
295
426
  /**
@@ -424,6 +555,117 @@ export async function listPipes(
424
555
  return data.pipes.map((p) => p.name);
425
556
  }
426
557
 
558
+ /**
559
+ * List all pipes from the v1 endpoint.
560
+ * Falls back to v0 when v1 is unavailable.
561
+ *
562
+ * @param config - API configuration
563
+ * @returns Array of pipe names
564
+ */
565
+ export async function listPipesV1(config: WorkspaceApiConfig): Promise<string[]> {
566
+ const endpoint = "/v1/pipes";
567
+ const url = new URL(endpoint, config.baseUrl);
568
+
569
+ const response = await tinybirdFetch(url.toString(), {
570
+ method: "GET",
571
+ headers: {
572
+ Authorization: `Bearer ${config.token}`,
573
+ },
574
+ });
575
+
576
+ // Older/self-hosted versions may not expose /v1/pipes.
577
+ if (response.status === 404) {
578
+ return listPipes(config);
579
+ }
580
+
581
+ const data = await handleApiResponse<Record<string, unknown>>(response, endpoint);
582
+ return extractNames(data, ["pipes", "data"]);
583
+ }
584
+
585
+ /**
586
+ * Get a datasource as native .datasource text
587
+ *
588
+ * @param config - API configuration
589
+ * @param name - Datasource name
590
+ * @returns Raw .datasource content
591
+ */
592
+ export async function getDatasourceFile(
593
+ config: WorkspaceApiConfig,
594
+ name: string
595
+ ): Promise<string> {
596
+ const encoded = encodeURIComponent(name);
597
+ return fetchTextFromAnyEndpoint(config, [
598
+ `/v0/datasources/${encoded}.datasource`,
599
+ `/v0/datasources/${encoded}?format=datasource`,
600
+ ]);
601
+ }
602
+
603
+ /**
604
+ * Get a pipe as native .pipe text
605
+ *
606
+ * @param config - API configuration
607
+ * @param name - Pipe name
608
+ * @returns Raw .pipe content
609
+ */
610
+ export async function getPipeFile(
611
+ config: WorkspaceApiConfig,
612
+ name: string
613
+ ): Promise<string> {
614
+ const encoded = encodeURIComponent(name);
615
+ return fetchTextFromAnyEndpoint(config, [
616
+ `/v1/pipes/${encoded}.pipe`,
617
+ `/v0/pipes/${encoded}.pipe`,
618
+ `/v1/pipes/${encoded}?format=pipe`,
619
+ `/v0/pipes/${encoded}?format=pipe`,
620
+ ]);
621
+ }
622
+
623
+ /**
624
+ * List all connectors in the workspace
625
+ *
626
+ * @param config - API configuration
627
+ * @returns Array of connector names
628
+ */
629
+ export async function listConnectors(
630
+ config: WorkspaceApiConfig
631
+ ): Promise<string[]> {
632
+ const endpoint = "/v0/connectors";
633
+ const url = new URL(endpoint, config.baseUrl);
634
+
635
+ const response = await tinybirdFetch(url.toString(), {
636
+ method: "GET",
637
+ headers: {
638
+ Authorization: `Bearer ${config.token}`,
639
+ },
640
+ });
641
+
642
+ // Not all workspaces expose connectors. Treat missing endpoint as no connectors.
643
+ if (response.status === 404) {
644
+ return [];
645
+ }
646
+
647
+ const data = await handleApiResponse<Record<string, unknown>>(response, endpoint);
648
+ return extractNames(data, ["connectors", "connections"]);
649
+ }
650
+
651
+ /**
652
+ * Get a connector as native .connection text
653
+ *
654
+ * @param config - API configuration
655
+ * @param name - Connector name
656
+ * @returns Raw .connection content
657
+ */
658
+ export async function getConnectorFile(
659
+ config: WorkspaceApiConfig,
660
+ name: string
661
+ ): Promise<string> {
662
+ const encoded = encodeURIComponent(name);
663
+ return fetchTextFromAnyEndpoint(config, [
664
+ `/v0/connectors/${encoded}.connection`,
665
+ `/v0/connectors/${encoded}?format=connection`,
666
+ ]);
667
+ }
668
+
427
669
  /**
428
670
  * Get detailed information about a specific pipe
429
671
  *
@@ -541,6 +783,55 @@ export async function fetchAllResources(
541
783
  return { datasources, pipes };
542
784
  }
543
785
 
786
+ /**
787
+ * Pull all datasource/pipe/connector datafiles from a workspace
788
+ *
789
+ * @param config - API configuration
790
+ * @returns Raw resource files grouped by type
791
+ */
792
+ export async function pullAllResourceFiles(
793
+ config: WorkspaceApiConfig
794
+ ): Promise<PulledResourceFiles> {
795
+ const [datasourceNames, pipeNames, connectorNames] = await Promise.all([
796
+ listDatasources(config),
797
+ listPipesV1(config),
798
+ listConnectors(config),
799
+ ]);
800
+
801
+ const [datasources, pipes, connections] = await Promise.all([
802
+ Promise.all(
803
+ datasourceNames.map(async (name): Promise<ResourceFile> => ({
804
+ name,
805
+ type: "datasource",
806
+ filename: `${name}.datasource`,
807
+ content: await getDatasourceFile(config, name),
808
+ }))
809
+ ),
810
+ Promise.all(
811
+ pipeNames.map(async (name): Promise<ResourceFile> => ({
812
+ name,
813
+ type: "pipe",
814
+ filename: `${name}.pipe`,
815
+ content: await getPipeFile(config, name),
816
+ }))
817
+ ),
818
+ Promise.all(
819
+ connectorNames.map(async (name): Promise<ResourceFile> => ({
820
+ name,
821
+ type: "connection",
822
+ filename: `${name}.connection`,
823
+ content: await getConnectorFile(config, name),
824
+ }))
825
+ ),
826
+ ]);
827
+
828
+ return {
829
+ datasources,
830
+ pipes,
831
+ connections,
832
+ };
833
+ }
834
+
544
835
  /**
545
836
  * Check if a workspace has any resources
546
837
  *