alchemy 0.8.0 → 0.9.0
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/alchemy.d.ts +11 -18
- package/lib/alchemy.js +34 -39
- package/lib/apply.js +3 -1
- package/lib/cloudflare/api-error.js +1 -1
- package/lib/cloudflare/bindings.d.ts +2 -1
- package/lib/cloudflare/bound.d.ts +2 -1
- package/lib/cloudflare/index.d.ts +3 -0
- package/lib/cloudflare/index.js +3 -0
- package/lib/cloudflare/pipeline.js +0 -3
- package/lib/cloudflare/vectorize-index.d.ts +136 -0
- package/lib/cloudflare/vectorize-index.js +170 -0
- package/lib/cloudflare/vectorize-metadata-index.d.ts +95 -0
- package/lib/cloudflare/vectorize-metadata-index.js +120 -0
- package/lib/cloudflare/vite-site.d.ts +39 -0
- package/lib/cloudflare/vite-site.js +57 -0
- package/lib/cloudflare/worker.js +7 -0
- package/lib/cloudflare/wrangler.json.d.ts +7 -0
- package/lib/cloudflare/wrangler.json.js +11 -1
- package/lib/destroy.js +14 -2
- package/lib/fs/file-system-state-store.d.ts +1 -0
- package/lib/fs/file-system-state-store.js +23 -13
- package/lib/scope.d.ts +1 -1
- package/lib/scope.js +5 -2
- package/lib/state.d.ts +2 -2
- package/lib/test/bun.d.ts +1 -6
- package/lib/test/bun.js +18 -37
- package/package.json +1 -1
- package/src/alchemy.ts +51 -45
- package/src/apply.ts +6 -2
- package/src/cloudflare/api-error.ts +1 -1
- package/src/cloudflare/bindings.ts +2 -0
- package/src/cloudflare/bound.ts +8 -5
- package/src/cloudflare/index.ts +3 -0
- package/src/cloudflare/pipeline.ts +0 -4
- package/src/cloudflare/vectorize-index.ts +331 -0
- package/src/cloudflare/vectorize-metadata-index.ts +239 -0
- package/src/cloudflare/vite-site.ts +111 -0
- package/src/cloudflare/worker.ts +6 -0
- package/src/cloudflare/wrangler.json.ts +19 -1
- package/src/destroy.ts +25 -8
- package/src/fs/file-system-state-store.ts +23 -14
- package/src/scope.ts +9 -5
- package/src/state.ts +3 -3
- package/src/test/bun.ts +31 -54
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { alchemy } from "../alchemy";
|
|
4
|
+
import { Exec } from "../os";
|
|
5
|
+
import { Assets } from "./assets";
|
|
6
|
+
import type { Bindings } from "./bindings";
|
|
7
|
+
import { Worker } from "./worker";
|
|
8
|
+
import { WranglerJson } from "./wrangler.json";
|
|
9
|
+
|
|
10
|
+
export interface ViteSiteProps<B extends Bindings> {
|
|
11
|
+
/**
|
|
12
|
+
* The command to run to build the site
|
|
13
|
+
*/
|
|
14
|
+
command: string;
|
|
15
|
+
/**
|
|
16
|
+
* The name of the worker
|
|
17
|
+
*
|
|
18
|
+
* @default id
|
|
19
|
+
*/
|
|
20
|
+
name?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The entrypoint to your server
|
|
23
|
+
*
|
|
24
|
+
* @default - a simple server that serves static assets is generated
|
|
25
|
+
*/
|
|
26
|
+
main?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The directory containing your static assets
|
|
29
|
+
*/
|
|
30
|
+
assets: string;
|
|
31
|
+
/**
|
|
32
|
+
* The bindings to pass to the worker
|
|
33
|
+
*/
|
|
34
|
+
bindings?: B;
|
|
35
|
+
/**
|
|
36
|
+
* @default process.cwd()
|
|
37
|
+
*/
|
|
38
|
+
cwd?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ViteSite<B extends Bindings> = Promise<
|
|
42
|
+
// don't allow the ASSETS to be overriden
|
|
43
|
+
B extends { ASSETS: any } ? never : Worker<B & { ASSETS: Assets }>
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
export async function ViteSite<B extends Bindings>(
|
|
47
|
+
id: string,
|
|
48
|
+
props: ViteSiteProps<B>
|
|
49
|
+
): ViteSite<B> {
|
|
50
|
+
if (props.bindings?.ASSETS) {
|
|
51
|
+
throw new Error("ASSETS binding is reserved for internal use");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// @ts-ignore - we know the types are correct
|
|
55
|
+
return await alchemy.run(id, async () => {
|
|
56
|
+
// Create minimal wrangler.jsonc if it doesn't exist
|
|
57
|
+
|
|
58
|
+
// `building the site requires a wrangler.jsonc file to start - so initialize an empty one if it doesn't exist`
|
|
59
|
+
const cwd = props.cwd || process.cwd();
|
|
60
|
+
const wranglerPath = path.join(cwd, "wrangler.jsonc");
|
|
61
|
+
try {
|
|
62
|
+
await fs.access(wranglerPath);
|
|
63
|
+
} catch {
|
|
64
|
+
await fs.writeFile(
|
|
65
|
+
wranglerPath,
|
|
66
|
+
JSON.stringify(
|
|
67
|
+
{
|
|
68
|
+
name: id,
|
|
69
|
+
main: props.main,
|
|
70
|
+
compatibility_date: new Date().toISOString().split("T")[0],
|
|
71
|
+
},
|
|
72
|
+
null,
|
|
73
|
+
2
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await Exec("build", {
|
|
79
|
+
command: props.command,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const staticAssets = await Assets("assets", {
|
|
83
|
+
path: "./dist",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const worker = await Worker("worker", {
|
|
87
|
+
name: props.name ?? id,
|
|
88
|
+
entrypoint: props.main,
|
|
89
|
+
script: props.main
|
|
90
|
+
? undefined
|
|
91
|
+
: `
|
|
92
|
+
export default {
|
|
93
|
+
async fetch(request, env) {
|
|
94
|
+
return env.ASSETS.fetch(request);
|
|
95
|
+
},
|
|
96
|
+
};`,
|
|
97
|
+
url: true,
|
|
98
|
+
adopt: true,
|
|
99
|
+
bindings: {
|
|
100
|
+
...props.bindings,
|
|
101
|
+
ASSETS: staticAssets,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await WranglerJson("wrangler.jsonc", {
|
|
106
|
+
worker,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return worker;
|
|
110
|
+
});
|
|
111
|
+
}
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -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}`);
|
|
@@ -55,7 +55,7 @@ export const WranglerJson = Resource(
|
|
|
55
55
|
props: WranglerJsonProps
|
|
56
56
|
): Promise<WranglerJson> {
|
|
57
57
|
// Default path is wrangler.json in current directory
|
|
58
|
-
const filePath = props.path || "wrangler.
|
|
58
|
+
const filePath = props.path || "wrangler.jsonc";
|
|
59
59
|
|
|
60
60
|
if (this.phase === "delete") {
|
|
61
61
|
return this.destroy();
|
|
@@ -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
|
}
|
package/src/destroy.ts
CHANGED
|
@@ -39,7 +39,7 @@ export async function destroy<Type extends string>(
|
|
|
39
39
|
...orphan.output,
|
|
40
40
|
Scope: scope,
|
|
41
41
|
})),
|
|
42
|
-
options
|
|
42
|
+
options
|
|
43
43
|
);
|
|
44
44
|
// finally, destroy the scope container
|
|
45
45
|
await scope.deinit();
|
|
@@ -52,10 +52,19 @@ export async function destroy<Type extends string>(
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
if (instance.Kind === "alchemy::Scope") {
|
|
56
|
+
const scope = new Scope({
|
|
57
|
+
parent: instance.Scope,
|
|
58
|
+
scopeName: instance.ID,
|
|
59
|
+
});
|
|
60
|
+
console.log("Destroying scope", scope.chain.join("/"));
|
|
61
|
+
return await destroy(scope, options);
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
const Provider: Provider<Type> | undefined = PROVIDERS.get(instance.Kind);
|
|
56
65
|
if (!Provider) {
|
|
57
66
|
throw new Error(
|
|
58
|
-
`Cannot destroy resource "${instance.FQN}" type ${instance.Kind} - no provider found. You may need to import the provider in your alchemy.config.ts
|
|
67
|
+
`Cannot destroy resource "${instance.FQN}" type ${instance.Kind} - no provider found. You may need to import the provider in your alchemy.config.ts.`
|
|
59
68
|
);
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -93,10 +102,18 @@ export async function destroy<Type extends string>(
|
|
|
93
102
|
let nestedScope: Scope | undefined;
|
|
94
103
|
try {
|
|
95
104
|
// BUG: this does not restore persisted scope
|
|
96
|
-
await alchemy.run(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
await alchemy.run(
|
|
106
|
+
instance.ID,
|
|
107
|
+
{
|
|
108
|
+
// TODO(sam): this is an awful hack to differentiate between naked scopes and resources
|
|
109
|
+
isResource: instance.Kind !== "alchemy::Scope",
|
|
110
|
+
parent: scope,
|
|
111
|
+
},
|
|
112
|
+
async (scope) => {
|
|
113
|
+
nestedScope = scope;
|
|
114
|
+
return await Provider.handler.bind(ctx)(instance.ID, state.props);
|
|
115
|
+
}
|
|
116
|
+
);
|
|
100
117
|
} catch (err) {
|
|
101
118
|
if (err instanceof DestroyedSignal) {
|
|
102
119
|
// TODO: should we fail if the DestroyedSignal is not thrown?
|
|
@@ -125,11 +142,11 @@ export namespace destroy {
|
|
|
125
142
|
if (options?.strategy !== "parallel") {
|
|
126
143
|
const sorted = resources.sort((a, b) => b.Seq - a.Seq);
|
|
127
144
|
for (const resource of sorted) {
|
|
128
|
-
await destroy(resource);
|
|
145
|
+
await destroy(resource, options);
|
|
129
146
|
}
|
|
130
147
|
} else {
|
|
131
148
|
await Promise.all(
|
|
132
|
-
resources.map((resource) => destroy(resource, options))
|
|
149
|
+
resources.map((resource) => destroy(resource, options))
|
|
133
150
|
);
|
|
134
151
|
}
|
|
135
152
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from "node:fs
|
|
1
|
+
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { Scope } from "../scope";
|
|
4
4
|
import { deserialize, serialize } from "../serde";
|
|
@@ -9,17 +9,21 @@ const stateRootDir = path.join(process.cwd(), ".alchemy");
|
|
|
9
9
|
|
|
10
10
|
export class FileSystemStateStore implements StateStore {
|
|
11
11
|
public readonly dir: string;
|
|
12
|
+
private initialized = false;
|
|
12
13
|
constructor(public readonly scope: Scope) {
|
|
13
14
|
this.dir = path.join(stateRootDir, ...scope.chain);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
async init(): Promise<void> {
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
if (this.initialized) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.initialized = true;
|
|
22
|
+
await fs.promises.mkdir(this.dir, { recursive: true });
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
async deinit(): Promise<void> {
|
|
22
|
-
await ignore("ENOENT", () => fs.rmdir(this.dir));
|
|
26
|
+
await ignore("ENOENT", () => fs.promises.rmdir(this.dir));
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
async count(): Promise<number> {
|
|
@@ -28,7 +32,7 @@ export class FileSystemStateStore implements StateStore {
|
|
|
28
32
|
|
|
29
33
|
async list(): Promise<string[]> {
|
|
30
34
|
try {
|
|
31
|
-
const files = await fs.readdir(this.dir, {
|
|
35
|
+
const files = await fs.promises.readdir(this.dir, {
|
|
32
36
|
withFileTypes: true,
|
|
33
37
|
});
|
|
34
38
|
return files
|
|
@@ -45,7 +49,7 @@ export class FileSystemStateStore implements StateStore {
|
|
|
45
49
|
|
|
46
50
|
async get(key: string): Promise<State | undefined> {
|
|
47
51
|
try {
|
|
48
|
-
const content = await fs.readFile(
|
|
52
|
+
const content = await fs.promises.readFile(this.getPath(key), "utf8");
|
|
49
53
|
const state = (await deserialize(
|
|
50
54
|
this.scope,
|
|
51
55
|
JSON.parse(content)
|
|
@@ -64,14 +68,22 @@ export class FileSystemStateStore implements StateStore {
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
async set(key: string, value: State): Promise<void> {
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
await this.init();
|
|
72
|
+
await fs.promises.writeFile(
|
|
73
|
+
this.getPath(key),
|
|
69
74
|
JSON.stringify(await serialize(this.scope, value), null, 2)
|
|
70
75
|
);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
async delete(key: string): Promise<void> {
|
|
74
|
-
|
|
79
|
+
try {
|
|
80
|
+
return await fs.promises.unlink(this.getPath(key));
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
if (error.code === "ENOENT") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
async all(): Promise<Record<string, State>> {
|
|
@@ -94,16 +106,13 @@ export class FileSystemStateStore implements StateStore {
|
|
|
94
106
|
);
|
|
95
107
|
}
|
|
96
108
|
|
|
97
|
-
private
|
|
109
|
+
private getPath(key: string): string {
|
|
98
110
|
if (key.includes(":")) {
|
|
99
111
|
throw new Error(`ID cannot include colons: ${key}`);
|
|
100
112
|
}
|
|
101
113
|
if (key.includes("/")) {
|
|
102
114
|
key = key.replaceAll("/", ":");
|
|
103
115
|
}
|
|
104
|
-
|
|
105
|
-
const dir = path.dirname(file);
|
|
106
|
-
await fs.mkdir(dir, { recursive: true });
|
|
107
|
-
return file;
|
|
116
|
+
return path.join(this.dir, `${key}.json`);
|
|
108
117
|
}
|
|
109
118
|
}
|
package/src/scope.ts
CHANGED
|
@@ -8,7 +8,7 @@ const scopeStorage = new AsyncLocalStorage<Scope>();
|
|
|
8
8
|
|
|
9
9
|
export type ScopeOptions = {
|
|
10
10
|
appName?: string;
|
|
11
|
-
stage
|
|
11
|
+
stage?: string;
|
|
12
12
|
parent?: Scope;
|
|
13
13
|
scopeName?: string;
|
|
14
14
|
password?: string;
|
|
@@ -16,6 +16,9 @@ export type ScopeOptions = {
|
|
|
16
16
|
quiet?: boolean;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// TODO: support browser
|
|
20
|
+
const DEFAULT_STAGE = process.env.ALCHEMY_STAGE ?? process.env.USER ?? "dev";
|
|
21
|
+
|
|
19
22
|
export class Scope {
|
|
20
23
|
public static get(): Scope | undefined {
|
|
21
24
|
return scopeStorage.getStore();
|
|
@@ -42,14 +45,14 @@ export class Scope {
|
|
|
42
45
|
|
|
43
46
|
constructor(options: ScopeOptions) {
|
|
44
47
|
this.appName = options.appName;
|
|
45
|
-
this.stage = options
|
|
48
|
+
this.stage = options?.stage ?? DEFAULT_STAGE;
|
|
46
49
|
this.scopeName = options.scopeName ?? null;
|
|
47
50
|
this.parent = options.parent ?? Scope.get();
|
|
48
51
|
this.quiet = options.quiet ?? this.parent?.quiet ?? false;
|
|
49
52
|
if (this.parent && !this.scopeName) {
|
|
50
53
|
throw new Error("Scope name is required when creating a child scope");
|
|
51
54
|
}
|
|
52
|
-
this.password = options.password;
|
|
55
|
+
this.password = options.password ?? this.parent?.password;
|
|
53
56
|
this.state = options.stateStore
|
|
54
57
|
? options.stateStore(this)
|
|
55
58
|
: new FileSystemStateStore(this);
|
|
@@ -90,6 +93,7 @@ export class Scope {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
public async deinit() {
|
|
96
|
+
await this.parent?.state.delete(this.scopeName!);
|
|
93
97
|
await this.state.deinit?.();
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -111,10 +115,10 @@ export class Scope {
|
|
|
111
115
|
const resourceIds = await this.state.list();
|
|
112
116
|
const aliveIds = new Set(this.resources.keys());
|
|
113
117
|
const orphanIds = Array.from(
|
|
114
|
-
resourceIds.filter((id) => !aliveIds.has(id))
|
|
118
|
+
resourceIds.filter((id) => !aliveIds.has(id))
|
|
115
119
|
);
|
|
116
120
|
const orphans = await Promise.all(
|
|
117
|
-
orphanIds.map(async (id) => (await this.state.get(id))!.output)
|
|
121
|
+
orphanIds.map(async (id) => (await this.state.get(id))!.output)
|
|
118
122
|
);
|
|
119
123
|
await destroy.all(orphans, {
|
|
120
124
|
quiet: this.quiet,
|
package/src/state.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Resource, ResourceProps } from "./resource";
|
|
2
2
|
import type { Scope } from "./scope";
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export type State<
|
|
5
5
|
Kind extends string = string,
|
|
6
6
|
Props extends ResourceProps | undefined = ResourceProps | undefined,
|
|
7
7
|
Out extends Resource = Resource,
|
|
8
|
-
> {
|
|
8
|
+
> = {
|
|
9
9
|
status:
|
|
10
10
|
| `creating`
|
|
11
11
|
| `created`
|
|
@@ -22,7 +22,7 @@ export interface State<
|
|
|
22
22
|
props: Props;
|
|
23
23
|
oldProps?: Props;
|
|
24
24
|
output: Out;
|
|
25
|
-
}
|
|
25
|
+
};
|
|
26
26
|
|
|
27
27
|
export type StateStoreType = (scope: Scope) => StateStore;
|
|
28
28
|
|