@tinybirdco/sdk 0.0.41 → 0.0.42

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 (73) hide show
  1. package/README.md +26 -0
  2. package/dist/api/resources.d.ts +72 -1
  3. package/dist/api/resources.d.ts.map +1 -1
  4. package/dist/api/resources.js +197 -1
  5. package/dist/api/resources.js.map +1 -1
  6. package/dist/api/resources.test.js +82 -1
  7. package/dist/api/resources.test.js.map +1 -1
  8. package/dist/cli/commands/migrate.d.ts +11 -0
  9. package/dist/cli/commands/migrate.d.ts.map +1 -0
  10. package/dist/cli/commands/migrate.js +196 -0
  11. package/dist/cli/commands/migrate.js.map +1 -0
  12. package/dist/cli/commands/migrate.test.d.ts +2 -0
  13. package/dist/cli/commands/migrate.test.d.ts.map +1 -0
  14. package/dist/cli/commands/migrate.test.js +473 -0
  15. package/dist/cli/commands/migrate.test.js.map +1 -0
  16. package/dist/cli/commands/pull.d.ts +59 -0
  17. package/dist/cli/commands/pull.d.ts.map +1 -0
  18. package/dist/cli/commands/pull.js +104 -0
  19. package/dist/cli/commands/pull.js.map +1 -0
  20. package/dist/cli/commands/pull.test.d.ts +2 -0
  21. package/dist/cli/commands/pull.test.d.ts.map +1 -0
  22. package/dist/cli/commands/pull.test.js +140 -0
  23. package/dist/cli/commands/pull.test.js.map +1 -0
  24. package/dist/cli/index.js +77 -0
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/migrate/discovery.d.ts +7 -0
  27. package/dist/migrate/discovery.d.ts.map +1 -0
  28. package/dist/migrate/discovery.js +125 -0
  29. package/dist/migrate/discovery.js.map +1 -0
  30. package/dist/migrate/emit-ts.d.ts +4 -0
  31. package/dist/migrate/emit-ts.d.ts.map +1 -0
  32. package/dist/migrate/emit-ts.js +387 -0
  33. package/dist/migrate/emit-ts.js.map +1 -0
  34. package/dist/migrate/parse-connection.d.ts +3 -0
  35. package/dist/migrate/parse-connection.d.ts.map +1 -0
  36. package/dist/migrate/parse-connection.js +74 -0
  37. package/dist/migrate/parse-connection.js.map +1 -0
  38. package/dist/migrate/parse-datasource.d.ts +3 -0
  39. package/dist/migrate/parse-datasource.d.ts.map +1 -0
  40. package/dist/migrate/parse-datasource.js +324 -0
  41. package/dist/migrate/parse-datasource.js.map +1 -0
  42. package/dist/migrate/parse-pipe.d.ts +3 -0
  43. package/dist/migrate/parse-pipe.d.ts.map +1 -0
  44. package/dist/migrate/parse-pipe.js +332 -0
  45. package/dist/migrate/parse-pipe.js.map +1 -0
  46. package/dist/migrate/parse.d.ts +3 -0
  47. package/dist/migrate/parse.d.ts.map +1 -0
  48. package/dist/migrate/parse.js +18 -0
  49. package/dist/migrate/parse.js.map +1 -0
  50. package/dist/migrate/parser-utils.d.ts +20 -0
  51. package/dist/migrate/parser-utils.d.ts.map +1 -0
  52. package/dist/migrate/parser-utils.js +130 -0
  53. package/dist/migrate/parser-utils.js.map +1 -0
  54. package/dist/migrate/types.d.ts +110 -0
  55. package/dist/migrate/types.d.ts.map +1 -0
  56. package/dist/migrate/types.js +2 -0
  57. package/dist/migrate/types.js.map +1 -0
  58. package/package.json +1 -1
  59. package/src/api/resources.test.ts +121 -0
  60. package/src/api/resources.ts +292 -1
  61. package/src/cli/commands/migrate.test.ts +564 -0
  62. package/src/cli/commands/migrate.ts +240 -0
  63. package/src/cli/commands/pull.test.ts +173 -0
  64. package/src/cli/commands/pull.ts +177 -0
  65. package/src/cli/index.ts +112 -0
  66. package/src/migrate/discovery.ts +151 -0
  67. package/src/migrate/emit-ts.ts +469 -0
  68. package/src/migrate/parse-connection.ts +128 -0
  69. package/src/migrate/parse-datasource.ts +453 -0
  70. package/src/migrate/parse-pipe.ts +518 -0
  71. package/src/migrate/parse.ts +20 -0
  72. package/src/migrate/parser-utils.ts +160 -0
  73. package/src/migrate/types.ts +125 -0
@@ -0,0 +1,564 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { runMigrate } from "./migrate.js";
6
+
7
+ function writeFile(dir: string, relativePath: string, content: string): void {
8
+ const fullPath = path.join(dir, relativePath);
9
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
10
+ fs.writeFileSync(fullPath, content);
11
+ }
12
+
13
+ const EXPECTED_COMPLEX_OUTPUT = `/**
14
+ * Generated by tinybird migrate.
15
+ * Review endpoint output schemas and any defaults before production use.
16
+ */
17
+
18
+ import { createKafkaConnection, defineDatasource, definePipe, defineMaterializedView, defineCopyPipe, node, t, engine, column, p } from "@tinybirdco/sdk";
19
+
20
+ // Connections
21
+
22
+ export const stream = createKafkaConnection("stream", {
23
+ bootstrapServers: "localhost:9092",
24
+ securityProtocol: "SASL_SSL",
25
+ saslMechanism: "PLAIN",
26
+ key: "api-key",
27
+ secret: "api-secret",
28
+ sslCaPem: "ca-pem-content",
29
+ });
30
+
31
+ // Datasources
32
+
33
+ /**
34
+ * Events from Kafka stream
35
+ */
36
+ export const events = defineDatasource("events", {
37
+ description: "Events from Kafka stream",
38
+ schema: {
39
+ event_id: column(t.string(), { jsonPath: "$.event_id" }),
40
+ user_id: column(t.uint64(), { jsonPath: "$.user.id" }),
41
+ env: column(t.string().default("prod"), { jsonPath: "$.env" }),
42
+ is_test: column(t.bool().default(false), { jsonPath: "$.meta.is_test" }),
43
+ updated_at: column(t.dateTime(), { jsonPath: "$.updated_at" }),
44
+ payload: column(t.string().default("{}").codec("ZSTD(1)"), { jsonPath: "$.payload" }),
45
+ },
46
+ engine: engine.replacingMergeTree({ sortingKey: ["event_id", "user_id"], partitionKey: "toYYYYMM(updated_at)", primaryKey: "event_id", ttl: "updated_at + toIntervalDay(30)", ver: "updated_at", settings: { "index_granularity": 8192, "enable_mixed_granularity_parts": true } }),
47
+ kafka: {
48
+ connection: stream,
49
+ topic: "events_topic",
50
+ groupId: "events-consumer",
51
+ autoOffsetReset: "earliest",
52
+ },
53
+ forwardQuery: \`
54
+ SELECT *
55
+ FROM events_mv
56
+ \`,
57
+ tokens: [
58
+ { name: "events_read", permissions: ["READ"] },
59
+ { name: "events_append", permissions: ["APPEND"] },
60
+ ],
61
+ sharedWith: ["workspace_a", "workspace_b"],
62
+ });
63
+
64
+ export const eventsRollup = defineDatasource("events_rollup", {
65
+ jsonPaths: false,
66
+ schema: {
67
+ user_id: t.uint64(),
68
+ total: t.uint64(),
69
+ },
70
+ engine: engine.summingMergeTree({ sortingKey: "user_id", columns: ["total"] }),
71
+ });
72
+
73
+ // Pipes
74
+
75
+ export const copyEvents = defineCopyPipe("copy_events", {
76
+ datasource: eventsRollup,
77
+ copy_mode: "replace",
78
+ copy_schedule: "@on-demand",
79
+ nodes: [
80
+ node({
81
+ name: "copy_node",
82
+ sql: \`
83
+ SELECT event_id, user_id
84
+ FROM events
85
+ \`,
86
+ }),
87
+ ],
88
+ tokens: [
89
+ { name: "copy_token" },
90
+ ],
91
+ });
92
+
93
+ /**
94
+ * Endpoint for filtered events
95
+ */
96
+ export const eventsEndpoint = definePipe("events_endpoint", {
97
+ description: "Endpoint for filtered events",
98
+ params: {
99
+ env: p.string().optional("prod"),
100
+ user_id: p.uint64(),
101
+ },
102
+ nodes: [
103
+ node({
104
+ name: "base",
105
+ description: "Base filter",
106
+ sql: \`
107
+ SELECT event_id, user_id, payload
108
+ FROM events
109
+ WHERE user_id = {{UInt64(user_id)}}
110
+ AND env = {{String(env, 'prod')}}
111
+ \`,
112
+ }),
113
+ node({
114
+ name: "endpoint",
115
+ sql: \`
116
+ SELECT event_id AS event_id, user_id AS user_id
117
+ FROM base
118
+ \`,
119
+ }),
120
+ ],
121
+ endpoint: { enabled: true, cache: { enabled: true, ttl: 120 } },
122
+ output: {
123
+ event_id: t.string(),
124
+ user_id: t.string(),
125
+ },
126
+ tokens: [
127
+ { name: "endpoint_token" },
128
+ ],
129
+ });
130
+
131
+ /**
132
+ * Materialized rollup
133
+ */
134
+ export const eventsMv = defineMaterializedView("events_mv", {
135
+ description: "Materialized rollup",
136
+ datasource: eventsRollup,
137
+ deploymentMethod: "alter",
138
+ nodes: [
139
+ node({
140
+ name: "rollup",
141
+ sql: \`
142
+ SELECT user_id, count() AS total
143
+ FROM events
144
+ GROUP BY user_id
145
+ \`,
146
+ }),
147
+ ],
148
+ tokens: [
149
+ { name: "mv_token" },
150
+ ],
151
+ });
152
+
153
+ export const statsPipe = definePipe("stats_pipe", {
154
+ params: {
155
+ min_total: p.uint32().optional(10),
156
+ },
157
+ nodes: [
158
+ node({
159
+ name: "agg",
160
+ sql: \`
161
+ SELECT user_id, count() AS total
162
+ FROM events
163
+ GROUP BY user_id
164
+ \`,
165
+ }),
166
+ node({
167
+ name: "final",
168
+ sql: \`
169
+ SELECT user_id, total
170
+ FROM agg
171
+ WHERE total > {{UInt32(min_total, 10)}}
172
+ \`,
173
+ }),
174
+ ],
175
+ tokens: [
176
+ { name: "stats_token" },
177
+ ],
178
+ });
179
+ `;
180
+
181
+ const EXPECTED_PARTIAL_OUTPUT = `/**
182
+ * Generated by tinybird migrate.
183
+ * Review endpoint output schemas and any defaults before production use.
184
+ */
185
+
186
+ import { createKafkaConnection, defineDatasource, definePipe, defineMaterializedView, defineCopyPipe, node, t, engine, p } from "@tinybirdco/sdk";
187
+
188
+ // Connections
189
+
190
+ export const stream = createKafkaConnection("stream", {
191
+ bootstrapServers: "localhost:9092",
192
+ });
193
+
194
+ // Datasources
195
+
196
+ export const events = defineDatasource("events", {
197
+ jsonPaths: false,
198
+ schema: {
199
+ event_id: t.string(),
200
+ user_id: t.uint64(),
201
+ created_at: t.dateTime(),
202
+ },
203
+ engine: engine.mergeTree({ sortingKey: "event_id" }),
204
+ kafka: {
205
+ connection: stream,
206
+ topic: "events_topic",
207
+ },
208
+ });
209
+
210
+ // Pipes
211
+
212
+ export const eventsEndpoint = definePipe("events_endpoint", {
213
+ params: {
214
+ user_id: p.uint64(),
215
+ },
216
+ nodes: [
217
+ node({
218
+ name: "source",
219
+ sql: \`
220
+ SELECT event_id, user_id
221
+ FROM events
222
+ \`,
223
+ }),
224
+ node({
225
+ name: "endpoint",
226
+ sql: \`
227
+ SELECT event_id AS event_id, user_id AS user_id
228
+ FROM source
229
+ WHERE user_id = {{UInt64(user_id)}}
230
+ \`,
231
+ }),
232
+ ],
233
+ endpoint: true,
234
+ output: {
235
+ event_id: t.string(),
236
+ user_id: t.string(),
237
+ },
238
+ tokens: [
239
+ { name: "endpoint_token" },
240
+ ],
241
+ });
242
+ `;
243
+
244
+ describe("runMigrate", () => {
245
+ const tempDirs: string[] = [];
246
+
247
+ afterEach(() => {
248
+ for (const dir of tempDirs) {
249
+ try {
250
+ fs.rmSync(dir, { recursive: true });
251
+ } catch {
252
+ // Ignore cleanup failures
253
+ }
254
+ }
255
+ tempDirs.length = 0;
256
+ });
257
+
258
+ it("migrates complex resources including endpoint, materialized, and copy pipes", async () => {
259
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
260
+ tempDirs.push(tempDir);
261
+
262
+ writeFile(
263
+ tempDir,
264
+ "stream.connection",
265
+ `TYPE kafka
266
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
267
+ KAFKA_SECURITY_PROTOCOL SASL_SSL
268
+ KAFKA_SASL_MECHANISM PLAIN
269
+ KAFKA_KEY api-key
270
+ KAFKA_SECRET api-secret
271
+ KAFKA_SSL_CA_PEM ca-pem-content
272
+ `
273
+ );
274
+
275
+ writeFile(
276
+ tempDir,
277
+ "events.datasource",
278
+ `DESCRIPTION >
279
+ Events from Kafka stream
280
+ SCHEMA >
281
+ event_id String \`json:$.event_id\`,
282
+ user_id UInt64 \`json:$.user.id\`,
283
+ env String \`json:$.env\` DEFAULT 'prod',
284
+ is_test Bool \`json:$.meta.is_test\` DEFAULT 0,
285
+ updated_at DateTime \`json:$.updated_at\`,
286
+ payload String \`json:$.payload\` DEFAULT '{}' CODEC(ZSTD(1))
287
+
288
+ ENGINE "ReplacingMergeTree"
289
+ ENGINE_SORTING_KEY "event_id, user_id"
290
+ ENGINE_PARTITION_KEY "toYYYYMM(updated_at)"
291
+ ENGINE_PRIMARY_KEY "event_id"
292
+ ENGINE_TTL "updated_at + toIntervalDay(30)"
293
+ ENGINE_VER "updated_at"
294
+ ENGINE_SETTINGS "index_granularity=8192, enable_mixed_granularity_parts=true"
295
+ KAFKA_CONNECTION_NAME stream
296
+ KAFKA_TOPIC events_topic
297
+ KAFKA_GROUP_ID events-consumer
298
+ KAFKA_AUTO_OFFSET_RESET earliest
299
+ TOKEN events_read READ
300
+ TOKEN events_append APPEND
301
+ SHARED_WITH >
302
+ workspace_a,
303
+ workspace_b
304
+ FORWARD_QUERY >
305
+ SELECT *
306
+ FROM events_mv
307
+ `
308
+ );
309
+
310
+ writeFile(
311
+ tempDir,
312
+ "events_rollup.datasource",
313
+ `SCHEMA >
314
+ user_id UInt64,
315
+ total UInt64
316
+
317
+ ENGINE "SummingMergeTree"
318
+ ENGINE_SORTING_KEY "user_id"
319
+ ENGINE_SUMMING_COLUMNS "total"
320
+ `
321
+ );
322
+
323
+ writeFile(
324
+ tempDir,
325
+ "events_endpoint.pipe",
326
+ `DESCRIPTION >
327
+ Endpoint for filtered events
328
+ NODE base
329
+ DESCRIPTION >
330
+ Base filter
331
+ SQL >
332
+ %
333
+ SELECT event_id, user_id, payload
334
+ FROM events
335
+ WHERE user_id = {{UInt64(user_id)}}
336
+ AND env = {{String(env, 'prod')}}
337
+ NODE endpoint
338
+ SQL >
339
+ SELECT event_id AS event_id, user_id AS user_id
340
+ FROM base
341
+ TYPE endpoint
342
+ CACHE 120
343
+ TOKEN endpoint_token READ
344
+ `
345
+ );
346
+
347
+ writeFile(
348
+ tempDir,
349
+ "events_mv.pipe",
350
+ `DESCRIPTION >
351
+ Materialized rollup
352
+ NODE rollup
353
+ SQL >
354
+ SELECT user_id, count() AS total
355
+ FROM events
356
+ GROUP BY user_id
357
+ TYPE MATERIALIZED
358
+ DATASOURCE events_rollup
359
+ DEPLOYMENT_METHOD alter
360
+ TOKEN mv_token READ
361
+ `
362
+ );
363
+
364
+ writeFile(
365
+ tempDir,
366
+ "copy_events.pipe",
367
+ `NODE copy_node
368
+ SQL >
369
+ SELECT event_id, user_id
370
+ FROM events
371
+ TYPE COPY
372
+ TARGET_DATASOURCE events_rollup
373
+ COPY_SCHEDULE @on-demand
374
+ COPY_MODE replace
375
+ TOKEN copy_token READ
376
+ `
377
+ );
378
+
379
+ writeFile(
380
+ tempDir,
381
+ "stats_pipe.pipe",
382
+ `NODE agg
383
+ SQL >
384
+ SELECT user_id, count() AS total
385
+ FROM events
386
+ GROUP BY user_id
387
+ NODE final
388
+ SQL >
389
+ SELECT user_id, total
390
+ FROM agg
391
+ WHERE total > {{UInt32(min_total, 10)}}
392
+ TOKEN stats_token READ
393
+ `
394
+ );
395
+
396
+ const result = await runMigrate({
397
+ cwd: tempDir,
398
+ patterns: ["."],
399
+ strict: true,
400
+ });
401
+
402
+ expect(result.success).toBe(true);
403
+ expect(result.errors).toHaveLength(0);
404
+ expect(result.migrated).toHaveLength(7);
405
+ expect(result.migrated.filter((resource) => resource.kind === "connection")).toHaveLength(1);
406
+ expect(result.migrated.filter((resource) => resource.kind === "datasource")).toHaveLength(2);
407
+ expect(result.migrated.filter((resource) => resource.kind === "pipe")).toHaveLength(4);
408
+ expect(path.basename(result.outputPath)).toBe("tinybird.migration.ts");
409
+ expect(fs.existsSync(result.outputPath)).toBe(true);
410
+
411
+ const output = fs.readFileSync(result.outputPath, "utf-8");
412
+ expect(output).toBe(EXPECTED_COMPLEX_OUTPUT);
413
+ });
414
+
415
+ it("continues processing and reports all errors while writing complex migratable resources", async () => {
416
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
417
+ tempDirs.push(tempDir);
418
+
419
+ writeFile(
420
+ tempDir,
421
+ "stream.connection",
422
+ `TYPE kafka
423
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
424
+ `
425
+ );
426
+
427
+ writeFile(
428
+ tempDir,
429
+ "events.datasource",
430
+ `SCHEMA >
431
+ event_id String,
432
+ user_id UInt64,
433
+ created_at DateTime
434
+
435
+ ENGINE "MergeTree"
436
+ ENGINE_SORTING_KEY "event_id"
437
+ KAFKA_CONNECTION_NAME stream
438
+ KAFKA_TOPIC events_topic
439
+ `
440
+ );
441
+
442
+ writeFile(
443
+ tempDir,
444
+ "events_endpoint.pipe",
445
+ `NODE source
446
+ SQL >
447
+ SELECT event_id, user_id
448
+ FROM events
449
+ NODE endpoint
450
+ SQL >
451
+ SELECT event_id AS event_id, user_id AS user_id
452
+ FROM source
453
+ WHERE user_id = {{UInt64(user_id)}}
454
+ TYPE endpoint
455
+ TOKEN endpoint_token READ
456
+ `
457
+ );
458
+
459
+ writeFile(
460
+ tempDir,
461
+ "events_mv.pipe",
462
+ `NODE rollup
463
+ SQL >
464
+ SELECT user_id, count() AS total
465
+ FROM events
466
+ GROUP BY user_id
467
+ TYPE MATERIALIZED
468
+ DATASOURCE missing_ds
469
+ `
470
+ );
471
+
472
+ writeFile(
473
+ tempDir,
474
+ "broken.pipe",
475
+ `NODE broken
476
+ SQL >
477
+ SELECT *
478
+ FROM events
479
+ TYPE endpoint
480
+ UNSUPPORTED_DIRECTIVE true
481
+ `
482
+ );
483
+
484
+ const result = await runMigrate({
485
+ cwd: tempDir,
486
+ patterns: ["."],
487
+ strict: true,
488
+ });
489
+
490
+ expect(result.success).toBe(false);
491
+ expect(result.errors).toHaveLength(2);
492
+ expect(result.errors.map((error) => error.message)).toEqual(
493
+ expect.arrayContaining([
494
+ 'Unsupported pipe directive in strict mode: "UNSUPPORTED_DIRECTIVE true"',
495
+ 'Materialized pipe references missing/unmigrated datasource "missing_ds".',
496
+ ])
497
+ );
498
+ expect(result.migrated.filter((resource) => resource.kind === "connection")).toHaveLength(1);
499
+ expect(result.migrated.filter((resource) => resource.kind === "datasource")).toHaveLength(1);
500
+ expect(result.migrated.filter((resource) => resource.kind === "pipe")).toHaveLength(1);
501
+ expect(fs.existsSync(result.outputPath)).toBe(true);
502
+
503
+ const output = fs.readFileSync(result.outputPath, "utf-8");
504
+ expect(output).toBe(EXPECTED_PARTIAL_OUTPUT);
505
+ });
506
+
507
+ it("returns exact output content in dry-run mode for complex migratable resources", async () => {
508
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
509
+ tempDirs.push(tempDir);
510
+
511
+ writeFile(
512
+ tempDir,
513
+ "stream.connection",
514
+ `TYPE kafka
515
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
516
+ `
517
+ );
518
+
519
+ writeFile(
520
+ tempDir,
521
+ "events.datasource",
522
+ `SCHEMA >
523
+ event_id String,
524
+ user_id UInt64,
525
+ created_at DateTime
526
+
527
+ ENGINE "MergeTree"
528
+ ENGINE_SORTING_KEY "event_id"
529
+ KAFKA_CONNECTION_NAME stream
530
+ KAFKA_TOPIC events_topic
531
+ `
532
+ );
533
+
534
+ writeFile(
535
+ tempDir,
536
+ "events_endpoint.pipe",
537
+ `NODE source
538
+ SQL >
539
+ SELECT event_id, user_id
540
+ FROM events
541
+ NODE endpoint
542
+ SQL >
543
+ SELECT event_id AS event_id, user_id AS user_id
544
+ FROM source
545
+ WHERE user_id = {{UInt64(user_id)}}
546
+ TYPE endpoint
547
+ TOKEN endpoint_token READ
548
+ `
549
+ );
550
+
551
+ const result = await runMigrate({
552
+ cwd: tempDir,
553
+ patterns: ["."],
554
+ strict: true,
555
+ dryRun: true,
556
+ });
557
+
558
+ expect(result.success).toBe(true);
559
+ expect(result.errors).toHaveLength(0);
560
+ expect(result.migrated).toHaveLength(3);
561
+ expect(result.outputContent).toBe(EXPECTED_PARTIAL_OUTPUT);
562
+ expect(fs.existsSync(result.outputPath)).toBe(false);
563
+ });
564
+ });