Skip to content

Commit 5700572

Browse files
authored
correctly handle non backend outputs from stack metadata (#2537)
1 parent 920f128 commit 5700572

File tree

3 files changed

+123
-3
lines changed

3 files changed

+123
-3
lines changed

.changeset/real-pants-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/deployed-backend-client': patch
3+
---
4+
5+
correctly handle non backend outputs from stack metadata

packages/deployed-backend-client/src/stack_metadata_output_retrieval_strategy.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,5 +532,85 @@ void describe('StackMetadataBackendOutputRetrievalStrategy', () => {
532532
},
533533
});
534534
});
535+
536+
void it('returns expected BackendOutput without other stack metadata entries', async () => {
537+
const cfnClientMock = {
538+
send: mock.fn((command) => {
539+
if (command instanceof GetTemplateSummaryCommand) {
540+
return {
541+
Metadata: JSON.stringify({
542+
[authOutputKey]: {
543+
version: '1',
544+
stackOutputs: ['testName1', 'testName2'],
545+
},
546+
[graphqlOutputKey]: {
547+
version: '2',
548+
stackOutputs: ['thing1', 'thing2'],
549+
},
550+
test: 'test',
551+
testArray: ['element0', 'element1'],
552+
testObj: {
553+
testA: 'testA',
554+
testB: 'testB',
555+
},
556+
}),
557+
};
558+
} else if (command instanceof DescribeStacksCommand) {
559+
return {
560+
Stacks: [
561+
{
562+
Outputs: [
563+
{
564+
OutputKey: 'testName1',
565+
OutputValue: 'testValue1',
566+
},
567+
{
568+
OutputKey: 'testName2',
569+
OutputValue: 'testValue2',
570+
},
571+
{
572+
OutputKey: 'thing1',
573+
OutputValue: 'The cat',
574+
},
575+
{
576+
OutputKey: 'thing2',
577+
OutputValue: 'in the hat',
578+
},
579+
],
580+
},
581+
],
582+
};
583+
}
584+
assert.fail(`Unknown command ${typeof command}`);
585+
}),
586+
} as unknown as CloudFormationClient;
587+
588+
const stackNameResolverMock: MainStackNameResolver = {
589+
resolveMainStackName: mock.fn(async () => 'testMainStack'),
590+
};
591+
592+
const retrievalStrategy = new StackMetadataBackendOutputRetrievalStrategy(
593+
cfnClientMock,
594+
stackNameResolverMock
595+
);
596+
597+
const output = await retrievalStrategy.fetchBackendOutput();
598+
assert.deepStrictEqual(output, {
599+
[authOutputKey]: {
600+
version: '1',
601+
payload: {
602+
testName1: 'testValue1',
603+
testName2: 'testValue2',
604+
},
605+
},
606+
[graphqlOutputKey]: {
607+
version: '2',
608+
payload: {
609+
thing1: 'The cat',
610+
thing2: 'in the hat',
611+
},
612+
},
613+
});
614+
});
535615
});
536616
});

packages/deployed-backend-client/src/stack_metadata_output_retrieval_strategy.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
BackendOutputRetrievalStrategy,
1010
MainStackNameResolver,
1111
} from '@aws-amplify/plugin-types';
12-
import { backendOutputStackMetadataSchema } from '@aws-amplify/backend-output-schemas';
12+
import {
13+
BackendOutputEntryStackMetadata,
14+
backendOutputStackMetadataSchema,
15+
} from '@aws-amplify/backend-output-schemas';
1316
import {
1417
BackendOutputClientError,
1518
BackendOutputClientErrorType,
@@ -87,9 +90,13 @@ export class StackMetadataBackendOutputRetrievalStrategy
8790
throw error;
8891
}
8992

93+
const filteredMetadataObject =
94+
this.filterBackendOutputEntryStackMetadata(metadataObject);
95+
9096
// parse and validate the metadata object
91-
const backendOutputMetadata =
92-
backendOutputStackMetadataSchema.parse(metadataObject);
97+
const backendOutputMetadata = backendOutputStackMetadataSchema.parse(
98+
filteredMetadataObject
99+
);
93100

94101
// DescribeStacks includes the template output
95102
const stackDescription = await this.cfnClient.send(
@@ -152,4 +159,32 @@ export class StackMetadataBackendOutputRetrievalStrategy
152159
});
153160
return result;
154161
};
162+
163+
/**
164+
* Filter out stack metadata entries that do not pertain to backend outputs
165+
*/
166+
private filterBackendOutputEntryStackMetadata<
167+
T extends Record<string, unknown>
168+
>(metadata: T) {
169+
return Object.fromEntries(
170+
Object.entries(metadata).filter(([, value]) =>
171+
this.isBackendOutputEntryStackMetadata(value)
172+
)
173+
);
174+
}
175+
176+
/**
177+
* Type predicate to determine if stack metadata entry is like a backend output entry.
178+
* Zod parsing validation will handle stricter type checking
179+
*/
180+
private isBackendOutputEntryStackMetadata(
181+
obj: unknown
182+
): obj is BackendOutputEntryStackMetadata {
183+
return (
184+
!!obj &&
185+
typeof obj === 'object' &&
186+
'version' in obj &&
187+
'stackOutputs' in obj
188+
);
189+
}
155190
}

0 commit comments

Comments
 (0)