alchemy 0.15.11 → 0.15.13

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.
@@ -0,0 +1,105 @@
1
+ import type { Secret } from "../secret.js";
2
+
3
+ /**
4
+ * Options for Upstash API requests
5
+ */
6
+ export interface UpstashApiOptions {
7
+ /**
8
+ * API key to use (overrides environment variable)
9
+ */
10
+ apiKey?: Secret;
11
+
12
+ /**
13
+ * Email to use (overrides environment variable)
14
+ */
15
+ email?: string;
16
+ }
17
+
18
+ /**
19
+ * Minimal API client using raw fetch
20
+ */
21
+ export class UpstashApi {
22
+ /** Base URL for API */
23
+ readonly baseUrl: string;
24
+
25
+ /** API key */
26
+ readonly apiKey: string;
27
+
28
+ /** Email */
29
+ readonly email: string;
30
+
31
+ /**
32
+ * Create a new API client
33
+ *
34
+ * @param options API options
35
+ */
36
+ constructor(options: UpstashApiOptions = {}) {
37
+ this.baseUrl = "https://api.upstash.com/v2";
38
+ this.apiKey =
39
+ options.apiKey?.unencrypted ?? process.env.UPSTASH_API_KEY ?? "";
40
+ this.email = options.email ?? process.env.UPSTASH_EMAIL ?? "";
41
+
42
+ if (!this.apiKey) {
43
+ throw new Error("UPSTASH_API_KEY environment variable is required");
44
+ }
45
+
46
+ if (!this.email) {
47
+ throw new Error("UPSTASH_EMAIL environment variable is required");
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Make a request to the API
53
+ *
54
+ * @param path API path (without base URL)
55
+ * @param init Fetch init options
56
+ * @returns Raw Response object from fetch
57
+ */
58
+ async fetch(path: string, init: RequestInit = {}): Promise<Response> {
59
+ const headers: Record<string, string> = {
60
+ "Content-Type": "application/json",
61
+ Authorization: `Basic ${btoa(`${this.email}:${this.apiKey}`)}`,
62
+ };
63
+
64
+ if (init.headers) {
65
+ const initHeaders = init.headers as Record<string, string>;
66
+ Object.keys(initHeaders).forEach((key) => {
67
+ headers[key] = initHeaders[key];
68
+ });
69
+ }
70
+
71
+ return fetch(`${this.baseUrl}${path}`, {
72
+ ...init,
73
+ headers,
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Helper for GET requests
79
+ */
80
+ async get(path: string, init: RequestInit = {}): Promise<Response> {
81
+ return this.fetch(path, { ...init, method: "GET" });
82
+ }
83
+
84
+ /**
85
+ * Helper for POST requests
86
+ */
87
+ async post(
88
+ path: string,
89
+ body: any,
90
+ init: RequestInit = {},
91
+ ): Promise<Response> {
92
+ return this.fetch(path, {
93
+ ...init,
94
+ method: "POST",
95
+ body: JSON.stringify(body),
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Helper for DELETE requests
101
+ */
102
+ async delete(path: string, init: RequestInit = {}): Promise<Response> {
103
+ return this.fetch(path, { ...init, method: "DELETE" });
104
+ }
105
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Custom error class for Upstash API errors
3
+ */
4
+ export class UpstashError extends Error {
5
+ /**
6
+ * HTTP status code
7
+ */
8
+ statusCode: number;
9
+
10
+ /**
11
+ * Original response object
12
+ */
13
+ response: Response;
14
+
15
+ /**
16
+ * Create a new Upstash error
17
+ *
18
+ * @param message Error message
19
+ * @param statusCode HTTP status code
20
+ * @param response Original response object
21
+ */
22
+ constructor(message: string, statusCode: number, response: Response) {
23
+ super(message);
24
+ this.name = "UpstashError";
25
+ this.statusCode = statusCode;
26
+ this.response = response;
27
+ }
28
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./api.js";
2
+ export * from "./error.js";
3
+ export * from "./redis.js";
@@ -0,0 +1,421 @@
1
+ import { alchemy } from "../alchemy.js";
2
+ import type { Context } from "../context.js";
3
+ import { Resource } from "../resource.js";
4
+ import type { Secret } from "../secret.js";
5
+ import { UpstashApi } from "./api.js";
6
+ import { UpstashError } from "./error.js";
7
+
8
+ /**
9
+ * Available regions for Upstash Redis databases
10
+ */
11
+ export type UpstashRegion =
12
+ | "us-east-1"
13
+ | "us-west-1"
14
+ | "us-west-2"
15
+ | "eu-west-1"
16
+ | "eu-central-1"
17
+ | "ap-southeast-1"
18
+ | "ap-southeast-2"
19
+ | "ap-northeast-1"
20
+ | "sa-east-1";
21
+
22
+ /**
23
+ * Properties for creating or updating an UpstashRedis database
24
+ */
25
+ export interface UpstashRedisProps {
26
+ /**
27
+ * Name of the database
28
+ */
29
+ name: string;
30
+
31
+ /**
32
+ * Primary region for the database
33
+ */
34
+ primaryRegion: UpstashRegion;
35
+
36
+ /**
37
+ * Read regions for the database
38
+ */
39
+ readRegions?: UpstashRegion[];
40
+
41
+ /**
42
+ * Monthly budget for the database
43
+ */
44
+ budget?: number;
45
+
46
+ /**
47
+ * Whether to enable eviction for the database
48
+ */
49
+ eviction?: boolean;
50
+
51
+ /**
52
+ * API key to use (overrides environment variable)
53
+ */
54
+ apiKey?: Secret;
55
+
56
+ /**
57
+ * Email to use (overrides environment variable)
58
+ */
59
+ email?: string;
60
+ }
61
+
62
+ /**
63
+ * Output returned after UpstashRedis creation/update
64
+ */
65
+ export interface UpstashRedis
66
+ extends Resource<"upstash::Redis">,
67
+ UpstashRedisProps {
68
+ /**
69
+ * ID of the database
70
+ */
71
+ id: string;
72
+
73
+ /**
74
+ * Type of the database in terms of pricing model
75
+ */
76
+ databaseType: string;
77
+
78
+ /**
79
+ * Region where database is hosted
80
+ */
81
+ region: "global";
82
+
83
+ /**
84
+ * Database port for clients to connect
85
+ */
86
+ port: number;
87
+
88
+ /**
89
+ * Creation time of the database as Unix time
90
+ */
91
+ createdAt: number;
92
+
93
+ /**
94
+ * State of database (active or deleted)
95
+ */
96
+ state: string;
97
+
98
+ /**
99
+ * Password of the database
100
+ */
101
+ password: Secret;
102
+
103
+ /**
104
+ * Email or team id of the owner of the database
105
+ */
106
+ userEmail: string;
107
+
108
+ /**
109
+ * Endpoint URL of the database
110
+ */
111
+ endpoint: string;
112
+
113
+ /**
114
+ * Whether TLS is enabled
115
+ */
116
+ tls: boolean;
117
+
118
+ /**
119
+ * REST token for the database
120
+ */
121
+ restToken: Secret;
122
+
123
+ /**
124
+ * Read-only REST token for the database
125
+ */
126
+ readOnlyRestToken: Secret;
127
+ }
128
+
129
+ /**
130
+ * Create and manage Upstash Redis databases
131
+ *
132
+ * @example
133
+ * // Create a basic Redis database in us-east-1:
134
+ * const redis = await UpstashRedis("my-redis", {
135
+ * name: "my-redis",
136
+ * primaryRegion: "us-east-1"
137
+ * });
138
+ *
139
+ * @example
140
+ * // Create a Redis database with read replicas:
141
+ * const redis = await UpstashRedis("my-redis", {
142
+ * name: "my-redis",
143
+ * primaryRegion: "us-east-1",
144
+ * readRegions: ["us-west-1", "us-west-2"]
145
+ * });
146
+ *
147
+ * @example
148
+ * // Create a Redis database with a monthly budget:
149
+ * const redis = await UpstashRedis("my-redis", {
150
+ * name: "my-redis",
151
+ * primaryRegion: "us-east-1",
152
+ * budget: 100
153
+ * });
154
+ */
155
+ export const UpstashRedis = Resource(
156
+ "upstash::Redis",
157
+ async function (
158
+ this: Context<UpstashRedis>,
159
+ id: string,
160
+ props: UpstashRedisProps,
161
+ ): Promise<UpstashRedis> {
162
+ const api = new UpstashApi({
163
+ apiKey: props.apiKey,
164
+ email: props.email,
165
+ });
166
+
167
+ if (this.phase === "delete") {
168
+ await deleteRedisDatabase(api, this.output.id);
169
+ return this.destroy();
170
+ }
171
+
172
+ const eviction = props.eviction ?? false;
173
+
174
+ // @ts-ignore This is overridden during update/create
175
+ let database: UpstashDatabaseResponse = {};
176
+
177
+ if (this.phase === "update") {
178
+ // Update name if changed
179
+ if (props.name !== this.output.name) {
180
+ await renameRedisDatabase(api, this.output.id, props.name);
181
+ }
182
+
183
+ // Update read regions if changed
184
+ if (
185
+ JSON.stringify(props.readRegions) !==
186
+ JSON.stringify(this.output.readRegions)
187
+ ) {
188
+ await updateRedisReadRegions(
189
+ api,
190
+ this.output.id,
191
+ props.readRegions || [],
192
+ );
193
+ }
194
+
195
+ // Handle eviction setting if changed
196
+ if (
197
+ props.eviction !== undefined &&
198
+ props.eviction !== this.output.eviction
199
+ ) {
200
+ await setRedisEviction(api, this.output.id, props.eviction);
201
+ }
202
+
203
+ // Get updated database info
204
+ database = await getRedisDatabase(api, this.output.id);
205
+ }
206
+
207
+ if (this.phase === "create") {
208
+ database = await createRedisDatabase(api, {
209
+ budget: props.budget,
210
+ name: props.name,
211
+ primary_region: props.primaryRegion,
212
+ read_regions: props.readRegions,
213
+ region: "global",
214
+ tls: true,
215
+ });
216
+
217
+ if (eviction) {
218
+ await setRedisEviction(api, database.database_id, eviction);
219
+ }
220
+ }
221
+
222
+ return this.create({
223
+ id: database.database_id,
224
+ name: database.database_name,
225
+ databaseType: database.database_type,
226
+ region: database.region,
227
+ port: database.port,
228
+ createdAt: database.creation_time,
229
+ state: database.state,
230
+ password: alchemy.secret(database.password),
231
+ userEmail: database.user_email,
232
+ endpoint: database.endpoint,
233
+ tls: database.tls,
234
+ restToken: alchemy.secret(database.rest_token),
235
+ readOnlyRestToken: alchemy.secret(database.read_only_rest_token),
236
+ primaryRegion: props.primaryRegion,
237
+ readRegions: props.readRegions,
238
+ budget: props.budget,
239
+ eviction: props.eviction ?? false,
240
+ });
241
+ },
242
+ );
243
+
244
+ /**
245
+ * Response from Upstash API for database operations
246
+ */
247
+ interface UpstashDatabaseResponse {
248
+ database_id: string;
249
+ database_name: string;
250
+ database_type: string;
251
+ region: "global";
252
+ type: string;
253
+ port: number;
254
+ creation_time: number;
255
+ state: string;
256
+ password: string;
257
+ user_email: string;
258
+ endpoint: string;
259
+ tls: boolean;
260
+ rest_token: string;
261
+ read_only_rest_token: string;
262
+ eviction: boolean;
263
+ read_regions?: UpstashRegion[];
264
+ }
265
+
266
+ /**
267
+ * Parameters for creating a Redis database
268
+ */
269
+ export interface CreateRedisDatabaseParams {
270
+ name: string;
271
+ primary_region: UpstashRegion;
272
+ read_regions?: UpstashRegion[];
273
+ region: "global";
274
+ tls: boolean;
275
+ budget?: number;
276
+ }
277
+
278
+ /**
279
+ * Delete a Redis database
280
+ *
281
+ * @param api Upstash API client
282
+ * @param databaseId ID of the database to delete
283
+ */
284
+ export async function deleteRedisDatabase(
285
+ api: UpstashApi,
286
+ databaseId: string,
287
+ ): Promise<void> {
288
+ const response = await api.delete(`/redis/database/${databaseId}`);
289
+
290
+ if (!response.ok && response.status !== 404) {
291
+ throw new UpstashError(
292
+ `Error deleting database: ${response.statusText}`,
293
+ response.status,
294
+ response,
295
+ );
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Rename a Redis database
301
+ *
302
+ * @param api Upstash API client
303
+ * @param databaseId ID of the database to rename
304
+ * @param name New name for the database
305
+ */
306
+ export async function renameRedisDatabase(
307
+ api: UpstashApi,
308
+ databaseId: string,
309
+ name: string,
310
+ ): Promise<void> {
311
+ const response = await api.post(`/redis/rename/${databaseId}`, {
312
+ name,
313
+ });
314
+
315
+ if (!response.ok) {
316
+ throw new UpstashError(
317
+ `API error updating name: ${response.statusText}`,
318
+ response.status,
319
+ response,
320
+ );
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Update read regions for a Redis database
326
+ *
327
+ * @param api Upstash API client
328
+ * @param databaseId ID of the database to update
329
+ * @param readRegions Array of read regions
330
+ */
331
+ export async function updateRedisReadRegions(
332
+ api: UpstashApi,
333
+ databaseId: string,
334
+ readRegions: UpstashRegion[],
335
+ ): Promise<void> {
336
+ const response = await api.post(`/redis/update-regions/${databaseId}`, {
337
+ read_regions: readRegions,
338
+ });
339
+
340
+ if (!response.ok) {
341
+ throw new UpstashError(
342
+ `API error updating regions: ${response.statusText}`,
343
+ response.status,
344
+ response,
345
+ );
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Enable or disable eviction for a Redis database
351
+ *
352
+ * @param api Upstash API client
353
+ * @param databaseId ID of the database to update
354
+ * @param enable Whether to enable or disable eviction
355
+ */
356
+ export async function setRedisEviction(
357
+ api: UpstashApi,
358
+ databaseId: string,
359
+ enable: boolean,
360
+ ): Promise<void> {
361
+ const evictionEndpoint = enable ? "enable-eviction" : "disable-eviction";
362
+
363
+ const response = await api.post(
364
+ `/redis/${evictionEndpoint}/${databaseId}`,
365
+ {},
366
+ );
367
+
368
+ if (!response.ok) {
369
+ console.warn(
370
+ `API error updating eviction (status: ${response.status}): ${response.statusText}. (Eviction may already be set)`,
371
+ );
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Get information about a Redis database
377
+ *
378
+ * @param api Upstash API client
379
+ * @param databaseId ID of the database to get information about
380
+ * @returns Database information
381
+ */
382
+ export async function getRedisDatabase(
383
+ api: UpstashApi,
384
+ databaseId: string,
385
+ ): Promise<UpstashDatabaseResponse> {
386
+ const response = await api.get(`/redis/database/${databaseId}`);
387
+
388
+ if (!response.ok) {
389
+ throw new UpstashError(
390
+ `API error: ${response.statusText}`,
391
+ response.status,
392
+ response,
393
+ );
394
+ }
395
+
396
+ return await response.json();
397
+ }
398
+
399
+ /**
400
+ * Create a new Redis database
401
+ *
402
+ * @param api Upstash API client
403
+ * @param params Parameters for the new database
404
+ * @returns Created database information
405
+ */
406
+ export async function createRedisDatabase(
407
+ api: UpstashApi,
408
+ params: CreateRedisDatabaseParams,
409
+ ): Promise<UpstashDatabaseResponse> {
410
+ const response = await api.post("/redis/database", params);
411
+
412
+ if (!response.ok) {
413
+ throw new UpstashError(
414
+ `API error creating database: ${response.statusText}`,
415
+ response.status,
416
+ response,
417
+ );
418
+ }
419
+
420
+ return await response.json();
421
+ }