alchemy 0.0.8 → 0.1.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/README.md +355 -0
- package/package.json +24 -20
- package/src/alchemize.ts +151 -0
- package/src/apply.ts +117 -0
- package/src/components/agent/agent.ts +45 -0
- package/src/components/agent/anthropic.ts +27 -0
- package/src/components/agent/index.ts +4 -0
- package/src/components/agent/model.ts +22 -0
- package/src/components/agent/openai.ts +44 -0
- package/src/components/agent/package.ts +126 -0
- package/src/components/agent/requirements.ts +111 -0
- package/src/components/agent/scrape.ts +47 -0
- package/src/components/agent/tsconfig.ts +150 -0
- package/src/components/agent/typescript.ts +177 -0
- package/src/components/aws/bucket.ts +168 -0
- package/src/components/aws/function.ts +302 -0
- package/src/components/aws/index.ts +6 -0
- package/src/components/aws/policy.ts +245 -0
- package/src/components/aws/queue.ts +222 -0
- package/src/components/aws/role.ts +245 -0
- package/src/components/aws/table.ts +172 -0
- package/src/components/esbuild.ts +128 -0
- package/src/components/fs.ts +45 -0
- package/src/destroy.ts +63 -0
- package/src/error.ts +16 -0
- package/src/global.ts +72 -0
- package/src/index.ts +6 -0
- package/src/input.ts +16 -0
- package/src/main.ts +34 -0
- package/src/output.ts +49 -0
- package/src/resource.ts +313 -0
- package/src/state.ts +100 -0
- package/test/aws/function.test.ts +144 -0
- package/test/aws/queue.test.ts +85 -0
- package/test/aws/role.test.ts +153 -0
- package/test/aws/table.test.ts +73 -0
- package/test/esbuild.test.ts +41 -0
- package/test/handler.ts +9 -0
- package/tsconfig.json +7 -0
- package/Column.js +0 -1
- package/Create.js +0 -1
- package/Delete.js +0 -1
- package/Drop.js +0 -1
- package/Insert.js +0 -1
- package/Makefile +0 -8
- package/Select.js +0 -1
- package/Table.js +0 -1
- package/Transaction.js +0 -1
- package/Types.js +0 -1
- package/Update.js +0 -1
- package/alchemy.js +0 -57
- package/alchemy.sublime-project +0 -9
- package/alchemy.sublime-workspace +0 -2304
- package/config/app.js +0 -8
- package/config/db.js +0 -20
- package/copy.js +0 -1
- package/defaultEngine.js +0 -8
- package/engine.js +0 -282
- package/getInstantiatorFunction.js +0 -28
- package/index.js +0 -52
- package/jstest.js +0 -3
- package/loggerFactory.js +0 -27
- package/logs/alchemy.log +0 -46
- package/logs/sql.log +0 -41
- package/sql/Column.js +0 -158
- package/sql/Columnable.js +0 -21
- package/sql/Create.js +0 -15
- package/sql/Delete.js +0 -34
- package/sql/Drop.js +0 -36
- package/sql/Insert.js +0 -96
- package/sql/SchemaElement.js +0 -11
- package/sql/Select.js +0 -192
- package/sql/SqlBase.js +0 -134
- package/sql/SqlStatement.js +0 -80
- package/sql/Table.js +0 -96
- package/sql/Transaction.js +0 -45
- package/sql/Update.js +0 -105
- package/sql/defineElement.js +0 -26
- package/sql/defineStatement.js +0 -26
- package/sql/types/BaseType.js +0 -43
- package/sql/types/BigSerialType.js +0 -5
- package/sql/types/IntegerType.js +0 -5
- package/sql/types/JsonType.js +0 -5
- package/sql/types/SerialType.js +0 -5
- package/sql/types/StringType.js +0 -31
- package/sql/types/TimestampType.js +0 -5
- package/sql/types/UUIDType.js +0 -5
- package/sql/types/defineType.js +0 -25
- package/sql/types/getType.js +0 -12
- package/sql/types/index.js +0 -11
- package/sql/where.js +0 -31
- package/sql/whereToString.js +0 -29
- package/test/ColumnTests.js +0 -80
- package/test/CreateTable.js +0 -54
- package/test/DeleteTests.js +0 -28
- package/test/DropTests.js +0 -23
- package/test/GetTypeTests.js +0 -56
- package/test/InsertTests.js +0 -86
- package/test/SelectExistsTests.js +0 -26
- package/test/SelectTests.js +0 -130
- package/test/TableTests.js +0 -46
- package/test.js +0 -8
- package/test.ls +0 -4
- package/validate.js +0 -26
- package/x.js +0 -5
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { CoreMessage } from "ai";
|
|
2
|
+
import { generateText } from "ai";
|
|
3
|
+
import { mkdir, unlink, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { Agent } from "./agent";
|
|
7
|
+
import { resolveModel } from "./model";
|
|
8
|
+
|
|
9
|
+
export type CodeProps = z.infer<typeof CodeProps>;
|
|
10
|
+
|
|
11
|
+
export type File = z.infer<typeof File>;
|
|
12
|
+
export const File = z.object({
|
|
13
|
+
path: z.string(),
|
|
14
|
+
content: z.string(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const CodeProps = z.object({
|
|
18
|
+
/**
|
|
19
|
+
* The ID of the model to use for generating TypeScript code
|
|
20
|
+
* @default "gpt-4o"
|
|
21
|
+
*/
|
|
22
|
+
modelId: z.string().optional(),
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The name of the file to write the code to
|
|
26
|
+
*/
|
|
27
|
+
path: z.string(),
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The requirements for the TypeScript file
|
|
31
|
+
*/
|
|
32
|
+
requirements: z.string(),
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* List of other code files that it depends on
|
|
36
|
+
*/
|
|
37
|
+
dependencies: z.array(File).optional(),
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Temperature setting for model generation (higher = more creative, lower = more focused)
|
|
41
|
+
* @default 0.7
|
|
42
|
+
*/
|
|
43
|
+
temperature: z.number().optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type CodeOutput = z.infer<typeof CodeOutput>;
|
|
47
|
+
export const CodeOutput = File;
|
|
48
|
+
|
|
49
|
+
export class TypeScriptFile extends Agent(
|
|
50
|
+
"code::TypeScriptFile",
|
|
51
|
+
{
|
|
52
|
+
description:
|
|
53
|
+
"This Agent is responsible for generating TypeScript code based on requirements and context from other code files.",
|
|
54
|
+
input: CodeProps,
|
|
55
|
+
output: CodeOutput,
|
|
56
|
+
},
|
|
57
|
+
async (ctx, props) => {
|
|
58
|
+
if (ctx.event === "delete") {
|
|
59
|
+
await unlink(props.path);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Get the appropriate model based on the ID
|
|
64
|
+
const { model } = await resolveModel(props.modelId ?? "gpt-4o");
|
|
65
|
+
|
|
66
|
+
// Generate the TypeScript code with retries
|
|
67
|
+
const content = await generateCodeWithRetries(model, props);
|
|
68
|
+
|
|
69
|
+
// Ensure the directory exists
|
|
70
|
+
await mkdir(dirname(props.path), { recursive: true });
|
|
71
|
+
|
|
72
|
+
// Write the TypeScript file
|
|
73
|
+
await writeFile(props.path, content);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
path: props.path,
|
|
77
|
+
content: content,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
) {}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Attempts to generate valid TypeScript code with retries
|
|
84
|
+
*/
|
|
85
|
+
async function generateCodeWithRetries(
|
|
86
|
+
model: any,
|
|
87
|
+
props: CodeProps,
|
|
88
|
+
maxAttempts = 3,
|
|
89
|
+
): Promise<string> {
|
|
90
|
+
const baseMessages: CoreMessage[] = [
|
|
91
|
+
{
|
|
92
|
+
role: "system",
|
|
93
|
+
content:
|
|
94
|
+
"You are an expert TypeScript developer that generates clean, well-documented code following best practices. " +
|
|
95
|
+
"Your response must follow this exact format:\n" +
|
|
96
|
+
"1. A single sentence describing what the code will do\n" +
|
|
97
|
+
"2. A single TypeScript code block surrounded by ```ts and ``` tags\n" +
|
|
98
|
+
"Do not include any other explanations or multiple code blocks.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
role: "user",
|
|
102
|
+
content: `Please generate TypeScript code based on the following specifications:
|
|
103
|
+
|
|
104
|
+
Requirements:
|
|
105
|
+
${props.requirements}
|
|
106
|
+
|
|
107
|
+
${
|
|
108
|
+
props.dependencies?.length
|
|
109
|
+
? `Context (other code files):
|
|
110
|
+
${props.dependencies.map((dep) => dep.content).join("\n\n")}`
|
|
111
|
+
: ""
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
The code should:
|
|
115
|
+
1. Be well-documented with JSDoc comments
|
|
116
|
+
2. Follow TypeScript best practices
|
|
117
|
+
3. Include proper type definitions
|
|
118
|
+
4. Be ready to use with the provided context files
|
|
119
|
+
|
|
120
|
+
Your response MUST follow this format:
|
|
121
|
+
- One sentence summary of what the code will do
|
|
122
|
+
- Single TypeScript code block surrounded by \`\`\`ts and \`\`\` tags
|
|
123
|
+
- No other explanations or multiple code blocks`,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const messages = [...baseMessages];
|
|
128
|
+
|
|
129
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
130
|
+
const { text } = await generateText({
|
|
131
|
+
model,
|
|
132
|
+
temperature: Math.max(
|
|
133
|
+
0.1,
|
|
134
|
+
(props.temperature ?? 0.7) * (1 - attempt * 0.2),
|
|
135
|
+
), // Reduce temperature with each attempt
|
|
136
|
+
messages,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const code = extractTypeScriptCode(text);
|
|
140
|
+
if (code) {
|
|
141
|
+
return code;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (attempt === maxAttempts) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"Failed to generate valid TypeScript code after maximum attempts",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add the failed attempt and correction request to the message chain
|
|
151
|
+
messages.push(
|
|
152
|
+
{
|
|
153
|
+
role: "assistant",
|
|
154
|
+
content: text,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
role: "user",
|
|
158
|
+
content:
|
|
159
|
+
"Your response did not match the required format. Please provide your response with exactly one sentence summary" +
|
|
160
|
+
" followed by a single TypeScript code block surrounded by ```ts and ``` tags. No other text or explanations.",
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// This should never be reached due to the throw above
|
|
166
|
+
throw new Error("Unexpected code generation failure");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extracts TypeScript code from a markdown code block
|
|
171
|
+
* @param text Response text that may contain markdown
|
|
172
|
+
* @returns The TypeScript code if found, undefined otherwise
|
|
173
|
+
*/
|
|
174
|
+
function extractTypeScriptCode(text: string): string | undefined {
|
|
175
|
+
const matches = text.match(/```(?:ts|typescript)\n([\s\S]*?)```/);
|
|
176
|
+
return matches?.[1]?.trim();
|
|
177
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateBucketCommand,
|
|
3
|
+
DeleteBucketCommand,
|
|
4
|
+
GetBucketAclCommand,
|
|
5
|
+
GetBucketLocationCommand,
|
|
6
|
+
GetBucketTaggingCommand,
|
|
7
|
+
GetBucketVersioningCommand,
|
|
8
|
+
HeadBucketCommand,
|
|
9
|
+
NoSuchBucket,
|
|
10
|
+
PutBucketTaggingCommand,
|
|
11
|
+
S3Client,
|
|
12
|
+
} from "@aws-sdk/client-s3";
|
|
13
|
+
import { ignore } from "../../error";
|
|
14
|
+
import { Resource } from "../../resource";
|
|
15
|
+
|
|
16
|
+
export interface BucketProps {
|
|
17
|
+
bucketName: string;
|
|
18
|
+
tags?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BucketOutput extends BucketProps {
|
|
22
|
+
id: string; // Same as bucketName for AWS
|
|
23
|
+
arn: string;
|
|
24
|
+
bucketDomainName: string; // Format: bucket-name.s3.amazonaws.com
|
|
25
|
+
bucketRegionalDomainName?: string; // Format: bucket-name.s3.region.amazonaws.com
|
|
26
|
+
hostedZoneId?: string; // S3 hosted zone ID for the region
|
|
27
|
+
region?: string; // AWS Region where bucket resides
|
|
28
|
+
websiteEndpoint?: string; // Only if website hosting is enabled
|
|
29
|
+
websiteDomain?: string; // Only if website hosting is enabled
|
|
30
|
+
versioningEnabled?: boolean;
|
|
31
|
+
acl?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class Bucket extends Resource(
|
|
35
|
+
"s3::Bucket",
|
|
36
|
+
async (ctx, props: BucketProps) => {
|
|
37
|
+
const client = new S3Client({});
|
|
38
|
+
|
|
39
|
+
if (ctx.event === "delete") {
|
|
40
|
+
await ignore(NoSuchBucket.name, () =>
|
|
41
|
+
client.send(
|
|
42
|
+
new DeleteBucketCommand({
|
|
43
|
+
Bucket: props.bucketName,
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
return props;
|
|
48
|
+
} else {
|
|
49
|
+
try {
|
|
50
|
+
// Check if bucket exists
|
|
51
|
+
await client.send(
|
|
52
|
+
new HeadBucketCommand({
|
|
53
|
+
Bucket: props.bucketName,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Update tags if they changed and bucket exists
|
|
58
|
+
if (ctx.event === "update" && props.tags) {
|
|
59
|
+
await client.send(
|
|
60
|
+
new PutBucketTaggingCommand({
|
|
61
|
+
Bucket: props.bucketName,
|
|
62
|
+
Tagging: {
|
|
63
|
+
TagSet: Object.entries(props.tags).map(([Key, Value]) => ({
|
|
64
|
+
Key,
|
|
65
|
+
Value,
|
|
66
|
+
})),
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
} catch (error: any) {
|
|
72
|
+
if (error.name === "NotFound") {
|
|
73
|
+
// Create bucket if it doesn't exist
|
|
74
|
+
await client.send(
|
|
75
|
+
new CreateBucketCommand({
|
|
76
|
+
Bucket: props.bucketName,
|
|
77
|
+
// Add tags during creation if specified
|
|
78
|
+
...(props.tags && {
|
|
79
|
+
Tagging: {
|
|
80
|
+
TagSet: Object.entries(props.tags).map(([Key, Value]) => ({
|
|
81
|
+
Key,
|
|
82
|
+
Value,
|
|
83
|
+
})),
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get bucket details
|
|
94
|
+
const [locationResponse, versioningResponse, aclResponse] =
|
|
95
|
+
await Promise.all([
|
|
96
|
+
client.send(
|
|
97
|
+
new GetBucketLocationCommand({ Bucket: props.bucketName }),
|
|
98
|
+
),
|
|
99
|
+
client.send(
|
|
100
|
+
new GetBucketVersioningCommand({ Bucket: props.bucketName }),
|
|
101
|
+
),
|
|
102
|
+
client.send(new GetBucketAclCommand({ Bucket: props.bucketName })),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const region = locationResponse.LocationConstraint || "us-east-1";
|
|
106
|
+
|
|
107
|
+
// Get tags if they exist
|
|
108
|
+
let tags = props.tags;
|
|
109
|
+
if (!tags) {
|
|
110
|
+
try {
|
|
111
|
+
const taggingResponse = await client.send(
|
|
112
|
+
new GetBucketTaggingCommand({ Bucket: props.bucketName }),
|
|
113
|
+
);
|
|
114
|
+
tags = Object.fromEntries(
|
|
115
|
+
taggingResponse.TagSet?.map(({ Key, Value }) => [Key, Value]) || [],
|
|
116
|
+
);
|
|
117
|
+
} catch (error: any) {
|
|
118
|
+
if (error.name !== "NoSuchTagSet") {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const output: BucketOutput = {
|
|
125
|
+
bucketName: props.bucketName,
|
|
126
|
+
id: props.bucketName,
|
|
127
|
+
arn: `arn:aws:s3:::${props.bucketName}`,
|
|
128
|
+
bucketDomainName: `${props.bucketName}.s3.amazonaws.com`,
|
|
129
|
+
bucketRegionalDomainName: `${props.bucketName}.s3.${region}.amazonaws.com`,
|
|
130
|
+
region,
|
|
131
|
+
hostedZoneId: getHostedZoneId(region),
|
|
132
|
+
versioningEnabled: versioningResponse.Status === "Enabled",
|
|
133
|
+
acl: aclResponse.Grants?.[0]?.Permission?.toLowerCase(),
|
|
134
|
+
...(tags && { tags }),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return output;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
) {}
|
|
141
|
+
|
|
142
|
+
// Helper function to get S3 hosted zone IDs by region
|
|
143
|
+
function getHostedZoneId(region: string): string {
|
|
144
|
+
const hostedZoneIds: Record<string, string> = {
|
|
145
|
+
"us-east-1": "Z3AQBSTGFYJSTF",
|
|
146
|
+
"us-east-2": "Z2O1EMRO9K5GLX",
|
|
147
|
+
"us-west-1": "Z2F56UZL2M1ACD",
|
|
148
|
+
"us-west-2": "Z3BJ6K6RIION7M",
|
|
149
|
+
"af-south-1": "Z11KHD8FBVPUYU",
|
|
150
|
+
"ap-east-1": "ZNB98KWMFR0R6",
|
|
151
|
+
"ap-south-1": "Z11RGJOFQNVJUP",
|
|
152
|
+
"ap-northeast-1": "Z2M4EHUR26P7ZW",
|
|
153
|
+
"ap-northeast-2": "Z3W03O7B5YMIYP",
|
|
154
|
+
"ap-northeast-3": "Z2YQB5RD63NC85",
|
|
155
|
+
"ap-southeast-1": "Z3O0J2DXBE1FTB",
|
|
156
|
+
"ap-southeast-2": "Z1WCIGYICN2BYD",
|
|
157
|
+
"ca-central-1": "Z1QDHH18159H29",
|
|
158
|
+
"eu-central-1": "Z21DNDUVLTQW6Q",
|
|
159
|
+
"eu-west-1": "Z1BKCTXD74EZPE",
|
|
160
|
+
"eu-west-2": "Z3GKZC51ZF0DB4",
|
|
161
|
+
"eu-west-3": "Z3R1K369G5AVDG",
|
|
162
|
+
"eu-north-1": "Z3BAZG2TWCNX0D",
|
|
163
|
+
"eu-south-1": "Z30OZKI7KPW7MI",
|
|
164
|
+
"me-south-1": "Z1MPMWCPA7YB62",
|
|
165
|
+
"sa-east-1": "Z7KQH4QJS55SO",
|
|
166
|
+
};
|
|
167
|
+
return hostedZoneIds[region] || "Z3AQBSTGFYJSTF"; // Default to us-east-1 if region not found
|
|
168
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Architecture,
|
|
3
|
+
CreateFunctionCommand,
|
|
4
|
+
DeleteFunctionCommand,
|
|
5
|
+
GetFunctionCommand,
|
|
6
|
+
GetFunctionConfigurationCommand,
|
|
7
|
+
LambdaClient,
|
|
8
|
+
ResourceNotFoundException,
|
|
9
|
+
Runtime,
|
|
10
|
+
UpdateFunctionCodeCommand,
|
|
11
|
+
UpdateFunctionConfigurationCommand,
|
|
12
|
+
} from "@aws-sdk/client-lambda";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { ignore } from "../../error";
|
|
16
|
+
import { Resource, type Context } from "../../resource";
|
|
17
|
+
|
|
18
|
+
async function resolveRegion(client: LambdaClient): Promise<string> {
|
|
19
|
+
const region = client.config.region;
|
|
20
|
+
if (typeof region === "string") return region;
|
|
21
|
+
if (typeof region === "function") return region();
|
|
22
|
+
throw new Error("Could not resolve AWS region");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FunctionProps {
|
|
26
|
+
functionName: string;
|
|
27
|
+
zipPath: string;
|
|
28
|
+
roleArn: string;
|
|
29
|
+
handler?: string;
|
|
30
|
+
runtime?: Runtime;
|
|
31
|
+
architecture?: Architecture;
|
|
32
|
+
description?: string;
|
|
33
|
+
timeout?: number;
|
|
34
|
+
memorySize?: number;
|
|
35
|
+
environment?: Record<string, string>;
|
|
36
|
+
tags?: Record<string, string>;
|
|
37
|
+
url?: {
|
|
38
|
+
authType?: "AWS_IAM" | "NONE";
|
|
39
|
+
cors?: {
|
|
40
|
+
allowCredentials?: boolean;
|
|
41
|
+
allowHeaders?: string[];
|
|
42
|
+
allowMethods?: string[];
|
|
43
|
+
allowOrigins?: string[];
|
|
44
|
+
exposeHeaders?: string[];
|
|
45
|
+
maxAge?: number;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FunctionOutput extends FunctionProps {
|
|
51
|
+
id: string; // Same as functionName
|
|
52
|
+
arn: string;
|
|
53
|
+
lastModified: string;
|
|
54
|
+
version: string;
|
|
55
|
+
qualifiedArn: string; // ARN with version
|
|
56
|
+
invokeArn: string; // ARN for API Gateway
|
|
57
|
+
sourceCodeHash: string;
|
|
58
|
+
sourceCodeSize: number;
|
|
59
|
+
ephemeralStorageSize?: number;
|
|
60
|
+
architectures: string[];
|
|
61
|
+
masterArn?: string; // Only for Lambda@Edge
|
|
62
|
+
revisionId: string;
|
|
63
|
+
state?: string;
|
|
64
|
+
stateReason?: string;
|
|
65
|
+
stateReasonCode?: string;
|
|
66
|
+
lastUpdateStatus?: string;
|
|
67
|
+
lastUpdateStatusReason?: string;
|
|
68
|
+
lastUpdateStatusReasonCode?: string;
|
|
69
|
+
packageType: string;
|
|
70
|
+
signingProfileVersionArn?: string;
|
|
71
|
+
signingJobArn?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class Function extends Resource(
|
|
75
|
+
"lambda::Function",
|
|
76
|
+
async (ctx: Context<FunctionOutput>, props: FunctionProps) => {
|
|
77
|
+
const client = new LambdaClient({});
|
|
78
|
+
const region = await resolveRegion(client);
|
|
79
|
+
|
|
80
|
+
const code = await zipCode(props.zipPath);
|
|
81
|
+
|
|
82
|
+
if (ctx.event === "delete") {
|
|
83
|
+
await ignore(ResourceNotFoundException.name, () =>
|
|
84
|
+
client.send(
|
|
85
|
+
new DeleteFunctionCommand({
|
|
86
|
+
FunctionName: props.functionName,
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Return a minimal FunctionOutput for deleted state
|
|
92
|
+
return {
|
|
93
|
+
...props,
|
|
94
|
+
id: props.functionName,
|
|
95
|
+
arn: `arn:aws:lambda:${region}:${process.env.AWS_ACCOUNT_ID}:function:${props.functionName}`,
|
|
96
|
+
lastModified: new Date().toISOString(),
|
|
97
|
+
version: "$LATEST",
|
|
98
|
+
qualifiedArn: `arn:aws:lambda:${region}:${process.env.AWS_ACCOUNT_ID}:function:${props.functionName}:$LATEST`,
|
|
99
|
+
invokeArn: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${region}:${process.env.AWS_ACCOUNT_ID}:function:${props.functionName}/invocations`,
|
|
100
|
+
sourceCodeHash: "",
|
|
101
|
+
sourceCodeSize: 0,
|
|
102
|
+
architectures: props.architecture
|
|
103
|
+
? [props.architecture]
|
|
104
|
+
: [Architecture.x86_64],
|
|
105
|
+
revisionId: "",
|
|
106
|
+
state: "Inactive",
|
|
107
|
+
packageType: "Zip",
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
// Check if function exists
|
|
112
|
+
const func = await client.send(
|
|
113
|
+
new GetFunctionCommand({
|
|
114
|
+
FunctionName: props.functionName,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (ctx.event === "update") {
|
|
119
|
+
// Wait for function to stabilize
|
|
120
|
+
await waitForFunctionStabilization(client, props.functionName);
|
|
121
|
+
|
|
122
|
+
// Update function code
|
|
123
|
+
await client.send(
|
|
124
|
+
new UpdateFunctionCodeCommand({
|
|
125
|
+
FunctionName: props.functionName,
|
|
126
|
+
ZipFile: code,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Wait for code update to stabilize
|
|
131
|
+
await waitForFunctionStabilization(client, props.functionName);
|
|
132
|
+
|
|
133
|
+
// Update function configuration
|
|
134
|
+
await client.send(
|
|
135
|
+
new UpdateFunctionConfigurationCommand({
|
|
136
|
+
FunctionName: props.functionName,
|
|
137
|
+
Handler: props.handler,
|
|
138
|
+
Runtime: props.runtime,
|
|
139
|
+
Role: props.roleArn,
|
|
140
|
+
Description: props.description,
|
|
141
|
+
Timeout: props.timeout,
|
|
142
|
+
MemorySize: props.memorySize,
|
|
143
|
+
Environment: props.environment
|
|
144
|
+
? { Variables: props.environment }
|
|
145
|
+
: undefined,
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Wait for configuration update to stabilize
|
|
150
|
+
await waitForFunctionStabilization(client, props.functionName);
|
|
151
|
+
}
|
|
152
|
+
} catch (error: any) {
|
|
153
|
+
if (error.name === "ResourceNotFoundException") {
|
|
154
|
+
// Create function if it doesn't exist
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
let delay = 100; // Start with 100ms delay
|
|
157
|
+
|
|
158
|
+
while (true) {
|
|
159
|
+
try {
|
|
160
|
+
await client.send(
|
|
161
|
+
new CreateFunctionCommand({
|
|
162
|
+
FunctionName: props.functionName,
|
|
163
|
+
Code: { ZipFile: code },
|
|
164
|
+
Handler: props.handler || "index.handler",
|
|
165
|
+
Runtime: props.runtime || Runtime.nodejs20x,
|
|
166
|
+
Role: props.roleArn,
|
|
167
|
+
Description: props.description,
|
|
168
|
+
Timeout: props.timeout || 3,
|
|
169
|
+
MemorySize: props.memorySize || 128,
|
|
170
|
+
Environment: props.environment
|
|
171
|
+
? { Variables: props.environment }
|
|
172
|
+
: undefined,
|
|
173
|
+
Architectures: props.architecture
|
|
174
|
+
? [props.architecture]
|
|
175
|
+
: [Architecture.x86_64],
|
|
176
|
+
Tags: props.tags,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
break; // Success - exit retry loop
|
|
180
|
+
} catch (createError: any) {
|
|
181
|
+
if (
|
|
182
|
+
createError.name !== "InvalidParameterValueException" ||
|
|
183
|
+
!createError.message?.includes("cannot be assumed by Lambda")
|
|
184
|
+
) {
|
|
185
|
+
throw createError; // Different error - rethrow
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (Date.now() - startTime > 10000) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"Timeout waiting for IAM role to be assumable by Lambda after 10s",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
195
|
+
delay = Math.min(delay * 2, 1000); // Exponential backoff capped at 1s
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Wait for function to be active
|
|
200
|
+
let isCreating = true;
|
|
201
|
+
while (isCreating) {
|
|
202
|
+
const config = await client.send(
|
|
203
|
+
new GetFunctionConfigurationCommand({
|
|
204
|
+
FunctionName: props.functionName,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
isCreating = config.State === "Pending";
|
|
208
|
+
if (isCreating) {
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Get complete function details
|
|
218
|
+
const [func, config] = await Promise.all([
|
|
219
|
+
client.send(
|
|
220
|
+
new GetFunctionCommand({
|
|
221
|
+
FunctionName: props.functionName,
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
client.send(
|
|
225
|
+
new GetFunctionConfigurationCommand({
|
|
226
|
+
FunctionName: props.functionName,
|
|
227
|
+
}),
|
|
228
|
+
),
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const output: FunctionOutput = {
|
|
232
|
+
...props,
|
|
233
|
+
id: props.functionName,
|
|
234
|
+
arn: config.FunctionArn!,
|
|
235
|
+
lastModified: config.LastModified!,
|
|
236
|
+
version: config.Version!,
|
|
237
|
+
qualifiedArn: `${config.FunctionArn}:${config.Version}`,
|
|
238
|
+
invokeArn: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${config.FunctionArn}/invocations`,
|
|
239
|
+
sourceCodeHash: config.CodeSha256!,
|
|
240
|
+
sourceCodeSize: config.CodeSize!,
|
|
241
|
+
ephemeralStorageSize: config.EphemeralStorage?.Size,
|
|
242
|
+
architectures: config.Architectures || [],
|
|
243
|
+
masterArn: config.MasterArn,
|
|
244
|
+
revisionId: config.RevisionId!,
|
|
245
|
+
state: config.State,
|
|
246
|
+
stateReason: config.StateReason,
|
|
247
|
+
stateReasonCode: config.StateReasonCode,
|
|
248
|
+
lastUpdateStatus: config.LastUpdateStatus,
|
|
249
|
+
lastUpdateStatusReason: config.LastUpdateStatusReason,
|
|
250
|
+
lastUpdateStatusReasonCode: config.LastUpdateStatusReasonCode,
|
|
251
|
+
packageType: config.PackageType!,
|
|
252
|
+
signingProfileVersionArn: config.SigningProfileVersionArn,
|
|
253
|
+
signingJobArn: config.SigningJobArn,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return output;
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
) {}
|
|
260
|
+
|
|
261
|
+
// Helper to wait for function to stabilize
|
|
262
|
+
async function waitForFunctionStabilization(
|
|
263
|
+
client: LambdaClient,
|
|
264
|
+
functionName: string,
|
|
265
|
+
) {
|
|
266
|
+
while (true) {
|
|
267
|
+
const config = await client.send(
|
|
268
|
+
new GetFunctionConfigurationCommand({
|
|
269
|
+
FunctionName: functionName,
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Check if function is in a stable state
|
|
274
|
+
if (config.State === "Active" && config.LastUpdateStatus === "Successful") {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// If there's a failure, throw an error
|
|
279
|
+
if (config.State === "Failed" || config.LastUpdateStatus === "Failed") {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Function failed to stabilize: ${config.StateReason || config.LastUpdateStatusReason}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Helper to zip the code
|
|
290
|
+
async function zipCode(filePath: string): Promise<Buffer> {
|
|
291
|
+
const fileContent = await fs.promises.readFile(filePath);
|
|
292
|
+
const fileName = path.basename(filePath);
|
|
293
|
+
|
|
294
|
+
// Create a zip buffer in memory
|
|
295
|
+
const zip = new (await import("jszip")).default();
|
|
296
|
+
zip.file(fileName, fileContent);
|
|
297
|
+
return zip.generateAsync({
|
|
298
|
+
type: "nodebuffer",
|
|
299
|
+
compression: "DEFLATE",
|
|
300
|
+
platform: "UNIX",
|
|
301
|
+
});
|
|
302
|
+
}
|