@tinybirdco/sdk 0.0.3 → 0.0.6

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 (122) hide show
  1. package/README.md +87 -14
  2. package/dist/api/deploy.d.ts +41 -3
  3. package/dist/api/deploy.d.ts.map +1 -1
  4. package/dist/api/deploy.js +141 -19
  5. package/dist/api/deploy.js.map +1 -1
  6. package/dist/api/deploy.test.js +77 -29
  7. package/dist/api/deploy.test.js.map +1 -1
  8. package/dist/api/local.d.ts +92 -0
  9. package/dist/api/local.d.ts.map +1 -0
  10. package/dist/api/local.js +176 -0
  11. package/dist/api/local.js.map +1 -0
  12. package/dist/api/local.test.d.ts +2 -0
  13. package/dist/api/local.test.d.ts.map +1 -0
  14. package/dist/api/local.test.js +182 -0
  15. package/dist/api/local.test.js.map +1 -0
  16. package/dist/api/resources.d.ts +178 -0
  17. package/dist/api/resources.d.ts.map +1 -0
  18. package/dist/api/resources.js +244 -0
  19. package/dist/api/resources.js.map +1 -0
  20. package/dist/api/resources.test.d.ts +2 -0
  21. package/dist/api/resources.test.d.ts.map +1 -0
  22. package/dist/api/resources.test.js +255 -0
  23. package/dist/api/resources.test.js.map +1 -0
  24. package/dist/cli/commands/build.d.ts +6 -4
  25. package/dist/cli/commands/build.d.ts.map +1 -1
  26. package/dist/cli/commands/build.js +95 -47
  27. package/dist/cli/commands/build.js.map +1 -1
  28. package/dist/cli/commands/deploy.d.ts +39 -0
  29. package/dist/cli/commands/deploy.d.ts.map +1 -0
  30. package/dist/cli/commands/deploy.js +90 -0
  31. package/dist/cli/commands/deploy.js.map +1 -0
  32. package/dist/cli/commands/dev.d.ts +9 -2
  33. package/dist/cli/commands/dev.d.ts.map +1 -1
  34. package/dist/cli/commands/dev.js +60 -31
  35. package/dist/cli/commands/dev.js.map +1 -1
  36. package/dist/cli/commands/init.d.ts +24 -1
  37. package/dist/cli/commands/init.d.ts.map +1 -1
  38. package/dist/cli/commands/init.js +174 -23
  39. package/dist/cli/commands/init.js.map +1 -1
  40. package/dist/cli/commands/init.test.js +190 -30
  41. package/dist/cli/commands/init.test.js.map +1 -1
  42. package/dist/cli/config.d.ts +14 -0
  43. package/dist/cli/config.d.ts.map +1 -1
  44. package/dist/cli/config.js +7 -0
  45. package/dist/cli/config.js.map +1 -1
  46. package/dist/cli/config.test.js +29 -0
  47. package/dist/cli/config.test.js.map +1 -1
  48. package/dist/cli/index.js +107 -11
  49. package/dist/cli/index.js.map +1 -1
  50. package/dist/cli/utils/package-manager.d.ts +8 -0
  51. package/dist/cli/utils/package-manager.d.ts.map +1 -0
  52. package/dist/cli/utils/package-manager.js +45 -0
  53. package/dist/cli/utils/package-manager.js.map +1 -0
  54. package/dist/cli/utils/package-manager.test.d.ts +2 -0
  55. package/dist/cli/utils/package-manager.test.d.ts.map +1 -0
  56. package/dist/cli/utils/package-manager.test.js +85 -0
  57. package/dist/cli/utils/package-manager.test.js.map +1 -0
  58. package/dist/codegen/index.d.ts +39 -0
  59. package/dist/codegen/index.d.ts.map +1 -0
  60. package/dist/codegen/index.js +300 -0
  61. package/dist/codegen/index.js.map +1 -0
  62. package/dist/codegen/index.test.d.ts +2 -0
  63. package/dist/codegen/index.test.d.ts.map +1 -0
  64. package/dist/codegen/index.test.js +310 -0
  65. package/dist/codegen/index.test.js.map +1 -0
  66. package/dist/codegen/type-mapper.d.ts +20 -0
  67. package/dist/codegen/type-mapper.d.ts.map +1 -0
  68. package/dist/codegen/type-mapper.js +238 -0
  69. package/dist/codegen/type-mapper.js.map +1 -0
  70. package/dist/codegen/type-mapper.test.d.ts +2 -0
  71. package/dist/codegen/type-mapper.test.d.ts.map +1 -0
  72. package/dist/codegen/type-mapper.test.js +167 -0
  73. package/dist/codegen/type-mapper.test.js.map +1 -0
  74. package/dist/codegen/utils.d.ts +46 -0
  75. package/dist/codegen/utils.d.ts.map +1 -0
  76. package/dist/codegen/utils.js +141 -0
  77. package/dist/codegen/utils.js.map +1 -0
  78. package/dist/codegen/utils.test.d.ts +2 -0
  79. package/dist/codegen/utils.test.d.ts.map +1 -0
  80. package/dist/codegen/utils.test.js +178 -0
  81. package/dist/codegen/utils.test.js.map +1 -0
  82. package/dist/generator/index.d.ts +3 -0
  83. package/dist/generator/index.d.ts.map +1 -1
  84. package/dist/generator/index.js +17 -1
  85. package/dist/generator/index.js.map +1 -1
  86. package/dist/generator/index.test.js +104 -1
  87. package/dist/generator/index.test.js.map +1 -1
  88. package/dist/generator/loader.d.ts +15 -0
  89. package/dist/generator/loader.d.ts.map +1 -1
  90. package/dist/generator/loader.js +24 -0
  91. package/dist/generator/loader.js.map +1 -1
  92. package/dist/test/handlers.d.ts +49 -0
  93. package/dist/test/handlers.d.ts.map +1 -1
  94. package/dist/test/handlers.js +45 -0
  95. package/dist/test/handlers.js.map +1 -1
  96. package/package.json +4 -2
  97. package/src/api/deploy.test.ts +135 -34
  98. package/src/api/deploy.ts +203 -23
  99. package/src/api/local.test.ts +250 -0
  100. package/src/api/local.ts +270 -0
  101. package/src/api/resources.test.ts +332 -0
  102. package/src/api/resources.ts +554 -0
  103. package/src/cli/commands/build.ts +115 -53
  104. package/src/cli/commands/deploy.ts +126 -0
  105. package/src/cli/commands/dev.ts +81 -36
  106. package/src/cli/commands/init.test.ts +239 -30
  107. package/src/cli/commands/init.ts +243 -26
  108. package/src/cli/config.test.ts +47 -0
  109. package/src/cli/config.ts +20 -0
  110. package/src/cli/index.ts +120 -11
  111. package/src/cli/utils/package-manager.test.ts +118 -0
  112. package/src/cli/utils/package-manager.ts +44 -0
  113. package/src/codegen/index.test.ts +367 -0
  114. package/src/codegen/index.ts +379 -0
  115. package/src/codegen/type-mapper.test.ts +224 -0
  116. package/src/codegen/type-mapper.ts +265 -0
  117. package/src/codegen/utils.test.ts +221 -0
  118. package/src/codegen/utils.ts +174 -0
  119. package/src/generator/index.test.ts +121 -1
  120. package/src/generator/index.ts +19 -1
  121. package/src/generator/loader.ts +43 -0
  122. package/src/test/handlers.ts +58 -0
@@ -46,6 +46,47 @@ export function createNoChangesResponse() {
46
46
  result: "no_changes",
47
47
  };
48
48
  }
49
+ /**
50
+ * Create deploy success response (for /v1/deploy endpoint)
51
+ * This returns a deployment object, not a build object
52
+ */
53
+ export function createDeploySuccessResponse(options) {
54
+ return {
55
+ result: "success",
56
+ deployment: {
57
+ id: options?.deploymentId ?? "deploy-123",
58
+ status: options?.status ?? "pending",
59
+ },
60
+ };
61
+ }
62
+ /**
63
+ * Create deployment status response (for /v1/deployments/:id endpoint)
64
+ */
65
+ export function createDeploymentStatusResponse(options) {
66
+ return {
67
+ result: "success",
68
+ deployment: {
69
+ id: options?.deploymentId ?? "deploy-123",
70
+ status: options?.status ?? "data_ready",
71
+ },
72
+ };
73
+ }
74
+ /**
75
+ * Create set-live success response (for /v1/deployments/:id/set-live endpoint)
76
+ */
77
+ export function createSetLiveSuccessResponse() {
78
+ return {
79
+ result: "success",
80
+ };
81
+ }
82
+ /**
83
+ * Create deployments list response (for /v1/deployments endpoint)
84
+ */
85
+ export function createDeploymentsListResponse(options) {
86
+ return {
87
+ deployments: options?.deployments ?? [],
88
+ };
89
+ }
49
90
  /**
50
91
  * Default handlers for build and deploy endpoints
51
92
  */
@@ -58,5 +99,9 @@ export const handlers = [
58
99
  http.post(`${BASE_URL}/v1/deploy`, () => {
59
100
  return HttpResponse.json(createBuildSuccessResponse());
60
101
  }),
102
+ // Deployments list endpoint - empty list by default (no stale deployments)
103
+ http.get(`${BASE_URL}/v1/deployments`, () => {
104
+ return HttpResponse.json(createDeploymentsListResponse());
105
+ }),
61
106
  ];
62
107
  //# sourceMappingURL=handlers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../src/test/handlers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,KAAK,CAAC;AAEzC,MAAM,CAAC,MAAM,QAAQ,GAAG,yBAAyB,CAAC;AAElD;;GAEG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAQ1C;IACC,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,KAAK,EAAE;YACL,EAAE,EAAE,OAAO,EAAE,OAAO,IAAI,WAAW;YACnC,kBAAkB,EAAE,OAAO,EAAE,YAAY,IAAI,EAAE;YAC/C,cAAc,EAAE,OAAO,EAAE,QAAQ,IAAI,EAAE;YACvC,kBAAkB,EAAE,OAAO,EAAE,YAAY,IAAI,EAAE;YAC/C,wBAAwB,EAAE,OAAO,EAAE,kBAAkB,IAAI,EAAE;YAC3D,oBAAoB,EAAE,OAAO,EAAE,cAAc,IAAI,EAAE;YACnD,wBAAwB,EAAE,OAAO,EAAE,kBAAkB,IAAI,EAAE;SAC5D;KACF,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAAa;IACtD,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,KAAK;KACN,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iCAAiC,CAC/C,MAAmD;IAEnD,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,MAAM;KACP,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO;QACL,MAAM,EAAE,YAAY;KACrB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,sCAAsC;IACtC,IAAI,CAAC,IAAI,CAAC,GAAG,QAAQ,WAAW,EAAE,GAAG,EAAE;QACrC,OAAO,YAAY,CAAC,IAAI,CAAC,0BAA0B,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC;IAEF,uCAAuC;IACvC,IAAI,CAAC,IAAI,CAAC,GAAG,QAAQ,YAAY,EAAE,GAAG,EAAE;QACtC,OAAO,YAAY,CAAC,IAAI,CAAC,0BAA0B,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC;CACH,CAAC"}
1
+ {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../src/test/handlers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,KAAK,CAAC;AAEzC,MAAM,CAAC,MAAM,QAAQ,GAAG,yBAAyB,CAAC;AAElD;;GAEG;AACH,MAAM,UAAU,0BAA0B,CAAC,OAQ1C;IACC,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,KAAK,EAAE;YACL,EAAE,EAAE,OAAO,EAAE,OAAO,IAAI,WAAW;YACnC,kBAAkB,EAAE,OAAO,EAAE,YAAY,IAAI,EAAE;YAC/C,cAAc,EAAE,OAAO,EAAE,QAAQ,IAAI,EAAE;YACvC,kBAAkB,EAAE,OAAO,EAAE,YAAY,IAAI,EAAE;YAC/C,wBAAwB,EAAE,OAAO,EAAE,kBAAkB,IAAI,EAAE;YAC3D,oBAAoB,EAAE,OAAO,EAAE,cAAc,IAAI,EAAE;YACnD,wBAAwB,EAAE,OAAO,EAAE,kBAAkB,IAAI,EAAE;SAC5D;KACF,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAAa;IACtD,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,KAAK;KACN,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iCAAiC,CAC/C,MAAmD;IAEnD,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,MAAM;KACP,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO;QACL,MAAM,EAAE,YAAY;KACrB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,OAG3C;IACC,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,UAAU,EAAE;YACV,EAAE,EAAE,OAAO,EAAE,YAAY,IAAI,YAAY;YACzC,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,SAAS;SACrC;KACF,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,8BAA8B,CAAC,OAG9C;IACC,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,UAAU,EAAE;YACV,EAAE,EAAE,OAAO,EAAE,YAAY,IAAI,YAAY;YACzC,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,YAAY;SACxC;KACF,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,4BAA4B;IAC1C,OAAO;QACL,MAAM,EAAE,SAAS;KAClB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,6BAA6B,CAAC,OAE7C;IACC,OAAO;QACL,WAAW,EAAE,OAAO,EAAE,WAAW,IAAI,EAAE;KACxC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,sCAAsC;IACtC,IAAI,CAAC,IAAI,CAAC,GAAG,QAAQ,WAAW,EAAE,GAAG,EAAE;QACrC,OAAO,YAAY,CAAC,IAAI,CAAC,0BAA0B,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC;IAEF,uCAAuC;IACvC,IAAI,CAAC,IAAI,CAAC,GAAG,QAAQ,YAAY,EAAE,GAAG,EAAE;QACtC,OAAO,YAAY,CAAC,IAAI,CAAC,0BAA0B,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC;IAEF,2EAA2E;IAC3E,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,iBAAiB,EAAE,GAAG,EAAE;QAC1C,OAAO,YAAY,CAAC,IAAI,CAAC,6BAA6B,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC;CACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinybirdco/sdk",
3
- "version": "0.0.3",
3
+ "version": "0.0.6",
4
4
  "description": "TypeScript SDK for Tinybird Forward - define datasources and pipes as TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,10 +31,12 @@
31
31
  "author": "Tinybird",
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
+ "@clack/prompts": "^1.0.0",
34
35
  "chokidar": "^4.0.0",
35
36
  "commander": "^12.0.0",
36
37
  "dotenv": "^16.0.0",
37
- "esbuild": "^0.24.0"
38
+ "esbuild": "^0.24.0",
39
+ "picocolors": "^1.1.1"
38
40
  },
39
41
  "devDependencies": {
40
42
  "@types/node": "^20.0.0",
@@ -1,20 +1,30 @@
1
- import { describe, it, expect, beforeAll, afterEach, afterAll } from "vitest";
1
+ import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest";
2
2
  import { setupServer } from "msw/node";
3
3
  import { http, HttpResponse } from "msw";
4
4
  import { deployToMain } from "./deploy.js";
5
5
  import type { BuildConfig } from "./build.js";
6
6
  import {
7
7
  BASE_URL,
8
- createBuildSuccessResponse,
8
+ createDeploySuccessResponse,
9
+ createDeploymentStatusResponse,
10
+ createSetLiveSuccessResponse,
9
11
  createBuildFailureResponse,
10
12
  createBuildMultipleErrorsResponse,
11
- createNoChangesResponse,
13
+ createDeploymentsListResponse,
12
14
  } from "../test/handlers.js";
13
15
  import type { GeneratedResources } from "../generator/index.js";
14
16
 
15
17
  const server = setupServer();
16
18
 
17
19
  beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
20
+ beforeEach(() => {
21
+ // Set up default handler for deployments list (used by stale deployment cleanup)
22
+ server.use(
23
+ http.get(`${BASE_URL}/v1/deployments`, () => {
24
+ return HttpResponse.json(createDeploymentsListResponse());
25
+ })
26
+ );
27
+ });
18
28
  afterEach(() => server.resetHandlers());
19
29
  afterAll(() => server.close());
20
30
 
@@ -34,42 +44,64 @@ describe("Deploy API", () => {
34
44
  connections: [],
35
45
  };
36
46
 
47
+ // Helper to set up successful deploy flow
48
+ function setupSuccessfulDeployFlow(deploymentId = "deploy-abc") {
49
+ server.use(
50
+ http.post(`${BASE_URL}/v1/deploy`, () => {
51
+ return HttpResponse.json(
52
+ createDeploySuccessResponse({ deploymentId, status: "pending" })
53
+ );
54
+ }),
55
+ http.get(`${BASE_URL}/v1/deployments/${deploymentId}`, () => {
56
+ return HttpResponse.json(
57
+ createDeploymentStatusResponse({ deploymentId, status: "data_ready" })
58
+ );
59
+ }),
60
+ http.post(`${BASE_URL}/v1/deployments/${deploymentId}/set-live`, () => {
61
+ return HttpResponse.json(createSetLiveSuccessResponse());
62
+ })
63
+ );
64
+ }
65
+
37
66
  describe("deployToMain", () => {
38
- it("successfully deploys resources", async () => {
39
- server.use(
40
- http.post(`${BASE_URL}/v1/deploy`, () => {
41
- return HttpResponse.json(
42
- createBuildSuccessResponse({
43
- buildId: "deploy-abc",
44
- newPipes: ["top_events"],
45
- newDatasources: ["events"],
46
- })
47
- );
48
- })
49
- );
67
+ it("successfully deploys resources with full flow", async () => {
68
+ setupSuccessfulDeployFlow("deploy-abc");
50
69
 
51
- const result = await deployToMain(config, resources);
70
+ const result = await deployToMain(config, resources, { pollIntervalMs: 1 });
52
71
 
53
72
  expect(result.success).toBe(true);
54
73
  expect(result.result).toBe("success");
55
74
  expect(result.buildId).toBe("deploy-abc");
56
75
  expect(result.datasourceCount).toBe(1);
57
76
  expect(result.pipeCount).toBe(1);
58
- expect(result.pipes?.created).toEqual(["top_events"]);
59
- expect(result.datasources?.created).toEqual(["events"]);
60
77
  });
61
78
 
62
- it("handles no changes response", async () => {
79
+ it("polls until deployment is ready", async () => {
80
+ let pollCount = 0;
81
+
63
82
  server.use(
64
83
  http.post(`${BASE_URL}/v1/deploy`, () => {
65
- return HttpResponse.json(createNoChangesResponse());
84
+ return HttpResponse.json(
85
+ createDeploySuccessResponse({ deploymentId: "deploy-poll", status: "pending" })
86
+ );
87
+ }),
88
+ http.get(`${BASE_URL}/v1/deployments/deploy-poll`, () => {
89
+ pollCount++;
90
+ // Return pending for first 2 polls, then data_ready
91
+ const status = pollCount < 3 ? "pending" : "data_ready";
92
+ return HttpResponse.json(
93
+ createDeploymentStatusResponse({ deploymentId: "deploy-poll", status })
94
+ );
95
+ }),
96
+ http.post(`${BASE_URL}/v1/deployments/deploy-poll/set-live`, () => {
97
+ return HttpResponse.json(createSetLiveSuccessResponse());
66
98
  })
67
99
  );
68
100
 
69
- const result = await deployToMain(config, resources);
101
+ const result = await deployToMain(config, resources, { pollIntervalMs: 1 });
70
102
 
71
103
  expect(result.success).toBe(true);
72
- expect(result.result).toBe("no_changes");
104
+ expect(pollCount).toBe(3);
73
105
  });
74
106
 
75
107
  it("handles deploy failure with single error", async () => {
@@ -146,31 +178,66 @@ describe("Deploy API", () => {
146
178
  server.use(
147
179
  http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
148
180
  capturedUrl = request.url;
149
- return HttpResponse.json(createBuildSuccessResponse());
181
+ return HttpResponse.json(
182
+ createDeploySuccessResponse({ deploymentId: "deploy-url-test" })
183
+ );
184
+ }),
185
+ http.get(`${BASE_URL}/v1/deployments/deploy-url-test`, () => {
186
+ return HttpResponse.json(
187
+ createDeploymentStatusResponse({ deploymentId: "deploy-url-test", status: "data_ready" })
188
+ );
189
+ }),
190
+ http.post(`${BASE_URL}/v1/deployments/deploy-url-test/set-live`, () => {
191
+ return HttpResponse.json(createSetLiveSuccessResponse());
150
192
  })
151
193
  );
152
194
 
153
- await deployToMain(config, resources);
195
+ await deployToMain(config, resources, { pollIntervalMs: 1 });
154
196
 
155
197
  expect(capturedUrl).toBe(`${BASE_URL}/v1/deploy`);
156
198
  });
157
199
 
158
- it("tracks changed and deleted resources", async () => {
200
+ it("handles failed deployment status", async () => {
159
201
  server.use(
160
202
  http.post(`${BASE_URL}/v1/deploy`, () => {
161
203
  return HttpResponse.json(
162
- createBuildSuccessResponse({
163
- changedPipes: ["top_events"],
164
- deletedDatasources: ["old_ds"],
165
- })
204
+ createDeploySuccessResponse({ deploymentId: "deploy-fail", status: "pending" })
205
+ );
206
+ }),
207
+ http.get(`${BASE_URL}/v1/deployments/deploy-fail`, () => {
208
+ return HttpResponse.json(
209
+ createDeploymentStatusResponse({ deploymentId: "deploy-fail", status: "failed" })
166
210
  );
167
211
  })
168
212
  );
169
213
 
170
- const result = await deployToMain(config, resources);
214
+ const result = await deployToMain(config, resources, { pollIntervalMs: 1 });
171
215
 
172
- expect(result.pipes?.changed).toEqual(["top_events"]);
173
- expect(result.datasources?.deleted).toEqual(["old_ds"]);
216
+ expect(result.success).toBe(false);
217
+ expect(result.error).toContain("Deployment failed with status: failed");
218
+ });
219
+
220
+ it("handles set-live failure", async () => {
221
+ server.use(
222
+ http.post(`${BASE_URL}/v1/deploy`, () => {
223
+ return HttpResponse.json(
224
+ createDeploySuccessResponse({ deploymentId: "deploy-setlive-fail" })
225
+ );
226
+ }),
227
+ http.get(`${BASE_URL}/v1/deployments/deploy-setlive-fail`, () => {
228
+ return HttpResponse.json(
229
+ createDeploymentStatusResponse({ deploymentId: "deploy-setlive-fail", status: "data_ready" })
230
+ );
231
+ }),
232
+ http.post(`${BASE_URL}/v1/deployments/deploy-setlive-fail/set-live`, () => {
233
+ return HttpResponse.json({ error: "Set live failed" }, { status: 500 });
234
+ })
235
+ );
236
+
237
+ const result = await deployToMain(config, resources, { pollIntervalMs: 1 });
238
+
239
+ expect(result.success).toBe(false);
240
+ expect(result.error).toContain("Failed to set deployment as live");
174
241
  });
175
242
 
176
243
  it("normalizes baseUrl with trailing slash", async () => {
@@ -179,16 +246,50 @@ describe("Deploy API", () => {
179
246
  server.use(
180
247
  http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
181
248
  capturedUrl = request.url;
182
- return HttpResponse.json(createBuildSuccessResponse());
249
+ return HttpResponse.json(
250
+ createDeploySuccessResponse({ deploymentId: "deploy-slash" })
251
+ );
252
+ }),
253
+ http.get(`${BASE_URL}/v1/deployments/deploy-slash`, () => {
254
+ return HttpResponse.json(
255
+ createDeploymentStatusResponse({ deploymentId: "deploy-slash", status: "data_ready" })
256
+ );
257
+ }),
258
+ http.post(`${BASE_URL}/v1/deployments/deploy-slash/set-live`, () => {
259
+ return HttpResponse.json(createSetLiveSuccessResponse());
183
260
  })
184
261
  );
185
262
 
186
263
  await deployToMain(
187
264
  { ...config, baseUrl: `${BASE_URL}/` },
188
- resources
265
+ resources,
266
+ { pollIntervalMs: 1 }
189
267
  );
190
268
 
191
269
  expect(capturedUrl).toBe(`${BASE_URL}/v1/deploy`);
192
270
  });
271
+
272
+ it("times out when deployment never becomes ready", async () => {
273
+ server.use(
274
+ http.post(`${BASE_URL}/v1/deploy`, () => {
275
+ return HttpResponse.json(
276
+ createDeploySuccessResponse({ deploymentId: "deploy-timeout", status: "pending" })
277
+ );
278
+ }),
279
+ http.get(`${BASE_URL}/v1/deployments/deploy-timeout`, () => {
280
+ return HttpResponse.json(
281
+ createDeploymentStatusResponse({ deploymentId: "deploy-timeout", status: "pending" })
282
+ );
283
+ })
284
+ );
285
+
286
+ const result = await deployToMain(config, resources, {
287
+ pollIntervalMs: 1,
288
+ maxPollAttempts: 3,
289
+ });
290
+
291
+ expect(result.success).toBe(false);
292
+ expect(result.error).toContain("Deployment timed out");
293
+ });
193
294
  });
194
295
  });
package/src/api/deploy.ts CHANGED
@@ -1,17 +1,54 @@
1
1
  /**
2
2
  * Deploy resources to Tinybird main workspace
3
- * Uses the /v1/deploy endpoint (same payload format as /v1/build)
3
+ * Uses the /v1/deploy endpoint to create a deployment, then sets it live
4
4
  */
5
5
 
6
6
  import type { GeneratedResources } from "../generator/index.js";
7
- import type { BuildConfig, BuildApiResult, BuildResponse } from "./build.js";
7
+ import type { BuildConfig, BuildApiResult } from "./build.js";
8
+
9
+ /**
10
+ * Deployment object returned by the /v1/deploy endpoint
11
+ */
12
+ export interface Deployment {
13
+ id: string;
14
+ status: string;
15
+ live?: boolean;
16
+ created_at?: string;
17
+ updated_at?: string;
18
+ }
19
+
20
+ /**
21
+ * Response from /v1/deployments list endpoint
22
+ */
23
+ export interface DeploymentsListResponse {
24
+ deployments: Deployment[];
25
+ }
26
+
27
+ /**
28
+ * Response from /v1/deploy endpoint
29
+ */
30
+ export interface DeployResponse {
31
+ result: "success" | "failed";
32
+ deployment?: Deployment;
33
+ error?: string;
34
+ errors?: Array<{ filename?: string; error: string }>;
35
+ }
36
+
37
+ /**
38
+ * Response from /v1/deployments/{id} endpoint
39
+ */
40
+ export interface DeploymentStatusResponse {
41
+ result: string;
42
+ deployment: Deployment;
43
+ }
8
44
 
9
45
  /**
10
46
  * Deploy generated resources to Tinybird main workspace
11
47
  *
12
48
  * Uses the /v1/deploy endpoint which accepts all resources in a single
13
- * multipart form request. This is used for deploying to the main workspace
14
- * (not branches).
49
+ * multipart form request. After creating the deployment, this function:
50
+ * 1. Polls until the deployment is ready (status === 'data_ready')
51
+ * 2. Sets the deployment as live via /v1/deployments/{id}/set-live
15
52
  *
16
53
  * @param config - Build configuration with API URL and token
17
54
  * @param resources - Generated resources to deploy
@@ -38,9 +75,13 @@ import type { BuildConfig, BuildApiResult, BuildResponse } from "./build.js";
38
75
  export async function deployToMain(
39
76
  config: BuildConfig,
40
77
  resources: GeneratedResources,
41
- options?: { debug?: boolean }
78
+ options?: { debug?: boolean; pollIntervalMs?: number; maxPollAttempts?: number }
42
79
  ): Promise<BuildApiResult> {
43
80
  const debug = options?.debug ?? !!process.env.TINYBIRD_DEBUG;
81
+ const pollIntervalMs = options?.pollIntervalMs ?? 1000;
82
+ const maxPollAttempts = options?.maxPollAttempts ?? 120; // 2 minutes max
83
+ const baseUrl = config.baseUrl.replace(/\/$/, "");
84
+
44
85
  const formData = new FormData();
45
86
 
46
87
  // Add datasources
@@ -73,14 +114,48 @@ export async function deployToMain(
73
114
  );
74
115
  }
75
116
 
76
- // Make the request to /v1/deploy (instead of /v1/build)
77
- const url = `${config.baseUrl.replace(/\/$/, "")}/v1/deploy`;
117
+ // Step 0: Clean up any stale non-live deployments that might block the new deployment
118
+ try {
119
+ const deploymentsUrl = `${baseUrl}/v1/deployments`;
120
+ const deploymentsResponse = await fetch(deploymentsUrl, {
121
+ headers: {
122
+ Authorization: `Bearer ${config.token}`,
123
+ },
124
+ });
125
+
126
+ if (deploymentsResponse.ok) {
127
+ const deploymentsBody = (await deploymentsResponse.json()) as DeploymentsListResponse;
128
+ const staleDeployments = deploymentsBody.deployments.filter(
129
+ (d) => !d.live && d.status !== "live"
130
+ );
131
+
132
+ for (const stale of staleDeployments) {
133
+ if (debug) {
134
+ console.log(`[debug] Cleaning up stale deployment: ${stale.id} (status: ${stale.status})`);
135
+ }
136
+ await fetch(`${baseUrl}/v1/deployments/${stale.id}`, {
137
+ method: "DELETE",
138
+ headers: {
139
+ Authorization: `Bearer ${config.token}`,
140
+ },
141
+ });
142
+ }
143
+ }
144
+ } catch (e) {
145
+ // Ignore errors during cleanup - we'll try to deploy anyway
146
+ if (debug) {
147
+ console.log(`[debug] Failed to clean up stale deployments: ${e}`);
148
+ }
149
+ }
150
+
151
+ // Step 1: Create deployment via /v1/deploy
152
+ const deployUrl = `${baseUrl}/v1/deploy`;
78
153
 
79
154
  if (debug) {
80
- console.log(`[debug] POST ${url}`);
155
+ console.log(`[debug] POST ${deployUrl}`);
81
156
  }
82
157
 
83
- const response = await fetch(url, {
158
+ const response = await fetch(deployUrl, {
84
159
  method: "POST",
85
160
  headers: {
86
161
  Authorization: `Bearer ${config.token}`,
@@ -89,7 +164,7 @@ export async function deployToMain(
89
164
  });
90
165
 
91
166
  // Parse response
92
- let body: BuildResponse;
167
+ let body: DeployResponse;
93
168
  const rawBody = await response.text();
94
169
 
95
170
  if (debug) {
@@ -98,7 +173,7 @@ export async function deployToMain(
98
173
  }
99
174
 
100
175
  try {
101
- body = JSON.parse(rawBody) as BuildResponse;
176
+ body = JSON.parse(rawBody) as DeployResponse;
102
177
  } catch {
103
178
  throw new Error(
104
179
  `Failed to parse response from Tinybird API: ${response.status} ${response.statusText}\nBody: ${rawBody}`
@@ -131,7 +206,7 @@ export async function deployToMain(
131
206
  }
132
207
 
133
208
  // Handle API result
134
- if (body.result === "failed") {
209
+ if (body.result === "failed" || !body.deployment) {
135
210
  return {
136
211
  success: false,
137
212
  result: "failed",
@@ -142,25 +217,130 @@ export async function deployToMain(
142
217
  };
143
218
  }
144
219
 
220
+ const deploymentId = body.deployment.id;
221
+
222
+ if (debug) {
223
+ console.log(`[debug] Deployment created with ID: ${deploymentId}`);
224
+ }
225
+
226
+ // Step 2: Poll until deployment is ready
227
+ let deployment = body.deployment;
228
+ let attempts = 0;
229
+
230
+ while (deployment.status !== "data_ready" && attempts < maxPollAttempts) {
231
+ await sleep(pollIntervalMs);
232
+ attempts++;
233
+
234
+ if (debug) {
235
+ console.log(`[debug] Polling deployment status (attempt ${attempts})...`);
236
+ }
237
+
238
+ const statusUrl = `${baseUrl}/v1/deployments/${deploymentId}`;
239
+ const statusResponse = await fetch(statusUrl, {
240
+ headers: {
241
+ Authorization: `Bearer ${config.token}`,
242
+ },
243
+ });
244
+
245
+ if (!statusResponse.ok) {
246
+ return {
247
+ success: false,
248
+ result: "failed",
249
+ error: `Failed to check deployment status: ${statusResponse.status} ${statusResponse.statusText}`,
250
+ datasourceCount: resources.datasources.length,
251
+ pipeCount: resources.pipes.length,
252
+ connectionCount: resources.connections?.length ?? 0,
253
+ buildId: deploymentId,
254
+ };
255
+ }
256
+
257
+ const statusBody = (await statusResponse.json()) as DeploymentStatusResponse;
258
+ deployment = statusBody.deployment;
259
+
260
+ if (debug) {
261
+ console.log(`[debug] Deployment status: ${deployment.status}`);
262
+ }
263
+
264
+ // Check for failed status
265
+ if (deployment.status === "failed" || deployment.status === "error") {
266
+ return {
267
+ success: false,
268
+ result: "failed",
269
+ error: `Deployment failed with status: ${deployment.status}`,
270
+ datasourceCount: resources.datasources.length,
271
+ pipeCount: resources.pipes.length,
272
+ connectionCount: resources.connections?.length ?? 0,
273
+ buildId: deploymentId,
274
+ };
275
+ }
276
+ }
277
+
278
+ if (deployment.status !== "data_ready") {
279
+ return {
280
+ success: false,
281
+ result: "failed",
282
+ error: `Deployment timed out after ${maxPollAttempts} attempts. Last status: ${deployment.status}`,
283
+ datasourceCount: resources.datasources.length,
284
+ pipeCount: resources.pipes.length,
285
+ connectionCount: resources.connections?.length ?? 0,
286
+ buildId: deploymentId,
287
+ };
288
+ }
289
+
290
+ // Step 3: Set the deployment as live
291
+ const setLiveUrl = `${baseUrl}/v1/deployments/${deploymentId}/set-live`;
292
+
293
+ if (debug) {
294
+ console.log(`[debug] POST ${setLiveUrl}`);
295
+ }
296
+
297
+ const setLiveResponse = await fetch(setLiveUrl, {
298
+ method: "POST",
299
+ headers: {
300
+ Authorization: `Bearer ${config.token}`,
301
+ },
302
+ });
303
+
304
+ if (!setLiveResponse.ok) {
305
+ const setLiveBody = await setLiveResponse.text();
306
+ return {
307
+ success: false,
308
+ result: "failed",
309
+ error: `Failed to set deployment as live: ${setLiveResponse.status} ${setLiveResponse.statusText}\n${setLiveBody}`,
310
+ datasourceCount: resources.datasources.length,
311
+ pipeCount: resources.pipes.length,
312
+ connectionCount: resources.connections?.length ?? 0,
313
+ buildId: deploymentId,
314
+ };
315
+ }
316
+
317
+ if (debug) {
318
+ console.log(`[debug] Deployment ${deploymentId} is now live`);
319
+ }
320
+
145
321
  return {
146
322
  success: true,
147
- result: body.result,
323
+ result: "success",
148
324
  datasourceCount: resources.datasources.length,
149
325
  pipeCount: resources.pipes.length,
150
326
  connectionCount: resources.connections?.length ?? 0,
151
- buildId: body.build?.id,
327
+ buildId: deploymentId,
152
328
  pipes: {
153
- changed: body.build?.changed_pipe_names ?? [],
154
- created: body.build?.new_pipe_names ?? [],
155
- deleted: body.build?.deleted_pipe_names ?? [],
329
+ changed: [],
330
+ created: [],
331
+ deleted: [],
156
332
  },
157
333
  datasources: {
158
- changed: body.build?.changed_datasource_names ?? [],
159
- created: body.build?.new_datasource_names ?? [],
160
- deleted: body.build?.deleted_datasource_names ?? [],
334
+ changed: [],
335
+ created: [],
336
+ deleted: [],
161
337
  },
162
- // Keep deprecated fields for backwards compatibility
163
- changedPipeNames: body.build?.changed_pipe_names ?? [],
164
- newPipeNames: body.build?.new_pipe_names ?? [],
165
338
  };
166
339
  }
340
+
341
+ /**
342
+ * Helper function to sleep for a given number of milliseconds
343
+ */
344
+ function sleep(ms: number): Promise<void> {
345
+ return new Promise((resolve) => setTimeout(resolve, ms));
346
+ }