UNPKG

@aws-amplify/amplify-category-api

Version:
255 lines (203 loc) 8.77 kB
import * as path from 'path'; import { mergeTypeDefs } from '@graphql-tools/merge'; import { $TSContext, AmplifyError, exitOnNextTick, FeatureFlags, pathManager, ResourceDoesNotExistError, stateManager, } from '@aws-amplify/amplify-cli-core'; import { printer } from '@aws-amplify/amplify-prompts'; import * as fs from 'fs-extra'; import * as graphql from 'graphql'; import { AuroraServerlessMySQLDatabaseReader, RelationalDBSchemaTransformer, RelationalDBTemplateGenerator, } from 'graphql-relational-schema-transformer'; import inquirer from 'inquirer'; import _ from 'lodash'; import { getEnvParamManager } from '@aws-amplify/amplify-environment-parameters'; import { supportedDataSources } from '../../provider-utils/supported-datasources'; const subcommand = 'add-graphql-datasource'; const category = 'api'; const providerName = 'awscloudformation'; export const name = subcommand; /** * Entry point for adding RDS data source */ export const run = async (context: $TSContext): Promise<void> => { try { const AWS = await getAwsClient(context, 'list'); const result: any = await datasourceSelectionPrompt(context, supportedDataSources); const providerController = await import(path.join('..', '..', 'provider-utils', result.providerName, 'index')); if (!providerController) { printer.error('Provider not configured for this category'); return; } const { datasource } = result; const answers = await providerController.addDatasource(context, category, datasource); const { resourceName, databaseName } = answers; /** * Setting the params using getEnvParamManager.getResourceParamManager.setParams updates * the team-provider-info file */ const currentEnv = context.amplify.getEnvInfo().envName; getEnvParamManager(currentEnv).getResourceParamManager(category, resourceName).setParams({ rdsRegion: answers.region, rdsClusterIdentifier: answers.dbClusterArn, rdsSecretStoreArn: answers.secretStoreArn, rdsDatabaseName: answers.databaseName, }); const backendConfig = stateManager.getBackendConfig(); backendConfig[category][resourceName].rdsInit = true; stateManager.setBackendConfig(undefined, backendConfig); /** * Load the MySqlRelationalDBReader */ const dbReader = new AuroraServerlessMySQLDatabaseReader( answers.region, answers.secretStoreArn, answers.dbClusterArn, answers.databaseName, AWS, ); /** * Instantiate a new Relational Schema Transformer and perform * the db introspection to get the GraphQL Schema and Template Context */ // eslint-disable-next-line spellcheck/spell-checker const improvePluralizationFlag = FeatureFlags.getBoolean('graphqltransformer.improvePluralization'); const relationalSchemaTransformer = new RelationalDBSchemaTransformer(dbReader, answers.databaseName, improvePluralizationFlag); const graphqlSchemaContext = await relationalSchemaTransformer.introspectDatabaseSchema(); if (graphqlSchemaContext === null) { printer.warn('No importable tables were found in the selected Database.'); printer.blankLine(); return; } /** * Merge the GraphQL Schema with the existing schema.graphql in the projects stack * */ const apiDirPath = pathManager.getResourceDirectoryPath(undefined, category, resourceName); fs.ensureDirSync(apiDirPath); const graphqlSchemaFilePath = path.join(apiDirPath, 'schema.graphql'); const rdsGraphQLSchemaDoc = graphqlSchemaContext.schemaDoc; const schemaDirectoryPath = path.join(apiDirPath, 'schema'); if (fs.existsSync(graphqlSchemaFilePath)) { const typesToBeMerged = [rdsGraphQLSchemaDoc]; const currentGraphQLSchemaDoc = readSchema(graphqlSchemaFilePath); if (currentGraphQLSchemaDoc) { typesToBeMerged.unshift(currentGraphQLSchemaDoc); } else { printer.warn(`Graphql Schema file "${graphqlSchemaFilePath}" is empty.`); printer.blankLine(); } const concatGraphQLSchemaDoc = mergeTypeDefs(typesToBeMerged); fs.writeFileSync(graphqlSchemaFilePath, graphql.print(concatGraphQLSchemaDoc), 'utf8'); } else if (fs.existsSync(schemaDirectoryPath)) { const rdsSchemaFilePath = path.join(schemaDirectoryPath, 'sql.graphql'); fs.writeFileSync(rdsSchemaFilePath, graphql.print(rdsGraphQLSchemaDoc), 'utf8'); } else { throw new AmplifyError('ApiCategorySchemaNotFoundError', { message: 'No schema found', resolution: `Your SQL schema should be in either ${graphqlSchemaFilePath} or schema directory ${schemaDirectoryPath}`, }); } const resolversDir = path.join(apiDirPath, 'resolvers'); /** * Instantiate a new Relational Template Generator and create * the template and relational resolvers */ const templateGenerator = new RelationalDBTemplateGenerator(graphqlSchemaContext); let template = templateGenerator.createTemplate(context); template = templateGenerator.addRelationalResolvers(template, resolversDir, improvePluralizationFlag); const cfn = templateGenerator.printCloudformationTemplate(template); /** * Add the generated the CFN to the appropriate nested stacks directory */ const stacksDir = path.join(apiDirPath, 'stacks'); const writeToPath = path.join(stacksDir, `${resourceName}-${databaseName}-rds.json`); fs.writeFileSync(writeToPath, cfn, 'utf8'); await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { forceCompile: true }); printer.success(`Successfully added the ${datasource} datasource locally`); printer.blankLine(); printer.success('Some next steps:'); printer.info('"amplify push" will build all your local backend resources and provision it in the cloud'); printer.info( '"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud', ); printer.blankLine(); } catch (error) { printer.error('There was an error adding the datasource'); throw error; } }; // eslint-disable-next-line @typescript-eslint/no-shadow const datasourceSelectionPrompt = async (context: $TSContext, supportedDataSources): Promise<unknown> => { const options = []; Object.keys(supportedDataSources).forEach((datasource) => { const optionName = supportedDataSources[datasource].alias || `${supportedDataSources[datasource].providerName}:${supportedDataSources[datasource].service}`; options.push({ name: optionName, value: { provider: supportedDataSources[datasource].provider, datasource, providerName: supportedDataSources[datasource].provider, }, }); }); if (options.length === 0) { const errMessage = `No data sources defined by configured providers for category: ${category}`; printer.error(errMessage); await context.usageData.emitError(new ResourceDoesNotExistError(errMessage)); exitOnNextTick(1); } if (options.length === 1) { // No need to ask questions printer.info(`Using datasource: ${options[0].value.datasource}, provided by: ${options[0].value.providerName}`); return new Promise((resolve) => { resolve(options[0].value); }); } const question = [ { name: 'datasource', message: 'Please select from one of the below mentioned data sources', type: 'list', choices: options, }, ]; return inquirer.prompt(question).then((answer) => answer.datasource); }; const getAwsClient = async (context: $TSContext, action: string): Promise<any> => { const providerPlugins = context.amplify.getProviderPlugins(context); // eslint-disable-next-line const provider = require(providerPlugins[providerName]); return provider.getConfiguredAWSClient(context, 'aurora-serverless', action); }; /** * Read the GraphQL schema */ export const readSchema = (graphqlSchemaFilePath: string): graphql.DocumentNode => { const graphqlSchemaRaw = fs.readFileSync(graphqlSchemaFilePath).toString(); if (graphqlSchemaRaw.trim().length === 0) { return null; } let currentGraphQLSchemaDoc: graphql.DocumentNode; try { currentGraphQLSchemaDoc = graphql.parse(graphqlSchemaRaw); } catch (err) { const relativePathToInput = path.relative(process.cwd(), graphqlSchemaRaw); throw new AmplifyError('UserInputError', { message: `Could not parse graphql scehma \n${relativePathToInput}\n`, details: err.message, link: 'https://docs.amplify.aws/cli-legacy/graphql-transformer/relational/', }); } return currentGraphQLSchemaDoc; };