@tinybirdco/sdk 0.0.37 → 0.0.39
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/README.md +94 -1
- package/dist/api/api.d.ts +67 -1
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +79 -0
- package/dist/api/api.js.map +1 -1
- package/dist/api/api.test.js +143 -0
- package/dist/api/api.test.js.map +1 -1
- package/dist/api/tokens.d.ts +79 -0
- package/dist/api/tokens.d.ts.map +1 -0
- package/dist/api/tokens.js +80 -0
- package/dist/api/tokens.js.map +1 -0
- package/dist/api/tokens.test.d.ts +2 -0
- package/dist/api/tokens.test.d.ts.map +1 -0
- package/dist/api/tokens.test.js +209 -0
- package/dist/api/tokens.test.js.map +1 -0
- package/dist/client/base.d.ts +19 -0
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +43 -0
- package/dist/client/base.js.map +1 -1
- package/dist/client/base.test.js +4 -0
- package/dist/client/base.test.js.map +1 -1
- package/dist/client/tokens.d.ts +42 -0
- package/dist/client/tokens.d.ts.map +1 -0
- package/dist/client/tokens.js +67 -0
- package/dist/client/tokens.js.map +1 -0
- package/dist/client/tokens.test.d.ts +2 -0
- package/dist/client/tokens.test.d.ts.map +1 -0
- package/dist/client/tokens.test.js +79 -0
- package/dist/client/tokens.test.js.map +1 -0
- package/dist/client/types.d.ts +50 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/schema/project.d.ts +27 -14
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/schema/project.js +63 -13
- package/dist/schema/project.js.map +1 -1
- package/dist/schema/project.test.js +25 -12
- package/dist/schema/project.test.js.map +1 -1
- package/package.json +1 -1
- package/src/api/api.test.ts +208 -0
- package/src/api/api.ts +183 -0
- package/src/api/tokens.test.ts +253 -0
- package/src/api/tokens.ts +169 -0
- package/src/client/base.test.ts +4 -0
- package/src/client/base.ts +65 -0
- package/src/client/tokens.test.ts +103 -0
- package/src/client/tokens.ts +69 -0
- package/src/client/types.ts +54 -0
- package/src/index.ts +21 -0
- package/src/schema/project.test.ts +25 -12
- package/src/schema/project.ts +113 -29
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createJWT,
|
|
4
|
+
TokenApiError,
|
|
5
|
+
type TokenApiConfig,
|
|
6
|
+
} from "./tokens.js";
|
|
7
|
+
|
|
8
|
+
// Mock fetch globally
|
|
9
|
+
const mockFetch = vi.fn();
|
|
10
|
+
global.fetch = mockFetch;
|
|
11
|
+
|
|
12
|
+
function expectFromParam(url: string) {
|
|
13
|
+
const parsed = new URL(url);
|
|
14
|
+
expect(parsed.searchParams.get("from")).toBe("ts-sdk");
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("Token API client", () => {
|
|
19
|
+
const config: TokenApiConfig = {
|
|
20
|
+
baseUrl: "https://api.tinybird.co",
|
|
21
|
+
token: "p.admin-token",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockFetch.mockReset();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("createJWT", () => {
|
|
29
|
+
it("creates a JWT token with scopes", async () => {
|
|
30
|
+
const mockResponse = { token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test" };
|
|
31
|
+
|
|
32
|
+
mockFetch.mockResolvedValueOnce({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: () => Promise.resolve(mockResponse),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = await createJWT(config, {
|
|
38
|
+
name: "user_token",
|
|
39
|
+
expiresAt: 1700000000,
|
|
40
|
+
scopes: [
|
|
41
|
+
{
|
|
42
|
+
type: "PIPES:READ",
|
|
43
|
+
resource: "analytics_pipe",
|
|
44
|
+
fixed_params: { user_id: 123 },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(result.token).toBe(mockResponse.token);
|
|
50
|
+
|
|
51
|
+
const [url, init] = mockFetch.mock.calls[0];
|
|
52
|
+
const parsed = expectFromParam(url);
|
|
53
|
+
expect(parsed.pathname).toBe("/v0/tokens/");
|
|
54
|
+
expect(parsed.searchParams.get("expiration_time")).toBe("1700000000");
|
|
55
|
+
expect(init.method).toBe("POST");
|
|
56
|
+
const headers = new Headers(init.headers);
|
|
57
|
+
expect(headers.get("Authorization")).toBe("Bearer p.admin-token");
|
|
58
|
+
expect(headers.get("Content-Type")).toBe("application/json");
|
|
59
|
+
|
|
60
|
+
const body = JSON.parse(init.body);
|
|
61
|
+
expect(body.name).toBe("user_token");
|
|
62
|
+
expect(body.scopes).toHaveLength(1);
|
|
63
|
+
expect(body.scopes[0].type).toBe("PIPES:READ");
|
|
64
|
+
expect(body.scopes[0].resource).toBe("analytics_pipe");
|
|
65
|
+
expect(body.scopes[0].fixed_params).toEqual({ user_id: 123 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("converts Date to Unix timestamp", async () => {
|
|
69
|
+
mockFetch.mockResolvedValueOnce({
|
|
70
|
+
ok: true,
|
|
71
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const expirationDate = new Date("2024-01-01T00:00:00Z");
|
|
75
|
+
await createJWT(config, {
|
|
76
|
+
name: "test",
|
|
77
|
+
expiresAt: expirationDate,
|
|
78
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const [url] = mockFetch.mock.calls[0];
|
|
82
|
+
const parsed = new URL(url);
|
|
83
|
+
expect(parsed.searchParams.get("expiration_time")).toBe("1704067200");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("converts ISO string to Unix timestamp", async () => {
|
|
87
|
+
mockFetch.mockResolvedValueOnce({
|
|
88
|
+
ok: true,
|
|
89
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await createJWT(config, {
|
|
93
|
+
name: "test",
|
|
94
|
+
expiresAt: "2024-01-01T00:00:00Z",
|
|
95
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const [url] = mockFetch.mock.calls[0];
|
|
99
|
+
const parsed = new URL(url);
|
|
100
|
+
expect(parsed.searchParams.get("expiration_time")).toBe("1704067200");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("includes limits when provided", async () => {
|
|
104
|
+
mockFetch.mockResolvedValueOnce({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await createJWT(config, {
|
|
110
|
+
name: "rate_limited_token",
|
|
111
|
+
expiresAt: 1700000000,
|
|
112
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
113
|
+
limits: { rps: 100 },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
117
|
+
const body = JSON.parse(init.body);
|
|
118
|
+
expect(body.limits).toEqual({ rps: 100 });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("uses custom fetch implementation when provided", async () => {
|
|
122
|
+
const customFetch = vi.fn().mockResolvedValueOnce({
|
|
123
|
+
ok: true,
|
|
124
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await createJWT(
|
|
128
|
+
{
|
|
129
|
+
...config,
|
|
130
|
+
fetch: customFetch as typeof fetch,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "custom_fetch_token",
|
|
134
|
+
expiresAt: 1700000000,
|
|
135
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(customFetch).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("supports datasource scope with filter", async () => {
|
|
144
|
+
mockFetch.mockResolvedValueOnce({
|
|
145
|
+
ok: true,
|
|
146
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await createJWT(config, {
|
|
150
|
+
name: "filtered_token",
|
|
151
|
+
expiresAt: 1700000000,
|
|
152
|
+
scopes: [
|
|
153
|
+
{
|
|
154
|
+
type: "DATASOURCES:READ",
|
|
155
|
+
resource: "events",
|
|
156
|
+
filter: "org_id = 'acme'",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
162
|
+
const body = JSON.parse(init.body);
|
|
163
|
+
expect(body.scopes[0].filter).toBe("org_id = 'acme'");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("throws TokenApiError on 403 with helpful message", async () => {
|
|
167
|
+
mockFetch.mockResolvedValueOnce({
|
|
168
|
+
ok: false,
|
|
169
|
+
status: 403,
|
|
170
|
+
statusText: "Forbidden",
|
|
171
|
+
text: () => Promise.resolve("Insufficient permissions"),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await expect(
|
|
175
|
+
createJWT(config, {
|
|
176
|
+
name: "test",
|
|
177
|
+
expiresAt: 1700000000,
|
|
178
|
+
scopes: [],
|
|
179
|
+
})
|
|
180
|
+
).rejects.toThrow(TokenApiError);
|
|
181
|
+
|
|
182
|
+
mockFetch.mockResolvedValueOnce({
|
|
183
|
+
ok: false,
|
|
184
|
+
status: 403,
|
|
185
|
+
statusText: "Forbidden",
|
|
186
|
+
text: () => Promise.resolve("Insufficient permissions"),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await createJWT(config, {
|
|
191
|
+
name: "test",
|
|
192
|
+
expiresAt: 1700000000,
|
|
193
|
+
scopes: [],
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
expect((error as TokenApiError).status).toBe(403);
|
|
197
|
+
expect((error as TokenApiError).message).toContain("TOKENS or ADMIN scope");
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("throws TokenApiError on 400 with validation error", async () => {
|
|
202
|
+
mockFetch.mockResolvedValueOnce({
|
|
203
|
+
ok: false,
|
|
204
|
+
status: 400,
|
|
205
|
+
statusText: "Bad Request",
|
|
206
|
+
text: () => Promise.resolve('{"error": "Invalid scope type"}'),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await expect(
|
|
210
|
+
createJWT(config, {
|
|
211
|
+
name: "test",
|
|
212
|
+
expiresAt: 1700000000,
|
|
213
|
+
scopes: [],
|
|
214
|
+
})
|
|
215
|
+
).rejects.toThrow(TokenApiError);
|
|
216
|
+
|
|
217
|
+
mockFetch.mockResolvedValueOnce({
|
|
218
|
+
ok: false,
|
|
219
|
+
status: 400,
|
|
220
|
+
statusText: "Bad Request",
|
|
221
|
+
text: () => Promise.resolve('{"error": "Invalid scope type"}'),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
await createJWT(config, {
|
|
226
|
+
name: "test",
|
|
227
|
+
expiresAt: 1700000000,
|
|
228
|
+
scopes: [],
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
expect((error as TokenApiError).status).toBe(400);
|
|
232
|
+
expect((error as TokenApiError).message).toContain("Invalid scope type");
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("does not include limits if not provided", async () => {
|
|
237
|
+
mockFetch.mockResolvedValueOnce({
|
|
238
|
+
ok: true,
|
|
239
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await createJWT(config, {
|
|
243
|
+
name: "simple_token",
|
|
244
|
+
expiresAt: 1700000000,
|
|
245
|
+
scopes: [{ type: "PIPES:READ", resource: "pipe" }],
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
249
|
+
const body = JSON.parse(init.body);
|
|
250
|
+
expect(body.limits).toBeUndefined();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tinybird Token API client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createTinybirdApi,
|
|
7
|
+
TinybirdApiError,
|
|
8
|
+
} from "./api.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* API configuration for token operations.
|
|
12
|
+
* Requires a token with TOKENS or ADMIN scope.
|
|
13
|
+
*/
|
|
14
|
+
export interface TokenApiConfig {
|
|
15
|
+
/** Tinybird API base URL */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
/** Workspace token with TOKENS or ADMIN scope */
|
|
18
|
+
token: string;
|
|
19
|
+
/** Custom fetch implementation (optional, defaults to global fetch) */
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
/** Default timeout in milliseconds (optional) */
|
|
22
|
+
timeout?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Error thrown by token API operations
|
|
27
|
+
*/
|
|
28
|
+
export class TokenApiError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
message: string,
|
|
31
|
+
public readonly status: number,
|
|
32
|
+
public readonly body?: unknown
|
|
33
|
+
) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "TokenApiError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Scope type for JWT tokens
|
|
41
|
+
*/
|
|
42
|
+
export type JWTScopeType =
|
|
43
|
+
| "PIPES:READ"
|
|
44
|
+
| "DATASOURCES:READ"
|
|
45
|
+
| "DATASOURCES:APPEND";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A scope definition for JWT tokens
|
|
49
|
+
*/
|
|
50
|
+
export interface JWTScope {
|
|
51
|
+
/** The type of access being granted */
|
|
52
|
+
type: JWTScopeType;
|
|
53
|
+
/** The resource name (pipe or datasource) */
|
|
54
|
+
resource: string;
|
|
55
|
+
/** Fixed parameters embedded in the JWT (for pipes) */
|
|
56
|
+
fixed_params?: Record<string, string | number | boolean>;
|
|
57
|
+
/** SQL filter expression (for datasources) */
|
|
58
|
+
filter?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Rate limiting configuration for JWT tokens
|
|
63
|
+
*/
|
|
64
|
+
export interface JWTLimits {
|
|
65
|
+
/** Requests per second limit */
|
|
66
|
+
rps?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Options for creating a JWT token
|
|
71
|
+
*/
|
|
72
|
+
export interface CreateJWTOptions {
|
|
73
|
+
/** Token name/identifier */
|
|
74
|
+
name: string;
|
|
75
|
+
/** Expiration time as Date, Unix timestamp (number), or ISO string */
|
|
76
|
+
expiresAt: Date | number | string;
|
|
77
|
+
/** Array of scopes defining access permissions */
|
|
78
|
+
scopes: JWTScope[];
|
|
79
|
+
/** Optional rate limiting configuration */
|
|
80
|
+
limits?: JWTLimits;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Result of creating a JWT token
|
|
85
|
+
*/
|
|
86
|
+
export interface CreateJWTResult {
|
|
87
|
+
/** The generated JWT token string */
|
|
88
|
+
token: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Request body for creating a JWT token
|
|
93
|
+
*/
|
|
94
|
+
interface CreateJWTRequestBody {
|
|
95
|
+
name: string;
|
|
96
|
+
scopes: JWTScope[];
|
|
97
|
+
limits?: JWTLimits;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Convert expiration input to Unix timestamp
|
|
102
|
+
*/
|
|
103
|
+
function toUnixTimestamp(expiresAt: Date | number | string): number {
|
|
104
|
+
if (typeof expiresAt === "number") {
|
|
105
|
+
return expiresAt;
|
|
106
|
+
}
|
|
107
|
+
if (expiresAt instanceof Date) {
|
|
108
|
+
return Math.floor(expiresAt.getTime() / 1000);
|
|
109
|
+
}
|
|
110
|
+
return Math.floor(new Date(expiresAt).getTime() / 1000);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a JWT token
|
|
115
|
+
* POST /v0/tokens/?expiration_time={unix_timestamp}
|
|
116
|
+
*
|
|
117
|
+
* @param config - API configuration (requires TOKENS or ADMIN scope)
|
|
118
|
+
* @param options - JWT creation options
|
|
119
|
+
* @returns The created JWT token
|
|
120
|
+
*/
|
|
121
|
+
export async function createJWT(
|
|
122
|
+
config: TokenApiConfig,
|
|
123
|
+
options: CreateJWTOptions
|
|
124
|
+
): Promise<CreateJWTResult> {
|
|
125
|
+
const expirationTime = toUnixTimestamp(options.expiresAt);
|
|
126
|
+
|
|
127
|
+
const body: CreateJWTRequestBody = {
|
|
128
|
+
name: options.name,
|
|
129
|
+
scopes: options.scopes,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (options.limits) {
|
|
133
|
+
body.limits = options.limits;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const api = createTinybirdApi({
|
|
137
|
+
baseUrl: config.baseUrl,
|
|
138
|
+
token: config.token,
|
|
139
|
+
fetch: config.fetch,
|
|
140
|
+
timeout: config.timeout,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const result = await api.createToken(body, {
|
|
145
|
+
expirationTime,
|
|
146
|
+
});
|
|
147
|
+
return { token: result.token };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (!(error instanceof TinybirdApiError)) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const responseBody = error.responseBody ?? error.message;
|
|
154
|
+
let message: string;
|
|
155
|
+
|
|
156
|
+
if (error.statusCode === 403) {
|
|
157
|
+
message =
|
|
158
|
+
`Permission denied creating JWT token. ` +
|
|
159
|
+
`Make sure the token has TOKENS or ADMIN scope. ` +
|
|
160
|
+
`API response: ${responseBody}`;
|
|
161
|
+
} else if (error.statusCode === 400) {
|
|
162
|
+
message = `Invalid JWT token request: ${responseBody}`;
|
|
163
|
+
} else {
|
|
164
|
+
message = `Failed to create JWT token: ${error.statusCode}. API response: ${responseBody}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw new TokenApiError(message, error.statusCode, responseBody);
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/client/base.test.ts
CHANGED
|
@@ -156,6 +156,8 @@ describe("TinybirdClient", () => {
|
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
expect(typeof client.datasources.append).toBe("function");
|
|
159
|
+
expect(typeof client.datasources.delete).toBe("function");
|
|
160
|
+
expect(typeof client.datasources.truncate).toBe("function");
|
|
159
161
|
});
|
|
160
162
|
|
|
161
163
|
it("datasources conforms to DatasourcesNamespace interface", () => {
|
|
@@ -167,6 +169,8 @@ describe("TinybirdClient", () => {
|
|
|
167
169
|
const datasources: DatasourcesNamespace = client.datasources;
|
|
168
170
|
expect(datasources).toBeDefined();
|
|
169
171
|
expect(typeof datasources.append).toBe("function");
|
|
172
|
+
expect(typeof datasources.delete).toBe("function");
|
|
173
|
+
expect(typeof datasources.truncate).toBe("function");
|
|
170
174
|
});
|
|
171
175
|
});
|
|
172
176
|
});
|
package/src/client/base.ts
CHANGED
|
@@ -8,13 +8,18 @@ import type {
|
|
|
8
8
|
ClientConfig,
|
|
9
9
|
ClientContext,
|
|
10
10
|
DatasourcesNamespace,
|
|
11
|
+
DeleteOptions,
|
|
12
|
+
DeleteResult,
|
|
11
13
|
QueryResult,
|
|
12
14
|
IngestResult,
|
|
13
15
|
QueryOptions,
|
|
14
16
|
IngestOptions,
|
|
17
|
+
TruncateOptions,
|
|
18
|
+
TruncateResult,
|
|
15
19
|
} from "./types.js";
|
|
16
20
|
import { TinybirdError } from "./types.js";
|
|
17
21
|
import { TinybirdApi, TinybirdApiError } from "../api/api.js";
|
|
22
|
+
import { TokensNamespace } from "./tokens.js";
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* Resolved token info from dev mode
|
|
@@ -65,6 +70,9 @@ export class TinybirdClient {
|
|
|
65
70
|
*/
|
|
66
71
|
readonly datasources: DatasourcesNamespace;
|
|
67
72
|
|
|
73
|
+
/** Token operations (JWT creation, etc.) */
|
|
74
|
+
public readonly tokens: TokensNamespace;
|
|
75
|
+
|
|
68
76
|
constructor(config: ClientConfig) {
|
|
69
77
|
// Validate required config
|
|
70
78
|
if (!config.baseUrl) {
|
|
@@ -85,7 +93,24 @@ export class TinybirdClient {
|
|
|
85
93
|
append: (datasourceName: string, options: AppendOptions): Promise<AppendResult> => {
|
|
86
94
|
return this.appendDatasource(datasourceName, options);
|
|
87
95
|
},
|
|
96
|
+
delete: (datasourceName: string, options: DeleteOptions): Promise<DeleteResult> => {
|
|
97
|
+
return this.deleteDatasource(datasourceName, options);
|
|
98
|
+
},
|
|
99
|
+
truncate: (
|
|
100
|
+
datasourceName: string,
|
|
101
|
+
options: TruncateOptions = {}
|
|
102
|
+
): Promise<TruncateResult> => {
|
|
103
|
+
return this.truncateDatasource(datasourceName, options);
|
|
104
|
+
},
|
|
88
105
|
};
|
|
106
|
+
|
|
107
|
+
// Initialize tokens namespace
|
|
108
|
+
this.tokens = new TokensNamespace(
|
|
109
|
+
() => this.getToken(),
|
|
110
|
+
this.config.baseUrl,
|
|
111
|
+
this.config.fetch,
|
|
112
|
+
this.config.timeout
|
|
113
|
+
);
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
/**
|
|
@@ -121,6 +146,46 @@ export class TinybirdClient {
|
|
|
121
146
|
}
|
|
122
147
|
}
|
|
123
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Delete rows from a datasource using a SQL condition
|
|
151
|
+
*
|
|
152
|
+
* @param datasourceName - Name of the datasource
|
|
153
|
+
* @param options - Delete options including deleteCondition
|
|
154
|
+
* @returns Delete job result
|
|
155
|
+
*/
|
|
156
|
+
private async deleteDatasource(
|
|
157
|
+
datasourceName: string,
|
|
158
|
+
options: DeleteOptions
|
|
159
|
+
): Promise<DeleteResult> {
|
|
160
|
+
const token = await this.getToken();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
return await this.getApi(token).deleteDatasource(datasourceName, options);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
this.rethrowApiError(error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Truncate all rows from a datasource
|
|
171
|
+
*
|
|
172
|
+
* @param datasourceName - Name of the datasource
|
|
173
|
+
* @param options - Truncate options
|
|
174
|
+
* @returns Truncate result
|
|
175
|
+
*/
|
|
176
|
+
private async truncateDatasource(
|
|
177
|
+
datasourceName: string,
|
|
178
|
+
options: TruncateOptions = {}
|
|
179
|
+
): Promise<TruncateResult> {
|
|
180
|
+
const token = await this.getToken();
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
return await this.getApi(token).truncateDatasource(datasourceName, options);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
this.rethrowApiError(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
124
189
|
/**
|
|
125
190
|
* Get the effective token, resolving branch token in dev mode if needed
|
|
126
191
|
*/
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { TinybirdClient } from "./base.js";
|
|
3
|
+
|
|
4
|
+
const mockFetch = vi.fn();
|
|
5
|
+
global.fetch = mockFetch;
|
|
6
|
+
|
|
7
|
+
describe("TokensNamespace", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockFetch.mockReset();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("uses client custom fetch for createJWT", async () => {
|
|
13
|
+
const customFetch = vi.fn().mockResolvedValueOnce({
|
|
14
|
+
ok: true,
|
|
15
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const client = new TinybirdClient({
|
|
19
|
+
baseUrl: "https://api.tinybird.co",
|
|
20
|
+
token: "p.admin-token",
|
|
21
|
+
fetch: customFetch as typeof fetch,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const result = await client.tokens.createJWT({
|
|
25
|
+
name: "user_token",
|
|
26
|
+
expiresAt: 1700000000,
|
|
27
|
+
scopes: [{ type: "PIPES:READ", resource: "analytics_pipe" }],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ token: "jwt-token" });
|
|
31
|
+
expect(customFetch).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
33
|
+
|
|
34
|
+
const [url, init] = customFetch.mock.calls[0] as [string, RequestInit];
|
|
35
|
+
const parsed = new URL(url);
|
|
36
|
+
expect(parsed.pathname).toBe("/v0/tokens/");
|
|
37
|
+
expect(parsed.searchParams.get("expiration_time")).toBe("1700000000");
|
|
38
|
+
expect(parsed.searchParams.get("from")).toBe("ts-sdk");
|
|
39
|
+
expect(init.method).toBe("POST");
|
|
40
|
+
const headers = new Headers(init.headers);
|
|
41
|
+
expect(headers.get("Authorization")).toBe("Bearer p.admin-token");
|
|
42
|
+
expect(headers.get("Content-Type")).toBe("application/json");
|
|
43
|
+
|
|
44
|
+
const body = JSON.parse(String(init.body)) as {
|
|
45
|
+
name: string;
|
|
46
|
+
scopes: Array<{ type: string; resource: string }>;
|
|
47
|
+
limits?: { rps?: number };
|
|
48
|
+
};
|
|
49
|
+
expect(body).toEqual({
|
|
50
|
+
name: "user_token",
|
|
51
|
+
scopes: [{ type: "PIPES:READ", resource: "analytics_pipe" }],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("sends full JWT payload when limits and scope fields are provided", async () => {
|
|
56
|
+
const customFetch = vi.fn().mockResolvedValueOnce({
|
|
57
|
+
ok: true,
|
|
58
|
+
json: () => Promise.resolve({ token: "jwt-token" }),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const client = new TinybirdClient({
|
|
62
|
+
baseUrl: "https://api.tinybird.co",
|
|
63
|
+
token: "p.admin-token",
|
|
64
|
+
fetch: customFetch as typeof fetch,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await client.tokens.createJWT({
|
|
68
|
+
name: "tenant_token",
|
|
69
|
+
expiresAt: 1700000000,
|
|
70
|
+
scopes: [
|
|
71
|
+
{
|
|
72
|
+
type: "PIPES:READ",
|
|
73
|
+
resource: "analytics_pipe",
|
|
74
|
+
fixed_params: { tenant_id: 123 },
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
limits: { rps: 10 },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const [, init] = customFetch.mock.calls[0] as [string, RequestInit];
|
|
81
|
+
const body = JSON.parse(String(init.body)) as {
|
|
82
|
+
name: string;
|
|
83
|
+
scopes: Array<{
|
|
84
|
+
type: string;
|
|
85
|
+
resource: string;
|
|
86
|
+
fixed_params?: Record<string, unknown>;
|
|
87
|
+
}>;
|
|
88
|
+
limits?: { rps?: number };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
expect(body).toEqual({
|
|
92
|
+
name: "tenant_token",
|
|
93
|
+
scopes: [
|
|
94
|
+
{
|
|
95
|
+
type: "PIPES:READ",
|
|
96
|
+
resource: "analytics_pipe",
|
|
97
|
+
fixed_params: { tenant_id: 123 },
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
limits: { rps: 10 },
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token operations namespace for TinybirdClient
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CreateJWTOptions, CreateJWTResult } from "../api/tokens.js";
|
|
6
|
+
import { createJWT as apiCreateJWT, TokenApiError } from "../api/tokens.js";
|
|
7
|
+
import { TinybirdError } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Token operations namespace for TinybirdClient
|
|
11
|
+
*/
|
|
12
|
+
export class TokensNamespace {
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly getToken: () => Promise<string>,
|
|
15
|
+
private readonly baseUrl: string,
|
|
16
|
+
private readonly fetchFn?: typeof globalThis.fetch,
|
|
17
|
+
private readonly timeout?: number
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a JWT token
|
|
22
|
+
*
|
|
23
|
+
* Creates a short-lived JWT token with specific scopes for secure,
|
|
24
|
+
* time-limited access to pipes and datasources.
|
|
25
|
+
*
|
|
26
|
+
* @param options - JWT creation options
|
|
27
|
+
* @returns The created JWT token
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const result = await client.tokens.createJWT({
|
|
32
|
+
* name: "user_123_token",
|
|
33
|
+
* expiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour
|
|
34
|
+
* scopes: [
|
|
35
|
+
* {
|
|
36
|
+
* type: "PIPES:READ",
|
|
37
|
+
* resource: "user_analytics",
|
|
38
|
+
* fixed_params: { user_id: 123 },
|
|
39
|
+
* },
|
|
40
|
+
* ],
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* console.log(result.token); // "eyJ..."
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
async createJWT(options: CreateJWTOptions): Promise<CreateJWTResult> {
|
|
47
|
+
const token = await this.getToken();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
return await apiCreateJWT(
|
|
51
|
+
{
|
|
52
|
+
baseUrl: this.baseUrl,
|
|
53
|
+
token,
|
|
54
|
+
fetch: this.fetchFn,
|
|
55
|
+
timeout: this.timeout,
|
|
56
|
+
},
|
|
57
|
+
options
|
|
58
|
+
);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof TokenApiError) {
|
|
61
|
+
throw new TinybirdError(error.message, error.status, {
|
|
62
|
+
error: error.message,
|
|
63
|
+
status: error.status,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|