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,268 @@
1
+ import type { Context } from "../context.js";
2
+ import { Resource } from "../resource.js";
3
+ import { CloudflareApiError, handleApiError } from "./api-error.js";
4
+ import {
5
+ createCloudflareApi,
6
+ type CloudflareApi,
7
+ type CloudflareApiOptions,
8
+ } from "./api.js";
9
+ import type { Worker } from "./worker.js";
10
+
11
+ /**
12
+ * Properties for creating or updating a Route
13
+ */
14
+ export interface RouteProps extends CloudflareApiOptions {
15
+ /**
16
+ * URL pattern for the route
17
+ * @example "example.com/*"
18
+ */
19
+ pattern: string;
20
+
21
+ /**
22
+ * Worker script for the route
23
+ * This can be a Worker resource or script name as a string
24
+ */
25
+ script: Worker | string;
26
+
27
+ /**
28
+ * Zone ID for the route
29
+ */
30
+ zoneId: string;
31
+ }
32
+
33
+ /**
34
+ * Output returned after Route creation/update
35
+ */
36
+ export interface Route extends Resource<"cloudflare::Route">, RouteProps {
37
+ /**
38
+ * The unique ID of the route
39
+ */
40
+ id: string;
41
+
42
+ /**
43
+ * The URL pattern for the route
44
+ */
45
+ pattern: string;
46
+
47
+ /**
48
+ * The Worker script name for the route
49
+ */
50
+ script: string;
51
+
52
+ /**
53
+ * The Zone ID for the route
54
+ */
55
+ zoneId: string;
56
+ }
57
+
58
+ /**
59
+ * Creates and manages Cloudflare Worker Routes.
60
+ *
61
+ * Routes map URL patterns to Worker scripts, allowing you to control which
62
+ * requests are handled by your Workers.
63
+ *
64
+ * @example
65
+ * // Create a route that maps all requests on a domain to a Worker
66
+ * const basicRoute = await Route("main-route", {
67
+ * pattern: "example.com/*",
68
+ * script: "my-worker",
69
+ * zoneId: "your-zone-id"
70
+ * });
71
+ *
72
+ * @example
73
+ * // Create a route using a Worker resource
74
+ * const worker = await Worker("api-worker", {
75
+ * script: `
76
+ * export default {
77
+ * fetch(request, env) {
78
+ * return new Response("Hello from API!");
79
+ * }
80
+ * }
81
+ * `
82
+ * });
83
+ *
84
+ * const apiRoute = await Route("api-route", {
85
+ * pattern: "api.example.com/*",
86
+ * script: worker,
87
+ * zoneId: "your-zone-id"
88
+ * });
89
+ *
90
+ * @see https://developers.cloudflare.com/workers/configuration/routes/
91
+ */
92
+ export const Route = Resource(
93
+ "cloudflare::Route",
94
+ async function (
95
+ this: Context<Route>,
96
+ id: string,
97
+ props: RouteProps,
98
+ ): Promise<Route> {
99
+ const api = await createCloudflareApi(props);
100
+
101
+ // Get script name from script prop (either a string or a Worker resource)
102
+ const scriptName =
103
+ typeof props.script === "string" ? props.script : props.script.name;
104
+
105
+ // Get zone ID from props
106
+ const { zoneId } = props;
107
+
108
+ if (this.phase === "delete") {
109
+ console.log("Deleting Route:", props.pattern);
110
+
111
+ // Only delete if we have an ID
112
+ if (this.output?.id) {
113
+ await deleteRoute(api, zoneId, this.output.id);
114
+ }
115
+
116
+ // Return void (a deleted route has no content)
117
+ return this.destroy();
118
+ }
119
+
120
+ let routeData: CloudflareRouteResponse;
121
+
122
+ if (this.phase === "update" && this.output?.id) {
123
+ console.log("Updating Route:", props.pattern);
124
+
125
+ // Update existing route
126
+ routeData = await updateRoute(
127
+ api,
128
+ zoneId,
129
+ this.output.id,
130
+ props.pattern,
131
+ scriptName,
132
+ );
133
+ } else {
134
+ console.log("Creating Route:", props.pattern);
135
+
136
+ // Create new route
137
+ routeData = await createRoute(api, zoneId, props.pattern, scriptName);
138
+ }
139
+
140
+ // Return the route resource
141
+ return this({
142
+ id: routeData.result.id,
143
+ pattern: routeData.result.pattern,
144
+ script: routeData.result.script,
145
+ zoneId,
146
+ });
147
+ },
148
+ );
149
+
150
+ interface CloudflareRouteResponse {
151
+ result: {
152
+ id: string;
153
+ pattern: string;
154
+ script: string;
155
+ };
156
+ success: boolean;
157
+ errors: Array<{ code: number; message: string }>;
158
+ messages: string[];
159
+ }
160
+
161
+ /**
162
+ * Create a new route
163
+ */
164
+ async function createRoute(
165
+ api: CloudflareApi,
166
+ zoneId: string,
167
+ pattern: string,
168
+ script: string,
169
+ ): Promise<CloudflareRouteResponse> {
170
+ const createResponse = await api.post(`/zones/${zoneId}/workers/routes`, {
171
+ pattern,
172
+ script,
173
+ });
174
+
175
+ if (!createResponse.ok) {
176
+ return await handleApiError(createResponse, "creating", "Route", pattern);
177
+ }
178
+
179
+ return (await createResponse.json()) as CloudflareRouteResponse;
180
+ }
181
+
182
+ /**
183
+ * Update a route
184
+ */
185
+ async function updateRoute(
186
+ api: CloudflareApi,
187
+ zoneId: string,
188
+ routeId: string,
189
+ pattern: string,
190
+ script: string,
191
+ ): Promise<CloudflareRouteResponse> {
192
+ const updateResponse = await api.put(
193
+ `/zones/${zoneId}/workers/routes/${routeId}`,
194
+ {
195
+ pattern,
196
+ script,
197
+ },
198
+ );
199
+
200
+ if (!updateResponse.ok) {
201
+ return await handleApiError(updateResponse, "updating", "Route", pattern);
202
+ }
203
+
204
+ return (await updateResponse.json()) as CloudflareRouteResponse;
205
+ }
206
+
207
+ /**
208
+ * Delete a route
209
+ */
210
+ async function deleteRoute(
211
+ api: CloudflareApi,
212
+ zoneId: string,
213
+ routeId: string,
214
+ ): Promise<void> {
215
+ const deleteResponse = await api.delete(
216
+ `/zones/${zoneId}/workers/routes/${routeId}`,
217
+ );
218
+
219
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
220
+ const errorData: any = await deleteResponse.json().catch(() => ({
221
+ errors: [{ message: deleteResponse.statusText }],
222
+ }));
223
+
224
+ throw new CloudflareApiError(
225
+ `Error deleting Route '${routeId}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
226
+ deleteResponse,
227
+ );
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Get a route by ID
233
+ */
234
+ export async function getRoute(
235
+ api: CloudflareApi,
236
+ zoneId: string,
237
+ routeId: string,
238
+ ): Promise<CloudflareRouteResponse> {
239
+ const response = await api.get(`/zones/${zoneId}/workers/routes/${routeId}`);
240
+
241
+ if (!response.ok) {
242
+ throw new CloudflareApiError(
243
+ `Failed to get route ${routeId}: ${response.statusText}`,
244
+ response,
245
+ );
246
+ }
247
+
248
+ return (await response.json()) as CloudflareRouteResponse;
249
+ }
250
+
251
+ /**
252
+ * List all routes for a zone
253
+ */
254
+ export async function listRoutes(
255
+ api: CloudflareApi,
256
+ zoneId: string,
257
+ ): Promise<CloudflareRouteResponse> {
258
+ const response = await api.get(`/zones/${zoneId}/workers/routes`);
259
+
260
+ if (!response.ok) {
261
+ throw new CloudflareApiError(
262
+ `Failed to list routes: ${response.statusText}`,
263
+ response,
264
+ );
265
+ }
266
+
267
+ return (await response.json()) as CloudflareRouteResponse;
268
+ }
@@ -275,6 +275,12 @@ export interface Worker<B extends Bindings = Bindings>
275
275
  * url: true
276
276
  * });
277
277
  *
278
+ * await Route("route", {
279
+ * zoneId: zone.zoneId,
280
+ * worker: api,
281
+ * pattern: "api.example.com/*",
282
+ * });
283
+ *
278
284
  * @example
279
285
  * // Create a real-time chat worker using Durable Objects
280
286
  * // for state management:
@@ -268,6 +268,15 @@ export interface WranglerJsonSpec {
268
268
  binding: string;
269
269
  };
270
270
 
271
+ /**
272
+ * Migrations
273
+ */
274
+ migrations?: {
275
+ tag: string;
276
+ new_sqlite_classes?: string[];
277
+ new_classes?: string[];
278
+ }[];
279
+
271
280
  /**
272
281
  * Workflow bindings
273
282
  */
@@ -327,6 +336,9 @@ function processBindings(
327
336
  consumers: [],
328
337
  };
329
338
 
339
+ const new_sqlite_classes: string[] = [];
340
+ const new_classes: string[] = [];
341
+
330
342
  const vectorizeIndexes: { binding: string; index_name: string }[] = [];
331
343
 
332
344
  for (const eventSource of eventSources ?? []) {
@@ -383,6 +395,11 @@ function processBindings(
383
395
  script_name: doBinding.scriptName,
384
396
  environment: doBinding.environment,
385
397
  });
398
+ if (doBinding.sqlite) {
399
+ new_sqlite_classes.push(doBinding.className);
400
+ } else {
401
+ new_classes.push(doBinding.className);
402
+ }
386
403
  } else if (binding.type === "r2_bucket") {
387
404
  r2Buckets.push({
388
405
  binding: bindingName,
@@ -466,4 +483,14 @@ function processBindings(
466
483
  if (vectorizeIndexes.length > 0) {
467
484
  spec.vectorize_indexes = vectorizeIndexes;
468
485
  }
486
+
487
+ if (new_sqlite_classes.length > 0 || new_classes.length > 0) {
488
+ spec.migrations = [
489
+ {
490
+ tag: "v1",
491
+ new_sqlite_classes,
492
+ new_classes,
493
+ },
494
+ ];
495
+ }
469
496
  }
@@ -6,6 +6,14 @@ import { Document } from "../../ai/document.js";
6
6
  import { alchemy } from "../../alchemy.js";
7
7
  import { Folder } from "../../fs/folder.js";
8
8
 
9
+ function getArg(arg: string) {
10
+ const idx = process.argv.findIndex((a) => a === arg);
11
+ return idx > -1 ? process.argv[idx + 1] : undefined;
12
+ }
13
+
14
+ const onlyProviderName = getArg("--provider");
15
+ const onlyResourceName = getArg("--resource");
16
+
9
17
  export interface DocsProps {
10
18
  /**
11
19
  * The output directory for the docs.
@@ -117,7 +125,7 @@ async function generateProviderDocs({
117
125
  reasoningEffort: "high",
118
126
  },
119
127
  },
120
- freeze: false,
128
+ freeze: onlyProviderName === undefined || onlyProviderName !== providerName,
121
129
  temperature: 0.1,
122
130
  schema: type({
123
131
  groups: type({
@@ -177,7 +185,8 @@ async function generateProviderDocs({
177
185
  providerDocsDir,
178
186
  `${g.filename.replace(".ts", "").replace(".md", "")}.md`,
179
187
  ),
180
- freeze: false,
188
+ freeze:
189
+ onlyResourceName !== undefined && onlyResourceName !== g.identifier,
181
190
  model: {
182
191
  id: "claude-3-5-sonnet-latest",
183
192
  provider: "anthropic",
package/src/scope.ts CHANGED
@@ -47,10 +47,15 @@ export class Scope {
47
47
 
48
48
  private isErrored = false;
49
49
 
50
- constructor(private readonly options: ScopeOptions) {
50
+ constructor(options: ScopeOptions) {
51
51
  this.appName = options.appName;
52
52
  this.stage = options?.stage ?? DEFAULT_STAGE;
53
53
  this.scopeName = options.scopeName ?? null;
54
+ if (this.scopeName?.includes(":")) {
55
+ throw new Error(
56
+ `Scope name ${this.scopeName} cannot contain double colons`,
57
+ );
58
+ }
54
59
  this.parent = options.parent ?? Scope.get();
55
60
  this.quiet = options.quiet ?? this.parent?.quiet ?? false;
56
61
  if (this.parent && !this.scopeName) {
@@ -1,3 +1,4 @@
1
1
  export * from "./price.js";
2
2
  export * from "./product.js";
3
3
  export * from "./webhook.js";
4
+ export * from "./meter.js";
@@ -0,0 +1,314 @@
1
+ import Stripe from "stripe";
2
+ import type { Context } from "../context.js";
3
+ import { Resource } from "../resource.js";
4
+
5
+ /**
6
+ * Properties for creating or updating a Stripe Meter.
7
+ * Note: Nested objects like defaultAggregation, customerMapping, valueSettings
8
+ * use camelCase for keys here (e.g., eventPayloadKey), and the resource
9
+ * implementation will map them to snake_case for Stripe API calls.
10
+ */
11
+ export interface MeterProps {
12
+ displayName: string;
13
+ eventName: string;
14
+ status?: "active" | "inactive"; // 'active' is default on create by Stripe
15
+ defaultAggregation: {
16
+ formula: Stripe.Billing.MeterCreateParams.DefaultAggregation.Formula;
17
+ };
18
+ customerMapping: {
19
+ eventPayloadKey: string;
20
+ type: Stripe.Billing.MeterCreateParams.CustomerMapping["type"];
21
+ };
22
+ valueSettings: {
23
+ eventPayloadKey: string;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Output returned after Stripe Meter creation/update.
29
+ */
30
+ export interface Meter extends Resource<"stripe::Meter"> {
31
+ id: string;
32
+ object: "billing.meter";
33
+ displayName: string;
34
+ eventName: string;
35
+ defaultAggregation: {
36
+ formula: Stripe.Billing.Meter.DefaultAggregation["formula"];
37
+ };
38
+ customerMapping: {
39
+ eventPayloadKey: string;
40
+ type: Stripe.Billing.Meter.CustomerMapping["type"];
41
+ };
42
+ valueSettings: {
43
+ eventPayloadKey: string;
44
+ };
45
+ status: "active" | "inactive";
46
+ createdAt: number; // Unix timestamp
47
+ updatedAt: number; // Unix timestamp
48
+ livemode: boolean;
49
+ statusTransitions?: {
50
+ deactivatedAt: number | null; // Unix timestamp
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Manages Stripe Billing Meters. Meters allow you to define how usage of a product
56
+ * is measured and aggregated for billing purposes. You can track events and their
57
+ * values, and Stripe will calculate the usage based on the meter's configuration.
58
+ *
59
+ * Stripe Meters have several properties that are immutable after creation:
60
+ * `displayName`, `eventName`, `defaultAggregation`, `customerMapping`, and
61
+ * `valueSettings`. Attempting to update these will result in an error.
62
+ * The `status` of a meter (active/inactive) can be updated.
63
+ *
64
+ * @example
65
+ * // Create a new active meter to track API calls, summing the 'count' field from events.
66
+ * const apiCallMeter = await Meter("apiUsageMeter", {
67
+ * displayName: "API Call Usage",
68
+ * eventName: "api.call.recorded", // The event name Stripe will listen for
69
+ * defaultAggregation: {
70
+ * formula: "sum" // Sum up the values of events
71
+ * },
72
+ * customerMapping: {
73
+ * eventPayloadKey: "customer_id", // Field in the event payload that identifies the Stripe Customer
74
+ * type: "by_id"
75
+ * },
76
+ * valueSettings: {
77
+ * eventPayloadKey: "count" // Field in the event payload that represents the value to aggregate
78
+ * }
79
+ * // status defaults to 'active' if not specified
80
+ * });
81
+ *
82
+ * @example
83
+ * // Create a new meter for data storage, initially inactive.
84
+ * // This meter will take the last reported 'gb_used' value within a billing period.
85
+ * const dataStorageMeterInactive = await Meter("dataStorageMeter", {
86
+ * displayName: "Data Storage GB",
87
+ * eventName: "data.storage.reported",
88
+ * status: "inactive", // Explicitly set to inactive
89
+ * defaultAggregation: {
90
+ * formula: "last_during_period"
91
+ * },
92
+ * customerMapping: {
93
+ * eventPayloadKey: "user_stripe_id",
94
+ * type: "by_id"
95
+ * },
96
+ * valueSettings: {
97
+ * eventPayloadKey: "gb_used"
98
+ * }
99
+ * });
100
+ *
101
+ * @example
102
+ * // (Scenario: Assuming 'dataStorageMeterInactive' from the previous example was created
103
+ * // and its ID is available, e.g., via `dataStorageMeterInactive.id`) *
104
+ * // If dataStorageMeterInactive was previously defined and we want to activate it:
105
+ * const activatedStorageMeter = await Meter("dataStorageMeter", { // Same logicalId
106
+ * displayName: "Data Storage GB", // Immutable, must match existing
107
+ * eventName: "data.storage.reported", // Immutable, must match existing
108
+ * status: "active", // Update: change status to active
109
+ * defaultAggregation: { // Immutable, must match existing
110
+ * formula: "last_during_period"
111
+ * },
112
+ * customerMapping: { // Immutable, must match existing
113
+ * eventPayloadKey: "user_stripe_id",
114
+ * type: "by_id"
115
+ * },
116
+ * valueSettings: { // Immutable, must match existing
117
+ * eventPayloadKey: "gb_used"
118
+ * }
119
+ * });
120
+ *
121
+ * @example
122
+ * // To deactivate an existing active meter (e.g. 'apiCallMeter' from first example):
123
+ * const deactivatedApiMeter = await Meter("apiUsageMeter", { // Same logicalId
124
+ * displayName: "API Call Usage", // Immutable, must match existing
125
+ * eventName: "api.call.recorded", // Immutable, must match existing
126
+ * status: "inactive", // Update: change status to inactive
127
+ * defaultAggregation: { // Immutable, must match existing
128
+ * formula: "sum"
129
+ * },
130
+ * customerMapping: { // Immutable, must match existing
131
+ * eventPayloadKey: "customer_id",
132
+ * type: "by_id"
133
+ * },
134
+ * valueSettings: { // Immutable, must match existing
135
+ * eventPayloadKey: "count"
136
+ * }
137
+ * });
138
+ */
139
+
140
+ export const Meter = Resource(
141
+ "stripe::Meter",
142
+ async function (
143
+ this: Context<Meter>,
144
+ logicalId: string,
145
+ props: MeterProps,
146
+ ): Promise<Meter> {
147
+ const apiKey = process.env.STRIPE_API_KEY;
148
+ if (!apiKey) {
149
+ throw new Error("STRIPE_API_KEY environment variable is required");
150
+ }
151
+ const stripe = new Stripe(apiKey);
152
+
153
+ const currentOutputId = this.output?.id;
154
+
155
+ // Helper to map MeterProps (camelCase nested) to Stripe API params (snake_case nested)
156
+ const mapPropsToStripeParams = (
157
+ inputProps: MeterProps,
158
+ ): Stripe.Billing.MeterCreateParams => ({
159
+ display_name: inputProps.displayName,
160
+ event_name: inputProps.eventName,
161
+ default_aggregation: {
162
+ formula: inputProps.defaultAggregation.formula,
163
+ },
164
+ customer_mapping: {
165
+ event_payload_key: inputProps.customerMapping.eventPayloadKey,
166
+ type: inputProps.customerMapping.type,
167
+ },
168
+ value_settings: {
169
+ event_payload_key: inputProps.valueSettings.eventPayloadKey,
170
+ },
171
+ });
172
+
173
+ // Helper to map Stripe API response (snake_case) to Meter output interface (camelCase)
174
+ const mapStripeObjectToMeterOutput = (
175
+ stripeMeter: Stripe.Billing.Meter,
176
+ ): Omit<Meter, keyof Resource<"stripe::Meter">> => ({
177
+ id: stripeMeter.id,
178
+ object: stripeMeter.object,
179
+ displayName: stripeMeter.display_name,
180
+ eventName: stripeMeter.event_name,
181
+ defaultAggregation: {
182
+ formula: stripeMeter.default_aggregation.formula,
183
+ },
184
+ customerMapping: {
185
+ eventPayloadKey: stripeMeter.customer_mapping.event_payload_key,
186
+ type: stripeMeter.customer_mapping.type,
187
+ },
188
+ valueSettings: {
189
+ eventPayloadKey: stripeMeter.value_settings.event_payload_key,
190
+ },
191
+ status: stripeMeter.status as "active" | "inactive",
192
+ createdAt: stripeMeter.created,
193
+ updatedAt: stripeMeter.updated,
194
+ livemode: stripeMeter.livemode,
195
+ statusTransitions: stripeMeter.status_transitions
196
+ ? {
197
+ deactivatedAt: stripeMeter.status_transitions.deactivated_at,
198
+ }
199
+ : undefined,
200
+ });
201
+
202
+ if (this.phase === "delete") {
203
+ if (currentOutputId) {
204
+ try {
205
+ const meter = await stripe.billing.meters.retrieve(currentOutputId);
206
+ if (meter.status === "active") {
207
+ await stripe.billing.meters.deactivate(currentOutputId);
208
+ }
209
+ } catch (error: any) {
210
+ if (error?.code !== "resource_missing") {
211
+ throw error;
212
+ }
213
+ }
214
+ }
215
+ return this.destroy();
216
+ }
217
+
218
+ // --- Create or Update Phase ---
219
+ let stripeAPIResponse: Stripe.Billing.Meter;
220
+
221
+ if (this.phase === "update" && currentOutputId) {
222
+ const existingStripeMeter =
223
+ await stripe.billing.meters.retrieve(currentOutputId);
224
+
225
+ // Normalize existingMeter's relevant fields for immutable comparison
226
+ const existingPropsForCompare = {
227
+ displayName: existingStripeMeter.display_name,
228
+ eventName: existingStripeMeter.event_name,
229
+ defaultAggregation: {
230
+ formula: existingStripeMeter.default_aggregation.formula,
231
+ },
232
+ customerMapping: {
233
+ eventPayloadKey:
234
+ existingStripeMeter.customer_mapping.event_payload_key,
235
+ type: existingStripeMeter.customer_mapping.type,
236
+ },
237
+ valueSettings: {
238
+ eventPayloadKey: existingStripeMeter.value_settings.event_payload_key,
239
+ },
240
+ };
241
+
242
+ // Check for immutable property changes (comparing against props passed to resource)
243
+ if (
244
+ props.displayName !== existingPropsForCompare.displayName ||
245
+ props.eventName !== existingPropsForCompare.eventName ||
246
+ JSON.stringify(props.defaultAggregation) !==
247
+ JSON.stringify(existingPropsForCompare.defaultAggregation) ||
248
+ JSON.stringify(props.customerMapping) !==
249
+ JSON.stringify(existingPropsForCompare.customerMapping) ||
250
+ JSON.stringify(props.valueSettings) !==
251
+ JSON.stringify(existingPropsForCompare.valueSettings)
252
+ ) {
253
+ throw new Error(
254
+ `Attempted to update immutable properties for Stripe Meter ${currentOutputId}. ` +
255
+ "displayName, eventName, defaultAggregation, customerMapping, and valueSettings cannot be changed.",
256
+ );
257
+ }
258
+
259
+ // Handle status change
260
+ if (props.status && props.status !== existingStripeMeter.status) {
261
+ if (
262
+ props.status === "inactive" &&
263
+ existingStripeMeter.status === "active"
264
+ ) {
265
+ stripeAPIResponse =
266
+ await stripe.billing.meters.deactivate(currentOutputId);
267
+ } else if (
268
+ props.status === "active" &&
269
+ existingStripeMeter.status === "inactive"
270
+ ) {
271
+ console.log(`Reactivating Stripe Meter ${currentOutputId}.`);
272
+ stripeAPIResponse =
273
+ await stripe.billing.meters.reactivate(currentOutputId);
274
+ } else {
275
+ stripeAPIResponse = existingStripeMeter; // No change in status
276
+ }
277
+ } else {
278
+ stripeAPIResponse = existingStripeMeter; // No status change requested
279
+ }
280
+ } else {
281
+ // Create phase
282
+ if (
283
+ !props.displayName ||
284
+ !props.eventName ||
285
+ !props.defaultAggregation ||
286
+ !props.customerMapping ||
287
+ !props.valueSettings
288
+ ) {
289
+ throw new Error(
290
+ "displayName, eventName, defaultAggregation, customerMapping, and valueSettings are required for creating a Stripe Meter.",
291
+ );
292
+ }
293
+
294
+ const createParams = mapPropsToStripeParams(props);
295
+ stripeAPIResponse = await stripe.billing.meters.create(createParams);
296
+
297
+ // If status 'inactive' is requested during creation, and Stripe created it as 'active'
298
+ if (
299
+ props.status === "inactive" &&
300
+ stripeAPIResponse.status === "active"
301
+ ) {
302
+ stripeAPIResponse = await stripe.billing.meters.deactivate(
303
+ stripeAPIResponse.id,
304
+ );
305
+ } else if (props.status && props.status !== stripeAPIResponse.status) {
306
+ console.warn(
307
+ `Meter ${stripeAPIResponse.id} created with status ${stripeAPIResponse.status} but requested ${props.status}. Ensure this is intended.`,
308
+ );
309
+ }
310
+ }
311
+
312
+ return this(mapStripeObjectToMeterOutput(stripeAPIResponse));
313
+ },
314
+ );