alchemy 0.0.8 → 0.1.1

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/package.json +23 -22
  2. package/src/alchemize.ts +151 -0
  3. package/src/apply.ts +117 -0
  4. package/src/components/agent/agent.ts +45 -0
  5. package/src/components/agent/anthropic.ts +27 -0
  6. package/src/components/agent/index.ts +4 -0
  7. package/src/components/agent/model.ts +22 -0
  8. package/src/components/agent/openai.ts +44 -0
  9. package/src/components/agent/package.ts +126 -0
  10. package/src/components/agent/requirements.ts +111 -0
  11. package/src/components/agent/scrape.ts +47 -0
  12. package/src/components/agent/tsconfig.ts +150 -0
  13. package/src/components/agent/typescript.ts +177 -0
  14. package/src/components/aws/bucket.ts +168 -0
  15. package/src/components/aws/function.ts +302 -0
  16. package/src/components/aws/index.ts +6 -0
  17. package/src/components/aws/policy.ts +245 -0
  18. package/src/components/aws/queue.ts +222 -0
  19. package/src/components/aws/role.ts +245 -0
  20. package/src/components/aws/table.ts +172 -0
  21. package/src/components/esbuild.ts +128 -0
  22. package/src/components/fs.ts +45 -0
  23. package/src/destroy.ts +63 -0
  24. package/src/error.ts +16 -0
  25. package/src/global.ts +72 -0
  26. package/src/index.ts +6 -0
  27. package/src/input.ts +16 -0
  28. package/src/main.ts +34 -0
  29. package/src/output.ts +49 -0
  30. package/src/resource.ts +313 -0
  31. package/src/state.ts +100 -0
  32. package/test/aws/function.test.ts +144 -0
  33. package/test/aws/queue.test.ts +85 -0
  34. package/test/aws/role.test.ts +153 -0
  35. package/test/aws/table.test.ts +73 -0
  36. package/test/esbuild.test.ts +41 -0
  37. package/test/handler.ts +9 -0
  38. package/tsconfig.json +7 -0
  39. package/Column.js +0 -1
  40. package/Create.js +0 -1
  41. package/Delete.js +0 -1
  42. package/Drop.js +0 -1
  43. package/Insert.js +0 -1
  44. package/Makefile +0 -8
  45. package/README.md +0 -0
  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,313 @@
1
+ import { deletions, nodes, providers } from "./global";
2
+ import type { Inputs } from "./input";
3
+ import { Output } from "./output";
4
+ import type { State, StateStore } from "./state";
5
+
6
+ export type ResourceID = string;
7
+ export type ResourceType = string;
8
+
9
+ export const ResourceID = Symbol.for("ResourceID");
10
+ export const Provider = Symbol.for("Provider");
11
+ export const Input = Symbol.for("Input");
12
+ export const Value = Symbol.for("Value");
13
+ export const Apply = Symbol.for("Apply");
14
+ export const Provide = Symbol.for("Provide");
15
+
16
+ // properties that pierce through the Proxy
17
+ const OrthogonalProperties = [
18
+ ResourceID,
19
+ Provider,
20
+ Input,
21
+ Value,
22
+ Apply,
23
+ Provide,
24
+ ] as const;
25
+
26
+ export type Resource<In extends any[] = any[], Out = any> = {
27
+ [ResourceID]: string;
28
+ [Provider]: Provider<any, any[], any>;
29
+ [Input]: Inputs<In>;
30
+ [Value]?: Out;
31
+ [Apply]: <O>(value: Out) => O;
32
+ [Provide]: (value: Out) => void;
33
+ } & Output<Out>;
34
+
35
+ export type Context<Outputs> = {
36
+ stage: string;
37
+ resourceID: ResourceID;
38
+ get<T>(key: string): Promise<T | undefined>;
39
+ set<T>(key: string, value: T): Promise<void>;
40
+ delete<T>(key: string): Promise<T | undefined>;
41
+ /**
42
+ * Indicate that this resource is being replaced.
43
+ * This will cause the resource to be deleted at the end of the stack's CREATE phase.
44
+ */
45
+ replace(): void;
46
+ } & (
47
+ | {
48
+ event: "create";
49
+ }
50
+ | {
51
+ event: "update" | "delete";
52
+ output: Outputs;
53
+ }
54
+ );
55
+
56
+ export type Provider<
57
+ Type extends ResourceType = ResourceType,
58
+ In extends any[] = any[],
59
+ Out = any,
60
+ > = {
61
+ type: Type;
62
+ update(
63
+ stage: string,
64
+ resource: Resource,
65
+ deps: Set<ResourceID>,
66
+ inputs: Inputs<In>,
67
+ stateStore: StateStore,
68
+ ): Promise<Awaited<Out>>;
69
+ delete(
70
+ stage: string,
71
+ resourceID: ResourceID,
72
+ state: State,
73
+ inputs: Inputs<In>,
74
+ ): Promise<void>;
75
+ } & (new (
76
+ id: string,
77
+ ...inputs: Inputs<In>
78
+ ) => Resource<In, Out>);
79
+
80
+ export function isResource(value: any): value is Resource {
81
+ return value?.[ResourceID] !== undefined;
82
+ }
83
+
84
+ export function Resource<
85
+ const Type extends ResourceType,
86
+ Args extends any[],
87
+ Out,
88
+ >(
89
+ type: Type,
90
+ func: (ctx: Context<Out>, ...args: Args) => Promise<Out | void> | Out | void,
91
+ ): Provider<Type, Args, Awaited<Out>> {
92
+ if (providers.has(type)) {
93
+ throw new Error(`Resource ${type} already exists`);
94
+ }
95
+
96
+ interface Resource {
97
+ [ResourceID]: ResourceID;
98
+ [Provider]: Provider<Type, Args, Out>;
99
+ [Input]: Inputs<Args>;
100
+ [Value]?: Out;
101
+ }
102
+
103
+ class Resource {
104
+ static readonly type = type;
105
+
106
+ constructor(id: ResourceID, ...input: Inputs<Args>) {
107
+ const node = {
108
+ provider: Resource,
109
+ resource: this,
110
+ } as const;
111
+ if (nodes.has(id)) {
112
+ // TODO(sam): do we want to throw?
113
+ // it's kind of awesome that you can re-create a resource and call apply
114
+ // console.warn(`Resource ${id} already exists in the stack: ${stack.id}`);
115
+ }
116
+ nodes.set(id, node as any);
117
+
118
+ this[ResourceID] = id;
119
+ this[Provider] = Resource as any;
120
+ this[Input] = input;
121
+ this[Value] = undefined;
122
+ this[Provide] = (value: Out) => {
123
+ this[Value] = value;
124
+ };
125
+
126
+ return new Proxy(this, {
127
+ // TODO(sam): this won't work for the sub-class (class Table extends Resource)
128
+ getPrototypeOf() {
129
+ return Resource.prototype;
130
+ },
131
+ get(target: any, prop) {
132
+ if (OrthogonalProperties.includes(prop as any)) {
133
+ return target[prop];
134
+ } else {
135
+ return target[Apply]((value: Out) => value[prop as keyof Out]);
136
+ }
137
+ },
138
+ });
139
+ }
140
+
141
+ public [Apply]<U>(fn: (value: Out) => U): Output<U> {
142
+ return new Output<Out, U>(this as any, fn);
143
+ }
144
+
145
+ private box?: {
146
+ value: Out;
147
+ } = undefined;
148
+
149
+ public [Provide](value: Out) {
150
+ if (this.box) {
151
+ throw new Error(`Output ${this[ResourceID]} already has a value`);
152
+ }
153
+ this.box = { value };
154
+ const subscribers = this.subscribers;
155
+ this.subscribers = [];
156
+ subscribers.forEach((fn) => fn(value));
157
+ }
158
+
159
+ private subscribers: ((value: Out) => void)[] = [];
160
+
161
+ /**
162
+ * @internal
163
+ */
164
+ public subscribe(fn: (value: Out) => Promise<void>) {
165
+ if (this.box) {
166
+ fn(this.box.value);
167
+ } else {
168
+ this.subscribers.push(fn);
169
+ }
170
+ }
171
+
172
+ static async update(
173
+ stage: string,
174
+ resource: Resource,
175
+ deps: Set<ResourceID>,
176
+ inputs: Args,
177
+ stateStore: StateStore,
178
+ ): Promise<Awaited<Out | void>> {
179
+ // const stack = resource[ResourceStack];
180
+ const resourceID = resource[ResourceID];
181
+
182
+ let resourceState: State | undefined = await stateStore.get(
183
+ stage,
184
+ resourceID,
185
+ );
186
+ if (resourceState === undefined) {
187
+ resourceState = {
188
+ provider: type,
189
+ status: "creating",
190
+ data: {},
191
+ output: undefined,
192
+ deps: [...deps],
193
+ inputs,
194
+ };
195
+ await stateStore.set(stage, resourceID, resourceState);
196
+ }
197
+
198
+ // Skip update if inputs haven't changed and resource is in a stable state
199
+ if (
200
+ (resourceState.status === "created" ||
201
+ resourceState.status === "updated") &&
202
+ JSON.stringify(resourceState.inputs) === JSON.stringify(inputs)
203
+ ) {
204
+ console.log(`Skip: ${resourceID} (no changes)`);
205
+ if (resourceState.output !== undefined) {
206
+ resource[Provide](resourceState.output);
207
+ }
208
+ return resourceState.output;
209
+ }
210
+
211
+ const event = resourceState.status === "creating" ? "create" : "update";
212
+ resourceState.status = event === "create" ? "creating" : "updating";
213
+ resourceState.oldInputs = resourceState.inputs;
214
+ resourceState.inputs = inputs;
215
+
216
+ console.log(
217
+ `${event === "create" ? "Create" : "Update"}: ${resourceID}`,
218
+ );
219
+
220
+ await stateStore.set(stage, resourceID, resourceState);
221
+
222
+ let isReplaced = false;
223
+
224
+ const result = await func(
225
+ {
226
+ stage,
227
+ resourceID,
228
+ event,
229
+ output: resourceState.output,
230
+ replace: () => {
231
+ if (isReplaced) {
232
+ console.warn(
233
+ `Resource ${type} ${resourceID} is already marked as REPLACE`,
234
+ );
235
+ return;
236
+ }
237
+ isReplaced = true;
238
+ deletions.push({
239
+ id: resourceID,
240
+ data: {
241
+ ...resourceState!.data,
242
+ },
243
+ inputs: inputs,
244
+ });
245
+ },
246
+ get: (key) => resourceState!.data[key],
247
+ set: async (key, value) => {
248
+ resourceState!.data[key] = value;
249
+ await stateStore.set(stage, resourceID, resourceState!);
250
+ },
251
+ delete: async (key) => {
252
+ const value = resourceState!.data[key];
253
+ delete resourceState!.data[key];
254
+ await stateStore.set(stage, resourceID, resourceState!);
255
+ return value;
256
+ },
257
+ },
258
+ ...inputs,
259
+ );
260
+
261
+ console.log(
262
+ `${event === "create" ? "Created" : "Updated"}: ${resourceID}`,
263
+ );
264
+ await stateStore.set(stage, resourceID, {
265
+ provider: type,
266
+ data: resourceState.data,
267
+ status: event === "create" ? "created" : "updated",
268
+ output: result,
269
+ inputs,
270
+ deps: [...deps],
271
+ });
272
+ if (result !== undefined) {
273
+ resource[Provide](result);
274
+ }
275
+ return result;
276
+ }
277
+
278
+ static async delete(
279
+ stage: string,
280
+ resourceID: ResourceID,
281
+ state: State,
282
+ inputs: Args,
283
+ ) {
284
+ console.log(`Delete: ${resourceID}`);
285
+ await func(
286
+ {
287
+ stage,
288
+ resourceID: resourceID,
289
+ event: "delete",
290
+ output: state.output,
291
+ replace() {
292
+ throw new Error("Cannot replace a resource that is being deleted");
293
+ },
294
+ get: (key) => {
295
+ return state.data[key];
296
+ },
297
+ set: async (key, value) => {
298
+ state.data[key] = value;
299
+ },
300
+ delete: async (key) => {
301
+ const value = state.data[key];
302
+ delete state.data[key];
303
+ return value;
304
+ },
305
+ },
306
+ ...inputs,
307
+ );
308
+ console.log(`Deleted: ${resourceID}`);
309
+ }
310
+ }
311
+ providers.set(type, Resource as any);
312
+ return Resource as any;
313
+ }
package/src/state.ts ADDED
@@ -0,0 +1,100 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export interface State {
5
+ status:
6
+ | `creating`
7
+ | `created`
8
+ | `updating`
9
+ | `updated`
10
+ | `deleting`
11
+ | `deleted`;
12
+ provider: string;
13
+ data: Record<string, any>;
14
+ deps: string[];
15
+ inputs: any[];
16
+ oldInputs?: any[];
17
+ output: any;
18
+ }
19
+
20
+ export interface StateStore {
21
+ init?(): Promise<void>;
22
+ /** List all resources in the given stage. */
23
+ list(stage: string): Promise<string[]>;
24
+ get(stage: string, key: string): Promise<State | undefined>;
25
+ getBatch(stage: string, ids: string[]): Promise<Record<string, State>>;
26
+ all(stage: string): Promise<Record<string, State>>;
27
+ set(stage: string, key: string, value: State): Promise<void>;
28
+ delete(stage: string, key: string): Promise<void>;
29
+ }
30
+
31
+ const stateFile = path.join(process.cwd(), ".alchemy");
32
+
33
+ /**
34
+ * @internal
35
+ */
36
+ export const defaultStore: StateStore = {
37
+ async init() {
38
+ await fs.promises.mkdir(stateFile, { recursive: true });
39
+ },
40
+ async list(stage) {
41
+ try {
42
+ return (await fs.promises.readdir(path.join(stateFile, stage))).map(
43
+ (file) => file.replace(/\.json$/, ""),
44
+ );
45
+ } catch (error: any) {
46
+ if (error.code === "ENOENT") {
47
+ return [];
48
+ }
49
+ throw error;
50
+ }
51
+ },
52
+ async get(stage, key) {
53
+ try {
54
+ const content = await fs.promises.readFile(
55
+ await getPath(stage, key),
56
+ "utf8",
57
+ );
58
+ return JSON.parse(content) as State;
59
+ } catch (error: any) {
60
+ if (error.code === "ENOENT") {
61
+ return undefined;
62
+ }
63
+ throw error;
64
+ }
65
+ },
66
+ async set(stage, key, value) {
67
+ return fs.promises.writeFile(
68
+ await getPath(stage, key),
69
+ JSON.stringify(value, null, 2),
70
+ );
71
+ },
72
+ async delete(stage, key) {
73
+ return fs.promises.unlink(await getPath(stage, key));
74
+ },
75
+ async all(stage: string): Promise<Record<string, State>> {
76
+ return this.getBatch(stage, await this.list(stage));
77
+ },
78
+ async getBatch(stage: string, ids: string[]): Promise<Record<string, State>> {
79
+ return Object.fromEntries(
80
+ (
81
+ await Promise.all(
82
+ Array.from(ids).flatMap(async (id) => {
83
+ const s = await this.get(stage, id);
84
+ if (s === undefined) {
85
+ return [] as const;
86
+ }
87
+ return [[id, s]] as const;
88
+ }),
89
+ )
90
+ ).flat(),
91
+ );
92
+ },
93
+ };
94
+
95
+ async function getPath(stage: string, key: string) {
96
+ const file = path.join(stateFile, stage, `${key}.json`);
97
+ const dir = path.dirname(file);
98
+ await fs.promises.mkdir(dir, { recursive: true });
99
+ return file;
100
+ }
@@ -0,0 +1,144 @@
1
+ import {
2
+ GetFunctionCommand,
3
+ InvokeCommand,
4
+ LambdaClient,
5
+ ResourceNotFoundException,
6
+ } from "@aws-sdk/client-lambda";
7
+ import { describe, expect, test } from "bun:test";
8
+ import path from "node:path";
9
+ import { apply } from "../../src/apply";
10
+ import { Function } from "../../src/components/aws/function";
11
+ import type { PolicyDocument } from "../../src/components/aws/policy";
12
+ import { Role } from "../../src/components/aws/role";
13
+ import { Bundle } from "../../src/components/esbuild";
14
+ import { destroy } from "../../src/destroy";
15
+
16
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
17
+
18
+ const lambda = new LambdaClient({});
19
+
20
+ describe("AWS Resources", () => {
21
+ describe("Function", () => {
22
+ test("create function with bundled code", async () => {
23
+ // First create the execution role
24
+ const assumeRolePolicy: PolicyDocument = {
25
+ Version: "2012-10-17",
26
+ Statement: [
27
+ {
28
+ Effect: "Allow",
29
+ Principal: {
30
+ Service: "lambda.amazonaws.com",
31
+ },
32
+ Action: "sts:AssumeRole",
33
+ },
34
+ ],
35
+ };
36
+
37
+ const logsPolicy: PolicyDocument = {
38
+ Version: "2012-10-17",
39
+ Statement: [
40
+ {
41
+ Effect: "Allow",
42
+ Action: [
43
+ "logs:CreateLogGroup",
44
+ "logs:CreateLogStream",
45
+ "logs:PutLogEvents",
46
+ ],
47
+ Resource: "*",
48
+ },
49
+ ],
50
+ };
51
+
52
+ const role = new Role("alchemy-test-lambda-role", {
53
+ roleName: "alchemy-test-lambda-role",
54
+ assumeRolePolicy,
55
+ description: "Test role for Lambda function",
56
+ policies: [
57
+ {
58
+ policyName: "logs",
59
+ policyDocument: logsPolicy,
60
+ },
61
+ ],
62
+ tags: {
63
+ Environment: "test",
64
+ },
65
+ });
66
+
67
+ const roleOutput = await apply(role);
68
+
69
+ // Bundle the handler code
70
+ const bundle = new Bundle("test-lambda-bundle", {
71
+ entryPoint: path.join(__dirname, "..", "handler.ts"),
72
+ outdir: ".out",
73
+ format: "cjs",
74
+ platform: "node",
75
+ target: "node18",
76
+ });
77
+
78
+ const bundleOutput = await apply(bundle);
79
+
80
+ // Create the Lambda function
81
+ const func = new Function("alchemy-test-function", {
82
+ functionName: "alchemy-test-function",
83
+ zipPath: bundleOutput.path,
84
+ roleArn: roleOutput.arn,
85
+ handler: "handler.handler",
86
+ runtime: "nodejs20.x",
87
+ tags: {
88
+ Environment: "test",
89
+ },
90
+ });
91
+
92
+ const output = await apply(func);
93
+ expect(output.id).toBe("alchemy-test-function");
94
+ expect(output.arn).toMatch(
95
+ /^arn:aws:lambda:[a-z0-9-]+:\d+:function:alchemy-test-function$/,
96
+ );
97
+ expect(output.state).toBe("Active");
98
+ expect(output.lastUpdateStatus).toBe("Successful");
99
+ expect(output.invokeArn).toMatch(
100
+ /^arn:aws:apigateway:[a-z0-9-]+:lambda:path\/2015-03-31\/functions\/arn:aws:lambda:[a-z0-9-]+:\d+:function:alchemy-test-function\/invocations$/,
101
+ );
102
+
103
+ // Immediately apply again to test stabilization logic
104
+ const secondOutput = await apply(func);
105
+ expect(secondOutput.state).toBe("Active");
106
+ expect(secondOutput.lastUpdateStatus).toBe("Successful");
107
+
108
+ // Invoke the function
109
+ const testEvent = { test: "event" };
110
+ const invokeResponse = await lambda.send(
111
+ new InvokeCommand({
112
+ FunctionName: output.functionName,
113
+ Payload: JSON.stringify(testEvent),
114
+ }),
115
+ );
116
+
117
+ // Parse the response
118
+ const responsePayload = new TextDecoder().decode(invokeResponse.Payload);
119
+ const response = JSON.parse(responsePayload);
120
+ expect(response.statusCode).toBe(200);
121
+
122
+ const body = JSON.parse(response.body);
123
+ expect(body.message).toBe("Hello from bundled handler!");
124
+ expect(body.event).toEqual(testEvent);
125
+
126
+ // Clean up
127
+ await destroy(func);
128
+ await destroy(bundle);
129
+ await destroy(role);
130
+
131
+ await assertFunctionNotExists("alchemy-test-function");
132
+ });
133
+ });
134
+ });
135
+
136
+ async function assertFunctionNotExists(functionName: string) {
137
+ await expect(
138
+ lambda.send(
139
+ new GetFunctionCommand({
140
+ FunctionName: functionName,
141
+ }),
142
+ ),
143
+ ).rejects.toThrow(ResourceNotFoundException);
144
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ GetQueueUrlCommand,
3
+ SQSClient,
4
+ SendMessageCommand,
5
+ } from "@aws-sdk/client-sqs";
6
+ import { describe, expect, test } from "bun:test";
7
+ import { apply } from "../../src/apply";
8
+ import { Queue } from "../../src/components/aws/queue";
9
+ import { destroy } from "../../src/destroy";
10
+
11
+ const sqs = new SQSClient({});
12
+
13
+ describe("AWS Resources", () => {
14
+ describe("Queue", () => {
15
+ const queueName = "alchemy-test-queue";
16
+
17
+ test("create queue, send message, delete, and recreate", async () => {
18
+ // Create initial queue
19
+ const queue = new Queue(queueName, {
20
+ queueName,
21
+ visibilityTimeout: 30,
22
+ messageRetentionPeriod: 345600, // 4 days
23
+ tags: {
24
+ Environment: "test",
25
+ },
26
+ });
27
+
28
+ const output = await apply(queue);
29
+ expect(output.id).toBe(queueName);
30
+ expect(output.arn).toMatch(
31
+ new RegExp(`^arn:aws:sqs:[a-z0-9-]+:\\d+:${queueName}$`),
32
+ );
33
+ expect(output.url).toMatch(
34
+ new RegExp(
35
+ `^https:\\/\\/sqs\\.[a-z0-9-]+\\.amazonaws\\.com\\/\\d+\\/${queueName}$`,
36
+ ),
37
+ );
38
+
39
+ // Send a test message
40
+ const messageResponse = await sqs.send(
41
+ new SendMessageCommand({
42
+ QueueUrl: output.url,
43
+ MessageBody: "Hello from test!",
44
+ }),
45
+ );
46
+ expect(messageResponse.MessageId).toBeTruthy();
47
+
48
+ // Delete the queue
49
+ await destroy(queue);
50
+
51
+ // Verify queue is fully deleted by checking if GetQueueUrl throws
52
+ await expect(
53
+ sqs.send(
54
+ new GetQueueUrlCommand({
55
+ QueueName: queueName,
56
+ }),
57
+ ),
58
+ ).rejects.toThrow("The specified queue does not exist");
59
+
60
+ // Immediately try to recreate the queue - this should handle the QueueDeletedRecently error
61
+ const recreatedQueue = new Queue(queueName, {
62
+ queueName,
63
+ visibilityTimeout: 30,
64
+ messageRetentionPeriod: 345600,
65
+ tags: {
66
+ Environment: "test",
67
+ },
68
+ });
69
+
70
+ const recreatedOutput = await apply(recreatedQueue);
71
+ expect(recreatedOutput.id).toBe(queueName);
72
+ expect(recreatedOutput.arn).toMatch(
73
+ new RegExp(`^arn:aws:sqs:[a-z0-9-]+:\\d+:${queueName}$`),
74
+ );
75
+ expect(recreatedOutput.url).toMatch(
76
+ new RegExp(
77
+ `^https:\\/\\/sqs\\.[a-z0-9-]+\\.amazonaws\\.com\\/\\d+\\/${queueName}$`,
78
+ ),
79
+ );
80
+
81
+ // Clean up
82
+ await destroy(recreatedQueue);
83
+ });
84
+ });
85
+ });