@aws/nx-plugin 1.0.0-rc.84 → 1.0.0-rc.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/generators.json +7 -0
  2. package/migrations.json +6 -1
  3. package/package.json +1 -1
  4. package/src/internal/test-matrix/generator.js +16 -0
  5. package/src/internal/test-matrix/generator.js.map +1 -1
  6. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/metadata.json +3 -0
  7. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.d.ts +6 -0
  8. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.js +146 -0
  9. package/src/migrations/latest/terraform-project-checkov-target-and-bootstrap-destroy/migration.js.map +1 -0
  10. package/src/open-api/json-metadata/generator.d.ts +24 -0
  11. package/src/open-api/json-metadata/generator.js +37 -0
  12. package/src/open-api/json-metadata/generator.js.map +1 -0
  13. package/src/open-api/json-metadata/schema.d.js +6 -0
  14. package/src/open-api/json-metadata/schema.d.js.map +1 -0
  15. package/src/open-api/json-metadata/schema.d.ts +9 -0
  16. package/src/open-api/json-metadata/schema.json +20 -0
  17. package/src/py/fast-api/__snapshots__/generator.terraform.spec.ts.snap +1604 -559
  18. package/src/py/fast-api/generator.js +2 -1
  19. package/src/py/fast-api/generator.js.map +1 -1
  20. package/src/sdk/open-api.d.ts +2 -0
  21. package/src/sdk/open-api.js +2 -1
  22. package/src/sdk/open-api.js.map +1 -1
  23. package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +1548 -440
  24. package/src/smithy/ts/api/generator.js +2 -1
  25. package/src/smithy/ts/api/generator.js.map +1 -1
  26. package/src/terraform/project/files/application/bootstrap/providers.tf.template +2 -0
  27. package/src/terraform/project/files/application/scripts/bootstrap-destroy.ts.template +111 -0
  28. package/src/terraform/project/files/application/src/providers.tf.template +2 -0
  29. package/src/terraform/project/files/checkov/checkov.yml.template +8 -0
  30. package/src/terraform/project/generator.js +47 -4
  31. package/src/terraform/project/generator.js.map +1 -1
  32. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +1552 -525
  33. package/src/trpc/backend/files-operations/scripts/generate-operations.ts.template +40 -0
  34. package/src/trpc/backend/generator.js +10 -0
  35. package/src/trpc/backend/generator.js.map +1 -1
  36. package/src/utils/api-constructs/api-constructs.js +11 -0
  37. package/src/utils/api-constructs/api-constructs.js.map +1 -1
  38. package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +243 -3
  39. package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +428 -1
  40. package/src/utils/api-constructs/open-api-metadata.d.ts +20 -3
  41. package/src/utils/api-constructs/open-api-metadata.js +64 -8
  42. package/src/utils/api-constructs/open-api-metadata.js.map +1 -1
  43. package/src/utils/api-constructs/trpc-operations-metadata.d.ts +21 -0
  44. package/src/utils/api-constructs/trpc-operations-metadata.js +55 -0
  45. package/src/utils/api-constructs/trpc-operations-metadata.js.map +1 -0
@@ -131,6 +131,95 @@ resource "random_string" "suffix" {
131
131
  upper = false
132
132
  }
133
133
 
134
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
135
+ locals {
136
+ # Generated at build time from the API definition
137
+ operations_file = "${path.module}/../../../generated/<%- apiNameKebabCase %>/operations.json"
138
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
139
+
140
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
141
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
142
+
143
+ function_name = { for op in keys(local.operations) : op =>
144
+ "${substr("<%- apiNameClassName %>-${local.operation_slug[op]}", 0, 64 - length(local.operation_hash[op]) - length(random_string.suffix.result) - 2)}-${local.operation_hash[op]}-${random_string.suffix.result}"
145
+ }
146
+ role_name = { for op in keys(local.operations) : op =>
147
+ "${substr("<%- apiNameClassName %>-${local.operation_slug[op]}-role", 0, 64 - length(local.operation_hash[op]) - length(random_string.suffix.result) - 2)}-${local.operation_hash[op]}-${random_string.suffix.result}"
148
+ }
149
+
150
+ # A REST API needs a resource per path segment, so each operation's path is
151
+ # split into the distinct path prefixes the resource tree is built from below
152
+ operation_segments = { for op, details in local.operations : op => compact(split("/", details.path)) }
153
+
154
+ path_nodes = merge([
155
+ for op, segments in local.operation_segments : {
156
+ for i, segment in segments : join("/", slice(segments, 0, i + 1)) => {
157
+ depth = i
158
+ parent = i == 0 ? "" : join("/", slice(segments, 0, i))
159
+ part = segment
160
+ }
161
+ }
162
+ ]...)
163
+
164
+ # Instances of a resource cannot reference one another, so each depth is a
165
+ # separate resource. Deeper paths are reported by the precondition below.
166
+ max_path_depth = <%- maxRestPathDepth %>
167
+ path_nodes_by_depth = { for depth in range(local.max_path_depth) : depth => {
168
+ for path, node in local.path_nodes : path => node if node.depth == depth
169
+ } }
170
+ paths_too_deep = [for path, node in local.path_nodes : path if node.depth >= local.max_path_depth]
171
+
172
+ resource_ids = merge(
173
+ <%_ for (let depth = 0; depth < maxRestPathDepth; depth++) { _%>
174
+ { for path, resource in aws_api_gateway_resource.path_depth_<%- depth %> : path => resource.id },
175
+ <%_ } _%>
176
+ )
177
+
178
+ operation_resource_id = { for op, segments in local.operation_segments : op =>
179
+ local.resource_ids[join("/", segments)]
180
+ }
181
+
182
+ permission_source_suffix = { for op, details in local.operations : op =>
183
+ "${upper(details.method)}/${replace(join("/", local.operation_segments[op]), "/{[^}]*}/", "*")}"
184
+ }
185
+ }
186
+
187
+ resource "terraform_data" "operations_metadata" {
188
+ # Fails the plan when the operations metadata has not been generated
189
+ input = local.operations_file
190
+
191
+ lifecycle {
192
+ precondition {
193
+ condition = fileexists(local.operations_file)
194
+ error_message = "Operations metadata not found at ${local.operations_file}. Build the <%- apiNameClassName %> project to generate it."
195
+ }
196
+ precondition {
197
+ condition = length(local.operations) > 0
198
+ error_message = "No operations found in ${local.operations_file}. The <%- apiNameClassName %> API must define at least one operation."
199
+ }
200
+ precondition {
201
+ condition = length(local.paths_too_deep) == 0
202
+ error_message = "Operation paths exceed the supported depth of ${local.max_path_depth} segments: ${join(", ", local.paths_too_deep)}. Add further aws_api_gateway_resource levels to this module to support them."
203
+ }
204
+ }
205
+ }
206
+
207
+ <%_ for (let depth = 0; depth < maxRestPathDepth; depth++) { _%>
208
+ # API Gateway resources at path depth <%- depth %>
209
+ resource "aws_api_gateway_resource" "path_depth_<%- depth %>" {
210
+ for_each = local.path_nodes_by_depth[<%- depth %>]
211
+
212
+ rest_api_id = module.rest_api.api_id
213
+ <%_ if (depth === 0) { _%>
214
+ parent_id = module.rest_api.api_root_resource_id
215
+ <%_ } else { _%>
216
+ parent_id = aws_api_gateway_resource.path_depth_<%- depth - 1 %>[each.value.parent].id
217
+ <%_ } _%>
218
+ path_part = each.value.part
219
+ }
220
+
221
+ <%_ } _%>
222
+ <%_ } _%>
134
223
  # Resources
135
224
 
136
225
  # Create Lambda ZIP file from the bundle directory
@@ -205,8 +294,14 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
205
294
  description = "Allow outbound HTTPS to AWS service endpoints"
206
295
  }
207
296
 
208
- # Lambda function
209
297
  resource "aws_lambda_function" "api_lambda" {
298
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
299
+ # One lambda function per operation, each serving just that operation
300
+ for_each = local.operations
301
+
302
+ <%_ } else { _%>
303
+ # A single "router" lambda serving all requests
304
+ <%_ } _%>
210
305
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
211
306
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
212
307
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -215,8 +310,13 @@ resource "aws_lambda_function" "api_lambda" {
215
310
  s3_bucket = aws_s3_object.lambda_zip.bucket
216
311
  s3_key = aws_s3_object.lambda_zip.key
217
312
  s3_object_version = aws_s3_object.lambda_zip.version_id
313
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
314
+ function_name = local.function_name[each.key]
315
+ role = aws_iam_role.lambda_execution_role[each.key].arn
316
+ <%_ } else { _%>
218
317
  function_name = "<%- apiNameClassName %>Handler-${random_string.suffix.result}"
219
318
  role = aws_iam_role.lambda_execution_role.arn
319
+ <%_ } _%>
220
320
  <%_ if (['trpc', 'smithy'].includes(backend.type)) { _%>
221
321
  handler = "index.handler"
222
322
  runtime = "nodejs22.x"
@@ -272,7 +372,13 @@ resource "aws_lambda_function" "api_lambda" {
272
372
 
273
373
  # IAM role for Lambda execution
274
374
  resource "aws_iam_role" "lambda_execution_role" {
375
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
376
+ for_each = local.operations
377
+
378
+ name = local.role_name[each.key]
379
+ <%_ } else { _%>
275
380
  name = "<%- apiNameClassName %>Handler-execution-role-${random_string.suffix.result}"
381
+ <%_ } _%>
276
382
 
277
383
  assume_role_policy = jsonencode({
278
384
  Version = "2012-10-17"
@@ -292,25 +398,57 @@ resource "aws_iam_role" "lambda_execution_role" {
292
398
 
293
399
  # Attach basic execution policy to Lambda role
294
400
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
401
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
402
+ for_each = local.operations
403
+
404
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
405
+ role = aws_iam_role.lambda_execution_role[each.key].name
406
+ <%_ } else { _%>
295
407
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
296
408
  role = aws_iam_role.lambda_execution_role.name
409
+ <%_ } _%>
297
410
  }
298
411
 
299
412
  # Attach X-Ray tracing policy to Lambda role
300
413
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
414
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
415
+ for_each = local.operations
416
+
417
+ policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
418
+ role = aws_iam_role.lambda_execution_role[each.key].name
419
+ <%_ } else { _%>
301
420
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
302
421
  role = aws_iam_role.lambda_execution_role.name
422
+ <%_ } _%>
303
423
  }
304
424
 
305
425
  # Attach VPC access policy to Lambda role when deployed into a VPC
306
426
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
427
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
428
+ for_each = var.enable_vpc ? local.operations : {}
429
+
430
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
431
+ role = aws_iam_role.lambda_execution_role[each.key].name
432
+ <%_ } else { _%>
307
433
  count = var.enable_vpc ? 1 : 0
308
434
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
309
435
  role = aws_iam_role.lambda_execution_role.name
436
+ <%_ } _%>
310
437
  }
311
438
 
312
439
  # Additional IAM policies for Lambda (if provided)
313
440
  resource "aws_iam_role_policy" "lambda_additional_policies" {
441
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
442
+ for_each = local.operations
443
+
444
+ name = "${substr(local.function_name[each.key], 0, 55)}-policies"
445
+ role = aws_iam_role.lambda_execution_role[each.key].id
446
+
447
+ policy = jsonencode({
448
+ Version = "2012-10-17"
449
+ Statement = local.lambda_policy_document_statements
450
+ })
451
+ <%_ } else { _%>
314
452
  count = length(local.lambda_policy_statements) > 0 ? 1 : 0
315
453
  name = "<%- apiNameClassName %>Handler-additional-policies-${random_string.suffix.result}"
316
454
  role = aws_iam_role.lambda_execution_role.id
@@ -319,6 +457,7 @@ resource "aws_iam_role_policy" "lambda_additional_policies" {
319
457
  Version = "2012-10-17"
320
458
  Statement = local.lambda_policy_statements
321
459
  })
460
+ <%_ } _%>
322
461
  }
323
462
 
324
463
  locals {
@@ -333,6 +472,17 @@ locals {
333
472
  Resource = ["${var.appconfig_application_arn}/*"]
334
473
  }
335
474
  ] : [], var.additional_iam_policy_statements)
475
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
476
+
477
+ # IAM rejects an empty statement list, so fall back to a no-op deny
478
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
479
+ {
480
+ Effect = "Deny"
481
+ Action = ["appconfig:GetLatestConfiguration"]
482
+ Resource = ["arn:aws:appconfig:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:application/none"]
483
+ }
484
+ ]
485
+ <%_ } _%>
336
486
  }
337
487
 
338
488
  # CloudWatch Log Group for Lambda
@@ -340,16 +490,31 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
340
490
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
341
491
  #checkov:skip=CKV_AWS_338:Log retention set to forever
342
492
  #checkov:skip=CKV_AWS_66:Log retention set to forever
493
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
494
+ for_each = local.operations
495
+
496
+ name = "/aws/lambda/${local.function_name[each.key]}"
497
+ tags = var.tags
498
+ <%_ } else { _%>
343
499
  name = "/aws/lambda/<%- apiNameClassName %>Handler-${random_string.suffix.result}"
344
500
  tags = var.tags
501
+ <%_ } _%>
345
502
  }
346
503
 
347
504
  <%_ if (backend.type === 'fastapi') { _%>
348
505
  # Lambda alias pointing to the latest published version for SnapStart
349
506
  resource "aws_lambda_alias" "live" {
507
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
508
+ for_each = local.operations
509
+
510
+ name = "live"
511
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
512
+ function_version = aws_lambda_function.api_lambda[each.key].version
513
+ <%_ } else { _%>
350
514
  name = "live"
351
515
  function_name = aws_lambda_function.api_lambda.function_name
352
516
  function_version = aws_lambda_function.api_lambda.version
517
+ <%_ } _%>
353
518
 
354
519
  depends_on = [aws_lambda_function.api_lambda]
355
520
  }
@@ -472,6 +637,161 @@ resource "aws_lambda_permission" "authorizer_invoke" {
472
637
  }
473
638
  <%_ } _%>
474
639
 
640
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
641
+ # Method per operation
642
+ resource "aws_api_gateway_method" "operation_methods" {
643
+ #checkov:skip=CKV2_AWS_53:Request validation not required for proxy integration as Lambda handles validation
644
+ for_each = local.operations
645
+
646
+ rest_api_id = module.rest_api.api_id
647
+ resource_id = local.operation_resource_id[each.key]
648
+ http_method = upper(each.value.method)
649
+
650
+ <%_ if (auth === 'iam') { _%>
651
+ authorization = "AWS_IAM"
652
+ <%_ } else if (auth === 'cognito') { _%>
653
+ authorization = "COGNITO_USER_POOLS"
654
+ authorizer_id = aws_api_gateway_authorizer.cognito_authorizer.id
655
+ # Accept access tokens from both sign-in flows: 'openid' (Cognito hosted UI)
656
+ # and 'aws.cognito.signin.user.admin' (Cognito admin/SRP auth APIs).
657
+ authorization_scopes = ["openid", "aws.cognito.signin.user.admin"]
658
+ <%_ } else if (auth === 'custom') { _%>
659
+ authorization = "CUSTOM"
660
+ authorizer_id = aws_api_gateway_authorizer.custom_authorizer.id
661
+ <%_ } _%>
662
+
663
+ # Path parameters must be declared on the method
664
+ request_parameters = {
665
+ for segment in local.operation_segments[each.key] :
666
+ "method.request.path.${trimsuffix(trimprefix(segment, "{"), "}")}" => true
667
+ if startswith(segment, "{")
668
+ }
669
+
670
+ depends_on = [<% if (auth === 'cognito') { %>aws_api_gateway_authorizer.cognito_authorizer<% } else if (auth === 'custom') { %>aws_api_gateway_authorizer.custom_authorizer<% } %>]
671
+ }
672
+
673
+ # Lambda integration per operation
674
+ resource "aws_api_gateway_integration" "lambda_integration" {
675
+ for_each = local.operations
676
+
677
+ rest_api_id = module.rest_api.api_id
678
+ resource_id = local.operation_resource_id[each.key]
679
+ http_method = aws_api_gateway_method.operation_methods[each.key].http_method
680
+
681
+ integration_http_method = "POST"
682
+ type = "AWS_PROXY"
683
+ <%_ if (backend.type === 'fastapi') { _%>
684
+ # Use the response streaming invocation path with the alias ARN for SnapStart
685
+ uri = "arn:aws:apigateway:${data.aws_region.current.region}:lambda:path/2021-11-15/functions/${aws_lambda_alias.live[each.key].arn}/response-streaming-invocations"
686
+ response_transfer_mode = "STREAM"
687
+ <%_ } else if (backend.type === 'trpc') { _%>
688
+ uri = aws_lambda_function.api_lambda[each.key].response_streaming_invoke_arn
689
+ response_transfer_mode = "STREAM"
690
+ <%_ } else { _%>
691
+ uri = aws_lambda_function.api_lambda[each.key].invoke_arn
692
+ <%_ } _%>
693
+
694
+ <%_ if (backend.type === 'fastapi') { _%>
695
+ depends_on = [aws_lambda_alias.live]
696
+ <%_ } else { _%>
697
+ depends_on = [aws_lambda_function.api_lambda]
698
+ <%_ } _%>
699
+ }
700
+
701
+ # OPTIONS method for CORS preflight, on every resource an operation is served from
702
+ resource "aws_api_gateway_method" "options_method" {
703
+ #checkov:skip=CKV2_AWS_70:OPTIONS method must be unauthenticated for CORS preflight requests
704
+ #checkov:skip=CKV2_AWS_53:Request validation not required for OPTIONS CORS preflight method
705
+ for_each = local.resource_ids
706
+
707
+ rest_api_id = module.rest_api.api_id
708
+ resource_id = each.value
709
+ http_method = "OPTIONS"
710
+ authorization = "NONE"
711
+ }
712
+
713
+ # CORS integration for OPTIONS methods
714
+ resource "aws_api_gateway_integration" "options_integration" {
715
+ for_each = local.resource_ids
716
+
717
+ rest_api_id = module.rest_api.api_id
718
+ resource_id = each.value
719
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
720
+
721
+ type = "MOCK"
722
+ request_templates = {
723
+ "application/json" = "{\"statusCode\": 204}"
724
+ }
725
+ }
726
+
727
+ # OPTIONS method responses
728
+ resource "aws_api_gateway_method_response" "options_response" {
729
+ for_each = local.resource_ids
730
+
731
+ rest_api_id = module.rest_api.api_id
732
+ resource_id = each.value
733
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
734
+ status_code = "204"
735
+
736
+ response_parameters = {
737
+ "method.response.header.Access-Control-Allow-Headers" = true
738
+ "method.response.header.Access-Control-Allow-Methods" = true
739
+ "method.response.header.Access-Control-Allow-Origin" = true
740
+ }
741
+ }
742
+
743
+ # OPTIONS integration responses
744
+ resource "aws_api_gateway_integration_response" "options_integration_response" {
745
+ for_each = local.resource_ids
746
+
747
+ rest_api_id = module.rest_api.api_id
748
+ resource_id = each.value
749
+ http_method = aws_api_gateway_method.options_method[each.key].http_method
750
+ status_code = aws_api_gateway_method_response.options_response[each.key].status_code
751
+
752
+ response_parameters = {
753
+ "method.response.header.Access-Control-Allow-Headers" = "'${join(",", var.cors_allow_headers)}'"
754
+ "method.response.header.Access-Control-Allow-Methods" = "'${join(",", var.cors_allow_methods)}'"
755
+ "method.response.header.Access-Control-Allow-Origin" = "'${join(",", var.cors_allow_origins)}'"
756
+ }
757
+
758
+ depends_on = [aws_api_gateway_integration.options_integration]
759
+ }
760
+
761
+ # API Gateway deployment
762
+ resource "aws_api_gateway_deployment" "api_deployment" {
763
+ rest_api_id = module.rest_api.api_id
764
+
765
+ triggers = {
766
+ redeployment = sha1(jsonencode(concat(
767
+ [for r in aws_api_gateway_resource.path_depth_0 : r.id],
768
+ <%_ for (let depth = 1; depth < maxRestPathDepth; depth++) { _%>
769
+ [for r in aws_api_gateway_resource.path_depth_<%- depth %> : r.id],
770
+ <%_ } _%>
771
+ [for m in aws_api_gateway_method.operation_methods : m.id],
772
+ [for i in aws_api_gateway_integration.lambda_integration : i.id],
773
+ [for m in aws_api_gateway_method.options_method : m.id],
774
+ [for i in aws_api_gateway_integration.options_integration : i.id],
775
+ )))
776
+ }
777
+
778
+ lifecycle {
779
+ create_before_destroy = true
780
+ }
781
+
782
+ depends_on = [
783
+ aws_api_gateway_method.operation_methods,
784
+ aws_api_gateway_integration.lambda_integration,
785
+ aws_api_gateway_method.options_method,
786
+ aws_api_gateway_integration.options_integration,
787
+ aws_api_gateway_method_response.options_response,
788
+ aws_api_gateway_integration_response.options_integration_response,
789
+ <%_ if (auth === 'custom') { _%>
790
+ aws_api_gateway_authorizer.custom_authorizer,
791
+ <%_ } _%>
792
+ ]
793
+ }
794
+ <%_ } else { _%>
475
795
  # Create proxy resource (captures all paths)
476
796
  resource "aws_api_gateway_resource" "proxy_resource" {
477
797
  rest_api_id = module.rest_api.api_id
@@ -613,6 +933,8 @@ resource "aws_api_gateway_deployment" "api_deployment" {
613
933
  ]
614
934
  }
615
935
 
936
+ <%_ } _%>
937
+
616
938
  # KMS key for encrypting access logs at rest
617
939
  resource "aws_kms_key" "access_logs" {
618
940
  description = "<%- apiNameClassName %> API access log encryption"
@@ -757,6 +1079,18 @@ resource "aws_api_gateway_rest_api_policy" "api_policy" {
757
1079
  # Lambda permission for API Gateway to invoke the function
758
1080
  <%_ } _%>
759
1081
  resource "aws_lambda_permission" "api_gateway_invoke" {
1082
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
1083
+ for_each = local.operations
1084
+
1085
+ statement_id = "AllowExecutionFromAPIGateway"
1086
+ action = "lambda:InvokeFunction"
1087
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
1088
+ <%_ if (backend.type === 'fastapi') { _%>
1089
+ qualifier = aws_lambda_alias.live[each.key].name
1090
+ <%_ } _%>
1091
+ principal = "apigateway.amazonaws.com"
1092
+ source_arn = "${module.rest_api.api_execution_arn}/*/${local.permission_source_suffix[each.key]}"
1093
+ <%_ } else { _%>
760
1094
  statement_id = "AllowExecutionFromAPIGateway"
761
1095
  action = "lambda:InvokeFunction"
762
1096
  function_name = aws_lambda_function.api_lambda.function_name
@@ -765,6 +1099,7 @@ resource "aws_lambda_permission" "api_gateway_invoke" {
765
1099
  <%_ } _%>
766
1100
  principal = "apigateway.amazonaws.com"
767
1101
  source_arn = "${module.rest_api.api_execution_arn}/*/*"
1102
+ <%_ } _%>
768
1103
 
769
1104
  <%_ if (backend.type === 'fastapi') { _%>
770
1105
  depends_on = [module.rest_api, aws_lambda_alias.live]
@@ -776,12 +1111,23 @@ resource "aws_lambda_permission" "api_gateway_invoke" {
776
1111
  <%_ if (backend.type === 'fastapi') { _%>
777
1112
  # Lambda permission for API Gateway to invoke with response streaming via alias
778
1113
  resource "aws_lambda_permission" "api_gateway_invoke_streaming" {
1114
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
1115
+ for_each = local.operations
1116
+
1117
+ statement_id = "AllowStreamingFromAPIGateway"
1118
+ action = "lambda:InvokeFunctionWithResponseStream"
1119
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
1120
+ qualifier = aws_lambda_alias.live[each.key].name
1121
+ principal = "apigateway.amazonaws.com"
1122
+ source_arn = "${module.rest_api.api_execution_arn}/*/${local.permission_source_suffix[each.key]}"
1123
+ <%_ } else { _%>
779
1124
  statement_id = "AllowStreamingFromAPIGateway"
780
1125
  action = "lambda:InvokeFunctionWithResponseStream"
781
1126
  function_name = aws_lambda_function.api_lambda.function_name
782
1127
  qualifier = aws_lambda_alias.live.name
783
1128
  principal = "apigateway.amazonaws.com"
784
1129
  source_arn = "${module.rest_api.api_execution_arn}/*/*"
1130
+ <%_ } _%>
785
1131
 
786
1132
  depends_on = [module.rest_api, aws_lambda_alias.live]
787
1133
  }
@@ -846,6 +1192,86 @@ output "stage_id" {
846
1192
  value = aws_api_gateway_stage.api_stage.id
847
1193
  }
848
1194
 
1195
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
1196
+ # Lambda Function Outputs, keyed by operation name
1197
+ output "operations" {
1198
+ description = "Names of the API operations, each with its own Lambda function"
1199
+ value = keys(local.operations)
1200
+ }
1201
+
1202
+ output "lambda_function_names" {
1203
+ description = "Name of each operation's Lambda function, keyed by operation name"
1204
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
1205
+ }
1206
+
1207
+ output "lambda_function_arns" {
1208
+ description = "ARN of each operation's Lambda function, keyed by operation name"
1209
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
1210
+ }
1211
+
1212
+ output "lambda_invoke_arns" {
1213
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
1214
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
1215
+ }
1216
+
1217
+ output "lambda_qualified_arns" {
1218
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
1219
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
1220
+ }
1221
+
1222
+ output "lambda_versions" {
1223
+ description = "Version of each operation's Lambda function, keyed by operation name"
1224
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
1225
+ }
1226
+
1227
+ output "lambda_source_code_hash" {
1228
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
1229
+ value = data.archive_file.lambda_zip.output_base64sha256
1230
+ }
1231
+
1232
+ # IAM Role Outputs, keyed by operation name
1233
+ output "lambda_execution_role_arns" {
1234
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
1235
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
1236
+ }
1237
+
1238
+ output "lambda_execution_role_names" {
1239
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
1240
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
1241
+ }
1242
+
1243
+ output "security_group_id" {
1244
+ description = "Security group ID shared by the API Lambda functions, for use in ingress rules on resources they must reach (e.g. a database). Null unless enable_vpc is true."
1245
+ value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
1246
+ }
1247
+
1248
+ # Integration Outputs, keyed by operation name
1249
+ output "integration_ids" {
1250
+ description = "ID of each operation's Lambda integration, keyed by operation name"
1251
+ value = { for op, i in aws_api_gateway_integration.lambda_integration : op => i.id }
1252
+ }
1253
+
1254
+ output "method_ids" {
1255
+ description = "ID of each operation's API Gateway method, keyed by operation name"
1256
+ value = { for op, m in aws_api_gateway_method.operation_methods : op => m.id }
1257
+ }
1258
+
1259
+ output "resource_ids" {
1260
+ description = "ID of each API Gateway resource, keyed by its path"
1261
+ value = local.resource_ids
1262
+ }
1263
+
1264
+ # CloudWatch Log Groups, keyed by operation name
1265
+ output "lambda_log_group_names" {
1266
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
1267
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
1268
+ }
1269
+
1270
+ output "lambda_log_group_arns" {
1271
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
1272
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
1273
+ }
1274
+ <%_ } else { _%>
849
1275
  # Lambda Function Outputs
850
1276
  output "lambda_function_name" {
851
1277
  description = "Name of the Lambda function"
@@ -924,3 +1350,4 @@ output "lambda_log_group_arn" {
924
1350
  description = "ARN of the Lambda CloudWatch log group"
925
1351
  value = aws_cloudwatch_log_group.lambda_logs.arn
926
1352
  }
1353
+ <%_ } _%>
@@ -9,9 +9,26 @@ export interface AddOpenApiMetadataGenerateTargetOptions {
9
9
  apiNameKebabCase: string;
10
10
  specPath: string;
11
11
  specBuildTargetName: string;
12
+ /**
13
+ * How integrations are mapped to operations. Terraform only needs the
14
+ * operations metadata for the `isolated` pattern, since the `shared` pattern
15
+ * routes every operation to one proxy Lambda.
16
+ */
17
+ integrationPattern?: 'isolated' | 'shared';
12
18
  }
13
19
  /**
14
- * Adds a generate:<api-name>-metadata target to shared constructs (CDK only)
15
- * This allows for API CDK constructs to have type-safety for integrations based on an OpenAPI Specification
20
+ * Adds a target which generates metadata about the API's operations from its
21
+ * OpenAPI specification, so that infrastructure can define an integration per
22
+ * operation.
23
+ *
24
+ * For CDK this generates TypeScript, giving the construct type-safe integrations.
25
+ * For Terraform this generates JSON, which the vended module reads to build one
26
+ * Lambda function and route per operation.
16
27
  */
17
- export declare const addSharedConstructsOpenApiMetadataGenerateTarget: (tree: Tree, { iac, apiNameKebabCase, specPath, specBuildTargetName, }: AddOpenApiMetadataGenerateTargetOptions) => void;
28
+ export declare const addSharedConstructsOpenApiMetadataGenerateTarget: (tree: Tree, options: AddOpenApiMetadataGenerateTargetOptions) => void;
29
+ /**
30
+ * Directory the operations metadata for an API is generated into, relative to
31
+ * the shared Terraform project's `src`. The vended module reads the file from
32
+ * here, so the module template and this target must agree on the location.
33
+ */
34
+ export declare const terraformOperationsMetadataDir: (apiNameKebabCase: string) => string;
@@ -4,16 +4,23 @@
4
4
  */ import { joinPathFragments, updateJson } from "@nx/devkit";
5
5
  import { updateGitIgnore } from "../git.js";
6
6
  import { addDependencyToTargetIfNotPresent } from "../nx.js";
7
- import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR } from "../shared-constructs-constants.js";
7
+ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, SHARED_TERRAFORM_DIR } from "../shared-constructs-constants.js";
8
8
  /**
9
- * Adds a generate:<api-name>-metadata target to shared constructs (CDK only)
10
- * This allows for API CDK constructs to have type-safety for integrations based on an OpenAPI Specification
11
- */ export const addSharedConstructsOpenApiMetadataGenerateTarget = (tree, { iac, apiNameKebabCase, specPath, specBuildTargetName })=>{
12
- if (iac !== 'cdk') {
13
- // For Terraform, we do not support type-safe integration builders, rather only the single lambda
14
- // router pattern, and therefore do not need to add depenencies on metadata generation.
15
- return;
9
+ * Adds a target which generates metadata about the API's operations from its
10
+ * OpenAPI specification, so that infrastructure can define an integration per
11
+ * operation.
12
+ *
13
+ * For CDK this generates TypeScript, giving the construct type-safe integrations.
14
+ * For Terraform this generates JSON, which the vended module reads to build one
15
+ * Lambda function and route per operation.
16
+ */ export const addSharedConstructsOpenApiMetadataGenerateTarget = (tree, options)=>{
17
+ if (options.iac === 'cdk') {
18
+ addCdkOpenApiMetadataGenerateTarget(tree, options);
19
+ } else if (options.iac === 'terraform') {
20
+ addTerraformOpenApiOperationsGenerateTarget(tree, options);
16
21
  }
22
+ };
23
+ const addCdkOpenApiMetadataGenerateTarget = (tree, { apiNameKebabCase, specPath, specBuildTargetName })=>{
17
24
  const generatedMetadataDir = joinPathFragments('generated', apiNameKebabCase);
18
25
  const generatedMetadataDirFromRoot = joinPathFragments(joinPathFragments(PACKAGES_DIR, SHARED_CONSTRUCTS_DIR), 'src', generatedMetadataDir);
19
26
  updateJson(tree, joinPathFragments(PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, 'project.json'), (config)=>{
@@ -58,5 +65,54 @@ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR } from "../shared-constructs-consta
58
65
  joinPathFragments('src', generatedMetadataDir)
59
66
  ]);
60
67
  };
68
+ /**
69
+ * Directory the operations metadata for an API is generated into, relative to
70
+ * the shared Terraform project's `src`. The vended module reads the file from
71
+ * here, so the module template and this target must agree on the location.
72
+ */ export const terraformOperationsMetadataDir = (apiNameKebabCase)=>joinPathFragments('generated', apiNameKebabCase);
73
+ const addTerraformOpenApiOperationsGenerateTarget = (tree, { apiNameKebabCase, specPath, specBuildTargetName, integrationPattern })=>{
74
+ // The `shared` pattern routes every operation to a single proxy Lambda, so the
75
+ // module has no need for the operations metadata.
76
+ if (integrationPattern !== 'isolated') {
77
+ return;
78
+ }
79
+ const generatedDirFromRoot = joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'src', terraformOperationsMetadataDir(apiNameKebabCase));
80
+ updateJson(tree, joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'project.json'), (config)=>{
81
+ config.targets ??= {};
82
+ // Terraform reads the operations metadata with `file()`, so it must exist
83
+ // before plan; the target is wired into `build` below.
84
+ const operationsTargetName = `generate:${apiNameKebabCase}-operations`;
85
+ if (!config.targets[operationsTargetName]) {
86
+ config.targets[operationsTargetName] = {
87
+ cache: true,
88
+ executor: 'nx:run-commands',
89
+ inputs: [
90
+ {
91
+ dependentTasksOutputFiles: '**/*.json'
92
+ }
93
+ ],
94
+ outputs: [
95
+ joinPathFragments('{workspaceRoot}', generatedDirFromRoot)
96
+ ],
97
+ options: {
98
+ commands: [
99
+ `nx g @aws/nx-plugin:open-api#json-metadata --openApiSpecPath="${specPath}" --outputPath="${generatedDirFromRoot}" --no-interactive`
100
+ ]
101
+ },
102
+ dependsOn: [
103
+ specBuildTargetName
104
+ ]
105
+ };
106
+ }
107
+ addDependencyToTargetIfNotPresent(config, 'build', operationsTargetName);
108
+ return config;
109
+ });
110
+ // Ignore the generated metadata by default
111
+ // Users can safely remove the entry from the .gitignore if they prefer to check it in
112
+ updateGitIgnore(tree, joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR), (patterns)=>[
113
+ ...patterns,
114
+ joinPathFragments('src', terraformOperationsMetadataDir(apiNameKebabCase))
115
+ ]);
116
+ };
61
117
 
62
118
  //# sourceMappingURL=open-api-metadata.js.map