Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/amplify-graphql-transformer-core/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,8 @@ export class TransformerResolver implements TransformerResolverProvider {
// (undocumented)
mapToStack: (stack: Stack) => void;
// (undocumented)
readonly resolverLogicalId: string;
// (undocumented)
setScope: (scope: Construct) => void;
// Warning: (ae-forgotten-export) The symbol "FunctionRuntimeTemplatePartialVTL" needs to be exported by the entry point index.d.ts
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
TransformerPluginProvider,
TransformHostProvider,
TransformerLog,
TransformerLogLevel,
NestedStackProvider,
SynthParameters,
LogConfig,
Expand Down Expand Up @@ -40,7 +41,7 @@ import { Construct } from 'constructs';
import { ResolverConfig } from '../config/transformer-config';
import { InvalidDirectiveError, InvalidTransformerError, SchemaValidationError, UnknownDirectiveError } from '../errors';
import { GraphQLApi } from '../graphql-api';
import { TransformerContext, NONE_DATA_SOURCE_NAME } from '../transformer-context';
import { TransformerContext, NONE_DATA_SOURCE_NAME, TransformerResolver } from '../transformer-context';
import { TransformerOutput } from '../transformer-context/output';
import { adoptAuthModes } from '../utils/authType';
import { MappingTemplate } from '../cdk-compat';
Expand Down Expand Up @@ -345,6 +346,7 @@ export class GraphQLTransform {
this.logs.push(...logs);
}
}

this.collectResolvers(context, context.api);
this.ensureNoneDataSource(context.api);
}
Expand Down Expand Up @@ -424,8 +426,14 @@ export class GraphQLTransform {

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

for (const [resolverName, resolver] of resolverEntries) {
const logicalId = (resolver as TransformerResolver).resolverLogicalId;
if (this.stackMappingOverrides[logicalId]) {
seenMappingKeys.add(logicalId);
}

const userSlots = this.userDefinedSlots[resolverName] || [];

userSlots.forEach((slot) => {
Expand All @@ -441,6 +449,16 @@ export class GraphQLTransform {

resolver.synthesize(context, api);
}

const unusedKeys = Object.keys(this.stackMappingOverrides).filter((key) => !seenMappingKeys.has(key));
if (unusedKeys.length > 0) {
this.logs.push({
level: TransformerLogLevel.WARN,
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.`,
});
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class TransformerResolver implements TransformerResolverProvider {
constructor(
private typeName: string,
private fieldName: string,
private resolverLogicalId: string,
readonly resolverLogicalId: string,
private mappingTemplate: FunctionRuntimeTemplate,
private requestSlots: string[],
private responseSlots: string[],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import * as path from 'path';
import * as os from 'os';
import { AssetProps, TransformerLog } from '@aws-amplify/graphql-transformer-interfaces';
import { DDB_DEFAULT_DATASOURCE_STRATEGY, constructDataSourceStrategies } from '@aws-amplify/graphql-transformer-core';
import { NestedStack, Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Asset } from 'aws-cdk-lib/aws-s3-assets';
import * as fs from 'fs-extra';
import { executeTransform, TransformConfig } from '../graphql-transformer';

const defaultTransformConfig: TransformConfig = {
transformersFactoryArgs: {},
transformParameters: {
shouldDeepMergeDirectiveConfigDefaults: false,
subscriptionsInheritPrimaryAuth: false,
disableResolverDeduping: false,
sandboxModeEnabled: false,
useSubUsernameForDefaultIdentityClaim: false,
populateOwnerFieldForStaticGroupAuth: false,
suppressApiKeyGeneration: false,
secondaryKeyAsGSI: false,
enableAutoIndexQueryNames: false,
respectPrimaryKeyAttributesOnConnectionField: false,
enableSearchNodeToNodeEncryption: false,
enableSearchEncryptionAtRest: true,
enableTransformerCfnOutputs: true,
allowDestructiveGraphqlSchemaUpdates: false,
replaceTableUponGsiUpdate: false,
allowGen1Patterns: true,
},
};

const createAssetProvider = () => {
const assets = new Map<string, string>();
const tempAssetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'transformer-assets'));
return {
provide: (scope: Construct, name: string, props: AssetProps) => {
assets.set(props.fileName, props.fileContent);
const filePath = path.join(tempAssetDir, props.fileName);
const fileDirName = path.dirname(filePath);
if (!fs.existsSync(fileDirName)) {
fs.mkdirSync(fileDirName, { recursive: true });
}
fs.writeFileSync(filePath, props.fileContent);
return new Asset(scope, name, { path: filePath });
},
};
};

describe('stackMappings validation', () => {
let logs: TransformerLog[];

beforeEach(() => {
logs = [];
});

const collectLogs = (log: TransformerLog): void => {
logs.push(log);
};

it('warns when stackMappings contains keys that do not match any generated resolver', () => {
const schema = /* GraphQL */ `
type Todo @model {
content: String!
}
`;

executeTransform({
scope: new Stack(),
nestedStackProvider: {
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
},
assetProvider: createAssetProvider(),
synthParameters: {
amplifyEnvironmentName: 'testenv',
apiName: 'testApi',
},
...defaultTransformConfig,
stackMapping: {
NonExistentResolver: 'MyCustomStack',
},
schema,
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
printTransformerLog: collectLogs,
});

expect(logs).toEqual(
expect.arrayContaining([
expect.objectContaining({
level: 'WARN',
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/,
),
}),
]),
);
});

it('warns listing multiple invalid keys', () => {
const schema = /* GraphQL */ `
type Todo @model {
content: String!
}
`;

executeTransform({
scope: new Stack(),
nestedStackProvider: {
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
},
assetProvider: createAssetProvider(),
synthParameters: {
amplifyEnvironmentName: 'testenv',
apiName: 'testApi',
},
...defaultTransformConfig,
stackMapping: {
FakeResolver1: 'MyCustomStack',
FakeResolver2: 'MyCustomStack',
},
schema,
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
printTransformerLog: collectLogs,
});

expect(logs).toEqual(
expect.arrayContaining([
expect.objectContaining({
level: 'WARN',
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/,
),
}),
]),
);
});

it('does not warn when stackMappings contains only valid resolver keys', () => {
const schema = /* GraphQL */ `
type Todo @model {
content: String!
}
`;

executeTransform({
scope: new Stack(),
nestedStackProvider: {
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
},
assetProvider: createAssetProvider(),
synthParameters: {
amplifyEnvironmentName: 'testenv',
apiName: 'testApi',
},
...defaultTransformConfig,
stackMapping: {
CreateTodoResolver: 'MyCustomStack',
},
schema,
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
printTransformerLog: collectLogs,
});

const warnLogs = logs.filter((log) => log.level === 'WARN');
expect(warnLogs).toHaveLength(0);
});

it('does not warn when stackMappings is empty', () => {
const schema = /* GraphQL */ `
type Todo @model {
content: String!
}
`;

executeTransform({
scope: new Stack(),
nestedStackProvider: {
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
},
assetProvider: createAssetProvider(),
synthParameters: {
amplifyEnvironmentName: 'testenv',
apiName: 'testApi',
},
...defaultTransformConfig,
stackMapping: {},
schema,
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
printTransformerLog: collectLogs,
});

const warnLogs = logs.filter((log) => log.level === 'WARN');
expect(warnLogs).toHaveLength(0);
});

it('does not warn when no stackMappings is provided', () => {
const schema = /* GraphQL */ `
type Todo @model {
content: String!
}
`;

executeTransform({
scope: new Stack(),
nestedStackProvider: {
provide: (scope: Construct, name: string) => new NestedStack(scope, name),
},
assetProvider: createAssetProvider(),
synthParameters: {
amplifyEnvironmentName: 'testenv',
apiName: 'testApi',
},
...defaultTransformConfig,
schema,
dataSourceStrategies: constructDataSourceStrategies(schema, DDB_DEFAULT_DATASOURCE_STRATEGY),
printTransformerLog: collectLogs,
});

const warnLogs = logs.filter((log) => log.level === 'WARN');
expect(warnLogs).toHaveLength(0);
});
});
Loading