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.
Files changed (105) hide show
  1. package/README.md +355 -0
  2. package/package.json +24 -20
  3. package/src/alchemize.ts +151 -0
  4. package/src/apply.ts +117 -0
  5. package/src/components/agent/agent.ts +45 -0
  6. package/src/components/agent/anthropic.ts +27 -0
  7. package/src/components/agent/index.ts +4 -0
  8. package/src/components/agent/model.ts +22 -0
  9. package/src/components/agent/openai.ts +44 -0
  10. package/src/components/agent/package.ts +126 -0
  11. package/src/components/agent/requirements.ts +111 -0
  12. package/src/components/agent/scrape.ts +47 -0
  13. package/src/components/agent/tsconfig.ts +150 -0
  14. package/src/components/agent/typescript.ts +177 -0
  15. package/src/components/aws/bucket.ts +168 -0
  16. package/src/components/aws/function.ts +302 -0
  17. package/src/components/aws/index.ts +6 -0
  18. package/src/components/aws/policy.ts +245 -0
  19. package/src/components/aws/queue.ts +222 -0
  20. package/src/components/aws/role.ts +245 -0
  21. package/src/components/aws/table.ts +172 -0
  22. package/src/components/esbuild.ts +128 -0
  23. package/src/components/fs.ts +45 -0
  24. package/src/destroy.ts +63 -0
  25. package/src/error.ts +16 -0
  26. package/src/global.ts +72 -0
  27. package/src/index.ts +6 -0
  28. package/src/input.ts +16 -0
  29. package/src/main.ts +34 -0
  30. package/src/output.ts +49 -0
  31. package/src/resource.ts +313 -0
  32. package/src/state.ts +100 -0
  33. package/test/aws/function.test.ts +144 -0
  34. package/test/aws/queue.test.ts +85 -0
  35. package/test/aws/role.test.ts +153 -0
  36. package/test/aws/table.test.ts +73 -0
  37. package/test/esbuild.test.ts +41 -0
  38. package/test/handler.ts +9 -0
  39. package/tsconfig.json +7 -0
  40. package/Column.js +0 -1
  41. package/Create.js +0 -1
  42. package/Delete.js +0 -1
  43. package/Drop.js +0 -1
  44. package/Insert.js +0 -1
  45. package/Makefile +0 -8
  46. package/Select.js +0 -1
  47. package/Table.js +0 -1
  48. package/Transaction.js +0 -1
  49. package/Types.js +0 -1
  50. package/Update.js +0 -1
  51. package/alchemy.js +0 -57
  52. package/alchemy.sublime-project +0 -9
  53. package/alchemy.sublime-workspace +0 -2304
  54. package/config/app.js +0 -8
  55. package/config/db.js +0 -20
  56. package/copy.js +0 -1
  57. package/defaultEngine.js +0 -8
  58. package/engine.js +0 -282
  59. package/getInstantiatorFunction.js +0 -28
  60. package/index.js +0 -52
  61. package/jstest.js +0 -3
  62. package/loggerFactory.js +0 -27
  63. package/logs/alchemy.log +0 -46
  64. package/logs/sql.log +0 -41
  65. package/sql/Column.js +0 -158
  66. package/sql/Columnable.js +0 -21
  67. package/sql/Create.js +0 -15
  68. package/sql/Delete.js +0 -34
  69. package/sql/Drop.js +0 -36
  70. package/sql/Insert.js +0 -96
  71. package/sql/SchemaElement.js +0 -11
  72. package/sql/Select.js +0 -192
  73. package/sql/SqlBase.js +0 -134
  74. package/sql/SqlStatement.js +0 -80
  75. package/sql/Table.js +0 -96
  76. package/sql/Transaction.js +0 -45
  77. package/sql/Update.js +0 -105
  78. package/sql/defineElement.js +0 -26
  79. package/sql/defineStatement.js +0 -26
  80. package/sql/types/BaseType.js +0 -43
  81. package/sql/types/BigSerialType.js +0 -5
  82. package/sql/types/IntegerType.js +0 -5
  83. package/sql/types/JsonType.js +0 -5
  84. package/sql/types/SerialType.js +0 -5
  85. package/sql/types/StringType.js +0 -31
  86. package/sql/types/TimestampType.js +0 -5
  87. package/sql/types/UUIDType.js +0 -5
  88. package/sql/types/defineType.js +0 -25
  89. package/sql/types/getType.js +0 -12
  90. package/sql/types/index.js +0 -11
  91. package/sql/where.js +0 -31
  92. package/sql/whereToString.js +0 -29
  93. package/test/ColumnTests.js +0 -80
  94. package/test/CreateTable.js +0 -54
  95. package/test/DeleteTests.js +0 -28
  96. package/test/DropTests.js +0 -23
  97. package/test/GetTypeTests.js +0 -56
  98. package/test/InsertTests.js +0 -86
  99. package/test/SelectExistsTests.js +0 -26
  100. package/test/SelectTests.js +0 -130
  101. package/test/TableTests.js +0 -46
  102. package/test.js +0 -8
  103. package/test.ls +0 -4
  104. package/validate.js +0 -26
  105. package/x.js +0 -5
@@ -0,0 +1,245 @@
1
+ import {
2
+ AttachRolePolicyCommand,
3
+ CreatePolicyCommand,
4
+ CreatePolicyVersionCommand,
5
+ DeletePolicyCommand,
6
+ DeletePolicyVersionCommand,
7
+ DetachRolePolicyCommand,
8
+ GetPolicyCommand,
9
+ GetPolicyVersionCommand,
10
+ IAMClient,
11
+ ListPolicyVersionsCommand,
12
+ NoSuchEntityException,
13
+ } from "@aws-sdk/client-iam";
14
+ import { ignore } from "../../error";
15
+ import { Resource } from "../../resource";
16
+
17
+ // Type-safe policy document types
18
+ export type Effect = "Allow" | "Deny";
19
+
20
+ export interface PolicyStatement {
21
+ Sid?: string;
22
+ Effect: Effect;
23
+ Action: string | string[];
24
+ Resource?: string | string[];
25
+ Condition?: Record<string, Record<string, string | string[]>>;
26
+ Principal?: Record<string, string | string[]>;
27
+ NotPrincipal?: Record<string, string | string[]>;
28
+ NotAction?: string | string[];
29
+ NotResource?: string | string[];
30
+ }
31
+
32
+ export interface PolicyDocument {
33
+ Version: "2012-10-17";
34
+ Statement: PolicyStatement[];
35
+ }
36
+
37
+ // Policy resource
38
+ export interface PolicyProps {
39
+ policyName: string;
40
+ document: PolicyDocument;
41
+ description?: string;
42
+ path?: string;
43
+ tags?: Record<string, string>;
44
+ }
45
+
46
+ export interface PolicyOutput extends PolicyProps {
47
+ id: string; // Same as policyName
48
+ arn: string;
49
+ defaultVersionId: string;
50
+ attachmentCount: number;
51
+ createDate: Date;
52
+ updateDate: Date;
53
+ isAttachable: boolean;
54
+ }
55
+
56
+ export class Policy extends Resource(
57
+ "iam::Policy",
58
+ async (ctx, props: PolicyProps): Promise<PolicyOutput> => {
59
+ const client = new IAMClient({});
60
+ const policyArn = `arn:aws:iam::${process.env.AWS_ACCOUNT_ID}:policy${props.path || "/"}${props.policyName}`;
61
+
62
+ if (ctx.event === "delete") {
63
+ try {
64
+ // List and delete all non-default versions first
65
+ const versions = await client.send(
66
+ new ListPolicyVersionsCommand({
67
+ PolicyArn: policyArn,
68
+ }),
69
+ );
70
+
71
+ for (const version of versions.Versions || []) {
72
+ if (!version.IsDefaultVersion) {
73
+ await client.send(
74
+ new DeletePolicyVersionCommand({
75
+ PolicyArn: policyArn,
76
+ VersionId: version.VersionId,
77
+ }),
78
+ );
79
+ }
80
+ }
81
+
82
+ // Delete the policy
83
+ await client.send(
84
+ new DeletePolicyCommand({
85
+ PolicyArn: policyArn,
86
+ }),
87
+ );
88
+ } catch (error: any) {
89
+ if (error.name !== NoSuchEntityException.name) {
90
+ throw error;
91
+ }
92
+ }
93
+ return {
94
+ ...props,
95
+ id: props.policyName,
96
+ arn: policyArn,
97
+ defaultVersionId: "v1",
98
+ attachmentCount: 0,
99
+ createDate: new Date(),
100
+ updateDate: new Date(),
101
+ isAttachable: true,
102
+ };
103
+ } else {
104
+ try {
105
+ // Check if policy exists
106
+ const existingPolicy = await client.send(
107
+ new GetPolicyCommand({
108
+ PolicyArn: policyArn,
109
+ }),
110
+ );
111
+
112
+ // Get current policy version
113
+ const currentVersion = await client.send(
114
+ new GetPolicyVersionCommand({
115
+ PolicyArn: policyArn,
116
+ VersionId: existingPolicy.Policy!.DefaultVersionId!,
117
+ }),
118
+ );
119
+
120
+ const currentDocument = JSON.parse(
121
+ decodeURIComponent(currentVersion.PolicyVersion!.Document!),
122
+ );
123
+
124
+ // If policy document changed, create new version
125
+ if (
126
+ JSON.stringify(currentDocument) !== JSON.stringify(props.document)
127
+ ) {
128
+ // List versions to check if we need to delete old ones
129
+ const versions = await client.send(
130
+ new ListPolicyVersionsCommand({
131
+ PolicyArn: policyArn,
132
+ }),
133
+ );
134
+
135
+ // Delete oldest version if we have 5 versions (maximum allowed)
136
+ if (versions.Versions?.length === 5) {
137
+ const oldestVersion = versions.Versions.sort(
138
+ (a, b) => a.CreateDate!.getTime() - b.CreateDate!.getTime(),
139
+ )[0];
140
+
141
+ if (!oldestVersion.IsDefaultVersion) {
142
+ await client.send(
143
+ new DeletePolicyVersionCommand({
144
+ PolicyArn: policyArn,
145
+ VersionId: oldestVersion.VersionId!,
146
+ }),
147
+ );
148
+ }
149
+ }
150
+
151
+ // Create new version
152
+ await client.send(
153
+ new CreatePolicyVersionCommand({
154
+ PolicyArn: policyArn,
155
+ PolicyDocument: JSON.stringify(props.document),
156
+ SetAsDefault: true,
157
+ }),
158
+ );
159
+ }
160
+
161
+ const policy = await client.send(
162
+ new GetPolicyCommand({
163
+ PolicyArn: policyArn,
164
+ }),
165
+ );
166
+
167
+ return {
168
+ ...props,
169
+ id: props.policyName,
170
+ arn: policy.Policy!.Arn!,
171
+ defaultVersionId: policy.Policy!.DefaultVersionId!,
172
+ attachmentCount: policy.Policy!.AttachmentCount!,
173
+ createDate: policy.Policy!.CreateDate!,
174
+ updateDate: policy.Policy!.UpdateDate!,
175
+ isAttachable: policy.Policy!.IsAttachable!,
176
+ };
177
+ } catch (error: any) {
178
+ if (error.name === "NoSuchEntity") {
179
+ // Create new policy
180
+ const newPolicy = await client.send(
181
+ new CreatePolicyCommand({
182
+ PolicyName: props.policyName,
183
+ PolicyDocument: JSON.stringify(props.document),
184
+ Description: props.description,
185
+ Path: props.path,
186
+ Tags: props.tags
187
+ ? Object.entries(props.tags).map(([Key, Value]) => ({
188
+ Key,
189
+ Value,
190
+ }))
191
+ : undefined,
192
+ }),
193
+ );
194
+
195
+ return {
196
+ ...props,
197
+ id: props.policyName,
198
+ arn: newPolicy.Policy!.Arn!,
199
+ defaultVersionId: newPolicy.Policy!.DefaultVersionId!,
200
+ attachmentCount: newPolicy.Policy!.AttachmentCount!,
201
+ createDate: newPolicy.Policy!.CreateDate!,
202
+ updateDate: newPolicy.Policy!.UpdateDate!,
203
+ isAttachable: newPolicy.Policy!.IsAttachable!,
204
+ };
205
+ }
206
+ throw error;
207
+ }
208
+ }
209
+ },
210
+ ) {}
211
+
212
+ // PolicyAttachment resource
213
+ export interface PolicyAttachmentProps {
214
+ policyArn: string;
215
+ roleName: string;
216
+ }
217
+
218
+ export interface PolicyAttachmentOutput extends PolicyAttachmentProps {}
219
+
220
+ export class PolicyAttachment extends Resource(
221
+ "iam::PolicyAttachment",
222
+ async (ctx, props: PolicyAttachmentProps) => {
223
+ const client = new IAMClient({});
224
+
225
+ if (ctx.event === "delete") {
226
+ await ignore(NoSuchEntityException.name, () =>
227
+ client.send(
228
+ new DetachRolePolicyCommand({
229
+ PolicyArn: props.policyArn,
230
+ RoleName: props.roleName,
231
+ }),
232
+ ),
233
+ );
234
+ } else {
235
+ await client.send(
236
+ new AttachRolePolicyCommand({
237
+ PolicyArn: props.policyArn,
238
+ RoleName: props.roleName,
239
+ }),
240
+ );
241
+ }
242
+
243
+ return props;
244
+ },
245
+ ) {}
@@ -0,0 +1,222 @@
1
+ import {
2
+ CreateQueueCommand,
3
+ DeleteQueueCommand,
4
+ GetQueueAttributesCommand,
5
+ GetQueueUrlCommand,
6
+ SQSClient,
7
+ } from "@aws-sdk/client-sqs";
8
+ import { Resource } from "../../resource";
9
+
10
+ export interface QueueProps {
11
+ queueName: string;
12
+ fifo?: boolean;
13
+ visibilityTimeout?: number;
14
+ messageRetentionPeriod?: number;
15
+ maximumMessageSize?: number;
16
+ delaySeconds?: number;
17
+ receiveMessageWaitTimeSeconds?: number;
18
+ contentBasedDeduplication?: boolean;
19
+ deduplicationScope?: "messageGroup" | "queue";
20
+ fifoThroughputLimit?: "perQueue" | "perMessageGroupId";
21
+ tags?: Record<string, string>;
22
+ }
23
+
24
+ export interface QueueOutput extends QueueProps {
25
+ id: string; // Same as queueName
26
+ arn: string;
27
+ url: string;
28
+ }
29
+
30
+ export class Queue extends Resource(
31
+ "sqs::Queue",
32
+ async (ctx, props: QueueProps) => {
33
+ const client = new SQSClient({});
34
+ const queueName = props.fifo ? `${props.queueName}.fifo` : props.queueName;
35
+
36
+ if (ctx.event === "delete") {
37
+ try {
38
+ // Get queue URL first
39
+ const urlResponse = await client.send(
40
+ new GetQueueUrlCommand({
41
+ QueueName: queueName,
42
+ }),
43
+ );
44
+
45
+ // Delete the queue
46
+ await client.send(
47
+ new DeleteQueueCommand({
48
+ QueueUrl: urlResponse.QueueUrl,
49
+ }),
50
+ );
51
+
52
+ // Wait for queue to be deleted
53
+ let queueDeleted = false;
54
+ while (!queueDeleted) {
55
+ try {
56
+ await client.send(
57
+ new GetQueueUrlCommand({
58
+ QueueName: queueName,
59
+ }),
60
+ );
61
+ // If we get here, queue still exists
62
+ await new Promise((resolve) => setTimeout(resolve, 1000));
63
+ } catch (error: any) {
64
+ if (error.name === "QueueDoesNotExist") {
65
+ queueDeleted = true;
66
+ } else {
67
+ throw error;
68
+ }
69
+ }
70
+ }
71
+ } catch (error: any) {
72
+ if (error.name !== "QueueDoesNotExist") {
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ return {
78
+ ...props,
79
+ id: props.queueName,
80
+ arn: `arn:aws:sqs:${client.config.region}:${process.env.AWS_ACCOUNT_ID}:${queueName}`,
81
+ url: "",
82
+ };
83
+ } else {
84
+ // Create queue with attributes
85
+ const attributes: Record<string, string> = {};
86
+
87
+ if (props.visibilityTimeout !== undefined) {
88
+ attributes["VisibilityTimeout"] = props.visibilityTimeout.toString();
89
+ }
90
+ if (props.messageRetentionPeriod !== undefined) {
91
+ attributes["MessageRetentionPeriod"] =
92
+ props.messageRetentionPeriod.toString();
93
+ }
94
+ if (props.maximumMessageSize !== undefined) {
95
+ attributes["MaximumMessageSize"] = props.maximumMessageSize.toString();
96
+ }
97
+ if (props.delaySeconds !== undefined) {
98
+ attributes["DelaySeconds"] = props.delaySeconds.toString();
99
+ }
100
+ if (props.receiveMessageWaitTimeSeconds !== undefined) {
101
+ attributes["ReceiveMessageWaitTimeSeconds"] =
102
+ props.receiveMessageWaitTimeSeconds.toString();
103
+ }
104
+
105
+ // FIFO specific attributes
106
+ if (props.fifo) {
107
+ attributes["FifoQueue"] = "true";
108
+ if (props.contentBasedDeduplication) {
109
+ attributes["ContentBasedDeduplication"] = "true";
110
+ }
111
+ if (props.deduplicationScope) {
112
+ attributes["DeduplicationScope"] = props.deduplicationScope;
113
+ }
114
+ if (props.fifoThroughputLimit) {
115
+ attributes["FifoThroughputLimit"] = props.fifoThroughputLimit;
116
+ }
117
+ }
118
+
119
+ // Convert tags to AWS format
120
+ const tags = props.tags
121
+ ? Object.entries(props.tags).reduce(
122
+ (acc, [key, value]) => ({ ...acc, [key]: value }),
123
+ {},
124
+ )
125
+ : undefined;
126
+
127
+ try {
128
+ // Create the queue
129
+ const createResponse = await client.send(
130
+ new CreateQueueCommand({
131
+ QueueName: queueName,
132
+ Attributes: attributes,
133
+ tags,
134
+ }),
135
+ );
136
+
137
+ // Get queue attributes
138
+ const attributesResponse = await client.send(
139
+ new GetQueueAttributesCommand({
140
+ QueueUrl: createResponse.QueueUrl,
141
+ AttributeNames: ["QueueArn"],
142
+ }),
143
+ );
144
+
145
+ return {
146
+ ...props,
147
+ id: props.queueName,
148
+ arn: attributesResponse.Attributes!.QueueArn!,
149
+ url: createResponse.QueueUrl!,
150
+ };
151
+ } catch (error: any) {
152
+ if (error.name === "QueueAlreadyExists") {
153
+ // Get existing queue URL
154
+ const urlResponse = await client.send(
155
+ new GetQueueUrlCommand({
156
+ QueueName: queueName,
157
+ }),
158
+ );
159
+
160
+ // Get queue attributes
161
+ const attributesResponse = await client.send(
162
+ new GetQueueAttributesCommand({
163
+ QueueUrl: urlResponse.QueueUrl,
164
+ AttributeNames: ["QueueArn"],
165
+ }),
166
+ );
167
+
168
+ return {
169
+ ...props,
170
+ id: props.queueName,
171
+ arn: attributesResponse.Attributes!.QueueArn!,
172
+ url: urlResponse.QueueUrl!,
173
+ };
174
+ } else if (error.name === "QueueDeletedRecently") {
175
+ // Queue was recently deleted, wait and retry
176
+ const maxRetries = 3;
177
+ let retryCount = 0;
178
+
179
+ while (retryCount < maxRetries) {
180
+ try {
181
+ // Wait for 60 seconds before retrying
182
+ await new Promise((resolve) => setTimeout(resolve, 61000));
183
+
184
+ // Retry creating the queue
185
+ const createResponse = await client.send(
186
+ new CreateQueueCommand({
187
+ QueueName: queueName,
188
+ Attributes: attributes,
189
+ tags,
190
+ }),
191
+ );
192
+
193
+ // Get queue attributes
194
+ const attributesResponse = await client.send(
195
+ new GetQueueAttributesCommand({
196
+ QueueUrl: createResponse.QueueUrl,
197
+ AttributeNames: ["QueueArn"],
198
+ }),
199
+ );
200
+
201
+ return {
202
+ ...props,
203
+ id: props.queueName,
204
+ arn: attributesResponse.Attributes!.QueueArn!,
205
+ url: createResponse.QueueUrl!,
206
+ };
207
+ } catch (retryError: any) {
208
+ if (
209
+ retryError.name !== "QueueDeletedRecently" ||
210
+ retryCount === maxRetries - 1
211
+ ) {
212
+ throw retryError;
213
+ }
214
+ retryCount++;
215
+ }
216
+ }
217
+ }
218
+ throw error;
219
+ }
220
+ }
221
+ },
222
+ ) {}
@@ -0,0 +1,245 @@
1
+ import {
2
+ CreateRoleCommand,
3
+ DeleteRoleCommand,
4
+ DeleteRolePolicyCommand,
5
+ EntityAlreadyExistsException,
6
+ GetRoleCommand,
7
+ IAMClient,
8
+ NoSuchEntityException,
9
+ PutRolePolicyCommand,
10
+ type Tag,
11
+ TagRoleCommand,
12
+ UpdateAssumeRolePolicyCommand,
13
+ UpdateRoleCommand,
14
+ } from "@aws-sdk/client-iam";
15
+ import { ignore } from "../../error";
16
+ import { type Context, Resource } from "../../resource";
17
+ import type { PolicyDocument } from "./policy";
18
+
19
+ export interface RoleProps {
20
+ roleName: string;
21
+ assumeRolePolicy: PolicyDocument;
22
+ description?: string;
23
+ path?: string;
24
+ maxSessionDuration?: number;
25
+ permissionsBoundary?: string;
26
+ policies?: Array<{
27
+ policyName: string;
28
+ policyDocument: PolicyDocument;
29
+ }>;
30
+ managedPolicyArns?: string[];
31
+ tags?: Record<string, string>;
32
+ }
33
+
34
+ export interface RoleOutput extends RoleProps {
35
+ id: string; // Same as roleName
36
+ arn: string;
37
+ uniqueId: string; // Unique identifier for the role
38
+ roleId: string; // The stable and unique string identifying the role
39
+ createDate: Date;
40
+ }
41
+
42
+ export class Role extends Resource(
43
+ "iam::Role",
44
+ async (ctx: Context<RoleOutput>, props: RoleProps): Promise<RoleOutput> => {
45
+ const client = new IAMClient({});
46
+
47
+ if (ctx.event === "delete") {
48
+ // Delete any inline policies first
49
+ if (props.policies) {
50
+ for (const policy of props.policies) {
51
+ await ignore(NoSuchEntityException.name, () =>
52
+ client.send(
53
+ new DeleteRolePolicyCommand({
54
+ RoleName: props.roleName,
55
+ PolicyName: policy.policyName,
56
+ }),
57
+ ),
58
+ );
59
+ }
60
+ }
61
+
62
+ await ignore(NoSuchEntityException.name, () =>
63
+ client.send(
64
+ new DeleteRoleCommand({
65
+ RoleName: props.roleName,
66
+ }),
67
+ ),
68
+ );
69
+ return {
70
+ ...props,
71
+ id: props.roleName,
72
+ arn: `arn:aws:iam::${process.env.AWS_ACCOUNT_ID}:role/${props.roleName}`,
73
+ uniqueId: "",
74
+ roleId: "",
75
+ createDate: new Date(),
76
+ };
77
+ }
78
+
79
+ const assumeRolePolicyDocument = JSON.stringify(props.assumeRolePolicy);
80
+ let role;
81
+
82
+ try {
83
+ if (ctx.event === "create") {
84
+ // Try to create the role
85
+ await client.send(
86
+ new CreateRoleCommand({
87
+ RoleName: props.roleName,
88
+ AssumeRolePolicyDocument: assumeRolePolicyDocument,
89
+ Description: props.description,
90
+ Path: props.path,
91
+ MaxSessionDuration: props.maxSessionDuration,
92
+ PermissionsBoundary: props.permissionsBoundary,
93
+ Tags: [
94
+ ...Object.entries(props.tags || {}).map(([Key, Value]) => ({
95
+ Key,
96
+ Value,
97
+ })),
98
+ {
99
+ Key: "alchemy_stage",
100
+ Value: ctx.stage,
101
+ },
102
+ {
103
+ Key: "alchemy_resource",
104
+ Value: ctx.resourceID,
105
+ },
106
+ ],
107
+ }),
108
+ );
109
+ }
110
+ } catch (error: any) {
111
+ if (
112
+ error instanceof EntityAlreadyExistsException &&
113
+ ctx.event === "create"
114
+ ) {
115
+ // Check if we were the ones who created it
116
+ const existingRole = await client.send(
117
+ new GetRoleCommand({
118
+ RoleName: props.roleName,
119
+ }),
120
+ );
121
+ const roleTags =
122
+ existingRole.Role?.Tags?.reduce(
123
+ (acc, tag) => {
124
+ acc[tag.Key!] = tag.Value!;
125
+ return acc;
126
+ },
127
+ {} as Record<string, string>,
128
+ ) || {};
129
+
130
+ if (
131
+ roleTags.alchemy_stage !== ctx.stage ||
132
+ roleTags.alchemy_resource !== ctx.resourceID
133
+ ) {
134
+ throw error;
135
+ }
136
+ } else if (error.name !== NoSuchEntityException.name) {
137
+ throw error;
138
+ }
139
+ }
140
+
141
+ // Get or update the role
142
+ role = await client.send(
143
+ new GetRoleCommand({
144
+ RoleName: props.roleName,
145
+ }),
146
+ );
147
+
148
+ // Update assume role policy if it changed
149
+ if (role.Role?.AssumeRolePolicyDocument !== assumeRolePolicyDocument) {
150
+ await client.send(
151
+ new UpdateAssumeRolePolicyCommand({
152
+ RoleName: props.roleName,
153
+ PolicyDocument: assumeRolePolicyDocument,
154
+ }),
155
+ );
156
+ }
157
+
158
+ // Update role description and max session duration if they changed
159
+ if (
160
+ role.Role?.Description !== props.description ||
161
+ role.Role?.MaxSessionDuration !== props.maxSessionDuration
162
+ ) {
163
+ await client.send(
164
+ new UpdateRoleCommand({
165
+ RoleName: props.roleName,
166
+ Description: props.description,
167
+ MaxSessionDuration: props.maxSessionDuration,
168
+ }),
169
+ );
170
+ }
171
+
172
+ // Update tags
173
+ const newTags = {
174
+ ...props.tags,
175
+ alchemy_stage: ctx.stage,
176
+ alchemy_resource: ctx.resourceID,
177
+ };
178
+ const tags: Tag[] = Object.entries(newTags).map(([Key, Value]) => ({
179
+ Key,
180
+ Value,
181
+ }));
182
+ await client.send(
183
+ new TagRoleCommand({
184
+ RoleName: props.roleName,
185
+ Tags: tags,
186
+ }),
187
+ );
188
+
189
+ // Handle policy changes
190
+ const previousPolicies =
191
+ ctx.event === "update" ? ctx.output.policies || [] : [];
192
+ const currentPolicies = props.policies || [];
193
+
194
+ // Delete policies that were removed
195
+ for (const oldPolicy of previousPolicies) {
196
+ if (
197
+ !currentPolicies.some(
198
+ (p: { policyName: string }) => p.policyName === oldPolicy.policyName,
199
+ )
200
+ ) {
201
+ await ignore(NoSuchEntityException.name, () =>
202
+ client.send(
203
+ new DeleteRolePolicyCommand({
204
+ RoleName: props.roleName,
205
+ PolicyName: oldPolicy.policyName,
206
+ }),
207
+ ),
208
+ );
209
+ }
210
+ }
211
+
212
+ // Update or create policies
213
+ for (const policy of currentPolicies) {
214
+ const oldPolicy = previousPolicies.find(
215
+ (p) => p.policyName === policy.policyName,
216
+ );
217
+ if (
218
+ !oldPolicy ||
219
+ JSON.stringify(oldPolicy.policyDocument) !==
220
+ JSON.stringify(policy.policyDocument)
221
+ ) {
222
+ await client.send(
223
+ new PutRolePolicyCommand({
224
+ RoleName: props.roleName,
225
+ PolicyName: policy.policyName,
226
+ PolicyDocument: JSON.stringify(policy.policyDocument),
227
+ }),
228
+ );
229
+ }
230
+ }
231
+
232
+ if (!role?.Role) {
233
+ throw new Error(`Failed to create or update role ${props.roleName}`);
234
+ }
235
+
236
+ return {
237
+ ...props,
238
+ id: props.roleName,
239
+ arn: role.Role.Arn!,
240
+ uniqueId: role.Role.RoleId!,
241
+ roleId: role.Role.RoleId!,
242
+ createDate: role.Role.CreateDate!,
243
+ };
244
+ },
245
+ ) {}