@tinybirdco/sdk 0.0.38 → 0.0.40

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 (67) hide show
  1. package/README.md +47 -2
  2. package/dist/api/api.d.ts +18 -1
  3. package/dist/api/api.d.ts.map +1 -1
  4. package/dist/api/api.js +56 -0
  5. package/dist/api/api.js.map +1 -1
  6. package/dist/api/api.test.js +111 -0
  7. package/dist/api/api.test.js.map +1 -1
  8. package/dist/cli/commands/dev.d.ts.map +1 -1
  9. package/dist/cli/commands/dev.js +4 -9
  10. package/dist/cli/commands/dev.js.map +1 -1
  11. package/dist/cli/commands/init.d.ts +12 -0
  12. package/dist/cli/commands/init.d.ts.map +1 -1
  13. package/dist/cli/commands/init.js +217 -2
  14. package/dist/cli/commands/init.js.map +1 -1
  15. package/dist/cli/commands/init.test.js +15 -0
  16. package/dist/cli/commands/init.test.js.map +1 -1
  17. package/dist/cli/config-types.d.ts +1 -1
  18. package/dist/cli/config-types.d.ts.map +1 -1
  19. package/dist/cli/config.d.ts +1 -1
  20. package/dist/cli/config.d.ts.map +1 -1
  21. package/dist/client/base.d.ts +16 -0
  22. package/dist/client/base.d.ts.map +1 -1
  23. package/dist/client/base.js +38 -0
  24. package/dist/client/base.js.map +1 -1
  25. package/dist/client/base.test.js +4 -0
  26. package/dist/client/base.test.js.map +1 -1
  27. package/dist/client/types.d.ts +50 -0
  28. package/dist/client/types.d.ts.map +1 -1
  29. package/dist/generator/include-paths.d.ts +7 -0
  30. package/dist/generator/include-paths.d.ts.map +1 -0
  31. package/dist/generator/include-paths.js +164 -0
  32. package/dist/generator/include-paths.js.map +1 -0
  33. package/dist/generator/index.d.ts +1 -1
  34. package/dist/generator/index.d.ts.map +1 -1
  35. package/dist/generator/index.test.js +36 -0
  36. package/dist/generator/index.test.js.map +1 -1
  37. package/dist/generator/loader.d.ts +1 -1
  38. package/dist/generator/loader.d.ts.map +1 -1
  39. package/dist/generator/loader.js +5 -8
  40. package/dist/generator/loader.js.map +1 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/schema/project.d.ts +8 -4
  45. package/dist/schema/project.d.ts.map +1 -1
  46. package/dist/schema/project.js +8 -0
  47. package/dist/schema/project.js.map +1 -1
  48. package/dist/schema/project.test.js +14 -2
  49. package/dist/schema/project.test.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/api/api.test.ts +165 -0
  52. package/src/api/api.ts +104 -0
  53. package/src/cli/commands/dev.ts +4 -9
  54. package/src/cli/commands/init.test.ts +17 -0
  55. package/src/cli/commands/init.ts +300 -2
  56. package/src/cli/config-types.ts +1 -1
  57. package/src/cli/config.ts +1 -1
  58. package/src/client/base.test.ts +4 -0
  59. package/src/client/base.ts +53 -0
  60. package/src/client/types.ts +54 -0
  61. package/src/generator/include-paths.ts +234 -0
  62. package/src/generator/index.test.ts +44 -0
  63. package/src/generator/index.ts +1 -1
  64. package/src/generator/loader.ts +6 -10
  65. package/src/index.ts +6 -0
  66. package/src/schema/project.test.ts +14 -2
  67. package/src/schema/project.ts +19 -3
@@ -0,0 +1,234 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export interface ResolvedIncludeFile {
5
+ sourcePath: string;
6
+ absolutePath: string;
7
+ }
8
+
9
+ const GLOB_SEGMENT_REGEX = /[*?[]/;
10
+ const IGNORED_DIRECTORIES = new Set([".git", "node_modules"]);
11
+ const SEGMENT_REGEX_CACHE = new Map<string, RegExp>();
12
+
13
+ function hasGlobPattern(value: string): boolean {
14
+ return GLOB_SEGMENT_REGEX.test(value);
15
+ }
16
+
17
+ function normalizePath(value: string): string {
18
+ return value.replace(/\\/g, "/");
19
+ }
20
+
21
+ function splitAbsolutePath(filePath: string): { root: string; segments: string[] } {
22
+ const absolutePath = path.resolve(filePath);
23
+ const root = path.parse(absolutePath).root;
24
+ const relative = path.relative(root, absolutePath);
25
+
26
+ return {
27
+ root: normalizePath(root),
28
+ segments: normalizePath(relative).split("/").filter(Boolean),
29
+ };
30
+ }
31
+
32
+ function segmentMatcher(segment: string): RegExp {
33
+ const cached = SEGMENT_REGEX_CACHE.get(segment);
34
+ if (cached) {
35
+ return cached;
36
+ }
37
+
38
+ const escaped = segment
39
+ .replace(/[.+^${}()|\\]/g, "\\$&")
40
+ .replace(/\*/g, "[^/]*")
41
+ .replace(/\?/g, "[^/]");
42
+
43
+ const matcher = new RegExp(`^${escaped}$`);
44
+ SEGMENT_REGEX_CACHE.set(segment, matcher);
45
+ return matcher;
46
+ }
47
+
48
+ function matchSegment(patternSegment: string, valueSegment: string): boolean {
49
+ if (!hasGlobPattern(patternSegment)) {
50
+ return patternSegment === valueSegment;
51
+ }
52
+
53
+ return segmentMatcher(patternSegment).test(valueSegment);
54
+ }
55
+
56
+ function matchGlobSegments(
57
+ patternSegments: string[],
58
+ pathSegments: string[],
59
+ patternIndex: number,
60
+ pathIndex: number,
61
+ memo: Map<string, boolean>
62
+ ): boolean {
63
+ const key = `${patternIndex}:${pathIndex}`;
64
+ const cached = memo.get(key);
65
+ if (cached !== undefined) {
66
+ return cached;
67
+ }
68
+
69
+ if (patternIndex === patternSegments.length) {
70
+ const matches = pathIndex === pathSegments.length;
71
+ memo.set(key, matches);
72
+ return matches;
73
+ }
74
+
75
+ const patternSegment = patternSegments[patternIndex];
76
+ let matches = false;
77
+
78
+ if (patternSegment === "**") {
79
+ matches = matchGlobSegments(patternSegments, pathSegments, patternIndex + 1, pathIndex, memo);
80
+
81
+ if (!matches && pathIndex < pathSegments.length) {
82
+ matches = matchGlobSegments(patternSegments, pathSegments, patternIndex, pathIndex + 1, memo);
83
+ }
84
+ } else if (
85
+ pathIndex < pathSegments.length &&
86
+ matchSegment(patternSegment, pathSegments[pathIndex])
87
+ ) {
88
+ matches = matchGlobSegments(patternSegments, pathSegments, patternIndex + 1, pathIndex + 1, memo);
89
+ }
90
+
91
+ memo.set(key, matches);
92
+ return matches;
93
+ }
94
+
95
+ function matchGlobPath(absolutePattern: string, absolutePath: string): boolean {
96
+ const patternParts = splitAbsolutePath(absolutePattern);
97
+ const pathParts = splitAbsolutePath(absolutePath);
98
+
99
+ if (patternParts.root.toLowerCase() !== pathParts.root.toLowerCase()) {
100
+ return false;
101
+ }
102
+
103
+ return matchGlobSegments(
104
+ patternParts.segments,
105
+ pathParts.segments,
106
+ 0,
107
+ 0,
108
+ new Map<string, boolean>()
109
+ );
110
+ }
111
+
112
+ function getGlobRootDirectory(absolutePattern: string): string {
113
+ const { root, segments } = splitAbsolutePath(absolutePattern);
114
+ const firstGlobIndex = segments.findIndex((segment) => hasGlobPattern(segment));
115
+ const baseSegments =
116
+ firstGlobIndex === -1 ? segments : segments.slice(0, firstGlobIndex);
117
+
118
+ return path.join(root, ...baseSegments);
119
+ }
120
+
121
+ function collectFilesRecursive(directory: string, result: string[]): void {
122
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
123
+
124
+ for (const entry of entries) {
125
+ const fullPath = path.join(directory, entry.name);
126
+
127
+ if (entry.isDirectory()) {
128
+ if (IGNORED_DIRECTORIES.has(entry.name)) {
129
+ continue;
130
+ }
131
+
132
+ collectFilesRecursive(fullPath, result);
133
+ continue;
134
+ }
135
+
136
+ if (entry.isFile()) {
137
+ result.push(fullPath);
138
+ }
139
+ }
140
+ }
141
+
142
+ function expandGlobPattern(absolutePattern: string): string[] {
143
+ const rootDirectory = getGlobRootDirectory(absolutePattern);
144
+
145
+ if (!fs.existsSync(rootDirectory)) {
146
+ return [];
147
+ }
148
+
149
+ if (!fs.statSync(rootDirectory).isDirectory()) {
150
+ return [];
151
+ }
152
+
153
+ const files: string[] = [];
154
+ collectFilesRecursive(rootDirectory, files);
155
+
156
+ return files
157
+ .filter((filePath) => matchGlobPath(absolutePattern, filePath))
158
+ .sort((a, b) => a.localeCompare(b));
159
+ }
160
+
161
+ export function resolveIncludeFiles(
162
+ includePaths: string[],
163
+ cwd: string
164
+ ): ResolvedIncludeFile[] {
165
+ const resolved: ResolvedIncludeFile[] = [];
166
+ const seen = new Set<string>();
167
+
168
+ for (const includePath of includePaths) {
169
+ const absoluteIncludePath = path.isAbsolute(includePath)
170
+ ? includePath
171
+ : path.resolve(cwd, includePath);
172
+
173
+ if (hasGlobPattern(includePath)) {
174
+ const matchedFiles = expandGlobPattern(absoluteIncludePath);
175
+
176
+ if (matchedFiles.length === 0) {
177
+ throw new Error(`Include pattern matched no files: ${includePath}`);
178
+ }
179
+
180
+ for (const matchedFile of matchedFiles) {
181
+ if (seen.has(matchedFile)) {
182
+ continue;
183
+ }
184
+
185
+ seen.add(matchedFile);
186
+ resolved.push({
187
+ sourcePath: path.isAbsolute(includePath)
188
+ ? matchedFile
189
+ : path.relative(cwd, matchedFile),
190
+ absolutePath: matchedFile,
191
+ });
192
+ }
193
+ continue;
194
+ }
195
+
196
+ if (!fs.existsSync(absoluteIncludePath)) {
197
+ throw new Error(`Include file not found: ${absoluteIncludePath}`);
198
+ }
199
+
200
+ if (seen.has(absoluteIncludePath)) {
201
+ continue;
202
+ }
203
+
204
+ seen.add(absoluteIncludePath);
205
+ resolved.push({
206
+ sourcePath: includePath,
207
+ absolutePath: absoluteIncludePath,
208
+ });
209
+ }
210
+
211
+ return resolved;
212
+ }
213
+
214
+ export function getIncludeWatchDirectories(
215
+ includePaths: string[],
216
+ cwd: string
217
+ ): string[] {
218
+ const watchDirs = new Set<string>();
219
+
220
+ for (const includePath of includePaths) {
221
+ const absoluteIncludePath = path.isAbsolute(includePath)
222
+ ? includePath
223
+ : path.resolve(cwd, includePath);
224
+
225
+ if (hasGlobPattern(includePath)) {
226
+ watchDirs.add(getGlobRootDirectory(absoluteIncludePath));
227
+ continue;
228
+ }
229
+
230
+ watchDirs.add(path.dirname(absoluteIncludePath));
231
+ }
232
+
233
+ return Array.from(watchDirs);
234
+ }
@@ -325,5 +325,49 @@ TYPE endpoint
325
325
  expect(rawPipe).toBeDefined();
326
326
  expect(rawPipe!.content).toBe(rawPipeContent);
327
327
  });
328
+
329
+ it("supports glob include patterns", async () => {
330
+ const nestedDir = path.join(tempDir, "tinybird", "legacy");
331
+ fs.mkdirSync(nestedDir, { recursive: true });
332
+
333
+ const datasourceContent = `SCHEMA >
334
+ id String
335
+
336
+ ENGINE "MergeTree"
337
+ ENGINE_SORTING_KEY "id"
338
+ `;
339
+ const datasourcePath = path.join(nestedDir, "events.datasource");
340
+ fs.writeFileSync(datasourcePath, datasourceContent);
341
+
342
+ const pipeContent = `NODE endpoint
343
+ SQL >
344
+ SELECT * FROM events
345
+
346
+ TYPE endpoint
347
+ `;
348
+ const pipePath = path.join(nestedDir, "events.pipe");
349
+ fs.writeFileSync(pipePath, pipeContent);
350
+
351
+ const result = await buildFromInclude({
352
+ includePaths: ["tinybird/**/*.datasource", "tinybird/**/*.pipe"],
353
+ cwd: tempDir,
354
+ });
355
+
356
+ expect(result.resources.datasources).toHaveLength(1);
357
+ expect(result.resources.datasources[0].name).toBe("events");
358
+ expect(result.resources.datasources[0].content).toBe(datasourceContent);
359
+ expect(result.resources.pipes).toHaveLength(1);
360
+ expect(result.resources.pipes[0].name).toBe("events");
361
+ expect(result.resources.pipes[0].content).toBe(pipeContent);
362
+ });
363
+
364
+ it("throws when include glob matches no files", async () => {
365
+ await expect(
366
+ buildFromInclude({
367
+ includePaths: ["tinybird/**/*.datasource"],
368
+ cwd: tempDir,
369
+ })
370
+ ).rejects.toThrow("Include pattern matched no files");
371
+ });
328
372
  });
329
373
  });
@@ -122,7 +122,7 @@ export async function build(options: BuildOptions): Promise<BuildResult> {
122
122
  * Build options using include paths
123
123
  */
124
124
  export interface BuildFromIncludeOptions {
125
- /** Array of file paths to scan for datasources and pipes */
125
+ /** Array of file paths or glob patterns to scan for datasources and pipes */
126
126
  includePaths: string[];
127
127
  /** Working directory (defaults to cwd) */
128
128
  cwd?: string;
@@ -11,6 +11,7 @@ import { isProjectDefinition, type ProjectDefinition, type DatasourcesDefinition
11
11
  import { isDatasourceDefinition, type DatasourceDefinition } from "../schema/datasource.js";
12
12
  import { isPipeDefinition, type PipeDefinition } from "../schema/pipe.js";
13
13
  import { isConnectionDefinition, type ConnectionDefinition } from "../schema/connection.js";
14
+ import { resolveIncludeFiles } from "./include-paths.js";
14
15
 
15
16
  /**
16
17
  * Result of loading a schema file
@@ -184,7 +185,7 @@ export interface LoadedEntities {
184
185
  * Options for loading entities
185
186
  */
186
187
  export interface LoadEntitiesOptions {
187
- /** Array of file paths to scan (can be relative or absolute) */
188
+ /** Array of file paths or glob patterns to scan (can be relative or absolute) */
188
189
  includePaths: string[];
189
190
  /** The working directory for resolution (defaults to cwd) */
190
191
  cwd?: string;
@@ -211,6 +212,7 @@ export interface LoadEntitiesOptions {
211
212
  */
212
213
  export async function loadEntities(options: LoadEntitiesOptions): Promise<LoadedEntities> {
213
214
  const cwd = options.cwd ?? process.cwd();
215
+ const includeFiles = resolveIncludeFiles(options.includePaths, cwd);
214
216
  const result: LoadedEntities = {
215
217
  datasources: {},
216
218
  pipes: {},
@@ -220,15 +222,9 @@ export async function loadEntities(options: LoadEntitiesOptions): Promise<Loaded
220
222
  sourceFiles: [],
221
223
  };
222
224
 
223
- for (const includePath of options.includePaths) {
224
- const absolutePath = path.isAbsolute(includePath)
225
- ? includePath
226
- : path.resolve(cwd, includePath);
227
-
228
- // Verify the file exists
229
- if (!fs.existsSync(absolutePath)) {
230
- throw new Error(`Include file not found: ${absolutePath}`);
231
- }
225
+ for (const includeFile of includeFiles) {
226
+ const includePath = includeFile.sourcePath;
227
+ const absolutePath = includeFile.absolutePath;
232
228
 
233
229
  result.sourceFiles.push(includePath);
234
230
 
package/src/index.ts CHANGED
@@ -208,10 +208,14 @@ export type {
208
208
  ClientContext,
209
209
  CsvDialectOptions,
210
210
  DatasourcesNamespace,
211
+ DeleteOptions,
212
+ DeleteResult,
211
213
  QueryResult,
212
214
  IngestResult,
213
215
  QueryOptions,
214
216
  IngestOptions,
217
+ TruncateOptions,
218
+ TruncateResult,
215
219
  ColumnMeta,
216
220
  QueryStatistics,
217
221
  TinybirdErrorResponse,
@@ -231,6 +235,8 @@ export type {
231
235
  TinybirdApiQueryOptions,
232
236
  TinybirdApiIngestOptions,
233
237
  TinybirdApiAppendOptions,
238
+ TinybirdApiDeleteOptions,
239
+ TinybirdApiTruncateOptions,
234
240
  TinybirdApiRequestInit,
235
241
  TinybirdApiTokenScope,
236
242
  TinybirdApiCreateTokenRequest,
@@ -94,7 +94,7 @@ describe("Project Schema", () => {
94
94
  expect((project.tinybird as unknown as Record<string, unknown>).pipes).toBeUndefined();
95
95
  });
96
96
 
97
- it("creates datasource accessors with append method", () => {
97
+ it("creates datasource accessors with append/delete/truncate methods", () => {
98
98
  const events = defineDatasource("events", {
99
99
  schema: { timestamp: t.dateTime() },
100
100
  });
@@ -105,6 +105,8 @@ describe("Project Schema", () => {
105
105
 
106
106
  expect(project.tinybird.events).toBeDefined();
107
107
  expect(typeof project.tinybird.events.append).toBe("function");
108
+ expect(typeof project.tinybird.events.delete).toBe("function");
109
+ expect(typeof project.tinybird.events.truncate).toBe("function");
108
110
  });
109
111
 
110
112
  it("creates multiple datasource accessors", () => {
@@ -123,6 +125,10 @@ describe("Project Schema", () => {
123
125
  expect(project.tinybird.pageViews).toBeDefined();
124
126
  expect(typeof project.tinybird.events.append).toBe("function");
125
127
  expect(typeof project.tinybird.pageViews.append).toBe("function");
128
+ expect(typeof project.tinybird.events.delete).toBe("function");
129
+ expect(typeof project.tinybird.pageViews.delete).toBe("function");
130
+ expect(typeof project.tinybird.events.truncate).toBe("function");
131
+ expect(typeof project.tinybird.pageViews.truncate).toBe("function");
126
132
  });
127
133
 
128
134
  it("throws error when accessing client before initialization", () => {
@@ -304,7 +310,7 @@ describe("Project Schema", () => {
304
310
  expect((client as unknown as Record<string, unknown>).pipes).toBeUndefined();
305
311
  });
306
312
 
307
- it("creates datasource accessors with append method", () => {
313
+ it("creates datasource accessors with append/delete/truncate methods", () => {
308
314
  const events = defineDatasource("events", {
309
315
  schema: { id: t.string() },
310
316
  });
@@ -316,6 +322,8 @@ describe("Project Schema", () => {
316
322
 
317
323
  expect(client.events).toBeDefined();
318
324
  expect(typeof client.events.append).toBe("function");
325
+ expect(typeof client.events.delete).toBe("function");
326
+ expect(typeof client.events.truncate).toBe("function");
319
327
  });
320
328
 
321
329
  it("creates multiple datasource accessors", () => {
@@ -335,6 +343,10 @@ describe("Project Schema", () => {
335
343
  expect(client.pageViews).toBeDefined();
336
344
  expect(typeof client.events.append).toBe("function");
337
345
  expect(typeof client.pageViews.append).toBe("function");
346
+ expect(typeof client.events.delete).toBe("function");
347
+ expect(typeof client.pageViews.delete).toBe("function");
348
+ expect(typeof client.events.truncate).toBe("function");
349
+ expect(typeof client.pageViews.truncate).toBe("function");
338
350
  });
339
351
 
340
352
  it("accepts devMode option", () => {
@@ -12,8 +12,12 @@ import type {
12
12
  AppendOptions,
13
13
  AppendResult,
14
14
  DatasourcesNamespace,
15
+ DeleteOptions,
16
+ DeleteResult,
15
17
  QueryOptions,
16
18
  QueryResult,
19
+ TruncateOptions,
20
+ TruncateResult,
17
21
  } from "../client/types.js";
18
22
  import type { InferRow, InferParams, InferOutputRow } from "../infer/index.js";
19
23
  import type { TokensNamespace } from "../client/tokens.js";
@@ -82,16 +86,20 @@ type IngestMethods<T extends DatasourcesDefinition> = {
82
86
  };
83
87
 
84
88
  /**
85
- * Type for a datasource accessor with append method
89
+ * Type for a datasource accessor with import/mutation methods
86
90
  */
87
91
  type DatasourceAccessor = {
88
92
  /** Append data from a URL or file */
89
93
  append(options: AppendOptions): Promise<AppendResult>;
94
+ /** Delete rows using a SQL condition */
95
+ delete(options: DeleteOptions): Promise<DeleteResult>;
96
+ /** Truncate all rows */
97
+ truncate(options?: TruncateOptions): Promise<TruncateResult>;
90
98
  };
91
99
 
92
100
  /**
93
101
  * Type for datasource accessors object
94
- * Maps each datasource to an accessor with append method
102
+ * Maps each datasource to an accessor with import/mutation methods
95
103
  */
96
104
  type DatasourceAccessors<T extends DatasourcesDefinition> = {
97
105
  [K in keyof T]: DatasourceAccessor;
@@ -105,7 +113,7 @@ interface ProjectClientBase<TDatasources extends DatasourcesDefinition> {
105
113
  ingest: IngestMethods<TDatasources>;
106
114
  /** Token operations (JWT creation, etc.) */
107
115
  readonly tokens: TokensNamespace;
108
- /** Datasource operations (append from URL/file) */
116
+ /** Datasource operations (append/delete/truncate) */
109
117
  readonly datasources: DatasourcesNamespace;
110
118
  /** Execute raw SQL queries */
111
119
  sql<T = unknown>(sql: string, options?: QueryOptions): Promise<QueryResult<T>>;
@@ -374,6 +382,14 @@ function buildProjectClient<
374
382
  const client = await getClient();
375
383
  return client.datasources.append(tinybirdName, options);
376
384
  },
385
+ delete: async (options: DeleteOptions) => {
386
+ const client = await getClient();
387
+ return client.datasources.delete(tinybirdName, options);
388
+ },
389
+ truncate: async (options: TruncateOptions = {}) => {
390
+ const client = await getClient();
391
+ return client.datasources.truncate(tinybirdName, options);
392
+ },
377
393
  };
378
394
  }
379
395