@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
@@ -0,0 +1,40 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import { dirname } from 'path';
3
+ import { appRouter } from '../src/router<% if (esm) { %>.js<% } %>';
4
+
5
+ /**
6
+ * Flattens the router into operation names (dot notation for nested routers)
7
+ * with the path and method each procedure is served on.
8
+ */
9
+ const toOperations = (
10
+ router: any,
11
+ prefix = '',
12
+ ): Record<string, { path: string; method: string }> =>
13
+ Object.fromEntries(
14
+ Object.entries(router._def.procedures).flatMap(
15
+ ([name, procedureOrRouter]: [string, any]) => {
16
+ const fullPath = prefix ? `${prefix}.${name}` : name;
17
+ return procedureOrRouter._def?.router
18
+ ? Object.entries(toOperations(procedureOrRouter, fullPath))
19
+ : [
20
+ [
21
+ fullPath,
22
+ {
23
+ path: fullPath,
24
+ method:
25
+ procedureOrRouter._def.type === 'mutation' ? 'POST' : 'GET',
26
+ },
27
+ ],
28
+ ];
29
+ },
30
+ ),
31
+ );
32
+
33
+ // Sorted by operation name so the file is stable and does not churn the Terraform plan
34
+ const operations = Object.fromEntries(
35
+ Object.entries(toOperations(appRouter)).sort(([a], [b]) => a.localeCompare(b)),
36
+ );
37
+
38
+ const outputFile = process.argv[2];
39
+ mkdirSync(dirname(outputFile), { recursive: true });
40
+ writeFileSync(outputFile, `${JSON.stringify(operations, null, 2)}\n`);
@@ -5,6 +5,7 @@
5
5
  import tsProjectGenerator from "../../ts/lib/generator.js";
6
6
  import { addTsDependencies } from "../../utils/add-dependencies.js";
7
7
  import { API_CONSTRUCTS_DEPENDENCIES, API_CONSTRUCTS_PY_DEPENDENCIES, addApiGatewayInfra } from "../../utils/api-constructs/api-constructs.js";
8
+ import { addTrpcOperationsMetadataTarget } from "../../utils/api-constructs/trpc-operations-metadata.js";
8
9
  import { addTypeScriptBundleTarget, BUNDLE_DEPENDENCIES } from "../../utils/bundle/bundle.js";
9
10
  import { declareDependencies, ownedElsewhere } from "../../utils/declared-dependencies.js";
10
11
  import { formatFilesInSubtree } from "../../utils/format.js";
@@ -216,6 +217,15 @@ export async function tsTrpcApiGenerator(tree, options) {
216
217
  }, DEPENDENCIES);
217
218
  }
218
219
  addDependencyToTargetIfNotPresent(projectConfig, 'build', 'bundle');
220
+ // Terraform defines one Lambda function per operation from a generated
221
+ // metadata file; CDK derives the same information from the router's types.
222
+ if (iac === 'terraform' && getIntegrationPattern(options) === 'isolated') {
223
+ addTrpcOperationsMetadataTarget(tree, {
224
+ apiNameKebabCase,
225
+ project: projectConfig,
226
+ templateOptions: esmVars(tree)
227
+ });
228
+ }
219
229
  }
220
230
  projectConfig.targets = sortObjectKeys(projectConfig.targets);
221
231
  updateProjectConfiguration(tree, projectConfig.name, projectConfig);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../packages/nx-plugin/src/trpc/backend/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n generateFiles,\n joinPathFragments,\n OverwriteStrategy,\n type Tree,\n updateProjectConfiguration,\n} from '@nx/devkit';\nimport tsProjectGenerator from '../../ts/lib/generator.js';\nimport { addTsDependencies } from '../../utils/add-dependencies.js';\nimport {\n API_CONSTRUCTS_DEPENDENCIES,\n API_CONSTRUCTS_PY_DEPENDENCIES,\n addApiGatewayInfra,\n} from '../../utils/api-constructs/api-constructs.js';\nimport {\n addTypeScriptBundleTarget,\n BUNDLE_DEPENDENCIES,\n} from '../../utils/bundle/bundle.js';\nimport {\n declareDependencies,\n ownedElsewhere,\n} from '../../utils/declared-dependencies.js';\nimport { formatFilesInSubtree } from '../../utils/format.js';\nimport { resolveIac } from '../../utils/iac.js';\nimport { installDependencies } from '../../utils/install.js';\nimport { addGeneratorMetricsIfApplicable } from '../../utils/metrics.js';\nimport { esmVars } from '../../utils/module-format.js';\nimport { kebabCase, toClassName } from '../../utils/names.js';\nimport { getNpmScopePrefix } from '../../utils/npm-scope.js';\nimport {\n addDependencyToTargetIfNotPresent,\n addGeneratorMetadata,\n getGeneratorInfo,\n type NxGeneratorInfo,\n readProjectConfigurationUnqualified,\n} from '../../utils/nx.js';\nimport { sortObjectKeys } from '../../utils/object.js';\nimport { getPackageManagerDisplayCommands } from '../../utils/pkg-manager.js';\nimport { assignPort } from '../../utils/port.js';\nimport {\n SHARED_CONSTRUCTS_DEPENDENCIES,\n sharedConstructsGenerator,\n} from '../../utils/shared-constructs.js';\nimport type { IacMetadata } from '../../utils/shared-constructs-constants.js';\nimport type { TsTrpcApiGeneratorSchema } from './schema';\n\n/** The metadata this generator records, which its predicates read. */\nexport interface TsTrpcApiMetadata extends IacMetadata {\n readonly apiName: string;\n readonly apiType: string;\n readonly auth: TsTrpcApiGeneratorSchema['auth'];\n readonly infra: TsTrpcApiGeneratorSchema['infra'];\n readonly integrationPattern: 'isolated' | 'shared';\n}\n\n// Each entry names the branch it belongs to, so the same declaration drives both\n// adding and the version sync.\nexport const DEPENDENCIES = declareDependencies<TsTrpcApiMetadata>()({\n ts: [\n { name: 'aws-xray-sdk-core' },\n { name: 'zod' },\n { name: '@aws-lambda-powertools/logger' },\n { name: '@aws-lambda-powertools/metrics' },\n { name: '@aws-lambda-powertools/parameters' },\n { name: '@aws-lambda-powertools/tracer' },\n { name: '@aws-sdk/client-appconfigdata' },\n { name: '@trpc/server' },\n { name: '@trpc/client' },\n { name: 'aws4fetch' },\n { name: '@aws-sdk/credential-providers' },\n // The custom authorizer handler wraps itself with middy and parses its event.\n { name: '@middy/core', when: (m) => m.auth === 'custom' },\n { name: '@aws-lambda-powertools/parser', when: (m) => m.auth === 'custom' },\n { name: '@types/aws-lambda', dev: true },\n { name: 'cors', dev: true },\n { name: '@types/cors', dev: true },\n // tsx runs the local server from the workspace root.\n { name: 'tsx', dev: true, root: true },\n ...ownedElsewhere(API_CONSTRUCTS_DEPENDENCIES),\n ...ownedElsewhere(BUNDLE_DEPENDENCIES),\n ...ownedElsewhere(SHARED_CONSTRUCTS_DEPENDENCIES),\n ],\n py: ownedElsewhere(API_CONSTRUCTS_PY_DEPENDENCIES),\n});\n\nexport const TRPC_BACKEND_GENERATOR_INFO: NxGeneratorInfo = getGeneratorInfo(\n import.meta.filename,\n);\n\nconst VALID_TRPC_INTEGRATION_PERMUTATIONS = new Set([\n 'rest-lambda::isolated',\n 'rest-lambda::shared',\n 'http-lambda::isolated',\n 'http-lambda::shared',\n 'none::isolated',\n 'none::shared',\n]);\n\nexport async function tsTrpcApiGenerator(\n tree: Tree,\n options: TsTrpcApiGeneratorSchema,\n) {\n // Recorded in the metadata below so the version sync can tell a CDK\n // project from a Terraform one; undefined when no infrastructure was\n // generated, in which case neither provider's packages were added.\n const iac =\n options.infra !== 'none' ? await resolveIac(tree, options.iac) : undefined;\n\n if (options.infra !== 'none') {\n validateTrpcInfraAndIntegrationPatternCombination(options);\n }\n\n const apiNamespace = getNpmScopePrefix(tree);\n const apiNameKebabCase = kebabCase(options.name);\n const apiNameClassName = toClassName(options.name);\n\n const backendName = apiNameKebabCase;\n const backendProjectName = `${apiNamespace}${backendName}`;\n\n let projectExists: boolean;\n try {\n readProjectConfigurationUnqualified(tree, backendProjectName);\n projectExists = true;\n } catch {\n projectExists = false;\n }\n\n if (!projectExists) {\n await tsProjectGenerator(tree, {\n name: backendName,\n directory: options.directory,\n subDirectory: options.subDirectory,\n preferInstallDependencies: false,\n });\n }\n\n const projectConfig = readProjectConfigurationUnqualified(\n tree,\n backendProjectName,\n );\n const backendRoot = projectConfig.root;\n\n const port = assignPort(tree, projectConfig, 2022);\n\n const enhancedOptions = {\n backendProjectName,\n backendProjectAlias: backendProjectName,\n apiNameKebabCase,\n apiNameClassName,\n backendRoot,\n pkgMgrCmd: getPackageManagerDisplayCommands().exec,\n apiGatewayEventType: getApiGatewayEventType(options),\n port,\n ...options,\n ...esmVars(tree),\n };\n\n if (options.infra !== 'none') {\n await sharedConstructsGenerator(\n tree,\n {\n iac,\n },\n DEPENDENCIES,\n );\n\n await addApiGatewayInfra(\n tree,\n {\n apiProjectName: backendProjectName,\n apiNameClassName,\n apiNameKebabCase,\n constructType: options.infra === 'http-lambda' ? 'http' : 'rest',\n backend: {\n type: 'trpc',\n projectAlias: enhancedOptions.backendProjectAlias,\n bundleOutputDir: joinPathFragments('dist', backendRoot, 'bundle'),\n integrationPattern: getIntegrationPattern(options),\n ...(options.auth === 'custom' && {\n authorizerBundleOutputDir: joinPathFragments(\n 'dist',\n backendRoot,\n 'bundle',\n 'authorizer',\n ),\n }),\n },\n auth: options.auth,\n iac,\n },\n DEPENDENCIES,\n );\n }\n\n // Recorded on the project below and read by the declaration's predicates, so\n // the packages added here are exactly the ones the version sync will own.\n const metadata: TsTrpcApiMetadata = {\n apiName: options.name,\n apiType: 'trpc',\n auth: options.auth,\n infra: options.infra,\n integrationPattern: getIntegrationPattern(options),\n ...(iac ? { iac } : {}),\n };\n\n projectConfig.metadata = {\n ...projectConfig.metadata,\n ...metadata,\n } as unknown;\n\n projectConfig.targets.serve = {\n executor: 'nx:run-commands',\n continuous: true,\n options: {\n commands: ['tsx --watch src/local-server.ts'],\n cwd: '{projectRoot}',\n },\n };\n\n projectConfig.targets['dev'] = {\n ...projectConfig.targets['dev'],\n ...projectConfig.targets.serve,\n options: {\n ...projectConfig.targets.serve.options,\n env: {\n LOCAL_DEV: 'true',\n },\n },\n };\n\n if (options.infra !== 'none') {\n await addTypeScriptBundleTarget(\n tree,\n projectConfig,\n {\n targetFilePath: 'src/handler.ts',\n external: [/@aws-sdk\\/.*/], // lambda runtime provides aws sdk\n },\n DEPENDENCIES,\n );\n\n if (options.auth === 'custom') {\n await addTypeScriptBundleTarget(\n tree,\n projectConfig,\n {\n targetFilePath: 'src/authorizer.ts',\n bundleOutputDir: 'authorizer',\n external: [/@aws-sdk\\/.*/],\n },\n DEPENDENCIES,\n );\n }\n\n addDependencyToTargetIfNotPresent(projectConfig, 'build', 'bundle');\n }\n\n projectConfig.targets = sortObjectKeys(projectConfig.targets);\n\n updateProjectConfiguration(tree, projectConfig.name, projectConfig);\n\n generateFiles(\n tree,\n joinPathFragments(import.meta.dirname, 'files'),\n backendRoot,\n enhancedOptions,\n {\n overwriteStrategy: OverwriteStrategy.Overwrite,\n },\n );\n\n tree.delete(joinPathFragments(backendRoot, 'src', 'lib'));\n\n if (options.infra !== 'none' && options.auth === 'custom') {\n const authorizerType = options.infra === 'http-lambda' ? 'http' : 'rest';\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n '..',\n '..',\n 'utils',\n 'api-constructs',\n 'files',\n 'cdk',\n 'authorizer',\n authorizerType,\n ),\n joinPathFragments(backendRoot, 'src'),\n {},\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n }\n\n // Remove streaming schema helper for HTTP APIs (API Gateway HTTP API doesn't support streaming)\n if (options.infra !== 'rest-lambda' && options.infra !== 'none') {\n tree.delete(\n joinPathFragments(backendRoot, 'src', 'schema', 'z-async-iterable.ts'),\n );\n }\n\n addTsDependencies(tree, DEPENDENCIES, {\n metadata,\n projectRoot: backendRoot,\n });\n addGeneratorMetadata(\n tree,\n backendName,\n TRPC_BACKEND_GENERATOR_INFO,\n metadata,\n );\n\n await addGeneratorMetricsIfApplicable(tree, [TRPC_BACKEND_GENERATOR_INFO]);\n\n await formatFilesInSubtree(tree);\n return () =>\n installDependencies(tree, options.preferInstallDependencies, {\n languages: ['typescript'],\n });\n}\n\nconst validateTrpcInfraAndIntegrationPatternCombination = (\n options: TsTrpcApiGeneratorSchema,\n) => {\n const integrationPattern = getIntegrationPattern(options);\n const permutation = `${options.infra}::${integrationPattern}`;\n\n if (!VALID_TRPC_INTEGRATION_PERMUTATIONS.has(permutation)) {\n throw new Error(\n `Invalid tRPC infra/integrationPattern combination: ${options.infra} + ${integrationPattern}.`,\n );\n }\n};\n\nconst getIntegrationPattern = (\n options: TsTrpcApiGeneratorSchema,\n): 'isolated' | 'shared' => {\n return options.integrationPattern ?? 'isolated';\n};\n\nconst getApiGatewayEventType = (options: TsTrpcApiGeneratorSchema): string => {\n if (options.infra === 'rest-lambda') {\n return 'APIGatewayProxyEvent';\n }\n if (options.auth === 'iam') {\n return 'APIGatewayProxyEventV2WithIAMAuthorizer';\n } else if (options.auth === 'cognito') {\n return 'APIGatewayProxyEventV2WithJWTAuthorizer';\n }\n return 'APIGatewayProxyEventV2';\n};\n\nexport default tsTrpcApiGenerator;\n"],"names":["generateFiles","joinPathFragments","OverwriteStrategy","updateProjectConfiguration","tsProjectGenerator","addTsDependencies","API_CONSTRUCTS_DEPENDENCIES","API_CONSTRUCTS_PY_DEPENDENCIES","addApiGatewayInfra","addTypeScriptBundleTarget","BUNDLE_DEPENDENCIES","declareDependencies","ownedElsewhere","formatFilesInSubtree","resolveIac","installDependencies","addGeneratorMetricsIfApplicable","esmVars","kebabCase","toClassName","getNpmScopePrefix","addDependencyToTargetIfNotPresent","addGeneratorMetadata","getGeneratorInfo","readProjectConfigurationUnqualified","sortObjectKeys","getPackageManagerDisplayCommands","assignPort","SHARED_CONSTRUCTS_DEPENDENCIES","sharedConstructsGenerator","DEPENDENCIES","ts","name","when","m","auth","dev","root","py","TRPC_BACKEND_GENERATOR_INFO","filename","VALID_TRPC_INTEGRATION_PERMUTATIONS","Set","tsTrpcApiGenerator","tree","options","iac","infra","undefined","validateTrpcInfraAndIntegrationPatternCombination","apiNamespace","apiNameKebabCase","apiNameClassName","backendName","backendProjectName","projectExists","directory","subDirectory","preferInstallDependencies","projectConfig","backendRoot","port","enhancedOptions","backendProjectAlias","pkgMgrCmd","exec","apiGatewayEventType","getApiGatewayEventType","apiProjectName","constructType","backend","type","projectAlias","bundleOutputDir","integrationPattern","getIntegrationPattern","authorizerBundleOutputDir","metadata","apiName","apiType","targets","serve","executor","continuous","commands","cwd","env","LOCAL_DEV","targetFilePath","external","dirname","overwriteStrategy","Overwrite","delete","authorizerType","KeepExisting","projectRoot","languages","permutation","has","Error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../packages/nx-plugin/src/trpc/backend/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n generateFiles,\n joinPathFragments,\n OverwriteStrategy,\n type Tree,\n updateProjectConfiguration,\n} from '@nx/devkit';\nimport tsProjectGenerator from '../../ts/lib/generator.js';\nimport { addTsDependencies } from '../../utils/add-dependencies.js';\nimport {\n API_CONSTRUCTS_DEPENDENCIES,\n API_CONSTRUCTS_PY_DEPENDENCIES,\n addApiGatewayInfra,\n} from '../../utils/api-constructs/api-constructs.js';\nimport { addTrpcOperationsMetadataTarget } from '../../utils/api-constructs/trpc-operations-metadata.js';\nimport {\n addTypeScriptBundleTarget,\n BUNDLE_DEPENDENCIES,\n} from '../../utils/bundle/bundle.js';\nimport {\n declareDependencies,\n ownedElsewhere,\n} from '../../utils/declared-dependencies.js';\nimport { formatFilesInSubtree } from '../../utils/format.js';\nimport { resolveIac } from '../../utils/iac.js';\nimport { installDependencies } from '../../utils/install.js';\nimport { addGeneratorMetricsIfApplicable } from '../../utils/metrics.js';\nimport { esmVars } from '../../utils/module-format.js';\nimport { kebabCase, toClassName } from '../../utils/names.js';\nimport { getNpmScopePrefix } from '../../utils/npm-scope.js';\nimport {\n addDependencyToTargetIfNotPresent,\n addGeneratorMetadata,\n getGeneratorInfo,\n type NxGeneratorInfo,\n readProjectConfigurationUnqualified,\n} from '../../utils/nx.js';\nimport { sortObjectKeys } from '../../utils/object.js';\nimport { getPackageManagerDisplayCommands } from '../../utils/pkg-manager.js';\nimport { assignPort } from '../../utils/port.js';\nimport {\n SHARED_CONSTRUCTS_DEPENDENCIES,\n sharedConstructsGenerator,\n} from '../../utils/shared-constructs.js';\nimport type { IacMetadata } from '../../utils/shared-constructs-constants.js';\nimport type { TsTrpcApiGeneratorSchema } from './schema';\n\n/** The metadata this generator records, which its predicates read. */\nexport interface TsTrpcApiMetadata extends IacMetadata {\n readonly apiName: string;\n readonly apiType: string;\n readonly auth: TsTrpcApiGeneratorSchema['auth'];\n readonly infra: TsTrpcApiGeneratorSchema['infra'];\n readonly integrationPattern: 'isolated' | 'shared';\n}\n\n// Each entry names the branch it belongs to, so the same declaration drives both\n// adding and the version sync.\nexport const DEPENDENCIES = declareDependencies<TsTrpcApiMetadata>()({\n ts: [\n { name: 'aws-xray-sdk-core' },\n { name: 'zod' },\n { name: '@aws-lambda-powertools/logger' },\n { name: '@aws-lambda-powertools/metrics' },\n { name: '@aws-lambda-powertools/parameters' },\n { name: '@aws-lambda-powertools/tracer' },\n { name: '@aws-sdk/client-appconfigdata' },\n { name: '@trpc/server' },\n { name: '@trpc/client' },\n { name: 'aws4fetch' },\n { name: '@aws-sdk/credential-providers' },\n // The custom authorizer handler wraps itself with middy and parses its event.\n { name: '@middy/core', when: (m) => m.auth === 'custom' },\n { name: '@aws-lambda-powertools/parser', when: (m) => m.auth === 'custom' },\n { name: '@types/aws-lambda', dev: true },\n { name: 'cors', dev: true },\n { name: '@types/cors', dev: true },\n // tsx runs the local server from the workspace root.\n { name: 'tsx', dev: true, root: true },\n ...ownedElsewhere(API_CONSTRUCTS_DEPENDENCIES),\n ...ownedElsewhere(BUNDLE_DEPENDENCIES),\n ...ownedElsewhere(SHARED_CONSTRUCTS_DEPENDENCIES),\n ],\n py: ownedElsewhere(API_CONSTRUCTS_PY_DEPENDENCIES),\n});\n\nexport const TRPC_BACKEND_GENERATOR_INFO: NxGeneratorInfo = getGeneratorInfo(\n import.meta.filename,\n);\n\nconst VALID_TRPC_INTEGRATION_PERMUTATIONS = new Set([\n 'rest-lambda::isolated',\n 'rest-lambda::shared',\n 'http-lambda::isolated',\n 'http-lambda::shared',\n 'none::isolated',\n 'none::shared',\n]);\n\nexport async function tsTrpcApiGenerator(\n tree: Tree,\n options: TsTrpcApiGeneratorSchema,\n) {\n // Recorded in the metadata below so the version sync can tell a CDK\n // project from a Terraform one; undefined when no infrastructure was\n // generated, in which case neither provider's packages were added.\n const iac =\n options.infra !== 'none' ? await resolveIac(tree, options.iac) : undefined;\n\n if (options.infra !== 'none') {\n validateTrpcInfraAndIntegrationPatternCombination(options);\n }\n\n const apiNamespace = getNpmScopePrefix(tree);\n const apiNameKebabCase = kebabCase(options.name);\n const apiNameClassName = toClassName(options.name);\n\n const backendName = apiNameKebabCase;\n const backendProjectName = `${apiNamespace}${backendName}`;\n\n let projectExists: boolean;\n try {\n readProjectConfigurationUnqualified(tree, backendProjectName);\n projectExists = true;\n } catch {\n projectExists = false;\n }\n\n if (!projectExists) {\n await tsProjectGenerator(tree, {\n name: backendName,\n directory: options.directory,\n subDirectory: options.subDirectory,\n preferInstallDependencies: false,\n });\n }\n\n const projectConfig = readProjectConfigurationUnqualified(\n tree,\n backendProjectName,\n );\n const backendRoot = projectConfig.root;\n\n const port = assignPort(tree, projectConfig, 2022);\n\n const enhancedOptions = {\n backendProjectName,\n backendProjectAlias: backendProjectName,\n apiNameKebabCase,\n apiNameClassName,\n backendRoot,\n pkgMgrCmd: getPackageManagerDisplayCommands().exec,\n apiGatewayEventType: getApiGatewayEventType(options),\n port,\n ...options,\n ...esmVars(tree),\n };\n\n if (options.infra !== 'none') {\n await sharedConstructsGenerator(\n tree,\n {\n iac,\n },\n DEPENDENCIES,\n );\n\n await addApiGatewayInfra(\n tree,\n {\n apiProjectName: backendProjectName,\n apiNameClassName,\n apiNameKebabCase,\n constructType: options.infra === 'http-lambda' ? 'http' : 'rest',\n backend: {\n type: 'trpc',\n projectAlias: enhancedOptions.backendProjectAlias,\n bundleOutputDir: joinPathFragments('dist', backendRoot, 'bundle'),\n integrationPattern: getIntegrationPattern(options),\n ...(options.auth === 'custom' && {\n authorizerBundleOutputDir: joinPathFragments(\n 'dist',\n backendRoot,\n 'bundle',\n 'authorizer',\n ),\n }),\n },\n auth: options.auth,\n iac,\n },\n DEPENDENCIES,\n );\n }\n\n // Recorded on the project below and read by the declaration's predicates, so\n // the packages added here are exactly the ones the version sync will own.\n const metadata: TsTrpcApiMetadata = {\n apiName: options.name,\n apiType: 'trpc',\n auth: options.auth,\n infra: options.infra,\n integrationPattern: getIntegrationPattern(options),\n ...(iac ? { iac } : {}),\n };\n\n projectConfig.metadata = {\n ...projectConfig.metadata,\n ...metadata,\n } as unknown;\n\n projectConfig.targets.serve = {\n executor: 'nx:run-commands',\n continuous: true,\n options: {\n commands: ['tsx --watch src/local-server.ts'],\n cwd: '{projectRoot}',\n },\n };\n\n projectConfig.targets['dev'] = {\n ...projectConfig.targets['dev'],\n ...projectConfig.targets.serve,\n options: {\n ...projectConfig.targets.serve.options,\n env: {\n LOCAL_DEV: 'true',\n },\n },\n };\n\n if (options.infra !== 'none') {\n await addTypeScriptBundleTarget(\n tree,\n projectConfig,\n {\n targetFilePath: 'src/handler.ts',\n external: [/@aws-sdk\\/.*/], // lambda runtime provides aws sdk\n },\n DEPENDENCIES,\n );\n\n if (options.auth === 'custom') {\n await addTypeScriptBundleTarget(\n tree,\n projectConfig,\n {\n targetFilePath: 'src/authorizer.ts',\n bundleOutputDir: 'authorizer',\n external: [/@aws-sdk\\/.*/],\n },\n DEPENDENCIES,\n );\n }\n\n addDependencyToTargetIfNotPresent(projectConfig, 'build', 'bundle');\n\n // Terraform defines one Lambda function per operation from a generated\n // metadata file; CDK derives the same information from the router's types.\n if (iac === 'terraform' && getIntegrationPattern(options) === 'isolated') {\n addTrpcOperationsMetadataTarget(tree, {\n apiNameKebabCase,\n project: projectConfig,\n templateOptions: esmVars(tree),\n });\n }\n }\n\n projectConfig.targets = sortObjectKeys(projectConfig.targets);\n\n updateProjectConfiguration(tree, projectConfig.name, projectConfig);\n\n generateFiles(\n tree,\n joinPathFragments(import.meta.dirname, 'files'),\n backendRoot,\n enhancedOptions,\n {\n overwriteStrategy: OverwriteStrategy.Overwrite,\n },\n );\n\n tree.delete(joinPathFragments(backendRoot, 'src', 'lib'));\n\n if (options.infra !== 'none' && options.auth === 'custom') {\n const authorizerType = options.infra === 'http-lambda' ? 'http' : 'rest';\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n '..',\n '..',\n 'utils',\n 'api-constructs',\n 'files',\n 'cdk',\n 'authorizer',\n authorizerType,\n ),\n joinPathFragments(backendRoot, 'src'),\n {},\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n }\n\n // Remove streaming schema helper for HTTP APIs (API Gateway HTTP API doesn't support streaming)\n if (options.infra !== 'rest-lambda' && options.infra !== 'none') {\n tree.delete(\n joinPathFragments(backendRoot, 'src', 'schema', 'z-async-iterable.ts'),\n );\n }\n\n addTsDependencies(tree, DEPENDENCIES, {\n metadata,\n projectRoot: backendRoot,\n });\n addGeneratorMetadata(\n tree,\n backendName,\n TRPC_BACKEND_GENERATOR_INFO,\n metadata,\n );\n\n await addGeneratorMetricsIfApplicable(tree, [TRPC_BACKEND_GENERATOR_INFO]);\n\n await formatFilesInSubtree(tree);\n return () =>\n installDependencies(tree, options.preferInstallDependencies, {\n languages: ['typescript'],\n });\n}\n\nconst validateTrpcInfraAndIntegrationPatternCombination = (\n options: TsTrpcApiGeneratorSchema,\n) => {\n const integrationPattern = getIntegrationPattern(options);\n const permutation = `${options.infra}::${integrationPattern}`;\n\n if (!VALID_TRPC_INTEGRATION_PERMUTATIONS.has(permutation)) {\n throw new Error(\n `Invalid tRPC infra/integrationPattern combination: ${options.infra} + ${integrationPattern}.`,\n );\n }\n};\n\nconst getIntegrationPattern = (\n options: TsTrpcApiGeneratorSchema,\n): 'isolated' | 'shared' => {\n return options.integrationPattern ?? 'isolated';\n};\n\nconst getApiGatewayEventType = (options: TsTrpcApiGeneratorSchema): string => {\n if (options.infra === 'rest-lambda') {\n return 'APIGatewayProxyEvent';\n }\n if (options.auth === 'iam') {\n return 'APIGatewayProxyEventV2WithIAMAuthorizer';\n } else if (options.auth === 'cognito') {\n return 'APIGatewayProxyEventV2WithJWTAuthorizer';\n }\n return 'APIGatewayProxyEventV2';\n};\n\nexport default tsTrpcApiGenerator;\n"],"names":["generateFiles","joinPathFragments","OverwriteStrategy","updateProjectConfiguration","tsProjectGenerator","addTsDependencies","API_CONSTRUCTS_DEPENDENCIES","API_CONSTRUCTS_PY_DEPENDENCIES","addApiGatewayInfra","addTrpcOperationsMetadataTarget","addTypeScriptBundleTarget","BUNDLE_DEPENDENCIES","declareDependencies","ownedElsewhere","formatFilesInSubtree","resolveIac","installDependencies","addGeneratorMetricsIfApplicable","esmVars","kebabCase","toClassName","getNpmScopePrefix","addDependencyToTargetIfNotPresent","addGeneratorMetadata","getGeneratorInfo","readProjectConfigurationUnqualified","sortObjectKeys","getPackageManagerDisplayCommands","assignPort","SHARED_CONSTRUCTS_DEPENDENCIES","sharedConstructsGenerator","DEPENDENCIES","ts","name","when","m","auth","dev","root","py","TRPC_BACKEND_GENERATOR_INFO","filename","VALID_TRPC_INTEGRATION_PERMUTATIONS","Set","tsTrpcApiGenerator","tree","options","iac","infra","undefined","validateTrpcInfraAndIntegrationPatternCombination","apiNamespace","apiNameKebabCase","apiNameClassName","backendName","backendProjectName","projectExists","directory","subDirectory","preferInstallDependencies","projectConfig","backendRoot","port","enhancedOptions","backendProjectAlias","pkgMgrCmd","exec","apiGatewayEventType","getApiGatewayEventType","apiProjectName","constructType","backend","type","projectAlias","bundleOutputDir","integrationPattern","getIntegrationPattern","authorizerBundleOutputDir","metadata","apiName","apiType","targets","serve","executor","continuous","commands","cwd","env","LOCAL_DEV","targetFilePath","external","project","templateOptions","dirname","overwriteStrategy","Overwrite","delete","authorizerType","KeepExisting","projectRoot","languages","permutation","has","Error"],"mappings":"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"}
@@ -39,6 +39,16 @@ import { PY_VERSIONS, terraformProviderVersions, withVersions } from "../version
39
39
  when: generatedTerraform
40
40
  }
41
41
  ];
42
+ /**
43
+ * Path segments a REST API operation may have in the generated Terraform.
44
+ *
45
+ * API Gateway REST APIs need a resource per path segment, and instances of a
46
+ * Terraform resource cannot reference one another
47
+ * (https://github.com/hashicorp/terraform/issues/26697), so the module declares
48
+ * one resource per level up to this depth. Deeper paths fail the plan with a
49
+ * message pointing at the fix. Empty levels create no resources, so the only
50
+ * cost of the headroom is the generated configuration itself.
51
+ */ const MAX_REST_PATH_DEPTH = 16;
42
52
  export const addApiGatewayInfra = async (tree, options, declaration)=>{
43
53
  if (options.iac === 'cdk') {
44
54
  await addApiGatewayCdkConstructs(tree, options, declaration);
@@ -105,6 +115,7 @@ export const addApiGatewayInfra = async (tree, options, declaration)=>{
105
115
  // Generate app specific terraform module
106
116
  generateFiles(tree, joinPathFragments(import.meta.dirname, 'files', 'terraform', 'app', 'apis', options.constructType), joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'src', 'app', 'apis'), {
107
117
  ...options,
118
+ maxRestPathDepth: MAX_REST_PATH_DEPTH,
108
119
  ...terraformProviderVersions()
109
120
  }, {
110
121
  overwriteStrategy: OverwriteStrategy.KeepExisting
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/api-constructs/api-constructs.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n generateFiles,\n joinPathFragments,\n OverwriteStrategy,\n type ProjectConfiguration,\n type Tree,\n updateJson,\n} from '@nx/devkit';\nimport { addStarExport } from '../ast.js';\nimport {\n type DeclaredPyDependency,\n type DeclaredTsDependency,\n type DependencyDeclaration,\n forDependencies,\n type MustDeclare,\n} from '../declared-dependencies.js';\nimport { addDependenciesToPackageJson } from '../dependencies.js';\nimport type { Iac } from '../iac.js';\nimport { esmVars } from '../module-format.js';\nimport { addDependencyToTargetIfNotPresent } from '../nx.js';\nimport {\n generatedInfrastructure,\n generatedTerraform,\n type IacMetadata,\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../shared-constructs-constants.js';\nimport {\n type IPyDepVersion,\n type ITsDepVersion,\n PY_VERSIONS,\n terraformProviderVersions,\n withVersions,\n} from '../versions.js';\n\n/**\n * Dependencies a caller must declare to add API Gateway infrastructure.\n *\n * Gated on infrastructure having been generated: `addApiGatewayInfra` only runs\n * on that branch, so a project generated with `infra: 'none'` never receives\n * these.\n */\nexport const API_CONSTRUCTS_DEPENDENCIES = [\n { name: '@aws-sdk/client-api-gateway', when: generatedInfrastructure },\n { name: '@aws-sdk/client-iam', when: generatedInfrastructure },\n { name: '@trpc/server', when: generatedInfrastructure },\n] as const satisfies readonly DeclaredTsDependency<\n ITsDepVersion,\n IacMetadata\n>[];\n\n/**\n * Python version the generated Terraform pins in the account module's inline\n * `uv run --with` script. Nothing installs it, so it is declared for its version\n * alone, and only on the Terraform branch that writes the script.\n */\nexport const API_CONSTRUCTS_PY_DEPENDENCIES = [\n { name: 'boto3', when: generatedTerraform },\n] as const satisfies readonly DeclaredPyDependency<\n IPyDepVersion,\n IacMetadata\n>[];\n\ninterface BackendOptions {\n type: 'trpc' | 'fastapi' | 'smithy';\n integrationPattern: 'isolated' | 'shared';\n}\n\nexport interface TrpcBackendOptions extends BackendOptions {\n type: 'trpc';\n projectAlias: string;\n bundleOutputDir: string;\n authorizerBundleOutputDir?: string;\n}\n\nexport interface FastApiBackendOptions extends BackendOptions {\n type: 'fastapi';\n moduleName: string;\n bundleOutputDir: string;\n}\n\nexport interface SmithyBackendOptions extends BackendOptions {\n type: 'smithy';\n bundleOutputDir: string;\n authorizerBundleOutputDir?: string;\n}\n\nexport interface AddApiGatewayConstructOptions {\n apiProjectName: string;\n apiNameClassName: string;\n apiNameKebabCase: string;\n constructType: 'http' | 'rest';\n backend: TrpcBackendOptions | FastApiBackendOptions | SmithyBackendOptions;\n auth: 'iam' | 'cognito' | 'custom';\n}\n\nexport const addApiGatewayInfra = async <const D extends DependencyDeclaration>(\n tree: Tree,\n options: AddApiGatewayConstructOptions & { iac: Iac },\n declaration: D & MustDeclare<typeof API_CONSTRUCTS_DEPENDENCIES, D>,\n) => {\n if (options.iac === 'cdk') {\n await addApiGatewayCdkConstructs(tree, options, declaration);\n } else if (options.iac === 'terraform') {\n addApiGatewayTerraformModules(tree, options);\n } else {\n throw new Error(`Unsupported iac ${options.iac}`);\n }\n\n updateJson(\n tree,\n joinPathFragments(\n PACKAGES_DIR,\n options.iac === 'cdk' ? SHARED_CONSTRUCTS_DIR : SHARED_TERRAFORM_DIR,\n 'project.json',\n ),\n (config: ProjectConfiguration) => {\n addDependencyToTargetIfNotPresent(\n config,\n 'build',\n `${options.apiProjectName}:build`,\n );\n return config;\n },\n );\n};\n\n/**\n * Add an API CDK construct, and update the Runtime Config type to export its url\n */\nconst addApiGatewayCdkConstructs = async (\n tree: Tree,\n options: AddApiGatewayConstructOptions,\n declaration: DependencyDeclaration,\n) => {\n const generateCoreApiFile = (name: string) => {\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n 'files',\n 'cdk',\n 'core',\n 'api',\n name,\n ),\n joinPathFragments(\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n 'src',\n 'core',\n 'api',\n ),\n { ...esmVars(tree) },\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n };\n\n // Generate relevant core CDK construct and utilities\n generateCoreApiFile(options.constructType);\n generateCoreApiFile('utils');\n if (options.backend.type === 'trpc') {\n generateCoreApiFile('trpc');\n }\n\n // Declare the deps the generated core construct files import.\n const constructDeps: (typeof API_CONSTRUCTS_DEPENDENCIES)[number]['name'][] =\n [];\n if (options.constructType === 'rest') {\n // REST account construct configures the account via the AWS SDK.\n constructDeps.push('@aws-sdk/client-api-gateway', '@aws-sdk/client-iam');\n }\n if (options.backend.type === 'trpc') {\n // trpc-utils.ts types the router with @trpc/server.\n constructDeps.push('@trpc/server');\n }\n if (constructDeps.length > 0) {\n addDependenciesToPackageJson(\n tree,\n withVersions(\n forDependencies<typeof API_CONSTRUCTS_DEPENDENCIES>(declaration),\n constructDeps,\n ),\n {},\n joinPathFragments(PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, 'package.json'),\n );\n }\n\n // Generate app specific CDK construct\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n 'files',\n 'cdk',\n 'app',\n 'apis',\n options.constructType,\n ),\n joinPathFragments(\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n 'src',\n 'app',\n 'apis',\n ),\n { ...options, ...esmVars(tree) },\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n\n // Export app specific CDK construct\n await addStarExport(\n tree,\n joinPathFragments(\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n 'src',\n 'app',\n 'apis',\n 'index.ts',\n ),\n `./${options.apiNameKebabCase}.js`,\n );\n await addStarExport(\n tree,\n joinPathFragments(\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n 'src',\n 'app',\n 'index.ts',\n ),\n './apis/index.js',\n );\n};\n\n/**\n * Add an API terraform module, and update the Runtime Config type to export its url\n */\nconst addApiGatewayTerraformModules = (\n tree: Tree,\n options: AddApiGatewayConstructOptions,\n) => {\n // Generate core terraform module\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n 'files',\n 'terraform',\n 'core',\n 'api',\n options.constructType,\n ),\n joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'src', 'core', 'api'),\n { boto3Version: PY_VERSIONS.boto3, ...terraformProviderVersions() },\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n\n // Generate app specific terraform module\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n 'files',\n 'terraform',\n 'app',\n 'apis',\n options.constructType,\n ),\n joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'src', 'app', 'apis'),\n { ...options, ...terraformProviderVersions() },\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n};\n"],"names":["generateFiles","joinPathFragments","OverwriteStrategy","updateJson","addStarExport","forDependencies","addDependenciesToPackageJson","esmVars","addDependencyToTargetIfNotPresent","generatedInfrastructure","generatedTerraform","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","PY_VERSIONS","terraformProviderVersions","withVersions","API_CONSTRUCTS_DEPENDENCIES","name","when","API_CONSTRUCTS_PY_DEPENDENCIES","addApiGatewayInfra","tree","options","declaration","iac","addApiGatewayCdkConstructs","addApiGatewayTerraformModules","Error","config","apiProjectName","generateCoreApiFile","dirname","overwriteStrategy","KeepExisting","constructType","backend","type","constructDeps","push","length","apiNameKebabCase","boto3Version","boto3"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../packages/nx-plugin/src/utils/api-constructs/api-constructs.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n generateFiles,\n joinPathFragments,\n OverwriteStrategy,\n type ProjectConfiguration,\n type Tree,\n updateJson,\n} from '@nx/devkit';\nimport { addStarExport } from '../ast.js';\nimport {\n type DeclaredPyDependency,\n type DeclaredTsDependency,\n type DependencyDeclaration,\n forDependencies,\n type MustDeclare,\n} from '../declared-dependencies.js';\nimport { addDependenciesToPackageJson } from '../dependencies.js';\nimport type { Iac } from '../iac.js';\nimport { esmVars } from '../module-format.js';\nimport { addDependencyToTargetIfNotPresent } from '../nx.js';\nimport {\n generatedInfrastructure,\n generatedTerraform,\n type IacMetadata,\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../shared-constructs-constants.js';\nimport {\n type IPyDepVersion,\n type ITsDepVersion,\n PY_VERSIONS,\n terraformProviderVersions,\n withVersions,\n} from '../versions.js';\n\n/**\n * Dependencies a caller must declare to add API Gateway infrastructure.\n *\n * Gated on infrastructure having been generated: `addApiGatewayInfra` only runs\n * on that branch, so a project generated with `infra: 'none'` never receives\n * these.\n */\nexport const API_CONSTRUCTS_DEPENDENCIES = [\n { name: '@aws-sdk/client-api-gateway', when: generatedInfrastructure },\n { name: '@aws-sdk/client-iam', when: generatedInfrastructure },\n { name: '@trpc/server', when: generatedInfrastructure },\n] as const satisfies readonly DeclaredTsDependency<\n ITsDepVersion,\n IacMetadata\n>[];\n\n/**\n * Python version the generated Terraform pins in the account module's inline\n * `uv run --with` script. Nothing installs it, so it is declared for its version\n * alone, and only on the Terraform branch that writes the script.\n */\nexport const API_CONSTRUCTS_PY_DEPENDENCIES = [\n { name: 'boto3', when: generatedTerraform },\n] as const satisfies readonly DeclaredPyDependency<\n IPyDepVersion,\n IacMetadata\n>[];\n\n/**\n * Path segments a REST API operation may have in the generated Terraform.\n *\n * API Gateway REST APIs need a resource per path segment, and instances of a\n * Terraform resource cannot reference one another\n * (https://github.com/hashicorp/terraform/issues/26697), so the module declares\n * one resource per level up to this depth. Deeper paths fail the plan with a\n * message pointing at the fix. Empty levels create no resources, so the only\n * cost of the headroom is the generated configuration itself.\n */\nconst MAX_REST_PATH_DEPTH = 16;\n\ninterface BackendOptions {\n type: 'trpc' | 'fastapi' | 'smithy';\n integrationPattern: 'isolated' | 'shared';\n}\n\nexport interface TrpcBackendOptions extends BackendOptions {\n type: 'trpc';\n projectAlias: string;\n bundleOutputDir: string;\n authorizerBundleOutputDir?: string;\n}\n\nexport interface FastApiBackendOptions extends BackendOptions {\n type: 'fastapi';\n moduleName: string;\n bundleOutputDir: string;\n}\n\nexport interface SmithyBackendOptions extends BackendOptions {\n type: 'smithy';\n bundleOutputDir: string;\n authorizerBundleOutputDir?: string;\n}\n\nexport interface AddApiGatewayConstructOptions {\n apiProjectName: string;\n apiNameClassName: string;\n apiNameKebabCase: string;\n constructType: 'http' | 'rest';\n backend: TrpcBackendOptions | FastApiBackendOptions | SmithyBackendOptions;\n auth: 'iam' | 'cognito' | 'custom';\n}\n\nexport const addApiGatewayInfra = async <const D extends DependencyDeclaration>(\n tree: Tree,\n options: AddApiGatewayConstructOptions & { iac: Iac },\n declaration: D & MustDeclare<typeof API_CONSTRUCTS_DEPENDENCIES, D>,\n) => {\n if (options.iac === 'cdk') {\n await addApiGatewayCdkConstructs(tree, options, declaration);\n } else if (options.iac === 'terraform') {\n addApiGatewayTerraformModules(tree, options);\n } else {\n throw new Error(`Unsupported iac ${options.iac}`);\n }\n\n updateJson(\n tree,\n joinPathFragments(\n PACKAGES_DIR,\n options.iac === 'cdk' ? SHARED_CONSTRUCTS_DIR : SHARED_TERRAFORM_DIR,\n 'project.json',\n ),\n (config: ProjectConfiguration) => {\n addDependencyToTargetIfNotPresent(\n config,\n 'build',\n `${options.apiProjectName}:build`,\n );\n return config;\n },\n );\n};\n\n/**\n * Add an API CDK construct, and update the Runtime Config type to export its url\n */\nconst addApiGatewayCdkConstructs = async (\n tree: Tree,\n options: AddApiGatewayConstructOptions,\n declaration: DependencyDeclaration,\n) => {\n const generateCoreApiFile = (name: string) => {\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n 'files',\n 'cdk',\n 'core',\n 'api',\n name,\n ),\n joinPathFragments(\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n 'src',\n 'core',\n 'api',\n ),\n { ...esmVars(tree) },\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n };\n\n // Generate relevant core CDK construct and utilities\n generateCoreApiFile(options.constructType);\n generateCoreApiFile('utils');\n if (options.backend.type === 'trpc') {\n generateCoreApiFile('trpc');\n }\n\n // Declare the deps the generated core construct files import.\n const constructDeps: (typeof API_CONSTRUCTS_DEPENDENCIES)[number]['name'][] =\n [];\n if (options.constructType === 'rest') {\n // REST account construct configures the account via the AWS SDK.\n constructDeps.push('@aws-sdk/client-api-gateway', '@aws-sdk/client-iam');\n }\n if (options.backend.type === 'trpc') {\n // trpc-utils.ts types the router with @trpc/server.\n constructDeps.push('@trpc/server');\n }\n if (constructDeps.length > 0) {\n addDependenciesToPackageJson(\n tree,\n withVersions(\n forDependencies<typeof API_CONSTRUCTS_DEPENDENCIES>(declaration),\n constructDeps,\n ),\n {},\n joinPathFragments(PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, 'package.json'),\n );\n }\n\n // Generate app specific CDK construct\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n 'files',\n 'cdk',\n 'app',\n 'apis',\n options.constructType,\n ),\n joinPathFragments(\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n 'src',\n 'app',\n 'apis',\n ),\n { ...options, ...esmVars(tree) },\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n\n // Export app specific CDK construct\n await addStarExport(\n tree,\n joinPathFragments(\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n 'src',\n 'app',\n 'apis',\n 'index.ts',\n ),\n `./${options.apiNameKebabCase}.js`,\n );\n await addStarExport(\n tree,\n joinPathFragments(\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n 'src',\n 'app',\n 'index.ts',\n ),\n './apis/index.js',\n );\n};\n\n/**\n * Add an API terraform module, and update the Runtime Config type to export its url\n */\nconst addApiGatewayTerraformModules = (\n tree: Tree,\n options: AddApiGatewayConstructOptions,\n) => {\n // Generate core terraform module\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n 'files',\n 'terraform',\n 'core',\n 'api',\n options.constructType,\n ),\n joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'src', 'core', 'api'),\n { boto3Version: PY_VERSIONS.boto3, ...terraformProviderVersions() },\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n\n // Generate app specific terraform module\n generateFiles(\n tree,\n joinPathFragments(\n import.meta.dirname,\n 'files',\n 'terraform',\n 'app',\n 'apis',\n options.constructType,\n ),\n joinPathFragments(PACKAGES_DIR, SHARED_TERRAFORM_DIR, 'src', 'app', 'apis'),\n {\n ...options,\n maxRestPathDepth: MAX_REST_PATH_DEPTH,\n ...terraformProviderVersions(),\n },\n {\n overwriteStrategy: OverwriteStrategy.KeepExisting,\n },\n );\n};\n"],"names":["generateFiles","joinPathFragments","OverwriteStrategy","updateJson","addStarExport","forDependencies","addDependenciesToPackageJson","esmVars","addDependencyToTargetIfNotPresent","generatedInfrastructure","generatedTerraform","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","PY_VERSIONS","terraformProviderVersions","withVersions","API_CONSTRUCTS_DEPENDENCIES","name","when","API_CONSTRUCTS_PY_DEPENDENCIES","MAX_REST_PATH_DEPTH","addApiGatewayInfra","tree","options","declaration","iac","addApiGatewayCdkConstructs","addApiGatewayTerraformModules","Error","config","apiProjectName","generateCoreApiFile","dirname","overwriteStrategy","KeepExisting","constructType","backend","type","constructDeps","push","length","apiNameKebabCase","boto3Version","boto3","maxRestPathDepth"],"mappings":"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"}
@@ -135,6 +135,51 @@ resource "random_string" "suffix" {
135
135
  upper = false
136
136
  }
137
137
 
138
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
139
+ locals {
140
+ # Generated at build time from the API definition
141
+ operations_file = "${path.module}/../../../generated/<%- apiNameKebabCase %>/operations.json"
142
+ operations = fileexists(local.operations_file) ? jsondecode(file(local.operations_file)) : {}
143
+
144
+ operation_slug = { for op in keys(local.operations) : op => replace(op, "/[^a-zA-Z0-9-_]/", "-") }
145
+ operation_hash = { for op in keys(local.operations) : op => substr(sha256(op), 0, 8) }
146
+
147
+ function_name = { for op in keys(local.operations) : op =>
148
+ "${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}"
149
+ }
150
+ role_name = { for op in keys(local.operations) : op =>
151
+ "${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}"
152
+ }
153
+
154
+ operation_path = { for op, details in local.operations : op =>
155
+ startswith(details.path, "/") ? details.path : "/${details.path}"
156
+ }
157
+ route_key = { for op, details in local.operations : op =>
158
+ "${upper(details.method)} ${local.operation_path[op]}"
159
+ }
160
+
161
+ permission_source_suffix = { for op, details in local.operations : op =>
162
+ "${upper(details.method)}${replace(local.operation_path[op], "/{[^}]*}/", "*")}"
163
+ }
164
+ }
165
+
166
+ resource "terraform_data" "operations_metadata" {
167
+ # Fails the plan when the operations metadata has not been generated
168
+ input = local.operations_file
169
+
170
+ lifecycle {
171
+ precondition {
172
+ condition = fileexists(local.operations_file)
173
+ error_message = "Operations metadata not found at ${local.operations_file}. Build the <%- apiNameClassName %> project to generate it."
174
+ }
175
+ precondition {
176
+ condition = length(local.operations) > 0
177
+ error_message = "No operations found in ${local.operations_file}. The <%- apiNameClassName %> API must define at least one operation."
178
+ }
179
+ }
180
+ }
181
+ <%_ } _%>
182
+
138
183
  # Resources
139
184
 
140
185
  # Create Lambda ZIP file from the bundle directory
@@ -195,9 +240,14 @@ resource "aws_vpc_security_group_egress_rule" "api_lambda_https" {
195
240
  description = "Allow outbound HTTPS to AWS service endpoints"
196
241
  }
197
242
 
198
- # Lambda function
199
- # This configures a single "router" lambda to serve all requests
200
243
  resource "aws_lambda_function" "api_lambda" {
244
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
245
+ # One lambda function per operation, each serving just that operation
246
+ for_each = local.operations
247
+
248
+ <%_ } else { _%>
249
+ # A single "router" lambda serving all requests
250
+ <%_ } _%>
201
251
  #checkov:skip=CKV_AWS_117:Lambda function is optionally deployed into a VPC via vpc_id/subnet_ids; not required for this use case
202
252
  #checkov:skip=CKV_AWS_116:Dead Letter Queue not required for this simple API use case
203
253
  #checkov:skip=CKV_AWS_272:Code signing not required for this use case
@@ -206,8 +256,13 @@ resource "aws_lambda_function" "api_lambda" {
206
256
  s3_bucket = aws_s3_object.lambda_zip.bucket
207
257
  s3_key = aws_s3_object.lambda_zip.key
208
258
  s3_object_version = aws_s3_object.lambda_zip.version_id
259
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
260
+ function_name = local.function_name[each.key]
261
+ role = aws_iam_role.lambda_execution_role[each.key].arn
262
+ <%_ } else { _%>
209
263
  function_name = "<%- apiNameClassName %>Handler-${random_string.suffix.result}"
210
264
  role = aws_iam_role.lambda_execution_role.arn
265
+ <%_ } _%>
211
266
  <%_ if (['trpc', 'smithy'].includes(backend.type)) { _%>
212
267
  handler = "index.handler"
213
268
  runtime = "nodejs22.x"
@@ -263,7 +318,13 @@ resource "aws_lambda_function" "api_lambda" {
263
318
 
264
319
  # IAM role for Lambda execution
265
320
  resource "aws_iam_role" "lambda_execution_role" {
321
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
322
+ for_each = local.operations
323
+
324
+ name = local.role_name[each.key]
325
+ <%_ } else { _%>
266
326
  name = "<%- apiNameClassName %>Handler-execution-role-${random_string.suffix.result}"
327
+ <%_ } _%>
267
328
 
268
329
  assume_role_policy = jsonencode({
269
330
  Version = "2012-10-17"
@@ -283,25 +344,57 @@ resource "aws_iam_role" "lambda_execution_role" {
283
344
 
284
345
  # Attach basic execution policy to Lambda role
285
346
  resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
347
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
348
+ for_each = local.operations
349
+
350
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
351
+ role = aws_iam_role.lambda_execution_role[each.key].name
352
+ <%_ } else { _%>
286
353
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
287
354
  role = aws_iam_role.lambda_execution_role.name
355
+ <%_ } _%>
288
356
  }
289
357
 
290
358
  # Attach X-Ray tracing policy to Lambda role
291
359
  resource "aws_iam_role_policy_attachment" "lambda_xray_execution" {
360
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
361
+ for_each = local.operations
362
+
363
+ policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
364
+ role = aws_iam_role.lambda_execution_role[each.key].name
365
+ <%_ } else { _%>
292
366
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
293
367
  role = aws_iam_role.lambda_execution_role.name
368
+ <%_ } _%>
294
369
  }
295
370
 
296
371
  # Attach VPC access policy to Lambda role when deployed into a VPC
297
372
  resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
373
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
374
+ for_each = var.enable_vpc ? local.operations : {}
375
+
376
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
377
+ role = aws_iam_role.lambda_execution_role[each.key].name
378
+ <%_ } else { _%>
298
379
  count = var.enable_vpc ? 1 : 0
299
380
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
300
381
  role = aws_iam_role.lambda_execution_role.name
382
+ <%_ } _%>
301
383
  }
302
384
 
303
385
  # Additional IAM policies for Lambda (if provided)
304
386
  resource "aws_iam_role_policy" "lambda_additional_policies" {
387
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
388
+ for_each = local.operations
389
+
390
+ name = "${substr(local.function_name[each.key], 0, 55)}-policies"
391
+ role = aws_iam_role.lambda_execution_role[each.key].id
392
+
393
+ policy = jsonencode({
394
+ Version = "2012-10-17"
395
+ Statement = local.lambda_policy_document_statements
396
+ })
397
+ <%_ } else { _%>
305
398
  count = length(local.lambda_policy_statements) > 0 ? 1 : 0
306
399
  name = "<%- apiNameClassName %>Handler-additional-policies-${random_string.suffix.result}"
307
400
  role = aws_iam_role.lambda_execution_role.id
@@ -310,6 +403,7 @@ resource "aws_iam_role_policy" "lambda_additional_policies" {
310
403
  Version = "2012-10-17"
311
404
  Statement = local.lambda_policy_statements
312
405
  })
406
+ <%_ } _%>
313
407
  }
314
408
 
315
409
  locals {
@@ -324,6 +418,17 @@ locals {
324
418
  Resource = ["${var.appconfig_application_arn}/*"]
325
419
  }
326
420
  ] : [], var.additional_iam_policy_statements)
421
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
422
+
423
+ # IAM rejects an empty statement list, so fall back to a no-op deny
424
+ lambda_policy_document_statements = length(local.lambda_policy_statements) > 0 ? local.lambda_policy_statements : [
425
+ {
426
+ Effect = "Deny"
427
+ Action = ["appconfig:GetLatestConfiguration"]
428
+ Resource = ["arn:aws:appconfig:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:application/none"]
429
+ }
430
+ ]
431
+ <%_ } _%>
327
432
  }
328
433
 
329
434
  # CloudWatch Log Group for Lambda
@@ -331,16 +436,31 @@ resource "aws_cloudwatch_log_group" "lambda_logs" {
331
436
  #checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
332
437
  #checkov:skip=CKV_AWS_338:Log retention set to forever
333
438
  #checkov:skip=CKV_AWS_66:Log retention set to forever
439
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
440
+ for_each = local.operations
441
+
442
+ name = "/aws/lambda/${local.function_name[each.key]}"
443
+ tags = var.tags
444
+ <%_ } else { _%>
334
445
  name = "/aws/lambda/<%- apiNameClassName %>Handler-${random_string.suffix.result}"
335
446
  tags = var.tags
447
+ <%_ } _%>
336
448
  }
337
449
 
338
450
  <%_ if (backend.type === 'fastapi') { _%>
339
451
  # Lambda alias pointing to the latest published version for SnapStart
340
452
  resource "aws_lambda_alias" "live" {
453
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
454
+ for_each = local.operations
455
+
456
+ name = "live"
457
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
458
+ function_version = aws_lambda_function.api_lambda[each.key].version
459
+ <%_ } else { _%>
341
460
  name = "live"
342
461
  function_name = aws_lambda_function.api_lambda.function_name
343
462
  function_version = aws_lambda_function.api_lambda.version
463
+ <%_ } _%>
344
464
 
345
465
  depends_on = [aws_lambda_function.api_lambda]
346
466
  }
@@ -455,7 +575,8 @@ resource "aws_apigatewayv2_authorizer" "custom_authorizer" {
455
575
  authorizer_type = "REQUEST"
456
576
  authorizer_uri = aws_lambda_function.authorizer_lambda.invoke_arn
457
577
  authorizer_payload_format_version = "2.0"
458
- authorizer_result_ttl_in_seconds = 0
578
+ authorizer_result_ttl_in_seconds = 300
579
+ identity_sources = ["$request.header.Authorization"]
459
580
  enable_simple_responses = true
460
581
  name = "<%- apiNameClassName %>Authorizer-${random_string.suffix.result}"
461
582
  }
@@ -471,6 +592,17 @@ resource "aws_lambda_permission" "authorizer_invoke" {
471
592
 
472
593
  # Lambda integration for HTTP API
473
594
  resource "aws_apigatewayv2_integration" "lambda_integration" {
595
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
596
+ for_each = local.operations
597
+
598
+ api_id = module.http_api.api_id
599
+ integration_type = "AWS_PROXY"
600
+ <%_ if (backend.type === 'fastapi') { _%>
601
+ integration_uri = aws_lambda_alias.live[each.key].invoke_arn
602
+ <%_ } else { _%>
603
+ integration_uri = aws_lambda_function.api_lambda[each.key].invoke_arn
604
+ <%_ } _%>
605
+ <%_ } else { _%>
474
606
  api_id = module.http_api.api_id
475
607
  integration_type = "AWS_PROXY"
476
608
  <%_ if (backend.type === 'fastapi') { _%>
@@ -478,6 +610,7 @@ resource "aws_apigatewayv2_integration" "lambda_integration" {
478
610
  <%_ } else { _%>
479
611
  integration_uri = aws_lambda_function.api_lambda.invoke_arn
480
612
  <%_ } _%>
613
+ <%_ } _%>
481
614
 
482
615
  payload_format_version = "2.0"
483
616
  timeout_milliseconds = 30000
@@ -489,6 +622,28 @@ resource "aws_apigatewayv2_integration" "lambda_integration" {
489
622
  <%_ } _%>
490
623
  }
491
624
 
625
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
626
+ # One route per operation, each targeting that operation's integration
627
+ resource "aws_apigatewayv2_route" "operation_routes" {
628
+ for_each = local.operations
629
+
630
+ api_id = module.http_api.api_id
631
+ route_key = local.route_key[each.key]
632
+ target = "integrations/${aws_apigatewayv2_integration.lambda_integration[each.key].id}"
633
+
634
+ <%_ if (auth === 'iam') { _%>
635
+ authorization_type = "AWS_IAM"
636
+ <%_ } else if (auth === 'cognito') { _%>
637
+ authorization_type = "JWT"
638
+ authorizer_id = aws_apigatewayv2_authorizer.cognito_authorizer.id
639
+ <%_ } else if (auth === 'custom') { _%>
640
+ authorization_type = "CUSTOM"
641
+ authorizer_id = aws_apigatewayv2_authorizer.custom_authorizer.id
642
+ <%_ } _%>
643
+
644
+ depends_on = [aws_apigatewayv2_integration.lambda_integration<% if (auth === 'cognito') { %>, aws_apigatewayv2_authorizer.cognito_authorizer<% } else if (auth === 'custom') { %>, aws_apigatewayv2_authorizer.custom_authorizer<% } %>]
645
+ }
646
+ <%_ } else { _%>
492
647
  # Route for proxy integration (catches all requests)
493
648
  resource "aws_apigatewayv2_route" "proxy_routes" {
494
649
  # NB: OPTIONS is omitted here since API Gateway manages responding to preflight requests
@@ -511,6 +666,7 @@ resource "aws_apigatewayv2_route" "proxy_routes" {
511
666
 
512
667
  depends_on = [aws_apigatewayv2_integration.lambda_integration<% if (auth === 'cognito') { %>, aws_apigatewayv2_authorizer.cognito_authorizer<% } else if (auth === 'custom') { %>, aws_apigatewayv2_authorizer.custom_authorizer<% } %>]
513
668
  }
669
+ <%_ } _%>
514
670
 
515
671
  # Add API url to runtime config
516
672
  module "add_url_to_runtime_config" {
@@ -525,6 +681,18 @@ module "add_url_to_runtime_config" {
525
681
 
526
682
  # Lambda permission for API Gateway to invoke the function<% if (backend.type === 'fastapi') { %> via alias<% } %>
527
683
  resource "aws_lambda_permission" "api_gateway_invoke" {
684
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
685
+ for_each = local.operations
686
+
687
+ statement_id = "AllowExecutionFromAPIGateway"
688
+ action = "lambda:InvokeFunction"
689
+ function_name = aws_lambda_function.api_lambda[each.key].function_name
690
+ <%_ if (backend.type === 'fastapi') { _%>
691
+ qualifier = aws_lambda_alias.live[each.key].name
692
+ <%_ } _%>
693
+ principal = "apigateway.amazonaws.com"
694
+ source_arn = "${module.http_api.api_execution_arn}/*/${local.permission_source_suffix[each.key]}"
695
+ <%_ } else { _%>
528
696
  statement_id = "AllowExecutionFromAPIGateway"
529
697
  action = "lambda:InvokeFunction"
530
698
  function_name = aws_lambda_function.api_lambda.function_name
@@ -533,6 +701,7 @@ resource "aws_lambda_permission" "api_gateway_invoke" {
533
701
  <%_ } _%>
534
702
  principal = "apigateway.amazonaws.com"
535
703
  source_arn = "${module.http_api.api_execution_arn}/*/*"
704
+ <%_ } _%>
536
705
 
537
706
  <%_ if (backend.type === 'fastapi') { _%>
538
707
  depends_on = [module.http_api, aws_lambda_alias.live]
@@ -579,6 +748,76 @@ output "stage_execution_arn" {
579
748
  value = module.http_api.stage_execution_arn
580
749
  }
581
750
 
751
+ <%_ if (backend.integrationPattern === 'isolated') { _%>
752
+ # Lambda Function Outputs, keyed by operation name
753
+ output "operations" {
754
+ description = "Names of the API operations, each with its own Lambda function"
755
+ value = keys(local.operations)
756
+ }
757
+
758
+ output "lambda_function_names" {
759
+ description = "Name of each operation's Lambda function, keyed by operation name"
760
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.function_name }
761
+ }
762
+
763
+ output "lambda_function_arns" {
764
+ description = "ARN of each operation's Lambda function, keyed by operation name"
765
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.arn }
766
+ }
767
+
768
+ output "lambda_invoke_arns" {
769
+ description = "Invoke ARN of each operation's Lambda function, keyed by operation name"
770
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.invoke_arn }
771
+ }
772
+
773
+ output "lambda_qualified_arns" {
774
+ description = "Qualified ARN of each operation's Lambda function, keyed by operation name"
775
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.qualified_arn }
776
+ }
777
+
778
+ output "lambda_versions" {
779
+ description = "Version of each operation's Lambda function, keyed by operation name"
780
+ value = { for op, fn in aws_lambda_function.api_lambda : op => fn.version }
781
+ }
782
+
783
+ output "lambda_source_code_hash" {
784
+ description = "Base64-encoded SHA256 hash of the Lambda deployment package, shared by every operation"
785
+ value = data.archive_file.lambda_zip.output_base64sha256
786
+ }
787
+
788
+ # IAM Role Outputs, keyed by operation name
789
+ output "lambda_execution_role_arns" {
790
+ description = "ARN of each operation's Lambda execution role, keyed by operation name"
791
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.arn }
792
+ }
793
+
794
+ output "lambda_execution_role_names" {
795
+ description = "Name of each operation's Lambda execution role, keyed by operation name. Attach additional policies to a specific operation's role by name."
796
+ value = { for op, role in aws_iam_role.lambda_execution_role : op => role.name }
797
+ }
798
+
799
+ output "security_group_id" {
800
+ 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."
801
+ value = var.enable_vpc ? aws_security_group.api_lambda[0].id : null
802
+ }
803
+
804
+ # Integration Outputs, keyed by operation name
805
+ output "integration_ids" {
806
+ description = "ID of each operation's Lambda integration, keyed by operation name"
807
+ value = { for op, i in aws_apigatewayv2_integration.lambda_integration : op => i.id }
808
+ }
809
+
810
+ # CloudWatch Log Groups, keyed by operation name
811
+ output "lambda_log_group_names" {
812
+ description = "Name of each operation's Lambda CloudWatch log group, keyed by operation name"
813
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.name }
814
+ }
815
+
816
+ output "lambda_log_group_arns" {
817
+ description = "ARN of each operation's Lambda CloudWatch log group, keyed by operation name"
818
+ value = { for op, lg in aws_cloudwatch_log_group.lambda_logs : op => lg.arn }
819
+ }
820
+ <%_ } else { _%>
582
821
  # Lambda Function Outputs
583
822
  output "lambda_function_name" {
584
823
  description = "Name of the Lambda function"
@@ -647,6 +886,7 @@ output "lambda_log_group_arn" {
647
886
  description = "ARN of the Lambda CloudWatch log group"
648
887
  value = aws_cloudwatch_log_group.lambda_logs.arn
649
888
  }
889
+ <%_ } _%>
650
890
 
651
891
  output "api_log_group_name" {
652
892
  description = "Name of the API Gateway CloudWatch log group"