@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.
Files changed (95) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +29 -3
  3. package/dist/api/resources.d.ts +72 -1
  4. package/dist/api/resources.d.ts.map +1 -1
  5. package/dist/api/resources.js +197 -1
  6. package/dist/api/resources.js.map +1 -1
  7. package/dist/api/resources.test.js +82 -1
  8. package/dist/api/resources.test.js.map +1 -1
  9. package/dist/cli/commands/migrate.d.ts +11 -0
  10. package/dist/cli/commands/migrate.d.ts.map +1 -0
  11. package/dist/cli/commands/migrate.js +196 -0
  12. package/dist/cli/commands/migrate.js.map +1 -0
  13. package/dist/cli/commands/migrate.test.d.ts +2 -0
  14. package/dist/cli/commands/migrate.test.d.ts.map +1 -0
  15. package/dist/cli/commands/migrate.test.js +473 -0
  16. package/dist/cli/commands/migrate.test.js.map +1 -0
  17. package/dist/cli/commands/pull.d.ts +59 -0
  18. package/dist/cli/commands/pull.d.ts.map +1 -0
  19. package/dist/cli/commands/pull.js +104 -0
  20. package/dist/cli/commands/pull.js.map +1 -0
  21. package/dist/cli/commands/pull.test.d.ts +2 -0
  22. package/dist/cli/commands/pull.test.d.ts.map +1 -0
  23. package/dist/cli/commands/pull.test.js +140 -0
  24. package/dist/cli/commands/pull.test.js.map +1 -0
  25. package/dist/cli/config.d.ts +10 -0
  26. package/dist/cli/config.d.ts.map +1 -1
  27. package/dist/cli/config.js +22 -0
  28. package/dist/cli/config.js.map +1 -1
  29. package/dist/cli/index.js +77 -0
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/generator/client.js +2 -2
  32. package/dist/generator/client.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/migrate/discovery.d.ts +7 -0
  38. package/dist/migrate/discovery.d.ts.map +1 -0
  39. package/dist/migrate/discovery.js +125 -0
  40. package/dist/migrate/discovery.js.map +1 -0
  41. package/dist/migrate/emit-ts.d.ts +4 -0
  42. package/dist/migrate/emit-ts.d.ts.map +1 -0
  43. package/dist/migrate/emit-ts.js +387 -0
  44. package/dist/migrate/emit-ts.js.map +1 -0
  45. package/dist/migrate/parse-connection.d.ts +3 -0
  46. package/dist/migrate/parse-connection.d.ts.map +1 -0
  47. package/dist/migrate/parse-connection.js +74 -0
  48. package/dist/migrate/parse-connection.js.map +1 -0
  49. package/dist/migrate/parse-datasource.d.ts +3 -0
  50. package/dist/migrate/parse-datasource.d.ts.map +1 -0
  51. package/dist/migrate/parse-datasource.js +324 -0
  52. package/dist/migrate/parse-datasource.js.map +1 -0
  53. package/dist/migrate/parse-pipe.d.ts +3 -0
  54. package/dist/migrate/parse-pipe.d.ts.map +1 -0
  55. package/dist/migrate/parse-pipe.js +332 -0
  56. package/dist/migrate/parse-pipe.js.map +1 -0
  57. package/dist/migrate/parse.d.ts +3 -0
  58. package/dist/migrate/parse.d.ts.map +1 -0
  59. package/dist/migrate/parse.js +18 -0
  60. package/dist/migrate/parse.js.map +1 -0
  61. package/dist/migrate/parser-utils.d.ts +20 -0
  62. package/dist/migrate/parser-utils.d.ts.map +1 -0
  63. package/dist/migrate/parser-utils.js +130 -0
  64. package/dist/migrate/parser-utils.js.map +1 -0
  65. package/dist/migrate/types.d.ts +110 -0
  66. package/dist/migrate/types.d.ts.map +1 -0
  67. package/dist/migrate/types.js +2 -0
  68. package/dist/migrate/types.js.map +1 -0
  69. package/dist/schema/project.d.ts +20 -9
  70. package/dist/schema/project.d.ts.map +1 -1
  71. package/dist/schema/project.js +127 -136
  72. package/dist/schema/project.js.map +1 -1
  73. package/dist/schema/project.test.js +22 -0
  74. package/dist/schema/project.test.js.map +1 -1
  75. package/package.json +2 -1
  76. package/src/api/resources.test.ts +121 -0
  77. package/src/api/resources.ts +292 -1
  78. package/src/cli/commands/migrate.test.ts +564 -0
  79. package/src/cli/commands/migrate.ts +240 -0
  80. package/src/cli/commands/pull.test.ts +173 -0
  81. package/src/cli/commands/pull.ts +177 -0
  82. package/src/cli/config.ts +26 -0
  83. package/src/cli/index.ts +112 -0
  84. package/src/generator/client.ts +2 -2
  85. package/src/index.ts +1 -1
  86. package/src/migrate/discovery.ts +151 -0
  87. package/src/migrate/emit-ts.ts +469 -0
  88. package/src/migrate/parse-connection.ts +128 -0
  89. package/src/migrate/parse-datasource.ts +453 -0
  90. package/src/migrate/parse-pipe.ts +518 -0
  91. package/src/migrate/parse.ts +20 -0
  92. package/src/migrate/parser-utils.ts +160 -0
  93. package/src/migrate/types.ts +125 -0
  94. package/src/schema/project.test.ts +28 -0
  95. package/src/schema/project.ts +173 -181
@@ -0,0 +1,453 @@
1
+ import type { DatasourceModel, DatasourceTokenModel, ResourceFile } from "./types.js";
2
+ import {
3
+ MigrationParseError,
4
+ isBlank,
5
+ parseDirectiveLine,
6
+ parseQuotedValue,
7
+ splitCommaSeparated,
8
+ splitLines,
9
+ splitTopLevelComma,
10
+ stripIndent,
11
+ } from "./parser-utils.js";
12
+
13
+ interface BlockReadResult {
14
+ lines: string[];
15
+ nextIndex: number;
16
+ }
17
+
18
+ function readIndentedBlock(lines: string[], startIndex: number): BlockReadResult {
19
+ const collected: string[] = [];
20
+ let i = startIndex;
21
+
22
+ while (i < lines.length) {
23
+ const line = lines[i] ?? "";
24
+ if (line.startsWith(" ")) {
25
+ collected.push(stripIndent(line));
26
+ i += 1;
27
+ continue;
28
+ }
29
+
30
+ if (isBlank(line)) {
31
+ let j = i + 1;
32
+ while (j < lines.length && isBlank(lines[j] ?? "")) {
33
+ j += 1;
34
+ }
35
+ if (j < lines.length && (lines[j] ?? "").startsWith(" ")) {
36
+ collected.push("");
37
+ i += 1;
38
+ continue;
39
+ }
40
+ }
41
+
42
+ break;
43
+ }
44
+
45
+ return { lines: collected, nextIndex: i };
46
+ }
47
+
48
+ function findTokenOutsideContexts(input: string, token: string): number {
49
+ let depth = 0;
50
+ let inSingle = false;
51
+ let inDouble = false;
52
+ let inBacktick = false;
53
+
54
+ for (let i = 0; i <= input.length - token.length; i += 1) {
55
+ const char = input[i];
56
+ const prev = i > 0 ? input[i - 1] : "";
57
+
58
+ if (char === "'" && !inDouble && !inBacktick && prev !== "\\") {
59
+ inSingle = !inSingle;
60
+ } else if (char === '"' && !inSingle && !inBacktick && prev !== "\\") {
61
+ inDouble = !inDouble;
62
+ } else if (char === "`" && !inSingle && !inDouble) {
63
+ inBacktick = !inBacktick;
64
+ } else if (!inSingle && !inDouble && !inBacktick) {
65
+ if (char === "(") {
66
+ depth += 1;
67
+ } else if (char === ")") {
68
+ depth -= 1;
69
+ }
70
+ }
71
+
72
+ if (!inSingle && !inDouble && !inBacktick && depth === 0) {
73
+ if (input.slice(i, i + token.length) === token) {
74
+ return i;
75
+ }
76
+ }
77
+ }
78
+
79
+ return -1;
80
+ }
81
+
82
+ function parseColumnLine(filePath: string, resourceName: string, rawLine: string) {
83
+ const line = rawLine.trim().replace(/,$/, "");
84
+ if (!line) {
85
+ throw new MigrationParseError(filePath, "datasource", resourceName, "Empty schema line.");
86
+ }
87
+
88
+ const firstSpace = line.search(/\s/);
89
+ if (firstSpace === -1) {
90
+ throw new MigrationParseError(
91
+ filePath,
92
+ "datasource",
93
+ resourceName,
94
+ `Invalid schema column definition: "${rawLine}"`
95
+ );
96
+ }
97
+
98
+ const columnName = line.slice(0, firstSpace).trim();
99
+ let rest = line.slice(firstSpace + 1).trim();
100
+
101
+ const codecMatch = rest.match(/\s+CODEC\((.+)\)\s*$/);
102
+ const codec = codecMatch ? codecMatch[1].trim() : undefined;
103
+ if (codecMatch?.index !== undefined) {
104
+ rest = rest.slice(0, codecMatch.index).trim();
105
+ }
106
+
107
+ let defaultExpression: string | undefined;
108
+ const defaultMarkerIndex = findTokenOutsideContexts(rest, " DEFAULT ");
109
+ if (defaultMarkerIndex >= 0) {
110
+ defaultExpression = rest.slice(defaultMarkerIndex + " DEFAULT ".length).trim();
111
+ rest = rest.slice(0, defaultMarkerIndex).trim();
112
+ }
113
+
114
+ let jsonPath: string | undefined;
115
+ const jsonMatch = rest.match(/`json:([^`]+)`/);
116
+ if (jsonMatch) {
117
+ jsonPath = jsonMatch[1].trim();
118
+ rest = rest.replace(/`json:[^`]+`/, "").trim();
119
+ }
120
+
121
+ if (!rest) {
122
+ throw new MigrationParseError(
123
+ filePath,
124
+ "datasource",
125
+ resourceName,
126
+ `Missing type in schema column: "${rawLine}"`
127
+ );
128
+ }
129
+
130
+ return {
131
+ name: columnName,
132
+ type: rest,
133
+ jsonPath,
134
+ defaultExpression,
135
+ codec,
136
+ };
137
+ }
138
+
139
+ function parseEngineSettings(value: string): Record<string, string | number | boolean> {
140
+ const raw = parseQuotedValue(value);
141
+ const parts = splitTopLevelComma(raw);
142
+ const settings: Record<string, string | number | boolean> = {};
143
+
144
+ for (const part of parts) {
145
+ const equalIndex = part.indexOf("=");
146
+ if (equalIndex === -1) {
147
+ throw new Error(`Invalid ENGINE_SETTINGS part: "${part}"`);
148
+ }
149
+ const key = part.slice(0, equalIndex).trim();
150
+ const rawValue = part.slice(equalIndex + 1).trim();
151
+ if (!key) {
152
+ throw new Error(`Invalid ENGINE_SETTINGS key in "${part}"`);
153
+ }
154
+
155
+ if (rawValue.startsWith("'") && rawValue.endsWith("'")) {
156
+ settings[key] = rawValue.slice(1, -1).replace(/\\'/g, "'");
157
+ continue;
158
+ }
159
+ if (/^-?\d+(\.\d+)?$/.test(rawValue)) {
160
+ settings[key] = Number(rawValue);
161
+ continue;
162
+ }
163
+ if (rawValue === "true") {
164
+ settings[key] = true;
165
+ continue;
166
+ }
167
+ if (rawValue === "false") {
168
+ settings[key] = false;
169
+ continue;
170
+ }
171
+
172
+ throw new Error(`Unsupported ENGINE_SETTINGS value: "${rawValue}"`);
173
+ }
174
+
175
+ return settings;
176
+ }
177
+
178
+ function parseToken(filePath: string, resourceName: string, value: string): DatasourceTokenModel {
179
+ const parts = value.split(/\s+/).filter(Boolean);
180
+ if (parts.length < 2) {
181
+ throw new MigrationParseError(
182
+ filePath,
183
+ "datasource",
184
+ resourceName,
185
+ `Invalid TOKEN line: "${value}"`
186
+ );
187
+ }
188
+
189
+ if (parts.length > 2) {
190
+ throw new MigrationParseError(
191
+ filePath,
192
+ "datasource",
193
+ resourceName,
194
+ `Unsupported TOKEN syntax in strict mode: "${value}"`
195
+ );
196
+ }
197
+
198
+ const name = parts[0];
199
+ const scope = parts[1];
200
+ if (scope !== "READ" && scope !== "APPEND") {
201
+ throw new MigrationParseError(
202
+ filePath,
203
+ "datasource",
204
+ resourceName,
205
+ `Unsupported datasource token scope: "${scope}"`
206
+ );
207
+ }
208
+
209
+ return { name, scope };
210
+ }
211
+
212
+ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
213
+ const lines = splitLines(resource.content);
214
+ const columns = [];
215
+ const tokens: DatasourceTokenModel[] = [];
216
+ const sharedWith: string[] = [];
217
+ let description: string | undefined;
218
+ let forwardQuery: string | undefined;
219
+
220
+ let engineType: string | undefined;
221
+ let sortingKey: string[] = [];
222
+ let partitionKey: string | undefined;
223
+ let primaryKey: string[] | undefined;
224
+ let ttl: string | undefined;
225
+ let ver: string | undefined;
226
+ let sign: string | undefined;
227
+ let version: string | undefined;
228
+ let summingColumns: string[] | undefined;
229
+ let settings: Record<string, string | number | boolean> | undefined;
230
+
231
+ let kafkaConnectionName: string | undefined;
232
+ let kafkaTopic: string | undefined;
233
+ let kafkaGroupId: string | undefined;
234
+ let kafkaAutoOffsetReset: "earliest" | "latest" | undefined;
235
+
236
+ let i = 0;
237
+ while (i < lines.length) {
238
+ const rawLine = lines[i] ?? "";
239
+ const line = rawLine.trim();
240
+ if (!line) {
241
+ i += 1;
242
+ continue;
243
+ }
244
+
245
+ if (line === "DESCRIPTION >") {
246
+ const block = readIndentedBlock(lines, i + 1);
247
+ if (block.lines.length === 0) {
248
+ throw new MigrationParseError(
249
+ resource.filePath,
250
+ "datasource",
251
+ resource.name,
252
+ "DESCRIPTION block is empty."
253
+ );
254
+ }
255
+ description = block.lines.join("\n");
256
+ i = block.nextIndex;
257
+ continue;
258
+ }
259
+
260
+ if (line === "SCHEMA >") {
261
+ const block = readIndentedBlock(lines, i + 1);
262
+ if (block.lines.length === 0) {
263
+ throw new MigrationParseError(
264
+ resource.filePath,
265
+ "datasource",
266
+ resource.name,
267
+ "SCHEMA block is empty."
268
+ );
269
+ }
270
+ for (const schemaLine of block.lines) {
271
+ if (isBlank(schemaLine)) {
272
+ continue;
273
+ }
274
+ columns.push(parseColumnLine(resource.filePath, resource.name, schemaLine));
275
+ }
276
+ i = block.nextIndex;
277
+ continue;
278
+ }
279
+
280
+ if (line === "FORWARD_QUERY >") {
281
+ const block = readIndentedBlock(lines, i + 1);
282
+ if (block.lines.length === 0) {
283
+ throw new MigrationParseError(
284
+ resource.filePath,
285
+ "datasource",
286
+ resource.name,
287
+ "FORWARD_QUERY block is empty."
288
+ );
289
+ }
290
+ forwardQuery = block.lines.join("\n");
291
+ i = block.nextIndex;
292
+ continue;
293
+ }
294
+
295
+ if (line === "SHARED_WITH >") {
296
+ const block = readIndentedBlock(lines, i + 1);
297
+ for (const sharedLine of block.lines) {
298
+ const normalized = sharedLine.trim().replace(/,$/, "");
299
+ if (normalized) {
300
+ sharedWith.push(normalized);
301
+ }
302
+ }
303
+ i = block.nextIndex;
304
+ continue;
305
+ }
306
+
307
+ const { key, value } = parseDirectiveLine(line);
308
+ switch (key) {
309
+ case "ENGINE":
310
+ engineType = parseQuotedValue(value);
311
+ break;
312
+ case "ENGINE_SORTING_KEY":
313
+ sortingKey = splitCommaSeparated(parseQuotedValue(value));
314
+ break;
315
+ case "ENGINE_PARTITION_KEY":
316
+ partitionKey = parseQuotedValue(value);
317
+ break;
318
+ case "ENGINE_PRIMARY_KEY":
319
+ primaryKey = splitCommaSeparated(parseQuotedValue(value));
320
+ break;
321
+ case "ENGINE_TTL":
322
+ ttl = parseQuotedValue(value);
323
+ break;
324
+ case "ENGINE_VER":
325
+ ver = parseQuotedValue(value);
326
+ break;
327
+ case "ENGINE_SIGN":
328
+ sign = parseQuotedValue(value);
329
+ break;
330
+ case "ENGINE_VERSION":
331
+ version = parseQuotedValue(value);
332
+ break;
333
+ case "ENGINE_SUMMING_COLUMNS":
334
+ summingColumns = splitCommaSeparated(parseQuotedValue(value));
335
+ break;
336
+ case "ENGINE_SETTINGS":
337
+ try {
338
+ settings = parseEngineSettings(value);
339
+ } catch (error) {
340
+ throw new MigrationParseError(
341
+ resource.filePath,
342
+ "datasource",
343
+ resource.name,
344
+ (error as Error).message
345
+ );
346
+ }
347
+ break;
348
+ case "KAFKA_CONNECTION_NAME":
349
+ kafkaConnectionName = value.trim();
350
+ break;
351
+ case "KAFKA_TOPIC":
352
+ kafkaTopic = value.trim();
353
+ break;
354
+ case "KAFKA_GROUP_ID":
355
+ kafkaGroupId = value.trim();
356
+ break;
357
+ case "KAFKA_AUTO_OFFSET_RESET":
358
+ if (value !== "earliest" && value !== "latest") {
359
+ throw new MigrationParseError(
360
+ resource.filePath,
361
+ "datasource",
362
+ resource.name,
363
+ `Invalid KAFKA_AUTO_OFFSET_RESET value: "${value}"`
364
+ );
365
+ }
366
+ kafkaAutoOffsetReset = value;
367
+ break;
368
+ case "TOKEN":
369
+ tokens.push(parseToken(resource.filePath, resource.name, value));
370
+ break;
371
+ default:
372
+ throw new MigrationParseError(
373
+ resource.filePath,
374
+ "datasource",
375
+ resource.name,
376
+ `Unsupported datasource directive in strict mode: "${line}"`
377
+ );
378
+ }
379
+
380
+ i += 1;
381
+ }
382
+
383
+ if (columns.length === 0) {
384
+ throw new MigrationParseError(
385
+ resource.filePath,
386
+ "datasource",
387
+ resource.name,
388
+ "SCHEMA block is required."
389
+ );
390
+ }
391
+
392
+ if (!engineType) {
393
+ throw new MigrationParseError(
394
+ resource.filePath,
395
+ "datasource",
396
+ resource.name,
397
+ "ENGINE directive is required."
398
+ );
399
+ }
400
+
401
+ if (sortingKey.length === 0) {
402
+ throw new MigrationParseError(
403
+ resource.filePath,
404
+ "datasource",
405
+ resource.name,
406
+ "ENGINE_SORTING_KEY directive is required."
407
+ );
408
+ }
409
+
410
+ const kafka =
411
+ kafkaConnectionName || kafkaTopic || kafkaGroupId || kafkaAutoOffsetReset
412
+ ? {
413
+ connectionName: kafkaConnectionName ?? "",
414
+ topic: kafkaTopic ?? "",
415
+ groupId: kafkaGroupId,
416
+ autoOffsetReset: kafkaAutoOffsetReset,
417
+ }
418
+ : undefined;
419
+
420
+ if (kafka && (!kafka.connectionName || !kafka.topic)) {
421
+ throw new MigrationParseError(
422
+ resource.filePath,
423
+ "datasource",
424
+ resource.name,
425
+ "KAFKA_CONNECTION_NAME and KAFKA_TOPIC are required when Kafka directives are used."
426
+ );
427
+ }
428
+
429
+ return {
430
+ kind: "datasource",
431
+ name: resource.name,
432
+ filePath: resource.filePath,
433
+ description,
434
+ columns,
435
+ engine: {
436
+ type: engineType,
437
+ sortingKey,
438
+ partitionKey,
439
+ primaryKey,
440
+ ttl,
441
+ ver,
442
+ sign,
443
+ version,
444
+ summingColumns,
445
+ settings,
446
+ },
447
+ kafka,
448
+ forwardQuery,
449
+ tokens,
450
+ sharedWith,
451
+ };
452
+ }
453
+