Skip to content

Commit 2e73ed5

Browse files
committed
feat(graphql-transformer): warn on unused stackMappings keys during synthesis
1 parent 9a2c742 commit 2e73ed5

3 files changed

Lines changed: 236 additions & 2 deletions

File tree

packages/amplify-graphql-transformer-core/src/transformation/transform.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
TransformerPluginProvider,
55
TransformHostProvider,
66
TransformerLog,
7+
TransformerLogLevel,
78
NestedStackProvider,
89
SynthParameters,
910
LogConfig,
@@ -40,7 +41,7 @@ import { Construct } from 'constructs';
4041
import { ResolverConfig } from '../config/transformer-config';
4142
import { InvalidDirectiveError, InvalidTransformerError, SchemaValidationError, UnknownDirectiveError } from '../errors';
4243
import { GraphQLApi } from '../graphql-api';
43-
import { TransformerContext, NONE_DATA_SOURCE_NAME } from '../transformer-context';
44+
import { TransformerContext, NONE_DATA_SOURCE_NAME, TransformerResolver } from '../transformer-context';
4445
import { TransformerOutput } from '../transformer-context/output';
4546
import { adoptAuthModes } from '../utils/authType';
4647
import { MappingTemplate } from '../cdk-compat';
@@ -345,6 +346,7 @@ export class GraphQLTransform {
345346
this.logs.push(...logs);
346347
}
347348
}
349+
348350
this.collectResolvers(context, context.api);
349351
this.ensureNoneDataSource(context.api);
350352
}
@@ -424,8 +426,14 @@ export class GraphQLTransform {
424426

425427
private collectResolvers(context: TransformerContext, api: GraphQLAPIProvider): void {
426428
const resolverEntries = context.resolvers.collectResolvers();
429+
const seenMappingKeys = new Set<string>();
427430

428431
for (const [resolverName, resolver] of resolverEntries) {
432+
const logicalId = (resolver as TransformerResolver).resolverLogicalId;
433+
if (this.stackMappingOverrides[logicalId]) {
434+
seenMappingKeys.add(logicalId);
435+
}
436+
429437
const userSlots = this.userDefinedSlots[resolverName] || [];
430438

431439
userSlots.forEach((slot) => {
@@ -441,6 +449,14 @@ export class GraphQLTransform {
441449

442450
resolver.synthesize(context, api);
443451
}
452+
453+
const unusedKeys = Object.keys(this.stackMappingOverrides).filter((key) => !seenMappingKeys.has(key));
454+
if (unusedKeys.length > 0) {
455+
this.logs.push({
456+
level: TransformerLogLevel.WARN,
457+
message: `stackMappings contains keys that don't match any generated resolver: [${unusedKeys.join(', ')}]. These keys will be ignored. You can discover valid resolver names by running \`npx ampx sandbox\` and examining the CloudFormation output, or by running \`cdk synth\` to see the generated template.`,
458+
});
459+
}
444460
}
445461

446462
/**

packages/amplify-graphql-transformer-core/src/transformer-context/resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export class TransformerResolver implements TransformerResolverProvider {
144144
constructor(
145145
private typeName: string,
146146
private fieldName: string,
147-
private resolverLogicalId: string,
147+
readonly resolverLogicalId: string,
148148
private mappingTemplate: FunctionRuntimeTemplate,
149149
private requestSlots: string[],
150150
private responseSlots: string[],
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import * as path from 'path';
2+
import * as os from 'os';
3+
import { AssetProps, TransformerLog } from '@aws-amplify/graphql-transformer-interfaces';
4+
import { DDB_DEFAULT_DATASOURCE_STRATEGY, constructDataSourceStrategies } from '@aws-amplify/graphql-transformer-core';
5+
import { NestedStack, Stack } from 'aws-cdk-lib';
6+
import { Construct } from 'constructs';
7+
import { Asset } from 'aws-cdk-lib/aws-s3-assets';
8+
import * as fs from 'fs-extra';
9+
import { executeTransform, TransformConfig } from '../graphql-transformer';
10+
11+
const defaultTransformConfig: TransformConfig = {
12+
transformersFactoryArgs: {},
13+
transformParameters: {
14+
shouldDeepMergeDirectiveConfigDefaults: false,
15+
subscriptionsInheritPrimaryAuth: false,
16+
disableResolverDeduping: false,
17+
sandboxModeEnabled: false,
18+
useSubUsernameForDefaultIdentityClaim: false,
19+
populateOwnerFieldForStaticGroupAuth: false,
20+
suppressApiKeyGeneration: false,
21+
secondaryKeyAsGSI: false,
22+
enableAutoIndexQueryNames: false,
23+
respectPrimaryKeyAttributesOnConnectionField: false,
24+
enableSearchNodeToNodeEncryption: false,
25+
enableSearchEncryptionAtRest: true,
26+
enableTransformerCfnOutputs: true,
27+
allowDestructiveGraphqlSchemaUpdates: false,
28+
replaceTableUponGsiUpdate: false,
29+
allowGen1Patterns: true,
30+
},
31+
};
32+
33+
const createAssetProvider = () => {
34+
const assets = new Map<string, string>();
35+
const tempAssetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'transformer-assets'));
36+
return {
37+
provide: (scope: Construct, name: string, props: AssetProps) => {
38+
assets.set(props.fileName, props.fileContent);
39+
const filePath = path.join(tempAssetDir, props.fileName);
40+
const fileDirName = path.dirname(filePath);
41+
if (!fs.existsSync(fileDirName)) {
42+
fs.mkdirSync(fileDirName, { recursive: true });
43+
}
44+
fs.writeFileSync(filePath, props.fileContent);
45+
return new Asset(scope, name, { path: filePath });
46+
},
47+
};
48+
};
49+
50+
describe('stackMappings validation', () => {
51+
let logs: TransformerLog[];
52+
53+
beforeEach(() => {
54+
logs = [];
55+
});
56+
57+
const collectLogs = (log: TransformerLog): void => {
58+
logs.push(log);
59+
};
60+
61+
it('warns when stackMappings contains keys that do not match any generated resolver', () => {
62+
const schema = /* GraphQL */ `
63+
type Todo @model {
64+
content: String!
65+
}
66+
`;
67+
68+
executeTransform({
69+
scope: new Stack(),
70+
nestedStackProvider: {
71+
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
72+
},
73+
assetProvider: createAssetProvider(),
74+
synthParameters: {
75+
amplifyEnvironmentName: 'testenv',
76+
apiName: 'testApi',
77+
},
78+
...defaultTransformConfig,
79+
stackMapping: {
80+
NonExistentResolver: 'MyCustomStack',
81+
},
82+
schema,
83+
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
84+
printTransformerLog: collectLogs,
85+
});
86+
87+
expect(logs).toEqual(
88+
expect.arrayContaining([
89+
expect.objectContaining({
90+
level: 'WARN',
91+
message: expect.stringMatching(/stackMappings contains keys that don't match any generated resolver: \[NonExistentResolver\]\. These keys will be ignored\. You can discover valid resolver names by running/),
92+
}),
93+
]),
94+
);
95+
});
96+
97+
it('warns listing multiple invalid keys', () => {
98+
const schema = /* GraphQL */ `
99+
type Todo @model {
100+
content: String!
101+
}
102+
`;
103+
104+
executeTransform({
105+
scope: new Stack(),
106+
nestedStackProvider: {
107+
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
108+
},
109+
assetProvider: createAssetProvider(),
110+
synthParameters: {
111+
amplifyEnvironmentName: 'testenv',
112+
apiName: 'testApi',
113+
},
114+
...defaultTransformConfig,
115+
stackMapping: {
116+
FakeResolver1: 'MyCustomStack',
117+
FakeResolver2: 'MyCustomStack',
118+
},
119+
schema,
120+
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
121+
printTransformerLog: collectLogs,
122+
});
123+
124+
expect(logs).toEqual(
125+
expect.arrayContaining([
126+
expect.objectContaining({
127+
level: 'WARN',
128+
message: expect.stringMatching(/stackMappings contains keys that don't match any generated resolver: \[.*FakeResolver1.*FakeResolver2.*\]\. These keys will be ignored\. You can discover valid resolver names by running/),
129+
}),
130+
]),
131+
);
132+
});
133+
134+
it('does not warn when stackMappings contains only valid resolver keys', () => {
135+
const schema = /* GraphQL */ `
136+
type Todo @model {
137+
content: String!
138+
}
139+
`;
140+
141+
executeTransform({
142+
scope: new Stack(),
143+
nestedStackProvider: {
144+
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
145+
},
146+
assetProvider: createAssetProvider(),
147+
synthParameters: {
148+
amplifyEnvironmentName: 'testenv',
149+
apiName: 'testApi',
150+
},
151+
...defaultTransformConfig,
152+
stackMapping: {
153+
CreateTodoResolver: 'MyCustomStack',
154+
},
155+
schema,
156+
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
157+
printTransformerLog: collectLogs,
158+
});
159+
160+
const warnLogs = logs.filter((log) => log.level === 'WARN');
161+
expect(warnLogs).toHaveLength(0);
162+
});
163+
164+
it('does not warn when stackMappings is empty', () => {
165+
const schema = /* GraphQL */ `
166+
type Todo @model {
167+
content: String!
168+
}
169+
`;
170+
171+
executeTransform({
172+
scope: new Stack(),
173+
nestedStackProvider: {
174+
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
175+
},
176+
assetProvider: createAssetProvider(),
177+
synthParameters: {
178+
amplifyEnvironmentName: 'testenv',
179+
apiName: 'testApi',
180+
},
181+
...defaultTransformConfig,
182+
stackMapping: {},
183+
schema,
184+
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
185+
printTransformerLog: collectLogs,
186+
});
187+
188+
const warnLogs = logs.filter((log) => log.level === 'WARN');
189+
expect(warnLogs).toHaveLength(0);
190+
});
191+
192+
it('does not warn when no stackMappings is provided', () => {
193+
const schema = /* GraphQL */ `
194+
type Todo @model {
195+
content: String!
196+
}
197+
`;
198+
199+
executeTransform({
200+
scope: new Stack(),
201+
nestedStackProvider: {
202+
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
203+
},
204+
assetProvider: createAssetProvider(),
205+
synthParameters: {
206+
amplifyEnvironmentName: 'testenv',
207+
apiName: 'testApi',
208+
},
209+
...defaultTransformConfig,
210+
schema,
211+
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
212+
printTransformerLog: collectLogs,
213+
});
214+
215+
const warnLogs = logs.filter((log) => log.level === 'WARN');
216+
expect(warnLogs).toHaveLength(0);
217+
});
218+
});

0 commit comments

Comments
 (0)