From 0e1222f000bb3ccbdcd421007d4e9dff03e0cdfd Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Wed, 17 Apr 2024 16:20:14 -0400 Subject: [PATCH 01/22] chore: improve log filtering (#6262) When `wing test` is run against the simulator, logs are streamed to the CLI output. But for technical reasons not all logs are associated with tests. For example, there's a singleton class named `TestRunner` which can have an overridden implementation per target. This is a side effect of how we currently implement test isolation as construct subtrees. As a small improvement, when events are emitted from these resources, this PR changes the current behavior so they're logged to standard output with the "(no test)" label instead of being ignored. ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [ ] Tests added (always) - [ ] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- apps/wing/src/commands/test/test.ts | 15 ++++++--------- .../sdk_tests/queue/push.test.w_test_sim.md | 2 ++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/wing/src/commands/test/test.ts b/apps/wing/src/commands/test/test.ts index 6818c853341..e7057d1ab99 100644 --- a/apps/wing/src/commands/test/test.ts +++ b/apps/wing/src/commands/test/test.ts @@ -464,16 +464,13 @@ async function testSimulator(synthDir: string, options: TestOptions) { const printEvent = async (event: std.Trace) => { const env = extractTestEnvFromPath(event.sourcePath); - if (env === undefined) { - // This event is not associated with any test environment, so skip it. - return; - } - const testName = testMappings[env]; - if (testName === undefined) { - // This event is not associated with any test environment, so skip it. - return; + + let testName = "(no test)"; + if (env !== undefined) { + testName = testMappings[env] ?? testName; } - if (testFilter && !testName.includes(testFilter)) { + + if (testFilter && !testName.includes(testFilter) && testName !== "(no test)") { // This test does not match the filter, so skip it. return; } diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/push.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/push.test.w_test_sim.md index a4bd738a6a5..910cc4f2c1b 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/push.test.w_test_sim.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/push.test.w_test_sim.md @@ -2,6 +2,8 @@ ## stdout.log ```log +[ERROR] (no test) | Push (messages=). Error: Empty messages are not allowed +[ERROR] (no test) | Push (messages=Foo,). Error: Empty messages are not allowed pass ─ push.test.wsim » root/env0/push Tests 1 passed (1) From d867af80f4d89f4e309dd502dbdde57dff9ec3dc Mon Sep 17 00:00:00 2001 From: yoav-steinberg Date: Wed, 17 Apr 2024 23:44:32 +0300 Subject: [PATCH 02/22] fix(compiler): mutable lifted object may be modified inflight (#6258) fixes #3069 The type checker will now modify the type of preflight expressions that are being used inflight to a non-mutable version of the same type: `MutArray` -> `Array`, `MutJson` -> `Json`... Note that since we change the type of the expression we don't get descriptive error telling us that we're trying to mutate a lifted mutable object, instead we just get the same error we'd expect if the lifted object was non-mutable to begin with. This isn't ideal but it makes the fix simpler. Let me know what you think... ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [x] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- .../02-concepts/01-preflight-and-inflight.md | 5 +- .../invalid/un_mut_lifted_objects.test.w | 30 +++++++++ libs/wingc/src/type_check.rs | 32 +++++++-- tools/hangar/__snapshots__/invalid.ts.snap | 66 +++++++++++++++++++ 4 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 examples/tests/invalid/un_mut_lifted_objects.test.w diff --git a/docs/docs/02-concepts/01-preflight-and-inflight.md b/docs/docs/02-concepts/01-preflight-and-inflight.md index c3208888995..418fdef12ad 100644 --- a/docs/docs/02-concepts/01-preflight-and-inflight.md +++ b/docs/docs/02-concepts/01-preflight-and-inflight.md @@ -252,7 +252,7 @@ new cloud.Function(checkEndpoint); ``` However, mutation to preflight data is not allowed. -This mean means that variables from preflight cannot be reassigned to, and mutable collections like `MutArray` and `MutMap` cannot be modified. +This mean means that variables from preflight cannot be reassigned to, and mutable collections like `MutArray` and `MutMap` cannot be modified (they're turned into their immutable counterparts, `Array` and `Map`, respectively when accessed inflight). ```js playground let var count = 3; @@ -263,8 +263,7 @@ names.push("Jack"); // OK inflight () => { count = count + 1; // error: Variable cannot be reassigned from inflight - names.push("Jill"); // error: variable "names" cannot be mutated in inflight - error message not - // implemented yet, see https://github.com/winglang/wing/issues/3069 + names.push("Jill"); // error: push doesn't exist in Array }; ``` diff --git a/examples/tests/invalid/un_mut_lifted_objects.test.w b/examples/tests/invalid/un_mut_lifted_objects.test.w new file mode 100644 index 00000000000..cf42bafa1c0 --- /dev/null +++ b/examples/tests/invalid/un_mut_lifted_objects.test.w @@ -0,0 +1,30 @@ +let ar = MutArray[]; +ar.push(1); + +let j = MutJson {a: 1}; +j.set("a", 2); + +let st = MutSet[1,2]; +st.add(3); + +let mp = MutMap{"a" => 1}; +mp.set("a", 2); + +let opt_ar: MutArray? = MutArray[]; +opt_ar?.push(1); + +let recursive_ar = MutArray>[]; +recursive_ar.push(MutArray[1]); +recursive_ar.at(0).push(2); + +inflight() => { + // Same code as above should be an error in inflight + ar.push(2); // Error: push doesn't exist in Array + ar[0] = 1; // Error: Cannot update elements of an immutable Array + j.set("a", 3); // Error: set doesn't exist in Json + st.add(4); // Error: add doesn't exist in Set + mp.set("a", 3); // Error: set doesn't exist in Map + opt_ar?.push(2); // Error: push doesn't exist in Array + recursive_ar.push(MutArray[2]); // Error: push doesn't exist in Array + recursive_ar.at(0).push(3); // Error: push doesn't exist in Array +}; diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs index 4518c5d76cf..524cac63e6d 100644 --- a/libs/wingc/src/type_check.rs +++ b/libs/wingc/src/type_check.rs @@ -2846,6 +2846,11 @@ impl<'a> TypeChecker<'a> { } }(exp, env); + // If we're inflight but the expression is a lifted (preflight) expression then make it immutable + if env.phase == Phase::Inflight && phase == Phase::Preflight { + t = self.make_immutable(t); + } + self.types.assign_type_to_expr(exp, t, phase); self.curr_expr_info.pop(); @@ -4488,10 +4493,6 @@ impl<'a> TypeChecker<'a> { fn type_check_assignment(&mut self, kind: &AssignmentKind, value: &Expr, variable: &Reference, env: &mut SymbolEnv) { let (exp_type, _) = self.type_check_exp(value, env); - - // TODO: we need to verify that if this variable is defined in a parent environment (i.e. - // being captured) it cannot be reassigned: https://github.com/winglang/wing/issues/3069 - let (var, var_phase) = self.resolve_reference(&variable, env, false); let var_type = match &var { ResolveReferenceResult::Variable(var) => var.type_, @@ -5691,6 +5692,29 @@ impl<'a> TypeChecker<'a> { .map(|_| base_udt) } + fn make_immutable(&mut self, type_: TypeRef) -> TypeRef { + match *type_ { + Type::MutArray(inner) => { + let inner = self.make_immutable(inner); + self.types.add_type(Type::Array(inner)) + } + Type::MutJson => self.types.json(), + Type::MutMap(inner) => { + let inner = self.make_immutable(inner); + self.types.add_type(Type::Map(inner)) + } + Type::MutSet(inner) => { + let inner = self.make_immutable(inner); + self.types.add_type(Type::Set(inner)) + } + Type::Optional(inner) => { + let inner = self.make_immutable(inner); + self.types.add_type(Type::Optional(inner)) + } + _ => type_, + } + } + fn resolve_reference( &mut self, reference: &Reference, diff --git a/tools/hangar/__snapshots__/invalid.ts.snap b/tools/hangar/__snapshots__/invalid.ts.snap index b605cb048ae..ebfa0d9e5b3 100644 --- a/tools/hangar/__snapshots__/invalid.ts.snap +++ b/tools/hangar/__snapshots__/invalid.ts.snap @@ -4255,6 +4255,72 @@ error: Expected type to be \\"num\\", but got \\"str\\" instead +Tests 1 failed (1) +Snapshots 1 skipped +Test Files 1 failed (1) +Duration " +`; + +exports[`un_mut_lifted_objects.test.w 1`] = ` +"error: Member \\"push\\" doesn't exist in \\"Array\\" + --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:22:6 + | +22 | ar.push(2); // Error: push doesn't exist in Array + | ^^^^ Member \\"push\\" doesn't exist in \\"Array\\" + + +error: Cannot update elements of an immutable Array + --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:23:3 + | +23 | ar[0] = 1; // Error: Cannot update elements of an immutable Array + | ^^^^^ Cannot update elements of an immutable Array + | + = hint: Consider using MutArray instead + + +error: Member \\"set\\" doesn't exist in \\"Json\\" + --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:24:5 + | +24 | j.set(\\"a\\", 3); // Error: set doesn't exist in Json + | ^^^ Member \\"set\\" doesn't exist in \\"Json\\" + + +error: Member \\"add\\" doesn't exist in \\"Set\\" + --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:25:6 + | +25 | st.add(4); // Error: add doesn't exist in Set + | ^^^ Member \\"add\\" doesn't exist in \\"Set\\" + + +error: Member \\"set\\" doesn't exist in \\"Map\\" + --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:26:6 + | +26 | mp.set(\\"a\\", 3); // Error: set doesn't exist in Map + | ^^^ Member \\"set\\" doesn't exist in \\"Map\\" + + +error: Member \\"push\\" doesn't exist in \\"Array\\" + --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:27:11 + | +27 | opt_ar?.push(2); // Error: push doesn't exist in Array + | ^^^^ Member \\"push\\" doesn't exist in \\"Array\\" + + +error: Member \\"push\\" doesn't exist in \\"Array\\" + --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:28:16 + | +28 | recursive_ar.push(MutArray[2]); // Error: push doesn't exist in Array + | ^^^^ Member \\"push\\" doesn't exist in \\"Array\\" + + +error: Member \\"push\\" doesn't exist in \\"Array\\" + --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:29:22 + | +29 | recursive_ar.at(0).push(3); // Error: push doesn't exist in Array + | ^^^^ Member \\"push\\" doesn't exist in \\"Array\\" + + + Tests 1 failed (1) Snapshots 1 skipped Test Files 1 failed (1) From 797829a3d3ef5f3b2244e975c856d21161e4bd16 Mon Sep 17 00:00:00 2001 From: Hasan <45375125+hasanaburayyan@users.noreply.github.com> Date: Wed, 17 Apr 2024 19:07:17 -0400 Subject: [PATCH 03/22] revert(compiler): mutable lifted object may be modified inflight (#6263) Reverts winglang/wing#6258 broke some winglibs due to strange behavior trying to call `copyMut` on immutable maps during inflight. See below image --- .../02-concepts/01-preflight-and-inflight.md | 5 +- .../invalid/un_mut_lifted_objects.test.w | 30 --------- libs/wingc/src/type_check.rs | 32 ++------- tools/hangar/__snapshots__/invalid.ts.snap | 66 ------------------- 4 files changed, 7 insertions(+), 126 deletions(-) delete mode 100644 examples/tests/invalid/un_mut_lifted_objects.test.w diff --git a/docs/docs/02-concepts/01-preflight-and-inflight.md b/docs/docs/02-concepts/01-preflight-and-inflight.md index 418fdef12ad..c3208888995 100644 --- a/docs/docs/02-concepts/01-preflight-and-inflight.md +++ b/docs/docs/02-concepts/01-preflight-and-inflight.md @@ -252,7 +252,7 @@ new cloud.Function(checkEndpoint); ``` However, mutation to preflight data is not allowed. -This mean means that variables from preflight cannot be reassigned to, and mutable collections like `MutArray` and `MutMap` cannot be modified (they're turned into their immutable counterparts, `Array` and `Map`, respectively when accessed inflight). +This mean means that variables from preflight cannot be reassigned to, and mutable collections like `MutArray` and `MutMap` cannot be modified. ```js playground let var count = 3; @@ -263,7 +263,8 @@ names.push("Jack"); // OK inflight () => { count = count + 1; // error: Variable cannot be reassigned from inflight - names.push("Jill"); // error: push doesn't exist in Array + names.push("Jill"); // error: variable "names" cannot be mutated in inflight - error message not + // implemented yet, see https://github.com/winglang/wing/issues/3069 }; ``` diff --git a/examples/tests/invalid/un_mut_lifted_objects.test.w b/examples/tests/invalid/un_mut_lifted_objects.test.w deleted file mode 100644 index cf42bafa1c0..00000000000 --- a/examples/tests/invalid/un_mut_lifted_objects.test.w +++ /dev/null @@ -1,30 +0,0 @@ -let ar = MutArray[]; -ar.push(1); - -let j = MutJson {a: 1}; -j.set("a", 2); - -let st = MutSet[1,2]; -st.add(3); - -let mp = MutMap{"a" => 1}; -mp.set("a", 2); - -let opt_ar: MutArray? = MutArray[]; -opt_ar?.push(1); - -let recursive_ar = MutArray>[]; -recursive_ar.push(MutArray[1]); -recursive_ar.at(0).push(2); - -inflight() => { - // Same code as above should be an error in inflight - ar.push(2); // Error: push doesn't exist in Array - ar[0] = 1; // Error: Cannot update elements of an immutable Array - j.set("a", 3); // Error: set doesn't exist in Json - st.add(4); // Error: add doesn't exist in Set - mp.set("a", 3); // Error: set doesn't exist in Map - opt_ar?.push(2); // Error: push doesn't exist in Array - recursive_ar.push(MutArray[2]); // Error: push doesn't exist in Array - recursive_ar.at(0).push(3); // Error: push doesn't exist in Array -}; diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs index 524cac63e6d..4518c5d76cf 100644 --- a/libs/wingc/src/type_check.rs +++ b/libs/wingc/src/type_check.rs @@ -2846,11 +2846,6 @@ impl<'a> TypeChecker<'a> { } }(exp, env); - // If we're inflight but the expression is a lifted (preflight) expression then make it immutable - if env.phase == Phase::Inflight && phase == Phase::Preflight { - t = self.make_immutable(t); - } - self.types.assign_type_to_expr(exp, t, phase); self.curr_expr_info.pop(); @@ -4493,6 +4488,10 @@ impl<'a> TypeChecker<'a> { fn type_check_assignment(&mut self, kind: &AssignmentKind, value: &Expr, variable: &Reference, env: &mut SymbolEnv) { let (exp_type, _) = self.type_check_exp(value, env); + + // TODO: we need to verify that if this variable is defined in a parent environment (i.e. + // being captured) it cannot be reassigned: https://github.com/winglang/wing/issues/3069 + let (var, var_phase) = self.resolve_reference(&variable, env, false); let var_type = match &var { ResolveReferenceResult::Variable(var) => var.type_, @@ -5692,29 +5691,6 @@ impl<'a> TypeChecker<'a> { .map(|_| base_udt) } - fn make_immutable(&mut self, type_: TypeRef) -> TypeRef { - match *type_ { - Type::MutArray(inner) => { - let inner = self.make_immutable(inner); - self.types.add_type(Type::Array(inner)) - } - Type::MutJson => self.types.json(), - Type::MutMap(inner) => { - let inner = self.make_immutable(inner); - self.types.add_type(Type::Map(inner)) - } - Type::MutSet(inner) => { - let inner = self.make_immutable(inner); - self.types.add_type(Type::Set(inner)) - } - Type::Optional(inner) => { - let inner = self.make_immutable(inner); - self.types.add_type(Type::Optional(inner)) - } - _ => type_, - } - } - fn resolve_reference( &mut self, reference: &Reference, diff --git a/tools/hangar/__snapshots__/invalid.ts.snap b/tools/hangar/__snapshots__/invalid.ts.snap index ebfa0d9e5b3..b605cb048ae 100644 --- a/tools/hangar/__snapshots__/invalid.ts.snap +++ b/tools/hangar/__snapshots__/invalid.ts.snap @@ -4255,72 +4255,6 @@ error: Expected type to be \\"num\\", but got \\"str\\" instead -Tests 1 failed (1) -Snapshots 1 skipped -Test Files 1 failed (1) -Duration " -`; - -exports[`un_mut_lifted_objects.test.w 1`] = ` -"error: Member \\"push\\" doesn't exist in \\"Array\\" - --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:22:6 - | -22 | ar.push(2); // Error: push doesn't exist in Array - | ^^^^ Member \\"push\\" doesn't exist in \\"Array\\" - - -error: Cannot update elements of an immutable Array - --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:23:3 - | -23 | ar[0] = 1; // Error: Cannot update elements of an immutable Array - | ^^^^^ Cannot update elements of an immutable Array - | - = hint: Consider using MutArray instead - - -error: Member \\"set\\" doesn't exist in \\"Json\\" - --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:24:5 - | -24 | j.set(\\"a\\", 3); // Error: set doesn't exist in Json - | ^^^ Member \\"set\\" doesn't exist in \\"Json\\" - - -error: Member \\"add\\" doesn't exist in \\"Set\\" - --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:25:6 - | -25 | st.add(4); // Error: add doesn't exist in Set - | ^^^ Member \\"add\\" doesn't exist in \\"Set\\" - - -error: Member \\"set\\" doesn't exist in \\"Map\\" - --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:26:6 - | -26 | mp.set(\\"a\\", 3); // Error: set doesn't exist in Map - | ^^^ Member \\"set\\" doesn't exist in \\"Map\\" - - -error: Member \\"push\\" doesn't exist in \\"Array\\" - --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:27:11 - | -27 | opt_ar?.push(2); // Error: push doesn't exist in Array - | ^^^^ Member \\"push\\" doesn't exist in \\"Array\\" - - -error: Member \\"push\\" doesn't exist in \\"Array\\" - --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:28:16 - | -28 | recursive_ar.push(MutArray[2]); // Error: push doesn't exist in Array - | ^^^^ Member \\"push\\" doesn't exist in \\"Array\\" - - -error: Member \\"push\\" doesn't exist in \\"Array\\" - --> ../../../examples/tests/invalid/un_mut_lifted_objects.test.w:29:22 - | -29 | recursive_ar.at(0).push(3); // Error: push doesn't exist in Array - | ^^^^ Member \\"push\\" doesn't exist in \\"Array\\" - - - Tests 1 failed (1) Snapshots 1 skipped Test Files 1 failed (1) From 962a6a2e31e936561f2de0214b78dfded242f432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Thu, 18 Apr 2024 11:23:34 +0200 Subject: [PATCH 04/22] chore(console): compile app dependencies when running dev (#5845) Now, running `turbo dev` on the console app package will make sure all of its dependencies are up to date. --- apps/wing-console/console/app/turbo.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/wing-console/console/app/turbo.json b/apps/wing-console/console/app/turbo.json index 55f42dab615..e26803eb250 100644 --- a/apps/wing-console/console/app/turbo.json +++ b/apps/wing-console/console/app/turbo.json @@ -2,6 +2,9 @@ "$schema": "https://turborepo.org/schema.json", "extends": ["//"], "pipeline": { + "dev": { + "dependsOn": ["^compile"] + }, "compile": { "outputs": ["dist/**"], "env": ["SEGMENT_WRITE_KEY", "VITE_WING_CLOUD_SIGN_IN_URL"] From 30f49777f818c591f748e41a913a6f57c24775e8 Mon Sep 17 00:00:00 2001 From: Tsuf Cohen <39455181+tsuf239@users.noreply.github.com> Date: Thu, 18 Apr 2024 15:19:44 +0300 Subject: [PATCH 05/22] fix(compiler): variadic docs are displayed wrong on hover (#6265) ## Checklist fixes https://github.com/winglang/wing/issues/4732 ![image](https://github.com/winglang/wing/assets/39455181/1122d878-14a3-4f8e-9679-3ca77b58a376) - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [x] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- libs/wingc/src/docs.rs | 8 +++++--- libs/wingc/src/lsp/hover.rs | 15 +++++++++++++++ .../snapshots/completions/capture_in_test.snap | 2 +- .../completions/forin_before_return_type_ref.snap | 2 +- .../lsp/snapshots/completions/hide_private.snap | 2 +- .../completions/if_before_return_type_ref.snap | 2 +- .../incomplete_inflight_namespace.snap | 4 ++-- .../completions/namespace_middle_dot.snap | 4 ++-- .../completions/new_expression_nested.snap | 2 +- .../partial_type_reference_annotation.snap | 4 ++-- .../lsp/snapshots/completions/show_private.snap | 2 +- .../variable_type_annotation_namespace.snap | 4 ++-- .../src/lsp/snapshots/hovers/variadic_args.snap | 14 ++++++++++++++ libs/wingc/src/type_check.rs | 3 ++- 14 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 libs/wingc/src/lsp/snapshots/hovers/variadic_args.snap diff --git a/libs/wingc/src/docs.rs b/libs/wingc/src/docs.rs index 81087211b38..2c9f0782fc5 100644 --- a/libs/wingc/src/docs.rs +++ b/libs/wingc/src/docs.rs @@ -272,11 +272,13 @@ fn render_signature_help(f: &FunctionSignature) -> String { } else { format!("— `{param_type}`") }; + let is_last_struct = is_last && param_type_unwrapped.is_struct(); + let prefix = if param.variadic || is_last_struct { "..." } else { "" }; - if !is_last || !param_type_unwrapped.is_struct() { - markdown.line(format!("- `{param_name}` {detail_text}")); + if !is_last_struct { + markdown.line(format!("- `{prefix}{param_name}` {detail_text}")); } else { - markdown.line(format!("- `...{param_name}` {detail_text}")); + markdown.line(format!("- `{prefix}{param_name}` {detail_text}")); let structy = param_type_unwrapped.as_struct().unwrap(); let struct_text = render_classlike_members(structy); diff --git a/libs/wingc/src/lsp/hover.rs b/libs/wingc/src/lsp/hover.rs index bf334d8830f..d0c4c9bc051 100644 --- a/libs/wingc/src/lsp/hover.rs +++ b/libs/wingc/src/lsp/hover.rs @@ -412,4 +412,19 @@ Json.stringify({}); //^ "# ); + + test_hover_list!( + variadic_args, + r#" + class Arr { + pub addMany(...items: Array) { + + } + } + + let arr = new Arr(); + arr.addMany("a","b","c"); + //^ + "#, + ); } diff --git a/libs/wingc/src/lsp/snapshots/completions/capture_in_test.snap b/libs/wingc/src/lsp/snapshots/completions/capture_in_test.snap index 7e2f99c0393..38bf9dfdd26 100644 --- a/libs/wingc/src/lsp/snapshots/completions/capture_in_test.snap +++ b/libs/wingc/src/lsp/snapshots/completions/capture_in_test.snap @@ -6,7 +6,7 @@ source: libs/wingc/src/lsp/completions.rs detail: Node documentation: kind: markdown - value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." + value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(...deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." sortText: ab|node - label: copy kind: 2 diff --git a/libs/wingc/src/lsp/snapshots/completions/forin_before_return_type_ref.snap b/libs/wingc/src/lsp/snapshots/completions/forin_before_return_type_ref.snap index 63c6637b5c7..9446eab7082 100644 --- a/libs/wingc/src/lsp/snapshots/completions/forin_before_return_type_ref.snap +++ b/libs/wingc/src/lsp/snapshots/completions/forin_before_return_type_ref.snap @@ -6,7 +6,7 @@ source: libs/wingc/src/lsp/completions.rs detail: Node documentation: kind: markdown - value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." + value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(...deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." sortText: ab|node - label: otherInflight kind: 2 diff --git a/libs/wingc/src/lsp/snapshots/completions/hide_private.snap b/libs/wingc/src/lsp/snapshots/completions/hide_private.snap index 23a993cc64a..8bcfe886210 100644 --- a/libs/wingc/src/lsp/snapshots/completions/hide_private.snap +++ b/libs/wingc/src/lsp/snapshots/completions/hide_private.snap @@ -6,7 +6,7 @@ source: libs/wingc/src/lsp/completions.rs detail: Node documentation: kind: markdown - value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." + value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(...deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." sortText: ab|node - label: onLift kind: 2 diff --git a/libs/wingc/src/lsp/snapshots/completions/if_before_return_type_ref.snap b/libs/wingc/src/lsp/snapshots/completions/if_before_return_type_ref.snap index 63c6637b5c7..9446eab7082 100644 --- a/libs/wingc/src/lsp/snapshots/completions/if_before_return_type_ref.snap +++ b/libs/wingc/src/lsp/snapshots/completions/if_before_return_type_ref.snap @@ -6,7 +6,7 @@ source: libs/wingc/src/lsp/completions.rs detail: Node documentation: kind: markdown - value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." + value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(...deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." sortText: ab|node - label: otherInflight kind: 2 diff --git a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap index ec5f074cc87..7a230715cf6 100644 --- a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap +++ b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -437,7 +437,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 8 documentation: kind: markdown - value: "```wing\ninterface IQueueClient\n```\n---\nInflight interface for `Queue`.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (messages: Array?): void` — Push one or more messages to the queue." + value: "```wing\ninterface IQueueClient\n```\n---\nInflight interface for `Queue`.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue." sortText: ii|IQueueClient - label: IQueueSetConsumerHandler kind: 8 diff --git a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap index ec5f074cc87..7a230715cf6 100644 --- a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap +++ b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -437,7 +437,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 8 documentation: kind: markdown - value: "```wing\ninterface IQueueClient\n```\n---\nInflight interface for `Queue`.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (messages: Array?): void` — Push one or more messages to the queue." + value: "```wing\ninterface IQueueClient\n```\n---\nInflight interface for `Queue`.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue." sortText: ii|IQueueClient - label: IQueueSetConsumerHandler kind: 8 diff --git a/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap b/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap index 2f4723ceb43..396726602d3 100644 --- a/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap +++ b/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap @@ -82,7 +82,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue insertText: Queue($1) insertTextFormat: 2 diff --git a/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap b/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap index ec5f074cc87..7a230715cf6 100644 --- a/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap +++ b/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -437,7 +437,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 8 documentation: kind: markdown - value: "```wing\ninterface IQueueClient\n```\n---\nInflight interface for `Queue`.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (messages: Array?): void` — Push one or more messages to the queue." + value: "```wing\ninterface IQueueClient\n```\n---\nInflight interface for `Queue`.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue." sortText: ii|IQueueClient - label: IQueueSetConsumerHandler kind: 8 diff --git a/libs/wingc/src/lsp/snapshots/completions/show_private.snap b/libs/wingc/src/lsp/snapshots/completions/show_private.snap index a760e5e3a70..1147b12503f 100644 --- a/libs/wingc/src/lsp/snapshots/completions/show_private.snap +++ b/libs/wingc/src/lsp/snapshots/completions/show_private.snap @@ -13,7 +13,7 @@ source: libs/wingc/src/lsp/completions.rs detail: Node documentation: kind: markdown - value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." + value: "```wing\npreflight node: Node\nclass Node\n```\n---\nRepresents the construct node in the scope tree.\n\n### Initializer\n- `host` — `Construct`\n- `scope` — `IConstruct`\n- `id` — `str`\n### Fields\n- `PATH_SEP` — `str` — Separator used to delimit construct path components.\n- `addr` — `str` — Returns an opaque tree-unique address for this construct.\n- `children` — `Array` — All direct children of this construct.\n- `dependencies` — `Array` — Return all dependencies registered on this node (non-recursive).\n- `id` — `str` — The id of this construct within the current scope.\n- `locked` — `bool` — Returns true if this construct or the scopes in which it is defined are locked.\n- `metadata` — `Array` — An immutable array of metadata objects associated with this construct.\n- `path` — `str` — The full, absolute path of this construct in the tree.\n- `root` — `IConstruct` — Returns the root of the construct tree.\n- `scopes` — `Array` — All parent scopes of this construct.\n- `defaultChild?` — `IConstruct?` — Returns the child construct that has the id `Default` or `Resource\"`.\n- `scope?` — `IConstruct?` — Returns the scope in which this construct is defined.\n### Methods\n- `addDependency` — `(...deps: Array?): void` — Add an ordering dependency on another construct.\n- `addMetadata` — `(type: str, data: any, options: MetadataOptions?): void` — Adds a metadata entry to this construct.\n- `addValidation` — `(validation: IValidation): void` — Adds a validation to this construct.\n- `findAll` — `(order: ConstructOrder?): Array` — Return this construct and all of its children in the given order.\n- `findChild` — `(id: str): IConstruct` — Return a direct child by id.\n- `getAllContext` — `(defaults: Json?): any` — Retrieves the all context of a node from tree context.\n- `getContext` — `(key: str): any` — Retrieves a value from tree context if present. Otherwise, would throw an error.\n- `lock` — `(): void` — Locks this construct from allowing more children to be added.\n- `of` — `(construct: IConstruct): Node` — Returns the node associated with a construct.\n- `setContext` — `(key: str, value: any): void` — This can be used to set contextual values.\n- `tryFindChild` — `(id: str): IConstruct?` — Return a direct child by id, or undefined.\n- `tryGetContext` — `(key: str): any` — Retrieves a value from tree context.\n- `tryRemoveChild` — `(childName: str): bool` — Remove the child with the given name, if present.\n- `validate` — `(): Array` — Validates this construct." sortText: ab|node - label: onLift kind: 2 diff --git a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap index ec5f074cc87..7a230715cf6 100644 --- a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap +++ b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -437,7 +437,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 8 documentation: kind: markdown - value: "```wing\ninterface IQueueClient\n```\n---\nInflight interface for `Queue`.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (messages: Array?): void` — Push one or more messages to the queue." + value: "```wing\ninterface IQueueClient\n```\n---\nInflight interface for `Queue`.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue." sortText: ii|IQueueClient - label: IQueueSetConsumerHandler kind: 8 diff --git a/libs/wingc/src/lsp/snapshots/hovers/variadic_args.snap b/libs/wingc/src/lsp/snapshots/hovers/variadic_args.snap new file mode 100644 index 00000000000..0b2a74a3189 --- /dev/null +++ b/libs/wingc/src/lsp/snapshots/hovers/variadic_args.snap @@ -0,0 +1,14 @@ +--- +source: libs/wingc/src/lsp/hover.rs +--- +contents: + kind: markdown + value: "```wing\npreflight addMany: preflight (...items: Array): void\n```\n---\n### Parameters\n- `...items` — `Array`" +range: + start: + line: 8 + character: 6 + end: + line: 8 + character: 13 + diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs index 4518c5d76cf..1336e16254a 100644 --- a/libs/wingc/src/type_check.rs +++ b/libs/wingc/src/type_check.rs @@ -904,7 +904,8 @@ impl Display for FunctionSignature { if a.name.is_empty() { format!("{}", a.typeref) } else { - format!("{}: {}", a.name, a.typeref) + let prefix = if a.variadic { "..." } else { "" }; + format!("{}{}: {}", prefix, a.name, a.typeref) } }) .collect::>() From b08a84f134119e4f18ed009127d06dda2caf2cb7 Mon Sep 17 00:00:00 2001 From: eladcon Date: Thu, 18 Apr 2024 16:01:23 +0300 Subject: [PATCH 06/22] chore: move JS inflight into the sdk (#6250) Moving `inflight()`, `lift()`, etc... into the SDK so it would be possible to create inflight closures from TS code instead of text ## Checklist - [ ] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [ ] Description explains motivation and solution - [ ] Tests added (always) - [ ] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- libs/@wingcloud/framework/src/index.ts | 2 +- libs/@wingcloud/framework/src/inflight.ts | 214 ------------------ libs/@wingcloud/framework/src/transformer.ts | 15 +- libs/wingsdk/src/core/inflight.ts | 210 +++++++++++++++++ .../src => wingsdk/src/core}/utility-types.ts | 7 +- 5 files changed, 220 insertions(+), 228 deletions(-) delete mode 100644 libs/@wingcloud/framework/src/inflight.ts rename libs/{@wingcloud/framework/src => wingsdk/src/core}/utility-types.ts (87%) diff --git a/libs/@wingcloud/framework/src/index.ts b/libs/@wingcloud/framework/src/index.ts index fc39ac6ae4c..0d8f4a90991 100644 --- a/libs/@wingcloud/framework/src/index.ts +++ b/libs/@wingcloud/framework/src/index.ts @@ -3,7 +3,7 @@ export { cloud, ex } from "@winglang/sdk"; export { Construct } from "@winglang/sdk/lib/core/types"; // typescript workflow primitives -export { inflight, lift } from "./inflight"; +export { inflight, lift } from "@winglang/sdk/lib/core"; export { main } from "./main"; // used internally by wing compiler diff --git a/libs/@wingcloud/framework/src/inflight.ts b/libs/@wingcloud/framework/src/inflight.ts deleted file mode 100644 index 3bc3f9fc991..00000000000 --- a/libs/@wingcloud/framework/src/inflight.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { LiftableMap, LiftedMap, PickNonFunctions } from "./utility-types"; -import { - liftObject, - closureId, - LiftDepsMatrixRaw, -} from "@winglang/sdk/lib/core"; -import { - AsyncFunction, - INFLIGHT_SYMBOL, - Inflight, - OperationsOf, -} from "@winglang/sdk/lib/core/types"; -import type { IHostedLiftable } from "@winglang/sdk/lib/std/resource"; - -/** - * Prepares preflight objects for use in inflight functions. - * - * Conventionally, this is used by passing in a `const` object to bind it with the same name - * - * ```ts - * const bucket = new cloud.Bucket(app, "Bucket"); - * const number = 5; - * - * lift({ bucket, number }) - * .inflight(({ bucket, number }) => { ... })) - * ``` - * - * However, the name is not required to match the variable in the current scope. - * - * This is especially useful/necessary when lifting data via a reference or some other expression - * - * ```ts - * const bucket = new cloud.Bucket(app, "Bucket"); - * - * lift({ bkt: bucket, sum: 2 + 2, field: bucket.field }) - * .inflight(({ bkt, sum, field }) => { ... })) - * ``` - */ -export function lift( - captures: TToLift -): Lifter, {}> { - return new Lifter().lift(captures); -} - -/** - * Creates a inflight function. - * - * This function must not reference any variables outside of its scope. - * If needed, use `lift` to bind variables to the scope of the function. - * - * Built-in NodeJS globals are available, such as `console` and `process`. - */ -export function inflight( - fn: (ctx: {}, ...args: Parameters) => ReturnType -) { - return new Lifter().inflight(fn); -} - -/** - * Manages the liftable objects and operations for an inflight function. - */ -class Lifter< - TLifted extends Record, - TOperations extends Record -> { - constructor( - private lifts: LiftableMap = {}, - private grants: Record = {} - ) {} - - /** - * Add additional liftable objects to the scope of the inflight function. - * Any existing liftable objects with the same name will be overwritten. - * - * Conventionally, this is used by passing in a `const` object to bind it with the same name - * - * ```ts - * const bucket = new cloud.Bucket(app, "Bucket"); - * const number = 5; - * - * lift({ bucket, number }) - * .inflight(({ bucket, number }) => { ... })) - * ``` - * - * However, the name is not required to match the variable in the current scope. - * - * This is especially useful/necessary when lifting data via a reference or some other expression - * - * ```ts - * const bucket = new cloud.Bucket(app, "Bucket"); - * - * lift({ bkt: bucket, sum: 2 + 2, field: bucket.field }) - * .inflight(({ bkt, sum, field }) => { ... })) - * ``` - */ - public lift(captures: TWillLift) { - return new Lifter< - Omit & LiftedMap, - TOperations - >( - { - ...this.lifts, - ...captures, - }, - this.grants - ); - } - - /** - * Grant permissions for lifted resources. - * - * By default, all all possible methods are granted to lifted resources. - * This function restricts those: - * - * ```ts - * const bucket = new cloud.Bucket(app, "Bucket"); - * - * lift({ bucket }) - * .grant({ bucket: ["get"] }) - * .inflight(({ bucket }) => { - * await bucket.get("key"); - * await bucket.set("key", "value"); // Error: set is not granted - * }); - * ``` - * - * fields are always accessible, even if not granted. - */ - public grant< - TNewOps extends Partial<{ - [K in keyof TLifted]: OperationsOf; - }> - >(grants: TNewOps) { - return new Lifter & TNewOps>( - this.lifts, - { - ...this.grants, - ...grants, - } - ); - } - - /** - * Create an inflight function with the available lifted data. - * - * This function must not reference any variables outside of its scope. - * If needed, use `lift` again to bind variables to the scope of the function. - * Bound variables will be available as properties on the `ctx` object passed as the first argument to the function. - * - * Built-in NodeJS globals are available, such as `console` and `process`. - */ - public inflight( - fn: ( - /** All lifted data available in this inflight */ - ctx: // Get all the lifted types which were not explicitly granted - Omit & { - // For each of the granted types, get the lifted type with only the granted operations available (and any fields as well) - [K in keyof TOperations & - keyof TLifted]: TOperations[K] extends (infer TGrantedOps extends keyof TLifted[K])[] - ? PickNonFunctions & Pick - : TLifted[K]; - }, - ...args: Parameters - ) => ReturnType - ): Inflight { - // This is a simplified version of the Wing compiler's _liftMap generation - // It specifies what transitive permissions need to be added based on what - // inflight methods are called on an object - // The SDK models inflight functions as objects with a "handle" property, - // so here we annotate that "handle" needs all of the required permissions - const _liftMap: LiftDepsMatrixRaw = { handle: [], $inflight_init: [] }; - for (const [key, obj] of Object.entries(this.lifts)) { - let knownOps = this.grants[key]; - if ( - knownOps === undefined && - typeof (obj as IHostedLiftable)?._supportedOps === "function" - ) { - knownOps = (obj as IHostedLiftable)._supportedOps(); - } - _liftMap.handle.push([obj, knownOps ?? []]); - } - - return { - _id: closureId(), - _toInflight: () => { - // Extremely advanced function serialization - const serializedFunction = fn.toString(); - - return `\ -(await (async () => { - const $func = ${serializedFunction} - const $ctx = { - ${Object.entries(this.lifts) - .map(([name, liftable]) => `${name}: ${liftObject(liftable)}`) - .join(",\n")} - }; - let newFunction = async (...args) => { - return $func($ctx, ...args); - }; - newFunction.handle = newFunction; - return newFunction; -} -)())`; - }, - _liftMap, - _supportedOps: () => [], - // @ts-expect-error This function's type doesn't actually match, but it will just throw anyways - [INFLIGHT_SYMBOL]: () => { - throw new Error( - "This is a inflight function and can only be invoked while inflight" - ); - }, - }; - } -} diff --git a/libs/@wingcloud/framework/src/transformer.ts b/libs/@wingcloud/framework/src/transformer.ts index 6527ed1909d..936faf9eca6 100644 --- a/libs/@wingcloud/framework/src/transformer.ts +++ b/libs/@wingcloud/framework/src/transformer.ts @@ -49,15 +49,14 @@ export class InflightTransformer { const decl = sym.declarations?.at(0); if (!decl) { return true; - } else if ( - decl - .getSourceFile() - .fileName.replaceAll("\\", "/") - .includes("/@wingcloud/framework/") - ) { - return true; } else { - return getImportSpecifier(decl) === '"@wingcloud/framework"'; + const fileName = decl.getSourceFile().fileName.replaceAll("\\", "/"); + if (fileName.includes("/@wingcloud/framework/") || fileName.includes("/wingsdk/lib/core/")) { + return true; + } + + const importSpecifier = getImportSpecifier(decl) + return importSpecifier === '"@wingcloud/framework"' || importSpecifier === '"@winglang/sdk"'; } } diff --git a/libs/wingsdk/src/core/inflight.ts b/libs/wingsdk/src/core/inflight.ts index 816f795dcbb..6061c233509 100644 --- a/libs/wingsdk/src/core/inflight.ts +++ b/libs/wingsdk/src/core/inflight.ts @@ -1,5 +1,14 @@ import { basename } from "path"; +import { liftObject, LiftDepsMatrixRaw } from "./lifting"; +import { + AsyncFunction, + INFLIGHT_SYMBOL, + Inflight, + OperationsOf, +} from "./types"; +import { LiftableMap, LiftedMap, PickNonFunctions } from "./utility-types"; import { normalPath } from "../shared/misc"; +import type { IHostedLiftable } from "../std/resource"; let closureCount = 0; @@ -53,3 +62,204 @@ export class InflightClient { private constructor() {} } + +/** + * Prepares preflight objects for use in inflight functions. + * + * Conventionally, this is used by passing in a `const` object to bind it with the same name + * + * ```ts + * const bucket = new cloud.Bucket(app, "Bucket"); + * const number = 5; + * + * lift({ bucket, number }) + * .inflight(({ bucket, number }) => { ... })) + * ``` + * + * However, the name is not required to match the variable in the current scope. + * + * This is especially useful/necessary when lifting data via a reference or some other expression + * + * ```ts + * const bucket = new cloud.Bucket(app, "Bucket"); + * + * lift({ bkt: bucket, sum: 2 + 2, field: bucket.field }) + * .inflight(({ bkt, sum, field }) => { ... })) + * ``` + */ +export function lift( + captures: TToLift +): Lifter, {}> { + return new Lifter().lift(captures); +} + +/** + * Creates a inflight function. + * + * This function must not reference any variables outside of its scope. + * If needed, use `lift` to bind variables to the scope of the function. + * + * Built-in NodeJS globals are available, such as `console` and `process`. + */ +export function inflight( + fn: (ctx: {}, ...args: Parameters) => ReturnType +) { + return new Lifter().inflight(fn); +} + +/** + * Manages the liftable objects and operations for an inflight function. + */ +class Lifter< + TLifted extends Record, + TOperations extends Record +> { + constructor( + private lifts: LiftableMap = {}, + private grants: Record = {} + ) {} + + /** + * Add additional liftable objects to the scope of the inflight function. + * Any existing liftable objects with the same name will be overwritten. + * + * Conventionally, this is used by passing in a `const` object to bind it with the same name + * + * ```ts + * const bucket = new cloud.Bucket(app, "Bucket"); + * const number = 5; + * + * lift({ bucket, number }) + * .inflight(({ bucket, number }) => { ... })) + * ``` + * + * However, the name is not required to match the variable in the current scope. + * + * This is especially useful/necessary when lifting data via a reference or some other expression + * + * ```ts + * const bucket = new cloud.Bucket(app, "Bucket"); + * + * lift({ bkt: bucket, sum: 2 + 2, field: bucket.field }) + * .inflight(({ bkt, sum, field }) => { ... })) + * ``` + */ + public lift(captures: TWillLift) { + return new Lifter< + Omit & LiftedMap, + TOperations + >( + { + ...this.lifts, + ...captures, + }, + this.grants + ); + } + + /** + * Grant permissions for lifted resources. + * + * By default, all all possible methods are granted to lifted resources. + * This function restricts those: + * + * ```ts + * const bucket = new cloud.Bucket(app, "Bucket"); + * + * lift({ bucket }) + * .grant({ bucket: ["get"] }) + * .inflight(({ bucket }) => { + * await bucket.get("key"); + * await bucket.set("key", "value"); // Error: set is not granted + * }); + * ``` + * + * fields are always accessible, even if not granted. + */ + public grant< + TNewOps extends Partial<{ + [K in keyof TLifted]: OperationsOf; + }> + >(grants: TNewOps) { + return new Lifter & TNewOps>( + this.lifts, + { + ...this.grants, + ...grants, + } + ); + } + + /** + * Create an inflight function with the available lifted data. + * + * This function must not reference any variables outside of its scope. + * If needed, use `lift` again to bind variables to the scope of the function. + * Bound variables will be available as properties on the `ctx` object passed as the first argument to the function. + * + * Built-in NodeJS globals are available, such as `console` and `process`. + */ + public inflight( + fn: ( + /** All lifted data available in this inflight */ + ctx: // Get all the lifted types which were not explicitly granted + Omit & { + // For each of the granted types, get the lifted type with only the granted operations available (and any fields as well) + [K in keyof TOperations & + keyof TLifted]: TOperations[K] extends (infer TGrantedOps extends keyof TLifted[K])[] + ? PickNonFunctions & Pick + : TLifted[K]; + }, + ...args: Parameters + ) => ReturnType + ): Inflight { + // This is a simplified version of the Wing compiler's _liftMap generation + // It specifies what transitive permissions need to be added based on what + // inflight methods are called on an object + // The SDK models inflight functions as objects with a "handle" property, + // so here we annotate that "handle" needs all of the required permissions + const _liftMap: LiftDepsMatrixRaw = { handle: [], $inflight_init: [] }; + for (const [key, obj] of Object.entries(this.lifts)) { + let knownOps = this.grants[key]; + if ( + knownOps === undefined && + typeof (obj as IHostedLiftable)?._supportedOps === "function" + ) { + knownOps = (obj as IHostedLiftable)._supportedOps(); + } + _liftMap.handle.push([obj, knownOps ?? []]); + } + + return { + _id: closureId(), + _toInflight: () => { + // Extremely advanced function serialization + const serializedFunction = fn.toString(); + + return `\ +(await (async () => { + const $func = ${serializedFunction} + const $ctx = { + ${Object.entries(this.lifts) + .map(([name, liftable]) => `${name}: ${liftObject(liftable)}`) + .join(",\n")} + }; + let newFunction = async (...args) => { + return $func($ctx, ...args); + }; + newFunction.handle = newFunction; + return newFunction; +} +)())`; + }, + _liftMap, + _supportedOps: () => [], + // @ts-expect-error This function's type doesn't actually match, but it will just throw anyways + [INFLIGHT_SYMBOL]: () => { + throw new Error( + "This is a inflight function and can only be invoked while inflight" + ); + }, + }; + } +} diff --git a/libs/@wingcloud/framework/src/utility-types.ts b/libs/wingsdk/src/core/utility-types.ts similarity index 87% rename from libs/@wingcloud/framework/src/utility-types.ts rename to libs/wingsdk/src/core/utility-types.ts index d88383e69af..5f0e7a3c450 100644 --- a/libs/@wingcloud/framework/src/utility-types.ts +++ b/libs/wingsdk/src/core/utility-types.ts @@ -1,8 +1,5 @@ -import type { - IHostedLiftable, - ILiftable, -} from "@winglang/sdk/lib/std/resource"; -import { INFLIGHT_SYMBOL } from "@winglang/sdk/lib/core/types"; +import { INFLIGHT_SYMBOL } from "./types"; +import type { IHostedLiftable, ILiftable } from "../std/resource"; /** * Extracts the non-function properties from a given type as an array of their discriminated keys. From 24178a41c3d4cea3abf2d22a62f62ed3fd9dfbcf Mon Sep 17 00:00:00 2001 From: partha04patel Date: Thu, 18 Apr 2024 09:31:00 -0400 Subject: [PATCH 07/22] feat(sdk)!: add delete method for MutJson class (#6236) ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [ ] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. BREAKING CHANGE: `has()` removed from static `Json`. It is now an instance method on `Json` objects. --- docs/docs/04-standard-library/std/json.md | 76 +++++++++++++------ examples/tests/sdk_tests/std/json.test.w | 12 ++- examples/tests/valid/json.test.w | 2 +- examples/tests/valid/json_static.test.w | 4 +- .../snapshots/completions/json_statics.snap | 12 --- .../completions/mut_json_methods.snap | 24 ++++++ .../completions/optional_chaining.snap | 12 +++ .../completions/optional_chaining_auto.snap | 21 +++++ .../completions/parentheses_expression.snap | 12 +++ .../static_completions_after_expression.snap | 12 --- .../static_json_after_expression.snap | 12 --- ...tatic_json_after_expression_statement.snap | 12 --- .../snapshots/hovers/static_method_root.snap | 2 +- libs/wingsdk/src/std/json.ts | 37 +++++++-- .../sdk_tests/std/json.test.w_test_sim.md | 3 +- .../valid/json.test.w_compile_tf-aws.md | 2 +- .../json_static.test.w_compile_tf-aws.md | 7 +- 17 files changed, 170 insertions(+), 92 deletions(-) diff --git a/docs/docs/04-standard-library/std/json.md b/docs/docs/04-standard-library/std/json.md index da8d8a1e80c..72436d5dc3f 100644 --- a/docs/docs/04-standard-library/std/json.md +++ b/docs/docs/04-standard-library/std/json.md @@ -21,6 +21,7 @@ Immutable Json. | asStr | Convert Json element to string if possible. | | get | Returns the value associated with the specified Json key. | | getAt | Returns a specified element at a given index from Json Array. | +| has | Checks if a Json object has a given key. | | tryAsBool | Convert Json element to boolean if possible. | | tryAsNum | Convert Json element to number if possible. | | tryAsStr | Convert Json element to string if possible. | @@ -85,6 +86,22 @@ The index of the element in the Json Array to return. --- +##### `has` + +```wing +has(key: str): bool +``` + +Checks if a Json object has a given key. + +###### `key`Required + +- *Type:* str + +The key to check. + +--- + ##### `tryAsBool` ```wing @@ -149,7 +166,6 @@ The index of the element in the Json Array to return. | deepCopyMut | Creates a mutable deep copy of the Json. | | delete | Deletes a key in a given Json. | | entries | Returns the entries from the Json. | -| has | Checks if a Json object has a given key. | | keys | Returns the keys from the Json. | | parse | Parse a string into a Json. | | stringify | Formats Json as string. | @@ -230,30 +246,6 @@ map to get the entries from. --- -##### `has` - -```wing -Json.has(json: Json, key: str); -``` - -Checks if a Json object has a given key. - -###### `json`Required - -- *Type:* Json - -The json object to inspect. - ---- - -###### `key`Required - -- *Type:* str - -The key to check. - ---- - ##### `keys` ```wing @@ -417,8 +409,10 @@ Mutable Json. | asBool | Convert Json element to boolean if possible. | | asNum | Convert Json element to number if possible. | | asStr | Convert Json element to string if possible. | +| delete | Removes the specified element from a map. | | get | Returns the value associated with the specified Json key. | | getAt | Returns a specified element at a given index from MutJson Array. | +| has | Checks if a Json object has a given key. | | set | Adds or updates an element in MutJson with a specific key and value. | | setAt | Set element in MutJson Array with a specific key and value. | | tryAsBool | Convert Json element to boolean if possible. | @@ -453,6 +447,22 @@ asStr(): str Convert Json element to string if possible. +##### `delete` + +```wing +delete(key: str): bool +``` + +Removes the specified element from a map. + +###### `key`Required + +- *Type:* str + +The key. + +--- + ##### `get` ```wing @@ -485,6 +495,22 @@ The index of the element in the MutJson Array to return. --- +##### `has` + +```wing +has(key: str): bool +``` + +Checks if a Json object has a given key. + +###### `key`Required + +- *Type:* str + +The key to check. + +--- + ##### `set` ```wing diff --git a/examples/tests/sdk_tests/std/json.test.w b/examples/tests/sdk_tests/std/json.test.w index d8f3b72d474..7339077e79e 100644 --- a/examples/tests/sdk_tests/std/json.test.w +++ b/examples/tests/sdk_tests/std/json.test.w @@ -6,8 +6,8 @@ bring cloud; test "has()" { let obj = Json { key1: 1, key2: 2}; - assert(Json.has(obj, "key1") == true); - assert(Json.has(obj, "key3") == false); + assert(obj.has("key1") == true); + assert(obj.has("key3") == false); } test "get()" { @@ -178,4 +178,12 @@ test "deepCopy(), deepCopyMut()" { assert(copy != copyMut); assert(copyMut.get("object") == mutation); +} + +test "delete() for MutJson" { + let mutObj = MutJson { x: 1, y: 2 }; + mutObj.delete("x"); + assert(mutObj.has("x") == false); + assert(mutObj.has("y")==true); + assert(mutObj.delete("random key that doesn't exist") == true); } \ No newline at end of file diff --git a/examples/tests/valid/json.test.w b/examples/tests/valid/json.test.w index fac7da364d5..83d0579264a 100644 --- a/examples/tests/valid/json.test.w +++ b/examples/tests/valid/json.test.w @@ -206,7 +206,7 @@ assert(notSpecified.get("foo") == "bar"); // Check that empty {} is a Json let empty = {}; -assert(Json.has(empty, "something") == false); +assert(empty.has("something") == false); struct Base { base: str; diff --git a/examples/tests/valid/json_static.test.w b/examples/tests/valid/json_static.test.w index 812a9351d31..360baee950e 100644 --- a/examples/tests/valid/json_static.test.w +++ b/examples/tests/valid/json_static.test.w @@ -58,6 +58,6 @@ test "Access Json static inflight" { // Check whether some key exists in a json test "has key or not" { let hasCheck = Json {a: "hello", b: "wing"}; - assert(Json.has(hasCheck, "a") == true); - assert(Json.has(hasCheck, "c") == false); + assert(hasCheck.has("a") == true); + assert(hasCheck.has("c") == false); } \ No newline at end of file diff --git a/libs/wingc/src/lsp/snapshots/completions/json_statics.snap b/libs/wingc/src/lsp/snapshots/completions/json_statics.snap index 365b8c2425c..ad6eb5ddf4c 100644 --- a/libs/wingc/src/lsp/snapshots/completions/json_statics.snap +++ b/libs/wingc/src/lsp/snapshots/completions/json_statics.snap @@ -49,18 +49,6 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints -- label: has - kind: 2 - detail: "(json: Json, key: str): bool" - documentation: - kind: markdown - value: "```wing\nstatic has: (json: Json, key: str): bool\n```\n---\nChecks if a Json object has a given key.\n### Parameters\n- `json` — `Json` — The json object to inspect.\n- `key` — `str` — The key to check.\n\n### Returns\nBoolean value corresponding to whether the key exists" - sortText: ff|has - insertText: has($1) - insertTextFormat: 2 - command: - title: triggerParameterHints - command: editor.action.triggerParameterHints - label: keys kind: 2 detail: "(json: any): Array" diff --git a/libs/wingc/src/lsp/snapshots/completions/mut_json_methods.snap b/libs/wingc/src/lsp/snapshots/completions/mut_json_methods.snap index c26469afc94..f3a29c0b31e 100644 --- a/libs/wingc/src/lsp/snapshots/completions/mut_json_methods.snap +++ b/libs/wingc/src/lsp/snapshots/completions/mut_json_methods.snap @@ -25,6 +25,18 @@ source: libs/wingc/src/lsp/completions.rs value: "```wing\nasStr: (): str\n```\n---\nConvert Json element to string if possible.\n\n### Returns\na string." sortText: ff|asStr insertText: asStr() +- label: delete + kind: 2 + detail: "(key: str): bool" + documentation: + kind: markdown + value: "```wing\ndelete: (key: str): bool\n```\n---\nRemoves the specified element from a map.\n### Parameters\n- `key` — `str` — The key.\n\n### Returns\ntrue if the given key is no longer present" + sortText: ff|delete + insertText: delete($1) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: get kind: 2 detail: "(key: str): MutJson" @@ -49,6 +61,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: has + kind: 2 + detail: "(key: str): bool" + documentation: + kind: markdown + value: "```wing\nhas: (key: str): bool\n```\n---\nChecks if a Json object has a given key.\n### Parameters\n- `key` — `str` — The key to check.\n\n### Returns\nBoolean value corresponding to whether the key exists" + sortText: ff|has + insertText: has($1) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: set kind: 2 detail: "(key: str, value: MutJson): void" diff --git a/libs/wingc/src/lsp/snapshots/completions/optional_chaining.snap b/libs/wingc/src/lsp/snapshots/completions/optional_chaining.snap index 9ffa80db684..59086c30f09 100644 --- a/libs/wingc/src/lsp/snapshots/completions/optional_chaining.snap +++ b/libs/wingc/src/lsp/snapshots/completions/optional_chaining.snap @@ -49,6 +49,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: has + kind: 2 + detail: "(key: str): bool" + documentation: + kind: markdown + value: "```wing\nhas: (key: str): bool\n```\n---\nChecks if a Json object has a given key.\n### Parameters\n- `key` — `str` — The key to check.\n\n### Returns\nBoolean value corresponding to whether the key exists" + sortText: ff|has + insertText: has($1) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: tryAsBool kind: 2 detail: "(): bool?" diff --git a/libs/wingc/src/lsp/snapshots/completions/optional_chaining_auto.snap b/libs/wingc/src/lsp/snapshots/completions/optional_chaining_auto.snap index c5129c8a827..ed82e23a65e 100644 --- a/libs/wingc/src/lsp/snapshots/completions/optional_chaining_auto.snap +++ b/libs/wingc/src/lsp/snapshots/completions/optional_chaining_auto.snap @@ -94,6 +94,27 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: has + kind: 2 + detail: "(key: str): bool" + documentation: + kind: markdown + value: "```wing\nhas: (key: str): bool\n```\n---\nChecks if a Json object has a given key.\n### Parameters\n- `key` — `str` — The key to check.\n\n### Returns\nBoolean value corresponding to whether the key exists" + sortText: ff|has + insertText: has($1) + insertTextFormat: 2 + additionalTextEdits: + - range: + start: + line: 2 + character: 12 + end: + line: 2 + character: 13 + newText: "?." + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: tryAsBool kind: 2 detail: "(): bool?" diff --git a/libs/wingc/src/lsp/snapshots/completions/parentheses_expression.snap b/libs/wingc/src/lsp/snapshots/completions/parentheses_expression.snap index 9ffa80db684..59086c30f09 100644 --- a/libs/wingc/src/lsp/snapshots/completions/parentheses_expression.snap +++ b/libs/wingc/src/lsp/snapshots/completions/parentheses_expression.snap @@ -49,6 +49,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: has + kind: 2 + detail: "(key: str): bool" + documentation: + kind: markdown + value: "```wing\nhas: (key: str): bool\n```\n---\nChecks if a Json object has a given key.\n### Parameters\n- `key` — `str` — The key to check.\n\n### Returns\nBoolean value corresponding to whether the key exists" + sortText: ff|has + insertText: has($1) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: tryAsBool kind: 2 detail: "(): bool?" diff --git a/libs/wingc/src/lsp/snapshots/completions/static_completions_after_expression.snap b/libs/wingc/src/lsp/snapshots/completions/static_completions_after_expression.snap index 365b8c2425c..ad6eb5ddf4c 100644 --- a/libs/wingc/src/lsp/snapshots/completions/static_completions_after_expression.snap +++ b/libs/wingc/src/lsp/snapshots/completions/static_completions_after_expression.snap @@ -49,18 +49,6 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints -- label: has - kind: 2 - detail: "(json: Json, key: str): bool" - documentation: - kind: markdown - value: "```wing\nstatic has: (json: Json, key: str): bool\n```\n---\nChecks if a Json object has a given key.\n### Parameters\n- `json` — `Json` — The json object to inspect.\n- `key` — `str` — The key to check.\n\n### Returns\nBoolean value corresponding to whether the key exists" - sortText: ff|has - insertText: has($1) - insertTextFormat: 2 - command: - title: triggerParameterHints - command: editor.action.triggerParameterHints - label: keys kind: 2 detail: "(json: any): Array" diff --git a/libs/wingc/src/lsp/snapshots/completions/static_json_after_expression.snap b/libs/wingc/src/lsp/snapshots/completions/static_json_after_expression.snap index 365b8c2425c..ad6eb5ddf4c 100644 --- a/libs/wingc/src/lsp/snapshots/completions/static_json_after_expression.snap +++ b/libs/wingc/src/lsp/snapshots/completions/static_json_after_expression.snap @@ -49,18 +49,6 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints -- label: has - kind: 2 - detail: "(json: Json, key: str): bool" - documentation: - kind: markdown - value: "```wing\nstatic has: (json: Json, key: str): bool\n```\n---\nChecks if a Json object has a given key.\n### Parameters\n- `json` — `Json` — The json object to inspect.\n- `key` — `str` — The key to check.\n\n### Returns\nBoolean value corresponding to whether the key exists" - sortText: ff|has - insertText: has($1) - insertTextFormat: 2 - command: - title: triggerParameterHints - command: editor.action.triggerParameterHints - label: keys kind: 2 detail: "(json: any): Array" diff --git a/libs/wingc/src/lsp/snapshots/completions/static_json_after_expression_statement.snap b/libs/wingc/src/lsp/snapshots/completions/static_json_after_expression_statement.snap index 365b8c2425c..ad6eb5ddf4c 100644 --- a/libs/wingc/src/lsp/snapshots/completions/static_json_after_expression_statement.snap +++ b/libs/wingc/src/lsp/snapshots/completions/static_json_after_expression_statement.snap @@ -49,18 +49,6 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints -- label: has - kind: 2 - detail: "(json: Json, key: str): bool" - documentation: - kind: markdown - value: "```wing\nstatic has: (json: Json, key: str): bool\n```\n---\nChecks if a Json object has a given key.\n### Parameters\n- `json` — `Json` — The json object to inspect.\n- `key` — `str` — The key to check.\n\n### Returns\nBoolean value corresponding to whether the key exists" - sortText: ff|has - insertText: has($1) - insertTextFormat: 2 - command: - title: triggerParameterHints - command: editor.action.triggerParameterHints - label: keys kind: 2 detail: "(json: any): Array" diff --git a/libs/wingc/src/lsp/snapshots/hovers/static_method_root.snap b/libs/wingc/src/lsp/snapshots/hovers/static_method_root.snap index 04e5543ad8d..e1800e030c5 100644 --- a/libs/wingc/src/lsp/snapshots/hovers/static_method_root.snap +++ b/libs/wingc/src/lsp/snapshots/hovers/static_method_root.snap @@ -3,7 +3,7 @@ source: libs/wingc/src/lsp/hover.rs --- contents: kind: markdown - value: "```wing\nclass Json\n```\n---\nImmutable Json.\n\n### Methods\n- `asBool` — `(): bool` — Convert Json element to boolean if possible.\n- `asNum` — `(): num` — Convert Json element to number if possible.\n- `asStr` — `(): str` — Convert Json element to string if possible.\n- `deepCopy` — `(json: MutJson): Json` — Creates an immutable deep copy of the Json.\n- `deepCopyMut` — `(json: Json): MutJson` — Creates a mutable deep copy of the Json.\n- `delete` — `(json: MutJson, key: str): void` — Deletes a key in a given Json.\n- `entries` — `(json: Json): Array` — Returns the entries from the Json.\n- `get` — `(key: str): Json` — Returns the value associated with the specified Json key.\n- `getAt` — `(index: num): Json` — Returns a specified element at a given index from Json Array.\n- `has` — `(json: Json, key: str): bool` — Checks if a Json object has a given key.\n- `keys` — `(json: any): Array` — Returns the keys from the Json.\n- `parse` — `(str: str): Json` — Parse a string into a Json.\n- `stringify` — `(json: any, options: JsonStringifyOptions?): str` — Formats Json as string.\n- `tryAsBool` — `(): bool?` — Convert Json element to boolean if possible.\n- `tryAsNum` — `(): num?` — Convert Json element to number if possible.\n- `tryAsStr` — `(): str?` — Convert Json element to string if possible.\n- `tryGet` — `(key: str): Json?` — Optionally returns an specified element from the Json.\n- `tryGetAt` — `(index: num): Json?` — Optionally returns a specified element at a given index from Json Array.\n- `tryParse` — `(str: str?): Json?` — Try to parse a string into a Json.\n- `values` — `(json: Json): Array` — Returns the values from the Json." + value: "```wing\nclass Json\n```\n---\nImmutable Json.\n\n### Methods\n- `asBool` — `(): bool` — Convert Json element to boolean if possible.\n- `asNum` — `(): num` — Convert Json element to number if possible.\n- `asStr` — `(): str` — Convert Json element to string if possible.\n- `deepCopy` — `(json: MutJson): Json` — Creates an immutable deep copy of the Json.\n- `deepCopyMut` — `(json: Json): MutJson` — Creates a mutable deep copy of the Json.\n- `delete` — `(json: MutJson, key: str): void` — Deletes a key in a given Json.\n- `entries` — `(json: Json): Array` — Returns the entries from the Json.\n- `get` — `(key: str): Json` — Returns the value associated with the specified Json key.\n- `getAt` — `(index: num): Json` — Returns a specified element at a given index from Json Array.\n- `has` — `(key: str): bool` — Checks if a Json object has a given key.\n- `keys` — `(json: any): Array` — Returns the keys from the Json.\n- `parse` — `(str: str): Json` — Parse a string into a Json.\n- `stringify` — `(json: any, options: JsonStringifyOptions?): str` — Formats Json as string.\n- `tryAsBool` — `(): bool?` — Convert Json element to boolean if possible.\n- `tryAsNum` — `(): num?` — Convert Json element to number if possible.\n- `tryAsStr` — `(): str?` — Convert Json element to string if possible.\n- `tryGet` — `(key: str): Json?` — Optionally returns an specified element from the Json.\n- `tryGetAt` — `(index: num): Json?` — Optionally returns a specified element at a given index from Json Array.\n- `tryParse` — `(str: str?): Json?` — Try to parse a string into a Json.\n- `values` — `(json: Json): Array` — Returns the values from the Json." range: start: line: 1 diff --git a/libs/wingsdk/src/std/json.ts b/libs/wingsdk/src/std/json.ts index 2a7673107d5..26a08e70a5a 100644 --- a/libs/wingsdk/src/std/json.ts +++ b/libs/wingsdk/src/std/json.ts @@ -155,24 +155,20 @@ export class Json { str; throw new Error("Macro"); } - + private constructor() {} /** * Checks if a Json object has a given key * - * @macro ((json, key) => { return json.hasOwnProperty(key); })($args$) + * @macro ((obj, key) => { return obj.hasOwnProperty(key); })($self$,$args$) * - * @param json The json object to inspect * @param key The key to check * @returns Boolean value corresponding to whether the key exists */ - public static has(json: Json, key: string): boolean { - json; + public has(key: string): boolean { key; throw new Error("Macro"); } - private constructor() {} - /** * Returns the value associated with the specified Json key * @@ -453,4 +449,31 @@ export class MutJson { public tryAsBool(): boolean | undefined { throw new Error("Macro"); } + + /** + * Removes the specified element from a map. + * + * @macro (delete ($self$)[$args$]) + * + * @param key The key + * @returns true if the given key is no longer present + */ + + public delete(key: string): boolean { + key; + throw new Error("Macro"); + } + + /** + * Checks if a Json object has a given key + * + * @macro ((obj, key) => { return obj.hasOwnProperty(key); })($self$,$args$) + * + * @param key The key to check + * @returns Boolean value corresponding to whether the key exists + */ + public has(key: string): boolean { + key; + throw new Error("Macro"); + } } diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/std/json.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/std/json.test.w_test_sim.md index 42f62dffa96..1ff2de3e9ea 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/std/json.test.w_test_sim.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/std/json.test.w_test_sim.md @@ -4,6 +4,7 @@ ```log pass ─ json.test.wsim » root/env0/test:has() pass ─ json.test.wsim » root/env1/test:get() +pass ─ json.test.wsim » root/env10/test:delete() for MutJson pass ─ json.test.wsim » root/env2/test:getAt() pass ─ json.test.wsim » root/env3/test:set() pass ─ json.test.wsim » root/env4/test:setAt() @@ -13,7 +14,7 @@ pass ─ json.test.wsim » root/env7/test:parse() pass ─ json.test.wsim » root/env8/test:tryParse() pass ─ json.test.wsim » root/env9/test:deepCopy(), deepCopyMut() -Tests 10 passed (10) +Tests 11 passed (11) Snapshots 1 skipped Test Files 1 passed (1) Duration diff --git a/tools/hangar/__snapshots__/test_corpus/valid/json.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/json.test.w_compile_tf-aws.md index 4190779ac7e..930a2c339f1 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/json.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/json.test.w_compile_tf-aws.md @@ -210,7 +210,7 @@ class $Root extends $stdlib.std.Resource { const notSpecified = ({"foo": "bar"}); $helpers.assert($helpers.eq(((obj, args) => { if (obj[args] === undefined) throw new Error(`Json property "${args}" does not exist`); return obj[args] })(notSpecified, "foo"), "bar"), "notSpecified.get(\"foo\") == \"bar\""); const empty = ({}); - $helpers.assert($helpers.eq(((json, key) => { return json.hasOwnProperty(key); })(empty, "something"), false), "Json.has(empty, \"something\") == false"); + $helpers.assert($helpers.eq(((obj, key) => { return obj.hasOwnProperty(key); })(empty,"something"), false), "empty.has(\"something\") == false"); const arrayStruct = [({"foo": "", "stuff": []})]; const setStruct = new Set([({"foo": "", "stuff": []})]); const mapStruct = ({["1"]: ({"foo": "", "stuff": []})}); diff --git a/tools/hangar/__snapshots__/test_corpus/valid/json_static.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/json_static.test.w_compile_tf-aws.md index 5001699ac15..cbef2880146 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/json_static.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/json_static.test.w_compile_tf-aws.md @@ -25,7 +25,7 @@ module.exports = function({ $jj, $std_Json }) { ```cjs "use strict"; const $helpers = require("@winglang/sdk/lib/helpers"); -module.exports = function({ $std_Json }) { +module.exports = function({ }) { class $Closure2 { constructor({ }) { const $obj = (...args) => this.handle(...args); @@ -34,8 +34,8 @@ module.exports = function({ $std_Json }) { } async handle() { const hasCheck = ({"a": "hello", "b": "wing"}); - $helpers.assert($helpers.eq(((json, key) => { return json.hasOwnProperty(key); })(hasCheck, "a"), true), "Json.has(hasCheck, \"a\") == true"); - $helpers.assert($helpers.eq(((json, key) => { return json.hasOwnProperty(key); })(hasCheck, "c"), false), "Json.has(hasCheck, \"c\") == false"); + $helpers.assert($helpers.eq(((obj, key) => { return obj.hasOwnProperty(key); })(hasCheck,"a"), true), "hasCheck.has(\"a\") == true"); + $helpers.assert($helpers.eq(((obj, key) => { return obj.hasOwnProperty(key); })(hasCheck,"c"), false), "hasCheck.has(\"c\") == false"); } } return $Closure2; @@ -121,7 +121,6 @@ class $Root extends $stdlib.std.Resource { static _toInflightType() { return ` require("${$helpers.normalPath(__dirname)}/inflight.$Closure2-1.cjs")({ - $std_Json: ${$stdlib.core.liftObject($stdlib.core.toLiftableModuleType(std.Json, "@winglang/sdk/std", "Json"))}, }) `; } From 8d9d77e3f6d5076e102f77d1c2f483659a933559 Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Fri, 19 Apr 2024 15:17:55 -0400 Subject: [PATCH 08/22] fix(compiler): array access operator precedence (#6277) Fixes #6276 by adding a precedence level for array access expressions. I inserted the precedence right below member access expressions, similar to JavaScript's operator precedence [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table). ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [ ] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- examples/tests/valid/indexing.test.w | 2 + libs/tree-sitter-wing/grammar.js | 9 ++-- libs/tree-sitter-wing/src/grammar.json | 8 +-- .../test/corpus/expressions.txt | 52 +++++++++++++++++++ .../valid/indexing.test.w_compile_tf-aws.md | 1 + 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/examples/tests/valid/indexing.test.w b/examples/tests/valid/indexing.test.w index b4ad2d837fb..7a34d28ee77 100644 --- a/examples/tests/valid/indexing.test.w +++ b/examples/tests/valid/indexing.test.w @@ -5,6 +5,8 @@ let arr = Array[1, 2, 3]; assert(arr[0] == 1); assert(arr[2 - 5] == 1); +assert(arr[0] != arr[1]); + try { arr[-5]; diff --git a/libs/tree-sitter-wing/grammar.js b/libs/tree-sitter-wing/grammar.js index fbadd02cd80..12a8f692b06 100644 --- a/libs/tree-sitter-wing/grammar.js +++ b/libs/tree-sitter-wing/grammar.js @@ -13,9 +13,10 @@ const PREC = { UNARY: 120, OPTIONAL_TEST: 130, POWER: 140, - MEMBER: 150, - CALL: 160, - OPTIONAL_UNWRAP: 170, + STRUCTURED_ACCESS: 150, // x[y] + MEMBER: 160, + CALL: 170, + OPTIONAL_UNWRAP: 180, }; module.exports = grammar({ @@ -674,7 +675,7 @@ module.exports = grammar({ map_literal_member: ($) => seq($.expression, "=>", $.expression), struct_literal_member: ($) => seq($.identifier, ":", $.expression), structured_access_expression: ($) => - prec.right(seq($.expression, "[", $.expression, "]")), + prec.right(PREC.STRUCTURED_ACCESS, seq($.expression, "[", $.expression, "]")), json_literal: ($) => choice( diff --git a/libs/tree-sitter-wing/src/grammar.json b/libs/tree-sitter-wing/src/grammar.json index 18ac32e2e77..837202c4aa1 100644 --- a/libs/tree-sitter-wing/src/grammar.json +++ b/libs/tree-sitter-wing/src/grammar.json @@ -192,7 +192,7 @@ }, "nested_identifier": { "type": "PREC", - "value": 150, + "value": 160, "content": { "type": "SEQ", "members": [ @@ -2206,7 +2206,7 @@ }, "call": { "type": "PREC_LEFT", - "value": 160, + "value": 170, "content": { "type": "SEQ", "members": [ @@ -3170,7 +3170,7 @@ }, "optional_unwrap": { "type": "PREC_RIGHT", - "value": 170, + "value": 180, "content": { "type": "SEQ", "members": [ @@ -4183,7 +4183,7 @@ }, "structured_access_expression": { "type": "PREC_RIGHT", - "value": 0, + "value": 150, "content": { "type": "SEQ", "members": [ diff --git a/libs/tree-sitter-wing/test/corpus/expressions.txt b/libs/tree-sitter-wing/test/corpus/expressions.txt index 925bdb0b9ab..df66f7d281f 100644 --- a/libs/tree-sitter-wing/test/corpus/expressions.txt +++ b/libs/tree-sitter-wing/test/corpus/expressions.txt @@ -596,3 +596,55 @@ let mo = 10mo; value: (duration (months value: (number))))) + +================================================================================ +Array access +================================================================================ + +log(x[y]); +x[y] += 3; +assert(x[y] == w[z]); + +-------------------------------------------------------------------------------- + +(source + (expression_statement + (call + (reference + (reference_identifier)) + (argument_list + (positional_argument + (reference + (structured_access_expression + (reference + (reference_identifier)) + (reference + (reference_identifier)))))))) + (variable_assignment_statement + (lvalue + (structured_access_expression + (reference + (reference_identifier)) + (reference + (reference_identifier)))) + (assignment_operator) + (number)) + (expression_statement + (call + (reference + (reference_identifier)) + (argument_list + (positional_argument + (binary_expression + (reference + (structured_access_expression + (reference + (reference_identifier)) + (reference + (reference_identifier)))) + (reference + (structured_access_expression + (reference + (reference_identifier)) + (reference + (reference_identifier)))))))))) diff --git a/tools/hangar/__snapshots__/test_corpus/valid/indexing.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/indexing.test.w_compile_tf-aws.md index 60b4a059e4e..11d7a61fd58 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/indexing.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/indexing.test.w_compile_tf-aws.md @@ -80,6 +80,7 @@ class $Root extends $stdlib.std.Resource { const arr = [1, 2, 3]; $helpers.assert($helpers.eq($helpers.lookup(arr, 0), 1), "arr[0] == 1"); $helpers.assert($helpers.eq($helpers.lookup(arr, (2 - 5)), 1), "arr[2 - 5] == 1"); + $helpers.assert($helpers.neq($helpers.lookup(arr, 0), $helpers.lookup(arr, 1)), "arr[0] != arr[1]"); try { $helpers.lookup(arr, (-5)); } From 2dea83565214fbceb6d8619f339ec249dcfbeae7 Mon Sep 17 00:00:00 2001 From: Marcio Cruz de Almeida <67694075+marciocadev@users.noreply.github.com> Date: Sat, 20 Apr 2024 15:56:54 -0300 Subject: [PATCH 09/22] feat(sdk): dead letter queue support for queues (#6060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⛔ As the dead-letter queue linked to the function is a specific case of AWS, I will remove it from the scope of this PR. - [x] dlq (with retries) for queue (tf-aws / awscdk) - [x] dlq (with retries) for queue (sim) ~dlq for function (tf-aws / awscdk)~ ~dlq for function (sim)~ Closes #6033 ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [ ] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- docs/docs/04-standard-library/cloud/queue.md | 76 +++++ docs/docs/04-standard-library/cloud/topic.md | 14 + .../sdk_tests/queue/dead-letter-queue.test.w | 63 ++++ libs/awscdk/src/queue.ts | 20 +- .../test/__snapshots__/queue.test.ts.snap | 3 + .../incomplete_inflight_namespace.snap | 12 +- .../completions/namespace_middle_dot.snap | 12 +- .../completions/new_expression_nested.snap | 2 +- .../partial_type_reference_annotation.snap | 12 +- .../variable_type_annotation_namespace.snap | 12 +- libs/wingsdk/src/cloud/queue.md | 16 + libs/wingsdk/src/cloud/queue.ts | 27 ++ .../shared-aws/queue.setconsumer.inflight.ts | 12 +- libs/wingsdk/src/target-sim/queue.inflight.ts | 60 +++- .../target-sim/queue.setconsumer.inflight.ts | 11 +- libs/wingsdk/src/target-sim/queue.ts | 21 ++ .../src/target-sim/schema-resources.ts | 9 + libs/wingsdk/src/target-tf-aws/queue.ts | 37 ++- .../__snapshots__/captures.test.ts.snap | 3 + .../__snapshots__/queue.test.ts.snap | 3 + tools/hangar/__snapshots__/platform.ts.snap | 18 + ...dead-letter-queue.test.w_compile_tf-aws.md | 307 ++++++++++++++++++ .../dead-letter-queue.test.w_test_sim.md | 13 + .../queue/queue-ref.test.w_compile_tf-aws.md | 5 +- .../set_consumer.test.w_compile_tf-aws.md | 10 +- .../subscribe-queue.test.w_compile_tf-aws.md | 10 +- .../valid/captures.test.w_compile_tf-aws.md | 5 +- .../file_counter.test.w_compile_tf-aws.md | 5 +- .../valid/hello.test.w_compile_tf-aws.md | 5 +- ...light-subscribers.test.w_compile_tf-aws.md | 5 +- .../valid/redis.test.w_compile_tf-aws.md | 5 +- .../valid/resource.test.w_compile_tf-aws.md | 5 +- .../while_loop_await.test.w_compile_tf-aws.md | 5 +- 33 files changed, 777 insertions(+), 46 deletions(-) create mode 100644 examples/tests/sdk_tests/queue/dead-letter-queue.test.w create mode 100644 tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_compile_tf-aws.md create mode 100644 tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_test_sim.md diff --git a/docs/docs/04-standard-library/cloud/queue.md b/docs/docs/04-standard-library/cloud/queue.md index 646d6f42184..2e9101782ea 100644 --- a/docs/docs/04-standard-library/cloud/queue.md +++ b/docs/docs/04-standard-library/cloud/queue.md @@ -62,6 +62,22 @@ new cloud.Function(inflight () => { }); ``` +### Adding a dead-letter queue + +Creating a queue and adding a dead-letter queue with the maximum number of attempts configured + +```ts playground +bring cloud; + +let dlq = new cloud.Queue() as "dead-letter queue"; +let q = new cloud.Queue( + dlq: { + queue: dlq, + maxDeliveryAttempts: 2 + } +); +``` + ### Referencing an external queue If you would like to reference an existing queue from within your application you can use the @@ -270,6 +286,52 @@ The tree node. ## Structs +### DeadLetterQueueProps + +Dead letter queue options. + +#### Initializer + +```wing +bring cloud; + +let DeadLetterQueueProps = cloud.DeadLetterQueueProps{ ... }; +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| queue | Queue | Queue to receive messages that failed processing. | +| maxDeliveryAttempts | num | Number of times a message will be processed before being sent to the dead-letter queue. | + +--- + +##### `queue`Required + +```wing +queue: Queue; +``` + +- *Type:* Queue + +Queue to receive messages that failed processing. + +--- + +##### `maxDeliveryAttempts`Optional + +```wing +maxDeliveryAttempts: num; +``` + +- *Type:* num +- *Default:* 1 + +Number of times a message will be processed before being sent to the dead-letter queue. + +--- + ### QueueProps Options for `Queue`. @@ -286,11 +348,25 @@ let QueueProps = cloud.QueueProps{ ... }; | **Name** | **Type** | **Description** | | --- | --- | --- | +| dlq | DeadLetterQueueProps | A dead-letter queue. | | retentionPeriod | duration | How long a queue retains a message. | | timeout | duration | How long a queue's consumers have to process a message. | --- +##### `dlq`Optional + +```wing +dlq: DeadLetterQueueProps; +``` + +- *Type:* DeadLetterQueueProps +- *Default:* no dead letter queue + +A dead-letter queue. + +--- + ##### `retentionPeriod`Optional ```wing diff --git a/docs/docs/04-standard-library/cloud/topic.md b/docs/docs/04-standard-library/cloud/topic.md index 3881a60741e..f07374488a7 100644 --- a/docs/docs/04-standard-library/cloud/topic.md +++ b/docs/docs/04-standard-library/cloud/topic.md @@ -395,11 +395,25 @@ let TopicSubscribeQueueOptions = cloud.TopicSubscribeQueueOptions{ ... }; | **Name** | **Type** | **Description** | | --- | --- | --- | +| dlq | DeadLetterQueueProps | A dead-letter queue. | | retentionPeriod | duration | How long a queue retains a message. | | timeout | duration | How long a queue's consumers have to process a message. | --- +##### `dlq`Optional + +```wing +dlq: DeadLetterQueueProps; +``` + +- *Type:* DeadLetterQueueProps +- *Default:* no dead letter queue + +A dead-letter queue. + +--- + ##### `retentionPeriod`Optional ```wing diff --git a/examples/tests/sdk_tests/queue/dead-letter-queue.test.w b/examples/tests/sdk_tests/queue/dead-letter-queue.test.w new file mode 100644 index 00000000000..a75c0587426 --- /dev/null +++ b/examples/tests/sdk_tests/queue/dead-letter-queue.test.w @@ -0,0 +1,63 @@ +bring cloud; +bring util; + +let counter_received_messages = new cloud.Counter(); + +let dlq_without_retries = new cloud.Queue() as "dlq without retries"; +let queue_without_retries = new cloud.Queue( + dlq: { queue: dlq_without_retries } +) as "queue without retries"; +queue_without_retries.setConsumer(inflight (msg: str) => { + counter_received_messages.inc(1, msg); + if msg == "fail" { + throw "error"; + } +}); + + +new std.Test( + inflight () => { + queue_without_retries.push("Hello"); + queue_without_retries.push("fail"); + queue_without_retries.push("World!"); + + // wait until it executes once. + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("Hello") == 1; })); + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("World!") == 1; })); + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("fail") == 1; })); + + // check if the "fail" message has arrived at the dead-letter queue + assert(util.waitUntil(inflight () => { return dlq_without_retries.pop() == "fail"; })); + }, + // To make this test work on AWS, it's necessary to set a high timeout + // because if the message fails, we have to wait for the visibility timeout + // to expire in order to retrieve the same message from the queue again. + timeout: 5m) as "one execution and send fail message to dead-letter queue"; + +let dlq_with_retries = new cloud.Queue() as "dlq with retries"; +let queue_with_retries = new cloud.Queue( + dlq: { + queue: dlq_with_retries, + maxDeliveryAttempts: 2 + } +) as "queue with retries"; +queue_with_retries.setConsumer(inflight (msg: str) => { + counter_received_messages.inc(1, msg); + if msg == "fail" { + throw "error"; + } +}); + +new std.Test(inflight () => { + queue_with_retries.push("Hello"); + queue_with_retries.push("fail"); + queue_with_retries.push("World!"); + + // wait until it executes once and retry one more times. + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("Hello") == 1; })); + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("World!") == 1; })); + assert(util.waitUntil(inflight () => { return counter_received_messages.peek("fail") == 2; })); + + // check if the "fail" message has arrived at the dead-letter queue + assert(util.waitUntil(inflight () => { return dlq_with_retries.pop() == "fail"; })); +}, timeout: 5m) as "one execution, two retries and send the fail message to dead-letter queue"; diff --git a/libs/awscdk/src/queue.ts b/libs/awscdk/src/queue.ts index 248fe7b3ca2..4ea5a0887fe 100644 --- a/libs/awscdk/src/queue.ts +++ b/libs/awscdk/src/queue.ts @@ -7,7 +7,7 @@ import { App } from "./app"; import { std, core, cloud } from "@winglang/sdk"; import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert"; import { calculateQueuePermissions } from "@winglang/sdk/lib/shared-aws/permissions"; -import { IAwsQueue } from "@winglang/sdk/lib/shared-aws/queue"; +import { IAwsQueue, Queue as AwsQueue } from "@winglang/sdk/lib/shared-aws/queue"; import { addPolicyStatements, isAwsCdkFunction } from "./function"; /** @@ -23,14 +23,27 @@ export class Queue extends cloud.Queue implements IAwsQueue { super(scope, id, props); this.timeout = props.timeout ?? std.Duration.fromSeconds(30); - this.queue = new SQSQueue(this, "Default", { + const queueOpt = props.dlq ? { visibilityTimeout: props.timeout ? Duration.seconds(props.timeout?.seconds) : Duration.seconds(30), retentionPeriod: props.retentionPeriod ? Duration.seconds(props.retentionPeriod?.seconds) : Duration.hours(1), - }); + deadLetterQueue: { + queue: SQSQueue.fromQueueArn(this, "DeadLetterQueue", AwsQueue.from(props.dlq.queue)?.queueArn!), + maxReceiveCount: props.dlq.maxDeliveryAttempts ?? cloud.DEFAULT_DELIVERY_ATTEMPTS, + } + } : { + visibilityTimeout: props.timeout + ? Duration.seconds(props.timeout?.seconds) + : Duration.seconds(30), + retentionPeriod: props.retentionPeriod + ? Duration.seconds(props.retentionPeriod?.seconds) + : Duration.hours(1), + } + + this.queue = new SQSQueue(this, "Default", queueOpt); } public setConsumer( @@ -63,6 +76,7 @@ export class Queue extends cloud.Queue implements IAwsQueue { const eventSource = new SqsEventSource(this.queue, { batchSize: props.batchSize ?? 1, + reportBatchItemFailures: true, }); fn.awscdkFunction.addEventSource(eventSource); diff --git a/libs/awscdk/test/__snapshots__/queue.test.ts.snap b/libs/awscdk/test/__snapshots__/queue.test.ts.snap index fce461496c1..ff7d8c6f540 100644 --- a/libs/awscdk/test/__snapshots__/queue.test.ts.snap +++ b/libs/awscdk/test/__snapshots__/queue.test.ts.snap @@ -192,6 +192,9 @@ exports[`queue with a consumer function 2`] = ` "FunctionName": { "Ref": "QueueSetConsumer06749388A", }, + "FunctionResponseTypes": [ + "ReportBatchItemFailures", + ], }, "Type": "AWS::Lambda::EventSourceMapping", }, diff --git a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap index 7a230715cf6..1fd2aa61a8f 100644 --- a/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap +++ b/libs/wingc/src/lsp/snapshots/completions/incomplete_inflight_namespace.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -229,6 +229,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct CounterProps\n```\n---\nOptions for `Counter`.\n### Fields\n- `initial?` — `num?` — The initial value of the counter." sortText: hh|CounterProps +- label: DeadLetterQueueProps + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct DeadLetterQueueProps\n```\n---\nDead letter queue options.\n### Fields\n- `queue` — `Queue` — Queue to receive messages that failed processing.\n- `maxDeliveryAttempts?` — `num?` — Number of times a message will be processed before being sent to the dead-letter queue." + sortText: hh|DeadLetterQueueProps - label: DomainProps kind: 22 documentation: @@ -269,7 +275,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." + value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." sortText: hh|QueueProps - label: QueueSetConsumerOptions kind: 22 @@ -323,7 +329,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" + value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?`\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" sortText: hh|TopicSubscribeQueueOptions - label: WebsiteDomainOptions kind: 22 diff --git a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap index 7a230715cf6..1fd2aa61a8f 100644 --- a/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap +++ b/libs/wingc/src/lsp/snapshots/completions/namespace_middle_dot.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -229,6 +229,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct CounterProps\n```\n---\nOptions for `Counter`.\n### Fields\n- `initial?` — `num?` — The initial value of the counter." sortText: hh|CounterProps +- label: DeadLetterQueueProps + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct DeadLetterQueueProps\n```\n---\nDead letter queue options.\n### Fields\n- `queue` — `Queue` — Queue to receive messages that failed processing.\n- `maxDeliveryAttempts?` — `num?` — Number of times a message will be processed before being sent to the dead-letter queue." + sortText: hh|DeadLetterQueueProps - label: DomainProps kind: 22 documentation: @@ -269,7 +275,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." + value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." sortText: hh|QueueProps - label: QueueSetConsumerOptions kind: 22 @@ -323,7 +329,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" + value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?`\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" sortText: hh|TopicSubscribeQueueOptions - label: WebsiteDomainOptions kind: 22 diff --git a/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap b/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap index 396726602d3..bfe3e0f0bf6 100644 --- a/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap +++ b/libs/wingc/src/lsp/snapshots/completions/new_expression_nested.snap @@ -82,7 +82,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue insertText: Queue($1) insertTextFormat: 2 diff --git a/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap b/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap index 7a230715cf6..1fd2aa61a8f 100644 --- a/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap +++ b/libs/wingc/src/lsp/snapshots/completions/partial_type_reference_annotation.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -229,6 +229,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct CounterProps\n```\n---\nOptions for `Counter`.\n### Fields\n- `initial?` — `num?` — The initial value of the counter." sortText: hh|CounterProps +- label: DeadLetterQueueProps + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct DeadLetterQueueProps\n```\n---\nDead letter queue options.\n### Fields\n- `queue` — `Queue` — Queue to receive messages that failed processing.\n- `maxDeliveryAttempts?` — `num?` — Number of times a message will be processed before being sent to the dead-letter queue." + sortText: hh|DeadLetterQueueProps - label: DomainProps kind: 22 documentation: @@ -269,7 +275,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." + value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." sortText: hh|QueueProps - label: QueueSetConsumerOptions kind: 22 @@ -323,7 +329,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" + value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?`\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" sortText: hh|TopicSubscribeQueueOptions - label: WebsiteDomainOptions kind: 22 diff --git a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap index 7a230715cf6..1fd2aa61a8f 100644 --- a/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap +++ b/libs/wingc/src/lsp/snapshots/completions/variable_type_annotation_namespace.snap @@ -47,7 +47,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 7 documentation: kind: markdown - value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." + value: "```wing\nclass Queue\n```\n---\nA queue.\n\n### Initializer\n- `...props` — `QueueProps?`\n \n - `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n - `retentionPeriod?` — `duration?` — How long a queue retains a message.\n - `timeout?` — `duration?` — How long a queue's consumers have to process a message.\n### Fields\n- `node` — `Node` — The tree node.\n### Methods\n- `approxSize` — `inflight (): num` — Retrieve the approximate number of messages in the queue.\n- `isConstruct` — `preflight (x: any): bool` — Checks if `x` is a construct.\n- `onLift` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this resource inflight.\n- `onLiftType` — `preflight (host: IInflightHost, ops: Array): void` — A hook called by the Wing compiler once for each inflight host that needs to use this type inflight.\n- `pop` — `inflight (): str?` — Pop a message from the queue.\n- `purge` — `inflight (): void` — Purge all of the messages in the queue.\n- `push` — `inflight (...messages: Array?): void` — Push one or more messages to the queue.\n- `setConsumer` — `preflight (handler: inflight (message: str): void, props: QueueSetConsumerOptions?): Function` — Create a function to consume messages from this queue.\n- `toString` — `preflight (): str` — Returns a string representation of this construct." sortText: gg|Queue - label: Schedule kind: 7 @@ -229,6 +229,12 @@ source: libs/wingc/src/lsp/completions.rs kind: markdown value: "```wing\nstruct CounterProps\n```\n---\nOptions for `Counter`.\n### Fields\n- `initial?` — `num?` — The initial value of the counter." sortText: hh|CounterProps +- label: DeadLetterQueueProps + kind: 22 + documentation: + kind: markdown + value: "```wing\nstruct DeadLetterQueueProps\n```\n---\nDead letter queue options.\n### Fields\n- `queue` — `Queue` — Queue to receive messages that failed processing.\n- `maxDeliveryAttempts?` — `num?` — Number of times a message will be processed before being sent to the dead-letter queue." + sortText: hh|DeadLetterQueueProps - label: DomainProps kind: 22 documentation: @@ -269,7 +275,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." + value: "```wing\nstruct QueueProps\n```\n---\nOptions for `Queue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?` — A dead-letter queue.\n- `retentionPeriod?` — `duration?` — How long a queue retains a message.\n- `timeout?` — `duration?` — How long a queue's consumers have to process a message." sortText: hh|QueueProps - label: QueueSetConsumerOptions kind: 22 @@ -323,7 +329,7 @@ source: libs/wingc/src/lsp/completions.rs kind: 22 documentation: kind: markdown - value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" + value: "```wing\nstruct TopicSubscribeQueueOptions extends QueueProps\n```\n---\nOptions for `Topic.subscribeQueue`.\n### Fields\n- `dlq?` — `DeadLetterQueueProps?`\n- `retentionPeriod?` — `duration?`\n- `timeout?` — `duration?`" sortText: hh|TopicSubscribeQueueOptions - label: WebsiteDomainOptions kind: 22 diff --git a/libs/wingsdk/src/cloud/queue.md b/libs/wingsdk/src/cloud/queue.md index 9e769410044..04c656ccffb 100644 --- a/libs/wingsdk/src/cloud/queue.md +++ b/libs/wingsdk/src/cloud/queue.md @@ -62,6 +62,22 @@ new cloud.Function(inflight () => { }); ``` +### Adding a dead-letter queue + +Creating a queue and adding a dead-letter queue with the maximum number of attempts configured + +```ts playground +bring cloud; + +let dlq = new cloud.Queue() as "dead-letter queue"; +let q = new cloud.Queue( + dlq: { + queue: dlq, + maxDeliveryAttempts: 2 + } +); +``` + ### Referencing an external queue If you would like to reference an existing queue from within your application you can use the diff --git a/libs/wingsdk/src/cloud/queue.ts b/libs/wingsdk/src/cloud/queue.ts index 00fc8740c8b..2380d2a7331 100644 --- a/libs/wingsdk/src/cloud/queue.ts +++ b/libs/wingsdk/src/cloud/queue.ts @@ -10,6 +10,27 @@ import { Duration, IInflight, Node, Resource } from "../std"; */ export const QUEUE_FQN = fqnForType("cloud.Queue"); +/** + * Dead-letter queue default retries + */ +export const DEFAULT_DELIVERY_ATTEMPTS = 1; + +/** + * Dead letter queue options. + */ +export interface DeadLetterQueueProps { + /** + * Queue to receive messages that failed processing. + */ + readonly queue: Queue; + /** + * Number of times a message will be processed before being + * sent to the dead-letter queue. + * @default 1 + */ + readonly maxDeliveryAttempts?: number; +} + /** * Options for `Queue`. */ @@ -25,6 +46,12 @@ export interface QueueProps { * @default 1h */ readonly retentionPeriod?: Duration; + + /** + * A dead-letter queue. + * @default - no dead letter queue + */ + readonly dlq?: DeadLetterQueueProps; } /** diff --git a/libs/wingsdk/src/shared-aws/queue.setconsumer.inflight.ts b/libs/wingsdk/src/shared-aws/queue.setconsumer.inflight.ts index 2a192a76149..b9b35caaec0 100644 --- a/libs/wingsdk/src/shared-aws/queue.setconsumer.inflight.ts +++ b/libs/wingsdk/src/shared-aws/queue.setconsumer.inflight.ts @@ -10,9 +10,17 @@ export class QueueSetConsumerHandlerClient constructor({ handler }: { handler: IFunctionHandlerClient }) { this.handler = handler; } - public async handle(event: any) { + public async handle(event: any): Promise { + const batchItemFailures = []; for (const record of event.Records ?? []) { - await this.handler.handle(record.body); + try { + await this.handler.handle(record.body); + } catch (error) { + batchItemFailures.push({ + itemIdentifier: record.messageId, + }); + } } + return { batchItemFailures }; } } diff --git a/libs/wingsdk/src/target-sim/queue.inflight.ts b/libs/wingsdk/src/target-sim/queue.inflight.ts index ade75198fb6..8700f5ba0b9 100644 --- a/libs/wingsdk/src/target-sim/queue.inflight.ts +++ b/libs/wingsdk/src/target-sim/queue.inflight.ts @@ -5,9 +5,15 @@ import { QueueSchema, QueueSubscriber, EventSubscription, + DeadLetterQueueSchema, ResourceHandle, } from "./schema-resources"; -import { IFunctionClient, IQueueClient, QUEUE_FQN } from "../cloud"; +import { + DEFAULT_DELIVERY_ATTEMPTS, + IFunctionClient, + IQueueClient, + QUEUE_FQN, +} from "../cloud"; import { ISimulatorContext, ISimulatorResourceInstance, @@ -24,10 +30,12 @@ export class Queue private _context: ISimulatorContext | undefined; private readonly timeoutSeconds: number; private readonly retentionPeriod: number; + private readonly dlq?: DeadLetterQueueSchema; constructor(props: QueueSchema) { this.timeoutSeconds = props.timeout; this.retentionPeriod = props.retentionPeriod; + this.dlq = props.dlq; this.processLoop = runEvery(100, async () => this.processMessages()); // every 0.1 seconds } @@ -85,7 +93,13 @@ export class Queue throw new Error("Empty messages are not allowed"); } for (const message of messages) { - this.messages.push(new QueueMessage(this.retentionPeriod, message)); + this.messages.push( + new QueueMessage( + this.retentionPeriod, + DEFAULT_DELIVERY_ATTEMPTS, + message + ) + ); } }, }); @@ -187,8 +201,38 @@ export class Queue // we don't use invokeAsync here because we want to wait for the function to finish // and requeue the messages if it fails - void fnClient - .invoke(JSON.stringify({ messages: messagesPayload })) + await fnClient + .invoke(JSON.stringify({ messages: messages })) + .then((result) => { + if (this.dlq && result) { + const errorList = JSON.parse(result); + let retriesMessages = []; + for (const msg of errorList) { + if ( + msg.remainingDeliveryAttempts < this.dlq.maxDeliveryAttempts + ) { + msg.remainingDeliveryAttempts++; + retriesMessages.push(msg); + } else { + let dlq = this.context.getClient( + this.dlq.dlqHandler + ) as IQueueClient; + void dlq.push(msg.payload).catch((err) => { + this.context.addTrace({ + type: TraceType.RESOURCE, + data: { + message: `Pushing messages to the dead-letter queue generates an error -> ${err}`, + }, + sourcePath: this.context.resourcePath, + sourceType: QUEUE_FQN, + timestamp: new Date().toISOString(), + }); + }); + } + } + this.messages.push(...retriesMessages); + } + }) .catch((err) => { // If the function is at a concurrency limit, pretend we just didn't call it if ( @@ -238,12 +282,18 @@ export class Queue class QueueMessage { public readonly retentionTimeout: Date; public readonly payload: string; + public remainingDeliveryAttempts: number; - constructor(retentionPeriod: number, message: string) { + constructor( + retentionPeriod: number, + remainingDeliveryAttempts: number, + message: string + ) { const currentTime = new Date(); currentTime.setSeconds(retentionPeriod + currentTime.getSeconds()); this.retentionTimeout = currentTime; this.payload = message; + this.remainingDeliveryAttempts = remainingDeliveryAttempts; } } diff --git a/libs/wingsdk/src/target-sim/queue.setconsumer.inflight.ts b/libs/wingsdk/src/target-sim/queue.setconsumer.inflight.ts index 55dad14f8b0..db2b90ca570 100644 --- a/libs/wingsdk/src/target-sim/queue.setconsumer.inflight.ts +++ b/libs/wingsdk/src/target-sim/queue.setconsumer.inflight.ts @@ -9,11 +9,18 @@ export class QueueSetConsumerHandlerClient implements IFunctionHandlerClient { this.handler = handler; } public async handle(event?: string) { + const batchItemFailures = []; let parsed = JSON.parse(event ?? "{}"); if (!parsed.messages) throw new Error('No "messages" field in event.'); for (const $message of parsed.messages) { - await this.handler.handle($message); + try { + await this.handler.handle($message.payload); + } catch (error) { + batchItemFailures.push($message); + } } - return undefined; + return batchItemFailures.length > 0 + ? JSON.stringify(batchItemFailures) + : undefined; } } diff --git a/libs/wingsdk/src/target-sim/queue.ts b/libs/wingsdk/src/target-sim/queue.ts index 006482ad3ac..30d5d661b2b 100644 --- a/libs/wingsdk/src/target-sim/queue.ts +++ b/libs/wingsdk/src/target-sim/queue.ts @@ -9,6 +9,7 @@ import { import { Policy } from "./policy"; import { ISimulatorResource } from "./resource"; import { QueueSchema } from "./schema-resources"; +import { simulatorHandleToken } from "./tokens"; import { bindSimulatorResource, makeSimulatorJsClient } from "./util"; import * as cloud from "../cloud"; import { NotImplementedError } from "../core/errors"; @@ -24,6 +25,7 @@ import { Duration, IInflightHost, Node, SDK_SOURCE_MODULE } from "../std"; export class Queue extends cloud.Queue implements ISimulatorResource { private readonly timeout: Duration; private readonly retentionPeriod: Duration; + private readonly dlq?: cloud.DeadLetterQueueProps; private readonly policy: Policy; constructor(scope: Construct, id: string, props: cloud.QueueProps = {}) { @@ -50,6 +52,18 @@ export class Queue extends cloud.Queue implements ISimulatorResource { } this.policy = new Policy(this, "Policy", { principal: this }); + + if (props.dlq && props.dlq.queue) { + this.dlq = props.dlq; + + this.policy.addStatement(this.dlq.queue, cloud.QueueInflightMethods.PUSH); + + Node.of(this).addConnection({ + source: this, + target: this.dlq.queue, + name: "dead-letter queue", + }); + } } /** @internal */ @@ -136,6 +150,13 @@ export class Queue extends cloud.Queue implements ISimulatorResource { const props: QueueSchema = { timeout: this.timeout.seconds, retentionPeriod: this.retentionPeriod.seconds, + dlq: this.dlq + ? { + dlqHandler: simulatorHandleToken(this.dlq.queue), + maxDeliveryAttempts: + this.dlq.maxDeliveryAttempts ?? cloud.DEFAULT_DELIVERY_ATTEMPTS, + } + : undefined, }; return { type: cloud.QUEUE_FQN, diff --git a/libs/wingsdk/src/target-sim/schema-resources.ts b/libs/wingsdk/src/target-sim/schema-resources.ts index 91a56fc763f..a482bbeb451 100644 --- a/libs/wingsdk/src/target-sim/schema-resources.ts +++ b/libs/wingsdk/src/target-sim/schema-resources.ts @@ -48,12 +48,21 @@ export interface FunctionSchema { /** Runtime attributes for cloud.Function */ export interface FunctionAttributes {} +export interface DeadLetterQueueSchema { + /** Dead-letter queue handler token */ + dlqHandler: string; + /** Number of time a message will be processed */ + maxDeliveryAttempts: number; +} + /** Schema for cloud.Queue */ export interface QueueSchema { /** How long a queue's consumers have to process a message, in seconds */ readonly timeout: number; /** How long a queue retains a message, in seconds */ readonly retentionPeriod: number; + /** Dead-letter queue options */ + readonly dlq?: DeadLetterQueueSchema; } /** Runtime attributes for cloud.Queue */ diff --git a/libs/wingsdk/src/target-tf-aws/queue.ts b/libs/wingsdk/src/target-tf-aws/queue.ts index 988ee0ac540..6a38a525e5b 100644 --- a/libs/wingsdk/src/target-tf-aws/queue.ts +++ b/libs/wingsdk/src/target-tf-aws/queue.ts @@ -10,6 +10,7 @@ import { convertBetweenHandlers } from "../shared/convert"; import { NameOptions, ResourceNames } from "../shared/resource-names"; import { IAwsQueue } from "../shared-aws"; import { calculateQueuePermissions } from "../shared-aws/permissions"; +import { Queue as AwsQueue } from "../shared-aws/queue"; import { Duration, IInflightHost, Node } from "../std"; /** @@ -32,15 +33,32 @@ export class Queue extends cloud.Queue implements IAwsQueue { constructor(scope: Construct, id: string, props: cloud.QueueProps = {}) { super(scope, id, props); - this.queue = new SqsQueue(this, "Default", { - visibilityTimeoutSeconds: props.timeout - ? props.timeout.seconds - : Duration.fromSeconds(30).seconds, - messageRetentionSeconds: props.retentionPeriod - ? props.retentionPeriod.seconds - : Duration.fromHours(1).seconds, - name: ResourceNames.generateName(this, NAME_OPTS), - }); + const queueOpt = props.dlq + ? { + visibilityTimeoutSeconds: props.timeout + ? props.timeout.seconds + : Duration.fromSeconds(30).seconds, + messageRetentionSeconds: props.retentionPeriod + ? props.retentionPeriod.seconds + : Duration.fromHours(1).seconds, + name: ResourceNames.generateName(this, NAME_OPTS), + redrivePolicy: JSON.stringify({ + deadLetterTargetArn: AwsQueue.from(props.dlq.queue)?.queueArn, + maxReceiveCount: + props.dlq.maxDeliveryAttempts ?? cloud.DEFAULT_DELIVERY_ATTEMPTS, + }), + } + : { + visibilityTimeoutSeconds: props.timeout + ? props.timeout.seconds + : Duration.fromSeconds(30).seconds, + messageRetentionSeconds: props.retentionPeriod + ? props.retentionPeriod.seconds + : Duration.fromHours(1).seconds, + name: ResourceNames.generateName(this, NAME_OPTS), + }; + + this.queue = new SqsQueue(this, "Default", queueOpt); } /** @internal */ @@ -99,6 +117,7 @@ export class Queue extends cloud.Queue implements IAwsQueue { functionName: fn.functionName, eventSourceArn: this.queue.arn, batchSize: props.batchSize ?? 1, + functionResponseTypes: ["ReportBatchItemFailures"], // It allows the function to return the messages that failed to the queue }); Node.of(this).addConnection({ diff --git a/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap b/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap index ecf8f57cca0..065e1c16ea2 100644 --- a/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap +++ b/libs/wingsdk/test/target-tf-aws/__snapshots__/captures.test.ts.snap @@ -222,6 +222,9 @@ exports[`function with a queue binding 3`] = ` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { diff --git a/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap b/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap index 28e841f6eb5..bf2f4ed0b91 100644 --- a/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap +++ b/libs/wingsdk/test/target-tf-aws/__snapshots__/queue.test.ts.snap @@ -831,6 +831,9 @@ exports[`queue with a consumer function 2`] = ` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/platform.ts.snap b/tools/hangar/__snapshots__/platform.ts.snap index 61e7c5e154e..9c3e6eefe06 100644 --- a/tools/hangar/__snapshots__/platform.ts.snap +++ b/tools/hangar/__snapshots__/platform.ts.snap @@ -74,6 +74,9 @@ exports[`Multiple platforms > only first platform app is used 1`] = ` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -236,6 +239,9 @@ exports[`Platform examples > AWS target platform > permission-boundary.js 1`] = "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -467,6 +473,9 @@ exports[`Platform examples > AWS target platform > replicate-s3.js 1`] = ` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -746,6 +755,9 @@ exports[`Platform examples > AWS target platform > tf-backend.js > azurerm backe "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -907,6 +919,9 @@ exports[`Platform examples > AWS target platform > tf-backend.js > gcp backend 1 "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { @@ -1068,6 +1083,9 @@ exports[`Platform examples > AWS target platform > tf-backend.js > s3 backend 1` "batch_size": 1, "event_source_arn": "\${aws_sqs_queue.Queue.arn}", "function_name": "\${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures", + ], }, }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_compile_tf-aws.md new file mode 100644 index 00000000000..74d09daa612 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_compile_tf-aws.md @@ -0,0 +1,307 @@ +# [dead-letter-queue.test.w](../../../../../../examples/tests/sdk_tests/queue/dead-letter-queue.test.w) | compile | tf-aws + +## main.tf.json +```json +{ + "//": { + "metadata": { + "backend": "local", + "stackName": "root", + "version": "0.20.3" + }, + "outputs": {} + }, + "provider": { + "aws": [ + {} + ] + }, + "resource": { + "aws_cloudwatch_log_group": { + "queuewithoutretries-SetConsumer0_CloudwatchLogGroup_9265B40C": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/CloudwatchLogGroup", + "uniqueId": "queuewithoutretries-SetConsumer0_CloudwatchLogGroup_9265B40C" + } + }, + "name": "/aws/lambda/queue-without-retries-SetConsumer0-c8ba2958", + "retention_in_days": 30 + }, + "queuewithretries-SetConsumer0_CloudwatchLogGroup_98D9A088": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/CloudwatchLogGroup", + "uniqueId": "queuewithretries-SetConsumer0_CloudwatchLogGroup_98D9A088" + } + }, + "name": "/aws/lambda/queue-with-retries-SetConsumer0-c82b1a26", + "retention_in_days": 30 + } + }, + "aws_dynamodb_table": { + "Counter": { + "//": { + "metadata": { + "path": "root/Default/Default/Counter/Default", + "uniqueId": "Counter" + } + }, + "attribute": [ + { + "name": "id", + "type": "S" + } + ], + "billing_mode": "PAY_PER_REQUEST", + "hash_key": "id", + "name": "wing-counter-Counter-c824ef62" + } + }, + "aws_iam_role": { + "queuewithoutretries-SetConsumer0_IamRole_F76F585E": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/IamRole", + "uniqueId": "queuewithoutretries-SetConsumer0_IamRole_F76F585E" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + }, + "queuewithretries-SetConsumer0_IamRole_B95C0A26": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/IamRole", + "uniqueId": "queuewithretries-SetConsumer0_IamRole_B95C0A26" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + } + }, + "aws_iam_role_policy": { + "queuewithoutretries-SetConsumer0_IamRolePolicy_2603CFA0": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/IamRolePolicy", + "uniqueId": "queuewithoutretries-SetConsumer0_IamRolePolicy_2603CFA0" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:ReceiveMessage\",\"sqs:ChangeMessageVisibility\",\"sqs:GetQueueUrl\",\"sqs:DeleteMessage\",\"sqs:GetQueueAttributes\"],\"Resource\":[\"${aws_sqs_queue.queuewithoutretries.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"dynamodb:UpdateItem\"],\"Resource\":[\"${aws_dynamodb_table.Counter.arn}\"],\"Effect\":\"Allow\"}]}", + "role": "${aws_iam_role.queuewithoutretries-SetConsumer0_IamRole_F76F585E.name}" + }, + "queuewithretries-SetConsumer0_IamRolePolicy_245846E1": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/IamRolePolicy", + "uniqueId": "queuewithretries-SetConsumer0_IamRolePolicy_245846E1" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:ReceiveMessage\",\"sqs:ChangeMessageVisibility\",\"sqs:GetQueueUrl\",\"sqs:DeleteMessage\",\"sqs:GetQueueAttributes\"],\"Resource\":[\"${aws_sqs_queue.queuewithretries.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"dynamodb:UpdateItem\"],\"Resource\":[\"${aws_dynamodb_table.Counter.arn}\"],\"Effect\":\"Allow\"}]}", + "role": "${aws_iam_role.queuewithretries-SetConsumer0_IamRole_B95C0A26.name}" + } + }, + "aws_iam_role_policy_attachment": { + "queuewithoutretries-SetConsumer0_IamRolePolicyAttachment_559EECE4": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/IamRolePolicyAttachment", + "uniqueId": "queuewithoutretries-SetConsumer0_IamRolePolicyAttachment_559EECE4" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.queuewithoutretries-SetConsumer0_IamRole_F76F585E.name}" + }, + "queuewithretries-SetConsumer0_IamRolePolicyAttachment_831C8294": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/IamRolePolicyAttachment", + "uniqueId": "queuewithretries-SetConsumer0_IamRolePolicyAttachment_831C8294" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.queuewithretries-SetConsumer0_IamRole_B95C0A26.name}" + } + }, + "aws_lambda_event_source_mapping": { + "queuewithoutretries_EventSourceMapping_963C2B4C": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries/EventSourceMapping", + "uniqueId": "queuewithoutretries_EventSourceMapping_963C2B4C" + } + }, + "batch_size": 1, + "event_source_arn": "${aws_sqs_queue.queuewithoutretries.arn}", + "function_name": "${aws_lambda_function.queuewithoutretries-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] + }, + "queuewithretries_EventSourceMapping_A0EC80F3": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries/EventSourceMapping", + "uniqueId": "queuewithretries_EventSourceMapping_A0EC80F3" + } + }, + "batch_size": 1, + "event_source_arn": "${aws_sqs_queue.queuewithretries.arn}", + "function_name": "${aws_lambda_function.queuewithretries-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] + } + }, + "aws_lambda_function": { + "queuewithoutretries-SetConsumer0": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/Default", + "uniqueId": "queuewithoutretries-SetConsumer0" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "DYNAMODB_TABLE_NAME_6cb5a3a4": "${aws_dynamodb_table.Counter.name}", + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "queue-without-retries-SetConsumer0-c8ba2958", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "queue-without-retries-SetConsumer0-c8ba2958", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.queuewithoutretries-SetConsumer0_IamRole_F76F585E.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.queuewithoutretries-SetConsumer0_S3Object_ECD1395D.key}", + "timeout": "${aws_sqs_queue.queuewithoutretries.visibility_timeout_seconds}", + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + }, + "queuewithretries-SetConsumer0": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/Default", + "uniqueId": "queuewithretries-SetConsumer0" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "DYNAMODB_TABLE_NAME_6cb5a3a4": "${aws_dynamodb_table.Counter.name}", + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "queue-with-retries-SetConsumer0-c82b1a26", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "queue-with-retries-SetConsumer0-c82b1a26", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.queuewithretries-SetConsumer0_IamRole_B95C0A26.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.queuewithretries-SetConsumer0_S3Object_29ED03EE.key}", + "timeout": "${aws_sqs_queue.queuewithretries.visibility_timeout_seconds}", + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } + } + }, + "aws_s3_bucket": { + "Code": { + "//": { + "metadata": { + "path": "root/Default/Code", + "uniqueId": "Code" + } + }, + "bucket_prefix": "code-c84a50b1-" + } + }, + "aws_s3_object": { + "queuewithoutretries-SetConsumer0_S3Object_ECD1395D": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries-SetConsumer0/S3Object", + "uniqueId": "queuewithoutretries-SetConsumer0_S3Object_ECD1395D" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + }, + "queuewithretries-SetConsumer0_S3Object_29ED03EE": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries-SetConsumer0/S3Object", + "uniqueId": "queuewithretries-SetConsumer0_S3Object_29ED03EE" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" + } + }, + "aws_sqs_queue": { + "dlqwithoutretries": { + "//": { + "metadata": { + "path": "root/Default/Default/dlq without retries/Default", + "uniqueId": "dlqwithoutretries" + } + }, + "message_retention_seconds": 3600, + "name": "dlq-without-retries-c83c0330", + "visibility_timeout_seconds": 30 + }, + "dlqwithretries": { + "//": { + "metadata": { + "path": "root/Default/Default/dlq with retries/Default", + "uniqueId": "dlqwithretries" + } + }, + "message_retention_seconds": 3600, + "name": "dlq-with-retries-c877f5c7", + "visibility_timeout_seconds": 30 + }, + "queuewithoutretries": { + "//": { + "metadata": { + "path": "root/Default/Default/queue without retries/Default", + "uniqueId": "queuewithoutretries" + } + }, + "message_retention_seconds": 3600, + "name": "queue-without-retries-c8a5001c", + "redrive_policy": "{\"deadLetterTargetArn\":\"${aws_sqs_queue.dlqwithoutretries.arn}\",\"maxReceiveCount\":1}", + "visibility_timeout_seconds": 30 + }, + "queuewithretries": { + "//": { + "metadata": { + "path": "root/Default/Default/queue with retries/Default", + "uniqueId": "queuewithretries" + } + }, + "message_retention_seconds": 3600, + "name": "queue-with-retries-c8a06dc7", + "redrive_policy": "{\"deadLetterTargetArn\":\"${aws_sqs_queue.dlqwithretries.arn}\",\"maxReceiveCount\":2}", + "visibility_timeout_seconds": 30 + } + } + } +} +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_test_sim.md new file mode 100644 index 00000000000..f35d5d2ed2d --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/dead-letter-queue.test.w_test_sim.md @@ -0,0 +1,13 @@ +# [dead-letter-queue.test.w](../../../../../../examples/tests/sdk_tests/queue/dead-letter-queue.test.w) | test | sim + +## stdout.log +```log +pass ─ dead-letter-queue.test.wsim » root/env0/one execution and send fail message to dead-letter queue +pass ─ dead-letter-queue.test.wsim » root/env1/one execution, two retries and send the fail message to dead-letter queue + +Tests 2 passed (2) +Snapshots 1 skipped +Test Files 1 passed (1) +Duration +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md index 5559abc4cb1..598c74b34e6 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/queue-ref.test.w_compile_tf-aws.md @@ -249,7 +249,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md index 6fea628fc06..e1da8b967b6 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md @@ -149,7 +149,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] }, "q2_EventSourceMapping_F484014F": { "//": { @@ -160,7 +163,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.q2.arn}", - "function_name": "${aws_lambda_function.q2-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.q2-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/topic/subscribe-queue.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/topic/subscribe-queue.test.w_compile_tf-aws.md index 8a0674fd361..f833a206df1 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/topic/subscribe-queue.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/topic/subscribe-queue.test.w_compile_tf-aws.md @@ -171,7 +171,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.q1.arn}", - "function_name": "${aws_lambda_function.q1-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.q1-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] }, "q2_EventSourceMapping_F484014F": { "//": { @@ -182,7 +185,10 @@ }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.q2.arn}", - "function_name": "${aws_lambda_function.q2-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.q2-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md index 746445c0c10..01ee869a182 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md @@ -349,7 +349,10 @@ module.exports = function({ $headers }) { }, "batch_size": 5, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/file_counter.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/file_counter.test.w_compile_tf-aws.md index bd433ead881..7cab4a39fc8 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/file_counter.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/file_counter.test.w_compile_tf-aws.md @@ -115,7 +115,10 @@ module.exports = function({ $bucket, $counter }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/hello.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/hello.test.w_compile_tf-aws.md index a89822806fa..27941b931ba 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/hello.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/hello.test.w_compile_tf-aws.md @@ -94,7 +94,10 @@ module.exports = function({ $bucket }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/inflight-subscribers.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/inflight-subscribers.test.w_compile_tf-aws.md index ae14796da53..32e4f4a90e5 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/inflight-subscribers.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/inflight-subscribers.test.w_compile_tf-aws.md @@ -153,7 +153,10 @@ module.exports = function({ }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md index fdc84d01919..8159814e3fa 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md @@ -223,7 +223,10 @@ module.exports = function({ $queue, $r, $r2, $util_Util }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md index f87bdd61606..c43fc4d5a66 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md @@ -397,7 +397,10 @@ module.exports = function({ }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.BigPublisher_Queue_2C024F97.arn}", - "function_name": "${aws_lambda_function.BigPublisher_Queue-SetConsumer0_55896C65.function_name}" + "function_name": "${aws_lambda_function.BigPublisher_Queue-SetConsumer0_55896C65.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { diff --git a/tools/hangar/__snapshots__/test_corpus/valid/while_loop_await.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/while_loop_await.test.w_compile_tf-aws.md index c73bf15bf1c..5baaa695c11 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/while_loop_await.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/while_loop_await.test.w_compile_tf-aws.md @@ -100,7 +100,10 @@ module.exports = function({ }) { }, "batch_size": 1, "event_source_arn": "${aws_sqs_queue.Queue.arn}", - "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}" + "function_name": "${aws_lambda_function.Queue-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { From 65d66c22a82d305b44f436aa3f809b3ede06147c Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Sat, 20 Apr 2024 18:21:55 -0400 Subject: [PATCH 10/22] fix(sdk): cloud.Queue does not send messages in parallel (#6280) Fixes a regression from #6060 that prevented messages from being processed in parallel (we noticed this bug when it caused test failures in one of our winglibs: https://github.com/winglang/winglibs/actions/runs/8767429765/job/24060674878). I confirmed the updated test (set_consumer.test.w) passes when ran on my AWS account using the `tf-aws` target. ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [ ] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- .../tests/sdk_tests/queue/set_consumer.test.w | 24 ++++ libs/wingsdk/src/target-sim/queue.inflight.ts | 2 +- .../set_consumer.test.w_compile_tf-aws.md | 124 ++++++++++++++++++ .../queue/set_consumer.test.w_test_sim.md | 7 +- 4 files changed, 153 insertions(+), 4 deletions(-) diff --git a/examples/tests/sdk_tests/queue/set_consumer.test.w b/examples/tests/sdk_tests/queue/set_consumer.test.w index 46a79949633..437cd9599cc 100644 --- a/examples/tests/sdk_tests/queue/set_consumer.test.w +++ b/examples/tests/sdk_tests/queue/set_consumer.test.w @@ -31,3 +31,27 @@ test "function can push back to the queue" { q2.push("hello"); util.waitUntil(inflight () => { return c2.peek() >= 2; }); } + +let q3 = new cloud.Queue() as "q3"; +let c3 = new cloud.Counter() as "c3"; + +q3.setConsumer(inflight (message) => { + util.sleep(10s); + c3.inc(); +}); + +test "messages pushed to queue can be processed concurrently" { + let t1 = datetime.utcNow(); + q3.push("message1"); + q3.push("message2"); + q3.push("message3"); + + util.waitUntil(inflight () => { return c3.peek() == 3; }); + + let t2 = datetime.utcNow(); + let elapsed = duration.fromMilliseconds(t2.timestampMs - t1.timestampMs); + + // If the messages were processed concurrently, the elapsed time should be less than 20s. + // Note: even though there is only one consumer, the consumer's default concurrency is more than 1. + assert(elapsed.seconds < 20, "Messages were likely not processed concurrently"); +} diff --git a/libs/wingsdk/src/target-sim/queue.inflight.ts b/libs/wingsdk/src/target-sim/queue.inflight.ts index 8700f5ba0b9..6ccb05d88c8 100644 --- a/libs/wingsdk/src/target-sim/queue.inflight.ts +++ b/libs/wingsdk/src/target-sim/queue.inflight.ts @@ -201,7 +201,7 @@ export class Queue // we don't use invokeAsync here because we want to wait for the function to finish // and requeue the messages if it fails - await fnClient + void fnClient .invoke(JSON.stringify({ messages: messages })) .then((result) => { if (this.dlq && result) { diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md index e1da8b967b6..df687695171 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_compile_tf-aws.md @@ -37,6 +37,16 @@ }, "name": "/aws/lambda/q2-SetConsumer0-c8ba098b", "retention_in_days": 30 + }, + "q3-SetConsumer0_CloudwatchLogGroup_1FE8714D": { + "//": { + "metadata": { + "path": "root/Default/Default/q3-SetConsumer0/CloudwatchLogGroup", + "uniqueId": "q3-SetConsumer0_CloudwatchLogGroup_1FE8714D" + } + }, + "name": "/aws/lambda/q3-SetConsumer0-c8cd013e", + "retention_in_days": 30 } }, "aws_dynamodb_table": { @@ -73,6 +83,23 @@ "billing_mode": "PAY_PER_REQUEST", "hash_key": "id", "name": "wing-counter-c2-c81701d2" + }, + "c3": { + "//": { + "metadata": { + "path": "root/Default/Default/c3/Default", + "uniqueId": "c3" + } + }, + "attribute": [ + { + "name": "id", + "type": "S" + } + ], + "billing_mode": "PAY_PER_REQUEST", + "hash_key": "id", + "name": "wing-counter-c3-c893f3c2" } }, "aws_iam_role": { @@ -93,6 +120,15 @@ } }, "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" + }, + "q3-SetConsumer0_IamRole_2934E47C": { + "//": { + "metadata": { + "path": "root/Default/Default/q3-SetConsumer0/IamRole", + "uniqueId": "q3-SetConsumer0_IamRole_2934E47C" + } + }, + "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Effect\":\"Allow\"}]}" } }, "aws_iam_role_policy": { @@ -115,6 +151,16 @@ }, "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:ReceiveMessage\",\"sqs:ChangeMessageVisibility\",\"sqs:GetQueueUrl\",\"sqs:DeleteMessage\",\"sqs:GetQueueAttributes\"],\"Resource\":[\"${aws_sqs_queue.q2.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"dynamodb:UpdateItem\"],\"Resource\":[\"${aws_dynamodb_table.c2.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"sqs:GetQueueUrl\"],\"Resource\":[\"${aws_sqs_queue.q2.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"sqs:SendMessage\"],\"Resource\":[\"${aws_sqs_queue.q2.arn}\"],\"Effect\":\"Allow\"}]}", "role": "${aws_iam_role.q2-SetConsumer0_IamRole_910A96B5.name}" + }, + "q3-SetConsumer0_IamRolePolicy_D9B45CEB": { + "//": { + "metadata": { + "path": "root/Default/Default/q3-SetConsumer0/IamRolePolicy", + "uniqueId": "q3-SetConsumer0_IamRolePolicy_D9B45CEB" + } + }, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"sqs:ReceiveMessage\",\"sqs:ChangeMessageVisibility\",\"sqs:GetQueueUrl\",\"sqs:DeleteMessage\",\"sqs:GetQueueAttributes\"],\"Resource\":[\"${aws_sqs_queue.q3.arn}\"],\"Effect\":\"Allow\"},{\"Action\":[\"dynamodb:UpdateItem\"],\"Resource\":[\"${aws_dynamodb_table.c3.arn}\"],\"Effect\":\"Allow\"}]}", + "role": "${aws_iam_role.q3-SetConsumer0_IamRole_2934E47C.name}" } }, "aws_iam_role_policy_attachment": { @@ -137,6 +183,16 @@ }, "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", "role": "${aws_iam_role.q2-SetConsumer0_IamRole_910A96B5.name}" + }, + "q3-SetConsumer0_IamRolePolicyAttachment_F54AC63B": { + "//": { + "metadata": { + "path": "root/Default/Default/q3-SetConsumer0/IamRolePolicyAttachment", + "uniqueId": "q3-SetConsumer0_IamRolePolicyAttachment_F54AC63B" + } + }, + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "${aws_iam_role.q3-SetConsumer0_IamRole_2934E47C.name}" } }, "aws_lambda_event_source_mapping": { @@ -167,6 +223,20 @@ "function_response_types": [ "ReportBatchItemFailures" ] + }, + "q3_EventSourceMapping_8BF6F975": { + "//": { + "metadata": { + "path": "root/Default/Default/q3/EventSourceMapping", + "uniqueId": "q3_EventSourceMapping_8BF6F975" + } + }, + "batch_size": 1, + "event_source_arn": "${aws_sqs_queue.q3.arn}", + "function_name": "${aws_lambda_function.q3-SetConsumer0.function_name}", + "function_response_types": [ + "ReportBatchItemFailures" + ] } }, "aws_lambda_function": { @@ -234,6 +304,38 @@ "security_group_ids": [], "subnet_ids": [] } + }, + "q3-SetConsumer0": { + "//": { + "metadata": { + "path": "root/Default/Default/q3-SetConsumer0/Default", + "uniqueId": "q3-SetConsumer0" + } + }, + "architectures": [ + "arm64" + ], + "environment": { + "variables": { + "DYNAMODB_TABLE_NAME_9b30b36d": "${aws_dynamodb_table.c3.name}", + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "q3-SetConsumer0-c8cd013e", + "WING_TARGET": "tf-aws" + } + }, + "function_name": "q3-SetConsumer0-c8cd013e", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "${aws_iam_role.q3-SetConsumer0_IamRole_2934E47C.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "${aws_s3_bucket.Code.bucket}", + "s3_key": "${aws_s3_object.q3-SetConsumer0_S3Object_69810AFF.key}", + "timeout": "${aws_sqs_queue.q3.visibility_timeout_seconds}", + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [] + } } }, "aws_s3_bucket": { @@ -269,6 +371,17 @@ "bucket": "${aws_s3_bucket.Code.bucket}", "key": "", "source": "" + }, + "q3-SetConsumer0_S3Object_69810AFF": { + "//": { + "metadata": { + "path": "root/Default/Default/q3-SetConsumer0/S3Object", + "uniqueId": "q3-SetConsumer0_S3Object_69810AFF" + } + }, + "bucket": "${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "" } }, "aws_sqs_queue": { @@ -293,6 +406,17 @@ "message_retention_seconds": 3600, "name": "q2-c8aa6380", "visibility_timeout_seconds": 30 + }, + "q3": { + "//": { + "metadata": { + "path": "root/Default/Default/q3/Default", + "uniqueId": "q3" + } + }, + "message_retention_seconds": 3600, + "name": "q3-c8ed13d1", + "visibility_timeout_seconds": 30 } } } diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_test_sim.md index e437d3573fb..182d94fc6a6 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_test_sim.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/queue/set_consumer.test.w_test_sim.md @@ -2,10 +2,11 @@ ## stdout.log ```log -pass ─ set_consumer.test.wsim » root/env0/test:setConsumer -pass ─ set_consumer.test.wsim » root/env1/test:function can push back to the queue +pass ─ set_consumer.test.wsim » root/env0/test:setConsumer +pass ─ set_consumer.test.wsim » root/env1/test:function can push back to the queue +pass ─ set_consumer.test.wsim » root/env2/test:messages pushed to queue can be processed concurrently -Tests 2 passed (2) +Tests 3 passed (3) Snapshots 1 skipped Test Files 1 passed (1) Duration From ddd52cb4becb9746e98527e35ae315665356a576 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 22 Apr 2024 00:26:26 +0300 Subject: [PATCH 11/22] fix(platforms): unable to perform awscdk context lookups (#6286) In order to fix #6279, we need synthesis to be triggered by the CDK CLI instead of directly from the Wing CLI. This is actually really easy to do. All you need is to create a `cdk.json` file (or use one from `cdk init`) and modify the `app` and the `output` options like so: ```json { "app": "CDK_STACK_NAME=MyStack wing compile --platform @winglang/platform-awscdk main.w", "output": "target/main.awscdk" } ``` Then, you can simply use `cdk deploy`, `cdk diff`, `cdk synth` as if it was a normal CDK app. No need to explicitly interact with the Wing CLI in this case. To allow context lookups, we need to support specifying the stack's AWS environment (account/region), so two new environment variables have been added: `CDK_AWS_ACCOUNT` and `CDK_AWS_REGION`. These can be set together with `CDK_STACK_NAME` in the `cdk.json` file mentioned above. ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [x] Docs updated (only required for features) - [x] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- docs/docs/055-platforms/awscdk.md | 149 ++++++++++++++++++++++++++---- libs/awscdk/package.json | 1 + libs/awscdk/src/app.ts | 14 ++- libs/awscdk/test/platform.test.ts | 66 +++++++++++++ 4 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 libs/awscdk/test/platform.test.ts diff --git a/docs/docs/055-platforms/awscdk.md b/docs/docs/055-platforms/awscdk.md index a6fd5ad5ba8..fa4dc4f1ec9 100644 --- a/docs/docs/055-platforms/awscdk.md +++ b/docs/docs/055-platforms/awscdk.md @@ -6,50 +6,161 @@ description: AWS CDK platform keywords: [Wing reference, Wing language, language, Wing language spec, Wing programming language, aws, awscdk, amazon web services, cloudformation] --- -The `@winglang/platform-awscdk` [platform](../02-concepts/03-platforms.md) compiles your program for the AWS CDK (CloudFormation). +The `@winglang/platform-awscdk` [platform](../02-concepts/03-platforms.md) compiles your program for +the AWS CDK and deployed through the CDK CLI (and AWS CloudFormation). + +## Prerequisites + +* Install the AWS CDK (or use via `npx cdk`): + ```sh + npm i aws-cdk + ``` + +* Install the Wing `awscdk` platform: + ```sh + npm i @winglang/platform-awscdk + ``` ## Usage -You will need to install the `@winglang/platform-awscdk` library in order to use this platform. +Let's create `main.w` with our Wing program: + +```js +bring cloud; + +new cloud.Bucket(); +``` + +> At this point, you can just run `wing it` (or `wing run`) to open the Wing Simulator. + +To use Wing with the AWS CDK, we will need to tell the CDK CLI to run `wing compile` as the CDK app +and that the synthesis output is in the Wing's target directory. + +This can be done by creating a `cdk.json` file manually or through `cdk init` and editing the `app` +and the `output` fields: + +```json +{ + "app": "CDK_STACK_NAME='MyStack' CDK_AWS_ACCOUNT='111111555555' CDK_AWS_REGION='us-east-1' wing compile --platform @winglang/platform-awscdk main.w", + "output": "target/main.awscdk", + + // ... rest of cdk.json +} +``` + +The `awscdk` platform uses the following environment variables as configuration options: + +* `CDK_STACK_NAME` (required) - sets the CloudFormation stack name to use. +- `CDK_AWS_ACCOUNT` and `CDK_AWS_REGION` (optional) - the AWS environment for deployment and context + lookups (e.g. VPC lookups). The default is to use the AWS account region defined in the CLI + environment. + +Now, the AWS CDK CLI will work as normal: + +* `npx cdk bootstrap` bootstrap your AWS account for AWS CDK use (once per account/region). +* `npx cdk deploy` deploy the Wing app to your default AWS account/region. +* `npx cdk synth` synthesize output to `cdk.out`. +* `npx cdk diff` show a diff between your code and the deployed version + +## Deployment + +Before you can first deploy an AWS CDK app to your account, you'll need to bootstrap the account/region: ```sh -$ npm i @winglang/platform-awscdk +npx cdk bootstrap ``` -This platform requires the environment variable `CDK_STACK_NAME` to be set to the name of the CDK -stack to synthesize. +Now, you can use `cdk deploy` to deploy the latest version: ```sh -$ export CDK_STACK_NAME="my-project" -$ wing compile --platform @winglang/platform-awscdk [entrypoint] +npx cdk deploy ``` -## Parameters +Let's make a change to your app: -The `CDK_STACK_NAME` environment variable specifies the name of the CDK stack to synthesize. +```js +bring cloud; -## Output +let b = new cloud.Bucket(); -The output includes both a AWS-CDK configuration file (under `target/.awscdk`) and -JavaScript bundles that include inflight code that executes on compute platforms such as AWS Lambda. +new cloud.Function(inflight () => { + b.put("hello.txt", "world"); +}); +``` -## Deployment +If we run `cdk diff` we should see the new resources that are about to be created: -To deploy your app, you will first need to install the [AWS CDK -CLI](https://docs.aws.amazon.com/cdk/v2/guide/cli.html). +```sh +$ npx cdk diff +IAM Statement Changes +┌───┬──────────────────────────────────┬────────┬──────────────────────────────────┬──────────────────────────────────┬───────────┐ +│ │ Resource │ Effect │ Action │ Principal │ Condition │ +├───┼──────────────────────────────────┼────────┼──────────────────────────────────┼──────────────────────────────────┼───────────┤ +│ + │ ${Default/Default/Bucket/Default │ Allow │ s3:Abort* │ AWS:${Default/Default/Function/D │ │ +│ │ .Arn} │ │ s3:PutObject* │ efault/ServiceRole} │ │ +│ │ ${Default/Default/Bucket/Default │ │ │ │ │ +│ │ .Arn}/* │ │ │ │ │ +├───┼──────────────────────────────────┼────────┼──────────────────────────────────┼──────────────────────────────────┼───────────┤ +│ + │ ${Default/Default/Function/Defau │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │ +│ │ lt/ServiceRole.Arn} │ │ │ │ │ +└───┴──────────────────────────────────┴────────┴──────────────────────────────────┴──────────────────────────────────┴───────────┘ +IAM Policy Changes +┌───┬──────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────┐ +│ │ Resource │ Managed Policy ARN │ +├───┼──────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ + │ ${Default/Default/Function/Default/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambda │ +│ │ │ BasicExecutionRole │ +└───┴──────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────┘ +(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299) + +Resources +[+] AWS::Logs::LogGroup Default/Default/Function/LogGroup FunctionLogGroup55B80E27 +[+] AWS::IAM::Role Default/Default/Function/Default/ServiceRole FunctionServiceRole675BB04A +[+] AWS::IAM::Policy Default/Default/Function/Default/ServiceRole/DefaultPolicy FunctionServiceRoleDefaultPolicy2F49994A +[+] AWS::Lambda::Function Default/Default/Function/Default Function76856677 + + +✨ Number of stacks with differences: 1 +``` -If not previously done, you will need to bootstrap your environment (account/region): +Sweet!, now deploy again: ```sh -$ cdk bootstrap --app target/app.awscdk +npx cdk deploy ``` -And then you can deploy: +To destroy your stack, you can use: ```sh -$ cdk deploy --app target/app.awscdk +npx cdk destroy +``` + +## Bringing AWS CDK constructs to your Wing code + +You can bring any AWS CDK library and use constructs in your Wing programs. + +> Using AWS CDK constructs directly in your Wing application will only have an effect when compiling +> and deploying AWS CDK applications and not when running in the Wing Simulator. + +The following example shows how to define an EC2 instance inside a VPC (from a lookup): + +```js +bring "aws-cdk-lib" as cdk; + +let myVpc = cdk.aws_ec2.Vpc.fromLookup(this, "MyVpc", vpcId: "vpc-111111111222ddddd"); + +let type = cdk.aws_ec2.InstanceType.of(cdk.aws_ec2.InstanceClass.T2, cdk.aws_ec2.InstanceSize.MICRO); + +new cdk.aws_ec2.Instance( + instanceType: type, + machineImage: cdk.aws_ec2.MachineImage.latestAmazonLinux2(), + vpc: myVpc, +); ``` +Note that in order for the VPC lookup to work, you will need to make sure `CDK_AWS_ACCOUNT` and +`CDK_AWS_REGION` are configured properly in your `cdk.json` `app` configuration. + ## Customizations ### Custom CDK Stack diff --git a/libs/awscdk/package.json b/libs/awscdk/package.json index 34b9f9e5d96..422cef6c2ba 100644 --- a/libs/awscdk/package.json +++ b/libs/awscdk/package.json @@ -33,6 +33,7 @@ }, "scripts": { "compile": "tsc", + "watch": "tsc --watch", "package": "bump-pack -b", "test": "vitest run --passWithNoTests --update" }, diff --git a/libs/awscdk/src/app.ts b/libs/awscdk/src/app.ts index 9e0a64fc52c..1d36def2b38 100644 --- a/libs/awscdk/src/app.ts +++ b/libs/awscdk/src/app.ts @@ -54,7 +54,7 @@ export interface CdkAppProps extends core.AppProps { * * @default - creates a standard `cdk.Stack` */ - readonly stackFactory?: (app: cdk.App, stackName: string) => cdk.Stack; + readonly stackFactory?: (app: cdk.App, stackName: string, props?: cdk.StackProps) => cdk.Stack; } /** @@ -76,6 +76,9 @@ export class App extends core.App { private synthedOutput: string | undefined; constructor(props: CdkAppProps) { + const account = process.env.CDK_AWS_ACCOUNT ?? process.env.CDK_DEFAULT_ACCOUNT; + const region = process.env.CDK_AWS_REGION ?? process.env.CDK_DEFAULT_REGION; + let stackName = props.stackName ?? process.env.CDK_STACK_NAME; if (stackName === undefined) { throw new Error( @@ -95,11 +98,14 @@ export class App extends core.App { const cdkApp = new cdk.App({ outdir: cdkOutdir }); const createStack = - props.stackFactory ?? ((app, stackName) => new cdk.Stack(app, stackName)); - const cdkStack = createStack(cdkApp, stackName); + props.stackFactory ?? ((app, stackName, props) => new cdk.Stack(app, stackName, props)); - super(cdkStack, props.rootId ?? "Default", props); + const cdkStack = createStack(cdkApp, stackName, { + env: { account, region } + }); + super(cdkStack, props.rootId ?? "Default", props); + // HACK: monkey patch the `new` method on the cdk app (which is the root of the tree) so that // we can intercept the creation of resources and replace them with our own. (cdkApp as any).new = ( diff --git a/libs/awscdk/test/platform.test.ts b/libs/awscdk/test/platform.test.ts new file mode 100644 index 00000000000..56de662133e --- /dev/null +++ b/libs/awscdk/test/platform.test.ts @@ -0,0 +1,66 @@ +import { test, expect } from "vitest"; +import { Platform } from "../src/platform" +import { mkdtemp } from "@winglang/sdk/test/util"; +import { readdirSync } from "fs"; +import { Bucket } from "../src"; +import { Stack } from "aws-cdk-lib"; + +test("wing platform", async () => { + const workdir = mkdtemp(); + const platform = new Platform(); + process.env.CDK_STACK_NAME = "MyStack"; + const app = platform.newApp?.({ entrypointDir: workdir, outdir: workdir }); + + new Bucket(app, "bucket"); + + const out = app.synth(); + + expect(JSON.parse(out).Resources.bucket43879C71).toStrictEqual({ + DeletionPolicy: "Delete", + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + SSEAlgorithm: "AES256", + }, + }, + ], + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + }, + Type: "AWS::S3::Bucket", + UpdateReplacePolicy: "Delete", + }); + + // output directory only contains wing artifacts. cdk artifacts will be in the cdk.out directory + // when the CDK CLI is used + expect(readdirSync(workdir)).toStrictEqual([ + "MyStack.assets.json", + "MyStack.template.json", + "cdk.out", + "connections.json", + "manifest.json", + "tree.json", + ]); +}); + +test("CDK_STACK_NAME, CDK_AWS_ACCOUNT, CDK_AWS_REGION", async () => { + const workdir = mkdtemp(); + const platform = new Platform(); + process.env.CDK_STACK_NAME = "YourStack"; + process.env.CDK_AWS_ACCOUNT = "123"; + process.env.CDK_AWS_REGION = "us-west-2"; + + const app = platform.newApp?.({ entrypointDir: workdir, outdir: workdir }); + const stack = Stack.of(app); + + expect(stack.resolve(stack.region)).toStrictEqual("us-west-2"); + expect(stack.resolve(stack.stackName)).toStrictEqual("YourStack"); + expect(stack.resolve(stack.account)).toStrictEqual("123"); +}); From 87c2713bc9e396ec115fd61fd70bd91ee457bff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Mon, 22 Apr 2024 12:03:43 +0200 Subject: [PATCH 12/22] fix(console): `option+cmd+i` command leaves the map in drag mode (#6291) When a user presses `option+cmd+i` to open the devtools, a spacebar keypress event is fired (on Chrome, at least). This unexpected keypress event leaves the map in drag mode and nodes can't be clicked until the spacebar is pressed again. This PR checks the `event.altKey` property to conditionally ignore this unexpected keypress event. --- .../console/ui/src/ui/zoom-pane.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/wing-console/console/ui/src/ui/zoom-pane.tsx b/apps/wing-console/console/ui/src/ui/zoom-pane.tsx index 6d293cedc7d..a3001cb1b62 100644 --- a/apps/wing-console/console/ui/src/ui/zoom-pane.tsx +++ b/apps/wing-console/console/ui/src/ui/zoom-pane.tsx @@ -1,7 +1,6 @@ import { Button, useTheme } from "@wingconsole/design-system"; import classNames from "classnames"; import { - MouseEventHandler, createContext, forwardRef, useCallback, @@ -14,7 +13,7 @@ import { } from "react"; import type { DetailedHTMLProps, HTMLAttributes } from "react"; import type { ReactNode } from "react"; -import { useEvent, useKeyPressEvent } from "react-use"; +import { useEvent } from "react-use"; import { MapControls } from "./map-controls.js"; @@ -158,16 +157,24 @@ export const ZoomPane = forwardRef((props, ref) => { // Keep track of whether the space key is pressed so we can show the user the grab cursor. // The map is draggable using click only when space is pressed. const [isSpacePressed, setSpacePressed] = useState(false); - useKeyPressEvent( - " ", - useCallback(() => { - setSpacePressed(true); - }, []), - useCallback(() => { - setSpacePressed(false); - setDragging(false); - }, []), - ); + useEffect(() => { + const listener = (event: KeyboardEvent) => { + // Pressing option+cmd+i on Mac (which is used to open the devtools) fires a keydown event with key " " on Chrome. + if (event.altKey) { + return; + } + + if (event.key === " ") { + setSpacePressed(event.type === "keydown"); + } + }; + window.addEventListener("keydown", listener); + window.addEventListener("keyup", listener); + return () => { + window.removeEventListener("keydown", listener); + window.removeEventListener("keyup", listener); + }; + }, []); const dragStart = useRef({ x: 0, y: 0 }); useEvent( From b7d87b788abe2064e9d454b4fe845e8a3a9a0902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Mon, 22 Apr 2024 15:30:32 +0200 Subject: [PATCH 13/22] fix: conflicting `SIGINT` handlers (#6289) There were two competing `SIGINT` handlers and the result was unreleased Console resources. This PR removes the `conf` package (which was registering a `SIGINT` handler) in favor of simple read and write methods for the config file. Fixes #6018. Supersedes #6275. --- apps/wing-console/console/app/package.json | 1 - apps/wing-console/console/server/package.json | 4 +- .../server/src/utils/terms-and-conditions.ts | 40 ++++++++--- pnpm-lock.yaml | 70 ++----------------- 4 files changed, 39 insertions(+), 76 deletions(-) diff --git a/apps/wing-console/console/app/package.json b/apps/wing-console/console/app/package.json index 55b4cd3f2aa..2d94f52ce7c 100644 --- a/apps/wing-console/console/app/package.json +++ b/apps/wing-console/console/app/package.json @@ -41,7 +41,6 @@ "@wingconsole/ui": "workspace:^", "autoprefixer": "^10.4.15", "bump-pack": "workspace:^", - "conf": "^11.0.2", "dotenv": "^16.3.1", "eslint": "^8.48.0", "nanoid": "^4.0.2", diff --git a/apps/wing-console/console/server/package.json b/apps/wing-console/console/server/package.json index 46c220146c6..f8f9d2f3b48 100644 --- a/apps/wing-console/console/server/package.json +++ b/apps/wing-console/console/server/package.json @@ -37,10 +37,10 @@ "@wingconsole/tsconfig": "workspace:^", "bump-pack": "workspace:^", "chokidar": "^3.5.3", - "conf": "^11.0.2", "constructs": "^10.3", "cors": "^2.8.5", "emittery": "^1.0.1", + "env-paths": "^3.0.0", "esbuild-plugin-raw": "^0.1.7", "eslint": "^8.48.0", "express": "^4.19.2", @@ -58,4 +58,4 @@ "volta": { "extends": "../../../../package.json" } -} \ No newline at end of file +} diff --git a/apps/wing-console/console/server/src/utils/terms-and-conditions.ts b/apps/wing-console/console/server/src/utils/terms-and-conditions.ts index b835a0823f7..748d419812e 100644 --- a/apps/wing-console/console/server/src/utils/terms-and-conditions.ts +++ b/apps/wing-console/console/server/src/utils/terms-and-conditions.ts @@ -1,5 +1,6 @@ -// @ts-ignore-next-line -import Conf from "conf"; +import * as fs from "node:fs"; + +import envPaths from "env-paths"; // @ts-ignore-next-line import License from "../../LICENSE.md?raw"; @@ -7,21 +8,40 @@ import License from "../../LICENSE.md?raw"; const PROJECT_NAME = "@wingconsole/server"; const CONFIG_KEY = "termsAndConditions"; +const paths = envPaths(PROJECT_NAME); + +const getConfig = () => { + try { + return JSON.stringify( + fs.readFileSync(paths.config, "utf8"), + ) as unknown as Record; + } catch (error) { + console.error(error); + } +}; + +const saveConfig = (config: Record) => { + try { + fs.writeFileSync(paths.config, JSON.stringify(config, undefined, 2)); + } catch (error) { + console.error(error); + } +}; + export const isTermsAccepted = (): boolean => { - const config = new Conf({ - projectName: PROJECT_NAME, - }); + const config = getConfig(); - const accepted = config.get(CONFIG_KEY) as boolean; + const accepted = config?.[CONFIG_KEY]; return accepted === true; }; export const acceptTerms = (value: boolean) => { - const config = new Conf({ - projectName: PROJECT_NAME, - }); + const config = getConfig(); - config.set(CONFIG_KEY, value); + saveConfig({ + ...config, + [CONFIG_KEY]: value, + }); }; export const getLicense = (): string => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c347e5afa2..9cd7e731b9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -506,9 +506,6 @@ importers: bump-pack: specifier: workspace:^ version: link:../../../../tools/bump-pack - conf: - specifier: ^11.0.2 - version: 11.0.2 dotenv: specifier: ^16.3.1 version: 16.3.1 @@ -688,9 +685,6 @@ importers: chokidar: specifier: ^3.5.3 version: 3.5.3 - conf: - specifier: ^11.0.2 - version: 11.0.2 constructs: specifier: ^10.3 version: 10.3.0 @@ -700,6 +694,9 @@ importers: emittery: specifier: ^1.0.1 version: 1.0.1 + env-paths: + specifier: ^3.0.0 + version: 3.0.0 esbuild-plugin-raw: specifier: ^0.1.7 version: 0.1.7(esbuild@0.15.18) @@ -11861,17 +11858,6 @@ packages: clean-stack: 2.2.0 indent-string: 4.0.0 - /ajv-formats@2.1.1(ajv@8.12.0): - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - dependencies: - ajv: 8.12.0 - dev: true - /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -12261,13 +12247,6 @@ packages: engines: {node: '>= 4.0.0'} dev: true - /atomically@2.0.2: - resolution: {integrity: sha512-Xfmb4q5QV7uqTlVdMSTtO5eF4DCHfNOdaPyKlbFShkzeNP+3lj3yjjcbdjSmEY4+pDBKJ9g26aP+ImTe88UHoQ==} - dependencies: - stubborn-fs: 1.2.5 - when-exit: 2.1.1 - dev: true - /auto-bind@4.0.0: resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} engines: {node: '>=8'} @@ -13416,20 +13395,6 @@ packages: readable-stream: 3.6.2 typedarray: 0.0.6 - /conf@11.0.2: - resolution: {integrity: sha512-jjyhlQ0ew/iwmtwsS2RaB6s8DBifcE2GYBEaw2SJDUY/slJJbNfY4GlDVzOs/ff8cM/Wua5CikqXgbFl5eu85A==} - engines: {node: '>=14.16'} - dependencies: - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) - atomically: 2.0.2 - debounce-fn: 5.1.2 - dot-prop: 7.2.0 - env-paths: 3.0.0 - json-schema-typed: 8.0.1 - semver: 7.5.4 - dev: true - /config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} dependencies: @@ -13801,13 +13766,6 @@ packages: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} dev: true - /debounce-fn@5.1.2: - resolution: {integrity: sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==} - engines: {node: '>=12'} - dependencies: - mimic-fn: 4.0.0 - dev: true - /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -14177,13 +14135,6 @@ packages: is-obj: 2.0.0 dev: true - /dot-prop@7.2.0: - resolution: {integrity: sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - type-fest: 2.19.0 - dev: true - /dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} @@ -14206,7 +14157,7 @@ packages: dependencies: semver: 7.5.4 shelljs: 0.8.5 - typescript: 5.5.0-dev.20240415 + typescript: 5.5.0-dev.20240422 dev: true /dset@3.1.2: @@ -17824,10 +17775,6 @@ packages: /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - /json-schema-typed@8.0.1: - resolution: {integrity: sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==} - dev: true - /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -21978,10 +21925,6 @@ packages: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} dev: false - /stubborn-fs@1.2.5: - resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} - dev: true - /stubs@3.0.0: resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} dev: false @@ -22839,8 +22782,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - /typescript@5.5.0-dev.20240415: - resolution: {integrity: sha512-2FlWR0afPLL3qaO+AcMp6rWmV6nrLoRhwDYAgGn7/Kw1/K8hcW83bZnn+gzGNWfXozx/EdJuG0KfN7V/RGysjg==} + /typescript@5.5.0-dev.20240422: + resolution: {integrity: sha512-GKfP7cqp5Rq/z6xNYZ6u6XfS4I0K3h8tw3MRKkttEkLyEWBxYS141wOr1WBcUYfvB1F5Luo/cTd7iZnySH3bIg==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -23605,6 +23548,7 @@ packages: /when-exit@2.1.1: resolution: {integrity: sha512-XLipGldz/UcleuGaoQjbYuWwD+ICRnzIjlldtwTaTWr7aZz8yQW49rXk6MHQnh+KxOiWiJpM1vIyaxprOnlW4g==} + dev: false /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} From 831382e35c9b974755ab4c68542574c21a55cede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Mon, 22 Apr 2024 16:16:28 +0200 Subject: [PATCH 14/22] fix(console): config file is not correctly accessed (#6293) Commit b7d87b788abe2064e9d454b4fe845e8a3a9a0902 introduced code that didn't correctly access the Console config file. --- .../console/server/src/utils/terms-and-conditions.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/wing-console/console/server/src/utils/terms-and-conditions.ts b/apps/wing-console/console/server/src/utils/terms-and-conditions.ts index 748d419812e..f2061463699 100644 --- a/apps/wing-console/console/server/src/utils/terms-and-conditions.ts +++ b/apps/wing-console/console/server/src/utils/terms-and-conditions.ts @@ -10,10 +10,16 @@ const CONFIG_KEY = "termsAndConditions"; const paths = envPaths(PROJECT_NAME); +const configFilename = `${paths.config}/config.json`; + const getConfig = () => { + if (!fs.existsSync(configFilename)) { + return; + } + try { return JSON.stringify( - fs.readFileSync(paths.config, "utf8"), + fs.readFileSync(configFilename, "utf8"), ) as unknown as Record; } catch (error) { console.error(error); @@ -22,7 +28,8 @@ const getConfig = () => { const saveConfig = (config: Record) => { try { - fs.writeFileSync(paths.config, JSON.stringify(config, undefined, 2)); + fs.mkdirSync(paths.config, { recursive: true }); + fs.writeFileSync(configFilename, JSON.stringify(config, undefined, 2)); } catch (error) { console.error(error); } From eca7c9b9ce50fc5b792a7945b03a28e47117fa17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pol=20Amor=C3=B3s?= Date: Mon, 22 Apr 2024 17:13:47 +0200 Subject: [PATCH 15/22] fix(console): tsoa ui http client is not maintaining state (#6288) Resolves #6287 ## Checklist - [ ] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [ ] Description explains motivation and solution - [ ] Tests added (always) - [ ] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*. --- apps/wing-console/console/app/demo/main.w | 2 +- .../ui/src/features/api-interaction-view.tsx | 16 +- .../console/ui/src/ui/api-interaction.tsx | 176 ++++++++++-------- .../ui/src/ui/custom-resource-http-client.tsx | 35 +++- 4 files changed, 134 insertions(+), 95 deletions(-) diff --git a/apps/wing-console/console/app/demo/main.w b/apps/wing-console/console/app/demo/main.w index df32ae67cd2..d6cb57b78a7 100644 --- a/apps/wing-console/console/app/demo/main.w +++ b/apps/wing-console/console/app/demo/main.w @@ -254,7 +254,7 @@ class ApiUsersService { "parameters": [ { "in": "header", - "name": "cookie", + "name": "accept", }, ], "requestBody": { diff --git a/apps/wing-console/console/ui/src/features/api-interaction-view.tsx b/apps/wing-console/console/ui/src/features/api-interaction-view.tsx index 0bac5ab408c..43a625335f0 100644 --- a/apps/wing-console/console/ui/src/features/api-interaction-view.tsx +++ b/apps/wing-console/console/ui/src/features/api-interaction-view.tsx @@ -1,4 +1,5 @@ import type { OpenApiSpec } from "@wingconsole/server/src/wingsdk"; +import { createPersistentState } from "@wingconsole/use-persistent-state"; import { memo, useCallback, useContext, useState } from "react"; import { AppContext } from "../AppContext.js"; @@ -13,11 +14,19 @@ export interface ApiViewProps { export const ApiInteractionView = memo(({ resourcePath }: ApiViewProps) => { const { appMode } = useContext(AppContext); + const { usePersistentState } = createPersistentState(resourcePath); - const [apiResponse, setApiResponse] = useState(); + const [apiResponse, setApiResponse] = usePersistentState< + ApiResponse | undefined + >(); const onFetchDataUpdate = useCallback( - (data: ApiResponse) => setApiResponse(data), - [], + (data: ApiResponse) => { + if (!data) { + return; + } + setApiResponse(data); + }, + [setApiResponse], ); const schema = trpc["api.schema"].useQuery({ resourcePath }); @@ -35,6 +44,7 @@ export const ApiInteractionView = memo(({ resourcePath }: ApiViewProps) => { callFetch={callFetch} isLoading={isLoading} apiResponse={apiResponse} + resetApiResponse={() => setApiResponse(undefined)} /> ); }); diff --git a/apps/wing-console/console/ui/src/ui/api-interaction.tsx b/apps/wing-console/console/ui/src/ui/api-interaction.tsx index d38c90fd0d1..1f882b61a39 100644 --- a/apps/wing-console/console/ui/src/ui/api-interaction.tsx +++ b/apps/wing-console/console/ui/src/ui/api-interaction.tsx @@ -1,3 +1,4 @@ +import type { KeyValueItem } from "@wingconsole/design-system"; import { Attribute, Button, @@ -36,6 +37,7 @@ export interface ApiInteractionProps { openApiSpec: OpenApiSpec; callFetch: (data: ApiRequest) => void; isLoading: boolean; + resetApiResponse?: () => void; } export const ApiInteraction = memo( @@ -47,6 +49,7 @@ export const ApiInteraction = memo( url, openApiSpec, isLoading, + resetApiResponse = () => {}, }: ApiInteractionProps) => { const { theme } = useTheme(); @@ -55,21 +58,23 @@ export const ApiInteraction = memo( const [routes, setRoutes] = useState([]); const [currentHeaderKey, setCurrentHeaderKey] = usePersistentState(""); - const [currentHeaderValues, setCurrentHeaderValues] = useState( - [], - ); - const [currentRoute, setCurrentRoute] = usePersistentState(""); - + const [currentHeaderValues, setCurrentHeaderValues] = usePersistentState< + string[] + >([]); const [currentMethod, setCurrentMethod] = usePersistentState("GET"); + const [currentRoute, setCurrentRoute] = usePersistentState(""); + const bodyId = useId(); - const [bodyPlaceholder, setBodyPlaceholder] = useState< + const [isBodyEdited, setIsBodyEdited] = usePersistentState(false); + const [body, setBody] = usePersistentState(""); + const [bodyPlaceholder, setBodyPlaceholder] = usePersistentState< string | undefined >(); - const [body, setBody] = usePersistentState(""); const [currentOptionsTab, setCurrentOptionsTab] = usePersistentState("headers"); + const [currentResponseTab, setCurrentResponseTab] = usePersistentState("body"); @@ -150,57 +155,62 @@ export const ApiInteraction = memo( const loadDataFromOpenApi = useCallback( (path: string, method: string) => { - // Set the headers - const headersFromSpec = getParametersFromOpenApi({ - path: path, - method: method, - openApi: openApiSpec, - type: "header", + setHeaders((headers) => { + const headersFromSpec = getParametersFromOpenApi({ + path: path, + method: method, + openApi: openApiSpec, + type: "header", + }); + const newHeaders = headersFromSpec.filter( + (header) => + !headers.some( + (existingHeader) => existingHeader.key === header.key, + ), + ); + return [ + ...headers.filter((header) => header.value !== ""), + ...newHeaders, + ]; }); - const newHeaders = headersFromSpec.filter( - (header) => - !headers.some( - (existingHeader) => existingHeader.key === header.key, - ), - ); - setHeaders((headers) => [...headers, ...newHeaders]); - - // Set Query Parameters - const queryParametersFromSpec = getParametersFromOpenApi({ - path: path, - method: method, - openApi: openApiSpec, - type: "query", + + setQueryParameters((queryParameters) => { + const queryParametersFromSpec = getParametersFromOpenApi({ + path: path, + method: method, + openApi: openApiSpec, + type: "query", + }); + const newQueryParameters = queryParametersFromSpec.filter( + (parameter) => + !queryParameters.some( + (existingParameter) => existingParameter.key === parameter.key, + ), + ); + return [ + ...queryParameters.filter((parameter) => parameter.value !== ""), + ...newQueryParameters, + ]; }); - const newQueryParameters = queryParametersFromSpec.filter( - (parameter) => - !queryParameters.some( - (existingParameter) => existingParameter.key === parameter.key, - ), - ); - setQueryParameters((queryParameters) => [ - ...queryParameters, - ...newQueryParameters, - ]); - - // Set Path Variables - const variablesFromSpec = getParametersFromOpenApi({ - path: path, - method: method, - openApi: openApiSpec, - type: "path", + setPathVariables((pathVariables) => { + const variablesFromSpec = getParametersFromOpenApi({ + path: path, + method: method, + openApi: openApiSpec, + type: "path", + }); + const newPathVariables = variablesFromSpec.filter( + (variable) => + !pathVariables.some( + (existingVariable) => existingVariable.key === variable.key, + ), + ); + return [ + ...pathVariables.filter((variable) => variable.value !== ""), + ...newPathVariables, + ]; }); - const newPathVariables = variablesFromSpec.filter( - (variable) => - !pathVariables.some( - (existingVariable) => existingVariable.key === variable.key, - ), - ); - setPathVariables((pathVariables) => [ - ...pathVariables, - ...newPathVariables, - ]); // Set the body const bodyFromSpec = getRequestBodyFromOpenApi( @@ -208,18 +218,23 @@ export const ApiInteraction = memo( method, openApiSpec, ); - setBody(JSON.stringify(bodyFromSpec, undefined, 2)); - setBodyPlaceholder(JSON.stringify(bodyFromSpec, undefined, 2)); + const body = bodyFromSpec + ? JSON.stringify(bodyFromSpec, undefined, 2) + : undefined; + + if (!isBodyEdited) { + setBody(body ?? ""); + } + setBodyPlaceholder(body); }, [ openApiSpec, setHeaders, setBody, + isBodyEdited, + setBodyPlaceholder, setQueryParameters, setPathVariables, - queryParameters, - pathVariables, - headers, ], ); @@ -252,18 +267,11 @@ export const ApiInteraction = memo( ); if (!isListedRoute) { - setHeaders([]); - setBody(""); setBodyPlaceholder(undefined); - setPathVariables([]); - setQueryParameters([]); } setQueryParameters(() => { - const newUrlParameters: { - key: string; - value: string; - }[] = []; + const newUrlParameters: KeyValueItem[] = []; for (const [key, value] of urlParameters.entries()) { newUrlParameters.push({ key, @@ -274,10 +282,7 @@ export const ApiInteraction = memo( }); setPathVariables(() => { - const newPathVariables: { - key: string; - value: string; - }[] = []; + const newPathVariables: KeyValueItem[] = []; const matches = newRoute.matchAll(/{(\w+)}/g) || []; for (const match of matches) { @@ -293,18 +298,21 @@ export const ApiInteraction = memo( }); if (isListedRoute && method) { - handleMethodChange(path, method); + setCurrentMethod(method); + loadDataFromOpenApi(path, method); } + resetApiResponse(); }, [ - setHeaders, routes, - setBody, + setBodyPlaceholder, setCurrentRoute, + setCurrentMethod, currentMethod, setPathVariables, setQueryParameters, - handleMethodChange, + loadDataFromOpenApi, + resetApiResponse, ], ); @@ -328,10 +336,10 @@ export const ApiInteraction = memo( handleMethodChange, ]); - // Load the routes from the OpenAPI spec + // load the routes from the open api spec on mount useEffect(() => { loadRoutesFromOpenApi(); - }, [openApiSpec, loadRoutesFromOpenApi]); + }, []); // Load the possible values for the current header key useEffect(() => { @@ -340,7 +348,7 @@ export const ApiInteraction = memo( return; } setCurrentHeaderValues(getHeaderValues(currentHeaderKey)); - }, [currentHeaderKey]); + }, [currentHeaderKey, setCurrentHeaderValues]); // Sync the query parameters with the current route. useEffect(() => { @@ -505,7 +513,7 @@ export const ApiInteraction = memo( }, { id: "body", - name: `Body ${body || bodyPlaceholder ? "*" : ""}`, + name: `Body ${isBodyEdited ? "*" : ""}`, panel: (