alchemy 0.7.3 → 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.
@@ -94,14 +94,21 @@ export interface Bundle extends Resource<"esbuild::Bundle"> {
94
94
  */
95
95
  export declare const Bundle: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<Bundle>, id: string, props: BundleProps) => Promise<Bundle>);
96
96
  export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<{
97
- bundle: boolean;
97
+ entryPoints: string[];
98
+ outfile: string;
99
+ bundle: true;
100
+ format: "iife" | "cjs" | "esm" | undefined;
101
+ target: string | string[] | undefined;
102
+ minify: boolean | undefined;
103
+ sourcemap: boolean | "inline" | "external" | "both" | undefined;
104
+ external: string[];
105
+ platform: "browser" | "node" | "neutral" | undefined;
106
+ metafile: true;
107
+ write: true;
98
108
  splitting?: boolean;
99
109
  preserveSymlinks?: boolean;
100
- outfile: string;
101
- metafile: boolean;
102
110
  outdir?: string;
103
111
  outbase?: string;
104
- external: string[];
105
112
  packages?: "bundle" | "external";
106
113
  alias?: Record<string, string>;
107
114
  loader?: {
@@ -110,7 +117,6 @@ export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<
110
117
  resolveExtensions?: string[];
111
118
  mainFields?: string[];
112
119
  conditions?: string[];
113
- write: boolean;
114
120
  allowOverwrite?: boolean;
115
121
  tsconfig?: string;
116
122
  outExtension?: {
@@ -127,30 +133,21 @@ export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<
127
133
  footer?: {
128
134
  [type: string]: string;
129
135
  };
130
- entryPoints: string[] | Record<string, string> | {
131
- in: string;
132
- out: string;
133
- }[];
134
136
  stdin?: esbuild.StdinOptions;
135
137
  plugins?: esbuild.Plugin[];
136
138
  absWorkingDir?: string;
137
139
  nodePaths?: string[];
138
- sourcemap: boolean | "linked" | "inline" | "external" | "both";
139
140
  legalComments?: "none" | "inline" | "eof" | "linked" | "external";
140
141
  sourceRoot?: string;
141
142
  sourcesContent?: boolean;
142
- format: esbuild.Format;
143
143
  globalName?: string;
144
- target: string | string[];
145
144
  supported?: Record<string, boolean>;
146
- platform: esbuild.Platform;
147
145
  mangleProps?: RegExp;
148
146
  reserveProps?: RegExp;
149
147
  mangleQuoted?: boolean;
150
148
  mangleCache?: Record<string, string | false>;
151
149
  drop?: esbuild.Drop[];
152
150
  dropLabels?: string[];
153
- minify: boolean;
154
151
  minifyWhitespace?: boolean;
155
152
  minifyIdentifiers?: boolean;
156
153
  minifySyntax?: boolean;
@@ -47,8 +47,8 @@ export const Bundle = Resource("esbuild::Bundle", {
47
47
  });
48
48
  export async function bundle(props) {
49
49
  const outputPath = getOutputPath(props);
50
- // Build the bundle
51
50
  return await esbuild.build({
51
+ ...props.options,
52
52
  entryPoints: [props.entryPoint],
53
53
  outfile: outputPath,
54
54
  bundle: true,
@@ -56,11 +56,14 @@ export async function bundle(props) {
56
56
  target: props.target,
57
57
  minify: props.minify,
58
58
  sourcemap: props.sourcemap,
59
- external: props.external,
59
+ external: [
60
+ "node:async_hooks",
61
+ ...(props.external ?? []),
62
+ ...(props.options?.external ?? []),
63
+ ],
60
64
  platform: props.platform,
61
65
  metafile: true,
62
66
  write: true,
63
- ...props.options,
64
67
  });
65
68
  }
66
69
  function getOutputPath(props) {
package/lib/test/bun.js CHANGED
@@ -79,6 +79,9 @@ export function test(meta, defaultOptions) {
79
79
  function test(...args) {
80
80
  const testName = args[0];
81
81
  const _options = typeof args[1] === "object" ? args[1] : undefined;
82
+ const timeout = typeof args[args.length - 1] === "number"
83
+ ? args[args.length - 1]
84
+ : 120000;
82
85
  const spread = (obj) => obj && typeof obj === "object"
83
86
  ? Object.fromEntries(Object.entries(obj).flatMap(([k, v]) => v !== undefined ? [[k, v]] : []))
84
87
  : {};
@@ -110,7 +113,7 @@ export function test(meta, defaultOptions) {
110
113
  await destroy(scope);
111
114
  }
112
115
  }
113
- });
116
+ }, timeout);
114
117
  });
115
118
  }
116
119
  }
@@ -35,7 +35,7 @@ export async function processFrontmatterFiles(directoryPath, linkPrefix) {
35
35
  const frontmatter = frontmatterMatch[1];
36
36
  const orderMatch = frontmatter.match(/order:\s*(\d+)/);
37
37
  if (orderMatch) {
38
- order = parseInt(orderMatch[1]);
38
+ order = parseFloat(orderMatch[1]);
39
39
  }
40
40
  const titleMatch = frontmatter.match(/title:\s*(.+)/);
41
41
  if (titleMatch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.7.3",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
package/src/aws/table.ts CHANGED
@@ -143,7 +143,7 @@ export const Table = Resource(
143
143
  async function (
144
144
  this: Context<Table>,
145
145
  id: string,
146
- props: TableProps,
146
+ props: TableProps
147
147
  ): Promise<Table> {
148
148
  const client = new DynamoDBClient({});
149
149
 
@@ -154,15 +154,13 @@ export const Table = Resource(
154
154
  client.send(
155
155
  new DeleteTableCommand({
156
156
  TableName: props.tableName,
157
- }),
158
- ),
157
+ })
158
+ )
159
159
  );
160
160
  },
161
- (error) =>
162
- error instanceof ResourceInUseException ||
163
- error instanceof InternalServerError,
161
+ isRetryableError,
164
162
  10, // Max attempts
165
- 200, // Initial delay in ms
163
+ 200 // Initial delay in ms
166
164
  );
167
165
 
168
166
  // Wait for table to be deleted
@@ -175,7 +173,7 @@ export const Table = Resource(
175
173
  await client.send(
176
174
  new DescribeTableCommand({
177
175
  TableName: props.tableName,
178
- }),
176
+ })
179
177
  );
180
178
  // If we get here, table still exists
181
179
  retryCount++;
@@ -193,7 +191,7 @@ export const Table = Resource(
193
191
 
194
192
  if (!tableDeleted) {
195
193
  throw new Error(
196
- `Timed out waiting for table ${props.tableName} to be deleted`,
194
+ `Timed out waiting for table ${props.tableName} to be deleted`
197
195
  );
198
196
  }
199
197
 
@@ -233,7 +231,7 @@ export const Table = Resource(
233
231
  const describeResponse = await client.send(
234
232
  new DescribeTableCommand({
235
233
  TableName: props.tableName,
236
- }),
234
+ })
237
235
  );
238
236
 
239
237
  // If table exists and is ACTIVE, no need to create it
@@ -267,16 +265,16 @@ export const Table = Resource(
267
265
  Value,
268
266
  }))
269
267
  : undefined,
270
- }),
268
+ })
271
269
  );
272
270
  } else {
273
271
  throw error;
274
272
  }
275
273
  }
276
274
  },
277
- (error) => error instanceof ResourceInUseException,
275
+ isRetryableError,
278
276
  10, // Max attempts
279
- 200, // Initial delay in ms
277
+ 200 // Initial delay in ms
280
278
  );
281
279
 
282
280
  // Wait for table to be active with timeout
@@ -290,7 +288,7 @@ export const Table = Resource(
290
288
  const response = await client.send(
291
289
  new DescribeTableCommand({
292
290
  TableName: props.tableName,
293
- }),
291
+ })
294
292
  );
295
293
 
296
294
  tableActive = response.Table?.TableStatus === "ACTIVE";
@@ -313,7 +311,7 @@ export const Table = Resource(
313
311
 
314
312
  if (!tableActive) {
315
313
  throw new Error(
316
- `Timed out waiting for table ${props.tableName} to become active`,
314
+ `Timed out waiting for table ${props.tableName} to become active`
317
315
  );
318
316
  }
319
317
 
@@ -324,5 +322,25 @@ export const Table = Resource(
324
322
  tableId: tableDescription!.TableId!,
325
323
  });
326
324
  }
327
- },
325
+ }
328
326
  );
327
+
328
+ const retryableErrors = [
329
+ "ResourceInUseException",
330
+ "ResourceNotFoundException",
331
+ "InternalServerError",
332
+ "ThrottlingException",
333
+ "ProvisionedThroughputExceededException",
334
+ "LimitExceededException",
335
+ "RequestLimitExceeded",
336
+ ];
337
+
338
+ function isRetryableError(error: any) {
339
+ return (
340
+ error instanceof ResourceInUseException ||
341
+ error instanceof InternalServerError ||
342
+ retryableErrors.includes(error?.name) ||
343
+ retryableErrors.includes(error?.code) ||
344
+ error?.$metadata?.httpStatusCode === 500
345
+ );
346
+ }
@@ -49,7 +49,7 @@ export async function handleApiError(
49
49
  resourceName: string
50
50
  ): Promise<never> {
51
51
  const json: any = await response.json();
52
- const errors: { message: string }[] = json.errors || [
52
+ const errors: { message: string }[] = json?.errors || [
53
53
  { message: response.statusText },
54
54
  ];
55
55
  const errorMessage = `Error ${action} ${resourceType} '${resourceName}': ${errors[0]?.message || response.statusText}`;
@@ -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
+ }