alchemy 0.17.2 → 0.18.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/ai/document.d.ts +11 -0
- package/lib/ai/document.d.ts.map +1 -1
- package/lib/ai/document.js +1 -1
- package/lib/ai/document.js.map +1 -1
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +1 -2
- package/lib/alchemy.js.map +1 -1
- package/lib/aws/control/client.d.ts +134 -0
- package/lib/aws/control/client.d.ts.map +1 -0
- package/lib/aws/control/client.js +232 -0
- package/lib/aws/control/client.js.map +1 -0
- package/lib/aws/control/error.d.ts +63 -0
- package/lib/aws/control/error.d.ts.map +1 -0
- package/lib/aws/control/error.js +93 -0
- package/lib/aws/control/error.js.map +1 -0
- package/lib/aws/control/index.d.ts +6 -0
- package/lib/aws/control/index.d.ts.map +1 -0
- package/lib/aws/control/index.js +6 -0
- package/lib/aws/control/index.js.map +1 -0
- package/lib/aws/control/properties.d.ts +1874 -0
- package/lib/aws/control/properties.d.ts.map +1 -0
- package/lib/aws/control/properties.js +6945 -0
- package/lib/aws/control/properties.js.map +1 -0
- package/lib/aws/control/proxy.d.ts +7 -0
- package/lib/aws/control/proxy.d.ts.map +1 -0
- package/lib/aws/control/proxy.js +29 -0
- package/lib/aws/control/proxy.js.map +1 -0
- package/lib/aws/control/resource.d.ts +110 -0
- package/lib/aws/control/resource.d.ts.map +1 -0
- package/lib/aws/control/resource.js +192 -0
- package/lib/aws/control/resource.js.map +1 -0
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +2 -2
- package/lib/destroy.js.map +1 -1
- package/lib/resource.d.ts +7 -1
- package/lib/resource.d.ts.map +1 -1
- package/lib/resource.js +20 -0
- package/lib/resource.js.map +1 -1
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/bun.js +1 -2
- package/lib/test/bun.js.map +1 -1
- package/package.json +5 -1
- package/src/ai/document.ts +1 -1
- package/src/alchemy.ts +2 -3
- package/src/aws/control/client.ts +450 -0
- package/src/aws/control/error.ts +84 -0
- package/src/aws/control/index.ts +6 -0
- package/src/aws/control/properties.ts +6946 -0
- package/src/aws/control/proxy.ts +40 -0
- package/src/aws/control/resource.ts +346 -0
- package/src/aws/control/types.d.ts +85129 -0
- package/src/destroy.ts +4 -7
- package/src/resource.ts +32 -1
- package/src/test/bun.ts +1 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createResourceType } from "./resource.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Proxy-based interface for dynamically accessing AWS resource handlers.
|
|
5
|
+
* This allows for a natural, namespace-like access pattern (e.g., AWS.S3.Bucket)
|
|
6
|
+
* while lazily creating resource handlers as needed.
|
|
7
|
+
*/
|
|
8
|
+
export const AWS = new Proxy(
|
|
9
|
+
{},
|
|
10
|
+
{
|
|
11
|
+
get(_, serviceName: string) {
|
|
12
|
+
// Skip internal properties
|
|
13
|
+
if (typeof serviceName !== "string" || serviceName.startsWith("_")) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Create a nested proxy for the service namespace
|
|
18
|
+
return new Proxy(
|
|
19
|
+
{},
|
|
20
|
+
{
|
|
21
|
+
get(_, resourceName: string) {
|
|
22
|
+
// Skip internal properties
|
|
23
|
+
if (
|
|
24
|
+
typeof resourceName !== "string" ||
|
|
25
|
+
resourceName.startsWith("_")
|
|
26
|
+
) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Construct the CloudFormation resource type name
|
|
31
|
+
const typeName = `AWS::${serviceName}::${resourceName}`;
|
|
32
|
+
|
|
33
|
+
// Return the memoized resource handler
|
|
34
|
+
return createResourceType(typeName);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
) as typeof import("./types");
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { compare } from "fast-json-patch";
|
|
2
|
+
import type { Context } from "../../context.js";
|
|
3
|
+
import {
|
|
4
|
+
registerDynamicResource,
|
|
5
|
+
Resource,
|
|
6
|
+
type Provider,
|
|
7
|
+
} from "../../resource.js";
|
|
8
|
+
import { createCloudControlClient, type ProgressEvent } from "./client.js";
|
|
9
|
+
import {
|
|
10
|
+
AlreadyExistsError,
|
|
11
|
+
ConcurrentOperationError,
|
|
12
|
+
UpdateFailedError,
|
|
13
|
+
} from "./error.js";
|
|
14
|
+
import readOnlyPropertiesMap from "./properties.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Properties for creating or updating a Cloud Control resource
|
|
18
|
+
*/
|
|
19
|
+
export interface CloudControlResourceProps {
|
|
20
|
+
/**
|
|
21
|
+
* The type name of the resource (e.g. AWS::S3::Bucket)
|
|
22
|
+
*/
|
|
23
|
+
typeName: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The desired state of the resource
|
|
27
|
+
*/
|
|
28
|
+
desiredState: Record<string, any>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* If true, adopt existing resource instead of failing when resource already exists
|
|
32
|
+
*/
|
|
33
|
+
adopt?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Optional AWS region
|
|
37
|
+
* @default AWS_REGION environment variable
|
|
38
|
+
*/
|
|
39
|
+
region?: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* AWS access key ID (overrides environment variable)
|
|
43
|
+
*/
|
|
44
|
+
accessKeyId?: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* AWS secret access key (overrides environment variable)
|
|
48
|
+
*/
|
|
49
|
+
secretAccessKey?: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* AWS session token for temporary credentials
|
|
53
|
+
*/
|
|
54
|
+
sessionToken?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Output returned after Cloud Control resource creation/update
|
|
59
|
+
*/
|
|
60
|
+
export interface CloudControlResource
|
|
61
|
+
extends Resource<"aws::CloudControlResource">,
|
|
62
|
+
CloudControlResourceProps {
|
|
63
|
+
/**
|
|
64
|
+
* The identifier of the resource
|
|
65
|
+
*/
|
|
66
|
+
id: string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Time at which the resource was created
|
|
70
|
+
*/
|
|
71
|
+
createdAt: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Register wildcard deletion handler for AWS::* pattern
|
|
75
|
+
// registerDeletionHandler(
|
|
76
|
+
// "AWS::*",
|
|
77
|
+
// //
|
|
78
|
+
// );
|
|
79
|
+
|
|
80
|
+
// Cache for memoizing resource handlers
|
|
81
|
+
const resourceHandlers: Record<string, any> = {};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Filters out read-only properties from a resource state
|
|
85
|
+
* @param typeName AWS resource type name (e.g., "AWS::S3::Bucket")
|
|
86
|
+
* @param state Resource state object
|
|
87
|
+
* @returns Filtered state object without read-only properties
|
|
88
|
+
*/
|
|
89
|
+
function filterReadOnlyProperties(
|
|
90
|
+
typeName: string,
|
|
91
|
+
state: Record<string, any>,
|
|
92
|
+
): Record<string, any> {
|
|
93
|
+
// Parse the type name to get service and resource
|
|
94
|
+
const [service, resource] = typeName.replace("AWS::", "").split("::");
|
|
95
|
+
const readOnlyProps =
|
|
96
|
+
(readOnlyPropertiesMap as any)[service]?.[resource] || [];
|
|
97
|
+
|
|
98
|
+
const filtered: Record<string, any> = {};
|
|
99
|
+
for (const [key, value] of Object.entries(state)) {
|
|
100
|
+
if (!readOnlyProps.includes(key)) {
|
|
101
|
+
filtered[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return filtered;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a memoized Resource handler for a CloudFormation resource type
|
|
110
|
+
*
|
|
111
|
+
* @param typeName CloudFormation resource type (e.g., "AWS::S3::Bucket")
|
|
112
|
+
* @returns A memoized Resource handler for the specified type
|
|
113
|
+
*/
|
|
114
|
+
export function createResourceType(typeName: string) {
|
|
115
|
+
return (resourceHandlers[typeName] ??= Resource(
|
|
116
|
+
typeName,
|
|
117
|
+
function (
|
|
118
|
+
this: Context<CloudControlResource, CloudControlResourceProps>,
|
|
119
|
+
id: string,
|
|
120
|
+
props: Record<string, any> & {
|
|
121
|
+
adopt?: boolean;
|
|
122
|
+
region?: string;
|
|
123
|
+
accessKeyId?: string;
|
|
124
|
+
secretAccessKey?: string;
|
|
125
|
+
sessionToken?: string;
|
|
126
|
+
},
|
|
127
|
+
) {
|
|
128
|
+
// Extract Alchemy-specific properties
|
|
129
|
+
const {
|
|
130
|
+
adopt,
|
|
131
|
+
region,
|
|
132
|
+
accessKeyId,
|
|
133
|
+
secretAccessKey,
|
|
134
|
+
sessionToken,
|
|
135
|
+
...desiredState
|
|
136
|
+
} = props;
|
|
137
|
+
|
|
138
|
+
return CloudControlLifecycle.bind(this)(id, {
|
|
139
|
+
typeName,
|
|
140
|
+
desiredState,
|
|
141
|
+
adopt,
|
|
142
|
+
region,
|
|
143
|
+
accessKeyId,
|
|
144
|
+
secretAccessKey,
|
|
145
|
+
sessionToken,
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* AWS Cloud Control Resource (Generic Handler)
|
|
153
|
+
*
|
|
154
|
+
* This exported resource provides a generic way to manage any AWS resource
|
|
155
|
+
* supported by the Cloud Control API by explicitly passing the `typeName`.
|
|
156
|
+
* It is intended for direct use when the specific resource type might not be
|
|
157
|
+
* known at compile time or when not using the typed Proxy interface.
|
|
158
|
+
*
|
|
159
|
+
* For the strongly-typed Proxy interface (e.g., `AWS.S3.Bucket(...)`), Alchemy
|
|
160
|
+
* uses internal handlers generated by the `createResourceType` factory function.
|
|
161
|
+
*
|
|
162
|
+
* Creates and manages AWS resources using the Cloud Control API.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* // Create an S3 bucket
|
|
166
|
+
* const bucket = await CloudControlResource("my-bucket", {
|
|
167
|
+
* typeName: "AWS::S3::Bucket",
|
|
168
|
+
* desiredState: {
|
|
169
|
+
* BucketName: "my-unique-bucket-name",
|
|
170
|
+
* VersioningConfiguration: {
|
|
171
|
+
* Status: "Enabled"
|
|
172
|
+
* }
|
|
173
|
+
* }
|
|
174
|
+
* });
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* // Create a DynamoDB table
|
|
178
|
+
* const table = await CloudControlResource("users-table", {
|
|
179
|
+
* typeName: "AWS::DynamoDB::Table",
|
|
180
|
+
* desiredState: {
|
|
181
|
+
* TableName: "users",
|
|
182
|
+
* AttributeDefinitions: [
|
|
183
|
+
* {
|
|
184
|
+
* AttributeName: "id",
|
|
185
|
+
* AttributeType: "S"
|
|
186
|
+
* }
|
|
187
|
+
* ],
|
|
188
|
+
* KeySchema: [
|
|
189
|
+
* {
|
|
190
|
+
* AttributeName: "id",
|
|
191
|
+
* KeyType: "HASH"
|
|
192
|
+
* }
|
|
193
|
+
* ],
|
|
194
|
+
* ProvisionedThroughput: {
|
|
195
|
+
* ReadCapacityUnits: 5,
|
|
196
|
+
* WriteCapacityUnits: 5
|
|
197
|
+
* }
|
|
198
|
+
* }
|
|
199
|
+
* });
|
|
200
|
+
*/
|
|
201
|
+
export const CloudControlResource = Resource(
|
|
202
|
+
"aws::CloudControlResource",
|
|
203
|
+
CloudControlLifecycle,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// register a catch-all for AWS::* resources (Resources created with the Control API)
|
|
207
|
+
registerDynamicResource((typeName) => {
|
|
208
|
+
if (typeName.startsWith("AWS::")) {
|
|
209
|
+
return Resource(typeName, CloudControlLifecycle) as unknown as Provider;
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
async function CloudControlLifecycle(
|
|
215
|
+
this: Context<CloudControlResource, CloudControlResourceProps>,
|
|
216
|
+
id: string,
|
|
217
|
+
props: CloudControlResourceProps,
|
|
218
|
+
) {
|
|
219
|
+
const client = await createCloudControlClient({
|
|
220
|
+
region: props.region,
|
|
221
|
+
accessKeyId: props.accessKeyId,
|
|
222
|
+
secretAccessKey: props.secretAccessKey,
|
|
223
|
+
sessionToken: props.sessionToken,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (this.phase === "delete") {
|
|
227
|
+
if (this.output?.id) {
|
|
228
|
+
try {
|
|
229
|
+
await client.deleteResource(props.typeName, this.output.id);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
// Log but don't throw on cleanup errors
|
|
232
|
+
console.error(`Error deleting resource ${id}:`, error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return this.destroy();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let response: ProgressEvent | undefined;
|
|
239
|
+
if (this.phase === "update" && this.output?.id) {
|
|
240
|
+
// Update existing resource
|
|
241
|
+
response = await updateResourceWithPatch(
|
|
242
|
+
client,
|
|
243
|
+
props.typeName,
|
|
244
|
+
this.output.id,
|
|
245
|
+
this.output.desiredState,
|
|
246
|
+
props.desiredState,
|
|
247
|
+
);
|
|
248
|
+
} else {
|
|
249
|
+
// Create new resource
|
|
250
|
+
try {
|
|
251
|
+
response = await client.createResource(
|
|
252
|
+
props.typeName,
|
|
253
|
+
props.desiredState,
|
|
254
|
+
);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error instanceof AlreadyExistsError && props.adopt) {
|
|
257
|
+
const resource = (await client.getResource(
|
|
258
|
+
props.typeName,
|
|
259
|
+
error.progressEvent.Identifier!,
|
|
260
|
+
))!;
|
|
261
|
+
|
|
262
|
+
response = await updateResourceWithPatch(
|
|
263
|
+
client,
|
|
264
|
+
props.typeName,
|
|
265
|
+
error.progressEvent.Identifier!,
|
|
266
|
+
resource,
|
|
267
|
+
props.desiredState,
|
|
268
|
+
);
|
|
269
|
+
} else if (error instanceof ConcurrentOperationError) {
|
|
270
|
+
// Handle concurrent operation exception
|
|
271
|
+
console.log(error.message);
|
|
272
|
+
if (!props.adopt) {
|
|
273
|
+
// If adopt is not true, concurrent operations are an error
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
console.log(
|
|
277
|
+
`Waiting for concurrent operation with request token '${error.requestToken}' to complete`,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Wait for the concurrent operation to complete by polling it
|
|
281
|
+
try {
|
|
282
|
+
// Poll the concurrent operation until it completes
|
|
283
|
+
const concurrentResult = await client.poll(error.requestToken);
|
|
284
|
+
|
|
285
|
+
// The concurrent operation succeeded, now adopt the resource
|
|
286
|
+
const resource = (await client.getResource(
|
|
287
|
+
props.typeName,
|
|
288
|
+
concurrentResult.Identifier!,
|
|
289
|
+
))!;
|
|
290
|
+
|
|
291
|
+
// Apply our desired state as a patch to the existing resource
|
|
292
|
+
response = await updateResourceWithPatch(
|
|
293
|
+
client,
|
|
294
|
+
props.typeName,
|
|
295
|
+
concurrentResult.Identifier!,
|
|
296
|
+
resource,
|
|
297
|
+
props.desiredState,
|
|
298
|
+
);
|
|
299
|
+
} catch (pollError) {
|
|
300
|
+
// If the concurrent operation failed, we can try to create the resource ourselves
|
|
301
|
+
if (pollError instanceof UpdateFailedError) {
|
|
302
|
+
response = await client.createResource(
|
|
303
|
+
props.typeName,
|
|
304
|
+
props.desiredState,
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
throw pollError;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (response.OperationStatus === "FAILED") {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Failed to ${this.phase} resource ${id}: ${response.ErrorCode}`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return this({
|
|
323
|
+
...props,
|
|
324
|
+
id: response.Identifier!,
|
|
325
|
+
createdAt: Date.now(),
|
|
326
|
+
...(await client.getResource(props.typeName, response.Identifier!)),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function updateResourceWithPatch(
|
|
331
|
+
client: any,
|
|
332
|
+
typeName: string,
|
|
333
|
+
resourceId: string,
|
|
334
|
+
currentState: Record<string, any>,
|
|
335
|
+
desiredState: Record<string, any>,
|
|
336
|
+
): Promise<ProgressEvent> {
|
|
337
|
+
// Filter out read-only properties to avoid patch conflicts
|
|
338
|
+
const filteredCurrentState = filterReadOnlyProperties(typeName, currentState);
|
|
339
|
+
|
|
340
|
+
// Create and apply patch
|
|
341
|
+
return await client.updateResource(
|
|
342
|
+
typeName,
|
|
343
|
+
resourceId,
|
|
344
|
+
compare(filteredCurrentState, desiredState),
|
|
345
|
+
);
|
|
346
|
+
}
|