@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,266 @@
1
+ /**
2
+ * Build and deploy resources to Tinybird API
3
+ * Uses the /v1/build endpoint to deploy all resources at once
4
+ */
5
+
6
+ import type { GeneratedResources } from "../generator/index.js";
7
+
8
+ /**
9
+ * Configuration for building/deploying to Tinybird
10
+ */
11
+ export interface BuildConfig {
12
+ /** Tinybird API base URL */
13
+ baseUrl: string;
14
+ /** API token for authentication */
15
+ token: string;
16
+ }
17
+
18
+ /**
19
+ * Resource info in the build response
20
+ */
21
+ export interface ResourceInfo {
22
+ name: string;
23
+ type: string;
24
+ }
25
+
26
+ /**
27
+ * Error details from the build endpoint
28
+ */
29
+ export interface BuildError {
30
+ filename?: string;
31
+ type?: string;
32
+ error: string;
33
+ }
34
+
35
+ /**
36
+ * Build response from the /v1/build endpoint
37
+ */
38
+ export interface BuildResponse {
39
+ /** Result status */
40
+ result: "success" | "failed" | "no_changes";
41
+ /** Error message if failed (simple error) */
42
+ error?: string;
43
+ /** Array of errors if multiple (validation errors) */
44
+ errors?: BuildError[];
45
+ /** Build details */
46
+ build?: {
47
+ id: string;
48
+ datasources?: ResourceInfo[];
49
+ pipes?: ResourceInfo[];
50
+ /** Names of pipes that were changed in this build */
51
+ changed_pipe_names?: string[];
52
+ /** Names of newly created pipes in this build */
53
+ new_pipe_names?: string[];
54
+ /** Names of pipes that were deleted in this build */
55
+ deleted_pipe_names?: string[];
56
+ /** Names of datasources that were changed in this build */
57
+ changed_datasource_names?: string[];
58
+ /** Names of newly created datasources in this build */
59
+ new_datasource_names?: string[];
60
+ /** Names of datasources that were deleted in this build */
61
+ deleted_datasource_names?: string[];
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Resource changes in a build
67
+ */
68
+ export interface ResourceChanges {
69
+ /** Names of resources that were changed */
70
+ changed: string[];
71
+ /** Names of newly created resources */
72
+ created: string[];
73
+ /** Names of resources that were deleted */
74
+ deleted: string[];
75
+ }
76
+
77
+ /**
78
+ * Build result with additional metadata
79
+ */
80
+ export interface BuildApiResult {
81
+ /** Whether the build was successful */
82
+ success: boolean;
83
+ /** Result status from API */
84
+ result: "success" | "failed" | "no_changes";
85
+ /** Error message if failed */
86
+ error?: string;
87
+ /** Number of datasources deployed */
88
+ datasourceCount: number;
89
+ /** Number of pipes deployed */
90
+ pipeCount: number;
91
+ /** Build ID if successful */
92
+ buildId?: string;
93
+ /** Pipe changes in this build */
94
+ pipes?: ResourceChanges;
95
+ /** Datasource changes in this build */
96
+ datasources?: ResourceChanges;
97
+ /** @deprecated Use pipes.changed instead */
98
+ changedPipeNames?: string[];
99
+ /** @deprecated Use pipes.created instead */
100
+ newPipeNames?: string[];
101
+ }
102
+
103
+ /**
104
+ * Build and deploy generated resources to Tinybird API
105
+ *
106
+ * Uses the /v1/build endpoint which accepts all resources in a single
107
+ * multipart form request.
108
+ *
109
+ * @param config - Build configuration with API URL and token
110
+ * @param resources - Generated resources to deploy
111
+ * @returns Build result
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const result = await buildToTinybird(
116
+ * {
117
+ * baseUrl: 'https://api.tinybird.co',
118
+ * token: 'p.xxx',
119
+ * },
120
+ * {
121
+ * datasources: [{ name: 'events', content: '...' }],
122
+ * pipes: [{ name: 'top_events', content: '...' }],
123
+ * }
124
+ * );
125
+ *
126
+ * if (result.success) {
127
+ * console.log('Build deployed successfully!');
128
+ * }
129
+ * ```
130
+ */
131
+ export async function buildToTinybird(
132
+ config: BuildConfig,
133
+ resources: GeneratedResources,
134
+ options?: { debug?: boolean }
135
+ ): Promise<BuildApiResult> {
136
+ const debug = options?.debug ?? !!process.env.TINYBIRD_DEBUG;
137
+ const formData = new FormData();
138
+
139
+ // Add datasources
140
+ for (const ds of resources.datasources) {
141
+ const fieldName = `data_project://`;
142
+ const fileName = `${ds.name}.datasource`;
143
+ if (debug) {
144
+ console.log(`[debug] Adding datasource: ${fieldName} (filename: ${fileName})`);
145
+ console.log(`[debug] Content:\n${ds.content}\n`);
146
+ }
147
+ formData.append(
148
+ fieldName,
149
+ new Blob([ds.content], { type: "text/plain" }),
150
+ fileName
151
+ );
152
+ }
153
+
154
+ // Add pipes
155
+ for (const pipe of resources.pipes) {
156
+ const fieldName = `data_project://`;
157
+ const fileName = `${pipe.name}.pipe`;
158
+ if (debug) {
159
+ console.log(`[debug] Adding pipe: ${fieldName} (filename: ${fileName})`);
160
+ console.log(`[debug] Content:\n${pipe.content}\n`);
161
+ }
162
+ formData.append(
163
+ fieldName,
164
+ new Blob([pipe.content], { type: "text/plain" }),
165
+ fileName
166
+ );
167
+ }
168
+
169
+ // Make the request
170
+ const url = `${config.baseUrl.replace(/\/$/, "")}/v1/build`;
171
+
172
+ if (debug) {
173
+ console.log(`[debug] POST ${url}`);
174
+ }
175
+
176
+ const response = await fetch(url, {
177
+ method: "POST",
178
+ headers: {
179
+ Authorization: `Bearer ${config.token}`,
180
+ },
181
+ body: formData,
182
+ });
183
+
184
+ // Parse response
185
+ let body: BuildResponse;
186
+ const rawBody = await response.text();
187
+
188
+ if (debug) {
189
+ console.log(`[debug] Response status: ${response.status}`);
190
+ console.log(`[debug] Response body: ${rawBody}`);
191
+ }
192
+
193
+ try {
194
+ body = JSON.parse(rawBody) as BuildResponse;
195
+ } catch {
196
+ throw new Error(
197
+ `Failed to parse response from Tinybird API: ${response.status} ${response.statusText}\nBody: ${rawBody}`
198
+ );
199
+ }
200
+
201
+ // Helper to format errors
202
+ const formatErrors = (): string => {
203
+ if (body.errors && body.errors.length > 0) {
204
+ return body.errors.map(e => {
205
+ const prefix = e.filename ? `[${e.filename}] ` : '';
206
+ return `${prefix}${e.error}`;
207
+ }).join('\n');
208
+ }
209
+ return body.error || `HTTP ${response.status}: ${response.statusText}`;
210
+ };
211
+
212
+ // Handle non-OK responses
213
+ if (!response.ok) {
214
+ return {
215
+ success: false,
216
+ result: "failed",
217
+ error: formatErrors(),
218
+ datasourceCount: resources.datasources.length,
219
+ pipeCount: resources.pipes.length,
220
+ };
221
+ }
222
+
223
+ // Handle API result
224
+ if (body.result === "failed") {
225
+ return {
226
+ success: false,
227
+ result: "failed",
228
+ error: formatErrors(),
229
+ datasourceCount: resources.datasources.length,
230
+ pipeCount: resources.pipes.length,
231
+ };
232
+ }
233
+
234
+ return {
235
+ success: true,
236
+ result: body.result,
237
+ datasourceCount: resources.datasources.length,
238
+ pipeCount: resources.pipes.length,
239
+ buildId: body.build?.id,
240
+ pipes: {
241
+ changed: body.build?.changed_pipe_names ?? [],
242
+ created: body.build?.new_pipe_names ?? [],
243
+ deleted: body.build?.deleted_pipe_names ?? [],
244
+ },
245
+ datasources: {
246
+ changed: body.build?.changed_datasource_names ?? [],
247
+ created: body.build?.new_datasource_names ?? [],
248
+ deleted: body.build?.deleted_datasource_names ?? [],
249
+ },
250
+ // Keep deprecated fields for backwards compatibility
251
+ changedPipeNames: body.build?.changed_pipe_names ?? [],
252
+ newPipeNames: body.build?.new_pipe_names ?? [],
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Validate that the configuration is complete
258
+ */
259
+ export function validateBuildConfig(config: Partial<BuildConfig>): asserts config is BuildConfig {
260
+ if (!config.baseUrl) {
261
+ throw new Error("Missing baseUrl in configuration");
262
+ }
263
+ if (!config.token) {
264
+ throw new Error("Missing token in configuration");
265
+ }
266
+ }
@@ -0,0 +1,193 @@
1
+ import { describe, it, expect, beforeAll, afterEach, afterAll } from "vitest";
2
+ import { setupServer } from "msw/node";
3
+ import { http, HttpResponse } from "msw";
4
+ import { deployToMain } from "./deploy.js";
5
+ import type { BuildConfig } from "./build.js";
6
+ import {
7
+ BASE_URL,
8
+ createBuildSuccessResponse,
9
+ createBuildFailureResponse,
10
+ createBuildMultipleErrorsResponse,
11
+ createNoChangesResponse,
12
+ } from "../test/handlers.js";
13
+ import type { GeneratedResources } from "../generator/index.js";
14
+
15
+ const server = setupServer();
16
+
17
+ beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
18
+ afterEach(() => server.resetHandlers());
19
+ afterAll(() => server.close());
20
+
21
+ describe("Deploy API", () => {
22
+ const config: BuildConfig = {
23
+ baseUrl: BASE_URL,
24
+ token: "p.test-token",
25
+ };
26
+
27
+ const resources: GeneratedResources = {
28
+ datasources: [
29
+ { name: "events", content: "SCHEMA > timestamp DateTime" },
30
+ ],
31
+ pipes: [
32
+ { name: "top_events", content: "NODE main\nSQL > SELECT * FROM events" },
33
+ ],
34
+ };
35
+
36
+ describe("deployToMain", () => {
37
+ it("successfully deploys resources", async () => {
38
+ server.use(
39
+ http.post(`${BASE_URL}/v1/deploy`, () => {
40
+ return HttpResponse.json(
41
+ createBuildSuccessResponse({
42
+ buildId: "deploy-abc",
43
+ newPipes: ["top_events"],
44
+ newDatasources: ["events"],
45
+ })
46
+ );
47
+ })
48
+ );
49
+
50
+ const result = await deployToMain(config, resources);
51
+
52
+ expect(result.success).toBe(true);
53
+ expect(result.result).toBe("success");
54
+ expect(result.buildId).toBe("deploy-abc");
55
+ expect(result.datasourceCount).toBe(1);
56
+ expect(result.pipeCount).toBe(1);
57
+ expect(result.pipes?.created).toEqual(["top_events"]);
58
+ expect(result.datasources?.created).toEqual(["events"]);
59
+ });
60
+
61
+ it("handles no changes response", async () => {
62
+ server.use(
63
+ http.post(`${BASE_URL}/v1/deploy`, () => {
64
+ return HttpResponse.json(createNoChangesResponse());
65
+ })
66
+ );
67
+
68
+ const result = await deployToMain(config, resources);
69
+
70
+ expect(result.success).toBe(true);
71
+ expect(result.result).toBe("no_changes");
72
+ });
73
+
74
+ it("handles deploy failure with single error", async () => {
75
+ server.use(
76
+ http.post(`${BASE_URL}/v1/deploy`, () => {
77
+ return HttpResponse.json(
78
+ createBuildFailureResponse("Permission denied"),
79
+ { status: 200 }
80
+ );
81
+ })
82
+ );
83
+
84
+ const result = await deployToMain(config, resources);
85
+
86
+ expect(result.success).toBe(false);
87
+ expect(result.result).toBe("failed");
88
+ expect(result.error).toBe("Permission denied");
89
+ });
90
+
91
+ it("handles deploy failure with multiple errors", async () => {
92
+ server.use(
93
+ http.post(`${BASE_URL}/v1/deploy`, () => {
94
+ return HttpResponse.json(
95
+ createBuildMultipleErrorsResponse([
96
+ { filename: "events.datasource", error: "Schema mismatch" },
97
+ { error: "General error without filename" },
98
+ ]),
99
+ { status: 200 }
100
+ );
101
+ })
102
+ );
103
+
104
+ const result = await deployToMain(config, resources);
105
+
106
+ expect(result.success).toBe(false);
107
+ expect(result.error).toContain("[events.datasource] Schema mismatch");
108
+ expect(result.error).toContain("General error without filename");
109
+ });
110
+
111
+ it("handles HTTP error responses", async () => {
112
+ server.use(
113
+ http.post(`${BASE_URL}/v1/deploy`, () => {
114
+ return HttpResponse.json(
115
+ { result: "failed", error: "Forbidden" },
116
+ { status: 403 }
117
+ );
118
+ })
119
+ );
120
+
121
+ const result = await deployToMain(config, resources);
122
+
123
+ expect(result.success).toBe(false);
124
+ expect(result.error).toBe("Forbidden");
125
+ });
126
+
127
+ it("handles malformed JSON response", async () => {
128
+ server.use(
129
+ http.post(`${BASE_URL}/v1/deploy`, () => {
130
+ return new HttpResponse("invalid json {", {
131
+ status: 200,
132
+ headers: { "Content-Type": "text/plain" },
133
+ });
134
+ })
135
+ );
136
+
137
+ await expect(deployToMain(config, resources)).rejects.toThrow(
138
+ "Failed to parse response"
139
+ );
140
+ });
141
+
142
+ it("uses /v1/deploy endpoint (not /v1/build)", async () => {
143
+ let capturedUrl: string | null = null;
144
+
145
+ server.use(
146
+ http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
147
+ capturedUrl = request.url;
148
+ return HttpResponse.json(createBuildSuccessResponse());
149
+ })
150
+ );
151
+
152
+ await deployToMain(config, resources);
153
+
154
+ expect(capturedUrl).toBe(`${BASE_URL}/v1/deploy`);
155
+ });
156
+
157
+ it("tracks changed and deleted resources", async () => {
158
+ server.use(
159
+ http.post(`${BASE_URL}/v1/deploy`, () => {
160
+ return HttpResponse.json(
161
+ createBuildSuccessResponse({
162
+ changedPipes: ["top_events"],
163
+ deletedDatasources: ["old_ds"],
164
+ })
165
+ );
166
+ })
167
+ );
168
+
169
+ const result = await deployToMain(config, resources);
170
+
171
+ expect(result.pipes?.changed).toEqual(["top_events"]);
172
+ expect(result.datasources?.deleted).toEqual(["old_ds"]);
173
+ });
174
+
175
+ it("normalizes baseUrl with trailing slash", async () => {
176
+ let capturedUrl: string | null = null;
177
+
178
+ server.use(
179
+ http.post(`${BASE_URL}/v1/deploy`, ({ request }) => {
180
+ capturedUrl = request.url;
181
+ return HttpResponse.json(createBuildSuccessResponse());
182
+ })
183
+ );
184
+
185
+ await deployToMain(
186
+ { ...config, baseUrl: `${BASE_URL}/` },
187
+ resources
188
+ );
189
+
190
+ expect(capturedUrl).toBe(`${BASE_URL}/v1/deploy`);
191
+ });
192
+ });
193
+ });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Deploy resources to Tinybird main workspace
3
+ * Uses the /v1/deploy endpoint (same payload format as /v1/build)
4
+ */
5
+
6
+ import type { GeneratedResources } from "../generator/index.js";
7
+ import type { BuildConfig, BuildApiResult, BuildResponse } from "./build.js";
8
+
9
+ /**
10
+ * Deploy generated resources to Tinybird main workspace
11
+ *
12
+ * 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).
15
+ *
16
+ * @param config - Build configuration with API URL and token
17
+ * @param resources - Generated resources to deploy
18
+ * @returns Build result
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const result = await deployToMain(
23
+ * {
24
+ * baseUrl: 'https://api.tinybird.co',
25
+ * token: 'p.xxx',
26
+ * },
27
+ * {
28
+ * datasources: [{ name: 'events', content: '...' }],
29
+ * pipes: [{ name: 'top_events', content: '...' }],
30
+ * }
31
+ * );
32
+ *
33
+ * if (result.success) {
34
+ * console.log('Deployed to main workspace!');
35
+ * }
36
+ * ```
37
+ */
38
+ export async function deployToMain(
39
+ config: BuildConfig,
40
+ resources: GeneratedResources,
41
+ options?: { debug?: boolean }
42
+ ): Promise<BuildApiResult> {
43
+ const debug = options?.debug ?? !!process.env.TINYBIRD_DEBUG;
44
+ const formData = new FormData();
45
+
46
+ // Add datasources
47
+ for (const ds of resources.datasources) {
48
+ const fieldName = `data_project://`;
49
+ const fileName = `${ds.name}.datasource`;
50
+ if (debug) {
51
+ console.log(`[debug] Adding datasource: ${fieldName} (filename: ${fileName})`);
52
+ console.log(`[debug] Content:\n${ds.content}\n`);
53
+ }
54
+ formData.append(
55
+ fieldName,
56
+ new Blob([ds.content], { type: "text/plain" }),
57
+ fileName
58
+ );
59
+ }
60
+
61
+ // Add pipes
62
+ for (const pipe of resources.pipes) {
63
+ const fieldName = `data_project://`;
64
+ const fileName = `${pipe.name}.pipe`;
65
+ if (debug) {
66
+ console.log(`[debug] Adding pipe: ${fieldName} (filename: ${fileName})`);
67
+ console.log(`[debug] Content:\n${pipe.content}\n`);
68
+ }
69
+ formData.append(
70
+ fieldName,
71
+ new Blob([pipe.content], { type: "text/plain" }),
72
+ fileName
73
+ );
74
+ }
75
+
76
+ // Make the request to /v1/deploy (instead of /v1/build)
77
+ const url = `${config.baseUrl.replace(/\/$/, "")}/v1/deploy`;
78
+
79
+ if (debug) {
80
+ console.log(`[debug] POST ${url}`);
81
+ }
82
+
83
+ const response = await fetch(url, {
84
+ method: "POST",
85
+ headers: {
86
+ Authorization: `Bearer ${config.token}`,
87
+ },
88
+ body: formData,
89
+ });
90
+
91
+ // Parse response
92
+ let body: BuildResponse;
93
+ const rawBody = await response.text();
94
+
95
+ if (debug) {
96
+ console.log(`[debug] Response status: ${response.status}`);
97
+ console.log(`[debug] Response body: ${rawBody}`);
98
+ }
99
+
100
+ try {
101
+ body = JSON.parse(rawBody) as BuildResponse;
102
+ } catch {
103
+ throw new Error(
104
+ `Failed to parse response from Tinybird API: ${response.status} ${response.statusText}\nBody: ${rawBody}`
105
+ );
106
+ }
107
+
108
+ // Helper to format errors
109
+ const formatErrors = (): string => {
110
+ if (body.errors && body.errors.length > 0) {
111
+ return body.errors
112
+ .map((e) => {
113
+ const prefix = e.filename ? `[${e.filename}] ` : "";
114
+ return `${prefix}${e.error}`;
115
+ })
116
+ .join("\n");
117
+ }
118
+ return body.error || `HTTP ${response.status}: ${response.statusText}`;
119
+ };
120
+
121
+ // Handle non-OK responses
122
+ if (!response.ok) {
123
+ return {
124
+ success: false,
125
+ result: "failed",
126
+ error: formatErrors(),
127
+ datasourceCount: resources.datasources.length,
128
+ pipeCount: resources.pipes.length,
129
+ };
130
+ }
131
+
132
+ // Handle API result
133
+ if (body.result === "failed") {
134
+ return {
135
+ success: false,
136
+ result: "failed",
137
+ error: formatErrors(),
138
+ datasourceCount: resources.datasources.length,
139
+ pipeCount: resources.pipes.length,
140
+ };
141
+ }
142
+
143
+ return {
144
+ success: true,
145
+ result: body.result,
146
+ datasourceCount: resources.datasources.length,
147
+ pipeCount: resources.pipes.length,
148
+ buildId: body.build?.id,
149
+ pipes: {
150
+ changed: body.build?.changed_pipe_names ?? [],
151
+ created: body.build?.new_pipe_names ?? [],
152
+ deleted: body.build?.deleted_pipe_names ?? [],
153
+ },
154
+ datasources: {
155
+ changed: body.build?.changed_datasource_names ?? [],
156
+ created: body.build?.new_datasource_names ?? [],
157
+ deleted: body.build?.deleted_datasource_names ?? [],
158
+ },
159
+ // Keep deprecated fields for backwards compatibility
160
+ changedPipeNames: body.build?.changed_pipe_names ?? [],
161
+ newPipeNames: body.build?.new_pipe_names ?? [],
162
+ };
163
+ }
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import {
3
+ getWorkspace,
4
+ WorkspaceApiError,
5
+ type WorkspaceApiConfig,
6
+ } from "./workspaces.js";
7
+
8
+ // Mock fetch globally
9
+ const mockFetch = vi.fn();
10
+ global.fetch = mockFetch;
11
+
12
+ describe("Workspace API client", () => {
13
+ const config: WorkspaceApiConfig = {
14
+ baseUrl: "https://api.tinybird.co",
15
+ token: "p.test-token",
16
+ };
17
+
18
+ beforeEach(() => {
19
+ mockFetch.mockReset();
20
+ });
21
+
22
+ describe("getWorkspace", () => {
23
+ it("returns workspace information", async () => {
24
+ const mockWorkspace = {
25
+ id: "9f42135e-3434-4d89-a90f-cb9cf74ce311",
26
+ name: "ts_client",
27
+ releases: [],
28
+ user_id: "412571dd-d2e6-4b3c-87b5-b29320414e22",
29
+ user_email: "user@example.com",
30
+ scope: "user",
31
+ main: null,
32
+ };
33
+
34
+ mockFetch.mockResolvedValueOnce({
35
+ ok: true,
36
+ json: () => Promise.resolve(mockWorkspace),
37
+ });
38
+
39
+ const result = await getWorkspace(config);
40
+
41
+ expect(mockFetch).toHaveBeenCalledWith(
42
+ "https://api.tinybird.co/v1/workspace",
43
+ {
44
+ method: "GET",
45
+ headers: {
46
+ Authorization: "Bearer p.test-token",
47
+ },
48
+ }
49
+ );
50
+ expect(result).toEqual(mockWorkspace);
51
+ });
52
+
53
+ it("throws WorkspaceApiError on failure", async () => {
54
+ mockFetch.mockResolvedValueOnce({
55
+ ok: false,
56
+ status: 401,
57
+ statusText: "Unauthorized",
58
+ text: () => Promise.resolve("Invalid token"),
59
+ });
60
+
61
+ await expect(getWorkspace(config)).rejects.toThrow(WorkspaceApiError);
62
+ });
63
+
64
+ it("includes status code in error", async () => {
65
+ mockFetch.mockResolvedValueOnce({
66
+ ok: false,
67
+ status: 403,
68
+ statusText: "Forbidden",
69
+ text: () => Promise.resolve("Access denied"),
70
+ });
71
+
72
+ try {
73
+ await getWorkspace(config);
74
+ expect.fail("Should have thrown");
75
+ } catch (error) {
76
+ expect(error).toBeInstanceOf(WorkspaceApiError);
77
+ expect((error as WorkspaceApiError).status).toBe(403);
78
+ }
79
+ });
80
+ });
81
+ });