@tinybirdco/sdk 0.0.11 → 0.0.13

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 (40) hide show
  1. package/dist/api/branches.d.ts +12 -1
  2. package/dist/api/branches.d.ts.map +1 -1
  3. package/dist/api/branches.js +21 -2
  4. package/dist/api/branches.js.map +1 -1
  5. package/dist/api/branches.test.js +95 -5
  6. package/dist/api/branches.test.js.map +1 -1
  7. package/dist/api/build.d.ts +3 -1
  8. package/dist/api/build.d.ts.map +1 -1
  9. package/dist/api/build.js +2 -0
  10. package/dist/api/build.js.map +1 -1
  11. package/dist/api/local.d.ts +15 -0
  12. package/dist/api/local.d.ts.map +1 -1
  13. package/dist/api/local.js +52 -0
  14. package/dist/api/local.js.map +1 -1
  15. package/dist/api/local.test.js +80 -1
  16. package/dist/api/local.test.js.map +1 -1
  17. package/dist/cli/commands/clear.d.ts +37 -0
  18. package/dist/cli/commands/clear.d.ts.map +1 -0
  19. package/dist/cli/commands/clear.js +141 -0
  20. package/dist/cli/commands/clear.js.map +1 -0
  21. package/dist/cli/index.js +144 -41
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/output.d.ts +88 -0
  24. package/dist/cli/output.d.ts.map +1 -0
  25. package/dist/cli/output.js +150 -0
  26. package/dist/cli/output.js.map +1 -0
  27. package/dist/cli/output.test.d.ts +5 -0
  28. package/dist/cli/output.test.d.ts.map +1 -0
  29. package/dist/cli/output.test.js +119 -0
  30. package/dist/cli/output.test.js.map +1 -0
  31. package/package.json +1 -1
  32. package/src/api/branches.test.ts +116 -4
  33. package/src/api/branches.ts +28 -2
  34. package/src/api/build.ts +5 -1
  35. package/src/api/local.test.ts +106 -0
  36. package/src/api/local.ts +77 -0
  37. package/src/cli/commands/clear.ts +194 -0
  38. package/src/cli/index.ts +159 -58
  39. package/src/cli/output.test.ts +144 -0
  40. package/src/cli/output.ts +173 -0
@@ -6,6 +6,8 @@ import {
6
6
  listLocalWorkspaces,
7
7
  createLocalWorkspace,
8
8
  getOrCreateLocalWorkspace,
9
+ deleteLocalWorkspace,
10
+ clearLocalWorkspace,
9
11
  isLocalRunning,
10
12
  getLocalWorkspaceName,
11
13
  LocalNotRunningError,
@@ -259,4 +261,108 @@ describe("Local API", () => {
259
261
  expect(name1).not.toBe(name2);
260
262
  });
261
263
  });
264
+
265
+ describe("deleteLocalWorkspace", () => {
266
+ it("deletes a workspace successfully", async () => {
267
+ server.use(
268
+ http.delete(`${LOCAL_BASE_URL}/v1/workspaces/ws-123`, () => {
269
+ return new HttpResponse(null, { status: 204 });
270
+ })
271
+ );
272
+
273
+ await deleteLocalWorkspace("user-token", "ws-123");
274
+ // No error means success
275
+ });
276
+
277
+ it("throws LocalApiError on failure", async () => {
278
+ server.use(
279
+ http.delete(`${LOCAL_BASE_URL}/v1/workspaces/ws-123`, () => {
280
+ return new HttpResponse("Not found", { status: 404 });
281
+ })
282
+ );
283
+
284
+ await expect(deleteLocalWorkspace("user-token", "ws-123")).rejects.toThrow(
285
+ LocalApiError
286
+ );
287
+ });
288
+ });
289
+
290
+ describe("clearLocalWorkspace", () => {
291
+ const tokens = {
292
+ user_token: "user-token",
293
+ admin_token: "admin-token",
294
+ workspace_admin_token: "default-token",
295
+ };
296
+
297
+ it("clears a workspace by deleting and recreating it", async () => {
298
+ let deleteCount = 0;
299
+ let createCount = 0;
300
+
301
+ server.use(
302
+ http.get(`${LOCAL_BASE_URL}/v1/user/workspaces`, () => {
303
+ // First call: workspace exists
304
+ // Second call: workspace deleted
305
+ // Third call: workspace recreated
306
+ if (deleteCount === 0) {
307
+ return HttpResponse.json({
308
+ organization_id: "org-123",
309
+ workspaces: [
310
+ { id: "ws-123", name: "MyWorkspace", token: "old-token" },
311
+ ],
312
+ });
313
+ } else if (createCount === 0) {
314
+ return HttpResponse.json({
315
+ organization_id: "org-123",
316
+ workspaces: [],
317
+ });
318
+ } else {
319
+ return HttpResponse.json({
320
+ organization_id: "org-123",
321
+ workspaces: [
322
+ { id: "ws-456", name: "MyWorkspace", token: "new-token" },
323
+ ],
324
+ });
325
+ }
326
+ }),
327
+ http.delete(`${LOCAL_BASE_URL}/v1/workspaces/ws-123`, () => {
328
+ deleteCount++;
329
+ return new HttpResponse(null, { status: 204 });
330
+ }),
331
+ http.post(`${LOCAL_BASE_URL}/v1/workspaces`, () => {
332
+ createCount++;
333
+ return HttpResponse.json({
334
+ id: "ws-456",
335
+ name: "MyWorkspace",
336
+ token: "new-token",
337
+ });
338
+ })
339
+ );
340
+
341
+ const result = await clearLocalWorkspace(tokens, "MyWorkspace");
342
+
343
+ expect(deleteCount).toBe(1);
344
+ expect(createCount).toBe(1);
345
+ expect(result.id).toBe("ws-456");
346
+ expect(result.name).toBe("MyWorkspace");
347
+ expect(result.token).toBe("new-token");
348
+ });
349
+
350
+ it("throws LocalApiError when workspace not found", async () => {
351
+ server.use(
352
+ http.get(`${LOCAL_BASE_URL}/v1/user/workspaces`, () => {
353
+ return HttpResponse.json({
354
+ organization_id: "org-123",
355
+ workspaces: [],
356
+ });
357
+ })
358
+ );
359
+
360
+ await expect(clearLocalWorkspace(tokens, "NonExistent")).rejects.toThrow(
361
+ LocalApiError
362
+ );
363
+ await expect(clearLocalWorkspace(tokens, "NonExistent")).rejects.toThrow(
364
+ "Workspace 'NonExistent' not found"
365
+ );
366
+ });
367
+ });
262
368
  });
package/src/api/local.ts CHANGED
@@ -269,3 +269,80 @@ export function getLocalWorkspaceName(
269
269
  const hash = crypto.createHash("sha256").update(cwd).digest("hex");
270
270
  return `Build_${hash.substring(0, 16)}`;
271
271
  }
272
+
273
+ /**
274
+ * Delete a workspace in local Tinybird
275
+ *
276
+ * @param userToken - User token from getLocalTokens()
277
+ * @param workspaceId - ID of the workspace to delete
278
+ */
279
+ export async function deleteLocalWorkspace(
280
+ userToken: string,
281
+ workspaceId: string
282
+ ): Promise<void> {
283
+ const url = `${LOCAL_BASE_URL}/v1/workspaces/${workspaceId}?hard_delete_confirmation=yes`;
284
+
285
+ const response = await tinybirdFetch(url, {
286
+ method: "DELETE",
287
+ headers: {
288
+ Authorization: `Bearer ${userToken}`,
289
+ },
290
+ });
291
+
292
+ if (!response.ok) {
293
+ const responseBody = await response.text();
294
+ throw new LocalApiError(
295
+ `Failed to delete local workspace: ${response.status} ${response.statusText}`,
296
+ response.status,
297
+ responseBody
298
+ );
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Clear a workspace in local Tinybird by deleting and recreating it
304
+ *
305
+ * @param tokens - Tokens from getLocalTokens()
306
+ * @param workspaceName - Name of the workspace to clear
307
+ * @returns The recreated workspace
308
+ */
309
+ export async function clearLocalWorkspace(
310
+ tokens: LocalTokens,
311
+ workspaceName: string
312
+ ): Promise<LocalWorkspace> {
313
+ // List existing workspaces to find the one to clear
314
+ const { workspaces, organizationId } = await listLocalWorkspaces(tokens.admin_token);
315
+
316
+ // Find the workspace by name
317
+ const workspace = workspaces.find((ws) => ws.name === workspaceName);
318
+ if (!workspace) {
319
+ throw new LocalApiError(`Workspace '${workspaceName}' not found`);
320
+ }
321
+
322
+ // Delete the workspace
323
+ await deleteLocalWorkspace(tokens.user_token, workspace.id);
324
+
325
+ // Verify it was deleted
326
+ const { workspaces: afterDelete } = await listLocalWorkspaces(tokens.admin_token);
327
+ const stillExists = afterDelete.find((ws) => ws.name === workspaceName);
328
+ if (stillExists) {
329
+ throw new LocalApiError(
330
+ `Workspace '${workspaceName}' was not deleted properly. Please try again.`
331
+ );
332
+ }
333
+
334
+ // Recreate the workspace
335
+ await createLocalWorkspace(tokens.user_token, workspaceName, organizationId);
336
+
337
+ // Fetch the workspace again to get the token
338
+ const { workspaces: afterCreate } = await listLocalWorkspaces(tokens.admin_token);
339
+ const newWorkspace = afterCreate.find((ws) => ws.name === workspaceName);
340
+
341
+ if (!newWorkspace) {
342
+ throw new LocalApiError(
343
+ `Workspace '${workspaceName}' was not recreated properly. Please try again.`
344
+ );
345
+ }
346
+
347
+ return newWorkspace;
348
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Clear command - clears a local workspace or branch by deleting and recreating it
3
+ */
4
+
5
+ import { loadConfig, type ResolvedConfig, type DevMode } from "../config.js";
6
+ import {
7
+ getLocalTokens,
8
+ clearLocalWorkspace,
9
+ getLocalWorkspaceName,
10
+ LocalNotRunningError,
11
+ LocalApiError,
12
+ } from "../../api/local.js";
13
+ import {
14
+ clearBranch,
15
+ BranchApiError,
16
+ } from "../../api/branches.js";
17
+ import {
18
+ setBranchToken,
19
+ removeBranch as removeCachedBranch,
20
+ } from "../branch-store.js";
21
+ import { getWorkspace } from "../../api/workspaces.js";
22
+
23
+ /**
24
+ * Clear command options
25
+ */
26
+ export interface ClearCommandOptions {
27
+ /** Working directory (defaults to cwd) */
28
+ cwd?: string;
29
+ /** Override the dev mode from config */
30
+ devModeOverride?: DevMode;
31
+ }
32
+
33
+ /**
34
+ * Result of clearing a workspace or branch
35
+ */
36
+ export interface ClearResult {
37
+ /** Whether the operation was successful */
38
+ success: boolean;
39
+ /** Name of the cleared workspace or branch */
40
+ name?: string;
41
+ /** Whether local mode was used */
42
+ isLocal?: boolean;
43
+ /** Error message if failed */
44
+ error?: string;
45
+ }
46
+
47
+ /**
48
+ * Clear a local workspace or branch by deleting and recreating it
49
+ *
50
+ * In local mode: deletes and recreates the local workspace
51
+ * In branch mode: deletes and recreates the Tinybird branch
52
+ *
53
+ * @param options - Command options
54
+ * @returns Clear result
55
+ */
56
+ export async function runClear(
57
+ options: ClearCommandOptions = {}
58
+ ): Promise<ClearResult> {
59
+ const cwd = options.cwd ?? process.cwd();
60
+
61
+ let config: ResolvedConfig;
62
+ try {
63
+ config = loadConfig(cwd);
64
+ } catch (error) {
65
+ return {
66
+ success: false,
67
+ error: (error as Error).message,
68
+ };
69
+ }
70
+
71
+ // Determine dev mode
72
+ const devMode = options.devModeOverride ?? config.devMode;
73
+
74
+ if (devMode === "local") {
75
+ return clearLocal(config);
76
+ } else {
77
+ return clearCloudBranch(config);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Clear a local workspace
83
+ */
84
+ async function clearLocal(config: ResolvedConfig): Promise<ClearResult> {
85
+ // Get workspace name from git branch or path hash
86
+ const workspaceName = getLocalWorkspaceName(config.tinybirdBranch, config.cwd);
87
+
88
+ try {
89
+ // Get local tokens
90
+ const tokens = await getLocalTokens();
91
+
92
+ // Clear the workspace
93
+ await clearLocalWorkspace(tokens, workspaceName);
94
+
95
+ return {
96
+ success: true,
97
+ name: workspaceName,
98
+ isLocal: true,
99
+ };
100
+ } catch (error) {
101
+ if (error instanceof LocalNotRunningError) {
102
+ return {
103
+ success: false,
104
+ error: error.message,
105
+ };
106
+ }
107
+
108
+ if (error instanceof LocalApiError) {
109
+ return {
110
+ success: false,
111
+ error: error.message,
112
+ };
113
+ }
114
+
115
+ return {
116
+ success: false,
117
+ error: (error as Error).message,
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Clear a cloud branch
124
+ */
125
+ async function clearCloudBranch(config: ResolvedConfig): Promise<ClearResult> {
126
+ // Must be on a non-main branch to clear
127
+ if (config.isMainBranch) {
128
+ return {
129
+ success: false,
130
+ error: "Cannot clear the main branch. Use 'tinybird deploy' to manage the main workspace.",
131
+ };
132
+ }
133
+
134
+ const branchName = config.tinybirdBranch;
135
+ if (!branchName) {
136
+ return {
137
+ success: false,
138
+ error: "Could not detect git branch. Make sure you are in a git repository.",
139
+ };
140
+ }
141
+
142
+ try {
143
+ // Get workspace ID for cache management
144
+ const workspace = await getWorkspace({
145
+ baseUrl: config.baseUrl,
146
+ token: config.token,
147
+ });
148
+
149
+ // Clear the branch (delete and recreate)
150
+ const newBranch = await clearBranch(
151
+ {
152
+ baseUrl: config.baseUrl,
153
+ token: config.token,
154
+ },
155
+ branchName
156
+ );
157
+
158
+ // Update the cached token with the new branch token
159
+ if (newBranch.token) {
160
+ setBranchToken(workspace.id, branchName, {
161
+ token: newBranch.token,
162
+ id: newBranch.id,
163
+ createdAt: newBranch.created_at,
164
+ });
165
+ } else {
166
+ // If no token in response, remove cached token
167
+ removeCachedBranch(workspace.id, branchName);
168
+ }
169
+
170
+ return {
171
+ success: true,
172
+ name: branchName,
173
+ isLocal: false,
174
+ };
175
+ } catch (error) {
176
+ if (error instanceof BranchApiError) {
177
+ if (error.status === 404) {
178
+ return {
179
+ success: false,
180
+ error: `Branch '${branchName}' does not exist. Run 'npx tinybird dev' to create it first.`,
181
+ };
182
+ }
183
+ return {
184
+ success: false,
185
+ error: error.message,
186
+ };
187
+ }
188
+
189
+ return {
190
+ success: false,
191
+ error: (error as Error).message,
192
+ };
193
+ }
194
+ }