Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions src/api/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1225,4 +1225,141 @@ describe("TinybirdApi", () => {
expect(authorizationHeader).toBe("Bearer p.override-token");
});
});

describe("sampleDatasource", () => {
it("sends default payload (max_files=1, full_export=false)", async () => {
let body: unknown;
let contentType: string | null = null;

server.use(
http.post(`${BASE_URL}/v0/datasources/events/sample`, async ({ request }) => {
contentType = request.headers.get("Content-Type");
body = await request.json();
return HttpResponse.json({ job_id: "job-1", status: "waiting" });
})
);

const api = createTinybirdApi({
baseUrl: BASE_URL,
token: "p.default-token",
});

const result = await api.sampleDatasource("events");

expect(result).toEqual({ job_id: "job-1", status: "waiting" });
expect(contentType).toBe("application/json");
expect(body).toEqual({ max_files: 1, full_export: false });
});

it("forwards maxFiles for blob storage connectors", async () => {
let body: unknown;

server.use(
http.post(`${BASE_URL}/v0/datasources/events/sample`, async ({ request }) => {
body = await request.json();
return HttpResponse.json({ job_id: "job-1" });
})
);

const api = createTinybirdApi({
baseUrl: BASE_URL,
token: "p.default-token",
});

await api.sampleDatasource("events", { maxFiles: 3 });

expect(body).toEqual({ max_files: 3, full_export: false });
});

it("forwards DynamoDB rows option", async () => {
let body: unknown;

server.use(
http.post(`${BASE_URL}/v0/datasources/ddb_ds/sample`, async ({ request }) => {
body = await request.json();
return HttpResponse.json({ job_id: "job-1" });
})
);

const api = createTinybirdApi({
baseUrl: BASE_URL,
token: "p.default-token",
});

await api.sampleDatasource("ddb_ds", { rows: 100000 });

expect(body).toEqual({ max_files: 1, full_export: false, rows: 100000 });
});

it("forwards DynamoDB maxBytes option", async () => {
let body: unknown;

server.use(
http.post(`${BASE_URL}/v0/datasources/ddb_ds/sample`, async ({ request }) => {
body = await request.json();
return HttpResponse.json({ job_id: "job-1" });
})
);

const api = createTinybirdApi({
baseUrl: BASE_URL,
token: "p.default-token",
});

await api.sampleDatasource("ddb_ds", { maxBytes: "1GB" });

expect(body).toEqual({ max_files: 1, full_export: false, max_bytes: "1GB" });
});

it("forwards DynamoDB fullExport option", async () => {
let body: unknown;

server.use(
http.post(`${BASE_URL}/v0/datasources/ddb_ds/sample`, async ({ request }) => {
body = await request.json();
return HttpResponse.json({ job_id: "job-1" });
})
);

const api = createTinybirdApi({
baseUrl: BASE_URL,
token: "p.default-token",
});

await api.sampleDatasource("ddb_ds", { fullExport: true });

expect(body).toEqual({ max_files: 1, full_export: true });
});

it("throws when rows and maxBytes are both provided", async () => {
const api = createTinybirdApi({
baseUrl: BASE_URL,
token: "p.default-token",
});

await expect(
api.sampleDatasource("ddb_ds", { rows: 10, maxBytes: "1GB" })
).rejects.toThrow("'rows' and 'maxBytes' are mutually exclusive; pass only one");
});

it("allows per-request token override", async () => {
let authorizationHeader: string | null = null;

server.use(
http.post(`${BASE_URL}/v0/datasources/events/sample`, ({ request }) => {
authorizationHeader = request.headers.get("Authorization");
return HttpResponse.json({ job_id: "job-1" });
})
);

const api = createTinybirdApi({
baseUrl: BASE_URL,
token: "p.default-token",
});

await api.sampleDatasource("events", {}, { token: "p.override-token" });

expect(authorizationHeader).toBe("Bearer p.override-token");
});
});
});
66 changes: 66 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
IngestResult,
QueryOptions,
QueryResult,
SampleOptions,
SampleResult,
TruncateOptions,
TruncateResult,
TinybirdErrorResponse,
Expand Down Expand Up @@ -86,6 +88,15 @@ export interface TinybirdApiTruncateOptions extends TruncateOptions {
token?: string;
}

export interface TinybirdApiSampleOptions {
/** Optional token override for this request */
token?: string;
/** Request timeout in milliseconds */
timeout?: number;
/** AbortController signal for cancellation */
signal?: AbortSignal;
}

/**
* Scope definition for token creation APIs
*/
Expand Down Expand Up @@ -505,6 +516,61 @@ export class TinybirdApi {
return this.parseOptionalJson(response);
}

/**
* Start a bounded sample import job for an S3/GCS/DynamoDB connected datasource.
*
* For blob storage (S3/GCS) connectors, `maxFiles` bounds how many files are
* imported (default 1, max 10). For DynamoDB, the sample is bounded by either
* `rows` or `maxBytes` (mutually exclusive), or `fullExport` triggers a full
* PITR export of the whole table instead of a bounded scan.
*/
async sampleDatasource(
datasourceName: string,
options: SampleOptions = {},
apiOptions: TinybirdApiSampleOptions = {}
): Promise<SampleResult> {
if (options.rows !== undefined && options.maxBytes !== undefined) {
throw new Error("'rows' and 'maxBytes' are mutually exclusive; pass only one");
}

const url = new URL(
`/v0/datasources/${encodeURIComponent(datasourceName)}/sample`,
`${this.baseUrl}/`
);

const payload: Record<string, unknown> = {
max_files: options.maxFiles ?? 1,
full_export: options.fullExport ?? false,
};

if (options.rows !== undefined) {
payload.rows = options.rows;
}

if (options.maxBytes !== undefined) {
payload.max_bytes = options.maxBytes;
}

const response = await this.request(url.toString(), {
method: "POST",
token: apiOptions.token,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
signal: this.createAbortSignal(
options.timeout ?? apiOptions.timeout,
options.signal ?? apiOptions.signal
),
});

if (!response.ok) {
await this.handleErrorResponse(response);
}

return (await response.json()) as SampleResult;
}

/**
* Create a token using Tinybird Token API.
* Supports both static and JWT token payloads.
Expand Down
4 changes: 3 additions & 1 deletion src/client/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ describe("TinybirdClient", () => {
expect(client.datasources).toBeDefined();
});

it("datasources namespace has ingest/append/replace/delete/truncate methods", () => {
it("datasources namespace has ingest/append/replace/delete/truncate/sample methods", () => {
const client = createClient({
baseUrl: "https://api.tinybird.co",
token: "test-token",
Expand All @@ -246,6 +246,7 @@ describe("TinybirdClient", () => {
expect(typeof client.datasources.replace).toBe("function");
expect(typeof client.datasources.delete).toBe("function");
expect(typeof client.datasources.truncate).toBe("function");
expect(typeof client.datasources.sample).toBe("function");
});

it("datasources conforms to DatasourcesNamespace interface", () => {
Expand All @@ -261,6 +262,7 @@ describe("TinybirdClient", () => {
expect(typeof datasources.replace).toBe("function");
expect(typeof datasources.delete).toBe("function");
expect(typeof datasources.truncate).toBe("function");
expect(typeof datasources.sample).toBe("function");
});
});
});
46 changes: 46 additions & 0 deletions src/client/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import type {
IngestResult,
QueryOptions,
IngestOptions,
SampleOptions,
SampleResult,
TruncateOptions,
TruncateResult,
} from "./types.js";
Expand Down Expand Up @@ -113,6 +115,12 @@ export class TinybirdClient {
): Promise<TruncateResult> => {
return this.truncateDatasource(datasourceName, options);
},
sample: (
datasourceName: string,
options: SampleOptions = {}
): Promise<SampleResult> => {
return this.sampleDatasource(datasourceName, options);
},
};

// Initialize tokens namespace
Expand Down Expand Up @@ -219,6 +227,44 @@ export class TinybirdClient {
}
}

/**
* Start a bounded sample import job for an S3/GCS/DynamoDB connected datasource
*
* For blob storage (S3/GCS) connectors, `maxFiles` bounds how many files are
* imported (default 1, max 10). For DynamoDB, the sample is bounded by either
* `rows` or `maxBytes` (mutually exclusive — passing both throws), or
* `fullExport` triggers a full PITR export of the whole table instead of a
* bounded scan.
*
* @param datasourceName - Name of the datasource
* @param options - Sample options (maxFiles, rows, maxBytes, fullExport)
* @returns Sample job result
*
* @example
* ```ts
* // Sample up to 3 files from an S3/GCS connected datasource
* await client.datasources.sample('s3_events', { maxFiles: 3 });
*
* // Bound a DynamoDB sample by row count
* await client.datasources.sample('ddb_events', { rows: 100_000 });
*
* // Trigger a full DynamoDB PITR export
* await client.datasources.sample('ddb_events', { fullExport: true });
* ```
*/
private async sampleDatasource(
datasourceName: string,
options: SampleOptions = {}
): Promise<SampleResult> {
const token = await this.getToken();

try {
return await this.getApi(token).sampleDatasource(datasourceName, options);
} catch (error) {
this.rethrowApiError(error);
}
}

/**
* Ingest a single event to a datasource
*
Expand Down
50 changes: 50 additions & 0 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,54 @@ export interface TruncateResult {
status?: string;
}

/**
* Options for starting a bounded sample import job on an S3/GCS/DynamoDB
* connected datasource.
*
* `rows` and `maxBytes` are mutually exclusive (DynamoDB only) and only one
* of them may be provided.
*/
export interface SampleOptions {
/**
* Maximum number of files to import for blob storage (S3/GCS) connectors.
* Defaults to 1 (max 10).
*/
maxFiles?: number;
/**
* DynamoDB only: maximum number of rows to scan and import.
* Mutually exclusive with `maxBytes`.
*/
rows?: number;
/**
* DynamoDB only: maximum approximate JSONEachRow bytes to import,
* e.g. `"500MB"`. Mutually exclusive with `rows`.
*/
maxBytes?: string;
/**
* DynamoDB only: trigger a full PITR export of the whole table instead of
* a bounded scan. Defaults to false.
*/
fullExport?: boolean;
/** Request timeout in milliseconds */
timeout?: number;
/** AbortController signal for cancellation */
signal?: AbortSignal;
}

/**
* Result of starting a datasource sample job
*/
export interface SampleResult {
/** Sample job ID */
id?: string;
/** Same value as id */
job_id?: string;
/** Job status URL */
job_url?: string;
/** Job status */
status?: string;
}

/**
* Datasources namespace interface for raw client
*/
Expand All @@ -309,4 +357,6 @@ export interface DatasourcesNamespace {
delete(datasourceName: string, options: DeleteOptions): Promise<DeleteResult>;
/** Truncate all rows from a datasource */
truncate(datasourceName: string, options?: TruncateOptions): Promise<TruncateResult>;
/** Start a bounded sample import job for an S3/GCS/DynamoDB connected datasource */
sample(datasourceName: string, options?: SampleOptions): Promise<SampleResult>;
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ export type {
IngestResult,
QueryOptions,
IngestOptions,
SampleOptions,
SampleResult,
TruncateOptions,
TruncateResult,
ColumnMeta,
Expand All @@ -280,6 +282,7 @@ export type {
TinybirdApiAppendOptions,
TinybirdApiDeleteOptions,
TinybirdApiTruncateOptions,
TinybirdApiSampleOptions,
TinybirdApiRequestInit,
TinybirdApiTokenScope,
TinybirdApiCreateTokenRequest,
Expand Down
Loading