Skip to content

Commit 75376f5

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

3 files changed

Lines changed: 219 additions & 2 deletions

File tree

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { Construct } from 'constructs';
4040
import { ResolverConfig } from '../config/transformer-config';
4141
import { InvalidDirectiveError, InvalidTransformerError, SchemaValidationError, UnknownDirectiveError } from '../errors';
4242
import { GraphQLApi } from '../graphql-api';
43-
import { TransformerContext, NONE_DATA_SOURCE_NAME } from '../transformer-context';
43+
import { TransformerContext, NONE_DATA_SOURCE_NAME, TransformerResolver } from '../transformer-context';
4444
import { TransformerOutput } from '../transformer-context/output';
4545
import { adoptAuthModes } from '../utils/authType';
4646
import { MappingTemplate } from '../cdk-compat';
@@ -345,7 +345,24 @@ export class GraphQLTransform {
345345
this.logs.push(...logs);
346346
}
347347
}
348+
348349
this.collectResolvers(context, context.api);
350+
351+
if (this.stackMappingOverrides && Object.keys(this.stackMappingOverrides).length > 0) {
352+
const allResolvers = context.resolvers.collectResolvers();
353+
const allResolverLogicalIds = new Set(
354+
[...allResolvers.values()].map((r) => (r as TransformerResolver).resolverLogicalId),
355+
);
356+
const mappingKeys = Object.keys(this.stackMappingOverrides);
357+
const unusedKeys = mappingKeys.filter((key) => !allResolverLogicalIds.has(key));
358+
359+
if (unusedKeys.length > 0) {
360+
console.warn(
361+
`stackMappings contains keys that don't match any generated resolver: [${unusedKeys.join(', ')}]. These keys will be ignored.`,
362+
);
363+
}
364+
}
365+
349366
this.ensureNoneDataSource(context.api);
350367
}
351368

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: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import * as path from 'path';
2+
import * as os from 'os';
3+
import { AssetProps } 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 warnSpy: jest.SpyInstance;
52+
53+
beforeEach(() => {
54+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
55+
});
56+
57+
afterEach(() => {
58+
warnSpy.mockRestore();
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+
});
85+
86+
expect(warnSpy).toHaveBeenCalledWith(
87+
expect.stringMatching(/stackMappings contains keys that don't match any generated resolver: \[NonExistentResolver\]\. These keys will be ignored\./),
88+
);
89+
});
90+
91+
it('warns listing multiple invalid keys', () => {
92+
const schema = /* GraphQL */ `
93+
type Todo @model {
94+
content: String!
95+
}
96+
`;
97+
98+
executeTransform({
99+
scope: new Stack(),
100+
nestedStackProvider: {
101+
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
102+
},
103+
assetProvider: createAssetProvider(),
104+
synthParameters: {
105+
amplifyEnvironmentName: 'testenv',
106+
apiName: 'testApi',
107+
},
108+
...defaultTransformConfig,
109+
stackMapping: {
110+
FakeResolver1: 'MyCustomStack',
111+
FakeResolver2: 'MyCustomStack',
112+
},
113+
schema,
114+
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
115+
});
116+
117+
expect(warnSpy).toHaveBeenCalledWith(
118+
expect.stringMatching(/stackMappings contains keys that don't match any generated resolver: \[.*FakeResolver1.*FakeResolver2.*\]\. These keys will be ignored\./),
119+
);
120+
});
121+
122+
it('does not warn when stackMappings contains only valid resolver keys', () => {
123+
const schema = /* GraphQL */ `
124+
type Todo @model {
125+
content: String!
126+
}
127+
`;
128+
129+
executeTransform({
130+
scope: new Stack(),
131+
nestedStackProvider: {
132+
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
133+
},
134+
assetProvider: createAssetProvider(),
135+
synthParameters: {
136+
amplifyEnvironmentName: 'testenv',
137+
apiName: 'testApi',
138+
},
139+
...defaultTransformConfig,
140+
stackMapping: {
141+
CreateTodoResolver: 'MyCustomStack',
142+
},
143+
schema,
144+
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
145+
});
146+
147+
expect(warnSpy).not.toHaveBeenCalled();
148+
});
149+
150+
it('does not warn when stackMappings is empty', () => {
151+
const schema = /* GraphQL */ `
152+
type Todo @model {
153+
content: String!
154+
}
155+
`;
156+
157+
executeTransform({
158+
scope: new Stack(),
159+
nestedStackProvider: {
160+
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
161+
},
162+
assetProvider: createAssetProvider(),
163+
synthParameters: {
164+
amplifyEnvironmentName: 'testenv',
165+
apiName: 'testApi',
166+
},
167+
...defaultTransformConfig,
168+
stackMapping: {},
169+
schema,
170+
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
171+
});
172+
173+
expect(warnSpy).not.toHaveBeenCalled();
174+
});
175+
176+
it('does not warn when no stackMappings is provided', () => {
177+
const schema = /* GraphQL */ `
178+
type Todo @model {
179+
content: String!
180+
}
181+
`;
182+
183+
executeTransform({
184+
scope: new Stack(),
185+
nestedStackProvider: {
186+
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
187+
},
188+
assetProvider: createAssetProvider(),
189+
synthParameters: {
190+
amplifyEnvironmentName: 'testenv',
191+
apiName: 'testApi',
192+
},
193+
...defaultTransformConfig,
194+
schema,
195+
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
196+
});
197+
198+
expect(warnSpy).not.toHaveBeenCalled();
199+
});
200+
});

0 commit comments

Comments
 (0)