@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.
- package/README.md +1 -0
- package/dist/cli/commands/migrate.test.js +21 -0
- package/dist/cli/commands/migrate.test.js.map +1 -1
- package/dist/generator/connection.d.ts.map +1 -1
- package/dist/generator/connection.js +10 -1
- package/dist/generator/connection.js.map +1 -1
- package/dist/generator/connection.test.js +19 -0
- package/dist/generator/connection.test.js.map +1 -1
- package/dist/generator/datasource.d.ts.map +1 -1
- package/dist/generator/datasource.js +9 -4
- package/dist/generator/datasource.js.map +1 -1
- package/dist/generator/datasource.test.js +9 -0
- package/dist/generator/datasource.test.js.map +1 -1
- package/dist/migrate/emit-ts.d.ts.map +1 -1
- package/dist/migrate/emit-ts.js +14 -9
- 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 +41 -2
- package/dist/migrate/parse-connection.js.map +1 -1
- package/dist/migrate/parse-connection.test.d.ts +2 -0
- package/dist/migrate/parse-connection.test.d.ts.map +1 -0
- package/dist/migrate/parse-connection.test.js +56 -0
- package/dist/migrate/parse-connection.test.js.map +1 -0
- package/dist/schema/types.d.ts +6 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +13 -0
- package/dist/schema/types.js.map +1 -1
- package/dist/schema/types.test.js +13 -0
- package/dist/schema/types.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/migrate.test.ts +30 -0
- package/src/generator/connection.test.ts +25 -0
- package/src/generator/connection.ts +9 -1
- package/src/generator/datasource.test.ts +11 -0
- package/src/generator/datasource.ts +8 -4
- package/src/migrate/emit-ts.ts +16 -12
- package/src/migrate/parse-connection.test.ts +98 -0
- package/src/migrate/parse-connection.ts +42 -1
- package/src/schema/types.test.ts +18 -0
- package/src/schema/types.ts +27 -0
package/src/migrate/emit-ts.ts
CHANGED
|
@@ -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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (parsedDefault ===
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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) {
|
package/src/schema/types.test.ts
CHANGED
|
@@ -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", () => {
|
package/src/schema/types.ts
CHANGED
|
@@ -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,
|