@tinybirdco/sdk 0.0.36 → 0.0.37
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/dist/api/api.d.ts +17 -1
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +91 -0
- package/dist/api/api.js.map +1 -1
- package/dist/api/api.test.js +160 -0
- package/dist/api/api.test.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -3
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/client/base.d.ts +26 -1
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +39 -0
- package/dist/client/base.js.map +1 -1
- package/dist/client/base.test.js +25 -0
- package/dist/client/base.test.js.map +1 -1
- package/dist/client/types.d.ts +49 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/schema/project.d.ts +22 -3
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/schema/project.js +12 -0
- package/dist/schema/project.js.map +1 -1
- package/dist/schema/project.test.js +52 -0
- package/dist/schema/project.test.js.map +1 -1
- package/package.json +1 -1
- package/src/api/api.test.ts +222 -0
- package/src/api/api.ts +117 -0
- package/src/cli/commands/init.ts +5 -3
- package/src/client/base.test.ts +32 -0
- package/src/client/base.ts +48 -0
- package/src/client/types.ts +54 -0
- package/src/index.ts +5 -0
- package/src/schema/project.test.ts +64 -0
- package/src/schema/project.ts +42 -3
package/src/api/api.test.ts
CHANGED
|
@@ -292,4 +292,226 @@ describe("TinybirdApi", () => {
|
|
|
292
292
|
},
|
|
293
293
|
});
|
|
294
294
|
});
|
|
295
|
+
|
|
296
|
+
describe("appendDatasource", () => {
|
|
297
|
+
it("appends data from URL", async () => {
|
|
298
|
+
let datasourceName: string | null = null;
|
|
299
|
+
let modeParam: string | null = null;
|
|
300
|
+
let formatParam: string | null = null;
|
|
301
|
+
let contentType: string | null = null;
|
|
302
|
+
let requestBody: string | null = null;
|
|
303
|
+
|
|
304
|
+
server.use(
|
|
305
|
+
http.post(`${BASE_URL}/v0/datasources`, async ({ request }) => {
|
|
306
|
+
const url = new URL(request.url);
|
|
307
|
+
datasourceName = url.searchParams.get("name");
|
|
308
|
+
modeParam = url.searchParams.get("mode");
|
|
309
|
+
formatParam = url.searchParams.get("format");
|
|
310
|
+
contentType = request.headers.get("content-type");
|
|
311
|
+
requestBody = await request.text();
|
|
312
|
+
|
|
313
|
+
return HttpResponse.json({
|
|
314
|
+
successful_rows: 100,
|
|
315
|
+
quarantined_rows: 0,
|
|
316
|
+
import_id: "import_123",
|
|
317
|
+
});
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const api = createTinybirdApi({
|
|
322
|
+
baseUrl: BASE_URL,
|
|
323
|
+
token: "p.default-token",
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const result = await api.appendDatasource("events", {
|
|
327
|
+
url: "https://example.com/data.csv",
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(result).toEqual({
|
|
331
|
+
successful_rows: 100,
|
|
332
|
+
quarantined_rows: 0,
|
|
333
|
+
import_id: "import_123",
|
|
334
|
+
});
|
|
335
|
+
expect(datasourceName).toBe("events");
|
|
336
|
+
expect(modeParam).toBe("append");
|
|
337
|
+
expect(formatParam).toBe("csv");
|
|
338
|
+
expect(contentType).toBe("application/x-www-form-urlencoded");
|
|
339
|
+
expect(requestBody).toBe("url=https%3A%2F%2Fexample.com%2Fdata.csv");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("detects ndjson format from URL extension", async () => {
|
|
343
|
+
let formatParam: string | null = null;
|
|
344
|
+
|
|
345
|
+
server.use(
|
|
346
|
+
http.post(`${BASE_URL}/v0/datasources`, async ({ request }) => {
|
|
347
|
+
const url = new URL(request.url);
|
|
348
|
+
formatParam = url.searchParams.get("format");
|
|
349
|
+
return HttpResponse.json({ successful_rows: 1, quarantined_rows: 0 });
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const api = createTinybirdApi({
|
|
354
|
+
baseUrl: BASE_URL,
|
|
355
|
+
token: "p.default-token",
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await api.appendDatasource("events", {
|
|
359
|
+
url: "https://example.com/data.ndjson",
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(formatParam).toBe("ndjson");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("detects jsonl as ndjson format", async () => {
|
|
366
|
+
let formatParam: string | null = null;
|
|
367
|
+
|
|
368
|
+
server.use(
|
|
369
|
+
http.post(`${BASE_URL}/v0/datasources`, async ({ request }) => {
|
|
370
|
+
const url = new URL(request.url);
|
|
371
|
+
formatParam = url.searchParams.get("format");
|
|
372
|
+
return HttpResponse.json({ successful_rows: 1, quarantined_rows: 0 });
|
|
373
|
+
})
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const api = createTinybirdApi({
|
|
377
|
+
baseUrl: BASE_URL,
|
|
378
|
+
token: "p.default-token",
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
await api.appendDatasource("events", {
|
|
382
|
+
url: "https://example.com/data.jsonl",
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(formatParam).toBe("ndjson");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("detects parquet format from URL extension", async () => {
|
|
389
|
+
let formatParam: string | null = null;
|
|
390
|
+
|
|
391
|
+
server.use(
|
|
392
|
+
http.post(`${BASE_URL}/v0/datasources`, async ({ request }) => {
|
|
393
|
+
const url = new URL(request.url);
|
|
394
|
+
formatParam = url.searchParams.get("format");
|
|
395
|
+
return HttpResponse.json({ successful_rows: 1, quarantined_rows: 0 });
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const api = createTinybirdApi({
|
|
400
|
+
baseUrl: BASE_URL,
|
|
401
|
+
token: "p.default-token",
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await api.appendDatasource("events", {
|
|
405
|
+
url: "https://example.com/data.parquet",
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(formatParam).toBe("parquet");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("strips query string when detecting format", async () => {
|
|
412
|
+
let formatParam: string | null = null;
|
|
413
|
+
|
|
414
|
+
server.use(
|
|
415
|
+
http.post(`${BASE_URL}/v0/datasources`, async ({ request }) => {
|
|
416
|
+
const url = new URL(request.url);
|
|
417
|
+
formatParam = url.searchParams.get("format");
|
|
418
|
+
return HttpResponse.json({ successful_rows: 1, quarantined_rows: 0 });
|
|
419
|
+
})
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const api = createTinybirdApi({
|
|
423
|
+
baseUrl: BASE_URL,
|
|
424
|
+
token: "p.default-token",
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await api.appendDatasource("events", {
|
|
428
|
+
url: "https://example.com/data.csv?token=abc",
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
expect(formatParam).toBe("csv");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("includes CSV dialect options", async () => {
|
|
435
|
+
let delimiterParam: string | null = null;
|
|
436
|
+
let newLineParam: string | null = null;
|
|
437
|
+
let escapeCharParam: string | null = null;
|
|
438
|
+
|
|
439
|
+
server.use(
|
|
440
|
+
http.post(`${BASE_URL}/v0/datasources`, async ({ request }) => {
|
|
441
|
+
const url = new URL(request.url);
|
|
442
|
+
delimiterParam = url.searchParams.get("dialect_delimiter");
|
|
443
|
+
newLineParam = url.searchParams.get("dialect_new_line");
|
|
444
|
+
escapeCharParam = url.searchParams.get("dialect_escapechar");
|
|
445
|
+
return HttpResponse.json({ successful_rows: 1, quarantined_rows: 0 });
|
|
446
|
+
})
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const api = createTinybirdApi({
|
|
450
|
+
baseUrl: BASE_URL,
|
|
451
|
+
token: "p.default-token",
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
await api.appendDatasource("events", {
|
|
455
|
+
url: "https://example.com/data.csv",
|
|
456
|
+
csvDialect: {
|
|
457
|
+
delimiter: ";",
|
|
458
|
+
newLine: "\r\n",
|
|
459
|
+
escapeChar: "\\",
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(delimiterParam).toBe(";");
|
|
464
|
+
expect(newLineParam).toBe("\r\n");
|
|
465
|
+
expect(escapeCharParam).toBe("\\");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("throws error when neither url nor file is provided", async () => {
|
|
469
|
+
const api = createTinybirdApi({
|
|
470
|
+
baseUrl: BASE_URL,
|
|
471
|
+
token: "p.default-token",
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await expect(api.appendDatasource("events", {})).rejects.toThrow(
|
|
475
|
+
"Either 'url' or 'file' must be provided in options"
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("throws error when both url and file are provided", async () => {
|
|
480
|
+
const api = createTinybirdApi({
|
|
481
|
+
baseUrl: BASE_URL,
|
|
482
|
+
token: "p.default-token",
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
await expect(
|
|
486
|
+
api.appendDatasource("events", {
|
|
487
|
+
url: "https://example.com/data.csv",
|
|
488
|
+
file: "./data.csv",
|
|
489
|
+
})
|
|
490
|
+
).rejects.toThrow("Only one of 'url' or 'file' can be provided, not both");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("allows per-request token override", async () => {
|
|
494
|
+
let authorizationHeader: string | null = null;
|
|
495
|
+
|
|
496
|
+
server.use(
|
|
497
|
+
http.post(`${BASE_URL}/v0/datasources`, async ({ request }) => {
|
|
498
|
+
authorizationHeader = request.headers.get("Authorization");
|
|
499
|
+
return HttpResponse.json({ successful_rows: 1, quarantined_rows: 0 });
|
|
500
|
+
})
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const api = createTinybirdApi({
|
|
504
|
+
baseUrl: BASE_URL,
|
|
505
|
+
token: "p.default-token",
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
await api.appendDatasource(
|
|
509
|
+
"events",
|
|
510
|
+
{ url: "https://example.com/data.csv" },
|
|
511
|
+
{ token: "p.override-token" }
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
expect(authorizationHeader).toBe("Bearer p.override-token");
|
|
515
|
+
});
|
|
516
|
+
});
|
|
295
517
|
});
|
package/src/api/api.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createTinybirdFetcher, type TinybirdFetch } from "./fetcher.js";
|
|
2
2
|
import type {
|
|
3
|
+
AppendOptions,
|
|
4
|
+
AppendResult,
|
|
3
5
|
IngestOptions,
|
|
4
6
|
IngestResult,
|
|
5
7
|
QueryOptions,
|
|
@@ -41,6 +43,11 @@ export interface TinybirdApiIngestOptions extends IngestOptions {
|
|
|
41
43
|
token?: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
export interface TinybirdApiAppendOptions extends Omit<AppendOptions, 'url' | 'file'> {
|
|
47
|
+
/** Optional token override for this request */
|
|
48
|
+
token?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
/**
|
|
45
52
|
* Error thrown by TinybirdApi when a response is not OK
|
|
46
53
|
*/
|
|
@@ -242,6 +249,116 @@ export class TinybirdApi {
|
|
|
242
249
|
return (await response.json()) as QueryResult<T>;
|
|
243
250
|
}
|
|
244
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Append data to a datasource from a URL or local file
|
|
254
|
+
*/
|
|
255
|
+
async appendDatasource(
|
|
256
|
+
datasourceName: string,
|
|
257
|
+
options: AppendOptions,
|
|
258
|
+
apiOptions: TinybirdApiAppendOptions = {}
|
|
259
|
+
): Promise<AppendResult> {
|
|
260
|
+
const { url: sourceUrl, file: filePath } = options;
|
|
261
|
+
|
|
262
|
+
if (!sourceUrl && !filePath) {
|
|
263
|
+
throw new Error("Either 'url' or 'file' must be provided in options");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (sourceUrl && filePath) {
|
|
267
|
+
throw new Error("Only one of 'url' or 'file' can be provided, not both");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const url = new URL("/v0/datasources", `${this.baseUrl}/`);
|
|
271
|
+
url.searchParams.set("name", datasourceName);
|
|
272
|
+
url.searchParams.set("mode", "append");
|
|
273
|
+
|
|
274
|
+
// Auto-detect format from file/url extension
|
|
275
|
+
const format = this.detectFormat(sourceUrl ?? filePath!);
|
|
276
|
+
if (format) {
|
|
277
|
+
url.searchParams.set("format", format);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Add CSV dialect options if applicable
|
|
281
|
+
if (options.csvDialect) {
|
|
282
|
+
if (options.csvDialect.delimiter) {
|
|
283
|
+
url.searchParams.set("dialect_delimiter", options.csvDialect.delimiter);
|
|
284
|
+
}
|
|
285
|
+
if (options.csvDialect.newLine) {
|
|
286
|
+
url.searchParams.set("dialect_new_line", options.csvDialect.newLine);
|
|
287
|
+
}
|
|
288
|
+
if (options.csvDialect.escapeChar) {
|
|
289
|
+
url.searchParams.set("dialect_escapechar", options.csvDialect.escapeChar);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let response: Response;
|
|
294
|
+
|
|
295
|
+
if (sourceUrl) {
|
|
296
|
+
// Remote URL: send as form-urlencoded with url parameter
|
|
297
|
+
response = await this.request(url.toString(), {
|
|
298
|
+
method: "POST",
|
|
299
|
+
token: apiOptions.token,
|
|
300
|
+
headers: {
|
|
301
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
302
|
+
},
|
|
303
|
+
body: `url=${encodeURIComponent(sourceUrl)}`,
|
|
304
|
+
signal: this.createAbortSignal(options.timeout ?? apiOptions.timeout, options.signal ?? apiOptions.signal),
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
// Local file: send as multipart form data
|
|
308
|
+
const formData = await this.createFileFormData(filePath!);
|
|
309
|
+
response = await this.request(url.toString(), {
|
|
310
|
+
method: "POST",
|
|
311
|
+
token: apiOptions.token,
|
|
312
|
+
body: formData,
|
|
313
|
+
signal: this.createAbortSignal(options.timeout ?? apiOptions.timeout, options.signal ?? apiOptions.signal),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (!response.ok) {
|
|
318
|
+
await this.handleErrorResponse(response);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return (await response.json()) as AppendResult;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Detect format from file path or URL extension
|
|
326
|
+
*/
|
|
327
|
+
private detectFormat(source: string): "csv" | "ndjson" | "parquet" | undefined {
|
|
328
|
+
// Remove query string if present
|
|
329
|
+
const pathOnly = source.split("?")[0];
|
|
330
|
+
const extension = pathOnly.split(".").pop()?.toLowerCase();
|
|
331
|
+
|
|
332
|
+
switch (extension) {
|
|
333
|
+
case "csv":
|
|
334
|
+
return "csv";
|
|
335
|
+
case "ndjson":
|
|
336
|
+
case "jsonl":
|
|
337
|
+
return "ndjson";
|
|
338
|
+
case "parquet":
|
|
339
|
+
return "parquet";
|
|
340
|
+
default:
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Create FormData for file upload
|
|
347
|
+
*/
|
|
348
|
+
private async createFileFormData(filePath: string): Promise<FormData> {
|
|
349
|
+
// Dynamic import for Node.js fs module (browser-safe)
|
|
350
|
+
const fs = await import("node:fs");
|
|
351
|
+
const path = await import("node:path");
|
|
352
|
+
|
|
353
|
+
const fileContent = await fs.promises.readFile(filePath);
|
|
354
|
+
const fileName = path.basename(filePath);
|
|
355
|
+
|
|
356
|
+
const formData = new FormData();
|
|
357
|
+
formData.append("csv", new Blob([fileContent]), fileName);
|
|
358
|
+
|
|
359
|
+
return formData;
|
|
360
|
+
}
|
|
361
|
+
|
|
245
362
|
private createAbortSignal(
|
|
246
363
|
timeout?: number,
|
|
247
364
|
existingSignal?: AbortSignal
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -22,7 +22,6 @@ import { getGitRoot } from "../git.js";
|
|
|
22
22
|
import { fetchAllResources } from "../../api/resources.js";
|
|
23
23
|
import { generateCombinedFile } from "../../codegen/index.js";
|
|
24
24
|
import { execSync } from "child_process";
|
|
25
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
26
25
|
import {
|
|
27
26
|
detectPackageManager,
|
|
28
27
|
getPackageManagerAddCmd,
|
|
@@ -313,6 +312,8 @@ export interface InitOptions {
|
|
|
313
312
|
includeCdWorkflow?: boolean;
|
|
314
313
|
/** Git provider for workflow templates */
|
|
315
314
|
workflowProvider?: "github" | "gitlab";
|
|
315
|
+
/** Skip auto-installing @tinybirdco/sdk dependency */
|
|
316
|
+
skipDependencyInstall?: boolean;
|
|
316
317
|
}
|
|
317
318
|
|
|
318
319
|
/**
|
|
@@ -415,6 +416,8 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
|
|
|
415
416
|
const cwd = options.cwd ?? process.cwd();
|
|
416
417
|
const force = options.force ?? false;
|
|
417
418
|
const skipLogin = options.skipLogin ?? false;
|
|
419
|
+
const skipDependencyInstall =
|
|
420
|
+
options.skipDependencyInstall ?? Boolean(process.env.VITEST);
|
|
418
421
|
|
|
419
422
|
const created: string[] = [];
|
|
420
423
|
const skipped: string[] = [];
|
|
@@ -739,14 +742,13 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
|
|
|
739
742
|
}
|
|
740
743
|
|
|
741
744
|
// Install @tinybirdco/sdk if not already installed
|
|
742
|
-
if (!hasTinybirdSdkDependency(cwd)) {
|
|
745
|
+
if (!skipDependencyInstall && !hasTinybirdSdkDependency(cwd)) {
|
|
743
746
|
const s = p.spinner();
|
|
744
747
|
s.start("Installing dependencies");
|
|
745
748
|
const packageManager = detectPackageManager(cwd);
|
|
746
749
|
const addCmd = getPackageManagerAddCmd(packageManager);
|
|
747
750
|
try {
|
|
748
751
|
execSync(`${addCmd} @tinybirdco/sdk`, { cwd, stdio: "pipe" });
|
|
749
|
-
await sleep(1000);
|
|
750
752
|
s.stop("Installed dependencies");
|
|
751
753
|
created.push("@tinybirdco/sdk");
|
|
752
754
|
} catch (error) {
|
package/src/client/base.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { TinybirdClient, createClient } from "./base.js";
|
|
3
|
+
import type { DatasourcesNamespace } from "./types.js";
|
|
3
4
|
|
|
4
5
|
describe("TinybirdClient", () => {
|
|
5
6
|
describe("constructor", () => {
|
|
@@ -137,4 +138,35 @@ describe("TinybirdClient", () => {
|
|
|
137
138
|
expect(context.baseUrl).toBe("https://api.tinybird.co");
|
|
138
139
|
});
|
|
139
140
|
});
|
|
141
|
+
|
|
142
|
+
describe("datasources", () => {
|
|
143
|
+
it("exposes datasources namespace", () => {
|
|
144
|
+
const client = createClient({
|
|
145
|
+
baseUrl: "https://api.tinybird.co",
|
|
146
|
+
token: "test-token",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(client.datasources).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("datasources namespace has append method", () => {
|
|
153
|
+
const client = createClient({
|
|
154
|
+
baseUrl: "https://api.tinybird.co",
|
|
155
|
+
token: "test-token",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(typeof client.datasources.append).toBe("function");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("datasources conforms to DatasourcesNamespace interface", () => {
|
|
162
|
+
const client = createClient({
|
|
163
|
+
baseUrl: "https://api.tinybird.co",
|
|
164
|
+
token: "test-token",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const datasources: DatasourcesNamespace = client.datasources;
|
|
168
|
+
expect(datasources).toBeDefined();
|
|
169
|
+
expect(typeof datasources.append).toBe("function");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
140
172
|
});
|
package/src/client/base.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
|
+
AppendOptions,
|
|
7
|
+
AppendResult,
|
|
6
8
|
ClientConfig,
|
|
7
9
|
ClientContext,
|
|
10
|
+
DatasourcesNamespace,
|
|
8
11
|
QueryResult,
|
|
9
12
|
IngestResult,
|
|
10
13
|
QueryOptions,
|
|
@@ -57,6 +60,11 @@ export class TinybirdClient {
|
|
|
57
60
|
private contextPromise: Promise<ClientContext> | null = null;
|
|
58
61
|
private resolvedContext: ClientContext | null = null;
|
|
59
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Datasources namespace for import operations
|
|
65
|
+
*/
|
|
66
|
+
readonly datasources: DatasourcesNamespace;
|
|
67
|
+
|
|
60
68
|
constructor(config: ClientConfig) {
|
|
61
69
|
// Validate required config
|
|
62
70
|
if (!config.baseUrl) {
|
|
@@ -71,6 +79,46 @@ export class TinybirdClient {
|
|
|
71
79
|
...config,
|
|
72
80
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
73
81
|
};
|
|
82
|
+
|
|
83
|
+
// Initialize datasources namespace
|
|
84
|
+
this.datasources = {
|
|
85
|
+
append: (datasourceName: string, options: AppendOptions): Promise<AppendResult> => {
|
|
86
|
+
return this.appendDatasource(datasourceName, options);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Append data to a datasource from a URL or local file
|
|
93
|
+
*
|
|
94
|
+
* @param datasourceName - Name of the datasource
|
|
95
|
+
* @param options - Append options including url or file source
|
|
96
|
+
* @returns Append result
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* // Append from URL
|
|
101
|
+
* await client.datasources.append('events', {
|
|
102
|
+
* url: 'https://example.com/data.csv',
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Append from local file
|
|
106
|
+
* await client.datasources.append('events', {
|
|
107
|
+
* file: './data/events.ndjson',
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
private async appendDatasource(
|
|
112
|
+
datasourceName: string,
|
|
113
|
+
options: AppendOptions
|
|
114
|
+
): Promise<AppendResult> {
|
|
115
|
+
const token = await this.getToken();
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
return await this.getApi(token).appendDatasource(datasourceName, options);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.rethrowApiError(error);
|
|
121
|
+
}
|
|
74
122
|
}
|
|
75
123
|
|
|
76
124
|
/**
|
package/src/client/types.ts
CHANGED
|
@@ -189,3 +189,57 @@ export interface TypedDatasourceIngest<TRow> {
|
|
|
189
189
|
/** Send multiple events in a batch */
|
|
190
190
|
sendBatch(events: TRow[], options?: IngestOptions): Promise<IngestResult>;
|
|
191
191
|
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Data format for append operations
|
|
195
|
+
*/
|
|
196
|
+
export type AppendFormat = "csv" | "ndjson" | "parquet";
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* CSV dialect options for customizing CSV parsing
|
|
200
|
+
*/
|
|
201
|
+
export interface CsvDialectOptions {
|
|
202
|
+
/** Field delimiter character (default: ',') */
|
|
203
|
+
delimiter?: string;
|
|
204
|
+
/** New line character(s) (default: '\n') */
|
|
205
|
+
newLine?: string;
|
|
206
|
+
/** Escape character for special characters */
|
|
207
|
+
escapeChar?: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Options for append operation
|
|
212
|
+
* Exactly one of `url` or `file` must be provided
|
|
213
|
+
*/
|
|
214
|
+
export interface AppendOptions {
|
|
215
|
+
/** Remote URL to import data from */
|
|
216
|
+
url?: string;
|
|
217
|
+
/** Local file path to import data from */
|
|
218
|
+
file?: string;
|
|
219
|
+
/** CSV dialect options (only applicable for csv format) */
|
|
220
|
+
csvDialect?: CsvDialectOptions;
|
|
221
|
+
/** Request timeout in milliseconds */
|
|
222
|
+
timeout?: number;
|
|
223
|
+
/** AbortController signal for cancellation */
|
|
224
|
+
signal?: AbortSignal;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Result of an append operation
|
|
229
|
+
*/
|
|
230
|
+
export interface AppendResult {
|
|
231
|
+
/** Number of rows successfully appended */
|
|
232
|
+
successful_rows: number;
|
|
233
|
+
/** Number of rows that failed to append (quarantined) */
|
|
234
|
+
quarantined_rows: number;
|
|
235
|
+
/** Import ID for tracking */
|
|
236
|
+
import_id?: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Datasources namespace interface for raw client
|
|
241
|
+
*/
|
|
242
|
+
export interface DatasourcesNamespace {
|
|
243
|
+
/** Append data to a datasource from a URL or file */
|
|
244
|
+
append(datasourceName: string, options: AppendOptions): Promise<AppendResult>;
|
|
245
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -202,8 +202,12 @@ export type {
|
|
|
202
202
|
export { TinybirdClient, createClient } from "./client/base.js";
|
|
203
203
|
export { TinybirdError } from "./client/types.js";
|
|
204
204
|
export type {
|
|
205
|
+
AppendOptions,
|
|
206
|
+
AppendResult,
|
|
205
207
|
ClientConfig,
|
|
206
208
|
ClientContext,
|
|
209
|
+
CsvDialectOptions,
|
|
210
|
+
DatasourcesNamespace,
|
|
207
211
|
QueryResult,
|
|
208
212
|
IngestResult,
|
|
209
213
|
QueryOptions,
|
|
@@ -226,6 +230,7 @@ export type {
|
|
|
226
230
|
TinybirdApiConfig,
|
|
227
231
|
TinybirdApiQueryOptions,
|
|
228
232
|
TinybirdApiIngestOptions,
|
|
233
|
+
TinybirdApiAppendOptions,
|
|
229
234
|
TinybirdApiRequestInit,
|
|
230
235
|
} from "./api/api.js";
|
|
231
236
|
|
|
@@ -93,6 +93,37 @@ describe("Project Schema", () => {
|
|
|
93
93
|
expect(typeof project.tinybird.ingest.eventsBatch).toBe("function");
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
it("creates datasource accessors with append method", () => {
|
|
97
|
+
const events = defineDatasource("events", {
|
|
98
|
+
schema: { timestamp: t.dateTime() },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const project = defineProject({
|
|
102
|
+
datasources: { events },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(project.tinybird.events).toBeDefined();
|
|
106
|
+
expect(typeof project.tinybird.events.append).toBe("function");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("creates multiple datasource accessors", () => {
|
|
110
|
+
const events = defineDatasource("events", {
|
|
111
|
+
schema: { timestamp: t.dateTime() },
|
|
112
|
+
});
|
|
113
|
+
const pageViews = defineDatasource("page_views", {
|
|
114
|
+
schema: { pathname: t.string() },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const project = defineProject({
|
|
118
|
+
datasources: { events, pageViews },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(project.tinybird.events).toBeDefined();
|
|
122
|
+
expect(project.tinybird.pageViews).toBeDefined();
|
|
123
|
+
expect(typeof project.tinybird.events.append).toBe("function");
|
|
124
|
+
expect(typeof project.tinybird.pageViews.append).toBe("function");
|
|
125
|
+
});
|
|
126
|
+
|
|
96
127
|
it("throws error when accessing client before initialization", () => {
|
|
97
128
|
const project = defineProject({});
|
|
98
129
|
|
|
@@ -269,6 +300,39 @@ describe("Project Schema", () => {
|
|
|
269
300
|
expect(typeof client.ingest.eventsBatch).toBe("function");
|
|
270
301
|
});
|
|
271
302
|
|
|
303
|
+
it("creates datasource accessors with append method", () => {
|
|
304
|
+
const events = defineDatasource("events", {
|
|
305
|
+
schema: { id: t.string() },
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const client = createTinybirdClient({
|
|
309
|
+
datasources: { events },
|
|
310
|
+
pipes: {},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(client.events).toBeDefined();
|
|
314
|
+
expect(typeof client.events.append).toBe("function");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("creates multiple datasource accessors", () => {
|
|
318
|
+
const events = defineDatasource("events", {
|
|
319
|
+
schema: { id: t.string() },
|
|
320
|
+
});
|
|
321
|
+
const pageViews = defineDatasource("page_views", {
|
|
322
|
+
schema: { pathname: t.string() },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const client = createTinybirdClient({
|
|
326
|
+
datasources: { events, pageViews },
|
|
327
|
+
pipes: {},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(client.events).toBeDefined();
|
|
331
|
+
expect(client.pageViews).toBeDefined();
|
|
332
|
+
expect(typeof client.events.append).toBe("function");
|
|
333
|
+
expect(typeof client.pageViews.append).toBe("function");
|
|
334
|
+
});
|
|
335
|
+
|
|
272
336
|
it("accepts devMode option", () => {
|
|
273
337
|
const events = defineDatasource("events", {
|
|
274
338
|
schema: { id: t.string() },
|