alchemy 0.15.5 → 0.15.7

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.
Files changed (45) hide show
  1. package/lib/cloudflare/index.d.ts +1 -0
  2. package/lib/cloudflare/index.d.ts.map +1 -1
  3. package/lib/cloudflare/index.js +1 -0
  4. package/lib/cloudflare/index.js.map +1 -1
  5. package/lib/cloudflare/kv-namespace.d.ts +39 -0
  6. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  7. package/lib/cloudflare/kv-namespace.js +95 -26
  8. package/lib/cloudflare/kv-namespace.js.map +1 -1
  9. package/lib/cloudflare/route.d.ts +102 -0
  10. package/lib/cloudflare/route.d.ts.map +1 -0
  11. package/lib/cloudflare/route.js +130 -0
  12. package/lib/cloudflare/route.js.map +1 -0
  13. package/lib/cloudflare/worker.d.ts +6 -0
  14. package/lib/cloudflare/worker.d.ts.map +1 -1
  15. package/lib/cloudflare/worker.js +6 -0
  16. package/lib/cloudflare/worker.js.map +1 -1
  17. package/lib/cloudflare/wrangler.json.d.ts +8 -0
  18. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  19. package/lib/cloudflare/wrangler.json.js +17 -0
  20. package/lib/cloudflare/wrangler.json.js.map +1 -1
  21. package/lib/internal/docs/providers.d.ts.map +1 -1
  22. package/lib/internal/docs/providers.js +8 -2
  23. package/lib/internal/docs/providers.js.map +1 -1
  24. package/lib/scope.d.ts +0 -1
  25. package/lib/scope.d.ts.map +1 -1
  26. package/lib/scope.js +3 -2
  27. package/lib/scope.js.map +1 -1
  28. package/lib/stripe/index.d.ts +1 -0
  29. package/lib/stripe/index.d.ts.map +1 -1
  30. package/lib/stripe/index.js +1 -0
  31. package/lib/stripe/index.js.map +1 -1
  32. package/lib/stripe/meter.d.ts +137 -0
  33. package/lib/stripe/meter.d.ts.map +1 -0
  34. package/lib/stripe/meter.js +226 -0
  35. package/lib/stripe/meter.js.map +1 -0
  36. package/package.json +1 -1
  37. package/src/cloudflare/index.ts +1 -0
  38. package/src/cloudflare/kv-namespace.ts +162 -36
  39. package/src/cloudflare/route.ts +268 -0
  40. package/src/cloudflare/worker.ts +6 -0
  41. package/src/cloudflare/wrangler.json.ts +27 -0
  42. package/src/internal/docs/providers.ts +11 -2
  43. package/src/scope.ts +6 -1
  44. package/src/stripe/index.ts +1 -0
  45. package/src/stripe/meter.ts +314 -0
@@ -0,0 +1,226 @@
1
+ import Stripe from "stripe";
2
+ import { Resource } from "../resource.js";
3
+ /**
4
+ * Manages Stripe Billing Meters. Meters allow you to define how usage of a product
5
+ * is measured and aggregated for billing purposes. You can track events and their
6
+ * values, and Stripe will calculate the usage based on the meter's configuration.
7
+ *
8
+ * Stripe Meters have several properties that are immutable after creation:
9
+ * `displayName`, `eventName`, `defaultAggregation`, `customerMapping`, and
10
+ * `valueSettings`. Attempting to update these will result in an error.
11
+ * The `status` of a meter (active/inactive) can be updated.
12
+ *
13
+ * @example
14
+ * // Create a new active meter to track API calls, summing the 'count' field from events.
15
+ * const apiCallMeter = await Meter("apiUsageMeter", {
16
+ * displayName: "API Call Usage",
17
+ * eventName: "api.call.recorded", // The event name Stripe will listen for
18
+ * defaultAggregation: {
19
+ * formula: "sum" // Sum up the values of events
20
+ * },
21
+ * customerMapping: {
22
+ * eventPayloadKey: "customer_id", // Field in the event payload that identifies the Stripe Customer
23
+ * type: "by_id"
24
+ * },
25
+ * valueSettings: {
26
+ * eventPayloadKey: "count" // Field in the event payload that represents the value to aggregate
27
+ * }
28
+ * // status defaults to 'active' if not specified
29
+ * });
30
+ *
31
+ * @example
32
+ * // Create a new meter for data storage, initially inactive.
33
+ * // This meter will take the last reported 'gb_used' value within a billing period.
34
+ * const dataStorageMeterInactive = await Meter("dataStorageMeter", {
35
+ * displayName: "Data Storage GB",
36
+ * eventName: "data.storage.reported",
37
+ * status: "inactive", // Explicitly set to inactive
38
+ * defaultAggregation: {
39
+ * formula: "last_during_period"
40
+ * },
41
+ * customerMapping: {
42
+ * eventPayloadKey: "user_stripe_id",
43
+ * type: "by_id"
44
+ * },
45
+ * valueSettings: {
46
+ * eventPayloadKey: "gb_used"
47
+ * }
48
+ * });
49
+ *
50
+ * @example
51
+ * // (Scenario: Assuming 'dataStorageMeterInactive' from the previous example was created
52
+ * // and its ID is available, e.g., via `dataStorageMeterInactive.id`) *
53
+ * // If dataStorageMeterInactive was previously defined and we want to activate it:
54
+ * const activatedStorageMeter = await Meter("dataStorageMeter", { // Same logicalId
55
+ * displayName: "Data Storage GB", // Immutable, must match existing
56
+ * eventName: "data.storage.reported", // Immutable, must match existing
57
+ * status: "active", // Update: change status to active
58
+ * defaultAggregation: { // Immutable, must match existing
59
+ * formula: "last_during_period"
60
+ * },
61
+ * customerMapping: { // Immutable, must match existing
62
+ * eventPayloadKey: "user_stripe_id",
63
+ * type: "by_id"
64
+ * },
65
+ * valueSettings: { // Immutable, must match existing
66
+ * eventPayloadKey: "gb_used"
67
+ * }
68
+ * });
69
+ *
70
+ * @example
71
+ * // To deactivate an existing active meter (e.g. 'apiCallMeter' from first example):
72
+ * const deactivatedApiMeter = await Meter("apiUsageMeter", { // Same logicalId
73
+ * displayName: "API Call Usage", // Immutable, must match existing
74
+ * eventName: "api.call.recorded", // Immutable, must match existing
75
+ * status: "inactive", // Update: change status to inactive
76
+ * defaultAggregation: { // Immutable, must match existing
77
+ * formula: "sum"
78
+ * },
79
+ * customerMapping: { // Immutable, must match existing
80
+ * eventPayloadKey: "customer_id",
81
+ * type: "by_id"
82
+ * },
83
+ * valueSettings: { // Immutable, must match existing
84
+ * eventPayloadKey: "count"
85
+ * }
86
+ * });
87
+ */
88
+ export const Meter = Resource("stripe::Meter", async function (logicalId, props) {
89
+ const apiKey = process.env.STRIPE_API_KEY;
90
+ if (!apiKey) {
91
+ throw new Error("STRIPE_API_KEY environment variable is required");
92
+ }
93
+ const stripe = new Stripe(apiKey);
94
+ const currentOutputId = this.output?.id;
95
+ // Helper to map MeterProps (camelCase nested) to Stripe API params (snake_case nested)
96
+ const mapPropsToStripeParams = (inputProps) => ({
97
+ display_name: inputProps.displayName,
98
+ event_name: inputProps.eventName,
99
+ default_aggregation: {
100
+ formula: inputProps.defaultAggregation.formula,
101
+ },
102
+ customer_mapping: {
103
+ event_payload_key: inputProps.customerMapping.eventPayloadKey,
104
+ type: inputProps.customerMapping.type,
105
+ },
106
+ value_settings: {
107
+ event_payload_key: inputProps.valueSettings.eventPayloadKey,
108
+ },
109
+ });
110
+ // Helper to map Stripe API response (snake_case) to Meter output interface (camelCase)
111
+ const mapStripeObjectToMeterOutput = (stripeMeter) => ({
112
+ id: stripeMeter.id,
113
+ object: stripeMeter.object,
114
+ displayName: stripeMeter.display_name,
115
+ eventName: stripeMeter.event_name,
116
+ defaultAggregation: {
117
+ formula: stripeMeter.default_aggregation.formula,
118
+ },
119
+ customerMapping: {
120
+ eventPayloadKey: stripeMeter.customer_mapping.event_payload_key,
121
+ type: stripeMeter.customer_mapping.type,
122
+ },
123
+ valueSettings: {
124
+ eventPayloadKey: stripeMeter.value_settings.event_payload_key,
125
+ },
126
+ status: stripeMeter.status,
127
+ createdAt: stripeMeter.created,
128
+ updatedAt: stripeMeter.updated,
129
+ livemode: stripeMeter.livemode,
130
+ statusTransitions: stripeMeter.status_transitions
131
+ ? {
132
+ deactivatedAt: stripeMeter.status_transitions.deactivated_at,
133
+ }
134
+ : undefined,
135
+ });
136
+ if (this.phase === "delete") {
137
+ if (currentOutputId) {
138
+ try {
139
+ const meter = await stripe.billing.meters.retrieve(currentOutputId);
140
+ if (meter.status === "active") {
141
+ await stripe.billing.meters.deactivate(currentOutputId);
142
+ }
143
+ }
144
+ catch (error) {
145
+ if (error?.code !== "resource_missing") {
146
+ throw error;
147
+ }
148
+ }
149
+ }
150
+ return this.destroy();
151
+ }
152
+ // --- Create or Update Phase ---
153
+ let stripeAPIResponse;
154
+ if (this.phase === "update" && currentOutputId) {
155
+ const existingStripeMeter = await stripe.billing.meters.retrieve(currentOutputId);
156
+ // Normalize existingMeter's relevant fields for immutable comparison
157
+ const existingPropsForCompare = {
158
+ displayName: existingStripeMeter.display_name,
159
+ eventName: existingStripeMeter.event_name,
160
+ defaultAggregation: {
161
+ formula: existingStripeMeter.default_aggregation.formula,
162
+ },
163
+ customerMapping: {
164
+ eventPayloadKey: existingStripeMeter.customer_mapping.event_payload_key,
165
+ type: existingStripeMeter.customer_mapping.type,
166
+ },
167
+ valueSettings: {
168
+ eventPayloadKey: existingStripeMeter.value_settings.event_payload_key,
169
+ },
170
+ };
171
+ // Check for immutable property changes (comparing against props passed to resource)
172
+ if (props.displayName !== existingPropsForCompare.displayName ||
173
+ props.eventName !== existingPropsForCompare.eventName ||
174
+ JSON.stringify(props.defaultAggregation) !==
175
+ JSON.stringify(existingPropsForCompare.defaultAggregation) ||
176
+ JSON.stringify(props.customerMapping) !==
177
+ JSON.stringify(existingPropsForCompare.customerMapping) ||
178
+ JSON.stringify(props.valueSettings) !==
179
+ JSON.stringify(existingPropsForCompare.valueSettings)) {
180
+ throw new Error(`Attempted to update immutable properties for Stripe Meter ${currentOutputId}. ` +
181
+ "displayName, eventName, defaultAggregation, customerMapping, and valueSettings cannot be changed.");
182
+ }
183
+ // Handle status change
184
+ if (props.status && props.status !== existingStripeMeter.status) {
185
+ if (props.status === "inactive" &&
186
+ existingStripeMeter.status === "active") {
187
+ stripeAPIResponse =
188
+ await stripe.billing.meters.deactivate(currentOutputId);
189
+ }
190
+ else if (props.status === "active" &&
191
+ existingStripeMeter.status === "inactive") {
192
+ console.log(`Reactivating Stripe Meter ${currentOutputId}.`);
193
+ stripeAPIResponse =
194
+ await stripe.billing.meters.reactivate(currentOutputId);
195
+ }
196
+ else {
197
+ stripeAPIResponse = existingStripeMeter; // No change in status
198
+ }
199
+ }
200
+ else {
201
+ stripeAPIResponse = existingStripeMeter; // No status change requested
202
+ }
203
+ }
204
+ else {
205
+ // Create phase
206
+ if (!props.displayName ||
207
+ !props.eventName ||
208
+ !props.defaultAggregation ||
209
+ !props.customerMapping ||
210
+ !props.valueSettings) {
211
+ throw new Error("displayName, eventName, defaultAggregation, customerMapping, and valueSettings are required for creating a Stripe Meter.");
212
+ }
213
+ const createParams = mapPropsToStripeParams(props);
214
+ stripeAPIResponse = await stripe.billing.meters.create(createParams);
215
+ // If status 'inactive' is requested during creation, and Stripe created it as 'active'
216
+ if (props.status === "inactive" &&
217
+ stripeAPIResponse.status === "active") {
218
+ stripeAPIResponse = await stripe.billing.meters.deactivate(stripeAPIResponse.id);
219
+ }
220
+ else if (props.status && props.status !== stripeAPIResponse.status) {
221
+ console.warn(`Meter ${stripeAPIResponse.id} created with status ${stripeAPIResponse.status} but requested ${props.status}. Ensure this is intended.`);
222
+ }
223
+ }
224
+ return this(mapStripeObjectToMeterOutput(stripeAPIResponse));
225
+ });
226
+ //# sourceMappingURL=meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.js","sourceRoot":"","sources":["../../src/stripe/meter.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.15.5",
3
+ "version": "0.15.7",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
@@ -23,6 +23,7 @@ export * from "./queue-consumer.js";
23
23
  export * from "./queue.js";
24
24
  export * from "./r2-rest-state-store.js";
25
25
  export * from "./redwood.js";
26
+ export * from "./route.js";
26
27
  export * from "./tanstack-start.js";
27
28
  export * from "./vectorize-index.js";
28
29
  export * from "./vectorize-metadata-index.js";
@@ -3,8 +3,8 @@ import { Resource } from "../resource.js";
3
3
  import { withExponentialBackoff } from "../util/retry.js";
4
4
  import { handleApiError } from "./api-error.js";
5
5
  import {
6
- type CloudflareApi,
7
6
  createCloudflareApi,
7
+ type CloudflareApi,
8
8
  type CloudflareApiOptions,
9
9
  } from "./api.js";
10
10
 
@@ -22,6 +22,22 @@ export interface KVNamespaceProps extends CloudflareApiOptions {
22
22
  * Only used for initial setup or updates
23
23
  */
24
24
  values?: KVPair[];
25
+
26
+ /**
27
+ * Whether to adopt an existing namespace with the same title if it exists
28
+ * If true and a namespace with the same title exists, it will be adopted rather than creating a new one
29
+ *
30
+ * @default false
31
+ */
32
+ adopt?: boolean;
33
+
34
+ /**
35
+ * Whether to delete the namespace.
36
+ * If set to false, the namespace will remain but the resource will be removed from state
37
+ *
38
+ * @default true
39
+ */
40
+ delete?: boolean;
25
41
  }
26
42
 
27
43
  /**
@@ -112,6 +128,24 @@ export interface KVNamespace
112
128
  * }
113
129
  * }]
114
130
  * });
131
+ *
132
+ * @example
133
+ * // Adopt an existing namespace if it already exists instead of failing
134
+ * const existingNamespace = await KVNamespace("existing-ns", {
135
+ * title: "existing-namespace",
136
+ * adopt: true,
137
+ * values: [{
138
+ * key: "config",
139
+ * value: { setting: "updated-value" }
140
+ * }]
141
+ * });
142
+ *
143
+ * @example
144
+ * // When removing from Alchemy state, keep the namespace in Cloudflare
145
+ * const preservedNamespace = await KVNamespace("preserve-ns", {
146
+ * title: "preserved-namespace",
147
+ * delete: false
148
+ * });
115
149
  */
116
150
  export const KVNamespace = Resource(
117
151
  "cloudflare::KVNamespace",
@@ -126,7 +160,7 @@ export const KVNamespace = Resource(
126
160
  if (this.phase === "delete") {
127
161
  // For delete operations, we need to check if the namespace ID exists in the output
128
162
  const namespaceId = this.output?.namespaceId;
129
- if (namespaceId) {
163
+ if (namespaceId && props.delete !== false) {
130
164
  await deleteKVNamespace(api, namespaceId);
131
165
  }
132
166
 
@@ -145,10 +179,39 @@ export const KVNamespace = Resource(
145
179
  if (this.phase === "update" && namespaceId) {
146
180
  // Can't update a KV namespace title directly, just work with existing ID
147
181
  } else {
148
- // TODO: if it already exists, then check the tags to see if we own it and continue
149
- const { id } = await createKVNamespace(api, props);
150
- createdAt = Date.now();
151
- namespaceId = id;
182
+ try {
183
+ // Try to create the KV namespace
184
+ const { id } = await createKVNamespace(api, props);
185
+ createdAt = Date.now();
186
+ namespaceId = id;
187
+ } catch (error) {
188
+ // Check if this is a "namespace already exists" error and adopt is enabled
189
+ if (
190
+ props.adopt &&
191
+ error instanceof Error &&
192
+ error.message.includes("already exists")
193
+ ) {
194
+ console.log(`Namespace '${props.title}' already exists, adopting it`);
195
+ // Find the existing namespace by title
196
+ const existingNamespace = await findKVNamespaceByTitle(
197
+ api,
198
+ props.title,
199
+ );
200
+
201
+ if (!existingNamespace) {
202
+ throw new Error(
203
+ `Failed to find existing namespace '${props.title}' for adoption`,
204
+ );
205
+ }
206
+
207
+ // Use the existing namespace ID
208
+ namespaceId = existingNamespace.id;
209
+ createdAt = existingNamespace.createdAt || Date.now();
210
+ } else {
211
+ // Re-throw the error if adopt is false or it's not a "namespace already exists" error
212
+ throw error;
213
+ }
214
+ }
152
215
  }
153
216
 
154
217
  await insertKVRecords(api, namespaceId, props);
@@ -232,37 +295,100 @@ export async function insertKVRecords(
232
295
  return item;
233
296
  });
234
297
 
235
- try {
236
- await withExponentialBackoff(
237
- async () => {
238
- const bulkResponse = await api.put(
239
- `/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/bulk`,
240
- bulkPayload,
241
- );
298
+ await withExponentialBackoff(
299
+ async () => {
300
+ const bulkResponse = await api.put(
301
+ `/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/bulk`,
302
+ bulkPayload,
303
+ );
304
+
305
+ if (!bulkResponse.ok) {
306
+ const errorData: any = await bulkResponse.json().catch(() => ({
307
+ errors: [{ message: bulkResponse.statusText }],
308
+ }));
309
+ const errorMessage =
310
+ errorData.errors?.[0]?.message || bulkResponse.statusText;
311
+
312
+ // Throw error to trigger retry
313
+ throw new Error(`Error writing KV batch: ${errorMessage}`);
314
+ }
315
+
316
+ return bulkResponse;
317
+ },
318
+ (error) => {
319
+ // Retry on "namespace not found" errors as they're likely propagation delays
320
+ return error.message?.includes("not found");
321
+ },
322
+ 5, // 5 retry attempts
323
+ 1000, // Start with 1 second delay
324
+ );
325
+ }
326
+ }
327
+ }
242
328
 
243
- if (!bulkResponse.ok) {
244
- const errorData: any = await bulkResponse.json().catch(() => ({
245
- errors: [{ message: bulkResponse.statusText }],
246
- }));
247
- const errorMessage =
248
- errorData.errors?.[0]?.message || bulkResponse.statusText;
249
-
250
- // Throw error to trigger retry
251
- throw new Error(`Error writing KV batch: ${errorMessage}`);
252
- }
253
-
254
- return bulkResponse;
255
- },
256
- (error) => {
257
- // Retry on "namespace not found" errors as they're likely propagation delays
258
- return error.message?.includes("not found");
259
- },
260
- 5, // 5 retry attempts
261
- 1000, // Start with 1 second delay
262
- );
263
- } catch (error: any) {
264
- console.warn(error.message);
265
- }
329
+ /**
330
+ * Interface representing a KV namespace as returned by Cloudflare API
331
+ */
332
+ interface CloudflareKVNamespace {
333
+ id: string;
334
+ title: string;
335
+ supports_url_encoding?: boolean;
336
+ created_on?: string;
337
+ }
338
+
339
+ /**
340
+ * Find a KV namespace by title with pagination support
341
+ */
342
+ export async function findKVNamespaceByTitle(
343
+ api: CloudflareApi,
344
+ title: string,
345
+ ): Promise<{ id: string; createdAt?: number } | null> {
346
+ let page = 1;
347
+ const perPage = 100; // Maximum allowed by API
348
+ let hasMorePages = true;
349
+
350
+ while (hasMorePages) {
351
+ const response = await api.get(
352
+ `/accounts/${api.accountId}/storage/kv/namespaces?page=${page}&per_page=${perPage}`,
353
+ );
354
+
355
+ if (!response.ok) {
356
+ await handleApiError(response, "list", "kv_namespace", "all");
266
357
  }
358
+
359
+ const data = (await response.json()) as {
360
+ result: CloudflareKVNamespace[];
361
+ result_info: {
362
+ count: number;
363
+ page: number;
364
+ per_page: number;
365
+ total_count: number;
366
+ };
367
+ success: boolean;
368
+ errors: any[];
369
+ };
370
+
371
+ const namespaces = data.result;
372
+ const resultInfo = data.result_info;
373
+
374
+ // Look for a namespace with matching title
375
+ const match = namespaces.find((ns) => ns.title === title);
376
+ if (match) {
377
+ return {
378
+ id: match.id,
379
+ // Convert ISO string to timestamp if available, otherwise use current time
380
+ createdAt: match.created_on
381
+ ? new Date(match.created_on).getTime()
382
+ : undefined,
383
+ };
384
+ }
385
+
386
+ // Check if we've seen all pages
387
+ hasMorePages =
388
+ resultInfo.page * resultInfo.per_page < resultInfo.total_count;
389
+ page++;
267
390
  }
391
+
392
+ // No matching namespace found
393
+ return null;
268
394
  }