@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,359 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import {
6
+ hasSrcFolder,
7
+ getLibDir,
8
+ getRelativeLibDir,
9
+ getTinybirdSchemaPath,
10
+ getRelativeSchemaPath,
11
+ findConfigFile,
12
+ loadConfig,
13
+ getConfigPath,
14
+ configExists,
15
+ updateConfig,
16
+ hasValidToken,
17
+ } from "./config.js";
18
+
19
+ describe("Config", () => {
20
+ let tempDir: string;
21
+
22
+ beforeEach(() => {
23
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-config-test-"));
24
+ });
25
+
26
+ afterEach(() => {
27
+ try {
28
+ fs.rmSync(tempDir, { recursive: true });
29
+ } catch {
30
+ // Ignore cleanup errors
31
+ }
32
+ vi.restoreAllMocks();
33
+ });
34
+
35
+ describe("hasSrcFolder", () => {
36
+ it("returns true when src folder exists", () => {
37
+ fs.mkdirSync(path.join(tempDir, "src"));
38
+
39
+ expect(hasSrcFolder(tempDir)).toBe(true);
40
+ });
41
+
42
+ it("returns false when src folder does not exist", () => {
43
+ expect(hasSrcFolder(tempDir)).toBe(false);
44
+ });
45
+
46
+ it("returns false when src is a file not a folder", () => {
47
+ fs.writeFileSync(path.join(tempDir, "src"), "not a folder");
48
+
49
+ expect(hasSrcFolder(tempDir)).toBe(false);
50
+ });
51
+ });
52
+
53
+ describe("getLibDir", () => {
54
+ it("returns src/tinybird when project has src folder", () => {
55
+ fs.mkdirSync(path.join(tempDir, "src"));
56
+
57
+ expect(getLibDir(tempDir)).toBe(path.join(tempDir, "src", "tinybird"));
58
+ });
59
+
60
+ it("returns tinybird when project does not have src folder", () => {
61
+ expect(getLibDir(tempDir)).toBe(path.join(tempDir, "tinybird"));
62
+ });
63
+ });
64
+
65
+ describe("getRelativeLibDir", () => {
66
+ it("returns src/tinybird when project has src folder", () => {
67
+ fs.mkdirSync(path.join(tempDir, "src"));
68
+
69
+ expect(getRelativeLibDir(tempDir)).toBe("src/tinybird");
70
+ });
71
+
72
+ it("returns tinybird when project does not have src folder", () => {
73
+ expect(getRelativeLibDir(tempDir)).toBe("tinybird");
74
+ });
75
+ });
76
+
77
+ describe("getTinybirdSchemaPath", () => {
78
+ it("returns src/tinybird/datasources.ts when project has src folder", () => {
79
+ fs.mkdirSync(path.join(tempDir, "src"));
80
+
81
+ expect(getTinybirdSchemaPath(tempDir)).toBe(
82
+ path.join(tempDir, "src", "tinybird", "datasources.ts")
83
+ );
84
+ });
85
+
86
+ it("returns tinybird/datasources.ts when project does not have src folder", () => {
87
+ expect(getTinybirdSchemaPath(tempDir)).toBe(
88
+ path.join(tempDir, "tinybird", "datasources.ts")
89
+ );
90
+ });
91
+ });
92
+
93
+ describe("getRelativeSchemaPath", () => {
94
+ it("returns src/tinybird/datasources.ts when project has src folder", () => {
95
+ fs.mkdirSync(path.join(tempDir, "src"));
96
+
97
+ expect(getRelativeSchemaPath(tempDir)).toBe("src/tinybird/datasources.ts");
98
+ });
99
+
100
+ it("returns tinybird/datasources.ts when project does not have src folder", () => {
101
+ expect(getRelativeSchemaPath(tempDir)).toBe("tinybird/datasources.ts");
102
+ });
103
+ });
104
+
105
+ describe("findConfigFile", () => {
106
+ it("finds tinybird.json in current directory", () => {
107
+ const configPath = path.join(tempDir, "tinybird.json");
108
+ fs.writeFileSync(configPath, "{}");
109
+
110
+ expect(findConfigFile(tempDir)).toBe(configPath);
111
+ });
112
+
113
+ it("finds tinybird.json in parent directory", () => {
114
+ const nestedDir = path.join(tempDir, "src", "app");
115
+ fs.mkdirSync(nestedDir, { recursive: true });
116
+ const configPath = path.join(tempDir, "tinybird.json");
117
+ fs.writeFileSync(configPath, "{}");
118
+
119
+ expect(findConfigFile(nestedDir)).toBe(configPath);
120
+ });
121
+
122
+ it("returns null when no config file exists", () => {
123
+ expect(findConfigFile(tempDir)).toBe(null);
124
+ });
125
+ });
126
+
127
+ describe("getConfigPath", () => {
128
+ it("returns path to tinybird.json in directory", () => {
129
+ expect(getConfigPath(tempDir)).toBe(path.join(tempDir, "tinybird.json"));
130
+ });
131
+ });
132
+
133
+ describe("configExists", () => {
134
+ it("returns true when config exists", () => {
135
+ fs.writeFileSync(path.join(tempDir, "tinybird.json"), "{}");
136
+
137
+ expect(configExists(tempDir)).toBe(true);
138
+ });
139
+
140
+ it("returns false when config does not exist", () => {
141
+ expect(configExists(tempDir)).toBe(false);
142
+ });
143
+ });
144
+
145
+ describe("loadConfig", () => {
146
+ beforeEach(() => {
147
+ // Mock git functions to avoid git dependency in tests
148
+ vi.mock("./git.js", () => ({
149
+ getCurrentGitBranch: () => "main",
150
+ isMainBranch: () => true,
151
+ getTinybirdBranchName: () => null,
152
+ }));
153
+ });
154
+
155
+ it("throws error when no config file exists", () => {
156
+ expect(() => loadConfig(tempDir)).toThrow("Could not find tinybird.json");
157
+ });
158
+
159
+ it("loads config with include array", () => {
160
+ const config = {
161
+ include: ["lib/datasources.ts", "lib/pipes.ts"],
162
+ token: "test-token",
163
+ };
164
+ fs.writeFileSync(
165
+ path.join(tempDir, "tinybird.json"),
166
+ JSON.stringify(config)
167
+ );
168
+
169
+ const result = loadConfig(tempDir);
170
+
171
+ expect(result.include).toEqual(["lib/datasources.ts", "lib/pipes.ts"]);
172
+ expect(result.token).toBe("test-token");
173
+ expect(result.baseUrl).toBe("https://api.tinybird.co");
174
+ });
175
+
176
+ it("loads legacy config with schema (backward compat)", () => {
177
+ const config = {
178
+ schema: "src/lib/tinybird.ts",
179
+ token: "test-token",
180
+ };
181
+ fs.writeFileSync(
182
+ path.join(tempDir, "tinybird.json"),
183
+ JSON.stringify(config)
184
+ );
185
+
186
+ const result = loadConfig(tempDir);
187
+
188
+ // Legacy schema is converted to include array
189
+ expect(result.include).toEqual(["src/lib/tinybird.ts"]);
190
+ });
191
+
192
+ it("loads config with custom baseUrl", () => {
193
+ const config = {
194
+ schema: "lib/tinybird.ts",
195
+ token: "test-token",
196
+ baseUrl: "https://api.us-east.tinybird.co",
197
+ };
198
+ fs.writeFileSync(
199
+ path.join(tempDir, "tinybird.json"),
200
+ JSON.stringify(config)
201
+ );
202
+
203
+ const result = loadConfig(tempDir);
204
+
205
+ expect(result.baseUrl).toBe("https://api.us-east.tinybird.co");
206
+ });
207
+
208
+ it("interpolates environment variables in token", () => {
209
+ process.env.TEST_TINYBIRD_TOKEN = "secret-token-from-env";
210
+ const config = {
211
+ schema: "lib/tinybird.ts",
212
+ token: "${TEST_TINYBIRD_TOKEN}",
213
+ };
214
+ fs.writeFileSync(
215
+ path.join(tempDir, "tinybird.json"),
216
+ JSON.stringify(config)
217
+ );
218
+
219
+ const result = loadConfig(tempDir);
220
+
221
+ expect(result.token).toBe("secret-token-from-env");
222
+ delete process.env.TEST_TINYBIRD_TOKEN;
223
+ });
224
+
225
+ it("throws error when env var is not set", () => {
226
+ delete process.env.NONEXISTENT_VAR;
227
+ const config = {
228
+ schema: "lib/tinybird.ts",
229
+ token: "${NONEXISTENT_VAR}",
230
+ };
231
+ fs.writeFileSync(
232
+ path.join(tempDir, "tinybird.json"),
233
+ JSON.stringify(config)
234
+ );
235
+
236
+ expect(() => loadConfig(tempDir)).toThrow(
237
+ "Environment variable NONEXISTENT_VAR is not set"
238
+ );
239
+ });
240
+
241
+ it("throws error when include field is missing", () => {
242
+ const config = {
243
+ token: "test-token",
244
+ };
245
+ fs.writeFileSync(
246
+ path.join(tempDir, "tinybird.json"),
247
+ JSON.stringify(config)
248
+ );
249
+
250
+ expect(() => loadConfig(tempDir)).toThrow("Missing 'include' field");
251
+ });
252
+
253
+ it("throws error when token field is missing", () => {
254
+ const config = {
255
+ schema: "lib/tinybird.ts",
256
+ };
257
+ fs.writeFileSync(
258
+ path.join(tempDir, "tinybird.json"),
259
+ JSON.stringify(config)
260
+ );
261
+
262
+ expect(() => loadConfig(tempDir)).toThrow("Missing 'token' field");
263
+ });
264
+
265
+ it("throws error for invalid JSON", () => {
266
+ fs.writeFileSync(path.join(tempDir, "tinybird.json"), "not valid json");
267
+
268
+ expect(() => loadConfig(tempDir)).toThrow("Failed to parse");
269
+ });
270
+ });
271
+
272
+ describe("updateConfig", () => {
273
+ it("updates existing config file", () => {
274
+ const configPath = path.join(tempDir, "tinybird.json");
275
+ const initialConfig = {
276
+ schema: "lib/tinybird.ts",
277
+ token: "${TINYBIRD_TOKEN}",
278
+ baseUrl: "https://api.tinybird.co",
279
+ };
280
+ fs.writeFileSync(configPath, JSON.stringify(initialConfig));
281
+
282
+ updateConfig(configPath, { baseUrl: "https://api.us-east.tinybird.co" });
283
+
284
+ const updatedConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
285
+ expect(updatedConfig.baseUrl).toBe("https://api.us-east.tinybird.co");
286
+ expect(updatedConfig.schema).toBe("lib/tinybird.ts");
287
+ expect(updatedConfig.token).toBe("${TINYBIRD_TOKEN}");
288
+ });
289
+
290
+ it("throws error when config file does not exist", () => {
291
+ const configPath = path.join(tempDir, "nonexistent.json");
292
+
293
+ expect(() => updateConfig(configPath, { baseUrl: "test" })).toThrow(
294
+ "Config not found"
295
+ );
296
+ });
297
+ });
298
+
299
+ describe("hasValidToken", () => {
300
+ it("returns false when no config exists", () => {
301
+ expect(hasValidToken(tempDir)).toBe(false);
302
+ });
303
+
304
+ it("returns true when token is literal value", () => {
305
+ const config = {
306
+ schema: "lib/tinybird.ts",
307
+ token: "p.some-token",
308
+ };
309
+ fs.writeFileSync(
310
+ path.join(tempDir, "tinybird.json"),
311
+ JSON.stringify(config)
312
+ );
313
+
314
+ expect(hasValidToken(tempDir)).toBe(true);
315
+ });
316
+
317
+ it("returns true when token env var is set", () => {
318
+ process.env.HAS_VALID_TOKEN_TEST = "some-value";
319
+ const config = {
320
+ schema: "lib/tinybird.ts",
321
+ token: "${HAS_VALID_TOKEN_TEST}",
322
+ };
323
+ fs.writeFileSync(
324
+ path.join(tempDir, "tinybird.json"),
325
+ JSON.stringify(config)
326
+ );
327
+
328
+ expect(hasValidToken(tempDir)).toBe(true);
329
+ delete process.env.HAS_VALID_TOKEN_TEST;
330
+ });
331
+
332
+ it("returns false when token env var is not set", () => {
333
+ delete process.env.MISSING_TOKEN_VAR;
334
+ const config = {
335
+ schema: "lib/tinybird.ts",
336
+ token: "${MISSING_TOKEN_VAR}",
337
+ };
338
+ fs.writeFileSync(
339
+ path.join(tempDir, "tinybird.json"),
340
+ JSON.stringify(config)
341
+ );
342
+
343
+ expect(hasValidToken(tempDir)).toBe(false);
344
+ });
345
+
346
+ it("returns false when token field is empty", () => {
347
+ const config = {
348
+ schema: "lib/tinybird.ts",
349
+ token: "",
350
+ };
351
+ fs.writeFileSync(
352
+ path.join(tempDir, "tinybird.json"),
353
+ JSON.stringify(config)
354
+ );
355
+
356
+ expect(hasValidToken(tempDir)).toBe(false);
357
+ });
358
+ });
359
+ });
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Configuration loader for tinybird.json
3
+ */
4
+
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { getCurrentGitBranch, isMainBranch, getTinybirdBranchName } from "./git.js";
8
+
9
+ /**
10
+ * Tinybird configuration file structure
11
+ */
12
+ export interface TinybirdConfig {
13
+ /** Array of TypeScript files to scan for datasources and pipes */
14
+ include?: string[];
15
+ /** @deprecated Use `include` instead. Path to the TypeScript schema entry point */
16
+ schema?: string;
17
+ /** API token (supports ${ENV_VAR} interpolation) */
18
+ token: string;
19
+ /** Tinybird API base URL (optional, defaults to EU region) */
20
+ baseUrl?: string;
21
+ }
22
+
23
+ /**
24
+ * Resolved configuration with all values expanded
25
+ */
26
+ export interface ResolvedConfig {
27
+ /** Array of TypeScript files to scan for datasources and pipes */
28
+ include: string[];
29
+ /** Resolved API token (workspace main token) */
30
+ token: string;
31
+ /** Tinybird API base URL */
32
+ baseUrl: string;
33
+ /** Path to the config file */
34
+ configPath: string;
35
+ /** Working directory */
36
+ cwd: string;
37
+ /** Current git branch (null if not in git repo or detached HEAD) */
38
+ gitBranch: string | null;
39
+ /** Sanitized branch name for Tinybird (symbols replaced with underscores) */
40
+ tinybirdBranch: string | null;
41
+ /** Whether we're on the main/master branch */
42
+ isMainBranch: boolean;
43
+ }
44
+
45
+ /**
46
+ * Default base URL (EU region)
47
+ */
48
+ const DEFAULT_BASE_URL = "https://api.tinybird.co";
49
+
50
+ /**
51
+ * Config file name
52
+ */
53
+ const CONFIG_FILE = "tinybird.json";
54
+
55
+ /**
56
+ * Tinybird folder name
57
+ */
58
+ const TINYBIRD_FOLDER = "tinybird";
59
+
60
+ /**
61
+ * Detect if project has a src folder
62
+ */
63
+ export function hasSrcFolder(cwd: string): boolean {
64
+ const srcPath = path.join(cwd, "src");
65
+ return fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory();
66
+ }
67
+
68
+ /**
69
+ * Get the tinybird directory path based on project structure
70
+ * Returns 'src/tinybird' if project has src folder, otherwise 'tinybird'
71
+ */
72
+ export function getTinybirdDir(cwd: string): string {
73
+ return hasSrcFolder(cwd) ? path.join(cwd, "src", TINYBIRD_FOLDER) : path.join(cwd, TINYBIRD_FOLDER);
74
+ }
75
+
76
+ /**
77
+ * Get the relative tinybird directory path based on project structure
78
+ */
79
+ export function getRelativeTinybirdDir(cwd: string): string {
80
+ return hasSrcFolder(cwd) ? `src/${TINYBIRD_FOLDER}` : TINYBIRD_FOLDER;
81
+ }
82
+
83
+ /**
84
+ * Get the datasources.ts path based on project structure
85
+ */
86
+ export function getDatasourcesPath(cwd: string): string {
87
+ return path.join(getTinybirdDir(cwd), "datasources.ts");
88
+ }
89
+
90
+ /**
91
+ * Get the pipes.ts path based on project structure
92
+ */
93
+ export function getPipesPath(cwd: string): string {
94
+ return path.join(getTinybirdDir(cwd), "pipes.ts");
95
+ }
96
+
97
+ /**
98
+ * Get the client.ts path based on project structure
99
+ */
100
+ export function getClientPath(cwd: string): string {
101
+ return path.join(getTinybirdDir(cwd), "client.ts");
102
+ }
103
+
104
+ // Legacy exports for backwards compatibility
105
+ export function getLibDir(cwd: string): string {
106
+ return getTinybirdDir(cwd);
107
+ }
108
+
109
+ export function getRelativeLibDir(cwd: string): string {
110
+ return getRelativeTinybirdDir(cwd);
111
+ }
112
+
113
+ export function getTinybirdSchemaPath(cwd: string): string {
114
+ return getDatasourcesPath(cwd);
115
+ }
116
+
117
+ export function getRelativeSchemaPath(cwd: string): string {
118
+ return `${getRelativeTinybirdDir(cwd)}/datasources.ts`;
119
+ }
120
+
121
+ /**
122
+ * Interpolate environment variables in a string
123
+ *
124
+ * Supports ${VAR_NAME} syntax
125
+ */
126
+ function interpolateEnvVars(value: string): string {
127
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
128
+ const envValue = process.env[envVar];
129
+ if (envValue === undefined) {
130
+ throw new Error(`Environment variable ${envVar} is not set`);
131
+ }
132
+ return envValue;
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Find the config file by walking up the directory tree
138
+ *
139
+ * @param startDir - Directory to start searching from
140
+ * @returns Path to the config file, or null if not found
141
+ */
142
+ export function findConfigFile(startDir: string): string | null {
143
+ let currentDir = startDir;
144
+
145
+ while (true) {
146
+ const configPath = path.join(currentDir, CONFIG_FILE);
147
+ if (fs.existsSync(configPath)) {
148
+ return configPath;
149
+ }
150
+
151
+ const parentDir = path.dirname(currentDir);
152
+ if (parentDir === currentDir) {
153
+ // Reached root
154
+ return null;
155
+ }
156
+ currentDir = parentDir;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Load and resolve the tinybird.json configuration
162
+ *
163
+ * @param cwd - Working directory to start searching from (defaults to process.cwd())
164
+ * @returns Resolved configuration
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const config = loadConfig();
169
+ * console.log(config.schema); // 'lib/tinybird.ts' or 'src/lib/tinybird.ts'
170
+ * console.log(config.token); // 'p.xxx' (resolved from ${TINYBIRD_TOKEN})
171
+ * ```
172
+ */
173
+ export function loadConfig(cwd: string = process.cwd()): ResolvedConfig {
174
+ const configPath = findConfigFile(cwd);
175
+
176
+ if (!configPath) {
177
+ throw new Error(
178
+ `Could not find ${CONFIG_FILE}. Run 'npx tinybird init' to create one.`
179
+ );
180
+ }
181
+
182
+ let rawContent: string;
183
+ try {
184
+ rawContent = fs.readFileSync(configPath, "utf-8");
185
+ } catch (error) {
186
+ throw new Error(`Failed to read ${configPath}: ${(error as Error).message}`);
187
+ }
188
+
189
+ let config: TinybirdConfig;
190
+ try {
191
+ config = JSON.parse(rawContent) as TinybirdConfig;
192
+ } catch (error) {
193
+ throw new Error(`Failed to parse ${configPath}: ${(error as Error).message}`);
194
+ }
195
+
196
+ // Validate required fields - need either include or schema
197
+ if (!config.include && !config.schema) {
198
+ throw new Error(`Missing 'include' field in ${configPath}. Add an array of files to scan for datasources and pipes.`);
199
+ }
200
+
201
+ if (!config.token) {
202
+ throw new Error(`Missing 'token' field in ${configPath}`);
203
+ }
204
+
205
+ // Resolve include paths (support legacy schema field)
206
+ let include: string[];
207
+ if (config.include) {
208
+ include = config.include;
209
+ } else if (config.schema) {
210
+ // Legacy mode: treat schema as a single include path
211
+ include = [config.schema];
212
+ } else {
213
+ // Should never reach here due to validation above
214
+ include = [];
215
+ }
216
+
217
+ // Get the directory containing the config file
218
+ const configDir = path.dirname(configPath);
219
+
220
+ // Resolve token (may contain env vars)
221
+ let resolvedToken: string;
222
+ try {
223
+ resolvedToken = interpolateEnvVars(config.token);
224
+ } catch (error) {
225
+ throw new Error(
226
+ `Failed to resolve token in ${configPath}: ${(error as Error).message}`
227
+ );
228
+ }
229
+
230
+ // Resolve base URL
231
+ let resolvedBaseUrl = DEFAULT_BASE_URL;
232
+ if (config.baseUrl) {
233
+ try {
234
+ resolvedBaseUrl = interpolateEnvVars(config.baseUrl);
235
+ } catch (error) {
236
+ throw new Error(
237
+ `Failed to resolve baseUrl in ${configPath}: ${(error as Error).message}`
238
+ );
239
+ }
240
+ }
241
+
242
+ // Detect git branch
243
+ const gitBranch = getCurrentGitBranch();
244
+ const tinybirdBranch = getTinybirdBranchName();
245
+
246
+ return {
247
+ include,
248
+ token: resolvedToken,
249
+ baseUrl: resolvedBaseUrl,
250
+ configPath,
251
+ cwd: configDir,
252
+ gitBranch,
253
+ tinybirdBranch,
254
+ isMainBranch: isMainBranch(),
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Check if a config file exists in the given directory
260
+ */
261
+ export function configExists(cwd: string = process.cwd()): boolean {
262
+ return findConfigFile(cwd) !== null;
263
+ }
264
+
265
+ /**
266
+ * Get the expected config file path for a directory
267
+ */
268
+ export function getConfigPath(cwd: string = process.cwd()): string {
269
+ return path.join(cwd, CONFIG_FILE);
270
+ }
271
+
272
+ /**
273
+ * Update specific fields in tinybird.json
274
+ *
275
+ * Throws an error if the config file doesn't exist to prevent creating
276
+ * partial config files that would break loadConfig.
277
+ *
278
+ * @param configPath - Path to the config file
279
+ * @param updates - Fields to update
280
+ * @throws Error if config file doesn't exist
281
+ */
282
+ export function updateConfig(
283
+ configPath: string,
284
+ updates: Partial<TinybirdConfig>
285
+ ): void {
286
+ if (!fs.existsSync(configPath)) {
287
+ throw new Error(`Config not found at ${configPath}`);
288
+ }
289
+
290
+ const content = fs.readFileSync(configPath, "utf-8");
291
+ const config = JSON.parse(content) as TinybirdConfig;
292
+
293
+ // Merge updates
294
+ const updated = { ...config, ...updates };
295
+
296
+ fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + "\n");
297
+ }
298
+
299
+ /**
300
+ * Check if a valid token is configured (either in file or via env var)
301
+ *
302
+ * @param cwd - Working directory to search from
303
+ * @returns true if a valid token exists
304
+ */
305
+ export function hasValidToken(cwd: string = process.cwd()): boolean {
306
+ try {
307
+ const configPath = findConfigFile(cwd);
308
+ if (!configPath) {
309
+ return false;
310
+ }
311
+
312
+ const content = fs.readFileSync(configPath, "utf-8");
313
+ const config = JSON.parse(content) as TinybirdConfig;
314
+
315
+ if (!config.token) {
316
+ return false;
317
+ }
318
+
319
+ // Check if token is a placeholder or env var reference
320
+ if (config.token.includes("${")) {
321
+ // Try to resolve the env var
322
+ try {
323
+ const resolved = interpolateEnvVars(config.token);
324
+ return Boolean(resolved);
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ // Token is a literal value
331
+ return Boolean(config.token);
332
+ } catch {
333
+ return false;
334
+ }
335
+ }