@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.
- package/README.md +5 -4
- package/dist/api/build.d.ts +2 -0
- package/dist/api/build.d.ts.map +1 -1
- package/dist/api/build.js +13 -0
- package/dist/api/build.js.map +1 -1
- package/dist/api/build.test.js +1 -0
- package/dist/api/build.test.js.map +1 -1
- package/dist/api/deploy.d.ts.map +1 -1
- package/dist/api/deploy.js +3 -0
- package/dist/api/deploy.js.map +1 -1
- package/dist/api/deploy.test.js +1 -0
- package/dist/api/deploy.test.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +26 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +43 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/generator/connection.d.ts +49 -0
- package/dist/generator/connection.d.ts.map +1 -0
- package/dist/generator/connection.js +78 -0
- package/dist/generator/connection.js.map +1 -0
- package/dist/generator/connection.test.d.ts +2 -0
- package/dist/generator/connection.test.d.ts.map +1 -0
- package/dist/generator/connection.test.js +106 -0
- package/dist/generator/connection.test.js.map +1 -0
- package/dist/generator/datasource.d.ts.map +1 -1
- package/dist/generator/datasource.js +20 -0
- package/dist/generator/datasource.js.map +1 -1
- package/dist/generator/datasource.test.js +92 -0
- package/dist/generator/datasource.test.js.map +1 -1
- package/dist/generator/index.d.ts +8 -2
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +10 -3
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/loader.d.ts +8 -1
- package/dist/generator/loader.d.ts.map +1 -1
- package/dist/generator/loader.js +17 -2
- package/dist/generator/loader.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/schema/connection.d.ts +83 -0
- package/dist/schema/connection.d.ts.map +1 -0
- package/dist/schema/connection.js +61 -0
- package/dist/schema/connection.js.map +1 -0
- package/dist/schema/connection.test.d.ts +2 -0
- package/dist/schema/connection.test.d.ts.map +1 -0
- package/dist/schema/connection.test.js +117 -0
- package/dist/schema/connection.test.js.map +1 -0
- package/dist/schema/datasource.d.ts +16 -0
- package/dist/schema/datasource.d.ts.map +1 -1
- package/dist/schema/datasource.js.map +1 -1
- package/dist/schema/project.d.ts +12 -3
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/schema/project.js +2 -0
- package/dist/schema/project.js.map +1 -1
- package/package.json +2 -1
- package/src/api/build.test.ts +1 -0
- package/src/api/build.ts +20 -0
- package/src/api/deploy.test.ts +1 -0
- package/src/api/deploy.ts +3 -0
- package/src/cli/commands/init.test.ts +72 -0
- package/src/cli/commands/init.ts +30 -0
- package/src/cli/index.ts +6 -0
- package/src/generator/connection.test.ts +135 -0
- package/src/generator/connection.ts +104 -0
- package/src/generator/datasource.test.ts +108 -0
- package/src/generator/datasource.ts +27 -1
- package/src/generator/index.ts +16 -4
- package/src/generator/loader.ts +21 -3
- package/src/index.ts +12 -0
- package/src/schema/connection.test.ts +149 -0
- package/src/schema/connection.ts +123 -0
- package/src/schema/datasource.ts +17 -0
- 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 ?? [],
|
package/src/api/deploy.test.ts
CHANGED
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);
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -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"),
|