diff --git a/.changeset/chatty-beans-begin.md b/.changeset/chatty-beans-begin.md new file mode 100644 index 00000000000..7e7be6e9662 --- /dev/null +++ b/.changeset/chatty-beans-begin.md @@ -0,0 +1,8 @@ +--- +'@aws-amplify/backend-function': minor +'@aws-amplify/backend-auth': minor +'@aws-amplify/backend': minor +'@aws-amplify/backend-storage': minor +--- + +expose stack property for backend, function resource, storage resource, and auth resource diff --git a/.changeset/four-drinks-yell.md b/.changeset/four-drinks-yell.md new file mode 100644 index 00000000000..99b9d3f2001 --- /dev/null +++ b/.changeset/four-drinks-yell.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/plugin-types': minor +--- + +add new type to handle exposing stack diff --git a/packages/backend-auth/API.md b/packages/backend-auth/API.md index b3cf7a91cd2..f9b6247cedf 100644 --- a/packages/backend-auth/API.md +++ b/packages/backend-auth/API.md @@ -20,6 +20,7 @@ import { OidcProviderProps } from '@aws-amplify/auth-construct'; import { ResourceAccessAcceptor } from '@aws-amplify/plugin-types'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; +import { StackProvider } from '@aws-amplify/plugin-types'; import { TriggerEvent } from '@aws-amplify/auth-construct'; // @public @@ -77,7 +78,7 @@ export type AuthLoginWithFactoryProps = Omit & ResourceAccessAcceptorFactory; +export type BackendAuth = ResourceProvider & ResourceAccessAcceptorFactory & StackProvider; // @public export const defineAuth: (props: AmplifyAuthProps) => ConstructFactory; diff --git a/packages/backend-auth/src/factory.test.ts b/packages/backend-auth/src/factory.test.ts index 3865fdcec40..bbf285b7cd8 100644 --- a/packages/backend-auth/src/factory.test.ts +++ b/packages/backend-auth/src/factory.test.ts @@ -79,12 +79,15 @@ void describe('AmplifyAuthFactory', () => { assert.strictEqual(instance1, instance2); }); + void it('verifies stack property exists and is equivalent to auth stack', () => { + const backendAuth = authFactory.getInstance(getInstanceProps); + assert.equal(backendAuth.stack, Stack.of(backendAuth.resources.userPool)); + }); + void it('adds construct to stack', () => { const backendAuth = authFactory.getInstance(getInstanceProps); - const template = Template.fromStack( - Stack.of(backendAuth.resources.userPool) - ); + const template = Template.fromStack(backendAuth.stack); template.resourceCountIs('AWS::Cognito::UserPool', 1); }); @@ -98,9 +101,7 @@ void describe('AmplifyAuthFactory', () => { const backendAuth = authFactory.getInstance(getInstanceProps); - const template = Template.fromStack( - Stack.of(backendAuth.resources.userPool) - ); + const template = Template.fromStack(backendAuth.stack); template.resourceCountIs('AWS::Cognito::UserPool', 1); template.hasResourceProperties('AWS::Cognito::UserPool', { @@ -247,9 +248,7 @@ void describe('AmplifyAuthFactory', () => { const backendAuth = authWithTriggerFactory.getInstance(getInstanceProps); - const template = Template.fromStack( - Stack.of(backendAuth.resources.userPool) - ); + const template = Template.fromStack(backendAuth.stack); template.hasResourceProperties('AWS::Cognito::UserPool', { LambdaConfig: { // The key in the CFN template is the trigger event name with the first character uppercase diff --git a/packages/backend-auth/src/factory.ts b/packages/backend-auth/src/factory.ts index c5184cb7439..02edd4695c4 100644 --- a/packages/backend-auth/src/factory.ts +++ b/packages/backend-auth/src/factory.ts @@ -18,6 +18,7 @@ import { ResourceAccessAcceptor, ResourceAccessAcceptorFactory, ResourceProvider, + StackProvider, } from '@aws-amplify/plugin-types'; import { translateToAuthConstructLoginWith } from './translate_auth_props.js'; import { authAccessBuilder as _authAccessBuilder } from './access_builder.js'; @@ -28,10 +29,11 @@ import { Expand, } from './types.js'; import { UserPoolAccessPolicyFactory } from './userpool_access_policy_factory.js'; -import { Tags } from 'aws-cdk-lib'; +import { Stack, Tags } from 'aws-cdk-lib'; export type BackendAuth = ResourceProvider & - ResourceAccessAcceptorFactory; + ResourceAccessAcceptorFactory & + StackProvider; export type AmplifyAuthProps = Expand< Omit & { @@ -195,6 +197,7 @@ class AmplifyAuthGenerator implements ConstructContainerEntryGenerator { policy.attachToRole(role); }, }), + stack: Stack.of(authConstruct), }; if (!this.props.access) { return authConstructMixin; diff --git a/packages/backend-function/API.md b/packages/backend-function/API.md index 86a98d7e1e6..bf82df50b04 100644 --- a/packages/backend-function/API.md +++ b/packages/backend-function/API.md @@ -9,6 +9,7 @@ import { ConstructFactory } from '@aws-amplify/plugin-types'; import { FunctionResources } from '@aws-amplify/plugin-types'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; +import { StackProvider } from '@aws-amplify/plugin-types'; // @public (undocumented) export type AddEnvironmentFactory = { @@ -19,7 +20,7 @@ export type AddEnvironmentFactory = { export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`; // @public -export const defineFunction: (props?: FunctionProps) => ConstructFactory & ResourceAccessAcceptorFactory & AddEnvironmentFactory>; +export const defineFunction: (props?: FunctionProps) => ConstructFactory & ResourceAccessAcceptorFactory & AddEnvironmentFactory & StackProvider>; // @public (undocumented) export type FunctionProps = { diff --git a/packages/backend-function/src/factory.test.ts b/packages/backend-function/src/factory.test.ts index 316271131e1..477ce69aec6 100644 --- a/packages/backend-function/src/factory.test.ts +++ b/packages/backend-function/src/factory.test.ts @@ -59,10 +59,16 @@ void describe('AmplifyFunctionFactory', () => { assert.strictEqual(instance1, instance2); }); + void it('verifies stack property exists and is equal to function stack', () => { + const functionFactory = defaultLambda; + const lambda = functionFactory.getInstance(getInstanceProps); + assert.equal(lambda.stack, Stack.of(lambda.resources.lambda)); + }); + void it('resolves default name and entry when no args specified', () => { const functionFactory = defaultLambda; const lambda = functionFactory.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', @@ -79,7 +85,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', }); const lambda = functionFactory.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', @@ -96,7 +102,7 @@ void describe('AmplifyFunctionFactory', () => { name: 'myCoolLambda', }); const lambda = functionFactory.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', @@ -113,7 +119,7 @@ void describe('AmplifyFunctionFactory', () => { name: 'myCoolLambda', }); const lambda = functionFactory.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { Tags: [{ Key: 'amplify:friendly-name', Value: 'myCoolLambda' }], @@ -137,7 +143,7 @@ void describe('AmplifyFunctionFactory', () => { void it('builds lambda with local and 3p dependencies', () => { const lambda = lambdaWithDependencies.getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); // There isn't a way to check the contents of the bundled lambda using the CDK Template utility // So we just check that the lambda was created properly in the CFN template. // There is an e2e test that validates proper lambda bundling @@ -159,7 +165,7 @@ void describe('AmplifyFunctionFactory', () => { }); const lambda = functionFactory.getInstance(getInstanceProps); lambda.addEnvironment('key1', 'value1'); - const stack = Stack.of(lambda.resources.lambda); + const stack = lambda.stack; const template = Template.fromStack(stack); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { @@ -177,7 +183,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', timeoutSeconds: 10, }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { Timeout: 10, @@ -230,7 +236,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', memoryMB: 234, }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { MemorySize: 234, @@ -241,7 +247,7 @@ void describe('AmplifyFunctionFactory', () => { const lambda = defineFunction({ entry: './test-assets/default-lambda/handler.ts', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { MemorySize: 512, @@ -294,7 +300,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', runtime: 16, }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { Runtime: Runtime.NODEJS_16_X.name, @@ -305,7 +311,7 @@ void describe('AmplifyFunctionFactory', () => { const lambda = defineFunction({ entry: './test-assets/default-lambda/handler.ts', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Lambda::Function', { Runtime: Runtime.NODEJS_18_X.name, @@ -340,7 +346,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', schedule: 'every 5m', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'cron(*/5 * * * ? *)', @@ -361,7 +367,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', schedule: '0 1 * * ?', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.hasResourceProperties('AWS::Events::Rule', { ScheduleExpression: 'cron(0 1 * * ? *)', @@ -382,7 +388,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', schedule: ['0 1 * * ?', 'every 5m'], }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Events::Rule', 2); @@ -399,7 +405,7 @@ void describe('AmplifyFunctionFactory', () => { const lambda = defineFunction({ entry: './test-assets/default-lambda/handler.ts', }).getInstance(getInstanceProps); - const template = Template.fromStack(Stack.of(lambda.resources.lambda)); + const template = Template.fromStack(lambda.stack); template.resourceCountIs('AWS::Events::Rule', 0); }); @@ -412,7 +418,7 @@ void describe('AmplifyFunctionFactory', () => { name: 'myCoolLambda', }); const lambda = functionFactory.getInstance(getInstanceProps); - const stack = Stack.of(lambda.resources.lambda); + const stack = lambda.stack; const policy = new Policy(stack, 'testPolicy', { statements: [ new PolicyStatement({ @@ -503,9 +509,7 @@ void describe('AmplifyFunctionFactory', () => { entry: './test-assets/default-lambda/handler.ts', name: 'anotherName', }); - const functionStack = Stack.of( - functionFactory.getInstance(getInstanceProps).resources.lambda - ); + const functionStack = functionFactory.getInstance(getInstanceProps).stack; anotherFunction.getInstance(getInstanceProps); const template = Template.fromStack(functionStack); assert.equal( diff --git a/packages/backend-function/src/factory.ts b/packages/backend-function/src/factory.ts index 0b184ddda27..46e66c15c47 100644 --- a/packages/backend-function/src/factory.ts +++ b/packages/backend-function/src/factory.ts @@ -11,6 +11,7 @@ import { ResourceNameValidator, ResourceProvider, SsmEnvironmentEntry, + StackProvider, } from '@aws-amplify/plugin-types'; import { Construct } from 'constructs'; import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'; @@ -64,7 +65,8 @@ export const defineFunction = ( ): ConstructFactory< ResourceProvider & ResourceAccessAcceptorFactory & - AddEnvironmentFactory + AddEnvironmentFactory & + StackProvider > => new FunctionFactory(props, new Error().stack); export type FunctionProps = { @@ -308,6 +310,7 @@ class AmplifyFunction AddEnvironmentFactory { readonly resources: FunctionResources; + readonly stack: Stack; private readonly functionEnvironmentTranslator: FunctionEnvironmentTranslator; constructor( scope: Construct, @@ -318,6 +321,8 @@ class AmplifyFunction ) { super(scope, id); + this.stack = Stack.of(scope); + const runtime = nodeVersionMap[props.runtime]; const require = createRequire(import.meta.url); diff --git a/packages/backend-storage/API.md b/packages/backend-storage/API.md index e7472b5770f..aa65ada47b2 100644 --- a/packages/backend-storage/API.md +++ b/packages/backend-storage/API.md @@ -14,6 +14,7 @@ import { IBucket } from 'aws-cdk-lib/aws-s3'; import { ResourceAccessAcceptor } from '@aws-amplify/plugin-types'; import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types'; import { ResourceProvider } from '@aws-amplify/plugin-types'; +import { StackProvider } from '@aws-amplify/plugin-types'; import { StorageOutput } from '@aws-amplify/backend-output-schemas'; // @public (undocumented) @@ -34,7 +35,7 @@ export type AmplifyStorageProps = { export type AmplifyStorageTriggerEvent = 'onDelete' | 'onUpload'; // @public -export const defineStorage: (props: AmplifyStorageFactoryProps) => ConstructFactory>; +export const defineStorage: (props: AmplifyStorageFactoryProps) => ConstructFactory & StackProvider>; // @public export type EntityId = 'identity'; diff --git a/packages/backend-storage/src/construct.ts b/packages/backend-storage/src/construct.ts index 1cbdb716701..d31f0b97ebc 100644 --- a/packages/backend-storage/src/construct.ts +++ b/packages/backend-storage/src/construct.ts @@ -12,6 +12,7 @@ import { ConstructFactory, FunctionResources, ResourceProvider, + StackProvider, } from '@aws-amplify/plugin-types'; import { StorageOutput } from '@aws-amplify/backend-output-schemas'; import { RemovalPolicy, Stack } from 'aws-cdk-lib'; @@ -77,8 +78,9 @@ export type StorageResources = { */ export class AmplifyStorage extends Construct - implements ResourceProvider + implements ResourceProvider, StackProvider { + readonly stack: Stack; readonly resources: StorageResources; readonly isDefault: boolean; readonly name: string; @@ -89,6 +91,7 @@ export class AmplifyStorage super(scope, id); this.isDefault = props.isDefault || false; this.name = props.name; + this.stack = Stack.of(scope); const bucketProps: BucketProps = { versioned: props.versioned || false, diff --git a/packages/backend-storage/src/factory.test.ts b/packages/backend-storage/src/factory.test.ts index d3f425a6d2b..0ffd504d67a 100644 --- a/packages/backend-storage/src/factory.test.ts +++ b/packages/backend-storage/src/factory.test.ts @@ -149,6 +149,16 @@ void describe('AmplifyStorageFactory', () => { }; }); + void it('verifies stack property exists and is equal to storage stack', () => { + const storageConstruct = defineStorage({ name: 'testName' }).getInstance( + getInstanceProps + ); + assert.equal( + storageConstruct.stack, + Stack.of(storageConstruct.resources.bucket) + ); + }); + void it('if more than one default bucket, throw', () => { storageFactory = defineStorage({ name: 'testName', isDefault: true }); storageFactory2 = defineStorage({ name: 'testName2', isDefault: true }); diff --git a/packages/backend-storage/src/factory.ts b/packages/backend-storage/src/factory.ts index 49eff9ba1e6..4c5212c9649 100644 --- a/packages/backend-storage/src/factory.ts +++ b/packages/backend-storage/src/factory.ts @@ -3,6 +3,7 @@ import { ConstructFactory, ConstructFactoryGetInstanceProps, ResourceProvider, + StackProvider, } from '@aws-amplify/plugin-types'; import * as path from 'path'; import { AmplifyStorage, StorageResources } from './construct.js'; @@ -74,5 +75,5 @@ export class AmplifyStorageFactory */ export const defineStorage = ( props: AmplifyStorageFactoryProps -): ConstructFactory> => +): ConstructFactory & StackProvider> => new AmplifyStorageFactory(props, new Error().stack); diff --git a/packages/backend/API.md b/packages/backend/API.md index 1cb83ee09f3..ba53fec1756 100644 --- a/packages/backend/API.md +++ b/packages/backend/API.md @@ -49,6 +49,7 @@ export type Backend = BackendBase & { export type BackendBase = { createStack: (name: string) => Stack; addOutput: (clientConfigPart: DeepPartialAmplifyGeneratedConfigs) => void; + stack: Stack; }; export { BackendOutputEntry } diff --git a/packages/backend/src/backend.ts b/packages/backend/src/backend.ts index a4e767ada23..bd2d5d3d2f7 100644 --- a/packages/backend/src/backend.ts +++ b/packages/backend/src/backend.ts @@ -12,6 +12,7 @@ export type BackendBase = { addOutput: ( clientConfigPart: DeepPartialAmplifyGeneratedConfigs ) => void; + stack: Stack; }; // Type that allows construct factories to be defined using any keys except those used in BackendHelpers diff --git a/packages/backend/src/backend_factory.test.ts b/packages/backend/src/backend_factory.test.ts index 4016e50b782..268bede3ffe 100644 --- a/packages/backend/src/backend_factory.test.ts +++ b/packages/backend/src/backend_factory.test.ts @@ -148,6 +148,11 @@ void describe('Backend', () => { ); }); + void it('verifies stack property exists and is equivalent to rootStack', () => { + const backend = new BackendFactory({}, rootStack); + assert.equal(rootStack, backend.stack); + }); + void it('stores attribution metadata in root stack', () => { new BackendFactory({}, rootStack); const rootStackTemplate = Template.fromStack(rootStack); diff --git a/packages/backend/src/backend_factory.ts b/packages/backend/src/backend_factory.ts index d9bf2f545a3..023f52244f6 100644 --- a/packages/backend/src/backend_factory.ts +++ b/packages/backend/src/backend_factory.ts @@ -49,6 +49,7 @@ export class BackendFactory< [K in keyof T]: ReturnType; }; + readonly stack; private readonly stackResolver: StackResolver; private readonly customOutputsAccumulator: CustomOutputsAccumulator; /** @@ -56,6 +57,7 @@ export class BackendFactory< * If no CDK App is specified a new one is created */ constructor(constructFactories: T, stack: Stack = createDefaultStack()) { + this.stack = stack; new AttributionMetadataStorage().storeAttributionMetadata( stack, rootStackTypeIdentifier, @@ -157,5 +159,6 @@ export const defineBackend = ( ...backend.resources, createStack: backend.createStack, addOutput: backend.addOutput, + stack: backend.stack, }; }; diff --git a/packages/integration-tests/src/define_backend_template_harness.ts b/packages/integration-tests/src/define_backend_template_harness.ts index 455c8b0cf64..ae484e4b9ef 100644 --- a/packages/integration-tests/src/define_backend_template_harness.ts +++ b/packages/integration-tests/src/define_backend_template_harness.ts @@ -66,10 +66,14 @@ const backendTemplatesCollector: SynthesizeBackendTemplates = < } as Partial<{ [K in keyof T]: Template }> & { root: Template }; for (const [key, resourceRecord] of Object.entries(backend)) { - // skip over the methods that we add on to the backend object + // skip over the properties and methods that we add on to the backend object if (typeof resourceRecord === 'function') { continue; } + // skip non-resource properties + if (!('resources' in resourceRecord)) { + continue; + } // find some construct in the resources exposed by the resourceRecord const firstConstruct = Object.values(resourceRecord.resources).find( (value) => value instanceof Construct diff --git a/packages/plugin-types/API.md b/packages/plugin-types/API.md index 58b1302c884..97bf48886ae 100644 --- a/packages/plugin-types/API.md +++ b/packages/plugin-types/API.md @@ -238,6 +238,11 @@ export type StableBackendIdentifiers = { getStableBackendHash: () => string; }; +// @public (undocumented) +export type StackProvider = { + stack: Stack; +}; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/plugin-types/src/index.ts b/packages/plugin-types/src/index.ts index 3b8c7bf18e8..ee50df55a3c 100644 --- a/packages/plugin-types/src/index.ts +++ b/packages/plugin-types/src/index.ts @@ -20,3 +20,4 @@ export * from './deep_partial.js'; export * from './stable_backend_identifiers.js'; export * from './resource_name_validator.js'; export * from './aws_client_provider.js'; +export * from './stack_provider.js'; diff --git a/packages/plugin-types/src/stack_provider.ts b/packages/plugin-types/src/stack_provider.ts new file mode 100644 index 00000000000..1c88482f7b2 --- /dev/null +++ b/packages/plugin-types/src/stack_provider.ts @@ -0,0 +1,5 @@ +import { Stack } from 'aws-cdk-lib'; + +export type StackProvider = { + stack: Stack; +};