-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add GoogleAds connector (#200)
- Loading branch information
Showing
9 changed files
with
1,346 additions
and
124 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/* | ||
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
export * from './type'; | ||
export * from './profile'; | ||
export * from './source'; | ||
export * from './util'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
import { SecretValue } from 'aws-cdk-lib'; | ||
import { CfnConnectorProfile } from 'aws-cdk-lib/aws-appflow'; | ||
import { Construct } from 'constructs'; | ||
import { GoogleAdsConnectorType } from './type'; | ||
import { ConnectorAuthenticationType } from '../core/connectors/connector-authentication-type'; | ||
import { ConnectorProfileBase, ConnectorProfileProps } from '../core/connectors/connector-profile'; | ||
import { OAuth2GrantType as OAuthGrantType } from '../core/connectors/oauth2-granttype'; | ||
|
||
export interface GoogleAdsConnectorProfileProps extends ConnectorProfileProps { | ||
readonly oAuth: GoogleAdsOAuthSettings; | ||
readonly apiVersion: string; | ||
readonly managerID?: SecretValue; | ||
readonly developerToken: SecretValue; | ||
} | ||
/** | ||
* Google's OAuth token and authorization endpoints | ||
*/ | ||
export interface GoogleAdsOAuthEndpoints { | ||
/** | ||
* The OAuth token endpoint URI | ||
*/ | ||
readonly token?: string; | ||
/** | ||
* The OAuth authorization endpoint URI | ||
*/ | ||
readonly authorization?: string; | ||
} | ||
/** | ||
* The OAuth elements required for the execution of the refresh token grant flow. | ||
*/ | ||
export interface GoogleAdsRefreshTokenGrantFlow { | ||
/** | ||
* A non-expired refresh token. | ||
*/ | ||
readonly refreshToken?: SecretValue; | ||
/** | ||
* The secret of the client app. | ||
*/ | ||
readonly clientSecret?: SecretValue; | ||
/** | ||
* The id of the client app. | ||
*/ | ||
readonly clientId?: SecretValue; | ||
} | ||
/** | ||
* Represents the OAuth flow enabled for the GoogleAds | ||
*/ | ||
export interface GoogleAdsOAuthFlow { | ||
/** | ||
* The details required for executing the refresh token grant flow | ||
*/ | ||
readonly refreshTokenGrant: GoogleAdsRefreshTokenGrantFlow; | ||
} | ||
export interface GoogleAdsOAuthSettings { | ||
/** | ||
* The access token to be used when interacting with Google Ads | ||
* | ||
* Note that if only the access token is provided AppFlow is not able to retrieve a fresh access token when the current one is expired | ||
* | ||
* @default Retrieves a fresh accessToken with the information in the [flow property]{@link GoogleAdsOAuthSettings#flow} | ||
*/ | ||
readonly accessToken?: SecretValue; | ||
/** | ||
* The OAuth flow used for obtaining a new accessToken when the old is not present or expired. | ||
* | ||
* @default undefined. AppFlow will not request any new accessToken after expiry. | ||
*/ | ||
readonly flow?: GoogleAdsOAuthFlow; | ||
/** | ||
* The OAuth token and authorization endpoints. | ||
*/ | ||
readonly endpoints?: GoogleAdsOAuthEndpoints; | ||
} | ||
|
||
export class GoogleAdsConnectorProfile extends ConnectorProfileBase { | ||
|
||
public static fromConnectionProfileArn(scope: Construct, id: string, arn: string) { | ||
return this._fromConnectorProfileAttributes(scope, id, { arn }) as GoogleAdsConnectorProfile; | ||
} | ||
public static fromConnectionProfileName(scope: Construct, id: string, name: string) { | ||
return this._fromConnectorProfileAttributes(scope, id, { name }) as GoogleAdsConnectorProfile; | ||
} | ||
|
||
private static readonly defaultTokenEndpoint: string = 'https://oauth2.googleapis.com/token'; | ||
|
||
constructor(scope: Construct, id: string, props: GoogleAdsConnectorProfileProps) { | ||
super(scope, id, props, GoogleAdsConnectorType.instance); | ||
} | ||
|
||
|
||
protected buildConnectorProfileProperties( | ||
props: ConnectorProfileProps, | ||
): CfnConnectorProfile.ConnectorProfilePropertiesProperty { | ||
const properties = (props as GoogleAdsConnectorProfileProps); | ||
return { | ||
customConnector: { | ||
profileProperties: { | ||
developerToken: properties.developerToken.unsafeUnwrap(), | ||
apiVersion: properties.apiVersion, | ||
managerID: properties.managerID?.unsafeUnwrap() as any /* can be undefined */, | ||
}, | ||
oAuth2Properties: { | ||
// INFO: even if we're using a refresh token grant flow this property is required | ||
oAuth2GrantType: OAuthGrantType.AUTHORIZATION_CODE, | ||
// INFO: even if we provide only the access token this property is required | ||
tokenUrl: properties.oAuth.endpoints?.token ?? GoogleAdsConnectorProfile.defaultTokenEndpoint, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
|
||
protected buildConnectorProfileCredentials( | ||
props: ConnectorProfileProps, | ||
): CfnConnectorProfile.ConnectorProfileCredentialsProperty { | ||
const properties = (props as GoogleAdsConnectorProfileProps); | ||
|
||
return { | ||
customConnector: { | ||
oauth2: { | ||
// INFO: when using Refresh Token Grant Flow - access token property is required | ||
accessToken: properties.oAuth.accessToken?.unsafeUnwrap() ?? 'dummyAccessToken', | ||
refreshToken: properties.oAuth.flow?.refreshTokenGrant.refreshToken?.unsafeUnwrap(), | ||
clientId: properties.oAuth.flow?.refreshTokenGrant.clientId?.unsafeUnwrap(), | ||
clientSecret: properties.oAuth.flow?.refreshTokenGrant.clientSecret?.unsafeUnwrap(), | ||
}, | ||
authenticationType: ConnectorAuthenticationType.OAUTH2, | ||
}, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
import { CfnFlow } from 'aws-cdk-lib/aws-appflow'; | ||
import { IConstruct } from 'constructs'; | ||
import { GoogleAdsConnectorProfile } from './profile'; | ||
import { GoogleAdsConnectorType } from './type'; | ||
import { ConnectorType } from '../core/connectors/connector-type'; | ||
import { IFlow } from '../core/flows'; | ||
import { ISource } from '../core/vertices'; | ||
/** | ||
* Properties of a Google Ads Source | ||
*/ | ||
export interface GoogleAdsSourceProps { | ||
readonly profile: GoogleAdsConnectorProfile; | ||
readonly apiVersion: string; | ||
readonly object: string; | ||
} | ||
/** | ||
* A class that represents a Google Ads v4 Source | ||
*/ | ||
export class GoogleAdsSource implements ISource { | ||
/** | ||
* The AppFlow type of the connector that this source is implemented for | ||
*/ | ||
public readonly connectorType: ConnectorType = GoogleAdsConnectorType.instance; | ||
|
||
constructor(private readonly props: GoogleAdsSourceProps) { } | ||
|
||
bind(scope: IFlow): CfnFlow.SourceFlowConfigProperty { | ||
|
||
this.tryAddNodeDependency(scope, this.props.profile); | ||
|
||
return { | ||
connectorType: this.connectorType.asProfileConnectorType, | ||
connectorProfileName: this.props.profile.name, | ||
apiVersion: this.props.apiVersion, | ||
sourceConnectorProperties: this.buildSourceConnectorProperties(), | ||
}; | ||
} | ||
|
||
private buildSourceConnectorProperties(): CfnFlow.SourceConnectorPropertiesProperty { | ||
return { | ||
customConnector: { | ||
entityName: this.props.object, | ||
}, | ||
}; | ||
} | ||
private tryAddNodeDependency(scope: IConstruct, resource?: IConstruct | string): void { | ||
if (resource && typeof resource !== 'string') { | ||
scope.node.addDependency(resource); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/* | ||
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
import { ConnectorType } from '../core/connectors/connector-type'; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export class GoogleAdsConnectorType extends ConnectorType { | ||
/** | ||
* Singleton | ||
*/ | ||
public static get instance(): ConnectorType { | ||
if (!GoogleAdsConnectorType.actualInstance) { | ||
GoogleAdsConnectorType.actualInstance = new GoogleAdsConnectorType(); | ||
} | ||
return GoogleAdsConnectorType.actualInstance; | ||
} | ||
|
||
private static actualInstance: ConnectorType; | ||
|
||
constructor() { | ||
super('GoogleAds', true); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/* | ||
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
/** | ||
* An enum representing the GoogleAds API versions. | ||
*/ | ||
export enum GoogleAdsApiVersion { | ||
V13 = 'v13', | ||
V14 = 'v14', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* | ||
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
import { SecretValue, Stack } from 'aws-cdk-lib'; | ||
import { Template } from 'aws-cdk-lib/assertions'; | ||
|
||
import { | ||
GoogleAdsConnectorProfile, | ||
GoogleAdsApiVersion, | ||
} from '../../src'; | ||
|
||
describe('GoogleAdsConnectorProfile', () => { | ||
|
||
test('OAuth2 profile with direct client credentials exists in the stack', () => { | ||
const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } }); | ||
|
||
// readonly oAuth: GoogleAdsOAuthSettings; | ||
// readonly apiVersion: string; | ||
// readonly managerID: string; | ||
// readonly developerToken: string; | ||
new GoogleAdsConnectorProfile(stack, 'TestProfile', { | ||
oAuth: { | ||
accessToken: SecretValue.unsafePlainText('accessToken'), | ||
flow: { | ||
refreshTokenGrant: { | ||
refreshToken: SecretValue.unsafePlainText('refreshToken'), | ||
clientId: SecretValue.unsafePlainText('clientId'), | ||
clientSecret: SecretValue.unsafePlainText('clientSecret'), | ||
}, | ||
}, | ||
}, | ||
apiVersion: GoogleAdsApiVersion.V14, | ||
managerID: SecretValue.unsafePlainText('managerId'), | ||
developerToken: SecretValue.unsafePlainText('developerToken'), | ||
}); | ||
|
||
Template.fromStack(stack).hasResourceProperties('AWS::AppFlow::ConnectorProfile', { | ||
ConnectionMode: 'Public', | ||
ConnectorLabel: 'GoogleAds', | ||
ConnectorProfileName: 'TestProfile', | ||
ConnectorType: 'CustomConnector', | ||
ConnectorProfileConfig: { | ||
ConnectorProfileCredentials: { | ||
CustomConnector: { | ||
AuthenticationType: 'OAUTH2', | ||
Oauth2: { | ||
AccessToken: 'accessToken', | ||
ClientId: 'clientId', | ||
ClientSecret: 'clientSecret', | ||
RefreshToken: 'refreshToken', | ||
}, | ||
}, | ||
}, | ||
ConnectorProfileProperties: { | ||
CustomConnector: { | ||
OAuth2Properties: { | ||
OAuth2GrantType: 'AUTHORIZATION_CODE', | ||
TokenUrl: 'https://oauth2.googleapis.com/token', | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.