@tinybirdco/sdk 0.0.35 → 0.0.37
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/api.d.ts +17 -1
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +91 -0
- package/dist/api/api.js.map +1 -1
- package/dist/api/api.test.js +160 -0
- package/dist/api/api.test.js.map +1 -1
- package/dist/api/regions.d.ts +33 -0
- package/dist/api/regions.d.ts.map +1 -0
- package/dist/api/regions.js +52 -0
- package/dist/api/regions.js.map +1 -0
- package/dist/api/regions.test.d.ts +2 -0
- package/dist/api/regions.test.d.ts.map +1 -0
- package/dist/api/regions.test.js +69 -0
- package/dist/api/regions.test.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +25 -11
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +7 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/login.d.ts.map +1 -1
- package/dist/cli/commands/login.js +13 -3
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/login.test.js +12 -1
- package/dist/cli/commands/login.test.js.map +1 -1
- package/dist/cli/region-selector.d.ts +39 -0
- package/dist/cli/region-selector.d.ts.map +1 -0
- package/dist/cli/region-selector.js +118 -0
- package/dist/cli/region-selector.js.map +1 -0
- package/dist/cli/region-selector.test.d.ts +2 -0
- package/dist/cli/region-selector.test.d.ts.map +1 -0
- package/dist/cli/region-selector.test.js +176 -0
- package/dist/cli/region-selector.test.js.map +1 -0
- package/dist/client/base.d.ts +26 -1
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +39 -0
- package/dist/client/base.js.map +1 -1
- package/dist/client/base.test.js +25 -0
- package/dist/client/base.test.js.map +1 -1
- package/dist/client/types.d.ts +49 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/schema/project.d.ts +22 -3
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/schema/project.js +12 -0
- package/dist/schema/project.js.map +1 -1
- package/dist/schema/project.test.js +52 -0
- package/dist/schema/project.test.js.map +1 -1
- package/package.json +1 -1
- package/src/api/api.test.ts +222 -0
- package/src/api/api.ts +117 -0
- package/src/api/regions.test.ts +89 -0
- package/src/api/regions.ts +75 -0
- package/src/cli/commands/init.test.ts +8 -0
- package/src/cli/commands/init.ts +30 -11
- package/src/cli/commands/login.test.ts +13 -1
- package/src/cli/commands/login.ts +14 -3
- package/src/cli/region-selector.test.ts +227 -0
- package/src/cli/region-selector.ts +151 -0
- package/src/client/base.test.ts +32 -0
- package/src/client/base.ts +48 -0
- package/src/client/types.ts +54 -0
- package/src/index.ts +5 -0
- package/src/schema/project.test.ts +64 -0
- package/src/schema/project.ts +42 -3
package/src/cli/commands/init.ts
CHANGED
|
@@ -17,11 +17,11 @@ import {
|
|
|
17
17
|
} from "../config.js";
|
|
18
18
|
import { browserLogin } from "../auth.js";
|
|
19
19
|
import { saveTinybirdToken } from "../env.js";
|
|
20
|
+
import { selectRegion } from "../region-selector.js";
|
|
20
21
|
import { getGitRoot } from "../git.js";
|
|
21
22
|
import { fetchAllResources } from "../../api/resources.js";
|
|
22
23
|
import { generateCombinedFile } from "../../codegen/index.js";
|
|
23
24
|
import { execSync } from "child_process";
|
|
24
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
25
25
|
import {
|
|
26
26
|
detectPackageManager,
|
|
27
27
|
getPackageManagerAddCmd,
|
|
@@ -312,6 +312,8 @@ export interface InitOptions {
|
|
|
312
312
|
includeCdWorkflow?: boolean;
|
|
313
313
|
/** Git provider for workflow templates */
|
|
314
314
|
workflowProvider?: "github" | "gitlab";
|
|
315
|
+
/** Skip auto-installing @tinybirdco/sdk dependency */
|
|
316
|
+
skipDependencyInstall?: boolean;
|
|
315
317
|
}
|
|
316
318
|
|
|
317
319
|
/**
|
|
@@ -414,6 +416,8 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
|
|
|
414
416
|
const cwd = options.cwd ?? process.cwd();
|
|
415
417
|
const force = options.force ?? false;
|
|
416
418
|
const skipLogin = options.skipLogin ?? false;
|
|
419
|
+
const skipDependencyInstall =
|
|
420
|
+
options.skipDependencyInstall ?? Boolean(process.env.VITEST);
|
|
417
421
|
|
|
418
422
|
const created: string[] = [];
|
|
419
423
|
const skipped: string[] = [];
|
|
@@ -738,14 +742,13 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
|
|
|
738
742
|
}
|
|
739
743
|
|
|
740
744
|
// Install @tinybirdco/sdk if not already installed
|
|
741
|
-
if (!hasTinybirdSdkDependency(cwd)) {
|
|
745
|
+
if (!skipDependencyInstall && !hasTinybirdSdkDependency(cwd)) {
|
|
742
746
|
const s = p.spinner();
|
|
743
747
|
s.start("Installing dependencies");
|
|
744
748
|
const packageManager = detectPackageManager(cwd);
|
|
745
749
|
const addCmd = getPackageManagerAddCmd(packageManager);
|
|
746
750
|
try {
|
|
747
751
|
execSync(`${addCmd} @tinybirdco/sdk`, { cwd, stdio: "pipe" });
|
|
748
|
-
await sleep(1000);
|
|
749
752
|
s.stop("Installed dependencies");
|
|
750
753
|
created.push("@tinybirdco/sdk");
|
|
751
754
|
} catch (error) {
|
|
@@ -893,7 +896,25 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
|
|
|
893
896
|
if (!skipLogin && !hasValidToken(cwd)) {
|
|
894
897
|
console.log("\nNo authentication found. Starting login flow...\n");
|
|
895
898
|
|
|
896
|
-
|
|
899
|
+
// Select region before login (init creates fresh config, so always prompt)
|
|
900
|
+
const regionResult = await selectRegion();
|
|
901
|
+
if (!regionResult.success || !regionResult.apiHost) {
|
|
902
|
+
return {
|
|
903
|
+
success: true,
|
|
904
|
+
created,
|
|
905
|
+
skipped,
|
|
906
|
+
loggedIn: false,
|
|
907
|
+
devMode,
|
|
908
|
+
clientPath: relativeTinybirdDir,
|
|
909
|
+
existingDatafiles:
|
|
910
|
+
existingDatafiles.length > 0 ? existingDatafiles : undefined,
|
|
911
|
+
ciWorkflowCreated,
|
|
912
|
+
cdWorkflowCreated,
|
|
913
|
+
workflowProvider,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const authResult = await browserLogin({ apiHost: regionResult.apiHost });
|
|
897
918
|
|
|
898
919
|
if (authResult.success && authResult.token) {
|
|
899
920
|
// Save token to .env.local
|
|
@@ -903,13 +924,11 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
|
|
|
903
924
|
created.push(".env.local");
|
|
904
925
|
}
|
|
905
926
|
|
|
906
|
-
//
|
|
907
|
-
const baseUrl = authResult.baseUrl ??
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
updateConfig(currentConfigPath, { baseUrl });
|
|
912
|
-
}
|
|
927
|
+
// Update config with selected region's baseUrl
|
|
928
|
+
const baseUrl = authResult.baseUrl ?? regionResult.apiHost;
|
|
929
|
+
const currentConfigPath = findExistingConfigPath(cwd);
|
|
930
|
+
if (currentConfigPath && currentConfigPath.endsWith(".json")) {
|
|
931
|
+
updateConfig(currentConfigPath, { baseUrl });
|
|
913
932
|
}
|
|
914
933
|
|
|
915
934
|
// Generate TypeScript from existing Tinybird resources if requested
|
|
@@ -9,10 +9,17 @@ vi.mock("../auth.js", () => ({
|
|
|
9
9
|
browserLogin: vi.fn(),
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// Mock the region-selector module to avoid interactive prompts
|
|
13
|
+
vi.mock("../region-selector.js", () => ({
|
|
14
|
+
getApiHostWithRegionSelection: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Import the mocked functions
|
|
13
18
|
import { browserLogin } from "../auth.js";
|
|
19
|
+
import { getApiHostWithRegionSelection } from "../region-selector.js";
|
|
14
20
|
|
|
15
21
|
const mockedBrowserLogin = vi.mocked(browserLogin);
|
|
22
|
+
const mockedGetApiHost = vi.mocked(getApiHostWithRegionSelection);
|
|
16
23
|
|
|
17
24
|
describe("Login Command", () => {
|
|
18
25
|
let tempDir: string;
|
|
@@ -20,6 +27,11 @@ describe("Login Command", () => {
|
|
|
20
27
|
beforeEach(() => {
|
|
21
28
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-login-test-"));
|
|
22
29
|
vi.clearAllMocks();
|
|
30
|
+
// Default mock for region selection - returns a default apiHost
|
|
31
|
+
mockedGetApiHost.mockResolvedValue({
|
|
32
|
+
apiHost: "https://api.tinybird.co",
|
|
33
|
+
fromConfig: false,
|
|
34
|
+
});
|
|
23
35
|
});
|
|
24
36
|
|
|
25
37
|
afterEach(() => {
|
|
@@ -6,6 +6,7 @@ import * as path from "path";
|
|
|
6
6
|
import { browserLogin, type LoginOptions, type AuthResult } from "../auth.js";
|
|
7
7
|
import { updateConfig, findConfigFile } from "../config.js";
|
|
8
8
|
import { saveTinybirdToken } from "../env.js";
|
|
9
|
+
import { getApiHostWithRegionSelection } from "../region-selector.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Login command options
|
|
@@ -57,11 +58,21 @@ export async function runLogin(options: RunLoginOptions = {}): Promise<LoginResu
|
|
|
57
58
|
// Get the directory containing the config file for .env.local
|
|
58
59
|
const configDir = path.dirname(configPath);
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// Determine API host: option > config baseUrl > region selection
|
|
62
|
+
let apiHost = options.apiHost;
|
|
63
|
+
if (!apiHost) {
|
|
64
|
+
const regionResult = await getApiHostWithRegionSelection(configPath);
|
|
65
|
+
if (!regionResult) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: "Region selection cancelled",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
apiHost = regionResult.apiHost;
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
const loginOptions: LoginOptions = { apiHost };
|
|
75
|
+
|
|
65
76
|
// Perform browser login
|
|
66
77
|
const authResult: AuthResult = await browserLogin(loginOptions);
|
|
67
78
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import { selectRegion, getApiHostWithRegionSelection } from "./region-selector.js";
|
|
6
|
+
|
|
7
|
+
// Mock the regions API
|
|
8
|
+
vi.mock("../api/regions.js", () => ({
|
|
9
|
+
fetchRegions: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Mock @clack/prompts
|
|
13
|
+
vi.mock("@clack/prompts", () => ({
|
|
14
|
+
select: vi.fn(),
|
|
15
|
+
isCancel: vi.fn(),
|
|
16
|
+
cancel: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { fetchRegions } from "../api/regions.js";
|
|
20
|
+
import * as p from "@clack/prompts";
|
|
21
|
+
|
|
22
|
+
const mockedFetchRegions = vi.mocked(fetchRegions);
|
|
23
|
+
const mockedSelect = vi.mocked(p.select);
|
|
24
|
+
const mockedIsCancel = vi.mocked(p.isCancel);
|
|
25
|
+
|
|
26
|
+
describe("selectRegion", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
mockedIsCancel.mockReturnValue(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("fetches regions and shows selection prompt with GCP first", async () => {
|
|
33
|
+
mockedFetchRegions.mockResolvedValue([
|
|
34
|
+
{ name: "us-east-1", api_host: "https://api.us.tinybird.co", provider: "aws" },
|
|
35
|
+
{ name: "europe-west3", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
|
|
36
|
+
]);
|
|
37
|
+
mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
|
|
38
|
+
|
|
39
|
+
const result = await selectRegion();
|
|
40
|
+
|
|
41
|
+
expect(result.success).toBe(true);
|
|
42
|
+
expect(result.apiHost).toBe("https://api.eu.tinybird.co");
|
|
43
|
+
expect(result.regionName).toBe("europe-west3");
|
|
44
|
+
// GCP regions should appear first, then AWS
|
|
45
|
+
expect(mockedSelect).toHaveBeenCalledWith({
|
|
46
|
+
message: "Select your Tinybird region",
|
|
47
|
+
options: [
|
|
48
|
+
{ value: "https://api.eu.tinybird.co", label: "gcp | europe-west3", hint: "api.eu.tinybird.co" },
|
|
49
|
+
{ value: "https://api.us.tinybird.co", label: "aws | us-east-1", hint: "api.us.tinybird.co" },
|
|
50
|
+
],
|
|
51
|
+
initialValue: undefined,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("uses fallback regions when API fails", async () => {
|
|
56
|
+
mockedFetchRegions.mockRejectedValue(new Error("Network error"));
|
|
57
|
+
mockedSelect.mockResolvedValue("https://api.europe-west2.gcp.tinybird.co");
|
|
58
|
+
|
|
59
|
+
const result = await selectRegion();
|
|
60
|
+
|
|
61
|
+
expect(result.success).toBe(true);
|
|
62
|
+
expect(result.apiHost).toBe("https://api.europe-west2.gcp.tinybird.co");
|
|
63
|
+
expect(mockedSelect).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns cancelled when user cancels", async () => {
|
|
67
|
+
mockedFetchRegions.mockResolvedValue([
|
|
68
|
+
{ name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
|
|
69
|
+
]);
|
|
70
|
+
mockedSelect.mockResolvedValue(Symbol("cancel"));
|
|
71
|
+
mockedIsCancel.mockReturnValue(true);
|
|
72
|
+
|
|
73
|
+
const result = await selectRegion();
|
|
74
|
+
|
|
75
|
+
expect(result.success).toBe(false);
|
|
76
|
+
expect(result.cancelled).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("uses fallback regions when API returns empty array", async () => {
|
|
80
|
+
mockedFetchRegions.mockResolvedValue([]);
|
|
81
|
+
mockedSelect.mockResolvedValue("https://api.tinybird.co");
|
|
82
|
+
|
|
83
|
+
const result = await selectRegion();
|
|
84
|
+
|
|
85
|
+
expect(result.success).toBe(true);
|
|
86
|
+
// Should have used fallback regions
|
|
87
|
+
expect(mockedSelect).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("pre-selects region when defaultApiHost is provided", async () => {
|
|
91
|
+
mockedFetchRegions.mockResolvedValue([
|
|
92
|
+
{ name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
|
|
93
|
+
{ name: "US", api_host: "https://api.us.tinybird.co", provider: "aws" },
|
|
94
|
+
]);
|
|
95
|
+
mockedSelect.mockResolvedValue("https://api.us.tinybird.co");
|
|
96
|
+
|
|
97
|
+
const result = await selectRegion("https://api.us.tinybird.co");
|
|
98
|
+
|
|
99
|
+
expect(result.success).toBe(true);
|
|
100
|
+
expect(mockedSelect).toHaveBeenCalledWith(
|
|
101
|
+
expect.objectContaining({
|
|
102
|
+
initialValue: "https://api.us.tinybird.co",
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("does not set initialValue when defaultApiHost does not match any region", async () => {
|
|
108
|
+
mockedFetchRegions.mockResolvedValue([
|
|
109
|
+
{ name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
|
|
110
|
+
]);
|
|
111
|
+
mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
|
|
112
|
+
|
|
113
|
+
await selectRegion("https://api.unknown.tinybird.co");
|
|
114
|
+
|
|
115
|
+
expect(mockedSelect).toHaveBeenCalledWith(
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
initialValue: undefined,
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("getApiHostWithRegionSelection", () => {
|
|
124
|
+
let tempDir: string;
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "region-selector-test-"));
|
|
128
|
+
vi.clearAllMocks();
|
|
129
|
+
mockedIsCancel.mockReturnValue(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
try {
|
|
134
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
135
|
+
} catch {
|
|
136
|
+
// Ignore cleanup errors
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("pre-selects baseUrl from config when present", async () => {
|
|
141
|
+
const configPath = path.join(tempDir, "tinybird.config.json");
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
configPath,
|
|
144
|
+
JSON.stringify({ baseUrl: "https://api.us-east.tinybird.co" })
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
mockedFetchRegions.mockResolvedValue([
|
|
148
|
+
{ name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
|
|
149
|
+
{ name: "US East", api_host: "https://api.us-east.tinybird.co", provider: "aws" },
|
|
150
|
+
]);
|
|
151
|
+
mockedSelect.mockResolvedValue("https://api.us-east.tinybird.co");
|
|
152
|
+
|
|
153
|
+
const result = await getApiHostWithRegionSelection(configPath);
|
|
154
|
+
|
|
155
|
+
expect(result).toEqual({
|
|
156
|
+
apiHost: "https://api.us-east.tinybird.co",
|
|
157
|
+
fromConfig: false,
|
|
158
|
+
});
|
|
159
|
+
// Should have called select with initialValue
|
|
160
|
+
expect(mockedSelect).toHaveBeenCalledWith(
|
|
161
|
+
expect.objectContaining({
|
|
162
|
+
initialValue: "https://api.us-east.tinybird.co",
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("prompts for region when config has no baseUrl", async () => {
|
|
168
|
+
const configPath = path.join(tempDir, "tinybird.config.json");
|
|
169
|
+
fs.writeFileSync(configPath, JSON.stringify({ token: "test" }));
|
|
170
|
+
|
|
171
|
+
mockedFetchRegions.mockResolvedValue([
|
|
172
|
+
{ name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
|
|
173
|
+
]);
|
|
174
|
+
mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
|
|
175
|
+
|
|
176
|
+
const result = await getApiHostWithRegionSelection(configPath);
|
|
177
|
+
|
|
178
|
+
expect(result).toEqual({
|
|
179
|
+
apiHost: "https://api.eu.tinybird.co",
|
|
180
|
+
fromConfig: false,
|
|
181
|
+
});
|
|
182
|
+
expect(mockedSelect).toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("prompts for region when config file is null", async () => {
|
|
186
|
+
mockedFetchRegions.mockResolvedValue([
|
|
187
|
+
{ name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
|
|
188
|
+
]);
|
|
189
|
+
mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
|
|
190
|
+
|
|
191
|
+
const result = await getApiHostWithRegionSelection(null);
|
|
192
|
+
|
|
193
|
+
expect(result).toEqual({
|
|
194
|
+
apiHost: "https://api.eu.tinybird.co",
|
|
195
|
+
fromConfig: false,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("prompts for region when config is not JSON", async () => {
|
|
200
|
+
mockedFetchRegions.mockResolvedValue([
|
|
201
|
+
{ name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
|
|
202
|
+
]);
|
|
203
|
+
mockedSelect.mockResolvedValue("https://api.eu.tinybird.co");
|
|
204
|
+
|
|
205
|
+
const result = await getApiHostWithRegionSelection("/path/to/config.mjs");
|
|
206
|
+
|
|
207
|
+
expect(result).toEqual({
|
|
208
|
+
apiHost: "https://api.eu.tinybird.co",
|
|
209
|
+
fromConfig: false,
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("returns null when user cancels region selection", async () => {
|
|
214
|
+
const configPath = path.join(tempDir, "tinybird.config.json");
|
|
215
|
+
fs.writeFileSync(configPath, JSON.stringify({ token: "test" }));
|
|
216
|
+
|
|
217
|
+
mockedFetchRegions.mockResolvedValue([
|
|
218
|
+
{ name: "EU", api_host: "https://api.eu.tinybird.co", provider: "gcp" },
|
|
219
|
+
]);
|
|
220
|
+
mockedSelect.mockResolvedValue(Symbol("cancel"));
|
|
221
|
+
mockedIsCancel.mockReturnValue(true);
|
|
222
|
+
|
|
223
|
+
const result = await getApiHostWithRegionSelection(configPath);
|
|
224
|
+
|
|
225
|
+
expect(result).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Region selection utility for CLI commands
|
|
3
|
+
*
|
|
4
|
+
* Provides interactive region selection using @clack/prompts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
import { fetchRegions, type TinybirdRegion } from "../api/regions.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default fallback regions if API call fails
|
|
13
|
+
*/
|
|
14
|
+
const FALLBACK_REGIONS: TinybirdRegion[] = [
|
|
15
|
+
{
|
|
16
|
+
name: "EU (GCP)",
|
|
17
|
+
api_host: "https://api.europe-west2.gcp.tinybird.co",
|
|
18
|
+
provider: "gcp",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "US East (AWS)",
|
|
22
|
+
api_host: "https://api.us-east.aws.tinybird.co",
|
|
23
|
+
provider: "aws",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "EU (Default)",
|
|
27
|
+
api_host: "https://api.tinybird.co",
|
|
28
|
+
provider: "gcp",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Result of region selection
|
|
34
|
+
*/
|
|
35
|
+
export interface RegionSelectionResult {
|
|
36
|
+
/** Whether selection was successful (not cancelled) */
|
|
37
|
+
success: boolean;
|
|
38
|
+
/** Selected API host URL */
|
|
39
|
+
apiHost?: string;
|
|
40
|
+
/** Selected region name (for display) */
|
|
41
|
+
regionName?: string;
|
|
42
|
+
/** Whether user cancelled */
|
|
43
|
+
cancelled?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prompt user to select a Tinybird region
|
|
48
|
+
*
|
|
49
|
+
* Fetches available regions from the API and presents an interactive selection.
|
|
50
|
+
* Falls back to hardcoded regions if the API call fails.
|
|
51
|
+
*
|
|
52
|
+
* @param defaultApiHost - Optional API host to pre-select in the prompt
|
|
53
|
+
* @returns Selected region info or cancellation result
|
|
54
|
+
*/
|
|
55
|
+
export async function selectRegion(
|
|
56
|
+
defaultApiHost?: string
|
|
57
|
+
): Promise<RegionSelectionResult> {
|
|
58
|
+
let regions: TinybirdRegion[];
|
|
59
|
+
|
|
60
|
+
// Try to fetch regions from API
|
|
61
|
+
try {
|
|
62
|
+
regions = await fetchRegions();
|
|
63
|
+
} catch {
|
|
64
|
+
// Fall back to hardcoded regions
|
|
65
|
+
regions = FALLBACK_REGIONS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Ensure we have at least one region
|
|
69
|
+
if (regions.length === 0) {
|
|
70
|
+
regions = FALLBACK_REGIONS;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Sort regions: GCP first, then AWS
|
|
74
|
+
regions.sort((a, b) => {
|
|
75
|
+
if (a.provider === b.provider) return 0;
|
|
76
|
+
return a.provider === "gcp" ? -1 : 1;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Build options for p.select
|
|
80
|
+
const options = regions.map((region) => ({
|
|
81
|
+
value: region.api_host,
|
|
82
|
+
label: `${region.provider} | ${region.name}`,
|
|
83
|
+
hint: region.api_host.replace("https://", ""),
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
// Find initial value if defaultApiHost is provided and matches a region
|
|
87
|
+
// Normalize URLs for comparison (remove trailing slashes, lowercase)
|
|
88
|
+
const normalizeUrl = (url: string) => url.toLowerCase().replace(/\/+$/, "");
|
|
89
|
+
const initialValue = defaultApiHost
|
|
90
|
+
? regions.find(
|
|
91
|
+
(r) => normalizeUrl(r.api_host) === normalizeUrl(defaultApiHost)
|
|
92
|
+
)?.api_host
|
|
93
|
+
: undefined;
|
|
94
|
+
|
|
95
|
+
const selected = await p.select({
|
|
96
|
+
message: "Select your Tinybird region",
|
|
97
|
+
options,
|
|
98
|
+
initialValue,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (p.isCancel(selected)) {
|
|
102
|
+
p.cancel("Operation cancelled");
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
cancelled: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const selectedRegion = regions.find((r) => r.api_host === selected);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
apiHost: selected as string,
|
|
114
|
+
regionName: selectedRegion?.name,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get API host from config file or prompt for region selection
|
|
120
|
+
*
|
|
121
|
+
* @param configPath - Path to config file (or null if no config)
|
|
122
|
+
* @returns API host URL and source, or null if cancelled
|
|
123
|
+
*/
|
|
124
|
+
export async function getApiHostWithRegionSelection(
|
|
125
|
+
configPath: string | null
|
|
126
|
+
): Promise<{ apiHost: string; fromConfig: boolean } | null> {
|
|
127
|
+
let existingBaseUrl: string | undefined;
|
|
128
|
+
|
|
129
|
+
// If we have a JSON config file, try to read baseUrl from it
|
|
130
|
+
if (configPath && configPath.endsWith(".json")) {
|
|
131
|
+
try {
|
|
132
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
133
|
+
const config = JSON.parse(content);
|
|
134
|
+
existingBaseUrl = config.baseUrl;
|
|
135
|
+
} catch {
|
|
136
|
+
// Ignore errors reading config
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Prompt for region selection, pre-selecting existing baseUrl if available
|
|
141
|
+
const result = await selectRegion(existingBaseUrl);
|
|
142
|
+
|
|
143
|
+
if (!result.success || !result.apiHost) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
apiHost: result.apiHost,
|
|
149
|
+
fromConfig: false,
|
|
150
|
+
};
|
|
151
|
+
}
|
package/src/client/base.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { TinybirdClient, createClient } from "./base.js";
|
|
3
|
+
import type { DatasourcesNamespace } from "./types.js";
|
|
3
4
|
|
|
4
5
|
describe("TinybirdClient", () => {
|
|
5
6
|
describe("constructor", () => {
|
|
@@ -137,4 +138,35 @@ describe("TinybirdClient", () => {
|
|
|
137
138
|
expect(context.baseUrl).toBe("https://api.tinybird.co");
|
|
138
139
|
});
|
|
139
140
|
});
|
|
141
|
+
|
|
142
|
+
describe("datasources", () => {
|
|
143
|
+
it("exposes datasources namespace", () => {
|
|
144
|
+
const client = createClient({
|
|
145
|
+
baseUrl: "https://api.tinybird.co",
|
|
146
|
+
token: "test-token",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(client.datasources).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("datasources namespace has append method", () => {
|
|
153
|
+
const client = createClient({
|
|
154
|
+
baseUrl: "https://api.tinybird.co",
|
|
155
|
+
token: "test-token",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(typeof client.datasources.append).toBe("function");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("datasources conforms to DatasourcesNamespace interface", () => {
|
|
162
|
+
const client = createClient({
|
|
163
|
+
baseUrl: "https://api.tinybird.co",
|
|
164
|
+
token: "test-token",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const datasources: DatasourcesNamespace = client.datasources;
|
|
168
|
+
expect(datasources).toBeDefined();
|
|
169
|
+
expect(typeof datasources.append).toBe("function");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
140
172
|
});
|
package/src/client/base.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
|
+
AppendOptions,
|
|
7
|
+
AppendResult,
|
|
6
8
|
ClientConfig,
|
|
7
9
|
ClientContext,
|
|
10
|
+
DatasourcesNamespace,
|
|
8
11
|
QueryResult,
|
|
9
12
|
IngestResult,
|
|
10
13
|
QueryOptions,
|
|
@@ -57,6 +60,11 @@ export class TinybirdClient {
|
|
|
57
60
|
private contextPromise: Promise<ClientContext> | null = null;
|
|
58
61
|
private resolvedContext: ClientContext | null = null;
|
|
59
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Datasources namespace for import operations
|
|
65
|
+
*/
|
|
66
|
+
readonly datasources: DatasourcesNamespace;
|
|
67
|
+
|
|
60
68
|
constructor(config: ClientConfig) {
|
|
61
69
|
// Validate required config
|
|
62
70
|
if (!config.baseUrl) {
|
|
@@ -71,6 +79,46 @@ export class TinybirdClient {
|
|
|
71
79
|
...config,
|
|
72
80
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
73
81
|
};
|
|
82
|
+
|
|
83
|
+
// Initialize datasources namespace
|
|
84
|
+
this.datasources = {
|
|
85
|
+
append: (datasourceName: string, options: AppendOptions): Promise<AppendResult> => {
|
|
86
|
+
return this.appendDatasource(datasourceName, options);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Append data to a datasource from a URL or local file
|
|
93
|
+
*
|
|
94
|
+
* @param datasourceName - Name of the datasource
|
|
95
|
+
* @param options - Append options including url or file source
|
|
96
|
+
* @returns Append result
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* // Append from URL
|
|
101
|
+
* await client.datasources.append('events', {
|
|
102
|
+
* url: 'https://example.com/data.csv',
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Append from local file
|
|
106
|
+
* await client.datasources.append('events', {
|
|
107
|
+
* file: './data/events.ndjson',
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
private async appendDatasource(
|
|
112
|
+
datasourceName: string,
|
|
113
|
+
options: AppendOptions
|
|
114
|
+
): Promise<AppendResult> {
|
|
115
|
+
const token = await this.getToken();
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
return await this.getApi(token).appendDatasource(datasourceName, options);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.rethrowApiError(error);
|
|
121
|
+
}
|
|
74
122
|
}
|
|
75
123
|
|
|
76
124
|
/**
|