diff --git a/packages/amplify-category-api/.npmignore b/packages/amplify-category-api/.npmignore index 3af03e1d7c0..89eeb1ee92a 100644 --- a/packages/amplify-category-api/.npmignore +++ b/packages/amplify-category-api/.npmignore @@ -1,6 +1,5 @@ **/__mocks__/** **/__tests__/** ./src -!resources/overrides-resource/tsconfig.json tsconfig.json tsconfig.tsbuildinfo diff --git a/packages/amplify-category-api/amplify-plugin.json b/packages/amplify-category-api/amplify-plugin.json index f0a7bb3671a..8bd97a6a3c7 100644 --- a/packages/amplify-category-api/amplify-plugin.json +++ b/packages/amplify-category-api/amplify-plugin.json @@ -1,7 +1,7 @@ { "name": "api", "type": "category", - "commands": ["add-graphql-datasource", "add", "console", "gql-compile", "override", "push", "rebuild", "remove", "update", "help"], + "commands": ["add-graphql-datasource", "add", "console", "gql-compile", "push", "rebuild", "remove", "update", "help"], "commandAliases": { "configure": "update" }, diff --git a/packages/amplify-category-api/package.json b/packages/amplify-category-api/package.json index 8bf589116de..fa76cb59454 100644 --- a/packages/amplify-category-api/package.json +++ b/packages/amplify-category-api/package.json @@ -1,6 +1,6 @@ { - "name": "@aws-amplify/amplify-category-api", - "version": "1.0.0", + "name": "amplify-category-api", + "version": "2.33.2", "description": "amplify-cli api plugin", "repository": { "type": "git", @@ -14,8 +14,7 @@ "build": "tsc", "watch": "tsc -w", "clean": "rimraf lib tsconfig.tsbuildinfo", - "test": "jest", - "generateSchemas": "ts-node ./scripts/generateApiSchemas.ts" + "test": "jest" }, "dependencies": { "@aws-cdk/assets": "~1.124.0", @@ -79,7 +78,7 @@ "js-yaml": "^4.0.0", "lodash": "^4.17.21", "ora": "^4.0.3", - "uuid": "^8.3.2" + "uuid": "^3.4.0" }, "devDependencies": { "@types/js-yaml": "^4.0.0" diff --git a/packages/amplify-category-api/resources/awscloudformation/cloudformation-templates/apigw-cloudformation-template-default.json.ejs b/packages/amplify-category-api/resources/awscloudformation/cloudformation-templates/apigw-cloudformation-template-default.json.ejs new file mode 100644 index 00000000000..959fb43c00d --- /dev/null +++ b/packages/amplify-category-api/resources/awscloudformation/cloudformation-templates/apigw-cloudformation-template-default.json.ejs @@ -0,0 +1,512 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "API Gateway resource stack creation using Amplify CLI", + <% if (props.dependsOn) { %> + "Parameters": { + "env": { + "Type": "String" + }<%if (props.dependsOn && props.dependsOn.length > 0) { %>,<% } %> + <% for(var i=0; i < props.dependsOn.length; i++) { %> + <% for(var j=0; j < props.dependsOn[i].attributes.length; j++) { %> + "<%= props.dependsOn[i].category %><%= props.dependsOn[i].resourceName %><%= props.dependsOn[i].attributes[j] %>": { + "Type": "String", + "Default": "<%= props.dependsOn[i].category %><%= props.dependsOn[i].resourceName %><%= props.dependsOn[i].attributes[j] %>" + }<%if (i !== props.dependsOn.length - 1 || j !== props.dependsOn[i].attributes.length - 1) { %>,<% } %> + + <% } %> + <% } %> + <% } %> + }, + "Conditions": { + "ShouldNotCreateEnvResources": { + "Fn::Equals": [ + { + "Ref": "env" + }, + "NONE" + ] + } + }, + "Resources": { + <% for(var i=0; i < props.paths.length; i++) { %> + <%if (props.paths[i].privacy && props.paths[i].privacy.userPoolGroups) { %> + <% let selectedUserPoolGroupList = Object.keys(props.paths[i].privacy.userPoolGroups); %> + <% for(var j=0; j < selectedUserPoolGroupList.length; j++) { %> + "<%=selectedUserPoolGroupList[j]%>Group<%= props.paths[i].name.replace(/[^-a-z0-9]/g, '')%>Policy": { + "DependsOn": [ + "<%= props.apiName %>" + ], + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyName": "<%= props.apiName %>-<%= props.paths[i].name.replace(/[^-a-z0-9]/g, '')%>-<%=selectedUserPoolGroupList[j]%>-group-policy", + "Roles": [ + { + "Fn::Join": [ + "", + [ + { + "Ref": "auth<%= props.authResourceName%>UserPoolId" + }, + "-<%=selectedUserPoolGroupList[j]%>GroupRole" + ] + ] + } + ], + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "execute-api:Invoke" + ], + "Resource": [ + + <% for(var x=0; x < props.paths[i].privacy.userPoolGroups[selectedUserPoolGroupList[j]].length; x++) { %> + { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "<%= props.apiName %>" + }, + "/", + { + "Fn::If": [ + "ShouldNotCreateEnvResources", + "Prod", + { + "Ref": "env" + } + ] + }, + "<%= props.paths[i].privacy.userPoolGroups[selectedUserPoolGroupList[j]][x] %>", + "<%= props.paths[i].policyResourceName %>/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "<%= props.apiName %>" + }, + "/", + { + "Fn::If": [ + "ShouldNotCreateEnvResources", + "Prod", + { + "Ref": "env" + } + ] + }, + "<%= props.paths[i].privacy.userPoolGroups[selectedUserPoolGroupList[j]][x] %>", + "<%= props.paths[i].policyResourceName %>" + ] + ] + } + <% if (x !== props.paths[i].privacy.userPoolGroups[selectedUserPoolGroupList[j]].length - 1) { %> + , + <% } %> + <% } %> + ] + } + ] + } + } + }, + <% } %> + <% } %> + <% } %> + "<%= props.apiName %>": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Description": "", + "Name": "<%= props.apiName %>", + "Body": { + "swagger": "2.0", + "info": { + "version": "2018-05-24T17:52:00Z", + "title": "<%= props.apiName %>" + }, + "host": { + "Fn::Join": [ + "", + [ + "apigateway.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + }, + "basePath": { + "Fn::If": [ + "ShouldNotCreateEnvResources", + "/Prod", + { + "Fn::Join": [ + "", + [ + "/", + { + "Ref": "env" + } + ] + ] + } + ] + }, + "schemes": [ + "https" + ], + "paths": { + <% for(var i=0; i < props.paths.length; i++) { %> + "<%= props.paths[i].name %>": { + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "passthroughBehavior": "when_no_match", + "type": "mock" + } + }, + "x-amazon-apigateway-any-method": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "RequestSchema", + "required": false, + "schema": { + "$ref": "#/definitions/RequestSchema" + } + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/ResponseSchema" + } + } + }, + <%if (!props.paths[i].privacy.open) { %> + "security": [ + { + "sigv4": [] + } + ], + <% } %> + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200" + } + }, + "uri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + <% if (props.paths[i].lambdaArn ) { %> + "<%= props.paths[i].lambdaArn %>", + <% } else { %> + { + + "Ref": "function<%= props.paths[i].lambdaFunction %>Arn" + }, + <% } %> + "/invocations" + ] + ] + }, + "passthroughBehavior": "when_no_match", + "httpMethod": "POST", + "type": "aws_proxy" + } + } + }, + "<%= props.paths[i].name %>/{proxy+}": { + "options": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "200 response", + "headers": { + "Access-Control-Allow-Origin": { + "type": "string" + }, + "Access-Control-Allow-Methods": { + "type": "string" + }, + "Access-Control-Allow-Headers": { + "type": "string" + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200", + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'", + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "passthroughBehavior": "when_no_match", + "type": "mock" + } + }, + "x-amazon-apigateway-any-method": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "RequestSchema", + "required": false, + "schema": { + "$ref": "#/definitions/RequestSchema" + } + } + ], + "responses": { + "200": { + "description": "200 response", + "schema": { + "$ref": "#/definitions/ResponseSchema" + } + } + }, + <%if (!props.paths[i].privacy.open) { %> + "security": [ + { + "sigv4": [] + } + ], + <% } %> + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200" + } + }, + "uri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + <% if (props.paths[i].lambdaArn) { %> + "<%= props.paths[i].lambdaArn %>", + <% } else { %> + { + + "Ref": "function<%= props.paths[i].lambdaFunction %>Arn" + }, + <% } %> + "/invocations" + ] + ] + }, + "passthroughBehavior": "when_no_match", + "httpMethod": "POST", + "type": "aws_proxy" + } + } + }<% if (i !== props.paths.length - 1) { %>,<% } %> + <% } %> + }, + "securityDefinitions": { + "sigv4": { + "type": "apiKey", + "name": "Authorization", + "in": "header", + "x-amazon-apigateway-authtype": "awsSigv4" + } + }, + "definitions": { + "RequestSchema": { + "type": "object", + "required": [ + "request" + ], + "properties": { + "request": { + "type": "string" + } + }, + "title": "Request Schema" + }, + "ResponseSchema": { + "type": "object", + "required": [ + "response" + ], + "properties": { + "response": { + "type": "string" + } + }, + "title": "Response Schema" + } + } + }, + "FailOnWarnings": true + } + }, + + <%if (props.functionArns) { %> + <% for (var i=0; i < props.functionArns.length; i++) { %> + + "function<%= props.functionArns[i].lambdaFunction.replace(/[^0-9a-zA-Z]/gi, '') %>Permission<%= props.apiName %>": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": <% if (props.functionArns[i].lambdaArn) {%> "<%= props.functionArns[i].lambdaArn %>", <% } else { %> + { + "Ref": "function<%= props.functionArns[i].lambdaFunction %>Name" + }, + <% } %> + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "<%= props.apiName %>" + }, + "/*/*/*" + ] + ] + } + } + }, + <% } %> + <% } %> + + "DeploymentAPIGW<%= props.apiName %><%= props.uuid %>": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "The Development stage deployment of your API.", + "StageName": { + "Fn::If": [ + "ShouldNotCreateEnvResources", + "Prod", + { + "Ref": "env" + } + ] + }, + "RestApiId": { + "Ref": "<%= props.apiName %>" + } + } + } + }, + "Outputs": { + "RootUrl": { + "Description": "Root URL of the API gateway", + "Value": {"Fn::Join": ["", ["https://", {"Ref": "<%= props.apiName %>"}, ".execute-api.", {"Ref": "AWS::Region"}, ".amazonaws.com/", {"Fn::If": ["ShouldNotCreateEnvResources","Prod", {"Ref": "env"} ]}]]} + }, + "ApiName": { + "Description": "API Friendly name", + "Value": "<%= props.resourceName %>" + }, + "ApiId": { + "Description": "API ID (prefix of API URL)", + "Value": {"Ref": "<%= props.apiName %>"} + } + } + } diff --git a/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/override.ts b/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/override.ts deleted file mode 100644 index ac3781476a1..00000000000 --- a/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/override.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file is used to override the REST API resource configuration -// import { AmplifyApigwResourceTemplate } from '@aws-amplify/cli-overrides-helper'; - -/* TODO: Need to change props to Root-Stack specific props when props are ready */ -export function overrideProps(props: any) { - /* Override props (AmplifyApigwResourceTemplate) with new parameters */ - return props; -} diff --git a/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/package.json b/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/package.json deleted file mode 100644 index 61f6fd48bc7..00000000000 --- a/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "overrides-for-root-stack", - "version": "1.0.0", - "description": "", - "scripts": { - "build": "tsc", - "watch": "tsc -w", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "fs-extra": "^9.1.0" - }, - "devDependencies": { - "@types/fs-extra": "^9.0.11", - "typescript": "^4.2.4" - } -} diff --git a/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/tsconfig.json b/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/tsconfig.json deleted file mode 100644 index c6f1a33b4d9..00000000000 --- a/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "strict": false, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "build" - } -} diff --git a/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/tsconfig.resource.json b/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/tsconfig.resource.json deleted file mode 100644 index 6504da80283..00000000000 --- a/packages/amplify-category-api/resources/awscloudformation/overrides-resource/APIGW/tsconfig.resource.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "strict": false, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "./", - "rootDir": "../" - }, - "include": ["../**/*"] -} diff --git a/packages/amplify-category-api/resources/schemas/aPIGateway/APIGatewayCLIInputs.schema.json b/packages/amplify-category-api/resources/schemas/aPIGateway/APIGatewayCLIInputs.schema.json deleted file mode 100644 index a0b2cbe39be..00000000000 --- a/packages/amplify-category-api/resources/schemas/aPIGateway/APIGatewayCLIInputs.schema.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "description": "Defines the json object expected by the amplify api category", - "type": "object", - "properties": { - "version": { - "description": "The schema version.", - "type": "number", - "enum": [1] - }, - "paths": { - "description": "map of paths in the REST API.", - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "lambdaFunction": { - "type": "string" - }, - "permissions": { - "type": "object", - "properties": { - "setting": { - "$ref": "#/definitions/PermissionSetting" - }, - "auth": { - "type": "array", - "items": { - "enum": ["CREATE", "DELETE", "READ", "UPDATE"], - "type": "string" - } - }, - "guest": { - "type": "array", - "items": { - "enum": ["CREATE", "DELETE", "READ", "UPDATE"], - "type": "string" - } - }, - "groups": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "enum": ["CREATE", "DELETE", "READ", "UPDATE"], - "type": "string" - } - } - } - } - }, - "required": ["setting"] - } - }, - "required": ["lambdaFunction", "permissions"] - } - } - }, - "required": ["paths", "version"], - "definitions": { - "PermissionSetting": { - "enum": ["open", "private", "protected"], - "type": "string" - } - }, - "$schema": "http://json-schema.org/draft-07/schema#" -} diff --git a/packages/amplify-category-api/scripts/generateApiSchemas.ts b/packages/amplify-category-api/scripts/generateApiSchemas.ts deleted file mode 100644 index e611a1a84b9..00000000000 --- a/packages/amplify-category-api/scripts/generateApiSchemas.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as SchemaGenerator from 'amplify-cli-core'; - -type TypeDef = SchemaGenerator.TypeDef; - -const ApigwTypeDef: TypeDef = { - typeName: 'APIGatewayCLIInputs', - service: 'API Gateway', -}; - -// Defines the type names and the paths to the TS files that define them -const apigwCategoryTypeDefs: TypeDef[] = [ApigwTypeDef]; - -const schemaGenerator = new SchemaGenerator.CLIInputSchemaGenerator(apigwCategoryTypeDefs); -schemaGenerator.generateJSONSchemas(); // convert CLI input data into json schemas. diff --git a/packages/amplify-category-api/src/__tests__/commands/api/add-graphql-datasource.test.ts b/packages/amplify-category-api/src/__tests__/commands/api/add-graphql-datasource.test.js similarity index 87% rename from packages/amplify-category-api/src/__tests__/commands/api/add-graphql-datasource.test.ts rename to packages/amplify-category-api/src/__tests__/commands/api/add-graphql-datasource.test.js index b0acae414dd..701aadaa8c5 100644 --- a/packages/amplify-category-api/src/__tests__/commands/api/add-graphql-datasource.test.ts +++ b/packages/amplify-category-api/src/__tests__/commands/api/add-graphql-datasource.test.js @@ -1,5 +1,5 @@ -import { readSchema } from '../../../commands/api/add-graphql-datasource'; -import * as path from 'path'; +const { readSchema } = require('../../../commands/api/add-graphql-datasource'); +const path = require('path'); describe('read schema', () => { it('Valid schema present in folder', async () => { diff --git a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/cfn-api-artifact-handler.test.ts b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/cfn-api-artifact-handler.test.ts index 1d3cbeefbfc..e8dfa51770a 100644 --- a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/cfn-api-artifact-handler.test.ts +++ b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/cfn-api-artifact-handler.test.ts @@ -1,23 +1,19 @@ -import { $TSContext, pathManager, stateManager } from 'amplify-cli-core'; +import path from 'path'; +import fs from 'fs-extra'; +import { ApiArtifactHandler } from '../../../provider-utils/api-artifact-handler'; +import { getCfnApiArtifactHandler } from '../../../provider-utils/awscloudformation/cfn-api-artifact-handler'; import { AddApiRequest, UpdateApiRequest } from 'amplify-headless-interface'; -import { printer } from 'amplify-prompts'; -import * as fs from 'fs-extra'; -import { writeTransformerConfiguration } from 'graphql-transformer-core'; -import _ from 'lodash'; -import * as path from 'path'; import { category } from '../../../category-constants'; -import { ApiArtifactHandler } from '../../../provider-utils/api-artifact-handler'; +import { writeTransformerConfiguration } from 'graphql-transformer-core'; import { rootAssetDir } from '../../../provider-utils/awscloudformation/aws-constants'; -import { getCfnApiArtifactHandler } from '../../../provider-utils/awscloudformation/cfn-api-artifact-handler'; import { - authConfigHasApiKey, - getAppSyncAuthConfig, getAppSyncResourceName, + getAppSyncAuthConfig, + authConfigHasApiKey, } from '../../../provider-utils/awscloudformation/utils/amplify-meta-utils'; +import _ from 'lodash'; jest.mock('fs-extra'); -const printer_mock = printer as jest.Mocked; -printer_mock.warn = jest.fn(); jest.mock('graphql-transformer-core', () => ({ readTransformerConfiguration: jest.fn(async () => ({})), @@ -34,26 +30,32 @@ jest.mock('../../../provider-utils/awscloudformation/utils/amplify-meta-utils', jest.mock('amplify-cli-core'); -const backendDirPathStub = 'backendDirPath'; -const testApiName = 'testApiName'; - -const pathManager_mock = pathManager as jest.Mocked; -pathManager_mock.getResourceDirectoryPath = jest.fn().mockReturnValue(`${backendDirPathStub}/api/${testApiName}`); -const stateManager_mock = stateManager as jest.Mocked; - const fs_mock = fs as unknown as jest.Mocked; const writeTransformerConfiguration_mock = writeTransformerConfiguration as jest.MockedFunction; const getAppSyncResourceName_mock = getAppSyncResourceName as jest.MockedFunction; const getAppSyncAuthConfig_mock = getAppSyncAuthConfig as jest.MockedFunction; const authConfigHasApiKey_mock = authConfigHasApiKey as jest.MockedFunction; +const backendDirPathStub = 'backendDirPath'; + +const testApiName = 'testApiName'; + const context_stub = { + print: { + success: jest.fn(), + warning: jest.fn(), + }, amplify: { updateamplifyMetaAfterResourceAdd: jest.fn(), updateamplifyMetaAfterResourceUpdate: jest.fn(), updateBackendConfigAfterResourceUpdate: jest.fn(), executeProviderUtils: jest.fn(), copyBatch: jest.fn(), + getProjectMeta: jest.fn(), + readJsonFile: jest.fn(), + pathManager: { + getBackendDirPath: jest.fn(() => backendDirPathStub), + }, }, }; @@ -78,7 +80,7 @@ describe('create artifacts', () => { }); beforeEach(() => { jest.clearAllMocks(); - cfnApiArtifactHandler = getCfnApiArtifactHandler(context_stub as unknown as $TSContext); + cfnApiArtifactHandler = getCfnApiArtifactHandler(context_stub); }); it('does not create a second API if one already exists', async () => { @@ -178,7 +180,7 @@ describe('update artifacts', () => { beforeEach(() => { jest.clearAllMocks(); updateRequestStub = _.cloneDeep(updateRequestStubBase); - cfnApiArtifactHandler = getCfnApiArtifactHandler(context_stub as unknown as $TSContext); + cfnApiArtifactHandler = getCfnApiArtifactHandler(context_stub); }); it('throws error if no GQL API in project', () => { @@ -234,12 +236,12 @@ describe('update artifacts', () => { it('prints warning when adding API key auth', async () => { authConfigHasApiKey_mock.mockImplementationOnce(() => false).mockImplementationOnce(() => true); await cfnApiArtifactHandler.updateArtifacts(updateRequestStub); - expect(printer_mock.warn.mock.calls.length).toBe(2); + expect(context_stub.print.warning.mock.calls.length).toBe(2); }); it('prints warning when removing API key auth', async () => { authConfigHasApiKey_mock.mockImplementationOnce(() => true).mockImplementationOnce(() => false); await cfnApiArtifactHandler.updateArtifacts(updateRequestStub); - expect(printer_mock.warn.mock.calls.length).toBe(3); + expect(context_stub.print.warning.mock.calls.length).toBe(3); }); }); diff --git a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/legacy-add-resource.test.ts b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/legacy-add-resource.test.ts index ada52d192ce..4dc7ed152f7 100644 --- a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/legacy-add-resource.test.ts +++ b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/legacy-add-resource.test.ts @@ -1,29 +1,22 @@ import { legacyAddResource } from '../../../provider-utils/awscloudformation/legacy-add-resource'; import { category } from '../../../category-constants'; -import { $TSAny, $TSContext } from 'amplify-cli-core'; jest.mock('fs-extra'); -jest.mock('amplify-cli-core', () => ({ - isResourceNameUnique: jest.fn().mockReturnValue(true), - JSONUtilities: { - readJson: jest.fn(), - writeJson: jest.fn(), - }, - pathManager: { - getResourceDirectoryPath: jest.fn(_ => 'mock/backend/path'), - }, -})); +jest.mock('amplify-cli-core'); describe('legacy add resource', () => { const contextStub = { amplify: { + pathManager: { + getBackendDirPath: jest.fn(_ => 'mock/backend/path'), + }, updateamplifyMetaAfterResourceAdd: jest.fn(), copyBatch: jest.fn(), }, }; it('sets policy resource name in paths object before copying template', async () => { - const stubWalkthroughPromise: Promise<$TSAny> = Promise.resolve({ + const stubWalkthroughPromise: Promise = Promise.resolve({ answers: { resourceName: 'mockResourceName', paths: [ @@ -36,7 +29,7 @@ describe('legacy add resource', () => { ], }, }); - await legacyAddResource(stubWalkthroughPromise, contextStub as unknown as $TSContext, category, 'API Gateway', {}); + await legacyAddResource(stubWalkthroughPromise, contextStub, category, 'API Gateway', {}); expect(contextStub.amplify.copyBatch.mock.calls[0][2]).toMatchSnapshot(); }); }); diff --git a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/legacy-update-resource.test.ts b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/legacy-update-resource.test.ts index cf6e04f21fc..135f62250fa 100644 --- a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/legacy-update-resource.test.ts +++ b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/legacy-update-resource.test.ts @@ -1,21 +1,14 @@ -import { $TSContext } from 'amplify-cli-core'; import { legacyUpdateResource } from '../../../provider-utils/awscloudformation/legacy-update-resource'; import { category } from '../../../category-constants'; jest.mock('fs-extra'); -jest.mock('amplify-cli-core', () => ({ - JSONUtilities: { - readJson: jest.fn(), - writeJson: jest.fn(), - }, - pathManager: { - getResourceDirectoryPath: jest.fn(_ => 'mock/backend/path'), - }, -})); describe('legacy update resource', () => { const contextStub = { amplify: { + pathManager: { + getBackendDirPath: jest.fn(_ => 'mock/backend/path'), + }, updateamplifyMetaAfterResourceUpdate: jest.fn(), copyBatch: jest.fn(), }, @@ -35,7 +28,7 @@ describe('legacy update resource', () => { ], }, }); - await legacyUpdateResource(stubWalkthroughPromise, contextStub as unknown as $TSContext, category, 'API Gateway'); + await legacyUpdateResource(stubWalkthroughPromise, contextStub, category, 'API Gateway'); expect(contextStub.amplify.copyBatch.mock.calls[0][2]).toMatchSnapshot(); }); }); diff --git a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.test.ts b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.test.ts index 9b2db83f7b8..6e6a75c8d8c 100644 --- a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.test.ts +++ b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.test.ts @@ -1,51 +1,42 @@ -import { $TSAny, $TSContext, FeatureFlags, pathManager, stateManager } from 'amplify-cli-core'; import { - askAdditionalAuthQuestions, getIAMPolicies, + askAdditionalAuthQuestions, } from '../../../../provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough'; import { authConfigHasApiKey, getAppSyncAuthConfig } from '../../../../provider-utils/awscloudformation/utils/amplify-meta-utils'; - +import { FeatureFlags } from 'amplify-cli-core'; jest.mock('../../../../provider-utils/awscloudformation/utils/amplify-meta-utils', () => ({ getAppSyncAuthConfig: jest.fn(), authConfigHasApiKey: jest.fn(), })); jest.mock('amplify-cli-core'); -const stateManager_mock = stateManager as jest.Mocked; -stateManager_mock.getMeta = jest.fn(); - -const pathManager_mock = pathManager as jest.Mocked; -pathManager_mock.getResourceDirectoryPath = jest.fn().mockReturnValue('mocked/resource/path'); - const mockGetBoolean = FeatureFlags.getBoolean as jest.Mock; const authConfigHasApiKey_mock = authConfigHasApiKey as jest.MockedFunction; const getAppSyncAuthConfig_mock = getAppSyncAuthConfig as jest.MockedFunction; const confirmPromptFalse_mock = jest.fn(() => false); -const context_stub = (prompt: jest.Mock) => - ({ - prompt: { - confirm: prompt, - }, - amplify: { - getProjectMeta: jest.fn(), - }, - } as unknown as $TSContext); +const context_stub = (prompt: jest.Mock) => ({ + prompt: { + confirm: prompt, + }, + amplify: { + getProjectMeta: jest.fn(), + }, +}); type IAMArtifact = { attributes: string[]; - policy: $TSAny; + policy: any; }; describe('get IAM policies', () => { beforeEach(() => { jest.resetModules(); }); - it('does not include API key if none exists', async () => { mockGetBoolean.mockImplementationOnce(() => true); authConfigHasApiKey_mock.mockImplementationOnce(() => false); - const iamArtifact: IAMArtifact = getIAMPolicies('testResourceName', ['Query']); + const iamArtifact: IAMArtifact = getIAMPolicies('testResourceName', ['Query'], context_stub(confirmPromptFalse_mock)); expect(iamArtifact.attributes).toMatchInlineSnapshot(` Array [ "GraphQLAPIIdOutput", @@ -58,7 +49,7 @@ describe('get IAM policies', () => { it('includes API key if it exists', async () => { mockGetBoolean.mockImplementationOnce(() => true); authConfigHasApiKey_mock.mockImplementationOnce(() => true); - const iamArtifact: IAMArtifact = getIAMPolicies('testResourceName', ['Query']); + const iamArtifact: IAMArtifact = getIAMPolicies('testResourceName', ['Query'], context_stub(confirmPromptFalse_mock)); expect(iamArtifact.attributes).toMatchInlineSnapshot(` Array [ "GraphQLAPIIdOutput", @@ -72,7 +63,7 @@ describe('get IAM policies', () => { it('policy path includes the new format for graphql operations', async () => { mockGetBoolean.mockImplementationOnce(() => true); authConfigHasApiKey_mock.mockImplementationOnce(() => false); - const iamArtifact: IAMArtifact = getIAMPolicies('testResourceName', ['Query', 'Mutate']); + const iamArtifact: IAMArtifact = getIAMPolicies('testResourceName', ['Query', 'Mutate'], context_stub(confirmPromptFalse_mock)); expect(iamArtifact.attributes).toMatchInlineSnapshot(` Array [ "GraphQLAPIIdOutput", @@ -82,11 +73,10 @@ describe('get IAM policies', () => { expect(iamArtifact.policy.Resource[0]['Fn::Join'][1][6]).toMatch('/types/Query/*'); expect(iamArtifact.policy.Resource[1]['Fn::Join'][1][6]).toMatch('/types/Mutate/*'); }); - it('policy path includes the old format for appsync api operations', async () => { mockGetBoolean.mockImplementationOnce(() => false); authConfigHasApiKey_mock.mockImplementationOnce(() => false); - const iamArtifact: IAMArtifact = getIAMPolicies('testResourceName', ['create', 'update']); + const iamArtifact: IAMArtifact = getIAMPolicies('testResourceName', ['create', 'update'], context_stub(confirmPromptFalse_mock)); expect(iamArtifact.attributes).toMatchInlineSnapshot(` Array [ "GraphQLAPIIdOutput", diff --git a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/rest-api-path-utils.test.ts b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/rest-api-path-utils.test.ts index 6facce6d56d..7c82402f421 100644 --- a/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/rest-api-path-utils.test.ts +++ b/packages/amplify-category-api/src/__tests__/provider-utils/awscloudformation/utils/rest-api-path-utils.test.ts @@ -4,7 +4,7 @@ import { formatCFNPathParamsForExpressJs, } from '../../../../provider-utils/awscloudformation/utils/rest-api-path-utils'; -const stubOtherPaths = ['/other/path', '/sub/path', '/path/{with}/{params}']; +const stubOtherPaths = [{ name: '/other/path' }, { name: '/sub/path' }, { name: '/path/{with}/{params}' }]; test('validatePathName_validPath', () => { expect(validatePathName('/some/path')).toBe(true); @@ -56,9 +56,9 @@ test('checkForPathOverlap_subPathParamsNoMatch', () => { }); test('checkForPathOverlap_pathMatch', () => { - expect(checkForPathOverlap(stubOtherPaths[0], stubOtherPaths)).toEqual({ - higherOrderPath: stubOtherPaths[0], - lowerOrderPath: stubOtherPaths[0], + expect(checkForPathOverlap(stubOtherPaths[0].name, stubOtherPaths)).toEqual({ + higherOrderPath: stubOtherPaths[0].name, + lowerOrderPath: stubOtherPaths[0].name, }); }); diff --git a/packages/amplify-category-api/src/commands/api.js b/packages/amplify-category-api/src/commands/api.js new file mode 100644 index 00000000000..e8c261e00df --- /dev/null +++ b/packages/amplify-category-api/src/commands/api.js @@ -0,0 +1,55 @@ +const featureName = 'api'; + +module.exports = { + name: featureName, + run: async context => { + if (/^win/.test(process.platform)) { + try { + const { run } = require(`./${featureName}/${context.parameters.first}`); + return run(context); + } catch (e) { + context.print.error('Command not found'); + } + } + const header = `amplify ${featureName} `; + const commands = [ + { + name: 'add', + description: `Takes you through a CLI flow to add a ${featureName} resource to your local backend`, + }, + { + name: 'push', + description: `Provisions ${featureName} cloud resources and its dependencies with the latest local developments`, + }, + { + name: 'remove', + description: `Removes ${featureName} resource from your local backend which would be removed from the cloud on the next push command`, + }, + { + name: 'update', + description: `Takes you through steps in the CLI to update an ${featureName} resource`, + }, + { + name: 'gql-compile', + description: 'Compiles your GraphQL schema and generates a corresponding cloudformation template', + }, + { + name: 'add-graphql-datasource', + description: 'Provisions the AppSync resources and its dependencies for the provided Aurora Serverless data source', + }, + { + name: 'console', + description: 'Opens the web console for the selected api service', + }, + { + name: 'rebuild', + description: + 'Removes and recreates all DynamoDB tables backing a GraphQL API. Useful for resetting test data during the development phase of an app', + }, + ]; + + context.amplify.showHelp(header, commands); + + context.print.info(''); + }, +}; diff --git a/packages/amplify-category-api/src/commands/api.ts b/packages/amplify-category-api/src/commands/api.ts deleted file mode 100644 index d9741de9a9b..00000000000 --- a/packages/amplify-category-api/src/commands/api.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { $TSContext, AmplifyCategories } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; -import * as path from 'path'; - -export const name = AmplifyCategories.API; - -export const run = async (context: $TSContext) => { - if (/^win/.test(process.platform)) { - try { - const { run } = await import(path.join('.', AmplifyCategories.API, context.parameters.first)); - return run(context); - } catch (e) { - printer.error('Command not found'); - } - } - const header = `amplify ${AmplifyCategories.API} `; - const commands = [ - { - name: 'add', - description: `Takes you through a CLI flow to add a ${AmplifyCategories.API} resource to your local backend`, - }, - { - name: 'push', - description: `Provisions ${AmplifyCategories.API} cloud resources and its dependencies with the latest local developments`, - }, - { - name: 'remove', - description: `Removes ${AmplifyCategories.API} resource from your local backend which would be removed from the cloud on the next push command`, - }, - { - name: 'update', - description: `Takes you through steps in the CLI to update an ${AmplifyCategories.API} resource`, - }, - { - name: 'gql-compile', - description: 'Compiles your GraphQL schema and generates a corresponding cloudformation template', - }, - { - name: 'add-graphql-datasource', - description: 'Provisions the AppSync resources and its dependencies for the provided Aurora Serverless data source', - }, - { - name: 'console', - description: 'Opens the web console for the selected api service', - }, - { - name: 'rebuild', - description: - 'Removes and recreates all DynamoDB tables backing a GraphQL API. Useful for resetting test data during the development phase of an app', - }, - { - name: 'override', - description: 'Generates overrides file to apply custom modifications to CloudFormation', - }, - ]; - - context.amplify.showHelp(header, commands); - - printer.blankLine(); -}; diff --git a/packages/amplify-category-api/src/commands/api/add-graphql-datasource.ts b/packages/amplify-category-api/src/commands/api/add-graphql-datasource.ts index b9a235b802d..d4bd9be8d84 100644 --- a/packages/amplify-category-api/src/commands/api/add-graphql-datasource.ts +++ b/packages/amplify-category-api/src/commands/api/add-graphql-datasource.ts @@ -1,169 +1,171 @@ -import { mergeTypeDefs } from '@graphql-tools/merge'; -import { $TSAny, $TSContext, exitOnNextTick, FeatureFlags, pathManager, ResourceDoesNotExistError, stateManager } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; import * as fs from 'fs-extra'; +import * as path from 'path'; import * as graphql from 'graphql'; +import _ from 'lodash'; +import inquirer from 'inquirer'; import { - AuroraServerlessMySQLDatabaseReader, RelationalDBSchemaTransformer, RelationalDBTemplateGenerator, + AuroraServerlessMySQLDatabaseReader, } from 'graphql-relational-schema-transformer'; -import inquirer from 'inquirer'; -import _ from 'lodash'; -import * as path from 'path'; +import { mergeTypeDefs } from '@graphql-tools/merge'; +import { FeatureFlags, ResourceDoesNotExistError, exitOnNextTick, $TSAny, $TSContext, stateManager, pathManager } from 'amplify-cli-core'; const subcommand = 'add-graphql-datasource'; const categories = 'categories'; const category = 'api'; const providerName = 'awscloudformation'; -export const name = subcommand; +module.exports = { + name: subcommand, + run: async (context: $TSContext) => { + try { + const servicesMetadata = (await import('../../provider-utils/supported-datasources')).supportedDatasources; -export const run = async (context: $TSContext) => { - try { - const servicesMetadata = (await import(path.join('..', '..', 'provider-utils', 'supported-services'))).supportedServices; - const AWS = await getAwsClient(context, 'list'); + const AWS = await getAwsClient(context, 'list'); - const result: $TSAny = await datasourceSelectionPrompt(context, servicesMetadata); + const result: $TSAny = await datasourceSelectionPrompt(context, servicesMetadata); - const providerController = await import(path.join('..', '..', 'provider-utils', result.providerName, 'index')); + const providerController = await import(`../../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); + if (!providerController) { + context.print.error('Provider not configured for this category'); + return; + } - const { resourceName, databaseName } = answers; + const { datasource } = result; + const answers = await providerController.addDatasource(context, category, datasource); + + const { resourceName, databaseName } = answers; + + /** + * Write the new env specific datasource information into + * the team-provider-info file + */ + const currEnv = context.amplify.getEnvInfo().envName; + const teamProviderInfo = stateManager.getTeamProviderInfo(); + + _.set(teamProviderInfo, [currEnv, categories, category, resourceName], { + rdsRegion: answers.region, + rdsClusterIdentifier: answers.dbClusterArn, + rdsSecretStoreArn: answers.secretStoreArn, + rdsDatabaseName: answers.databaseName, + }); + + stateManager.setTeamProviderInfo(undefined, teamProviderInfo); + + 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 instrospection to get the GraphQL Schema and Template Context + */ + const improvePluralizationFlag = FeatureFlags.getBoolean('graphqltransformer.improvePluralization'); + const relationalSchemaTransformer = new RelationalDBSchemaTransformer(dbReader, answers.databaseName, improvePluralizationFlag); + const graphqlSchemaContext = await relationalSchemaTransformer.introspectDatabaseSchema(); + + if (graphqlSchemaContext === null) { + context.print.warning('No importable tables were found in the selected Database.'); + context.print.info(''); + return; + } - /** - * Write the new env specific datasource information into - * the team-provider-info file - */ - const currEnv = context.amplify.getEnvInfo().envName; - const teamProviderInfo = stateManager.getTeamProviderInfo(); + /** + * Merge the GraphQL Schema with the existing schema.graphql in the projects stack + * + */ + const apiDirPath = path.join(pathManager.getBackendDirPath(), category, resourceName); - _.set(teamProviderInfo, [currEnv, categories, category, resourceName], { - rdsRegion: answers.region, - rdsClusterIdentifier: answers.dbClusterArn, - rdsSecretStoreArn: answers.secretStoreArn, - rdsDatabaseName: answers.databaseName, - }); + fs.ensureDirSync(apiDirPath); - stateManager.setTeamProviderInfo(undefined, teamProviderInfo); - - 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 instrospection to get the GraphQL Schema and Template Context - */ - 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.info(''); - return; - } + const graphqlSchemaFilePath = path.join(apiDirPath, 'schema.graphql'); + const rdsGraphQLSchemaDoc = graphqlSchemaContext.schemaDoc; + const schemaDirectoryPath = path.join(apiDirPath, 'schema'); - /** - * Merge the GraphQL Schema with the existing schema.graphql in the projects stack - * - */ - const apiDirPath = pathManager.getResourceDirectoryPath(undefined, category, resourceName); + if (fs.existsSync(graphqlSchemaFilePath)) { + const typesToBeMerged = [rdsGraphQLSchemaDoc]; + const currGraphQLSchemaDoc = readSchema(graphqlSchemaFilePath); - fs.ensureDirSync(apiDirPath); + if (currGraphQLSchemaDoc) { + typesToBeMerged.unshift(currGraphQLSchemaDoc); + } else { + context.print.warning(`Graphql Schema file "${graphqlSchemaFilePath}" is empty.`); + context.print.info(''); + } - const graphqlSchemaFilePath = path.join(apiDirPath, 'schema.graphql'); - const rdsGraphQLSchemaDoc = graphqlSchemaContext.schemaDoc; - const schemaDirectoryPath = path.join(apiDirPath, 'schema'); + const concatGraphQLSchemaDoc = mergeTypeDefs(typesToBeMerged); - if (fs.existsSync(graphqlSchemaFilePath)) { - const typesToBeMerged = [rdsGraphQLSchemaDoc]; - const currGraphQLSchemaDoc = readSchema(graphqlSchemaFilePath); + fs.writeFileSync(graphqlSchemaFilePath, graphql.print(concatGraphQLSchemaDoc), 'utf8'); + } else if (fs.existsSync(schemaDirectoryPath)) { + const rdsSchemaFilePath = path.join(schemaDirectoryPath, 'rds.graphql'); - if (currGraphQLSchemaDoc) { - typesToBeMerged.unshift(currGraphQLSchemaDoc); + fs.writeFileSync(rdsSchemaFilePath, graphql.print(rdsGraphQLSchemaDoc), 'utf8'); } else { - printer.warn(`Graphql Schema file "${graphqlSchemaFilePath}" is empty.`); - printer.info(''); + throw new Error(`Could not find a schema in either ${graphqlSchemaFilePath} or schema directory at ${schemaDirectoryPath}`); } - const concatGraphQLSchemaDoc = mergeTypeDefs(typesToBeMerged); + const resolversDir = path.join(apiDirPath, 'resolvers'); - fs.writeFileSync(graphqlSchemaFilePath, graphql.print(concatGraphQLSchemaDoc), 'utf8'); - } else if (fs.existsSync(schemaDirectoryPath)) { - const rdsSchemaFilePath = path.join(schemaDirectoryPath, 'rds.graphql'); + /** + * Instantiate a new Relational Template Generator and create + * the template and relational resolvers + */ - fs.writeFileSync(rdsSchemaFilePath, graphql.print(rdsGraphQLSchemaDoc), 'utf8'); - } else { - throw new Error(`Could not find a schema in either ${graphqlSchemaFilePath} or schema directory at ${schemaDirectoryPath}`); - } + const templateGenerator = new RelationalDBTemplateGenerator(graphqlSchemaContext); - const resolversDir = path.join(apiDirPath, 'resolvers'); + let template = templateGenerator.createTemplate(context); - /** - * Instantiate a new Relational Template Generator and create - * the template and relational resolvers - */ + template = templateGenerator.addRelationalResolvers(template, resolversDir, improvePluralizationFlag); - const templateGenerator = new RelationalDBTemplateGenerator(graphqlSchemaContext); + const cfn = templateGenerator.printCloudformationTemplate(template); - let template = templateGenerator.createTemplate(context); + /** + * Add the generated the CFN to the appropriate nested stacks directory + */ - template = templateGenerator.addRelationalResolvers(template, resolversDir, improvePluralizationFlag); + const stacksDir = path.join(apiDirPath, 'stacks'); + const writeToPath = path.join(stacksDir, `${resourceName}-${databaseName}-rds.json`); - const cfn = templateGenerator.printCloudformationTemplate(template); + fs.writeFileSync(writeToPath, cfn, 'utf8'); - /** - * Add the generated the CFN to the appropriate nested stacks directory - */ + context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { forceCompile: true }); - const stacksDir = path.join(apiDirPath, 'stacks'); - const writeToPath = path.join(stacksDir, `${resourceName}-${databaseName}-rds.json`); + context.print.success(`Successfully added the ${datasource} datasource locally`); + context.print.info(''); + context.print.success('Some next steps:'); + context.print.info('"amplify push" will build all your local backend resources and provision it in the cloud'); + context.print.info( + '"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud', + ); + context.print.info(''); + } catch (error) { + context.print.info(error.stack); + context.print.error('There was an error adding the datasource'); - fs.writeFileSync(writeToPath, cfn, 'utf8'); + await context.usageData.emitError(error); - 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.info(error.stack); - printer.error('There was an error adding the datasource'); - - await context.usageData.emitError(error); - - process.exitCode = 1; - } + process.exitCode = 1; + } + }, + readSchema, }; -async function datasourceSelectionPrompt(context: $TSContext, supportedDatasources) { +async function datasourceSelectionPrompt(context, supportedDatasources) { const options = []; Object.keys(supportedDatasources).forEach(datasource => { const optionName = @@ -182,7 +184,7 @@ async function datasourceSelectionPrompt(context: $TSContext, supportedDatasourc if (options.length === 0) { const errMessage = `No datasources defined by configured providers for category: ${category}`; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new ResourceDoesNotExistError(errMessage)); @@ -191,7 +193,7 @@ async function datasourceSelectionPrompt(context: $TSContext, supportedDatasourc if (options.length === 1) { // No need to ask questions - printer.info(`Using datasource: ${options[0].value.datasource}, provided by: ${options[0].value.providerName}`); + context.print.info(`Using datasource: ${options[0].value.datasource}, provided by: ${options[0].value.providerName}`); return new Promise(resolve => { resolve(options[0].value); @@ -217,7 +219,7 @@ async function getAwsClient(context: $TSContext, action: string) { return await provider.getConfiguredAWSClient(context, 'aurora-serverless', action); } -export function readSchema(graphqlSchemaFilePath: string) { +function readSchema(graphqlSchemaFilePath) { const graphqlSchemaRaw = fs.readFileSync(graphqlSchemaFilePath).toString(); if (graphqlSchemaRaw.trim().length === 0) { diff --git a/packages/amplify-category-api/src/commands/api/add.js b/packages/amplify-category-api/src/commands/api/add.js new file mode 100644 index 00000000000..d883208b457 --- /dev/null +++ b/packages/amplify-category-api/src/commands/api/add.js @@ -0,0 +1,75 @@ +const inquirer = require('inquirer'); +const subcommand = 'add'; +const category = 'api'; +const apiGatewayService = 'API Gateway'; + +let options; + +module.exports = { + name: subcommand, + run: async context => { + const { amplify } = context; + const servicesMetadata = require('../../provider-utils/supported-services').supportedServices; + return amplify + .serviceSelectionPrompt(context, category, servicesMetadata) + .then(async result => { + options = { + service: result.service, + providerPlugin: result.providerName, + }; + const providerController = require(`../../provider-utils/${result.providerName}/index`); + if (!providerController) { + context.print.error('Provider not configured for this category'); + return; + } + + if ((await shouldUpdateExistingRestApi(context, result.service)) === true) { + return providerController.updateResource(context, category, result.service, { allowContainers: false }); + } + + return providerController.addResource(context, category, result.service, options); + }) + .then(resourceName => { + const { print } = context; + print.success(`Successfully added resource ${resourceName} locally`); + print.info(''); + print.success('Some next steps:'); + print.info('"amplify push" will build all your local backend resources and provision it in the cloud'); + print.info( + '"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud', + ); + print.info(''); + }) + .catch(err => { + context.print.info(err.stack); + context.print.error('There was an error adding the API resource'); + context.usageData.emitError(err); + process.exitCode = 1; + }); + }, +}; + +async function shouldUpdateExistingRestApi(context, selectedService) { + if (selectedService !== apiGatewayService) { + return false; + } + + const { allResources } = await context.amplify.getResourceStatus(); + const hasRestApis = allResources.some(resource => resource.service === apiGatewayService && resource.mobileHubMigrated !== true); + + if (!hasRestApis) { + return false; + } + + const question = [ + { + name: 'update', + message: 'Would you like to add a new path to an existing REST API:', + type: 'confirm', + default: true, + }, + ]; + const answer = await inquirer.prompt(question); + + return answer.update; +} diff --git a/packages/amplify-category-api/src/commands/api/add.ts b/packages/amplify-category-api/src/commands/api/add.ts deleted file mode 100644 index bf2f64e8c25..00000000000 --- a/packages/amplify-category-api/src/commands/api/add.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { $TSContext, $TSObject, AmplifyCategories, AmplifySupportedService } from 'amplify-cli-core'; -import { printer, prompter } from 'amplify-prompts'; -import * as path from 'path'; - -const subcommand = 'add'; -const category = AmplifyCategories.API; - -export const name = subcommand; - -export const run = async (context: $TSContext) => { - const servicesMetadata = (await import(path.join('..', '..', 'provider-utils', 'supported-services'))).supportedServices; - return context.amplify - .serviceSelectionPrompt(context, category, servicesMetadata) - .then(async result => { - const options = { - service: result.service, - providerPlugin: result.providerName, - }; - const providerController = await import(path.join('..', '..', 'provider-utils', result.providerName, 'index')); - if (!providerController) { - printer.error('Provider not configured for this category'); - return; - } - - if ((await shouldUpdateExistingRestApi(context, result.service)) === true) { - return providerController.updateResource(context, category, result.service, { allowContainers: false }); - } - - return providerController.addResource(context, result.service, options); - }) - .then((resourceName: string) => { - printer.success(`Successfully added resource ${resourceName} 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(async err => { - printer.info(err.stack); - printer.error('There was an error adding the API resource'); - await context.usageData.emitError(err); - process.exitCode = 1; - }); -}; - -async function shouldUpdateExistingRestApi(context: $TSContext, selectedService: string): Promise { - if (selectedService !== AmplifySupportedService.APIGW) { - return false; - } - - const { allResources } = await context.amplify.getResourceStatus(); - const hasRestApis = allResources.some( - (resource: $TSObject) => resource.service === AmplifySupportedService.APIGW && resource.mobileHubMigrated !== true, - ); - - if (!hasRestApis) { - return false; - } - - return prompter.confirmContinue('Would you like to add a new path to an existing REST API:'); -} diff --git a/packages/amplify-category-api/src/commands/api/console.js b/packages/amplify-category-api/src/commands/api/console.js new file mode 100644 index 00000000000..a8bc4d57bc4 --- /dev/null +++ b/packages/amplify-category-api/src/commands/api/console.js @@ -0,0 +1,25 @@ +const subcommand = 'console'; +const category = 'api'; + +module.exports = { + name: subcommand, + run: async context => { + const { amplify } = context; + const servicesMetadata = require('../../provider-utils/supported-services').supportedServices; + return amplify + .serviceSelectionPrompt(context, category, servicesMetadata) + .then(async result => { + const providerController = require(`../../provider-utils/${result.providerName}/index`); + if (!providerController) { + throw new Error(`Provider "${result.providerName}" is not configured for this category`); + } + return await providerController.console(context, result.service); + }) + .catch(err => { + context.print.error('Error opening console.'); + context.print.info(err.message); + context.usageData.emitError(err); + process.exitCode = 1; + }); + }, +}; diff --git a/packages/amplify-category-api/src/commands/api/console.ts b/packages/amplify-category-api/src/commands/api/console.ts deleted file mode 100644 index 2e2430047b0..00000000000 --- a/packages/amplify-category-api/src/commands/api/console.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { $TSContext, AmplifyCategories } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; -import * as path from 'path'; - -const subcommand = 'console'; - -export const name = subcommand; - -export const run = async (context: $TSContext) => { - const servicesMetadata = (await import(path.join('..', '..', 'provider-utils', 'supported-services'))).supportedServices; - const result = await context.amplify.serviceSelectionPrompt(context, AmplifyCategories.API, servicesMetadata); - try { - const providerController = await import(path.join('..', '..', 'provider-utils', result.providerName, 'index')); - if (!providerController) { - throw new Error(`Provider "${result.providerName}" is not configured for this category`); - } - return providerController.console(context, result.service); - } catch (err) { - printer.error('Error opening console.'); - printer.info(err.message); - await context.usageData.emitError(err); - process.exitCode = 1; - } -}; diff --git a/packages/amplify-category-api/src/commands/api/gql-compile.js b/packages/amplify-category-api/src/commands/api/gql-compile.js new file mode 100644 index 00000000000..a53e64d0feb --- /dev/null +++ b/packages/amplify-category-api/src/commands/api/gql-compile.js @@ -0,0 +1,20 @@ +const subcommand = 'gql-compile'; + +module.exports = { + name: subcommand, + run: async context => { + try { + const { + parameters: { options }, + } = context; + await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { + forceCompile: true, + minify: options['minify'], + }); + } catch (err) { + context.print.error(err.toString()); + context.usageData.emitError(err); + process.exitCode = 1; + } + }, +}; diff --git a/packages/amplify-category-api/src/commands/api/gql-compile.ts b/packages/amplify-category-api/src/commands/api/gql-compile.ts deleted file mode 100644 index bcbbfba4f83..00000000000 --- a/packages/amplify-category-api/src/commands/api/gql-compile.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { $TSContext } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; - -const subcommand = 'gql-compile'; - -export const name = subcommand; - -export const run = async (context: $TSContext) => { - try { - const { - parameters: { options }, - } = context; - await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { - forceCompile: true, - minify: options['minify'], - }); - } catch (err) { - printer.error(err.toString()); - await context.usageData.emitError(err); - process.exitCode = 1; - } -}; diff --git a/packages/amplify-category-api/src/commands/api/override.ts b/packages/amplify-category-api/src/commands/api/override.ts deleted file mode 100644 index b5c57c0bef4..00000000000 --- a/packages/amplify-category-api/src/commands/api/override.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - $TSContext, - AmplifyCategories, - AmplifySupportedService, - generateOverrideSkeleton, - pathManager, - stateManager, -} from 'amplify-cli-core'; -import { printer, prompter } from 'amplify-prompts'; -import * as path from 'path'; -import { ApigwInputState } from '../../provider-utils/awscloudformation/apigw-input-state'; -import { ApigwStackTransform } from '../../provider-utils/awscloudformation/cdk-stack-builder'; - -export const name = 'override'; - -export const run = async (context: $TSContext) => { - const amplifyMeta = stateManager.getMeta(); - const apiResources: string[] = []; - - if (amplifyMeta[AmplifyCategories.API]) { - Object.keys(amplifyMeta[AmplifyCategories.API]).forEach(resourceName => { - apiResources.push(resourceName); - }); - } - - if (apiResources.length === 0) { - const errMessage = 'No resources to override. You need to add a resource.'; - printer.error(errMessage); - return; - } - - let selectedResourceName: string = apiResources[0]; - - if (apiResources.length > 1) { - selectedResourceName = await prompter.pick('Which resource would you like to add overrides for?', apiResources); - } - - const { service }: { service: string } = amplifyMeta[AmplifyCategories.API][selectedResourceName]; - const destPath = pathManager.getResourceDirectoryPath(undefined, AmplifyCategories.API, selectedResourceName); - - const srcPath = path.join( - __dirname, - '..', - '..', - '..', - 'resources', - 'awscloudformation', - 'overrides-resource', - service === AmplifySupportedService.APIGW ? 'APIGW' : service, // avoid space in filename - ); - - // Make sure to migrate first - if (service === AmplifySupportedService.APPSYNC) { - throw 'To be implemented'; - } else if (service === AmplifySupportedService.APIGW) { - // Migration logic goes in here - const apigwInputState = new ApigwInputState(context, selectedResourceName); - if (!apigwInputState.cliInputsFileExists()) { - if (await prompter.yesOrNo('File migration required to continue. Do you want to continue?', true)) { - await apigwInputState.migrateApigwResource(selectedResourceName); - const stackGenerator = new ApigwStackTransform(context, selectedResourceName); - stackGenerator.transform(); - } else { - return; - } - } - } - - await generateOverrideSkeleton(context, srcPath, destPath); -}; diff --git a/packages/amplify-category-api/src/commands/api/push.js b/packages/amplify-category-api/src/commands/api/push.js new file mode 100644 index 00000000000..8a0da0bb5a6 --- /dev/null +++ b/packages/amplify-category-api/src/commands/api/push.js @@ -0,0 +1,17 @@ +const subcommand = 'push'; +const category = 'api'; + +module.exports = { + name: subcommand, + run: async context => { + const { amplify, parameters } = context; + const resourceName = parameters.first; + context.amplify.constructExeInfo(context); + return amplify.pushResources(context, category, resourceName).catch(err => { + context.print.error('There was an error pushing the API resource'); + context.print.error(err.toString()); + context.usageData.emitError(err); + process.exitCode = 1; + }); + }, +}; diff --git a/packages/amplify-category-api/src/commands/api/push.ts b/packages/amplify-category-api/src/commands/api/push.ts deleted file mode 100644 index ce32110c1cf..00000000000 --- a/packages/amplify-category-api/src/commands/api/push.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { $TSContext, AmplifyCategories } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; - -const subcommand = 'push'; - -export const name = subcommand; - -export const run = async (context: $TSContext) => { - const resourceName = context.parameters.first; - context.amplify.constructExeInfo(context); - return context.amplify.pushResources(context, AmplifyCategories.API, resourceName).catch(async err => { - printer.error('There was an error pushing the API resource'); - printer.error(err.toString()); - await context.usageData.emitError(err); - process.exitCode = 1; - }); -}; diff --git a/packages/amplify-category-api/src/commands/api/rebuild.ts b/packages/amplify-category-api/src/commands/api/rebuild.ts index 55c269da95a..26b8ead3641 100644 --- a/packages/amplify-category-api/src/commands/api/rebuild.ts +++ b/packages/amplify-category-api/src/commands/api/rebuild.ts @@ -1,7 +1,8 @@ -import { $TSAny, $TSContext, AmplifyCategories, FeatureFlags, stateManager } from 'amplify-cli-core'; +import { $TSAny, $TSContext, FeatureFlags, stateManager } from 'amplify-cli-core'; import { printer, prompter, exact } from 'amplify-prompts'; const subcommand = 'rebuild'; +const category = 'api'; export const name = subcommand; @@ -35,5 +36,5 @@ export const run = async (context: $TSContext) => { const { amplify, parameters } = context; const resourceName = parameters.first; amplify.constructExeInfo(context); - return amplify.pushResources(context, AmplifyCategories.API, resourceName, undefined, rebuild); + return amplify.pushResources(context, category, resourceName, undefined, rebuild); }; diff --git a/packages/amplify-category-api/src/commands/api/remove.js b/packages/amplify-category-api/src/commands/api/remove.js new file mode 100644 index 00000000000..8c334611a5d --- /dev/null +++ b/packages/amplify-category-api/src/commands/api/remove.js @@ -0,0 +1,31 @@ +const path = require('path'); + +const subcommand = 'remove'; +const category = 'api'; +const gqlConfigFilename = '.graphqlconfig.yml'; + +module.exports = { + name: subcommand, + run: async context => { + const { amplify, parameters } = context; + const resourceName = parameters.first; + + return amplify + .removeResource(context, category, resourceName) + .then(resourceValues => { + if (!resourceValues) return; // indicates that the customer selected "no" at the confirmation prompt + if (resourceValues.service === 'AppSync') { + const { projectPath } = amplify.getEnvInfo(); + + const gqlConfigFile = path.normalize(path.join(projectPath, gqlConfigFilename)); + context.filesystem.remove(gqlConfigFile); + } + }) + .catch(err => { + context.print.info(err.stack); + context.print.error('There was an error removing the api resource'); + context.usageData.emitError(err); + process.exitCode = 1; + }); + }, +}; diff --git a/packages/amplify-category-api/src/commands/api/remove.ts b/packages/amplify-category-api/src/commands/api/remove.ts deleted file mode 100644 index ac67429d87c..00000000000 --- a/packages/amplify-category-api/src/commands/api/remove.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { $TSContext, AmplifyCategories, AmplifySupportedService } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; -import * as path from 'path'; - -const subcommand = 'remove'; -const gqlConfigFilename = '.graphqlconfig.yml'; - -export const name = subcommand; - -export const run = async (context: $TSContext) => { - const resourceName = context.parameters.first; - - const resourceValues = await context.amplify.removeResource(context, AmplifyCategories.API, resourceName, { - serviceSuffix: { [AmplifySupportedService.APPSYNC]: '(GraphQL API)', [AmplifySupportedService.APIGW]: '(REST API)' }, - }); - try { - if (!resourceValues) { - return; - } // indicates that the customer selected "no" at the confirmation prompt - if (resourceValues.service === AmplifySupportedService.APPSYNC) { - const { projectPath } = context.amplify.getEnvInfo(); - - const gqlConfigFile = path.normalize(path.join(projectPath, gqlConfigFilename)); - context.filesystem.remove(gqlConfigFile); - } - } catch (err) { - printer.info(err.stack); - printer.error('There was an error removing the api resource'); - await context.usageData.emitError(err); - process.exitCode = 1; - } -}; diff --git a/packages/amplify-category-api/src/commands/api/update.js b/packages/amplify-category-api/src/commands/api/update.js new file mode 100644 index 00000000000..03dbb328f12 --- /dev/null +++ b/packages/amplify-category-api/src/commands/api/update.js @@ -0,0 +1,29 @@ +const subcommand = 'update'; +const category = 'api'; + +module.exports = { + name: subcommand, + alias: ['configure'], + run: async context => { + const { amplify } = context; + const servicesMetadata = require('../../provider-utils/supported-services').supportedServices; + + return amplify + .serviceSelectionPrompt(context, category, servicesMetadata) + .then(result => { + const providerController = require(`../../provider-utils/${result.providerName}/index`); + if (!providerController) { + context.print.error('Provider not configured for this category'); + return; + } + return providerController.updateResource(context, category, result.service); + }) + .then(() => context.print.success('Successfully updated resource')) + .catch(err => { + context.print.error(err.message); + console.log(err.stack); + context.usageData.emitError(err); + process.exitCode = 1; + }); + }, +}; diff --git a/packages/amplify-category-api/src/commands/api/update.ts b/packages/amplify-category-api/src/commands/api/update.ts deleted file mode 100644 index 8813a045bef..00000000000 --- a/packages/amplify-category-api/src/commands/api/update.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { $TSContext, AmplifyCategories } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; -import * as path from 'path'; - -const subcommand = 'update'; - -export const name = subcommand; -export const alias = ['configure']; - -export const run = async (context: $TSContext) => { - const servicesMetadata = (await import(path.join('..', '..', 'provider-utils', 'supported-services'))).supportedServices; - - return context.amplify - .serviceSelectionPrompt(context, AmplifyCategories.API, servicesMetadata) - .then(async result => { - const providerController = await import(path.join('..', '..', 'provider-utils', result.providerName, 'index')); - if (!providerController) { - printer.error('Provider not configured for this category'); - return; - } - return providerController.updateResource(context, AmplifyCategories.API, result.service); - }) - .then(() => printer.success('Successfully updated resource')) - .catch(async err => { - printer.error(err.message); - printer.info(err.stack); - await context.usageData.emitError(err); - process.exitCode = 1; - }); -}; diff --git a/packages/amplify-category-api/src/index.ts b/packages/amplify-category-api/src/index.ts index f5561484768..4ada1ab74d5 100644 --- a/packages/amplify-category-api/src/index.ts +++ b/packages/amplify-category-api/src/index.ts @@ -1,57 +1,42 @@ -import { - $TSContext, - $TSObject, - AmplifyCategories, - AmplifySupportedService, - buildOverrideDir, - pathManager, - stateManager, -} from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; import { validateAddApiRequest, validateUpdateApiRequest } from 'amplify-util-headless-input'; -import * as fs from 'fs-extra'; -import * as path from 'path'; +import fs from 'fs-extra'; +import path from 'path'; import { run } from './commands/api/console'; -import { getAppSyncAuthConfig, getAppSyncResourceName } from './provider-utils/awscloudformation//utils/amplify-meta-utils'; -import { provider } from './provider-utils/awscloudformation/aws-constants'; -import { ApigwStackTransform } from './provider-utils/awscloudformation/cdk-stack-builder'; import { getCfnApiArtifactHandler } from './provider-utils/awscloudformation/cfn-api-artifact-handler'; import { askAuthQuestions } from './provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough'; +import { getAppSyncResourceName, getAppSyncAuthConfig } from './provider-utils/awscloudformation//utils/amplify-meta-utils'; import { authConfigToAppSyncAuthType } from './provider-utils/awscloudformation/utils/auth-config-to-app-sync-auth-type-bi-di-mapper'; export { NETWORK_STACK_LOGICAL_ID } from './category-constants'; -export { addAdminQueriesApi, updateAdminQueriesApi } from './provider-utils/awscloudformation/'; export { DEPLOYMENT_MECHANISM } from './provider-utils/awscloudformation/base-api-stack'; -export { getContainers } from './provider-utils/awscloudformation/docker-compose'; -export { EcsAlbStack } from './provider-utils/awscloudformation/ecs-alb-stack'; export { EcsStack } from './provider-utils/awscloudformation/ecs-apigw-stack'; -export { promptToAddApiKey } from './provider-utils/awscloudformation/prompt-to-add-api-key'; +export { EcsAlbStack } from './provider-utils/awscloudformation/ecs-alb-stack'; +export { getGitHubOwnerRepoFromPath } from './provider-utils/awscloudformation/utils/github'; export { - ApiResource, generateContainersArtifacts, + ApiResource, processDockerConfig, } from './provider-utils/awscloudformation/utils/containers-artifacts'; -export { getGitHubOwnerRepoFromPath } from './provider-utils/awscloudformation/utils/github'; +export { getContainers } from './provider-utils/awscloudformation/docker-compose'; +export { promptToAddApiKey } from './provider-utils/awscloudformation/prompt-to-add-api-key'; + +const category = 'api'; -const category = AmplifyCategories.API; const categories = 'categories'; -export async function console(context: $TSContext) { +export async function console(context) { await run(context); } -export async function migrate(context: $TSContext, serviceName?: string) { - const { projectPath } = context.migrationInfo; - const amplifyMeta = stateManager.getMeta(); +export async function migrate(context, serviceName) { + const { projectPath, amplifyMeta } = context.migrationInfo; const migrateResourcePromises = []; - for (const categoryName of Object.keys(amplifyMeta)) { + Object.keys(amplifyMeta).forEach(categoryName => { if (categoryName === category) { - for (const resourceName of Object.keys(amplifyMeta[category])) { + Object.keys(amplifyMeta[category]).forEach(resourceName => { try { if (amplifyMeta[category][resourceName].providerPlugin) { - const providerController = await import( - path.join('.', 'provider-utils', amplifyMeta[category][resourceName].providerPlugin, 'index') - ); + const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); if (providerController) { if (!serviceName || serviceName === amplifyMeta[category][resourceName].service) { migrateResourcePromises.push( @@ -60,20 +45,20 @@ export async function migrate(context: $TSContext, serviceName?: string) { } } } else { - printer.error(`Provider not configured for ${category}: ${resourceName}`); + context.print.error(`Provider not configured for ${category}: ${resourceName}`); } } catch (e) { - printer.warn(`Could not run migration for ${category}: ${resourceName}`); + context.print.warning(`Could not run migration for ${category}: ${resourceName}`); throw e; } - } + }); } - } + }); await Promise.all(migrateResourcePromises); } -export async function initEnv(context: $TSContext) { +export async function initEnv(context) { const datasource = 'Aurora Serverless'; const service = 'service'; const rdsInit = 'rdsInit'; @@ -88,7 +73,7 @@ export async function initEnv(context: $TSContext) { * Check if we need to do the walkthrough, by looking to see if previous environments have * configured an RDS datasource */ - const backendConfigFilePath = pathManager.getBackendConfigFilePath(); + const backendConfigFilePath = amplify.pathManager.getBackendConfigFilePath(); // If this is a mobile hub migrated project without locally added resources then there is no // backend config exists yet. @@ -96,7 +81,7 @@ export async function initEnv(context: $TSContext) { return; } - const backendConfig = stateManager.getBackendConfig(); + const backendConfig = amplify.readJsonFile(backendConfigFilePath); if (!backendConfig[category]) { return; @@ -104,9 +89,9 @@ export async function initEnv(context: $TSContext) { let resourceName; const apis = Object.keys(backendConfig[category]); - for (const api of apis) { - if (backendConfig[category][api][service] === AmplifySupportedService.APPSYNC) { - resourceName = api; + for (let i = 0; i < apis.length; i += 1) { + if (backendConfig[category][apis[i]][service] === 'AppSync') { + resourceName = apis[i]; break; } } @@ -121,10 +106,10 @@ export async function initEnv(context: $TSContext) { return; } - const providerController = await import(path.join('.', 'provider-utils', provider, 'index')); + const providerController = require('./provider-utils/awscloudformation/index'); if (!providerController) { - printer.error('Provider not configured for this category'); + context.print.error('Provider not configured for this category'); return; } @@ -132,7 +117,8 @@ export async function initEnv(context: $TSContext) { * Check team provider info to ensure it hasn't already been created for current env */ const currEnv = amplify.getEnvInfo().envName; - const teamProviderInfo = stateManager.getTeamProviderInfo(); + const teamProviderInfoFilePath = amplify.pathManager.getProviderInfoFilePath(); + const teamProviderInfo = amplify.readJsonFile(teamProviderInfoFilePath); if ( teamProviderInfo[currEnv][categories] && teamProviderInfo[currEnv][categories][category] && @@ -167,15 +153,16 @@ export async function initEnv(context: $TSContext) { teamProviderInfo[currEnv][categories][category][resourceName][rdsSecretStoreArn] = answers.secretStoreArn; teamProviderInfo[currEnv][categories][category][resourceName][rdsDatabaseName] = answers.databaseName; - stateManager.setTeamProviderInfo(undefined, teamProviderInfo); + fs.writeFileSync(teamProviderInfoFilePath, JSON.stringify(teamProviderInfo, null, 4)); }) .then(() => { context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { forceCompile: true }); }); } -export async function getPermissionPolicies(context: $TSContext, resourceOpsMapping: $TSObject) { - const amplifyMeta = stateManager.getMeta(); +export async function getPermissionPolicies(context, resourceOpsMapping) { + const amplifyMetaFilePath = context.amplify.pathManager.getAmplifyMetaFilePath(); + const amplifyMeta = context.amplify.readJsonFile(amplifyMetaFilePath); const permissionPolicies = []; const resourceAttributes = []; @@ -184,7 +171,7 @@ export async function getPermissionPolicies(context: $TSContext, resourceOpsMapp try { const providerName = amplifyMeta[category][resourceName].providerPlugin; if (providerName) { - const providerController = await import(path.join('.', 'provider-utils', providerName, 'index')); + const providerController = require(`./provider-utils/${providerName}/index`); const { policy, attributes } = await providerController.getPermissionPolicies( context, amplifyMeta[category][resourceName].service, @@ -194,10 +181,10 @@ export async function getPermissionPolicies(context: $TSContext, resourceOpsMapp permissionPolicies.push(policy); resourceAttributes.push({ resourceName, attributes, category }); } else { - printer.error(`Provider not configured for ${category}: ${resourceName}`); + context.print.error(`Provider not configured for ${category}: ${resourceName}`); } } catch (e) { - printer.warn(`Could not get policies for ${category}: ${resourceName}`); + context.print.warning(`Could not get policies for ${category}: ${resourceName}`); throw e; } }), @@ -205,7 +192,7 @@ export async function getPermissionPolicies(context: $TSContext, resourceOpsMapp return { permissionPolicies, resourceAttributes }; } -export async function executeAmplifyCommand(context: $TSContext) { +export async function executeAmplifyCommand(context) { let commandPath = path.normalize(path.join(__dirname, 'commands')); if (context.input.command === 'help') { commandPath = path.join(commandPath, category); @@ -213,11 +200,11 @@ export async function executeAmplifyCommand(context: $TSContext) { commandPath = path.join(commandPath, category, context.input.command); } - const commandModule = await import(commandPath); + const commandModule = require(commandPath); await commandModule.run(context); } -export const executeAmplifyHeadlessCommand = async (context: $TSContext, headlessPayload: string) => { +export const executeAmplifyHeadlessCommand = async (context, headlessPayload: string) => { switch (context.input.command) { case 'add': await getCfnApiArtifactHandler(context).createArtifacts(await validateAddApiRequest(headlessPayload)); @@ -226,24 +213,23 @@ export const executeAmplifyHeadlessCommand = async (context: $TSContext, headles await getCfnApiArtifactHandler(context).updateArtifacts(await validateUpdateApiRequest(headlessPayload)); break; default: - printer.error(`Headless mode for ${context.input.command} api is not implemented yet`); + context.print.error(`Headless mode for ${context.input.command} api is not implemented yet`); } }; -export async function handleAmplifyEvent(context: $TSContext, args) { - printer.info(`${category} handleAmplifyEvent to be implemented`); - printer.info(`Received event args ${args}`); +export async function handleAmplifyEvent(context, args) { + context.print.info(`${category} handleAmplifyEvent to be implemented`); + context.print.info(`Received event args ${args}`); } -export async function addGraphQLAuthorizationMode(context: $TSContext, args: $TSObject) { +export async function addGraphQLAuthorizationMode(context, args) { const { authType, printLeadText, authSettings } = args; - const meta = stateManager.getMeta(); - const apiName = getAppSyncResourceName(meta); + const apiName = getAppSyncResourceName(context.amplify.getProjectMeta()); if (!apiName) { return; } - const authConfig = getAppSyncAuthConfig(meta); + const authConfig = getAppSyncAuthConfig(context.amplify.getProjectMeta()); const addAuthConfig = await askAuthQuestions(authType, context, printLeadText, authSettings); authConfig.additionalAuthenticationProviders.push(addAuthConfig); await context.amplify.updateamplifyMetaAfterResourceUpdate(category, apiName, 'output', { authConfig }); @@ -264,23 +250,3 @@ export async function addGraphQLAuthorizationMode(context: $TSContext, args: $TS return addAuthConfig; } - -export async function transformCategoryStack(context: $TSContext, resource: $TSObject) { - if (resource.service === AmplifySupportedService.APIGW) { - if (canResourceBeTransformed(resource.resourceName)) { - const backendDir = pathManager.getBackendDirPath(); - const overrideDir = pathManager.getResourceDirectoryPath(undefined, AmplifyCategories.API, resource.resourceName); - await buildOverrideDir(backendDir, overrideDir).catch(error => { - printer.debug(`Skipping build as ${error.message}`); - return false; - }); - // Rebuild CFN - const apigwStack = new ApigwStackTransform(context, resource.resourceName); - apigwStack.transform(); - } - } -} - -function canResourceBeTransformed(resourceName: string) { - return stateManager.resourceInputsJsonExists(undefined, AmplifyCategories.API, resourceName); -} diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/apigw-input-state.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/apigw-input-state.ts deleted file mode 100644 index 0eb42cb4b3a..00000000000 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/apigw-input-state.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { - $TSContext, - $TSObject, - AmplifyCategories, - AmplifySupportedService, - CLIInputSchemaValidator, - isResourceNameUnique, - JSONUtilities, - PathConstants, - pathManager, - stateManager, -} from 'amplify-cli-core'; -import { printer, prompter } from 'amplify-prompts'; -import * as fs from 'fs-extra'; -import { join } from 'path'; -import { ApigwInputs, ApigwStackTransform, CrudOperation, Path, PermissionSetting } from './cdk-stack-builder'; -import { ApigwWalkthroughReturnPromise } from './service-walkthrough-types/apigw-types'; - -export class ApigwInputState { - projectRootPath: string; - resourceName: string; - paths: { [pathName: string]: Path }; - - constructor(private readonly context: $TSContext, resourceName?: string) { - this.projectRootPath = pathManager.findProjectRoot(); - this.resourceName = resourceName; - } - - public addAdminQueriesResource = async (adminQueriesProps: AdminQueriesProps) => { - this.resourceName = adminQueriesProps.apiName; - this.paths = { - '/{proxy+}': { - lambdaFunction: adminQueriesProps.functionName, - permissions: { - setting: PermissionSetting.PRIVATE, - auth: [CrudOperation.CREATE, CrudOperation.READ, CrudOperation.UPDATE, CrudOperation.DELETE], - }, - }, - }; - - await this.createApigwArtifacts(); - - // Update amplify-meta and backend-config - const backendConfigs = { - service: AmplifySupportedService.APIGW, - providerPlugin: 'awscloudformation', - authorizationType: 'AMAZON_COGNITO_USER_POOLS', - dependsOn: adminQueriesProps.dependsOn, - }; - - await this.context.amplify.updateamplifyMetaAfterResourceAdd(AmplifyCategories.API, adminQueriesProps.apiName, backendConfigs); - }; - - public updateAdminQueriesResource = async (adminQueriesProps: AdminQueriesProps) => { - this.resourceName = adminQueriesProps.apiName; - this.paths = { - '/{proxy+}': { - lambdaFunction: adminQueriesProps.functionName, - permissions: { - setting: PermissionSetting.PRIVATE, - auth: [CrudOperation.CREATE, CrudOperation.READ, CrudOperation.UPDATE, CrudOperation.DELETE], - }, - }, - }; - - await this.createApigwArtifacts(); - - await this.context.amplify.updateamplifyMetaAfterResourceUpdate( - AmplifyCategories.API, - adminQueriesProps.apiName, - 'dependsOn', - adminQueriesProps.dependsOn, - ); - }; - - public addApigwResource = async (serviceWalkthroughPromise: ApigwWalkthroughReturnPromise, options: $TSObject) => { - const { answers } = await serviceWalkthroughPromise; - - this.resourceName = answers.resourceName; - this.paths = answers.paths; - options.dependsOn = answers.dependsOn; - - isResourceNameUnique(AmplifyCategories.API, this.resourceName); - - await this.createApigwArtifacts(); - - this.context.amplify.updateamplifyMetaAfterResourceAdd(AmplifyCategories.API, this.resourceName, options); - return this.resourceName; - }; - - public updateApigwResource = async (updateWalkthroughPromise: Promise<$TSObject>) => { - const { answers } = await updateWalkthroughPromise; - - this.resourceName = answers.resourceName; - this.paths = answers.paths; - - // this.addPolicyResourceNameToPaths(answers.paths); - await this.createApigwArtifacts(); - - this.context.amplify.updateamplifyMetaAfterResourceUpdate(AmplifyCategories.API, this.resourceName, 'dependsOn', answers.dependsOn); - return this.resourceName; - }; - - public migrateAdminQueries = async (adminQueriesProps: AdminQueriesProps) => { - this.resourceName = this.resourceName ?? adminQueriesProps.apiName; - if (!(await prompter.confirmContinue(`Migration for ${this.resourceName} is required. Continue?`))) { - return; - } - const resourceDirPath = pathManager.getResourceDirectoryPath(this.projectRootPath, AmplifyCategories.API, this.resourceName); - - this.context.filesystem.remove(join(resourceDirPath, PathConstants.ParametersJsonFileName)); - this.context.filesystem.remove(join(resourceDirPath, 'admin-queries-cloudformation-template.json')); - - return this.updateAdminQueriesResource(adminQueriesProps); - }; - - public migrateApigwResource = async (resourceName: string) => { - this.resourceName = this.resourceName ?? resourceName; - if (!(await prompter.confirmContinue(`Migration for ${this.resourceName} is required. Continue?`))) { - return; - } - const deprecatedParametersFileName = 'api-params.json'; - const resourceDirPath = pathManager.getResourceDirectoryPath(this.projectRootPath, AmplifyCategories.API, this.resourceName); - const deprecatedParametersFilePath = join(resourceDirPath, deprecatedParametersFileName); - let deprecatedParameters: $TSObject; - try { - deprecatedParameters = JSONUtilities.readJson<$TSObject>(deprecatedParametersFilePath); - } catch (e) { - printer.error(`Error reading ${deprecatedParametersFileName} file for ${this.resourceName} resource`); - throw e; - } - - this.paths = {}; - - function convertDeprecatedPermissionToCRUD(deprecatedPrivacy: string) { - let privacyList: string[]; - if (deprecatedPrivacy === 'r') { - privacyList = [CrudOperation.READ]; - } else if (deprecatedPrivacy === 'rw') { - privacyList = [CrudOperation.CREATE, CrudOperation.READ, CrudOperation.UPDATE, CrudOperation.DELETE]; - } - return privacyList; - } - - deprecatedParameters.paths.forEach((path: $TSObject) => { - let pathPermissionSetting = - path.privacy.open === true - ? PermissionSetting.OPEN - : path.privacy.private === true - ? PermissionSetting.PRIVATE - : PermissionSetting.PROTECTED; - - let auth; - let guest; - // convert deprecated permissions to CRUD structure - if (path.privacy.auth && ['r', 'rw'].includes(path.privacy.auth)) { - auth = convertDeprecatedPermissionToCRUD(path.privacy.auth); - } - if (path.privacy.unauth && ['r', 'rw'].includes(path.privacy.unauth)) { - auth = convertDeprecatedPermissionToCRUD(path.privacy.unauth); - } - - this.paths[path.name] = { - permissions: { - setting: pathPermissionSetting, - auth, - guest, - }, - lambdaFunction: path.lambdaFunction, - }; - }); - - await this.createApigwArtifacts(); - - this.context.filesystem.remove(deprecatedParametersFilePath); - this.context.filesystem.remove(join(resourceDirPath, PathConstants.ParametersJsonFileName)); - this.context.filesystem.remove(join(resourceDirPath, PathConstants.CfnFileName(this.resourceName))); - }; - - public cliInputsFileExists() { - return stateManager.resourceInputsJsonExists(this.projectRootPath, AmplifyCategories.API, this.resourceName); - } - - public getCliInputPayload() { - return stateManager.getResourceInputsJson(this.projectRootPath, AmplifyCategories.API, this.resourceName); - } - - public isCLIInputsValid(cliInputs?: ApigwInputs) { - if (!cliInputs) { - cliInputs = this.getCliInputPayload(); - } - - const schemaValidator = new CLIInputSchemaValidator(AmplifySupportedService.APIGW, AmplifyCategories.API, 'APIGatewayCLIInputs'); - schemaValidator.validateInput(JSONUtilities.stringify(cliInputs)); - } - - private async createApigwArtifacts() { - const resourceDirPath = pathManager.getResourceDirectoryPath(this.projectRootPath, AmplifyCategories.API, this.resourceName); - fs.ensureDirSync(resourceDirPath); - - const buildDirPath = join(resourceDirPath, PathConstants.BuildDirName); - fs.ensureDirSync(buildDirPath); - - stateManager.setResourceInputsJson(this.projectRootPath, AmplifyCategories.API, this.resourceName, { version: 1, paths: this.paths }); - - stateManager.setResourceParametersJson(this.projectRootPath, AmplifyCategories.API, this.resourceName, {}); - - const stack = new ApigwStackTransform(this.context, this.resourceName, this); - await stack.transform(); - } - - convertCrudOperationsToPermissions(crudOps: CrudOperation[]) { - const output = []; - for (const op of crudOps) { - switch (op) { - case CrudOperation.CREATE: - output.push('/POST'); - break; - case CrudOperation.READ: - output.push('/GET'); - break; - case CrudOperation.UPDATE: - output.push('/PUT'); - output.push('/PATCH'); - break; - case CrudOperation.DELETE: - output.push('/DELETE'); - break; - } - } - return output; - } -} - -type AdminQueriesProps = { - apiName: string; - functionName: string; - authResourceName: string; - dependsOn: $TSObject[]; -}; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/aws-constants.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/aws-constants.ts index 418b53b47ff..824a2cafef6 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/aws-constants.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/aws-constants.ts @@ -1,8 +1,8 @@ -import * as path from 'path'; +import path from 'path'; export const provider = 'awscloudformation'; export const parametersFileName = 'api-params.json'; export const cfnParametersFilename = 'parameters.json'; export const gqlSchemaFilename = 'schema.graphql'; -export const rootAssetDir = path.resolve(path.join(__dirname, '..', '..', '..', 'resources', 'awscloudformation')); +export const rootAssetDir = path.resolve(path.join(__dirname, '../../../resources/awscloudformation')); diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/apigw-stack-builder.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/apigw-stack-builder.ts deleted file mode 100644 index 085021435b0..00000000000 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/apigw-stack-builder.ts +++ /dev/null @@ -1,406 +0,0 @@ -import * as apigw from '@aws-cdk/aws-apigateway'; -import * as lambda from '@aws-cdk/aws-lambda'; -import * as cdk from '@aws-cdk/core'; -import { $TSObject, JSONUtilities } from 'amplify-cli-core'; -import { AmplifyApigwResourceTemplate, ApigwInputs, ApigwPathPolicy } from './types'; - -const CFN_TEMPLATE_FORMAT_VERSION = '2010-09-09'; -const ROOT_CFN_DESCRIPTION = 'API Gateway Resource for AWS Amplify CLI'; - -export class AmplifyApigwResourceStack extends cdk.Stack implements AmplifyApigwResourceTemplate { - _scope: cdk.Construct; - restApi!: apigw.CfnRestApi; - deploymentResource: apigw.CfnDeployment; - _lambdaPermission: lambda.CfnPermission; - _props: ApigwInputs; - paths: $TSObject; - policies: { [pathName: string]: ApigwPathPolicy }; - _cfnParameterMap: Map = new Map(); - - constructor(scope: cdk.Construct, id: string, props: ApigwInputs) { - super(scope, id, undefined); - this._scope = scope; - this._props = props; - this.paths = {}; - this.templateOptions.templateFormatVersion = CFN_TEMPLATE_FORMAT_VERSION; - this.templateOptions.description = ROOT_CFN_DESCRIPTION; - } - - /** - * - * @param props - * @param logicalId - */ - addCfnOutput(props: cdk.CfnOutputProps, logicalId: string): void { - new cdk.CfnOutput(this, logicalId, props); - } - - /** - * - * @param props - * @param logicalId - */ - addCfnMapping(props: cdk.CfnMappingProps, logicalId: string): void { - new cdk.CfnMapping(this, logicalId, props); - } - - /** - * - * @param props - * @param logicalId - */ - addCfnCondition(props: cdk.CfnConditionProps, logicalId: string): void { - new cdk.CfnCondition(this, logicalId, props); - } - - /** - * - * @param props - * @param logicalId - */ - addCfnResource(props: cdk.CfnResourceProps, logicalId: string): void { - new cdk.CfnResource(this, logicalId, props); - } - - /** - * - * @param props - * @param logicalId - */ - addLambdaPermissionCfnResource(props: lambda.CfnPermissionProps, logicalId: string): void { - new lambda.CfnPermission(this, logicalId, props); - } - - /** - * - * @param props - * @param logicalId - */ - addCfnParameter(props: cdk.CfnParameterProps, logicalId: string): void { - if (this._cfnParameterMap.has(logicalId)) { - throw new Error(`logical id "${logicalId}" already exists`); - } - this._cfnParameterMap.set(logicalId, new cdk.CfnParameter(this, logicalId, props)); - } - - renderCloudFormationTemplate = (): string => { - return JSONUtilities.stringify(this._toCloudFormation()); - }; - - generateAdminQueriesStack = (resourceName: string, authResourceName: string) => { - this._constructCfnPaths(resourceName); - - this.restApi = new apigw.CfnRestApi(this, resourceName, { - description: '', // TODO - left blank in current CLI - name: resourceName, - body: { - swagger: '2.0', - info: { - version: '2018-05-24T17:52:00Z', - title: resourceName, - }, - host: cdk.Fn.join('', ['apigateway.', cdk.Fn.ref('AWS::Region'), '.amazonaws.com']), - basePath: cdk.Fn.conditionIf('ShouldNotCreateEnvResources', '/Prod', cdk.Fn.join('', ['/', cdk.Fn.ref('env')])), - schemes: ['https'], - paths: this.paths, - securityDefinitions: { - Cognito: { - type: 'apiKey', - name: 'Authorization', - in: 'header', - 'x-amazon-apigateway-authtype': 'cognito_user_pools', - 'x-amazon-apigateway-authorizer': { - providerARNs: [ - cdk.Fn.join('', [ - 'arn:aws:cognito-idp:', - cdk.Fn.ref('AWS::Region'), - ':', - cdk.Fn.ref('AWS::AccountId'), - ':userpool/', - cdk.Fn.ref(`auth${authResourceName}UserPoolId`), - ]), - ], - type: 'cognito_user_pools', - }, - }, - }, - definitions: { - Empty: { - type: 'object', - title: 'Empty Schema', - }, - }, - 'x-amazon-apigateway-request-validators': { - 'Validate query string parameters and headers': { - validateRequestParameters: true, - validateRequestBody: false, - }, - }, - }, - }); - - this._setDeploymentResource(resourceName); - }; - - generateStackResources = (resourceName: string) => { - this._constructCfnPaths(resourceName); - - this.restApi = new apigw.CfnRestApi(this, resourceName, { - description: '', // TODO - left blank in current CLI - failOnWarnings: true, - name: resourceName, - body: { - swagger: '2.0', - info: { - version: '2018-05-24T17:52:00Z', - title: resourceName, - }, - host: cdk.Fn.join('', ['apigateway.', cdk.Fn.ref('AWS::Region'), '.amazonaws.com']), - basePath: cdk.Fn.conditionIf('ShouldNotCreateEnvResources', '/Prod', cdk.Fn.join('', ['/', cdk.Fn.ref('env')])), - schemes: ['https'], - paths: this.paths, - securityDefinitions: { - sigv4: { - type: 'apiKey', - name: 'Authorization', - in: 'header', - 'x-amazon-apigateway-authtype': 'awsSigv4', - }, - }, - definitions: { - RequestSchema: { - type: 'object', - required: ['request'], - properties: { - request: { - type: 'string', - }, - }, - title: 'Request Schema', - }, - ResponseSchema: { - type: 'object', - required: ['response'], - properties: { - response: { - type: 'string', - }, - }, - title: 'Response Schema', - }, - }, - }, - }); - - this._setDeploymentResource(resourceName); - }; - - private _constructCfnPaths(resourceName: string) { - const addedFunctionPermissions = new Set(); - for (const [pathName, path] of Object.entries(this._props.paths)) { - let lambdaPermissionLogicalId: string; - if (resourceName === 'AdminQueries') { - this.paths[`/{proxy+}`] = getAdminQueriesPathObject(path.lambdaFunction); - lambdaPermissionLogicalId = 'AdminQueriesAPIGWPolicyForLambda'; - } else { - this.paths[pathName] = getDefaultPathObject(path.lambdaFunction); - this.paths[`${pathName}/{proxy+}`] = getDefaultPathObject(path.lambdaFunction); - lambdaPermissionLogicalId = `function${path.lambdaFunction}Permission${resourceName}`; - } - - if (addedFunctionPermissions.has(path.lambdaFunction)) { - addedFunctionPermissions.add(path.lambdaFunction); - this.addLambdaPermissionCfnResource( - { - functionName: cdk.Fn.ref(`function${path.lambdaFunction}Name`), - action: 'lambda:InvokeFunction', - principal: 'apigateway.amazonaws.com', - sourceArn: cdk.Fn.join('', [ - 'arn:aws:execute-api:', - cdk.Fn.ref('AWS::Region'), - ':', - cdk.Fn.ref('AWS::AccountId'), - ':', - cdk.Fn.ref(resourceName), - '/*/*/*', - ]), - }, - lambdaPermissionLogicalId, - ); - } - } - } - - private _setDeploymentResource = (resourceName: string) => { - this.deploymentResource = new apigw.CfnDeployment(this, `DeploymentAPIGW${resourceName}`, { - restApiId: cdk.Fn.ref(resourceName), - stageName: cdk.Fn.conditionIf('ShouldNotCreateEnvResources', 'Prod', cdk.Fn.ref('env')).toString(), - }); - }; -} - -const getAdminQueriesPathObject = (lambdaFunctionName: string) => ({ - options: { - consumes: ['application/json'], - produces: ['application/json'], - responses: { - '200': { - description: '200 response', - schema: { - $ref: '#/definitions/Empty', - }, - headers: { - 'Access-Control-Allow-Origin': { - type: 'string', - }, - 'Access-Control-Allow-Methods': { - type: 'string', - }, - 'Access-Control-Allow-Headers': { - type: 'string', - }, - }, - }, - }, - 'x-amazon-apigateway-integration': { - responses: { - default: { - statusCode: '200', - responseParameters: { - 'method.response.header.Access-Control-Allow-Methods': "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'", - 'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", - 'method.response.header.Access-Control-Allow-Origin': "'*'", - }, - }, - }, - passthroughBehavior: 'when_no_match', - requestTemplates: { - 'application/json': '{"statusCode": 200}', - }, - type: 'mock', - }, - }, - 'x-amazon-apigateway-any-method': { - produces: ['application/json'], - parameters: [ - { - name: 'proxy', - in: 'path', - required: true, - type: 'string', - }, - { - name: 'Authorization', - in: 'header', - required: false, - type: 'string', - }, - ], - responses: {}, - security: [ - { - Cognito: ['aws.cognito.signin.user.admin'], - }, - ], - 'x-amazon-apigateway-request-validator': 'Validate query string parameters and headers', - 'x-amazon-apigateway-integration': { - uri: cdk.Fn.join('', [ - 'arn:aws:apigateway:', - cdk.Fn.ref('AWS::Region'), - ':lambda:path/2015-03-31/functions/', - cdk.Fn.ref(`function${lambdaFunctionName}Arn`), - '/invocations', - ]), - passthroughBehavior: 'when_no_match', - httpMethod: 'POST', - cacheNamespace: 'n40eb9', - cacheKeyParameters: ['method.request.path.proxy'], - contentHandling: 'CONVERT_TO_TEXT', - type: 'aws_proxy', - }, - }, -}); - -const getDefaultPathObject = (lambdaFunctionName: string) => ({ - options: { - consumes: ['application/json'], - produces: ['application/json'], - responses: { - '200': response200, - }, - 'x-amazon-apigateway-integration': { - responses: { - default: defaultCorsResponseObject, - }, - requestTemplates: { - 'application/json': '{"statusCode": 200}', - }, - passthroughBehavior: 'when_no_match', - type: 'mock', - }, - }, - 'x-amazon-apigateway-any-method': { - consumes: ['application/json'], - produces: ['application/json'], - parameters: [ - { - in: 'body', - name: 'RequestSchema', - required: false, - schema: { - $ref: '#/definitions/RequestSchema', - }, - }, - ], - responses: { - '200': { - description: '200 response', - schema: { - $ref: '#/definitions/ResponseSchema', - }, - }, - }, - 'x-amazon-apigateway-integration': { - responses: { - default: { - statusCode: '200', - }, - }, - uri: cdk.Fn.join('', [ - 'arn:aws:apigateway:', - cdk.Fn.ref('AWS::Region'), - ':lambda:path/2015-03-31/functions/', - cdk.Fn.ref(`function${lambdaFunctionName}Arn`), - '/invocations', - ]), - passthroughBehavior: 'when_no_match', - httpMethod: 'POST', - type: 'aws_proxy', - }, - }, -}); - -const defaultCorsResponseObject = { - statusCode: '200', - responseParameters: { - 'method.response.header.Access-Control-Allow-Methods': "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'", - 'method.response.header.Access-Control-Allow-Headers': - "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", - 'method.response.header.Access-Control-Allow-Origin': "'*'", - }, -}; - -const response200 = { - description: '200 response', - headers: { - 'Access-Control-Allow-Origin': { - type: 'string', - }, - 'Access-Control-Allow-Methods': { - type: 'string', - }, - 'Access-Control-Allow-Headers': { - type: 'string', - }, - }, -}; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/apigw-stack-transform.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/apigw-stack-transform.ts deleted file mode 100644 index a7ec91c018d..00000000000 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/apigw-stack-transform.ts +++ /dev/null @@ -1,201 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import { - $TSAny, - $TSContext, - AmplifyCategories, - buildOverrideDir, - getAmplifyResourceByCategories, - JSONUtilities, - PathConstants, - pathManager, - stateManager, - Template, - writeCFNTemplate, -} from 'amplify-cli-core'; -import { formatter, printer } from 'amplify-prompts'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { AmplifyApigwResourceStack, ApigwInputs } from '.'; -import { category } from '../../../category-constants'; -import { ApigwInputState } from '../apigw-input-state'; - -export class ApigwStackTransform { - _app: cdk.App; - cliInputs: ApigwInputs; - resourceTemplateObj: AmplifyApigwResourceStack | undefined; - cliInputsState: ApigwInputState; - cfn!: Template; - cfnInputParams!: {}; - resourceName: string; - - constructor(context: $TSContext, resourceName: string, cliInputState?: ApigwInputState) { - this._app = new cdk.App(); - this.resourceName = resourceName; - - // Validate the cli-inputs.json for the resource - this.cliInputsState = cliInputState ?? new ApigwInputState(context, resourceName); - this.cliInputs = this.cliInputsState.getCliInputPayload(); - this.cliInputsState.isCLIInputsValid(); - } - - async transform() { - let authResourceName: string; - if (this.resourceName === 'AdminQueries') { - [authResourceName] = getAmplifyResourceByCategories(AmplifyCategories.AUTH).filter(resourceName => resourceName !== 'userPoolGroups'); - } - - // Generate cloudformation stack from cli-inputs.json - this.generateStack(authResourceName); - - // Generate cloudformation stack input params from cli-inputs.json - this.generateCfnInputParameters(); - - // Modify cloudformation files based on overrides - await this.applyOverrides(); - - // Save generated cloudformation.json and parameters.json files - await this.saveBuildFiles(); - } - - // TODO generate params - generateCfnInputParameters() { - this.cfnInputParams = {}; - } - - generateStack(authResourceName?: string) { - this.resourceTemplateObj = new AmplifyApigwResourceStack(this._app, 'AmplifyApigwResourceStack', this.cliInputs); - - if (authResourceName) { - this.resourceTemplateObj.addCfnParameter( - { - type: 'String', - default: `auth${authResourceName}UserPoolId`, - }, - `auth${authResourceName}UserPoolId`, - ); - } - - // Add Parameters - const addedFunctions = new Set(); - for (const path of Object.values(this.cliInputs.paths)) { - if (!addedFunctions.has(path.lambdaFunction)) { - addedFunctions.add(path.lambdaFunction); - this.resourceTemplateObj.addCfnParameter( - { - type: 'String', - default: `function${path.lambdaFunction}Name`, - }, - `function${path.lambdaFunction}Name`, - ); - this.resourceTemplateObj.addCfnParameter( - { - type: 'String', - default: `function${path.lambdaFunction}Arn`, - }, - `function${path.lambdaFunction}Arn`, - ); - } - } - - this.resourceTemplateObj.addCfnParameter( - { - type: 'String', - }, - 'env', - ); - - // Add conditions - this.resourceTemplateObj.addCfnCondition( - { - expression: cdk.Fn.conditionEquals(cdk.Fn.ref('env'), 'NONE'), - }, - 'ShouldNotCreateEnvResources', - ); - - // Add outputs - this.resourceTemplateObj.addCfnOutput( - { - value: cdk.Fn.join('', [ - 'https://', - this.cliInputsState.resourceName, - '.execute-api.', - cdk.Fn.ref('AWS::Region'), - '.amazonaws.com/', - cdk.Fn.conditionIf('ShouldNotCreateEnvResources', 'Prod', cdk.Fn.ref('env')) as unknown as string, - ]), - description: 'Root URL of the API gateway', - }, - 'RootUrl', - ); - - this.resourceTemplateObj.addCfnOutput( - { - value: this.resourceName, - description: 'API Friendly name', - }, - 'ApiName', - ); - - this.resourceTemplateObj.addCfnOutput( - { - value: cdk.Fn.ref(this.resourceName), - description: 'API ID (prefix of API URL)', - }, - 'ApiId', - ); - - // Add resources - this.resourceName === 'AdminQueries' - ? this.resourceTemplateObj.generateAdminQueriesStack(this.resourceName, authResourceName) - : this.resourceTemplateObj.generateStackResources(this.resourceName); - } - - async applyOverrides() { - const backendDir = pathManager.getBackendDirPath(); - const overrideFilePath = pathManager.getResourceDirectoryPath(undefined, AmplifyCategories.API, this.resourceName); - - const isBuild = await buildOverrideDir(backendDir, overrideFilePath).catch(error => { - printer.debug(`Skipping build as ${error.message}`); - return false; - }); - // skip if packageManager or override.ts not found - if (isBuild) { - const { overrideProps } = await import(path.join(overrideFilePath, 'build', 'override.js')).catch(error => { - formatter.list(['No override file found', `To override ${this.resourceName} run "amplify override api"`]); - return undefined; - }); - - // TODO: Check Script Options - if (overrideProps && typeof overrideProps === 'function') { - try { - this.resourceTemplateObj = overrideProps(this.resourceTemplateObj); - - // The vm module enables compiling and running code within V8 Virtual Machine contexts. - // The vm module is not a security mechanism. Do not use it to run untrusted code. - // const script = new vm.Script(overrideCode); - // script.runInContext(vm.createContext(cognitoStackTemplateObj)); - return; - } catch (error: $TSAny) { - throw new Error(error); - } - } - } - } - - async saveBuildFiles() { - if (this.resourceTemplateObj) { - this.cfn = JSONUtilities.parse(this.resourceTemplateObj.renderCloudFormationTemplate()); - } - - const resourceDirPath = pathManager.getResourceDirectoryPath(undefined, category, this.resourceName); - fs.ensureDirSync(resourceDirPath); - - const buildDirPath = path.join(resourceDirPath, PathConstants.BuildDirName); - fs.ensureDirSync(buildDirPath); - - stateManager.setResourceParametersJson(undefined, AmplifyCategories.API, this.resourceName, this.cfnInputParams); - - const cfnFilePath = path.resolve(path.join(buildDirPath, `${this.resourceName}-cloudformation-template.json`)); - return writeCFNTemplate(this.cfn, cfnFilePath); - } -} diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/index.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/index.ts deleted file mode 100644 index 7b96c26fd5c..00000000000 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './apigw-stack-builder'; -export * from './apigw-stack-transform'; -export * from './types'; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/types.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/types.ts deleted file mode 100644 index 2d01ec4b3fb..00000000000 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/cdk-stack-builder/types.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import * as apigwCdk from '@aws-cdk/aws-apigateway'; -import * as iamCdk from '@aws-cdk/aws-iam'; - -export type ApigwInputs = { - version: number; - paths: Path[]; -}; - -export type Path = { - lambdaFunction: string; - permissions: { - setting: PermissionSetting; - auth?: CrudOperation[]; - guest?: CrudOperation[]; - groups?: { [groupName: string]: CrudOperation[] }; - }; -}; - -export enum CrudOperation { - CREATE = 'CREATE', - READ = 'READ', - UPDATE = 'UPDATE', - DELETE = 'DELETE', -} - -export enum PermissionSetting { - PRIVATE = 'private', - PROTECTED = 'protected', - OPEN = 'open', -} - -type AmplifyCDKL1 = { - addCfnCondition(props: cdk.CfnConditionProps, logicalId: string): void; - addCfnMapping(props: cdk.CfnMappingProps, logicalId: string): void; - addCfnOutput(props: cdk.CfnOutputProps, logicalId: string): void; - addCfnParameter(props: cdk.CfnParameterProps, logicalId: string): void; - addCfnResource(props: cdk.CfnResourceProps, logicalId: string): void; -}; - -export type AmplifyApigwResourceTemplate = { - restApi: apigwCdk.CfnRestApi; - deploymentResource: apigwCdk.CfnDeployment; - policies?: { - [pathName: string]: ApigwPathPolicy; - }; -} & AmplifyCDKL1; - -export type ApigwPathPolicy = { - auth: iamCdk.CfnPolicy; - guest?: iamCdk.CfnPolicy; - groups?: { [groupName: string]: iamCdk.CfnPolicy }; -}; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/cfn-api-artifact-handler.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/cfn-api-artifact-handler.ts index 3b5ed5d8c88..f06fc279414 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/cfn-api-artifact-handler.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/cfn-api-artifact-handler.ts @@ -1,4 +1,4 @@ -import { $TSAny, $TSContext, isResourceNameUnique, JSONUtilities, pathManager, stateManager } from 'amplify-cli-core'; +import { isResourceNameUnique } from 'amplify-cli-core'; import { AddApiRequest, AppSyncServiceConfiguration, @@ -6,12 +6,11 @@ import { ResolutionStrategy, UpdateApiRequest, } from 'amplify-headless-interface'; -import { printer } from 'amplify-prompts'; import * as fs from 'fs-extra'; import { readTransformerConfiguration, TRANSFORM_CURRENT_VERSION, writeTransformerConfiguration } from 'graphql-transformer-core'; import _ from 'lodash'; import * as path from 'path'; -import { v4 as uuid } from 'uuid'; +import uuid from 'uuid'; import { category } from '../../category-constants'; import { ApiArtifactHandler, ApiArtifactHandlerOptions } from '../api-artifact-handler'; import { cfnParametersFilename, gqlSchemaFilename, provider, rootAssetDir } from './aws-constants'; @@ -23,7 +22,7 @@ import { conflictResolutionToResolverConfig } from './utils/resolver-config-to-c // keep in sync with ServiceName in amplify-category-function, but probably it will not change const FunctionServiceNameLambdaFunction = 'Lambda'; -export const getCfnApiArtifactHandler = (context: $TSContext): ApiArtifactHandler => { +export const getCfnApiArtifactHandler = (context): ApiArtifactHandler => { return new CfnApiArtifactHandler(context); }; const resolversDirName = 'resolvers'; @@ -36,17 +35,16 @@ const defaultCfnParameters = (apiName: string) => ({ DynamoDBEnableServerSideEncryption: false, }); class CfnApiArtifactHandler implements ApiArtifactHandler { - private readonly context: $TSContext; + private readonly context: any; - constructor(context: $TSContext) { + constructor(context) { this.context = context; } // TODO once the AddApiRequest contains multiple services this class should depend on an ApiArtifactHandler // for each service and delegate to the correct one createArtifacts = async (request: AddApiRequest): Promise => { - const meta = stateManager.getMeta(); - const existingApiName = getAppSyncResourceName(meta); + const existingApiName = getAppSyncResourceName(this.context.amplify.getProjectMeta()); if (existingApiName) { throw new Error(`GraphQL API ${existingApiName} already exists in the project. Use 'amplify update api' to make modifications.`); } @@ -100,7 +98,7 @@ class CfnApiArtifactHandler implements ApiArtifactHandler { // for each service and delegate to the correct one updateArtifacts = async (request: UpdateApiRequest, opts?: ApiArtifactHandlerOptions): Promise => { const updates = request.serviceModification; - const apiName = getAppSyncResourceName(stateManager.getMeta()); + const apiName = getAppSyncResourceName(this.context.amplify.getProjectMeta()); if (!apiName) { throw new Error(`No AppSync API configured in the project. Use 'amplify add api' to create an API.`); } @@ -112,7 +110,7 @@ class CfnApiArtifactHandler implements ApiArtifactHandler { updates.conflictResolution = await this.createResolverResources(updates.conflictResolution); await writeResolverConfig(updates.conflictResolution, resourceDir); } - const authConfig = getAppSyncAuthConfig(stateManager.getMeta()); + const authConfig = getAppSyncAuthConfig(this.context.amplify.getProjectMeta()); const oldConfigHadApiKey = authConfigHasApiKey(authConfig); if (updates.defaultAuthType) { authConfig.defaultAuthentication = appSyncAuthTypeToAuthConfig(updates.defaultAuthType); @@ -131,14 +129,14 @@ class CfnApiArtifactHandler implements ApiArtifactHandler { this.context.amplify.updateamplifyMetaAfterResourceUpdate(category, apiName, 'output', { authConfig }); this.context.amplify.updateBackendConfigAfterResourceUpdate(category, apiName, 'output', { authConfig }); - printApiKeyWarnings(oldConfigHadApiKey, authConfigHasApiKey(authConfig)); + printApiKeyWarnings(this.context, oldConfigHadApiKey, authConfigHasApiKey(authConfig)); }; private writeSchema = (resourceDir: string, schema: string) => { fs.writeFileSync(path.join(resourceDir, gqlSchemaFilename), schema); }; - private getResourceDir = (apiName: string) => pathManager.getResourceDirectoryPath(undefined, category, apiName); + private getResourceDir = (apiName: string) => path.join(this.context.amplify.pathManager.getBackendDirPath(), category, apiName); private createAmplifyMeta = authConfig => ({ service: 'AppSync', @@ -182,8 +180,8 @@ class CfnApiArtifactHandler implements ApiArtifactHandler { }; private getCfnParameters = (apiName: string, authConfig, resourceDir: string) => { - const cfnPath = path.join(resourceDir, cfnParametersFilename); - const params = JSONUtilities.readJson<$TSAny>(cfnPath, { throwIfNotExist: false }) || defaultCfnParameters(apiName); + const params = + this.context.amplify.readJsonFile(path.join(resourceDir, cfnParametersFilename), undefined, false) || defaultCfnParameters(apiName); const cognitoPool = this.getCognitoUserPool(authConfig); if (cognitoPool) { params.AuthCognitoUserPoolId = cognitoPool; @@ -200,7 +198,7 @@ class CfnApiArtifactHandler implements ApiArtifactHandler { const defaultAuth = authConfig.defaultAuthentication || {}; if (defaultAuth.authenticationType === 'AMAZON_COGNITO_USER_POOLS' || additionalUserPoolProvider) { let userPoolId; - const configuredUserPoolName = checkIfAuthExists(); + const configuredUserPoolName = checkIfAuthExists(this.context); if (authConfig.userPoolConfig) { ({ userPoolId } = authConfig.userPoolConfig); @@ -219,7 +217,7 @@ class CfnApiArtifactHandler implements ApiArtifactHandler { }; private createSyncFunction = async () => { - const targetDir = pathManager.getBackendDirPath(); + const targetDir = this.context.amplify.pathManager.getBackendDirPath(); const assetDir = path.normalize(path.join(rootAssetDir, 'sync-conflict-handler')); const [shortId] = uuid().split('-'); @@ -234,17 +232,17 @@ class CfnApiArtifactHandler implements ApiArtifactHandler { { dir: assetDir, template: 'sync-conflict-handler-index.js.ejs', - target: path.join(targetDir, 'function', functionName, 'src', 'index.js'), + target: `${targetDir}/function/${functionName}/src/index.js`, }, { dir: assetDir, template: 'sync-conflict-handler-package.json.ejs', - target: path.join(targetDir, 'function', functionName, 'src', 'package.json'), + target: `${targetDir}/function/${functionName}/src/package.json`, }, { dir: assetDir, template: 'sync-conflict-handler-template.json.ejs', - target: path.join(targetDir, 'function', functionName, `${functionName}-cloudformation-template.json`), + target: `${targetDir}/function/${functionName}/${functionName}-cloudformation-template.json`, }, ]; @@ -258,7 +256,7 @@ class CfnApiArtifactHandler implements ApiArtifactHandler { }; await this.context.amplify.updateamplifyMetaAfterResourceAdd('function', functionName, backendConfigs); - printer.success(`Successfully added ${functionName} function locally`); + this.context.print.success(`Successfully added ${functionName} function locally`); return functionName + '-${env}'; }; @@ -270,7 +268,7 @@ class CfnApiArtifactHandler implements ApiArtifactHandler { * * write to the transformer conf if the resolverConfig is valid */ -export const writeResolverConfig = async (conflictResolution: ConflictResolution, resourceDir: string) => { +export const writeResolverConfig = async (conflictResolution: ConflictResolution, resourceDir) => { const localTransformerConfig = await readTransformerConfiguration(resourceDir); localTransformerConfig.ResolverConfig = conflictResolutionToResolverConfig(conflictResolution); await writeTransformerConfiguration(resourceDir, localTransformerConfig); diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/containers-handler.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/containers-handler.ts index 1bc866ab6c5..0a402bd123d 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/containers-handler.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/containers-handler.ts @@ -1,22 +1,22 @@ -import { $TSContext, createDefaultCustomPoliciesFile, pathManager } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { v4 as uuid } from 'uuid'; +import fs from 'fs-extra'; +import path from 'path'; +import uuid from 'uuid'; import { NETWORK_STACK_LOGICAL_ID } from '../../category-constants'; import { DEPLOYMENT_MECHANISM } from './base-api-stack'; import { GitHubSourceActionInfo } from './pipeline-with-awaiter'; import { API_TYPE, IMAGE_SOURCE_TYPE, ResourceDependency, ServiceConfiguration } from './service-walkthroughs/containers-walkthrough'; import { ApiResource, generateContainersArtifacts } from './utils/containers-artifacts'; +import { createDefaultCustomPoliciesFile, pathManager } from 'amplify-cli-core'; export const addResource = async ( serviceWalkthroughPromise: Promise, - context: $TSContext, - category: string, + context, + category, service, options, apiType: API_TYPE, ) => { + const projectBackendDirPath = context.amplify.pathManager.getBackendDirPath(); const walkthroughOptions = await serviceWalkthroughPromise; const { @@ -31,7 +31,7 @@ export const addResource = async ( dependsOn = [], mutableParametersState, } = walkthroughOptions; - const resourceDirPath = pathManager.getResourceDirectoryPath(undefined, category, resourceName); + const resourceDirPath = path.join(projectBackendDirPath, category, resourceName); let [authName, updatedDependsOn] = await getResourceDependencies({ dependsOn, restrictAccess, category, resourceName, context }); @@ -88,37 +88,36 @@ export const addResource = async ( if (imageSource.type === IMAGE_SOURCE_TYPE.TEMPLATE) { fs.copySync( - path.join(__dirname, '..', '..', '..', 'resources', 'awscloudformation/container-templates', imageSource.template), + path.join(__dirname, '../../../resources/awscloudformation/container-templates', imageSource.template), path.join(resourceDirPath, 'src'), - { recursive: true }, + { recursive: true } ); const { exposedContainer } = await generateContainersArtifacts(context, apiResource); await context.amplify.updateamplifyMetaAfterResourceUpdate(category, options.resourceName, 'exposedContainer', exposedContainer); + } createDefaultCustomPoliciesFile(category, resourceName); const customPoliciesPath = pathManager.getCustomPoliciesPath(category, resourceName); - printer.success(`Successfully added resource ${resourceName} locally.`); - printer.info(''); - printer.success('Next steps:'); + context.print.success(`Successfully added resource ${resourceName} locally.`); + context.print.info(''); + context.print.success('Next steps:'); if (deploymentMechanism === DEPLOYMENT_MECHANISM.FULLY_MANAGED) { - printer.info( - `- Place your Dockerfile, docker-compose.yml and any related container source files in "amplify/backend/api/${resourceName}/src"`, - ); + context.print.info(`- Place your Dockerfile, docker-compose.yml and any related container source files in "amplify/backend/api/${resourceName}/src"`); } else if (deploymentMechanism === DEPLOYMENT_MECHANISM.INDENPENDENTLY_MANAGED) { - printer.info( + context.print.info( `- Ensure you have the Dockerfile, docker-compose.yml and any related container source files in your Github path: ${gitHubInfo.path}`, ); } - printer.info( + context.print.info( `- Amplify CLI infers many configuration settings from the "docker-compose.yaml" file. Learn more: docs.amplify.aws/cli/usage/containers`, ); - printer.info(`- To access AWS resources outside of this Amplify app, edit the ${customPoliciesPath}`); - printer.info('- Run "amplify push" to build and deploy your image'); + context.print.info(`- To access AWS resources outside of this Amplify app, edit the ${customPoliciesPath}`); + context.print.info('- Run "amplify push" to build and deploy your image'); return resourceName; }; @@ -132,7 +131,7 @@ const getResourceDependencies = async ({ }: { restrictAccess: boolean; dependsOn: ResourceDependency[]; - context: $TSContext; + context: any; category: string; resourceName: string; }) => { @@ -166,7 +165,7 @@ const getResourceDependencies = async ({ apiRequirements, ]); } catch (e) { - printer.error(e); + context.print.error(e); throw e; } } else { @@ -193,7 +192,7 @@ const getResourceDependencies = async ({ return [authName, updatedDependsOn]; }; -export const updateResource = async (serviceWalkthroughPromise: Promise, context: $TSContext, category: string) => { +export const updateResource = async (serviceWalkthroughPromise: Promise, context, category) => { const options = await serviceWalkthroughPromise; const { diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/apigw-defaults.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/apigw-defaults.js similarity index 68% rename from packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/apigw-defaults.ts rename to packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/apigw-defaults.js index 60dbcd9da47..7ab876f8fa1 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/apigw-defaults.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/apigw-defaults.js @@ -1,6 +1,6 @@ -import { v4 as uuid } from 'uuid'; +const uuid = require('uuid'); -export const getAllDefaults = (project: { projectConfig: { projectName: string } }) => { +const getAllDefaults = project => { const name = project.projectConfig.projectName.toLowerCase().replace(/[^0-9a-zA-Z]/gi, ''); const [shortId] = uuid().split('-'); const defaults = { @@ -11,3 +11,7 @@ export const getAllDefaults = (project: { projectConfig: { projectName: string } return defaults; }; + +module.exports = { + getAllDefaults, +}; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/appSync-defaults.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/appSync-defaults.js similarity index 71% rename from packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/appSync-defaults.ts rename to packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/appSync-defaults.js index 8c48966c22c..0d5cb71570a 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/appSync-defaults.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/appSync-defaults.js @@ -1,7 +1,6 @@ -import { $TSMeta } from 'amplify-cli-core'; -import { v4 as uuid } from 'uuid'; +const uuid = require('uuid'); -export const getAllDefaults = (project: { amplifyMeta: $TSMeta; projectConfig: { projectName: string } }) => { +const getAllDefaults = project => { const name = project.projectConfig.projectName.toLowerCase(); const region = project.amplifyMeta.providers.awscloudformation.Region; const [shortId] = uuid().split('-'); @@ -17,3 +16,7 @@ export const getAllDefaults = (project: { amplifyMeta: $TSMeta; projectConfig: { return defaults; }; + +module.exports = { + getAllDefaults, +}; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/containers-defaults.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/containers-defaults.ts index 0f77dee83e9..da44caf946f 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/containers-defaults.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/default-values/containers-defaults.ts @@ -1,6 +1,6 @@ -import { v4 as uuid } from 'uuid'; +const uuid = require('uuid'); -export const getAllDefaults = () => { +const getAllDefaults = project => { const [shortId] = uuid().split('-'); const defaults = { resourceName: `container${shortId}`, @@ -8,3 +8,7 @@ export const getAllDefaults = () => { return defaults; }; + +module.exports = { + getAllDefaults, +}; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/docker-compose/index.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/docker-compose/index.ts index a66729ca105..39f92df12e2 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/docker-compose/index.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/docker-compose/index.ts @@ -1 +1,3 @@ -export { getContainers } from './converter'; +import { getContainers } from "./converter"; + +export { getContainers }; \ No newline at end of file diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-alb-stack.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-alb-stack.ts index 01ca720b389..c66f3719603 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-alb-stack.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/ecs-alb-stack.ts @@ -7,8 +7,8 @@ import * as elb2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as route53 from '@aws-cdk/aws-route53'; import * as route53targets from '@aws-cdk/aws-route53-targets'; import * as cdk from '@aws-cdk/core'; -import { v4 as uuid } from 'uuid'; import { ContainersStack, ContainersStackProps } from './base-api-stack'; +import { v4 as uuid } from 'uuid'; type EcsStackProps = ContainersStackProps & Readonly<{ diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/index.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/index.ts index f8e36add0fb..4ee7ce6fee8 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/index.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/index.ts @@ -1,52 +1,28 @@ -import { $TSAny, $TSContext, $TSObject, AmplifySupportedService, exitOnNextTick, NotImplementedError } from 'amplify-cli-core'; -import { UpdateApiRequest } from 'amplify-headless-interface'; -import { printer } from 'amplify-prompts'; -import inquirer from 'inquirer'; -import * as path from 'path'; -import { category } from '../../category-constants'; -import { ApigwInputState } from './apigw-input-state'; +import { serviceWalkthroughResultToAddApiRequest } from './utils/service-walkthrough-result-to-add-api-request'; import { getCfnApiArtifactHandler } from './cfn-api-artifact-handler'; -import { addResource as addContainer, updateResource as updateContainer } from './containers-handler'; +import { serviceMetadataFor, getServiceWalkthrough, datasourceMetadataFor } from './utils/dynamic-imports'; import { legacyAddResource } from './legacy-add-resource'; +import { legacyUpdateResource } from './legacy-update-resource'; +import { UpdateApiRequest } from 'amplify-headless-interface'; +import { editSchemaFlow } from './utils/edit-schema-flow'; +import { NotImplementedError, exitOnNextTick } from 'amplify-cli-core'; +import { addResource as addContainer, updateResource as updateContainer } from './containers-handler'; +import inquirer from 'inquirer'; import { API_TYPE, - getPermissionPolicies as getContainerPermissionPolicies, ServiceConfiguration, + getPermissionPolicies as getContainerPermissionPolicies, } 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: $TSObject[] }, -) { - 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: $TSObject[] }, -) { - const apigwInputState = new ApigwInputState(context, apiProps.apiName); - // Check for migration - - if (!apigwInputState.cliInputsFileExists()) { - await apigwInputState.migrateAdminQueries(apiProps); - } else { - return apigwInputState.updateAdminQueriesResource(apiProps); - } -} +import { category } from '../../category-constants'; -export async function console(context: $TSContext, service: string) { +export async function console(context, service) { const { serviceWalkthroughFilename } = await serviceMetadataFor(service); - const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename); - const { openConsole } = await import(serviceWalkthroughSrc); + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { openConsole } = require(serviceWalkthroughSrc); if (!openConsole) { const errMessage = 'Opening console functionality not available for this option'; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new NotImplementedError(errMessage)); exitOnNextTick(0); } @@ -54,23 +30,24 @@ export async function console(context: $TSContext, service: string) { return openConsole(context); } -async function addContainerResource(context: $TSContext, service: string, options, apiType: API_TYPE) { +async function addContainerResource(context, category, service, options, apiType) { const serviceWalkthroughFilename = 'containers-walkthrough.js'; + const defaultValuesFilename = 'containers-defaults.js'; const serviceWalkthrough = await getServiceWalkthrough(serviceWalkthroughFilename); - const serviceWalkthroughPromise: Promise<$TSAny> = serviceWalkthrough(context, apiType); + const serviceWalkthroughPromise: Promise = serviceWalkthrough(context, defaultValuesFilename, apiType); return await addContainer(serviceWalkthroughPromise, context, category, service, options, apiType); } -async function addNonContainerResource(context: $TSContext, service: string, options) { +async function addNonContainerResource(context, category, service, options) { const serviceMetadata = await serviceMetadataFor(service); - const { serviceWalkthroughFilename } = serviceMetadata; + const { serviceWalkthroughFilename, defaultValuesFilename } = serviceMetadata; const serviceWalkthrough = await getServiceWalkthrough(serviceWalkthroughFilename); - const serviceWalkthroughPromise: Promise<$TSAny> = serviceWalkthrough(context, serviceMetadata); + const serviceWalkthroughPromise: Promise = serviceWalkthrough(context, defaultValuesFilename, serviceMetadata); switch (service) { - case AmplifySupportedService.APPSYNC: + case 'AppSync': const walkthroughResult = await serviceWalkthroughPromise; const askToEdit = walkthroughResult.askToEdit; const apiName = await getCfnApiArtifactHandler(context).createArtifacts(serviceWalkthroughResultToAddApiRequest(walkthroughResult)); @@ -78,26 +55,23 @@ async function addNonContainerResource(context: $TSContext, service: string, opt 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) { +export async function addResource(context, category, service, options) { let useContainerResource = false; let apiType = API_TYPE.GRAPHQL; if (isContainersEnabled(context)) { switch (service) { - case AmplifySupportedService.APPSYNC: - useContainerResource = await isGraphQLContainer(); + case 'AppSync': + useContainerResource = await isGraphQLContainer(context); apiType = API_TYPE.GRAPHQL; break; - case AmplifySupportedService.APIGW: - useContainerResource = await isRestContainer(); + case 'API Gateway': + useContainerResource = await isRestContainer(context); apiType = API_TYPE.REST; break; default: @@ -106,11 +80,11 @@ export async function addResource(context: $TSContext, service: string, options) } return useContainerResource - ? addContainerResource(context, service, options, apiType) - : addNonContainerResource(context, service, options); + ? addContainerResource(context, category, service, options, apiType) + : addNonContainerResource(context, category, service, options); } -function isContainersEnabled(context: $TSContext) { +function isContainersEnabled(context) { const { frontend } = context.amplify.getProjectConfig(); if (frontend) { const { config: { ServerlessContainers = false } = {} } = context.amplify.getProjectConfig()[frontend] || {}; @@ -121,14 +95,14 @@ function isContainersEnabled(context: $TSContext) { return false; } -async function isGraphQLContainer(): Promise { +async function isGraphQLContainer(context): Promise { const { graphqlSelection } = await inquirer.prompt({ name: 'graphqlSelection', message: 'Which service would you like to use', type: 'list', choices: [ { - name: AmplifySupportedService.APPSYNC, + name: 'AppSync', value: false, }, { @@ -141,7 +115,7 @@ async function isGraphQLContainer(): Promise { return graphqlSelection; } -async function isRestContainer() { +async function isRestContainer(context) { const { restSelection } = await inquirer.prompt({ name: 'restSelection', message: 'Which service would you like to use', @@ -161,18 +135,22 @@ async function isRestContainer() { return restSelection; } -export async function updateResource(context: $TSContext, category: string, service: string, options) { +export async function updateResource(context, category, service, 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); + const { + hasAPIGatewayContainerResource, + hasAPIGatewayLambdaResource, + hasGraphQLAppSyncResource, + hasGraphqlContainerResource, + } = await describeApiResourcesBySubCategory(context); switch (service) { - case AmplifySupportedService.APPSYNC: + case 'AppSync': if (hasGraphQLAppSyncResource && hasGraphqlContainerResource) { - useContainerResource = await isGraphQLContainer(); + useContainerResource = await isGraphQLContainer(context); } else if (hasGraphqlContainerResource) { useContainerResource = true; } else { @@ -180,9 +158,9 @@ export async function updateResource(context: $TSContext, category: string, serv } apiType = API_TYPE.GRAPHQL; break; - case AmplifySupportedService.APIGW: + case 'API Gateway': if (hasAPIGatewayContainerResource && hasAPIGatewayLambdaResource) { - useContainerResource = await isRestContainer(); + useContainerResource = await isRestContainer(context); } else if (hasAPIGatewayContainerResource) { useContainerResource = true; } else { @@ -195,10 +173,12 @@ export async function updateResource(context: $TSContext, category: string, serv } } - return useContainerResource ? updateContainerResource(context, category, service, apiType) : updateNonContainerResource(context, service); + return useContainerResource + ? updateContainerResource(context, category, service, apiType) + : updateNonContainerResource(context, category, service); } -async function describeApiResourcesBySubCategory(context: $TSContext) { +async function describeApiResourcesBySubCategory(context) { const { allResources } = await context.amplify.getResourceStatus(); const resources = allResources.filter(resource => resource.category === category && resource.mobileHubMigrated !== true); @@ -211,9 +191,9 @@ async function describeApiResourcesBySubCategory(context: $TSContext) { hasAPIGatewayContainerResource = hasAPIGatewayContainerResource || (resource.service === 'ElasticContainer' && resource.apiType === API_TYPE.REST); - hasAPIGatewayLambdaResource = hasAPIGatewayLambdaResource || resource.service === AmplifySupportedService.APIGW; + hasAPIGatewayLambdaResource = hasAPIGatewayLambdaResource || resource.service === 'API Gateway'; - hasGraphQLAppSyncResource = hasGraphQLAppSyncResource || resource.service === AmplifySupportedService.APPSYNC; + hasGraphQLAppSyncResource = hasGraphQLAppSyncResource || resource.service === 'AppSync'; hasGraphqlContainerResource = hasGraphqlContainerResource || (resource.service === 'ElasticContainer' && resource.apiType === API_TYPE.GRAPHQL); @@ -227,32 +207,33 @@ async function describeApiResourcesBySubCategory(context: $TSContext) { }; } -async function updateContainerResource(context: $TSContext, category: string, service: string, apiType: API_TYPE) { +async function updateContainerResource(context, category, service, apiType: API_TYPE) { const serviceWalkthroughFilename = 'containers-walkthrough'; - const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename); - const { updateWalkthrough } = await import(serviceWalkthroughSrc); + const defaultValuesFilename = 'containers-defaults.js'; + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { updateWalkthrough } = require(serviceWalkthroughSrc); if (!updateWalkthrough) { const errMessage = 'Update functionality not available for this option'; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new NotImplementedError(errMessage)); exitOnNextTick(0); } - const updateWalkthroughPromise: Promise = updateWalkthrough(context, apiType); + const updateWalkthroughPromise: Promise = updateWalkthrough(context, defaultValuesFilename, apiType); updateContainer(updateWalkthroughPromise, context, category); } -async function updateNonContainerResource(context: $TSContext, service: string) { +async function updateNonContainerResource(context, category, service) { const serviceMetadata = await serviceMetadataFor(service); const { defaultValuesFilename, serviceWalkthroughFilename } = serviceMetadata; - const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename); - const { updateWalkthrough } = await import(serviceWalkthroughSrc); + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { updateWalkthrough } = require(serviceWalkthroughSrc); if (!updateWalkthrough) { const errMessage = 'Update functionality not available for this option'; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new NotImplementedError(errMessage)); exitOnNextTick(0); } @@ -260,15 +241,14 @@ async function updateNonContainerResource(context: $TSContext, service: string) const updateWalkthroughPromise: Promise = updateWalkthrough(context, defaultValuesFilename, serviceMetadata); switch (service) { - case AmplifySupportedService.APPSYNC: + case 'AppSync': return updateWalkthroughPromise.then(getCfnApiArtifactHandler(context).updateArtifacts); default: - const apigwInputState = new ApigwInputState(context); - return apigwInputState.updateApigwResource(updateWalkthroughPromise); + return legacyUpdateResource(updateWalkthroughPromise, context, category, service); } } -export async function migrateResource(context: $TSContext, projectPath: string, service: string, resourceName: string) { +export async function migrateResource(context, projectPath, service, resourceName) { if (service === 'ElasticContainer') { return migrateResourceContainer(context, projectPath, service, resourceName); } else { @@ -276,53 +256,53 @@ export async function migrateResource(context: $TSContext, projectPath: string, } } -async function migrateResourceContainer(context: $TSContext, projectPath: string, service: string, resourceName: string) { - printer.info(`No migration required for ${resourceName}`); +async function migrateResourceContainer(context, projectPath, service, resourceName) { + context.print.info(`No migration required for ${resourceName}`); return; } -async function migrateResourceNonContainer(context: $TSContext, projectPath: string, service: string, resourceName: string) { +async function migrateResourceNonContainer(context, projectPath, service, resourceName) { const serviceMetadata = await serviceMetadataFor(service); const { serviceWalkthroughFilename } = serviceMetadata; - const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename); - const { migrate } = await import(serviceWalkthroughSrc); + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { migrate } = require(serviceWalkthroughSrc); if (!migrate) { - printer.info(`No migration required for ${resourceName}`); + context.print.info(`No migration required for ${resourceName}`); return; } return await migrate(context, projectPath, resourceName); } -export async function addDatasource(context: $TSContext, category, datasource) { +export async function addDatasource(context, category, datasource) { const serviceMetadata = await datasourceMetadataFor(datasource); - const { serviceWalkthroughFilename } = serviceMetadata; - return (await getServiceWalkthrough(serviceWalkthroughFilename))(context, serviceMetadata); + const { defaultValuesFilename, serviceWalkthroughFilename } = serviceMetadata; + return (await getServiceWalkthrough(serviceWalkthroughFilename))(context, defaultValuesFilename, serviceMetadata); } -export async function getPermissionPolicies(context: $TSContext, service: string, resourceName: string, crudOptions) { +export async function getPermissionPolicies(context, service, resourceName, crudOptions) { if (service === 'ElasticContainer') { return getPermissionPoliciesContainer(context, service, resourceName, crudOptions); } else { - return getPermissionPoliciesNonContainer(service, resourceName, crudOptions); + return getPermissionPoliciesNonContainer(context, service, resourceName, crudOptions); } } -async function getPermissionPoliciesContainer(context: $TSContext, service: string, resourceName: string, crudOptions) { +async function getPermissionPoliciesContainer(context, service, resourceName, crudOptions) { return getContainerPermissionPolicies(context, service, resourceName, crudOptions); } -async function getPermissionPoliciesNonContainer(service: string, resourceName: string, crudOptions: string[]) { +async function getPermissionPoliciesNonContainer(context, service, resourceName, crudOptions) { const serviceMetadata = await serviceMetadataFor(service); const { serviceWalkthroughFilename } = serviceMetadata; - const serviceWalkthroughSrc = path.join(__dirname, 'service-walkthroughs', serviceWalkthroughFilename); - const { getIAMPolicies } = await import(serviceWalkthroughSrc); + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { getIAMPolicies } = require(serviceWalkthroughSrc); if (!getIAMPolicies) { - printer.info(`No policies found for ${resourceName}`); + context.print.info(`No policies found for ${resourceName}`); return; } - return getIAMPolicies(resourceName, crudOptions); + return getIAMPolicies(resourceName, crudOptions, context); } diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/legacy-add-resource.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/legacy-add-resource.ts index 29ced6ba3b4..ff7e33a1890 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/legacy-add-resource.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/legacy-add-resource.ts @@ -1,4 +1,4 @@ -import { $TSAny, $TSContext, $TSObject, isResourceNameUnique, JSONUtilities, pathManager } from 'amplify-cli-core'; +import { isResourceNameUnique, JSONUtilities } from 'amplify-cli-core'; import * as fs from 'fs-extra'; import * as path from 'path'; import { cfnParametersFilename, parametersFileName, rootAssetDir } from './aws-constants'; @@ -6,15 +6,10 @@ import { serviceMetadataFor } from './utils/dynamic-imports'; // this is the old logic for generating resources in the project directory // it is still used for adding REST APIs -export const legacyAddResource = async ( - serviceWalkthroughPromise: Promise<$TSAny>, - context: $TSContext, - category: string, - service: string, - options: $TSObject, -) => { +export const legacyAddResource = async (serviceWalkthroughPromise: Promise, context, category, service, options) => { let answers; let { cfnFilename } = await serviceMetadataFor(service); + const projectBackendDirPath = context.amplify.pathManager.getBackendDirPath(); const result = await serviceWalkthroughPromise; @@ -35,7 +30,7 @@ export const legacyAddResource = async ( copyCfnTemplate(context, category, answers, cfnFilename); const parameters = { ...answers }; - const resourceDirPath = pathManager.getResourceDirectoryPath(undefined, category, parameters.resourceName); + const resourceDirPath = path.join(projectBackendDirPath, category, parameters.resourceName); isResourceNameUnique(category, parameters.resourceName); @@ -52,14 +47,15 @@ export const legacyAddResource = async ( }; // exported because the update flow still uses this method directly for now -export const copyCfnTemplate = (context: $TSContext, category: string, options, cfnFilename) => { - const resourceDirPath = pathManager.getResourceDirectoryPath(undefined, category, options.resourceName); +export const copyCfnTemplate = (context, category, options, cfnFilename) => { + const { amplify } = context; + const targetDir = amplify.pathManager.getBackendDirPath(); const copyJobs = [ { dir: path.join(rootAssetDir, 'cloudformation-templates'), template: cfnFilename, - target: path.join(resourceDirPath, `${options.resourceName}-cloudformation-template.json`), + target: `${targetDir}/${category}/${options.resourceName}/${options.resourceName}-cloudformation-template.json`, }, ]; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/legacy-update-resource.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/legacy-update-resource.ts index 930523a24ad..99a15a9365f 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/legacy-update-resource.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/legacy-update-resource.ts @@ -1,15 +1,15 @@ -import { $TSAny, $TSContext, JSONUtilities, pathManager } from 'amplify-cli-core'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { parametersFileName } from './aws-constants'; -import { addPolicyResourceNameToPaths, copyCfnTemplate } from './legacy-add-resource'; import { serviceMetadataFor } from './utils/dynamic-imports'; +import { copyCfnTemplate, addPolicyResourceNameToPaths } from './legacy-add-resource'; +import fs from 'fs-extra'; +import path from 'path'; +import { parametersFileName } from './aws-constants'; -export const legacyUpdateResource = async (updateWalkthroughPromise: Promise<$TSAny>, context: $TSContext, category: string, service) => { +export const legacyUpdateResource = async (updateWalkthroughPromise: Promise, context, category, service) => { let answers; let { cfnFilename } = await serviceMetadataFor(service); + const projectBackendDirPath = context.amplify.pathManager.getBackendDirPath(); const result = await updateWalkthroughPromise; - const options: $TSAny = {}; + const options: any = {}; if (result) { if (result.answers) { ({ answers } = result); @@ -25,10 +25,11 @@ export const legacyUpdateResource = async (updateWalkthroughPromise: Promise<$TS addPolicyResourceNameToPaths(answers.paths); copyCfnTemplate(context, category, answers, cfnFilename); const parameters = { ...answers }; - const resourceDirPath = pathManager.getResourceDirectoryPath(undefined, category, parameters.resourceName); + const resourceDirPath = path.join(projectBackendDirPath, category, parameters.resourceName); fs.ensureDirSync(resourceDirPath); const parametersFilePath = path.join(resourceDirPath, parametersFileName); - JSONUtilities.writeJson(parametersFilePath, parameters); + const jsonString = JSON.stringify(parameters, null, 4); + fs.writeFileSync(parametersFilePath, jsonString, 'utf8'); context.amplify.updateamplifyMetaAfterResourceUpdate(category, answers.resourceName, 'dependsOn', answers.dependsOn); } } diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthrough-types/aPIGateway-user-input-types.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthrough-types/aPIGateway-user-input-types.ts deleted file mode 100644 index f17cb20de11..00000000000 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthrough-types/aPIGateway-user-input-types.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Defines the json object expected by the amplify api category - */ -export interface APIGatewayCLIInputs { - /** - * The schema version. - */ - version: 1; - - /** - * map of paths in the REST API. - */ - paths: { [pathName: string]: Path }; -} - -type Path = { - lambdaFunction: string; - permissions: { - setting: PermissionSetting; - auth?: CrudOperation[]; - guest?: CrudOperation[]; - groups?: { [groupName: string]: CrudOperation[] }[]; - }; -}; - -enum CrudOperation { - CREATE = 'CREATE', - READ = 'READ', - UPDATE = 'UPDATE', - DELETE = 'DELETE', -} - -enum PermissionSetting { - PRIVATE = 'private', - PROTECTED = 'protected', - OPEN = 'open', -} diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthrough-types/apigw-types.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthrough-types/apigw-types.ts deleted file mode 100644 index 3e535880265..00000000000 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthrough-types/apigw-types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { $TSObject } from 'amplify-cli-core'; -import { CrudOperation, PermissionSetting } from '../cdk-stack-builder'; - -export type ApigwPath = { - name: string; - permissions: { - setting: PermissionSetting; - auth?: CrudOperation[]; - unauth?: CrudOperation[]; - userPoolGroups?: { - [userPoolGroupName: string]: CrudOperation[]; - }; - }; - lambdaFunction: string; -}; - -export type ApiRequirements = { authSelections: 'identityPoolAndUserPool'; allowUnauthenticatedIdentities?: boolean }; - -export type ApigwWalkthroughReturnPromise = Promise<{ - answers: ApigwAnswers; -}>; - -export type ApigwAnswers = { - paths: { [pathName: string]: ApigwPath }; - resourceName: string; - functionArns?: string[]; - dependsOn?: $TSObject[]; -}; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.ts index dea104bcc0d..ac3390df3e3 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.ts @@ -1,95 +1,113 @@ -import { - $TSAny, - $TSContext, - $TSObject, - AmplifyCategories, - AmplifySupportedService, - exitOnNextTick, - isResourceNameUnique, - open, - pathManager, - ResourceDoesNotExistError, - stateManager, -} from 'amplify-cli-core'; -import { printer, prompter } from 'amplify-prompts'; +import { $TSContext, exitOnNextTick, isResourceNameUnique, open, ResourceDoesNotExistError, stateManager } from 'amplify-cli-core'; +import * as fs from 'fs-extra'; import inquirer from 'inquirer'; import os from 'os'; -import { v4 as uuid } from 'uuid'; -import { ApigwInputState } from '../apigw-input-state'; -import { CrudOperation, PermissionSetting } from '../cdk-stack-builder'; -import { getAllDefaults } from '../default-values/apigw-defaults'; -import { ApigwAnswers, ApigwPath, ApigwWalkthroughReturnPromise, ApiRequirements } from '../service-walkthrough-types/apigw-types'; +import * as path from 'path'; +import uuid from 'uuid'; +import { rootAssetDir } from '../aws-constants'; import { checkForPathOverlap, formatCFNPathParamsForExpressJs, validatePathName } from '../utils/rest-api-path-utils'; -const category = AmplifyCategories.API; -const serviceName = AmplifySupportedService.APIGW; +// keep in sync with ServiceName in amplify-category-function, but probably it will not change +const FunctionServiceNameLambdaFunction = 'Lambda'; + +const category = 'api'; +const serviceName = 'API Gateway'; const elasticContainerServiceName = 'ElasticContainer'; +const parametersFileName = 'api-params.json'; +const cfnParametersFilename = 'parameters.json'; + +export async function serviceWalkthrough(context, defaultValuesFilename) { + const { amplify } = context; + const defaultValuesSrc = `${__dirname}/../default-values/${defaultValuesFilename}`; + const { getAllDefaults } = await import(defaultValuesSrc); + const allDefaultValues = getAllDefaults(amplify.getProjectDetails()); -export async function serviceWalkthrough(context: $TSContext): ApigwWalkthroughReturnPromise { - const allDefaultValues = getAllDefaults(context.amplify.getProjectDetails()); + let answers = { + paths: [], + }; - const resourceName = await askApiName(context, allDefaultValues.resourceName); - const answers = { paths: {}, resourceName, dependsOn: undefined }; + const apiNames = await askApiNames(context, allDefaultValues); + answers = { ...answers, ...apiNames }; return pathFlow(context, answers); } -export async function updateWalkthrough(context: $TSContext) { +export async function updateWalkthrough(context, defaultValuesFilename) { + const { amplify } = context; const { allResources } = await context.amplify.getResourceStatus(); - const allDefaultValues = getAllDefaults(context.amplify.getProjectDetails()); + const defaultValuesSrc = `${__dirname}/../default-values/${defaultValuesFilename}`; + const { getAllDefaults } = await import(defaultValuesSrc); + const allDefaultValues = getAllDefaults(amplify.getProjectDetails()); const resources = allResources .filter(resource => resource.service === serviceName && resource.mobileHubMigrated !== true) .map(resource => resource.resourceName); + // There can only be one appsync resource if (resources.length === 0) { - const errMessage = 'No REST API resource to update. Use "amplify add api" command to create a new REST API'; - printer.error(errMessage); + const errMessage = 'No REST API resource to update. Please use "amplify add api" command to create a new REST API'; + context.print.error(errMessage); await context.usageData.emitError(new ResourceDoesNotExistError(errMessage)); exitOnNextTick(0); return; } - let answers: $TSAny = { + let answers: any = { paths: [], }; - const selectedApiName = await prompter.pick<'one', string>('Select the REST API you want to update:', resources); - let updateApiOperation = await prompter.pick<'one', string>('What would you like to do?', [ - { name: 'Add another path', value: 'add' }, - { name: 'Update path', value: 'update' }, - { name: 'Remove path', value: 'remove' }, - ]); + const question = [ + { + name: 'resourceName', + message: 'Please select the REST API you would want to update', + type: 'list', + choices: resources, + }, + { + name: 'operation', + message: 'What would you like to do', + type: 'list', + when: context.input.command !== 'add', + choices: [ + { name: 'Add another path', value: 'add' }, + { name: 'Update path', value: 'update' }, + { name: 'Remove path', value: 'remove' }, + ], + }, + ]; + + const updateApi = await inquirer.prompt(question); // Inquirer does not currently support combining 'when' and 'default', so // manually set the operation if the user ended up here via amplify api add. if (context.input.command === 'add') { - updateApiOperation = 'add'; + updateApi.operation = 'add'; } - if (selectedApiName === 'AdminQueries') { + if (updateApi.resourceName === 'AdminQueries') { const errMessage = `The Admin Queries API is maintained through the Auth category and should be updated using 'amplify update auth' command`; - printer.warn(errMessage); + context.print.warning(errMessage); await context.usageData.emitError(new ResourceDoesNotExistError(errMessage)); exitOnNextTick(0); } - const projRoot = pathManager.findProjectRoot(); - if (!stateManager.resourceInputsJsonExists(projRoot, category, selectedApiName)) { - // Not yet migrated - console.log(selectedApiName); - await migrate(context, projRoot, selectedApiName); + const projectBackendDirPath = context.amplify.pathManager.getBackendDirPath(); + const resourceDirPath = path.join(projectBackendDirPath, category, updateApi.resourceName as string); + const parametersFilePath = path.join(resourceDirPath, parametersFileName); + let parameters; + try { + parameters = context.amplify.readJsonFile(parametersFilePath); + } catch (e) { + parameters = {}; } - - const parameters = stateManager.getResourceInputsJson(projRoot, category, selectedApiName); - parameters.resourceName = selectedApiName; + parameters.resourceName = updateApi.resourceName; Object.assign(allDefaultValues, parameters); answers = { ...answers, ...parameters }; [answers.uuid] = uuid().split('-'); - const pathNames = Object.keys(answers.paths); + const pathList = answers.paths.map(path => path.name); let updatedResult = {}; - switch (updateApiOperation) { + switch (updateApi.operation) { case 'add': { updatedResult = pathFlow(context, answers); break; @@ -97,52 +115,76 @@ export async function updateWalkthrough(context: $TSContext) { case 'remove': { const pathToRemove = await inquirer.prompt({ name: 'path', - message: 'Select the path you would want to remove', + message: 'Please select the path you would want to remove', type: 'list', - choices: pathNames, + choices: pathList, }); - delete answers.paths[pathToRemove.path]; + answers.paths = answers.paths.filter(path => path.name !== pathToRemove.path); - const { dependsOn, functionArns } = await findDependsOn(answers.paths); + const { dependsOn, functionArns } = await findDependsOn(answers.paths, context); answers.dependsOn = dependsOn; answers.functionArns = functionArns; - updatedResult = { answers }; + updatedResult = { answers, dependsOn }; break; } case 'update': { const pathToEdit = await inquirer.prompt({ - name: 'pathName', - message: 'Select the path you would want to edit', + name: 'path', + message: 'Please select the path you would want to edit', type: 'list', - choices: pathNames, + choices: pathList, }); // removing path from paths list - const currentPath: ApigwPath = answers.paths[pathToEdit.pathName]; - delete answers.paths[pathToEdit.pathName]; + const currentPath = answers.paths.find(path => path.name === pathToEdit.path); + answers.paths = answers.paths.filter(path => path.name !== pathToEdit.path); updatedResult = pathFlow(context, answers, currentPath); break; } default: { - throw new Error(`Unrecognized API update operation "${updateApiOperation}"`); + updatedResult = {}; } } return updatedResult; } -async function pathFlow(context: $TSContext, answers: ApigwAnswers, currentPath?: ApigwPath): ApigwWalkthroughReturnPromise { +async function pathFlow(context, answers, currentPath?) { const pathsAnswer = await askPaths(context, answers, currentPath); + answers = { ...answers, paths: pathsAnswer.paths, functionArns: pathsAnswer.functionArns }; + const { dependsOn } = pathsAnswer; - return { answers: pathsAnswer }; + const privacy = { + auth: pathsAnswer.paths.filter(path => path.privacy.auth && path.privacy.auth.length > 0).length, + unauth: pathsAnswer.paths.filter(path => path.privacy.unauth && path.privacy.unauth.length > 0).length, + }; + + answers = { ...answers, privacy, dependsOn }; + + if ( + context.amplify.getProjectDetails() && + context.amplify.getProjectDetails().amplifyMeta && + context.amplify.getProjectDetails().amplifyMeta.providers && + context.amplify.getProjectDetails().amplifyMeta.providers.awscloudformation + ) { + // TODO: read from utility functions (Dustin PR) + const { amplifyMeta } = context.amplify.getProjectDetails(); + const providerInfo = amplifyMeta.providers.awscloudformation; + + answers.privacy.authRoleName = providerInfo.AuthRoleName; + answers.privacy.unAuthRoleName = providerInfo.UnauthRoleName; + } + + return { answers, dependsOn }; } -async function askApiName(context: $TSContext, defaultResourceName: string) { +async function askApiNames(context, defaults) { + const { amplify } = context; const apiNameValidator = (input: string) => { - const amplifyValidatorOutput = context.amplify.inputValidation({ + const amplifyValidatorOutput = amplify.inputValidation({ validation: { operator: 'regex', value: '^[a-zA-Z0-9]+$', @@ -151,11 +193,6 @@ async function askApiName(context: $TSContext, defaultResourceName: string) { required: true, })(input); - const adminQueriesName = 'AdminQueries'; - if (input === adminQueriesName) { - return `${adminQueriesName} is a reserved name for REST API resources for use by the auth category. Run "amplify update auth" to create an Admin Queries API.`; - } - let uniqueCheck = false; try { uniqueCheck = isResourceNameUnique(category, input); @@ -165,90 +202,111 @@ async function askApiName(context: $TSContext, defaultResourceName: string) { return typeof amplifyValidatorOutput === 'string' ? amplifyValidatorOutput : uniqueCheck; }; - const resourceName = await prompter.input<'one', string>( - 'Provide a friendly name for your resource to be used as a label for this category in the project:', - { initial: defaultResourceName, validate: apiNameValidator }, - ); + const answer: { apiName?: string; resourceName: string } = await inquirer.prompt([ + { + name: 'resourceName', + type: 'input', + message: 'Provide a friendly name for your resource to be used as a label for this category in the project:', + default: defaults.resourceName, + validate: apiNameValidator, + }, + ]); + + answer.apiName = answer.resourceName; - return resourceName; + return answer; } -async function askPermissions( - context: $TSContext, - answers: $TSObject, - currentPath: ApigwPath, -): Promise<{ setting?: PermissionSetting; auth?: CrudOperation[]; open?: boolean; userPoolGroups?: $TSObject; unauth?: CrudOperation[] }> { +async function askPrivacy(context, answers, currentPath) { while (true) { - const apiAccess = await prompter.yesOrNo('Restrict API access', currentPath?.permissions?.setting !== PermissionSetting.OPEN); + const apiAccess = await inquirer.prompt({ + name: 'restrict', + type: 'confirm', + default: !(currentPath && currentPath.open), + message: 'Restrict API access', + }); - if (!apiAccess) { - return { setting: PermissionSetting.OPEN }; + if (!apiAccess.restrict) { + return { open: true }; } - const userPoolGroupList = context.amplify.getUserPoolGroupList(); + const userPoolGroupList = await context.amplify.getUserPoolGroupList(context); let permissionSelected = 'Auth/Guest Users'; - const permissions: $TSAny = {}; + const privacy: any = {}; if (userPoolGroupList.length > 0) { do { if (permissionSelected === 'Learn more') { - printer.blankLine(); - printer.info( - 'You can restrict access using CRUD policies for Authenticated Users, Guest Users, or on individual Group that users belong to' + - ' in a User Pool. If a user logs into your application and is not a member of any group they will use policy set for ' + - '“Authenticated Users”, however if they belong to a group they will only get the policy associated with that specific group.', + context.print.info(''); + context.print.info( + 'You can restrict access using CRUD policies for Authenticated Users, Guest Users, or on individual Group that users belong to in a User Pool. If a user logs into your application and is not a member of any group they will use policy set for “Authenticated Users”, however if they belong to a group they will only get the policy associated with that specific group.', ); - printer.blankLine(); + context.print.info(''); } - const permissionSelection = await prompter.pick<'one', string>('Restrict access by?', [ - 'Auth/Guest Users', - 'Individual Groups', - 'Both', - 'Learn more', - ]); - - permissionSelected = permissionSelection; + const permissionSelection = await inquirer.prompt({ + name: 'selection', + type: 'list', + message: 'Restrict access by?', + choices: ['Auth/Guest Users', 'Individual Groups', 'Both', 'Learn more'], + default: 'Auth/Guest Users', + }); + + permissionSelected = permissionSelection.selection; } while (permissionSelected === 'Learn more'); } if (permissionSelected === 'Both' || permissionSelected === 'Auth/Guest Users') { - const permissionSetting = await prompter.pick<'one', string>( - 'Who should have access?', - [ + const answer = await inquirer.prompt({ + name: 'privacy', + type: 'list', + message: 'Who should have access?', + choices: [ { name: 'Authenticated users only', - value: PermissionSetting.PRIVATE, + value: 'private', }, { name: 'Authenticated and Guest users', - value: PermissionSetting.PROTECTED, + value: 'protected', }, ], - { initial: currentPath?.permissions?.setting === PermissionSetting.PROTECTED ? 1 : 0 }, - ); + default: currentPath && currentPath.privacy && currentPath.privacy.protected ? 'protected' : 'private', + }); + + privacy[answer.privacy] = true; - permissions.setting = permissionSetting; + context.api = { + privacy: answer.privacy, + }; let { - permissions: { auth: authPermissions }, - } = currentPath || { permissions: { auth: [] } }; + privacy: { auth: authPrivacy }, + } = currentPath || { privacy: {} }; let { - permissions: { unauth: unauthPermissions }, - } = currentPath || { permissions: { unauth: [] } }; + privacy: { unauth: unauthPrivacy }, + } = currentPath || { privacy: {} }; - if (permissionSetting === PermissionSetting.PRIVATE) { - permissions.auth = await askCRUD('Authenticated', authPermissions); + // convert legacy permissions to CRUD structure + if (authPrivacy && ['r', 'rw'].includes(authPrivacy)) { + authPrivacy = convertToCRUD(authPrivacy); + } + if (unauthPrivacy && ['r', 'rw'].includes(unauthPrivacy)) { + unauthPrivacy = convertToCRUD(unauthPrivacy); + } + + if (answer.privacy === 'private') { + privacy.auth = await askReadWrite('Authenticated', context, authPrivacy); - const apiRequirements: ApiRequirements = { authSelections: 'identityPoolAndUserPool' }; + const apiRequirements = { authSelections: 'identityPoolAndUserPool' }; await ensureAuth(context, apiRequirements, answers.resourceName); } - if (permissionSetting === PermissionSetting.PROTECTED) { - permissions.auth = await askCRUD('Authenticated', authPermissions); - permissions.unauth = await askCRUD('Guest', unauthPermissions); - const apiRequirements: ApiRequirements = { authSelections: 'identityPoolAndUserPool', allowUnauthenticatedIdentities: true }; + if (answer.privacy === 'protected') { + privacy.auth = await askReadWrite('Authenticated', context, authPrivacy); + privacy.unauth = await askReadWrite('Guest', context, unauthPrivacy); + const apiRequirements = { authSelections: 'identityPoolAndUserPool', allowUnauthenticatedIdentities: true }; await ensureAuth(context, apiRequirements, answers.resourceName); } @@ -257,53 +315,60 @@ async function askPermissions( if (permissionSelected === 'Both' || permissionSelected === 'Individual Groups') { // Enable Auth if not enabled - const apiRequirements: ApiRequirements = { authSelections: 'identityPoolAndUserPool' }; + const apiRequirements = { authSelections: 'identityPoolAndUserPool' }; await ensureAuth(context, apiRequirements, answers.resourceName); // Get Auth resource name - const authResourceName = getAuthResourceName(); + const authResourceName = await getAuthResourceName(context); answers.authResourceName = authResourceName; let defaultSelectedGroups = []; - if (currentPath?.permissions?.userPoolGroups) { - defaultSelectedGroups = Object.keys(currentPath.permissions.userPoolGroups); + if (currentPath && currentPath.privacy && currentPath.privacy.userPoolGroups) { + defaultSelectedGroups = Object.keys(currentPath.privacy.userPoolGroups); } - const userPoolGroupSelection = await inquirer.prompt({ - name: 'userpoolGroups', - type: 'checkbox', - message: 'Select groups:', - choices: userPoolGroupList, - default: defaultSelectedGroups, - validate: inputs => { - if (inputs.length === 0) { - return 'Select at least one option'; - } - return true; + const userPoolGroupSelection = await inquirer.prompt([ + { + name: 'userpoolGroups', + type: 'checkbox', + message: 'Select groups:', + choices: userPoolGroupList, + default: defaultSelectedGroups, + validate: inputs => { + if (inputs.length === 0) { + return 'Select at least one option'; + } + return true; + }, }, - }); + ]); const selectedUserPoolGroupList = userPoolGroupSelection.userpoolGroups; - for (const selectedUserPoolGroup of selectedUserPoolGroupList) { + for (let i = 0; i < selectedUserPoolGroupList.length; i += 1) { let defaults = []; - if (currentPath?.permissions?.userPoolGroups?.[selectedUserPoolGroup]) { - defaults = currentPath.permissions.userPoolGroups[selectedUserPoolGroup]; + if ( + currentPath && + currentPath.privacy && + currentPath.privacy.userPoolGroups && + currentPath.privacy.userPoolGroups[selectedUserPoolGroupList[i]] + ) { + defaults = currentPath.privacy.userPoolGroups[selectedUserPoolGroupList[i]]; } - if (!permissions.userPoolGroups) { - permissions.userPoolGroups = {}; + if (!privacy.userPoolGroups) { + privacy.userPoolGroups = {}; } - permissions.userPoolGroups[selectedUserPoolGroup] = await askCRUD(selectedUserPoolGroup, defaults); + privacy.userPoolGroups[selectedUserPoolGroupList[i]] = await askReadWrite(selectedUserPoolGroupList[i], context, defaults); } } - return permissions; + return privacy; } } -async function ensureAuth(context: $TSContext, apiRequirements: ApiRequirements, resourceName: string) { - const checkResult: $TSAny = await context.amplify.invokePluginMethod(context, 'auth', undefined, 'checkRequirements', [ +async function ensureAuth(context, apiRequirements, resourceName) { + const checkResult = await context.amplify.invokePluginMethod(context, 'auth', undefined, 'checkRequirements', [ apiRequirements, context, 'api', @@ -317,7 +382,7 @@ async function ensureAuth(context: $TSContext, apiRequirements: ApiRequirements, } if (checkResult.errors && checkResult.errors.length > 0) { - printer.warn(checkResult.errors.join(os.EOL)); + context.print.warning(checkResult.errors.join(os.EOL)); } // If auth is not imported and there were errors, adjust or enable auth configuration @@ -325,36 +390,61 @@ async function ensureAuth(context: $TSContext, apiRequirements: ApiRequirements, try { await context.amplify.invokePluginMethod(context, 'auth', undefined, 'externalAuthEnable', [ context, - AmplifyCategories.API, + 'api', resourceName, apiRequirements, ]); } catch (error) { - printer.error(error); + context.print.error(error); throw error; } } } -async function askCRUD(userType: string, permissions: string[] = []) { - const crudOptions = ['create', 'read', 'update', 'delete']; - const crudAnswers = await prompter.pick<'many', string>(`What permissions do you want to grant to ${userType} users?`, crudOptions, { - returnSize: 'many', - initial: permissions.map(p => crudOptions.indexOf(p)), - }); +async function askReadWrite(userType, context, privacy) { + const permissionMap = { + create: ['/POST'], + read: ['/GET'], + update: ['/PUT', '/PATCH'], + delete: ['/DELETE'], + }; + + const defaults = []; + if (privacy) { + Object.values(permissionMap).forEach((el, index) => { + if (el.every(i => privacy.includes(i))) { + defaults.push(Object.keys(permissionMap)[index]); + } + }); + } + + const crudAnswers = await context.amplify.crudFlow(userType, permissionMap, defaults); return crudAnswers; } -async function askPaths(context: $TSContext, answers: $TSObject, currentPath: ApigwPath): Promise { - const existingFunctions = functionsExist(); +async function askPaths(context, answers, currentPath) { + // const existingLambdaArns = true; - let defaultFunctionType = 'newFunction'; - const defaultChoice = { - name: 'Create a new Lambda function', - value: defaultFunctionType, - }; - const choices = [defaultChoice]; + const existingFunctions = functionsExist(context); + + const choices = [ + { + name: 'Create a new Lambda function', + value: 'newFunction', + }, + ]; + + /* + Removing this option for now in favor of multi-env support + - NOT CRITICAL + if (existingLambdaArns) { + choices.push({ + name: 'Use a Lambda function already deployed on AWS', + value: 'arn', + }); + } + */ if (existingFunctions) { choices.push({ @@ -363,19 +453,28 @@ async function askPaths(context: $TSContext, answers: $TSObject, currentPath: Ap }); } - const paths = answers.paths; + let defaultFunctionType = 'newFunction'; + if (currentPath) { + defaultFunctionType = currentPath.lambdaArn ? 'arn' : 'projectFunction'; + } + + const paths = [...answers.paths]; - let addAnotherPath: boolean; + let addAnotherPath; do { - let pathName: string; - let isPathValid: boolean; + let pathName; + let isPathValid; do { - pathName = await prompter.input('Provide a path (e.g., /book/{isbn}):', { - initial: currentPath ? currentPath.name : '/items', - validate: validatePathName, + const pathAnswer = await inquirer.prompt({ + name: 'name', + type: 'input', + message: 'Provide a path (e.g., /book/{isbn}):', + default: currentPath ? currentPath.name : '/items', + validate: value => validatePathName(value), }); + pathName = pathAnswer.name; - const overlapCheckResult = checkForPathOverlap(pathName, Object.keys(paths)); + const overlapCheckResult = checkForPathOverlap(pathName, paths); if (overlapCheckResult === false) { // The path provided by the user is valid, and doesn't overlap with any other endpoints that they've stood up with API Gateway. isPathValid = true; @@ -384,64 +483,81 @@ async function askPaths(context: $TSContext, answers: $TSObject, currentPath: Ap // Ask them if they're okay with this. If they are, then we'll consider their provided path to be valid. const higherOrderPath = overlapCheckResult.higherOrderPath; const lowerOrderPath = overlapCheckResult.lowerOrderPath; - - isPathValid = await prompter.confirmContinue( - `The path ${lowerOrderPath} overlaps with ${higherOrderPath}. Users authorized to access ${higherOrderPath} will also have access` + - ` to ${lowerOrderPath}. Are you sure you want to continue?`, - ); + isPathValid = ( + await inquirer.prompt({ + name: 'isOverlappingPathOK', + type: 'confirm', + message: `The path ${lowerOrderPath} overlaps with ${higherOrderPath}. Users authorized to access ${higherOrderPath} will also have access to ${lowerOrderPath}. Are you sure you want to continue?`, + default: false, + }) + ).isOverlappingPathOK; } } while (!isPathValid); - const functionType = await prompter.pick<'one', string>('Choose a Lambda source', choices, { initial: choices.indexOf(defaultChoice) }); + const lambdaAnswer = await inquirer.prompt({ + name: 'functionType', + type: 'list', + message: 'Choose a Lambda source', + choices, + default: defaultFunctionType, + }); + // TODO: add path validation like awsmobile-cli does let path = { name: pathName }; let lambda; do { - lambda = await askLambdaSource(context, functionType, pathName, currentPath); + lambda = await askLambdaSource(context, lambdaAnswer.functionType, path.name, currentPath); } while (!lambda); - const permissions = await askPermissions(context, answers, currentPath); - path = { ...path, ...lambda, permissions }; - paths[pathName] = path; + const privacy = await askPrivacy(context, answers, currentPath); + path = { ...path, ...lambda, privacy }; + paths.push(path); if (currentPath) { break; } - addAnotherPath = await prompter.confirmContinue('Do you want to add another path?'); + addAnotherPath = ( + await inquirer.prompt({ + name: 'anotherPath', + type: 'confirm', + message: 'Do you want to add another path?', + default: false, + }) + ).anotherPath; } while (addAnotherPath); - const { dependsOn, functionArns } = await findDependsOn(paths); + const { dependsOn, functionArns } = await findDependsOn(paths, context); - return { paths, dependsOn, resourceName: answers.resourceName, functionArns }; + return { paths, dependsOn, functionArns }; } -async function findDependsOn(paths: $TSObject[]) { +async function findDependsOn(paths, context) { // go thru all paths and add lambdaFunctions to dependsOn and functionArns uniquely const dependsOn = []; const functionArns = []; - for (const path of Object.values(paths)) { - if (path.lambdaFunction && !path.lambdaArn) { - if (!dependsOn.find(func => func.resourceName === path.lambdaFunction)) { + for (let i = 0; i < paths.length; i += 1) { + if (paths[i].lambdaFunction && !paths[i].lambdaArn) { + if (!dependsOn.find(func => func.resourceName === paths[i].lambdaFunction)) { dependsOn.push({ category: 'function', - resourceName: path.lambdaFunction, + resourceName: paths[i].lambdaFunction, attributes: ['Name', 'Arn'], }); } } - if (!functionArns.find(func => func.lambdaFunction === path.lambdaFunction)) { + if (!functionArns.find(func => func.lambdaFunction === paths[i].lambdaFunction)) { functionArns.push({ - lambdaFunction: path.lambdaFunction, - lambdaArn: path.lambdaArn, + lambdaFunction: paths[i].lambdaFunction, + lambdaArn: paths[i].lambdaArn, }); } - if (path?.permissions?.userPoolGroups) { - const userPoolGroups = Object.keys(path.privacy.userPoolGroups); + if (paths[i].privacy && paths[i].privacy.userPoolGroups) { + const userPoolGroups = Object.keys(paths[i].privacy.userPoolGroups); if (userPoolGroups.length > 0) { // Get auth resource name - const authResourceName = getAuthResourceName(); + const authResourceName = await getAuthResourceName(context); if (!dependsOn.find(resource => resource.resourceName === authResourceName)) { dependsOn.push({ @@ -466,29 +582,26 @@ async function findDependsOn(paths: $TSObject[]) { return { dependsOn, functionArns }; } -function getAuthResourceName(): string { - const meta = stateManager.getMeta(); - const authResources = (Object.entries(meta?.auth) || []).filter( - ([_, resource]: [key: string, resource: $TSObject]) => resource.service === AmplifySupportedService.COGNITO, - ); +async function getAuthResourceName(context) { + let authResources = (await context.amplify.getResourceStatus('auth')).allResources; + authResources = authResources.filter(resource => resource.service === 'Cognito'); if (authResources.length === 0) { - throw new Error('No auth resource found. Add it using amplify add auth'); + throw new Error('No auth resource found. Please add it using amplify add auth'); } - const [authResourceName] = authResources[0]; + const authResourceName = authResources[0].resourceName; return authResourceName; } -function functionsExist() { - const meta = stateManager.getMeta(); - if (!meta.function) { +function functionsExist(context) { + if (!context.amplify.getProjectDetails().amplifyMeta.function) { return false; } - const functionResources = meta.function; + const functionResources = context.amplify.getProjectDetails().amplifyMeta.function; const lambdaFunctions = []; Object.keys(functionResources).forEach(resourceName => { - if (functionResources[resourceName].service === AmplifySupportedService.LAMBDA) { + if (functionResources[resourceName].service === FunctionServiceNameLambdaFunction) { lambdaFunctions.push(resourceName); } }); @@ -500,20 +613,26 @@ function functionsExist() { return true; } -async function askLambdaSource(context: $TSContext, functionType: string, path: string, currentPath: ApigwPath) { +async function askLambdaSource(context, functionType, path, currentPath) { switch (functionType) { case 'arn': return askLambdaArn(context, currentPath); case 'projectFunction': - return askLambdaFromProject(currentPath); + return askLambdaFromProject(context, currentPath); case 'newFunction': - return newLambdaFunction(context as $TSAny, path); + return newLambdaFunction(context, path); default: throw new Error('Type not supported'); } } -async function newLambdaFunction(context: $TSContext, path: string) { +async function newLambdaFunction(context, path) { + context.api = { + path, + // ExpressJS represents path parameters as /:param instead of /{param}. This expression performs this replacement. + expressPath: formatCFNPathParamsForExpressJs(path), + functionTemplate: 'serverless', + }; let params = { functionTemplate: { parameters: { @@ -523,35 +642,39 @@ async function newLambdaFunction(context: $TSContext, path: string) { }, }; - const resourceName = await context.amplify.invokePluginMethod(context, AmplifyCategories.FUNCTION, undefined, 'add', [ + const resourceName = await context.amplify.invokePluginMethod(context, 'function', undefined, 'add', [ context, 'awscloudformation', - AmplifySupportedService.LAMBDA, + FunctionServiceNameLambdaFunction, params, ]); - printer.success('Succesfully added the Lambda function locally'); + context.print.success('Succesfully added the Lambda function locally'); return { lambdaFunction: resourceName }; } -async function askLambdaFromProject(currentPath?: ApigwPath) { - const meta = stateManager.getMeta(); +async function askLambdaFromProject(context, currentPath) { + const functionResources = context.amplify.getProjectDetails().amplifyMeta.function; const lambdaFunctions = []; - Object.keys(meta?.function || {}).forEach(resourceName => { - if (meta.function[resourceName].service === AmplifySupportedService.LAMBDA) { + Object.keys(functionResources).forEach(resourceName => { + if (functionResources[resourceName].service === FunctionServiceNameLambdaFunction) { lambdaFunctions.push(resourceName); } }); - const lambdaFunction = await prompter.pick<'one', string>('Choose the Lambda function to invoke by this path', lambdaFunctions, { - initial: currentPath ? lambdaFunctions.indexOf(currentPath.lambdaFunction) : 0, + const answer = await inquirer.prompt({ + name: 'lambdaFunction', + type: 'list', + message: 'Choose the Lambda function to invoke by this path', + choices: lambdaFunctions, + default: currentPath ? currentPath.lambdaFunction : lambdaFunctions[0], }); - return { lambdaFunction }; + return { lambdaFunction: answer.lambdaFunction }; } -async function askLambdaArn(context: $TSContext, currentPath?: ApigwPath) { +async function askLambdaArn(context, currentPath) { const lambdaFunctions = await context.amplify.executeProviderUtils(context, 'awscloudformation', 'getLambdaFunctions'); const lambdaOptions = lambdaFunctions.map(lambdaFunction => ({ @@ -560,16 +683,16 @@ async function askLambdaArn(context: $TSContext, currentPath?: ApigwPath) { })); if (lambdaOptions.length === 0) { - printer.error('You do not have any Lambda functions configured for the selected Region'); + context.print.error('You do not have any Lambda functions configured for the selected Region'); return null; } const lambdaCloudOptionQuestion = { type: 'list', name: 'lambdaChoice', - message: 'Select a Lambda function', + message: 'Please select a Lambda function', choices: lambdaOptions, - default: currentPath && currentPath.lambdaFunction ? `${currentPath.lambdaFunction}` : `${lambdaOptions[0].value}`, + default: currentPath && currentPath.lambdaArn ? `${currentPath.lambdaArn}` : `${lambdaOptions[0].value}`, }; let lambdaOption; @@ -577,7 +700,7 @@ async function askLambdaArn(context: $TSContext, currentPath?: ApigwPath) { try { lambdaOption = await inquirer.prompt([lambdaCloudOptionQuestion]); } catch (err) { - printer.error('Select a Lambda Function'); + context.print.error('Select a Lambda Function'); } } @@ -589,12 +712,56 @@ async function askLambdaArn(context: $TSContext, currentPath?: ApigwPath) { }; } -export async function migrate(context: $TSContext, projectPath: string, resourceName: string) { - const apigwInputState = new ApigwInputState(context, resourceName); - return apigwInputState.migrateApigwResource(resourceName); +export async function migrate(context, projectPath, resourceName) { + const { amplify } = context; + + const targetDir = amplify.pathManager.getBackendDirPath(); + const resourceDirPath = path.join(targetDir, category, resourceName); + const parametersFilePath = path.join(resourceDirPath, parametersFileName); + let parameters; + try { + parameters = amplify.readJsonFile(parametersFilePath); + } catch (e) { + context.print.error(`Error reading api-params.json file for ${resourceName} resource`); + throw e; + } + const copyJobs = [ + { + dir: path.join(rootAssetDir, 'cloudformation-templates'), + template: 'apigw-cloudformation-template-default.json.ejs', + target: `${targetDir}/${category}/${resourceName}/${resourceName}-cloudformation-template.json`, + }, + ]; + + // copy over the files + await context.amplify.copyBatch(context, copyJobs, parameters, true, false); + + // Create parameters.json file + const cfnParameters = { + authRoleName: { + Ref: 'AuthRoleName', + }, + unauthRoleName: { + Ref: 'UnauthRoleName', + }, + }; + + const cfnParametersFilePath = path.join(resourceDirPath, cfnParametersFilename); + const jsonString = JSON.stringify(cfnParameters, null, 4); + fs.writeFileSync(cfnParametersFilePath, jsonString, 'utf8'); +} + +function convertToCRUD(privacy) { + if (privacy === 'r') { + privacy = ['/GET']; + } else if (privacy === 'rw') { + privacy = ['/POST', '/GET', '/PUT', '/PATCH', '/DELETE']; + } + + return privacy; } -export function getIAMPolicies(resourceName: string, crudOptions: string[]) { +export function getIAMPolicies(resourceName, crudOptions) { let policy = {}; const actions = []; @@ -613,7 +780,7 @@ export function getIAMPolicies(resourceName: string, crudOptions: string[]) { actions.push('apigateway:DELETE'); break; default: - printer.info(`${crudOption} not supported`); + console.log(`${crudOption} not supported`); } }); @@ -645,7 +812,7 @@ export function getIAMPolicies(resourceName: string, crudOptions: string[]) { return { policy, attributes }; } -export const openConsole = async (context?: $TSContext) => { +export const openConsole = async (context: $TSContext) => { const amplifyMeta = stateManager.getMeta(); const categoryAmplifyMeta = amplifyMeta[category]; const { Region } = amplifyMeta.providers.awscloudformation; @@ -663,7 +830,12 @@ export const openConsole = async (context?: $TSContext) => { let selectedApi = restApis[0]; if (restApis.length > 1) { - selectedApi = await prompter.pick<'one', string>('Select the API', restApis); + ({ selectedApi } = await inquirer.prompt({ + type: 'list', + name: 'selectedApi', + choices: restApis, + message: 'Please select the API', + })); } const selectedResource = categoryAmplifyMeta[selectedApi]; @@ -681,29 +853,34 @@ export const openConsole = async (context?: $TSContext) => { const codePipeline = 'CodePipeline'; const elasticContainer = 'ElasticContainer'; - const selectedConsole = await prompter.pick<'one', string>('Which console you want to open', [ - { - name: 'Elastic Container Service (Deployed container status)', - value: elasticContainer, - }, - { - name: 'CodePipeline (Container build status)', - value: codePipeline, - }, - ]); + const { selectedConsole } = await inquirer.prompt({ + name: 'selectedConsole', + message: 'Which console you want to open', + type: 'list', + choices: [ + { + name: 'Elastic Container Service (Deployed container status)', + value: elasticContainer, + }, + { + name: 'CodePipeline (Container build status)', + value: codePipeline, + }, + ], + }); if (selectedConsole === elasticContainer) { url = `https://console.aws.amazon.com/ecs/home?region=${Region}#/clusters/${ClusterName}/services/${ServiceName}/details`; } else if (selectedConsole === codePipeline) { url = `https://${Region}.console.aws.amazon.com/codesuite/codepipeline/pipelines/${PipelineName}/view`; } else { - printer.error('Option not available'); + context.print.error('Option not available'); return; } } open(url, { wait: false }); } else { - printer.error('There are no REST APIs pushed to the cloud'); + context.print.error('There are no REST APIs pushed to the cloud'); } }; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-rds-walkthrough.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-rds-walkthrough.ts index 10d0b2eac01..1d0a92fb11f 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-rds-walkthrough.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-rds-walkthrough.ts @@ -1,22 +1,21 @@ -import { $TSContext, $TSObject, exitOnNextTick, ResourceCredentialsNotFoundError, ResourceDoesNotExistError } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; -import chalk from 'chalk'; -import { DataApiParams } from 'graphql-relational-schema-transformer'; import inquirer from 'inquirer'; +import chalk from 'chalk'; import ora from 'ora'; +import { DataApiParams } from 'graphql-relational-schema-transformer'; +import { ResourceDoesNotExistError, ResourceCredentialsNotFoundError, exitOnNextTick, $TSContext, $TSObject } from 'amplify-cli-core'; const spinner = ora(''); const category = 'api'; const providerName = 'awscloudformation'; -export async function serviceWalkthrough(context: $TSContext, datasourceMetadata: $TSObject) { +export async function serviceWalkthrough(context: $TSContext, defaultValuesFilename: string, datasourceMetadata: $TSObject) { const amplifyMeta = context.amplify.getProjectMeta(); // Verify that an API exists in the project before proceeding. if (amplifyMeta == null || amplifyMeta[category] == null || Object.keys(amplifyMeta[category]).length === 0) { const errMessage = 'You must create an AppSync API in your project before adding a graphql datasource. Please use "amplify api add" to create the API.'; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new ResourceDoesNotExistError(errMessage)); exitOnNextTick(0); } @@ -25,9 +24,9 @@ export async function serviceWalkthrough(context: $TSContext, datasourceMetadata let appSyncApi: string; const apis = Object.keys(amplifyMeta[category]); - for (const api of apis) { - if (amplifyMeta[category][api].service === 'AppSync') { - appSyncApi = api; + for (let i = 0; i < apis.length; i += 1) { + if (amplifyMeta[category][apis[i]].service === 'AppSync') { + appSyncApi = apis[i]; break; } } @@ -36,7 +35,7 @@ export async function serviceWalkthrough(context: $TSContext, datasourceMetadata if (!appSyncApi) { const errMessage = 'You must create an AppSync API in your project before adding a graphql datasource. Please use "amplify api add" to create the API.'; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new ResourceDoesNotExistError(errMessage)); exitOnNextTick(0); } @@ -87,7 +86,7 @@ async function selectCluster(context: $TSContext, inputs, AWS) { if (serverlessClusters.length === 0) { const errMessage = 'No properly configured Aurora Serverless clusters found.'; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new ResourceDoesNotExistError(errMessage)); @@ -111,7 +110,7 @@ async function selectCluster(context: $TSContext, inputs, AWS) { // Pick first and only value const firstCluster = Array.from(clusters.values())[0]; - printer.info(`${chalk.green('✔')} Only one Cluster was found: '${firstCluster.DBClusterIdentifier}' was automatically selected.`); + context.print.info(`${chalk.green('✔')} Only one Cluster was found: '${firstCluster.DBClusterIdentifier}' was automatically selected.`); return { selectedClusterArn: firstCluster.DBClusterArn, @@ -149,7 +148,7 @@ async function getSecretStoreArn(context: $TSContext, inputs, clusterResourceId, if (secretsForCluster.length === 0) { const errMessage = 'No RDS access credentials found in the AWS Secrect Manager.'; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new ResourceCredentialsNotFoundError(errMessage)); @@ -170,7 +169,7 @@ async function getSecretStoreArn(context: $TSContext, inputs, clusterResourceId, // Pick first and only value selectedSecretArn = Array.from(secrets.values())[0]; - printer.info(`${chalk.green('✔')} Only one Secret was found for the cluster: '${selectedSecretArn}' was automatically selected.`); + context.print.info(`${chalk.green('✔')} Only one Secret was found for the cluster: '${selectedSecretArn}' was automatically selected.`); } return selectedSecretArn; @@ -207,14 +206,14 @@ async function selectDatabase(context: $TSContext, inputs, clusterArn, secretArn const msg = `Ensure that '${secretArn}' contains your database credentials. ` + 'Please note that Aurora Serverless does not support IAM database authentication.'; - printer.error(msg); + context.print.error(msg); } } if (databaseList.length === 0) { const errMessage = 'No database found in the selected cluster.'; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new ResourceDoesNotExistError(errMessage)); @@ -225,7 +224,7 @@ async function selectDatabase(context: $TSContext, inputs, clusterArn, secretArn return await promptWalkthroughQuestion(inputs, 3, databaseList); } - printer.info(`${chalk.green('✔')} Only one Database was found: '${databaseList[0]}' was automatically selected.`); + context.print.info(`${chalk.green('✔')} Only one Database was found: '${databaseList[0]}' was automatically selected.`); return databaseList[0]; } diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.ts index 508160615fe..933134e813d 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.ts @@ -1,33 +1,30 @@ -import { Duration, Expiration } from '@aws-cdk/core'; +import { ListQuestion, CheckboxQuestion, ListChoiceOptions } from 'inquirer'; +import { dataStoreLearnMore } from '../sync-conflict-handler-assets/syncAssets'; +import inquirer from 'inquirer'; +import fs from 'fs-extra'; +import path from 'path'; +import { rootAssetDir, provider } from '../aws-constants'; +import { collectDirectivesByTypeNames, readProjectConfiguration } from 'graphql-transformer-core'; +import { category } from '../../../category-constants'; +import { UpdateApiRequest } from '../../../../../amplify-headless-interface/lib/interface/api/update'; +import { authConfigToAppSyncAuthType } from '../utils/auth-config-to-app-sync-auth-type-bi-di-mapper'; +import { resolverConfigToConflictResolution } from '../utils/resolver-config-to-conflict-resolution-bi-di-mapper'; +import _ from 'lodash'; +import chalk from 'chalk'; +import uuid from 'uuid'; +import { getAppSyncAuthConfig, checkIfAuthExists, authConfigHasApiKey } from '../utils/amplify-meta-utils'; import { - $TSContext, - $TSObject, - exitOnNextTick, - FeatureFlags, - open, - pathManager, ResourceAlreadyExistsError, ResourceDoesNotExistError, - stateManager, UnknownResourceTypeError, + exitOnNextTick, + stateManager, + FeatureFlags, + $TSContext, + open, } from 'amplify-cli-core'; -import { UpdateApiRequest } from 'amplify-headless-interface'; -import { printer } from 'amplify-prompts'; -import chalk from 'chalk'; -import * as fs from 'fs-extra'; -import { collectDirectivesByTypeNames, readProjectConfiguration } from 'graphql-transformer-core'; -import inquirer, { CheckboxQuestion, ListChoiceOptions, ListQuestion } from 'inquirer'; -import _ from 'lodash'; -import * as path from 'path'; -import { v4 as uuid } from 'uuid'; -import { category } from '../../../category-constants'; -import { rootAssetDir } from '../aws-constants'; -import { getAllDefaults } from '../default-values/appSync-defaults'; -import { dataStoreLearnMore } from '../sync-conflict-handler-assets/syncAssets'; -import { authConfigHasApiKey, checkIfAuthExists, getAppSyncAuthConfig } from '../utils/amplify-meta-utils'; -import { authConfigToAppSyncAuthType } from '../utils/auth-config-to-app-sync-auth-type-bi-di-mapper'; +import { Duration, Expiration } from '@aws-cdk/core'; import { defineGlobalSandboxMode } from '../utils/global-sandbox-mode'; -import { resolverConfigToConflictResolution } from '../utils/resolver-config-to-conflict-resolution-bi-di-mapper'; const serviceName = 'AppSync'; const elasticContainerServiceName = 'ElasticContainer'; @@ -154,11 +151,11 @@ export const openConsole = async (context: $TSContext) => { url = `https://console.aws.amazon.com/appsync/home?region=${Region}#/${GraphQLAPIIdOutput}/v1/queries`; - const providerPlugin = await import(context.amplify.getProviderPlugins(context)[providerName]); + const providerPlugin = await import(context.amplify.getProviderPlugins(context)[provider]); const { isAdminApp, region } = await providerPlugin.isAmplifyAdminApp(appId); if (isAdminApp) { if (region !== Region) { - printer.warn(`Region mismatch: Amplify service returned '${region}', but found '${Region}' in amplify-meta.json.`); + context.print.warning(`Region mismatch: Amplify service returned '${region}', but found '${Region}' in amplify-meta.json.`); } const { envName } = context.amplify.getEnvInfo(); const baseUrl: string = providerPlugin.adminBackendMap[region].amplifyAdminUrl; @@ -193,24 +190,26 @@ export const openConsole = async (context: $TSContext) => { } else if (selectedConsole === codePipeline) { url = `https://${Region}.console.aws.amazon.com/codesuite/codepipeline/pipelines/${PipelineName}/view`; } else { - printer.error('Option not available'); + context.print.error('Option not available'); return; } } open(url, { wait: false }); } else { - printer.error('AppSync API is not pushed in the cloud.'); + context.print.error('AppSync API is not pushed in the cloud.'); } }; -const serviceApiInputWalkthrough = async (context: $TSContext, serviceMetadata) => { +const serviceApiInputWalkthrough = async (context: $TSContext, defaultValuesFilename, serviceMetadata) => { let continuePrompt = false; let authConfig; let defaultAuthType; let resolverConfig; const { amplify } = context; const { inputs } = serviceMetadata; + const defaultValuesSrc = `${__dirname}/../default-values/${defaultValuesFilename}`; + const { getAllDefaults } = require(defaultValuesSrc); const allDefaultValues = getAllDefaults(amplify.getProjectDetails()); let resourceAnswers = {}; @@ -337,7 +336,7 @@ const serviceApiInputWalkthrough = async (context: $TSContext, serviceMetadata) }; }; -const updateApiInputWalkthrough = async (context: $TSContext, project: $TSObject, resolverConfig, modelTypes) => { +const updateApiInputWalkthrough = async (context, project, resolverConfig, modelTypes) => { let authConfig; let defaultAuthType; const updateChoices = [ @@ -389,16 +388,15 @@ const updateApiInputWalkthrough = async (context: $TSContext, project: $TSObject }; }; -export const serviceWalkthrough = async (context: $TSContext, serviceMetadata: $TSObject) => { - const resourceName = resourceAlreadyExists(); - const providerPlugin = await import(context.amplify.getProviderPlugins(context)[providerName]); +export const serviceWalkthrough = async (context: $TSContext, defaultValuesFilename, serviceMetadata) => { + const resourceName = resourceAlreadyExists(context); + const providerPlugin = await import(context.amplify.getProviderPlugins(context)[provider]); const transformerVersion = providerPlugin.getTransformerVersion(context); await addLambdaAuthorizerChoice(context); - if (resourceName) { const errMessage = 'You already have an AppSync API in your project. Use the "amplify update api" command to update your existing AppSync API.'; - printer.warn(errMessage); + context.print.warning(errMessage); await context.usageData.emitError(new ResourceAlreadyExistsError(errMessage)); exitOnNextTick(0); } @@ -406,7 +404,7 @@ export const serviceWalkthrough = async (context: $TSContext, serviceMetadata: $ const { amplify } = context; const { inputs } = serviceMetadata; - const basicInfoAnswers = await serviceApiInputWalkthrough(context, serviceMetadata); + const basicInfoAnswers = await serviceApiInputWalkthrough(context, defaultValuesFilename, serviceMetadata); let schemaContent = ''; let askToEdit = true; @@ -433,7 +431,7 @@ export const serviceWalkthrough = async (context: $TSContext, serviceMetadata: $ }; }; -export const updateWalkthrough = async (context: $TSContext): Promise => { +export const updateWalkthrough = async (context): Promise => { const { allResources } = await context.amplify.getResourceStatus(); let resourceDir; let resourceName; @@ -452,10 +450,11 @@ export const updateWalkthrough = async (context: $TSContext): Promise printer.info(authMode)); - printer.info(''); + context.print.success('Authorization modes'); + authModes.forEach(authMode => context.print.info(authMode)); + context.print.info(''); - printer.success('Conflict detection (required for DataStore)'); + context.print.success('Conflict detection (required for DataStore)'); if (project.config && !_.isEmpty(project.config.ResolverConfig)) { - printer.info( + context.print.info( `- Conflict resolution strategy: ${ conflictResolutionHanlderChoices.find(choice => choice.value === project.config.ResolverConfig.project.ConflictHandler).name }`, ); } else { - printer.info('- Disabled'); + context.print.info('- Disabled'); } - printer.info(''); + context.print.info(''); } -async function displayAuthMode(context: $TSContext, resource: $TSObject, authMode: string) { - if (authMode === 'API_KEY' && resource.output.GraphQLAPIKeyOutput) { +async function displayAuthMode(context, resource, authMode) { + if (authMode == 'API_KEY' && resource.output.GraphQLAPIKeyOutput) { let { apiKeys } = await context.amplify.executeProviderUtils(context, 'awscloudformation', 'getAppSyncApiKeys', { apiId: resource.output.GraphQLAPIIdOutput, }); @@ -547,13 +546,13 @@ async function displayAuthMode(context: $TSContext, resource: $TSObject, authMod return authProviderChoices.find(choice => choice.value === authMode).name; } -async function askAdditionalQuestions(context: $TSContext, authConfig, defaultAuthType, modelTypes?) { +async function askAdditionalQuestions(context, authConfig, defaultAuthType, modelTypes?) { authConfig = await askAdditionalAuthQuestions(context, authConfig, defaultAuthType); return { authConfig }; } -async function askResolverConflictQuestion(context: $TSContext, resolverConfig, modelTypes?) { - let resolverConfigResponse: $TSObject = {}; +async function askResolverConflictQuestion(context, resolverConfig, modelTypes?) { + let resolverConfigResponse: any = {}; if (await context.prompt.confirm('Enable conflict detection?', !resolverConfig?.project)) { resolverConfigResponse = await askResolverConflictHandlerQuestion(context, modelTypes); @@ -562,8 +561,8 @@ async function askResolverConflictQuestion(context: $TSContext, resolverConfig, return resolverConfigResponse; } -async function askResolverConflictHandlerQuestion(context: $TSContext, modelTypes?) { - let resolverConfig: $TSObject = {}; +async function askResolverConflictHandlerQuestion(context, modelTypes?) { + let resolverConfig: any = {}; const askConflictResolutionStrategy = async msg => { let conflictResolutionStrategy; @@ -581,13 +580,13 @@ async function askResolverConflictHandlerQuestion(context: $TSContext, modelType ({ conflictResolutionStrategy } = await inquirer.prompt([conflictResolutionQuestion])); } while (conflictResolutionStrategy === 'Learn More'); - let syncConfig: $TSObject = { + let syncConfig: any = { ConflictHandler: conflictResolutionStrategy, ConflictDetection: 'VERSION', }; if (conflictResolutionStrategy === 'LAMBDA') { - const { newFunction, lambdaFunctionName } = await askSyncFunctionQuestion(); + const { newFunction, lambdaFunctionName } = await askSyncFunctionQuestion(context); syncConfig.LambdaConflictHandler = { name: lambdaFunctionName, new: newFunction, @@ -614,8 +613,10 @@ async function askResolverConflictHandlerQuestion(context: $TSContext, modelType if (selectedModelTypes.length > 0) { resolverConfig.models = {}; - for (const modelType of selectedModelTypes) { - resolverConfig.models[modelType] = await askConflictResolutionStrategy(`Select the resolution strategy for ${modelType} model`); + for (let i = 0; i < selectedModelTypes.length; i += 1) { + resolverConfig.models[selectedModelTypes[i]] = await askConflictResolutionStrategy( + `Select the resolution strategy for ${selectedModelTypes[i]} model`, + ); } } } @@ -624,7 +625,7 @@ async function askResolverConflictHandlerQuestion(context: $TSContext, modelType return resolverConfig; } -async function askSyncFunctionQuestion() { +async function askSyncFunctionQuestion(context) { const syncLambdaQuestion = { type: 'list', name: 'syncLambdaAnswer', @@ -659,8 +660,8 @@ async function askSyncFunctionQuestion() { return { newFunction, lambdaFunctionName }; } -async function addLambdaAuthorizerChoice(context: $TSContext) { - const providerPlugin = await import(context.amplify.getProviderPlugins(context)[providerName]); +async function addLambdaAuthorizerChoice(context) { + const providerPlugin = await import(context.amplify.getProviderPlugins(context)[provider]); const transformerVersion = providerPlugin.getTransformerVersion(context); if (transformerVersion === 2 && !authProviderChoices.some(choice => choice.value == 'AWS_LAMBDA')) { authProviderChoices.push({ @@ -670,9 +671,9 @@ async function addLambdaAuthorizerChoice(context: $TSContext) { } } -async function askDefaultAuthQuestion(context: $TSContext) { +async function askDefaultAuthQuestion(context) { await addLambdaAuthorizerChoice(context); - const currentAuthConfig = getAppSyncAuthConfig(stateManager.getMeta()); + const currentAuthConfig = getAppSyncAuthConfig(context.amplify.getProjectMeta()); const currentDefaultAuth = currentAuthConfig && currentAuthConfig.defaultAuthentication ? currentAuthConfig.defaultAuthentication.authenticationType : undefined; @@ -697,8 +698,8 @@ async function askDefaultAuthQuestion(context: $TSContext) { }; } -export async function askAdditionalAuthQuestions(context: $TSContext, authConfig: $TSObject, defaultAuthType) { - const currentAuthConfig = getAppSyncAuthConfig(stateManager.getMeta()); +export async function askAdditionalAuthQuestions(context, authConfig, defaultAuthType) { + const currentAuthConfig = getAppSyncAuthConfig(context.amplify.getProjectMeta()); authConfig.additionalAuthenticationProviders = []; if (await context.prompt.confirm('Configure additional auth types?')) { // Get additional auth configured @@ -719,7 +720,9 @@ export async function askAdditionalAuthQuestions(context: $TSContext, authConfig const additionalProvidersAnswer = await inquirer.prompt([additionalProvidersQuestion]); - for (const authProvider of additionalProvidersAnswer.authType) { + for (let i = 0; i < additionalProvidersAnswer.authType.length; i += 1) { + const authProvider = additionalProvidersAnswer.authType[i]; + const config = await askAuthQuestions( authProvider, context, @@ -737,10 +740,10 @@ export async function askAdditionalAuthQuestions(context: $TSContext, authConfig return authConfig; } -export async function askAuthQuestions(authType: string, context: $TSContext, printLeadText = false, authSettings) { +export async function askAuthQuestions(authType, context, printLeadText = false, authSettings) { if (authType === 'AMAZON_COGNITO_USER_POOLS') { if (printLeadText) { - printer.info('Cognito UserPool configuration'); + context.print.info('Cognito UserPool configuration'); } const userPoolConfig = await askUserPoolQuestions(context); @@ -750,7 +753,7 @@ export async function askAuthQuestions(authType: string, context: $TSContext, pr if (authType === 'API_KEY') { if (printLeadText) { - printer.info('API key configuration'); + context.print.info('API key configuration'); } const apiKeyConfig = await askApiKeyQuestions(authSettings); @@ -766,7 +769,7 @@ export async function askAuthQuestions(authType: string, context: $TSContext, pr if (authType === 'OPENID_CONNECT') { if (printLeadText) { - printer.info('OpenID Connect configuration'); + context.print.info('OpenID Connect configuration'); } const openIDConnectConfig = await askOpenIDConnectQuestions(authSettings); @@ -785,17 +788,17 @@ export async function askAuthQuestions(authType: string, context: $TSContext, pr } const errMessage = `Unknown authType: ${authType}`; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new UnknownResourceTypeError(errMessage)); exitOnNextTick(1); } -async function askUserPoolQuestions(context: $TSContext) { - let authResourceName = checkIfAuthExists(); +async function askUserPoolQuestions(context) { + let authResourceName = checkIfAuthExists(context); if (!authResourceName) { authResourceName = await context.amplify.invokePluginMethod(context, 'auth', undefined, 'add', [context, true]); } else { - printer.info('Use a Cognito user pool configured as a part of this project.'); + context.print.info('Use a Cognito user pool configured as a part of this project.'); } // Added resources are prefixed with auth @@ -809,7 +812,7 @@ async function askUserPoolQuestions(context: $TSContext) { }; } -export async function askApiKeyQuestions(authSettings: $TSObject = undefined) { +export async function askApiKeyQuestions(authSettings = undefined) { let defaultValues = { apiKeyExpirationDays: 7, description: undefined, @@ -850,7 +853,7 @@ export async function askApiKeyQuestions(authSettings: $TSObject = undefined) { }; } -async function askOpenIDConnectQuestions(authSettings: $TSObject) { +async function askOpenIDConnectQuestions(authSettings) { let defaultValues = { authTTL: undefined, clientId: undefined, @@ -904,7 +907,7 @@ async function askOpenIDConnectQuestions(authSettings: $TSObject) { }; } -async function validateDays(input: string) { +async function validateDays(input) { const isValid = /^\d{0,3}$/.test(input); const days = isValid ? parseInt(input, 10) : 0; if (!isValid || days < 1 || days > 365) { @@ -914,7 +917,7 @@ async function validateDays(input: string) { return true; } -function validateIssuerUrl(input: string) { +function validateIssuerUrl(input) { const isValid = /^(((?!http:\/\/(?!localhost))([a-zA-Z0-9.]{1,}):\/\/([a-zA-Z0-9-._~:?#@!$&'()*+,;=/]{1,})\/)|(?!http)(?!https)([a-zA-Z0-9.]{1,}):\/\/)$/.test( input, @@ -927,7 +930,7 @@ function validateIssuerUrl(input: string) { return true; } -function validateTTL(input: string) { +function validateTTL(input) { const isValid = /^\d+$/.test(input); if (!isValid) { @@ -937,32 +940,32 @@ function validateTTL(input: string) { return true; } -function resourceAlreadyExists() { - const meta = stateManager.getMeta(); +function resourceAlreadyExists(context) { + const { amplify } = context; + const { amplifyMeta } = amplify.getProjectDetails(); let resourceName; - if (meta[category]) { - const categoryResources = meta[category]; - for (const resource of Object.keys(categoryResources)) { + if (amplifyMeta[category]) { + const categoryResources = amplifyMeta[category]; + Object.keys(categoryResources).forEach(resource => { if (categoryResources[resource].service === serviceName) { resourceName = resource; - break; } - } + }); } return resourceName; } -export const migrate = async (context: $TSContext) => { +export const migrate = async context => { await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { forceCompile: true, migrate: true, }); }; -export const getIAMPolicies = (resourceName: string, operations: string[]) => { - let policy: $TSObject = {}; +export const getIAMPolicies = (resourceName: string, operations: string[], context: any) => { + let policy: any = {}; const resources = []; const actions = []; if (!FeatureFlags.getBoolean('appSync.generateGraphQLPermissions')) { @@ -982,7 +985,7 @@ export const getIAMPolicies = (resourceName: string, operations: string[]) => { actions.push('appsync:Delete*'); break; default: - printer.info(`${crudOption} not supported`); + console.log(`${crudOption} not supported`); } }); resources.push(buildPolicyResource(resourceName, null)); @@ -998,7 +1001,7 @@ export const getIAMPolicies = (resourceName: string, operations: string[]) => { }; const attributes = ['GraphQLAPIIdOutput', 'GraphQLAPIEndpointOutput']; - if (authConfigHasApiKey(getAppSyncAuthConfig(stateManager.getMeta()))) { + if (authConfigHasApiKey(getAppSyncAuthConfig(context.amplify.getProjectMeta()))) { attributes.push('GraphQLAPIKeyOutput'); } @@ -1174,7 +1177,7 @@ async function createLambdaAuthorizerFunction(context: $TSContext) { const backendConfigs = { service: FunctionServiceNameLambdaFunction, - providerPlugin: providerName, + providerPlugin: provider, build: true, }; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/containers-walkthrough.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/containers-walkthrough.ts index 5ba0df021bf..9b38fb744f7 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/containers-walkthrough.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/service-walkthroughs/containers-walkthrough.ts @@ -1,10 +1,8 @@ -import { $TSAny, $TSContext, $TSObject, exitOnNextTick, ResourceDoesNotExistError } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; +import { exitOnNextTick, ResourceDoesNotExistError } from 'amplify-cli-core'; import inquirer from 'inquirer'; import { category } from '../../../category-constants'; import { DEPLOYMENT_MECHANISM } from '../base-api-stack'; import { GitHubSourceActionInfo } from '../pipeline-with-awaiter'; -import { getAllDefaults } from '../default-values/containers-defaults'; const serviceName = 'ElasticContainer'; @@ -46,8 +44,11 @@ export type ServiceConfiguration = { gitHubInfo?: GitHubSourceActionInfo; }; -export async function serviceWalkthrough(context: $TSContext, apiType: API_TYPE): Promise> { - const allDefaultValues = getAllDefaults(); +export async function serviceWalkthrough(context, defaultValuesFilename, apiType: API_TYPE): Promise> { + const { amplify } = context; + const defaultValuesSrc = `${__dirname}/../default-values/${defaultValuesFilename}`; + const { getAllDefaults } = await import(defaultValuesSrc); + const allDefaultValues = getAllDefaults(amplify.getProjectDetails()); const resourceName = await askResourceName(context, allDefaultValues); @@ -56,7 +57,7 @@ export async function serviceWalkthrough(context: $TSContext, apiType: API_TYPE) return { resourceName, ...containerInfo }; } -async function askResourceName(context: $TSContext, allDefaultValues: $TSObject) { +async function askResourceName(context, allDefaultValues) { const { amplify } = context; const { resourceName } = await inquirer.prompt([ @@ -79,7 +80,7 @@ async function askResourceName(context: $TSContext, allDefaultValues: $TSObject) return resourceName; } -async function askContainerSource(context: $TSContext, resourceName: string, apiType: API_TYPE): Promise> { +async function askContainerSource(context, resourceName: string, apiType: API_TYPE): Promise> { return newContainer(context, resourceName, apiType); } @@ -88,7 +89,7 @@ export enum IMAGE_SOURCE_TYPE { CUSTOM = 'CUSTOM', } -async function newContainer(context: $TSContext, resourceName: string, apiType: API_TYPE): Promise> { +async function newContainer(context, resourceName: string, apiType: API_TYPE): Promise> { let imageSource: { type: IMAGE_SOURCE_TYPE; template?: string }; let choices = []; @@ -170,8 +171,8 @@ async function newContainer(context: $TSContext, resourceName: string, apiType: let gitHubToken: string; if (deploymentMechanismQuestion.deploymentMechanism === DEPLOYMENT_MECHANISM.INDENPENDENTLY_MANAGED) { - printer.info('We need a Github Personal Access Token to automatically build & deploy your Fargate task on every Github commit.'); - printer.info( + context.print.info('We need a Github Personal Access Token to automatically build & deploy your Fargate task on every Github commit.'); + context.print.info( 'Learn more about Github Personal Access Token here: https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token', ); @@ -233,7 +234,7 @@ async function newContainer(context: $TSContext, resourceName: string, apiType: }; } -export async function updateWalkthrough(context: $TSContext, apiType: API_TYPE) { +export async function updateWalkthrough(context, defaultValuesFilename, apiType: API_TYPE) { const { allResources } = await context.amplify.getResourceStatus(); const resources = allResources @@ -246,7 +247,7 @@ export async function updateWalkthrough(context: $TSContext, apiType: API_TYPE) // There can only be one appsync resource if (resources.length === 0) { const errMessage = `No ${apiType} API resource to update. Use "amplify add api" command to create a new ${apiType} API`; - printer.error(errMessage); + context.print.error(errMessage); await context.usageData.emitError(new ResourceDoesNotExistError(errMessage)); exitOnNextTick(0); return; @@ -301,7 +302,7 @@ export async function updateWalkthrough(context: $TSContext, apiType: API_TYPE) const hasAccessableResources = ['storage', 'function'].some(categoryName => { return Object.keys(meta[categoryName] ?? {}).length > 0; }); - let rolePermissions: $TSAny = {}; + let rolePermissions: any = {}; if ( hasAccessableResources && (await context.amplify.confirmPrompt('Do you want to access other resources in this project from your api?')) diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/amplify-meta-utils.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/amplify-meta-utils.ts index e3385279de3..d920a8d9c96 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/amplify-meta-utils.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/amplify-meta-utils.ts @@ -1,7 +1,6 @@ -import { $TSAny, $TSMeta, $TSObject, AmplifyCategories, AmplifySupportedService, stateManager } from 'amplify-cli-core'; import _ from 'lodash'; -export const authConfigHasApiKey = (authConfig?: $TSAny) => { +export const authConfigHasApiKey = authConfig => { if (!authConfig) { return false; } @@ -14,11 +13,12 @@ export const authConfigHasApiKey = (authConfig?: $TSAny) => { ); }; -export const checkIfAuthExists = () => { - const amplifyMeta = stateManager.getMeta(); +export const checkIfAuthExists = context => { + const { amplify } = context; + const { amplifyMeta } = amplify.getProjectDetails(); let authResourceName; - const authServiceName = AmplifySupportedService.COGNITO; - const authCategoryName = AmplifyCategories.AUTH; + const authServiceName = 'Cognito'; + const authCategoryName = 'auth'; if (amplifyMeta[authCategoryName] && Object.keys(amplifyMeta[authCategoryName]).length > 0) { const categoryResources = amplifyMeta[authCategoryName]; @@ -34,15 +34,15 @@ export const checkIfAuthExists = () => { // some utility functions to extract the AppSync API name and config from amplify-meta -export const getAppSyncAuthConfig = (projectMeta: $TSMeta) => { +export const getAppSyncAuthConfig = projectMeta => { const entry = getAppSyncAmplifyMetaEntry(projectMeta); if (entry) { - const value = entry[1] as $TSAny; + const value = entry[1] as any; return value && value.output ? value.output.authConfig : {}; } }; -export const getAppSyncResourceName = (projectMeta: $TSMeta): string | undefined => { +export const getAppSyncResourceName = (projectMeta: any): string | undefined => { const entry = getAppSyncAmplifyMetaEntry(projectMeta); if (entry) { return entry[0]; @@ -51,8 +51,6 @@ export const getAppSyncResourceName = (projectMeta: $TSMeta): string | undefined // project meta is the contents of amplify-meta.json // typically retreived using context.amplify.getProjectMeta() -const getAppSyncAmplifyMetaEntry = (projectMeta: $TSMeta) => { - return Object.entries(projectMeta[AmplifyCategories.API] || {}).find( - ([, value]) => (value as $TSObject).service === AmplifySupportedService.APPSYNC, - ); +const getAppSyncAmplifyMetaEntry = (projectMeta: any) => { + return Object.entries(projectMeta.api || {}).find(([, value]) => (value as any).service === 'AppSync'); }; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/containers-artifacts.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/containers-artifacts.ts index 5a1368cfa8d..51da9da2da7 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/containers-artifacts.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/containers-artifacts.ts @@ -2,14 +2,14 @@ import { Octokit } from '@octokit/rest'; import * as fs from 'fs-extra'; import inquirer from 'inquirer'; import * as path from 'path'; -import { v4 as uuid } from 'uuid'; +import uuid from 'uuid'; import { provider as cloudformationProviderName } from '../../../provider-utils/awscloudformation/aws-constants'; import { getContainers } from '../../../provider-utils/awscloudformation/docker-compose'; import Container from '../docker-compose/ecs-objects/container'; import { EcsStack } from '../ecs-apigw-stack'; import { API_TYPE, ResourceDependency } from '../../../provider-utils/awscloudformation/service-walkthroughs/containers-walkthrough'; import { getGitHubOwnerRepoFromPath } from '../../../provider-utils/awscloudformation/utils/github'; -import { $TSAny, $TSContext, JSONUtilities, pathManager, readCFNTemplate } from 'amplify-cli-core'; +import { JSONUtilities, pathManager, readCFNTemplate } from 'amplify-cli-core'; import { DEPLOYMENT_MECHANISM } from '../base-api-stack'; import { setExistingSecretArns } from './containers/set-existing-secret-arns'; import { category } from '../../../category-constants'; @@ -28,9 +28,9 @@ export type ApiResource = { restrictAccess: boolean; dependsOn: ResourceDependency[]; environmentMap: Record; - categoryPolicies: $TSAny[]; - mutableParametersState: $TSAny; - output?: Record; + categoryPolicies: any[]; + mutableParametersState: any; + output?: Record; apiType?: API_TYPE; exposedContainer?: { name: string; port: number }; }; @@ -46,7 +46,7 @@ type ContainerArtifactsMetadata = { }; export async function generateContainersArtifacts( - context: $TSContext, + context: any, resource: ApiResource, askForExposedContainer: boolean = false, ): Promise { @@ -116,12 +116,7 @@ export async function generateContainersArtifacts( }; } -export async function processDockerConfig( - context: $TSContext, - resource: ApiResource, - srcPath: string, - askForExposedContainer: boolean = false, -) { +export async function processDockerConfig(context: any, resource: ApiResource, srcPath: string, askForExposedContainer: boolean = false) { const { providers: { [cloudformationProviderName]: provider }, } = context.amplify.getProjectMeta(); @@ -311,7 +306,7 @@ export async function processDockerConfig( }; } -async function shouldUpdateSecrets(context: $TSContext, secrets: Record): Promise { +async function shouldUpdateSecrets(context: any, secrets: Record): Promise { const hasSecrets = Object.keys(secrets).length > 0; if (!hasSecrets || context.exeInfo.inputParams.yes) { diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/dynamic-imports.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/dynamic-imports.ts index 2c7adced535..05b3688da0d 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/dynamic-imports.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/dynamic-imports.ts @@ -1,10 +1,4 @@ -import * as path from 'path'; - -export const serviceMetadataFor = async (service: string) => - (await import(path.join('..', '..', 'supported-services'))).supportedServices[service]; - -export const datasourceMetadataFor = async (datasource: string) => - (await import(path.join('..', '..', 'supported-datasources'))).supportedDatasources[datasource]; - -export const getServiceWalkthrough = async (walkthroughFilename: string) => - (await import(path.join('..', 'service-walkthroughs', walkthroughFilename))).serviceWalkthrough; +export const serviceMetadataFor = async service => (await import('../../supported-services')).supportedServices[service]; +export const datasourceMetadataFor = async datasource => (await import('../../supported-datasources')).supportedDatasources[datasource]; +export const getServiceWalkthrough = async walkthroughFilename => + (await import(`../service-walkthroughs/${walkthroughFilename}`)).serviceWalkthrough; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/edit-schema-flow.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/edit-schema-flow.ts index 3d339aecd0b..11964238731 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/edit-schema-flow.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/edit-schema-flow.ts @@ -1,14 +1,18 @@ -import { $TSContext, pathManager } from 'amplify-cli-core'; -import { prompter } from 'amplify-prompts'; -import * as path from 'path'; +import inquirer, { ConfirmQuestion } from 'inquirer'; +import path from 'path'; import { category } from '../../../category-constants'; import { gqlSchemaFilename } from '../aws-constants'; -export const editSchemaFlow = async (context: $TSContext, apiName: string) => { - if (!(await prompter.yesOrNo('Do you want to edit the schema now?', true))) { - return; - } +export const editSchemaFlow = async (context: any, apiName: string) => { + const prompt: ConfirmQuestion = { + type: 'confirm', + name: 'editNow', + message: 'Do you want to edit the schema now?', + default: true, + }; - const schemaPath = path.join(pathManager.getResourceDirectoryPath(undefined, category, apiName), gqlSchemaFilename); + if (!(await inquirer.prompt(prompt)).editNow) return; + + const schemaPath = path.join(context.amplify.pathManager.getBackendDirPath(), category, apiName, gqlSchemaFilename); await context.amplify.openEditor(context, schemaPath, false); }; diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/print-api-key-warnings.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/print-api-key-warnings.ts index 14eeb6a9339..f98601878b2 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/print-api-key-warnings.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/print-api-key-warnings.ts @@ -1,18 +1,16 @@ -import { printer } from 'amplify-prompts'; - // If adding or removing the API_KEY auth type, print a warning that resources that depend on the API must re-add the API as a dependency to have the API key parameter added / removed. -export const printApiKeyWarnings = (oldConfigHadApiKey: boolean, newConfigHasApiKey: boolean) => { +export const printApiKeyWarnings = (context, oldConfigHadApiKey: boolean, newConfigHasApiKey: boolean) => { if (oldConfigHadApiKey && !newConfigHasApiKey) { - printer.warn('The API_KEY auth type has been removed from the API.'); - printer.warn( + context.print.warning('The API_KEY auth type has been removed from the API.'); + context.print.warning( 'If other resources depend on this API, run "amplify update " and reselect this API to remove the dependency on the API key.', ); - printer.warn('⚠️ This must be done before running "amplify push" to prevent a push failure'); + context.print.warning('⚠️ This must be done before running "amplify push" to prevent a push failure'); } if (!oldConfigHadApiKey && newConfigHasApiKey) { - printer.warn('The API_KEY auth type has been added to the API.'); - printer.warn( + context.print.warning('The API_KEY auth type has been added to the API.'); + context.print.warning( '⚠️ If other resources depend on this API and need access to the API key, run "amplify update " and reselect this API as a dependency to add the API key dependency.', ); } diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/rest-api-path-utils.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/rest-api-path-utils.ts index 2893bfe9a98..37370c2deac 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/rest-api-path-utils.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/utils/rest-api-path-utils.ts @@ -37,7 +37,7 @@ export const validatePathName = (name: string) => { // } // // checkForPathOverlap assumes that all provided paths have previously been run through validatePathName(). -export const checkForPathOverlap = (name: string, paths: string[]) => { +export const checkForPathOverlap = (name: string, paths: { name: string }[]) => { // Split name into an array of its components. const split = name.split('/').filter(sub => sub !== ''); // Because name starts with a /, this filters out the first empty element @@ -59,7 +59,7 @@ export const checkForPathOverlap = (name: string, paths: string[]) => { subpath = `${subpath}/${sub}`; // Explicitly check for the path / since it overlaps with any other valid path. // If the path isn't /, replace all of its parameters with '{}' when checking for overlap in find(). - overlappingPath = paths.find(name => name === '/' || name.replace(/{[a-zA-Z0-9\-]+}/g, '{}') === subpath); + overlappingPath = paths.map(path => path.name).find(name => name === '/' || name.replace(/{[a-zA-Z0-9\-]+}/g, '{}') === subpath); return overlappingPath !== undefined; }); if (subMatch) { diff --git a/packages/amplify-category-api/src/provider-utils/supported-datasources.ts b/packages/amplify-category-api/src/provider-utils/supported-datasources.ts index b0e2a08bf69..65b5b019106 100644 --- a/packages/amplify-category-api/src/provider-utils/supported-datasources.ts +++ b/packages/amplify-category-api/src/provider-utils/supported-datasources.ts @@ -27,6 +27,7 @@ export const supportedDatasources = { }, ], alias: 'Aurora Serverless', + defaultValuesFilename: 'appSync-rds-defaults.js', serviceWalkthroughFilename: 'appSync-rds-walkthrough.js', cfnFilename: 'appSync-rds-cloudformation-template-default.yml.ejs', provider: 'awscloudformation', diff --git a/packages/amplify-category-api/src/provider-utils/supported-services.ts b/packages/amplify-category-api/src/provider-utils/supported-services.ts index 70734fee701..99cc2e128bc 100644 --- a/packages/amplify-category-api/src/provider-utils/supported-services.ts +++ b/packages/amplify-category-api/src/provider-utils/supported-services.ts @@ -122,6 +122,7 @@ export const supportedServices = { }, ], alias: 'GraphQL', + defaultValuesFilename: 'appSync-defaults.js', serviceWalkthroughFilename: 'appSync-walkthrough.js', cfnFilename: 'appSync-cloudformation-template-default.yml.ejs', provider: 'awscloudformation', @@ -147,7 +148,9 @@ export const supportedServices = { }, ], alias: 'REST', + defaultValuesFilename: 'apigw-defaults.js', serviceWalkthroughFilename: 'apigw-walkthrough.js', + cfnFilename: 'apigw-cloudformation-template-default.json.ejs', provider: 'awscloudformation', }, }; diff --git a/packages/amplify-category-api/tsconfig.json b/packages/amplify-category-api/tsconfig.json index 1d2c1d6a744..0fc8b8735ce 100644 --- a/packages/amplify-category-api/tsconfig.json +++ b/packages/amplify-category-api/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "lib", "rootDir": "src", "strict": false, // because package has been converted from js - "allowJs": false + "allowJs": true }, "exclude": [ "coverage", @@ -12,15 +12,13 @@ "resources/awscloudformation/lambdas", "resources/awscloudformation/container-templates", "resources/awscloudformation/graphql-lambda-authorizer", - "resources/awscloudformation/overrides-resource", - "scripts", "src/__tests__" ], "references": [ - { "path": "../amplify-cli-core" }, - { "path": "../amplify-headless-interface" }, - { "path": "../amplify-prompts" }, - { "path": "../amplify-util-headless-input" }, - { "path": "../graphql-transformer-core" } + {"path": "../amplify-cli-core"}, + {"path": "../amplify-headless-interface"}, + {"path": "../amplify-prompts"}, + {"path": "../graphql-transformer-core"}, + {"path": "../amplify-util-headless-input"}, ] } diff --git a/packages/amplify-category-auth/resources/adminAuth/admin-queries-api-params.json b/packages/amplify-category-auth/resources/adminAuth/admin-queries-api-params.json new file mode 100644 index 00000000000..7116196989c --- /dev/null +++ b/packages/amplify-category-auth/resources/adminAuth/admin-queries-api-params.json @@ -0,0 +1,8 @@ +{ + "authRoleName": { + "Ref": "AuthRoleName" + }, + "unauthRoleName": { + "Ref": "UnauthRoleName" + } +} diff --git a/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-inputs-manager/auth-input-state.test.ts b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-inputs-manager/auth-input-state.test.ts index c9b63e0925e..7004caa3338 100644 --- a/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-inputs-manager/auth-input-state.test.ts +++ b/packages/amplify-category-auth/src/__tests__/provider-utils/awscloudformation/auth-inputs-manager/auth-input-state.test.ts @@ -93,7 +93,6 @@ jest.mock('amplify-cli-core', () => ({ }, }) .mockReturnValueOnce({}), - parse: JSON.parse, }, })); diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/handlers/resource-handlers.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/handlers/resource-handlers.ts index 39d70d5dbcf..d7cadd6ac02 100644 --- a/packages/amplify-category-auth/src/provider-utils/awscloudformation/handlers/resource-handlers.ts +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/handlers/resource-handlers.ts @@ -67,7 +67,7 @@ export const getAddAuthHandler = // cdk transformation in this function // start auth transform here await generateAuthStackTemplate(context, cognitoCLIInputs.cognitoConfig.resourceName); - // remove this when api and functions transform are done + // remoe this when api and functions transform are done await getResourceSynthesizer(context, requestWithDefaults); getPostAddAuthMetaUpdater(context, { service: cognitoCLIInputs.cognitoConfig.serviceName, providerName: provider })( diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/synthesize-resources.ts b/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/synthesize-resources.ts index be14df59abf..1ae21e59679 100644 --- a/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/synthesize-resources.ts +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/synthesize-resources.ts @@ -1,22 +1,15 @@ -import { - $TSAny, - $TSContext, - $TSObject, - AmplifyCategories, - AmplifySupportedService, - FeatureFlags, - JSONUtilities, - pathManager, -} from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; -import { copySync, ensureDirSync, existsSync } from 'fs-extra'; -import { get } from 'lodash'; +import { AuthTriggerConfig, AuthTriggerConnection } from '../service-walkthrough-types/cognito-user-input-types'; import * as path from 'path'; +import { existsSync, copySync, outputFileSync } from 'fs-extra'; import uuid from 'uuid'; -import { adminAuthAssetRoot, cfnTemplateRoot, privateKeys, triggerRoot } from '../constants'; -import { CognitoConfiguration } from '../service-walkthrough-types/awsCognito-user-input-types'; -import { AuthTriggerConfig, AuthTriggerConnection } from '../service-walkthrough-types/cognito-user-input-types'; +import { cfnTemplateRoot, privateKeys, adminAuthAssetRoot, triggerRoot } from '../constants'; +import { pathManager, JSONUtilities, FeatureFlags, $TSAny } from 'amplify-cli-core'; +import { get } from 'lodash'; import { generateUserPoolGroupStackTemplate } from './generate-user-pool-group-stack-template'; +import { CognitoConfiguration } from '../service-walkthrough-types/awsCognito-user-input-types'; + +// keep in sync with ServiceName in amplify-category-function, but probably it will not change +const FunctionServiceNameLambdaFunction = 'Lambda'; /** * Factory function that returns a function that synthesizes all resources based on a CognitoCLIInputs request. @@ -25,7 +18,7 @@ import { generateUserPoolGroupStackTemplate } from './generate-user-pool-group-s * @param cfnFilename The template CFN filename * @param provider The cloud provider name */ -export const getResourceSynthesizer = async (context: $TSContext, request: Readonly) => { +export const getResourceSynthesizer = async (context: any, request: Readonly) => { await lambdaTriggers(request, context, null); // transformation handled in api and functions. await addAdminAuth(context, request.resourceName!, 'add', request.adminQueryGroup); @@ -43,11 +36,11 @@ export const getResourceSynthesizer = async (context: $TSContext, request: Reado * @param cfnFilename The template CFN filename * @param provider The cloud provider name */ -export const getResourceUpdater = async (context: $TSContext, request: Readonly) => { +export const getResourceUpdater = async (context: $TSAny, request: Readonly) => { const resources = context.amplify.getProjectMeta(); const adminQueriesFunctionName = get<{ category: string; resourceName: string }[]>(resources, ['api', 'AdminQueries', 'dependsOn'], []) - .filter(resource => resource.category === AmplifyCategories.FUNCTION) + .filter(resource => resource.category === 'function') .map(resource => resource.resourceName) .find(resourceName => resourceName.includes('AdminQueries')); if (adminQueriesFunctionName) { @@ -57,9 +50,7 @@ export const getResourceUpdater = async (context: $TSContext, request: Readonly< } const previouslySaved = - typeof context.updatingAuth.triggers === 'string' - ? JSONUtilities.parse<$TSAny>(context.updatingAuth.triggers) - : context.updatingAuth.triggers; + typeof context.updatingAuth.triggers === 'string' ? JSON.parse(context.updatingAuth.triggers) : context.updatingAuth.triggers; await lambdaTriggers(request, context, previouslySaved); await copyS3Assets(request); @@ -69,7 +60,7 @@ export const getResourceUpdater = async (context: $TSContext, request: Readonly< /** * The 3 functions below should not be exported, but they are for now because externalAuthEnable still uses them individually */ -export const copyCfnTemplate = async (context: $TSContext, category: string, options: $TSObject, cfnFilename: string) => { +export const copyCfnTemplate = async (context: any, category: string, options: any, cfnFilename: string) => { const targetDir = path.join(pathManager.getBackendDirPath(), category, options.resourceName); // enable feature flag to remove trigger dependency from auth template @@ -89,12 +80,12 @@ export const copyCfnTemplate = async (context: $TSContext, category: string, opt }; export const saveResourceParameters = ( - context: $TSContext, + context: any, providerName: string, category: string, resource: string, - params: $TSObject, - envSpecificParams: $TSAny[] = [], + params: any, + envSpecificParams: any[] = [], ) => { const provider = context.amplify.getPluginInstance(context, providerName); let privateParams = Object.assign({}, params); @@ -103,7 +94,7 @@ export const saveResourceParameters = ( provider.saveResourceParameters(context, category, resource, privateParams, envSpecificParams); }; -export const removeDeprecatedProps = (props: $TSObject) => { +export const removeDeprecatedProps = (props: any) => { [ 'authRoleName', 'unauthRoleName', @@ -132,12 +123,12 @@ export const removeDeprecatedProps = (props: $TSObject) => { return props; }; -const lambdaTriggers = async (coreAnswers: $TSObject, context: $TSContext, previouslySaved: $TSAny) => { - const { handleTriggers } = await import('./trigger-flow-auth-helper'); +const lambdaTriggers = async (coreAnswers: any, context: any, previouslySaved: any) => { + const { handleTriggers } = require('./trigger-flow-auth-helper'); let triggerKeyValues = {}; let authTriggerConnections: AuthTriggerConnection[]; if (coreAnswers.triggers) { - const triggerConfig = (await handleTriggers(context, coreAnswers, previouslySaved)) as AuthTriggerConfig; + const triggerConfig: AuthTriggerConfig = await handleTriggers(context, coreAnswers, previouslySaved); triggerKeyValues = triggerConfig.triggers; authTriggerConnections = triggerConfig.authTriggerConnections; coreAnswers.triggers = triggerKeyValues ? JSONUtilities.stringify(triggerKeyValues) : '{}'; @@ -155,12 +146,7 @@ const lambdaTriggers = async (coreAnswers: $TSObject, context: $TSContext, previ } // determine permissions needed for each trigger module - coreAnswers.permissions = await context.amplify.getTriggerPermissions( - context, - coreAnswers.triggers, - AmplifyCategories.AUTH, - coreAnswers.resourceName, - ); + coreAnswers.permissions = await context.amplify.getTriggerPermissions(context, coreAnswers.triggers, 'auth', coreAnswers.resourceName); } else if (previouslySaved) { const targetDir = pathManager.getBackendDirPath(); Object.keys(previouslySaved).forEach(p => { @@ -178,31 +164,20 @@ const lambdaTriggers = async (coreAnswers: $TSObject, context: $TSContext, previ coreAnswers.dependsOn = context.amplify.dependsOnBlock(context, dependsOnKeys, 'Cognito'); }; -export const createUserPoolGroups = async (context: $TSContext, resourceName: string, userPoolGroupList?: string[]) => { +export const createUserPoolGroups = async (context: any, resourceName: string, userPoolGroupList?: string[]) => { if (userPoolGroupList && userPoolGroupList.length > 0) { const userPoolGroupPrecedenceList = []; - for (let i = 0; i < userPoolGroupList.length; ++i) { + for (let i = 0; i < userPoolGroupList.length; i += 1) { userPoolGroupPrecedenceList.push({ groupName: userPoolGroupList[i], precedence: i + 1, }); } - const userPoolGroupFile = path.join( - pathManager.getBackendDirPath(), - AmplifyCategories.AUTH, - 'userPoolGroups', - 'user-pool-group-precedence.json', - ); + const userPoolGroupFile = path.join(pathManager.getBackendDirPath(), 'auth', 'userPoolGroups', 'user-pool-group-precedence.json'); - const userPoolGroupParams = path.join( - pathManager.getBackendDirPath(), - AmplifyCategories.AUTH, - 'userPoolGroups', - 'build', - 'parameters.json', - ); + const userPoolGroupParams = path.join(pathManager.getBackendDirPath(), 'auth', 'userPoolGroups', 'build', 'parameters.json'); /* eslint-disable */ const groupParams = { @@ -218,12 +193,12 @@ export const createUserPoolGroups = async (context: $TSContext, resourceName: st JSONUtilities.writeJson(userPoolGroupParams, groupParams); JSONUtilities.writeJson(userPoolGroupFile, userPoolGroupPrecedenceList); - context.amplify.updateamplifyMetaAfterResourceAdd(AmplifyCategories.AUTH, 'userPoolGroups', { + context.amplify.updateamplifyMetaAfterResourceAdd('auth', 'userPoolGroups', { service: 'Cognito-UserPool-Groups', providerPlugin: 'awscloudformation', dependsOn: [ { - category: AmplifyCategories.AUTH, + category: 'auth', resourceName, attributes: ['UserPoolId', 'AppClientIDWeb', 'AppClientID', 'IdentityPoolId'], }, @@ -234,27 +209,32 @@ export const createUserPoolGroups = async (context: $TSContext, resourceName: st } }; -export const updateUserPoolGroups = async (context: $TSContext, resourceName: string, userPoolGroupList?: string[]) => { +export const updateUserPoolGroups = async (context: any, resourceName: string, userPoolGroupList?: string[]) => { if (userPoolGroupList && userPoolGroupList.length > 0) { const userPoolGroupPrecedenceList = []; - for (let i = 0; i < userPoolGroupList.length; ++i) { + for (let i = 0; i < userPoolGroupList.length; i += 1) { userPoolGroupPrecedenceList.push({ groupName: userPoolGroupList[i], precedence: i + 1, }); } - const userPoolGroupFolder = path.join(pathManager.getBackendDirPath(), AmplifyCategories.AUTH, 'userPoolGroups'); - ensureDirSync(userPoolGroupFolder); - JSONUtilities.writeJson(path.join(userPoolGroupFolder, 'user-pool-group-precedence.json'), userPoolGroupPrecedenceList); + const userPoolGroupFile = path.join( + context.amplify.pathManager.getBackendDirPath(), + 'auth', + 'userPoolGroups', + 'user-pool-group-precedence.json', + ); + + outputFileSync(userPoolGroupFile, JSON.stringify(userPoolGroupPrecedenceList, null, 4)); - context.amplify.updateamplifyMetaAfterResourceUpdate(AmplifyCategories.AUTH, 'userPoolGroups', 'userPoolGroups', { + context.amplify.updateamplifyMetaAfterResourceUpdate('auth', 'userPoolGroups', { service: 'Cognito-UserPool-Groups', providerPlugin: 'awscloudformation', dependsOn: [ { - category: AmplifyCategories.AUTH, + category: 'auth', resourceName, attributes: ['UserPoolId', 'AppClientIDWeb', 'AppClientID', 'IdentityPoolId'], }, @@ -266,7 +246,7 @@ export const updateUserPoolGroups = async (context: $TSContext, resourceName: st }; const addAdminAuth = async ( - context: $TSContext, + context: any, authResourceName: string, operation: 'update' | 'add', adminGroup?: string, @@ -283,19 +263,19 @@ const addAdminAuth = async ( }; const createAdminAuthFunction = async ( - context: $TSContext, + context: any, authResourceName: string, functionName: string, adminGroup: string, operation: 'update' | 'add', ) => { - const targetDir = path.join(pathManager.getBackendDirPath(), AmplifyCategories.FUNCTION, functionName); + const targetDir = path.join(pathManager.getBackendDirPath(), 'function', functionName); let lambdaGroupVar = adminGroup; const dependsOn = []; dependsOn.push({ - category: AmplifyCategories.AUTH, + category: 'auth', resourceName: authResourceName, attributes: ['UserPoolId'], }); @@ -346,53 +326,78 @@ const createAdminAuthFunction = async ( if (operation === 'add') { // add amplify-meta and backend-config const backendConfigs = { - service: AmplifySupportedService.LAMBDA, + service: FunctionServiceNameLambdaFunction, providerPlugin: 'awscloudformation', build: true, dependsOn, }; - await context.amplify.updateamplifyMetaAfterResourceAdd(AmplifyCategories.FUNCTION, functionName, backendConfigs); - printer.success(`Successfully added ${functionName} function locally`); + await context.amplify.updateamplifyMetaAfterResourceAdd('function', functionName, backendConfigs); + context.print.success(`Successfully added ${functionName} function locally`); } else { - printer.success(`Successfully updated ${functionName} function locally`); + context.print.success(`Successfully updated ${functionName} function locally`); } }; -const createAdminAuthAPI = async (context: $TSContext, authResourceName: string, functionName: string, operation: 'update' | 'add') => { +const createAdminAuthAPI = async (context: any, authResourceName: string, functionName: string, operation: 'update' | 'add') => { const apiName = 'AdminQueries'; - const dependsOn = [ + const targetDir = path.join(pathManager.getBackendDirPath(), 'api', apiName); + const dependsOn = []; + + dependsOn.push( { - category: AmplifyCategories.AUTH, + category: 'auth', resourceName: authResourceName, attributes: ['UserPoolId'], }, { - category: AmplifyCategories.FUNCTION, + category: 'function', resourceName: functionName, attributes: ['Arn', 'Name'], }, - ]; + ); const apiProps = { - apiName, functionName, authResourceName, dependsOn, }; + const copyJobs = [ + { + dir: adminAuthAssetRoot, + template: 'admin-queries-api-template.json.ejs', + target: path.join(targetDir, 'admin-queries-cloudformation-template.json'), + }, + { + dir: adminAuthAssetRoot, + template: 'admin-queries-api-params.json', + target: path.join(targetDir, 'parameters.json'), + }, + ]; + + // copy over the files + await context.amplify.copyBatch(context, copyJobs, apiProps, true); + if (operation === 'add') { - await context.amplify.invokePluginMethod(context, AmplifyCategories.API, undefined, 'addAdminQueriesApi', [context, apiProps]); - printer.success(`Successfully added ${apiName} API locally`); + // Update amplify-meta and backend-config + const backendConfigs = { + service: 'API Gateway', + providerPlugin: 'awscloudformation', + authorizationType: 'AMAZON_COGNITO_USER_POOLS', + dependsOn, + }; + + await context.amplify.updateamplifyMetaAfterResourceAdd('api', apiName, backendConfigs); + context.print.success(`Successfully added ${apiName} API locally`); } else { - await context.amplify.invokePluginMethod(context, AmplifyCategories.API, undefined, 'updateAdminQueriesApi', [context, apiProps]); - printer.success(`Successfully updated ${apiName} API locally`); + context.print.success(`Successfully updated ${apiName} API locally`); } }; const copyS3Assets = async (request: CognitoConfiguration) => { - const targetDir = path.join(pathManager.getBackendDirPath(), AmplifyCategories.AUTH, request.resourceName!, 'assets'); - const triggers = request.triggers ? JSONUtilities.parse<$TSAny>(request.triggers) : null; + const targetDir = path.join(pathManager.getBackendDirPath(), 'auth', request.resourceName!, 'assets'); + const triggers = request.triggers ? JSONUtilities.parse(request.triggers) : null; const confirmationFileNeeded = request.triggers && triggers.CustomMessage && triggers.CustomMessage.includes('verification-link'); if (confirmationFileNeeded) { if (!existsSync(targetDir)) { diff --git a/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/transform-user-pool-group.js b/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/transform-user-pool-group.js index a2cc363ab4e..d2712bde9e2 100644 --- a/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/transform-user-pool-group.js +++ b/packages/amplify-category-auth/src/provider-utils/awscloudformation/utils/transform-user-pool-group.js @@ -1,10 +1,15 @@ -const { JSONUtilities, pathManager } = require('amplify-cli-core'); const path = require('path'); +const fs = require('fs'); const { generateUserPoolGroupStackTemplate } = require('./generate-user-pool-group-stack-template'); const { AuthInputState } = require('../auth-inputs-manager/auth-input-state'); async function transformUserPoolGroupSchema(context) { - const userPoolPrecedencePath = path.join(pathManager.getBackendDirPath(), 'auth', 'userPoolGroups', 'user-pool-group-precedence.json'); + const resourceDirPath = path.join( + context.amplify.pathManager.getBackendDirPath(), + 'auth', + 'userPoolGroups', + 'user-pool-group-precedence.json', + ); const { allResources } = await context.amplify.getResourceStatus(); const authResource = allResources.filter(resource => resource.service === 'Cognito'); @@ -17,7 +22,7 @@ async function transformUserPoolGroupSchema(context) { throw new Error('Cognito UserPool does not exists'); } - const groups = JSONUtilities.readJson(userPoolPrecedencePath); + const groups = context.amplify.readJsonFile(resourceDirPath); // Replace env vars with subs diff --git a/packages/amplify-category-auth/tsconfig.json b/packages/amplify-category-auth/tsconfig.json index 5ba92ce0dfa..eff2879c722 100644 --- a/packages/amplify-category-auth/tsconfig.json +++ b/packages/amplify-category-auth/tsconfig.json @@ -13,13 +13,12 @@ "resources", "scripts", "provider-utils/awscloudformation/triggers", - "src/__tests__" + "src/__tests__", ], "references": [ - { "path": "../amplify-cli-core" }, - { "path": "../amplify-headless-interface" }, - { "path": "../amplify-prompts" }, - { "path": "../amplify-util-headless-input" }, - { "path": "../amplify-util-import" } + {"path": "../amplify-cli-core"}, + {"path": "../amplify-headless-interface"}, + {"path": "../amplify-util-headless-input"}, + {"path": "../amplify-util-import"}, ] } diff --git a/packages/amplify-cli-core/src/__tests__/__snapshots__/cliViewAPI.test.ts.snap b/packages/amplify-cli-core/src/__tests__/__snapshots__/cliViewAPI.test.ts.snap index 3c4925b7167..0765b9ff796 100644 --- a/packages/amplify-cli-core/src/__tests__/__snapshots__/cliViewAPI.test.ts.snap +++ b/packages/amplify-cli-core/src/__tests__/__snapshots__/cliViewAPI.test.ts.snap @@ -6,22 +6,22 @@ NAME amplify status -- Shows the state of local resources not yet pushed to the cloud (Create/Update/Delete) SYNOPSIS -amplify status [-v|--verbose] [category ...] +amplify status [-v|--verbose] [category ...] DESCRIPTION The amplify status command displays the difference between the deployed state and the local state of the application. -The following options are available: +The following options are available: [category ...] : (Summary mode) Displays the summary of local state vs deployed state of the application usage: #> amplify status #> amplify status api storage --v [category ...] : (Verbose mode) Displays the cloudformation diff for all resources for the specified category. +-v [category ...] : (Verbose mode) Displays the cloudformation diff for all resources for the specified category. If no category is provided, it shows the diff for all categories. usage: #> amplify status -v #> amplify status -v api storage - - " + + " `; diff --git a/packages/amplify-cli-core/src/__tests__/cliViewAPI.test.ts b/packages/amplify-cli-core/src/__tests__/cliViewAPI.test.ts index e47dc456f29..adb641e1ecb 100644 --- a/packages/amplify-cli-core/src/__tests__/cliViewAPI.test.ts +++ b/packages/amplify-cli-core/src/__tests__/cliViewAPI.test.ts @@ -3,60 +3,63 @@ import chalk from 'chalk'; import stripAnsi from 'strip-ansi'; describe('CLI View tests', () => { - test('Verbose mode CLI status with category list should correctly initialize ViewResourceTableParams [Non-Help]', () => { - const cliParams: CLIParams = { - cliCommand: 'status', - cliSubcommands: undefined, - cliOptions: { - storage: true, - api: true, - verbose: true, - yes: false, - }, - }; - const view = new ViewResourceTableParams(cliParams); - expect(view.command).toBe('status'); - expect(view.categoryList).toStrictEqual(['storage', 'api']); - expect(view.help).toBe(false); - expect(view.verbose).toBe(true); - }); + test('Verbose mode CLI status with category list should correctly initialize ViewResourceTableParams [Non-Help]', () => { + const cliParams : CLIParams = { + cliCommand: 'status', + cliSubcommands: undefined, + cliOptions: { + storage: true, + api: true, + verbose: true, + yes: false + } + } + const view = new ViewResourceTableParams(cliParams); + expect( view.command ).toBe("status"); + expect( view.categoryList).toStrictEqual(['storage', 'api']); + expect( view.help ).toBe(false); + expect( view.verbose ).toBe(true); + }); - test('Status Help CLI should correctly return styled help message', () => { - const cliParams: CLIParams = { - cliCommand: 'status', - cliSubcommands: ['help'], - cliOptions: { yes: false }, - }; + test('Status Help CLI should correctly return styled help message', () => { + const cliParams : CLIParams = { + cliCommand: 'status', + cliSubcommands: [ 'help' ], + cliOptions: { yes: false } + }; - const view = new ViewResourceTableParams(cliParams); - expect(view.command).toBe('status'); - expect(view.categoryList).toStrictEqual([]); - expect(view.help).toBe(true); - expect(view.verbose).toBe(false); - const styledHelp = stripAnsi(chalk.reset(view.getStyledHelp())); - expect(styledHelp).toMatchSnapshot(); - }); + const view = new ViewResourceTableParams(cliParams); + expect( view.command ).toBe("status"); + expect( view.categoryList).toStrictEqual([]); + expect( view.help ).toBe(true); + expect( view.verbose ).toBe(false); + const styledHelp = stripAnsi(chalk.reset(view.getStyledHelp())); + expect(styledHelp).toMatchSnapshot(); + }); - test('Status Command should print error message to the screen', () => { - const cliParams: CLIParams = { - cliCommand: 'status', - cliSubcommands: ['help'], - cliOptions: { yes: false }, - }; - const view = new ViewResourceTableParams(cliParams); - const errorMockFn = jest.fn(); + test('Status Command should print error message to the screen', () => { + const cliParams : CLIParams = { + cliCommand: 'status', + cliSubcommands: [ 'help' ], + cliOptions: { yes: false } + }; + const view = new ViewResourceTableParams(cliParams); + const errorMockFn = jest.fn(); - const context: any = { - print: { - error: errorMockFn, - }, - }; - const errorMessage = 'Something bad happened'; - try { - throw new Error(errorMessage); - } catch (e) { - view.logErrorException(e, context); - expect(errorMockFn).toBeCalledTimes(1); - } - }); -}); + const context: any = { + print : { + error: errorMockFn + } + }; + const errorMessage = "Something bad happened" + try { + throw new Error(errorMessage); + } + catch(e) { + view.logErrorException(e, context); + expect(errorMockFn).toBeCalledTimes(1); + } + + }); + +} ); \ No newline at end of file diff --git a/packages/amplify-cli-core/src/category-interfaces/category-base-schema-generator.ts b/packages/amplify-cli-core/src/category-interfaces/category-base-schema-generator.ts index 762774fca9e..42b63d363bb 100644 --- a/packages/amplify-cli-core/src/category-interfaces/category-base-schema-generator.ts +++ b/packages/amplify-cli-core/src/category-interfaces/category-base-schema-generator.ts @@ -4,11 +4,10 @@ * can be used for run-time validation of Walkthrough/Headless structures. */ import { getProgramFromFiles, buildGenerator, PartialArgs } from 'typescript-json-schema'; -import * as fs from 'fs-extra'; -import * as path from 'path'; +import fs from 'fs-extra'; +import path from 'path'; import Ajv from 'ajv'; import { printer } from 'amplify-prompts'; -import { $TSAny, JSONUtilities } from '..'; // Interface types are expected to be exported as "typeName" in the file export type TypeDef = { @@ -16,21 +15,10 @@ export type TypeDef = { service: string; }; -/** - * Normalize Service Name for use in filepaths - * e.g Convert DynamoDB to dynamoDB , S3 to s3 as filename prefix - * @param svcName - * @returns normalizedSvcName - */ -function normalizeServiceToFilePrefix(serviceName: string): string { - serviceName = serviceName.replace(' ', ''); - return `${serviceName[0].toLowerCase()}${serviceName.slice(1)}`; -} - export class CLIInputSchemaGenerator { // Paths are relative to the package root - TYPES_SRC_ROOT = path.join('.', 'src', 'provider-utils', 'awscloudformation', 'service-walkthrough-types'); - SCHEMA_FILES_ROOT = path.join('.', 'resources', 'schemas'); + TYPES_SRC_ROOT = path.join('.','src','provider-utils','awscloudformation','service-walkthrough-types'); + SCHEMA_FILES_ROOT = path.join('.','resources','schemas'); OVERWRITE_SCHEMA_FLAG = '--overwrite'; private serviceTypeDefs: TypeDef[]; @@ -44,9 +32,18 @@ export class CLIInputSchemaGenerator { } private getTypesSrcRootForSvc(normalizedSvcName: string): string { - return path.join(this.TYPES_SRC_ROOT, `${normalizedSvcName}-user-input-types.ts`); + return path.join(this.TYPES_SRC_ROOT,`${normalizedSvcName}-user-input-types.ts`); } + /** + * Normalize Service Name for use in filepaths + * e.g Convert DynamoDB to dynamoDB , S3 to s3 as filename prefix + * @param svcName + * @returns normalizedSvcName + */ + private normalizeServiceToFilePrefix( svcName : string ): string { + return `${svcName[0].toLowerCase()}${svcName.slice(1)}` + } private printWarningSchemaFileExists() { printer.info('The interface version must be bumped after any changes.'); printer.info(`Use the ${this.OVERWRITE_SCHEMA_FLAG} flag to overwrite existing versions`); @@ -58,7 +55,7 @@ export class CLIInputSchemaGenerator { printer.info(`Output Path: ${schemaFilePath}`); } - private printGeneratingSchemaMessage(svcAbsoluteFilePath: string, serviceName: string) { + private printGeneratingSchemaMessage(svcAbsoluteFilePath: string, serviceName : string){ printer.info(`Generating Schema for ${serviceName}`); printer.info(`Input Path: ${svcAbsoluteFilePath}`); } @@ -77,7 +74,7 @@ export class CLIInputSchemaGenerator { }; for (const typeDef of this.serviceTypeDefs) { - const normalizedServiceName = normalizeServiceToFilePrefix(typeDef.service); + const normalizedServiceName = this.normalizeServiceToFilePrefix( typeDef.service ) ; //get absolute file path to the user-input types for the given service const svcAbsoluteFilePath = this.getSvcFileAbsolutePath(normalizedServiceName); this.printGeneratingSchemaMessage(svcAbsoluteFilePath, typeDef.service); @@ -94,7 +91,7 @@ export class CLIInputSchemaGenerator { return generatedFilePaths; } fs.ensureFileSync(outputSchemaFilePath); - JSONUtilities.writeJson(outputSchemaFilePath, typeSchema); + fs.writeFileSync(outputSchemaFilePath, JSON.stringify(typeSchema, undefined, 4)); //print success status to the terminal this.printSuccessSchemaFileWritten(outputSchemaFilePath, typeDef.typeName); generatedFilePaths.push(outputSchemaFilePath); @@ -112,7 +109,7 @@ export class CLIInputSchemaValidator { constructor(service: string, category: string, schemaFileName: string) { this._category = category; - this._service = normalizeServiceToFilePrefix(service); + this._service = service; this._schemaFileName = schemaFileName; this._ajv = new Ajv(); } @@ -128,14 +125,12 @@ export class CLIInputSchemaValidator { async validateInput(userInput: string): Promise { const userInputSchema = await this.getUserInputSchema(); if (userInputSchema.dependencySchemas) { - userInputSchema.dependencySchemas.reduce((acc: { addSchema: (arg0: $TSAny) => $TSAny }, it: $TSAny) => acc.addSchema(it), this._ajv); + userInputSchema.dependencySchemas.reduce((acc: { addSchema: (arg0: any) => any }, it: any) => acc.addSchema(it), this._ajv); } const validate = this._ajv.compile(userInputSchema); - const input = JSONUtilities.parse(userInput); + const input = JSON.parse(userInput); if (!validate(input) as boolean) { - throw new Error( - `Data did not validate against the supplied schema. Underlying errors were ${JSONUtilities.stringify(validate.errors)}`, - ); + throw new Error(`Data did not validate against the supplied schema. Underlying errors were ${JSON.stringify(validate.errors)}`); } return true; } diff --git a/packages/amplify-cli-core/src/cliConstants.ts b/packages/amplify-cli-core/src/cliConstants.ts index 3205c237b59..7dcba841c6e 100644 --- a/packages/amplify-cli-core/src/cliConstants.ts +++ b/packages/amplify-cli-core/src/cliConstants.ts @@ -17,7 +17,7 @@ export enum CLISubCommandType { CONSOLE = 'console', IMPORT = 'import', OVERRIDE = 'override', - MIGRATE = 'migrate', + MIGRATE = 'migrate' } export const AmplifyCategories = { STORAGE: 'storage', @@ -32,13 +32,11 @@ export const AmplifyCategories = { }; export const AmplifySupportedService = { - APIGW: 'API Gateway', - APPSYNC: 'AppSync', S3: 'S3', DYNAMODB: 'DynamoDB', COGNITO: 'Cognito', COGNITOUSERPOOLGROUPS: 'Cognito-UserPool-Groups', - LAMBDA: 'Lambda', + LAMBDA : 'Lambda' }; export const overriddenCategories = [AmplifyCategories.AUTH, AmplifyCategories.STORAGE]; diff --git a/packages/amplify-cli-core/src/cliViewAPI.ts b/packages/amplify-cli-core/src/cliViewAPI.ts index 506e693c192..365d30b8b74 100644 --- a/packages/amplify-cli-core/src/cliViewAPI.ts +++ b/packages/amplify-cli-core/src/cliViewAPI.ts @@ -3,95 +3,86 @@ import chalk from 'chalk'; import { $TSAny, $TSContext } from '.'; export interface CLIParams { - cliCommand: string; - cliSubcommands: string[] | undefined; - cliOptions: Record; + cliCommand: string; + cliSubcommands: string[] | undefined; + cliOptions: Record; } //Resource Table filter and display params (params used for summary/display view of resource table) export class ViewResourceTableParams { - private _command: string; - private _verbose: boolean; //display table in verbose mode - private _help: boolean; //display help for the command - private _categoryList: string[] | []; //categories to display - private _filteredResourceList: $TSAny; //resources to *not* display - TBD define union of valid types - - public get command() { - return this._command; - } - - public get verbose() { - return this._verbose; - } - - public get help() { - return this._help; - } - - public get categoryList() { - return this._categoryList; - } - - getCategoryFromCLIOptions(cliOptions: object) { - if (cliOptions) { - return Object.keys(cliOptions) - .filter(key => key != 'verbose' && key !== 'yes') - .map(category => category.toLowerCase()); - } else { - return []; + private _command: string; + private _verbose: boolean; //display table in verbose mode + private _help: boolean; //display help for the command + private _categoryList: string[] | []; //categories to display + private _filteredResourceList: any; //resources to *not* display - TBD define union of valid types + + public get command() { + return this._command; } - } - - styleHeader(str: string) { - return chalk.italic(chalk.bgGray.whiteBright(str)); - } - - styleCommand(str: string) { - return chalk.greenBright(str); - } - - styleOption(str: string) { - return chalk.yellowBright(str); - } - - stylePrompt(str: string) { - return chalk.bold(chalk.yellowBright(str)); - } - - public getStyledHelp() { - return ` + public get verbose() { + return this._verbose; + } + public get help() { + return this._help; + } + public get categoryList() { + return this._categoryList; + } + getCategoryFromCLIOptions(cliOptions: object) { + if (cliOptions) { + return Object.keys(cliOptions) + .filter(key => key != 'verbose' && key !== 'yes') + .map(category => category.toLowerCase()); + } else { + return []; + } + } + styleHeader(str: string) { + return chalk.italic(chalk.bgGray.whiteBright(str)); + } + styleCommand(str: string) { + return chalk.greenBright(str); + } + styleOption(str: string) { + return chalk.yellowBright(str); + } + stylePrompt(str: string) { + return chalk.bold(chalk.yellowBright(str)); + } + public getStyledHelp() { + return ` ${this.styleHeader('NAME')} ${this.styleCommand('amplify status')} -- Shows the state of local resources not yet pushed to the cloud (Create/Update/Delete) ${this.styleHeader('SYNOPSIS')} -${this.styleCommand('amplify status')} [${this.styleCommand('-v')}|${this.styleCommand('--verbose')}] [${this.styleOption('category ...')}] +${this.styleCommand('amplify status')} [${this.styleCommand('-v')}|${this.styleCommand('--verbose')}] [${this.styleOption('category ...')}] ${this.styleHeader('DESCRIPTION')} The amplify status command displays the difference between the deployed state and the local state of the application. -The following options are available: +The following options are available: ${this.styleCommand('[category ...]')} : (Summary mode) Displays the summary of local state vs deployed state of the application usage: ${this.stylePrompt('#>')} ${this.styleCommand('amplify status')} ${this.stylePrompt('#>')} ${this.styleCommand('amplify status')} ${this.styleOption('api storage')} -${this.styleCommand('-v [category ...]')} : (Verbose mode) Displays the cloudformation diff for all resources for the specified category. +${this.styleCommand('-v [category ...]')} : (Verbose mode) Displays the cloudformation diff for all resources for the specified category. If no category is provided, it shows the diff for all categories. usage: ${this.stylePrompt('#>')} ${this.styleCommand('amplify status -v')} ${this.stylePrompt('#>')} ${this.styleCommand('amplify status -v ')}${this.styleOption('api storage')} + + `; + } - `; - } - - public logErrorException(e: Error, context: $TSContext) { - context.print.error(`Name: ${e.name} : Message: ${e.message}`); - } + public logErrorException( e : Error , context : $TSContext ){ + context.print.error(`Name: ${e.name} : Message: ${e.message}`); + } - public constructor(cliParams: CLIParams) { - this._command = cliParams.cliCommand; - this._verbose = cliParams.cliOptions?.verbose === true; - this._categoryList = this.getCategoryFromCLIOptions(cliParams.cliOptions); - this._filteredResourceList = []; //TBD - add support to provide resources - this._help = cliParams.cliSubcommands ? cliParams.cliSubcommands.includes('help') : false; - } + public constructor(cliParams: CLIParams) { + this._command = cliParams.cliCommand; + this._verbose = cliParams.cliOptions?.verbose === true; + this._categoryList = this.getCategoryFromCLIOptions(cliParams.cliOptions); + this._filteredResourceList = []; //TBD - add support to provide resources + this._help = cliParams.cliSubcommands ? cliParams.cliSubcommands.includes('help') : false; + } } diff --git a/packages/amplify-cli-core/src/index.ts b/packages/amplify-cli-core/src/index.ts index 084fb5b2ec9..d964af1cb6d 100644 --- a/packages/amplify-cli-core/src/index.ts +++ b/packages/amplify-cli-core/src/index.ts @@ -179,10 +179,10 @@ export type GetPackageAssetPaths = () => Promise; export type $IPluginManifest = $TSAny; // Use it for all file content read from amplify-meta.json -export type $TSMeta = $TSAny; +export type $TSMeta = any; // Use it for all file content read from team-provider-info.json -export type $TSTeamProviderInfo = $TSAny; +export type $TSTeamProviderInfo = any; // Use it for all object initializer usages: {} export type $TSObject = Record; @@ -215,7 +215,7 @@ export interface ProviderContext { projectName: string; } -export type $TSCopyJob = $TSAny; +export type $TSCopyJob = any; // Temporary interface until Context refactor interface AmplifyToolkit { @@ -223,9 +223,9 @@ interface AmplifyToolkit { constants: $TSAny; constructExeInfo: (context: $TSContext) => $TSAny; copyBatch: (context: $TSContext, jobs: $TSCopyJob[], props: object, force?: boolean, writeParams?: boolean | object) => $TSAny; - crudFlow: (role: string, permissionMap?: $TSObject, defaults?: string[]) => Promise; + crudFlow: (role: string, permissionMap?: $TSObject, defaults?: $TSAny[]) => $TSAny; deleteProject: () => $TSAny; - executeProviderUtils: (context: $TSContext, providerName: string, utilName: string, options?: $TSAny) => $TSAny; + executeProviderUtils: (context: $TSContext, providerName: string, utilName: string, options: $TSAny) => $TSAny; getAllEnvs: () => string[]; getPlugin: () => $TSAny; getCategoryPluginInfo: (context: $TSContext, category?: string, service?: string) => $TSAny; @@ -237,10 +237,6 @@ interface AmplifyToolkit { getPluginInstance: (context: $TSContext, pluginName: string) => $TSAny; getProjectConfig: () => $TSAny; getProjectDetails: () => $TSAny; - - /** - * @deprecated Use stateManager.getMeta() from amplify-cli-core - */ getProjectMeta: () => $TSMeta; getResourceStatus: (category?: $TSAny, resourceName?: $TSAny, providerName?: $TSAny, filteredResources?: $TSAny) => $TSAny; getResourceOutputs: () => $TSAny; @@ -250,10 +246,6 @@ interface AmplifyToolkit { */ inputValidation: (input: $TSAny) => (value: $TSAny) => boolean | string; listCategories: () => $TSAny; - - /** - * @deprecated use uuid - */ makeId: (n?: number) => string; openEditor: (context: $TSContext, target: string, waitToContinue?: boolean) => Promise; onCategoryOutputsChange: (context: $TSContext, currentAmplifyMeta: $TSMeta | undefined, amplifyMeta?: $TSMeta) => $TSAny; @@ -267,23 +259,15 @@ interface AmplifyToolkit { rebuild?: boolean, ) => $TSAny; storeCurrentCloudBackend: () => $TSAny; - - /** - * @deprecated use stateManager or JSONUtilities from amplify-cli-core - */ readJsonFile: (fileName: string) => $TSAny; removeDeploymentSecrets: (context: $TSContext, category: string, resource: string) => void; removeResource: ( context: $TSContext, category: string, resource: string, - questionOptions?: { - headless?: boolean; - serviceSuffix?: { [serviceName: string]: string }; - serviceDeletionInfo?: { [serviceName: string]: string }; - }, + questionOptions?: $TSAny, resourceNameCallback?: (resourceName: string) => Promise, - ) => Promise<{ service: string; resourceName: string } | undefined>; + ) => $TSAny; sharedQuestions: () => $TSAny; showAllHelp: () => $TSAny; showHelp: (header: string, commands: { name: string; description: string }[]) => $TSAny; @@ -317,17 +301,17 @@ interface AmplifyToolkit { // buildType is from amplify-function-plugin-interface but can't be imported here because it would create a circular dependency updateamplifyMetaAfterBuild: (resource: ResourceTuple, buildType?: string) => void; updateAmplifyMetaAfterPackage: (resource: ResourceTuple, zipFilename: string, hash?: { resourceKey: string; hashValue: string }) => void; - updateBackendConfigAfterResourceAdd: (category: string, resourceName: string, resourceData: $TSObject) => void; - updateBackendConfigAfterResourceUpdate: (category: string, resourceName: string, attribute: string, value: $TSAny) => void; - updateBackendConfigAfterResourceRemove: (category: string, resourceName: string) => void; + updateBackendConfigAfterResourceAdd: (category: string, resourceName: string, resourceData: $TSAny) => $TSAny; + updateBackendConfigAfterResourceUpdate: () => $TSAny; + updateBackendConfigAfterResourceRemove: () => $TSAny; loadEnvResourceParameters: (context: $TSContext, category: string, resourceName: string) => $TSAny; saveEnvResourceParameters: (context: $TSContext, category: string, resourceName: string, envSpecificParams?: $TSObject) => void; removeResourceParameters: (context: $TSContext, category: string, resource: string) => void; triggerFlow: () => $TSAny; addTrigger: () => $TSAny; updateTrigger: () => $TSAny; - deleteTrigger: (context: $TSContext, name: string, dir: string) => Promise; - deleteAllTriggers: (previouslySaved: $TSAny, resourceName: string, targetDir: string, context: $TSContext) => Promise; + deleteTrigger: () => $TSAny; + deleteAllTriggers: () => $TSAny; deleteDeselectedTriggers: () => $TSAny; dependsOnBlock: (context: $TSContext, dependsOnKeys: string[], service: string) => $TSAny; getTriggerMetadata: () => $TSAny; @@ -335,7 +319,7 @@ interface AmplifyToolkit { getTriggerEnvVariables: () => $TSAny; getTriggerEnvInputs: () => $TSAny; getUserPoolGroupList: () => $TSAny[]; - forceRemoveResource: (context: $TSContext, categoryName: string, name: string, dir: string) => $TSAny; + forceRemoveResource: () => $TSAny; writeObjectAsJson: () => $TSAny; hashDir: (dir: string, exclude: string[]) => Promise; leaveBreadcrumbs: (category: string, resourceName: string, breadcrumbs: unknown) => void; @@ -349,5 +333,5 @@ interface AmplifyToolkit { unauthRoleArn?: string; unauthRoleName?: string; }; - invokePluginMethod: (context: $TSContext, category: string, service: string | undefined, method: string, args: $TSAny[]) => Promise; + invokePluginMethod: (context: $TSContext, category: string, service: string | undefined, method: string, args: any[]) => Promise; } diff --git a/packages/amplify-cli-core/src/overrides-manager/override-skeleton-generator.ts b/packages/amplify-cli-core/src/overrides-manager/override-skeleton-generator.ts index af00f9df708..001ff1ca8db 100644 --- a/packages/amplify-cli-core/src/overrides-manager/override-skeleton-generator.ts +++ b/packages/amplify-cli-core/src/overrides-manager/override-skeleton-generator.ts @@ -1,14 +1,13 @@ -import { printer, prompter } from 'amplify-prompts'; +import fs from 'fs-extra'; +import { $TSContext, getPackageManager } from '../index'; import execa from 'execa'; -import * as fs from 'fs-extra'; -import { EOL } from 'os'; import * as path from 'path'; -import { $TSAny, $TSContext, getPackageManager, pathManager } from '../index'; +import { printer, prompter } from 'amplify-prompts'; import { JSONUtilities } from '../jsonUtilities'; export const generateOverrideSkeleton = async (context: $TSContext, srcResourceDirPath: string, destDirPath: string): Promise => { // 1. Create skeleton package - const backendDir = pathManager.getBackendDirPath(); + const backendDir = context.amplify.pathManager.getBackendDirPath(); const overrideFile = path.join(destDirPath, 'override.ts'); if (fs.existsSync(overrideFile)) { await context.amplify.openEditor(context, overrideFile); @@ -65,11 +64,11 @@ export async function buildOverrideDir(cwd: string, destDirPath: string): Promis encoding: 'utf-8', }); return true; - } catch (error: $TSAny) { - if (error.code === 'ENOENT') { + } catch (error) { + if ((error as any).code === 'ENOENT') { throw new Error(`Packaging overrides failed. Could not find ${packageManager} executable in the PATH.`); } else { - throw new Error(`Packaging overrides failed with the error:${EOL}${error.message}`); + throw new Error(`Packaging overrides failed with the error \n${error.message}`); } } } diff --git a/packages/amplify-cli-core/src/state-manager/stateManager.ts b/packages/amplify-cli-core/src/state-manager/stateManager.ts index a06437a7904..cafa3f838ce 100644 --- a/packages/amplify-cli-core/src/state-manager/stateManager.ts +++ b/packages/amplify-cli-core/src/state-manager/stateManager.ts @@ -290,14 +290,6 @@ export class StateManager { JSONUtilities.writeJson(filePath, inputs); }; - resourceInputsJsonExists = (projectPath: string | undefined, category: string, resourceName: string): boolean => { - try { - return fs.existsSync(pathManager.getResourceInputsJsonFilePath(projectPath, category, resourceName)); - } catch (e) { - return false; - } - }; - cliJSONFileExists = (projectPath: string, env?: string): boolean => { try { return fs.existsSync(pathManager.getCLIJSONFilePath(projectPath, env)); diff --git a/packages/amplify-cli/package.json b/packages/amplify-cli/package.json index 62a69e133e8..1300d698e56 100644 --- a/packages/amplify-cli/package.json +++ b/packages/amplify-cli/package.json @@ -36,7 +36,7 @@ "@aws-cdk/cloudformation-diff": "~1.124.0", "amplify-app": "3.0.16", "amplify-category-analytics": "2.21.24", - "@aws-amplify/amplify-category-api": "1.0.0", + "amplify-category-api": "2.33.2", "@aws-amplify/amplify-category-auth": "1.0.0", "@aws-amplify/amplify-category-custom": "1.0.0", "amplify-category-function": "2.36.1", diff --git a/packages/amplify-cli/src/commands/build-override.ts b/packages/amplify-cli/src/commands/build-override.ts index 3e09d08b0f7..96975b9a8b6 100644 --- a/packages/amplify-cli/src/commands/build-override.ts +++ b/packages/amplify-cli/src/commands/build-override.ts @@ -44,19 +44,19 @@ export const run = async (context: $TSContext) => { export const getResources = async (context: $TSContext): Promise => { const resources: IAmplifyResource[] = []; const { resourcesToBeCreated, resourcesToBeUpdated } = await context.amplify.getResourceStatus(); - resourcesToBeCreated.forEach((resourceCreated: IAmplifyResource) => { + resourcesToBeCreated.forEach(resourceCreated => { resources.push({ - service: resourceCreated.service, - category: resourceCreated.category, - resourceName: resourceCreated.resourceName, + service: resourceCreated.service as string, + category: resourceCreated.category as string, + resourceName: resourceCreated.resourceName as string, }); }); - resourcesToBeUpdated.forEach((resourceUpdated: IAmplifyResource) => { + resourcesToBeUpdated.forEach(resourceUpdated => { resources.push({ - service: resourceUpdated.service, - category: resourceUpdated.category, - resourceName: resourceUpdated.resourceName, + service: resourceUpdated.service as string, + category: resourceUpdated.category as string, + resourceName: resourceUpdated.resourceName as string, }); }); return resources; diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/execute-provider-utils.ts b/packages/amplify-cli/src/extensions/amplify-helpers/execute-provider-utils.ts index 934f259e946..96fc64cfd40 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/execute-provider-utils.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/execute-provider-utils.ts @@ -1,8 +1,8 @@ import { $TSAny, $TSContext } from 'amplify-cli-core'; import { getProviderPlugins } from './get-provider-plugins'; -export async function executeProviderUtils(context: $TSContext, providerName: string, utilName: string, options?: $TSAny) { +export async function executeProviderUtils(context: $TSContext, providerName: string, utilName: string, options: $TSAny) { const providerPlugins = getProviderPlugins(context); - const pluginModule = await import(providerPlugins[providerName]); + const pluginModule = require(providerPlugins[providerName]); return pluginModule.providerUtils[utilName](context, options); } diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/get-category-pluginInfo.ts b/packages/amplify-cli/src/extensions/amplify-helpers/get-category-pluginInfo.ts index cc4aa013f9c..8906d3c4913 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/get-category-pluginInfo.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/get-category-pluginInfo.ts @@ -1,6 +1,6 @@ -import { $TSContext } from 'amplify-cli-core'; +import { FeatureFlags } from 'amplify-cli-core'; -export function getCategoryPluginInfo(context: $TSContext, category: string, service?: string) { +export function getCategoryPluginInfo(context, category, service?) { let categoryPluginInfo; const pluginInfosForCategory = context.pluginPlatform.plugins[category]; @@ -17,7 +17,9 @@ export function getCategoryPluginInfo(context: $TSContext, category: string, ser categoryPluginInfo = pluginInfosForCategory[0]; } } else { - const overidedPlugin = pluginInfosForCategory.find(plugin => plugin.packageName === `@aws-amplify/amplify-category-${category}`); + const overidedPlugin = pluginInfosForCategory.find(plugin => { + return plugin.packageName === `@aws-amplify/amplify-category-${category}` && FeatureFlags.getBoolean(`overrides.${category}`); + }); if (overidedPlugin !== undefined) { return overidedPlugin; } diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/permission-flow.ts b/packages/amplify-cli/src/extensions/amplify-helpers/permission-flow.ts index a4c7fed7fb6..78e99d8072c 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/permission-flow.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/permission-flow.ts @@ -1,7 +1,7 @@ import * as inquirer from 'inquirer'; import _ from 'lodash'; -export const crudFlow = async (role: string, permissionMap = {}, defaults: string[] = []) => { +export const crudFlow = async (role, permissionMap = {}, defaults = []) => { if (!role) throw new Error('No role provided to permission question flow'); const possibleOperations = Object.keys(permissionMap).map(el => ({ name: el, value: el })); diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/remove-resource.ts b/packages/amplify-cli/src/extensions/amplify-helpers/remove-resource.ts index 3a28ecd021c..4ee3d36c9e0 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/remove-resource.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/remove-resource.ts @@ -37,11 +37,7 @@ export async function removeResource( context: $TSContext, category: string, resourceName?: string, - options: { - headless?: boolean; - serviceSuffix?: { [serviceName: string]: string }; - serviceDeletionInfo?: { [serviceName: string]: string }; - } = { headless: false }, + options: { headless?: boolean; serviceSuffix?: { [serviceName: string]: string }; serviceDeletionInfo?: {} } = { headless: false }, resourceNameCallback?: (resourceName: string) => Promise, ) { const amplifyMeta = stateManager.getMeta(); @@ -139,9 +135,8 @@ const deleteResourceFiles = async (context: $TSContext, category: string, resour } }); } - const serviceName: string = amplifyMeta[category][resourceName].service; const resourceValues = { - service: serviceName, + service: amplifyMeta[category][resourceName].service, resourceName, }; if (amplifyMeta[category][resourceName] !== undefined) { diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/trigger-flow.ts b/packages/amplify-cli/src/extensions/amplify-helpers/trigger-flow.ts index aeb9f850bc5..787d81ccf1b 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/trigger-flow.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/trigger-flow.ts @@ -1,10 +1,10 @@ -import { $TSAny, $TSContext, $TSObject, exitOnNextTick, JSONUtilities } from 'amplify-cli-core'; +import * as inquirer from 'inquirer'; import chalk from 'chalk'; import * as fs from 'fs-extra'; -import * as inquirer from 'inquirer'; -import Separator from 'inquirer/lib/objects/separator'; -import _ from 'lodash'; import * as path from 'path'; +import _ from 'lodash'; +import { exitOnNextTick, JSONUtilities, $TSAny } from 'amplify-cli-core'; +import Separator from 'inquirer/lib/objects/separator'; // keep in sync with ServiceName in amplify-category-function, but probably it will not change const FunctionServiceNameLambdaFunction = 'Lambda'; @@ -211,7 +211,7 @@ export const deleteDeselectedTriggers = async (currentTriggers, previousTriggers } }; -export const deleteTrigger = async (context: $TSContext, name: string, dir: string) => { +export const deleteTrigger = async (context, name, dir) => { try { await context.amplify.forceRemoveResource(context, 'function', name, dir); } catch (e) { @@ -219,7 +219,7 @@ export const deleteTrigger = async (context: $TSContext, name: string, dir: stri } }; -export const deleteAllTriggers = async (triggers: $TSObject, functionName: string, dir: string, context: $TSContext) => { +export const deleteAllTriggers = async (triggers, functionName, dir, context) => { const previousKeys = Object.keys(triggers); for (let y = 0; y < previousKeys.length; y += 1) { const targetPath = `${dir}/function/${functionName}`; diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/update-backend-config.ts b/packages/amplify-cli/src/extensions/amplify-helpers/update-backend-config.ts index 9f2ac9f66e2..1ae624d5487 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/update-backend-config.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/update-backend-config.ts @@ -1,7 +1,7 @@ -import { $TSAny, $TSObject, stateManager } from 'amplify-cli-core'; import _ from 'lodash'; +import { stateManager } from 'amplify-cli-core'; -export function updateBackendConfigAfterResourceAdd(category: string, resourceName: string, options: $TSObject) { +export function updateBackendConfigAfterResourceAdd(category, resourceName, options) { const backendConfig = stateManager.getBackendConfig(undefined, { throwIfNotExist: false, default: {}, @@ -20,7 +20,7 @@ export function updateBackendConfigAfterResourceAdd(category: string, resourceNa stateManager.setBackendConfig(undefined, backendConfig); } -export function updateBackendConfigAfterResourceUpdate(category: string, resourceName: string, attribute: string, value: $TSAny) { +export function updateBackendConfigAfterResourceUpdate(category, resourceName, attribute, value) { const backendConfig = stateManager.getBackendConfig(undefined, { throwIfNotExist: false, default: {}, @@ -31,7 +31,7 @@ export function updateBackendConfigAfterResourceUpdate(category: string, resourc stateManager.setBackendConfig(undefined, backendConfig); } -export function updateBackendConfigAfterResourceRemove(category: string, resourceName: string) { +export function updateBackendConfigAfterResourceRemove(category, resourceName) { const backendConfig = stateManager.getBackendConfig(undefined, { throwIfNotExist: false, default: {}, diff --git a/packages/amplify-container-hosting/lib/ElasticContainer/index.js b/packages/amplify-container-hosting/lib/ElasticContainer/index.js index f37512d1985..a19956e57d8 100644 --- a/packages/amplify-container-hosting/lib/ElasticContainer/index.js +++ b/packages/amplify-container-hosting/lib/ElasticContainer/index.js @@ -6,7 +6,7 @@ const path = require('path'); const constants = require('../constants'); -const { EcsAlbStack, NETWORK_STACK_LOGICAL_ID, DEPLOYMENT_MECHANISM, processDockerConfig } = require('@aws-amplify/amplify-category-api'); +const { EcsAlbStack, NETWORK_STACK_LOGICAL_ID, DEPLOYMENT_MECHANISM, processDockerConfig } = require('amplify-category-api'); const { open } = require('amplify-cli-core'); const serviceName = 'ElasticContainer'; @@ -228,7 +228,7 @@ export async function generateHostingResources( const projectBackendDirPath = context.amplify.pathManager.getBackendDirPath(); - /** @type {import('@aws-amplify/amplify-category-api').ApiResource & {service: string, domain: string, providerPlugin:string, hostedZoneId: string, iamAccessUnavailable: boolean}} */ + /** @type {import('amplify-category-api').ApiResource & {service: string, domain: string, providerPlugin:string, hostedZoneId: string, iamAccessUnavailable: boolean}} */ const resource = { resourceName, service: serviceName, diff --git a/packages/amplify-container-hosting/package.json b/packages/amplify-container-hosting/package.json index 679de427cea..c8ad678787b 100644 --- a/packages/amplify-container-hosting/package.json +++ b/packages/amplify-container-hosting/package.json @@ -18,7 +18,7 @@ "test": "jest --coverage --passWithNoTests" }, "dependencies": { - "@aws-amplify/amplify-category-api": "1.0.0", + "amplify-category-api": "2.33.2", "chalk": "^4.1.1", "fs-extra": "^8.1.0", "inquirer": "^7.3.3", diff --git a/packages/amplify-e2e-core/src/categories/api.ts b/packages/amplify-e2e-core/src/categories/api.ts index 6c6458e7353..e7bbb8fc8d7 100644 --- a/packages/amplify-e2e-core/src/categories/api.ts +++ b/packages/amplify-e2e-core/src/categories/api.ts @@ -1,9 +1,10 @@ +import { getCLIPath, updateSchema, nspawn as spawn, KEY_DOWN_ARROW } from '..'; import * as fs from 'fs-extra'; -import _ from 'lodash'; import * as path from 'path'; -import { ExecutionContext, getCLIPath, nspawn as spawn, RETURN, updateSchema } from '..'; -import { multiSelect, singleSelect } from '../utils/selectors'; import { selectRuntime, selectTemplate } from './lambda-function'; +import { singleSelect, multiSelect } from '../utils/selectors'; +import _ from 'lodash'; +import { EOL } from 'os'; import { modifiedApi } from './resources/modified-api-index'; export function getSchemaPath(schemaName: string): string { @@ -400,145 +401,126 @@ export function updateAPIWithResolutionStrategyWithModels(cwd: string, settings: } // Either settings.existingLambda or settings.isCrud is required - -type RestApiSettings = { - allowGuestUsers?: boolean; - existingLambda?: boolean; - hasUserPoolGroups?: boolean; - isFirstRestApi?: boolean; - isCrud?: boolean; - path?: string; - resourceName?: string; - restrictAccess?: boolean; -}; - -export function addRestApi(cwd: string, settings: RestApiSettings) { +export function addRestApi(cwd: string, settings: any) { return new Promise((resolve, reject) => { - const isFirstRestApi = settings.isFirstRestApi ?? true; - const chain = spawn(getCLIPath(), ['add', 'api'], { cwd, stripColors: true }) - .wait('Select from one of the below mentioned services') - .sendKeyDown() - .sendCarriageReturn(); // REST + if (!('existingLambda' in settings) && !('isCrud' in settings)) { + reject(new Error('Missing property in settings object in addRestApi()')); + } else { + const isFirstRestApi = settings.isFirstRestApi ?? true; + let chain = spawn(getCLIPath(), ['add', 'api'], { cwd, stripColors: true }) + .wait('Select from one of the below mentioned services') + .send(KEY_DOWN_ARROW) + .sendCarriageReturn(); // REST + + if (!isFirstRestApi) { + chain.wait('Would you like to add a new path to an existing REST API'); + + if (settings.path) { + chain + .sendConfirmYes() + .wait('Please select the REST API you would want to update') + .sendCarriageReturn() // Select the first REST API + .wait('Provide a path') + .sendLine(settings.path) + .wait('Choose a lambda source') + .send(KEY_DOWN_ARROW) + .sendCarriageReturn() // Existing lambda + .wait('Choose the Lambda function to invoke by this path') + .sendCarriageReturn() // Pick first one + .wait('Restrict API access') + .sendConfirmNo() // Do not restrict access + .wait('Do you want to add another path') + .sendConfirmNo() // Do not add another path + .sendEof() + .run((err: Error) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + return; + } else { + chain.sendConfirmNo(); + } + } - if (!isFirstRestApi) { - chain.wait('Would you like to add a new path to an existing REST API'); + chain + .wait('Provide a friendly name for your resource to be used as a label for this category in the project') + .sendCarriageReturn() + .wait('Provide a path') + .sendCarriageReturn() + .wait('Choose a lambda source'); - if (settings.path) { + if (settings.existingLambda) { chain - .sendConfirmYes() - .wait('Select the REST API you want to update') - .sendCarriageReturn() // Select the first REST API - .wait('What would you like to do?') - .sendCarriageReturn() - .wait('Provide a path') - .sendLine(settings.path ?? RETURN) - .wait('Choose a lambda source') - .sendKeyDown() + .send(KEY_DOWN_ARROW) .sendCarriageReturn() // Existing lambda .wait('Choose the Lambda function to invoke by this path') - .sendCarriageReturn() // Pick first one - .wait('Restrict API access') - .sendConfirmNo() // Do not restrict access - .wait('Do you want to add another path') - .sendConfirmNo() // Do not add another path - .sendEof() - .run((err: Error) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - return; + .sendCarriageReturn(); // Pick first one } else { - chain.sendConfirmNo(); - } - } + chain + .sendCarriageReturn() // Create new Lambda function + .wait('Provide an AWS Lambda function name') + .sendCarriageReturn(); - chain - .wait('Provide a friendly name for your resource to be used as a label for this category in the project') - .sendLine(settings.resourceName ?? RETURN) - .wait('Provide a path') - .sendCarriageReturn() - .wait('Choose a lambda source'); + selectRuntime(chain, 'nodejs'); - if (settings.existingLambda) { - chain - .sendKeyDown() - .sendCarriageReturn() // Existing lambda - .wait('Choose the Lambda function to invoke by this path') - .sendCarriageReturn(); // Pick first one - } else { - chain - .sendCarriageReturn() // Create new Lambda function - .wait('Provide an AWS Lambda function name') - .sendCarriageReturn(); - - selectRuntime(chain, 'nodejs'); + const templateName = settings.isCrud + ? 'CRUD function for DynamoDB (Integration with API Gateway)' + : 'Serverless ExpressJS function (Integration with API Gateway)'; + selectTemplate(chain, templateName, 'nodejs'); - const templateName = settings.isCrud - ? 'CRUD function for DynamoDB (Integration with API Gateway)' - : 'Serverless ExpressJS function (Integration with API Gateway)'; - selectTemplate(chain, templateName, 'nodejs'); + if (settings.isCrud) { + chain + .wait('Choose a DynamoDB data source option') + .sendCarriageReturn() // Use DDB table configured in current project + .wait('Choose from one of the already configured DynamoDB tables') + .sendCarriageReturn(); // Use first one in the list + } - if (settings.isCrud) { chain - .wait('Choose a DynamoDB data source option') - .sendCarriageReturn() // Use DDB table configured in current project - .wait('Choose from one of the already configured DynamoDB tables') - .sendCarriageReturn(); // Use first one in the list + .wait('Do you want to configure advanced settings?') + .sendConfirmNo() + .wait('Do you want to edit the local lambda function now') + .sendConfirmNo(); } - chain - .wait('Do you want to configure advanced settings?') - .sendConfirmNo() - .wait('Do you want to edit the local lambda function now') - .sendConfirmNo(); - } + chain.wait('Restrict API access'); - chain.wait('Restrict API access'); + if (settings.restrictAccess) { + chain.sendConfirmYes().wait('Who should have access'); - if (settings.restrictAccess) { - chain.sendConfirmYes(); - - if (settings.hasUserPoolGroups) { - chain.wait('Restrict access by?').sendCarriageReturn(); // Auth/Guest Users - } - - chain.wait('Who should have access?'); - - if (!settings.allowGuestUsers) { - chain - .sendCarriageReturn() // Authenticated users only - .wait('What permissions do you want to grant to Authenticated') - .sendCtrlA() - .sendCarriageReturn(); // CRUD permissions + if (!settings.allowGuestUsers) { + chain + .sendCarriageReturn() // Authenticated users only + .wait('What kind of access do you want for Authenticated users') + .sendLine('a'); // CRUD permissions + } else { + chain + .sendLine(KEY_DOWN_ARROW) + .sendCarriageReturn() // Authenticated and Guest users + .wait('What kind of access do you want for Authenticated users') + .sendLine('a') // CRUD permissions for authenticated users + .wait('What kind of access do you want for Guest users') + .sendLine('a'); // CRUD permissions for guest users + } } else { - chain - .sendKeyDown() - .sendCarriageReturn() // Authenticated and Guest users - .wait('What permissions do you want to grant to Authenticated') - .sendCtrlA() // CRUD permissions for Authenticated users - .sendCarriageReturn() - .wait('What permissions do you want to grant to Guest') - .send(' ') // C permissions for guest users - .sendCarriageReturn(); + chain.sendConfirmNo(); // Do not restrict access } - } else { - chain.sendConfirmNo(); // Do not restrict access - } - chain - .wait('Do you want to add another path') - .sendConfirmNo() - .sendEof() - .run((err: Error) => { - if (!err) { - resolve(); - } else { - reject(err); - } - }); + chain + .wait('Do you want to add another path') + .sendConfirmNo() + .sendEof() + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + } }); } @@ -602,7 +584,7 @@ export function addApi(projectDir: string, settings?: any) { }); } -function setupAuthType(authType: string, chain: ExecutionContext, settings?: any) { +function setupAuthType(authType: string, chain: any, settings?: any) { switch (authType) { case 'API key': setupAPIKey(chain); @@ -619,7 +601,7 @@ function setupAuthType(authType: string, chain: ExecutionContext, settings?: any } } -function setupAPIKey(chain: ExecutionContext) { +function setupAPIKey(chain: any) { chain .wait('Enter a description for the API key') .sendCarriageReturn() @@ -627,7 +609,7 @@ function setupAPIKey(chain: ExecutionContext) { .sendCarriageReturn(); } -function setupCognitoUserPool(chain: ExecutionContext) { +function setupCognitoUserPool(chain: any) { chain .wait('Do you want to use the default authentication and security configuration') .sendCarriageReturn() @@ -641,7 +623,7 @@ function setupIAM(chain: any) { //no need to do anything } -function setupOIDC(chain: ExecutionContext, settings?: any) { +function setupOIDC(chain: any, settings?: any) { if (!settings || !settings['OpenID Connect']) { throw new Error('Must provide OIDC auth settings.'); } diff --git a/packages/amplify-e2e-core/src/categories/auth.ts b/packages/amplify-e2e-core/src/categories/auth.ts index b7772be12f2..226811c18ad 100644 --- a/packages/amplify-e2e-core/src/categories/auth.ts +++ b/packages/amplify-e2e-core/src/categories/auth.ts @@ -949,7 +949,7 @@ export function addAuthWithGroups(cwd: string): Promise { } // creates 2 groups: Admins, Users -export function addAuthWithGroupsAndAdminAPI(cwd: string, settings?: any): Promise { +export function addAuthWithGroupsAndAdminAPI(cwd: string, settings: any): Promise { return new Promise((resolve, reject) => { spawn(getCLIPath(), ['add', 'auth'], { cwd, stripColors: true }) .wait('Do you want to use the default authentication and security configuration') diff --git a/packages/amplify-e2e-core/src/utils/nexpect.ts b/packages/amplify-e2e-core/src/utils/nexpect.ts index 049a1724137..264f0094711 100644 --- a/packages/amplify-e2e-core/src/utils/nexpect.ts +++ b/packages/amplify-e2e-core/src/utils/nexpect.ts @@ -23,7 +23,7 @@ import { join, parse } from 'path'; import * as fs from 'fs-extra'; import * as os from 'os'; import { getScriptRunnerPath, isTestingWithLatestCodebase } from '..'; -export const RETURN = process.platform === 'win32' ? '\r' : EOL; +const RETURN = process.platform === 'win32' ? '\r' : EOL; const DEFAULT_NO_OUTPUT_TIMEOUT = process.env.AMPLIFY_TEST_TIMEOUT_SEC ? Number.parseInt(process.env.AMPLIFY_TEST_TIMEOUT_SEC, 10) * 1000 : 5 * 60 * 1000; // 5 Minutes diff --git a/packages/amplify-e2e-tests/src/__tests__/apigw.test.ts b/packages/amplify-e2e-tests/src/__tests__/apigw.test.ts deleted file mode 100644 index 38d4dd74e8a..00000000000 --- a/packages/amplify-e2e-tests/src/__tests__/apigw.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - addRestApi, - createNewProjectDir, - initJSProjectWithProfile, - deleteProject, - deleteProjectDir, - getProjectMeta, - amplifyPushAuth, - addAuthWithGroupsAndAdminAPI, -} from 'amplify-e2e-core'; -import { v4 as uuid } from 'uuid'; - -const [shortId] = uuid().split('-'); -const projName = `apigwtest${shortId}`; - -let projRoot: string; -beforeAll(async () => { - projRoot = await createNewProjectDir(projName); - await initJSProjectWithProfile(projRoot, { name: projName }); -}); - -afterAll(async () => { - await deleteProject(projRoot); - deleteProjectDir(projRoot); -}); - -describe('API Gateway e2e tests', () => { - it('adds multiple rest apis and pushes', async () => { - await addRestApi(projRoot, {}); - await amplifyPushAuth(projRoot); - await addAuthWithGroupsAndAdminAPI(projRoot); // Groups: Admins, Users - await amplifyPushAuth(projRoot); - await addRestApi(projRoot, { isFirstRestApi: false, path: '/foo' }); - await addRestApi(projRoot, { isFirstRestApi: false, restrictAccess: true, allowGuestUsers: true, hasUserPoolGroups: true }); - await amplifyPushAuth(projRoot); // Pushes multiple rest api updates - const projMeta = getProjectMeta(projRoot); - expect(projMeta).toBeDefined(); - expect(projMeta.api).toBeDefined(); - }); -}); diff --git a/packages/amplify-e2e-tests/src/__tests__/auth_5.test.ts b/packages/amplify-e2e-tests/src/__tests__/auth_5.test.ts index 96a3d91a4d6..26a208c642f 100644 --- a/packages/amplify-e2e-tests/src/__tests__/auth_5.test.ts +++ b/packages/amplify-e2e-tests/src/__tests__/auth_5.test.ts @@ -190,7 +190,7 @@ describe('headless auth', () => { await initJSProjectWithProfile(projRoot, defaultsSettings); await addAuthWithDefault(projRoot, {}); - await updateHeadlessAuth(projRoot, updateAuthRequest, {}); + await updateHeadlessAuth(projRoot, updateAuthRequest); await amplifyPushAuth(projRoot); const meta = getProjectMeta(projRoot); const id = Object.keys(meta.auth).map(key => meta.auth[key])[0].output.UserPoolId; diff --git a/packages/amplify-e2e-tests/tsconfig.json b/packages/amplify-e2e-tests/tsconfig.json index 7a63b4cb0c6..0d0ec689241 100644 --- a/packages/amplify-e2e-tests/tsconfig.json +++ b/packages/amplify-e2e-tests/tsconfig.json @@ -13,7 +13,7 @@ "typeRoots": ["../../node_modules/@types", "node_modules/@types", "./typings"] }, "references": [ - { "path": "../amplify-e2e-core" } + {"path": "../amplify-e2e-core"}, ], "exclude": ["node_modules", "lib", "__tests__", "custom-resources", "overrides"] -} +} \ No newline at end of file diff --git a/packages/amplify-e2e-tests/tsconfig.tests.json b/packages/amplify-e2e-tests/tsconfig.tests.json index 6807354862a..1cb95fd0b68 100644 --- a/packages/amplify-e2e-tests/tsconfig.tests.json +++ b/packages/amplify-e2e-tests/tsconfig.tests.json @@ -13,5 +13,5 @@ "typeRoots": ["../../node_modules/@types", "node_modules/@types", "./typings"] }, "references": [{ "path": "../amplify-e2e-core" }], - "exclude": ["node_modules", "lib", "__tests__", "custom-resources", "overrides"] + "exclude": ["node_modules", "lib", "__tests__"] }