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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.83] - 2026-07-20

### Fixed

- Query params that are plain objects (including `p.json()` values) are now JSON-serialized instead of becoming `"[object Object]"` via `String(value)`. Pre-stringified JSON strings and primitive params are unchanged.

## [0.0.82] - 2026-07-15

### Changed
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.82",
"version": "0.0.83",
"description": "TypeScript SDK for Tinybird Forward - define datasources and pipes as TypeScript",
"type": "module",
"main": "./dist/index.js",
Expand Down
63 changes: 63 additions & 0 deletions src/api/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,69 @@ describe("TinybirdApi", () => {
).rejects.toThrow("Date values are not supported for query parameter");
});

it("JSON-stringifies plain object query params (p.json())", async () => {
let configOverridesParam: string | null = null;
let preStringifiedParam: string | null = null;
let objectArrayParams: string[] = [];
let limitParam: string | null = null;
let tagsParams: string[] = [];

server.use(
http.get(`${BASE_URL}/v0/pipes/json_echo.json`, ({ request }) => {
const url = new URL(request.url);
configOverridesParam = url.searchParams.get("configOverrides");
preStringifiedParam = url.searchParams.get("preStringified");
objectArrayParams = url.searchParams.getAll("items");
limitParam = url.searchParams.get("limit");
tagsParams = url.searchParams.getAll("tags");

return HttpResponse.json({
data: [{ key_count: 1 }],
meta: [{ name: "key_count", type: "UInt64" }],
rows: 1,
statistics: {
elapsed: 0.001,
rows_read: 1,
bytes_read: 10,
},
});
})
);

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

await api.query("json_echo", {
configOverrides: { foo: 1 },
preStringified: '{"foo":1}',
items: [{ a: 1 }, { b: 2 }],
limit: 5,
tags: ["a", "b"],
});

expect(configOverridesParam).toBe('{"foo":1}');
expect(configOverridesParam).not.toBe("[object Object]");
expect(preStringifiedParam).toBe('{"foo":1}');
expect(objectArrayParams).toEqual(['{"a":1}', '{"b":2}']);
expect(limitParam).toBe("5");
expect(tagsParams).toEqual(["a", "b"]);
});

it("throws when array query params include Date values", async () => {
const api = createTinybirdApi({
baseUrl: BASE_URL,
token: "p.default-token",
});

await expect(
api.query("top_pages", {
tags: [new Date("2024-01-01T00:00:00.000Z")],
})
).rejects.toThrow("Date values are not supported for query parameter");
});

it("ingests rows via tinybirdApi.ingest", async () => {
let datasourceName: string | null = null;
let waitParam: string | null = null;
Expand Down
37 changes: 22 additions & 15 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ const DEFAULT_TIMEOUT = 30000;
const DEFAULT_INGEST_RETRY_503_BASE_DELAY_MS = 200;
const DEFAULT_INGEST_RETRY_503_MAX_DELAY_MS = 3000;

/**
* Serialize a single query-param value for the Tinybird pipes API.
* Plain objects (e.g. p.json() params) become JSON strings; Dates are rejected;
* everything else uses String(value).
*/
function serializeQueryParamValue(key: string, value: unknown): string {
if (value instanceof Date) {
throw new Error(
`Date values are not supported for query parameter "${key}". ` +
"Pass a string in YYYY-MM-DD HH:MM:SS format (or YYYY-MM-DD HH:MM:SS.sss for DateTime64)."
);
}

if (typeof value === "object" && value !== null && !Array.isArray(value)) {
return JSON.stringify(value);
}

return String(value);
}

/**
* Public, decoupled Tinybird API wrapper configuration
*/
Expand Down Expand Up @@ -214,25 +234,12 @@ export class TinybirdApi {

if (Array.isArray(value)) {
for (const item of value) {
if (item instanceof Date) {
throw new Error(
`Date values are not supported for query parameter "${key}". ` +
"Pass a string in YYYY-MM-DD HH:MM:SS format (or YYYY-MM-DD HH:MM:SS.sss for DateTime64)."
);
}
url.searchParams.append(key, String(item));
url.searchParams.append(key, serializeQueryParamValue(key, item));
}
continue;
}

if (value instanceof Date) {
throw new Error(
`Date values are not supported for query parameter "${key}". ` +
"Pass a string in YYYY-MM-DD HH:MM:SS format (or YYYY-MM-DD HH:MM:SS.sss for DateTime64)."
);
}

url.searchParams.set(key, String(value));
url.searchParams.set(key, serializeQueryParamValue(key, value));
}

const response = await this.request(url.toString(), {
Expand Down
Loading