@tinybirdco/sdk 0.0.47 → 0.0.49
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 +53 -3
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +32 -0
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/migrate.test.js +585 -8
- 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/generator/pipe.d.ts.map +1 -1
- package/dist/generator/pipe.js +31 -1
- package/dist/generator/pipe.js.map +1 -1
- package/dist/generator/pipe.test.js +50 -1
- package/dist/generator/pipe.test.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- 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 +159 -41
- 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 +115 -52
- 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 +257 -46
- package/dist/migrate/parse-pipe.js.map +1 -1
- package/dist/migrate/parser-utils.d.ts +5 -0
- package/dist/migrate/parser-utils.d.ts.map +1 -1
- package/dist/migrate/parser-utils.js +22 -0
- package/dist/migrate/parser-utils.js.map +1 -1
- package/dist/migrate/types.d.ts +25 -3
- 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 +13 -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/pipe.d.ts +90 -3
- package/dist/schema/pipe.d.ts.map +1 -1
- package/dist/schema/pipe.js +84 -0
- package/dist/schema/pipe.js.map +1 -1
- package/dist/schema/pipe.test.js +70 -1
- package/dist/schema/pipe.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/package.json +1 -1
- package/src/cli/commands/migrate.test.ts +859 -8
- package/src/cli/commands/migrate.ts +35 -0
- 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/generator/pipe.test.ts +56 -1
- package/src/generator/pipe.ts +41 -1
- package/src/index.test.ts +4 -0
- package/src/index.ts +12 -0
- package/src/migrate/emit-ts.ts +161 -48
- package/src/migrate/parse-connection.ts +15 -2
- package/src/migrate/parse-datasource.ts +134 -71
- package/src/migrate/parse-pipe.ts +364 -69
- package/src/migrate/parser-utils.ts +36 -1
- package/src/migrate/types.ts +28 -3
- package/src/schema/connection.ts +2 -0
- package/src/schema/datasource.test.ts +17 -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/pipe.test.ts +89 -0
- package/src/schema/pipe.ts +188 -4
- 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
|
@@ -3,49 +3,40 @@ import {
|
|
|
3
3
|
MigrationParseError,
|
|
4
4
|
isBlank,
|
|
5
5
|
parseDirectiveLine,
|
|
6
|
+
parseQuotedValue,
|
|
7
|
+
readDirectiveBlock,
|
|
6
8
|
splitLines,
|
|
7
9
|
splitTopLevelComma,
|
|
8
|
-
stripIndent,
|
|
9
10
|
} from "./parser-utils.js";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (isBlank(line)) {
|
|
29
|
-
let j = i + 1;
|
|
30
|
-
while (j < lines.length && isBlank(lines[j] ?? "")) {
|
|
31
|
-
j += 1;
|
|
32
|
-
}
|
|
33
|
-
if (j < lines.length && (lines[j] ?? "").startsWith(" ")) {
|
|
34
|
-
collected.push("");
|
|
35
|
-
i += 1;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
break;
|
|
12
|
+
const PIPE_DIRECTIVES = new Set([
|
|
13
|
+
"DESCRIPTION",
|
|
14
|
+
"NODE",
|
|
15
|
+
"SQL",
|
|
16
|
+
"TYPE",
|
|
17
|
+
"CACHE",
|
|
18
|
+
"DATASOURCE",
|
|
19
|
+
"DEPLOYMENT_METHOD",
|
|
20
|
+
"TARGET_DATASOURCE",
|
|
21
|
+
"COPY_SCHEDULE",
|
|
22
|
+
"COPY_MODE",
|
|
23
|
+
"TOKEN",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function isPipeDirectiveLine(line: string): boolean {
|
|
27
|
+
if (!line) {
|
|
28
|
+
return false;
|
|
41
29
|
}
|
|
42
|
-
|
|
43
|
-
return
|
|
30
|
+
const { key } = parseDirectiveLine(line);
|
|
31
|
+
return PIPE_DIRECTIVES.has(key);
|
|
44
32
|
}
|
|
45
33
|
|
|
46
34
|
function nextNonBlank(lines: string[], startIndex: number): number {
|
|
47
35
|
let i = startIndex;
|
|
48
|
-
while (
|
|
36
|
+
while (
|
|
37
|
+
i < lines.length &&
|
|
38
|
+
(isBlank(lines[i] ?? "") || (lines[i] ?? "").trim().startsWith("#"))
|
|
39
|
+
) {
|
|
49
40
|
i += 1;
|
|
50
41
|
}
|
|
51
42
|
return i;
|
|
@@ -114,11 +105,14 @@ function mapTemplateFunctionToParamType(func: string): string | null {
|
|
|
114
105
|
return null;
|
|
115
106
|
}
|
|
116
107
|
|
|
117
|
-
function parseParamDefault(rawValue: string): string | number {
|
|
108
|
+
function parseParamDefault(rawValue: string): string | number | boolean {
|
|
118
109
|
const trimmed = rawValue.trim();
|
|
119
110
|
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
120
111
|
return Number(trimmed);
|
|
121
112
|
}
|
|
113
|
+
if (/^(true|false)$/i.test(trimmed)) {
|
|
114
|
+
return trimmed.toLowerCase() === "true";
|
|
115
|
+
}
|
|
122
116
|
if (
|
|
123
117
|
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
124
118
|
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
@@ -128,6 +122,92 @@ function parseParamDefault(rawValue: string): string | number {
|
|
|
128
122
|
throw new Error(`Unsupported parameter default value: "${rawValue}"`);
|
|
129
123
|
}
|
|
130
124
|
|
|
125
|
+
function parseKeywordArgument(rawArg: string): { key: string; value: string } | null {
|
|
126
|
+
const equalsIndex = rawArg.indexOf("=");
|
|
127
|
+
if (equalsIndex <= 0) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const key = rawArg.slice(0, equalsIndex).trim();
|
|
132
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const value = rawArg.slice(equalsIndex + 1).trim();
|
|
137
|
+
if (!value) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { key, value };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseRequiredFlag(rawValue: string): boolean {
|
|
145
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
146
|
+
if (normalized === "true" || normalized === "1") {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (normalized === "false" || normalized === "0") {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`Unsupported required value: "${rawValue}"`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseParamOptions(rawArgs: string[]): {
|
|
156
|
+
defaultValue?: string | number | boolean;
|
|
157
|
+
required?: boolean;
|
|
158
|
+
description?: string;
|
|
159
|
+
} {
|
|
160
|
+
let positionalDefault: string | number | boolean | undefined;
|
|
161
|
+
let keywordDefault: string | number | boolean | undefined;
|
|
162
|
+
let required: boolean | undefined;
|
|
163
|
+
let description: string | undefined;
|
|
164
|
+
|
|
165
|
+
for (const rawArg of rawArgs) {
|
|
166
|
+
const trimmed = rawArg.trim();
|
|
167
|
+
if (!trimmed) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const keyword = parseKeywordArgument(trimmed);
|
|
172
|
+
if (!keyword) {
|
|
173
|
+
if (positionalDefault === undefined) {
|
|
174
|
+
positionalDefault = parseParamDefault(trimmed);
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const keyLower = keyword.key.toLowerCase();
|
|
180
|
+
if (keyLower === "default") {
|
|
181
|
+
keywordDefault = parseParamDefault(keyword.value);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (keyLower === "required") {
|
|
185
|
+
required = parseRequiredFlag(keyword.value);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (keyLower === "description") {
|
|
189
|
+
const parsedDescription = parseParamDefault(keyword.value);
|
|
190
|
+
if (typeof parsedDescription !== "string") {
|
|
191
|
+
throw new Error(`Unsupported description value: "${keyword.value}"`);
|
|
192
|
+
}
|
|
193
|
+
description = parsedDescription;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let defaultValue = keywordDefault ?? positionalDefault;
|
|
199
|
+
if (keywordDefault !== undefined && positionalDefault !== undefined) {
|
|
200
|
+
if (keywordDefault !== positionalDefault) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Parameter has conflicting positional and keyword defaults: "${positionalDefault}" and "${keywordDefault}".`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
defaultValue = positionalDefault;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { defaultValue, required, description };
|
|
209
|
+
}
|
|
210
|
+
|
|
131
211
|
function inferParamsFromSql(
|
|
132
212
|
sql: string,
|
|
133
213
|
filePath: string,
|
|
@@ -170,10 +250,15 @@ function inferParamsFromSql(
|
|
|
170
250
|
);
|
|
171
251
|
}
|
|
172
252
|
|
|
173
|
-
let defaultValue: string | number | undefined;
|
|
253
|
+
let defaultValue: string | number | boolean | undefined;
|
|
254
|
+
let required: boolean | undefined;
|
|
255
|
+
let description: string | undefined;
|
|
174
256
|
if (args.length > 1) {
|
|
175
257
|
try {
|
|
176
|
-
|
|
258
|
+
const parsedOptions = parseParamOptions(args.slice(1));
|
|
259
|
+
defaultValue = parsedOptions.defaultValue;
|
|
260
|
+
required = parsedOptions.required;
|
|
261
|
+
description = parsedOptions.description;
|
|
177
262
|
} catch (error) {
|
|
178
263
|
throw new MigrationParseError(
|
|
179
264
|
filePath,
|
|
@@ -206,14 +291,21 @@ function inferParamsFromSql(
|
|
|
206
291
|
}
|
|
207
292
|
if (existing.defaultValue === undefined && defaultValue !== undefined) {
|
|
208
293
|
existing.defaultValue = defaultValue;
|
|
209
|
-
existing.required = false;
|
|
210
294
|
}
|
|
295
|
+
if (existing.description === undefined && description !== undefined) {
|
|
296
|
+
existing.description = description;
|
|
297
|
+
}
|
|
298
|
+
const optionalInAnyUsage =
|
|
299
|
+
existing.required === false || required === false || defaultValue !== undefined;
|
|
300
|
+
existing.required = !optionalInAnyUsage;
|
|
211
301
|
} else {
|
|
302
|
+
const isRequired = required ?? defaultValue === undefined;
|
|
212
303
|
params.set(paramName, {
|
|
213
304
|
name: paramName,
|
|
214
305
|
type: mappedType,
|
|
215
|
-
required:
|
|
306
|
+
required: isRequired,
|
|
216
307
|
defaultValue,
|
|
308
|
+
description,
|
|
217
309
|
});
|
|
218
310
|
}
|
|
219
311
|
|
|
@@ -224,7 +316,13 @@ function inferParamsFromSql(
|
|
|
224
316
|
}
|
|
225
317
|
|
|
226
318
|
function parseToken(filePath: string, resourceName: string, value: string): PipeTokenModel {
|
|
227
|
-
const
|
|
319
|
+
const trimmed = value.trim();
|
|
320
|
+
const quotedMatch = trimmed.match(/^"([^"]+)"(?:\s+(READ))?$/);
|
|
321
|
+
if (quotedMatch) {
|
|
322
|
+
return { name: quotedMatch[1] ?? "", scope: "READ" };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
228
326
|
if (parts.length === 0) {
|
|
229
327
|
throw new MigrationParseError(filePath, "pipe", resourceName, "Invalid TOKEN line.");
|
|
230
328
|
}
|
|
@@ -237,7 +335,13 @@ function parseToken(filePath: string, resourceName: string, value: string): Pipe
|
|
|
237
335
|
);
|
|
238
336
|
}
|
|
239
337
|
|
|
240
|
-
const
|
|
338
|
+
const rawTokenName = parts[0] ?? "";
|
|
339
|
+
const tokenName =
|
|
340
|
+
rawTokenName.startsWith('"') &&
|
|
341
|
+
rawTokenName.endsWith('"') &&
|
|
342
|
+
rawTokenName.length >= 2
|
|
343
|
+
? rawTokenName.slice(1, -1)
|
|
344
|
+
: rawTokenName;
|
|
241
345
|
const scope = parts[1] ?? "READ";
|
|
242
346
|
if (scope !== "READ") {
|
|
243
347
|
throw new MigrationParseError(
|
|
@@ -263,27 +367,27 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
263
367
|
let copyTargetDatasource: string | undefined;
|
|
264
368
|
let copySchedule: string | undefined;
|
|
265
369
|
let copyMode: "append" | "replace" | undefined;
|
|
370
|
+
let exportService: "kafka" | "s3" | undefined;
|
|
371
|
+
let exportConnectionName: string | undefined;
|
|
372
|
+
let exportTopic: string | undefined;
|
|
373
|
+
let exportBucketUri: string | undefined;
|
|
374
|
+
let exportFileTemplate: string | undefined;
|
|
375
|
+
let exportFormat: string | undefined;
|
|
376
|
+
let exportSchedule: string | undefined;
|
|
377
|
+
let exportStrategy: "create_new" | "replace" | undefined;
|
|
378
|
+
let exportCompression: "none" | "gzip" | "snappy" | undefined;
|
|
266
379
|
|
|
267
380
|
let i = 0;
|
|
268
381
|
while (i < lines.length) {
|
|
269
382
|
const line = (lines[i] ?? "").trim();
|
|
270
|
-
if (!line) {
|
|
383
|
+
if (!line || line.startsWith("#")) {
|
|
271
384
|
i += 1;
|
|
272
385
|
continue;
|
|
273
386
|
}
|
|
274
387
|
|
|
275
388
|
if (line === "DESCRIPTION >") {
|
|
276
|
-
const block =
|
|
277
|
-
if (
|
|
278
|
-
throw new MigrationParseError(
|
|
279
|
-
resource.filePath,
|
|
280
|
-
"pipe",
|
|
281
|
-
resource.name,
|
|
282
|
-
"DESCRIPTION block is empty."
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (!description) {
|
|
389
|
+
const block = readDirectiveBlock(lines, i + 1, isPipeDirectiveLine);
|
|
390
|
+
if (description === undefined) {
|
|
287
391
|
description = block.lines.join("\n");
|
|
288
392
|
} else if (nodes.length > 0) {
|
|
289
393
|
nodes[nodes.length - 1] = {
|
|
@@ -318,15 +422,7 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
318
422
|
|
|
319
423
|
let nodeDescription: string | undefined;
|
|
320
424
|
if ((lines[i] ?? "").trim() === "DESCRIPTION >") {
|
|
321
|
-
const descriptionBlock =
|
|
322
|
-
if (descriptionBlock.lines.length === 0) {
|
|
323
|
-
throw new MigrationParseError(
|
|
324
|
-
resource.filePath,
|
|
325
|
-
"pipe",
|
|
326
|
-
resource.name,
|
|
327
|
-
`Node "${nodeName}" has an empty DESCRIPTION block.`
|
|
328
|
-
);
|
|
329
|
-
}
|
|
425
|
+
const descriptionBlock = readDirectiveBlock(lines, i + 1, isPipeDirectiveLine);
|
|
330
426
|
nodeDescription = descriptionBlock.lines.join("\n");
|
|
331
427
|
i = descriptionBlock.nextIndex;
|
|
332
428
|
i = nextNonBlank(lines, i);
|
|
@@ -340,7 +436,7 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
340
436
|
`Node "${nodeName}" is missing SQL > block.`
|
|
341
437
|
);
|
|
342
438
|
}
|
|
343
|
-
const sqlBlock =
|
|
439
|
+
const sqlBlock = readDirectiveBlock(lines, i + 1, isPipeDirectiveLine);
|
|
344
440
|
if (sqlBlock.lines.length === 0) {
|
|
345
441
|
throw new MigrationParseError(
|
|
346
442
|
resource.filePath,
|
|
@@ -374,22 +470,26 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
374
470
|
|
|
375
471
|
const { key, value } = parseDirectiveLine(line);
|
|
376
472
|
switch (key) {
|
|
377
|
-
case "TYPE":
|
|
378
|
-
|
|
473
|
+
case "TYPE": {
|
|
474
|
+
const normalizedType = parseQuotedValue(value).toLowerCase();
|
|
475
|
+
if (normalizedType === "endpoint") {
|
|
379
476
|
pipeType = "endpoint";
|
|
380
|
-
} else if (
|
|
477
|
+
} else if (normalizedType === "materialized") {
|
|
381
478
|
pipeType = "materialized";
|
|
382
|
-
} else if (
|
|
479
|
+
} else if (normalizedType === "copy") {
|
|
383
480
|
pipeType = "copy";
|
|
481
|
+
} else if (normalizedType === "sink") {
|
|
482
|
+
pipeType = "sink";
|
|
384
483
|
} else {
|
|
385
484
|
throw new MigrationParseError(
|
|
386
485
|
resource.filePath,
|
|
387
486
|
"pipe",
|
|
388
487
|
resource.name,
|
|
389
|
-
`Unsupported TYPE value in strict mode: "${value}"`
|
|
488
|
+
`Unsupported TYPE value in strict mode: "${parseQuotedValue(value)}"`
|
|
390
489
|
);
|
|
391
490
|
}
|
|
392
491
|
break;
|
|
492
|
+
}
|
|
393
493
|
case "CACHE": {
|
|
394
494
|
const ttl = Number(value);
|
|
395
495
|
if (!Number.isFinite(ttl) || ttl < 0) {
|
|
@@ -434,6 +534,63 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
434
534
|
}
|
|
435
535
|
copyMode = value;
|
|
436
536
|
break;
|
|
537
|
+
case "EXPORT_SERVICE": {
|
|
538
|
+
const normalized = parseQuotedValue(value).toLowerCase();
|
|
539
|
+
if (normalized !== "kafka" && normalized !== "s3") {
|
|
540
|
+
throw new MigrationParseError(
|
|
541
|
+
resource.filePath,
|
|
542
|
+
"pipe",
|
|
543
|
+
resource.name,
|
|
544
|
+
`Unsupported EXPORT_SERVICE in strict mode: "${value}"`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
exportService = normalized;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case "EXPORT_CONNECTION_NAME":
|
|
551
|
+
exportConnectionName = parseQuotedValue(value);
|
|
552
|
+
break;
|
|
553
|
+
case "EXPORT_KAFKA_TOPIC":
|
|
554
|
+
exportTopic = parseQuotedValue(value);
|
|
555
|
+
break;
|
|
556
|
+
case "EXPORT_BUCKET_URI":
|
|
557
|
+
exportBucketUri = parseQuotedValue(value);
|
|
558
|
+
break;
|
|
559
|
+
case "EXPORT_FILE_TEMPLATE":
|
|
560
|
+
exportFileTemplate = parseQuotedValue(value);
|
|
561
|
+
break;
|
|
562
|
+
case "EXPORT_FORMAT":
|
|
563
|
+
exportFormat = parseQuotedValue(value);
|
|
564
|
+
break;
|
|
565
|
+
case "EXPORT_SCHEDULE":
|
|
566
|
+
exportSchedule = parseQuotedValue(value);
|
|
567
|
+
break;
|
|
568
|
+
case "EXPORT_STRATEGY": {
|
|
569
|
+
const normalized = parseQuotedValue(value).toLowerCase();
|
|
570
|
+
if (normalized !== "create_new" && normalized !== "replace") {
|
|
571
|
+
throw new MigrationParseError(
|
|
572
|
+
resource.filePath,
|
|
573
|
+
"pipe",
|
|
574
|
+
resource.name,
|
|
575
|
+
`Unsupported EXPORT_STRATEGY in strict mode: "${value}"`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
exportStrategy = normalized;
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case "EXPORT_COMPRESSION": {
|
|
582
|
+
const normalized = parseQuotedValue(value).toLowerCase();
|
|
583
|
+
if (normalized !== "none" && normalized !== "gzip" && normalized !== "snappy") {
|
|
584
|
+
throw new MigrationParseError(
|
|
585
|
+
resource.filePath,
|
|
586
|
+
"pipe",
|
|
587
|
+
resource.name,
|
|
588
|
+
`Unsupported EXPORT_COMPRESSION in strict mode: "${value}"`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
exportCompression = normalized;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
437
594
|
case "TOKEN":
|
|
438
595
|
tokens.push(parseToken(resource.filePath, resource.name, value));
|
|
439
596
|
break;
|
|
@@ -485,6 +642,144 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
485
642
|
);
|
|
486
643
|
}
|
|
487
644
|
|
|
645
|
+
const hasSinkDirectives =
|
|
646
|
+
exportService !== undefined ||
|
|
647
|
+
exportConnectionName !== undefined ||
|
|
648
|
+
exportTopic !== undefined ||
|
|
649
|
+
exportBucketUri !== undefined ||
|
|
650
|
+
exportFileTemplate !== undefined ||
|
|
651
|
+
exportFormat !== undefined ||
|
|
652
|
+
exportSchedule !== undefined ||
|
|
653
|
+
exportStrategy !== undefined ||
|
|
654
|
+
exportCompression !== undefined;
|
|
655
|
+
|
|
656
|
+
if (pipeType !== "sink" && hasSinkDirectives) {
|
|
657
|
+
throw new MigrationParseError(
|
|
658
|
+
resource.filePath,
|
|
659
|
+
"pipe",
|
|
660
|
+
resource.name,
|
|
661
|
+
"EXPORT_* directives are only supported for TYPE sink."
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
let sink: PipeModel["sink"];
|
|
666
|
+
if (pipeType === "sink") {
|
|
667
|
+
if (!exportConnectionName) {
|
|
668
|
+
throw new MigrationParseError(
|
|
669
|
+
resource.filePath,
|
|
670
|
+
"pipe",
|
|
671
|
+
resource.name,
|
|
672
|
+
"EXPORT_CONNECTION_NAME is required for TYPE sink."
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const hasKafkaDirectives = exportTopic !== undefined;
|
|
677
|
+
const hasS3Directives =
|
|
678
|
+
exportBucketUri !== undefined ||
|
|
679
|
+
exportFileTemplate !== undefined ||
|
|
680
|
+
exportFormat !== undefined ||
|
|
681
|
+
exportCompression !== undefined;
|
|
682
|
+
|
|
683
|
+
if (hasKafkaDirectives && hasS3Directives) {
|
|
684
|
+
throw new MigrationParseError(
|
|
685
|
+
resource.filePath,
|
|
686
|
+
"pipe",
|
|
687
|
+
resource.name,
|
|
688
|
+
"Sink pipe cannot mix Kafka and S3 export directives."
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const inferredService =
|
|
693
|
+
exportService ?? (hasKafkaDirectives ? "kafka" : hasS3Directives ? "s3" : undefined);
|
|
694
|
+
|
|
695
|
+
if (!inferredService) {
|
|
696
|
+
throw new MigrationParseError(
|
|
697
|
+
resource.filePath,
|
|
698
|
+
"pipe",
|
|
699
|
+
resource.name,
|
|
700
|
+
"Sink pipe must define EXPORT_SERVICE or include service-specific export directives."
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (inferredService === "kafka") {
|
|
705
|
+
if (hasS3Directives) {
|
|
706
|
+
throw new MigrationParseError(
|
|
707
|
+
resource.filePath,
|
|
708
|
+
"pipe",
|
|
709
|
+
resource.name,
|
|
710
|
+
"S3 export directives are not valid for Kafka sinks."
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
if (!exportTopic) {
|
|
714
|
+
throw new MigrationParseError(
|
|
715
|
+
resource.filePath,
|
|
716
|
+
"pipe",
|
|
717
|
+
resource.name,
|
|
718
|
+
"EXPORT_KAFKA_TOPIC is required for Kafka sinks."
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
if (!exportSchedule) {
|
|
722
|
+
throw new MigrationParseError(
|
|
723
|
+
resource.filePath,
|
|
724
|
+
"pipe",
|
|
725
|
+
resource.name,
|
|
726
|
+
"EXPORT_SCHEDULE is required for Kafka sinks."
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
if (exportStrategy !== undefined) {
|
|
730
|
+
throw new MigrationParseError(
|
|
731
|
+
resource.filePath,
|
|
732
|
+
"pipe",
|
|
733
|
+
resource.name,
|
|
734
|
+
"EXPORT_STRATEGY is only valid for S3 sinks."
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
if (exportCompression !== undefined) {
|
|
738
|
+
throw new MigrationParseError(
|
|
739
|
+
resource.filePath,
|
|
740
|
+
"pipe",
|
|
741
|
+
resource.name,
|
|
742
|
+
"EXPORT_COMPRESSION is only valid for S3 sinks."
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
sink = {
|
|
747
|
+
service: "kafka",
|
|
748
|
+
connectionName: exportConnectionName,
|
|
749
|
+
topic: exportTopic,
|
|
750
|
+
schedule: exportSchedule,
|
|
751
|
+
};
|
|
752
|
+
} else {
|
|
753
|
+
if (hasKafkaDirectives) {
|
|
754
|
+
throw new MigrationParseError(
|
|
755
|
+
resource.filePath,
|
|
756
|
+
"pipe",
|
|
757
|
+
resource.name,
|
|
758
|
+
"Kafka export directives are not valid for S3 sinks."
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
if (!exportBucketUri || !exportFileTemplate || !exportFormat || !exportSchedule) {
|
|
762
|
+
throw new MigrationParseError(
|
|
763
|
+
resource.filePath,
|
|
764
|
+
"pipe",
|
|
765
|
+
resource.name,
|
|
766
|
+
"S3 sinks require EXPORT_BUCKET_URI, EXPORT_FILE_TEMPLATE, EXPORT_FORMAT, and EXPORT_SCHEDULE."
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
sink = {
|
|
771
|
+
service: "s3",
|
|
772
|
+
connectionName: exportConnectionName,
|
|
773
|
+
bucketUri: exportBucketUri,
|
|
774
|
+
fileTemplate: exportFileTemplate,
|
|
775
|
+
format: exportFormat,
|
|
776
|
+
schedule: exportSchedule,
|
|
777
|
+
strategy: exportStrategy,
|
|
778
|
+
compression: exportCompression,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
488
783
|
const params =
|
|
489
784
|
pipeType === "materialized" || pipeType === "copy"
|
|
490
785
|
? []
|
|
@@ -510,9 +805,9 @@ export function parsePipeFile(resource: ResourceFile): PipeModel {
|
|
|
510
805
|
copyTargetDatasource,
|
|
511
806
|
copySchedule,
|
|
512
807
|
copyMode,
|
|
808
|
+
sink,
|
|
513
809
|
tokens,
|
|
514
810
|
params,
|
|
515
811
|
inferredOutputColumns,
|
|
516
812
|
};
|
|
517
813
|
}
|
|
518
|
-
|
|
@@ -27,6 +27,42 @@ export function stripIndent(line: string): string {
|
|
|
27
27
|
return line.trimStart();
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export interface BlockReadResult {
|
|
31
|
+
lines: string[];
|
|
32
|
+
nextIndex: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readDirectiveBlock(
|
|
36
|
+
lines: string[],
|
|
37
|
+
startIndex: number,
|
|
38
|
+
isDirectiveLine: (line: string) => boolean
|
|
39
|
+
): BlockReadResult {
|
|
40
|
+
const collected: string[] = [];
|
|
41
|
+
let i = startIndex;
|
|
42
|
+
|
|
43
|
+
while (i < lines.length) {
|
|
44
|
+
const line = (lines[i] ?? "").trim();
|
|
45
|
+
if (isDirectiveLine(line)) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
collected.push(line);
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let first = 0;
|
|
53
|
+
while (first < collected.length && collected[first] === "") {
|
|
54
|
+
first += 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let last = collected.length - 1;
|
|
58
|
+
while (last >= first && collected[last] === "") {
|
|
59
|
+
last -= 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const normalized = first <= last ? collected.slice(first, last + 1) : [];
|
|
63
|
+
return { lines: normalized, nextIndex: i };
|
|
64
|
+
}
|
|
65
|
+
|
|
30
66
|
export function splitCommaSeparated(input: string): string[] {
|
|
31
67
|
return input
|
|
32
68
|
.split(",")
|
|
@@ -157,4 +193,3 @@ export function splitTopLevelComma(input: string): string[] {
|
|
|
157
193
|
|
|
158
194
|
return parts;
|
|
159
195
|
}
|
|
160
|
-
|
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 {
|
|
@@ -61,7 +63,7 @@ export interface DatasourceModel {
|
|
|
61
63
|
filePath: string;
|
|
62
64
|
description?: string;
|
|
63
65
|
columns: DatasourceColumnModel[];
|
|
64
|
-
engine
|
|
66
|
+
engine?: DatasourceEngineModel;
|
|
65
67
|
kafka?: DatasourceKafkaModel;
|
|
66
68
|
s3?: DatasourceS3Model;
|
|
67
69
|
forwardQuery?: string;
|
|
@@ -80,13 +82,34 @@ export interface PipeTokenModel {
|
|
|
80
82
|
scope: "READ";
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
export type PipeTypeModel = "pipe" | "endpoint" | "materialized" | "copy";
|
|
85
|
+
export type PipeTypeModel = "pipe" | "endpoint" | "materialized" | "copy" | "sink";
|
|
86
|
+
|
|
87
|
+
export interface PipeKafkaSinkModel {
|
|
88
|
+
service: "kafka";
|
|
89
|
+
connectionName: string;
|
|
90
|
+
topic: string;
|
|
91
|
+
schedule: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface PipeS3SinkModel {
|
|
95
|
+
service: "s3";
|
|
96
|
+
connectionName: string;
|
|
97
|
+
bucketUri: string;
|
|
98
|
+
fileTemplate: string;
|
|
99
|
+
format: string;
|
|
100
|
+
schedule: string;
|
|
101
|
+
strategy?: "create_new" | "replace";
|
|
102
|
+
compression?: "none" | "gzip" | "snappy";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type PipeSinkModel = PipeKafkaSinkModel | PipeS3SinkModel;
|
|
84
106
|
|
|
85
107
|
export interface PipeParamModel {
|
|
86
108
|
name: string;
|
|
87
109
|
type: string;
|
|
88
110
|
required: boolean;
|
|
89
|
-
defaultValue?: string | number;
|
|
111
|
+
defaultValue?: string | number | boolean;
|
|
112
|
+
description?: string;
|
|
90
113
|
}
|
|
91
114
|
|
|
92
115
|
export interface PipeModel {
|
|
@@ -102,6 +125,7 @@ export interface PipeModel {
|
|
|
102
125
|
copyTargetDatasource?: string;
|
|
103
126
|
copySchedule?: string;
|
|
104
127
|
copyMode?: "append" | "replace";
|
|
128
|
+
sink?: PipeSinkModel;
|
|
105
129
|
tokens: PipeTokenModel[];
|
|
106
130
|
params: PipeParamModel[];
|
|
107
131
|
inferredOutputColumns: string[];
|
|
@@ -117,6 +141,7 @@ export interface KafkaConnectionModel {
|
|
|
117
141
|
saslMechanism?: "PLAIN" | "SCRAM-SHA-256" | "SCRAM-SHA-512" | "OAUTHBEARER";
|
|
118
142
|
key?: string;
|
|
119
143
|
secret?: string;
|
|
144
|
+
schemaRegistryUrl?: string;
|
|
120
145
|
sslCaPem?: string;
|
|
121
146
|
}
|
|
122
147
|
|
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
|
}
|