@tinybirdco/sdk 0.0.44 → 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.
- package/README.md +53 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +4 -3
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/migrate.test.js +42 -4
- package/dist/cli/commands/migrate.test.js.map +1 -1
- package/dist/generator/connection.d.ts +1 -1
- package/dist/generator/connection.d.ts.map +1 -1
- package/dist/generator/connection.js +25 -2
- package/dist/generator/connection.js.map +1 -1
- package/dist/generator/connection.test.js +37 -14
- package/dist/generator/connection.test.js.map +1 -1
- package/dist/generator/datasource.d.ts.map +1 -1
- package/dist/generator/datasource.js +23 -0
- package/dist/generator/datasource.js.map +1 -1
- package/dist/generator/datasource.test.js +49 -5
- package/dist/generator/datasource.test.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/migrate/emit-ts.d.ts.map +1 -1
- package/dist/migrate/emit-ts.js +69 -13
- package/dist/migrate/emit-ts.js.map +1 -1
- package/dist/migrate/parse-connection.d.ts +2 -2
- package/dist/migrate/parse-connection.d.ts.map +1 -1
- package/dist/migrate/parse-connection.js +61 -18
- 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 +31 -0
- package/dist/migrate/parse-datasource.js.map +1 -1
- package/dist/migrate/types.d.ts +18 -1
- package/dist/migrate/types.d.ts.map +1 -1
- package/dist/schema/connection.d.ts +49 -6
- package/dist/schema/connection.d.ts.map +1 -1
- package/dist/schema/connection.js +44 -9
- package/dist/schema/connection.js.map +1 -1
- package/dist/schema/connection.test.js +72 -17
- package/dist/schema/connection.test.js.map +1 -1
- package/dist/schema/datasource.d.ts +16 -1
- package/dist/schema/datasource.d.ts.map +1 -1
- package/dist/schema/datasource.js +3 -0
- package/dist/schema/datasource.js.map +1 -1
- package/dist/schema/datasource.test.js +21 -0
- package/dist/schema/datasource.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/migrate.test.ts +58 -4
- package/src/cli/commands/migrate.ts +6 -4
- package/src/generator/connection.test.ts +45 -14
- package/src/generator/connection.ts +30 -2
- package/src/generator/datasource.test.ts +57 -5
- package/src/generator/datasource.ts +38 -1
- package/src/index.ts +12 -1
- package/src/migrate/emit-ts.ts +80 -16
- package/src/migrate/parse-connection.ts +108 -30
- package/src/migrate/parse-datasource.ts +46 -1
- package/src/migrate/types.ts +24 -2
- package/src/schema/connection.test.ts +92 -17
- package/src/schema/connection.ts +86 -10
- package/src/schema/datasource.test.ts +25 -0
- 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(
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
package/src/migrate/types.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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("
|
|
12
|
+
describe("defineKafkaConnection", () => {
|
|
11
13
|
it("creates a Kafka connection with required fields", () => {
|
|
12
|
-
const conn =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
77
|
+
defineKafkaConnection("123invalid", {
|
|
76
78
|
bootstrapServers: "kafka.example.com:9092",
|
|
77
79
|
})
|
|
78
80
|
).toThrow("Invalid connection name");
|
|
79
81
|
|
|
80
82
|
expect(() =>
|
|
81
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
});
|
package/src/schema/connection.ts
CHANGED
|
@@ -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
|
-
*
|
|
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 {
|
|
104
|
+
* import { defineKafkaConnection } from '@tinybirdco/sdk';
|
|
68
105
|
*
|
|
69
|
-
* export const myKafka =
|
|
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
|
|
115
|
+
export function defineKafkaConnection(
|
|
79
116
|
name: string,
|
|
80
117
|
options: KafkaConnectionOptions
|
|
81
118
|
): KafkaConnectionDefinition {
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
*/
|