From 40969cbcf607374021fe980eb77f951b8ecb00ed Mon Sep 17 00:00:00 2001 From: Piotr Srebniak Date: Tue, 12 Dec 2023 13:47:35 +0100 Subject: [PATCH 1/3] [60]: add SalesforceDataTransferApi option to source --- src/salesforce/destination.ts | 16 ++-------------- src/salesforce/index.ts | 3 ++- src/salesforce/source.ts | 10 +++++++++- src/salesforce/util.ts | 13 +++++++++++++ 4 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 src/salesforce/util.ts diff --git a/src/salesforce/destination.ts b/src/salesforce/destination.ts index 0e9963df..55e93cd4 100644 --- a/src/salesforce/destination.ts +++ b/src/salesforce/destination.ts @@ -12,20 +12,8 @@ import { ErrorHandlingConfiguration } from '../core/error-handling'; import { IFlow } from '../core/flows'; import { IDestination } from '../core/vertices/destination'; import { WriteOperation } from '../core/write-operation'; +import { SalesforceDataTransferApi } from './util'; -/** - * The default. Amazon AppFlow selects which API to use based on the number of records that your flow transfers to Salesforce. If your flow transfers fewer than 1,000 records, Amazon AppFlow uses Salesforce REST API. If your flow transfers 1,000 records or more, Amazon AppFlow uses Salesforce Bulk API 2.0. - * - * Each of these Salesforce APIs structures data differently. If Amazon AppFlow selects the API automatically, be aware that, for recurring flows, the data output might vary from one flow run to the next. For example, if a flow runs daily, it might use REST API on one day to transfer 900 records, and it might use Bulk API 2.0 on the next day to transfer 1,100 records. For each of these flow runs, the respective Salesforce API formats the data differently. Some of the differences include how dates are formatted and null values are represented. Also, Bulk API 2.0 doesn't transfer Salesforce compound fields. - * - * By choosing this option, you optimize flow performance for both small and large data transfers, but the tradeoff is inconsistent formatting in the output. - */ -export enum SalesforceDataTransferApi { - - AUTOMATIC = 'AUTOMATIC', - BULKV2 = 'BULKV2', - REST_SYNC = 'REST_SYNC' -} export interface SalesforceDestinationProps { @@ -90,4 +78,4 @@ export class SalesforceDestination implements IDestination { } } -} \ No newline at end of file +} diff --git a/src/salesforce/index.ts b/src/salesforce/index.ts index 728c66d2..59169792 100644 --- a/src/salesforce/index.ts +++ b/src/salesforce/index.ts @@ -5,4 +5,5 @@ SPDX-License-Identifier: Apache-2.0 export * from './type'; export * from './profile'; export * from './source'; -export * from './destination'; \ No newline at end of file +export * from './destination'; +export * from './util'; diff --git a/src/salesforce/source.ts b/src/salesforce/source.ts index b5b2ad61..3e053d06 100644 --- a/src/salesforce/source.ts +++ b/src/salesforce/source.ts @@ -9,9 +9,16 @@ import { SalesforceConnectorType } from './type'; import { ConnectorType } from '../core/connectors/connector-type'; import { IFlow } from '../core/flows'; import { ISource } from '../core/vertices/source'; +import { SalesforceDataTransferApi } from './util'; export interface SalesforceSourceProps { readonly profile: SalesforceConnectorProfile; + + /** + * Specifies which Salesforce API is used by Amazon AppFlow when your flow transfers data from Salesforce. + */ + readonly dataTransferApi?: SalesforceDataTransferApi; + readonly object: string; readonly apiVersion?: string; readonly enableDynamicFieldUpdate?: boolean; @@ -39,6 +46,7 @@ export class SalesforceSource implements ISource { private buildSourceConnectorProperties(): CfnFlow.SourceConnectorPropertiesProperty { return { salesforce: { + dataTransferApi: this.props.dataTransferApi, enableDynamicFieldUpdate: this.props.enableDynamicFieldUpdate, includeDeletedRecords: this.props.includeDeletedRecords, object: this.props.object, @@ -51,4 +59,4 @@ export class SalesforceSource implements ISource { scope.node.addDependency(resource); } } -} \ No newline at end of file +} diff --git a/src/salesforce/util.ts b/src/salesforce/util.ts new file mode 100644 index 00000000..2b871fe5 --- /dev/null +++ b/src/salesforce/util.ts @@ -0,0 +1,13 @@ +/** + * The default. Amazon AppFlow selects which API to use based on the number of records that your flow transfers to Salesforce. If your flow transfers fewer than 1,000 records, Amazon AppFlow uses Salesforce REST API. If your flow transfers 1,000 records or more, Amazon AppFlow uses Salesforce Bulk API 2.0. + * + * Each of these Salesforce APIs structures data differently. If Amazon AppFlow selects the API automatically, be aware that, for recurring flows, the data output might vary from one flow run to the next. For example, if a flow runs daily, it might use REST API on one day to transfer 900 records, and it might use Bulk API 2.0 on the next day to transfer 1,100 records. For each of these flow runs, the respective Salesforce API formats the data differently. Some of the differences include how dates are formatted and null values are represented. Also, Bulk API 2.0 doesn't transfer Salesforce compound fields. + * + * By choosing this option, you optimize flow performance for both small and large data transfers, but the tradeoff is inconsistent formatting in the output. + */ +export enum SalesforceDataTransferApi { + + AUTOMATIC = 'AUTOMATIC', + BULKV2 = 'BULKV2', + REST_SYNC = 'REST_SYNC' +} From 5d43071719f2bfbb080302197a0953a35768aa87 Mon Sep 17 00:00:00 2001 From: Piotr Srebniak Date: Tue, 12 Dec 2023 13:58:26 +0100 Subject: [PATCH 2/3] #60: add SalesforceDataTransferApi option to source --- src/salesforce/destination.ts | 2 +- src/salesforce/index.ts | 2 +- src/salesforce/{util.ts => salesforce-data-transfer-api.ts} | 0 src/salesforce/source.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/salesforce/{util.ts => salesforce-data-transfer-api.ts} (100%) diff --git a/src/salesforce/destination.ts b/src/salesforce/destination.ts index 55e93cd4..d2283c74 100644 --- a/src/salesforce/destination.ts +++ b/src/salesforce/destination.ts @@ -12,7 +12,7 @@ import { ErrorHandlingConfiguration } from '../core/error-handling'; import { IFlow } from '../core/flows'; import { IDestination } from '../core/vertices/destination'; import { WriteOperation } from '../core/write-operation'; -import { SalesforceDataTransferApi } from './util'; +import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; export interface SalesforceDestinationProps { diff --git a/src/salesforce/index.ts b/src/salesforce/index.ts index 59169792..2691ef48 100644 --- a/src/salesforce/index.ts +++ b/src/salesforce/index.ts @@ -6,4 +6,4 @@ export * from './type'; export * from './profile'; export * from './source'; export * from './destination'; -export * from './util'; +export * from './salesforce-data-transfer-api'; diff --git a/src/salesforce/util.ts b/src/salesforce/salesforce-data-transfer-api.ts similarity index 100% rename from src/salesforce/util.ts rename to src/salesforce/salesforce-data-transfer-api.ts diff --git a/src/salesforce/source.ts b/src/salesforce/source.ts index 3e053d06..1ce45f98 100644 --- a/src/salesforce/source.ts +++ b/src/salesforce/source.ts @@ -9,7 +9,7 @@ import { SalesforceConnectorType } from './type'; import { ConnectorType } from '../core/connectors/connector-type'; import { IFlow } from '../core/flows'; import { ISource } from '../core/vertices/source'; -import { SalesforceDataTransferApi } from './util'; +import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; export interface SalesforceSourceProps { readonly profile: SalesforceConnectorProfile; From f6f9d35250a1abe03fc39fc808a4847c9f539333 Mon Sep 17 00:00:00 2001 From: Piotr Srebniak Date: Tue, 12 Dec 2023 21:26:32 +0100 Subject: [PATCH 3/3] feat: #60 add unit tests --- API.md | 13 ++ src/salesforce/destination.ts | 2 +- src/salesforce/source.ts | 2 +- test/salesforce/destination.test.ts | 243 ++++++++++++++++++++++++++++ test/salesforce/profile.test.ts | 218 +++++++++++++++++++++++++ test/salesforce/source.test.ts | 232 ++++++++++++++++++++++++++ 6 files changed, 708 insertions(+), 2 deletions(-) create mode 100644 test/salesforce/destination.test.ts create mode 100644 test/salesforce/profile.test.ts create mode 100644 test/salesforce/source.test.ts diff --git a/API.md b/API.md index 9f89afad..66ce922b 100644 --- a/API.md +++ b/API.md @@ -7960,6 +7960,7 @@ const salesforceSourceProps: SalesforceSourceProps = { ... } | object | string | *No description.* | | profile | SalesforceConnectorProfile | *No description.* | | apiVersion | string | *No description.* | +| dataTransferApi | SalesforceDataTransferApi | Specifies which Salesforce API is used by Amazon AppFlow when your flow transfers data from Salesforce. | | enableDynamicFieldUpdate | boolean | *No description.* | | includeDeletedRecords | boolean | *No description.* | @@ -7995,6 +7996,18 @@ public readonly apiVersion: string; --- +##### `dataTransferApi`Optional + +```typescript +public readonly dataTransferApi: SalesforceDataTransferApi; +``` + +- *Type:* SalesforceDataTransferApi + +Specifies which Salesforce API is used by Amazon AppFlow when your flow transfers data from Salesforce. + +--- + ##### `enableDynamicFieldUpdate`Optional ```typescript diff --git a/src/salesforce/destination.ts b/src/salesforce/destination.ts index d2283c74..db324088 100644 --- a/src/salesforce/destination.ts +++ b/src/salesforce/destination.ts @@ -5,6 +5,7 @@ SPDX-License-Identifier: Apache-2.0 import { CfnFlow } from 'aws-cdk-lib/aws-appflow'; import { IConstruct } from 'constructs'; import { SalesforceConnectorProfile } from './profile'; +import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; import { SalesforceConnectorType } from './type'; import { AppFlowPermissionsManager } from '../core/appflow-permissions-manager'; import { ConnectorType } from '../core/connectors/connector-type'; @@ -12,7 +13,6 @@ import { ErrorHandlingConfiguration } from '../core/error-handling'; import { IFlow } from '../core/flows'; import { IDestination } from '../core/vertices/destination'; import { WriteOperation } from '../core/write-operation'; -import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; export interface SalesforceDestinationProps { diff --git a/src/salesforce/source.ts b/src/salesforce/source.ts index 1ce45f98..b561b827 100644 --- a/src/salesforce/source.ts +++ b/src/salesforce/source.ts @@ -5,11 +5,11 @@ SPDX-License-Identifier: Apache-2.0 import { CfnFlow } from 'aws-cdk-lib/aws-appflow'; import { IConstruct } from 'constructs'; import { SalesforceConnectorProfile } from './profile'; +import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; import { SalesforceConnectorType } from './type'; import { ConnectorType } from '../core/connectors/connector-type'; import { IFlow } from '../core/flows'; import { ISource } from '../core/vertices/source'; -import { SalesforceDataTransferApi } from './salesforce-data-transfer-api'; export interface SalesforceSourceProps { readonly profile: SalesforceConnectorProfile; diff --git a/test/salesforce/destination.test.ts b/test/salesforce/destination.test.ts new file mode 100644 index 00000000..f21c46a0 --- /dev/null +++ b/test/salesforce/destination.test.ts @@ -0,0 +1,243 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; + +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { + Mapping, + OnDemandFlow, + S3InputFileType, + S3Source, + SalesforceConnectorProfile, + SalesforceConnectorType, + SalesforceDataTransferApi, + SalesforceDestination, + WriteOperation, +} from '../../src'; + +describe('SalesforceDestination', () => { + + test('Destination with only connector name', () => { + const stack = new Stack(undefined, 'TestStack'); + const destination = new SalesforceDestination({ + profile: SalesforceConnectorProfile.fromConnectionProfileName(stack, 'TestProfile', 'dummy-profile'), + object: 'Account', + operation: WriteOperation.insert(), + }); + + const expectedConnectorType = SalesforceConnectorType.instance; + expect(destination.connectorType.asProfileConnectorLabel).toEqual(expectedConnectorType.asProfileConnectorLabel); + expect(destination.connectorType.asProfileConnectorType).toEqual(expectedConnectorType.asProfileConnectorType); + expect(destination.connectorType.asTaskConnectorOperatorOrigin).toEqual(expectedConnectorType.asTaskConnectorOperatorOrigin); + expect(destination.connectorType.isCustom).toEqual(expectedConnectorType.isCustom); + }); + + test('Destination in a Flow is in the stack', () => { + const stack = new Stack(undefined, 'TestStack'); + + const s3Bucket = new Bucket(stack, 'TestBucket', {}); + const source = new S3Source({ + bucket: s3Bucket, + prefix: '', + format: { + type: S3InputFileType.JSON, + }, + }); + + const destination = new SalesforceDestination({ + profile: SalesforceConnectorProfile.fromConnectionProfileName(stack, 'TestProfile', 'dummy-profile'), + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + object: 'Account', + operation: WriteOperation.insert(), + }); + + new OnDemandFlow(stack, 'TestFlow', { + source: source, + destination: destination, + mappings: [Mapping.mapAll()], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::AppFlow::Flow', { + DestinationFlowConfigList: [ + { + ConnectorProfileName: 'dummy-profile', + ConnectorType: 'Salesforce', + DestinationConnectorProperties: { + Salesforce: { + DataTransferApi: 'REST_SYNC', + Object: 'Account', + WriteOperationType: 'INSERT', + }, + }, + }, + ], + FlowName: 'TestFlow', + SourceFlowConfig: { + ConnectorType: 'S3', + SourceConnectorProperties: { + S3: { + BucketName: { + Ref: 'TestBucket560B80BC', + }, + BucketPrefix: '', + S3InputFormatConfig: { + S3InputFileType: 'JSON', + }, + }, + }, + }, + Tasks: [ + { + ConnectorOperator: { + S3: 'NO_OP', + }, + SourceFields: [], + TaskProperties: [ + { + Key: 'EXCLUDE_SOURCE_FIELDS_LIST', + Value: '[]', + }, + ], + TaskType: 'Map_all', + }, + ], + TriggerConfig: { + TriggerType: 'OnDemand', + }, + }); + }); + + test('Destination for dummy-profile in a Flow is in the stack', () => { + const stack = new Stack(undefined, 'TestStack'); + + const secret = Secret.fromSecretNameV2(stack, 'TestSecret', 'appflow/salesforce/client'); + const profile = new SalesforceConnectorProfile(stack, 'TestProfile', { + oAuth: { + accessToken: 'accessToken', + flow: { + refreshTokenGrant: { + refreshToken: 'refreshToken', + client: secret, + }, + }, + }, + instanceUrl: 'https://instance-id.develop.my.salesforce.com', + }); + + + const s3Bucket = new Bucket(stack, 'TestBucket', {}); + const source = new S3Source({ + bucket: s3Bucket, + prefix: '', + format: { + type: S3InputFileType.JSON, + }, + }); + + const destination = new SalesforceDestination({ + profile: profile, + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + object: 'Account', + operation: WriteOperation.insert(), + }); + + new OnDemandFlow(stack, 'TestFlow', { + source: source, + destination: destination, + mappings: [Mapping.mapAll()], + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::AppFlow::ConnectorProfile', { + ConnectionMode: 'Public', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: 'accessToken', + ClientCredentialsArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':secretsmanager:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':secret:appflow/salesforce/client', + ], + ], + }, + RefreshToken: 'refreshToken', + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: 'https://instance-id.develop.my.salesforce.com', + }, + }, + }, + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + }); + + template.hasResourceProperties('AWS::AppFlow::Flow', { + DestinationFlowConfigList: [ + { + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + DestinationConnectorProperties: { + Salesforce: { + DataTransferApi: 'REST_SYNC', + Object: 'Account', + WriteOperationType: 'INSERT', + }, + }, + }, + ], + FlowName: 'TestFlow', + SourceFlowConfig: { + ConnectorType: 'S3', + SourceConnectorProperties: { + S3: { + BucketName: { + Ref: 'TestBucket560B80BC', + }, + BucketPrefix: '', + S3InputFormatConfig: { + S3InputFileType: 'JSON', + }, + }, + }, + }, + Tasks: [ + { + ConnectorOperator: { + S3: 'NO_OP', + }, + SourceFields: [], + TaskProperties: [ + { + Key: 'EXCLUDE_SOURCE_FIELDS_LIST', + Value: '[]', + }, + ], + TaskType: 'Map_all', + }, + ], + TriggerConfig: { + TriggerType: 'OnDemand', + }, + }); + }); +}); diff --git a/test/salesforce/profile.test.ts b/test/salesforce/profile.test.ts new file mode 100644 index 00000000..31c2518d --- /dev/null +++ b/test/salesforce/profile.test.ts @@ -0,0 +1,218 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Key } from 'aws-cdk-lib/aws-kms'; +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; + +import { SalesforceConnectorProfile } from '../../src'; + +describe('SalesforceConnectorProfileProps', () => { + + test('OAuth2 profile with direct client credentials exists in the stack', () => { + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } }); + + const clientSecret = new Secret(stack, 'TestSecret'); + + new SalesforceConnectorProfile(stack, 'TestProfile', { + oAuth: { + accessToken: 'accessToken', + flow: { + refreshTokenGrant: { + refreshToken: 'refreshToken', + client: clientSecret, + }, + }, + }, + instanceUrl: 'https://instance-id.develop.my.salesforce.com', + }); + Template.fromStack(stack).hasResourceProperties('AWS::AppFlow::ConnectorProfile', { + ConnectionMode: 'Public', + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: 'accessToken', + ClientCredentialsArn: { + Ref: 'TestSecret16AF87B1', + }, + RefreshToken: 'refreshToken', + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: 'https://instance-id.develop.my.salesforce.com', + }, + }, + }, + }); + }); + + test('OAuth2 profile with client credentials as secret elements exists in the stack', () => { + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } }); + + const secret = new Secret(stack, 'TestSecret'); + + new SalesforceConnectorProfile(stack, 'TestProfile', { + oAuth: { + accessToken: secret.secretValueFromJson('accessToken').toString(), + flow: { + refreshTokenGrant: { + refreshToken: secret.secretValueFromJson('refreshToken').toString(), + client: secret, + }, + }, + }, + instanceUrl: secret.secretValueFromJson('instanceUrl').toString(), + }); + Template.fromStack(stack).hasResourceProperties('AWS::AppFlow::ConnectorProfile', { + ConnectionMode: 'Public', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:accessToken::}}', + ], + ], + }, + ClientCredentialsArn: { + Ref: 'TestSecret16AF87B1', + }, + RefreshToken: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:refreshToken::}}', + ], + ], + }, + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:instanceUrl::}}', + ], + ], + }, + }, + }, + }, + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + }); + }); + + + test('OAuth2 profile with a dedicated KMS key and client credentials as secret elements exists in the stack', () => { + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } }); + + const key = new Key(stack, 'TestKey'); + + const secret = new Secret(stack, 'TestSecret'); + + new SalesforceConnectorProfile(stack, 'TestProfile', { + key: key, + oAuth: { + accessToken: secret.secretValueFromJson('accessToken').toString(), + flow: { + refreshTokenGrant: { + refreshToken: secret.secretValueFromJson('refreshToken').toString(), + client: secret, + }, + }, + }, + instanceUrl: secret.secretValueFromJson('instanceUrl').toString(), + }); + + Template.fromStack(stack).hasResource('AWS::AppFlow::ConnectorProfile', { + DependsOn: [ + 'TestKey4CACAF33', + 'TestSecret16AF87B1', + ], + Properties: { + ConnectionMode: 'Public', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:accessToken::}}', + ], + ], + }, + ClientCredentialsArn: { + Ref: 'TestSecret16AF87B1', + }, + RefreshToken: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:refreshToken::}}', + ], + ], + }, + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'TestSecret16AF87B1', + }, + ':SecretString:instanceUrl::}}', + ], + ], + }, + }, + }, + }, + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + KMSArn: { + 'Fn::GetAtt': [ + 'TestKey4CACAF33', + 'Arn', + ], + }, + }, + Type: 'AWS::AppFlow::ConnectorProfile', + }); + }); + +}); diff --git a/test/salesforce/source.test.ts b/test/salesforce/source.test.ts new file mode 100644 index 00000000..c9a7a7eb --- /dev/null +++ b/test/salesforce/source.test.ts @@ -0,0 +1,232 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; + +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { + Mapping, + OnDemandFlow, + S3Destination, + SalesforceConnectorProfile, + SalesforceDataTransferApi, + SalesforceSource, + SalesforceConnectorType, +} from '../../src'; + +describe('SalesforceSource', () => { + + test('Source with only connector name', () => { + const stack = new Stack(undefined, 'TestStack'); + const source = new SalesforceSource({ + profile: SalesforceConnectorProfile.fromConnectionProfileName(stack, 'TestProfile', 'dummy-profile'), + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + enableDynamicFieldUpdate: true, + apiVersion: '1', + includeDeletedRecords: true, + object: 'Account', + }); + + const expectedConnectorType = SalesforceConnectorType.instance; + expect(source.connectorType.asProfileConnectorLabel).toEqual(expectedConnectorType.asProfileConnectorLabel); + expect(source.connectorType.asProfileConnectorType).toEqual(expectedConnectorType.asProfileConnectorType); + expect(source.connectorType.asTaskConnectorOperatorOrigin).toEqual(expectedConnectorType.asTaskConnectorOperatorOrigin); + expect(source.connectorType.isCustom).toEqual(expectedConnectorType.isCustom); + }); + + test('Source in a Flow is in the stack', () => { + const stack = new Stack(undefined, 'TestStack'); + const source = new SalesforceSource({ + profile: SalesforceConnectorProfile.fromConnectionProfileName(stack, 'TestProfile', 'dummy-profile'), + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + enableDynamicFieldUpdate: true, + apiVersion: '1', + includeDeletedRecords: true, + object: 'Account', + }); + + const destination = new S3Destination({ + location: { bucket: new Bucket(stack, 'TestBucket') }, + }); + + new OnDemandFlow(stack, 'TestFlow', { + source: source, + destination: destination, + mappings: [Mapping.mapAll()], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::AppFlow::Flow', { + DestinationFlowConfigList: [ + { + ConnectorType: 'S3', + DestinationConnectorProperties: { + S3: { + BucketName: { + Ref: 'TestBucket560B80BC', + }, + }, + }, + }, + ], + FlowName: 'TestFlow', + SourceFlowConfig: { + ApiVersion: '1', + ConnectorProfileName: 'dummy-profile', + ConnectorType: 'Salesforce', + SourceConnectorProperties: { + Salesforce: { + DataTransferApi: 'REST_SYNC', + EnableDynamicFieldUpdate: true, + IncludeDeletedRecords: true, + Object: 'Account', + }, + }, + }, + Tasks: [ + { + ConnectorOperator: { + Salesforce: 'NO_OP', + }, + SourceFields: [], + TaskProperties: [ + { + Key: 'EXCLUDE_SOURCE_FIELDS_LIST', + Value: '[]', + }, + ], + TaskType: 'Map_all', + }, + ], + TriggerConfig: { + TriggerType: 'OnDemand', + }, + }); + }); + + test('Source for dummy-profile in a Flow is in the stack', () => { + const stack = new Stack(undefined, 'TestStack'); + + const secret = Secret.fromSecretNameV2(stack, 'TestSecret', 'appflow/salesforce/client'); + const profile = new SalesforceConnectorProfile(stack, 'TestProfile', { + oAuth: { + accessToken: 'accessToken', + flow: { + refreshTokenGrant: { + refreshToken: 'refreshToken', + client: secret, + }, + }, + }, + instanceUrl: 'https://instance-id.develop.my.salesforce.com', + }); + + const source = new SalesforceSource({ + profile: profile, + dataTransferApi: SalesforceDataTransferApi.REST_SYNC, + enableDynamicFieldUpdate: true, + apiVersion: '1', + includeDeletedRecords: true, + object: 'Account', + }); + + const destination = new S3Destination({ + location: { bucket: new Bucket(stack, 'TestBucket') }, + }); + + new OnDemandFlow(stack, 'TestFlow', { + source: source, + destination: destination, + mappings: [Mapping.mapAll()], + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::AppFlow::ConnectorProfile', { + ConnectionMode: 'Public', + ConnectorProfileConfig: { + ConnectorProfileCredentials: { + Salesforce: { + AccessToken: 'accessToken', + ClientCredentialsArn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':secretsmanager:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':secret:appflow/salesforce/client', + ], + ], + }, + RefreshToken: 'refreshToken', + }, + }, + ConnectorProfileProperties: { + Salesforce: { + InstanceUrl: 'https://instance-id.develop.my.salesforce.com', + }, + }, + }, + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + }); + + template.hasResourceProperties('AWS::AppFlow::Flow', { + DestinationFlowConfigList: [ + { + ConnectorType: 'S3', + DestinationConnectorProperties: { + S3: { + BucketName: { + Ref: 'TestBucket560B80BC', + }, + }, + }, + }, + ], + FlowName: 'TestFlow', + SourceFlowConfig: { + ApiVersion: '1', + ConnectorProfileName: 'TestProfile', + ConnectorType: 'Salesforce', + SourceConnectorProperties: { + Salesforce: { + DataTransferApi: 'REST_SYNC', + EnableDynamicFieldUpdate: true, + IncludeDeletedRecords: true, + Object: 'Account', + }, + }, + }, + Tasks: [ + { + ConnectorOperator: { + Salesforce: 'NO_OP', + }, + SourceFields: [], + TaskProperties: [ + { + Key: 'EXCLUDE_SOURCE_FIELDS_LIST', + Value: '[]', + }, + ], + TaskType: 'Map_all', + }, + ], + TriggerConfig: { + TriggerType: 'OnDemand', + }, + }); + }); +});