@tinybirdco/sdk 0.0.48 → 0.0.50

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 (84) hide show
  1. package/README.md +71 -4
  2. package/dist/cli/commands/migrate.d.ts.map +1 -1
  3. package/dist/cli/commands/migrate.js +68 -1
  4. package/dist/cli/commands/migrate.js.map +1 -1
  5. package/dist/cli/commands/migrate.test.js +458 -1
  6. package/dist/cli/commands/migrate.test.js.map +1 -1
  7. package/dist/generator/connection.d.ts.map +1 -1
  8. package/dist/generator/connection.js +14 -1
  9. package/dist/generator/connection.js.map +1 -1
  10. package/dist/generator/connection.test.js +20 -4
  11. package/dist/generator/connection.test.js.map +1 -1
  12. package/dist/generator/datasource.d.ts.map +1 -1
  13. package/dist/generator/datasource.js +20 -10
  14. package/dist/generator/datasource.js.map +1 -1
  15. package/dist/generator/datasource.test.js +26 -1
  16. package/dist/generator/datasource.test.js.map +1 -1
  17. package/dist/generator/pipe.d.ts.map +1 -1
  18. package/dist/generator/pipe.js +31 -1
  19. package/dist/generator/pipe.js.map +1 -1
  20. package/dist/generator/pipe.test.js +50 -1
  21. package/dist/generator/pipe.test.js.map +1 -1
  22. package/dist/index.d.ts +5 -5
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/migrate/emit-ts.d.ts.map +1 -1
  27. package/dist/migrate/emit-ts.js +95 -20
  28. package/dist/migrate/emit-ts.js.map +1 -1
  29. package/dist/migrate/parse-connection.d.ts +2 -2
  30. package/dist/migrate/parse-connection.d.ts.map +1 -1
  31. package/dist/migrate/parse-connection.js +34 -4
  32. package/dist/migrate/parse-connection.js.map +1 -1
  33. package/dist/migrate/parse-datasource.d.ts.map +1 -1
  34. package/dist/migrate/parse-datasource.js +79 -51
  35. package/dist/migrate/parse-datasource.js.map +1 -1
  36. package/dist/migrate/parse-pipe.d.ts.map +1 -1
  37. package/dist/migrate/parse-pipe.js +254 -44
  38. package/dist/migrate/parse-pipe.js.map +1 -1
  39. package/dist/migrate/parser-utils.d.ts +5 -0
  40. package/dist/migrate/parser-utils.d.ts.map +1 -1
  41. package/dist/migrate/parser-utils.js +22 -0
  42. package/dist/migrate/parser-utils.js.map +1 -1
  43. package/dist/migrate/types.d.ts +37 -4
  44. package/dist/migrate/types.d.ts.map +1 -1
  45. package/dist/schema/connection.d.ts +34 -1
  46. package/dist/schema/connection.d.ts.map +1 -1
  47. package/dist/schema/connection.js +26 -0
  48. package/dist/schema/connection.js.map +1 -1
  49. package/dist/schema/connection.test.js +35 -1
  50. package/dist/schema/connection.test.js.map +1 -1
  51. package/dist/schema/datasource.d.ts +16 -1
  52. package/dist/schema/datasource.d.ts.map +1 -1
  53. package/dist/schema/datasource.js +3 -2
  54. package/dist/schema/datasource.js.map +1 -1
  55. package/dist/schema/datasource.test.js +33 -3
  56. package/dist/schema/datasource.test.js.map +1 -1
  57. package/dist/schema/pipe.d.ts +90 -3
  58. package/dist/schema/pipe.d.ts.map +1 -1
  59. package/dist/schema/pipe.js +84 -0
  60. package/dist/schema/pipe.js.map +1 -1
  61. package/dist/schema/pipe.test.js +70 -1
  62. package/dist/schema/pipe.test.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/cli/commands/migrate.test.ts +671 -1
  65. package/src/cli/commands/migrate.ts +74 -1
  66. package/src/generator/connection.test.ts +29 -4
  67. package/src/generator/connection.ts +25 -2
  68. package/src/generator/datasource.test.ts +30 -1
  69. package/src/generator/datasource.ts +22 -10
  70. package/src/generator/pipe.test.ts +56 -1
  71. package/src/generator/pipe.ts +41 -1
  72. package/src/index.ts +14 -0
  73. package/src/migrate/emit-ts.ts +106 -24
  74. package/src/migrate/parse-connection.ts +56 -6
  75. package/src/migrate/parse-datasource.ts +84 -70
  76. package/src/migrate/parse-pipe.ts +359 -66
  77. package/src/migrate/parser-utils.ts +36 -1
  78. package/src/migrate/types.ts +43 -4
  79. package/src/schema/connection.test.ts +48 -0
  80. package/src/schema/connection.ts +60 -1
  81. package/src/schema/datasource.test.ts +39 -3
  82. package/src/schema/datasource.ts +24 -3
  83. package/src/schema/pipe.test.ts +89 -0
  84. package/src/schema/pipe.ts +188 -4
@@ -107,7 +107,7 @@ export const eventsEndpoint = definePipe("events_endpoint", {
107
107
  SELECT event_id, user_id, payload
108
108
  FROM events
109
109
  WHERE user_id = {{UInt64(user_id)}}
110
- AND env = {{String(env, 'prod')}}
110
+ AND env = {{String(env, 'prod')}}
111
111
  \`,
112
112
  }),
113
113
  node({
@@ -616,6 +616,98 @@ IMPORT_FROM_TIMESTAMP 2024-01-01T00:00:00Z
616
616
  expect(output).toContain('fromTimestamp: "2024-01-01T00:00:00Z"');
617
617
  });
618
618
 
619
+ it("migrates gcs connection and import datasource directives", async () => {
620
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
621
+ tempDirs.push(tempDir);
622
+
623
+ writeFile(
624
+ tempDir,
625
+ "gcsample.connection",
626
+ `TYPE gcs
627
+ GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON {{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}
628
+ `
629
+ );
630
+
631
+ writeFile(
632
+ tempDir,
633
+ "events_gcs_landing.datasource",
634
+ `SCHEMA >
635
+ timestamp DateTime,
636
+ session_id String
637
+
638
+ ENGINE "MergeTree"
639
+ ENGINE_SORTING_KEY "timestamp"
640
+ IMPORT_CONNECTION_NAME gcsample
641
+ IMPORT_BUCKET_URI gs://my-gcs-bucket/events/*.csv
642
+ IMPORT_SCHEDULE @auto
643
+ IMPORT_FROM_TIMESTAMP 2024-01-01T00:00:00Z
644
+ `
645
+ );
646
+
647
+ const result = await runMigrate({
648
+ cwd: tempDir,
649
+ patterns: ["."],
650
+ strict: true,
651
+ });
652
+
653
+ expect(result.success).toBe(true);
654
+ expect(result.errors).toHaveLength(0);
655
+ expect(result.migrated.filter((resource) => resource.kind === "connection")).toHaveLength(1);
656
+ expect(result.migrated.filter((resource) => resource.kind === "datasource")).toHaveLength(1);
657
+
658
+ const output = fs.readFileSync(result.outputPath, "utf-8");
659
+ expect(output).toContain("defineGCSConnection");
660
+ expect(output).toContain('export const gcsample = defineGCSConnection("gcsample", {');
661
+ expect(output).toContain(
662
+ 'serviceAccountCredentialsJson: secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON"),'
663
+ );
664
+ expect(output).toContain("gcs: {");
665
+ expect(output).toContain("connection: gcsample");
666
+ expect(output).toContain('bucketUri: "gs://my-gcs-bucket/events/*.csv"');
667
+ expect(output).toContain('schedule: "@auto"');
668
+ expect(output).toContain('fromTimestamp: "2024-01-01T00:00:00Z"');
669
+ });
670
+
671
+ it("reports an error when import directives use a non-bucket connection type", async () => {
672
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
673
+ tempDirs.push(tempDir);
674
+
675
+ writeFile(
676
+ tempDir,
677
+ "stream.connection",
678
+ `TYPE kafka
679
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
680
+ `
681
+ );
682
+
683
+ writeFile(
684
+ tempDir,
685
+ "events_landing.datasource",
686
+ `SCHEMA >
687
+ id String
688
+
689
+ ENGINE "MergeTree"
690
+ ENGINE_SORTING_KEY "id"
691
+ IMPORT_CONNECTION_NAME stream
692
+ IMPORT_BUCKET_URI gs://my-gcs-bucket/events/*.csv
693
+ `
694
+ );
695
+
696
+ const result = await runMigrate({
697
+ cwd: tempDir,
698
+ patterns: ["."],
699
+ strict: true,
700
+ });
701
+
702
+ expect(result.success).toBe(false);
703
+ expect(result.errors.map((error) => error.message)).toEqual(
704
+ expect.arrayContaining([
705
+ 'Datasource import directives require an s3 or gcs connection, found "kafka".',
706
+ ])
707
+ );
708
+ });
709
+
710
+
619
711
  it("migrates KAFKA_STORE_RAW_VALUE datasource directive", async () => {
620
712
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
621
713
  tempDirs.push(tempDir);
@@ -737,6 +829,375 @@ DATASOURCE events_state
737
829
  );
738
830
  });
739
831
 
832
+ it("migrates TYPE copy in lowercase", async () => {
833
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
834
+ tempDirs.push(tempDir);
835
+
836
+ writeFile(
837
+ tempDir,
838
+ "copy_target.datasource",
839
+ `SCHEMA >
840
+ id String
841
+
842
+ ENGINE "MergeTree"
843
+ ENGINE_SORTING_KEY "id"
844
+ `
845
+ );
846
+
847
+ writeFile(
848
+ tempDir,
849
+ "copy_lower.pipe",
850
+ `NODE copy_node
851
+ SQL >
852
+ SELECT id
853
+ FROM copy_target
854
+ TYPE copy
855
+ TARGET_DATASOURCE copy_target
856
+ `
857
+ );
858
+
859
+ const result = await runMigrate({
860
+ cwd: tempDir,
861
+ patterns: ["."],
862
+ strict: true,
863
+ });
864
+
865
+ expect(result.success).toBe(true);
866
+ expect(result.errors).toHaveLength(0);
867
+
868
+ const output = fs.readFileSync(result.outputPath, "utf-8");
869
+ expect(output).toContain('export const copyLower = defineCopyPipe("copy_lower", {');
870
+ expect(output).toContain("datasource: copyTarget,");
871
+ });
872
+
873
+ it("migrates TYPE ENDPOINT in uppercase", async () => {
874
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
875
+ tempDirs.push(tempDir);
876
+
877
+ writeFile(
878
+ tempDir,
879
+ "endpoint_upper.pipe",
880
+ `NODE endpoint
881
+ SQL >
882
+ SELECT 1 AS id
883
+ FROM system.numbers
884
+ LIMIT 1
885
+ TYPE ENDPOINT
886
+ `
887
+ );
888
+
889
+ const result = await runMigrate({
890
+ cwd: tempDir,
891
+ patterns: ["."],
892
+ strict: true,
893
+ });
894
+
895
+ expect(result.success).toBe(true);
896
+ expect(result.errors).toHaveLength(0);
897
+
898
+ const output = fs.readFileSync(result.outputPath, "utf-8");
899
+ expect(output).toContain('export const endpointUpper = definePipe("endpoint_upper", {');
900
+ expect(output).toContain("endpoint: true,");
901
+ });
902
+
903
+ it("parses multiline datasource blocks with flexible indentation", async () => {
904
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
905
+ tempDirs.push(tempDir);
906
+
907
+ writeFile(
908
+ tempDir,
909
+ "flex.datasource",
910
+ `DESCRIPTION >
911
+ Flexible indentation description
912
+
913
+ SCHEMA >
914
+ id String,
915
+ env String
916
+
917
+ ENGINE "MergeTree"
918
+ ENGINE_SORTING_KEY "id"
919
+ FORWARD_QUERY >
920
+ SELECT id, env
921
+ FROM upstream_ds
922
+ SHARED_WITH >
923
+ workspace_a,
924
+ workspace_b
925
+ `
926
+ );
927
+
928
+ const result = await runMigrate({
929
+ cwd: tempDir,
930
+ patterns: ["."],
931
+ strict: true,
932
+ });
933
+
934
+ expect(result.success).toBe(true);
935
+ expect(result.errors).toHaveLength(0);
936
+
937
+ const output = fs.readFileSync(result.outputPath, "utf-8");
938
+ expect(output).toContain('description: "Flexible indentation description"');
939
+ expect(output).toContain("forwardQuery: `");
940
+ expect(output).toContain("SELECT id, env");
941
+ expect(output).toContain("FROM upstream_ds");
942
+ expect(output).toContain('sharedWith: ["workspace_a", "workspace_b"],');
943
+ });
944
+
945
+ it("parses node SQL blocks with flexible indentation", async () => {
946
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
947
+ tempDirs.push(tempDir);
948
+
949
+ writeFile(
950
+ tempDir,
951
+ "flex_pipe.pipe",
952
+ `NODE endpoint_node
953
+ DESCRIPTION >
954
+ Endpoint node description
955
+ SQL >
956
+ %
957
+ SELECT 1 AS id
958
+ FROM system.numbers
959
+ TYPE endpoint
960
+ `
961
+ );
962
+
963
+ const result = await runMigrate({
964
+ cwd: tempDir,
965
+ patterns: ["."],
966
+ strict: true,
967
+ });
968
+
969
+ expect(result.success).toBe(true);
970
+ expect(result.errors).toHaveLength(0);
971
+
972
+ const output = fs.readFileSync(result.outputPath, "utf-8");
973
+ expect(output).toContain('description: "Endpoint node description"');
974
+ expect(output).toContain("SELECT 1 AS id");
975
+ expect(output).toContain("FROM system.numbers");
976
+ expect(output).toContain("endpoint: true,");
977
+ });
978
+
979
+ it("migrates datasource without engine directives", async () => {
980
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
981
+ tempDirs.push(tempDir);
982
+
983
+ writeFile(
984
+ tempDir,
985
+ "no_engine.datasource",
986
+ `SCHEMA >
987
+ id String,
988
+ name String
989
+ `
990
+ );
991
+
992
+ const result = await runMigrate({
993
+ cwd: tempDir,
994
+ patterns: ["."],
995
+ strict: true,
996
+ });
997
+
998
+ expect(result.success).toBe(true);
999
+ expect(result.errors).toHaveLength(0);
1000
+
1001
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1002
+ expect(output).toContain('export const noEngine = defineDatasource("no_engine", {');
1003
+ expect(output).not.toContain("engine:");
1004
+ expect(output).not.toContain(", engine,");
1005
+ });
1006
+
1007
+ it("infers MergeTree when engine options exist without ENGINE directive", async () => {
1008
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1009
+ tempDirs.push(tempDir);
1010
+
1011
+ writeFile(
1012
+ tempDir,
1013
+ "implicit_engine.datasource",
1014
+ `SCHEMA >
1015
+ id String,
1016
+ event_date Date
1017
+
1018
+ ENGINE_SORTING_KEY "id"
1019
+ ENGINE_PARTITION_KEY "toYYYYMM(event_date)"
1020
+ `
1021
+ );
1022
+
1023
+ const result = await runMigrate({
1024
+ cwd: tempDir,
1025
+ patterns: ["."],
1026
+ strict: true,
1027
+ });
1028
+
1029
+ expect(result.success).toBe(true);
1030
+ expect(result.errors).toHaveLength(0);
1031
+
1032
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1033
+ expect(output).toContain('export const implicitEngine = defineDatasource("implicit_engine", {');
1034
+ expect(output).toContain(
1035
+ 'engine: engine.mergeTree({ sortingKey: "id", partitionKey: "toYYYYMM(event_date)" }),'
1036
+ );
1037
+ });
1038
+
1039
+ it("supports quoted datasource token names with spaces", async () => {
1040
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1041
+ tempDirs.push(tempDir);
1042
+
1043
+ writeFile(
1044
+ tempDir,
1045
+ "token_spaces.datasource",
1046
+ `TOKEN "ingestion (Data Source append)" APPEND
1047
+
1048
+ SCHEMA >
1049
+ id String
1050
+
1051
+ ENGINE "MergeTree"
1052
+ ENGINE_SORTING_KEY "id"
1053
+ `
1054
+ );
1055
+
1056
+ const result = await runMigrate({
1057
+ cwd: tempDir,
1058
+ patterns: ["."],
1059
+ strict: true,
1060
+ });
1061
+
1062
+ expect(result.success).toBe(true);
1063
+ expect(result.errors).toHaveLength(0);
1064
+
1065
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1066
+ expect(output).toContain(
1067
+ '{ name: "ingestion (Data Source append)", permissions: ["APPEND"] },'
1068
+ );
1069
+ });
1070
+
1071
+ it("rejects unquoted datasource token names with spaces", async () => {
1072
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1073
+ tempDirs.push(tempDir);
1074
+
1075
+ writeFile(
1076
+ tempDir,
1077
+ "bad_token.datasource",
1078
+ `TOKEN ingestion data APPEND
1079
+
1080
+ SCHEMA >
1081
+ id String
1082
+
1083
+ ENGINE "MergeTree"
1084
+ ENGINE_SORTING_KEY "id"
1085
+ `
1086
+ );
1087
+
1088
+ const result = await runMigrate({
1089
+ cwd: tempDir,
1090
+ patterns: ["."],
1091
+ strict: true,
1092
+ });
1093
+
1094
+ expect(result.success).toBe(false);
1095
+ expect(result.errors).toHaveLength(1);
1096
+ expect(result.errors[0]?.message).toContain("Unsupported TOKEN syntax in strict mode");
1097
+ });
1098
+
1099
+ it("allows empty datasource DESCRIPTION block", async () => {
1100
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1101
+ tempDirs.push(tempDir);
1102
+
1103
+ writeFile(
1104
+ tempDir,
1105
+ "empty_ds_desc.datasource",
1106
+ `DESCRIPTION >
1107
+
1108
+ SCHEMA >
1109
+ id String
1110
+
1111
+ ENGINE "MergeTree"
1112
+ ENGINE_SORTING_KEY "id"
1113
+ `
1114
+ );
1115
+
1116
+ const result = await runMigrate({
1117
+ cwd: tempDir,
1118
+ patterns: ["."],
1119
+ strict: true,
1120
+ });
1121
+
1122
+ expect(result.success).toBe(true);
1123
+ expect(result.errors).toHaveLength(0);
1124
+
1125
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1126
+ expect(output).toContain(
1127
+ 'export const emptyDsDesc = defineDatasource("empty_ds_desc", {'
1128
+ );
1129
+ expect(output).toContain('description: "",');
1130
+ });
1131
+
1132
+ it("allows empty node DESCRIPTION block", async () => {
1133
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1134
+ tempDirs.push(tempDir);
1135
+
1136
+ writeFile(
1137
+ tempDir,
1138
+ "empty_node_desc.pipe",
1139
+ `NODE helper
1140
+ DESCRIPTION >
1141
+
1142
+ SQL >
1143
+ SELECT 1 AS id
1144
+ TYPE endpoint
1145
+ `
1146
+ );
1147
+
1148
+ const result = await runMigrate({
1149
+ cwd: tempDir,
1150
+ patterns: ["."],
1151
+ strict: true,
1152
+ });
1153
+
1154
+ expect(result.success).toBe(true);
1155
+ expect(result.errors).toHaveLength(0);
1156
+
1157
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1158
+ expect(output).toContain('export const emptyNodeDesc = definePipe("empty_node_desc", {');
1159
+ expect(output).toContain('description: "",');
1160
+ });
1161
+
1162
+ it("supports keyword-style pipe param arguments", async () => {
1163
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1164
+ tempDirs.push(tempDir);
1165
+
1166
+ writeFile(
1167
+ tempDir,
1168
+ "keyword_params.pipe",
1169
+ `NODE endpoint
1170
+ SQL >
1171
+ SELECT 1 AS id
1172
+ WHERE tenant_id = {{ String(tenant_id, description="Tenant ID to filter", default="") }}
1173
+ AND event_date >= {{ Date(from_date, description="Starting date", required=False) }}
1174
+ AND days >= {{ Int32(days, 1, description="Days to include", required=True) }}
1175
+ TYPE endpoint
1176
+ `
1177
+ );
1178
+
1179
+ const result = await runMigrate({
1180
+ cwd: tempDir,
1181
+ patterns: ["."],
1182
+ strict: true,
1183
+ });
1184
+
1185
+ expect(result.success).toBe(true);
1186
+ expect(result.errors).toHaveLength(0);
1187
+
1188
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1189
+ expect(output).toContain('export const keywordParams = definePipe("keyword_params", {');
1190
+ expect(output).toContain(
1191
+ 'tenant_id: p.string().optional("").describe("Tenant ID to filter"),'
1192
+ );
1193
+ expect(output).toContain(
1194
+ 'from_date: p.date().optional().describe("Starting date"),'
1195
+ );
1196
+ expect(output).toContain(
1197
+ 'days: p.int32().optional(1).describe("Days to include"),'
1198
+ );
1199
+ });
1200
+
740
1201
  it("migrates datasource with mixed explicit and default json paths", async () => {
741
1202
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
742
1203
  tempDirs.push(tempDir);
@@ -887,4 +1348,213 @@ KAFKA_GROUP_ID events-group
887
1348
  expect(output).not.toContain("const secret = (name: string, defaultValue?: string) =>");
888
1349
  expect(output).toContain('groupId: "events-group",');
889
1350
  });
1351
+
1352
+ it("migrates Kafka sink pipes and emits sink config in TypeScript", async () => {
1353
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1354
+ tempDirs.push(tempDir);
1355
+
1356
+ writeFile(
1357
+ tempDir,
1358
+ "events_kafka.connection",
1359
+ `TYPE kafka
1360
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
1361
+ `
1362
+ );
1363
+
1364
+ writeFile(
1365
+ tempDir,
1366
+ "events_sink.pipe",
1367
+ `NODE publish
1368
+ SQL >
1369
+ SELECT *
1370
+ FROM events
1371
+ WHERE env = {{String(env, 'prod')}}
1372
+ TYPE sink
1373
+ EXPORT_CONNECTION_NAME events_kafka
1374
+ EXPORT_KAFKA_TOPIC events_out
1375
+ EXPORT_SCHEDULE @on-demand
1376
+ `
1377
+ );
1378
+
1379
+ const result = await runMigrate({
1380
+ cwd: tempDir,
1381
+ patterns: ["."],
1382
+ strict: true,
1383
+ });
1384
+
1385
+ expect(result.success).toBe(true);
1386
+ expect(result.errors).toHaveLength(0);
1387
+ expect(result.migrated.filter((resource) => resource.kind === "connection")).toHaveLength(1);
1388
+ expect(result.migrated.filter((resource) => resource.kind === "pipe")).toHaveLength(1);
1389
+
1390
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1391
+ expect(output).toContain('export const eventsSink = defineSinkPipe("events_sink", {');
1392
+ expect(output).toContain("params: {");
1393
+ expect(output).toContain('env: p.string().optional("prod"),');
1394
+ expect(output).toContain("sink: {");
1395
+ expect(output).toContain("connection: eventsKafka");
1396
+ expect(output).toContain('topic: "events_out"');
1397
+ expect(output).toContain('schedule: "@on-demand"');
1398
+ expect(output).not.toContain('strategy:');
1399
+ });
1400
+
1401
+ it("migrates S3 sink pipes and emits compression/strategy config in TypeScript", async () => {
1402
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1403
+ tempDirs.push(tempDir);
1404
+
1405
+ writeFile(
1406
+ tempDir,
1407
+ "exports_s3.connection",
1408
+ `TYPE s3
1409
+ S3_REGION "us-east-1"
1410
+ S3_ARN "arn:aws:iam::123456789012:role/tinybird-s3-access"
1411
+ `
1412
+ );
1413
+
1414
+ writeFile(
1415
+ tempDir,
1416
+ "events_s3_sink.pipe",
1417
+ `NODE export
1418
+ SQL >
1419
+ SELECT * FROM events
1420
+ TYPE sink
1421
+ EXPORT_CONNECTION_NAME exports_s3
1422
+ EXPORT_BUCKET_URI s3://exports/events/
1423
+ EXPORT_FILE_TEMPLATE events_{date}
1424
+ EXPORT_SCHEDULE @once
1425
+ EXPORT_FORMAT ndjson
1426
+ EXPORT_STRATEGY create_new
1427
+ EXPORT_COMPRESSION gzip
1428
+ `
1429
+ );
1430
+
1431
+ const result = await runMigrate({
1432
+ cwd: tempDir,
1433
+ patterns: ["."],
1434
+ strict: true,
1435
+ });
1436
+
1437
+ expect(result.success).toBe(true);
1438
+ expect(result.errors).toHaveLength(0);
1439
+
1440
+ const output = fs.readFileSync(result.outputPath, "utf-8");
1441
+ expect(output).toContain('export const eventsS3Sink = defineSinkPipe("events_s3_sink", {');
1442
+ expect(output).toContain("sink: {");
1443
+ expect(output).toContain("connection: exportsS3");
1444
+ expect(output).toContain('bucketUri: "s3://exports/events/"');
1445
+ expect(output).toContain('fileTemplate: "events_{date}"');
1446
+ expect(output).toContain('schedule: "@once"');
1447
+ expect(output).toContain('format: "ndjson"');
1448
+ expect(output).toContain('strategy: "create_new"');
1449
+ expect(output).toContain('compression: "gzip"');
1450
+ });
1451
+
1452
+ it("reports an error when sink pipe references a missing connection", async () => {
1453
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1454
+ tempDirs.push(tempDir);
1455
+
1456
+ writeFile(
1457
+ tempDir,
1458
+ "events_sink.pipe",
1459
+ `NODE publish
1460
+ SQL >
1461
+ SELECT * FROM events
1462
+ TYPE sink
1463
+ EXPORT_CONNECTION_NAME missing_connection
1464
+ EXPORT_KAFKA_TOPIC events_out
1465
+ EXPORT_SCHEDULE @on-demand
1466
+ `
1467
+ );
1468
+
1469
+ const result = await runMigrate({
1470
+ cwd: tempDir,
1471
+ patterns: ["."],
1472
+ strict: true,
1473
+ });
1474
+
1475
+ expect(result.success).toBe(false);
1476
+ expect(result.errors.map((error) => error.message)).toEqual(
1477
+ expect.arrayContaining([
1478
+ 'Sink pipe references missing/unmigrated connection "missing_connection".',
1479
+ ])
1480
+ );
1481
+ });
1482
+
1483
+ it("reports an error when sink connection type does not match sink service", async () => {
1484
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1485
+ tempDirs.push(tempDir);
1486
+
1487
+ writeFile(
1488
+ tempDir,
1489
+ "events_kafka.connection",
1490
+ `TYPE kafka
1491
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
1492
+ `
1493
+ );
1494
+
1495
+ writeFile(
1496
+ tempDir,
1497
+ "events_s3_sink.pipe",
1498
+ `NODE export
1499
+ SQL >
1500
+ SELECT * FROM events
1501
+ TYPE sink
1502
+ EXPORT_CONNECTION_NAME events_kafka
1503
+ EXPORT_BUCKET_URI s3://exports/events/
1504
+ EXPORT_FILE_TEMPLATE events_{date}
1505
+ EXPORT_SCHEDULE @once
1506
+ EXPORT_FORMAT csv
1507
+ `
1508
+ );
1509
+
1510
+ const result = await runMigrate({
1511
+ cwd: tempDir,
1512
+ patterns: ["."],
1513
+ strict: true,
1514
+ });
1515
+
1516
+ expect(result.success).toBe(false);
1517
+ expect(result.errors.map((error) => error.message)).toEqual(
1518
+ expect.arrayContaining([
1519
+ 'Sink pipe service "s3" is incompatible with connection "events_kafka" type "kafka".',
1520
+ ])
1521
+ );
1522
+ });
1523
+
1524
+ it("supports comments in sink pipe files", async () => {
1525
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tinybird-migrate-"));
1526
+ tempDirs.push(tempDir);
1527
+
1528
+ writeFile(
1529
+ tempDir,
1530
+ "events_kafka.connection",
1531
+ `TYPE kafka
1532
+ KAFKA_BOOTSTRAP_SERVERS localhost:9092
1533
+ `
1534
+ );
1535
+
1536
+ writeFile(
1537
+ tempDir,
1538
+ "events_sink.pipe",
1539
+ `# this pipe publishes records
1540
+ NODE publish
1541
+ SQL >
1542
+ SELECT * FROM events
1543
+ TYPE sink
1544
+ # Kafka target
1545
+ EXPORT_CONNECTION_NAME events_kafka
1546
+ EXPORT_KAFKA_TOPIC events_out
1547
+ EXPORT_SCHEDULE @on-demand
1548
+ `
1549
+ );
1550
+
1551
+ const result = await runMigrate({
1552
+ cwd: tempDir,
1553
+ patterns: ["."],
1554
+ strict: true,
1555
+ });
1556
+
1557
+ expect(result.success).toBe(true);
1558
+ expect(result.errors).toHaveLength(0);
1559
+ });
890
1560
  });