@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
@@ -112,6 +112,9 @@ export async function runMigrate(
112
112
  const migrated: ParsedResource[] = [];
113
113
  const migratedConnectionNames = new Set<string>();
114
114
  const migratedDatasourceNames = new Set<string>();
115
+ const parsedConnectionTypeByName = new Map(
116
+ parsedConnections.map((connection) => [connection.name, connection.connectionType] as const)
117
+ );
115
118
 
116
119
  for (const connection of parsedConnections) {
117
120
  try {
@@ -130,7 +133,9 @@ export async function runMigrate(
130
133
 
131
134
  for (const datasource of parsedDatasources) {
132
135
  const referencedConnectionName =
133
- datasource.kafka?.connectionName ?? datasource.s3?.connectionName;
136
+ datasource.kafka?.connectionName ??
137
+ datasource.s3?.connectionName ??
138
+ datasource.gcs?.connectionName;
134
139
 
135
140
  if (
136
141
  referencedConnectionName &&
@@ -145,6 +150,42 @@ export async function runMigrate(
145
150
  continue;
146
151
  }
147
152
 
153
+ if (datasource.kafka) {
154
+ const kafkaConnectionType = parsedConnectionTypeByName.get(datasource.kafka.connectionName);
155
+ if (kafkaConnectionType !== "kafka") {
156
+ errors.push({
157
+ filePath: datasource.filePath,
158
+ resourceName: datasource.name,
159
+ resourceKind: datasource.kind,
160
+ message: `Datasource kafka ingestion requires a kafka connection, found "${kafkaConnectionType ?? "(none)"}".`,
161
+ });
162
+ continue;
163
+ }
164
+ }
165
+
166
+ const importConfig = datasource.s3 ?? datasource.gcs;
167
+ if (importConfig) {
168
+ const importConnectionType = parsedConnectionTypeByName.get(importConfig.connectionName);
169
+ if (importConnectionType !== "s3" && importConnectionType !== "gcs") {
170
+ errors.push({
171
+ filePath: datasource.filePath,
172
+ resourceName: datasource.name,
173
+ resourceKind: datasource.kind,
174
+ message:
175
+ `Datasource import directives require an s3 or gcs connection, found "${importConnectionType ?? "(none)"}".`,
176
+ });
177
+ continue;
178
+ }
179
+
180
+ if (importConnectionType === "gcs") {
181
+ datasource.gcs = { ...importConfig };
182
+ datasource.s3 = undefined;
183
+ } else {
184
+ datasource.s3 = { ...importConfig };
185
+ datasource.gcs = undefined;
186
+ }
187
+ }
188
+
148
189
  try {
149
190
  validateResourceForEmission(datasource);
150
191
  migrated.push(datasource);
@@ -160,6 +201,38 @@ export async function runMigrate(
160
201
  }
161
202
 
162
203
  for (const pipe of parsedPipes) {
204
+ if (pipe.type === "sink") {
205
+ const sinkConnectionName = pipe.sink?.connectionName;
206
+ if (!sinkConnectionName || !migratedConnectionNames.has(sinkConnectionName)) {
207
+ errors.push({
208
+ filePath: pipe.filePath,
209
+ resourceName: pipe.name,
210
+ resourceKind: pipe.kind,
211
+ message: `Sink pipe references missing/unmigrated connection "${sinkConnectionName ?? "(none)"}".`,
212
+ });
213
+ continue;
214
+ }
215
+ const sinkConnectionType = parsedConnectionTypeByName.get(sinkConnectionName);
216
+ if (!sinkConnectionType) {
217
+ errors.push({
218
+ filePath: pipe.filePath,
219
+ resourceName: pipe.name,
220
+ resourceKind: pipe.kind,
221
+ message: `Sink pipe connection "${sinkConnectionName}" could not be resolved.`,
222
+ });
223
+ continue;
224
+ }
225
+ if (sinkConnectionType !== pipe.sink?.service) {
226
+ errors.push({
227
+ filePath: pipe.filePath,
228
+ resourceName: pipe.name,
229
+ resourceKind: pipe.kind,
230
+ message: `Sink pipe service "${pipe.sink?.service}" is incompatible with connection "${sinkConnectionName}" type "${sinkConnectionType}".`,
231
+ });
232
+ continue;
233
+ }
234
+ }
235
+
163
236
  if (
164
237
  pipe.type === "materialized" &&
165
238
  (!pipe.materializedDatasource ||
@@ -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
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { generatePipe, generateAllPipes } from './pipe.js';
3
- import { definePipe, defineMaterializedView, node } from '../schema/pipe.js';
3
+ import { definePipe, defineMaterializedView, defineSinkPipe, node } from '../schema/pipe.js';
4
4
  import { defineDatasource } from '../schema/datasource.js';
5
+ import { defineKafkaConnection, defineS3Connection } from '../schema/connection.js';
5
6
  import { defineToken } from '../schema/token.js';
6
7
  import { t } from '../schema/types.js';
7
8
  import { p } from '../schema/params.js';
@@ -472,6 +473,60 @@ GROUP BY day, country
472
473
  });
473
474
  });
474
475
 
476
+ describe('Sink configuration', () => {
477
+ it('generates Kafka sink directives', () => {
478
+ const kafka = defineKafkaConnection('events_kafka', {
479
+ bootstrapServers: 'localhost:9092',
480
+ });
481
+
482
+ const pipe = defineSinkPipe('events_sink', {
483
+ nodes: [node({ name: 'publish', sql: 'SELECT * FROM events' })],
484
+ sink: {
485
+ connection: kafka,
486
+ topic: 'events_out',
487
+ schedule: '@on-demand',
488
+ },
489
+ });
490
+
491
+ const result = generatePipe(pipe);
492
+ expect(result.content).toContain('TYPE sink');
493
+ expect(result.content).toContain('EXPORT_CONNECTION_NAME events_kafka');
494
+ expect(result.content).toContain('EXPORT_KAFKA_TOPIC events_out');
495
+ expect(result.content).toContain('EXPORT_SCHEDULE @on-demand');
496
+ expect(result.content).not.toContain('EXPORT_STRATEGY');
497
+ });
498
+
499
+ it('generates S3 sink directives', () => {
500
+ const s3 = defineS3Connection('exports_s3', {
501
+ region: 'us-east-1',
502
+ arn: 'arn:aws:iam::123456789012:role/tinybird-s3-access',
503
+ });
504
+
505
+ const pipe = defineSinkPipe('events_s3_sink', {
506
+ nodes: [node({ name: 'export', sql: 'SELECT * FROM events' })],
507
+ sink: {
508
+ connection: s3,
509
+ bucketUri: 's3://bucket/events/',
510
+ fileTemplate: 'events_{date}',
511
+ format: 'csv',
512
+ schedule: '@once',
513
+ compression: 'gzip',
514
+ strategy: 'replace',
515
+ },
516
+ });
517
+
518
+ const result = generatePipe(pipe);
519
+ expect(result.content).toContain('TYPE sink');
520
+ expect(result.content).toContain('EXPORT_CONNECTION_NAME exports_s3');
521
+ expect(result.content).toContain('EXPORT_BUCKET_URI s3://bucket/events/');
522
+ expect(result.content).toContain('EXPORT_FILE_TEMPLATE events_{date}');
523
+ expect(result.content).toContain('EXPORT_FORMAT csv');
524
+ expect(result.content).toContain('EXPORT_SCHEDULE @once');
525
+ expect(result.content).toContain('EXPORT_STRATEGY replace');
526
+ expect(result.content).toContain('EXPORT_COMPRESSION gzip');
527
+ });
528
+ });
529
+
475
530
  describe('Token generation', () => {
476
531
  it('generates TOKEN lines with inline config', () => {
477
532
  const pipe = definePipe('test_pipe', {
@@ -9,9 +9,15 @@ import type {
9
9
  EndpointConfig,
10
10
  MaterializedConfig,
11
11
  CopyConfig,
12
+ SinkConfig,
12
13
  PipeTokenConfig,
13
14
  } from "../schema/pipe.js";
14
- import { getEndpointConfig, getMaterializedConfig, getCopyConfig } from "../schema/pipe.js";
15
+ import {
16
+ getEndpointConfig,
17
+ getMaterializedConfig,
18
+ getCopyConfig,
19
+ getSinkConfig,
20
+ } from "../schema/pipe.js";
15
21
 
16
22
  /**
17
23
  * Generated pipe content
@@ -114,6 +120,33 @@ function generateCopy(config: CopyConfig): string {
114
120
  return parts.join("\n");
115
121
  }
116
122
 
123
+ /**
124
+ * Generate the TYPE sink section
125
+ */
126
+ function generateSink(config: SinkConfig): string {
127
+ const parts: string[] = ["TYPE sink"];
128
+
129
+ parts.push(`EXPORT_CONNECTION_NAME ${config.connection._name}`);
130
+
131
+ if ("topic" in config) {
132
+ parts.push(`EXPORT_KAFKA_TOPIC ${config.topic}`);
133
+ parts.push(`EXPORT_SCHEDULE ${config.schedule}`);
134
+ } else {
135
+ parts.push(`EXPORT_BUCKET_URI ${config.bucketUri}`);
136
+ parts.push(`EXPORT_FILE_TEMPLATE ${config.fileTemplate}`);
137
+ parts.push(`EXPORT_SCHEDULE ${config.schedule}`);
138
+ parts.push(`EXPORT_FORMAT ${config.format}`);
139
+ if (config.strategy) {
140
+ parts.push(`EXPORT_STRATEGY ${config.strategy}`);
141
+ }
142
+ if (config.compression) {
143
+ parts.push(`EXPORT_COMPRESSION ${config.compression}`);
144
+ }
145
+ }
146
+
147
+ return parts.join("\n");
148
+ }
149
+
117
150
  /**
118
151
  * Generate TOKEN lines for a pipe
119
152
  */
@@ -220,6 +253,13 @@ export function generatePipe(pipe: PipeDefinition): GeneratedPipe {
220
253
  parts.push(generateCopy(copyConfig));
221
254
  }
222
255
 
256
+ // Add sink configuration if this is a sink pipe
257
+ const sinkConfig = getSinkConfig(pipe);
258
+ if (sinkConfig) {
259
+ parts.push("");
260
+ parts.push(generateSink(sinkConfig));
261
+ }
262
+
223
263
  // Add tokens if present
224
264
  const tokenLines = generateTokens(pipe.options.tokens);
225
265
  if (tokenLines.length > 0) {
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 ============
@@ -149,14 +154,17 @@ export {
149
154
  defineEndpoint,
150
155
  defineMaterializedView,
151
156
  defineCopyPipe,
157
+ defineSinkPipe,
152
158
  node,
153
159
  isPipeDefinition,
154
160
  isNodeDefinition,
155
161
  getEndpointConfig,
156
162
  getMaterializedConfig,
157
163
  getCopyConfig,
164
+ getSinkConfig,
158
165
  isMaterializedView,
159
166
  isCopyPipe,
167
+ isSinkPipe,
160
168
  getNodeNames,
161
169
  getNode,
162
170
  sql,
@@ -166,7 +174,13 @@ export type {
166
174
  PipeOptions,
167
175
  EndpointOptions,
168
176
  CopyPipeOptions,
177
+ SinkPipeOptions,
169
178
  CopyConfig,
179
+ SinkConfig,
180
+ SinkStrategy,
181
+ SinkCompression,
182
+ KafkaSinkConfig,
183
+ S3SinkConfig,
170
184
  NodeDefinition,
171
185
  NodeOptions,
172
186
  ParamsDefinition,