@tinybirdco/sdk 0.0.59 → 0.0.61

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 (40) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/commands/migrate.test.js +21 -0
  3. package/dist/cli/commands/migrate.test.js.map +1 -1
  4. package/dist/generator/connection.d.ts.map +1 -1
  5. package/dist/generator/connection.js +10 -1
  6. package/dist/generator/connection.js.map +1 -1
  7. package/dist/generator/connection.test.js +19 -0
  8. package/dist/generator/connection.test.js.map +1 -1
  9. package/dist/generator/datasource.d.ts.map +1 -1
  10. package/dist/generator/datasource.js +9 -4
  11. package/dist/generator/datasource.js.map +1 -1
  12. package/dist/generator/datasource.test.js +9 -0
  13. package/dist/generator/datasource.test.js.map +1 -1
  14. package/dist/migrate/emit-ts.d.ts.map +1 -1
  15. package/dist/migrate/emit-ts.js +14 -9
  16. package/dist/migrate/emit-ts.js.map +1 -1
  17. package/dist/migrate/parse-connection.d.ts.map +1 -1
  18. package/dist/migrate/parse-connection.js +41 -2
  19. package/dist/migrate/parse-connection.js.map +1 -1
  20. package/dist/migrate/parse-connection.test.d.ts +2 -0
  21. package/dist/migrate/parse-connection.test.d.ts.map +1 -0
  22. package/dist/migrate/parse-connection.test.js +56 -0
  23. package/dist/migrate/parse-connection.test.js.map +1 -0
  24. package/dist/schema/types.d.ts +6 -0
  25. package/dist/schema/types.d.ts.map +1 -1
  26. package/dist/schema/types.js +13 -0
  27. package/dist/schema/types.js.map +1 -1
  28. package/dist/schema/types.test.js +13 -0
  29. package/dist/schema/types.test.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/cli/commands/migrate.test.ts +30 -0
  32. package/src/generator/connection.test.ts +25 -0
  33. package/src/generator/connection.ts +9 -1
  34. package/src/generator/datasource.test.ts +11 -0
  35. package/src/generator/datasource.ts +8 -4
  36. package/src/migrate/emit-ts.ts +16 -12
  37. package/src/migrate/parse-connection.test.ts +98 -0
  38. package/src/migrate/parse-connection.ts +42 -1
  39. package/src/schema/types.test.ts +18 -0
  40. package/src/schema/types.ts +27 -0
@@ -288,20 +288,24 @@ function emitDatasource(ds: DatasourceModel): string {
288
288
  const columnKey = emitObjectKey(column.name);
289
289
 
290
290
  if (column.defaultExpression !== undefined) {
291
- const parsedDefault = parseLiteralFromDatafile(column.defaultExpression);
292
- let literalValue = parsedDefault;
293
- if (typeof parsedDefault === "number" && isBooleanType(column.type)) {
294
- if (parsedDefault === 0 || parsedDefault === 1) {
295
- literalValue = parsedDefault === 1;
296
- } else {
297
- throw new Error(
298
- `Boolean default value must be 0 or 1 for column "${column.name}" in datasource "${ds.name}".`
299
- );
291
+ try {
292
+ const parsedDefault = parseLiteralFromDatafile(column.defaultExpression);
293
+ let literalValue = parsedDefault;
294
+ if (typeof parsedDefault === "number" && isBooleanType(column.type)) {
295
+ if (parsedDefault === 0 || parsedDefault === 1) {
296
+ literalValue = parsedDefault === 1;
297
+ } else {
298
+ throw new Error(
299
+ `Boolean default value must be 0 or 1 for column "${column.name}" in datasource "${ds.name}".`
300
+ );
301
+ }
300
302
  }
303
+ validator += `.default(${toTsLiteral(
304
+ literalValue as string | number | boolean | null | Record<string, unknown> | unknown[]
305
+ )})`;
306
+ } catch {
307
+ validator += `.defaultExpr(${escapeString(column.defaultExpression)})`;
301
308
  }
302
- validator += `.default(${toTsLiteral(
303
- literalValue as string | number | boolean | null | Record<string, unknown> | unknown[]
304
- )})`;
305
309
  }
306
310
 
307
311
  if (column.codec) {
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseConnectionFile } from "./parse-connection.js";
3
+ import type { ResourceFile } from "./types.js";
4
+
5
+ function resource(name: string, content: string): ResourceFile {
6
+ return {
7
+ kind: "connection",
8
+ name,
9
+ filePath: `${name}.connection`,
10
+ absolutePath: `/tmp/${name}.connection`,
11
+ content,
12
+ };
13
+ }
14
+
15
+ describe("parseConnectionFile", () => {
16
+ it("parses basic Kafka connection", () => {
17
+ const result = parseConnectionFile(
18
+ resource(
19
+ "my_kafka",
20
+ `TYPE kafka
21
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092`
22
+ )
23
+ );
24
+
25
+ expect(result.connectionType).toBe("kafka");
26
+ expect(result).toHaveProperty("bootstrapServers", "localhost:9092");
27
+ });
28
+
29
+ it("parses single-quoted TYPE", () => {
30
+ const result = parseConnectionFile(
31
+ resource(
32
+ "my_kafka",
33
+ `TYPE 'kafka'
34
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
35
+ KAFKA_SCHEMA_REGISTRY_URL https://registry.example.com`
36
+ )
37
+ );
38
+
39
+ expect(result.connectionType).toBe("kafka");
40
+ expect(result).toHaveProperty("schemaRegistryUrl", "https://registry.example.com");
41
+ });
42
+
43
+ it("parses multiline SSL CA PEM with > syntax", () => {
44
+ const result = parseConnectionFile(
45
+ resource(
46
+ "my_kafka",
47
+ `TYPE kafka
48
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
49
+ KAFKA_SECURITY_PROTOCOL SASL_SSL
50
+ KAFKA_SSL_CA_PEM >
51
+ -----BEGIN CERTIFICATE-----
52
+ MIIDXTCCAkWgAwIBAgIJAM
53
+ -----END CERTIFICATE-----`
54
+ )
55
+ );
56
+
57
+ expect(result).toHaveProperty(
58
+ "sslCaPem",
59
+ "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAM\n-----END CERTIFICATE-----"
60
+ );
61
+ });
62
+
63
+ it("parses multiline SSL CA PEM with directives after the block", () => {
64
+ const result = parseConnectionFile(
65
+ resource(
66
+ "my_kafka",
67
+ `TYPE kafka
68
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
69
+ KAFKA_SSL_CA_PEM >
70
+ -----BEGIN CERTIFICATE-----
71
+ MIIDXTCCAkWgAwIBAgIJAM
72
+ -----END CERTIFICATE-----
73
+ KAFKA_SECURITY_PROTOCOL SASL_SSL
74
+ KAFKA_KEY mykey`
75
+ )
76
+ );
77
+
78
+ expect(result).toHaveProperty(
79
+ "sslCaPem",
80
+ "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAM\n-----END CERTIFICATE-----"
81
+ );
82
+ expect(result).toHaveProperty("securityProtocol", "SASL_SSL");
83
+ expect(result).toHaveProperty("key", "mykey");
84
+ });
85
+
86
+ it("parses single-line SSL CA PEM (e.g. a secret reference)", () => {
87
+ const result = parseConnectionFile(
88
+ resource(
89
+ "my_kafka",
90
+ `TYPE kafka
91
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
92
+ KAFKA_SSL_CA_PEM {{ tb_secret('KAFKA_SSL_CA_PEM') }}`
93
+ )
94
+ );
95
+
96
+ expect(result).toHaveProperty("sslCaPem", "{{ tb_secret('KAFKA_SSL_CA_PEM') }}");
97
+ });
98
+ });
@@ -9,9 +9,34 @@ import {
9
9
  isBlank,
10
10
  parseDirectiveLine,
11
11
  parseQuotedValue,
12
+ readDirectiveBlock,
12
13
  splitLines,
13
14
  } from "./parser-utils.js";
14
15
 
16
+ const CONNECTION_DIRECTIVES = new Set([
17
+ "TYPE",
18
+ "KAFKA_BOOTSTRAP_SERVERS",
19
+ "KAFKA_SECURITY_PROTOCOL",
20
+ "KAFKA_SASL_MECHANISM",
21
+ "KAFKA_KEY",
22
+ "KAFKA_SECRET",
23
+ "KAFKA_SCHEMA_REGISTRY_URL",
24
+ "KAFKA_SSL_CA_PEM",
25
+ "S3_REGION",
26
+ "S3_ARN",
27
+ "S3_ACCESS_KEY",
28
+ "S3_SECRET",
29
+ "GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON",
30
+ ]);
31
+
32
+ function isConnectionDirectiveLine(line: string): boolean {
33
+ const trimmed = line.trim();
34
+ if (!trimmed || trimmed.startsWith("#")) return false;
35
+ const firstSpace = trimmed.indexOf(" ");
36
+ const word = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
37
+ return CONNECTION_DIRECTIVES.has(word);
38
+ }
39
+
15
40
  export function parseConnectionFile(
16
41
  resource: ResourceFile
17
42
  ): KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel {
@@ -41,9 +66,12 @@ export function parseConnectionFile(
41
66
  let accessSecret: string | undefined;
42
67
  let serviceAccountCredentialsJson: string | undefined;
43
68
 
44
- for (const rawLine of lines) {
69
+ let i = 0;
70
+ while (i < lines.length) {
71
+ const rawLine = lines[i] ?? "";
45
72
  const line = rawLine.trim();
46
73
  if (isBlank(line) || line.startsWith("#")) {
74
+ i += 1;
47
75
  continue;
48
76
  }
49
77
 
@@ -92,6 +120,12 @@ export function parseConnectionFile(
92
120
  schemaRegistryUrl = value;
93
121
  break;
94
122
  case "KAFKA_SSL_CA_PEM":
123
+ if (value === ">") {
124
+ const block = readDirectiveBlock(lines, i + 1, isConnectionDirectiveLine);
125
+ sslCaPem = block.lines.join("\n");
126
+ i = block.nextIndex;
127
+ continue;
128
+ }
95
129
  sslCaPem = value;
96
130
  break;
97
131
  case "S3_REGION":
@@ -107,6 +141,12 @@ export function parseConnectionFile(
107
141
  accessSecret = parseQuotedValue(value);
108
142
  break;
109
143
  case "GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON":
144
+ if (value === ">") {
145
+ const block = readDirectiveBlock(lines, i + 1, isConnectionDirectiveLine);
146
+ serviceAccountCredentialsJson = block.lines.join("\n");
147
+ i = block.nextIndex;
148
+ continue;
149
+ }
110
150
  serviceAccountCredentialsJson = parseQuotedValue(value);
111
151
  break;
112
152
  default:
@@ -117,6 +157,7 @@ export function parseConnectionFile(
117
157
  `Unsupported connection directive in strict mode: "${line}"`
118
158
  );
119
159
  }
160
+ i += 1;
120
161
  }
121
162
 
122
163
  if (!connectionType) {
@@ -109,6 +109,24 @@ describe("Type Validators (t.*)", () => {
109
109
  const type = t.int32().default(42);
110
110
  expect(type._modifiers.defaultValue).toBe(42);
111
111
  });
112
+
113
+ it("stores default SQL expression in modifiers", () => {
114
+ const type = t.uuid().defaultExpr("generateUUIDv4()");
115
+ expect(type._modifiers.hasDefault).toBe(true);
116
+ expect(type._modifiers.defaultExpression).toBe("generateUUIDv4()");
117
+ expect(type._modifiers.defaultValue).toBeUndefined();
118
+ });
119
+
120
+ it("trims default SQL expression", () => {
121
+ const type = t.uuid().defaultExpr(" generateUUIDv4() ");
122
+ expect(type._modifiers.defaultExpression).toBe("generateUUIDv4()");
123
+ });
124
+
125
+ it("throws on empty default SQL expression", () => {
126
+ expect(() => t.uuid().defaultExpr(" ")).toThrow(
127
+ "Default expression cannot be empty."
128
+ );
129
+ });
112
130
  });
113
131
 
114
132
  describe("Codec modifier", () => {
@@ -44,6 +44,14 @@ export interface TypeValidator<
44
44
  TTinybirdType,
45
45
  TModifiers & { hasDefault: true; defaultValue: TType }
46
46
  >;
47
+ /** Set a default SQL expression for the column (for example: generateUUIDv4()) */
48
+ defaultExpr(
49
+ expression: string,
50
+ ): TypeValidator<
51
+ TType,
52
+ TTinybirdType,
53
+ TModifiers & { hasDefault: true; defaultExpression: string }
54
+ >;
47
55
  /** Set a codec for compression */
48
56
  codec(
49
57
  codec: string,
@@ -59,6 +67,7 @@ export interface TypeModifiers {
59
67
  lowCardinality?: boolean;
60
68
  hasDefault?: boolean;
61
69
  defaultValue?: unknown;
70
+ defaultExpression?: string;
62
71
  codec?: string;
63
72
  jsonPath?: string;
64
73
  }
@@ -144,6 +153,7 @@ function createValidator<TType, TTinybirdType extends string>(
144
153
  ...modifiers,
145
154
  hasDefault: true,
146
155
  defaultValue: value,
156
+ defaultExpression: undefined,
147
157
  }) as TypeValidator<
148
158
  TType,
149
159
  TTinybirdType,
@@ -151,6 +161,23 @@ function createValidator<TType, TTinybirdType extends string>(
151
161
  >;
152
162
  },
153
163
 
164
+ defaultExpr(expression: string) {
165
+ const trimmed = expression.trim();
166
+ if (!trimmed) {
167
+ throw new Error("Default expression cannot be empty.");
168
+ }
169
+ return createValidator<TType, TTinybirdType>(tinybirdType, {
170
+ ...modifiers,
171
+ hasDefault: true,
172
+ defaultValue: undefined,
173
+ defaultExpression: trimmed,
174
+ }) as TypeValidator<
175
+ TType,
176
+ TTinybirdType,
177
+ TypeModifiers & { hasDefault: true; defaultExpression: string }
178
+ >;
179
+ },
180
+
154
181
  codec(codec: string) {
155
182
  return createValidator<TType, TTinybirdType>(tinybirdType, {
156
183
  ...modifiers,