@tinybirdco/sdk 0.0.1

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 (258) hide show
  1. package/README.md +518 -0
  2. package/bin/tinybird.js +7 -0
  3. package/dist/api/branches.d.ts +98 -0
  4. package/dist/api/branches.d.ts.map +1 -0
  5. package/dist/api/branches.js +203 -0
  6. package/dist/api/branches.js.map +1 -0
  7. package/dist/api/branches.test.d.ts +2 -0
  8. package/dist/api/branches.test.d.ts.map +1 -0
  9. package/dist/api/branches.test.js +286 -0
  10. package/dist/api/branches.test.js.map +1 -0
  11. package/dist/api/build.d.ts +130 -0
  12. package/dist/api/build.d.ts.map +1 -0
  13. package/dist/api/build.js +143 -0
  14. package/dist/api/build.js.map +1 -0
  15. package/dist/api/build.test.d.ts +2 -0
  16. package/dist/api/build.test.d.ts.map +1 -0
  17. package/dist/api/build.test.js +138 -0
  18. package/dist/api/build.test.js.map +1 -0
  19. package/dist/api/deploy.d.ts +39 -0
  20. package/dist/api/deploy.d.ts.map +1 -0
  21. package/dist/api/deploy.js +135 -0
  22. package/dist/api/deploy.js.map +1 -0
  23. package/dist/api/deploy.test.d.ts +2 -0
  24. package/dist/api/deploy.test.d.ts.map +1 -0
  25. package/dist/api/deploy.test.js +118 -0
  26. package/dist/api/deploy.test.js.map +1 -0
  27. package/dist/api/workspaces.d.ts +46 -0
  28. package/dist/api/workspaces.d.ts.map +1 -0
  29. package/dist/api/workspaces.js +39 -0
  30. package/dist/api/workspaces.js.map +1 -0
  31. package/dist/api/workspaces.test.d.ts +2 -0
  32. package/dist/api/workspaces.test.d.ts.map +1 -0
  33. package/dist/api/workspaces.test.js +65 -0
  34. package/dist/api/workspaces.test.js.map +1 -0
  35. package/dist/cli/auth.d.ts +86 -0
  36. package/dist/cli/auth.d.ts.map +1 -0
  37. package/dist/cli/auth.js +284 -0
  38. package/dist/cli/auth.js.map +1 -0
  39. package/dist/cli/branch-store.d.ts +53 -0
  40. package/dist/cli/branch-store.d.ts.map +1 -0
  41. package/dist/cli/branch-store.js +91 -0
  42. package/dist/cli/branch-store.js.map +1 -0
  43. package/dist/cli/branch-store.test.d.ts +2 -0
  44. package/dist/cli/branch-store.test.d.ts.map +1 -0
  45. package/dist/cli/branch-store.test.js +115 -0
  46. package/dist/cli/branch-store.test.js.map +1 -0
  47. package/dist/cli/commands/branch.d.ts +82 -0
  48. package/dist/cli/commands/branch.d.ts.map +1 -0
  49. package/dist/cli/commands/branch.js +215 -0
  50. package/dist/cli/commands/branch.js.map +1 -0
  51. package/dist/cli/commands/build.d.ts +43 -0
  52. package/dist/cli/commands/build.d.ts.map +1 -0
  53. package/dist/cli/commands/build.js +138 -0
  54. package/dist/cli/commands/build.js.map +1 -0
  55. package/dist/cli/commands/dev.d.ts +78 -0
  56. package/dist/cli/commands/dev.d.ts.map +1 -0
  57. package/dist/cli/commands/dev.js +226 -0
  58. package/dist/cli/commands/dev.js.map +1 -0
  59. package/dist/cli/commands/init.d.ts +45 -0
  60. package/dist/cli/commands/init.d.ts.map +1 -0
  61. package/dist/cli/commands/init.js +277 -0
  62. package/dist/cli/commands/init.js.map +1 -0
  63. package/dist/cli/commands/init.test.d.ts +2 -0
  64. package/dist/cli/commands/init.test.d.ts.map +1 -0
  65. package/dist/cli/commands/init.test.js +158 -0
  66. package/dist/cli/commands/init.test.js.map +1 -0
  67. package/dist/cli/commands/login.d.ts +37 -0
  68. package/dist/cli/commands/login.d.ts.map +1 -0
  69. package/dist/cli/commands/login.js +64 -0
  70. package/dist/cli/commands/login.js.map +1 -0
  71. package/dist/cli/config.d.ts +114 -0
  72. package/dist/cli/config.d.ts.map +1 -0
  73. package/dist/cli/config.js +258 -0
  74. package/dist/cli/config.js.map +1 -0
  75. package/dist/cli/config.test.d.ts +2 -0
  76. package/dist/cli/config.test.d.ts.map +1 -0
  77. package/dist/cli/config.test.js +243 -0
  78. package/dist/cli/config.test.js.map +1 -0
  79. package/dist/cli/env.d.ts +29 -0
  80. package/dist/cli/env.d.ts.map +1 -0
  81. package/dist/cli/env.js +66 -0
  82. package/dist/cli/env.js.map +1 -0
  83. package/dist/cli/git.d.ts +29 -0
  84. package/dist/cli/git.d.ts.map +1 -0
  85. package/dist/cli/git.js +114 -0
  86. package/dist/cli/git.js.map +1 -0
  87. package/dist/cli/git.test.d.ts +2 -0
  88. package/dist/cli/git.test.d.ts.map +1 -0
  89. package/dist/cli/git.test.js +125 -0
  90. package/dist/cli/git.test.js.map +1 -0
  91. package/dist/cli/index.d.ts +7 -0
  92. package/dist/cli/index.d.ts.map +1 -0
  93. package/dist/cli/index.js +337 -0
  94. package/dist/cli/index.js.map +1 -0
  95. package/dist/cli/utils/schema-validation.d.ts +95 -0
  96. package/dist/cli/utils/schema-validation.d.ts.map +1 -0
  97. package/dist/cli/utils/schema-validation.js +175 -0
  98. package/dist/cli/utils/schema-validation.js.map +1 -0
  99. package/dist/cli/utils/schema-validation.test.d.ts +5 -0
  100. package/dist/cli/utils/schema-validation.test.d.ts.map +1 -0
  101. package/dist/cli/utils/schema-validation.test.js +173 -0
  102. package/dist/cli/utils/schema-validation.test.js.map +1 -0
  103. package/dist/client/base.d.ts +116 -0
  104. package/dist/client/base.d.ts.map +1 -0
  105. package/dist/client/base.js +328 -0
  106. package/dist/client/base.js.map +1 -0
  107. package/dist/client/types.d.ts +137 -0
  108. package/dist/client/types.d.ts.map +1 -0
  109. package/dist/client/types.js +43 -0
  110. package/dist/client/types.js.map +1 -0
  111. package/dist/generator/client.d.ts +44 -0
  112. package/dist/generator/client.d.ts.map +1 -0
  113. package/dist/generator/client.js +144 -0
  114. package/dist/generator/client.js.map +1 -0
  115. package/dist/generator/datasource.d.ts +57 -0
  116. package/dist/generator/datasource.d.ts.map +1 -0
  117. package/dist/generator/datasource.js +169 -0
  118. package/dist/generator/datasource.js.map +1 -0
  119. package/dist/generator/datasource.test.d.ts +2 -0
  120. package/dist/generator/datasource.test.d.ts.map +1 -0
  121. package/dist/generator/datasource.test.js +254 -0
  122. package/dist/generator/datasource.test.js.map +1 -0
  123. package/dist/generator/index.d.ts +131 -0
  124. package/dist/generator/index.d.ts.map +1 -0
  125. package/dist/generator/index.js +121 -0
  126. package/dist/generator/index.js.map +1 -0
  127. package/dist/generator/index.test.d.ts +2 -0
  128. package/dist/generator/index.test.d.ts.map +1 -0
  129. package/dist/generator/index.test.js +175 -0
  130. package/dist/generator/index.test.js.map +1 -0
  131. package/dist/generator/loader.d.ts +156 -0
  132. package/dist/generator/loader.d.ts.map +1 -0
  133. package/dist/generator/loader.js +295 -0
  134. package/dist/generator/loader.js.map +1 -0
  135. package/dist/generator/pipe.d.ts +72 -0
  136. package/dist/generator/pipe.d.ts.map +1 -0
  137. package/dist/generator/pipe.js +174 -0
  138. package/dist/generator/pipe.js.map +1 -0
  139. package/dist/generator/pipe.test.d.ts +2 -0
  140. package/dist/generator/pipe.test.d.ts.map +1 -0
  141. package/dist/generator/pipe.test.js +393 -0
  142. package/dist/generator/pipe.test.js.map +1 -0
  143. package/dist/index.d.ts +74 -0
  144. package/dist/index.d.ts.map +1 -0
  145. package/dist/index.js +73 -0
  146. package/dist/index.js.map +1 -0
  147. package/dist/infer/index.d.ts +202 -0
  148. package/dist/infer/index.d.ts.map +1 -0
  149. package/dist/infer/index.js +5 -0
  150. package/dist/infer/index.js.map +1 -0
  151. package/dist/schema/datasource.d.ts +135 -0
  152. package/dist/schema/datasource.d.ts.map +1 -0
  153. package/dist/schema/datasource.js +105 -0
  154. package/dist/schema/datasource.js.map +1 -0
  155. package/dist/schema/datasource.test.d.ts +2 -0
  156. package/dist/schema/datasource.test.d.ts.map +1 -0
  157. package/dist/schema/datasource.test.js +142 -0
  158. package/dist/schema/datasource.test.js.map +1 -0
  159. package/dist/schema/engines.d.ts +157 -0
  160. package/dist/schema/engines.d.ts.map +1 -0
  161. package/dist/schema/engines.js +155 -0
  162. package/dist/schema/engines.js.map +1 -0
  163. package/dist/schema/engines.test.d.ts +2 -0
  164. package/dist/schema/engines.test.d.ts.map +1 -0
  165. package/dist/schema/engines.test.js +221 -0
  166. package/dist/schema/engines.test.js.map +1 -0
  167. package/dist/schema/params.d.ts +106 -0
  168. package/dist/schema/params.d.ts.map +1 -0
  169. package/dist/schema/params.js +138 -0
  170. package/dist/schema/params.js.map +1 -0
  171. package/dist/schema/params.test.d.ts +2 -0
  172. package/dist/schema/params.test.d.ts.map +1 -0
  173. package/dist/schema/params.test.js +175 -0
  174. package/dist/schema/params.test.js.map +1 -0
  175. package/dist/schema/pipe.d.ts +436 -0
  176. package/dist/schema/pipe.d.ts.map +1 -0
  177. package/dist/schema/pipe.js +484 -0
  178. package/dist/schema/pipe.js.map +1 -0
  179. package/dist/schema/pipe.test.d.ts +2 -0
  180. package/dist/schema/pipe.test.d.ts.map +1 -0
  181. package/dist/schema/pipe.test.js +488 -0
  182. package/dist/schema/pipe.test.js.map +1 -0
  183. package/dist/schema/project.d.ts +202 -0
  184. package/dist/schema/project.d.ts.map +1 -0
  185. package/dist/schema/project.js +188 -0
  186. package/dist/schema/project.js.map +1 -0
  187. package/dist/schema/project.test.d.ts +2 -0
  188. package/dist/schema/project.test.d.ts.map +1 -0
  189. package/dist/schema/project.test.js +180 -0
  190. package/dist/schema/project.test.js.map +1 -0
  191. package/dist/schema/types.d.ts +140 -0
  192. package/dist/schema/types.d.ts.map +1 -0
  193. package/dist/schema/types.js +174 -0
  194. package/dist/schema/types.js.map +1 -0
  195. package/dist/schema/types.test.d.ts +2 -0
  196. package/dist/schema/types.test.d.ts.map +1 -0
  197. package/dist/schema/types.test.js +176 -0
  198. package/dist/schema/types.test.js.map +1 -0
  199. package/dist/test/handlers.d.ts +58 -0
  200. package/dist/test/handlers.d.ts.map +1 -0
  201. package/dist/test/handlers.js +62 -0
  202. package/dist/test/handlers.js.map +1 -0
  203. package/dist/test/setup.d.ts +5 -0
  204. package/dist/test/setup.d.ts.map +1 -0
  205. package/dist/test/setup.js +11 -0
  206. package/dist/test/setup.js.map +1 -0
  207. package/package.json +57 -0
  208. package/src/api/branches.test.ts +377 -0
  209. package/src/api/branches.ts +334 -0
  210. package/src/api/build.test.ts +216 -0
  211. package/src/api/build.ts +266 -0
  212. package/src/api/deploy.test.ts +193 -0
  213. package/src/api/deploy.ts +163 -0
  214. package/src/api/workspaces.test.ts +81 -0
  215. package/src/api/workspaces.ts +77 -0
  216. package/src/cli/auth.ts +358 -0
  217. package/src/cli/branch-store.test.ts +139 -0
  218. package/src/cli/branch-store.ts +137 -0
  219. package/src/cli/commands/branch.ts +306 -0
  220. package/src/cli/commands/build.ts +183 -0
  221. package/src/cli/commands/dev.ts +334 -0
  222. package/src/cli/commands/init.test.ts +249 -0
  223. package/src/cli/commands/init.ts +323 -0
  224. package/src/cli/commands/login.ts +98 -0
  225. package/src/cli/config.test.ts +359 -0
  226. package/src/cli/config.ts +335 -0
  227. package/src/cli/env.ts +86 -0
  228. package/src/cli/git.test.ts +147 -0
  229. package/src/cli/git.ts +125 -0
  230. package/src/cli/index.ts +382 -0
  231. package/src/cli/utils/schema-validation.test.ts +222 -0
  232. package/src/cli/utils/schema-validation.ts +272 -0
  233. package/src/client/base.ts +414 -0
  234. package/src/client/types.ts +165 -0
  235. package/src/generator/client.ts +194 -0
  236. package/src/generator/datasource.test.ts +297 -0
  237. package/src/generator/datasource.ts +217 -0
  238. package/src/generator/index.test.ts +209 -0
  239. package/src/generator/index.ts +203 -0
  240. package/src/generator/loader.ts +406 -0
  241. package/src/generator/pipe.test.ts +441 -0
  242. package/src/generator/pipe.ts +220 -0
  243. package/src/index.ts +191 -0
  244. package/src/infer/index.ts +247 -0
  245. package/src/schema/datasource.test.ts +187 -0
  246. package/src/schema/datasource.ts +195 -0
  247. package/src/schema/engines.test.ts +247 -0
  248. package/src/schema/engines.ts +271 -0
  249. package/src/schema/params.test.ts +208 -0
  250. package/src/schema/params.ts +249 -0
  251. package/src/schema/pipe.test.ts +588 -0
  252. package/src/schema/pipe.ts +832 -0
  253. package/src/schema/project.test.ts +236 -0
  254. package/src/schema/project.ts +394 -0
  255. package/src/schema/types.test.ts +212 -0
  256. package/src/schema/types.ts +366 -0
  257. package/src/test/handlers.ts +79 -0
  258. package/src/test/setup.ts +13 -0
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Tinybird Branch (Environment) API client
3
+ * Uses the /v1/environments endpoints (Forward API)
4
+ */
5
+
6
+ /**
7
+ * Branch information from Tinybird API
8
+ */
9
+ export interface TinybirdBranch {
10
+ /** Branch ID */
11
+ id: string;
12
+ /** Branch name */
13
+ name: string;
14
+ /** Branch token (only present when requested with with_token=true) */
15
+ token?: string;
16
+ /** When the branch was created */
17
+ created_at: string;
18
+ }
19
+
20
+ /**
21
+ * Result of getOrCreateBranch operation
22
+ */
23
+ export interface GetOrCreateBranchResult extends TinybirdBranch {
24
+ /** Whether the branch was newly created (vs already existed) */
25
+ wasCreated: boolean;
26
+ }
27
+
28
+ /**
29
+ * API configuration for branch operations
30
+ */
31
+ export interface BranchApiConfig {
32
+ /** Tinybird API base URL */
33
+ baseUrl: string;
34
+ /** Parent workspace token (used to create/manage branches) */
35
+ token: string;
36
+ }
37
+
38
+ /**
39
+ * Job response from async operations
40
+ */
41
+ interface JobResponse {
42
+ job: {
43
+ id: string;
44
+ status: string;
45
+ job_url?: string;
46
+ };
47
+ workspace?: {
48
+ id: string;
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Job status response
54
+ */
55
+ interface JobStatusResponse {
56
+ id: string;
57
+ status: "waiting" | "working" | "done" | "error";
58
+ error?: string;
59
+ }
60
+
61
+ /**
62
+ * Error thrown by branch API operations
63
+ */
64
+ export class BranchApiError extends Error {
65
+ constructor(
66
+ message: string,
67
+ public readonly status: number,
68
+ public readonly body?: unknown
69
+ ) {
70
+ super(message);
71
+ this.name = "BranchApiError";
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Poll a job until it completes
77
+ *
78
+ * @param config - API configuration
79
+ * @param jobId - Job ID to poll
80
+ * @param maxAttempts - Maximum polling attempts (default: 120, i.e. 2 minutes)
81
+ * @param intervalMs - Polling interval in milliseconds (default: 1000)
82
+ * @returns Job status when complete
83
+ */
84
+ async function pollJob(
85
+ config: BranchApiConfig,
86
+ jobId: string,
87
+ maxAttempts = 120,
88
+ intervalMs = 1000
89
+ ): Promise<JobStatusResponse> {
90
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
91
+ const url = new URL(`/v0/jobs/${jobId}`, config.baseUrl);
92
+
93
+ const response = await fetch(url.toString(), {
94
+ method: "GET",
95
+ headers: {
96
+ Authorization: `Bearer ${config.token}`,
97
+ },
98
+ });
99
+
100
+ if (!response.ok) {
101
+ const body = await response.text();
102
+ throw new BranchApiError(
103
+ `Failed to poll job '${jobId}': ${response.status} ${response.statusText}\nAPI response: ${body}`,
104
+ response.status,
105
+ body
106
+ );
107
+ }
108
+
109
+ const jobStatus = (await response.json()) as JobStatusResponse;
110
+
111
+ if (jobStatus.status === "done") {
112
+ return jobStatus;
113
+ }
114
+
115
+ if (jobStatus.status === "error") {
116
+ throw new BranchApiError(
117
+ `Job '${jobId}' failed: ${jobStatus.error ?? "Unknown error"}`,
118
+ 500,
119
+ jobStatus
120
+ );
121
+ }
122
+
123
+ // Wait before next poll
124
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
125
+ }
126
+
127
+ throw new BranchApiError(
128
+ `Job '${jobId}' timed out after ${maxAttempts} attempts`,
129
+ 408
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Create a new branch
135
+ * POST /v1/environments?name={name}
136
+ *
137
+ * This is an async operation that returns a job. We poll the job until
138
+ * it completes, then fetch the branch with its token.
139
+ *
140
+ * @param config - API configuration
141
+ * @param name - Branch name to create
142
+ * @returns The created branch with token
143
+ */
144
+ export async function createBranch(
145
+ config: BranchApiConfig,
146
+ name: string
147
+ ): Promise<TinybirdBranch> {
148
+ const url = new URL("/v1/environments", config.baseUrl);
149
+ url.searchParams.set("name", name);
150
+
151
+ const response = await fetch(url.toString(), {
152
+ method: "POST",
153
+ headers: {
154
+ Authorization: `Bearer ${config.token}`,
155
+ },
156
+ });
157
+
158
+ if (!response.ok) {
159
+ const body = await response.text();
160
+
161
+ // Provide helpful error message for common cases, but include raw response for debugging
162
+ let message = `Failed to create branch '${name}': ${response.status} ${response.statusText}`;
163
+ if (response.status === 403) {
164
+ message = `Permission denied creating branch '${name}'. ` +
165
+ `Make sure TINYBIRD_TOKEN is a workspace admin token (not a branch token). ` +
166
+ `Branch tokens cannot create new branches.\n` +
167
+ `API response: ${body}`;
168
+ } else if (response.status === 409) {
169
+ message = `Branch '${name}' already exists.`;
170
+ } else {
171
+ message += `\nAPI response: ${body}`;
172
+ }
173
+
174
+ throw new BranchApiError(message, response.status, body);
175
+ }
176
+
177
+ // Parse the job response
178
+ const jobResponse = (await response.json()) as JobResponse;
179
+
180
+ if (!jobResponse.job?.id) {
181
+ throw new BranchApiError(
182
+ `Unexpected response from branch creation: no job ID returned`,
183
+ 500,
184
+ jobResponse
185
+ );
186
+ }
187
+
188
+ // Poll the job until it completes
189
+ await pollJob(config, jobResponse.job.id);
190
+
191
+ // Now fetch the branch with its token using the branch name
192
+ const branch = await getBranch(config, name);
193
+ return branch;
194
+ }
195
+
196
+ /**
197
+ * List all branches in the workspace
198
+ * GET /v1/environments
199
+ *
200
+ * @param config - API configuration
201
+ * @returns Array of branches
202
+ */
203
+ export async function listBranches(
204
+ config: BranchApiConfig
205
+ ): Promise<TinybirdBranch[]> {
206
+ const url = new URL("/v1/environments", config.baseUrl);
207
+
208
+ const response = await fetch(url.toString(), {
209
+ method: "GET",
210
+ headers: {
211
+ Authorization: `Bearer ${config.token}`,
212
+ },
213
+ });
214
+
215
+ if (!response.ok) {
216
+ const body = await response.text();
217
+ throw new BranchApiError(
218
+ `Failed to list branches: ${response.status} ${response.statusText}`,
219
+ response.status,
220
+ body
221
+ );
222
+ }
223
+
224
+ const data = (await response.json()) as { environments: TinybirdBranch[] };
225
+ return data.environments ?? [];
226
+ }
227
+
228
+ /**
229
+ * Get a branch by name with its token
230
+ * GET /v0/environments/{name}?with_token=true
231
+ *
232
+ * @param config - API configuration
233
+ * @param name - Branch name
234
+ * @returns Branch with token
235
+ */
236
+ export async function getBranch(
237
+ config: BranchApiConfig,
238
+ name: string
239
+ ): Promise<TinybirdBranch> {
240
+ const url = new URL(`/v0/environments/${encodeURIComponent(name)}`, config.baseUrl);
241
+ url.searchParams.set("with_token", "true");
242
+
243
+ const response = await fetch(url.toString(), {
244
+ method: "GET",
245
+ headers: {
246
+ Authorization: `Bearer ${config.token}`,
247
+ },
248
+ });
249
+
250
+ if (!response.ok) {
251
+ const body = await response.text();
252
+ throw new BranchApiError(
253
+ `Failed to get branch '${name}': ${response.status} ${response.statusText}`,
254
+ response.status,
255
+ body
256
+ );
257
+ }
258
+
259
+ const data = (await response.json()) as TinybirdBranch;
260
+ return data;
261
+ }
262
+
263
+ /**
264
+ * Delete a branch
265
+ * DELETE /v1/environments/{name}
266
+ *
267
+ * @param config - API configuration
268
+ * @param name - Branch name to delete
269
+ */
270
+ export async function deleteBranch(
271
+ config: BranchApiConfig,
272
+ name: string
273
+ ): Promise<void> {
274
+ const url = new URL(`/v1/environments/${encodeURIComponent(name)}`, config.baseUrl);
275
+
276
+ const response = await fetch(url.toString(), {
277
+ method: "DELETE",
278
+ headers: {
279
+ Authorization: `Bearer ${config.token}`,
280
+ },
281
+ });
282
+
283
+ if (!response.ok) {
284
+ const body = await response.text();
285
+ throw new BranchApiError(
286
+ `Failed to delete branch '${name}': ${response.status} ${response.statusText}`,
287
+ response.status,
288
+ body
289
+ );
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Check if a branch exists
295
+ *
296
+ * @param config - API configuration
297
+ * @param name - Branch name to check
298
+ * @returns true if branch exists, false if not
299
+ * @throws BranchApiError on API/network/auth failures
300
+ */
301
+ export async function branchExists(
302
+ config: BranchApiConfig,
303
+ name: string
304
+ ): Promise<boolean> {
305
+ const branches = await listBranches(config);
306
+ return branches.some((b) => b.name === name);
307
+ }
308
+
309
+ /**
310
+ * Get or create a branch
311
+ * If the branch exists, returns it with token.
312
+ * If it doesn't exist, creates it.
313
+ *
314
+ * @param config - API configuration
315
+ * @param name - Branch name
316
+ * @returns Branch with token
317
+ */
318
+ export async function getOrCreateBranch(
319
+ config: BranchApiConfig,
320
+ name: string
321
+ ): Promise<GetOrCreateBranchResult> {
322
+ // First try to get the existing branch
323
+ try {
324
+ const branch = await getBranch(config, name);
325
+ return { ...branch, wasCreated: false };
326
+ } catch (error) {
327
+ // If it's a 404, create the branch
328
+ if (error instanceof BranchApiError && error.status === 404) {
329
+ const branch = await createBranch(config, name);
330
+ return { ...branch, wasCreated: true };
331
+ }
332
+ throw error;
333
+ }
334
+ }
@@ -0,0 +1,216 @@
1
+ import { describe, it, expect, beforeAll, afterEach, afterAll } from "vitest";
2
+ import { setupServer } from "msw/node";
3
+ import { http, HttpResponse } from "msw";
4
+ import { buildToTinybird, validateBuildConfig, type BuildConfig } from "./build.js";
5
+ import {
6
+ BASE_URL,
7
+ createBuildSuccessResponse,
8
+ createBuildFailureResponse,
9
+ createBuildMultipleErrorsResponse,
10
+ createNoChangesResponse,
11
+ } from "../test/handlers.js";
12
+ import type { GeneratedResources } from "../generator/index.js";
13
+
14
+ const server = setupServer();
15
+
16
+ beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
17
+ afterEach(() => server.resetHandlers());
18
+ afterAll(() => server.close());
19
+
20
+ describe("Build API", () => {
21
+ const config: BuildConfig = {
22
+ baseUrl: BASE_URL,
23
+ token: "p.test-token",
24
+ };
25
+
26
+ const resources: GeneratedResources = {
27
+ datasources: [
28
+ { name: "events", content: "SCHEMA > timestamp DateTime" },
29
+ { name: "users", content: "SCHEMA > id String" },
30
+ ],
31
+ pipes: [
32
+ { name: "top_events", content: "NODE main\nSQL > SELECT * FROM events" },
33
+ ],
34
+ };
35
+
36
+ describe("buildToTinybird", () => {
37
+ it("successfully builds resources", async () => {
38
+ server.use(
39
+ http.post(`${BASE_URL}/v1/build`, () => {
40
+ return HttpResponse.json(
41
+ createBuildSuccessResponse({
42
+ buildId: "build-abc",
43
+ newPipes: ["top_events"],
44
+ newDatasources: ["events", "users"],
45
+ })
46
+ );
47
+ })
48
+ );
49
+
50
+ const result = await buildToTinybird(config, resources);
51
+
52
+ expect(result.success).toBe(true);
53
+ expect(result.result).toBe("success");
54
+ expect(result.buildId).toBe("build-abc");
55
+ expect(result.datasourceCount).toBe(2);
56
+ expect(result.pipeCount).toBe(1);
57
+ expect(result.pipes?.created).toEqual(["top_events"]);
58
+ expect(result.datasources?.created).toEqual(["events", "users"]);
59
+ });
60
+
61
+ it("handles no changes response", async () => {
62
+ server.use(
63
+ http.post(`${BASE_URL}/v1/build`, () => {
64
+ return HttpResponse.json(createNoChangesResponse());
65
+ })
66
+ );
67
+
68
+ const result = await buildToTinybird(config, resources);
69
+
70
+ expect(result.success).toBe(true);
71
+ expect(result.result).toBe("no_changes");
72
+ });
73
+
74
+ it("handles build failure with single error", async () => {
75
+ server.use(
76
+ http.post(`${BASE_URL}/v1/build`, () => {
77
+ return HttpResponse.json(
78
+ createBuildFailureResponse("Invalid SQL syntax"),
79
+ { status: 200 }
80
+ );
81
+ })
82
+ );
83
+
84
+ const result = await buildToTinybird(config, resources);
85
+
86
+ expect(result.success).toBe(false);
87
+ expect(result.result).toBe("failed");
88
+ expect(result.error).toBe("Invalid SQL syntax");
89
+ });
90
+
91
+ it("handles build failure with multiple errors", async () => {
92
+ server.use(
93
+ http.post(`${BASE_URL}/v1/build`, () => {
94
+ return HttpResponse.json(
95
+ createBuildMultipleErrorsResponse([
96
+ { filename: "events.datasource", error: "Invalid schema" },
97
+ { filename: "top_events.pipe", error: "Unknown column" },
98
+ ]),
99
+ { status: 200 }
100
+ );
101
+ })
102
+ );
103
+
104
+ const result = await buildToTinybird(config, resources);
105
+
106
+ expect(result.success).toBe(false);
107
+ expect(result.error).toContain("[events.datasource] Invalid schema");
108
+ expect(result.error).toContain("[top_events.pipe] Unknown column");
109
+ });
110
+
111
+ it("handles HTTP error responses", async () => {
112
+ server.use(
113
+ http.post(`${BASE_URL}/v1/build`, () => {
114
+ return HttpResponse.json(
115
+ { result: "failed", error: "Unauthorized" },
116
+ { status: 401 }
117
+ );
118
+ })
119
+ );
120
+
121
+ const result = await buildToTinybird(config, resources);
122
+
123
+ expect(result.success).toBe(false);
124
+ expect(result.error).toBe("Unauthorized");
125
+ });
126
+
127
+ it("handles malformed JSON response", async () => {
128
+ server.use(
129
+ http.post(`${BASE_URL}/v1/build`, () => {
130
+ return new HttpResponse("not json", {
131
+ status: 200,
132
+ headers: { "Content-Type": "text/plain" },
133
+ });
134
+ })
135
+ );
136
+
137
+ await expect(buildToTinybird(config, resources)).rejects.toThrow(
138
+ "Failed to parse response"
139
+ );
140
+ });
141
+
142
+ it("tracks changed pipes and datasources", async () => {
143
+ server.use(
144
+ http.post(`${BASE_URL}/v1/build`, () => {
145
+ return HttpResponse.json(
146
+ createBuildSuccessResponse({
147
+ changedPipes: ["top_events"],
148
+ changedDatasources: ["events"],
149
+ deletedPipes: ["old_pipe"],
150
+ })
151
+ );
152
+ })
153
+ );
154
+
155
+ const result = await buildToTinybird(config, resources);
156
+
157
+ expect(result.pipes?.changed).toEqual(["top_events"]);
158
+ expect(result.pipes?.deleted).toEqual(["old_pipe"]);
159
+ expect(result.datasources?.changed).toEqual(["events"]);
160
+ // Deprecated fields should still work
161
+ expect(result.changedPipeNames).toEqual(["top_events"]);
162
+ });
163
+
164
+ it("sends correct authorization header", async () => {
165
+ let capturedAuth: string | null = null;
166
+
167
+ server.use(
168
+ http.post(`${BASE_URL}/v1/build`, ({ request }) => {
169
+ capturedAuth = request.headers.get("Authorization");
170
+ return HttpResponse.json(createBuildSuccessResponse());
171
+ })
172
+ );
173
+
174
+ await buildToTinybird(config, resources);
175
+
176
+ expect(capturedAuth).toBe("Bearer p.test-token");
177
+ });
178
+
179
+ it("sends resources as multipart form data", async () => {
180
+ let capturedFormData: FormData | null = null;
181
+
182
+ server.use(
183
+ http.post(`${BASE_URL}/v1/build`, async ({ request }) => {
184
+ capturedFormData = await request.formData();
185
+ return HttpResponse.json(createBuildSuccessResponse());
186
+ })
187
+ );
188
+
189
+ await buildToTinybird(config, resources);
190
+
191
+ expect(capturedFormData).not.toBeNull();
192
+ // FormData has 3 entries: 2 datasources + 1 pipe
193
+ // Use getAll since FormData.entries() is not available in Node.js types
194
+ const allValues = capturedFormData!.getAll("data_project://");
195
+ expect(allValues.length).toBe(3);
196
+ });
197
+ });
198
+
199
+ describe("validateBuildConfig", () => {
200
+ it("passes with valid config", () => {
201
+ expect(() => validateBuildConfig(config)).not.toThrow();
202
+ });
203
+
204
+ it("throws on missing baseUrl", () => {
205
+ expect(() =>
206
+ validateBuildConfig({ token: "test" })
207
+ ).toThrow("Missing baseUrl");
208
+ });
209
+
210
+ it("throws on missing token", () => {
211
+ expect(() =>
212
+ validateBuildConfig({ baseUrl: "https://api.tinybird.co" })
213
+ ).toThrow("Missing token");
214
+ });
215
+ });
216
+ });