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.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
+ }
@@ -5,6 +5,7 @@ export * from "./policy-attachment.ts";
5
5
  export * from "./policy.ts";
6
6
  export * from "./queue.ts";
7
7
  export * from "./role.ts";
8
+ export * from "./s3-state-store.ts";
8
9
  export * from "./ses.ts";
9
10
  export * from "./ssm-parameter.ts";
10
11
  export * from "./table.ts";
@@ -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
@@ -5,6 +5,7 @@ export * from "./policy-attachment.js";
5
5
  export * from "./policy.js";
6
6
  export * from "./queue.js";
7
7
  export * from "./role.js";
8
+ export * from "./s3-state-store.js";
8
9
  export * from "./ses.js";
9
10
  export * from "./ssm-parameter.js";
10
11
  export * from "./table.js";
@@ -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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"}