@tinybirdco/sdk 0.0.56 → 0.0.58

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.
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { runGenerate } from "./generate.js";
6
+
7
+ vi.mock("../config.js", () => ({
8
+ loadConfigAsync: vi.fn(),
9
+ }));
10
+
11
+ vi.mock("../../generator/index.js", () => ({
12
+ buildFromInclude: vi.fn(),
13
+ }));
14
+
15
+ describe("Generate command", () => {
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.resetAllMocks();
22
+ });
23
+
24
+ it("returns generated artifacts with stable relative paths", async () => {
25
+ const { loadConfigAsync } = await import("../config.js");
26
+ const { buildFromInclude } = await import("../../generator/index.js");
27
+
28
+ vi.mocked(loadConfigAsync).mockResolvedValue({
29
+ include: ["lib/tinybird.ts"],
30
+ token: "p.test-token",
31
+ baseUrl: "https://api.tinybird.co",
32
+ configPath: "/tmp/tinybird.config.json",
33
+ cwd: "/tmp",
34
+ gitBranch: "feature-x",
35
+ tinybirdBranch: "feature_x",
36
+ isMainBranch: false,
37
+ devMode: "branch",
38
+ });
39
+
40
+ vi.mocked(buildFromInclude).mockResolvedValue({
41
+ resources: {
42
+ datasources: [{ name: "events", content: "SCHEMA >" }],
43
+ pipes: [{ name: "events_endpoint", content: "TYPE endpoint" }],
44
+ connections: [{ name: "kafka_main", content: "TYPE kafka" }],
45
+ },
46
+ entities: {
47
+ datasources: {},
48
+ pipes: {},
49
+ connections: {},
50
+ rawDatasources: [],
51
+ rawPipes: [],
52
+ sourceFiles: [],
53
+ },
54
+ stats: {
55
+ datasourceCount: 1,
56
+ pipeCount: 1,
57
+ connectionCount: 1,
58
+ },
59
+ });
60
+
61
+ const result = await runGenerate();
62
+
63
+ expect(result.success).toBe(true);
64
+ expect(result.artifacts).toEqual([
65
+ {
66
+ type: "datasource",
67
+ name: "events",
68
+ relativePath: "datasources/events.datasource",
69
+ content: "SCHEMA >",
70
+ },
71
+ {
72
+ type: "pipe",
73
+ name: "events_endpoint",
74
+ relativePath: "pipes/events_endpoint.pipe",
75
+ content: "TYPE endpoint",
76
+ },
77
+ {
78
+ type: "connection",
79
+ name: "kafka_main",
80
+ relativePath: "connections/kafka_main.connection",
81
+ content: "TYPE kafka",
82
+ },
83
+ ]);
84
+ expect(result.stats?.totalCount).toBe(3);
85
+ });
86
+
87
+ it("returns an error when loading config fails", async () => {
88
+ const { loadConfigAsync } = await import("../config.js");
89
+ vi.mocked(loadConfigAsync).mockRejectedValue(new Error("No tinybird config"));
90
+
91
+ const result = await runGenerate();
92
+
93
+ expect(result.success).toBe(false);
94
+ expect(result.error).toContain("No tinybird config");
95
+ });
96
+
97
+ it("writes artifacts to outputDir when requested", async () => {
98
+ const { loadConfigAsync } = await import("../config.js");
99
+ const { buildFromInclude } = await import("../../generator/index.js");
100
+
101
+ vi.mocked(loadConfigAsync).mockResolvedValue({
102
+ include: ["lib/tinybird.ts"],
103
+ token: "p.test-token",
104
+ baseUrl: "https://api.tinybird.co",
105
+ configPath: "/tmp/tinybird.config.json",
106
+ cwd: "/tmp",
107
+ gitBranch: "feature-x",
108
+ tinybirdBranch: "feature_x",
109
+ isMainBranch: false,
110
+ devMode: "branch",
111
+ });
112
+
113
+ vi.mocked(buildFromInclude).mockResolvedValue({
114
+ resources: {
115
+ datasources: [{ name: "events", content: "SCHEMA >" }],
116
+ pipes: [{ name: "events_endpoint", content: "TYPE endpoint" }],
117
+ connections: [{ name: "kafka_main", content: "TYPE kafka" }],
118
+ },
119
+ entities: {
120
+ datasources: {},
121
+ pipes: {},
122
+ connections: {},
123
+ rawDatasources: [],
124
+ rawPipes: [],
125
+ sourceFiles: [],
126
+ },
127
+ stats: {
128
+ datasourceCount: 1,
129
+ pipeCount: 1,
130
+ connectionCount: 1,
131
+ },
132
+ });
133
+
134
+ const outputDir = await mkdtemp(join(tmpdir(), "tb-generate-test-"));
135
+ try {
136
+ const result = await runGenerate({ outputDir });
137
+
138
+ expect(result.success).toBe(true);
139
+ expect(
140
+ await readFile(join(outputDir, "datasources/events.datasource"), "utf-8")
141
+ ).toBe("SCHEMA >");
142
+ expect(
143
+ await readFile(join(outputDir, "pipes/events_endpoint.pipe"), "utf-8")
144
+ ).toBe("TYPE endpoint");
145
+ expect(
146
+ await readFile(
147
+ join(outputDir, "connections/kafka_main.connection"),
148
+ "utf-8"
149
+ )
150
+ ).toBe("TYPE kafka");
151
+ } finally {
152
+ await rm(outputDir, { recursive: true, force: true });
153
+ }
154
+ });
155
+
156
+ it("returns an error when buildFromInclude fails", async () => {
157
+ const { loadConfigAsync } = await import("../config.js");
158
+ const { buildFromInclude } = await import("../../generator/index.js");
159
+
160
+ vi.mocked(loadConfigAsync).mockResolvedValue({
161
+ include: ["lib/tinybird.ts"],
162
+ token: "p.test-token",
163
+ baseUrl: "https://api.tinybird.co",
164
+ configPath: "/tmp/tinybird.config.json",
165
+ cwd: "/tmp",
166
+ gitBranch: "feature-x",
167
+ tinybirdBranch: "feature_x",
168
+ isMainBranch: false,
169
+ devMode: "branch",
170
+ });
171
+ vi.mocked(buildFromInclude).mockRejectedValue(
172
+ new Error("generator failed")
173
+ );
174
+
175
+ const result = await runGenerate();
176
+
177
+ expect(result.success).toBe(false);
178
+ expect(result.error).toContain("generator failed");
179
+ });
180
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Generate command - build Tinybird resources from TypeScript include paths
3
+ * and expose a stable artifact contract for external consumers.
4
+ */
5
+
6
+ import { mkdir, writeFile } from "node:fs/promises";
7
+ import { dirname, join } from "node:path";
8
+ import { loadConfigAsync } from "../config.js";
9
+ import { buildFromInclude, type BuildFromIncludeResult } from "../../generator/index.js";
10
+
11
+ export type GeneratedResourceType = "datasource" | "pipe" | "connection";
12
+
13
+ export interface GeneratedResourceArtifact {
14
+ type: GeneratedResourceType;
15
+ name: string;
16
+ relativePath: string;
17
+ content: string;
18
+ }
19
+
20
+ export interface GenerateCommandOptions {
21
+ cwd?: string;
22
+ outputDir?: string;
23
+ }
24
+
25
+ export interface GenerateCommandResult {
26
+ success: boolean;
27
+ artifacts?: GeneratedResourceArtifact[];
28
+ stats?: {
29
+ datasourceCount: number;
30
+ pipeCount: number;
31
+ connectionCount: number;
32
+ totalCount: number;
33
+ };
34
+ outputDir?: string;
35
+ configPath?: string;
36
+ error?: string;
37
+ durationMs: number;
38
+ }
39
+
40
+ function toArtifacts(build: BuildFromIncludeResult): GeneratedResourceArtifact[] {
41
+ const artifacts: GeneratedResourceArtifact[] = [];
42
+
43
+ for (const datasource of build.resources.datasources) {
44
+ artifacts.push({
45
+ type: "datasource",
46
+ name: datasource.name,
47
+ relativePath: `datasources/${datasource.name}.datasource`,
48
+ content: datasource.content,
49
+ });
50
+ }
51
+
52
+ for (const pipe of build.resources.pipes) {
53
+ artifacts.push({
54
+ type: "pipe",
55
+ name: pipe.name,
56
+ relativePath: `pipes/${pipe.name}.pipe`,
57
+ content: pipe.content,
58
+ });
59
+ }
60
+
61
+ for (const connection of build.resources.connections) {
62
+ artifacts.push({
63
+ type: "connection",
64
+ name: connection.name,
65
+ relativePath: `connections/${connection.name}.connection`,
66
+ content: connection.content,
67
+ });
68
+ }
69
+
70
+ return artifacts;
71
+ }
72
+
73
+ async function writeArtifacts(outputDir: string, artifacts: GeneratedResourceArtifact[]): Promise<void> {
74
+ for (const artifact of artifacts) {
75
+ const targetPath = join(outputDir, artifact.relativePath);
76
+ const targetDir = dirname(targetPath);
77
+ await mkdir(targetDir, { recursive: true });
78
+ await writeFile(targetPath, artifact.content, "utf-8");
79
+ }
80
+ }
81
+
82
+ export async function runGenerate(
83
+ options: GenerateCommandOptions = {}
84
+ ): Promise<GenerateCommandResult> {
85
+ const startTime = Date.now();
86
+ const cwd = options.cwd ?? process.cwd();
87
+
88
+ try {
89
+ const config = await loadConfigAsync(cwd);
90
+ const build = await buildFromInclude({
91
+ includePaths: config.include,
92
+ cwd: config.cwd,
93
+ });
94
+
95
+ const artifacts = toArtifacts(build);
96
+
97
+ if (options.outputDir) {
98
+ await writeArtifacts(options.outputDir, artifacts);
99
+ }
100
+
101
+ return {
102
+ success: true,
103
+ artifacts,
104
+ stats: {
105
+ datasourceCount: build.stats.datasourceCount,
106
+ pipeCount: build.stats.pipeCount,
107
+ connectionCount: build.stats.connectionCount,
108
+ totalCount: artifacts.length,
109
+ },
110
+ outputDir: options.outputDir,
111
+ configPath: config.configPath,
112
+ durationMs: Date.now() - startTime,
113
+ };
114
+ } catch (error) {
115
+ return {
116
+ success: false,
117
+ error: (error as Error).message,
118
+ durationMs: Date.now() - startTime,
119
+ };
120
+ }
121
+ }
package/src/cli/index.ts CHANGED
@@ -18,6 +18,7 @@ import pc from "picocolors";
18
18
  import { runInit } from "./commands/init.js";
19
19
  import { runBuild } from "./commands/build.js";
20
20
  import { runDeploy } from "./commands/deploy.js";
21
+ import { runGenerate } from "./commands/generate.js";
21
22
  import { runPreview } from "./commands/preview.js";
22
23
  import { runDev } from "./commands/dev.js";
23
24
  import { runMigrate } from "./commands/migrate.js";
@@ -360,6 +361,46 @@ function createCli(): Command {
360
361
  }
361
362
  });
362
363
 
364
+ // Generate command
365
+ program
366
+ .command("generate")
367
+ .description("Generate Tinybird datafiles from TypeScript definitions")
368
+ .option("--json", "Output JSON artifacts (for external consumers)")
369
+ .option(
370
+ "-o, --output-dir <path>",
371
+ "Write generated files to a target directory"
372
+ )
373
+ .action(async (options) => {
374
+ const result = await runGenerate({
375
+ outputDir: options.outputDir,
376
+ });
377
+
378
+ if (!result.success) {
379
+ console.error(`Error: ${result.error}`);
380
+ process.exit(1);
381
+ }
382
+
383
+ if (options.json) {
384
+ console.log(JSON.stringify(result, null, 2));
385
+ return;
386
+ }
387
+
388
+ const stats = result.stats ?? {
389
+ datasourceCount: 0,
390
+ pipeCount: 0,
391
+ connectionCount: 0,
392
+ totalCount: 0,
393
+ };
394
+
395
+ console.log(
396
+ `Generated ${stats.totalCount} resources (${stats.datasourceCount} datasources, ${stats.pipeCount} pipes, ${stats.connectionCount} connections)`
397
+ );
398
+ if (result.outputDir) {
399
+ console.log(`Written to: ${result.outputDir}`);
400
+ }
401
+ console.log(`Completed in ${output.formatDuration(result.durationMs)}`);
402
+ });
403
+
363
404
  // Migrate command
364
405
  program
365
406
  .command("migrate")
@@ -152,6 +152,11 @@ export interface IngestOptions {
152
152
  signal?: AbortSignal;
153
153
  /** Wait for the ingestion to complete before returning */
154
154
  wait?: boolean;
155
+ /**
156
+ * Number of retry attempts after the first request.
157
+ * Retries are disabled by default when undefined.
158
+ */
159
+ maxRetries?: number;
155
160
  }
156
161
 
157
162
  /**