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.
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/kv-namespace.d.ts +39 -0
- package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
- package/lib/cloudflare/kv-namespace.js +95 -26
- package/lib/cloudflare/kv-namespace.js.map +1 -1
- package/lib/cloudflare/route.d.ts +102 -0
- package/lib/cloudflare/route.d.ts.map +1 -0
- package/lib/cloudflare/route.js +130 -0
- package/lib/cloudflare/route.js.map +1 -0
- package/lib/cloudflare/worker.d.ts +6 -0
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +6 -0
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +8 -0
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +17 -0
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/internal/docs/providers.d.ts.map +1 -1
- package/lib/internal/docs/providers.js +8 -2
- package/lib/internal/docs/providers.js.map +1 -1
- package/lib/scope.d.ts +0 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +3 -2
- package/lib/scope.js.map +1 -1
- package/lib/stripe/index.d.ts +1 -0
- package/lib/stripe/index.d.ts.map +1 -1
- package/lib/stripe/index.js +1 -0
- package/lib/stripe/index.js.map +1 -1
- package/lib/stripe/meter.d.ts +137 -0
- package/lib/stripe/meter.d.ts.map +1 -0
- package/lib/stripe/meter.js +226 -0
- package/lib/stripe/meter.js.map +1 -0
- package/package.json +1 -1
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/kv-namespace.ts +162 -36
- package/src/cloudflare/route.ts +268 -0
- package/src/cloudflare/worker.ts +6 -0
- package/src/cloudflare/wrangler.json.ts +27 -0
- package/src/internal/docs/providers.ts +11 -2
- package/src/scope.ts +6 -1
- package/src/stripe/index.ts +1 -0
- 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
package/src/cloudflare/index.ts
CHANGED
|
@@ -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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
}
|