diff --git a/packages/amplify-category-storage/src/__tests__/commands/add.test.ts b/packages/amplify-category-storage/src/__tests__/commands/add.test.ts new file mode 100644 index 00000000000..131d6a1d400 --- /dev/null +++ b/packages/amplify-category-storage/src/__tests__/commands/add.test.ts @@ -0,0 +1,37 @@ +import { $TSContext, $TSObject } from 'amplify-cli-core'; +import { run } from '../../commands/storage/add'; +import * as providerController from '../../provider-utils/awscloudformation/index'; + +jest.mock('../../provider-utils/awscloudformation/index'); +jest.mock('amplify-cli-core'); + +const providerController_mock = providerController as jest.Mocked; +providerController_mock.addResource.mockImplementation = jest.fn().mockImplementation(async () => { + return 'mockResourceName'; +}); + +describe('add ddb command tests', () => { + const provider = 'awscloudformation'; + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: {}, + } as unknown as $TSContext; + }); + + it('add resource workflow is invoked for DDB', async () => { + const service = 'DynamoDB'; + mockContext.amplify.serviceSelectionPrompt = jest.fn().mockImplementation(async () => { + return { service: service, providerName: provider }; + }); + + await run(mockContext); + + expect(providerController_mock.addResource).toHaveBeenCalledWith(mockContext, 'storage', service, { + service: service, + providerPlugin: provider, + }); + }); +}); diff --git a/packages/amplify-category-storage/src/__tests__/commands/override.test.ts b/packages/amplify-category-storage/src/__tests__/commands/override.test.ts new file mode 100644 index 00000000000..2d8f9799cfe --- /dev/null +++ b/packages/amplify-category-storage/src/__tests__/commands/override.test.ts @@ -0,0 +1,92 @@ +import { $TSContext, $TSObject, stateManager, generateOverrideSkeleton, pathManager } from 'amplify-cli-core'; +import { run } from '../../commands/storage/override'; +import { printer, prompter } from 'amplify-prompts'; +import path from 'path'; +import { DynamoDBInputState } from '../../provider-utils/awscloudformation/service-walkthroughs/dynamoDB-input-state'; + +jest.mock('amplify-cli-core'); +jest.mock('amplify-prompts'); +jest.mock('path'); +jest.mock('../../provider-utils/awscloudformation/service-walkthroughs/dynamoDB-input-state'); +jest.mock('../../provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform'); + +const generateOverrideSkeleton_mock = generateOverrideSkeleton as jest.MockedFunction; +generateOverrideSkeleton_mock.mockImplementation = jest.fn().mockImplementation(async () => { + return 'mockResourceName'; +}); + +describe('override ddb command tests', () => { + let mockContext: $TSContext; + let mockAmplifyMeta: $TSObject = {}; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: {}, + } as unknown as $TSContext; + }); + + it('override ddb when two ddb storage resources present', async () => { + mockAmplifyMeta = { + storage: { + dynamo73399689: { + service: 'DynamoDB', + providerPlugin: 'awscloudformation', + }, + dynamoefb50875: { + service: 'DynamoDB', + providerPlugin: 'awscloudformation', + }, + }, + }; + + const destDir = 'mockDir'; + const srcDir = 'mockSrcDir'; + + stateManager.getMeta = jest.fn().mockReturnValue(mockAmplifyMeta); + pathManager.getResourceDirectoryPath = jest.fn().mockReturnValue(destDir); + path.join = jest.fn().mockReturnValue(srcDir); + + prompter.pick = jest.fn().mockReturnValue('dynamo73399689'); + jest.spyOn(DynamoDBInputState.prototype, 'cliInputFileExists').mockImplementation(() => true); + + await run(mockContext); + + expect(prompter.pick).toBeCalledTimes(1); + expect(generateOverrideSkeleton).toHaveBeenCalledWith(mockContext, srcDir, destDir); + }); + + it('override ddb when one ddb storage resource present', async () => { + mockAmplifyMeta = { + storage: { + dynamo73399689: { + service: 'DynamoDB', + providerPlugin: 'awscloudformation', + }, + }, + }; + + const destDir = 'mockDir'; + const srcDir = 'mockSrcDir'; + + stateManager.getMeta = jest.fn().mockReturnValue(mockAmplifyMeta); + pathManager.getResourceDirectoryPath = jest.fn().mockReturnValue(destDir); + path.join = jest.fn().mockReturnValue(srcDir); + + jest.spyOn(DynamoDBInputState.prototype, 'cliInputFileExists').mockImplementation(() => true); + + await run(mockContext); + + // Prompter should not be called when only one ddb/storage resource present + expect(prompter.pick).toBeCalledTimes(0); + expect(generateOverrideSkeleton).toHaveBeenCalledWith(mockContext, srcDir, destDir); + }); + + it('override ddb when no ddb storage resource present', async () => { + mockAmplifyMeta = {}; + stateManager.getMeta = jest.fn().mockReturnValue(mockAmplifyMeta); + + await run(mockContext); + expect(printer.error).toHaveBeenCalledWith('No resources to override. You need to add a resource.'); + }); +}); diff --git a/packages/amplify-category-storage/src/__tests__/commands/remove.test.ts b/packages/amplify-category-storage/src/__tests__/commands/remove.test.ts new file mode 100644 index 00000000000..d51facdb6c6 --- /dev/null +++ b/packages/amplify-category-storage/src/__tests__/commands/remove.test.ts @@ -0,0 +1,46 @@ +import { $TSContext, $TSObject } from 'amplify-cli-core'; +import { run } from '../../commands/storage/remove'; +import * as providerController from '../../provider-utils/awscloudformation/index'; + +jest.mock('../../provider-utils/awscloudformation/index'); +jest.mock('amplify-cli-core'); + +const providerController_mock = providerController as jest.Mocked; +providerController_mock.updateResource.mockImplementation = jest.fn().mockImplementation(async () => { + return 'mockResourceName'; +}); + +describe('remove ddb command tests', () => { + const provider = 'awscloudformation'; + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: {}, + parameters: {}, + } as unknown as $TSContext; + }); + + it('update resource workflow is invoked for DDB with no params', async () => { + mockContext.amplify.removeResource = jest.fn().mockImplementation(async () => { + return; + }); + + await run(mockContext); + + expect(mockContext.amplify.removeResource).toHaveBeenCalledWith(mockContext, 'storage', undefined); + }); + + it('update resource workflow is invoked for DDB with params as resourceName', async () => { + const mockResourceName = 'mockResourceName'; + mockContext.parameters.first = mockResourceName; + mockContext.amplify.removeResource = jest.fn().mockImplementation(async () => { + return; + }); + + await run(mockContext); + + expect(mockContext.amplify.removeResource).toHaveBeenCalledWith(mockContext, 'storage', mockResourceName); + }); +}); diff --git a/packages/amplify-category-storage/src/__tests__/commands/update.test.ts b/packages/amplify-category-storage/src/__tests__/commands/update.test.ts new file mode 100644 index 00000000000..2bc7152c460 --- /dev/null +++ b/packages/amplify-category-storage/src/__tests__/commands/update.test.ts @@ -0,0 +1,34 @@ +import { $TSContext, $TSObject } from 'amplify-cli-core'; +import { run } from '../../commands/storage/update'; +import * as providerController from '../../provider-utils/awscloudformation/index'; + +jest.mock('../../provider-utils/awscloudformation/index'); +jest.mock('amplify-cli-core'); + +const providerController_mock = providerController as jest.Mocked; +providerController_mock.updateResource.mockImplementation = jest.fn().mockImplementation(async () => { + return 'mockResourceName'; +}); + +describe('update ddb command tests', () => { + const provider = 'awscloudformation'; + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: {}, + } as unknown as $TSContext; + }); + + it('update resource workflow is invoked for DDB', async () => { + const service = 'DynamoDB'; + mockContext.amplify.serviceSelectionPrompt = jest.fn().mockImplementation(async () => { + return { service: service, providerName: provider }; + }); + + await run(mockContext); + + expect(providerController_mock.updateResource).toHaveBeenCalledWith(mockContext, 'storage', service); + }); +}); diff --git a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/__snapshots__/ddb-stack-transform.test.ts.snap b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/__snapshots__/ddb-stack-transform.test.ts.snap new file mode 100644 index 00000000000..c43cfb4473d --- /dev/null +++ b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/__snapshots__/ddb-stack-transform.test.ts.snap @@ -0,0 +1,190 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test DDB transform generates correct CFN template Generated ddb template with all CLI configurations set with no overrides 1`] = ` +Object { + "AWSTemplateFormatVersion": "2010-09-09", + "Conditions": Object { + "ShouldNotCreateEnvResources": Object { + "Fn::Equals": Array [ + Object { + "Ref": "env", + }, + "NONE", + ], + }, + }, + "Description": "DDB Resource for AWS Amplify CLI", + "Outputs": Object { + "Arn": Object { + "Value": Object { + "Fn::GetAtt": Array [ + "DynamoDBTable", + "Arn", + ], + }, + }, + "Name": Object { + "Value": Object { + "Ref": "DynamoDBTable", + }, + }, + "PartitionKeyName": Object { + "Value": Object { + "Ref": "partitionKeyName", + }, + }, + "PartitionKeyType": Object { + "Value": Object { + "Ref": "partitionKeyType", + }, + }, + "Region": Object { + "Value": Object { + "Ref": "AWS::Region", + }, + }, + "SortKeyName": Object { + "Value": Object { + "Ref": "sortKeyName", + }, + }, + "SortKeyType": Object { + "Value": Object { + "Ref": "sortKeyType", + }, + }, + "StreamArn": Object { + "Value": Object { + "Fn::GetAtt": Array [ + "DynamoDBTable", + "StreamArn", + ], + }, + }, + }, + "Parameters": Object { + "env": Object { + "Type": "String", + }, + "partitionKeyName": Object { + "Type": "String", + }, + "partitionKeyType": Object { + "Type": "String", + }, + "sortKeyName": Object { + "Type": "String", + }, + "sortKeyType": Object { + "Type": "String", + }, + "tableName": Object { + "Type": "String", + }, + }, + "Resources": Object { + "DynamoDBTable": Object { + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "id", + "AttributeType": "S", + }, + Object { + "AttributeName": "name", + "AttributeType": "N", + }, + Object { + "AttributeName": "col", + "AttributeType": "S", + }, + ], + "GlobalSecondaryIndexes": Array [ + Object { + "IndexName": "gsiname", + "KeySchema": Array [ + Object { + "AttributeName": "name", + "KeyType": "HASH", + }, + ], + "Projection": Object { + "ProjectionType": "ALL", + }, + "ProvisionedThroughput": Object { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, + }, + Object { + "IndexName": "updategsiname", + "KeySchema": Array [ + Object { + "AttributeName": "col", + "KeyType": "HASH", + }, + ], + "Projection": Object { + "ProjectionType": "ALL", + }, + "ProvisionedThroughput": Object { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, + }, + ], + "KeySchema": Array [ + Object { + "AttributeName": "id", + "KeyType": "HASH", + }, + Object { + "AttributeName": "name", + "KeyType": "RANGE", + }, + ], + "ProvisionedThroughput": Object { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, + "StreamSpecification": Object { + "StreamViewType": "NEW_IMAGE", + }, + "TableName": Object { + "Fn::If": Array [ + "ShouldNotCreateEnvResources", + Object { + "Ref": "tableName", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Ref": "tableName", + }, + "-", + Object { + "Ref": "env", + }, + ], + ], + }, + ], + }, + }, + "Type": "AWS::DynamoDB::Table", + }, + }, +} +`; + +exports[`Test DDB transform generates correct CFN template Generated ddb template with all CLI configurations set with no overrides 2`] = ` +Object { + "partitionKeyName": "id", + "partitionKeyType": "string", + "sortKeyName": "name", + "sortKeyType": "number", + "tableName": "mocktablename", +} +`; diff --git a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.test.ts b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.test.ts new file mode 100644 index 00000000000..2ed25da6c73 --- /dev/null +++ b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.test.ts @@ -0,0 +1,84 @@ +/* These tests, test the tranform and in rurn the cdk builder too which is used within this file */ + +import { JSONUtilities, buildOverrideDir, pathManager } from 'amplify-cli-core'; +import * as fs from 'fs-extra'; +import { DDBStackTransform } from '../../../../provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform'; +import { + DynamoDBCLIInputs, + FieldType, +} from '../../../../provider-utils/awscloudformation/service-walkthrough-types/dynamoDB-user-input-types'; +import { DynamoDBInputState } from '../../../../provider-utils/awscloudformation/service-walkthroughs/dynamoDB-input-state'; +import path from 'path'; + +jest.mock('amplify-cli-core', () => ({ + buildOverrideDir: jest.fn().mockResolvedValue(false), + JSONUtilities: { + writeJson: jest.fn(), + readJson: jest.fn(), + }, + pathManager: { + getBackendDirPath: jest.fn().mockReturnValue('mockbackendpath'), + getResourceDirectoryPath: jest.fn().mockReturnValue('mockresourcepath'), + }, +})); +jest.mock('fs-extra', () => ({ + readFileSync: () => '{ "Cognito": { "provider": "aws"}}', + existsSync: () => true, + ensureDirSync: jest.fn(), +})); + +jest.mock('path', () => ({ + join: jest.fn().mockReturnValue('mockjoinedpath'), + resolve: jest.fn().mockReturnValue('mockjoinedpath'), +})); + +jest.mock('../../../../provider-utils/awscloudformation/service-walkthroughs/dynamoDB-input-state'); + +describe('Test DDB transform generates correct CFN template', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Generated ddb template with all CLI configurations set with no overrides', async () => { + const resourceName = 'mockResource'; + const cliInputsJSON: DynamoDBCLIInputs = { + resourceName: resourceName, + tableName: 'mocktablename', + partitionKey: { + fieldName: 'id', + fieldType: FieldType.string, + }, + sortKey: { + fieldName: 'name', + fieldType: FieldType.number, + }, + gsi: [ + { + name: 'gsiname', + partitionKey: { + fieldName: 'name', + fieldType: FieldType.number, + }, + }, + { + name: 'updategsiname', + partitionKey: { + fieldName: 'col', + fieldType: FieldType.string, + }, + }, + ], + triggerFunctions: [], + }; + + jest.spyOn(DynamoDBInputState.prototype, 'getCliInputPayload').mockImplementation(() => cliInputsJSON); + const ddbTransform = new DDBStackTransform(resourceName); + await ddbTransform.transform(); + + console.log(ddbTransform._cfn); + console.log(ddbTransform._cfnInputParams); + + expect(ddbTransform._cfn).toMatchSnapshot(); + expect(ddbTransform._cfnInputParams).toMatchSnapshot(); + }); +}); diff --git a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.test.ts b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.test.ts new file mode 100644 index 00000000000..d6af9e7020c --- /dev/null +++ b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.test.ts @@ -0,0 +1,239 @@ +import { $TSContext, stateManager } from 'amplify-cli-core'; +import { prompter } from 'amplify-prompts'; +import { DynamoDBInputState } from '../../../../provider-utils/awscloudformation/service-walkthroughs/dynamoDB-input-state'; +import { DDBStackTransform } from '../../../../provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform'; +import { addWalkthrough, updateWalkthrough } from '../../../../provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough'; +import { + DynamoDBCLIInputs, + FieldType, +} from '../../../../provider-utils/awscloudformation/service-walkthrough-types/dynamoDB-user-input-types'; + +jest.mock('amplify-cli-core'); +jest.mock('amplify-prompts'); +jest.mock('../../../../provider-utils/awscloudformation/service-walkthroughs/dynamoDB-input-state'); +jest.mock('../../../../provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform'); + +describe('add ddb walkthrough tests', () => { + let mockContext: $TSContext; + + beforeEach(() => { + mockContext = { + amplify: { + getProjectDetails: () => { + return { + projectConfig: { + projectName: 'mockProject', + }, + }; + }, + }, + } as unknown as $TSContext; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('addWalkthrough() test', async () => { + jest.spyOn(DynamoDBInputState.prototype, 'saveCliInputPayload').mockImplementation(() => true); + jest.spyOn(DDBStackTransform.prototype, 'transform').mockImplementation(() => Promise.resolve()); + + const expectedCLIInputsJSON: DynamoDBCLIInputs = { + resourceName: 'mockresourcename', + tableName: 'mocktablename', + partitionKey: { + fieldName: 'id', + fieldType: FieldType.string, + }, + sortKey: { + fieldName: 'name', + fieldType: FieldType.number, + }, + gsi: [ + { + name: 'gsiname', + partitionKey: { + fieldName: 'name', + fieldType: FieldType.number, + }, + }, + { + name: 'secondgsiname', + partitionKey: { + fieldName: 'id', + fieldType: FieldType.string, + }, + sortKey: { + fieldName: 'name', + fieldType: FieldType.number, + }, + }, + ], + triggerFunctions: [], + }; + + prompter.input = jest + .fn() + .mockReturnValueOnce('mockresourcename') // Provide a friendly name + .mockResolvedValueOnce('mocktablename') // Provide table name + .mockResolvedValueOnce('id') // What would you like to name this column + .mockResolvedValueOnce('name') // What would you like to name this column + .mockResolvedValueOnce('gsiname') // Provide the GSI name + .mockResolvedValueOnce('secondgsiname'); // Provide the GSI name + + prompter.pick = jest + .fn() + .mockReturnValueOnce('string') // Choose the data type + .mockReturnValueOnce('number') // Choose the data type + .mockReturnValueOnce('id') // Choose partition key for the table + .mockReturnValueOnce('name') // Choose sort key for the table + .mockReturnValueOnce('name') // Choose partition key for the GSI + .mockReturnValueOnce('id') // Choose partition key for the GSI + .mockReturnValueOnce('name'); // Choose sort key for the GSI + + prompter.yesOrNo = jest + .fn() + .mockReturnValueOnce(true) // Would you like to add another column + .mockReturnValueOnce(false) // Would you like to add another column + .mockReturnValueOnce(true) // Do you want to add a sort key to your table? + .mockReturnValueOnce(true) // Do you want to add global secondary indexes to your table? + .mockReturnValueOnce(false) // Do you want to add a sort key to your global secondary index? + .mockReturnValueOnce(true) // Do you want to add more global secondary indexes to your table? + .mockReturnValueOnce(true) // Do you want to add a sort key to your global secondary index? + .mockReturnValueOnce(false); // Do you want to add more global secondary indexes to your table? + + prompter.confirmContinue = jest.fn().mockReturnValueOnce(false); // Do you want to add a Lambda Trigger for your Table? + + const returnedResourcename = await addWalkthrough(mockContext, 'dynamoDb-defaults'); + + expect(returnedResourcename).toEqual('mockresourcename'); + expect(DynamoDBInputState.prototype.saveCliInputPayload).toHaveBeenCalledWith(expectedCLIInputsJSON); + }); +}); + +describe('update ddb walkthrough tests', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.mock('amplify-prompts'); + mockContext = { + amplify: { + getProjectDetails: () => { + return { + projectConfig: { + projectName: 'mockProject', + }, + }; + }, + }, + } as unknown as $TSContext; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('updateWalkthrough() test to add gsi', async () => { + let mockAmplifyMeta = { + storage: { + mockresourcename: { + service: 'DynamoDB', + providerPlugin: 'awscloudformation', + }, + dynamoefb50875: { + service: 'DynamoDB', + providerPlugin: 'awscloudformation', + }, + }, + }; + + stateManager.getMeta = jest.fn().mockReturnValue(mockAmplifyMeta); + + const currentCLIInputsJSON: DynamoDBCLIInputs = { + resourceName: 'mockresourcename', + tableName: 'mocktablename', + partitionKey: { + fieldName: 'id', + fieldType: FieldType.string, + }, + sortKey: { + fieldName: 'name', + fieldType: FieldType.number, + }, + gsi: [ + { + name: 'gsiname', + partitionKey: { + fieldName: 'name', + fieldType: FieldType.number, + }, + }, + ], + triggerFunctions: [], + }; + + jest.spyOn(DynamoDBInputState.prototype, 'getCliInputPayload').mockImplementation(() => currentCLIInputsJSON); + + jest.spyOn(DynamoDBInputState.prototype, 'saveCliInputPayload').mockImplementation(() => true); + jest.spyOn(DynamoDBInputState.prototype, 'cliInputFileExists').mockImplementation(() => true); + jest.spyOn(DDBStackTransform.prototype, 'transform').mockImplementation(() => Promise.resolve()); + + prompter.input = jest + .fn() + .mockResolvedValueOnce('col') // What would you like to name this column + .mockResolvedValueOnce('updategsiname'); // Provide the GSI name + + prompter.pick = jest + .fn() + .mockReturnValueOnce('mockresourcename') // Specify the resource that you would want to update + .mockReturnValueOnce('string') // Choose the data type + .mockReturnValueOnce('col') // Choose partition key for the GSI + .mockReturnValueOnce('name'); // Choose sort key for the GSI + + prompter.yesOrNo = jest + .fn() + .mockReturnValueOnce(true) // Would you like to add another column + .mockReturnValueOnce(false) // Would you like to add another column + .mockReturnValueOnce(true) // Do you want to keep existing global seconday indexes created on your table? + .mockReturnValueOnce(true) // Do you want to add global secondary indexes to your table? + .mockReturnValueOnce(false) // Do you want to add a sort key to your global secondary index + .mockReturnValueOnce(false); // Do you want to add more global secondary indexes to your table? + + prompter.confirmContinue = jest.fn().mockReturnValueOnce(false); // Do you want to add a Lambda Trigger for your Table? + + const returnedCLIInputs = await updateWalkthrough(mockContext); + + const expectedCLIInputsJSON: DynamoDBCLIInputs = { + resourceName: 'mockresourcename', + tableName: 'mocktablename', + partitionKey: { + fieldName: 'id', + fieldType: FieldType.string, + }, + sortKey: { + fieldName: 'name', + fieldType: FieldType.number, + }, + gsi: [ + { + name: 'gsiname', + partitionKey: { + fieldName: 'name', + fieldType: FieldType.number, + }, + }, + { + name: 'updategsiname', + partitionKey: { + fieldName: 'col', + fieldType: FieldType.string, + }, + }, + ], + triggerFunctions: [], + }; + + expect(returnedCLIInputs).toEqual(expectedCLIInputsJSON); + expect(DynamoDBInputState.prototype.saveCliInputPayload).toHaveBeenCalledWith(expectedCLIInputsJSON); + }); +}); diff --git a/packages/amplify-category-storage/src/commands/storage/override.ts b/packages/amplify-category-storage/src/commands/storage/override.ts index 4b56910d6a1..02371347487 100644 --- a/packages/amplify-category-storage/src/commands/storage/override.ts +++ b/packages/amplify-category-storage/src/commands/storage/override.ts @@ -2,10 +2,8 @@ entry code for amplify override root */ -import { generateOverrideSkeleton, $TSContext, FeatureFlags, stateManager, pathManager } from 'amplify-cli-core'; -import { printer } from 'amplify-prompts'; -import * as fs from 'fs-extra'; -import inquirer from 'inquirer'; +import { generateOverrideSkeleton, $TSContext, stateManager, pathManager } from 'amplify-cli-core'; +import { printer, prompter } from 'amplify-prompts'; import { DynamoDBInputState } from '../../provider-utils/awscloudformation/service-walkthroughs/dynamoDB-input-state'; import { DDBStackTransform } from '../../provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform'; import * as path from 'path'; @@ -14,70 +12,55 @@ import { categoryName } from '../../constants'; export const name = 'override'; export const run = async (context: $TSContext) => { - if (FeatureFlags.getBoolean('overrides.project')) { - const { amplify } = context; - const amplifyMeta = stateManager.getMeta(); + const amplifyMeta = stateManager.getMeta(); - const storageResources: string[] = []; + const storageResources: string[] = []; + if (amplifyMeta[categoryName]) { Object.keys(amplifyMeta[categoryName]).forEach(resourceName => { storageResources.push(resourceName); }); + } - if (storageResources.length === 0) { - const errMessage = 'No resources to override. You need to add a resource.'; - printer.error(errMessage); - return; - } + if (storageResources.length === 0) { + const errMessage = 'No resources to override. You need to add a resource.'; + printer.error(errMessage); + return; + } - let selectedResourceName: string = storageResources[0]; + let selectedResourceName: string = storageResources[0]; - if (storageResources.length > 1) { - const resourceAnswer = await inquirer.prompt({ - type: 'list', - name: 'resource', - message: 'Which resource would you like to add overrides for?', - choices: storageResources, - }); - selectedResourceName = resourceAnswer.resource; - } + if (storageResources.length > 1) { + selectedResourceName = await prompter.pick('Which resource would you like to add overrides for?', storageResources); + } - const destPath = pathManager.getResourceDirectoryPath(undefined, categoryName, selectedResourceName); - fs.ensureDirSync(destPath); + const destPath = pathManager.getResourceDirectoryPath(undefined, categoryName, selectedResourceName); - const srcPath = path.join( - __dirname, - '..', - '..', - '..', - 'resources', - 'overrides-resource', - amplifyMeta[categoryName][selectedResourceName].service, - ); + const srcPath = path.join( + __dirname, + '..', + '..', + '..', + 'resources', + 'overrides-resource', + amplifyMeta[categoryName][selectedResourceName].service, + ); - // Make sure to migrate first - if (amplifyMeta[categoryName][selectedResourceName].service === 'DynamoDB') { - const resourceInputState = new DynamoDBInputState(selectedResourceName); - if (!resourceInputState.cliInputFileExists()) { - if (await amplify.confirmPrompt('File migration required to continue. Do you want to continue?', true)) { - resourceInputState.migrate(); - const stackGenerator = new DDBStackTransform(selectedResourceName); - stackGenerator.transform(); - } else { - return; - } + // Make sure to migrate first + if (amplifyMeta[categoryName][selectedResourceName].service === 'DynamoDB') { + const resourceInputState = new DynamoDBInputState(selectedResourceName); + if (!resourceInputState.cliInputFileExists()) { + if (await prompter.yesOrNo('File migration required to continue. Do you want to continue?', true)) { + resourceInputState.migrate(); + const stackGenerator = new DDBStackTransform(selectedResourceName); + stackGenerator.transform(); + } else { + return; } - } else if (amplifyMeta[categoryName][selectedResourceName].service === 'S3') { - // S3 migration logic goes in here } - - await generateOverrideSkeleton(context, srcPath, destPath); - } else { - printer.info('Storage overrides is currently not turned on. In amplify/cli.json file please include the following:'); - printer.info(`{ - override: { - storage: true - } - }`); + } else if (amplifyMeta[categoryName][selectedResourceName].service === 'S3') { + // S3 migration logic goes in here } + + await generateOverrideSkeleton(context, srcPath, destPath); }; diff --git a/packages/amplify-category-storage/src/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.ts b/packages/amplify-category-storage/src/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.ts index 2934af82a24..80befacebd8 100644 --- a/packages/amplify-category-storage/src/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.ts +++ b/packages/amplify-category-storage/src/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import uuid from 'uuid'; import { alphanumeric, printer, prompter, Validator } from 'amplify-prompts'; -import { $TSContext, AmplifyCategories, ResourceDoesNotExistError, exitOnNextTick } from 'amplify-cli-core'; +import { $TSContext, AmplifyCategories, ResourceDoesNotExistError, exitOnNextTick, stateManager } from 'amplify-cli-core'; import { DynamoDBInputState } from './dynamoDB-input-state'; import { DynamoDBAttributeDefType, @@ -11,19 +11,20 @@ import { DynamoDBCLIInputsKeyType, } from '../service-walkthrough-types/dynamoDB-user-input-types'; import { DDBStackTransform } from '../cdk-stack-builder/ddb-stack-transform'; +import { ConfigSnapshotDeliveryProperties } from 'cloudform-types/types/config/deliveryChannel'; // keep in sync with ServiceName in amplify-AmplifyCategories.STORAGE-function, but probably it will not change const FunctionServiceNameLambdaFunction = 'Lambda'; const serviceName = 'DynamoDB'; -async function addWalkthrough(context: $TSContext, defaultValuesFilename: string) { +export async function addWalkthrough(context: $TSContext, defaultValuesFilename: string) { printer.blankLine(); printer.info('Welcome to the NoSQL DynamoDB database wizard'); printer.info('This wizard asks you a series of questions to help determine how to set up your NoSQL database table.'); printer.blankLine(); const defaultValuesSrc = path.join(__dirname, '..', 'default-values', defaultValuesFilename); - const { getAllDefaults } = require(defaultValuesSrc); + const { getAllDefaults } = await import(defaultValuesSrc); const { amplify } = context; const defaultValues = getAllDefaults(amplify.getProjectDetails()); @@ -55,11 +56,8 @@ async function addWalkthrough(context: $TSContext, defaultValuesFilename: string return cliInputs.resourceName; } -async function updateWalkthrough(context: $TSContext) { - // const resourceName = resourceAlreadyExists(context); - const { amplify } = context; - const { amplifyMeta } = amplify.getProjectDetails(); - +export async function updateWalkthrough(context: $TSContext) { + const amplifyMeta = stateManager.getMeta(); const dynamoDbResources: any = {}; Object.keys(amplifyMeta[AmplifyCategories.STORAGE]).forEach(resourceName => { @@ -232,7 +230,7 @@ async function askGSIQuestion( gsiList = existingGSIList; } - if (await prompter.confirmContinue('Do you want to add global secondary indexes to your table?')) { + if (await prompter.yesOrNo('Do you want to add global secondary indexes to your table?', true)) { let continuewithGSIQuestions = true; while (continuewithGSIQuestions) { @@ -264,12 +262,11 @@ async function askGSIQuestion( const sortKeyOptions = indexableAttributeList.filter((att: string) => att !== gsiPartitionKeyName); if (sortKeyOptions.length > 0) { - if (await prompter.confirmContinue('Do you want to add a sort key to your global secondary index?')) { + if (await prompter.yesOrNo('Do you want to add a sort key to your global secondary index?', true)) { const gsiSortKeyName = await prompter.pick('Choose sort key for the GSI', [...new Set(sortKeyOptions)]); const gsiSortKeyIndex = attributeDefinitions.findIndex( (attr: DynamoDBAttributeDefType) => attr.AttributeName === gsiSortKeyName, ); - gsiItem.sortKey = { fieldName: gsiSortKeyName, fieldType: attributeDefinitions[gsiSortKeyIndex].AttributeType, @@ -278,7 +275,7 @@ async function askGSIQuestion( } gsiList.push(gsiItem); - continuewithGSIQuestions = await prompter.confirmContinue('Do you want to add more global secondary indexes to your table?'); + continuewithGSIQuestions = await prompter.yesOrNo('Do you want to add more global secondary indexes to your table?', true); } else { printer.error('You do not have any other attributes remaining to configure'); break; @@ -293,7 +290,7 @@ async function askSortKeyQuestion( attributeDefinitions: DynamoDBAttributeDefType[], partitionKeyFieldName: string, ): Promise { - if (await prompter.confirmContinue('Do you want to add a sort key to your table?')) { + if (await prompter.yesOrNo('Do you want to add a sort key to your table?', true)) { // Ask for sort key if (attributeDefinitions.length > 1) { const sortKeyName = await prompter.pick( @@ -369,7 +366,7 @@ async function askAttributeListQuestion(existingAttributeDefinitions?: DynamoDBC if (existingAttributes.length > 0) { attributeAnswers = existingAttributes; indexableAttributeList = attributeAnswers.map((attr: DynamoDBAttributeDefType) => attr.AttributeName); - continueAttributeQuestion = await prompter.confirmContinue('Would you like to add another column?'); + continueAttributeQuestion = await prompter.yesOrNo('Would you like to add another column?', true); } while (continueAttributeQuestion) { @@ -400,7 +397,7 @@ async function askAttributeListQuestion(existingAttributeDefinitions?: DynamoDBC indexableAttributeList.push(attributeName); } - continueAttributeQuestion = await prompter.confirmContinue('Would you like to add another column?'); + continueAttributeQuestion = await prompter.yesOrNo('Would you like to add another column?', true); } return { attributeAnswers, indexableAttributeList }; @@ -412,7 +409,7 @@ async function askTableNameQuestion(defaultValues: any, resourceName: string) { (input: string) => /^[a-zA-Z0-9._-]+$/.test(input) ? true : message; - const tableName = await prompter.input('Provide table name:', { + const tableName = await prompter.input('Provide table name', { validate: tableNameValidator('You can use the following characters: a-z A-Z 0-9 . - _'), initial: resourceName || defaultValues['tableName'], }); @@ -421,13 +418,10 @@ async function askTableNameQuestion(defaultValues: any, resourceName: string) { } async function askResourceNameQuestion(defaultValues: any): Promise { - const resourceName = await prompter.input( - 'Provide a friendly name for your resource that will be used to label this category in the project', - { - validate: alphanumeric(), - initial: defaultValues['resourceName'], - }, - ); + const resourceName = await prompter.input('Provide a friendly name', { + validate: alphanumeric(), + initial: defaultValues['resourceName'], + }); return resourceName; } diff --git a/packages/amplify-e2e-core/src/categories/storage.ts b/packages/amplify-e2e-core/src/categories/storage.ts index a5f111caf4b..6adad6021d2 100644 --- a/packages/amplify-e2e-core/src/categories/storage.ts +++ b/packages/amplify-e2e-core/src/categories/storage.ts @@ -17,17 +17,23 @@ export function addSimpleDDB(cwd: string, settings: any): Promise { spawn(getCLIPath(), ['add', 'storage'], { cwd, stripColors: true }) .wait('Please select from one of the below mentioned services') .sendLine(KEY_DOWN_ARROW) - .wait('Please provide a friendly name for your resource') - .sendLine(settings.name || '') - .wait('Please provide table name') + .wait('Provide a friendly name') + .sendLine(settings.name || '\r') + .wait('Provide table name') .sendCarriageReturn() .wait('What would you like to name this column') .sendLine('id') - .wait('Please choose the data type') + .wait('Choose the data type') + .sendCarriageReturn() + .wait('Would you like to add another column') + .sendLine('y') + .wait('What would you like to name this column') + .sendLine('col2') + .wait('Choose the data type') .sendCarriageReturn() .wait('Would you like to add another column') .sendLine('n') - .wait('Please choose partition key for the table') + .wait('Choose partition key for the table') .sendCarriageReturn() .wait('Do you want to add a sort key to your table') .sendLine('n') @@ -51,7 +57,7 @@ export function addDDBWithTrigger(cwd: string, settings: { ddbResourceName?: str const chain = spawn(getCLIPath(), ['add', 'storage'], { cwd, stripColors: true }) .wait('Please select from one of the below mentioned services') .sendLine(KEY_DOWN_ARROW) - .wait('Please provide a friendly name for your resource'); + .wait('Provide a friendly name'); if (settings.ddbResourceName) { chain.sendLine(settings.ddbResourceName); } else { @@ -59,15 +65,21 @@ export function addDDBWithTrigger(cwd: string, settings: { ddbResourceName?: str } chain .sendCarriageReturn() - .wait('Please provide table name') + .wait('Provide table name') .sendCarriageReturn() .wait('What would you like to name this column') .sendLine('id') - .wait('Please choose the data type') + .wait('Choose the data type') + .sendCarriageReturn() + .wait('Would you like to add another column') + .sendLine('y') + .wait('What would you like to name this column') + .sendLine('col2') + .wait('Choose the data type') .sendCarriageReturn() .wait('Would you like to add another column') .sendLine('n') - .wait('Please choose partition key for the table') + .wait('Choose partition key for the table') .sendCarriageReturn() .wait('Do you want to add a sort key to your table') .sendLine('n') @@ -107,8 +119,6 @@ export function updateDDBWithTrigger(cwd: string, settings: any): Promise .sendLine(KEY_DOWN_ARROW) .wait('Do you want to edit the local') .sendLine('n') - .wait('overwrite') - .sendLine('y') .sendEof() .run((err: Error) => { if (!err) { @@ -132,15 +142,17 @@ export function updateSimpleDDBwithGSI(cwd: string, settings: any): Promise { if (!err) { @@ -171,31 +179,31 @@ export function addSimpleDDBwithGSI(cwd: string, settings: any): Promise { .wait('Please select from one of the below mentioned services') .send(KEY_DOWN_ARROW) .sendCarriageReturn() - .wait('Please provide a friendly name for your resource') + .wait('Provide a friendly name') .sendCarriageReturn() - .wait('Please provide table name') + .wait('Provide table name') .sendCarriageReturn() .wait('What would you like to name this column') .sendLine('id') - .wait('Please choose the data type') + .wait('Choose the data type') .sendCarriageReturn() .wait('Would you like to add another column') .sendLine('y') .wait('What would you like to name this column') .sendLine('gsi-col1') - .wait('Please choose the data type') + .wait('Choose the data type') .sendCarriageReturn() .wait('Would you like to add another column') .sendLine('n') - .wait('Please choose partition key for the table') + .wait('Choose partition key for the table') .sendCarriageReturn() .wait('Do you want to add a sort key to your table') .sendLine('n') .wait('Do you want to add global secondary indexes to your table?') .sendLine('y') - .wait('Please provide the GSI name') + .wait('Provide the GSI name') .sendLine('gsi1') - .wait('Please choose partition key for the GSI') + .wait('Choose partition key for the GSI') .send(KEY_DOWN_ARROW) .sendCarriageReturn() .wait('Do you want to add a sort key to your global secondary index?') @@ -215,6 +223,115 @@ export function addSimpleDDBwithGSI(cwd: string, settings: any): Promise { }); } +export function overrideDDB(cwd: string, settings: {}) { + return new Promise((resolve, reject) => { + const args = ['override', 'storage']; + + spawn(getCLIPath(), args, { cwd, stripColors: true }) + .wait('Do you want to edit override.ts file now?') + .sendConfirmNo() + .sendEof() + .run((err: Error) => { + if (!err) { + resolve({}); + } else { + reject(err); + } + }); + }); +} + +export function buildOverrideStorage(cwd: string, settings: {}) { + return new Promise((resolve, reject) => { + // Add 'storage' as a category param once implemented + const args = ['build-override']; + + spawn(getCLIPath(), args, { cwd, stripColors: true }) + .sendEof() + .run((err: Error) => { + if (!err) { + resolve({}); + } else { + reject(err); + } + }); + }); +} + +export function addDynamoDBWithGSIWithSettings(projectDir: string, settings: AddDynamoDBSettings): Promise { + return new Promise((resolve, reject) => { + let chain = spawn(getCLIPath(), ['add', 'storage'], { cwd: projectDir, stripColors: true }); + + singleSelect(chain.wait('Please select from one of the below mentioned services:'), 'NoSQL Database', [ + 'Content (Images, audio, video, etc.)', + 'NoSQL Database', + ]); + + const addColumn = (name, type) => { + chain.wait('What would you like to name this column').sendLine(name); + + singleSelect(chain.wait('Choose the data type'), type, ['string', 'number', 'binary', 'boolean', 'list', 'map', 'null']); + }; + + const addAnotherColumn = () => { + chain.wait('Would you like to add another column').sendConfirmYes(); + }; + + chain.wait('Provide a friendly name').sendLine(settings.resourceName).wait('Provide table name').sendLine(settings.tableName); + + addColumn('pk', 'string'); + addAnotherColumn(); + + addColumn('sk', 'string'); + addAnotherColumn(); + + addColumn('gsi-pk', 'string'); + addAnotherColumn(); + + addColumn('gsi-sk', 'string'); + addAnotherColumn(); + + addColumn('title', 'string'); + addAnotherColumn(); + + addColumn('description', 'string'); + + chain.wait('Would you like to add another column').sendConfirmNo(); + + singleSelect(chain.wait('Choose the data type'), 'pk', ['pk', 'sk', 'gsi-pk', 'gsi-sk', 'title', 'description']); + + chain.wait('Do you want to add a sort key to your table').sendConfirmYes(); + + singleSelect(chain.wait('Choose sort key for the table'), 'sk', ['sk', 'gsi-pk', 'gsi-sk', 'title', 'description']); + + chain + .wait('Do you want to add global secondary indexes to your table?') + .sendConfirmYes() + .wait('Provide the GSI name') + .sendLine(settings.gsiName); + + singleSelect(chain.wait('Choose partition key for the GSI'), 'gsi-pk', ['sk', 'gsi-pk', 'gsi-sk', 'title', 'description']); + + chain.wait('Do you want to add a sort key to your global secondary index').sendConfirmYes(); + + singleSelect(chain.wait('Choose sort key for the GSI'), 'gsi-sk', ['sk', 'gsi-sk', 'title', 'description']); + + chain + .wait('Do you want to add more global secondary indexes to your table') + .sendConfirmNo() + .wait('Do you want to add a Lambda Trigger for your Table') + .sendConfirmNo() + .sendEof() + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + export function addS3(cwd: string, settings: any): Promise { return new Promise((resolve, reject) => { spawn(getCLIPath(), ['add', 'storage'], { cwd, stripColors: true }) @@ -493,81 +610,3 @@ export function addS3StorageWithSettings(projectDir: string, settings: AddStorag }); }); } - -export function addDynamoDBWithGSIWithSettings(projectDir: string, settings: AddDynamoDBSettings): Promise { - return new Promise((resolve, reject) => { - let chain = spawn(getCLIPath(), ['add', 'storage'], { cwd: projectDir, stripColors: true }); - - singleSelect(chain.wait('Please select from one of the below mentioned services:'), 'NoSQL Database', [ - 'Content (Images, audio, video, etc.)', - 'NoSQL Database', - ]); - - const addColumn = (name, type) => { - chain.wait('What would you like to name this column').sendLine(name); - - singleSelect(chain.wait('Please choose the data type:'), type, ['string', 'number', 'binary', 'boolean', 'list', 'map', 'null']); - }; - - const addAnotherColumn = () => { - chain.wait('Would you like to add another column').sendConfirmYes(); - }; - - chain - .wait('Please provide a friendly name for your resource') - .sendLine(settings.resourceName) - .wait('Please provide table name') - .sendLine(settings.tableName); - - addColumn('pk', 'string'); - addAnotherColumn(); - - addColumn('sk', 'string'); - addAnotherColumn(); - - addColumn('gsi-pk', 'string'); - addAnotherColumn(); - - addColumn('gsi-sk', 'string'); - addAnotherColumn(); - - addColumn('title', 'string'); - addAnotherColumn(); - - addColumn('description', 'string'); - - chain.wait('Would you like to add another column').sendConfirmNo(); - - singleSelect(chain.wait('Please choose partition key for the table'), 'pk', ['pk', 'sk', 'gsi-pk', 'gsi-sk', 'title', 'description']); - - chain.wait('Do you want to add a sort key to your table').sendConfirmYes(); - - singleSelect(chain.wait('Please choose sort key for the table'), 'sk', ['sk', 'gsi-pk', 'gsi-sk', 'title', 'description']); - - chain - .wait('Do you want to add global secondary indexes to your table?') - .sendConfirmYes() - .wait('Please provide the GSI name') - .sendLine(settings.gsiName); - - singleSelect(chain.wait('Please choose partition key for the GSI'), 'gsi-pk', ['sk', 'gsi-pk', 'gsi-sk', 'title', 'description']); - - chain.wait('Do you want to add a sort key to your global secondary index').sendConfirmYes(); - - singleSelect(chain.wait('Please choose sort key for the GSI'), 'gsi-sk', ['sk', 'gsi-sk', 'title', 'description']); - - chain - .wait('Do you want to add more global secondary indexes to your table') - .sendConfirmNo() - .wait('Do you want to add a Lambda Trigger for your Table') - .sendConfirmNo() - .sendEof() - .run((err: Error) => { - if (!err) { - resolve(); - } else { - reject(err); - } - }); - }); -} diff --git a/packages/amplify-e2e-tests/Readme.md b/packages/amplify-e2e-tests/Readme.md index 9335c8cb261..77e5daccbbf 100644 --- a/packages/amplify-e2e-tests/Readme.md +++ b/packages/amplify-e2e-tests/Readme.md @@ -19,7 +19,7 @@ You can run a single test while adding a new test by running ```bash cd /packages/amplify-e2e-tests/ -npm run e2e __tests__/init.test.ts +npm run e2e src/__tests__/init.test.ts ``` ## Writing a new integration test diff --git a/packages/amplify-e2e-tests/overrides/override-storage-ddb.ts b/packages/amplify-e2e-tests/overrides/override-storage-ddb.ts new file mode 100644 index 00000000000..d80e8db9bda --- /dev/null +++ b/packages/amplify-e2e-tests/overrides/override-storage-ddb.ts @@ -0,0 +1,6 @@ +export function overrideProps(props: any) { + props.dynamoDBTable.streamSpecification = { + streamViewType: 'NEW_AND_OLD_IMAGES', + }; + return props; +} diff --git a/packages/amplify-e2e-tests/package.json b/packages/amplify-e2e-tests/package.json index cd9917f05c9..a7c8f486e2d 100644 --- a/packages/amplify-e2e-tests/package.json +++ b/packages/amplify-e2e-tests/package.json @@ -44,7 +44,8 @@ "yargs": "^15.1.0" }, "devDependencies": { - "ts-node": "^8.9.0" + "ts-node": "^8.9.0", + "jest": "^27.2.4" }, "jest": { "verbose": false, diff --git a/packages/amplify-e2e-tests/src/__tests__/storage.test.ts b/packages/amplify-e2e-tests/src/__tests__/storage.test.ts index 08d71dcfe06..efd4d818e5b 100644 --- a/packages/amplify-e2e-tests/src/__tests__/storage.test.ts +++ b/packages/amplify-e2e-tests/src/__tests__/storage.test.ts @@ -1,7 +1,10 @@ +import { JSONUtilities } from 'amplify-cli-core'; import { initJSProjectWithProfile, initFlutterProjectWithProfile, deleteProject, amplifyPushAuth } from 'amplify-e2e-core'; import { addAuthWithDefault, addAuthWithGroupsAndAdminAPI } from 'amplify-e2e-core'; import { addSimpleDDB, + overrideDDB, + buildOverrideStorage, addDDBWithTrigger, updateDDBWithTrigger, addSimpleDDBwithGSI, @@ -15,6 +18,7 @@ import { import { createNewProjectDir, deleteProjectDir, getProjectMeta, getDDBTable, checkIfBucketExists } from 'amplify-e2e-core'; import * as fs from 'fs-extra'; import * as path from 'path'; +import uuid from 'uuid'; describe('amplify add/update storage(S3)', () => { let projRoot: string; @@ -128,9 +132,12 @@ describe('amplify add/update storage(DDB)', () => { await amplifyPushAuth(projRoot); const meta = getProjectMeta(projRoot); - const { Name: table1Name, Arn: table1Arn, Region: table1Region, StreamArn: table1StreamArn } = Object.keys(meta.storage).map( - key => meta.storage[key], - )[0].output; + const { + Name: table1Name, + Arn: table1Arn, + Region: table1Region, + StreamArn: table1StreamArn, + } = Object.keys(meta.storage).map(key => meta.storage[key])[0].output; expect(table1Name).toBeDefined(); expect(table1Arn).toBeDefined(); @@ -140,9 +147,12 @@ describe('amplify add/update storage(DDB)', () => { expect(table1Configs.Table.TableArn).toEqual(table1Arn); - const { Name: table2Name, Arn: table2Arn, Region: table2Region, StreamArn: table2StreamArn } = Object.keys(meta.storage).map( - key => meta.storage[key], - )[1].output; + const { + Name: table2Name, + Arn: table2Arn, + Region: table2Region, + StreamArn: table2StreamArn, + } = Object.keys(meta.storage).map(key => meta.storage[key])[1].output; expect(table2Name).toBeDefined(); expect(table2Arn).toBeDefined(); @@ -152,3 +162,58 @@ describe('amplify add/update storage(DDB)', () => { expect(table2Configs.Table.TableArn).toEqual(table2Arn); }); }); + +describe('ddb override tests', () => { + let projRoot: string; + beforeEach(async () => { + projRoot = await createNewProjectDir('ddb-overrides'); + }); + + afterEach(async () => { + await deleteProject(projRoot); + deleteProjectDir(projRoot); + }); + + it('override DDB StreamSpecification property', async () => { + const resourceName = `dynamo${uuid.v4().split('-')[0]}`; + await initJSProjectWithProfile(projRoot, {}); + await addSimpleDDB(projRoot, { name: resourceName }); + await overrideDDB(projRoot, {}); + + const srcOverrideFilePath = path.join(__dirname, '..', '..', 'overrides', 'override-storage-ddb.ts'); + const destOverrideFilePath = path.join(projRoot, 'amplify', 'backend', 'storage', resourceName, 'override.ts'); + const cfnFilePath = path.join(projRoot, 'amplify', 'backend', 'storage', resourceName, 'build', 'cloudformation-template.json'); + + fs.copyFileSync(srcOverrideFilePath, destOverrideFilePath); + + await buildOverrideStorage(projRoot, {}); + + let ddbCFNFileJSON: any = JSONUtilities.readJson(cfnFilePath); + + // check if overrides are applied to the cfn file + expect(ddbCFNFileJSON?.Resources?.DynamoDBTable?.Properties?.StreamSpecification?.StreamViewType).toEqual('NEW_AND_OLD_IMAGES'); + + await updateDDBWithTrigger(projRoot, {}); + + // check if override persists after an update + ddbCFNFileJSON = JSONUtilities.readJson(cfnFilePath); + expect(ddbCFNFileJSON?.Resources?.DynamoDBTable?.Properties?.StreamSpecification?.StreamViewType).toEqual('NEW_AND_OLD_IMAGES'); + + await amplifyPushAuth(projRoot); + + const meta = getProjectMeta(projRoot); + const { + Name: table1Name, + Arn: table1Arn, + Region: table1Region, + StreamArn: table1StreamArn, + } = Object.keys(meta.storage).map(key => meta.storage[key])[0].output; + + expect(table1Name).toBeDefined(); + expect(table1Arn).toBeDefined(); + expect(table1Region).toBeDefined(); + expect(table1StreamArn).toBeDefined(); + const table1Configs = await getDDBTable(table1Name, table1Region); + expect(table1Configs.Table.TableArn).toEqual(table1Arn); + }); +});