@tinybirdco/sdk 0.0.7 → 0.0.9

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 (56) hide show
  1. package/dist/api/resources.d.ts +2 -0
  2. package/dist/api/resources.d.ts.map +1 -1
  3. package/dist/api/resources.js +1 -0
  4. package/dist/api/resources.js.map +1 -1
  5. package/dist/cli/commands/init.d.ts.map +1 -1
  6. package/dist/cli/commands/init.js +301 -205
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/init.test.js +67 -93
  9. package/dist/cli/commands/init.test.js.map +1 -1
  10. package/dist/cli/config.d.ts +3 -7
  11. package/dist/cli/config.d.ts.map +1 -1
  12. package/dist/cli/config.js +9 -20
  13. package/dist/cli/config.js.map +1 -1
  14. package/dist/cli/config.test.js +11 -29
  15. package/dist/cli/config.test.js.map +1 -1
  16. package/dist/cli/git.d.ts +5 -0
  17. package/dist/cli/git.d.ts.map +1 -1
  18. package/dist/cli/git.js +15 -0
  19. package/dist/cli/git.js.map +1 -1
  20. package/dist/cli/index.js +42 -25
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/utils/package-manager.d.ts +9 -0
  23. package/dist/cli/utils/package-manager.d.ts.map +1 -1
  24. package/dist/cli/utils/package-manager.js +130 -35
  25. package/dist/cli/utils/package-manager.js.map +1 -1
  26. package/dist/cli/utils/package-manager.test.js +124 -32
  27. package/dist/cli/utils/package-manager.test.js.map +1 -1
  28. package/dist/codegen/index.d.ts +4 -0
  29. package/dist/codegen/index.d.ts.map +1 -1
  30. package/dist/codegen/index.js +96 -0
  31. package/dist/codegen/index.js.map +1 -1
  32. package/dist/codegen/index.test.js +10 -0
  33. package/dist/codegen/index.test.js.map +1 -1
  34. package/dist/generator/datasource.d.ts.map +1 -1
  35. package/dist/generator/datasource.js +20 -0
  36. package/dist/generator/datasource.js.map +1 -1
  37. package/dist/generator/datasource.test.js +11 -0
  38. package/dist/generator/datasource.test.js.map +1 -1
  39. package/dist/schema/datasource.d.ts +5 -0
  40. package/dist/schema/datasource.d.ts.map +1 -1
  41. package/dist/schema/datasource.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/api/resources.ts +4 -0
  44. package/src/cli/commands/init.test.ts +67 -107
  45. package/src/cli/commands/init.ts +350 -218
  46. package/src/cli/config.test.ts +12 -42
  47. package/src/cli/config.ts +9 -23
  48. package/src/cli/git.ts +15 -0
  49. package/src/cli/index.ts +46 -27
  50. package/src/cli/utils/package-manager.test.ts +165 -33
  51. package/src/cli/utils/package-manager.ts +133 -30
  52. package/src/codegen/index.test.ts +12 -0
  53. package/src/codegen/index.ts +120 -0
  54. package/src/generator/datasource.test.ts +13 -0
  55. package/src/generator/datasource.ts +24 -0
  56. package/src/schema/datasource.ts +5 -0
@@ -3,42 +3,145 @@
3
3
  */
4
4
 
5
5
  import { existsSync, readFileSync } from "node:fs";
6
- import { join } from "node:path";
6
+ import { dirname, join, resolve } from "node:path";
7
7
 
8
- /**
9
- * Detect package manager and return the appropriate run command
10
- */
11
- export function detectPackageManagerRunCmd(cwd: string = process.cwd()): string {
12
- // Check lockfiles first (most reliable)
13
- if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
14
- return "pnpm";
8
+ export type PackageManager = "pnpm" | "yarn" | "bun" | "npm";
9
+ const TINYBIRD_SDK_PACKAGE = "@tinybirdco/sdk";
10
+
11
+ function detectPackageManagerFromLockfile(dir: string): PackageManager | undefined {
12
+ if (existsSync(join(dir, "pnpm-lock.yaml"))) return "pnpm";
13
+ if (existsSync(join(dir, "yarn.lock"))) return "yarn";
14
+ if (existsSync(join(dir, "bun.lockb"))) return "bun";
15
+ if (existsSync(join(dir, "package-lock.json"))) return "npm";
16
+ return undefined;
17
+ }
18
+
19
+ function detectPackageManagerFromWorkspace(dir: string): PackageManager | undefined {
20
+ if (existsSync(join(dir, "pnpm-workspace.yaml"))) return "pnpm";
21
+ if (existsSync(join(dir, "pnpm-workspace.yml"))) return "pnpm";
22
+ return undefined;
23
+ }
24
+
25
+ function detectPackageManagerFromPackageJson(
26
+ dir: string
27
+ ): PackageManager | undefined {
28
+ const packageJsonPath = join(dir, "package.json");
29
+ if (!existsSync(packageJsonPath)) return undefined;
30
+ try {
31
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
32
+ const pm = packageJson.packageManager;
33
+ if (typeof pm !== "string") return undefined;
34
+ if (pm.startsWith("pnpm")) return "pnpm";
35
+ if (pm.startsWith("yarn")) return "yarn";
36
+ if (pm.startsWith("bun")) return "bun";
37
+ if (pm.startsWith("npm")) return "npm";
38
+ } catch {
39
+ return undefined;
15
40
  }
16
- if (existsSync(join(cwd, "yarn.lock"))) {
17
- return "yarn";
41
+ return undefined;
42
+ }
43
+
44
+ function getSearchDirs(start: string): string[] {
45
+ const dirs: string[] = [];
46
+ let current = resolve(start);
47
+ while (true) {
48
+ dirs.push(current);
49
+ const parent = dirname(current);
50
+ if (parent === current) break;
51
+ current = parent;
52
+ }
53
+ return dirs;
54
+ }
55
+
56
+ function findNearestPackageJson(start: string): string | undefined {
57
+ for (const dir of getSearchDirs(start)) {
58
+ const packageJsonPath = join(dir, "package.json");
59
+ if (existsSync(packageJsonPath)) return packageJsonPath;
18
60
  }
19
- if (existsSync(join(cwd, "bun.lockb"))) {
20
- return "bun run";
61
+ return undefined;
62
+ }
63
+
64
+ export function getPackageManagerRunCmd(packageManager: PackageManager): string {
65
+ switch (packageManager) {
66
+ case "pnpm":
67
+ return "pnpm run";
68
+ case "yarn":
69
+ return "yarn";
70
+ case "bun":
71
+ return "bun run";
72
+ case "npm":
73
+ default:
74
+ return "npm run";
75
+ }
76
+ }
77
+
78
+ export function getPackageManagerInstallCmd(
79
+ packageManager: PackageManager
80
+ ): string {
81
+ switch (packageManager) {
82
+ case "pnpm":
83
+ return "pnpm install";
84
+ case "yarn":
85
+ return "yarn install";
86
+ case "bun":
87
+ return "bun install";
88
+ case "npm":
89
+ default:
90
+ return "npm install";
21
91
  }
22
- if (existsSync(join(cwd, "package-lock.json"))) {
23
- return "npm run";
92
+ }
93
+
94
+ /**
95
+ * Detect package manager (npm, pnpm, yarn, or bun)
96
+ */
97
+ export function detectPackageManager(cwd: string = process.cwd()): PackageManager {
98
+ for (const dir of getSearchDirs(cwd)) {
99
+ const fromLockfile = detectPackageManagerFromLockfile(dir);
100
+ if (fromLockfile) return fromLockfile;
101
+
102
+ const fromWorkspace = detectPackageManagerFromWorkspace(dir);
103
+ if (fromWorkspace) return fromWorkspace;
104
+
105
+ const fromPackageJson = detectPackageManagerFromPackageJson(dir);
106
+ if (fromPackageJson) return fromPackageJson;
24
107
  }
25
108
 
26
- // Check packageManager field in package.json
27
- const packageJsonPath = join(cwd, "package.json");
28
- if (existsSync(packageJsonPath)) {
29
- try {
30
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
31
- const pm = packageJson.packageManager;
32
- if (typeof pm === "string") {
33
- if (pm.startsWith("pnpm")) return "pnpm";
34
- if (pm.startsWith("yarn")) return "yarn";
35
- if (pm.startsWith("bun")) return "bun run";
36
- }
37
- } catch {
38
- // Ignore parse errors
39
- }
109
+ return "npm";
110
+ }
111
+
112
+ export function detectPackageManagerInstallCmd(
113
+ cwd: string = process.cwd()
114
+ ): string {
115
+ return getPackageManagerInstallCmd(detectPackageManager(cwd));
116
+ }
117
+
118
+ export function hasTinybirdSdkDependency(cwd: string = process.cwd()): boolean {
119
+ const packageJsonPath = findNearestPackageJson(cwd);
120
+ if (!packageJsonPath) return false;
121
+
122
+ try {
123
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
124
+ const dependencyFields = [
125
+ packageJson.dependencies,
126
+ packageJson.devDependencies,
127
+ packageJson.peerDependencies,
128
+ packageJson.optionalDependencies,
129
+ ];
130
+
131
+ return dependencyFields.some(
132
+ (deps) =>
133
+ deps &&
134
+ typeof deps === "object" &&
135
+ Object.prototype.hasOwnProperty.call(deps, TINYBIRD_SDK_PACKAGE)
136
+ );
137
+ } catch {
138
+ return false;
40
139
  }
140
+ }
41
141
 
42
- // Default to npm
43
- return "npm run";
142
+ /**
143
+ * Detect package manager and return the appropriate run command
144
+ */
145
+ export function detectPackageManagerRunCmd(cwd: string = process.cwd()): string {
146
+ return getPackageManagerRunCmd(detectPackageManager(cwd));
44
147
  }
@@ -74,6 +74,18 @@ describe("generateDatasourceCode", () => {
74
74
  expect(code).toContain(" * Event tracking data");
75
75
  expect(code).toContain(" */");
76
76
  });
77
+
78
+ it("includes forward query when present", () => {
79
+ const ds: DatasourceInfo = {
80
+ name: "events",
81
+ columns: [{ name: "id", type: "String" }],
82
+ engine: { type: "MergeTree", sorting_key: "id" },
83
+ forward_query: "SELECT id",
84
+ };
85
+
86
+ const code = generateDatasourceCode(ds);
87
+ expect(code).toContain("forwardQuery: `SELECT id`");
88
+ });
77
89
  });
78
90
 
79
91
  describe("generatePipeCode", () => {
@@ -20,6 +20,9 @@ export function generateDatasourceCode(ds: DatasourceInfo): string {
20
20
  const typeName = toPascalCase(ds.name);
21
21
  const lines: string[] = [];
22
22
 
23
+ // Check if any columns have jsonpath set
24
+ const hasJsonpath = ds.columns.some((col) => col.jsonpath);
25
+
23
26
  // JSDoc comment
24
27
  if (ds.description) {
25
28
  lines.push("/**");
@@ -33,6 +36,11 @@ export function generateDatasourceCode(ds: DatasourceInfo): string {
33
36
  lines.push(` description: "${escapeString(ds.description)}",`);
34
37
  }
35
38
 
39
+ // Add jsonPaths: false if no columns use jsonpath
40
+ if (!hasJsonpath) {
41
+ lines.push(" jsonPaths: false,");
42
+ }
43
+
36
44
  // Schema
37
45
  lines.push(" schema: {");
38
46
  for (const col of ds.columns) {
@@ -45,6 +53,11 @@ export function generateDatasourceCode(ds: DatasourceInfo): string {
45
53
  const engineCode = generateEngineCode(ds.engine);
46
54
  lines.push(` engine: ${engineCode},`);
47
55
 
56
+ if (ds.forward_query) {
57
+ const formattedQuery = formatSqlForTemplate(ds.forward_query);
58
+ lines.push(` forwardQuery: \`${formattedQuery}\`,`);
59
+ }
60
+
48
61
  lines.push("});");
49
62
  lines.push("");
50
63
  lines.push(`export type ${typeName}Row = InferRow<typeof ${varName}>;`);
@@ -377,3 +390,110 @@ export function generateAllFiles(
377
390
  pipeCount: pipes.length,
378
391
  };
379
392
  }
393
+
394
+ /**
395
+ * Generate a single combined tinybird.ts file with all definitions
396
+ */
397
+ export function generateCombinedFile(
398
+ datasources: DatasourceInfo[],
399
+ pipes: PipeInfo[]
400
+ ): string {
401
+ const lines: string[] = [
402
+ "/**",
403
+ " * Tinybird Definitions",
404
+ " *",
405
+ " * This file contains all datasource and endpoint definitions.",
406
+ " * Generated from existing workspace resources.",
407
+ " */",
408
+ "",
409
+ ];
410
+
411
+ // Build imports
412
+ const sdkImports: string[] = ["createTinybirdClient", "t"];
413
+
414
+ if (datasources.length > 0) {
415
+ sdkImports.push("defineDatasource", "engine", "type InferRow");
416
+ }
417
+
418
+ const hasMaterialized = pipes.some((p) => p.type === "materialized");
419
+ const hasCopy = pipes.some((p) => p.type === "copy");
420
+ const hasEndpoint = pipes.some((p) => p.type === "endpoint");
421
+ const hasPlainPipe = pipes.some((p) => p.type === "pipe");
422
+ const hasParams = pipes.some(
423
+ (p) => p.params.length > 0 && p.type !== "materialized" && p.type !== "copy"
424
+ );
425
+
426
+ if (pipes.length > 0) {
427
+ sdkImports.push("node");
428
+ }
429
+ if (hasParams) {
430
+ sdkImports.push("p");
431
+ }
432
+ if (hasEndpoint) {
433
+ sdkImports.push("defineEndpoint", "type InferParams", "type InferOutputRow");
434
+ }
435
+ if (hasMaterialized) {
436
+ sdkImports.push("defineMaterializedView");
437
+ }
438
+ if (hasCopy) {
439
+ sdkImports.push("defineCopyPipe");
440
+ }
441
+ if (hasPlainPipe) {
442
+ sdkImports.push("definePipe");
443
+ }
444
+
445
+ lines.push(`import {`);
446
+ lines.push(` ${sdkImports.join(",\n ")},`);
447
+ lines.push(`} from "@tinybirdco/sdk";`);
448
+ lines.push("");
449
+
450
+ // Datasources section
451
+ if (datasources.length > 0) {
452
+ lines.push("// ============================================================================");
453
+ lines.push("// Datasources");
454
+ lines.push("// ============================================================================");
455
+ lines.push("");
456
+
457
+ for (const ds of datasources) {
458
+ lines.push(generateDatasourceCode(ds));
459
+ lines.push("");
460
+ }
461
+ }
462
+
463
+ // Pipes/Endpoints section
464
+ if (pipes.length > 0) {
465
+ lines.push("// ============================================================================");
466
+ lines.push("// Endpoints");
467
+ lines.push("// ============================================================================");
468
+ lines.push("");
469
+
470
+ for (const pipe of pipes) {
471
+ lines.push(generatePipeCode(pipe));
472
+ lines.push("");
473
+ }
474
+ }
475
+
476
+ // Client section
477
+ lines.push("// ============================================================================");
478
+ lines.push("// Client");
479
+ lines.push("// ============================================================================");
480
+ lines.push("");
481
+
482
+ const dsNames =
483
+ datasources.length > 0
484
+ ? datasources.map((ds) => toCamelCase(ds.name)).join(", ")
485
+ : "";
486
+ const endpoints = pipes.filter((p) => p.type === "endpoint");
487
+ const pipeNames =
488
+ endpoints.length > 0
489
+ ? endpoints.map((p) => toCamelCase(p.name)).join(", ")
490
+ : "";
491
+
492
+ lines.push("export const tinybird = createTinybirdClient({");
493
+ lines.push(` datasources: { ${dsNames} },`);
494
+ lines.push(` pipes: { ${pipeNames} },`);
495
+ lines.push("});");
496
+ lines.push("");
497
+
498
+ return lines.join("\n");
499
+ }
@@ -81,6 +81,19 @@ describe('Datasource Generator', () => {
81
81
  const result = generateDatasource(ds);
82
82
  expect(result.content).toContain('ENGINE_TTL "timestamp + INTERVAL 90 DAY"');
83
83
  });
84
+
85
+ it('includes forward query when provided', () => {
86
+ const ds = defineDatasource('test_ds', {
87
+ schema: {
88
+ id: t.string(),
89
+ },
90
+ forwardQuery: 'SELECT id',
91
+ });
92
+
93
+ const result = generateDatasource(ds);
94
+ expect(result.content).toContain('FORWARD_QUERY >');
95
+ expect(result.content).toContain(' SELECT id');
96
+ });
84
97
  });
85
98
 
86
99
  describe('Column formatting', () => {
@@ -163,6 +163,23 @@ function generateKafkaConfig(kafka: KafkaConfig): string {
163
163
  return parts.join("\n");
164
164
  }
165
165
 
166
+ /**
167
+ * Generate forward query section
168
+ */
169
+ function generateForwardQuery(forwardQuery?: string): string | null {
170
+ if (!forwardQuery) {
171
+ return null;
172
+ }
173
+
174
+ const trimmed = forwardQuery.trim();
175
+ if (!trimmed) {
176
+ return null;
177
+ }
178
+
179
+ const lines = trimmed.split(/\r?\n/);
180
+ return ["FORWARD_QUERY >", ...lines.map((line) => ` ${line}`)].join("\n");
181
+ }
182
+
166
183
  /**
167
184
  * Generate a .datasource file content from a DatasourceDefinition
168
185
  *
@@ -224,6 +241,13 @@ export function generateDatasource(
224
241
  parts.push(generateKafkaConfig(datasource.options.kafka));
225
242
  }
226
243
 
244
+ // Add forward query if present
245
+ const forwardQuery = generateForwardQuery(datasource.options.forwardQuery);
246
+ if (forwardQuery) {
247
+ parts.push("");
248
+ parts.push(forwardQuery);
249
+ }
250
+
227
251
  return {
228
252
  name: datasource._name,
229
253
  content: parts.join("\n"),
@@ -71,6 +71,11 @@ export interface DatasourceOptions<TSchema extends SchemaDefinition> {
71
71
  * Defaults to true.
72
72
  */
73
73
  jsonPaths?: boolean;
74
+ /**
75
+ * Forward query used to evolve a datasource with incompatible schema changes.
76
+ * This should be the SELECT clause only (no FROM/WHERE).
77
+ */
78
+ forwardQuery?: string;
74
79
  /** Kafka ingestion configuration */
75
80
  kafka?: KafkaConfig;
76
81
  }