alchemy 0.34.0 → 0.34.2
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/bin/alchemy.mjs +38018 -3538
- package/bin/alchemy.ts +26 -0
- package/bin/bootstrap-s3.ts +255 -0
- package/lib/aws/index.d.ts +1 -0
- package/lib/aws/index.d.ts.map +1 -1
- package/lib/aws/index.js +1 -0
- package/lib/aws/index.js.map +1 -1
- package/lib/aws/s3-state-store.d.ts +117 -0
- package/lib/aws/s3-state-store.d.ts.map +1 -0
- package/lib/aws/s3-state-store.js +224 -0
- package/lib/aws/s3-state-store.js.map +1 -0
- package/lib/cloudflare/index.d.ts +2 -1
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +2 -1
- package/lib/cloudflare/index.js.map +1 -1
- package/package.json +2 -1
- package/src/aws/index.ts +1 -0
- package/src/aws/s3-state-store.ts +316 -0
- package/src/cloudflare/index.ts +2 -1
package/bin/alchemy.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { createAlchemy } from "./create-alchemy.ts";
|
|
4
|
+
import { bootstrapS3 } from "./bootstrap-s3.ts";
|
|
4
5
|
|
|
5
6
|
// Parse command-line arguments. We allow unknown flags because different
|
|
6
7
|
// sub-commands may accept different sets.
|
|
@@ -13,6 +14,8 @@ const { values, positionals } = parseArgs({
|
|
|
13
14
|
overwrite: { type: "boolean" },
|
|
14
15
|
help: { type: "boolean", short: "h" },
|
|
15
16
|
version: { type: "boolean", short: "v" },
|
|
17
|
+
region: { type: "string" },
|
|
18
|
+
prefix: { type: "string" },
|
|
16
19
|
},
|
|
17
20
|
});
|
|
18
21
|
|
|
@@ -23,6 +26,11 @@ const usage = `Usage: alchemy <command> [options]
|
|
|
23
26
|
|
|
24
27
|
Available commands:
|
|
25
28
|
create Scaffold a new project
|
|
29
|
+
bootstrap Bootstrap cloud resources for alchemy
|
|
30
|
+
|
|
31
|
+
Bootstrap options:
|
|
32
|
+
--region AWS region (defaults to AWS profile default)
|
|
33
|
+
--prefix S3 bucket name prefix (default: alchemy-state)
|
|
26
34
|
`;
|
|
27
35
|
|
|
28
36
|
if (!command) {
|
|
@@ -61,6 +69,24 @@ switch (command) {
|
|
|
61
69
|
break;
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
case "bootstrap": {
|
|
73
|
+
const subcommand = positionals.shift();
|
|
74
|
+
|
|
75
|
+
if (subcommand === "s3") {
|
|
76
|
+
await bootstrapS3({
|
|
77
|
+
region: values.region as string | undefined,
|
|
78
|
+
prefix: values.prefix as string | undefined,
|
|
79
|
+
help: values.help as boolean | undefined,
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
console.error(`Unknown bootstrap subcommand: ${subcommand || "(none)"}`);
|
|
83
|
+
console.error("Available bootstrap subcommands:");
|
|
84
|
+
console.error(" s3 Create S3 bucket for state storage");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
default:
|
|
65
91
|
console.error(`Unknown command: ${command}`);
|
|
66
92
|
process.exit(1);
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GetResourcesCommand,
|
|
3
|
+
ResourceGroupsTaggingAPIClient,
|
|
4
|
+
} from "@aws-sdk/client-resource-groups-tagging-api";
|
|
5
|
+
import {
|
|
6
|
+
CreateBucketCommand,
|
|
7
|
+
PutBucketTaggingCommand,
|
|
8
|
+
S3Client,
|
|
9
|
+
} from "@aws-sdk/client-s3";
|
|
10
|
+
import { loadConfig } from "@smithy/node-config-provider";
|
|
11
|
+
|
|
12
|
+
export interface BootstrapS3Options {
|
|
13
|
+
region?: string;
|
|
14
|
+
prefix?: string;
|
|
15
|
+
help?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BOOTSTRAP_TAG_KEY = "alchemy:bootstrap";
|
|
19
|
+
const BOOTSTRAP_TAG_VALUE = "s3-state-store";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a random suffix for bucket names to avoid conflicts
|
|
23
|
+
*/
|
|
24
|
+
function generateRandomSuffix(): string {
|
|
25
|
+
return Math.random().toString(36).substring(2, 8);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find existing bootstrap bucket by checking tags using Resource Groups API
|
|
30
|
+
*/
|
|
31
|
+
async function findExistingBootstrapBucket(
|
|
32
|
+
region: string,
|
|
33
|
+
prefix: string,
|
|
34
|
+
): Promise<string | null> {
|
|
35
|
+
try {
|
|
36
|
+
const resourceGroupsClient = new ResourceGroupsTaggingAPIClient({ region });
|
|
37
|
+
|
|
38
|
+
const result = await resourceGroupsClient.send(
|
|
39
|
+
new GetResourcesCommand({
|
|
40
|
+
ResourceTypeFilters: ["s3:bucket"],
|
|
41
|
+
TagFilters: [
|
|
42
|
+
{
|
|
43
|
+
Key: BOOTSTRAP_TAG_KEY,
|
|
44
|
+
Values: [BOOTSTRAP_TAG_VALUE],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (!result.ResourceTagMappingList) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find a bucket that matches our prefix
|
|
55
|
+
for (const resource of result.ResourceTagMappingList) {
|
|
56
|
+
if (resource.ResourceARN) {
|
|
57
|
+
// Extract bucket name from ARN: arn:aws:s3:::bucket-name
|
|
58
|
+
const bucketName = resource.ResourceARN.split(":::")[1];
|
|
59
|
+
if (bucketName?.startsWith(prefix)) {
|
|
60
|
+
return bucketName;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error: any) {
|
|
65
|
+
console.error("Error finding bootstrap buckets:", error.message);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a new S3 bucket with bootstrap tags
|
|
74
|
+
*/
|
|
75
|
+
async function createBootstrapBucket(
|
|
76
|
+
s3Client: S3Client,
|
|
77
|
+
bucketName: string,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
// Create the bucket
|
|
81
|
+
const createCommand = new CreateBucketCommand({
|
|
82
|
+
Bucket: bucketName,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await s3Client.send(createCommand);
|
|
86
|
+
console.log(`✅ Created S3 bucket: ${bucketName}`);
|
|
87
|
+
|
|
88
|
+
// Add bootstrap tags
|
|
89
|
+
await s3Client.send(
|
|
90
|
+
new PutBucketTaggingCommand({
|
|
91
|
+
Bucket: bucketName,
|
|
92
|
+
Tagging: {
|
|
93
|
+
TagSet: [
|
|
94
|
+
{
|
|
95
|
+
Key: BOOTSTRAP_TAG_KEY,
|
|
96
|
+
Value: BOOTSTRAP_TAG_VALUE,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
Key: "Purpose",
|
|
100
|
+
Value: "Alchemy state storage",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
Key: "CreatedBy",
|
|
104
|
+
Value: "alchemy-cli",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
console.log("✅ Tagged bucket with bootstrap markers");
|
|
112
|
+
} catch (error: any) {
|
|
113
|
+
if (error.name === "BucketAlreadyExists") {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Bucket name '${bucketName}' is already taken. Please try again to generate a new random suffix.`,
|
|
116
|
+
);
|
|
117
|
+
} else if (error.name === "BucketAlreadyOwnedByYou") {
|
|
118
|
+
console.log(`✅ Bucket ${bucketName} already exists and is owned by you`);
|
|
119
|
+
|
|
120
|
+
// Still add tags if they're missing
|
|
121
|
+
try {
|
|
122
|
+
await s3Client.send(
|
|
123
|
+
new PutBucketTaggingCommand({
|
|
124
|
+
Bucket: bucketName,
|
|
125
|
+
Tagging: {
|
|
126
|
+
TagSet: [
|
|
127
|
+
{
|
|
128
|
+
Key: BOOTSTRAP_TAG_KEY,
|
|
129
|
+
Value: BOOTSTRAP_TAG_VALUE,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
Key: "Purpose",
|
|
133
|
+
Value: "Alchemy state storage",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
Key: "CreatedBy",
|
|
137
|
+
Value: "alchemy-cli",
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
console.log("✅ Added bootstrap tags to existing bucket");
|
|
144
|
+
} catch (tagError: any) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Failed to tag existing bucket '${bucketName}': ${tagError.message}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the default AWS region from the current profile/configuration
|
|
157
|
+
*/
|
|
158
|
+
async function getDefaultRegion(): Promise<string> {
|
|
159
|
+
try {
|
|
160
|
+
const regionProvider = loadConfig({
|
|
161
|
+
environmentVariableSelector: (env) =>
|
|
162
|
+
env.AWS_REGION || env.AWS_DEFAULT_REGION,
|
|
163
|
+
configFileSelector: (profile) => profile.region,
|
|
164
|
+
default: "us-east-1",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return await regionProvider();
|
|
168
|
+
} catch (_error) {
|
|
169
|
+
return "us-east-1"; // Default fallback
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Bootstrap S3 bucket for Alchemy state storage
|
|
175
|
+
*/
|
|
176
|
+
export async function bootstrapS3(options: BootstrapS3Options): Promise<void> {
|
|
177
|
+
if (options.help) {
|
|
178
|
+
console.log(`
|
|
179
|
+
Usage: alchemy bootstrap s3 [options]
|
|
180
|
+
|
|
181
|
+
Create an S3 bucket for Alchemy state storage with proper tagging.
|
|
182
|
+
|
|
183
|
+
Options:
|
|
184
|
+
--region AWS region (defaults to AWS profile default: us-east-1)
|
|
185
|
+
--prefix S3 bucket name prefix (default: alchemy-state)
|
|
186
|
+
--help Show this help message
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
alchemy bootstrap s3 # Use defaults
|
|
190
|
+
alchemy bootstrap s3 --region us-west-2 # Specify region
|
|
191
|
+
alchemy bootstrap s3 --prefix my-app-state # Custom prefix
|
|
192
|
+
alchemy bootstrap s3 --region eu-west-1 --prefix app # Both options
|
|
193
|
+
|
|
194
|
+
The created bucket will be tagged with 'alchemy:bootstrap=s3-state-store' to
|
|
195
|
+
identify it for future bootstrap operations and avoid creating duplicates.
|
|
196
|
+
`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const prefix = options.prefix || "alchemy-state";
|
|
201
|
+
const region = options.region || (await getDefaultRegion());
|
|
202
|
+
|
|
203
|
+
console.log("🚀 Bootstrapping S3 state storage...");
|
|
204
|
+
console.log(` Region: ${region}`);
|
|
205
|
+
console.log(` Prefix: ${prefix}`);
|
|
206
|
+
|
|
207
|
+
// Initialize S3 client
|
|
208
|
+
const s3Client = new S3Client({
|
|
209
|
+
region,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
// Check for existing bootstrap bucket
|
|
214
|
+
const existingBucket = await findExistingBootstrapBucket(region, prefix);
|
|
215
|
+
|
|
216
|
+
if (existingBucket) {
|
|
217
|
+
console.log(`✅ Found existing bootstrap bucket: ${existingBucket}`);
|
|
218
|
+
console.log("\n📝 To use this bucket in your alchemy.run.ts file:\n");
|
|
219
|
+
console.log(`import { S3StateStore } from "alchemy/aws";`);
|
|
220
|
+
console.log("\nconst stateStore = new S3StateStore(scope, {");
|
|
221
|
+
console.log(` bucketName: "${existingBucket}",`);
|
|
222
|
+
console.log(` region: "${region}",`);
|
|
223
|
+
console.log("});\n");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Generate a new bucket name with random suffix
|
|
228
|
+
const bucketName = `${prefix}-${generateRandomSuffix()}`;
|
|
229
|
+
console.log(`📦 Creating new bucket: ${bucketName}`);
|
|
230
|
+
|
|
231
|
+
// Create the bucket
|
|
232
|
+
await createBootstrapBucket(s3Client, bucketName);
|
|
233
|
+
|
|
234
|
+
console.log("\n🎉 Bootstrap complete!");
|
|
235
|
+
console.log("\n📝 To use this bucket in your alchemy.run.ts file:\n");
|
|
236
|
+
console.log(`import { S3StateStore } from "alchemy/aws";`);
|
|
237
|
+
console.log("\nconst stateStore = new S3StateStore(scope, {");
|
|
238
|
+
console.log(` bucketName: "${bucketName}",`);
|
|
239
|
+
console.log(` region: "${region}",`);
|
|
240
|
+
console.log("});\n");
|
|
241
|
+
} catch (error: any) {
|
|
242
|
+
console.error(`❌ Failed to bootstrap S3: ${error.message}`);
|
|
243
|
+
|
|
244
|
+
if (error.message?.includes("credentials")) {
|
|
245
|
+
console.error("\n💡 Make sure your AWS credentials are configured:");
|
|
246
|
+
console.error(` - Run 'aws configure' to set up credentials`);
|
|
247
|
+
console.error(
|
|
248
|
+
" - Or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables",
|
|
249
|
+
);
|
|
250
|
+
console.error(" - Or use IAM roles if running on EC2/Lambda");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
}
|
package/lib/aws/index.d.ts
CHANGED
package/lib/aws/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/aws/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/aws/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,UAAU,CAAC;AACzB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC"}
|
package/lib/aws/index.js
CHANGED
package/lib/aws/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/aws/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/aws/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,UAAU,CAAC;AACzB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Scope } from "../scope.ts";
|
|
2
|
+
import { type State, type StateStore } from "../state.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Options for S3StateStore
|
|
5
|
+
*/
|
|
6
|
+
export interface S3StateStoreOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The prefix to use for object keys in the S3 bucket
|
|
9
|
+
* This allows multiple state stores to use the same S3 bucket
|
|
10
|
+
*/
|
|
11
|
+
prefix?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The S3 bucket name to use
|
|
14
|
+
* Required - the bucket must already exist
|
|
15
|
+
*/
|
|
16
|
+
bucketName?: string;
|
|
17
|
+
/**
|
|
18
|
+
* AWS region for the S3 client
|
|
19
|
+
* If not provided, uses the default AWS region configuration
|
|
20
|
+
*/
|
|
21
|
+
region?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* State store implementation using AWS S3
|
|
25
|
+
* Provides reliable, scalable state storage with eventual consistency
|
|
26
|
+
*/
|
|
27
|
+
export declare class S3StateStore implements StateStore {
|
|
28
|
+
readonly scope: Scope;
|
|
29
|
+
private readonly options;
|
|
30
|
+
private client;
|
|
31
|
+
private prefix;
|
|
32
|
+
private bucketName;
|
|
33
|
+
private initialized;
|
|
34
|
+
/**
|
|
35
|
+
* Create a new S3StateStore
|
|
36
|
+
*
|
|
37
|
+
* @param scope The scope this store belongs to
|
|
38
|
+
* @param options Options for the state store
|
|
39
|
+
*/
|
|
40
|
+
constructor(scope: Scope, options?: S3StateStoreOptions);
|
|
41
|
+
/**
|
|
42
|
+
* Initialize the S3 client and verify bucket access
|
|
43
|
+
*/
|
|
44
|
+
init(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* S3 buckets cannot be deleted programmatically via this method
|
|
47
|
+
*/
|
|
48
|
+
deinit(): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* List all resources in the state store
|
|
51
|
+
*/
|
|
52
|
+
list(): Promise<string[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Count the number of items in the state store
|
|
55
|
+
*/
|
|
56
|
+
count(): Promise<number>;
|
|
57
|
+
/**
|
|
58
|
+
* Get a state by key
|
|
59
|
+
*
|
|
60
|
+
* @param key The key to look up
|
|
61
|
+
* @returns The state or undefined if not found
|
|
62
|
+
*/
|
|
63
|
+
get(key: string): Promise<State | undefined>;
|
|
64
|
+
/**
|
|
65
|
+
* Get multiple states by their keys
|
|
66
|
+
*
|
|
67
|
+
* @param ids Array of keys to fetch
|
|
68
|
+
* @returns Record mapping keys to their states
|
|
69
|
+
*/
|
|
70
|
+
getBatch(ids: string[]): Promise<Record<string, State>>;
|
|
71
|
+
/**
|
|
72
|
+
* Get all states in the store
|
|
73
|
+
*
|
|
74
|
+
* @returns Record mapping all keys to their states
|
|
75
|
+
*/
|
|
76
|
+
all(): Promise<Record<string, State>>;
|
|
77
|
+
/**
|
|
78
|
+
* Set a state for a key
|
|
79
|
+
*
|
|
80
|
+
* @param key The key to set
|
|
81
|
+
* @param value The state to store
|
|
82
|
+
*/
|
|
83
|
+
set(key: string, value: State): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Delete a state by key
|
|
86
|
+
*
|
|
87
|
+
* @param key The key to delete
|
|
88
|
+
*/
|
|
89
|
+
delete(key: string): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Convert key for storage by replacing slashes with colons
|
|
92
|
+
* since S3 treats slashes as directory separators
|
|
93
|
+
*
|
|
94
|
+
* @param key The original key
|
|
95
|
+
* @returns Key with slashes replaced by colons
|
|
96
|
+
*/
|
|
97
|
+
private convertKeyForStorage;
|
|
98
|
+
/**
|
|
99
|
+
* Convert key from storage by replacing colons with slashes
|
|
100
|
+
*
|
|
101
|
+
* @param key The storage key
|
|
102
|
+
* @returns Key with colons replaced by slashes
|
|
103
|
+
*/
|
|
104
|
+
private convertKeyFromStorage;
|
|
105
|
+
/**
|
|
106
|
+
* Get the full object key for storage
|
|
107
|
+
*
|
|
108
|
+
* @param key The original key
|
|
109
|
+
* @returns The key with prefix for use in the S3 bucket
|
|
110
|
+
*/
|
|
111
|
+
private getObjectKey;
|
|
112
|
+
/**
|
|
113
|
+
* Ensure the store is initialized before operations
|
|
114
|
+
*/
|
|
115
|
+
private ensureInitialized;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=s3-state-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-state-store.d.ts","sourceRoot":"","sources":["../../src/aws/s3-state-store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAoB,KAAK,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAI5E;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,qBAAa,YAAa,YAAW,UAAU;aAa3B,KAAK,EAAE,KAAK;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAb1B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAS;IAE5B;;;;;OAKG;gBAEe,KAAK,EAAE,KAAK,EACX,OAAO,GAAE,mBAAwB;IAepD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB3B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAgC/B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAK9B;;;;;OAKG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;IAuClD;;;;;OAKG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAe7D;;;;OAIG;IACG,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAK3C;;;;;OAKG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBnD;;;;OAIG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAexC;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;IAI5B;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAI7B;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;YACW,iBAAiB;CAKhC"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, NoSuchBucket, NoSuchKey, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
|
|
2
|
+
import { ResourceScope } from "../resource.js";
|
|
3
|
+
import { serialize } from "../serde.js";
|
|
4
|
+
import { deserializeState } from "../state.js";
|
|
5
|
+
import { ignore } from "../util/ignore.js";
|
|
6
|
+
import { retry } from "./retry.js";
|
|
7
|
+
/**
|
|
8
|
+
* State store implementation using AWS S3
|
|
9
|
+
* Provides reliable, scalable state storage with eventual consistency
|
|
10
|
+
*/
|
|
11
|
+
export class S3StateStore {
|
|
12
|
+
scope;
|
|
13
|
+
options;
|
|
14
|
+
client;
|
|
15
|
+
prefix;
|
|
16
|
+
bucketName;
|
|
17
|
+
initialized = false;
|
|
18
|
+
/**
|
|
19
|
+
* Create a new S3StateStore
|
|
20
|
+
*
|
|
21
|
+
* @param scope The scope this store belongs to
|
|
22
|
+
* @param options Options for the state store
|
|
23
|
+
*/
|
|
24
|
+
constructor(scope, options = {}) {
|
|
25
|
+
this.scope = scope;
|
|
26
|
+
this.options = options;
|
|
27
|
+
// Use the scope's chain to build the prefix, similar to how FileSystemStateStore builds its directory
|
|
28
|
+
const scopePath = scope.chain.join("/");
|
|
29
|
+
this.prefix = options.prefix
|
|
30
|
+
? `${options.prefix}${scopePath}/`
|
|
31
|
+
: `alchemy/${scopePath}/`;
|
|
32
|
+
this.bucketName = options.bucketName ?? "alchemy-state";
|
|
33
|
+
this.client = new S3Client({
|
|
34
|
+
region: options.region,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Initialize the S3 client and verify bucket access
|
|
39
|
+
*/
|
|
40
|
+
async init() {
|
|
41
|
+
if (this.initialized)
|
|
42
|
+
return;
|
|
43
|
+
// Verify bucket exists and is accessible
|
|
44
|
+
try {
|
|
45
|
+
await retry(() => this.client.send(new ListObjectsV2Command({
|
|
46
|
+
Bucket: this.bucketName,
|
|
47
|
+
MaxKeys: 1,
|
|
48
|
+
})));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.name === NoSuchBucket.name) {
|
|
52
|
+
throw new Error(`S3 bucket '${this.bucketName}' does not exist. Please create the bucket first.`);
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
this.initialized = true;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* S3 buckets cannot be deleted programmatically via this method
|
|
60
|
+
*/
|
|
61
|
+
async deinit() {
|
|
62
|
+
// We don't delete the bucket here, only via explicit resource deletion
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* List all resources in the state store
|
|
66
|
+
*/
|
|
67
|
+
async list() {
|
|
68
|
+
await this.ensureInitialized();
|
|
69
|
+
const keys = [];
|
|
70
|
+
let continuationToken;
|
|
71
|
+
do {
|
|
72
|
+
const response = await retry(() => this.client.send(new ListObjectsV2Command({
|
|
73
|
+
Bucket: this.bucketName,
|
|
74
|
+
Prefix: this.prefix,
|
|
75
|
+
ContinuationToken: continuationToken,
|
|
76
|
+
})));
|
|
77
|
+
if (response.Contents) {
|
|
78
|
+
keys.push(...response.Contents.map((obj) => {
|
|
79
|
+
const key = obj.Key.slice(this.prefix.length);
|
|
80
|
+
return this.convertKeyFromStorage(key);
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
continuationToken = response.NextContinuationToken;
|
|
84
|
+
} while (continuationToken);
|
|
85
|
+
return keys;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Count the number of items in the state store
|
|
89
|
+
*/
|
|
90
|
+
async count() {
|
|
91
|
+
const keys = await this.list();
|
|
92
|
+
return keys.length;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get a state by key
|
|
96
|
+
*
|
|
97
|
+
* @param key The key to look up
|
|
98
|
+
* @returns The state or undefined if not found
|
|
99
|
+
*/
|
|
100
|
+
async get(key) {
|
|
101
|
+
await this.ensureInitialized();
|
|
102
|
+
try {
|
|
103
|
+
const response = await retry(() => this.client.send(new GetObjectCommand({
|
|
104
|
+
Bucket: this.bucketName,
|
|
105
|
+
Key: this.getObjectKey(key),
|
|
106
|
+
})));
|
|
107
|
+
if (!response.Body) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
// Read the stream into a string
|
|
111
|
+
const content = await response.Body.transformToString();
|
|
112
|
+
// Parse and deserialize the state data
|
|
113
|
+
const state = await deserializeState(this.scope, content);
|
|
114
|
+
// Create a new state object with proper output
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
output: {
|
|
118
|
+
...(state.output || {}),
|
|
119
|
+
[ResourceScope]: this.scope,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error.name === NoSuchKey.name) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get multiple states by their keys
|
|
132
|
+
*
|
|
133
|
+
* @param ids Array of keys to fetch
|
|
134
|
+
* @returns Record mapping keys to their states
|
|
135
|
+
*/
|
|
136
|
+
async getBatch(ids) {
|
|
137
|
+
const result = {};
|
|
138
|
+
// S3 doesn't have a batch get operation, so we need to make multiple requests
|
|
139
|
+
const promises = ids.map(async (id) => {
|
|
140
|
+
const state = await this.get(id);
|
|
141
|
+
if (state) {
|
|
142
|
+
result[id] = state;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
await Promise.all(promises);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get all states in the store
|
|
150
|
+
*
|
|
151
|
+
* @returns Record mapping all keys to their states
|
|
152
|
+
*/
|
|
153
|
+
async all() {
|
|
154
|
+
const keys = await this.list();
|
|
155
|
+
return this.getBatch(keys);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Set a state for a key
|
|
159
|
+
*
|
|
160
|
+
* @param key The key to set
|
|
161
|
+
* @param value The state to store
|
|
162
|
+
*/
|
|
163
|
+
async set(key, value) {
|
|
164
|
+
await this.ensureInitialized();
|
|
165
|
+
const objectKey = this.getObjectKey(key);
|
|
166
|
+
// Serialize the state to handle cyclic structures
|
|
167
|
+
const serializedData = JSON.stringify(await serialize(this.scope, value), null, 2);
|
|
168
|
+
await retry(() => this.client.send(new PutObjectCommand({
|
|
169
|
+
Bucket: this.bucketName,
|
|
170
|
+
Key: objectKey,
|
|
171
|
+
Body: serializedData,
|
|
172
|
+
ContentType: "application/json",
|
|
173
|
+
})));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Delete a state by key
|
|
177
|
+
*
|
|
178
|
+
* @param key The key to delete
|
|
179
|
+
*/
|
|
180
|
+
async delete(key) {
|
|
181
|
+
await this.ensureInitialized();
|
|
182
|
+
await ignore(NoSuchKey.name, () => retry(() => this.client.send(new DeleteObjectCommand({
|
|
183
|
+
Bucket: this.bucketName,
|
|
184
|
+
Key: this.getObjectKey(key),
|
|
185
|
+
}))));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Convert key for storage by replacing slashes with colons
|
|
189
|
+
* since S3 treats slashes as directory separators
|
|
190
|
+
*
|
|
191
|
+
* @param key The original key
|
|
192
|
+
* @returns Key with slashes replaced by colons
|
|
193
|
+
*/
|
|
194
|
+
convertKeyForStorage(key) {
|
|
195
|
+
return key.replaceAll("/", ":");
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Convert key from storage by replacing colons with slashes
|
|
199
|
+
*
|
|
200
|
+
* @param key The storage key
|
|
201
|
+
* @returns Key with colons replaced by slashes
|
|
202
|
+
*/
|
|
203
|
+
convertKeyFromStorage(key) {
|
|
204
|
+
return key.replaceAll(":", "/");
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get the full object key for storage
|
|
208
|
+
*
|
|
209
|
+
* @param key The original key
|
|
210
|
+
* @returns The key with prefix for use in the S3 bucket
|
|
211
|
+
*/
|
|
212
|
+
getObjectKey(key) {
|
|
213
|
+
return `${this.prefix}${this.convertKeyForStorage(key)}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Ensure the store is initialized before operations
|
|
217
|
+
*/
|
|
218
|
+
async ensureInitialized() {
|
|
219
|
+
if (!this.initialized) {
|
|
220
|
+
await this.init();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=s3-state-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-state-store.js","sourceRoot":"","sources":["../../src/aws/s3-state-store.ts"],"names":[],"mappings":"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"}
|