@tinybirdco/sdk 0.0.2 → 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 (67) hide show
  1. package/dist/api/build.d.ts +2 -0
  2. package/dist/api/build.d.ts.map +1 -1
  3. package/dist/api/build.js +13 -0
  4. package/dist/api/build.js.map +1 -1
  5. package/dist/api/build.test.js +1 -0
  6. package/dist/api/build.test.js.map +1 -1
  7. package/dist/api/deploy.d.ts.map +1 -1
  8. package/dist/api/deploy.js +3 -0
  9. package/dist/api/deploy.js.map +1 -1
  10. package/dist/api/deploy.test.js +1 -0
  11. package/dist/api/deploy.test.js.map +1 -1
  12. package/dist/generator/connection.d.ts +49 -0
  13. package/dist/generator/connection.d.ts.map +1 -0
  14. package/dist/generator/connection.js +78 -0
  15. package/dist/generator/connection.js.map +1 -0
  16. package/dist/generator/connection.test.d.ts +2 -0
  17. package/dist/generator/connection.test.d.ts.map +1 -0
  18. package/dist/generator/connection.test.js +106 -0
  19. package/dist/generator/connection.test.js.map +1 -0
  20. package/dist/generator/datasource.d.ts.map +1 -1
  21. package/dist/generator/datasource.js +20 -0
  22. package/dist/generator/datasource.js.map +1 -1
  23. package/dist/generator/datasource.test.js +92 -0
  24. package/dist/generator/datasource.test.js.map +1 -1
  25. package/dist/generator/index.d.ts +8 -2
  26. package/dist/generator/index.d.ts.map +1 -1
  27. package/dist/generator/index.js +10 -3
  28. package/dist/generator/index.js.map +1 -1
  29. package/dist/generator/loader.d.ts +8 -1
  30. package/dist/generator/loader.d.ts.map +1 -1
  31. package/dist/generator/loader.js +17 -2
  32. package/dist/generator/loader.js.map +1 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/schema/connection.d.ts +83 -0
  38. package/dist/schema/connection.d.ts.map +1 -0
  39. package/dist/schema/connection.js +61 -0
  40. package/dist/schema/connection.js.map +1 -0
  41. package/dist/schema/connection.test.d.ts +2 -0
  42. package/dist/schema/connection.test.d.ts.map +1 -0
  43. package/dist/schema/connection.test.js +117 -0
  44. package/dist/schema/connection.test.js.map +1 -0
  45. package/dist/schema/datasource.d.ts +16 -0
  46. package/dist/schema/datasource.d.ts.map +1 -1
  47. package/dist/schema/datasource.js.map +1 -1
  48. package/dist/schema/project.d.ts +12 -3
  49. package/dist/schema/project.d.ts.map +1 -1
  50. package/dist/schema/project.js +2 -0
  51. package/dist/schema/project.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/api/build.test.ts +1 -0
  54. package/src/api/build.ts +20 -0
  55. package/src/api/deploy.test.ts +1 -0
  56. package/src/api/deploy.ts +3 -0
  57. package/src/generator/connection.test.ts +135 -0
  58. package/src/generator/connection.ts +104 -0
  59. package/src/generator/datasource.test.ts +108 -0
  60. package/src/generator/datasource.ts +27 -1
  61. package/src/generator/index.ts +16 -4
  62. package/src/generator/loader.ts +21 -3
  63. package/src/index.ts +12 -0
  64. package/src/schema/connection.test.ts +149 -0
  65. package/src/schema/connection.ts +123 -0
  66. package/src/schema/datasource.ts +17 -0
  67. package/src/schema/project.ts +20 -5
@@ -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"),
@@ -6,7 +6,8 @@
6
6
  import { loadSchema, loadEntities, entitiesToProject, type LoadedEntities } from "./loader.js";
7
7
  import { generateAllDatasources, type GeneratedDatasource } from "./datasource.js";
8
8
  import { generateAllPipes, type GeneratedPipe } from "./pipe.js";
9
- import type { ProjectDefinition, DatasourcesDefinition, PipesDefinition } from "../schema/project.js";
9
+ import { generateAllConnections, type GeneratedConnection } from "./connection.js";
10
+ import type { ProjectDefinition, DatasourcesDefinition, PipesDefinition, ConnectionsDefinition } from "../schema/project.js";
10
11
 
11
12
  /**
12
13
  * Generated resources ready for API push
@@ -16,6 +17,8 @@ export interface GeneratedResources {
16
17
  datasources: GeneratedDatasource[];
17
18
  /** Generated pipe files */
18
19
  pipes: GeneratedPipe[];
20
+ /** Generated connection files */
21
+ connections: GeneratedConnection[];
19
22
  }
20
23
 
21
24
  /**
@@ -34,6 +37,7 @@ export interface BuildResult {
34
37
  stats: {
35
38
  datasourceCount: number;
36
39
  pipeCount: number;
40
+ connectionCount: number;
37
41
  };
38
42
  }
39
43
 
@@ -46,10 +50,12 @@ export interface BuildResult {
46
50
  export function generateResources(project: ProjectDefinition): GeneratedResources {
47
51
  const datasources = generateAllDatasources(project.datasources);
48
52
  const pipes = generateAllPipes(project.pipes);
53
+ const connections = generateAllConnections(project.connections);
49
54
 
50
55
  return {
51
56
  datasources,
52
57
  pipes,
58
+ connections,
53
59
  };
54
60
  }
55
61
 
@@ -107,6 +113,7 @@ export async function build(options: BuildOptions): Promise<BuildResult> {
107
113
  stats: {
108
114
  datasourceCount: resources.datasources.length,
109
115
  pipeCount: resources.pipes.length,
116
+ connectionCount: resources.connections.length,
110
117
  },
111
118
  };
112
119
  }
@@ -133,6 +140,7 @@ export interface BuildFromIncludeResult {
133
140
  stats: {
134
141
  datasourceCount: number;
135
142
  pipeCount: number;
143
+ connectionCount: number;
136
144
  };
137
145
  }
138
146
 
@@ -141,11 +149,13 @@ export interface BuildFromIncludeResult {
141
149
  */
142
150
  export function generateResourcesFromEntities(
143
151
  datasources: DatasourcesDefinition,
144
- pipes: PipesDefinition
152
+ pipes: PipesDefinition,
153
+ connections: ConnectionsDefinition = {}
145
154
  ): GeneratedResources {
146
155
  return {
147
156
  datasources: generateAllDatasources(datasources),
148
157
  pipes: generateAllPipes(pipes),
158
+ connections: generateAllConnections(connections),
149
159
  };
150
160
  }
151
161
 
@@ -181,10 +191,10 @@ export async function buildFromInclude(
181
191
  });
182
192
 
183
193
  // Convert to format for generators
184
- const { datasources, pipes } = entitiesToProject(entities);
194
+ const { datasources, pipes, connections } = entitiesToProject(entities);
185
195
 
186
196
  // Generate resources
187
- const resources = generateResourcesFromEntities(datasources, pipes);
197
+ const resources = generateResourcesFromEntities(datasources, pipes, connections);
188
198
 
189
199
  return {
190
200
  resources,
@@ -192,6 +202,7 @@ export async function buildFromInclude(
192
202
  stats: {
193
203
  datasourceCount: resources.datasources.length,
194
204
  pipeCount: resources.pipes.length,
205
+ connectionCount: resources.connections.length,
195
206
  },
196
207
  };
197
208
  }
@@ -200,4 +211,5 @@ export async function buildFromInclude(
200
211
  export { loadSchema, loadEntities, entitiesToProject, type LoaderOptions, type LoadedSchema, type LoadedEntities, type LoadEntitiesOptions } from "./loader.js";
201
212
  export { generateDatasource, generateAllDatasources, type GeneratedDatasource } from "./datasource.js";
202
213
  export { generatePipe, generateAllPipes, type GeneratedPipe } from "./pipe.js";
214
+ export { generateConnection, generateAllConnections, type GeneratedConnection } from "./connection.js";
203
215
  export { generateClientFile, type GenerateClientOptions, type GeneratedClient } from "./client.js";
@@ -7,9 +7,10 @@ import * as esbuild from "esbuild";
7
7
  import * as path from "path";
8
8
  import * as fs from "fs";
9
9
  import { watch as chokidarWatch, type FSWatcher } from "chokidar";
10
- import { isProjectDefinition, type ProjectDefinition, type DatasourcesDefinition, type PipesDefinition } from "../schema/project.js";
10
+ import { isProjectDefinition, type ProjectDefinition, type DatasourcesDefinition, type PipesDefinition, type ConnectionsDefinition } from "../schema/project.js";
11
11
  import { isDatasourceDefinition, type DatasourceDefinition } from "../schema/datasource.js";
12
12
  import { isPipeDefinition, type PipeDefinition } from "../schema/pipe.js";
13
+ import { isConnectionDefinition, type ConnectionDefinition } from "../schema/connection.js";
13
14
 
14
15
  /**
15
16
  * Result of loading a schema file
@@ -157,6 +158,8 @@ export interface LoadedEntities {
157
158
  datasources: Record<string, { definition: DatasourceDefinition; info: EntityInfo }>;
158
159
  /** Discovered pipes with their metadata */
159
160
  pipes: Record<string, { definition: PipeDefinition; info: EntityInfo }>;
161
+ /** Discovered connections with their metadata */
162
+ connections: Record<string, { definition: ConnectionDefinition; info: EntityInfo }>;
160
163
  /** All source files that were scanned */
161
164
  sourceFiles: string[];
162
165
  }
@@ -195,6 +198,7 @@ export async function loadEntities(options: LoadEntitiesOptions): Promise<Loaded
195
198
  const result: LoadedEntities = {
196
199
  datasources: {},
197
200
  pipes: {},
201
+ connections: {},
198
202
  sourceFiles: [],
199
203
  };
200
204
 
@@ -237,7 +241,7 @@ export async function loadEntities(options: LoadEntitiesOptions): Promise<Loaded
237
241
  const moduleUrl = `file://${outfile}`;
238
242
  const module = await import(moduleUrl);
239
243
 
240
- // Scan all exports for datasources and pipes
244
+ // Scan all exports for datasources, pipes, and connections
241
245
  for (const [exportName, value] of Object.entries(module)) {
242
246
  if (isDatasourceDefinition(value)) {
243
247
  result.datasources[exportName] = {
@@ -255,6 +259,14 @@ export async function loadEntities(options: LoadEntitiesOptions): Promise<Loaded
255
259
  sourceFile: includePath,
256
260
  },
257
261
  };
262
+ } else if (isConnectionDefinition(value)) {
263
+ result.connections[exportName] = {
264
+ definition: value,
265
+ info: {
266
+ exportName,
267
+ sourceFile: includePath,
268
+ },
269
+ };
258
270
  }
259
271
  }
260
272
  } finally {
@@ -282,9 +294,11 @@ export async function loadEntities(options: LoadEntitiesOptions): Promise<Loaded
282
294
  export function entitiesToProject(entities: LoadedEntities): {
283
295
  datasources: DatasourcesDefinition;
284
296
  pipes: PipesDefinition;
297
+ connections: ConnectionsDefinition;
285
298
  } {
286
299
  const datasources: DatasourcesDefinition = {};
287
300
  const pipes: PipesDefinition = {};
301
+ const connections: ConnectionsDefinition = {};
288
302
 
289
303
  for (const [name, { definition }] of Object.entries(entities.datasources)) {
290
304
  datasources[name] = definition;
@@ -294,7 +308,11 @@ export function entitiesToProject(entities: LoadedEntities): {
294
308
  pipes[name] = definition;
295
309
  }
296
310
 
297
- return { datasources, pipes };
311
+ for (const [name, { definition }] of Object.entries(entities.connections)) {
312
+ connections[name] = definition;
313
+ }
314
+
315
+ return { datasources, pipes, connections };
298
316
  }
299
317
 
300
318
  /**
package/src/index.ts CHANGED
@@ -106,8 +106,19 @@ export type {
106
106
  ColumnDefinition,
107
107
  TokenConfig,
108
108
  ExtractSchema,
109
+ KafkaConfig,
109
110
  } from "./schema/datasource.js";
110
111
 
112
+ // ============ Connection ============
113
+ export { createKafkaConnection, isConnectionDefinition, isKafkaConnectionDefinition, getConnectionType } from "./schema/connection.js";
114
+ export type {
115
+ ConnectionDefinition,
116
+ KafkaConnectionDefinition,
117
+ KafkaConnectionOptions,
118
+ KafkaSecurityProtocol,
119
+ KafkaSaslMechanism,
120
+ } from "./schema/connection.js";
121
+
111
122
  // ============ Pipe ============
112
123
  export {
113
124
  definePipe,
@@ -153,6 +164,7 @@ export type {
153
164
  TinybirdClientConfig,
154
165
  DatasourcesDefinition,
155
166
  PipesDefinition,
167
+ ConnectionsDefinition,
156
168
  ExtractDatasources,
157
169
  ExtractPipes,
158
170
  DataModel,
@@ -0,0 +1,149 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ createKafkaConnection,
4
+ isConnectionDefinition,
5
+ isKafkaConnectionDefinition,
6
+ getConnectionType,
7
+ } from "./connection.js";
8
+
9
+ describe("Connection Schema", () => {
10
+ describe("createKafkaConnection", () => {
11
+ it("creates a Kafka connection with required fields", () => {
12
+ const conn = createKafkaConnection("my_kafka", {
13
+ bootstrapServers: "kafka.example.com:9092",
14
+ });
15
+
16
+ expect(conn._name).toBe("my_kafka");
17
+ expect(conn._type).toBe("connection");
18
+ expect(conn._connectionType).toBe("kafka");
19
+ expect(conn.options.bootstrapServers).toBe("kafka.example.com:9092");
20
+ });
21
+
22
+ it("creates a Kafka connection with all options", () => {
23
+ const conn = createKafkaConnection("my_kafka", {
24
+ bootstrapServers: "kafka.example.com:9092",
25
+ securityProtocol: "SASL_SSL",
26
+ saslMechanism: "PLAIN",
27
+ key: '{{ tb_secret("KAFKA_KEY") }}',
28
+ secret: '{{ tb_secret("KAFKA_SECRET") }}',
29
+ sslCaPem: '{{ tb_secret("KAFKA_CA_CERT") }}',
30
+ });
31
+
32
+ expect(conn.options.securityProtocol).toBe("SASL_SSL");
33
+ expect(conn.options.saslMechanism).toBe("PLAIN");
34
+ expect(conn.options.key).toBe('{{ tb_secret("KAFKA_KEY") }}');
35
+ expect(conn.options.secret).toBe('{{ tb_secret("KAFKA_SECRET") }}');
36
+ expect(conn.options.sslCaPem).toBe('{{ tb_secret("KAFKA_CA_CERT") }}');
37
+ });
38
+
39
+ it("supports different SASL mechanisms", () => {
40
+ const scramConn = createKafkaConnection("scram_kafka", {
41
+ bootstrapServers: "kafka.example.com:9092",
42
+ saslMechanism: "SCRAM-SHA-256",
43
+ });
44
+ expect(scramConn.options.saslMechanism).toBe("SCRAM-SHA-256");
45
+
46
+ const scram512Conn = createKafkaConnection("scram512_kafka", {
47
+ bootstrapServers: "kafka.example.com:9092",
48
+ saslMechanism: "SCRAM-SHA-512",
49
+ });
50
+ expect(scram512Conn.options.saslMechanism).toBe("SCRAM-SHA-512");
51
+
52
+ const oauthConn = createKafkaConnection("oauth_kafka", {
53
+ bootstrapServers: "kafka.example.com:9092",
54
+ saslMechanism: "OAUTHBEARER",
55
+ });
56
+ expect(oauthConn.options.saslMechanism).toBe("OAUTHBEARER");
57
+ });
58
+
59
+ it("supports different security protocols", () => {
60
+ const plaintext = createKafkaConnection("plaintext_kafka", {
61
+ bootstrapServers: "localhost:9092",
62
+ securityProtocol: "PLAINTEXT",
63
+ });
64
+ expect(plaintext.options.securityProtocol).toBe("PLAINTEXT");
65
+
66
+ const saslPlaintext = createKafkaConnection("sasl_plaintext_kafka", {
67
+ bootstrapServers: "localhost:9092",
68
+ securityProtocol: "SASL_PLAINTEXT",
69
+ });
70
+ expect(saslPlaintext.options.securityProtocol).toBe("SASL_PLAINTEXT");
71
+ });
72
+
73
+ it("throws error for invalid connection name", () => {
74
+ expect(() =>
75
+ createKafkaConnection("123invalid", {
76
+ bootstrapServers: "kafka.example.com:9092",
77
+ })
78
+ ).toThrow("Invalid connection name");
79
+
80
+ expect(() =>
81
+ createKafkaConnection("my-connection", {
82
+ bootstrapServers: "kafka.example.com:9092",
83
+ })
84
+ ).toThrow("Invalid connection name");
85
+
86
+ expect(() =>
87
+ createKafkaConnection("", {
88
+ bootstrapServers: "kafka.example.com:9092",
89
+ })
90
+ ).toThrow("Invalid connection name");
91
+ });
92
+
93
+ it("allows valid naming patterns", () => {
94
+ const conn1 = createKafkaConnection("_private_kafka", {
95
+ bootstrapServers: "kafka.example.com:9092",
96
+ });
97
+ expect(conn1._name).toBe("_private_kafka");
98
+
99
+ const conn2 = createKafkaConnection("kafka_v2", {
100
+ bootstrapServers: "kafka.example.com:9092",
101
+ });
102
+ expect(conn2._name).toBe("kafka_v2");
103
+ });
104
+ });
105
+
106
+ describe("isConnectionDefinition", () => {
107
+ it("returns true for valid connection", () => {
108
+ const conn = createKafkaConnection("my_kafka", {
109
+ bootstrapServers: "kafka.example.com:9092",
110
+ });
111
+
112
+ expect(isConnectionDefinition(conn)).toBe(true);
113
+ });
114
+
115
+ it("returns false for non-connection objects", () => {
116
+ expect(isConnectionDefinition({})).toBe(false);
117
+ expect(isConnectionDefinition(null)).toBe(false);
118
+ expect(isConnectionDefinition(undefined)).toBe(false);
119
+ expect(isConnectionDefinition("string")).toBe(false);
120
+ expect(isConnectionDefinition(123)).toBe(false);
121
+ expect(isConnectionDefinition({ _name: "test" })).toBe(false);
122
+ });
123
+ });
124
+
125
+ describe("isKafkaConnectionDefinition", () => {
126
+ it("returns true for Kafka connection", () => {
127
+ const conn = createKafkaConnection("my_kafka", {
128
+ bootstrapServers: "kafka.example.com:9092",
129
+ });
130
+
131
+ expect(isKafkaConnectionDefinition(conn)).toBe(true);
132
+ });
133
+
134
+ it("returns false for non-Kafka objects", () => {
135
+ expect(isKafkaConnectionDefinition({})).toBe(false);
136
+ expect(isKafkaConnectionDefinition(null)).toBe(false);
137
+ });
138
+ });
139
+
140
+ describe("getConnectionType", () => {
141
+ it("returns the connection type", () => {
142
+ const conn = createKafkaConnection("my_kafka", {
143
+ bootstrapServers: "kafka.example.com:9092",
144
+ });
145
+
146
+ expect(getConnectionType(conn)).toBe("kafka");
147
+ });
148
+ });
149
+ });