@tinybirdco/sdk 0.0.41 → 0.0.43
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/LICENSE +7 -0
- package/README.md +29 -3
- package/dist/api/resources.d.ts +72 -1
- package/dist/api/resources.d.ts.map +1 -1
- package/dist/api/resources.js +197 -1
- package/dist/api/resources.js.map +1 -1
- package/dist/api/resources.test.js +82 -1
- package/dist/api/resources.test.js.map +1 -1
- package/dist/cli/commands/migrate.d.ts +11 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -0
- package/dist/cli/commands/migrate.js +196 -0
- package/dist/cli/commands/migrate.js.map +1 -0
- package/dist/cli/commands/migrate.test.d.ts +2 -0
- package/dist/cli/commands/migrate.test.d.ts.map +1 -0
- package/dist/cli/commands/migrate.test.js +473 -0
- package/dist/cli/commands/migrate.test.js.map +1 -0
- package/dist/cli/commands/pull.d.ts +59 -0
- package/dist/cli/commands/pull.d.ts.map +1 -0
- package/dist/cli/commands/pull.js +104 -0
- package/dist/cli/commands/pull.js.map +1 -0
- package/dist/cli/commands/pull.test.d.ts +2 -0
- package/dist/cli/commands/pull.test.d.ts.map +1 -0
- package/dist/cli/commands/pull.test.js +140 -0
- package/dist/cli/commands/pull.test.js.map +1 -0
- package/dist/cli/config.d.ts +10 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +22 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/index.js +77 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/generator/client.js +2 -2
- package/dist/generator/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/migrate/discovery.d.ts +7 -0
- package/dist/migrate/discovery.d.ts.map +1 -0
- package/dist/migrate/discovery.js +125 -0
- package/dist/migrate/discovery.js.map +1 -0
- package/dist/migrate/emit-ts.d.ts +4 -0
- package/dist/migrate/emit-ts.d.ts.map +1 -0
- package/dist/migrate/emit-ts.js +387 -0
- package/dist/migrate/emit-ts.js.map +1 -0
- package/dist/migrate/parse-connection.d.ts +3 -0
- package/dist/migrate/parse-connection.d.ts.map +1 -0
- package/dist/migrate/parse-connection.js +74 -0
- package/dist/migrate/parse-connection.js.map +1 -0
- package/dist/migrate/parse-datasource.d.ts +3 -0
- package/dist/migrate/parse-datasource.d.ts.map +1 -0
- package/dist/migrate/parse-datasource.js +324 -0
- package/dist/migrate/parse-datasource.js.map +1 -0
- package/dist/migrate/parse-pipe.d.ts +3 -0
- package/dist/migrate/parse-pipe.d.ts.map +1 -0
- package/dist/migrate/parse-pipe.js +332 -0
- package/dist/migrate/parse-pipe.js.map +1 -0
- package/dist/migrate/parse.d.ts +3 -0
- package/dist/migrate/parse.d.ts.map +1 -0
- package/dist/migrate/parse.js +18 -0
- package/dist/migrate/parse.js.map +1 -0
- package/dist/migrate/parser-utils.d.ts +20 -0
- package/dist/migrate/parser-utils.d.ts.map +1 -0
- package/dist/migrate/parser-utils.js +130 -0
- package/dist/migrate/parser-utils.js.map +1 -0
- package/dist/migrate/types.d.ts +110 -0
- package/dist/migrate/types.d.ts.map +1 -0
- package/dist/migrate/types.js +2 -0
- package/dist/migrate/types.js.map +1 -0
- package/dist/schema/project.d.ts +20 -9
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/schema/project.js +127 -136
- package/dist/schema/project.js.map +1 -1
- package/dist/schema/project.test.js +22 -0
- package/dist/schema/project.test.js.map +1 -1
- package/package.json +2 -1
- package/src/api/resources.test.ts +121 -0
- package/src/api/resources.ts +292 -1
- package/src/cli/commands/migrate.test.ts +564 -0
- package/src/cli/commands/migrate.ts +240 -0
- package/src/cli/commands/pull.test.ts +173 -0
- package/src/cli/commands/pull.ts +177 -0
- package/src/cli/config.ts +26 -0
- package/src/cli/index.ts +112 -0
- package/src/generator/client.ts +2 -2
- package/src/index.ts +1 -1
- package/src/migrate/discovery.ts +151 -0
- package/src/migrate/emit-ts.ts +469 -0
- package/src/migrate/parse-connection.ts +128 -0
- package/src/migrate/parse-datasource.ts +453 -0
- package/src/migrate/parse-pipe.ts +518 -0
- package/src/migrate/parse.ts +20 -0
- package/src/migrate/parser-utils.ts +160 -0
- package/src/migrate/types.ts +125 -0
- package/src/schema/project.test.ts +28 -0
- package/src/schema/project.ts +173 -181
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { clickhouseTypeToValidator } from "../codegen/type-mapper.js";
|
|
2
|
+
import { toCamelCase } from "../codegen/utils.js";
|
|
3
|
+
import { parseLiteralFromDatafile, toTsLiteral } from "./parser-utils.js";
|
|
4
|
+
import type {
|
|
5
|
+
DatasourceModel,
|
|
6
|
+
KafkaConnectionModel,
|
|
7
|
+
ParsedResource,
|
|
8
|
+
PipeModel,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
function escapeString(value: string): string {
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizedBaseType(type: string): string {
|
|
16
|
+
let current = type.trim();
|
|
17
|
+
let updated = true;
|
|
18
|
+
while (updated) {
|
|
19
|
+
updated = false;
|
|
20
|
+
const nullable = current.match(/^Nullable\((.+)\)$/);
|
|
21
|
+
if (nullable?.[1]) {
|
|
22
|
+
current = nullable[1];
|
|
23
|
+
updated = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const lowCard = current.match(/^LowCardinality\((.+)\)$/);
|
|
27
|
+
if (lowCard?.[1]) {
|
|
28
|
+
current = lowCard[1];
|
|
29
|
+
updated = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isBooleanType(type: string): boolean {
|
|
37
|
+
const base = normalizedBaseType(type);
|
|
38
|
+
return base === "Bool" || base === "Boolean";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function strictColumnTypeToValidator(type: string): string {
|
|
42
|
+
const validator = clickhouseTypeToValidator(type);
|
|
43
|
+
if (validator.includes("TODO: Unknown type") || validator.includes("/*")) {
|
|
44
|
+
throw new Error(`Unsupported column type in strict mode: "${type}"`);
|
|
45
|
+
}
|
|
46
|
+
return validator;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function strictParamBaseValidator(type: string): string {
|
|
50
|
+
const map: Record<string, string> = {
|
|
51
|
+
String: "p.string()",
|
|
52
|
+
UUID: "p.uuid()",
|
|
53
|
+
Int8: "p.int8()",
|
|
54
|
+
Int16: "p.int16()",
|
|
55
|
+
Int32: "p.int32()",
|
|
56
|
+
Int64: "p.int64()",
|
|
57
|
+
UInt8: "p.uint8()",
|
|
58
|
+
UInt16: "p.uint16()",
|
|
59
|
+
UInt32: "p.uint32()",
|
|
60
|
+
UInt64: "p.uint64()",
|
|
61
|
+
Float32: "p.float32()",
|
|
62
|
+
Float64: "p.float64()",
|
|
63
|
+
Boolean: "p.boolean()",
|
|
64
|
+
Bool: "p.boolean()",
|
|
65
|
+
Date: "p.date()",
|
|
66
|
+
DateTime: "p.dateTime()",
|
|
67
|
+
DateTime64: "p.dateTime64()",
|
|
68
|
+
Array: "p.array(p.string())",
|
|
69
|
+
};
|
|
70
|
+
const validator = map[type];
|
|
71
|
+
if (!validator) {
|
|
72
|
+
throw new Error(`Unsupported parameter type in strict mode: "${type}"`);
|
|
73
|
+
}
|
|
74
|
+
return validator;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function applyParamOptional(
|
|
78
|
+
baseValidator: string,
|
|
79
|
+
required: boolean,
|
|
80
|
+
defaultValue: string | number | undefined
|
|
81
|
+
): string {
|
|
82
|
+
const withDefault = defaultValue !== undefined;
|
|
83
|
+
if (!withDefault && required) {
|
|
84
|
+
return baseValidator;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const optionalSuffix = withDefault
|
|
88
|
+
? `.optional(${typeof defaultValue === "string" ? JSON.stringify(defaultValue) : defaultValue})`
|
|
89
|
+
: ".optional()";
|
|
90
|
+
|
|
91
|
+
if (baseValidator.endsWith(")")) {
|
|
92
|
+
return `${baseValidator}${optionalSuffix}`;
|
|
93
|
+
}
|
|
94
|
+
return `${baseValidator}${optionalSuffix}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function engineFunctionName(type: string): string {
|
|
98
|
+
const map: Record<string, string> = {
|
|
99
|
+
MergeTree: "mergeTree",
|
|
100
|
+
ReplacingMergeTree: "replacingMergeTree",
|
|
101
|
+
SummingMergeTree: "summingMergeTree",
|
|
102
|
+
AggregatingMergeTree: "aggregatingMergeTree",
|
|
103
|
+
CollapsingMergeTree: "collapsingMergeTree",
|
|
104
|
+
VersionedCollapsingMergeTree: "versionedCollapsingMergeTree",
|
|
105
|
+
};
|
|
106
|
+
const functionName = map[type];
|
|
107
|
+
if (!functionName) {
|
|
108
|
+
throw new Error(`Unsupported engine type in strict mode: "${type}"`);
|
|
109
|
+
}
|
|
110
|
+
return functionName;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function emitEngineOptions(ds: DatasourceModel): string {
|
|
114
|
+
const options: string[] = [];
|
|
115
|
+
const { engine } = ds;
|
|
116
|
+
|
|
117
|
+
if (engine.sortingKey.length === 1) {
|
|
118
|
+
options.push(`sortingKey: ${escapeString(engine.sortingKey[0]!)}`);
|
|
119
|
+
} else {
|
|
120
|
+
options.push(
|
|
121
|
+
`sortingKey: [${engine.sortingKey.map((k) => escapeString(k)).join(", ")}]`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (engine.partitionKey) {
|
|
126
|
+
options.push(`partitionKey: ${escapeString(engine.partitionKey)}`);
|
|
127
|
+
}
|
|
128
|
+
if (engine.primaryKey && engine.primaryKey.length > 0) {
|
|
129
|
+
if (engine.primaryKey.length === 1) {
|
|
130
|
+
options.push(`primaryKey: ${escapeString(engine.primaryKey[0]!)}`);
|
|
131
|
+
} else {
|
|
132
|
+
options.push(
|
|
133
|
+
`primaryKey: [${engine.primaryKey.map((k) => escapeString(k)).join(", ")}]`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (engine.ttl) {
|
|
138
|
+
options.push(`ttl: ${escapeString(engine.ttl)}`);
|
|
139
|
+
}
|
|
140
|
+
if (engine.ver) {
|
|
141
|
+
options.push(`ver: ${escapeString(engine.ver)}`);
|
|
142
|
+
}
|
|
143
|
+
if (engine.sign) {
|
|
144
|
+
options.push(`sign: ${escapeString(engine.sign)}`);
|
|
145
|
+
}
|
|
146
|
+
if (engine.version) {
|
|
147
|
+
options.push(`version: ${escapeString(engine.version)}`);
|
|
148
|
+
}
|
|
149
|
+
if (engine.summingColumns && engine.summingColumns.length > 0) {
|
|
150
|
+
options.push(
|
|
151
|
+
`columns: [${engine.summingColumns.map((k) => escapeString(k)).join(", ")}]`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (engine.settings && Object.keys(engine.settings).length > 0) {
|
|
155
|
+
const settingsEntries = Object.entries(engine.settings).map(([k, v]) => {
|
|
156
|
+
if (typeof v === "string") {
|
|
157
|
+
return `${escapeString(k)}: ${escapeString(v)}`;
|
|
158
|
+
}
|
|
159
|
+
return `${escapeString(k)}: ${v}`;
|
|
160
|
+
});
|
|
161
|
+
options.push(`settings: { ${settingsEntries.join(", ")} }`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const engineFn = engineFunctionName(engine.type);
|
|
165
|
+
return `engine.${engineFn}({ ${options.join(", ")} })`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function emitDatasource(ds: DatasourceModel): string {
|
|
169
|
+
const variableName = toCamelCase(ds.name);
|
|
170
|
+
const lines: string[] = [];
|
|
171
|
+
const hasJsonPath = ds.columns.some((column) => column.jsonPath !== undefined);
|
|
172
|
+
const hasMissingJsonPath = ds.columns.some((column) => column.jsonPath === undefined);
|
|
173
|
+
|
|
174
|
+
if (hasJsonPath && hasMissingJsonPath) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Datasource "${ds.name}" has mixed json path usage. This is not representable in strict mode.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (ds.description) {
|
|
181
|
+
lines.push("/**");
|
|
182
|
+
for (const row of ds.description.split("\n")) {
|
|
183
|
+
lines.push(` * ${row}`);
|
|
184
|
+
}
|
|
185
|
+
lines.push(" */");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
lines.push(`export const ${variableName} = defineDatasource(${escapeString(ds.name)}, {`);
|
|
189
|
+
if (ds.description) {
|
|
190
|
+
lines.push(` description: ${escapeString(ds.description)},`);
|
|
191
|
+
}
|
|
192
|
+
if (!hasJsonPath) {
|
|
193
|
+
lines.push(" jsonPaths: false,");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
lines.push(" schema: {");
|
|
197
|
+
for (const column of ds.columns) {
|
|
198
|
+
let validator = strictColumnTypeToValidator(column.type);
|
|
199
|
+
|
|
200
|
+
if (column.defaultExpression !== undefined) {
|
|
201
|
+
const parsedDefault = parseLiteralFromDatafile(column.defaultExpression);
|
|
202
|
+
let literalValue = parsedDefault;
|
|
203
|
+
if (typeof parsedDefault === "number" && isBooleanType(column.type)) {
|
|
204
|
+
if (parsedDefault === 0 || parsedDefault === 1) {
|
|
205
|
+
literalValue = parsedDefault === 1;
|
|
206
|
+
} else {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Boolean default value must be 0 or 1 for column "${column.name}" in datasource "${ds.name}".`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
validator += `.default(${toTsLiteral(
|
|
213
|
+
literalValue as string | number | boolean | null | Record<string, unknown> | unknown[]
|
|
214
|
+
)})`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (column.codec) {
|
|
218
|
+
validator += `.codec(${escapeString(column.codec)})`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (column.jsonPath) {
|
|
222
|
+
lines.push(
|
|
223
|
+
` ${column.name}: column(${validator}, { jsonPath: ${escapeString(column.jsonPath)} }),`
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
lines.push(` ${column.name}: ${validator},`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
lines.push(" },");
|
|
230
|
+
lines.push(` engine: ${emitEngineOptions(ds)},`);
|
|
231
|
+
|
|
232
|
+
if (ds.kafka) {
|
|
233
|
+
const connectionVar = toCamelCase(ds.kafka.connectionName);
|
|
234
|
+
lines.push(" kafka: {");
|
|
235
|
+
lines.push(` connection: ${connectionVar},`);
|
|
236
|
+
lines.push(` topic: ${escapeString(ds.kafka.topic)},`);
|
|
237
|
+
if (ds.kafka.groupId) {
|
|
238
|
+
lines.push(` groupId: ${escapeString(ds.kafka.groupId)},`);
|
|
239
|
+
}
|
|
240
|
+
if (ds.kafka.autoOffsetReset) {
|
|
241
|
+
lines.push(` autoOffsetReset: ${escapeString(ds.kafka.autoOffsetReset)},`);
|
|
242
|
+
}
|
|
243
|
+
lines.push(" },");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (ds.forwardQuery) {
|
|
247
|
+
lines.push(" forwardQuery: `");
|
|
248
|
+
lines.push(ds.forwardQuery.replace(/`/g, "\\`").replace(/\${/g, "\\${"));
|
|
249
|
+
lines.push(" `,");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (ds.tokens.length > 0) {
|
|
253
|
+
lines.push(" tokens: [");
|
|
254
|
+
for (const token of ds.tokens) {
|
|
255
|
+
lines.push(
|
|
256
|
+
` { name: ${escapeString(token.name)}, permissions: [${escapeString(token.scope)}] },`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
lines.push(" ],");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (ds.sharedWith.length > 0) {
|
|
263
|
+
lines.push(
|
|
264
|
+
` sharedWith: [${ds.sharedWith.map((workspace) => escapeString(workspace)).join(", ")}],`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
lines.push("});");
|
|
269
|
+
lines.push("");
|
|
270
|
+
return lines.join("\n");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function emitConnection(connection: KafkaConnectionModel): string {
|
|
274
|
+
const variableName = toCamelCase(connection.name);
|
|
275
|
+
const lines: string[] = [];
|
|
276
|
+
lines.push(
|
|
277
|
+
`export const ${variableName} = createKafkaConnection(${escapeString(connection.name)}, {`
|
|
278
|
+
);
|
|
279
|
+
lines.push(` bootstrapServers: ${escapeString(connection.bootstrapServers)},`);
|
|
280
|
+
if (connection.securityProtocol) {
|
|
281
|
+
lines.push(` securityProtocol: ${escapeString(connection.securityProtocol)},`);
|
|
282
|
+
}
|
|
283
|
+
if (connection.saslMechanism) {
|
|
284
|
+
lines.push(` saslMechanism: ${escapeString(connection.saslMechanism)},`);
|
|
285
|
+
}
|
|
286
|
+
if (connection.key) {
|
|
287
|
+
lines.push(` key: ${escapeString(connection.key)},`);
|
|
288
|
+
}
|
|
289
|
+
if (connection.secret) {
|
|
290
|
+
lines.push(` secret: ${escapeString(connection.secret)},`);
|
|
291
|
+
}
|
|
292
|
+
if (connection.sslCaPem) {
|
|
293
|
+
lines.push(` sslCaPem: ${escapeString(connection.sslCaPem)},`);
|
|
294
|
+
}
|
|
295
|
+
lines.push("});");
|
|
296
|
+
lines.push("");
|
|
297
|
+
return lines.join("\n");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function emitPipe(pipe: PipeModel): string {
|
|
301
|
+
const variableName = toCamelCase(pipe.name);
|
|
302
|
+
const lines: string[] = [];
|
|
303
|
+
const endpointOutputColumns =
|
|
304
|
+
pipe.inferredOutputColumns.length > 0 ? pipe.inferredOutputColumns : ["result"];
|
|
305
|
+
|
|
306
|
+
if (pipe.description) {
|
|
307
|
+
lines.push("/**");
|
|
308
|
+
for (const row of pipe.description.split("\n")) {
|
|
309
|
+
lines.push(` * ${row}`);
|
|
310
|
+
}
|
|
311
|
+
lines.push(" */");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (pipe.type === "materialized") {
|
|
315
|
+
lines.push(`export const ${variableName} = defineMaterializedView(${escapeString(pipe.name)}, {`);
|
|
316
|
+
} else if (pipe.type === "copy") {
|
|
317
|
+
lines.push(`export const ${variableName} = defineCopyPipe(${escapeString(pipe.name)}, {`);
|
|
318
|
+
} else {
|
|
319
|
+
lines.push(`export const ${variableName} = definePipe(${escapeString(pipe.name)}, {`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (pipe.description) {
|
|
323
|
+
lines.push(` description: ${escapeString(pipe.description)},`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (pipe.type === "pipe" || pipe.type === "endpoint") {
|
|
327
|
+
if (pipe.params.length > 0) {
|
|
328
|
+
lines.push(" params: {");
|
|
329
|
+
for (const param of pipe.params) {
|
|
330
|
+
const baseValidator = strictParamBaseValidator(param.type);
|
|
331
|
+
const validator = applyParamOptional(
|
|
332
|
+
baseValidator,
|
|
333
|
+
param.required,
|
|
334
|
+
param.defaultValue
|
|
335
|
+
);
|
|
336
|
+
lines.push(` ${param.name}: ${validator},`);
|
|
337
|
+
}
|
|
338
|
+
lines.push(" },");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (pipe.type === "materialized") {
|
|
343
|
+
lines.push(` datasource: ${toCamelCase(pipe.materializedDatasource ?? "")},`);
|
|
344
|
+
if (pipe.deploymentMethod) {
|
|
345
|
+
lines.push(` deploymentMethod: ${escapeString(pipe.deploymentMethod)},`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (pipe.type === "copy") {
|
|
350
|
+
lines.push(` datasource: ${toCamelCase(pipe.copyTargetDatasource ?? "")},`);
|
|
351
|
+
if (pipe.copyMode) {
|
|
352
|
+
lines.push(` copy_mode: ${escapeString(pipe.copyMode)},`);
|
|
353
|
+
}
|
|
354
|
+
if (pipe.copySchedule) {
|
|
355
|
+
lines.push(` copy_schedule: ${escapeString(pipe.copySchedule)},`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
lines.push(" nodes: [");
|
|
360
|
+
for (const node of pipe.nodes) {
|
|
361
|
+
lines.push(" node({");
|
|
362
|
+
lines.push(` name: ${escapeString(node.name)},`);
|
|
363
|
+
if (node.description) {
|
|
364
|
+
lines.push(` description: ${escapeString(node.description)},`);
|
|
365
|
+
}
|
|
366
|
+
lines.push(" sql: `");
|
|
367
|
+
lines.push(node.sql.replace(/`/g, "\\`").replace(/\${/g, "\\${"));
|
|
368
|
+
lines.push(" `,");
|
|
369
|
+
lines.push(" }),");
|
|
370
|
+
}
|
|
371
|
+
lines.push(" ],");
|
|
372
|
+
|
|
373
|
+
if (pipe.type === "endpoint") {
|
|
374
|
+
if (pipe.cacheTtl !== undefined) {
|
|
375
|
+
lines.push(` endpoint: { enabled: true, cache: { enabled: true, ttl: ${pipe.cacheTtl} } },`);
|
|
376
|
+
} else {
|
|
377
|
+
lines.push(" endpoint: true,");
|
|
378
|
+
}
|
|
379
|
+
lines.push(" output: {");
|
|
380
|
+
for (const columnName of endpointOutputColumns) {
|
|
381
|
+
lines.push(` ${columnName}: t.string(),`);
|
|
382
|
+
}
|
|
383
|
+
lines.push(" },");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (pipe.tokens.length > 0) {
|
|
387
|
+
lines.push(" tokens: [");
|
|
388
|
+
for (const token of pipe.tokens) {
|
|
389
|
+
lines.push(` { name: ${escapeString(token.name)} },`);
|
|
390
|
+
}
|
|
391
|
+
lines.push(" ],");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
lines.push("});");
|
|
395
|
+
lines.push("");
|
|
396
|
+
return lines.join("\n");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function emitMigrationFileContent(resources: ParsedResource[]): string {
|
|
400
|
+
const connections = resources.filter(
|
|
401
|
+
(resource): resource is KafkaConnectionModel => resource.kind === "connection"
|
|
402
|
+
);
|
|
403
|
+
const datasources = resources.filter(
|
|
404
|
+
(resource): resource is DatasourceModel => resource.kind === "datasource"
|
|
405
|
+
);
|
|
406
|
+
const pipes = resources.filter(
|
|
407
|
+
(resource): resource is PipeModel => resource.kind === "pipe"
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const needsColumn = datasources.some((ds) =>
|
|
411
|
+
ds.columns.some((column) => column.jsonPath !== undefined)
|
|
412
|
+
);
|
|
413
|
+
const needsParams = pipes.some((pipe) => pipe.params.length > 0);
|
|
414
|
+
|
|
415
|
+
const imports = new Set<string>(["createKafkaConnection", "defineDatasource", "definePipe", "defineMaterializedView", "defineCopyPipe", "node", "t", "engine"]);
|
|
416
|
+
if (needsColumn) {
|
|
417
|
+
imports.add("column");
|
|
418
|
+
}
|
|
419
|
+
if (needsParams) {
|
|
420
|
+
imports.add("p");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const lines: string[] = [];
|
|
424
|
+
lines.push("/**");
|
|
425
|
+
lines.push(" * Generated by tinybird migrate.");
|
|
426
|
+
lines.push(" * Review endpoint output schemas and any defaults before production use.");
|
|
427
|
+
lines.push(" */");
|
|
428
|
+
lines.push("");
|
|
429
|
+
lines.push(`import { ${Array.from(imports).join(", ")} } from "@tinybirdco/sdk";`);
|
|
430
|
+
lines.push("");
|
|
431
|
+
|
|
432
|
+
if (connections.length > 0) {
|
|
433
|
+
lines.push("// Connections");
|
|
434
|
+
lines.push("");
|
|
435
|
+
for (const connection of connections) {
|
|
436
|
+
lines.push(emitConnection(connection));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (datasources.length > 0) {
|
|
441
|
+
lines.push("// Datasources");
|
|
442
|
+
lines.push("");
|
|
443
|
+
for (const datasource of datasources) {
|
|
444
|
+
lines.push(emitDatasource(datasource));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (pipes.length > 0) {
|
|
449
|
+
lines.push("// Pipes");
|
|
450
|
+
lines.push("");
|
|
451
|
+
for (const pipe of pipes) {
|
|
452
|
+
lines.push(emitPipe(pipe));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function validateResourceForEmission(resource: ParsedResource): void {
|
|
460
|
+
if (resource.kind === "connection") {
|
|
461
|
+
emitConnection(resource);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (resource.kind === "datasource") {
|
|
465
|
+
emitDatasource(resource);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
emitPipe(resource);
|
|
469
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { KafkaConnectionModel, ResourceFile } from "./types.js";
|
|
2
|
+
import {
|
|
3
|
+
MigrationParseError,
|
|
4
|
+
isBlank,
|
|
5
|
+
parseDirectiveLine,
|
|
6
|
+
splitLines,
|
|
7
|
+
} from "./parser-utils.js";
|
|
8
|
+
|
|
9
|
+
export function parseConnectionFile(resource: ResourceFile): KafkaConnectionModel {
|
|
10
|
+
const lines = splitLines(resource.content);
|
|
11
|
+
let connectionType: string | undefined;
|
|
12
|
+
let bootstrapServers: string | undefined;
|
|
13
|
+
let securityProtocol:
|
|
14
|
+
| "SASL_SSL"
|
|
15
|
+
| "PLAINTEXT"
|
|
16
|
+
| "SASL_PLAINTEXT"
|
|
17
|
+
| undefined;
|
|
18
|
+
let saslMechanism:
|
|
19
|
+
| "PLAIN"
|
|
20
|
+
| "SCRAM-SHA-256"
|
|
21
|
+
| "SCRAM-SHA-512"
|
|
22
|
+
| "OAUTHBEARER"
|
|
23
|
+
| undefined;
|
|
24
|
+
let key: string | undefined;
|
|
25
|
+
let secret: string | undefined;
|
|
26
|
+
let sslCaPem: string | undefined;
|
|
27
|
+
|
|
28
|
+
for (const rawLine of lines) {
|
|
29
|
+
const line = rawLine.trim();
|
|
30
|
+
if (isBlank(line)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { key: directive, value } = parseDirectiveLine(line);
|
|
35
|
+
switch (directive) {
|
|
36
|
+
case "TYPE":
|
|
37
|
+
connectionType = value;
|
|
38
|
+
break;
|
|
39
|
+
case "KAFKA_BOOTSTRAP_SERVERS":
|
|
40
|
+
bootstrapServers = value;
|
|
41
|
+
break;
|
|
42
|
+
case "KAFKA_SECURITY_PROTOCOL":
|
|
43
|
+
if (value !== "SASL_SSL" && value !== "PLAINTEXT" && value !== "SASL_PLAINTEXT") {
|
|
44
|
+
throw new MigrationParseError(
|
|
45
|
+
resource.filePath,
|
|
46
|
+
"connection",
|
|
47
|
+
resource.name,
|
|
48
|
+
`Unsupported KAFKA_SECURITY_PROTOCOL: "${value}"`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
securityProtocol = value;
|
|
52
|
+
break;
|
|
53
|
+
case "KAFKA_SASL_MECHANISM":
|
|
54
|
+
if (
|
|
55
|
+
value !== "PLAIN" &&
|
|
56
|
+
value !== "SCRAM-SHA-256" &&
|
|
57
|
+
value !== "SCRAM-SHA-512" &&
|
|
58
|
+
value !== "OAUTHBEARER"
|
|
59
|
+
) {
|
|
60
|
+
throw new MigrationParseError(
|
|
61
|
+
resource.filePath,
|
|
62
|
+
"connection",
|
|
63
|
+
resource.name,
|
|
64
|
+
`Unsupported KAFKA_SASL_MECHANISM: "${value}"`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
saslMechanism = value;
|
|
68
|
+
break;
|
|
69
|
+
case "KAFKA_KEY":
|
|
70
|
+
key = value;
|
|
71
|
+
break;
|
|
72
|
+
case "KAFKA_SECRET":
|
|
73
|
+
secret = value;
|
|
74
|
+
break;
|
|
75
|
+
case "KAFKA_SSL_CA_PEM":
|
|
76
|
+
sslCaPem = value;
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
throw new MigrationParseError(
|
|
80
|
+
resource.filePath,
|
|
81
|
+
"connection",
|
|
82
|
+
resource.name,
|
|
83
|
+
`Unsupported connection directive in strict mode: "${line}"`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!connectionType) {
|
|
89
|
+
throw new MigrationParseError(
|
|
90
|
+
resource.filePath,
|
|
91
|
+
"connection",
|
|
92
|
+
resource.name,
|
|
93
|
+
"TYPE directive is required."
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (connectionType !== "kafka") {
|
|
98
|
+
throw new MigrationParseError(
|
|
99
|
+
resource.filePath,
|
|
100
|
+
"connection",
|
|
101
|
+
resource.name,
|
|
102
|
+
`Unsupported connection type in strict mode: "${connectionType}"`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!bootstrapServers) {
|
|
107
|
+
throw new MigrationParseError(
|
|
108
|
+
resource.filePath,
|
|
109
|
+
"connection",
|
|
110
|
+
resource.name,
|
|
111
|
+
"KAFKA_BOOTSTRAP_SERVERS is required for kafka connections."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
kind: "connection",
|
|
117
|
+
name: resource.name,
|
|
118
|
+
filePath: resource.filePath,
|
|
119
|
+
connectionType: "kafka",
|
|
120
|
+
bootstrapServers,
|
|
121
|
+
securityProtocol,
|
|
122
|
+
saslMechanism,
|
|
123
|
+
key,
|
|
124
|
+
secret,
|
|
125
|
+
sslCaPem,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|