@tinybirdco/sdk 0.0.46 → 0.0.48
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/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +6 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +69 -1
- 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 +3 -1
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/login.test.js +9 -3
- package/dist/cli/commands/login.test.js.map +1 -1
- package/dist/cli/commands/migrate.test.js +187 -7
- package/dist/cli/commands/migrate.test.js.map +1 -1
- package/dist/generator/connection.d.ts.map +1 -1
- package/dist/generator/connection.js +3 -0
- package/dist/generator/connection.js.map +1 -1
- package/dist/generator/connection.test.js +8 -0
- package/dist/generator/connection.test.js.map +1 -1
- package/dist/generator/datasource.d.ts.map +1 -1
- package/dist/generator/datasource.js +3 -0
- package/dist/generator/datasource.js.map +1 -1
- package/dist/generator/datasource.test.js +50 -0
- package/dist/generator/datasource.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +3 -0
- package/dist/index.test.js.map +1 -1
- package/dist/migrate/emit-ts.d.ts.map +1 -1
- package/dist/migrate/emit-ts.js +109 -32
- package/dist/migrate/emit-ts.js.map +1 -1
- package/dist/migrate/parse-connection.d.ts.map +1 -1
- package/dist/migrate/parse-connection.js +13 -2
- package/dist/migrate/parse-connection.js.map +1 -1
- package/dist/migrate/parse-datasource.d.ts.map +1 -1
- package/dist/migrate/parse-datasource.js +39 -4
- package/dist/migrate/parse-datasource.js.map +1 -1
- package/dist/migrate/parse-pipe.d.ts.map +1 -1
- package/dist/migrate/parse-pipe.js +3 -2
- package/dist/migrate/parse-pipe.js.map +1 -1
- package/dist/migrate/types.d.ts +3 -0
- package/dist/migrate/types.d.ts.map +1 -1
- package/dist/schema/connection.d.ts +2 -0
- package/dist/schema/connection.d.ts.map +1 -1
- package/dist/schema/connection.js.map +1 -1
- package/dist/schema/datasource.d.ts +3 -1
- package/dist/schema/datasource.d.ts.map +1 -1
- package/dist/schema/datasource.js +8 -1
- package/dist/schema/datasource.js.map +1 -1
- package/dist/schema/datasource.test.js +12 -0
- package/dist/schema/datasource.test.js.map +1 -1
- package/dist/schema/engines.d.ts.map +1 -1
- package/dist/schema/engines.js +3 -0
- package/dist/schema/engines.js.map +1 -1
- package/dist/schema/engines.test.js +16 -0
- package/dist/schema/engines.test.js.map +1 -1
- package/dist/schema/secret.d.ts +6 -0
- package/dist/schema/secret.d.ts.map +1 -0
- package/dist/schema/secret.js +14 -0
- package/dist/schema/secret.js.map +1 -0
- package/dist/schema/secret.test.d.ts +2 -0
- package/dist/schema/secret.test.d.ts.map +1 -0
- package/dist/schema/secret.test.js +14 -0
- package/dist/schema/secret.test.js.map +1 -0
- package/dist/schema/types.d.ts +5 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +6 -0
- package/dist/schema/types.js.map +1 -1
- package/dist/schema/types.test.js +12 -0
- package/dist/schema/types.test.js.map +1 -1
- package/extension/tinybird-ts-sdk-extension-0.1.0.vsix +0 -0
- package/package.json +3 -2
- package/src/cli/commands/init.test.ts +89 -1
- package/src/cli/commands/init.ts +10 -2
- package/src/cli/commands/login.test.ts +13 -3
- package/src/cli/commands/login.ts +3 -1
- package/src/cli/commands/migrate.test.ts +279 -7
- package/src/generator/connection.test.ts +13 -0
- package/src/generator/connection.ts +4 -0
- package/src/generator/datasource.test.ts +60 -0
- package/src/generator/datasource.ts +3 -0
- package/src/index.test.ts +4 -0
- package/src/index.ts +3 -0
- package/src/migrate/emit-ts.ts +109 -38
- package/src/migrate/parse-connection.ts +15 -2
- package/src/migrate/parse-datasource.ts +53 -4
- package/src/migrate/parse-pipe.ts +5 -3
- package/src/migrate/types.ts +3 -0
- package/src/schema/connection.ts +2 -0
- package/src/schema/datasource.test.ts +16 -0
- package/src/schema/datasource.ts +13 -2
- package/src/schema/engines.test.ts +18 -0
- package/src/schema/engines.ts +3 -0
- package/src/schema/secret.test.ts +19 -0
- package/src/schema/secret.ts +16 -0
- package/src/schema/types.test.ts +14 -0
- package/src/schema/types.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinybirdco/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.48",
|
|
4
4
|
"description": "TypeScript SDK for Tinybird Forward - define datasources and pipes as TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"files": [
|
|
43
43
|
"dist",
|
|
44
44
|
"bin",
|
|
45
|
-
"src"
|
|
45
|
+
"src",
|
|
46
|
+
"extension/*.vsix"
|
|
46
47
|
],
|
|
47
48
|
"keywords": [
|
|
48
49
|
"tinybird",
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as os from "os";
|
|
5
|
-
import { runInit, findExistingDatafiles } from "./init.js";
|
|
5
|
+
import { runInit, findExistingDatafiles, findSyntaxHighlightingVsix } from "./init.js";
|
|
6
6
|
|
|
7
7
|
// Mock the auth module to avoid browser login
|
|
8
8
|
vi.mock("../auth.js", () => ({
|
|
@@ -17,11 +17,23 @@ vi.mock("../region-selector.js", () => ({
|
|
|
17
17
|
}),
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
+
import { browserLogin } from "../auth.js";
|
|
21
|
+
import { selectRegion } from "../region-selector.js";
|
|
22
|
+
|
|
23
|
+
const mockedBrowserLogin = vi.mocked(browserLogin);
|
|
24
|
+
const mockedSelectRegion = vi.mocked(selectRegion);
|
|
25
|
+
|
|
20
26
|
describe("Init Command", () => {
|
|
21
27
|
let tempDir: string;
|
|
22
28
|
|
|
23
29
|
beforeEach(() => {
|
|
24
30
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-init-test-"));
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
mockedSelectRegion.mockResolvedValue({
|
|
33
|
+
success: true,
|
|
34
|
+
apiHost: "https://api.tinybird.co",
|
|
35
|
+
});
|
|
36
|
+
mockedBrowserLogin.mockResolvedValue({ success: false });
|
|
25
37
|
});
|
|
26
38
|
|
|
27
39
|
afterEach(() => {
|
|
@@ -532,4 +544,80 @@ describe("Init Command", () => {
|
|
|
532
544
|
expect(result.installedTools).toBeUndefined();
|
|
533
545
|
});
|
|
534
546
|
});
|
|
547
|
+
|
|
548
|
+
describe("syntax highlighting VSIX lookup", () => {
|
|
549
|
+
it("finds the packaged VSIX when cwd is outside a git repo", () => {
|
|
550
|
+
const originalCwd = process.cwd();
|
|
551
|
+
const externalCwd = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-init-cwd-"));
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
process.chdir(externalCwd);
|
|
555
|
+
const vsixPath = findSyntaxHighlightingVsix(externalCwd);
|
|
556
|
+
|
|
557
|
+
expect(vsixPath).toBeDefined();
|
|
558
|
+
expect(path.basename(vsixPath ?? "")).toMatch(
|
|
559
|
+
/^tinybird-ts-sdk-extension.*\.vsix$/
|
|
560
|
+
);
|
|
561
|
+
} finally {
|
|
562
|
+
process.chdir(originalCwd);
|
|
563
|
+
fs.rmSync(externalCwd, { recursive: true, force: true });
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
describe("login flow", () => {
|
|
569
|
+
it("saves TINYBIRD_URL during init login", async () => {
|
|
570
|
+
mockedBrowserLogin.mockResolvedValue({
|
|
571
|
+
success: true,
|
|
572
|
+
token: "new-token-123",
|
|
573
|
+
baseUrl: "https://api.us-east.tinybird.co",
|
|
574
|
+
workspaceName: "test-workspace",
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const result = await runInit({
|
|
578
|
+
cwd: tempDir,
|
|
579
|
+
skipLogin: false,
|
|
580
|
+
devMode: "branch",
|
|
581
|
+
clientPath: "lib/tinybird.ts",
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
expect(result.success).toBe(true);
|
|
585
|
+
expect(result.loggedIn).toBe(true);
|
|
586
|
+
|
|
587
|
+
const envContent = fs.readFileSync(
|
|
588
|
+
path.join(tempDir, ".env.local"),
|
|
589
|
+
"utf-8"
|
|
590
|
+
);
|
|
591
|
+
expect(envContent).toContain("TINYBIRD_TOKEN=new-token-123");
|
|
592
|
+
expect(envContent).toContain("TINYBIRD_URL=https://api.us-east.tinybird.co");
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("uses selected region as TINYBIRD_URL when auth response has no baseUrl", async () => {
|
|
596
|
+
mockedSelectRegion.mockResolvedValue({
|
|
597
|
+
success: true,
|
|
598
|
+
apiHost: "https://api.eu-west.tinybird.co",
|
|
599
|
+
});
|
|
600
|
+
mockedBrowserLogin.mockResolvedValue({
|
|
601
|
+
success: true,
|
|
602
|
+
token: "new-token-456",
|
|
603
|
+
workspaceName: "test-workspace",
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const result = await runInit({
|
|
607
|
+
cwd: tempDir,
|
|
608
|
+
skipLogin: false,
|
|
609
|
+
devMode: "branch",
|
|
610
|
+
clientPath: "lib/tinybird.ts",
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
expect(result.success).toBe(true);
|
|
614
|
+
expect(result.loggedIn).toBe(true);
|
|
615
|
+
|
|
616
|
+
const envContent = fs.readFileSync(
|
|
617
|
+
path.join(tempDir, ".env.local"),
|
|
618
|
+
"utf-8"
|
|
619
|
+
);
|
|
620
|
+
expect(envContent).toContain("TINYBIRD_URL=https://api.eu-west.tinybird.co");
|
|
621
|
+
});
|
|
622
|
+
});
|
|
535
623
|
});
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -6,6 +6,7 @@ import * as fs from "fs";
|
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
8
|
import pc from "picocolors";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
9
10
|
import {
|
|
10
11
|
hasValidToken,
|
|
11
12
|
getRelativeTinybirdDir,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
type DevMode,
|
|
17
18
|
} from "../config.js";
|
|
18
19
|
import { browserLogin } from "../auth.js";
|
|
19
|
-
import { saveTinybirdToken } from "../env.js";
|
|
20
|
+
import { saveTinybirdBaseUrl, saveTinybirdToken } from "../env.js";
|
|
20
21
|
import { selectRegion } from "../region-selector.js";
|
|
21
22
|
import { getGitRoot } from "../git.js";
|
|
22
23
|
import { fetchAllResources } from "../../api/resources.js";
|
|
@@ -999,6 +1000,7 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
|
|
|
999
1000
|
|
|
1000
1001
|
// Update config with selected region's baseUrl
|
|
1001
1002
|
const baseUrl = authResult.baseUrl ?? regionResult.apiHost;
|
|
1003
|
+
saveTinybirdBaseUrl(cwd, baseUrl);
|
|
1002
1004
|
const currentConfigPath = findExistingConfigPath(cwd);
|
|
1003
1005
|
if (currentConfigPath && currentConfigPath.endsWith(".json")) {
|
|
1004
1006
|
updateConfig(currentConfigPath, { baseUrl });
|
|
@@ -1133,6 +1135,10 @@ const SKILLS_INSTALL_ARGS = [
|
|
|
1133
1135
|
];
|
|
1134
1136
|
const SYNTAX_EXTENSION_DIR = path.join("extension");
|
|
1135
1137
|
const SYNTAX_EXTENSION_PREFIX = "tinybird-ts-sdk-extension";
|
|
1138
|
+
const SDK_MODULE_ROOT = path.resolve(
|
|
1139
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
1140
|
+
"../../.."
|
|
1141
|
+
);
|
|
1136
1142
|
|
|
1137
1143
|
async function installSelectedTools(
|
|
1138
1144
|
cwd: string,
|
|
@@ -1294,8 +1300,10 @@ function isCommandAvailable(command: string): boolean {
|
|
|
1294
1300
|
}
|
|
1295
1301
|
}
|
|
1296
1302
|
|
|
1297
|
-
function findSyntaxHighlightingVsix(cwd: string): string | undefined {
|
|
1303
|
+
export function findSyntaxHighlightingVsix(cwd: string): string | undefined {
|
|
1298
1304
|
const searchRoots = new Set<string>();
|
|
1305
|
+
searchRoots.add(SDK_MODULE_ROOT);
|
|
1306
|
+
|
|
1299
1307
|
const gitRoot = getGitRoot();
|
|
1300
1308
|
if (gitRoot) {
|
|
1301
1309
|
searchRoots.add(gitRoot);
|
|
@@ -161,7 +161,7 @@ describe("Login Command", () => {
|
|
|
161
161
|
);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
it("creates .env.local with token", async () => {
|
|
164
|
+
it("creates .env.local with token and base URL", async () => {
|
|
165
165
|
mockedBrowserLogin.mockResolvedValue({
|
|
166
166
|
success: true,
|
|
167
167
|
token: "new-token-123",
|
|
@@ -178,19 +178,20 @@ describe("Login Command", () => {
|
|
|
178
178
|
"utf-8"
|
|
179
179
|
);
|
|
180
180
|
expect(envContent).toContain("TINYBIRD_TOKEN=new-token-123");
|
|
181
|
+
expect(envContent).toContain("TINYBIRD_URL=https://api.tinybird.co");
|
|
181
182
|
});
|
|
182
183
|
|
|
183
184
|
it("updates existing token in .env.local", async () => {
|
|
184
185
|
// Create existing .env.local with old token
|
|
185
186
|
fs.writeFileSync(
|
|
186
187
|
path.join(tempDir, ".env.local"),
|
|
187
|
-
"TINYBIRD_TOKEN=old-token\nOTHER_VAR=value\n"
|
|
188
|
+
"TINYBIRD_TOKEN=old-token\nTINYBIRD_URL=https://old.tinybird.co\nOTHER_VAR=value\n"
|
|
188
189
|
);
|
|
189
190
|
|
|
190
191
|
mockedBrowserLogin.mockResolvedValue({
|
|
191
192
|
success: true,
|
|
192
193
|
token: "new-token-456",
|
|
193
|
-
baseUrl: "https://api.tinybird.co",
|
|
194
|
+
baseUrl: "https://api.us-east.tinybird.co",
|
|
194
195
|
workspaceName: "test-workspace",
|
|
195
196
|
});
|
|
196
197
|
|
|
@@ -203,8 +204,10 @@ describe("Login Command", () => {
|
|
|
203
204
|
"utf-8"
|
|
204
205
|
);
|
|
205
206
|
expect(envContent).toContain("TINYBIRD_TOKEN=new-token-456");
|
|
207
|
+
expect(envContent).toContain("TINYBIRD_URL=https://api.us-east.tinybird.co");
|
|
206
208
|
expect(envContent).toContain("OTHER_VAR=value");
|
|
207
209
|
expect(envContent).not.toContain("old-token");
|
|
210
|
+
expect(envContent).not.toContain("https://old.tinybird.co");
|
|
208
211
|
});
|
|
209
212
|
|
|
210
213
|
it("appends token to existing .env.local without TINYBIRD_TOKEN", async () => {
|
|
@@ -231,6 +234,7 @@ describe("Login Command", () => {
|
|
|
231
234
|
);
|
|
232
235
|
expect(envContent).toContain("OTHER_VAR=value");
|
|
233
236
|
expect(envContent).toContain("TINYBIRD_TOKEN=new-token-789");
|
|
237
|
+
expect(envContent).toContain("TINYBIRD_URL=https://api.tinybird.co");
|
|
234
238
|
});
|
|
235
239
|
|
|
236
240
|
it("saves .env.local in same directory as tinybird.json (monorepo)", async () => {
|
|
@@ -312,6 +316,12 @@ describe("Login Command", () => {
|
|
|
312
316
|
);
|
|
313
317
|
// Original baseUrl preserved
|
|
314
318
|
expect(config.baseUrl).toBe("https://api.tinybird.co");
|
|
319
|
+
|
|
320
|
+
const envContent = fs.readFileSync(
|
|
321
|
+
path.join(tempDir, ".env.local"),
|
|
322
|
+
"utf-8"
|
|
323
|
+
);
|
|
324
|
+
expect(envContent).toContain("TINYBIRD_URL=https://api.tinybird.co");
|
|
315
325
|
});
|
|
316
326
|
});
|
|
317
327
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
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
|
-
import { saveTinybirdToken } from "../env.js";
|
|
8
|
+
import { saveTinybirdBaseUrl, saveTinybirdToken } from "../env.js";
|
|
9
9
|
import { getApiHostWithRegionSelection } from "../region-selector.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -87,6 +87,8 @@ export async function runLogin(options: RunLoginOptions = {}): Promise<LoginResu
|
|
|
87
87
|
// Save token to .env.local (in same directory as config file)
|
|
88
88
|
try {
|
|
89
89
|
saveTinybirdToken(configDir, authResult.token);
|
|
90
|
+
const baseUrl = authResult.baseUrl ?? apiHost;
|
|
91
|
+
saveTinybirdBaseUrl(configDir, baseUrl);
|
|
90
92
|
|
|
91
93
|
// Update baseUrl in config file if it changed (only for JSON configs)
|
|
92
94
|
if (authResult.baseUrl && configPath.endsWith(".json")) {
|
|
@@ -15,7 +15,7 @@ const EXPECTED_COMPLEX_OUTPUT = `/**
|
|
|
15
15
|
* Review endpoint output schemas and any defaults before production use.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { defineKafkaConnection, defineDatasource, definePipe, defineMaterializedView, defineCopyPipe, node, t, engine,
|
|
18
|
+
import { defineKafkaConnection, defineDatasource, definePipe, defineMaterializedView, defineCopyPipe, node, t, engine, p } from "@tinybirdco/sdk";
|
|
19
19
|
|
|
20
20
|
// Connections
|
|
21
21
|
|
|
@@ -36,12 +36,12 @@ export const stream = defineKafkaConnection("stream", {
|
|
|
36
36
|
export const events = defineDatasource("events", {
|
|
37
37
|
description: "Events from Kafka stream",
|
|
38
38
|
schema: {
|
|
39
|
-
event_id:
|
|
40
|
-
user_id:
|
|
41
|
-
env:
|
|
42
|
-
is_test:
|
|
43
|
-
updated_at:
|
|
44
|
-
payload:
|
|
39
|
+
event_id: t.string().jsonPath("$.event_id"),
|
|
40
|
+
user_id: t.uint64().jsonPath("$.user.id"),
|
|
41
|
+
env: t.string().default("prod").jsonPath("$.env"),
|
|
42
|
+
is_test: t.bool().default(false).jsonPath("$.meta.is_test"),
|
|
43
|
+
updated_at: t.dateTime().jsonPath("$.updated_at"),
|
|
44
|
+
payload: t.string().default("{}").codec("ZSTD(1)").jsonPath("$.payload"),
|
|
45
45
|
},
|
|
46
46
|
engine: engine.replacingMergeTree({ sortingKey: ["event_id", "user_id"], partitionKey: "toYYYYMM(updated_at)", primaryKey: "event_id", ttl: "updated_at + toIntervalDay(30)", ver: "updated_at", settings: { "index_granularity": 8192, "enable_mixed_granularity_parts": true } }),
|
|
47
47
|
kafka: {
|
|
@@ -615,4 +615,276 @@ IMPORT_FROM_TIMESTAMP 2024-01-01T00:00:00Z
|
|
|
615
615
|
expect(output).toContain('schedule: "@auto"');
|
|
616
616
|
expect(output).toContain('fromTimestamp: "2024-01-01T00:00:00Z"');
|
|
617
617
|
});
|
|
618
|
+
|
|
619
|
+
it("migrates KAFKA_STORE_RAW_VALUE datasource directive", async () => {
|
|
620
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
|
|
621
|
+
tempDirs.push(tempDir);
|
|
622
|
+
|
|
623
|
+
writeFile(
|
|
624
|
+
tempDir,
|
|
625
|
+
"stream.connection",
|
|
626
|
+
`TYPE kafka
|
|
627
|
+
KAFKA_BOOTSTRAP_SERVERS localhost:9092
|
|
628
|
+
`
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
writeFile(
|
|
632
|
+
tempDir,
|
|
633
|
+
"events.datasource",
|
|
634
|
+
`SCHEMA >
|
|
635
|
+
event_id String
|
|
636
|
+
|
|
637
|
+
ENGINE "MergeTree"
|
|
638
|
+
ENGINE_SORTING_KEY "event_id"
|
|
639
|
+
KAFKA_CONNECTION_NAME stream
|
|
640
|
+
KAFKA_TOPIC events_topic
|
|
641
|
+
KAFKA_STORE_RAW_VALUE True
|
|
642
|
+
`
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
const result = await runMigrate({
|
|
646
|
+
cwd: tempDir,
|
|
647
|
+
patterns: ["."],
|
|
648
|
+
strict: true,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
expect(result.success).toBe(true);
|
|
652
|
+
expect(result.errors).toHaveLength(0);
|
|
653
|
+
|
|
654
|
+
const output = fs.readFileSync(result.outputPath, "utf-8");
|
|
655
|
+
expect(output).toContain("kafka: {");
|
|
656
|
+
expect(output).toContain("connection: stream");
|
|
657
|
+
expect(output).toContain('topic: "events_topic"');
|
|
658
|
+
expect(output).toContain("storeRawValue: true");
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it("migrates kafka schema registry and engine is deleted directives", async () => {
|
|
662
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
|
|
663
|
+
tempDirs.push(tempDir);
|
|
664
|
+
|
|
665
|
+
writeFile(
|
|
666
|
+
tempDir,
|
|
667
|
+
"stream.connection",
|
|
668
|
+
`TYPE kafka
|
|
669
|
+
KAFKA_BOOTSTRAP_SERVERS kafka.example.com:9092
|
|
670
|
+
KAFKA_SCHEMA_REGISTRY_URL https://registry-user:registry-pass@registry.example.com
|
|
671
|
+
# Optional registry auth details
|
|
672
|
+
`
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
writeFile(
|
|
676
|
+
tempDir,
|
|
677
|
+
"events.datasource",
|
|
678
|
+
`SCHEMA >
|
|
679
|
+
event_id String
|
|
680
|
+
|
|
681
|
+
ENGINE "MergeTree"
|
|
682
|
+
ENGINE_SORTING_KEY "event_id"
|
|
683
|
+
KAFKA_CONNECTION_NAME stream
|
|
684
|
+
KAFKA_TOPIC events_topic
|
|
685
|
+
KAFKA_STORE_RAW_VALUE True
|
|
686
|
+
`
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
writeFile(
|
|
690
|
+
tempDir,
|
|
691
|
+
"events_state.datasource",
|
|
692
|
+
`SCHEMA >
|
|
693
|
+
# logical delete marker
|
|
694
|
+
_is_deleted UInt8,
|
|
695
|
+
event_id String,
|
|
696
|
+
version_ts DateTime
|
|
697
|
+
|
|
698
|
+
ENGINE "ReplacingMergeTree"
|
|
699
|
+
ENGINE_SORTING_KEY "event_id"
|
|
700
|
+
ENGINE_VER "version_ts"
|
|
701
|
+
ENGINE_IS_DELETED "_is_deleted"
|
|
702
|
+
`
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
writeFile(
|
|
706
|
+
tempDir,
|
|
707
|
+
"events_state_mv.pipe",
|
|
708
|
+
`NODE latest
|
|
709
|
+
SQL >
|
|
710
|
+
SELECT
|
|
711
|
+
toUInt8(0) AS _is_deleted,
|
|
712
|
+
event_id,
|
|
713
|
+
now() AS version_ts
|
|
714
|
+
FROM events
|
|
715
|
+
# materialized definition
|
|
716
|
+
TYPE MATERIALIZED
|
|
717
|
+
DATASOURCE events_state
|
|
718
|
+
`
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
const result = await runMigrate({
|
|
722
|
+
cwd: tempDir,
|
|
723
|
+
patterns: ["."],
|
|
724
|
+
strict: true,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
expect(result.success).toBe(true);
|
|
728
|
+
expect(result.errors).toHaveLength(0);
|
|
729
|
+
|
|
730
|
+
const output = fs.readFileSync(result.outputPath, "utf-8");
|
|
731
|
+
expect(output).toContain(
|
|
732
|
+
'schemaRegistryUrl: "https://registry-user:registry-pass@registry.example.com"'
|
|
733
|
+
);
|
|
734
|
+
expect(output).toContain("storeRawValue: true");
|
|
735
|
+
expect(output).toContain(
|
|
736
|
+
'engine: engine.replacingMergeTree({ sortingKey: "event_id", ver: "version_ts", isDeleted: "_is_deleted" })'
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it("migrates datasource with mixed explicit and default json paths", async () => {
|
|
741
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
|
|
742
|
+
tempDirs.push(tempDir);
|
|
743
|
+
|
|
744
|
+
writeFile(
|
|
745
|
+
tempDir,
|
|
746
|
+
"mixed_paths.datasource",
|
|
747
|
+
`SCHEMA >
|
|
748
|
+
event_id String \`json:$.payload.id\`,
|
|
749
|
+
event_type String
|
|
750
|
+
|
|
751
|
+
ENGINE "MergeTree"
|
|
752
|
+
ENGINE_SORTING_KEY "event_id"
|
|
753
|
+
`
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
const result = await runMigrate({
|
|
757
|
+
cwd: tempDir,
|
|
758
|
+
patterns: ["."],
|
|
759
|
+
strict: true,
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
expect(result.success).toBe(true);
|
|
763
|
+
expect(result.errors).toHaveLength(0);
|
|
764
|
+
|
|
765
|
+
const output = fs.readFileSync(result.outputPath, "utf-8");
|
|
766
|
+
expect(output).toContain('event_id: t.string().jsonPath("$.payload.id")');
|
|
767
|
+
expect(output).toContain("event_type: t.string()");
|
|
768
|
+
expect(output).not.toContain("jsonPaths: false");
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it("normalizes backticked schema column names to valid object keys", async () => {
|
|
772
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
|
|
773
|
+
tempDirs.push(tempDir);
|
|
774
|
+
|
|
775
|
+
writeFile(
|
|
776
|
+
tempDir,
|
|
777
|
+
"backticked.datasource",
|
|
778
|
+
`SCHEMA >
|
|
779
|
+
\`_is_deleted\` UInt8 \`json:$._is_deleted\`,
|
|
780
|
+
\`id\` UUID \`json:$.id\`
|
|
781
|
+
|
|
782
|
+
ENGINE "MergeTree"
|
|
783
|
+
ENGINE_SORTING_KEY "id"
|
|
784
|
+
`
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
const result = await runMigrate({
|
|
788
|
+
cwd: tempDir,
|
|
789
|
+
patterns: ["."],
|
|
790
|
+
strict: true,
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
expect(result.success).toBe(true);
|
|
794
|
+
expect(result.errors).toHaveLength(0);
|
|
795
|
+
|
|
796
|
+
const output = fs.readFileSync(result.outputPath, "utf-8");
|
|
797
|
+
expect(output).toContain('_is_deleted: t.uint8().jsonPath("$._is_deleted")');
|
|
798
|
+
expect(output).toContain('id: t.uuid().jsonPath("$.id")');
|
|
799
|
+
expect(output).not.toContain("`_is_deleted`:");
|
|
800
|
+
expect(output).not.toContain("`id`:");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it("emits secret helper for tb_secret template values", async () => {
|
|
804
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
|
|
805
|
+
tempDirs.push(tempDir);
|
|
806
|
+
|
|
807
|
+
writeFile(
|
|
808
|
+
tempDir,
|
|
809
|
+
"stream.connection",
|
|
810
|
+
`TYPE kafka
|
|
811
|
+
KAFKA_BOOTSTRAP_SERVERS localhost:9092
|
|
812
|
+
`
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
writeFile(
|
|
816
|
+
tempDir,
|
|
817
|
+
"events.datasource",
|
|
818
|
+
`SCHEMA >
|
|
819
|
+
id UUID
|
|
820
|
+
|
|
821
|
+
ENGINE "MergeTree"
|
|
822
|
+
ENGINE_SORTING_KEY "id"
|
|
823
|
+
KAFKA_CONNECTION_NAME stream
|
|
824
|
+
KAFKA_TOPIC events_topic
|
|
825
|
+
KAFKA_GROUP_ID {{ tb_secret("KAFKA_GROUP_ID_LOCAL_ds_accounts", "accounts_1737295200") }}
|
|
826
|
+
`
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
const result = await runMigrate({
|
|
830
|
+
cwd: tempDir,
|
|
831
|
+
patterns: ["."],
|
|
832
|
+
strict: true,
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
expect(result.success).toBe(true);
|
|
836
|
+
expect(result.errors).toHaveLength(0);
|
|
837
|
+
|
|
838
|
+
const output = fs.readFileSync(result.outputPath, "utf-8");
|
|
839
|
+
expect(output).toContain('import {');
|
|
840
|
+
expect(output).toContain('secret } from "@tinybirdco/sdk";');
|
|
841
|
+
expect(output).not.toContain("const secret = (name: string, defaultValue?: string) =>");
|
|
842
|
+
expect(output).toContain(
|
|
843
|
+
'groupId: secret("KAFKA_GROUP_ID_LOCAL_ds_accounts", "accounts_1737295200"),'
|
|
844
|
+
);
|
|
845
|
+
expect(output).not.toContain(
|
|
846
|
+
'groupId: "{{ tb_secret(\\"KAFKA_GROUP_ID_LOCAL_ds_accounts\\", \\"accounts_1737295200\\") }}",'
|
|
847
|
+
);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it("does not emit secret helper when no tb_secret template values are present", async () => {
|
|
851
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
|
|
852
|
+
tempDirs.push(tempDir);
|
|
853
|
+
|
|
854
|
+
writeFile(
|
|
855
|
+
tempDir,
|
|
856
|
+
"stream.connection",
|
|
857
|
+
`TYPE kafka
|
|
858
|
+
KAFKA_BOOTSTRAP_SERVERS localhost:9092
|
|
859
|
+
`
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
writeFile(
|
|
863
|
+
tempDir,
|
|
864
|
+
"events.datasource",
|
|
865
|
+
`SCHEMA >
|
|
866
|
+
id UUID
|
|
867
|
+
|
|
868
|
+
ENGINE "MergeTree"
|
|
869
|
+
ENGINE_SORTING_KEY "id"
|
|
870
|
+
KAFKA_CONNECTION_NAME stream
|
|
871
|
+
KAFKA_TOPIC events_topic
|
|
872
|
+
KAFKA_GROUP_ID events-group
|
|
873
|
+
`
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
const result = await runMigrate({
|
|
877
|
+
cwd: tempDir,
|
|
878
|
+
patterns: ["."],
|
|
879
|
+
strict: true,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
expect(result.success).toBe(true);
|
|
883
|
+
expect(result.errors).toHaveLength(0);
|
|
884
|
+
|
|
885
|
+
const output = fs.readFileSync(result.outputPath, "utf-8");
|
|
886
|
+
expect(output).not.toContain(", secret,");
|
|
887
|
+
expect(output).not.toContain("const secret = (name: string, defaultValue?: string) =>");
|
|
888
|
+
expect(output).toContain('groupId: "events-group",');
|
|
889
|
+
});
|
|
618
890
|
});
|
|
@@ -51,6 +51,19 @@ describe("Connection Generator", () => {
|
|
|
51
51
|
expect(result.content).toContain('KAFKA_SECRET {{ tb_secret("KAFKA_SECRET") }}');
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it("includes schema registry URL when provided", () => {
|
|
55
|
+
const conn = defineKafkaConnection("my_kafka", {
|
|
56
|
+
bootstrapServers: "kafka.example.com:9092",
|
|
57
|
+
schemaRegistryUrl: "https://registry-user:registry-pass@registry.example.com",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const result = generateConnection(conn);
|
|
61
|
+
|
|
62
|
+
expect(result.content).toContain(
|
|
63
|
+
"KAFKA_SCHEMA_REGISTRY_URL https://registry-user:registry-pass@registry.example.com"
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
54
67
|
it("includes SSL CA PEM when provided", () => {
|
|
55
68
|
const conn = defineKafkaConnection("my_kafka", {
|
|
56
69
|
bootstrapServers: "kafka.example.com:9092",
|
|
@@ -42,6 +42,10 @@ function generateKafkaConnection(connection: KafkaConnectionDefinition): string
|
|
|
42
42
|
parts.push(`KAFKA_SECRET ${options.secret}`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
if (options.schemaRegistryUrl) {
|
|
46
|
+
parts.push(`KAFKA_SCHEMA_REGISTRY_URL ${options.schemaRegistryUrl}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
if (options.sslCaPem) {
|
|
46
50
|
parts.push(`KAFKA_SSL_CA_PEM ${options.sslCaPem}`);
|
|
47
51
|
}
|
|
@@ -263,6 +263,43 @@ describe('Datasource Generator', () => {
|
|
|
263
263
|
expect(schemaLines[1]).toContain(',');
|
|
264
264
|
expect(schemaLines[2]).not.toContain(',');
|
|
265
265
|
});
|
|
266
|
+
|
|
267
|
+
it('autogenerates jsonPath when jsonPaths is enabled and no explicit path is set', () => {
|
|
268
|
+
const ds = defineDatasource('test_ds', {
|
|
269
|
+
schema: {
|
|
270
|
+
event_id: t.string().nullable(),
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const result = generateDatasource(ds);
|
|
275
|
+
expect(result.content).toContain('event_id Nullable(String) `json:$.event_id`');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('uses explicit jsonPath from validator modifier when jsonPaths is enabled', () => {
|
|
279
|
+
const ds = defineDatasource('test_ds', {
|
|
280
|
+
schema: {
|
|
281
|
+
event_id: t.string().nullable().jsonPath('$.explicit_path'),
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const result = generateDatasource(ds);
|
|
286
|
+
expect(result.content).toContain('event_id Nullable(String) `json:$.explicit_path`');
|
|
287
|
+
expect(result.content).not.toContain('`json:$.event_id`');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('omits json paths when jsonPaths is false even if column has explicit jsonPath modifier', () => {
|
|
291
|
+
const ds = defineDatasource('test_ds', {
|
|
292
|
+
jsonPaths: false,
|
|
293
|
+
schema: {
|
|
294
|
+
event_id: t.string().nullable().jsonPath('$.explicit_path'),
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const result = generateDatasource(ds);
|
|
299
|
+
expect(result.content).toContain('event_id Nullable(String)');
|
|
300
|
+
expect(result.content).not.toContain('`json:$.explicit_path`');
|
|
301
|
+
expect(result.content).not.toContain('`json:$.event_id`');
|
|
302
|
+
});
|
|
266
303
|
});
|
|
267
304
|
|
|
268
305
|
describe('generateAllDatasources', () => {
|
|
@@ -380,6 +417,29 @@ describe('Datasource Generator', () => {
|
|
|
380
417
|
expect(result.content).toContain('KAFKA_AUTO_OFFSET_RESET earliest');
|
|
381
418
|
});
|
|
382
419
|
|
|
420
|
+
it('includes store raw value when provided', () => {
|
|
421
|
+
const kafkaConn = defineKafkaConnection('my_kafka', {
|
|
422
|
+
bootstrapServers: 'kafka.example.com:9092',
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const ds = defineDatasource('kafka_events', {
|
|
426
|
+
schema: {
|
|
427
|
+
timestamp: t.dateTime(),
|
|
428
|
+
event: t.string(),
|
|
429
|
+
},
|
|
430
|
+
engine: engine.mergeTree({ sortingKey: ['timestamp'] }),
|
|
431
|
+
kafka: {
|
|
432
|
+
connection: kafkaConn,
|
|
433
|
+
topic: 'events',
|
|
434
|
+
storeRawValue: true,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const result = generateDatasource(ds);
|
|
439
|
+
|
|
440
|
+
expect(result.content).toContain('KAFKA_STORE_RAW_VALUE True');
|
|
441
|
+
});
|
|
442
|
+
|
|
383
443
|
it('generates complete Kafka datasource with all options', () => {
|
|
384
444
|
const kafkaConn = defineKafkaConnection('my_kafka', {
|
|
385
445
|
bootstrapServers: 'kafka.example.com:9092',
|
|
@@ -166,6 +166,9 @@ function generateKafkaConfig(kafka: KafkaConfig): string {
|
|
|
166
166
|
if (kafka.autoOffsetReset) {
|
|
167
167
|
parts.push(`KAFKA_AUTO_OFFSET_RESET ${kafka.autoOffsetReset}`);
|
|
168
168
|
}
|
|
169
|
+
if (kafka.storeRawValue !== undefined) {
|
|
170
|
+
parts.push(`KAFKA_STORE_RAW_VALUE ${kafka.storeRawValue ? "True" : "False"}`);
|
|
171
|
+
}
|
|
169
172
|
|
|
170
173
|
return parts.join("\n");
|
|
171
174
|
}
|
package/src/index.test.ts
CHANGED