@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.
- package/README.md +518 -0
- package/bin/tinybird.js +7 -0
- package/dist/api/branches.d.ts +98 -0
- package/dist/api/branches.d.ts.map +1 -0
- package/dist/api/branches.js +203 -0
- package/dist/api/branches.js.map +1 -0
- package/dist/api/branches.test.d.ts +2 -0
- package/dist/api/branches.test.d.ts.map +1 -0
- package/dist/api/branches.test.js +286 -0
- package/dist/api/branches.test.js.map +1 -0
- package/dist/api/build.d.ts +130 -0
- package/dist/api/build.d.ts.map +1 -0
- package/dist/api/build.js +143 -0
- package/dist/api/build.js.map +1 -0
- package/dist/api/build.test.d.ts +2 -0
- package/dist/api/build.test.d.ts.map +1 -0
- package/dist/api/build.test.js +138 -0
- package/dist/api/build.test.js.map +1 -0
- package/dist/api/deploy.d.ts +39 -0
- package/dist/api/deploy.d.ts.map +1 -0
- package/dist/api/deploy.js +135 -0
- package/dist/api/deploy.js.map +1 -0
- package/dist/api/deploy.test.d.ts +2 -0
- package/dist/api/deploy.test.d.ts.map +1 -0
- package/dist/api/deploy.test.js +118 -0
- package/dist/api/deploy.test.js.map +1 -0
- package/dist/api/workspaces.d.ts +46 -0
- package/dist/api/workspaces.d.ts.map +1 -0
- package/dist/api/workspaces.js +39 -0
- package/dist/api/workspaces.js.map +1 -0
- package/dist/api/workspaces.test.d.ts +2 -0
- package/dist/api/workspaces.test.d.ts.map +1 -0
- package/dist/api/workspaces.test.js +65 -0
- package/dist/api/workspaces.test.js.map +1 -0
- package/dist/cli/auth.d.ts +86 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +284 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/branch-store.d.ts +53 -0
- package/dist/cli/branch-store.d.ts.map +1 -0
- package/dist/cli/branch-store.js +91 -0
- package/dist/cli/branch-store.js.map +1 -0
- package/dist/cli/branch-store.test.d.ts +2 -0
- package/dist/cli/branch-store.test.d.ts.map +1 -0
- package/dist/cli/branch-store.test.js +115 -0
- package/dist/cli/branch-store.test.js.map +1 -0
- package/dist/cli/commands/branch.d.ts +82 -0
- package/dist/cli/commands/branch.d.ts.map +1 -0
- package/dist/cli/commands/branch.js +215 -0
- package/dist/cli/commands/branch.js.map +1 -0
- package/dist/cli/commands/build.d.ts +43 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/build.js +138 -0
- package/dist/cli/commands/build.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +78 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +226 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/init.d.ts +45 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +277 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/init.test.d.ts +2 -0
- package/dist/cli/commands/init.test.d.ts.map +1 -0
- package/dist/cli/commands/init.test.js +158 -0
- package/dist/cli/commands/init.test.js.map +1 -0
- package/dist/cli/commands/login.d.ts +37 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +64 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/config.d.ts +114 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +258 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/config.test.d.ts +2 -0
- package/dist/cli/config.test.d.ts.map +1 -0
- package/dist/cli/config.test.js +243 -0
- package/dist/cli/config.test.js.map +1 -0
- package/dist/cli/env.d.ts +29 -0
- package/dist/cli/env.d.ts.map +1 -0
- package/dist/cli/env.js +66 -0
- package/dist/cli/env.js.map +1 -0
- package/dist/cli/git.d.ts +29 -0
- package/dist/cli/git.d.ts.map +1 -0
- package/dist/cli/git.js +114 -0
- package/dist/cli/git.js.map +1 -0
- package/dist/cli/git.test.d.ts +2 -0
- package/dist/cli/git.test.d.ts.map +1 -0
- package/dist/cli/git.test.js +125 -0
- package/dist/cli/git.test.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +337 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/schema-validation.d.ts +95 -0
- package/dist/cli/utils/schema-validation.d.ts.map +1 -0
- package/dist/cli/utils/schema-validation.js +175 -0
- package/dist/cli/utils/schema-validation.js.map +1 -0
- package/dist/cli/utils/schema-validation.test.d.ts +5 -0
- package/dist/cli/utils/schema-validation.test.d.ts.map +1 -0
- package/dist/cli/utils/schema-validation.test.js +173 -0
- package/dist/cli/utils/schema-validation.test.js.map +1 -0
- package/dist/client/base.d.ts +116 -0
- package/dist/client/base.d.ts.map +1 -0
- package/dist/client/base.js +328 -0
- package/dist/client/base.js.map +1 -0
- package/dist/client/types.d.ts +137 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +43 -0
- package/dist/client/types.js.map +1 -0
- package/dist/generator/client.d.ts +44 -0
- package/dist/generator/client.d.ts.map +1 -0
- package/dist/generator/client.js +144 -0
- package/dist/generator/client.js.map +1 -0
- package/dist/generator/datasource.d.ts +57 -0
- package/dist/generator/datasource.d.ts.map +1 -0
- package/dist/generator/datasource.js +169 -0
- package/dist/generator/datasource.js.map +1 -0
- package/dist/generator/datasource.test.d.ts +2 -0
- package/dist/generator/datasource.test.d.ts.map +1 -0
- package/dist/generator/datasource.test.js +254 -0
- package/dist/generator/datasource.test.js.map +1 -0
- package/dist/generator/index.d.ts +131 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +121 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/index.test.d.ts +2 -0
- package/dist/generator/index.test.d.ts.map +1 -0
- package/dist/generator/index.test.js +175 -0
- package/dist/generator/index.test.js.map +1 -0
- package/dist/generator/loader.d.ts +156 -0
- package/dist/generator/loader.d.ts.map +1 -0
- package/dist/generator/loader.js +295 -0
- package/dist/generator/loader.js.map +1 -0
- package/dist/generator/pipe.d.ts +72 -0
- package/dist/generator/pipe.d.ts.map +1 -0
- package/dist/generator/pipe.js +174 -0
- package/dist/generator/pipe.js.map +1 -0
- package/dist/generator/pipe.test.d.ts +2 -0
- package/dist/generator/pipe.test.d.ts.map +1 -0
- package/dist/generator/pipe.test.js +393 -0
- package/dist/generator/pipe.test.js.map +1 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/infer/index.d.ts +202 -0
- package/dist/infer/index.d.ts.map +1 -0
- package/dist/infer/index.js +5 -0
- package/dist/infer/index.js.map +1 -0
- package/dist/schema/datasource.d.ts +135 -0
- package/dist/schema/datasource.d.ts.map +1 -0
- package/dist/schema/datasource.js +105 -0
- package/dist/schema/datasource.js.map +1 -0
- package/dist/schema/datasource.test.d.ts +2 -0
- package/dist/schema/datasource.test.d.ts.map +1 -0
- package/dist/schema/datasource.test.js +142 -0
- package/dist/schema/datasource.test.js.map +1 -0
- package/dist/schema/engines.d.ts +157 -0
- package/dist/schema/engines.d.ts.map +1 -0
- package/dist/schema/engines.js +155 -0
- package/dist/schema/engines.js.map +1 -0
- package/dist/schema/engines.test.d.ts +2 -0
- package/dist/schema/engines.test.d.ts.map +1 -0
- package/dist/schema/engines.test.js +221 -0
- package/dist/schema/engines.test.js.map +1 -0
- package/dist/schema/params.d.ts +106 -0
- package/dist/schema/params.d.ts.map +1 -0
- package/dist/schema/params.js +138 -0
- package/dist/schema/params.js.map +1 -0
- package/dist/schema/params.test.d.ts +2 -0
- package/dist/schema/params.test.d.ts.map +1 -0
- package/dist/schema/params.test.js +175 -0
- package/dist/schema/params.test.js.map +1 -0
- package/dist/schema/pipe.d.ts +436 -0
- package/dist/schema/pipe.d.ts.map +1 -0
- package/dist/schema/pipe.js +484 -0
- package/dist/schema/pipe.js.map +1 -0
- package/dist/schema/pipe.test.d.ts +2 -0
- package/dist/schema/pipe.test.d.ts.map +1 -0
- package/dist/schema/pipe.test.js +488 -0
- package/dist/schema/pipe.test.js.map +1 -0
- package/dist/schema/project.d.ts +202 -0
- package/dist/schema/project.d.ts.map +1 -0
- package/dist/schema/project.js +188 -0
- package/dist/schema/project.js.map +1 -0
- package/dist/schema/project.test.d.ts +2 -0
- package/dist/schema/project.test.d.ts.map +1 -0
- package/dist/schema/project.test.js +180 -0
- package/dist/schema/project.test.js.map +1 -0
- package/dist/schema/types.d.ts +140 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +174 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/schema/types.test.d.ts +2 -0
- package/dist/schema/types.test.d.ts.map +1 -0
- package/dist/schema/types.test.js +176 -0
- package/dist/schema/types.test.js.map +1 -0
- package/dist/test/handlers.d.ts +58 -0
- package/dist/test/handlers.d.ts.map +1 -0
- package/dist/test/handlers.js +62 -0
- package/dist/test/handlers.js.map +1 -0
- package/dist/test/setup.d.ts +5 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +11 -0
- package/dist/test/setup.js.map +1 -0
- package/package.json +57 -0
- package/src/api/branches.test.ts +377 -0
- package/src/api/branches.ts +334 -0
- package/src/api/build.test.ts +216 -0
- package/src/api/build.ts +266 -0
- package/src/api/deploy.test.ts +193 -0
- package/src/api/deploy.ts +163 -0
- package/src/api/workspaces.test.ts +81 -0
- package/src/api/workspaces.ts +77 -0
- package/src/cli/auth.ts +358 -0
- package/src/cli/branch-store.test.ts +139 -0
- package/src/cli/branch-store.ts +137 -0
- package/src/cli/commands/branch.ts +306 -0
- package/src/cli/commands/build.ts +183 -0
- package/src/cli/commands/dev.ts +334 -0
- package/src/cli/commands/init.test.ts +249 -0
- package/src/cli/commands/init.ts +323 -0
- package/src/cli/commands/login.ts +98 -0
- package/src/cli/config.test.ts +359 -0
- package/src/cli/config.ts +335 -0
- package/src/cli/env.ts +86 -0
- package/src/cli/git.test.ts +147 -0
- package/src/cli/git.ts +125 -0
- package/src/cli/index.ts +382 -0
- package/src/cli/utils/schema-validation.test.ts +222 -0
- package/src/cli/utils/schema-validation.ts +272 -0
- package/src/client/base.ts +414 -0
- package/src/client/types.ts +165 -0
- package/src/generator/client.ts +194 -0
- package/src/generator/datasource.test.ts +297 -0
- package/src/generator/datasource.ts +217 -0
- package/src/generator/index.test.ts +209 -0
- package/src/generator/index.ts +203 -0
- package/src/generator/loader.ts +406 -0
- package/src/generator/pipe.test.ts +441 -0
- package/src/generator/pipe.ts +220 -0
- package/src/index.ts +191 -0
- package/src/infer/index.ts +247 -0
- package/src/schema/datasource.test.ts +187 -0
- package/src/schema/datasource.ts +195 -0
- package/src/schema/engines.test.ts +247 -0
- package/src/schema/engines.ts +271 -0
- package/src/schema/params.test.ts +208 -0
- package/src/schema/params.ts +249 -0
- package/src/schema/pipe.test.ts +588 -0
- package/src/schema/pipe.ts +832 -0
- package/src/schema/project.test.ts +236 -0
- package/src/schema/project.ts +394 -0
- package/src/schema/types.test.ts +212 -0
- package/src/schema/types.ts +366 -0
- package/src/test/handlers.ts +79 -0
- package/src/test/setup.ts +13 -0
package/src/api/build.ts
ADDED
|
@@ -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
|
+
});
|