Skip to content
Merged
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ export const tinybird = new Tinybird({
pipes: { topPages },
});

// Optional: customize fetch for framework-specific cache or runtime behavior
export const tinybirdNoStore = new Tinybird({
datasources: { pageViews },
pipes: { topPages },
fetch: (url, init) => fetch(url, { ...init, cache: "no-store" }),
});

// Re-export types for convenience
export type { PageViewsRow, TopPagesParams, TopPagesOutput };
export { pageViews, topPages };
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tinybirdco/sdk",
"version": "0.0.64",
"version": "0.0.65",
"description": "TypeScript SDK for Tinybird Forward - define datasources and pipes as TypeScript",
"type": "module",
"main": "./dist/index.js",
Expand Down
39 changes: 39 additions & 0 deletions src/api/branches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,45 @@ describe("Branch API client", () => {
BranchApiError
);
});

it("uses custom fetch when provided", async () => {
const customFetch = vi
.fn()
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
job: { id: "job-123", status: "waiting" },
workspace: { id: "ws-123" },
}),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ id: "job-123", status: "done" }),
})
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: "branch-123",
name: "my-feature",
token: "p.branch-token",
created_at: "2024-01-01T00:00:00Z",
}),
});

const result = await createBranch(
{
...config,
fetch: customFetch as typeof fetch,
},
"my-feature"
);

expect(customFetch).toHaveBeenCalledTimes(3);
expect(mockFetch).not.toHaveBeenCalled();
expect(result.token).toBe("p.branch-token");
});
});

describe("listBranches", () => {
Expand Down
24 changes: 18 additions & 6 deletions src/api/branches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Uses the /v1/environments endpoints (Forward API)
*/

import { tinybirdFetch } from "./fetcher.js";
import { createTinybirdFetcher } from "./fetcher.js";

/**
* Branch information from Tinybird API
Expand Down Expand Up @@ -35,6 +35,12 @@ export interface BranchApiConfig {
baseUrl: string;
/** Parent workspace token (used to create/manage branches) */
token: string;
/** Custom fetch implementation (optional) */
fetch?: typeof fetch;
}

function getFetch(config: BranchApiConfig) {
return createTinybirdFetcher(config.fetch ?? globalThis.fetch);
}

/**
Expand Down Expand Up @@ -89,10 +95,12 @@ async function pollJob(
maxAttempts = 120,
intervalMs = 1000
): Promise<JobStatusResponse> {
const fetchFn = getFetch(config);

for (let attempt = 0; attempt < maxAttempts; attempt++) {
const url = new URL(`/v0/jobs/${jobId}`, config.baseUrl);

const response = await tinybirdFetch(url.toString(), {
const response = await fetchFn(url.toString(), {
method: "GET",
headers: {
Authorization: `Bearer ${config.token}`,
Expand Down Expand Up @@ -153,6 +161,7 @@ export async function createBranch(
name: string,
options?: CreateBranchOptions
): Promise<TinybirdBranch> {
const fetchFn = getFetch(config);
const url = new URL("/v1/environments", config.baseUrl);
url.searchParams.set("name", name);
if (options?.lastPartition) {
Expand All @@ -164,7 +173,7 @@ export async function createBranch(
console.log(`[debug] POST ${url.toString()}`);
}

const response = await tinybirdFetch(url.toString(), {
const response = await fetchFn(url.toString(), {
method: "POST",
headers: {
Authorization: `Bearer ${config.token}`,
Expand Down Expand Up @@ -219,9 +228,10 @@ export async function createBranch(
export async function listBranches(
config: BranchApiConfig
): Promise<TinybirdBranch[]> {
const fetchFn = getFetch(config);
const url = new URL("/v1/environments", config.baseUrl);

const response = await tinybirdFetch(url.toString(), {
const response = await fetchFn(url.toString(), {
method: "GET",
headers: {
Authorization: `Bearer ${config.token}`,
Expand Down Expand Up @@ -253,10 +263,11 @@ export async function getBranch(
config: BranchApiConfig,
name: string
): Promise<TinybirdBranch> {
const fetchFn = getFetch(config);
const url = new URL(`/v0/environments/${encodeURIComponent(name)}`, config.baseUrl);
url.searchParams.set("with_token", "true");

const response = await tinybirdFetch(url.toString(), {
const response = await fetchFn(url.toString(), {
method: "GET",
headers: {
Authorization: `Bearer ${config.token}`,
Expand Down Expand Up @@ -292,10 +303,11 @@ export async function deleteBranch(
): Promise<void> {
// First get the branch to find its ID
const branch = await getBranch(config, name);
const fetchFn = getFetch(config);

const url = new URL(`/v0/environments/${branch.id}`, config.baseUrl);

const response = await tinybirdFetch(url.toString(), {
const response = await fetchFn(url.toString(), {
method: "DELETE",
headers: {
Authorization: `Bearer ${config.token}`,
Expand Down
86 changes: 85 additions & 1 deletion src/client/base.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,46 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { TinybirdClient, createClient } from "./base.js";
import type { DatasourcesNamespace } from "./types.js";
import { loadConfigAsync } from "../cli/config.js";
import { getOrCreateBranch } from "../api/branches.js";

vi.mock("../cli/config.js", () => ({
loadConfigAsync: vi.fn(),
}));

vi.mock("../api/branches.js", () => ({
getOrCreateBranch: vi.fn(),
}));

describe("TinybirdClient", () => {
const originalEnv = { ...process.env };
const mockedLoadConfigAsync = vi.mocked(loadConfigAsync);
const mockedGetOrCreateBranch = vi.mocked(getOrCreateBranch);

beforeEach(() => {
process.env = { ...originalEnv };
delete process.env.VERCEL_ENV;
delete process.env.GITHUB_HEAD_REF;
delete process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
delete process.env.CI;
delete process.env.TINYBIRD_PREVIEW_MODE;
delete process.env.VERCEL_GIT_COMMIT_REF;
delete process.env.GITHUB_REF_NAME;
delete process.env.CI_COMMIT_BRANCH;
delete process.env.CIRCLE_BRANCH;
delete process.env.BUILD_SOURCEBRANCHNAME;
delete process.env.BITBUCKET_BRANCH;
delete process.env.TINYBIRD_BRANCH_NAME;
delete process.env.TINYBIRD_BRANCH_TOKEN;
mockedLoadConfigAsync.mockReset();
mockedGetOrCreateBranch.mockReset();
});

afterEach(() => {
process.env = { ...originalEnv };
vi.restoreAllMocks();
});

describe("constructor", () => {
it("throws error when baseUrl is missing", () => {
expect(() => new TinybirdClient({ baseUrl: "", token: "test-token" })).toThrow(
Expand Down Expand Up @@ -115,6 +153,52 @@ describe("TinybirdClient", () => {
expect(context.baseUrl).toBe("https://api.us-east.tinybird.co");
expect(context.token).toBe("us-token");
});

it("passes custom fetch to devMode branch resolution", async () => {
const customFetch = vi.fn();

mockedLoadConfigAsync.mockResolvedValue({
include: [],
token: "workspace-token",
baseUrl: "https://api.tinybird.co",
configPath: "/tmp/tinybird.config.json",
cwd: "/tmp",
gitBranch: "feature/add-fetch",
tinybirdBranch: "feature_add_fetch",
isMainBranch: false,
devMode: "branch",
});
mockedGetOrCreateBranch.mockResolvedValue({
id: "branch-123",
name: "feature_add_fetch",
token: "branch-token",
created_at: "2024-01-01T00:00:00Z",
wasCreated: false,
});

const client = new TinybirdClient({
baseUrl: "https://api.tinybird.co",
token: "workspace-token",
fetch: customFetch as typeof fetch,
devMode: true,
});

const context = await client.getContext();

expect(mockedLoadConfigAsync).toHaveBeenCalled();
expect(mockedGetOrCreateBranch).toHaveBeenCalledWith(
{
baseUrl: "https://api.tinybird.co",
token: "workspace-token",
fetch: customFetch,
},
"feature_add_fetch"
);
expect(context.token).toBe("branch-token");
expect(context.isBranchToken).toBe(true);
expect(context.branchName).toBe("feature_add_fetch");
expect(context.gitBranch).toBe("feature/add-fetch");
});
});

describe("createClient", () => {
Expand Down
6 changes: 5 additions & 1 deletion src/client/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,11 @@ export class TinybirdClient {

// Get or create branch (always fetch fresh to avoid stale cache issues)
const branch = await getOrCreateBranch(
{ baseUrl: this.config.baseUrl, token: this.config.token },
{
baseUrl: this.config.baseUrl,
token: this.config.token,
fetch: this.config.fetch,
},
branchName
);

Expand Down
44 changes: 44 additions & 0 deletions src/client/preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,50 @@ describe("Preview environment detection", () => {
const token = await resolveToken({ token: "my-token" });
expect(token).toBe("my-token");
});

it("uses custom fetch for preview branch token resolution", async () => {
process.env.VERCEL_ENV = "preview";
process.env.VERCEL_GIT_COMMIT_REF = "feature/add-fetch";

const customFetch = vi.fn().mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
id: "branch-123",
name: "tmp_ci_feature_add_fetch",
token: "branch-token",
created_at: "2024-01-01T00:00:00Z",
}),
});
const originalFetch = global.fetch;
const globalFetch = vi.fn().mockRejectedValue(
new Error("global fetch should not be called")
);
global.fetch = globalFetch as typeof fetch;

try {
const token = await resolveToken({
baseUrl: "https://api.tinybird.co",
token: "workspace-token",
fetch: customFetch as typeof fetch,
});

expect(token).toBe("branch-token");
expect(customFetch).toHaveBeenCalledTimes(1);
expect(globalFetch).not.toHaveBeenCalled();

const [url, init] = customFetch.mock.calls[0] as [string, RequestInit];
const parsed = new URL(url);
expect(parsed.pathname).toBe("/v0/environments/tmp_ci_feature_add_fetch");
expect(parsed.searchParams.get("with_token")).toBe("true");
expect(parsed.searchParams.get("from")).toBe("ts-sdk");
expect(new Headers(init.headers).get("Authorization")).toBe(
"Bearer workspace-token"
);
} finally {
global.fetch = originalFetch;
}
});
});

describe("clearTokenCache", () => {
Expand Down
14 changes: 11 additions & 3 deletions src/client/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Tinybird branch token for the current git branch.
*/

import { tinybirdFetch } from "../api/fetcher.js";
import { createTinybirdFetcher } from "../api/fetcher.js";

/**
* Branch information with token
Expand Down Expand Up @@ -110,13 +110,15 @@ function sanitizeBranchName(branchName: string): string {
async function fetchBranchToken(
baseUrl: string,
workspaceToken: string,
branchName: string
branchName: string,
fetchFn?: typeof fetch
): Promise<string | null> {
const sanitizedName = sanitizeBranchName(branchName);
// Look for the preview branch with tmp_ci_ prefix (matches what tinybird preview creates)
const previewBranchName = `tmp_ci_${sanitizedName}`;
const url = new URL(`/v0/environments/${encodeURIComponent(previewBranchName)}`, baseUrl);
url.searchParams.set("with_token", "true");
const tinybirdFetch = createTinybirdFetcher(fetchFn ?? globalThis.fetch);

try {
const response = await tinybirdFetch(url.toString(), {
Expand Down Expand Up @@ -153,6 +155,7 @@ async function fetchBranchToken(
export async function resolveToken(options?: {
baseUrl?: string;
token?: string;
fetch?: typeof fetch;
}): Promise<string> {
// 1. Check for explicit branch token override
if (process.env.TINYBIRD_BRANCH_TOKEN) {
Expand Down Expand Up @@ -181,7 +184,12 @@ export async function resolveToken(options?: {
const baseUrl = options?.baseUrl ?? process.env.TINYBIRD_URL ?? "https://api.tinybird.co";

// Fetch branch token
const branchToken = await fetchBranchToken(baseUrl, configuredToken, branchName);
const branchToken = await fetchBranchToken(
baseUrl,
configuredToken,
branchName,
options?.fetch
);

if (branchToken) {
// Cache for subsequent calls
Expand Down
Loading
Loading