alchemy 0.8.0 → 0.8.1

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.
@@ -11,6 +11,7 @@ import type { DurableObjectNamespace } from "./durable-object-namespace";
11
11
  import type { KVNamespace } from "./kv-namespace";
12
12
  import type { Pipeline } from "./pipeline";
13
13
  import type { Queue } from "./queue";
14
+ import type { VectorizeIndex } from "./vectorize-index";
14
15
  import type { Worker } from "./worker";
15
16
  import type { Workflow } from "./workflow";
16
17
 
@@ -31,6 +32,7 @@ export type Binding =
31
32
  | R2Bucket
32
33
  | Secret
33
34
  | string
35
+ | VectorizeIndex
34
36
  | Worker
35
37
  | Workflow;
36
38
 
@@ -8,6 +8,7 @@ import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durabl
8
8
  import type { KVNamespace as _KVNamespace } from "./kv-namespace";
9
9
  import type { Pipeline as _Pipeline } from "./pipeline";
10
10
  import type { Queue as _Queue } from "./queue";
11
+ import type { VectorizeIndex as _VectorizeIndex } from "./vectorize-index";
11
12
  import type { Worker as _Worker } from "./worker";
12
13
  import type { Workflow as _Workflow } from "./workflow";
13
14
 
@@ -27,8 +28,10 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace
27
28
  ? Workflow<P>
28
29
  : T extends _D1Database
29
30
  ? D1Database
30
- : T extends _Queue
31
- ? Queue
32
- : T extends _Pipeline<infer R>
33
- ? Pipeline<R>
34
- : Service;
31
+ : T extends _VectorizeIndex
32
+ ? VectorizeIndex
33
+ : T extends _Queue
34
+ ? Queue
35
+ : T extends _Pipeline<infer R>
36
+ ? Pipeline<R>
37
+ : Service;
@@ -13,6 +13,8 @@ export * from "./permission-groups";
13
13
  export * from "./pipeline";
14
14
  export * from "./queue";
15
15
  export * from "./r2-rest-state-store";
16
+ export * from "./vectorize-index";
17
+ export * from "./vectorize-metadata-index";
16
18
  export * from "./worker";
17
19
  export { Workflow } from "./workflow";
18
20
  export * from "./wrangler.json";
@@ -0,0 +1,331 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import {
4
+ CloudflareApi,
5
+ createCloudflareApi,
6
+ type CloudflareApiOptions,
7
+ } from "./api";
8
+ import { CloudflareApiError, handleApiError } from "./api-error";
9
+
10
+ /**
11
+ * Properties for creating or updating a Vectorize Index
12
+ */
13
+ export interface VectorizeIndexProps extends CloudflareApiOptions {
14
+ /**
15
+ * Name of the index
16
+ */
17
+ name: string;
18
+
19
+ /**
20
+ * Optional description of the index
21
+ */
22
+ description?: string;
23
+
24
+ /**
25
+ * Dimensions of the vectors
26
+ */
27
+ dimensions: number;
28
+
29
+ /**
30
+ * Distance metric used for vector similarity
31
+ */
32
+ metric: "cosine" | "euclidean" | "dot_product";
33
+
34
+ /**
35
+ * Whether to delete the index if removed
36
+ * If set to false, the index will remain but the resource will be removed from state
37
+ *
38
+ * @default true
39
+ */
40
+ delete?: boolean;
41
+
42
+ /**
43
+ * Whether to adopt an existing index with the same name if it exists
44
+ * If true and an index with the same name exists, it will be adopted rather than creating a new one
45
+ *
46
+ * @default false
47
+ */
48
+ adopt?: boolean;
49
+ }
50
+
51
+ /**
52
+ * Output returned after Vectorize Index creation/update
53
+ */
54
+ export interface VectorizeIndex
55
+ extends Resource<"cloudflare::VectorizeIndex">,
56
+ VectorizeIndexProps {
57
+ type: "vectorize";
58
+
59
+ /**
60
+ * The unique identifier for the index (same as name)
61
+ */
62
+ id: string;
63
+
64
+ /**
65
+ * Time at which the index was created
66
+ */
67
+ createdAt?: number;
68
+ }
69
+
70
+ /**
71
+ * Creates and manages Cloudflare Vectorize Indexes.
72
+ *
73
+ * Vectorize is Cloudflare's vector database that enables vector search within Cloudflare Workers.
74
+ *
75
+ * @example
76
+ * // Create a basic vector index for text embeddings
77
+ * const basicIndex = await VectorizeIndex("text-embeddings", {
78
+ * name: "text-embeddings",
79
+ * config: {
80
+ * dimensions: 768,
81
+ * metric: "cosine"
82
+ * }
83
+ * });
84
+ *
85
+ * @example
86
+ * // Create a vector index with a description
87
+ * const descIndex = await VectorizeIndex("image-embeddings", {
88
+ * name: "image-embeddings",
89
+ * description: "Vector index for image embeddings using CLIP model",
90
+ * config: {
91
+ * dimensions: 512,
92
+ * metric: "cosine"
93
+ * }
94
+ * });
95
+ *
96
+ * @example
97
+ * // Adopt an existing index if it already exists instead of failing
98
+ * const existingIndex = await VectorizeIndex("existing-index", {
99
+ * name: "existing-index",
100
+ * adopt: true,
101
+ * config: {
102
+ * dimensions: 1024,
103
+ * metric: "euclidean"
104
+ * }
105
+ * });
106
+ *
107
+ * @see https://developers.cloudflare.com/vectorize/
108
+ */
109
+ export const VectorizeIndex = Resource(
110
+ "cloudflare::VectorizeIndex",
111
+ async function (
112
+ this: Context<VectorizeIndex>,
113
+ id: string,
114
+ props: VectorizeIndexProps
115
+ ): Promise<VectorizeIndex> {
116
+ const api = await createCloudflareApi(props);
117
+ const indexName = props.name || id;
118
+
119
+ if (this.phase === "delete") {
120
+ console.log("Deleting Vectorize index:", indexName);
121
+ if (props.delete !== false) {
122
+ // Delete Vectorize index
123
+ await deleteIndex(api, indexName);
124
+ }
125
+
126
+ // Return void (a deleted index has no content)
127
+ return this.destroy();
128
+ } else {
129
+ let indexData: CloudflareVectorizeResponse;
130
+
131
+ if (this.phase === "create") {
132
+ console.log("Creating Vectorize index:", indexName);
133
+ try {
134
+ indexData = await createIndex(api, indexName, {
135
+ ...props,
136
+ name: indexName,
137
+ });
138
+ } catch (error) {
139
+ // Check if this is a "index already exists" error and adopt is enabled
140
+ if (
141
+ props.adopt &&
142
+ error instanceof CloudflareApiError &&
143
+ error.message.includes("already exists")
144
+ ) {
145
+ console.log(`Index ${indexName} already exists, adopting it`);
146
+ // Find the existing index
147
+ indexData = await getIndex(api, indexName);
148
+ } else {
149
+ // Re-throw the error if adopt is false or it's not a "index already exists" error
150
+ throw error;
151
+ }
152
+ }
153
+ } else {
154
+ // Update operation is not supported by Vectorize API
155
+ throw new Error(
156
+ "Updating Vectorize indexes is not supported by the Cloudflare API. " +
157
+ "To change an index, delete it and create a new one with the desired configuration."
158
+ );
159
+ }
160
+
161
+ return this({
162
+ type: "vectorize",
163
+ id: indexName,
164
+ name: indexName,
165
+ description: props.description,
166
+ dimensions: indexData.result.config.dimensions,
167
+ metric: indexData.result.config.metric as
168
+ | "cosine"
169
+ | "euclidean"
170
+ | "dot_product",
171
+ accountId: api.accountId,
172
+ createdAt: indexData.result.created_on
173
+ ? new Date(indexData.result.created_on).getTime()
174
+ : undefined,
175
+ });
176
+ }
177
+ }
178
+ );
179
+
180
+ interface CloudflareVectorizeResponse {
181
+ result: {
182
+ name: string;
183
+ description?: string;
184
+ created_on?: string;
185
+ config: {
186
+ dimensions: number;
187
+ metric: string;
188
+ };
189
+ };
190
+ success: boolean;
191
+ errors: Array<{ code: number; message: string }>;
192
+ messages: string[];
193
+ }
194
+
195
+ /**
196
+ * Create a new Vectorize index
197
+ */
198
+ export async function createIndex(
199
+ api: CloudflareApi,
200
+ indexName: string,
201
+ props: VectorizeIndexProps
202
+ ): Promise<CloudflareVectorizeResponse> {
203
+ // Create new Vectorize index
204
+ const createPayload: any = {
205
+ name: indexName,
206
+ config: {
207
+ dimensions: props.dimensions,
208
+ metric: props.metric,
209
+ },
210
+ };
211
+
212
+ if (props.description) {
213
+ createPayload.description = props.description;
214
+ }
215
+
216
+ const createResponse = await api.post(
217
+ `/accounts/${api.accountId}/vectorize/v2/indexes`,
218
+ createPayload
219
+ );
220
+
221
+ if (!createResponse.ok) {
222
+ return await handleApiError(
223
+ createResponse,
224
+ "creating",
225
+ "Vectorize index",
226
+ indexName
227
+ );
228
+ }
229
+
230
+ return (await createResponse.json()) as CloudflareVectorizeResponse;
231
+ }
232
+
233
+ /**
234
+ * Get a Vectorize index
235
+ */
236
+ export async function getIndex(
237
+ api: CloudflareApi,
238
+ indexName: string
239
+ ): Promise<CloudflareVectorizeResponse> {
240
+ const response = await api.get(
241
+ `/accounts/${api.accountId}/vectorize/v2/indexes/${indexName}`
242
+ );
243
+
244
+ if (!response.ok) {
245
+ return await handleApiError(
246
+ response,
247
+ "getting",
248
+ "Vectorize index",
249
+ indexName
250
+ );
251
+ }
252
+
253
+ return (await response.json()) as CloudflareVectorizeResponse;
254
+ }
255
+
256
+ /**
257
+ * Delete a Vectorize index
258
+ */
259
+ export async function deleteIndex(
260
+ api: CloudflareApi,
261
+ indexName: string
262
+ ): Promise<void> {
263
+ // Delete Vectorize index
264
+ const deleteResponse = await api.delete(
265
+ `/accounts/${api.accountId}/vectorize/v2/indexes/${indexName}`
266
+ );
267
+
268
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
269
+ const errorData: any = await deleteResponse.json().catch(() => ({
270
+ errors: [{ message: deleteResponse.statusText }],
271
+ }));
272
+ throw new CloudflareApiError(
273
+ `Error deleting Vectorize index '${indexName}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
274
+ deleteResponse
275
+ );
276
+ }
277
+ }
278
+
279
+ /**
280
+ * List all Vectorize indexes in an account
281
+ */
282
+ export async function listIndexes(
283
+ api: CloudflareApi
284
+ ): Promise<{ name: string; description?: string }[]> {
285
+ const response = await api.get(
286
+ `/accounts/${api.accountId}/vectorize/v2/indexes`
287
+ );
288
+
289
+ if (!response.ok) {
290
+ throw new CloudflareApiError(
291
+ `Failed to list indexes: ${response.statusText}`,
292
+ response
293
+ );
294
+ }
295
+
296
+ const data = (await response.json()) as {
297
+ success: boolean;
298
+ errors?: Array<{ code: number; message: string }>;
299
+ result?: Array<{
300
+ name: string;
301
+ description?: string;
302
+ }>;
303
+ };
304
+
305
+ if (!data.success) {
306
+ const errorMessage = data.errors?.[0]?.message || "Unknown error";
307
+ throw new Error(`Failed to list indexes: ${errorMessage}`);
308
+ }
309
+
310
+ // Transform API response
311
+ return (data.result || []).map((index) => ({
312
+ name: index.name,
313
+ description: index.description,
314
+ }));
315
+ }
316
+
317
+ /**
318
+ * Update a Vectorize index
319
+ *
320
+ * Note: The Cloudflare Vectorize API does not support updating indexes.
321
+ * This function will always throw an error.
322
+ */
323
+ export async function updateIndex(
324
+ api: CloudflareApi,
325
+ indexName: string,
326
+ props: VectorizeIndexProps
327
+ ): Promise<CloudflareVectorizeResponse> {
328
+ throw new Error(
329
+ "Updating Vectorize indexes is not supported by the Cloudflare API. To change an index, delete it and create a new one."
330
+ );
331
+ }
@@ -0,0 +1,239 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import {
4
+ CloudflareApi,
5
+ createCloudflareApi,
6
+ type CloudflareApiOptions,
7
+ } from "./api";
8
+ import { CloudflareApiError, handleApiError } from "./api-error";
9
+ import { VectorizeIndex } from "./vectorize-index";
10
+
11
+ /**
12
+ * Properties for creating or deleting a Vectorize Metadata Index
13
+ */
14
+ export interface VectorizeMetadataIndexProps extends CloudflareApiOptions {
15
+ /**
16
+ * Parent Vectorize Index
17
+ */
18
+ index: VectorizeIndex;
19
+
20
+ /**
21
+ * Name of the property in the metadata to create an index for
22
+ */
23
+ propertyName: string;
24
+
25
+ /**
26
+ * Type of the metadata index
27
+ */
28
+ indexType: "string" | "number" | "boolean";
29
+ }
30
+
31
+ /**
32
+ * Output returned after Vectorize Metadata Index creation/deletion
33
+ */
34
+ export interface VectorizeMetadataIndex
35
+ extends Resource<"cloudflare::VectorizeMetadataIndex">,
36
+ VectorizeMetadataIndexProps {
37
+ /**
38
+ * ID of this metadata index (derived from propertyName)
39
+ */
40
+ id: string;
41
+
42
+ /**
43
+ * Mutation ID returned by the API after creation
44
+ */
45
+ mutationId?: string;
46
+ }
47
+
48
+ /**
49
+ * Creates and manages Cloudflare Vectorize Metadata Indexes.
50
+ *
51
+ * Vectorize Metadata Indexes enable filtering based on metadata properties when querying vectors.
52
+ * Each Vectorize Index can have up to 10 metadata indexes.
53
+ *
54
+ * @example
55
+ * // First create a Vectorize Index
56
+ * const vectorIndex = await VectorizeIndex("documents", {
57
+ * name: "documents",
58
+ * config: {
59
+ * dimensions: 768,
60
+ * metric: "cosine"
61
+ * }
62
+ * });
63
+ *
64
+ * // Then create a metadata index for the "category" property
65
+ * const categoryIndex = await VectorizeMetadataIndex("category-index", {
66
+ * index: vectorIndex,
67
+ * propertyName: "category",
68
+ * indexType: "string"
69
+ * });
70
+ *
71
+ * @example
72
+ * // Create a metadata index for a numeric property
73
+ * const yearIndex = await VectorizeMetadataIndex("year-index", {
74
+ * index: vectorIndex,
75
+ * propertyName: "year",
76
+ * indexType: "number"
77
+ * });
78
+ *
79
+ * @see https://developers.cloudflare.com/vectorize/
80
+ */
81
+ export const VectorizeMetadataIndex = Resource(
82
+ "cloudflare::VectorizeMetadataIndex",
83
+ async function (
84
+ this: Context<VectorizeMetadataIndex>,
85
+ id: string,
86
+ props: VectorizeMetadataIndexProps
87
+ ): Promise<VectorizeMetadataIndex> {
88
+ const api = await createCloudflareApi(props);
89
+ const indexName = props.index.name;
90
+ const propertyName = props.propertyName;
91
+
92
+ if (this.phase === "delete") {
93
+ // Delete metadata index
94
+ try {
95
+ await deleteMetadataIndex(api, indexName, propertyName);
96
+ } catch (error) {
97
+ if (
98
+ error instanceof CloudflareApiError &&
99
+ error.status === 400 &&
100
+ error.message.includes("does not exist")
101
+ ) {
102
+ // Index doesn't exist, which is what we want
103
+ } else {
104
+ throw error;
105
+ }
106
+ }
107
+ return this.destroy();
108
+ } else if (this.phase === "update") {
109
+ // Update operation is not supported
110
+ throw new Error(
111
+ "Updating Vectorize metadata indexes is not supported by the Cloudflare API. " +
112
+ "To change a metadata index, delete it and create a new one with the desired configuration."
113
+ );
114
+ } else {
115
+ const indexData = await createMetadataIndex(api, indexName, props);
116
+
117
+ return this({
118
+ id: propertyName, // Use propertyName as ID
119
+ index: props.index,
120
+ propertyName: props.propertyName,
121
+ indexType: props.indexType,
122
+ accountId: api.accountId,
123
+ mutationId: indexData.result.mutationId,
124
+ });
125
+ }
126
+ }
127
+ );
128
+
129
+ interface CloudflareMetadataIndexResponse {
130
+ result: {
131
+ mutationId: string;
132
+ };
133
+ success: boolean;
134
+ errors: Array<{ code: number; message: string }>;
135
+ messages: string[];
136
+ }
137
+
138
+ interface CloudflareMetadataIndexListResponse {
139
+ result: {
140
+ metadataIndexes: Array<{
141
+ propertyName: string;
142
+ indexType: "string" | "number" | "boolean";
143
+ }>;
144
+ };
145
+ success: boolean;
146
+ errors: Array<{ code: number; message: string }>;
147
+ messages: string[];
148
+ }
149
+
150
+ /**
151
+ * Create a new Vectorize metadata index
152
+ */
153
+ export async function createMetadataIndex(
154
+ api: CloudflareApi,
155
+ indexName: string,
156
+ props: VectorizeMetadataIndexProps
157
+ ): Promise<CloudflareMetadataIndexResponse> {
158
+ // Create new metadata index
159
+ const createPayload = {
160
+ propertyName: props.propertyName,
161
+ indexType: props.indexType,
162
+ };
163
+
164
+ const createResponse = await api.post(
165
+ `/accounts/${api.accountId}/vectorize/v2/indexes/${indexName}/metadata_index/create`,
166
+ createPayload
167
+ );
168
+
169
+ if (!createResponse.ok) {
170
+ return await handleApiError(
171
+ createResponse,
172
+ "creating",
173
+ "Vectorize metadata index",
174
+ props.propertyName
175
+ );
176
+ }
177
+
178
+ return (await createResponse.json()) as CloudflareMetadataIndexResponse;
179
+ }
180
+
181
+ /**
182
+ * Delete a Vectorize metadata index
183
+ */
184
+ export async function deleteMetadataIndex(
185
+ api: CloudflareApi,
186
+ indexName: string,
187
+ propertyName: string
188
+ ): Promise<void> {
189
+ const deleteResponse = await api.post(
190
+ `/accounts/${api.accountId}/vectorize/v2/indexes/${indexName}/metadata_index/delete`,
191
+ {
192
+ propertyName,
193
+ }
194
+ );
195
+
196
+ if (!deleteResponse.ok) {
197
+ await handleApiError(
198
+ deleteResponse,
199
+ "deleting",
200
+ "Vectorize metadata index",
201
+ propertyName
202
+ );
203
+ }
204
+ }
205
+
206
+ /**
207
+ * List all metadata indexes for a Vectorize index
208
+ */
209
+ export async function listMetadataIndexes(
210
+ api: CloudflareApi,
211
+ indexName: string
212
+ ): Promise<
213
+ { propertyName: string; indexType: "string" | "number" | "boolean" }[]
214
+ > {
215
+ const response = await api.get(
216
+ `/accounts/${api.accountId}/vectorize/v2/indexes/${indexName}/metadata_index/list`
217
+ );
218
+
219
+ if (response.status === 410) {
220
+ // Gone
221
+ return [];
222
+ }
223
+
224
+ if (!response.ok) {
225
+ throw new CloudflareApiError(
226
+ `Failed to list metadata indexes: ${response.statusText}`,
227
+ response
228
+ );
229
+ }
230
+
231
+ const data = (await response.json()) as CloudflareMetadataIndexListResponse;
232
+
233
+ if (!data.success) {
234
+ const errorMessage = data.errors?.[0]?.message || "Unknown error";
235
+ throw new Error(`Failed to list metadata indexes: ${errorMessage}`);
236
+ }
237
+
238
+ return data.result.metadataIndexes;
239
+ }
@@ -611,6 +611,12 @@ async function prepareWorkerMetadata<B extends Bindings>(
611
611
  name: bindingName,
612
612
  pipeline: binding.name,
613
613
  });
614
+ } else if (binding.type === "vectorize") {
615
+ meta.bindings.push({
616
+ type: "vectorize",
617
+ name: bindingName,
618
+ index_name: binding.name,
619
+ });
614
620
  } else {
615
621
  // @ts-expect-error - we should never reach here
616
622
  throw new Error(`Unsupported binding type: ${binding.type}`);
@@ -187,6 +187,14 @@ export interface WranglerJsonSpec {
187
187
  class_name: string;
188
188
  }[];
189
189
 
190
+ /**
191
+ * Vectorize index bindings
192
+ */
193
+ vectorize_indexes?: {
194
+ binding: string;
195
+ index_name: string;
196
+ }[];
197
+
190
198
  /**
191
199
  * Plain text bindings (vars)
192
200
  */
@@ -239,6 +247,7 @@ function processBindings(spec: WranglerJsonSpec, bindings: Bindings): void {
239
247
  database_name: string;
240
248
  }[] = [];
241
249
  const queues: { binding: string; queue: string }[] = [];
250
+ const vectorizeIndexes: { binding: string; index_name: string }[] = [];
242
251
 
243
252
  // Process each binding
244
253
  for (const [bindingName, binding] of Object.entries(bindings)) {
@@ -302,6 +311,11 @@ function processBindings(spec: WranglerJsonSpec, bindings: Bindings): void {
302
311
  binding: bindingName,
303
312
  queue: binding.name,
304
313
  });
314
+ } else if (binding.type === "vectorize") {
315
+ vectorizeIndexes.push({
316
+ binding: bindingName,
317
+ index_name: binding.name,
318
+ });
305
319
  }
306
320
  }
307
321
 
@@ -327,4 +341,8 @@ function processBindings(spec: WranglerJsonSpec, bindings: Bindings): void {
327
341
  if (queues.length > 0) {
328
342
  spec.queues = queues;
329
343
  }
344
+
345
+ if (vectorizeIndexes.length > 0) {
346
+ spec.vectorize_indexes = vectorizeIndexes;
347
+ }
330
348
  }