@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.
- package/dist/api/branches.d.ts +12 -1
- package/dist/api/branches.d.ts.map +1 -1
- package/dist/api/branches.js +21 -2
- package/dist/api/branches.js.map +1 -1
- package/dist/api/branches.test.js +95 -5
- package/dist/api/branches.test.js.map +1 -1
- package/dist/api/build.d.ts +3 -1
- package/dist/api/build.d.ts.map +1 -1
- package/dist/api/build.js +2 -0
- package/dist/api/build.js.map +1 -1
- package/dist/api/local.d.ts +15 -0
- package/dist/api/local.d.ts.map +1 -1
- package/dist/api/local.js +52 -0
- package/dist/api/local.js.map +1 -1
- package/dist/api/local.test.js +80 -1
- package/dist/api/local.test.js.map +1 -1
- package/dist/cli/commands/clear.d.ts +37 -0
- package/dist/cli/commands/clear.d.ts.map +1 -0
- package/dist/cli/commands/clear.js +141 -0
- package/dist/cli/commands/clear.js.map +1 -0
- package/dist/cli/index.js +144 -41
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/output.d.ts +88 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +150 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/output.test.d.ts +5 -0
- package/dist/cli/output.test.d.ts.map +1 -0
- package/dist/cli/output.test.js +119 -0
- package/dist/cli/output.test.js.map +1 -0
- package/package.json +1 -1
- package/src/api/branches.test.ts +116 -4
- package/src/api/branches.ts +28 -2
- package/src/api/build.ts +5 -1
- package/src/api/local.test.ts +106 -0
- package/src/api/local.ts +77 -0
- package/src/cli/commands/clear.ts +194 -0
- package/src/cli/index.ts +159 -58
- package/src/cli/output.test.ts +144 -0
- package/src/cli/output.ts +173 -0
package/src/api/local.test.ts
CHANGED
|
@@ -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
|
+
}
|