@tinybirdco/sdk 0.0.43 → 0.0.45

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 (82) hide show
  1. package/README.md +53 -0
  2. package/dist/cli/commands/init.js +2 -2
  3. package/dist/cli/commands/init.test.js +1 -1
  4. package/dist/cli/commands/init.test.js.map +1 -1
  5. package/dist/cli/commands/migrate.d.ts.map +1 -1
  6. package/dist/cli/commands/migrate.js +4 -3
  7. package/dist/cli/commands/migrate.js.map +1 -1
  8. package/dist/cli/commands/migrate.test.js +42 -4
  9. package/dist/cli/commands/migrate.test.js.map +1 -1
  10. package/dist/client/preview.js +1 -1
  11. package/dist/client/preview.js.map +1 -1
  12. package/dist/codegen/index.js +4 -4
  13. package/dist/codegen/index.js.map +1 -1
  14. package/dist/codegen/index.test.js +3 -3
  15. package/dist/codegen/index.test.js.map +1 -1
  16. package/dist/generator/client.js +1 -1
  17. package/dist/generator/connection.d.ts +1 -1
  18. package/dist/generator/connection.d.ts.map +1 -1
  19. package/dist/generator/connection.js +25 -2
  20. package/dist/generator/connection.js.map +1 -1
  21. package/dist/generator/connection.test.js +37 -14
  22. package/dist/generator/connection.test.js.map +1 -1
  23. package/dist/generator/datasource.d.ts.map +1 -1
  24. package/dist/generator/datasource.js +23 -0
  25. package/dist/generator/datasource.js.map +1 -1
  26. package/dist/generator/datasource.test.js +49 -5
  27. package/dist/generator/datasource.test.js.map +1 -1
  28. package/dist/index.d.ts +5 -5
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.test.d.ts +2 -0
  33. package/dist/index.test.d.ts.map +1 -0
  34. package/dist/index.test.js +12 -0
  35. package/dist/index.test.js.map +1 -0
  36. package/dist/migrate/emit-ts.d.ts.map +1 -1
  37. package/dist/migrate/emit-ts.js +69 -13
  38. package/dist/migrate/emit-ts.js.map +1 -1
  39. package/dist/migrate/parse-connection.d.ts +2 -2
  40. package/dist/migrate/parse-connection.d.ts.map +1 -1
  41. package/dist/migrate/parse-connection.js +61 -18
  42. package/dist/migrate/parse-connection.js.map +1 -1
  43. package/dist/migrate/parse-datasource.d.ts.map +1 -1
  44. package/dist/migrate/parse-datasource.js +31 -0
  45. package/dist/migrate/parse-datasource.js.map +1 -1
  46. package/dist/migrate/types.d.ts +18 -1
  47. package/dist/migrate/types.d.ts.map +1 -1
  48. package/dist/schema/connection.d.ts +49 -6
  49. package/dist/schema/connection.d.ts.map +1 -1
  50. package/dist/schema/connection.js +44 -9
  51. package/dist/schema/connection.js.map +1 -1
  52. package/dist/schema/connection.test.js +72 -17
  53. package/dist/schema/connection.test.js.map +1 -1
  54. package/dist/schema/datasource.d.ts +16 -1
  55. package/dist/schema/datasource.d.ts.map +1 -1
  56. package/dist/schema/datasource.js +3 -0
  57. package/dist/schema/datasource.js.map +1 -1
  58. package/dist/schema/datasource.test.js +21 -0
  59. package/dist/schema/datasource.test.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/cli/commands/init.test.ts +1 -1
  62. package/src/cli/commands/init.ts +2 -2
  63. package/src/cli/commands/migrate.test.ts +58 -4
  64. package/src/cli/commands/migrate.ts +6 -4
  65. package/src/client/preview.ts +1 -1
  66. package/src/codegen/index.test.ts +3 -3
  67. package/src/codegen/index.ts +4 -4
  68. package/src/generator/client.ts +1 -1
  69. package/src/generator/connection.test.ts +45 -14
  70. package/src/generator/connection.ts +30 -2
  71. package/src/generator/datasource.test.ts +57 -5
  72. package/src/generator/datasource.ts +38 -1
  73. package/src/index.test.ts +13 -0
  74. package/src/index.ts +21 -4
  75. package/src/migrate/emit-ts.ts +80 -16
  76. package/src/migrate/parse-connection.ts +108 -30
  77. package/src/migrate/parse-datasource.ts +46 -1
  78. package/src/migrate/types.ts +24 -2
  79. package/src/schema/connection.test.ts +92 -17
  80. package/src/schema/connection.ts +86 -10
  81. package/src/schema/datasource.test.ts +25 -0
  82. package/src/schema/datasource.ts +21 -1
@@ -1,14 +1,18 @@
1
- import type { KafkaConnectionModel, ResourceFile } from "./types.js";
1
+ import type { KafkaConnectionModel, ResourceFile, S3ConnectionModel } from "./types.js";
2
2
  import {
3
3
  MigrationParseError,
4
4
  isBlank,
5
5
  parseDirectiveLine,
6
+ parseQuotedValue,
6
7
  splitLines,
7
8
  } from "./parser-utils.js";
8
9
 
9
- export function parseConnectionFile(resource: ResourceFile): KafkaConnectionModel {
10
+ export function parseConnectionFile(
11
+ resource: ResourceFile
12
+ ): KafkaConnectionModel | S3ConnectionModel {
10
13
  const lines = splitLines(resource.content);
11
14
  let connectionType: string | undefined;
15
+
12
16
  let bootstrapServers: string | undefined;
13
17
  let securityProtocol:
14
18
  | "SASL_SSL"
@@ -25,6 +29,11 @@ export function parseConnectionFile(resource: ResourceFile): KafkaConnectionMode
25
29
  let secret: string | undefined;
26
30
  let sslCaPem: string | undefined;
27
31
 
32
+ let region: string | undefined;
33
+ let arn: string | undefined;
34
+ let accessKey: string | undefined;
35
+ let accessSecret: string | undefined;
36
+
28
37
  for (const rawLine of lines) {
29
38
  const line = rawLine.trim();
30
39
  if (isBlank(line)) {
@@ -34,7 +43,7 @@ export function parseConnectionFile(resource: ResourceFile): KafkaConnectionMode
34
43
  const { key: directive, value } = parseDirectiveLine(line);
35
44
  switch (directive) {
36
45
  case "TYPE":
37
- connectionType = value;
46
+ connectionType = parseQuotedValue(value);
38
47
  break;
39
48
  case "KAFKA_BOOTSTRAP_SERVERS":
40
49
  bootstrapServers = value;
@@ -75,6 +84,18 @@ export function parseConnectionFile(resource: ResourceFile): KafkaConnectionMode
75
84
  case "KAFKA_SSL_CA_PEM":
76
85
  sslCaPem = value;
77
86
  break;
87
+ case "S3_REGION":
88
+ region = parseQuotedValue(value);
89
+ break;
90
+ case "S3_ARN":
91
+ arn = parseQuotedValue(value);
92
+ break;
93
+ case "S3_ACCESS_KEY":
94
+ accessKey = parseQuotedValue(value);
95
+ break;
96
+ case "S3_SECRET":
97
+ accessSecret = parseQuotedValue(value);
98
+ break;
78
99
  default:
79
100
  throw new MigrationParseError(
80
101
  resource.filePath,
@@ -94,35 +115,92 @@ export function parseConnectionFile(resource: ResourceFile): KafkaConnectionMode
94
115
  );
95
116
  }
96
117
 
97
- if (connectionType !== "kafka") {
98
- throw new MigrationParseError(
99
- resource.filePath,
100
- "connection",
101
- resource.name,
102
- `Unsupported connection type in strict mode: "${connectionType}"`
103
- );
118
+ if (connectionType === "kafka") {
119
+ if (region || arn || accessKey || accessSecret) {
120
+ throw new MigrationParseError(
121
+ resource.filePath,
122
+ "connection",
123
+ resource.name,
124
+ "S3 directives are not valid for kafka connections."
125
+ );
126
+ }
127
+
128
+ if (!bootstrapServers) {
129
+ throw new MigrationParseError(
130
+ resource.filePath,
131
+ "connection",
132
+ resource.name,
133
+ "KAFKA_BOOTSTRAP_SERVERS is required for kafka connections."
134
+ );
135
+ }
136
+
137
+ return {
138
+ kind: "connection",
139
+ name: resource.name,
140
+ filePath: resource.filePath,
141
+ connectionType: "kafka",
142
+ bootstrapServers,
143
+ securityProtocol,
144
+ saslMechanism,
145
+ key,
146
+ secret,
147
+ sslCaPem,
148
+ };
104
149
  }
105
150
 
106
- if (!bootstrapServers) {
107
- throw new MigrationParseError(
108
- resource.filePath,
109
- "connection",
110
- resource.name,
111
- "KAFKA_BOOTSTRAP_SERVERS is required for kafka connections."
112
- );
151
+ if (connectionType === "s3") {
152
+ if (bootstrapServers || securityProtocol || saslMechanism || key || secret || sslCaPem) {
153
+ throw new MigrationParseError(
154
+ resource.filePath,
155
+ "connection",
156
+ resource.name,
157
+ "Kafka directives are not valid for s3 connections."
158
+ );
159
+ }
160
+
161
+ if (!region) {
162
+ throw new MigrationParseError(
163
+ resource.filePath,
164
+ "connection",
165
+ resource.name,
166
+ "S3_REGION is required for s3 connections."
167
+ );
168
+ }
169
+
170
+ if (!arn && !(accessKey && accessSecret)) {
171
+ throw new MigrationParseError(
172
+ resource.filePath,
173
+ "connection",
174
+ resource.name,
175
+ "S3 connections require S3_ARN or both S3_ACCESS_KEY and S3_SECRET."
176
+ );
177
+ }
178
+
179
+ if ((accessKey && !accessSecret) || (!accessKey && accessSecret)) {
180
+ throw new MigrationParseError(
181
+ resource.filePath,
182
+ "connection",
183
+ resource.name,
184
+ "S3_ACCESS_KEY and S3_SECRET must be provided together."
185
+ );
186
+ }
187
+
188
+ return {
189
+ kind: "connection",
190
+ name: resource.name,
191
+ filePath: resource.filePath,
192
+ connectionType: "s3",
193
+ region,
194
+ arn,
195
+ accessKey,
196
+ secret: accessSecret,
197
+ };
113
198
  }
114
199
 
115
- return {
116
- kind: "connection",
117
- name: resource.name,
118
- filePath: resource.filePath,
119
- connectionType: "kafka",
120
- bootstrapServers,
121
- securityProtocol,
122
- saslMechanism,
123
- key,
124
- secret,
125
- sslCaPem,
126
- };
200
+ throw new MigrationParseError(
201
+ resource.filePath,
202
+ "connection",
203
+ resource.name,
204
+ `Unsupported connection type in strict mode: "${connectionType}"`
205
+ );
127
206
  }
128
-
@@ -233,6 +233,11 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
233
233
  let kafkaGroupId: string | undefined;
234
234
  let kafkaAutoOffsetReset: "earliest" | "latest" | undefined;
235
235
 
236
+ let importConnectionName: string | undefined;
237
+ let importBucketUri: string | undefined;
238
+ let importSchedule: string | undefined;
239
+ let importFromTimestamp: string | undefined;
240
+
236
241
  let i = 0;
237
242
  while (i < lines.length) {
238
243
  const rawLine = lines[i] ?? "";
@@ -365,6 +370,18 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
365
370
  }
366
371
  kafkaAutoOffsetReset = value;
367
372
  break;
373
+ case "IMPORT_CONNECTION_NAME":
374
+ importConnectionName = parseQuotedValue(value);
375
+ break;
376
+ case "IMPORT_BUCKET_URI":
377
+ importBucketUri = parseQuotedValue(value);
378
+ break;
379
+ case "IMPORT_SCHEDULE":
380
+ importSchedule = parseQuotedValue(value);
381
+ break;
382
+ case "IMPORT_FROM_TIMESTAMP":
383
+ importFromTimestamp = parseQuotedValue(value);
384
+ break;
368
385
  case "TOKEN":
369
386
  tokens.push(parseToken(resource.filePath, resource.name, value));
370
387
  break;
@@ -426,6 +443,34 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
426
443
  );
427
444
  }
428
445
 
446
+ const s3 =
447
+ importConnectionName || importBucketUri || importSchedule || importFromTimestamp
448
+ ? {
449
+ connectionName: importConnectionName ?? "",
450
+ bucketUri: importBucketUri ?? "",
451
+ schedule: importSchedule,
452
+ fromTimestamp: importFromTimestamp,
453
+ }
454
+ : undefined;
455
+
456
+ if (s3 && (!s3.connectionName || !s3.bucketUri)) {
457
+ throw new MigrationParseError(
458
+ resource.filePath,
459
+ "datasource",
460
+ resource.name,
461
+ "IMPORT_CONNECTION_NAME and IMPORT_BUCKET_URI are required when S3 import directives are used."
462
+ );
463
+ }
464
+
465
+ if (kafka && s3) {
466
+ throw new MigrationParseError(
467
+ resource.filePath,
468
+ "datasource",
469
+ resource.name,
470
+ "Datasource cannot mix Kafka directives with S3 import directives."
471
+ );
472
+ }
473
+
429
474
  return {
430
475
  kind: "datasource",
431
476
  name: resource.name,
@@ -445,9 +490,9 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
445
490
  settings,
446
491
  },
447
492
  kafka,
493
+ s3,
448
494
  forwardQuery,
449
495
  tokens,
450
496
  sharedWith,
451
497
  };
452
498
  }
453
-
@@ -43,6 +43,13 @@ export interface DatasourceKafkaModel {
43
43
  autoOffsetReset?: "earliest" | "latest";
44
44
  }
45
45
 
46
+ export interface DatasourceS3Model {
47
+ connectionName: string;
48
+ bucketUri: string;
49
+ schedule?: string;
50
+ fromTimestamp?: string;
51
+ }
52
+
46
53
  export interface DatasourceTokenModel {
47
54
  name: string;
48
55
  scope: "READ" | "APPEND";
@@ -56,6 +63,7 @@ export interface DatasourceModel {
56
63
  columns: DatasourceColumnModel[];
57
64
  engine: DatasourceEngineModel;
58
65
  kafka?: DatasourceKafkaModel;
66
+ s3?: DatasourceS3Model;
59
67
  forwardQuery?: string;
60
68
  tokens: DatasourceTokenModel[];
61
69
  sharedWith: string[];
@@ -112,7 +120,22 @@ export interface KafkaConnectionModel {
112
120
  sslCaPem?: string;
113
121
  }
114
122
 
115
- export type ParsedResource = DatasourceModel | PipeModel | KafkaConnectionModel;
123
+ export interface S3ConnectionModel {
124
+ kind: "connection";
125
+ name: string;
126
+ filePath: string;
127
+ connectionType: "s3";
128
+ region: string;
129
+ arn?: string;
130
+ accessKey?: string;
131
+ secret?: string;
132
+ }
133
+
134
+ export type ParsedResource =
135
+ | DatasourceModel
136
+ | PipeModel
137
+ | KafkaConnectionModel
138
+ | S3ConnectionModel;
116
139
 
117
140
  export interface MigrationResult {
118
141
  success: boolean;
@@ -122,4 +145,3 @@ export interface MigrationResult {
122
145
  dryRun: boolean;
123
146
  outputContent?: string;
124
147
  }
125
-
@@ -1,15 +1,17 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import {
3
- createKafkaConnection,
3
+ defineKafkaConnection,
4
+ defineS3Connection,
4
5
  isConnectionDefinition,
5
6
  isKafkaConnectionDefinition,
7
+ isS3ConnectionDefinition,
6
8
  getConnectionType,
7
9
  } from "./connection.js";
8
10
 
9
11
  describe("Connection Schema", () => {
10
- describe("createKafkaConnection", () => {
12
+ describe("defineKafkaConnection", () => {
11
13
  it("creates a Kafka connection with required fields", () => {
12
- const conn = createKafkaConnection("my_kafka", {
14
+ const conn = defineKafkaConnection("my_kafka", {
13
15
  bootstrapServers: "kafka.example.com:9092",
14
16
  });
15
17
 
@@ -20,7 +22,7 @@ describe("Connection Schema", () => {
20
22
  });
21
23
 
22
24
  it("creates a Kafka connection with all options", () => {
23
- const conn = createKafkaConnection("my_kafka", {
25
+ const conn = defineKafkaConnection("my_kafka", {
24
26
  bootstrapServers: "kafka.example.com:9092",
25
27
  securityProtocol: "SASL_SSL",
26
28
  saslMechanism: "PLAIN",
@@ -37,19 +39,19 @@ describe("Connection Schema", () => {
37
39
  });
38
40
 
39
41
  it("supports different SASL mechanisms", () => {
40
- const scramConn = createKafkaConnection("scram_kafka", {
42
+ const scramConn = defineKafkaConnection("scram_kafka", {
41
43
  bootstrapServers: "kafka.example.com:9092",
42
44
  saslMechanism: "SCRAM-SHA-256",
43
45
  });
44
46
  expect(scramConn.options.saslMechanism).toBe("SCRAM-SHA-256");
45
47
 
46
- const scram512Conn = createKafkaConnection("scram512_kafka", {
48
+ const scram512Conn = defineKafkaConnection("scram512_kafka", {
47
49
  bootstrapServers: "kafka.example.com:9092",
48
50
  saslMechanism: "SCRAM-SHA-512",
49
51
  });
50
52
  expect(scram512Conn.options.saslMechanism).toBe("SCRAM-SHA-512");
51
53
 
52
- const oauthConn = createKafkaConnection("oauth_kafka", {
54
+ const oauthConn = defineKafkaConnection("oauth_kafka", {
53
55
  bootstrapServers: "kafka.example.com:9092",
54
56
  saslMechanism: "OAUTHBEARER",
55
57
  });
@@ -57,13 +59,13 @@ describe("Connection Schema", () => {
57
59
  });
58
60
 
59
61
  it("supports different security protocols", () => {
60
- const plaintext = createKafkaConnection("plaintext_kafka", {
62
+ const plaintext = defineKafkaConnection("plaintext_kafka", {
61
63
  bootstrapServers: "localhost:9092",
62
64
  securityProtocol: "PLAINTEXT",
63
65
  });
64
66
  expect(plaintext.options.securityProtocol).toBe("PLAINTEXT");
65
67
 
66
- const saslPlaintext = createKafkaConnection("sasl_plaintext_kafka", {
68
+ const saslPlaintext = defineKafkaConnection("sasl_plaintext_kafka", {
67
69
  bootstrapServers: "localhost:9092",
68
70
  securityProtocol: "SASL_PLAINTEXT",
69
71
  });
@@ -72,40 +74,88 @@ describe("Connection Schema", () => {
72
74
 
73
75
  it("throws error for invalid connection name", () => {
74
76
  expect(() =>
75
- createKafkaConnection("123invalid", {
77
+ defineKafkaConnection("123invalid", {
76
78
  bootstrapServers: "kafka.example.com:9092",
77
79
  })
78
80
  ).toThrow("Invalid connection name");
79
81
 
80
82
  expect(() =>
81
- createKafkaConnection("my-connection", {
83
+ defineKafkaConnection("my-connection", {
82
84
  bootstrapServers: "kafka.example.com:9092",
83
85
  })
84
86
  ).toThrow("Invalid connection name");
85
87
 
86
88
  expect(() =>
87
- createKafkaConnection("", {
89
+ defineKafkaConnection("", {
88
90
  bootstrapServers: "kafka.example.com:9092",
89
91
  })
90
92
  ).toThrow("Invalid connection name");
91
93
  });
92
94
 
93
95
  it("allows valid naming patterns", () => {
94
- const conn1 = createKafkaConnection("_private_kafka", {
96
+ const conn1 = defineKafkaConnection("_private_kafka", {
95
97
  bootstrapServers: "kafka.example.com:9092",
96
98
  });
97
99
  expect(conn1._name).toBe("_private_kafka");
98
100
 
99
- const conn2 = createKafkaConnection("kafka_v2", {
101
+ const conn2 = defineKafkaConnection("kafka_v2", {
100
102
  bootstrapServers: "kafka.example.com:9092",
101
103
  });
102
104
  expect(conn2._name).toBe("kafka_v2");
103
105
  });
104
106
  });
105
107
 
108
+ describe("defineS3Connection", () => {
109
+ it("creates an S3 connection with IAM role auth", () => {
110
+ const conn = defineS3Connection("my_s3", {
111
+ region: "us-east-1",
112
+ arn: "arn:aws:iam::123456789012:role/tinybird-s3-access",
113
+ });
114
+
115
+ expect(conn._name).toBe("my_s3");
116
+ expect(conn._type).toBe("connection");
117
+ expect(conn._connectionType).toBe("s3");
118
+ expect(conn.options.region).toBe("us-east-1");
119
+ expect(conn.options.arn).toBe("arn:aws:iam::123456789012:role/tinybird-s3-access");
120
+ });
121
+
122
+ it("creates an S3 connection with access key auth", () => {
123
+ const conn = defineS3Connection("my_s3", {
124
+ region: "us-east-1",
125
+ accessKey: '{{ tb_secret("S3_ACCESS_KEY") }}',
126
+ secret: '{{ tb_secret("S3_SECRET") }}',
127
+ });
128
+
129
+ expect(conn.options.accessKey).toBe('{{ tb_secret("S3_ACCESS_KEY") }}');
130
+ expect(conn.options.secret).toBe('{{ tb_secret("S3_SECRET") }}');
131
+ });
132
+
133
+ it("throws when auth config is incomplete", () => {
134
+ expect(() =>
135
+ defineS3Connection("my_s3", {
136
+ region: "us-east-1",
137
+ })
138
+ ).toThrow("S3 connection requires either `arn` or both `accessKey` and `secret`.");
139
+
140
+ expect(() =>
141
+ defineS3Connection("my_s3", {
142
+ region: "us-east-1",
143
+ accessKey: "key-only",
144
+ })
145
+ ).toThrow("S3 connection requires either `arn` or both `accessKey` and `secret`.");
146
+
147
+ expect(() =>
148
+ defineS3Connection("my_s3", {
149
+ region: "us-east-1",
150
+ secret: "secret-only",
151
+ })
152
+ ).toThrow("S3 connection requires either `arn` or both `accessKey` and `secret`.");
153
+ });
154
+ });
155
+
106
156
  describe("isConnectionDefinition", () => {
107
157
  it("returns true for valid connection", () => {
108
- const conn = createKafkaConnection("my_kafka", {
158
+ const conn = defineKafkaConnection("my_kafka", {
109
159
  bootstrapServers: "kafka.example.com:9092",
110
160
  });
111
161
 
@@ -124,7 +174,7 @@ describe("Connection Schema", () => {
124
174
 
125
175
  describe("isKafkaConnectionDefinition", () => {
126
176
  it("returns true for Kafka connection", () => {
127
- const conn = createKafkaConnection("my_kafka", {
177
+ const conn = defineKafkaConnection("my_kafka", {
128
178
  bootstrapServers: "kafka.example.com:9092",
129
179
  });
130
180
 
@@ -137,13 +187,38 @@ describe("Connection Schema", () => {
137
187
  });
138
188
  });
139
189
 
190
+ describe("isS3ConnectionDefinition", () => {
191
+ it("returns true for S3 connection", () => {
192
+ const conn = defineS3Connection("my_s3", {
193
+ region: "us-east-1",
194
+ arn: "arn:aws:iam::123456789012:role/tinybird-s3-access",
195
+ });
196
+
197
+ expect(isS3ConnectionDefinition(conn)).toBe(true);
198
+ });
199
+
200
+ it("returns false for non-S3 objects", () => {
201
+ expect(isS3ConnectionDefinition({})).toBe(false);
202
+ expect(isS3ConnectionDefinition(null)).toBe(false);
203
+ });
204
+ });
205
+
140
206
  describe("getConnectionType", () => {
141
207
  it("returns the connection type", () => {
142
- const conn = createKafkaConnection("my_kafka", {
208
+ const conn = defineKafkaConnection("my_kafka", {
143
209
  bootstrapServers: "kafka.example.com:9092",
144
210
  });
145
211
 
146
212
  expect(getConnectionType(conn)).toBe("kafka");
147
213
  });
214
+
215
+ it("returns the s3 connection type", () => {
216
+ const conn = defineS3Connection("my_s3", {
217
+ region: "us-east-1",
218
+ arn: "arn:aws:iam::123456789012:role/tinybird-s3-access",
219
+ });
220
+
221
+ expect(getConnectionType(conn)).toBe("s3");
222
+ });
148
223
  });
149
224
  });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Connection definition for Tinybird
3
- * Define external connections (Kafka, etc.) as TypeScript with full type safety
3
+ * Define external connections (Kafka, S3, etc.) as TypeScript with full type safety
4
4
  */
5
5
 
6
6
  // Symbol for brand typing - use Symbol.for() for global registry
@@ -50,13 +50,50 @@ export interface KafkaConnectionDefinition {
50
50
  readonly options: KafkaConnectionOptions;
51
51
  }
52
52
 
53
+ /**
54
+ * Options for defining an S3 connection
55
+ */
56
+ export interface S3ConnectionOptions {
57
+ /** S3 bucket region (for example: us-east-1) */
58
+ region: string;
59
+ /** IAM role ARN used by Tinybird to access the bucket */
60
+ arn?: string;
61
+ /** S3 access key for key/secret auth */
62
+ accessKey?: string;
63
+ /** S3 secret key for key/secret auth */
64
+ secret?: string;
65
+ }
66
+
67
+ /**
68
+ * S3-specific connection definition
69
+ */
70
+ export interface S3ConnectionDefinition {
71
+ readonly [CONNECTION_BRAND]: true;
72
+ /** Connection name */
73
+ readonly _name: string;
74
+ /** Type marker for inference */
75
+ readonly _type: "connection";
76
+ /** Connection type */
77
+ readonly _connectionType: "s3";
78
+ /** S3 options */
79
+ readonly options: S3ConnectionOptions;
80
+ }
81
+
53
82
  /**
54
83
  * A connection definition - union of all connection types
55
84
  */
56
- export type ConnectionDefinition = KafkaConnectionDefinition;
85
+ export type ConnectionDefinition = KafkaConnectionDefinition | S3ConnectionDefinition;
86
+
87
+ function validateConnectionName(name: string): void {
88
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
89
+ throw new Error(
90
+ `Invalid connection name: "${name}". Must start with a letter or underscore and contain only alphanumeric characters and underscores.`
91
+ );
92
+ }
93
+ }
57
94
 
58
95
  /**
59
- * Create a Kafka connection
96
+ * Define a Kafka connection
60
97
  *
61
98
  * @param name - The connection name (must be valid identifier)
62
99
  * @param options - Kafka connection configuration
@@ -64,9 +101,9 @@ export type ConnectionDefinition = KafkaConnectionDefinition;
64
101
  *
65
102
  * @example
66
103
  * ```ts
67
- * import { createKafkaConnection } from '@tinybirdco/sdk';
104
+ * import { defineKafkaConnection } from '@tinybirdco/sdk';
68
105
  *
69
- * export const myKafka = createKafkaConnection('my_kafka', {
106
+ * export const myKafka = defineKafkaConnection('my_kafka', {
70
107
  * bootstrapServers: 'kafka.example.com:9092',
71
108
  * securityProtocol: 'SASL_SSL',
72
109
  * saslMechanism: 'PLAIN',
@@ -75,22 +112,54 @@ export type ConnectionDefinition = KafkaConnectionDefinition;
75
112
  * });
76
113
  * ```
77
114
  */
78
- export function createKafkaConnection(
115
+ export function defineKafkaConnection(
79
116
  name: string,
80
117
  options: KafkaConnectionOptions
81
118
  ): KafkaConnectionDefinition {
82
- // Validate name is a valid identifier
83
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
119
+ validateConnectionName(name);
120
+
121
+ return {
122
+ [CONNECTION_BRAND]: true,
123
+ _name: name,
124
+ _type: "connection",
125
+ _connectionType: "kafka",
126
+ options,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * @deprecated Use defineKafkaConnection instead.
132
+ */
133
+ export const createKafkaConnection = defineKafkaConnection;
134
+
135
+ /**
136
+ * Define an S3 connection
137
+ *
138
+ * @param name - The connection name (must be valid identifier)
139
+ * @param options - S3 connection configuration
140
+ * @returns A connection definition that can be used in a project
141
+ */
142
+ export function defineS3Connection(
143
+ name: string,
144
+ options: S3ConnectionOptions
145
+ ): S3ConnectionDefinition {
146
+ validateConnectionName(name);
147
+
148
+ if (!options.arn && !(options.accessKey && options.secret)) {
84
149
  throw new Error(
85
- `Invalid connection name: "${name}". Must start with a letter or underscore and contain only alphanumeric characters and underscores.`
150
+ "S3 connection requires either `arn` or both `accessKey` and `secret`."
86
151
  );
87
152
  }
88
153
 
154
+ if ((options.accessKey && !options.secret) || (!options.accessKey && options.secret)) {
155
+ throw new Error("S3 connection `accessKey` and `secret` must be provided together.");
156
+ }
157
+
89
158
  return {
90
159
  [CONNECTION_BRAND]: true,
91
160
  _name: name,
92
161
  _type: "connection",
93
- _connectionType: "kafka",
162
+ _connectionType: "s3",
94
163
  options,
95
164
  };
96
165
  }
@@ -114,6 +183,13 @@ export function isKafkaConnectionDefinition(value: unknown): value is KafkaConne
114
183
  return isConnectionDefinition(value) && value._connectionType === "kafka";
115
184
  }
116
185
 
186
+ /**
187
+ * Check if a value is an S3 connection definition
188
+ */
189
+ export function isS3ConnectionDefinition(value: unknown): value is S3ConnectionDefinition {
190
+ return isConnectionDefinition(value) && value._connectionType === "s3";
191
+ }
192
+
117
193
  /**
118
194
  * Get the connection type from a connection definition
119
195
  */