@tinybirdco/sdk 0.0.49 → 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 (59) hide show
  1. package/README.md +19 -2
  2. package/dist/cli/commands/migrate.d.ts.map +1 -1
  3. package/dist/cli/commands/migrate.js +36 -1
  4. package/dist/cli/commands/migrate.js.map +1 -1
  5. package/dist/cli/commands/migrate.test.js +60 -0
  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/index.d.ts +3 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/migrate/emit-ts.d.ts.map +1 -1
  22. package/dist/migrate/emit-ts.js +45 -11
  23. package/dist/migrate/emit-ts.js.map +1 -1
  24. package/dist/migrate/parse-connection.d.ts +2 -2
  25. package/dist/migrate/parse-connection.d.ts.map +1 -1
  26. package/dist/migrate/parse-connection.js +34 -4
  27. package/dist/migrate/parse-connection.js.map +1 -1
  28. package/dist/migrate/parse-datasource.js +2 -2
  29. package/dist/migrate/parse-datasource.js.map +1 -1
  30. package/dist/migrate/types.d.ts +15 -1
  31. package/dist/migrate/types.d.ts.map +1 -1
  32. package/dist/schema/connection.d.ts +34 -1
  33. package/dist/schema/connection.d.ts.map +1 -1
  34. package/dist/schema/connection.js +26 -0
  35. package/dist/schema/connection.js.map +1 -1
  36. package/dist/schema/connection.test.js +35 -1
  37. package/dist/schema/connection.test.js.map +1 -1
  38. package/dist/schema/datasource.d.ts +16 -1
  39. package/dist/schema/datasource.d.ts.map +1 -1
  40. package/dist/schema/datasource.js +3 -2
  41. package/dist/schema/datasource.js.map +1 -1
  42. package/dist/schema/datasource.test.js +32 -3
  43. package/dist/schema/datasource.test.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/cli/commands/migrate.test.ts +91 -0
  46. package/src/cli/commands/migrate.ts +39 -1
  47. package/src/generator/connection.test.ts +29 -4
  48. package/src/generator/connection.ts +25 -2
  49. package/src/generator/datasource.test.ts +30 -1
  50. package/src/generator/datasource.ts +22 -10
  51. package/src/index.ts +5 -0
  52. package/src/migrate/emit-ts.ts +54 -14
  53. package/src/migrate/parse-connection.ts +56 -6
  54. package/src/migrate/parse-datasource.ts +2 -2
  55. package/src/migrate/types.ts +18 -1
  56. package/src/schema/connection.test.ts +48 -0
  57. package/src/schema/connection.ts +60 -1
  58. package/src/schema/datasource.test.ts +38 -3
  59. package/src/schema/datasource.ts +24 -3
@@ -1,6 +1,10 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { generateConnection, generateAllConnections } from "./connection.js";
3
- import { defineKafkaConnection, defineS3Connection } from "../schema/connection.js";
3
+ import {
4
+ defineKafkaConnection,
5
+ defineS3Connection,
6
+ defineGCSConnection,
7
+ } from "../schema/connection.js";
4
8
 
5
9
  describe("Connection Generator", () => {
6
10
  describe("generateConnection", () => {
@@ -152,6 +156,20 @@ describe("Connection Generator", () => {
152
156
  expect(result.content).toContain('S3_ACCESS_KEY {{ tb_secret("S3_ACCESS_KEY") }}');
153
157
  expect(result.content).toContain('S3_SECRET {{ tb_secret("S3_SECRET") }}');
154
158
  });
159
+
160
+ it("generates GCS connection with service account credentials", () => {
161
+ const conn = defineGCSConnection("my_gcs", {
162
+ serviceAccountCredentialsJson: '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}',
163
+ });
164
+
165
+ const result = generateConnection(conn);
166
+
167
+ expect(result.name).toBe("my_gcs");
168
+ expect(result.content).toContain("TYPE gcs");
169
+ expect(result.content).toContain(
170
+ 'GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON {{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}'
171
+ );
172
+ });
155
173
  });
156
174
 
157
175
  describe("generateAllConnections", () => {
@@ -163,11 +181,18 @@ describe("Connection Generator", () => {
163
181
  region: "us-east-1",
164
182
  arn: "arn:aws:iam::123456789012:role/tinybird-s3-access",
165
183
  });
184
+ const conn3 = defineGCSConnection("gcs_landing", {
185
+ serviceAccountCredentialsJson: '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}',
186
+ });
166
187
 
167
- const results = generateAllConnections({ kafka1: conn1, s3_logs: conn2 });
188
+ const results = generateAllConnections({
189
+ kafka1: conn1,
190
+ s3_logs: conn2,
191
+ gcs_landing: conn3,
192
+ });
168
193
 
169
- expect(results).toHaveLength(2);
170
- expect(results.map((r) => r.name).sort()).toEqual(["kafka1", "s3_logs"]);
194
+ expect(results).toHaveLength(3);
195
+ expect(results.map((r) => r.name).sort()).toEqual(["gcs_landing", "kafka1", "s3_logs"]);
171
196
  });
172
197
 
173
198
  it("returns empty array for empty connections", () => {
@@ -3,8 +3,16 @@
3
3
  * Converts ConnectionDefinition to native .connection file format
4
4
  */
5
5
 
6
- import type { ConnectionDefinition, KafkaConnectionDefinition } from "../schema/connection.js";
7
- import { isS3ConnectionDefinition, type S3ConnectionDefinition } from "../schema/connection.js";
6
+ import type {
7
+ ConnectionDefinition,
8
+ KafkaConnectionDefinition,
9
+ GCSConnectionDefinition,
10
+ } from "../schema/connection.js";
11
+ import {
12
+ isS3ConnectionDefinition,
13
+ isGCSConnectionDefinition,
14
+ type S3ConnectionDefinition,
15
+ } from "../schema/connection.js";
8
16
 
9
17
  /**
10
18
  * Generated connection content
@@ -78,6 +86,19 @@ function generateS3Connection(connection: S3ConnectionDefinition): string {
78
86
  return parts.join("\n");
79
87
  }
80
88
 
89
+ /**
90
+ * Generate a GCS connection content
91
+ */
92
+ function generateGCSConnection(connection: GCSConnectionDefinition): string {
93
+ const parts: string[] = [];
94
+ const options = connection.options;
95
+
96
+ parts.push("TYPE gcs");
97
+ parts.push(`GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON ${options.serviceAccountCredentialsJson}`);
98
+
99
+ return parts.join("\n");
100
+ }
101
+
81
102
  /**
82
103
  * Generate a .connection file content from a ConnectionDefinition
83
104
  *
@@ -113,6 +134,8 @@ export function generateConnection(
113
134
  content = generateKafkaConnection(connection as KafkaConnectionDefinition);
114
135
  } else if (isS3ConnectionDefinition(connection)) {
115
136
  content = generateS3Connection(connection);
137
+ } else if (isGCSConnectionDefinition(connection)) {
138
+ content = generateGCSConnection(connection);
116
139
  } else {
117
140
  throw new Error("Unsupported connection type.");
118
141
  }
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { generateDatasource, generateAllDatasources } from './datasource.js';
3
3
  import { defineDatasource } from '../schema/datasource.js';
4
- import { defineKafkaConnection, defineS3Connection } from '../schema/connection.js';
4
+ import { defineKafkaConnection, defineS3Connection, defineGCSConnection } from '../schema/connection.js';
5
5
  import { defineToken } from '../schema/token.js';
6
6
  import { t } from '../schema/types.js';
7
7
  import { engine } from '../schema/engines.js';
@@ -529,6 +529,35 @@ describe('Datasource Generator', () => {
529
529
  });
530
530
  });
531
531
 
532
+ describe('GCS configuration', () => {
533
+ it('includes GCS import directives', () => {
534
+ const gcsConn = defineGCSConnection('my_gcs', {
535
+ serviceAccountCredentialsJson: '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}',
536
+ });
537
+
538
+ const ds = defineDatasource('gcs_events', {
539
+ schema: {
540
+ timestamp: t.dateTime(),
541
+ event: t.string(),
542
+ },
543
+ engine: engine.mergeTree({ sortingKey: ['timestamp'] }),
544
+ gcs: {
545
+ connection: gcsConn,
546
+ bucketUri: 'gs://my-bucket/events/*.csv',
547
+ schedule: '@auto',
548
+ fromTimestamp: '2024-01-01T00:00:00Z',
549
+ },
550
+ });
551
+
552
+ const result = generateDatasource(ds);
553
+
554
+ expect(result.content).toContain('IMPORT_CONNECTION_NAME my_gcs');
555
+ expect(result.content).toContain('IMPORT_BUCKET_URI gs://my-bucket/events/*.csv');
556
+ expect(result.content).toContain('IMPORT_SCHEDULE @auto');
557
+ expect(result.content).toContain('IMPORT_FROM_TIMESTAMP 2024-01-01T00:00:00Z');
558
+ });
559
+ });
560
+
532
561
  describe('Token generation', () => {
533
562
  it('generates TOKEN lines with inline config', () => {
534
563
  const ds = defineDatasource('test_ds', {
@@ -9,6 +9,7 @@ import type {
9
9
  ColumnDefinition,
10
10
  KafkaConfig,
11
11
  S3Config,
12
+ GCSConfig,
12
13
  TokenConfig,
13
14
  } from "../schema/datasource.js";
14
15
  import type { AnyTypeValidator, TypeModifiers } from "../schema/types.js";
@@ -176,18 +177,18 @@ function generateKafkaConfig(kafka: KafkaConfig): string {
176
177
  /**
177
178
  * Generate S3 import configuration lines
178
179
  */
179
- function generateS3Config(s3: S3Config): string {
180
+ function generateImportConfig(importConfig: S3Config | GCSConfig): string {
180
181
  const parts: string[] = [];
181
182
 
182
- parts.push(`IMPORT_CONNECTION_NAME ${s3.connection._name}`);
183
- parts.push(`IMPORT_BUCKET_URI ${s3.bucketUri}`);
183
+ parts.push(`IMPORT_CONNECTION_NAME ${importConfig.connection._name}`);
184
+ parts.push(`IMPORT_BUCKET_URI ${importConfig.bucketUri}`);
184
185
 
185
- if (s3.schedule) {
186
- parts.push(`IMPORT_SCHEDULE ${s3.schedule}`);
186
+ if (importConfig.schedule) {
187
+ parts.push(`IMPORT_SCHEDULE ${importConfig.schedule}`);
187
188
  }
188
189
 
189
- if (s3.fromTimestamp) {
190
- parts.push(`IMPORT_FROM_TIMESTAMP ${s3.fromTimestamp}`);
190
+ if (importConfig.fromTimestamp) {
191
+ parts.push(`IMPORT_FROM_TIMESTAMP ${importConfig.fromTimestamp}`);
191
192
  }
192
193
 
193
194
  return parts.join("\n");
@@ -291,8 +292,13 @@ export function generateDatasource(
291
292
  ): GeneratedDatasource {
292
293
  const parts: string[] = [];
293
294
 
294
- if (datasource.options.kafka && datasource.options.s3) {
295
- throw new Error("Datasource cannot define both `kafka` and `s3` ingestion options.");
295
+ const ingestionConfigCount = [
296
+ datasource.options.kafka,
297
+ datasource.options.s3,
298
+ datasource.options.gcs,
299
+ ].filter(Boolean).length;
300
+ if (ingestionConfigCount > 1) {
301
+ throw new Error("Datasource can only define one ingestion option: `kafka`, `s3`, or `gcs`.");
296
302
  }
297
303
 
298
304
  // Add description if present
@@ -320,7 +326,13 @@ export function generateDatasource(
320
326
  // Add S3 configuration if present
321
327
  if (datasource.options.s3) {
322
328
  parts.push("");
323
- parts.push(generateS3Config(datasource.options.s3));
329
+ parts.push(generateImportConfig(datasource.options.s3));
330
+ }
331
+
332
+ // Add GCS configuration if present
333
+ if (datasource.options.gcs) {
334
+ parts.push("");
335
+ parts.push(generateImportConfig(datasource.options.gcs));
324
336
  }
325
337
 
326
338
  // Add forward query if present
package/src/index.ts CHANGED
@@ -113,6 +113,7 @@ export type {
113
113
  ExtractSchema,
114
114
  KafkaConfig,
115
115
  S3Config,
116
+ GCSConfig,
116
117
  } from "./schema/datasource.js";
117
118
 
118
119
  // ============ Connection ============
@@ -120,9 +121,11 @@ export {
120
121
  defineKafkaConnection,
121
122
  createKafkaConnection,
122
123
  defineS3Connection,
124
+ defineGCSConnection,
123
125
  isConnectionDefinition,
124
126
  isKafkaConnectionDefinition,
125
127
  isS3ConnectionDefinition,
128
+ isGCSConnectionDefinition,
126
129
  getConnectionType,
127
130
  } from "./schema/connection.js";
128
131
  export type {
@@ -133,6 +136,8 @@ export type {
133
136
  KafkaSaslMechanism,
134
137
  S3ConnectionDefinition,
135
138
  S3ConnectionOptions,
139
+ GCSConnectionDefinition,
140
+ GCSConnectionOptions,
136
141
  } from "./schema/connection.js";
137
142
 
138
143
  // ============ Token ============
@@ -4,6 +4,7 @@ import { parseLiteralFromDatafile, toTsLiteral } from "./parser-utils.js";
4
4
  import type {
5
5
  DatasourceModel,
6
6
  DatasourceEngineModel,
7
+ GCSConnectionModel,
7
8
  KafkaConnectionModel,
8
9
  ParsedResource,
9
10
  PipeModel,
@@ -59,11 +60,13 @@ function hasSecretTemplate(resources: ParsedResource[]): boolean {
59
60
  if (resource.secret) values.push(resource.secret);
60
61
  if (resource.sslCaPem) values.push(resource.sslCaPem);
61
62
  if (resource.schemaRegistryUrl) values.push(resource.schemaRegistryUrl);
62
- } else {
63
+ } else if (resource.connectionType === "s3") {
63
64
  values.push(resource.region);
64
65
  if (resource.arn) values.push(resource.arn);
65
66
  if (resource.accessKey) values.push(resource.accessKey);
66
67
  if (resource.secret) values.push(resource.secret);
68
+ } else {
69
+ values.push(resource.serviceAccountCredentialsJson);
67
70
  }
68
71
  continue;
69
72
  }
@@ -80,6 +83,11 @@ function hasSecretTemplate(resources: ParsedResource[]): boolean {
80
83
  if (resource.s3.schedule) values.push(resource.s3.schedule);
81
84
  if (resource.s3.fromTimestamp) values.push(resource.s3.fromTimestamp);
82
85
  }
86
+ if (resource.gcs) {
87
+ values.push(resource.gcs.bucketUri);
88
+ if (resource.gcs.schedule) values.push(resource.gcs.schedule);
89
+ if (resource.gcs.fromTimestamp) values.push(resource.gcs.fromTimestamp);
90
+ }
83
91
  continue;
84
92
  }
85
93
  }
@@ -337,6 +345,20 @@ function emitDatasource(ds: DatasourceModel): string {
337
345
  lines.push(" },");
338
346
  }
339
347
 
348
+ if (ds.gcs) {
349
+ const connectionVar = toCamelCase(ds.gcs.connectionName);
350
+ lines.push(" gcs: {");
351
+ lines.push(` connection: ${connectionVar},`);
352
+ lines.push(` bucketUri: ${emitStringOrSecret(ds.gcs.bucketUri)},`);
353
+ if (ds.gcs.schedule) {
354
+ lines.push(` schedule: ${emitStringOrSecret(ds.gcs.schedule)},`);
355
+ }
356
+ if (ds.gcs.fromTimestamp) {
357
+ lines.push(` fromTimestamp: ${emitStringOrSecret(ds.gcs.fromTimestamp)},`);
358
+ }
359
+ lines.push(" },");
360
+ }
361
+
340
362
  if (ds.forwardQuery) {
341
363
  lines.push(" forwardQuery: `");
342
364
  lines.push(ds.forwardQuery.replace(/`/g, "\\`").replace(/\${/g, "\\${"));
@@ -364,7 +386,9 @@ function emitDatasource(ds: DatasourceModel): string {
364
386
  return lines.join("\n");
365
387
  }
366
388
 
367
- function emitConnection(connection: KafkaConnectionModel | S3ConnectionModel): string {
389
+ function emitConnection(
390
+ connection: KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel
391
+ ): string {
368
392
  const variableName = toCamelCase(connection.name);
369
393
  const lines: string[] = [];
370
394
 
@@ -396,19 +420,31 @@ function emitConnection(connection: KafkaConnectionModel | S3ConnectionModel): s
396
420
  return lines.join("\n");
397
421
  }
398
422
 
423
+ if (connection.connectionType === "s3") {
424
+ lines.push(
425
+ `export const ${variableName} = defineS3Connection(${escapeString(connection.name)}, {`
426
+ );
427
+ lines.push(` region: ${emitStringOrSecret(connection.region)},`);
428
+ if (connection.arn) {
429
+ lines.push(` arn: ${emitStringOrSecret(connection.arn)},`);
430
+ }
431
+ if (connection.accessKey) {
432
+ lines.push(` accessKey: ${emitStringOrSecret(connection.accessKey)},`);
433
+ }
434
+ if (connection.secret) {
435
+ lines.push(` secret: ${emitStringOrSecret(connection.secret)},`);
436
+ }
437
+ lines.push("});");
438
+ lines.push("");
439
+ return lines.join("\n");
440
+ }
441
+
399
442
  lines.push(
400
- `export const ${variableName} = defineS3Connection(${escapeString(connection.name)}, {`
443
+ `export const ${variableName} = defineGCSConnection(${escapeString(connection.name)}, {`
444
+ );
445
+ lines.push(
446
+ ` serviceAccountCredentialsJson: ${emitStringOrSecret(connection.serviceAccountCredentialsJson)},`
401
447
  );
402
- lines.push(` region: ${emitStringOrSecret(connection.region)},`);
403
- if (connection.arn) {
404
- lines.push(` arn: ${emitStringOrSecret(connection.arn)},`);
405
- }
406
- if (connection.accessKey) {
407
- lines.push(` accessKey: ${emitStringOrSecret(connection.accessKey)},`);
408
- }
409
- if (connection.secret) {
410
- lines.push(` secret: ${emitStringOrSecret(connection.secret)},`);
411
- }
412
448
  lines.push("});");
413
449
  lines.push("");
414
450
  return lines.join("\n");
@@ -542,7 +578,7 @@ function emitPipe(pipe: PipeModel): string {
542
578
 
543
579
  export function emitMigrationFileContent(resources: ParsedResource[]): string {
544
580
  const connections = resources.filter(
545
- (resource): resource is KafkaConnectionModel | S3ConnectionModel =>
581
+ (resource): resource is KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel =>
546
582
  resource.kind === "connection"
547
583
  );
548
584
  const datasources = resources.filter(
@@ -569,6 +605,9 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
569
605
  if (connections.some((connection) => connection.connectionType === "s3")) {
570
606
  imports.add("defineS3Connection");
571
607
  }
608
+ if (connections.some((connection) => connection.connectionType === "gcs")) {
609
+ imports.add("defineGCSConnection");
610
+ }
572
611
  if (needsParams) {
573
612
  imports.add("p");
574
613
  }
@@ -585,6 +624,7 @@ export function emitMigrationFileContent(resources: ParsedResource[]): string {
585
624
  const orderedImports = [
586
625
  "defineKafkaConnection",
587
626
  "defineS3Connection",
627
+ "defineGCSConnection",
588
628
  "defineDatasource",
589
629
  "definePipe",
590
630
  "defineMaterializedView",
@@ -1,4 +1,9 @@
1
- import type { KafkaConnectionModel, ResourceFile, S3ConnectionModel } from "./types.js";
1
+ import type {
2
+ GCSConnectionModel,
3
+ KafkaConnectionModel,
4
+ ResourceFile,
5
+ S3ConnectionModel,
6
+ } from "./types.js";
2
7
  import {
3
8
  MigrationParseError,
4
9
  isBlank,
@@ -9,7 +14,7 @@ import {
9
14
 
10
15
  export function parseConnectionFile(
11
16
  resource: ResourceFile
12
- ): KafkaConnectionModel | S3ConnectionModel {
17
+ ): KafkaConnectionModel | S3ConnectionModel | GCSConnectionModel {
13
18
  const lines = splitLines(resource.content);
14
19
  let connectionType: string | undefined;
15
20
 
@@ -34,6 +39,7 @@ export function parseConnectionFile(
34
39
  let arn: string | undefined;
35
40
  let accessKey: string | undefined;
36
41
  let accessSecret: string | undefined;
42
+ let serviceAccountCredentialsJson: string | undefined;
37
43
 
38
44
  for (const rawLine of lines) {
39
45
  const line = rawLine.trim();
@@ -100,6 +106,9 @@ export function parseConnectionFile(
100
106
  case "S3_SECRET":
101
107
  accessSecret = parseQuotedValue(value);
102
108
  break;
109
+ case "GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON":
110
+ serviceAccountCredentialsJson = parseQuotedValue(value);
111
+ break;
103
112
  default:
104
113
  throw new MigrationParseError(
105
114
  resource.filePath,
@@ -120,12 +129,12 @@ export function parseConnectionFile(
120
129
  }
121
130
 
122
131
  if (connectionType === "kafka") {
123
- if (region || arn || accessKey || accessSecret) {
132
+ if (region || arn || accessKey || accessSecret || serviceAccountCredentialsJson) {
124
133
  throw new MigrationParseError(
125
134
  resource.filePath,
126
135
  "connection",
127
136
  resource.name,
128
- "S3 directives are not valid for kafka connections."
137
+ "S3/GCS directives are not valid for kafka connections."
129
138
  );
130
139
  }
131
140
 
@@ -161,13 +170,14 @@ export function parseConnectionFile(
161
170
  key ||
162
171
  secret ||
163
172
  schemaRegistryUrl ||
164
- sslCaPem
173
+ sslCaPem ||
174
+ serviceAccountCredentialsJson
165
175
  ) {
166
176
  throw new MigrationParseError(
167
177
  resource.filePath,
168
178
  "connection",
169
179
  resource.name,
170
- "Kafka directives are not valid for s3 connections."
180
+ "Kafka/GCS directives are not valid for s3 connections."
171
181
  );
172
182
  }
173
183
 
@@ -210,6 +220,46 @@ export function parseConnectionFile(
210
220
  };
211
221
  }
212
222
 
223
+ if (connectionType === "gcs") {
224
+ if (
225
+ bootstrapServers ||
226
+ securityProtocol ||
227
+ saslMechanism ||
228
+ key ||
229
+ secret ||
230
+ schemaRegistryUrl ||
231
+ sslCaPem ||
232
+ region ||
233
+ arn ||
234
+ accessKey ||
235
+ accessSecret
236
+ ) {
237
+ throw new MigrationParseError(
238
+ resource.filePath,
239
+ "connection",
240
+ resource.name,
241
+ "Kafka/S3 directives are not valid for gcs connections."
242
+ );
243
+ }
244
+
245
+ if (!serviceAccountCredentialsJson) {
246
+ throw new MigrationParseError(
247
+ resource.filePath,
248
+ "connection",
249
+ resource.name,
250
+ "GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON is required for gcs connections."
251
+ );
252
+ }
253
+
254
+ return {
255
+ kind: "connection",
256
+ name: resource.name,
257
+ filePath: resource.filePath,
258
+ connectionType: "gcs",
259
+ serviceAccountCredentialsJson,
260
+ };
261
+ }
262
+
213
263
  throw new MigrationParseError(
214
264
  resource.filePath,
215
265
  "connection",
@@ -518,7 +518,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
518
518
  resource.filePath,
519
519
  "datasource",
520
520
  resource.name,
521
- "IMPORT_CONNECTION_NAME and IMPORT_BUCKET_URI are required when S3 import directives are used."
521
+ "IMPORT_CONNECTION_NAME and IMPORT_BUCKET_URI are required when import directives are used."
522
522
  );
523
523
  }
524
524
 
@@ -527,7 +527,7 @@ export function parseDatasourceFile(resource: ResourceFile): DatasourceModel {
527
527
  resource.filePath,
528
528
  "datasource",
529
529
  resource.name,
530
- "Datasource cannot mix Kafka directives with S3 import directives."
530
+ "Datasource cannot mix Kafka directives with import directives."
531
531
  );
532
532
  }
533
533
 
@@ -52,6 +52,13 @@ export interface DatasourceS3Model {
52
52
  fromTimestamp?: string;
53
53
  }
54
54
 
55
+ export interface DatasourceGCSModel {
56
+ connectionName: string;
57
+ bucketUri: string;
58
+ schedule?: string;
59
+ fromTimestamp?: string;
60
+ }
61
+
55
62
  export interface DatasourceTokenModel {
56
63
  name: string;
57
64
  scope: "READ" | "APPEND";
@@ -66,6 +73,7 @@ export interface DatasourceModel {
66
73
  engine?: DatasourceEngineModel;
67
74
  kafka?: DatasourceKafkaModel;
68
75
  s3?: DatasourceS3Model;
76
+ gcs?: DatasourceGCSModel;
69
77
  forwardQuery?: string;
70
78
  tokens: DatasourceTokenModel[];
71
79
  sharedWith: string[];
@@ -156,11 +164,20 @@ export interface S3ConnectionModel {
156
164
  secret?: string;
157
165
  }
158
166
 
167
+ export interface GCSConnectionModel {
168
+ kind: "connection";
169
+ name: string;
170
+ filePath: string;
171
+ connectionType: "gcs";
172
+ serviceAccountCredentialsJson: string;
173
+ }
174
+
159
175
  export type ParsedResource =
160
176
  | DatasourceModel
161
177
  | PipeModel
162
178
  | KafkaConnectionModel
163
- | S3ConnectionModel;
179
+ | S3ConnectionModel
180
+ | GCSConnectionModel;
164
181
 
165
182
  export interface MigrationResult {
166
183
  success: boolean;
@@ -2,9 +2,11 @@ import { describe, it, expect } from "vitest";
2
2
  import {
3
3
  defineKafkaConnection,
4
4
  defineS3Connection,
5
+ defineGCSConnection,
5
6
  isConnectionDefinition,
6
7
  isKafkaConnectionDefinition,
7
8
  isS3ConnectionDefinition,
9
+ isGCSConnectionDefinition,
8
10
  getConnectionType,
9
11
  } from "./connection.js";
10
12
 
@@ -153,6 +155,29 @@ describe("Connection Schema", () => {
153
155
  });
154
156
  });
155
157
 
158
+ describe("defineGCSConnection", () => {
159
+ it("creates a GCS connection with required fields", () => {
160
+ const conn = defineGCSConnection("my_gcs", {
161
+ serviceAccountCredentialsJson: '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}',
162
+ });
163
+
164
+ expect(conn._name).toBe("my_gcs");
165
+ expect(conn._type).toBe("connection");
166
+ expect(conn._connectionType).toBe("gcs");
167
+ expect(conn.options.serviceAccountCredentialsJson).toBe(
168
+ '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}'
169
+ );
170
+ });
171
+
172
+ it("throws when credentials json is empty", () => {
173
+ expect(() =>
174
+ defineGCSConnection("my_gcs", {
175
+ serviceAccountCredentialsJson: " ",
176
+ })
177
+ ).toThrow("GCS connection `serviceAccountCredentialsJson` is required.");
178
+ });
179
+ });
180
+
156
181
  describe("isConnectionDefinition", () => {
157
182
  it("returns true for valid connection", () => {
158
183
  const conn = defineKafkaConnection("my_kafka", {
@@ -203,6 +228,21 @@ describe("Connection Schema", () => {
203
228
  });
204
229
  });
205
230
 
231
+ describe("isGCSConnectionDefinition", () => {
232
+ it("returns true for GCS connection", () => {
233
+ const conn = defineGCSConnection("my_gcs", {
234
+ serviceAccountCredentialsJson: '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}',
235
+ });
236
+
237
+ expect(isGCSConnectionDefinition(conn)).toBe(true);
238
+ });
239
+
240
+ it("returns false for non-GCS objects", () => {
241
+ expect(isGCSConnectionDefinition({})).toBe(false);
242
+ expect(isGCSConnectionDefinition(null)).toBe(false);
243
+ });
244
+ });
245
+
206
246
  describe("getConnectionType", () => {
207
247
  it("returns the connection type", () => {
208
248
  const conn = defineKafkaConnection("my_kafka", {
@@ -220,5 +260,13 @@ describe("Connection Schema", () => {
220
260
 
221
261
  expect(getConnectionType(conn)).toBe("s3");
222
262
  });
263
+
264
+ it("returns the gcs connection type", () => {
265
+ const conn = defineGCSConnection("my_gcs", {
266
+ serviceAccountCredentialsJson: '{{ tb_secret("GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON") }}',
267
+ });
268
+
269
+ expect(getConnectionType(conn)).toBe("gcs");
270
+ });
223
271
  });
224
272
  });