diff --git a/API.md b/API.md
index 99868529..dd79813c 100644
--- a/API.md
+++ b/API.md
@@ -8525,6 +8525,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.* |
@@ -8560,6 +8561,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 0e9963df..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';
@@ -13,19 +14,6 @@ import { IFlow } from '../core/flows';
import { IDestination } from '../core/vertices/destination';
import { WriteOperation } from '../core/write-operation';
-/**
- * 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..2691ef48 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 './salesforce-data-transfer-api';
diff --git a/src/salesforce/salesforce-data-transfer-api.ts b/src/salesforce/salesforce-data-transfer-api.ts
new file mode 100644
index 00000000..2b871fe5
--- /dev/null
+++ b/src/salesforce/salesforce-data-transfer-api.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'
+}
diff --git a/src/salesforce/source.ts b/src/salesforce/source.ts
index b5b2ad61..b561b827 100644
--- a/src/salesforce/source.ts
+++ b/src/salesforce/source.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 { ConnectorType } from '../core/connectors/connector-type';
import { IFlow } from '../core/flows';
@@ -12,6 +13,12 @@ import { ISource } from '../core/vertices/source';
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/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',
+ },
+ });
+ });
+});