@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,272 @@
1
+ /**
2
+ * Schema validation for pipe output
3
+ * Validates that query responses match the declared output schema
4
+ */
5
+
6
+ import { TinybirdClient } from "../../client/base.js";
7
+ import type { ProjectDefinition, PipesDefinition } from "../../schema/project.js";
8
+ import type { PipeDefinition, OutputDefinition } from "../../schema/pipe.js";
9
+ import type { ColumnMeta } from "../../client/types.js";
10
+ import type { LoadedEntities } from "../../generator/loader.js";
11
+
12
+ /**
13
+ * Options for schema validation
14
+ */
15
+ export interface SchemaValidationOptions {
16
+ /** The project definition containing pipe schemas (legacy) */
17
+ project?: ProjectDefinition;
18
+ /** The loaded entities containing pipe schemas (new) */
19
+ entities?: LoadedEntities;
20
+ /** Names of pipes to validate */
21
+ pipeNames: string[];
22
+ /** Tinybird API base URL */
23
+ baseUrl: string;
24
+ /** API token for authentication */
25
+ token: string;
26
+ }
27
+
28
+ /**
29
+ * A single validation issue
30
+ */
31
+ export interface ValidationIssue {
32
+ /** Name of the pipe with the issue */
33
+ pipeName: string;
34
+ /** Issue severity */
35
+ type: "error" | "warning";
36
+ /** Human-readable description of the issue */
37
+ message: string;
38
+ }
39
+
40
+ /**
41
+ * Result of schema validation
42
+ */
43
+ export interface SchemaValidationResult {
44
+ /** Whether all validations passed (no errors) */
45
+ valid: boolean;
46
+ /** List of validation issues found */
47
+ issues: ValidationIssue[];
48
+ /** Names of pipes that were successfully validated */
49
+ pipesValidated: string[];
50
+ /** Names of pipes that were skipped (e.g., require params) */
51
+ pipesSkipped: string[];
52
+ }
53
+
54
+ /**
55
+ * Internal result of validating a single pipe's output schema
56
+ */
57
+ interface ColumnValidation {
58
+ valid: boolean;
59
+ missingColumns: { name: string; expectedType: string }[];
60
+ extraColumns: { name: string; actualType: string }[];
61
+ typeMismatches: { name: string; expectedType: string; actualType: string }[];
62
+ }
63
+
64
+ /**
65
+ * Validate pipe schemas by querying them and comparing response to output definition
66
+ *
67
+ * @param options - Validation options
68
+ * @returns Validation result with issues found
69
+ */
70
+ export async function validatePipeSchemas(
71
+ options: SchemaValidationOptions
72
+ ): Promise<SchemaValidationResult> {
73
+ const client = new TinybirdClient({
74
+ baseUrl: options.baseUrl,
75
+ token: options.token,
76
+ });
77
+
78
+ const result: SchemaValidationResult = {
79
+ valid: true,
80
+ issues: [],
81
+ pipesValidated: [],
82
+ pipesSkipped: [],
83
+ };
84
+
85
+ // Get pipes from either project or entities
86
+ const pipes: PipesDefinition = options.entities
87
+ ? Object.fromEntries(
88
+ Object.entries(options.entities.pipes).map(([name, { definition }]) => [name, definition])
89
+ )
90
+ : options.project?.pipes ?? {};
91
+
92
+ // Only validate the specified pipes
93
+ for (const pipeName of options.pipeNames) {
94
+ // Find pipe by name
95
+ const pipe = Object.values(pipes).find(
96
+ (p) => p._name === pipeName
97
+ );
98
+
99
+ if (!pipe) {
100
+ // Pipe exists in Tinybird but not in local schema (could be deleted or renamed)
101
+ continue;
102
+ }
103
+
104
+ // Skip if pipe has required params without defaults
105
+ if (hasRequiredParams(pipe)) {
106
+ result.pipesSkipped.push(pipeName);
107
+ continue;
108
+ }
109
+
110
+ // Skip if pipe has no output schema (reusable pipes)
111
+ if (!pipe._output) {
112
+ result.pipesSkipped.push(pipeName);
113
+ continue;
114
+ }
115
+
116
+ // Build params using defaults
117
+ const params = buildDefaultParams(pipe);
118
+
119
+ try {
120
+ const response = await client.query(pipeName, params);
121
+ const validation = validateOutputSchema(response.meta, pipe._output);
122
+
123
+ if (!validation.valid) {
124
+ result.valid = false;
125
+ }
126
+
127
+ // Add missing column errors
128
+ for (const missing of validation.missingColumns) {
129
+ result.issues.push({
130
+ pipeName,
131
+ type: "error",
132
+ message: `Missing column '${missing.name}' (expected: ${missing.expectedType})`,
133
+ });
134
+ }
135
+
136
+ // Add type mismatch errors
137
+ for (const mismatch of validation.typeMismatches) {
138
+ result.issues.push({
139
+ pipeName,
140
+ type: "error",
141
+ message: `Type mismatch '${mismatch.name}': expected ${mismatch.expectedType}, got ${mismatch.actualType}`,
142
+ });
143
+ }
144
+
145
+ // Add extra column warnings
146
+ for (const extra of validation.extraColumns) {
147
+ result.issues.push({
148
+ pipeName,
149
+ type: "warning",
150
+ message: `Extra column '${extra.name}' (${extra.actualType}) not in output schema`,
151
+ });
152
+ }
153
+
154
+ result.pipesValidated.push(pipeName);
155
+ } catch {
156
+ // Query failed - skip validation for this pipe
157
+ // This could happen if the pipe doesn't exist yet, network issues, etc.
158
+ result.pipesSkipped.push(pipeName);
159
+ }
160
+ }
161
+
162
+ return result;
163
+ }
164
+
165
+ /**
166
+ * Check if a pipe has any required parameters without defaults
167
+ */
168
+ function hasRequiredParams(pipe: PipeDefinition): boolean {
169
+ if (!pipe._params) return false;
170
+
171
+ for (const param of Object.values(pipe._params)) {
172
+ if (param._required && param._default === undefined) {
173
+ return true;
174
+ }
175
+ }
176
+ return false;
177
+ }
178
+
179
+ /**
180
+ * Build a params object using default values from the pipe definition
181
+ */
182
+ function buildDefaultParams(pipe: PipeDefinition): Record<string, unknown> {
183
+ const params: Record<string, unknown> = {};
184
+
185
+ if (!pipe._params) return params;
186
+
187
+ for (const [name, param] of Object.entries(pipe._params)) {
188
+ if (param._default !== undefined) {
189
+ params[name] = param._default;
190
+ }
191
+ }
192
+
193
+ return params;
194
+ }
195
+
196
+ /**
197
+ * Validate response metadata against the expected output schema
198
+ */
199
+ function validateOutputSchema(
200
+ responseMeta: ColumnMeta[],
201
+ outputSchema: OutputDefinition
202
+ ): ColumnValidation {
203
+ const result: ColumnValidation = {
204
+ valid: true,
205
+ missingColumns: [],
206
+ extraColumns: [],
207
+ typeMismatches: [],
208
+ };
209
+
210
+ // Build a map of response columns for lookup
211
+ const responseColumns = new Map(
212
+ responseMeta.map((col) => [col.name, col.type])
213
+ );
214
+
215
+ // Check each expected column from the schema
216
+ for (const [name, validator] of Object.entries(outputSchema)) {
217
+ const expectedType = validator._tinybirdType;
218
+ const actualType = responseColumns.get(name);
219
+
220
+ if (!actualType) {
221
+ // Column missing from response
222
+ result.missingColumns.push({ name, expectedType });
223
+ result.valid = false;
224
+ } else if (!typesAreCompatible(actualType, expectedType)) {
225
+ // Column exists but type doesn't match
226
+ result.typeMismatches.push({ name, expectedType, actualType });
227
+ result.valid = false;
228
+ }
229
+
230
+ // Remove from map so we can find extra columns
231
+ responseColumns.delete(name);
232
+ }
233
+
234
+ // Remaining columns are extras (warnings, not errors)
235
+ for (const [name, actualType] of responseColumns) {
236
+ result.extraColumns.push({ name, actualType });
237
+ }
238
+
239
+ return result;
240
+ }
241
+
242
+ /**
243
+ * Check if two ClickHouse types are compatible
244
+ * Handles Nullable, LowCardinality, and timezone variations
245
+ */
246
+ function typesAreCompatible(actual: string, expected: string): boolean {
247
+ const normalize = (t: string): string => {
248
+ let normalized = t;
249
+ // Remove LowCardinality(Nullable(...)) to just the inner type (must be before individual removals)
250
+ normalized = normalized.replace(
251
+ /^LowCardinality\(Nullable\((.+)\)\)$/,
252
+ "$1"
253
+ );
254
+ // Remove Nullable wrapper
255
+ normalized = normalized.replace(/^Nullable\((.+)\)$/, "$1");
256
+ // Remove LowCardinality wrapper
257
+ normalized = normalized.replace(/^LowCardinality\((.+)\)$/, "$1");
258
+ // Remove timezone from DateTime
259
+ normalized = normalized.replace(/^DateTime\('.+'\)$/, "DateTime");
260
+ // Remove precision and timezone from DateTime64
261
+ normalized = normalized.replace(/^DateTime64\(\d+(, '.+')?\)$/, "DateTime64");
262
+ return normalized;
263
+ };
264
+
265
+ return normalize(actual) === normalize(expected);
266
+ }
267
+
268
+ // Export internal functions for testing
269
+ export { typesAreCompatible as _typesAreCompatible };
270
+ export { validateOutputSchema as _validateOutputSchema };
271
+ export { hasRequiredParams as _hasRequiredParams };
272
+ export { buildDefaultParams as _buildDefaultParams };
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Tinybird client for querying pipes and ingesting events
3
+ */
4
+
5
+ import type {
6
+ ClientConfig,
7
+ QueryResult,
8
+ IngestResult,
9
+ QueryOptions,
10
+ IngestOptions,
11
+ TinybirdErrorResponse,
12
+ } from "./types.js";
13
+ import { TinybirdError } from "./types.js";
14
+
15
+ /**
16
+ * Default timeout for requests (30 seconds)
17
+ */
18
+ const DEFAULT_TIMEOUT = 30000;
19
+
20
+ /**
21
+ * Resolved token info from dev mode
22
+ */
23
+ interface ResolvedTokenInfo {
24
+ token: string;
25
+ isBranchToken: boolean;
26
+ branchName?: string;
27
+ }
28
+
29
+ /**
30
+ * Tinybird API client
31
+ *
32
+ * Provides methods for querying pipe endpoints and ingesting events to datasources.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { TinybirdClient } from '@tinybirdco/sdk';
37
+ *
38
+ * const client = new TinybirdClient({
39
+ * baseUrl: 'https://api.tinybird.co',
40
+ * token: process.env.TINYBIRD_TOKEN,
41
+ * });
42
+ *
43
+ * // Query a pipe
44
+ * const result = await client.query('top_events', {
45
+ * start_date: '2024-01-01',
46
+ * end_date: '2024-01-31',
47
+ * });
48
+ *
49
+ * // Ingest an event
50
+ * await client.ingest('events', {
51
+ * timestamp: new Date().toISOString(),
52
+ * event_type: 'page_view',
53
+ * user_id: 'user_123',
54
+ * });
55
+ * ```
56
+ */
57
+ export class TinybirdClient {
58
+ private readonly config: ClientConfig;
59
+ private readonly fetchFn: typeof fetch;
60
+ private tokenPromise: Promise<ResolvedTokenInfo> | null = null;
61
+ private resolvedToken: string | null = null;
62
+
63
+ constructor(config: ClientConfig) {
64
+ // Validate required config
65
+ if (!config.baseUrl) {
66
+ throw new Error("baseUrl is required");
67
+ }
68
+ if (!config.token) {
69
+ throw new Error("token is required");
70
+ }
71
+
72
+ // Normalize base URL (remove trailing slash)
73
+ this.config = {
74
+ ...config,
75
+ baseUrl: config.baseUrl.replace(/\/$/, ""),
76
+ };
77
+
78
+ this.fetchFn = config.fetch ?? globalThis.fetch;
79
+ }
80
+
81
+ /**
82
+ * Get the effective token, resolving branch token in dev mode if needed
83
+ */
84
+ private async getToken(): Promise<string> {
85
+ // If already resolved, return it
86
+ if (this.resolvedToken) {
87
+ return this.resolvedToken;
88
+ }
89
+
90
+ // If not in dev mode, use the configured token
91
+ if (!this.config.devMode) {
92
+ this.resolvedToken = this.config.token;
93
+ return this.resolvedToken;
94
+ }
95
+
96
+ // In dev mode, lazily resolve the branch token
97
+ if (!this.tokenPromise) {
98
+ this.tokenPromise = this.resolveBranchToken();
99
+ }
100
+
101
+ const resolved = await this.tokenPromise;
102
+ this.resolvedToken = resolved.token;
103
+ return this.resolvedToken;
104
+ }
105
+
106
+ /**
107
+ * Resolve the branch token in dev mode
108
+ */
109
+ private async resolveBranchToken(): Promise<ResolvedTokenInfo> {
110
+ try {
111
+ // Dynamic import to avoid circular dependencies and to keep CLI code
112
+ // out of the client bundle when not using dev mode
113
+ const { loadConfig } = await import("../cli/config.js");
114
+ const { getOrCreateBranch } = await import("../api/branches.js");
115
+
116
+ const config = loadConfig();
117
+
118
+ // If on main branch, use the workspace token
119
+ if (config.isMainBranch || !config.tinybirdBranch) {
120
+ return { token: this.config.token, isBranchToken: false };
121
+ }
122
+
123
+ const branchName = config.tinybirdBranch;
124
+
125
+ // Get or create branch (always fetch fresh to avoid stale cache issues)
126
+ const branch = await getOrCreateBranch(
127
+ { baseUrl: this.config.baseUrl, token: this.config.token },
128
+ branchName
129
+ );
130
+
131
+ if (!branch.token) {
132
+ // Fall back to workspace token if no branch token
133
+ return { token: this.config.token, isBranchToken: false };
134
+ }
135
+
136
+ return {
137
+ token: branch.token,
138
+ isBranchToken: true,
139
+ branchName,
140
+ };
141
+ } catch {
142
+ // If anything fails, fall back to the workspace token
143
+ return { token: this.config.token, isBranchToken: false };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Query a pipe endpoint
149
+ *
150
+ * @param pipeName - Name of the pipe to query
151
+ * @param params - Query parameters
152
+ * @param options - Additional request options
153
+ * @returns Query result with typed data
154
+ */
155
+ async query<T = unknown>(
156
+ pipeName: string,
157
+ params: Record<string, unknown> = {},
158
+ options: QueryOptions = {}
159
+ ): Promise<QueryResult<T>> {
160
+ const token = await this.getToken();
161
+ const url = new URL(`/v0/pipes/${pipeName}.json`, this.config.baseUrl);
162
+
163
+ // Add parameters to query string
164
+ for (const [key, value] of Object.entries(params)) {
165
+ if (value !== undefined && value !== null) {
166
+ if (Array.isArray(value)) {
167
+ // Handle array parameters
168
+ for (const item of value) {
169
+ url.searchParams.append(key, String(item));
170
+ }
171
+ } else if (value instanceof Date) {
172
+ // Handle Date objects
173
+ url.searchParams.set(key, value.toISOString());
174
+ } else {
175
+ url.searchParams.set(key, String(value));
176
+ }
177
+ }
178
+ }
179
+
180
+ const response = await this.fetch(url.toString(), {
181
+ method: "GET",
182
+ headers: {
183
+ Authorization: `Bearer ${token}`,
184
+ },
185
+ signal: this.createAbortSignal(options.timeout, options.signal),
186
+ });
187
+
188
+ if (!response.ok) {
189
+ await this.handleErrorResponse(response);
190
+ }
191
+
192
+ const result = (await response.json()) as QueryResult<T>;
193
+ return result;
194
+ }
195
+
196
+ /**
197
+ * Ingest a single event to a datasource
198
+ *
199
+ * @param datasourceName - Name of the datasource
200
+ * @param event - Event data to ingest
201
+ * @param options - Additional request options
202
+ * @returns Ingest result
203
+ */
204
+ async ingest<T extends Record<string, unknown>>(
205
+ datasourceName: string,
206
+ event: T,
207
+ options: IngestOptions = {}
208
+ ): Promise<IngestResult> {
209
+ return this.ingestBatch(datasourceName, [event], options);
210
+ }
211
+
212
+ /**
213
+ * Ingest multiple events to a datasource
214
+ *
215
+ * @param datasourceName - Name of the datasource
216
+ * @param events - Array of events to ingest
217
+ * @param options - Additional request options
218
+ * @returns Ingest result
219
+ */
220
+ async ingestBatch<T extends Record<string, unknown>>(
221
+ datasourceName: string,
222
+ events: T[],
223
+ options: IngestOptions = {}
224
+ ): Promise<IngestResult> {
225
+ if (events.length === 0) {
226
+ return { successful_rows: 0, quarantined_rows: 0 };
227
+ }
228
+
229
+ const token = await this.getToken();
230
+ const url = new URL("/v0/events", this.config.baseUrl);
231
+ url.searchParams.set("name", datasourceName);
232
+
233
+ if (options.wait !== false) {
234
+ url.searchParams.set("wait", "true");
235
+ }
236
+
237
+ // Convert events to NDJSON format
238
+ const ndjson = events
239
+ .map((event) => JSON.stringify(this.serializeEvent(event)))
240
+ .join("\n");
241
+
242
+ const response = await this.fetch(url.toString(), {
243
+ method: "POST",
244
+ headers: {
245
+ Authorization: `Bearer ${token}`,
246
+ "Content-Type": "application/x-ndjson",
247
+ },
248
+ body: ndjson,
249
+ signal: this.createAbortSignal(options.timeout, options.signal),
250
+ });
251
+
252
+ if (!response.ok) {
253
+ await this.handleErrorResponse(response);
254
+ }
255
+
256
+ const result = (await response.json()) as IngestResult;
257
+ return result;
258
+ }
259
+
260
+ /**
261
+ * Execute a raw SQL query
262
+ *
263
+ * @param sql - SQL query to execute
264
+ * @param options - Additional request options
265
+ * @returns Query result
266
+ */
267
+ async sql<T = unknown>(
268
+ sql: string,
269
+ options: QueryOptions = {}
270
+ ): Promise<QueryResult<T>> {
271
+ const token = await this.getToken();
272
+ const url = new URL("/v0/sql", this.config.baseUrl);
273
+
274
+ const response = await this.fetch(url.toString(), {
275
+ method: "POST",
276
+ headers: {
277
+ Authorization: `Bearer ${token}`,
278
+ "Content-Type": "text/plain",
279
+ },
280
+ body: sql,
281
+ signal: this.createAbortSignal(options.timeout, options.signal),
282
+ });
283
+
284
+ if (!response.ok) {
285
+ await this.handleErrorResponse(response);
286
+ }
287
+
288
+ const result = (await response.json()) as QueryResult<T>;
289
+ return result;
290
+ }
291
+
292
+ /**
293
+ * Serialize an event for ingestion, handling Date objects and other special types
294
+ */
295
+ private serializeEvent<T extends Record<string, unknown>>(
296
+ event: T
297
+ ): Record<string, unknown> {
298
+ const serialized: Record<string, unknown> = {};
299
+
300
+ for (const [key, value] of Object.entries(event)) {
301
+ if (value instanceof Date) {
302
+ // Convert Date to ISO string
303
+ serialized[key] = value.toISOString();
304
+ } else if (value instanceof Map) {
305
+ // Convert Map to object
306
+ serialized[key] = Object.fromEntries(value);
307
+ } else if (typeof value === "bigint") {
308
+ // Convert BigInt to string (ClickHouse will parse it)
309
+ serialized[key] = value.toString();
310
+ } else if (Array.isArray(value)) {
311
+ // Recursively serialize array elements
312
+ serialized[key] = value.map((item) =>
313
+ typeof item === "object" && item !== null
314
+ ? this.serializeEvent(item as Record<string, unknown>)
315
+ : item instanceof Date
316
+ ? item.toISOString()
317
+ : item
318
+ );
319
+ } else if (typeof value === "object" && value !== null) {
320
+ // Recursively serialize nested objects
321
+ serialized[key] = this.serializeEvent(value as Record<string, unknown>);
322
+ } else {
323
+ serialized[key] = value;
324
+ }
325
+ }
326
+
327
+ return serialized;
328
+ }
329
+
330
+ /**
331
+ * Create an AbortSignal with timeout
332
+ */
333
+ private createAbortSignal(
334
+ timeout?: number,
335
+ existingSignal?: AbortSignal
336
+ ): AbortSignal | undefined {
337
+ const timeoutMs = timeout ?? this.config.timeout ?? DEFAULT_TIMEOUT;
338
+
339
+ // If no timeout and no existing signal, return undefined
340
+ if (!timeoutMs && !existingSignal) {
341
+ return undefined;
342
+ }
343
+
344
+ // If only existing signal, return it
345
+ if (!timeoutMs && existingSignal) {
346
+ return existingSignal;
347
+ }
348
+
349
+ // Create timeout signal
350
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
351
+
352
+ // If only timeout, return timeout signal
353
+ if (!existingSignal) {
354
+ return timeoutSignal;
355
+ }
356
+
357
+ // Combine both signals
358
+ return AbortSignal.any([timeoutSignal, existingSignal]);
359
+ }
360
+
361
+ /**
362
+ * Handle error responses from the API
363
+ */
364
+ private async handleErrorResponse(response: Response): Promise<never> {
365
+ let errorResponse: TinybirdErrorResponse | undefined;
366
+ let rawBody: string | undefined;
367
+
368
+ try {
369
+ rawBody = await response.text();
370
+ errorResponse = JSON.parse(rawBody) as TinybirdErrorResponse;
371
+ } catch {
372
+ // Failed to parse error response - include raw body in message
373
+ if (rawBody) {
374
+ throw new TinybirdError(
375
+ `Request failed with status ${response.status}: ${rawBody}`,
376
+ response.status,
377
+ undefined
378
+ );
379
+ }
380
+ }
381
+
382
+ const message =
383
+ errorResponse?.error ?? `Request failed with status ${response.status}`;
384
+
385
+ throw new TinybirdError(message, response.status, errorResponse);
386
+ }
387
+
388
+ /**
389
+ * Internal fetch wrapper
390
+ */
391
+ private fetch(url: string, init?: RequestInit): Promise<Response> {
392
+ return this.fetchFn(url, init);
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Create a Tinybird client
398
+ *
399
+ * @param config - Client configuration
400
+ * @returns Configured Tinybird client
401
+ *
402
+ * @example
403
+ * ```ts
404
+ * import { createClient } from '@tinybirdco/sdk';
405
+ *
406
+ * const client = createClient({
407
+ * baseUrl: process.env.TINYBIRD_URL,
408
+ * token: process.env.TINYBIRD_TOKEN,
409
+ * });
410
+ * ```
411
+ */
412
+ export function createClient(config: ClientConfig): TinybirdClient {
413
+ return new TinybirdClient(config);
414
+ }