@tinybirdco/sdk 0.0.46 → 0.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +6 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +69 -1
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/login.d.ts.map +1 -1
- package/dist/cli/commands/login.js +3 -1
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/login.test.js +9 -3
- package/dist/cli/commands/login.test.js.map +1 -1
- package/dist/cli/commands/migrate.test.js +187 -7
- package/dist/cli/commands/migrate.test.js.map +1 -1
- package/dist/generator/connection.d.ts.map +1 -1
- package/dist/generator/connection.js +3 -0
- package/dist/generator/connection.js.map +1 -1
- package/dist/generator/connection.test.js +8 -0
- package/dist/generator/connection.test.js.map +1 -1
- package/dist/generator/datasource.d.ts.map +1 -1
- package/dist/generator/datasource.js +3 -0
- package/dist/generator/datasource.js.map +1 -1
- package/dist/generator/datasource.test.js +50 -0
- package/dist/generator/datasource.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +3 -0
- package/dist/index.test.js.map +1 -1
- package/dist/migrate/emit-ts.d.ts.map +1 -1
- package/dist/migrate/emit-ts.js +109 -32
- package/dist/migrate/emit-ts.js.map +1 -1
- package/dist/migrate/parse-connection.d.ts.map +1 -1
- package/dist/migrate/parse-connection.js +13 -2
- package/dist/migrate/parse-connection.js.map +1 -1
- package/dist/migrate/parse-datasource.d.ts.map +1 -1
- package/dist/migrate/parse-datasource.js +39 -4
- package/dist/migrate/parse-datasource.js.map +1 -1
- package/dist/migrate/parse-pipe.d.ts.map +1 -1
- package/dist/migrate/parse-pipe.js +3 -2
- package/dist/migrate/parse-pipe.js.map +1 -1
- package/dist/migrate/types.d.ts +3 -0
- package/dist/migrate/types.d.ts.map +1 -1
- package/dist/schema/connection.d.ts +2 -0
- package/dist/schema/connection.d.ts.map +1 -1
- package/dist/schema/connection.js.map +1 -1
- package/dist/schema/datasource.d.ts +3 -1
- package/dist/schema/datasource.d.ts.map +1 -1
- package/dist/schema/datasource.js +8 -1
- package/dist/schema/datasource.js.map +1 -1
- package/dist/schema/datasource.test.js +12 -0
- package/dist/schema/datasource.test.js.map +1 -1
- package/dist/schema/engines.d.ts.map +1 -1
- package/dist/schema/engines.js +3 -0
- package/dist/schema/engines.js.map +1 -1
- package/dist/schema/engines.test.js +16 -0
- package/dist/schema/engines.test.js.map +1 -1
- package/dist/schema/secret.d.ts +6 -0
- package/dist/schema/secret.d.ts.map +1 -0
- package/dist/schema/secret.js +14 -0
- package/dist/schema/secret.js.map +1 -0
- package/dist/schema/secret.test.d.ts +2 -0
- package/dist/schema/secret.test.d.ts.map +1 -0
- package/dist/schema/secret.test.js +14 -0
- package/dist/schema/secret.test.js.map +1 -0
- package/dist/schema/types.d.ts +5 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +6 -0
- package/dist/schema/types.js.map +1 -1
- package/dist/schema/types.test.js +12 -0
- package/dist/schema/types.test.js.map +1 -1
- package/extension/tinybird-ts-sdk-extension-0.1.0.vsix +0 -0
- package/package.json +3 -2
- package/src/cli/commands/init.test.ts +89 -1
- package/src/cli/commands/init.ts +10 -2
- package/src/cli/commands/login.test.ts +13 -3
- package/src/cli/commands/login.ts +3 -1
- package/src/cli/commands/migrate.test.ts +279 -7
- package/src/generator/connection.test.ts +13 -0
- package/src/generator/connection.ts +4 -0
- package/src/generator/datasource.test.ts +60 -0
- package/src/generator/datasource.ts +3 -0
- package/src/index.test.ts +4 -0
- package/src/index.ts +3 -0
- package/src/migrate/emit-ts.ts +109 -38
- package/src/migrate/parse-connection.ts +15 -2
- package/src/migrate/parse-datasource.ts +53 -4
- package/src/migrate/parse-pipe.ts +5 -3
- package/src/migrate/types.ts +3 -0
- package/src/schema/connection.ts +2 -0
- package/src/schema/datasource.test.ts +16 -0
- package/src/schema/datasource.ts +13 -2
- package/src/schema/engines.test.ts +18 -0
- package/src/schema/engines.ts +3 -0
- package/src/schema/secret.test.ts +19 -0
- package/src/schema/secret.ts +16 -0
- package/src/schema/types.test.ts +14 -0
- package/src/schema/types.ts +10 -0
package/src/index.ts
CHANGED
|
@@ -97,6 +97,9 @@ export type {
|
|
|
97
97
|
VersionedCollapsingMergeTreeConfig,
|
|
98
98
|
} from "./schema/engines.js";
|
|
99
99
|
|
|
100
|
+
// ============ Utilities ============
|
|
101
|
+
export { secret } from "./schema/secret.js";
|
|
102
|
+
|
|
100
103
|
// ============ Datasource ============
|
|
101
104
|
export { defineDatasource, isDatasourceDefinition, column, getColumnType, getColumnJsonPath, getColumnNames } from "./schema/datasource.js";
|
|
102
105
|
export type {
|
package/src/migrate/emit-ts.ts
CHANGED
|
@@ -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: ${
|
|
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
|
-
|
|
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: ${
|
|
304
|
+
lines.push(` topic: ${emitStringOrSecret(ds.kafka.topic)},`);
|
|
238
305
|
if (ds.kafka.groupId) {
|
|
239
|
-
lines.push(` groupId: ${
|
|
306
|
+
lines.push(` groupId: ${emitStringOrSecret(ds.kafka.groupId)},`);
|
|
240
307
|
}
|
|
241
308
|
if (ds.kafka.autoOffsetReset) {
|
|
242
|
-
lines.push(` 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: ${
|
|
321
|
+
lines.push(` bucketUri: ${emitStringOrSecret(ds.s3.bucketUri)},`);
|
|
252
322
|
if (ds.s3.schedule) {
|
|
253
|
-
lines.push(` schedule: ${
|
|
323
|
+
lines.push(` schedule: ${emitStringOrSecret(ds.s3.schedule)},`);
|
|
254
324
|
}
|
|
255
325
|
if (ds.s3.fromTimestamp) {
|
|
256
|
-
lines.push(` 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: ${
|
|
366
|
+
lines.push(` bootstrapServers: ${emitStringOrSecret(connection.bootstrapServers)},`);
|
|
297
367
|
if (connection.securityProtocol) {
|
|
298
|
-
lines.push(` securityProtocol: ${
|
|
368
|
+
lines.push(` securityProtocol: ${emitStringOrSecret(connection.securityProtocol)},`);
|
|
299
369
|
}
|
|
300
370
|
if (connection.saslMechanism) {
|
|
301
|
-
lines.push(` saslMechanism: ${
|
|
371
|
+
lines.push(` saslMechanism: ${emitStringOrSecret(connection.saslMechanism)},`);
|
|
302
372
|
}
|
|
303
373
|
if (connection.key) {
|
|
304
|
-
lines.push(` key: ${
|
|
374
|
+
lines.push(` key: ${emitStringOrSecret(connection.key)},`);
|
|
305
375
|
}
|
|
306
376
|
if (connection.secret) {
|
|
307
|
-
lines.push(` 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: ${
|
|
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: ${
|
|
393
|
+
lines.push(` region: ${emitStringOrSecret(connection.region)},`);
|
|
321
394
|
if (connection.arn) {
|
|
322
|
-
lines.push(` arn: ${
|
|
395
|
+
lines.push(` arn: ${emitStringOrSecret(connection.arn)},`);
|
|
323
396
|
}
|
|
324
397
|
if (connection.accessKey) {
|
|
325
|
-
lines.push(` accessKey: ${
|
|
398
|
+
lines.push(` accessKey: ${emitStringOrSecret(connection.accessKey)},`);
|
|
326
399
|
}
|
|
327
400
|
if (connection.secret) {
|
|
328
|
-
lines.push(` 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
|
-
"
|
|
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 (
|
|
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
|
|
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 ||
|
|
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 (
|
|
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
|
-
|
package/src/migrate/types.ts
CHANGED
|
@@ -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
|
|
package/src/schema/connection.ts
CHANGED
|
@@ -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", () => {
|
package/src/schema/datasource.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Define table schemas as TypeScript with full type safety
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type
|
|
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 (
|
|
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'],
|
package/src/schema/engines.ts
CHANGED
|
@@ -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
|
+
|
package/src/schema/types.test.ts
CHANGED
|
@@ -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());
|