Skip to content

Commit

Permalink
feat: add GoogleAds connector (#200)
Browse files Browse the repository at this point in the history
  • Loading branch information
remitache authored May 24, 2024
1 parent a4fd04f commit e218561
Show file tree
Hide file tree
Showing 9 changed files with 1,346 additions and 124 deletions.
945 changes: 822 additions & 123 deletions API.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/googleads/index.ts
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';
135 changes: 135 additions & 0 deletions src/googleads/profile.ts
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,
},
};
}
}
55 changes: 55 additions & 0 deletions src/googleads/source.ts
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);
}
}
}
26 changes: 26 additions & 0 deletions src/googleads/type.ts
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);
}
}
12 changes: 12 additions & 0 deletions src/googleads/util.ts
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',
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export * from './slack';
export * from './snowflake';
export * from './redshift';
export * from './zendesk';
export * from './mailchimp';
export * from './mailchimp';
export * from './googleads';
66 changes: 66 additions & 0 deletions test/googleads/profile.test.ts
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',
},
},
},
},
});
});
});
Loading

0 comments on commit e218561

Please sign in to comment.