@tinybirdco/sdk 0.0.10 → 0.0.12
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 +35 -0
- 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/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 +45 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/generator/datasource.d.ts.map +1 -1
- package/dist/generator/datasource.js +28 -0
- package/dist/generator/datasource.js.map +1 -1
- package/dist/generator/datasource.test.js +65 -0
- package/dist/generator/datasource.test.js.map +1 -1
- package/dist/generator/pipe.d.ts.map +1 -1
- package/dist/generator/pipe.js +22 -0
- package/dist/generator/pipe.js.map +1 -1
- package/dist/generator/pipe.test.js +55 -0
- package/dist/generator/pipe.test.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/schema/datasource.d.ts +18 -3
- package/dist/schema/datasource.d.ts.map +1 -1
- package/dist/schema/datasource.js.map +1 -1
- package/dist/schema/pipe.d.ts +17 -2
- package/dist/schema/pipe.d.ts.map +1 -1
- package/dist/schema/pipe.js.map +1 -1
- package/dist/schema/token.d.ts +49 -0
- package/dist/schema/token.d.ts.map +1 -0
- package/dist/schema/token.js +47 -0
- package/dist/schema/token.js.map +1 -0
- package/dist/schema/token.test.d.ts +2 -0
- package/dist/schema/token.test.d.ts.map +1 -0
- package/dist/schema/token.test.js +50 -0
- package/dist/schema/token.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/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 +56 -0
- package/src/generator/datasource.test.ts +77 -0
- package/src/generator/datasource.ts +33 -1
- package/src/generator/pipe.test.ts +63 -0
- package/src/generator/pipe.ts +26 -0
- package/src/index.ts +12 -0
- package/src/schema/datasource.ts +20 -3
- package/src/schema/pipe.ts +19 -2
- package/src/schema/token.test.ts +60 -0
- package/src/schema/token.ts +75 -0
package/src/api/branches.test.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
deleteBranch,
|
|
8
8
|
branchExists,
|
|
9
9
|
getOrCreateBranch,
|
|
10
|
+
clearBranch,
|
|
10
11
|
type BranchApiConfig,
|
|
11
12
|
} from "./branches.js";
|
|
12
13
|
|
|
@@ -271,16 +272,39 @@ describe("Branch API client", () => {
|
|
|
271
272
|
|
|
272
273
|
describe("deleteBranch", () => {
|
|
273
274
|
it("deletes a branch successfully", async () => {
|
|
275
|
+
const mockBranch = {
|
|
276
|
+
id: "branch-123",
|
|
277
|
+
name: "my-feature",
|
|
278
|
+
token: "p.branch-token",
|
|
279
|
+
created_at: "2024-01-01T00:00:00Z",
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// 1. getBranch to get the ID
|
|
283
|
+
mockFetch.mockResolvedValueOnce({
|
|
284
|
+
ok: true,
|
|
285
|
+
json: () => Promise.resolve(mockBranch),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// 2. DELETE the branch by ID
|
|
274
289
|
mockFetch.mockResolvedValueOnce({
|
|
275
290
|
ok: true,
|
|
276
291
|
});
|
|
277
292
|
|
|
278
293
|
await deleteBranch(config, "my-feature");
|
|
279
294
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
295
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
296
|
+
|
|
297
|
+
// First call: get branch
|
|
298
|
+
const [getUrl, getInit] = mockFetch.mock.calls[0];
|
|
299
|
+
const getParsed = expectFromParam(getUrl);
|
|
300
|
+
expect(getParsed.pathname).toBe("/v0/environments/my-feature");
|
|
301
|
+
expect(getInit.method).toBe("GET");
|
|
302
|
+
|
|
303
|
+
// Second call: delete branch by ID
|
|
304
|
+
const [deleteUrl, deleteInit] = mockFetch.mock.calls[1];
|
|
305
|
+
const deleteParsed = expectFromParam(deleteUrl);
|
|
306
|
+
expect(deleteParsed.pathname).toBe("/v0/environments/branch-123");
|
|
307
|
+
expect(deleteInit).toEqual({
|
|
284
308
|
method: "DELETE",
|
|
285
309
|
headers: {
|
|
286
310
|
Authorization: "Bearer p.test-token",
|
|
@@ -382,4 +406,92 @@ describe("Branch API client", () => {
|
|
|
382
406
|
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
383
407
|
});
|
|
384
408
|
});
|
|
409
|
+
|
|
410
|
+
describe("clearBranch", () => {
|
|
411
|
+
it("clears a branch by deleting and recreating it", async () => {
|
|
412
|
+
const existingBranch = {
|
|
413
|
+
id: "branch-old",
|
|
414
|
+
name: "my-feature",
|
|
415
|
+
token: "p.old-token",
|
|
416
|
+
created_at: "2024-01-01T00:00:00Z",
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const newBranch = {
|
|
420
|
+
id: "branch-new",
|
|
421
|
+
name: "my-feature",
|
|
422
|
+
token: "p.new-token",
|
|
423
|
+
created_at: "2024-01-02T00:00:00Z",
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// 1. GET branch to get ID (for delete)
|
|
427
|
+
mockFetch.mockResolvedValueOnce({
|
|
428
|
+
ok: true,
|
|
429
|
+
json: () => Promise.resolve(existingBranch),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// 2. DELETE branch by ID
|
|
433
|
+
mockFetch.mockResolvedValueOnce({
|
|
434
|
+
ok: true,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// 3. POST to /v1/environments returns a job
|
|
438
|
+
mockFetch.mockResolvedValueOnce({
|
|
439
|
+
ok: true,
|
|
440
|
+
json: () => Promise.resolve({
|
|
441
|
+
job: { id: "job-789", status: "waiting" },
|
|
442
|
+
workspace: { id: "ws-789" },
|
|
443
|
+
}),
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// 4. Poll job - done
|
|
447
|
+
mockFetch.mockResolvedValueOnce({
|
|
448
|
+
ok: true,
|
|
449
|
+
json: () => Promise.resolve({ id: "job-789", status: "done" }),
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// 5. Get branch with token (after create)
|
|
453
|
+
mockFetch.mockResolvedValueOnce({
|
|
454
|
+
ok: true,
|
|
455
|
+
json: () => Promise.resolve(newBranch),
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const result = await clearBranch(config, "my-feature");
|
|
459
|
+
|
|
460
|
+
expect(mockFetch).toHaveBeenCalledTimes(5);
|
|
461
|
+
|
|
462
|
+
// Verify get was called first
|
|
463
|
+
const [getUrl, getInit] = mockFetch.mock.calls[0];
|
|
464
|
+
const getParsed = expectFromParam(getUrl);
|
|
465
|
+
expect(getParsed.pathname).toBe("/v0/environments/my-feature");
|
|
466
|
+
expect(getInit.method).toBe("GET");
|
|
467
|
+
|
|
468
|
+
// Verify delete was called with ID
|
|
469
|
+
const [deleteUrl, deleteInit] = mockFetch.mock.calls[1];
|
|
470
|
+
const deleteParsed = expectFromParam(deleteUrl);
|
|
471
|
+
expect(deleteParsed.pathname).toBe("/v0/environments/branch-old");
|
|
472
|
+
expect(deleteInit.method).toBe("DELETE");
|
|
473
|
+
|
|
474
|
+
// Verify create was called
|
|
475
|
+
const [createUrl, createInit] = mockFetch.mock.calls[2];
|
|
476
|
+
const createParsed = expectFromParam(createUrl);
|
|
477
|
+
expect(createParsed.pathname).toBe("/v1/environments");
|
|
478
|
+
expect(createParsed.searchParams.get("name")).toBe("my-feature");
|
|
479
|
+
expect(createInit.method).toBe("POST");
|
|
480
|
+
|
|
481
|
+
expect(result).toEqual(newBranch);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("throws BranchApiError when branch does not exist", async () => {
|
|
485
|
+
mockFetch.mockResolvedValueOnce({
|
|
486
|
+
ok: false,
|
|
487
|
+
status: 404,
|
|
488
|
+
statusText: "Not Found",
|
|
489
|
+
text: () => Promise.resolve("Branch not found"),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
await expect(clearBranch(config, "nonexistent")).rejects.toThrow(
|
|
493
|
+
BranchApiError
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
385
497
|
});
|
package/src/api/branches.ts
CHANGED
|
@@ -264,7 +264,10 @@ export async function getBranch(
|
|
|
264
264
|
|
|
265
265
|
/**
|
|
266
266
|
* Delete a branch
|
|
267
|
-
* DELETE /
|
|
267
|
+
* DELETE /v0/environments/{id}
|
|
268
|
+
*
|
|
269
|
+
* Note: The API requires the branch ID, not name. This function first
|
|
270
|
+
* fetches the branch to get its ID, then deletes it.
|
|
268
271
|
*
|
|
269
272
|
* @param config - API configuration
|
|
270
273
|
* @param name - Branch name to delete
|
|
@@ -273,7 +276,10 @@ export async function deleteBranch(
|
|
|
273
276
|
config: BranchApiConfig,
|
|
274
277
|
name: string
|
|
275
278
|
): Promise<void> {
|
|
276
|
-
|
|
279
|
+
// First get the branch to find its ID
|
|
280
|
+
const branch = await getBranch(config, name);
|
|
281
|
+
|
|
282
|
+
const url = new URL(`/v0/environments/${branch.id}`, config.baseUrl);
|
|
277
283
|
|
|
278
284
|
const response = await tinybirdFetch(url.toString(), {
|
|
279
285
|
method: "DELETE",
|
|
@@ -334,3 +340,23 @@ export async function getOrCreateBranch(
|
|
|
334
340
|
throw error;
|
|
335
341
|
}
|
|
336
342
|
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Clear a branch by deleting and recreating it
|
|
346
|
+
*
|
|
347
|
+
* @param config - API configuration
|
|
348
|
+
* @param name - Branch name to clear
|
|
349
|
+
* @returns The recreated branch with token
|
|
350
|
+
*/
|
|
351
|
+
export async function clearBranch(
|
|
352
|
+
config: BranchApiConfig,
|
|
353
|
+
name: string
|
|
354
|
+
): Promise<TinybirdBranch> {
|
|
355
|
+
// Delete the branch
|
|
356
|
+
await deleteBranch(config, name);
|
|
357
|
+
|
|
358
|
+
// Recreate the branch
|
|
359
|
+
const branch = await createBranch(config, name);
|
|
360
|
+
|
|
361
|
+
return branch;
|
|
362
|
+
}
|
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
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
runBranchStatus,
|
|
25
25
|
runBranchDelete,
|
|
26
26
|
} from "./commands/branch.js";
|
|
27
|
+
import { runClear } from "./commands/clear.js";
|
|
27
28
|
import {
|
|
28
29
|
detectPackageManagerInstallCmd,
|
|
29
30
|
detectPackageManagerRunCmd,
|
|
@@ -532,6 +533,61 @@ function createCli(): Command {
|
|
|
532
533
|
|
|
533
534
|
program.addCommand(branchCommand);
|
|
534
535
|
|
|
536
|
+
// Clear command
|
|
537
|
+
program
|
|
538
|
+
.command("clear")
|
|
539
|
+
.description("Clear the workspace or branch by deleting and recreating it")
|
|
540
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
541
|
+
.option("--local", "Use local Tinybird container")
|
|
542
|
+
.option("--branch", "Use Tinybird cloud with branches")
|
|
543
|
+
.action(async (options) => {
|
|
544
|
+
// Determine devMode override
|
|
545
|
+
let devModeOverride: DevMode | undefined;
|
|
546
|
+
if (options.local) {
|
|
547
|
+
devModeOverride = "local";
|
|
548
|
+
} else if (options.branch) {
|
|
549
|
+
devModeOverride = "branch";
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const modeLabel = devModeOverride === "local" ? "local workspace" : "branch";
|
|
553
|
+
|
|
554
|
+
// Confirmation prompt unless --yes is passed
|
|
555
|
+
if (!options.yes) {
|
|
556
|
+
const readline = await import("readline");
|
|
557
|
+
const rl = readline.createInterface({
|
|
558
|
+
input: process.stdin,
|
|
559
|
+
output: process.stdout,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const answer = await new Promise<string>((resolve) => {
|
|
563
|
+
rl.question(
|
|
564
|
+
`Are you sure you want to clear the ${modeLabel}? This will delete all resources. [y/N]: `,
|
|
565
|
+
(ans) => {
|
|
566
|
+
rl.close();
|
|
567
|
+
resolve(ans);
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
573
|
+
console.log("Aborted.");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
console.log(`Clearing ${modeLabel}...`);
|
|
579
|
+
|
|
580
|
+
const result = await runClear({ devModeOverride });
|
|
581
|
+
|
|
582
|
+
if (!result.success) {
|
|
583
|
+
console.error(`Error: ${result.error}`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const typeLabel = result.isLocal ? "Workspace" : "Branch";
|
|
588
|
+
console.log(`${typeLabel} '${result.name}' cleared successfully.`);
|
|
589
|
+
});
|
|
590
|
+
|
|
535
591
|
return program;
|
|
536
592
|
}
|
|
537
593
|
|