@tinybirdco/sdk 0.0.1 → 0.0.3

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 (78) hide show
  1. package/README.md +5 -4
  2. package/dist/api/build.d.ts +2 -0
  3. package/dist/api/build.d.ts.map +1 -1
  4. package/dist/api/build.js +13 -0
  5. package/dist/api/build.js.map +1 -1
  6. package/dist/api/build.test.js +1 -0
  7. package/dist/api/build.test.js.map +1 -1
  8. package/dist/api/deploy.d.ts.map +1 -1
  9. package/dist/api/deploy.js +3 -0
  10. package/dist/api/deploy.js.map +1 -1
  11. package/dist/api/deploy.test.js +1 -0
  12. package/dist/api/deploy.test.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts.map +1 -1
  14. package/dist/cli/commands/init.js +26 -0
  15. package/dist/cli/commands/init.js.map +1 -1
  16. package/dist/cli/commands/init.test.js +43 -0
  17. package/dist/cli/commands/init.test.js.map +1 -1
  18. package/dist/cli/index.js +4 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/generator/connection.d.ts +49 -0
  21. package/dist/generator/connection.d.ts.map +1 -0
  22. package/dist/generator/connection.js +78 -0
  23. package/dist/generator/connection.js.map +1 -0
  24. package/dist/generator/connection.test.d.ts +2 -0
  25. package/dist/generator/connection.test.d.ts.map +1 -0
  26. package/dist/generator/connection.test.js +106 -0
  27. package/dist/generator/connection.test.js.map +1 -0
  28. package/dist/generator/datasource.d.ts.map +1 -1
  29. package/dist/generator/datasource.js +20 -0
  30. package/dist/generator/datasource.js.map +1 -1
  31. package/dist/generator/datasource.test.js +92 -0
  32. package/dist/generator/datasource.test.js.map +1 -1
  33. package/dist/generator/index.d.ts +8 -2
  34. package/dist/generator/index.d.ts.map +1 -1
  35. package/dist/generator/index.js +10 -3
  36. package/dist/generator/index.js.map +1 -1
  37. package/dist/generator/loader.d.ts +8 -1
  38. package/dist/generator/loader.d.ts.map +1 -1
  39. package/dist/generator/loader.js +17 -2
  40. package/dist/generator/loader.js.map +1 -1
  41. package/dist/index.d.ts +4 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/schema/connection.d.ts +83 -0
  46. package/dist/schema/connection.d.ts.map +1 -0
  47. package/dist/schema/connection.js +61 -0
  48. package/dist/schema/connection.js.map +1 -0
  49. package/dist/schema/connection.test.d.ts +2 -0
  50. package/dist/schema/connection.test.d.ts.map +1 -0
  51. package/dist/schema/connection.test.js +117 -0
  52. package/dist/schema/connection.test.js.map +1 -0
  53. package/dist/schema/datasource.d.ts +16 -0
  54. package/dist/schema/datasource.d.ts.map +1 -1
  55. package/dist/schema/datasource.js.map +1 -1
  56. package/dist/schema/project.d.ts +12 -3
  57. package/dist/schema/project.d.ts.map +1 -1
  58. package/dist/schema/project.js +2 -0
  59. package/dist/schema/project.js.map +1 -1
  60. package/package.json +2 -1
  61. package/src/api/build.test.ts +1 -0
  62. package/src/api/build.ts +20 -0
  63. package/src/api/deploy.test.ts +1 -0
  64. package/src/api/deploy.ts +3 -0
  65. package/src/cli/commands/init.test.ts +72 -0
  66. package/src/cli/commands/init.ts +30 -0
  67. package/src/cli/index.ts +6 -0
  68. package/src/generator/connection.test.ts +135 -0
  69. package/src/generator/connection.ts +104 -0
  70. package/src/generator/datasource.test.ts +108 -0
  71. package/src/generator/datasource.ts +27 -1
  72. package/src/generator/index.ts +16 -4
  73. package/src/generator/loader.ts +21 -3
  74. package/src/index.ts +12 -0
  75. package/src/schema/connection.test.ts +149 -0
  76. package/src/schema/connection.ts +123 -0
  77. package/src/schema/datasource.ts +17 -0
  78. package/src/schema/project.ts +20 -5
package/src/api/build.ts CHANGED
@@ -88,6 +88,8 @@ export interface BuildApiResult {
88
88
  datasourceCount: number;
89
89
  /** Number of pipes deployed */
90
90
  pipeCount: number;
91
+ /** Number of connections deployed */
92
+ connectionCount: number;
91
93
  /** Build ID if successful */
92
94
  buildId?: string;
93
95
  /** Pipe changes in this build */
@@ -166,6 +168,21 @@ export async function buildToTinybird(
166
168
  );
167
169
  }
168
170
 
171
+ // Add connections
172
+ for (const conn of resources.connections ?? []) {
173
+ const fieldName = `data_project://`;
174
+ const fileName = `${conn.name}.connection`;
175
+ if (debug) {
176
+ console.log(`[debug] Adding connection: ${fieldName} (filename: ${fileName})`);
177
+ console.log(`[debug] Content:\n${conn.content}\n`);
178
+ }
179
+ formData.append(
180
+ fieldName,
181
+ new Blob([conn.content], { type: "text/plain" }),
182
+ fileName
183
+ );
184
+ }
185
+
169
186
  // Make the request
170
187
  const url = `${config.baseUrl.replace(/\/$/, "")}/v1/build`;
171
188
 
@@ -217,6 +234,7 @@ export async function buildToTinybird(
217
234
  error: formatErrors(),
218
235
  datasourceCount: resources.datasources.length,
219
236
  pipeCount: resources.pipes.length,
237
+ connectionCount: resources.connections?.length ?? 0,
220
238
  };
221
239
  }
222
240
 
@@ -228,6 +246,7 @@ export async function buildToTinybird(
228
246
  error: formatErrors(),
229
247
  datasourceCount: resources.datasources.length,
230
248
  pipeCount: resources.pipes.length,
249
+ connectionCount: resources.connections?.length ?? 0,
231
250
  };
232
251
  }
233
252
 
@@ -236,6 +255,7 @@ export async function buildToTinybird(
236
255
  result: body.result,
237
256
  datasourceCount: resources.datasources.length,
238
257
  pipeCount: resources.pipes.length,
258
+ connectionCount: resources.connections?.length ?? 0,
239
259
  buildId: body.build?.id,
240
260
  pipes: {
241
261
  changed: body.build?.changed_pipe_names ?? [],
@@ -31,6 +31,7 @@ describe("Deploy API", () => {
31
31
  pipes: [
32
32
  { name: "top_events", content: "NODE main\nSQL > SELECT * FROM events" },
33
33
  ],
34
+ connections: [],
34
35
  };
35
36
 
36
37
  describe("deployToMain", () => {
package/src/api/deploy.ts CHANGED
@@ -126,6 +126,7 @@ export async function deployToMain(
126
126
  error: formatErrors(),
127
127
  datasourceCount: resources.datasources.length,
128
128
  pipeCount: resources.pipes.length,
129
+ connectionCount: resources.connections?.length ?? 0,
129
130
  };
130
131
  }
131
132
 
@@ -137,6 +138,7 @@ export async function deployToMain(
137
138
  error: formatErrors(),
138
139
  datasourceCount: resources.datasources.length,
139
140
  pipeCount: resources.pipes.length,
141
+ connectionCount: resources.connections?.length ?? 0,
140
142
  };
141
143
  }
142
144
 
@@ -145,6 +147,7 @@ export async function deployToMain(
145
147
  result: body.result,
146
148
  datasourceCount: resources.datasources.length,
147
149
  pipeCount: resources.pipes.length,
150
+ connectionCount: resources.connections?.length ?? 0,
148
151
  buildId: body.build?.id,
149
152
  pipes: {
150
153
  changed: body.build?.changed_pipe_names ?? [],
@@ -228,6 +228,78 @@ describe("Init Command", () => {
228
228
  });
229
229
  });
230
230
 
231
+ describe("package.json scripts", () => {
232
+ it("adds tinybird:dev and tinybird:build scripts to existing package.json", async () => {
233
+ const packageJson = { name: "test-project", scripts: { dev: "next dev" } };
234
+ fs.writeFileSync(
235
+ path.join(tempDir, "package.json"),
236
+ JSON.stringify(packageJson, null, 2)
237
+ );
238
+
239
+ const result = await runInit({ cwd: tempDir, skipLogin: true });
240
+
241
+ expect(result.success).toBe(true);
242
+ expect(result.created).toContain("package.json (added tinybird scripts)");
243
+
244
+ const updatedPackageJson = JSON.parse(
245
+ fs.readFileSync(path.join(tempDir, "package.json"), "utf-8")
246
+ );
247
+ expect(updatedPackageJson.scripts["tinybird:dev"]).toBe("tinybird dev");
248
+ expect(updatedPackageJson.scripts["tinybird:build"]).toBe("tinybird build");
249
+ expect(updatedPackageJson.scripts.dev).toBe("next dev"); // preserved
250
+ });
251
+
252
+ it("does not overwrite existing tinybird scripts", async () => {
253
+ const packageJson = {
254
+ name: "test-project",
255
+ scripts: {
256
+ "tinybird:dev": "custom dev command",
257
+ "tinybird:build": "custom build command",
258
+ },
259
+ };
260
+ fs.writeFileSync(
261
+ path.join(tempDir, "package.json"),
262
+ JSON.stringify(packageJson, null, 2)
263
+ );
264
+
265
+ const result = await runInit({ cwd: tempDir, skipLogin: true });
266
+
267
+ expect(result.success).toBe(true);
268
+ expect(result.created).not.toContain("package.json (added tinybird scripts)");
269
+
270
+ const updatedPackageJson = JSON.parse(
271
+ fs.readFileSync(path.join(tempDir, "package.json"), "utf-8")
272
+ );
273
+ expect(updatedPackageJson.scripts["tinybird:dev"]).toBe("custom dev command");
274
+ expect(updatedPackageJson.scripts["tinybird:build"]).toBe("custom build command");
275
+ });
276
+
277
+ it("creates scripts object if package.json has no scripts", async () => {
278
+ const packageJson = { name: "test-project" };
279
+ fs.writeFileSync(
280
+ path.join(tempDir, "package.json"),
281
+ JSON.stringify(packageJson, null, 2)
282
+ );
283
+
284
+ const result = await runInit({ cwd: tempDir, skipLogin: true });
285
+
286
+ expect(result.success).toBe(true);
287
+
288
+ const updatedPackageJson = JSON.parse(
289
+ fs.readFileSync(path.join(tempDir, "package.json"), "utf-8")
290
+ );
291
+ expect(updatedPackageJson.scripts["tinybird:dev"]).toBe("tinybird dev");
292
+ expect(updatedPackageJson.scripts["tinybird:build"]).toBe("tinybird build");
293
+ });
294
+
295
+ it("does not fail if no package.json exists", async () => {
296
+ const result = await runInit({ cwd: tempDir, skipLogin: true });
297
+
298
+ expect(result.success).toBe(true);
299
+ expect(result.created).not.toContain("package.json (added tinybird scripts)");
300
+ });
301
+ });
302
+
231
303
  describe("directory creation", () => {
232
304
  it("creates tinybird directory if it does not exist", async () => {
233
305
  expect(fs.existsSync(path.join(tempDir, "tinybird"))).toBe(false);
@@ -267,6 +267,36 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
267
267
  }
268
268
  }
269
269
 
270
+ // Add scripts to package.json if it exists
271
+ const packageJsonPath = path.join(cwd, "package.json");
272
+ if (fs.existsSync(packageJsonPath)) {
273
+ try {
274
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
275
+ let modified = false;
276
+
277
+ if (!packageJson.scripts) {
278
+ packageJson.scripts = {};
279
+ }
280
+
281
+ if (!packageJson.scripts["tinybird:dev"]) {
282
+ packageJson.scripts["tinybird:dev"] = "tinybird dev";
283
+ modified = true;
284
+ }
285
+
286
+ if (!packageJson.scripts["tinybird:build"]) {
287
+ packageJson.scripts["tinybird:build"] = "tinybird build";
288
+ modified = true;
289
+ }
290
+
291
+ if (modified) {
292
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
293
+ created.push("package.json (added tinybird scripts)");
294
+ }
295
+ } catch {
296
+ // Silently ignore package.json errors - not critical
297
+ }
298
+ }
299
+
270
300
  // Check if login is needed
271
301
  if (!skipLogin && !hasValidToken(cwd)) {
272
302
  console.log("\nNo authentication found. Starting login flow...\n");
package/src/cli/index.ts CHANGED
@@ -4,6 +4,12 @@
4
4
  * Commands for building and deploying Tinybird projects
5
5
  */
6
6
 
7
+ import { config } from "dotenv";
8
+
9
+ // Load .env files in priority order (later files don't override earlier ones)
10
+ config({ path: ".env.local" });
11
+ config({ path: ".env" });
12
+
7
13
  import { readFileSync } from "node:fs";
8
14
  import { fileURLToPath } from "node:url";
9
15
  import { dirname, resolve } from "node:path";
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { generateConnection, generateAllConnections } from "./connection.js";
3
+ import { createKafkaConnection } from "../schema/connection.js";
4
+
5
+ describe("Connection Generator", () => {
6
+ describe("generateConnection", () => {
7
+ it("generates basic Kafka connection with required fields", () => {
8
+ const conn = createKafkaConnection("my_kafka", {
9
+ bootstrapServers: "kafka.example.com:9092",
10
+ });
11
+
12
+ const result = generateConnection(conn);
13
+
14
+ expect(result.name).toBe("my_kafka");
15
+ expect(result.content).toContain("TYPE kafka");
16
+ expect(result.content).toContain("KAFKA_BOOTSTRAP_SERVERS kafka.example.com:9092");
17
+ });
18
+
19
+ it("includes security protocol when provided", () => {
20
+ const conn = createKafkaConnection("my_kafka", {
21
+ bootstrapServers: "kafka.example.com:9092",
22
+ securityProtocol: "SASL_SSL",
23
+ });
24
+
25
+ const result = generateConnection(conn);
26
+
27
+ expect(result.content).toContain("KAFKA_SECURITY_PROTOCOL SASL_SSL");
28
+ });
29
+
30
+ it("includes SASL mechanism when provided", () => {
31
+ const conn = createKafkaConnection("my_kafka", {
32
+ bootstrapServers: "kafka.example.com:9092",
33
+ saslMechanism: "PLAIN",
34
+ });
35
+
36
+ const result = generateConnection(conn);
37
+
38
+ expect(result.content).toContain("KAFKA_SASL_MECHANISM PLAIN");
39
+ });
40
+
41
+ it("includes key and secret when provided", () => {
42
+ const conn = createKafkaConnection("my_kafka", {
43
+ bootstrapServers: "kafka.example.com:9092",
44
+ key: '{{ tb_secret("KAFKA_KEY") }}',
45
+ secret: '{{ tb_secret("KAFKA_SECRET") }}',
46
+ });
47
+
48
+ const result = generateConnection(conn);
49
+
50
+ expect(result.content).toContain('KAFKA_KEY {{ tb_secret("KAFKA_KEY") }}');
51
+ expect(result.content).toContain('KAFKA_SECRET {{ tb_secret("KAFKA_SECRET") }}');
52
+ });
53
+
54
+ it("includes SSL CA PEM when provided", () => {
55
+ const conn = createKafkaConnection("my_kafka", {
56
+ bootstrapServers: "kafka.example.com:9092",
57
+ sslCaPem: '{{ tb_secret("KAFKA_CA_CERT") }}',
58
+ });
59
+
60
+ const result = generateConnection(conn);
61
+
62
+ expect(result.content).toContain('KAFKA_SSL_CA_PEM {{ tb_secret("KAFKA_CA_CERT") }}');
63
+ });
64
+
65
+ it("generates full Kafka connection with all options", () => {
66
+ const conn = createKafkaConnection("my_kafka", {
67
+ bootstrapServers: "kafka.example.com:9092",
68
+ securityProtocol: "SASL_SSL",
69
+ saslMechanism: "SCRAM-SHA-256",
70
+ key: '{{ tb_secret("KAFKA_KEY") }}',
71
+ secret: '{{ tb_secret("KAFKA_SECRET") }}',
72
+ sslCaPem: '{{ tb_secret("KAFKA_CA_CERT") }}',
73
+ });
74
+
75
+ const result = generateConnection(conn);
76
+
77
+ expect(result.name).toBe("my_kafka");
78
+ expect(result.content).toContain("TYPE kafka");
79
+ expect(result.content).toContain("KAFKA_BOOTSTRAP_SERVERS kafka.example.com:9092");
80
+ expect(result.content).toContain("KAFKA_SECURITY_PROTOCOL SASL_SSL");
81
+ expect(result.content).toContain("KAFKA_SASL_MECHANISM SCRAM-SHA-256");
82
+ expect(result.content).toContain('KAFKA_KEY {{ tb_secret("KAFKA_KEY") }}');
83
+ expect(result.content).toContain('KAFKA_SECRET {{ tb_secret("KAFKA_SECRET") }}');
84
+ expect(result.content).toContain('KAFKA_SSL_CA_PEM {{ tb_secret("KAFKA_CA_CERT") }}');
85
+ });
86
+
87
+ it("supports PLAINTEXT security protocol", () => {
88
+ const conn = createKafkaConnection("local_kafka", {
89
+ bootstrapServers: "localhost:9092",
90
+ securityProtocol: "PLAINTEXT",
91
+ });
92
+
93
+ const result = generateConnection(conn);
94
+
95
+ expect(result.content).toContain("KAFKA_SECURITY_PROTOCOL PLAINTEXT");
96
+ });
97
+
98
+ it("supports different SASL mechanisms", () => {
99
+ const mechanisms = ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512", "OAUTHBEARER"] as const;
100
+
101
+ mechanisms.forEach((mechanism) => {
102
+ const conn = createKafkaConnection("my_kafka", {
103
+ bootstrapServers: "kafka.example.com:9092",
104
+ saslMechanism: mechanism,
105
+ });
106
+
107
+ const result = generateConnection(conn);
108
+
109
+ expect(result.content).toContain(`KAFKA_SASL_MECHANISM ${mechanism}`);
110
+ });
111
+ });
112
+ });
113
+
114
+ describe("generateAllConnections", () => {
115
+ it("generates all connections", () => {
116
+ const conn1 = createKafkaConnection("kafka1", {
117
+ bootstrapServers: "kafka1.example.com:9092",
118
+ });
119
+ const conn2 = createKafkaConnection("kafka2", {
120
+ bootstrapServers: "kafka2.example.com:9092",
121
+ });
122
+
123
+ const results = generateAllConnections({ kafka1: conn1, kafka2: conn2 });
124
+
125
+ expect(results).toHaveLength(2);
126
+ expect(results.map((r) => r.name).sort()).toEqual(["kafka1", "kafka2"]);
127
+ });
128
+
129
+ it("returns empty array for empty connections", () => {
130
+ const results = generateAllConnections({});
131
+
132
+ expect(results).toHaveLength(0);
133
+ });
134
+ });
135
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Connection content generator
3
+ * Converts ConnectionDefinition to native .connection file format
4
+ */
5
+
6
+ import type { ConnectionDefinition, KafkaConnectionDefinition } from "../schema/connection.js";
7
+
8
+ /**
9
+ * Generated connection content
10
+ */
11
+ export interface GeneratedConnection {
12
+ /** Connection name */
13
+ name: string;
14
+ /** The generated .connection file content */
15
+ content: string;
16
+ }
17
+
18
+ /**
19
+ * Generate a Kafka connection content
20
+ */
21
+ function generateKafkaConnection(connection: KafkaConnectionDefinition): string {
22
+ const parts: string[] = [];
23
+ const options = connection.options;
24
+
25
+ parts.push("TYPE kafka");
26
+ parts.push(`KAFKA_BOOTSTRAP_SERVERS ${options.bootstrapServers}`);
27
+
28
+ if (options.securityProtocol) {
29
+ parts.push(`KAFKA_SECURITY_PROTOCOL ${options.securityProtocol}`);
30
+ }
31
+
32
+ if (options.saslMechanism) {
33
+ parts.push(`KAFKA_SASL_MECHANISM ${options.saslMechanism}`);
34
+ }
35
+
36
+ if (options.key) {
37
+ parts.push(`KAFKA_KEY ${options.key}`);
38
+ }
39
+
40
+ if (options.secret) {
41
+ parts.push(`KAFKA_SECRET ${options.secret}`);
42
+ }
43
+
44
+ if (options.sslCaPem) {
45
+ parts.push(`KAFKA_SSL_CA_PEM ${options.sslCaPem}`);
46
+ }
47
+
48
+ return parts.join("\n");
49
+ }
50
+
51
+ /**
52
+ * Generate a .connection file content from a ConnectionDefinition
53
+ *
54
+ * @param connection - The connection definition
55
+ * @returns Generated connection content
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const myKafka = createKafkaConnection('my_kafka', {
60
+ * bootstrapServers: 'kafka.example.com:9092',
61
+ * securityProtocol: 'SASL_SSL',
62
+ * saslMechanism: 'PLAIN',
63
+ * key: '{{ tb_secret("KAFKA_KEY") }}',
64
+ * secret: '{{ tb_secret("KAFKA_SECRET") }}',
65
+ * });
66
+ *
67
+ * const { content } = generateConnection(myKafka);
68
+ * // Returns:
69
+ * // TYPE kafka
70
+ * // KAFKA_BOOTSTRAP_SERVERS kafka.example.com:9092
71
+ * // KAFKA_SECURITY_PROTOCOL SASL_SSL
72
+ * // KAFKA_SASL_MECHANISM PLAIN
73
+ * // KAFKA_KEY {{ tb_secret("KAFKA_KEY") }}
74
+ * // KAFKA_SECRET {{ tb_secret("KAFKA_SECRET") }}
75
+ * ```
76
+ */
77
+ export function generateConnection(
78
+ connection: ConnectionDefinition
79
+ ): GeneratedConnection {
80
+ let content: string;
81
+
82
+ if (connection._connectionType === "kafka") {
83
+ content = generateKafkaConnection(connection as KafkaConnectionDefinition);
84
+ } else {
85
+ throw new Error(`Unsupported connection type: ${connection._connectionType}`);
86
+ }
87
+
88
+ return {
89
+ name: connection._name,
90
+ content,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Generate .connection files for all connections in a project
96
+ *
97
+ * @param connections - Record of connection definitions
98
+ * @returns Array of generated connection content
99
+ */
100
+ export function generateAllConnections(
101
+ connections: Record<string, ConnectionDefinition>
102
+ ): GeneratedConnection[] {
103
+ return Object.values(connections).map(generateConnection);
104
+ }
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { generateDatasource, generateAllDatasources } from './datasource.js';
3
3
  import { defineDatasource } from '../schema/datasource.js';
4
+ import { createKafkaConnection } from '../schema/connection.js';
4
5
  import { t } from '../schema/types.js';
5
6
  import { engine } from '../schema/engines.js';
6
7
 
@@ -294,4 +295,111 @@ describe('Datasource Generator', () => {
294
295
  expect(result.content).toContain('ENGINE_TTL "timestamp + INTERVAL 90 DAY"');
295
296
  });
296
297
  });
298
+
299
+ describe('Kafka configuration', () => {
300
+ it('includes Kafka connection name and topic', () => {
301
+ const kafkaConn = createKafkaConnection('my_kafka', {
302
+ bootstrapServers: 'kafka.example.com:9092',
303
+ });
304
+
305
+ const ds = defineDatasource('kafka_events', {
306
+ schema: {
307
+ timestamp: t.dateTime(),
308
+ event: t.string(),
309
+ },
310
+ engine: engine.mergeTree({ sortingKey: ['timestamp'] }),
311
+ kafka: {
312
+ connection: kafkaConn,
313
+ topic: 'events',
314
+ },
315
+ });
316
+
317
+ const result = generateDatasource(ds);
318
+
319
+ expect(result.content).toContain('KAFKA_CONNECTION_NAME my_kafka');
320
+ expect(result.content).toContain('KAFKA_TOPIC events');
321
+ });
322
+
323
+ it('includes Kafka group ID when provided', () => {
324
+ const kafkaConn = createKafkaConnection('my_kafka', {
325
+ bootstrapServers: 'kafka.example.com:9092',
326
+ });
327
+
328
+ const ds = defineDatasource('kafka_events', {
329
+ schema: {
330
+ timestamp: t.dateTime(),
331
+ event: t.string(),
332
+ },
333
+ engine: engine.mergeTree({ sortingKey: ['timestamp'] }),
334
+ kafka: {
335
+ connection: kafkaConn,
336
+ topic: 'events',
337
+ groupId: 'my-consumer-group',
338
+ },
339
+ });
340
+
341
+ const result = generateDatasource(ds);
342
+
343
+ expect(result.content).toContain('KAFKA_GROUP_ID my-consumer-group');
344
+ });
345
+
346
+ it('includes auto offset reset when provided', () => {
347
+ const kafkaConn = createKafkaConnection('my_kafka', {
348
+ bootstrapServers: 'kafka.example.com:9092',
349
+ });
350
+
351
+ const ds = defineDatasource('kafka_events', {
352
+ schema: {
353
+ timestamp: t.dateTime(),
354
+ event: t.string(),
355
+ },
356
+ engine: engine.mergeTree({ sortingKey: ['timestamp'] }),
357
+ kafka: {
358
+ connection: kafkaConn,
359
+ topic: 'events',
360
+ autoOffsetReset: 'earliest',
361
+ },
362
+ });
363
+
364
+ const result = generateDatasource(ds);
365
+
366
+ expect(result.content).toContain('KAFKA_AUTO_OFFSET_RESET earliest');
367
+ });
368
+
369
+ it('generates complete Kafka datasource with all options', () => {
370
+ const kafkaConn = createKafkaConnection('my_kafka', {
371
+ bootstrapServers: 'kafka.example.com:9092',
372
+ securityProtocol: 'SASL_SSL',
373
+ saslMechanism: 'PLAIN',
374
+ });
375
+
376
+ const ds = defineDatasource('kafka_events', {
377
+ description: 'Events from Kafka',
378
+ schema: {
379
+ timestamp: t.dateTime(),
380
+ event_type: t.string(),
381
+ payload: t.string(),
382
+ },
383
+ engine: engine.mergeTree({ sortingKey: ['timestamp'] }),
384
+ kafka: {
385
+ connection: kafkaConn,
386
+ topic: 'events',
387
+ groupId: 'my-consumer-group',
388
+ autoOffsetReset: 'earliest',
389
+ },
390
+ });
391
+
392
+ const result = generateDatasource(ds);
393
+
394
+ expect(result.name).toBe('kafka_events');
395
+ expect(result.content).toContain('DESCRIPTION >');
396
+ expect(result.content).toContain('Events from Kafka');
397
+ expect(result.content).toContain('SCHEMA >');
398
+ expect(result.content).toContain('ENGINE "MergeTree"');
399
+ expect(result.content).toContain('KAFKA_CONNECTION_NAME my_kafka');
400
+ expect(result.content).toContain('KAFKA_TOPIC events');
401
+ expect(result.content).toContain('KAFKA_GROUP_ID my-consumer-group');
402
+ expect(result.content).toContain('KAFKA_AUTO_OFFSET_RESET earliest');
403
+ });
404
+ });
297
405
  });
@@ -3,7 +3,7 @@
3
3
  * Converts DatasourceDefinition to native .datasource file format
4
4
  */
5
5
 
6
- import type { DatasourceDefinition, SchemaDefinition, ColumnDefinition } from "../schema/datasource.js";
6
+ import type { DatasourceDefinition, SchemaDefinition, ColumnDefinition, KafkaConfig } from "../schema/datasource.js";
7
7
  import type { AnyTypeValidator, TypeModifiers } from "../schema/types.js";
8
8
  import { getColumnType, getColumnJsonPath } from "../schema/datasource.js";
9
9
  import { getEngineClause, type EngineConfig } from "../schema/engines.js";
@@ -143,6 +143,26 @@ function generateEngineConfig(engine?: EngineConfig): string {
143
143
  return getEngineClause(engine);
144
144
  }
145
145
 
146
+ /**
147
+ * Generate Kafka configuration lines
148
+ */
149
+ function generateKafkaConfig(kafka: KafkaConfig): string {
150
+ const parts: string[] = [];
151
+
152
+ parts.push(`KAFKA_CONNECTION_NAME ${kafka.connection._name}`);
153
+ parts.push(`KAFKA_TOPIC ${kafka.topic}`);
154
+
155
+ if (kafka.groupId) {
156
+ parts.push(`KAFKA_GROUP_ID ${kafka.groupId}`);
157
+ }
158
+
159
+ if (kafka.autoOffsetReset) {
160
+ parts.push(`KAFKA_AUTO_OFFSET_RESET ${kafka.autoOffsetReset}`);
161
+ }
162
+
163
+ return parts.join("\n");
164
+ }
165
+
146
166
  /**
147
167
  * Generate a .datasource file content from a DatasourceDefinition
148
168
  *
@@ -198,6 +218,12 @@ export function generateDatasource(
198
218
  // Add engine configuration
199
219
  parts.push(generateEngineConfig(datasource.options.engine));
200
220
 
221
+ // Add Kafka configuration if present
222
+ if (datasource.options.kafka) {
223
+ parts.push("");
224
+ parts.push(generateKafkaConfig(datasource.options.kafka));
225
+ }
226
+
201
227
  return {
202
228
  name: datasource._name,
203
229
  content: parts.join("\n"),