@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.
- package/README.md +71 -4
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +68 -1
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/migrate.test.js +458 -1
- package/dist/cli/commands/migrate.test.js.map +1 -1
- package/dist/generator/connection.d.ts.map +1 -1
- package/dist/generator/connection.js +14 -1
- package/dist/generator/connection.js.map +1 -1
- package/dist/generator/connection.test.js +20 -4
- package/dist/generator/connection.test.js.map +1 -1
- package/dist/generator/datasource.d.ts.map +1 -1
- package/dist/generator/datasource.js +20 -10
- package/dist/generator/datasource.js.map +1 -1
- package/dist/generator/datasource.test.js +26 -1
- package/dist/generator/datasource.test.js.map +1 -1
- package/dist/generator/pipe.d.ts.map +1 -1
- package/dist/generator/pipe.js +31 -1
- package/dist/generator/pipe.js.map +1 -1
- package/dist/generator/pipe.test.js +50 -1
- package/dist/generator/pipe.test.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/migrate/emit-ts.d.ts.map +1 -1
- package/dist/migrate/emit-ts.js +95 -20
- 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 +34 -4
- 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 +79 -51
- package/dist/migrate/parse-datasource.js.map +1 -1
- package/dist/migrate/parse-pipe.d.ts.map +1 -1
- package/dist/migrate/parse-pipe.js +254 -44
- package/dist/migrate/parse-pipe.js.map +1 -1
- package/dist/migrate/parser-utils.d.ts +5 -0
- package/dist/migrate/parser-utils.d.ts.map +1 -1
- package/dist/migrate/parser-utils.js +22 -0
- package/dist/migrate/parser-utils.js.map +1 -1
- package/dist/migrate/types.d.ts +37 -4
- package/dist/migrate/types.d.ts.map +1 -1
- package/dist/schema/connection.d.ts +34 -1
- package/dist/schema/connection.d.ts.map +1 -1
- package/dist/schema/connection.js +26 -0
- package/dist/schema/connection.js.map +1 -1
- package/dist/schema/connection.test.js +35 -1
- 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 -2
- package/dist/schema/datasource.js.map +1 -1
- package/dist/schema/datasource.test.js +33 -3
- package/dist/schema/datasource.test.js.map +1 -1
- package/dist/schema/pipe.d.ts +90 -3
- package/dist/schema/pipe.d.ts.map +1 -1
- package/dist/schema/pipe.js +84 -0
- package/dist/schema/pipe.js.map +1 -1
- package/dist/schema/pipe.test.js +70 -1
- package/dist/schema/pipe.test.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/migrate.test.ts +671 -1
- package/src/cli/commands/migrate.ts +74 -1
- package/src/generator/connection.test.ts +29 -4
- package/src/generator/connection.ts +25 -2
- package/src/generator/datasource.test.ts +30 -1
- package/src/generator/datasource.ts +22 -10
- package/src/generator/pipe.test.ts +56 -1
- package/src/generator/pipe.ts +41 -1
- package/src/index.ts +14 -0
- package/src/migrate/emit-ts.ts +106 -24
- package/src/migrate/parse-connection.ts +56 -6
- package/src/migrate/parse-datasource.ts +84 -70
- package/src/migrate/parse-pipe.ts +359 -66
- package/src/migrate/parser-utils.ts +36 -1
- package/src/migrate/types.ts +43 -4
- package/src/schema/connection.test.ts +48 -0
- package/src/schema/connection.ts +60 -1
- package/src/schema/datasource.test.ts +39 -3
- package/src/schema/datasource.ts +24 -3
- package/src/schema/pipe.test.ts +89 -0
- 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 ??
|
|
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 {
|
|
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({
|
|
188
|
+
const results = generateAllConnections({
|
|
189
|
+
kafka1: conn1,
|
|
190
|
+
s3_logs: conn2,
|
|
191
|
+
gcs_landing: conn3,
|
|
192
|
+
});
|
|
168
193
|
|
|
169
|
-
expect(results).toHaveLength(
|
|
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 {
|
|
7
|
-
|
|
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
|
|
180
|
+
function generateImportConfig(importConfig: S3Config | GCSConfig): string {
|
|
180
181
|
const parts: string[] = [];
|
|
181
182
|
|
|
182
|
-
parts.push(`IMPORT_CONNECTION_NAME ${
|
|
183
|
-
parts.push(`IMPORT_BUCKET_URI ${
|
|
183
|
+
parts.push(`IMPORT_CONNECTION_NAME ${importConfig.connection._name}`);
|
|
184
|
+
parts.push(`IMPORT_BUCKET_URI ${importConfig.bucketUri}`);
|
|
184
185
|
|
|
185
|
-
if (
|
|
186
|
-
parts.push(`IMPORT_SCHEDULE ${
|
|
186
|
+
if (importConfig.schedule) {
|
|
187
|
+
parts.push(`IMPORT_SCHEDULE ${importConfig.schedule}`);
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
if (
|
|
190
|
-
parts.push(`IMPORT_FROM_TIMESTAMP ${
|
|
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
|
-
|
|
295
|
-
|
|
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(
|
|
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', {
|
package/src/generator/pipe.ts
CHANGED
|
@@ -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 {
|
|
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,
|