@tinybirdco/sdk 0.0.47 → 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 (82) hide show
  1. package/dist/cli/commands/migrate.test.js +187 -7
  2. package/dist/cli/commands/migrate.test.js.map +1 -1
  3. package/dist/generator/connection.d.ts.map +1 -1
  4. package/dist/generator/connection.js +3 -0
  5. package/dist/generator/connection.js.map +1 -1
  6. package/dist/generator/connection.test.js +8 -0
  7. package/dist/generator/connection.test.js.map +1 -1
  8. package/dist/generator/datasource.d.ts.map +1 -1
  9. package/dist/generator/datasource.js +3 -0
  10. package/dist/generator/datasource.js.map +1 -1
  11. package/dist/generator/datasource.test.js +50 -0
  12. package/dist/generator/datasource.test.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.test.js +3 -0
  18. package/dist/index.test.js.map +1 -1
  19. package/dist/migrate/emit-ts.d.ts.map +1 -1
  20. package/dist/migrate/emit-ts.js +109 -32
  21. package/dist/migrate/emit-ts.js.map +1 -1
  22. package/dist/migrate/parse-connection.d.ts.map +1 -1
  23. package/dist/migrate/parse-connection.js +13 -2
  24. package/dist/migrate/parse-connection.js.map +1 -1
  25. package/dist/migrate/parse-datasource.d.ts.map +1 -1
  26. package/dist/migrate/parse-datasource.js +39 -4
  27. package/dist/migrate/parse-datasource.js.map +1 -1
  28. package/dist/migrate/parse-pipe.d.ts.map +1 -1
  29. package/dist/migrate/parse-pipe.js +3 -2
  30. package/dist/migrate/parse-pipe.js.map +1 -1
  31. package/dist/migrate/types.d.ts +3 -0
  32. package/dist/migrate/types.d.ts.map +1 -1
  33. package/dist/schema/connection.d.ts +2 -0
  34. package/dist/schema/connection.d.ts.map +1 -1
  35. package/dist/schema/connection.js.map +1 -1
  36. package/dist/schema/datasource.d.ts +3 -1
  37. package/dist/schema/datasource.d.ts.map +1 -1
  38. package/dist/schema/datasource.js +8 -1
  39. package/dist/schema/datasource.js.map +1 -1
  40. package/dist/schema/datasource.test.js +12 -0
  41. package/dist/schema/datasource.test.js.map +1 -1
  42. package/dist/schema/engines.d.ts.map +1 -1
  43. package/dist/schema/engines.js +3 -0
  44. package/dist/schema/engines.js.map +1 -1
  45. package/dist/schema/engines.test.js +16 -0
  46. package/dist/schema/engines.test.js.map +1 -1
  47. package/dist/schema/secret.d.ts +6 -0
  48. package/dist/schema/secret.d.ts.map +1 -0
  49. package/dist/schema/secret.js +14 -0
  50. package/dist/schema/secret.js.map +1 -0
  51. package/dist/schema/secret.test.d.ts +2 -0
  52. package/dist/schema/secret.test.d.ts.map +1 -0
  53. package/dist/schema/secret.test.js +14 -0
  54. package/dist/schema/secret.test.js.map +1 -0
  55. package/dist/schema/types.d.ts +5 -0
  56. package/dist/schema/types.d.ts.map +1 -1
  57. package/dist/schema/types.js +6 -0
  58. package/dist/schema/types.js.map +1 -1
  59. package/dist/schema/types.test.js +12 -0
  60. package/dist/schema/types.test.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/cli/commands/migrate.test.ts +279 -7
  63. package/src/generator/connection.test.ts +13 -0
  64. package/src/generator/connection.ts +4 -0
  65. package/src/generator/datasource.test.ts +60 -0
  66. package/src/generator/datasource.ts +3 -0
  67. package/src/index.test.ts +4 -0
  68. package/src/index.ts +3 -0
  69. package/src/migrate/emit-ts.ts +109 -38
  70. package/src/migrate/parse-connection.ts +15 -2
  71. package/src/migrate/parse-datasource.ts +53 -4
  72. package/src/migrate/parse-pipe.ts +5 -3
  73. package/src/migrate/types.ts +3 -0
  74. package/src/schema/connection.ts +2 -0
  75. package/src/schema/datasource.test.ts +16 -0
  76. package/src/schema/datasource.ts +13 -2
  77. package/src/schema/engines.test.ts +18 -0
  78. package/src/schema/engines.ts +3 -0
  79. package/src/schema/secret.test.ts +19 -0
  80. package/src/schema/secret.ts +16 -0
  81. package/src/schema/types.test.ts +14 -0
  82. package/src/schema/types.ts +10 -0
@@ -13,6 +13,79 @@ function escapeString(value: string): string {
13
13
  return JSON.stringify(value);
14
14
  }
15
15
 
16
+ function emitObjectKey(key: string): string {
17
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : escapeString(key);
18
+ }
19
+
20
+ interface ParsedSecretTemplate {
21
+ name: string;
22
+ defaultValue?: string;
23
+ }
24
+
25
+ function parseTbSecretTemplate(value: string): ParsedSecretTemplate | null {
26
+ const trimmed = value.trim();
27
+ const regex =
28
+ /^\{\{\s*tb_secret\(\s*["']([^"']+)["'](?:\s*,\s*["']([^"']*)["'])?\s*\)\s*\}\}$/;
29
+ const match = trimmed.match(regex);
30
+ if (!match) {
31
+ return null;
32
+ }
33
+ return {
34
+ name: match[1] ?? "",
35
+ defaultValue: match[2],
36
+ };
37
+ }
38
+
39
+ function emitStringOrSecret(value: string): string {
40
+ const parsed = parseTbSecretTemplate(value);
41
+ if (!parsed) {
42
+ return escapeString(value);
43
+ }
44
+ if (parsed.defaultValue !== undefined) {
45
+ return `secret(${escapeString(parsed.name)}, ${escapeString(parsed.defaultValue)})`;
46
+ }
47
+ return `secret(${escapeString(parsed.name)})`;
48
+ }
49
+
50
+ function hasSecretTemplate(resources: ParsedResource[]): boolean {
51
+ const values: string[] = [];
52
+
53
+ for (const resource of resources) {
54
+ if (resource.kind === "connection") {
55
+ if (resource.connectionType === "kafka") {
56
+ values.push(resource.bootstrapServers);
57
+ if (resource.key) values.push(resource.key);
58
+ if (resource.secret) values.push(resource.secret);
59
+ if (resource.sslCaPem) values.push(resource.sslCaPem);
60
+ if (resource.schemaRegistryUrl) values.push(resource.schemaRegistryUrl);
61
+ } else {
62
+ values.push(resource.region);
63
+ if (resource.arn) values.push(resource.arn);
64
+ if (resource.accessKey) values.push(resource.accessKey);
65
+ if (resource.secret) values.push(resource.secret);
66
+ }
67
+ continue;
68
+ }
69
+
70
+ if (resource.kind === "datasource") {
71
+ if (resource.description) values.push(resource.description);
72
+ if (resource.kafka) {
73
+ values.push(resource.kafka.topic);
74
+ if (resource.kafka.groupId) values.push(resource.kafka.groupId);
75
+ if (resource.kafka.autoOffsetReset) values.push(resource.kafka.autoOffsetReset);
76
+ }
77
+ if (resource.s3) {
78
+ values.push(resource.s3.bucketUri);
79
+ if (resource.s3.schedule) values.push(resource.s3.schedule);
80
+ if (resource.s3.fromTimestamp) values.push(resource.s3.fromTimestamp);
81
+ }
82
+ continue;
83
+ }
84
+ }
85
+
86
+ return values.some((value) => parseTbSecretTemplate(value) !== null);
87
+ }
88
+
16
89
  function normalizedBaseType(type: string): string {
17
90
  let current = type.trim();
18
91
  let updated = true;
@@ -141,6 +214,9 @@ function emitEngineOptions(ds: DatasourceModel): string {
141
214
  if (engine.ver) {
142
215
  options.push(`ver: ${escapeString(engine.ver)}`);
143
216
  }
217
+ if (engine.isDeleted) {
218
+ options.push(`isDeleted: ${escapeString(engine.isDeleted)}`);
219
+ }
144
220
  if (engine.sign) {
145
221
  options.push(`sign: ${escapeString(engine.sign)}`);
146
222
  }
@@ -170,13 +246,6 @@ function emitDatasource(ds: DatasourceModel): string {
170
246
  const variableName = toCamelCase(ds.name);
171
247
  const lines: string[] = [];
172
248
  const hasJsonPath = ds.columns.some((column) => column.jsonPath !== undefined);
173
- const hasMissingJsonPath = ds.columns.some((column) => column.jsonPath === undefined);
174
-
175
- if (hasJsonPath && hasMissingJsonPath) {
176
- throw new Error(
177
- `Datasource "${ds.name}" has mixed json path usage. This is not representable in strict mode.`
178
- );
179
- }
180
249
 
181
250
  if (ds.description) {
182
251
  lines.push("/**");
@@ -188,7 +257,7 @@ function emitDatasource(ds: DatasourceModel): string {
188
257
 
189
258
  lines.push(`export const ${variableName} = defineDatasource(${escapeString(ds.name)}, {`);
190
259
  if (ds.description) {
191
- lines.push(` description: ${escapeString(ds.description)},`);
260
+ lines.push(` description: ${emitStringOrSecret(ds.description)},`);
192
261
  }
193
262
  if (!hasJsonPath) {
194
263
  lines.push(" jsonPaths: false,");
@@ -197,6 +266,7 @@ function emitDatasource(ds: DatasourceModel): string {
197
266
  lines.push(" schema: {");
198
267
  for (const column of ds.columns) {
199
268
  let validator = strictColumnTypeToValidator(column.type);
269
+ const columnKey = emitObjectKey(column.name);
200
270
 
201
271
  if (column.defaultExpression !== undefined) {
202
272
  const parsedDefault = parseLiteralFromDatafile(column.defaultExpression);
@@ -220,12 +290,9 @@ function emitDatasource(ds: DatasourceModel): string {
220
290
  }
221
291
 
222
292
  if (column.jsonPath) {
223
- lines.push(
224
- ` ${column.name}: column(${validator}, { jsonPath: ${escapeString(column.jsonPath)} }),`
225
- );
226
- } else {
227
- lines.push(` ${column.name}: ${validator},`);
293
+ validator += `.jsonPath(${escapeString(column.jsonPath)})`;
228
294
  }
295
+ lines.push(` ${columnKey}: ${validator},`);
229
296
  }
230
297
  lines.push(" },");
231
298
  lines.push(` engine: ${emitEngineOptions(ds)},`);
@@ -234,12 +301,15 @@ function emitDatasource(ds: DatasourceModel): string {
234
301
  const connectionVar = toCamelCase(ds.kafka.connectionName);
235
302
  lines.push(" kafka: {");
236
303
  lines.push(` connection: ${connectionVar},`);
237
- lines.push(` topic: ${escapeString(ds.kafka.topic)},`);
304
+ lines.push(` topic: ${emitStringOrSecret(ds.kafka.topic)},`);
238
305
  if (ds.kafka.groupId) {
239
- lines.push(` groupId: ${escapeString(ds.kafka.groupId)},`);
306
+ lines.push(` groupId: ${emitStringOrSecret(ds.kafka.groupId)},`);
240
307
  }
241
308
  if (ds.kafka.autoOffsetReset) {
242
- lines.push(` autoOffsetReset: ${escapeString(ds.kafka.autoOffsetReset)},`);
309
+ lines.push(` autoOffsetReset: ${emitStringOrSecret(ds.kafka.autoOffsetReset)},`);
310
+ }
311
+ if (ds.kafka.storeRawValue !== undefined) {
312
+ lines.push(` storeRawValue: ${ds.kafka.storeRawValue},`);
243
313
  }
244
314
  lines.push(" },");
245
315
  }
@@ -248,12 +318,12 @@ function emitDatasource(ds: DatasourceModel): string {
248
318
  const connectionVar = toCamelCase(ds.s3.connectionName);
249
319
  lines.push(" s3: {");
250
320
  lines.push(` connection: ${connectionVar},`);
251
- lines.push(` bucketUri: ${escapeString(ds.s3.bucketUri)},`);
321
+ lines.push(` bucketUri: ${emitStringOrSecret(ds.s3.bucketUri)},`);
252
322
  if (ds.s3.schedule) {
253
- lines.push(` schedule: ${escapeString(ds.s3.schedule)},`);
323
+ lines.push(` schedule: ${emitStringOrSecret(ds.s3.schedule)},`);
254
324
  }
255
325
  if (ds.s3.fromTimestamp) {
256
- lines.push(` fromTimestamp: ${escapeString(ds.s3.fromTimestamp)},`);
326
+ lines.push(` fromTimestamp: ${emitStringOrSecret(ds.s3.fromTimestamp)},`);
257
327
  }
258
328
  lines.push(" },");
259
329
  }
@@ -293,21 +363,24 @@ function emitConnection(connection: KafkaConnectionModel | S3ConnectionModel): s
293
363
  lines.push(
294
364
  `export const ${variableName} = defineKafkaConnection(${escapeString(connection.name)}, {`
295
365
  );
296
- lines.push(` bootstrapServers: ${escapeString(connection.bootstrapServers)},`);
366
+ lines.push(` bootstrapServers: ${emitStringOrSecret(connection.bootstrapServers)},`);
297
367
  if (connection.securityProtocol) {
298
- lines.push(` securityProtocol: ${escapeString(connection.securityProtocol)},`);
368
+ lines.push(` securityProtocol: ${emitStringOrSecret(connection.securityProtocol)},`);
299
369
  }
300
370
  if (connection.saslMechanism) {
301
- lines.push(` saslMechanism: ${escapeString(connection.saslMechanism)},`);
371
+ lines.push(` saslMechanism: ${emitStringOrSecret(connection.saslMechanism)},`);
302
372
  }
303
373
  if (connection.key) {
304
- lines.push(` key: ${escapeString(connection.key)},`);
374
+ lines.push(` key: ${emitStringOrSecret(connection.key)},`);
305
375
  }
306
376
  if (connection.secret) {
307
- lines.push(` secret: ${escapeString(connection.secret)},`);
377
+ lines.push(` secret: ${emitStringOrSecret(connection.secret)},`);
378
+ }
379
+ if (connection.schemaRegistryUrl) {
380
+ lines.push(` schemaRegistryUrl: ${emitStringOrSecret(connection.schemaRegistryUrl)},`);
308
381
  }
309
382
  if (connection.sslCaPem) {
310
- lines.push(` sslCaPem: ${escapeString(connection.sslCaPem)},`);
383
+ lines.push(` sslCaPem: ${emitStringOrSecret(connection.sslCaPem)},`);
311
384
  }
312
385
  lines.push("});");
313
386
  lines.push("");
@@ -317,15 +390,15 @@ function emitConnection(connection: KafkaConnectionModel | S3ConnectionModel): s
317
390
  lines.push(
318
391
  `export const ${variableName} = defineS3Connection(${escapeString(connection.name)}, {`
319
392
  );
320
- lines.push(` region: ${escapeString(connection.region)},`);
393
+ lines.push(` region: ${emitStringOrSecret(connection.region)},`);
321
394
  if (connection.arn) {
322
- lines.push(` arn: ${escapeString(connection.arn)},`);
395
+ lines.push(` arn: ${emitStringOrSecret(connection.arn)},`);
323
396
  }
324
397
  if (connection.accessKey) {
325
- lines.push(` accessKey: ${escapeString(connection.accessKey)},`);
398
+ lines.push(` accessKey: ${emitStringOrSecret(connection.accessKey)},`);
326
399
  }
327
400
  if (connection.secret) {
328
- lines.push(` secret: ${escapeString(connection.secret)},`);
401
+ lines.push(` secret: ${emitStringOrSecret(connection.secret)},`);
329
402
  }
330
403
  lines.push("});");
331
404
  lines.push("");
@@ -368,7 +441,7 @@ function emitPipe(pipe: PipeModel): string {
368
441
  param.required,
369
442
  param.defaultValue
370
443
  );
371
- lines.push(` ${param.name}: ${validator},`);
444
+ lines.push(` ${emitObjectKey(param.name)}: ${validator},`);
372
445
  }
373
446
  lines.push(" },");
374
447
  }
@@ -413,7 +486,7 @@ function emitPipe(pipe: PipeModel): string {
413
486
  }
414
487
  lines.push(" output: {");
415
488
  for (const columnName of endpointOutputColumns) {
416
- lines.push(` ${columnName}: t.string(),`);
489
+ lines.push(` ${emitObjectKey(columnName)}: t.string(),`);
417
490
  }
418
491
  lines.push(" },");
419
492
  }
@@ -443,10 +516,8 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
443
516
  (resource): resource is PipeModel => resource.kind === "pipe"
444
517
  );
445
518
 
446
- const needsColumn = datasources.some((ds) =>
447
- ds.columns.some((column) => column.jsonPath !== undefined)
448
- );
449
519
  const needsParams = pipes.some((pipe) => pipe.params.length > 0);
520
+ const needsSecret = hasSecretTemplate(resources);
450
521
 
451
522
  const imports = new Set<string>([
452
523
  "defineDatasource",
@@ -463,12 +534,12 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
463
534
  if (connections.some((connection) => connection.connectionType === "s3")) {
464
535
  imports.add("defineS3Connection");
465
536
  }
466
- if (needsColumn) {
467
- imports.add("column");
468
- }
469
537
  if (needsParams) {
470
538
  imports.add("p");
471
539
  }
540
+ if (needsSecret) {
541
+ imports.add("secret");
542
+ }
472
543
 
473
544
  const orderedImports = [
474
545
  "defineKafkaConnection",
@@ -480,7 +551,7 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
480
551
  "node",
481
552
  "t",
482
553
  "engine",
483
- "column",
554
+ "secret",
484
555
  "p",
485
556
  ].filter((name) => imports.has(name));
486
557
 
@@ -27,6 +27,7 @@ export function parseConnectionFile(
27
27
  | undefined;
28
28
  let key: string | undefined;
29
29
  let secret: string | undefined;
30
+ let schemaRegistryUrl: string | undefined;
30
31
  let sslCaPem: string | undefined;
31
32
 
32
33
  let region: string | undefined;
@@ -36,7 +37,7 @@ export function parseConnectionFile(
36
37
 
37
38
  for (const rawLine of lines) {
38
39
  const line = rawLine.trim();
39
- if (isBlank(line)) {
40
+ if (isBlank(line) || line.startsWith("#")) {
40
41
  continue;
41
42
  }
42
43
 
@@ -81,6 +82,9 @@ export function parseConnectionFile(
81
82
  case "KAFKA_SECRET":
82
83
  secret = value;
83
84
  break;
85
+ case "KAFKA_SCHEMA_REGISTRY_URL":
86
+ schemaRegistryUrl = value;
87
+ break;
84
88
  case "KAFKA_SSL_CA_PEM":
85
89
  sslCaPem = value;
86
90
  break;
@@ -144,12 +148,21 @@ export function parseConnectionFile(
144
148
  saslMechanism,
145
149
  key,
146
150
  secret,
151
+ schemaRegistryUrl,
147
152
  sslCaPem,
148
153
  };
149
154
  }
150
155
 
151
156
  if (connectionType === "s3") {
152
- if (bootstrapServers || securityProtocol || saslMechanism || key || secret || sslCaPem) {
157
+ if (
158
+ bootstrapServers ||
159
+ securityProtocol ||
160
+ saslMechanism ||
161
+ key ||
162
+ secret ||
163
+ schemaRegistryUrl ||
164
+ sslCaPem
165
+ ) {
153
166
  throw new MigrationParseError(
154
167
  resource.filePath,
155
168
  "connection",
@@ -95,9 +95,19 @@ function parseColumnLine(filePath: string, resourceName: string, rawLine: string
95
95
  );
96
96
  }
97
97
 
98
- const columnName = line.slice(0, firstSpace).trim();
98
+ const rawColumnName = line.slice(0, firstSpace).trim();
99
+ const columnName = normalizeColumnName(rawColumnName);
99
100
  let rest = line.slice(firstSpace + 1).trim();
100
101
 
102
+ if (!columnName) {
103
+ throw new MigrationParseError(
104
+ filePath,
105
+ "datasource",
106
+ resourceName,
107
+ `Invalid schema column name: "${rawLine}"`
108
+ );
109
+ }
110
+
101
111
  const codecMatch = rest.match(/\s+CODEC\((.+)\)\s*$/);
102
112
  const codec = codecMatch ? codecMatch[1].trim() : undefined;
103
113
  if (codecMatch?.index !== undefined) {
@@ -136,6 +146,17 @@ function parseColumnLine(filePath: string, resourceName: string, rawLine: string
136
146
  };
137
147
  }
138
148
 
149
+ function normalizeColumnName(value: string): string {
150
+ const trimmed = value.trim();
151
+ if (
152
+ (trimmed.startsWith("`") && trimmed.endsWith("`")) ||
153
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))
154
+ ) {
155
+ return trimmed.slice(1, -1);
156
+ }
157
+ return trimmed;
158
+ }
159
+
139
160
  function parseEngineSettings(value: string): Record<string, string | number | boolean> {
140
161
  const raw = parseQuotedValue(value);
141
162
  const parts = splitTopLevelComma(raw);
@@ -223,6 +244,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
223
244
  let primaryKey: string[] | undefined;
224
245
  let ttl: string | undefined;
225
246
  let ver: string | undefined;
247
+ let isDeleted: string | undefined;
226
248
  let sign: string | undefined;
227
249
  let version: string | undefined;
228
250
  let summingColumns: string[] | undefined;
@@ -232,6 +254,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
232
254
  let kafkaTopic: string | undefined;
233
255
  let kafkaGroupId: string | undefined;
234
256
  let kafkaAutoOffsetReset: "earliest" | "latest" | undefined;
257
+ let kafkaStoreRawValue: boolean | undefined;
235
258
 
236
259
  let importConnectionName: string | undefined;
237
260
  let importBucketUri: string | undefined;
@@ -242,7 +265,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
242
265
  while (i < lines.length) {
243
266
  const rawLine = lines[i] ?? "";
244
267
  const line = rawLine.trim();
245
- if (!line) {
268
+ if (!line || line.startsWith("#")) {
246
269
  i += 1;
247
270
  continue;
248
271
  }
@@ -273,7 +296,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
273
296
  );
274
297
  }
275
298
  for (const schemaLine of block.lines) {
276
- if (isBlank(schemaLine)) {
299
+ if (isBlank(schemaLine) || schemaLine.trim().startsWith("#")) {
277
300
  continue;
278
301
  }
279
302
  columns.push(parseColumnLine(resource.filePath, resource.name, schemaLine));
@@ -329,6 +352,9 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
329
352
  case "ENGINE_VER":
330
353
  ver = parseQuotedValue(value);
331
354
  break;
355
+ case "ENGINE_IS_DELETED":
356
+ isDeleted = parseQuotedValue(value);
357
+ break;
332
358
  case "ENGINE_SIGN":
333
359
  sign = parseQuotedValue(value);
334
360
  break;
@@ -370,6 +396,23 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
370
396
  }
371
397
  kafkaAutoOffsetReset = value;
372
398
  break;
399
+ case "KAFKA_STORE_RAW_VALUE": {
400
+ const normalized = value.toLowerCase();
401
+ if (normalized === "true" || normalized === "1") {
402
+ kafkaStoreRawValue = true;
403
+ break;
404
+ }
405
+ if (normalized === "false" || normalized === "0") {
406
+ kafkaStoreRawValue = false;
407
+ break;
408
+ }
409
+ throw new MigrationParseError(
410
+ resource.filePath,
411
+ "datasource",
412
+ resource.name,
413
+ `Invalid KAFKA_STORE_RAW_VALUE value: "${value}"`
414
+ );
415
+ }
373
416
  case "IMPORT_CONNECTION_NAME":
374
417
  importConnectionName = parseQuotedValue(value);
375
418
  break;
@@ -425,12 +468,17 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
425
468
  }
426
469
 
427
470
  const kafka =
428
- kafkaConnectionName || kafkaTopic || kafkaGroupId || kafkaAutoOffsetReset
471
+ kafkaConnectionName ||
472
+ kafkaTopic ||
473
+ kafkaGroupId ||
474
+ kafkaAutoOffsetReset ||
475
+ kafkaStoreRawValue !== undefined
429
476
  ? {
430
477
  connectionName: kafkaConnectionName ?? "",
431
478
  topic: kafkaTopic ?? "",
432
479
  groupId: kafkaGroupId,
433
480
  autoOffsetReset: kafkaAutoOffsetReset,
481
+ storeRawValue: kafkaStoreRawValue,
434
482
  }
435
483
  : undefined;
436
484
 
@@ -484,6 +532,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
484
532
  primaryKey,
485
533
  ttl,
486
534
  ver,
535
+ isDeleted,
487
536
  sign,
488
537
  version,
489
538
  summingColumns,
@@ -45,7 +45,10 @@ function readIndentedBlock(lines: string[], startIndex: number): BlockReadResult
45
45
 
46
46
  function nextNonBlank(lines: string[], startIndex: number): number {
47
47
  let i = startIndex;
48
- while (i < lines.length && isBlank(lines[i] ?? "")) {
48
+ while (
49
+ i < lines.length &&
50
+ (isBlank(lines[i] ?? "") || (lines[i] ?? "").trim().startsWith("#"))
51
+ ) {
49
52
  i += 1;
50
53
  }
51
54
  return i;
@@ -267,7 +270,7 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
267
270
  let i = 0;
268
271
  while (i < lines.length) {
269
272
  const line = (lines[i] ?? "").trim();
270
- if (!line) {
273
+ if (!line || line.startsWith("#")) {
271
274
  i += 1;
272
275
  continue;
273
276
  }
@@ -515,4 +518,3 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
515
518
  inferredOutputColumns,
516
519
  };
517
520
  }
518
-
@@ -30,6 +30,7 @@ export interface DatasourceEngineModel {
30
30
  primaryKey?: string[];
31
31
  ttl?: string;
32
32
  ver?: string;
33
+ isDeleted?: string;
33
34
  sign?: string;
34
35
  version?: string;
35
36
  summingColumns?: string[];
@@ -41,6 +42,7 @@ export interface DatasourceKafkaModel {
41
42
  topic: string;
42
43
  groupId?: string;
43
44
  autoOffsetReset?: "earliest" | "latest";
45
+ storeRawValue?: boolean;
44
46
  }
45
47
 
46
48
  export interface DatasourceS3Model {
@@ -117,6 +119,7 @@ export interface KafkaConnectionModel {
117
119
  saslMechanism?: "PLAIN" | "SCRAM-SHA-256" | "SCRAM-SHA-512" | "OAUTHBEARER";
118
120
  key?: string;
119
121
  secret?: string;
122
+ schemaRegistryUrl?: string;
120
123
  sslCaPem?: string;
121
124
  }
122
125
 
@@ -31,6 +31,8 @@ export interface KafkaConnectionOptions {
31
31
  key?: string;
32
32
  /** Kafka secret/password - can use {{ tb_secret(...) }} */
33
33
  secret?: string;
34
+ /** Schema Registry URL (optionally with embedded auth credentials) */
35
+ schemaRegistryUrl?: string;
34
36
  /** SSL CA certificate PEM - for private CA certs */
35
37
  sslCaPem?: string;
36
38
  }
@@ -169,6 +169,22 @@ describe("Datasource Schema", () => {
169
169
 
170
170
  expect(result).toBeUndefined();
171
171
  });
172
+
173
+ it("returns jsonPath from validator modifier", () => {
174
+ const validator = t.string().jsonPath("$.user.id");
175
+ const result = getColumnJsonPath(validator);
176
+
177
+ expect(result).toBe("$.user.id");
178
+ });
179
+
180
+ it("prefers column definition jsonPath over validator modifier", () => {
181
+ const col = column(t.string().jsonPath("$.from_validator"), {
182
+ jsonPath: "$.from_column",
183
+ });
184
+ const result = getColumnJsonPath(col);
185
+
186
+ expect(result).toBe("$.from_column");
187
+ });
172
188
  });
173
189
 
174
190
  describe("getColumnNames", () => {
@@ -3,7 +3,7 @@
3
3
  * Define table schemas as TypeScript with full type safety
4
4
  */
5
5
 
6
- import type { AnyTypeValidator } from "./types.js";
6
+ import { getModifiers, isTypeValidator, type AnyTypeValidator } from "./types.js";
7
7
  import type { EngineConfig } from "./engines.js";
8
8
  import type { KafkaConnectionDefinition, S3ConnectionDefinition } from "./connection.js";
9
9
  import type { TokenDefinition, DatasourceTokenScope } from "./token.js";
@@ -66,6 +66,8 @@ export interface KafkaConfig {
66
66
  groupId?: string;
67
67
  /** Where to start reading: 'earliest' or 'latest' (default: 'latest') */
68
68
  autoOffsetReset?: "earliest" | "latest";
69
+ /** Whether to store the raw Kafka value payload */
70
+ storeRawValue?: boolean;
69
71
  }
70
72
 
71
73
  /**
@@ -207,9 +209,18 @@ export function getColumnType(column: AnyTypeValidator | ColumnDefinition): AnyT
207
209
  * Get the JSON path for a column if defined
208
210
  */
209
211
  export function getColumnJsonPath(column: AnyTypeValidator | ColumnDefinition): string | undefined {
210
- if ("jsonPath" in column) {
212
+ if (isTypeValidator(column)) {
213
+ return getModifiers(column).jsonPath;
214
+ }
215
+
216
+ if (column.jsonPath !== undefined) {
211
217
  return column.jsonPath;
212
218
  }
219
+
220
+ if (isTypeValidator(column.type)) {
221
+ return getModifiers(column.type).jsonPath;
222
+ }
223
+
213
224
  return undefined;
214
225
  }
215
226
 
@@ -44,6 +44,14 @@ describe('Engine Configurations', () => {
44
44
  });
45
45
  expect(config.ver).toBe('updated_at');
46
46
  });
47
+
48
+ it('supports isDeleted column', () => {
49
+ const config = engine.replacingMergeTree({
50
+ sortingKey: ['id'],
51
+ isDeleted: '_is_deleted',
52
+ });
53
+ expect(config.isDeleted).toBe('_is_deleted');
54
+ });
47
55
  });
48
56
 
49
57
  describe('SummingMergeTree', () => {
@@ -138,6 +146,16 @@ describe('Engine Configurations', () => {
138
146
  expect(clause).toContain('ENGINE_VER "updated_at"');
139
147
  });
140
148
 
149
+ it('includes ReplacingMergeTree isDeleted column', () => {
150
+ const config = engine.replacingMergeTree({
151
+ sortingKey: ['id'],
152
+ isDeleted: '_is_deleted',
153
+ });
154
+ const clause = getEngineClause(config);
155
+ expect(clause).toContain('ENGINE "ReplacingMergeTree"');
156
+ expect(clause).toContain('ENGINE_IS_DELETED "_is_deleted"');
157
+ });
158
+
141
159
  it('includes SummingMergeTree columns', () => {
142
160
  const config = engine.summingMergeTree({
143
161
  sortingKey: ['date'],
@@ -241,6 +241,9 @@ export function getEngineClause(config: EngineConfig): string {
241
241
  if (config.type === "ReplacingMergeTree" && config.ver) {
242
242
  parts.push(`ENGINE_VER "${config.ver}"`);
243
243
  }
244
+ if (config.type === "ReplacingMergeTree" && config.isDeleted) {
245
+ parts.push(`ENGINE_IS_DELETED "${config.isDeleted}"`);
246
+ }
244
247
 
245
248
  if (config.type === "CollapsingMergeTree" || config.type === "VersionedCollapsingMergeTree") {
246
249
  parts.push(`ENGINE_SIGN "${config.sign}"`);
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { secret } from "./secret.js";
3
+
4
+ describe("secret helper", () => {
5
+ it("creates a secret template without default", () => {
6
+ expect(secret("KAFKA_KEY")).toBe('{{ tb_secret("KAFKA_KEY") }}');
7
+ });
8
+
9
+ it("creates a secret template with default", () => {
10
+ expect(secret("KAFKA_GROUP_ID", "events_group")).toBe(
11
+ '{{ tb_secret("KAFKA_GROUP_ID", "events_group") }}'
12
+ );
13
+ });
14
+
15
+ it("throws on empty secret name", () => {
16
+ expect(() => secret("")).toThrow("Secret name must be a non-empty string.");
17
+ });
18
+ });
19
+
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Secret template helper.
3
+ * Produces Tinybird-compatible `tb_secret(...)` template strings.
4
+ */
5
+ export function secret(name: string, defaultValue?: string): string {
6
+ if (!name || name.trim().length === 0) {
7
+ throw new Error("Secret name must be a non-empty string.");
8
+ }
9
+
10
+ if (defaultValue === undefined) {
11
+ return `{{ tb_secret("${name}") }}`;
12
+ }
13
+
14
+ return `{{ tb_secret("${name}", "${defaultValue}") }}`;
15
+ }
16
+
@@ -115,6 +115,20 @@ describe('Type Validators (t.*)', () => {
115
115
  });
116
116
  });
117
117
 
118
+ describe('jsonPath modifier', () => {
119
+ it('sets jsonPath in modifiers', () => {
120
+ const type = t.string().jsonPath('$.payload.id');
121
+ expect(type._modifiers.jsonPath).toBe('$.payload.id');
122
+ });
123
+
124
+ it('supports chaining with other modifiers', () => {
125
+ const type = t.string().nullable().jsonPath('$.user.name');
126
+ expect(type._tinybirdType).toBe('Nullable(String)');
127
+ expect(type._modifiers.nullable).toBe(true);
128
+ expect(type._modifiers.jsonPath).toBe('$.user.name');
129
+ });
130
+ });
131
+
118
132
  describe('Complex types', () => {
119
133
  it('generates Array type', () => {
120
134
  const type = t.array(t.string());