diff --git a/packages/amplify-category-api/amplify-plugin.json b/packages/amplify-category-api/amplify-plugin.json index f9ee43e924..1eb8aaf25b 100644 --- a/packages/amplify-category-api/amplify-plugin.json +++ b/packages/amplify-category-api/amplify-plugin.json @@ -12,7 +12,9 @@ "rebuild", "remove", "update", - "help" + "help", + "snapshot-stack-mappings", + "assign-stack-mappings" ], "commandAliases": { "configure": "update" diff --git a/packages/amplify-category-api/src/commands/api.ts b/packages/amplify-category-api/src/commands/api.ts index b8f6709dd2..05904fcd32 100644 --- a/packages/amplify-category-api/src/commands/api.ts +++ b/packages/amplify-category-api/src/commands/api.ts @@ -56,6 +56,14 @@ export const run = async (context: $TSContext) => { name: 'override', description: 'Generates overrides file to apply custom modifications to CloudFormation', }, + { + name: 'snapshot-stack-mappings', + description: 'Snapshots the stack mappings for the current project', + }, + { + name: 'assign-stack-mappings', + description: 'Assign stack mappings for the newly built resources', + }, ]; context.amplify.showHelp(header, commands); diff --git a/packages/amplify-category-api/src/commands/api/assign-stack-mappings.ts b/packages/amplify-category-api/src/commands/api/assign-stack-mappings.ts new file mode 100644 index 0000000000..301ffea567 --- /dev/null +++ b/packages/amplify-category-api/src/commands/api/assign-stack-mappings.ts @@ -0,0 +1,15 @@ +import { $TSContext} from '@aws-amplify/amplify-cli-core'; +import { assignStackMappings } from '../../provider-utils/awscloudformation/stack-mapping-manager'; + +/** + * Pull all resolver and functions from the current cloud backend and snapshot them in the transform.conf.json file. + * `amplify pull` may need to be run first. + */ +export const run = async (context: $TSContext): Promise => { + const stackName = context.parameters?.options?.['stack-name']; + if (!stackName) { + throw new Error('need to provide --stack-name '); + } + + return assignStackMappings(stackName); +}; diff --git a/packages/amplify-category-api/src/commands/api/snapshot-stack-mappings.ts b/packages/amplify-category-api/src/commands/api/snapshot-stack-mappings.ts new file mode 100644 index 0000000000..cf5edfe12b --- /dev/null +++ b/packages/amplify-category-api/src/commands/api/snapshot-stack-mappings.ts @@ -0,0 +1,8 @@ +import { $TSContext } from '@aws-amplify/amplify-cli-core'; +import { snapshotStackMappings } from '../../provider-utils/awscloudformation/stack-mapping-manager'; + +/** + * Pull all resolver and functions from the current cloud backend and snapshot them in the transform.conf.json file. + * `amplify pull` may need to be run first. + */ +export const run = async (context: $TSContext): Promise => snapshotStackMappings(); diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/stack-mapping-manager.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/stack-mapping-manager.ts new file mode 100644 index 0000000000..1aa7ce2d75 --- /dev/null +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/stack-mapping-manager.ts @@ -0,0 +1,108 @@ +import { pathManager, stateManager } from '@aws-amplify/amplify-cli-core'; +import { getAppSyncResourceName } from '../../provider-utils/awscloudformation/utils/amplify-meta-utils'; +import { readFromPath } from 'graphql-transformer-core/lib/util/fileUtils'; +import { loadConfig, writeConfig } from 'graphql-transformer-core'; +import * as fs from 'fs-extra'; +import * as path from 'path'; + +/** + * Pull all resolver and functions from the current cloud backend and snapshot them in the transform.conf.json file. + * `amplify pull` may need to be run first. + */ +export const snapshotStackMappings = async (): Promise => { + const apiName = getAppSyncResourceName(stateManager.getMeta()); + if (!apiName) { + throw new Error('Could not find api name.'); + } + + const currentApiStacksPath = path.join(pathManager.getCurrentCloudBackendDirPath(), 'api', apiName, 'build', 'stacks'); + if (!fs.pathExistsSync(currentApiStacksPath)) { + throw new Error('Could not find current cloud backend api stacks path.'); + } + + const apiPath = path.join(pathManager.getAmplifyDirPath(), 'backend', 'api', apiName); + if (!fs.pathExistsSync(apiPath)) { + throw new Error('Could not find api path.'); + } + + const currentApiStacks: Record = await readFromPath(currentApiStacksPath); + + const getResourceIdsForTypes = (stackDefinition: any, resourceTypes: string[]): string[] => { + const resourceTypeSet = new Set(resourceTypes); + return Object.entries(stackDefinition.Resources) + .filter(([_, resource]: [string, any]) => resourceTypeSet.has(resource.Type)) + .map(([resourceName, _]) => resourceName); + }; + + const stackMappings = Object.fromEntries(Object.entries(currentApiStacks).flatMap(([stackFileName, stackContentsString]) => { + const stackName = stackFileName.split('.')[0]; + const stackContents = JSON.parse(stackContentsString); + return getResourceIdsForTypes(stackContents, ['AWS::AppSync::FunctionConfiguration', 'AWS::AppSync::Resolver']) + .map(id => [id, stackName]); + })); + + const config: any = await loadConfig(apiPath); + + Object.entries(stackMappings).forEach(([resourceId, stackName]) => { + if (!config.StackMapping) { + config.StackMapping = {}; + } + if (!(resourceId in config.StackMapping)) { + config.StackMapping[resourceId] = stackName; + } + }); + + await writeConfig(apiPath, config); +}; + +/** + * Pull all resolver and functions from the current cloud backend and snapshot them in the transform.conf.json file. + * `amplify pull` may need to be run first. + */ +export const assignStackMappings = async (stackNameAssignment: string): Promise => { + await snapshotStackMappings(); + + const apiName = getAppSyncResourceName(stateManager.getMeta()); + if (!apiName) { + throw new Error('Could not find api name.'); + } + + const apiPath = path.join(pathManager.getAmplifyDirPath(), 'backend', 'api', apiName); + if (!fs.pathExistsSync(apiPath)) { + throw new Error('Could not find api path.'); + } + + const buildApiStacksPath = path.join(apiPath, 'build', 'stacks'); + if (!fs.pathExistsSync(buildApiStacksPath)) { + throw new Error('need to build first, run `amplify api gql-compile`'); + } + + const apiStacks: Record = await readFromPath(buildApiStacksPath); + + const getResourceIdsForTypes = (stackDefinition: any, resourceTypes: string[]): string[] => { + const resourceTypeSet = new Set(resourceTypes); + return Object.entries(stackDefinition.Resources) + .filter(([_, resource]: [string, any]) => resourceTypeSet.has(resource.Type)) + .map(([resourceName, _]) => resourceName); + }; + + const stackMappings = Object.fromEntries(Object.entries(apiStacks).flatMap(([stackFileName, stackContentsString]) => { + const stackName = stackFileName.split('.')[0]; + const stackContents = JSON.parse(stackContentsString); + return getResourceIdsForTypes(stackContents, ['AWS::AppSync::FunctionConfiguration', 'AWS::AppSync::Resolver']) + .map(id => [id, stackName]); + })); + + const config: any = await loadConfig(apiPath); + + Object.entries(stackMappings).forEach(([resourceId, stackName]) => { + if (!config.StackMapping) { + config.StackMapping = {}; + } + if (!(resourceId in config.StackMapping)) { + config.StackMapping[resourceId] = stackNameAssignment; + } + }); + + await writeConfig(apiPath, config); +};