@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.
Files changed (98) hide show
  1. package/dist/cli/commands/init.d.ts +1 -0
  2. package/dist/cli/commands/init.d.ts.map +1 -1
  3. package/dist/cli/commands/init.js +6 -2
  4. package/dist/cli/commands/init.js.map +1 -1
  5. package/dist/cli/commands/init.test.js +69 -1
  6. package/dist/cli/commands/init.test.js.map +1 -1
  7. package/dist/cli/commands/login.d.ts.map +1 -1
  8. package/dist/cli/commands/login.js +3 -1
  9. package/dist/cli/commands/login.js.map +1 -1
  10. package/dist/cli/commands/login.test.js +9 -3
  11. package/dist/cli/commands/login.test.js.map +1 -1
  12. package/dist/cli/commands/migrate.test.js +187 -7
  13. package/dist/cli/commands/migrate.test.js.map +1 -1
  14. package/dist/generator/connection.d.ts.map +1 -1
  15. package/dist/generator/connection.js +3 -0
  16. package/dist/generator/connection.js.map +1 -1
  17. package/dist/generator/connection.test.js +8 -0
  18. package/dist/generator/connection.test.js.map +1 -1
  19. package/dist/generator/datasource.d.ts.map +1 -1
  20. package/dist/generator/datasource.js +3 -0
  21. package/dist/generator/datasource.js.map +1 -1
  22. package/dist/generator/datasource.test.js +50 -0
  23. package/dist/generator/datasource.test.js.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.test.js +3 -0
  29. package/dist/index.test.js.map +1 -1
  30. package/dist/migrate/emit-ts.d.ts.map +1 -1
  31. package/dist/migrate/emit-ts.js +109 -32
  32. package/dist/migrate/emit-ts.js.map +1 -1
  33. package/dist/migrate/parse-connection.d.ts.map +1 -1
  34. package/dist/migrate/parse-connection.js +13 -2
  35. package/dist/migrate/parse-connection.js.map +1 -1
  36. package/dist/migrate/parse-datasource.d.ts.map +1 -1
  37. package/dist/migrate/parse-datasource.js +39 -4
  38. package/dist/migrate/parse-datasource.js.map +1 -1
  39. package/dist/migrate/parse-pipe.d.ts.map +1 -1
  40. package/dist/migrate/parse-pipe.js +3 -2
  41. package/dist/migrate/parse-pipe.js.map +1 -1
  42. package/dist/migrate/types.d.ts +3 -0
  43. package/dist/migrate/types.d.ts.map +1 -1
  44. package/dist/schema/connection.d.ts +2 -0
  45. package/dist/schema/connection.d.ts.map +1 -1
  46. package/dist/schema/connection.js.map +1 -1
  47. package/dist/schema/datasource.d.ts +3 -1
  48. package/dist/schema/datasource.d.ts.map +1 -1
  49. package/dist/schema/datasource.js +8 -1
  50. package/dist/schema/datasource.js.map +1 -1
  51. package/dist/schema/datasource.test.js +12 -0
  52. package/dist/schema/datasource.test.js.map +1 -1
  53. package/dist/schema/engines.d.ts.map +1 -1
  54. package/dist/schema/engines.js +3 -0
  55. package/dist/schema/engines.js.map +1 -1
  56. package/dist/schema/engines.test.js +16 -0
  57. package/dist/schema/engines.test.js.map +1 -1
  58. package/dist/schema/secret.d.ts +6 -0
  59. package/dist/schema/secret.d.ts.map +1 -0
  60. package/dist/schema/secret.js +14 -0
  61. package/dist/schema/secret.js.map +1 -0
  62. package/dist/schema/secret.test.d.ts +2 -0
  63. package/dist/schema/secret.test.d.ts.map +1 -0
  64. package/dist/schema/secret.test.js +14 -0
  65. package/dist/schema/secret.test.js.map +1 -0
  66. package/dist/schema/types.d.ts +5 -0
  67. package/dist/schema/types.d.ts.map +1 -1
  68. package/dist/schema/types.js +6 -0
  69. package/dist/schema/types.js.map +1 -1
  70. package/dist/schema/types.test.js +12 -0
  71. package/dist/schema/types.test.js.map +1 -1
  72. package/extension/tinybird-ts-sdk-extension-0.1.0.vsix +0 -0
  73. package/package.json +3 -2
  74. package/src/cli/commands/init.test.ts +89 -1
  75. package/src/cli/commands/init.ts +10 -2
  76. package/src/cli/commands/login.test.ts +13 -3
  77. package/src/cli/commands/login.ts +3 -1
  78. package/src/cli/commands/migrate.test.ts +279 -7
  79. package/src/generator/connection.test.ts +13 -0
  80. package/src/generator/connection.ts +4 -0
  81. package/src/generator/datasource.test.ts +60 -0
  82. package/src/generator/datasource.ts +3 -0
  83. package/src/index.test.ts +4 -0
  84. package/src/index.ts +3 -0
  85. package/src/migrate/emit-ts.ts +109 -38
  86. package/src/migrate/parse-connection.ts +15 -2
  87. package/src/migrate/parse-datasource.ts +53 -4
  88. package/src/migrate/parse-pipe.ts +5 -3
  89. package/src/migrate/types.ts +3 -0
  90. package/src/schema/connection.ts +2 -0
  91. package/src/schema/datasource.test.ts +16 -0
  92. package/src/schema/datasource.ts +13 -2
  93. package/src/schema/engines.test.ts +18 -0
  94. package/src/schema/engines.ts +3 -0
  95. package/src/schema/secret.test.ts +19 -0
  96. package/src/schema/secret.ts +16 -0
  97. package/src/schema/types.test.ts +14 -0
  98. 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.46",
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
  });
@@ -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, column, p } from "@tinybirdco/sdk";
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: column(t.string(), { jsonPath: "$.event_id" }),
40
- user_id: column(t.uint64(), { jsonPath: "$.user.id" }),
41
- env: column(t.string().default("prod"), { jsonPath: "$.env" }),
42
- is_test: column(t.bool().default(false), { jsonPath: "$.meta.is_test" }),
43
- updated_at: column(t.dateTime(), { jsonPath: "$.updated_at" }),
44
- payload: column(t.string().default("{}").codec("ZSTD(1)"), { jsonPath: "$.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
@@ -10,4 +10,8 @@ describe("root public exports", () => {
10
10
  expect(typeof sdk.defineProject).toBe("function");
11
11
  expect(typeof sdk.Tinybird).toBe("function");
12
12
  });
13
+
14
+ it("exports secret utility", () => {
15
+ expect(typeof sdk.secret).toBe("function");
16
+ });
13
17
  });