@aws-amplify/amplify-category-api
Version:
Amplify CLI API Category Plugin
332 lines (281 loc) • 13.1 kB
text/typescript
import * as path from 'path';
import { $TSContext, AmplifySupportedService, exitOnNextTick, NotImplementedError } from '@aws-amplify/amplify-cli-core';
import { UpdateApiRequest } from 'amplify-headless-interface';
import { printer } from '@aws-amplify/amplify-prompts';
import inquirer from 'inquirer';
import { category } from '../../category-constants';
import { ApigwInputState } from './apigw-input-state';
import { getCfnApiArtifactHandler } from './cfn-api-artifact-handler';
import { addResource as addContainer, updateResource as updateContainer } from './containers-handler';
import { legacyAddResource } from './legacy-add-resource';
import {
API_TYPE,
getPermissionPolicies as getContainerPermissionPolicies,
ServiceConfiguration,
} from './service-walkthroughs/containers-walkthrough';
import { datasourceMetadataFor, getServiceWalkthrough, serviceMetadataFor } from './utils/dynamic-imports';
import { editSchemaFlow } from './utils/edit-schema-flow';
import { serviceWalkthroughResultToAddApiRequest } from './utils/service-walkthrough-result-to-add-api-request';
export async function addAdminQueriesApi(
context: $TSContext,
apiProps: { apiName: string; functionName: string; authResourceName: string; dependsOn: Record<string, any>[] },
) {
const apigwInputState = new ApigwInputState(context, apiProps.apiName);
return apigwInputState.addAdminQueriesResource(apiProps);
}
export async function updateAdminQueriesApi(
context: $TSContext,
apiProps: { apiName: string; functionName: string; authResourceName: string; dependsOn: Record<string, any>[] },
) {
const apigwInputState = new ApigwInputState(context, apiProps.apiName);
// Check for migration
if (!apigwInputState.cliInputsFileExists()) {
await apigwInputState.migrateAdminQueries(apiProps);
} else {
return apigwInputState.updateAdminQueriesResource(apiProps);
}
}
export async function console(context: $TSContext, service: string) {
const { serviceWalkthroughFilename } = await serviceMetadataFor(service);
const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename);
const { openConsole } = await import(serviceWalkthroughSrc);
if (!openConsole) {
const errMessage = 'Opening console functionality not available for this option';
printer.error(errMessage);
await context.usageData.emitError(new NotImplementedError(errMessage));
exitOnNextTick(0);
}
return openConsole(context);
}
async function addContainerResource(context: $TSContext, service: string, options, apiType: API_TYPE) {
const serviceWalkthroughFilename = 'containers-walkthrough.js';
const serviceWalkthrough = await getServiceWalkthrough(serviceWalkthroughFilename);
const serviceWalkthroughPromise: Promise<any> = serviceWalkthrough(context, apiType);
return await addContainer(serviceWalkthroughPromise, context, category, service, options, apiType);
}
async function addNonContainerResource(context: $TSContext, service: string, options) {
const serviceMetadata = await serviceMetadataFor(service);
const { serviceWalkthroughFilename, defaultValuesFilename } = serviceMetadata;
const serviceWalkthrough = await getServiceWalkthrough(serviceWalkthroughFilename);
const serviceWalkthroughPromise: Promise<any> = serviceWalkthrough(context, serviceMetadata);
switch (service) {
case AmplifySupportedService.APPSYNC: {
const walkthroughResult = await serviceWalkthroughPromise;
const askToEdit = walkthroughResult.askToEdit;
const apiName = await getCfnApiArtifactHandler(context).createArtifacts(serviceWalkthroughResultToAddApiRequest(walkthroughResult));
if (askToEdit) {
await editSchemaFlow(context, apiName);
}
return apiName;
}
case AmplifySupportedService.APIGW: {
const apigwInputState = new ApigwInputState(context);
return apigwInputState.addApigwResource(serviceWalkthroughPromise, options);
}
default:
return legacyAddResource(serviceWalkthroughPromise, context, category, service, options);
}
}
export async function addResource(context: $TSContext, service: string, options) {
let useContainerResource = false;
let apiType = API_TYPE.GRAPHQL;
if (isContainersEnabled(context)) {
switch (service) {
case AmplifySupportedService.APPSYNC:
useContainerResource = await isGraphQLContainer();
apiType = API_TYPE.GRAPHQL;
break;
case AmplifySupportedService.APIGW:
useContainerResource = await isRestContainer();
apiType = API_TYPE.REST;
break;
default:
throw new Error(`${service} not exists`);
}
}
return useContainerResource
? addContainerResource(context, service, options, apiType)
: addNonContainerResource(context, service, options);
}
function isContainersEnabled(context: $TSContext) {
const { frontend } = context.amplify.getProjectConfig();
if (frontend) {
const { config: { ServerlessContainers = false } = {} } = context.amplify.getProjectConfig()[frontend] || {};
return ServerlessContainers;
}
return false;
}
async function isGraphQLContainer(): Promise<boolean> {
const { graphqlSelection } = await inquirer.prompt({
name: 'graphqlSelection',
message: 'Which service would you like to use',
type: 'list',
choices: [
{
name: AmplifySupportedService.APPSYNC,
value: false,
},
{
name: 'AWS Fargate (Container-based)',
value: true,
},
],
});
return graphqlSelection;
}
async function isRestContainer() {
const { restSelection } = await inquirer.prompt({
name: 'restSelection',
message: 'Which service would you like to use',
type: 'list',
choices: [
{
name: 'API Gateway + Lambda',
value: false,
},
{
name: 'API Gateway + AWS Fargate (Container-based)',
value: true,
},
],
});
return restSelection;
}
export async function updateResource(context: $TSContext, category: string, service: string, options) {
const allowContainers = options?.allowContainers ?? true;
let useContainerResource = false;
let apiType = API_TYPE.GRAPHQL;
if (allowContainers && isContainersEnabled(context)) {
const { hasAPIGatewayContainerResource, hasAPIGatewayLambdaResource, hasGraphQLAppSyncResource, hasGraphqlContainerResource } =
await describeApiResourcesBySubCategory(context);
switch (service) {
case AmplifySupportedService.APPSYNC:
if (hasGraphQLAppSyncResource && hasGraphqlContainerResource) {
useContainerResource = await isGraphQLContainer();
} else if (hasGraphqlContainerResource) {
useContainerResource = true;
} else {
useContainerResource = false;
}
apiType = API_TYPE.GRAPHQL;
break;
case AmplifySupportedService.APIGW:
if (hasAPIGatewayContainerResource && hasAPIGatewayLambdaResource) {
useContainerResource = await isRestContainer();
} else if (hasAPIGatewayContainerResource) {
useContainerResource = true;
} else {
useContainerResource = false;
}
apiType = API_TYPE.REST;
break;
default:
throw new Error(`${service} not exists`);
}
}
return useContainerResource ? updateContainerResource(context, category, service, apiType) : updateNonContainerResource(context, service);
}
async function describeApiResourcesBySubCategory(context: $TSContext) {
const { allResources } = await context.amplify.getResourceStatus();
const resources = allResources.filter((resource) => resource.category === category && resource.mobileHubMigrated !== true);
let hasAPIGatewayContainerResource = false;
let hasAPIGatewayLambdaResource = false;
let hasGraphQLAppSyncResource = false;
let hasGraphqlContainerResource = false;
resources.forEach((resource) => {
hasAPIGatewayContainerResource =
hasAPIGatewayContainerResource || (resource.service === 'ElasticContainer' && resource.apiType === API_TYPE.REST);
hasAPIGatewayLambdaResource = hasAPIGatewayLambdaResource || resource.service === AmplifySupportedService.APIGW;
hasGraphQLAppSyncResource = hasGraphQLAppSyncResource || resource.service === AmplifySupportedService.APPSYNC;
hasGraphqlContainerResource =
hasGraphqlContainerResource || (resource.service === 'ElasticContainer' && resource.apiType === API_TYPE.GRAPHQL);
});
return {
hasAPIGatewayLambdaResource,
hasAPIGatewayContainerResource,
hasGraphQLAppSyncResource,
hasGraphqlContainerResource,
};
}
async function updateContainerResource(context: $TSContext, category: string, service: string, apiType: API_TYPE) {
const serviceWalkthroughFilename = 'containers-walkthrough';
const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename);
const { updateWalkthrough } = await import(serviceWalkthroughSrc);
if (!updateWalkthrough) {
const errMessage = 'Update functionality not available for this option';
printer.error(errMessage);
await context.usageData.emitError(new NotImplementedError(errMessage));
exitOnNextTick(0);
}
const updateWalkthroughPromise: Promise<ServiceConfiguration> = updateWalkthrough(context, apiType);
await updateContainer(updateWalkthroughPromise, context, category);
}
async function updateNonContainerResource(context: $TSContext, service: string) {
const serviceMetadata = await serviceMetadataFor(service);
const { defaultValuesFilename, serviceWalkthroughFilename } = serviceMetadata;
const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename);
const { updateWalkthrough } = await import(serviceWalkthroughSrc);
if (!updateWalkthrough) {
const errMessage = 'Update functionality not available for this option';
printer.error(errMessage);
await context.usageData.emitError(new NotImplementedError(errMessage));
exitOnNextTick(0);
}
const updateWalkthroughPromise: Promise<UpdateApiRequest> = updateWalkthrough(context, defaultValuesFilename, serviceMetadata);
switch (service) {
case AmplifySupportedService.APPSYNC:
return updateWalkthroughPromise.then(getCfnApiArtifactHandler(context).updateArtifacts);
default: {
const apigwInputState = new ApigwInputState(context);
return apigwInputState.updateApigwResource(updateWalkthroughPromise);
}
}
}
export async function migrateResource(context: $TSContext, projectPath: string, service: string, resourceName: string) {
if (service === 'ElasticContainer') {
return migrateResourceContainer(context, projectPath, service, resourceName);
} else {
return migrateResourceNonContainer(context, projectPath, service, resourceName);
}
}
async function migrateResourceContainer(context: $TSContext, projectPath: string, service: string, resourceName: string) {
printer.info(`No migration required for ${resourceName}`);
return;
}
async function migrateResourceNonContainer(context: $TSContext, projectPath: string, service: string, resourceName: string) {
const serviceMetadata = await serviceMetadataFor(service);
const { serviceWalkthroughFilename } = serviceMetadata;
const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename);
const { migrate } = await import(serviceWalkthroughSrc);
if (!migrate) {
printer.info(`No migration required for ${resourceName}`);
return;
}
return await migrate(context, projectPath, resourceName);
}
export async function addDatasource(context: $TSContext, category, datasource) {
const serviceMetadata = await datasourceMetadataFor(datasource);
const { serviceWalkthroughFilename } = serviceMetadata;
return (await getServiceWalkthrough(serviceWalkthroughFilename))(context, serviceMetadata);
}
export async function getPermissionPolicies(context: $TSContext, service: string, resourceName: string, crudOptions) {
if (service === 'ElasticContainer') {
return getPermissionPoliciesContainer(context, service, resourceName, crudOptions);
} else {
return getPermissionPoliciesNonContainer(service, resourceName, crudOptions);
}
}
async function getPermissionPoliciesContainer(context: $TSContext, service: string, resourceName: string, crudOptions) {
return getContainerPermissionPolicies(context, service, resourceName, crudOptions);
}
async function getPermissionPoliciesNonContainer(service: string, resourceName: string, crudOptions: string[]) {
const serviceMetadata = await serviceMetadataFor(service);
const { serviceWalkthroughFilename } = serviceMetadata;
const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename);
const { getIAMPolicies } = await import(serviceWalkthroughSrc);
if (!getIAMPolicies) {
printer.info(`No policies found for ${resourceName}`);
return;
}
return getIAMPolicies(resourceName, crudOptions);
}