Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(appsync): import existing graphql api #9254

Merged
merged 22 commits into from
Aug 15, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-appsync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ demoDS.createResolver({
});
```

## Imports

Any GraphQL Api can be imported from another stack. Utilizing the `fromXxx`
functions, you have the ability to add data sources and resolvers through
a `IGraphQLApi` interface.

```ts
const importedApi = appsync.GraphQLApi.fromGraphQLApiId(...);
importedApi.addDynamoDbDataSource(...);
```

GraphQL Apis can be imported in three ways:
- `arn` through `GraphQLApi.fromGraphQLApiArn`: `apiId` is generated from parameter
- `apiId` through `GraphQLApi.fromGraphQLApiId`: `arn` is generated from parameter
- or both through `GraphQLApi.fromGraphQLApiAttributes`: both are specified


## Permissions

When using `AWS_IAM` as the authorization type for GraphQL API, an IAM Role
Expand Down
6 changes: 3 additions & 3 deletions packages/@aws-cdk/aws-appsync/lib/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IGrantable, IPrincipal, IRole, Role, ServicePrincipal } from '@aws-cdk/
import { IFunction } from '@aws-cdk/aws-lambda';
import { Construct, IResolvable } from '@aws-cdk/core';
import { CfnDataSource } from './appsync.generated';
import { GraphQLApi } from './graphqlapi';
import { IGraphQLApi } from './graphqlapi-base';
import { BaseResolverProps, Resolver } from './resolver';

/**
Expand All @@ -13,7 +13,7 @@ export interface BaseDataSourceProps {
/**
* The API to attach this data source to
*/
readonly api: GraphQLApi;
readonly api: IGraphQLApi;
/**
* The name of the data source
*/
Expand Down Expand Up @@ -91,7 +91,7 @@ export abstract class BaseDataSource extends Construct {
*/
public readonly ds: CfnDataSource;

protected api: GraphQLApi;
protected api: IGraphQLApi;
protected serviceRole?: IRole;

constructor(scope: Construct, id: string, props: BackedDataSourceProps, extended: ExtendedDataSourceProps) {
Expand Down
142 changes: 142 additions & 0 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { ITable } from '@aws-cdk/aws-dynamodb';
import { IFunction } from '@aws-cdk/aws-lambda';
import { CfnResource, IResource, Resource } from '@aws-cdk/core';
import { DynamoDbDataSource, HttpDataSource, LambdaDataSource, NoneDataSource } from './data-source';
/**
* Interface for GraphQL
*/
export interface IGraphQLApi extends IResource {
/**
* the id of the GraphQL API
*
* @attribute
*/
readonly apiId: string;
/**
* the ARN of the API
*
* @attribute
*/
readonly arn: string;
/**
* add a new dummy data source to this API
* @param name The name of the data source
* @param description The description of the data source
*/
addNoneDataSource(name: string, description: string): NoneDataSource;

/**
* add a new DynamoDB data source to this API
* @param name The name of the data source
* @param description The description of the data source
* @param table The DynamoDB table backing this data source [disable-awslint:ref-via-interface]
*/
addDynamoDbDataSource( name: string, description: string, table: ITable ): DynamoDbDataSource;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
addDynamoDbDataSource( name: string, description: string, table: ITable ): DynamoDbDataSource;
addDynamoDbDataSource(name: string, description: string, table: ITable ): DynamoDbDataSource;

it feels like the signature should be using our pattern of ...Options as an input parameter. Maybe something like addDynamoDbDataSource(name: string, options: DynamoDbDataSourceOptions)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that? there aren't that many configurable parameters so i'm not sure if an options prop is necessary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's consistent with the rest of the construct library and also compliant with design guidelines


/**
* add a new http data source to this API
* @param name The name of the data source
* @param description The description of the data source
* @param endpoint The http endpoint
*/
addHttpDataSource(name: string, description: string, endpoint: string): HttpDataSource;

/**
* add a new Lambda data source to this API
* @param name The name of the data source
* @param description The description of the data source
* @param lambdaFunction The Lambda function to call to interact with this data source
*/
addLambdaDataSource( name: string, description: string, lambdaFunction: IFunction ): LambdaDataSource;

/**
* Add schema dependency if not imported
* @param construct the construct that has a dependency
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clarify - is this the depender or the dependee

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

construct is the dependee

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we provide a more helpful description.
what happens if this method is called and it is imported? it's unclear what the implications are if it is imported

*/
addSchemaDependency( construct: CfnResource ): boolean;
}

/**
* Base Class for GraphQL API
*/
export abstract class GraphQLApiBase extends Resource implements IGraphQLApi {
/**
* the id of the GraphQL API
*/
public abstract readonly apiId: string;
/**
* the ARN of the API
*/
public abstract readonly arn: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we want to give this a specific prefix before the arn - since we're introducing this, let's make sure it aligns with how we define arn elsewhere

Copy link
Contributor Author

@BryanPan342 BryanPan342 Aug 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we change this here i feel like we need to change it for every where else.. including the GraphQLAPI.. i think this breaking change should happen in another pr


/**
* add a new dummy data source to this API
* @param name The name of the data source
* @param description The description of the data source
*/
public addNoneDataSource(name: string, description: string): NoneDataSource {
return new NoneDataSource(this, `${name}DS`, {
api: this,
description,
name,
});
}

/**
* add a new DynamoDB data source to this API
* @param name The name of the data source
* @param description The description of the data source
* @param table The DynamoDB table backing this data source [disable-awslint:ref-via-interface]
*/
public addDynamoDbDataSource(
name: string,
description: string,
table: ITable,
): DynamoDbDataSource {
return new DynamoDbDataSource(this, `${name}DS`, {
api: this,
description,
name,
table,
});
}

/**
* add a new http data source to this API
* @param name The name of the data source
* @param description The description of the data source
* @param endpoint The http endpoint
*/
public addHttpDataSource(name: string, description: string, endpoint: string): HttpDataSource {
return new HttpDataSource(this, `${name}DS`, {
api: this,
description,
endpoint,
name,
});
}

/**
* add a new Lambda data source to this API
* @param name The name of the data source
* @param description The description of the data source
* @param lambdaFunction The Lambda function to call to interact with this data source
*/
public addLambdaDataSource( name: string, description: string, lambdaFunction: IFunction ): LambdaDataSource {
return new LambdaDataSource(this, `${name}DS`, {
api: this,
description,
name,
lambdaFunction,
});
}

/**
* Add schema dependency if not imported
* @param construct the construct that has a dependency
*/
public addSchemaDependency( construct: CfnResource ): boolean {
construct;
return false;
}
}
152 changes: 77 additions & 75 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { readFileSync } from 'fs';
import { IUserPool } from '@aws-cdk/aws-cognito';
import { ITable } from '@aws-cdk/aws-dynamodb';
import { Grant, IGrantable, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam';
import { IFunction } from '@aws-cdk/aws-lambda';
import { Construct, Duration, IResolvable, Stack } from '@aws-cdk/core';
import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated';
import { DynamoDbDataSource, HttpDataSource, LambdaDataSource, NoneDataSource } from './data-source';
import { ManagedPolicy, Role, ServicePrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam';
import { CfnResource, Construct, Duration, Fn, IResolvable, Stack } from '@aws-cdk/core';
import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated';
import { IGraphQLApi, GraphQLApiBase } from './graphqlapi-base';

/**
* enum with all possible values for AppSync authorization type
Expand Down Expand Up @@ -206,7 +204,6 @@ export interface LogConfig {
* Properties for an AppSync GraphQL API
*/
export interface GraphQLApiProps {

/**
* the name of the GraphQL API
*/
Expand Down Expand Up @@ -300,11 +297,71 @@ export class IamResource {
}
}

/**
* Attributes for GraphQL From
*/
export interface GraphQLApiAttributes {
/**
* the arn for the GraphQL Api
*/
readonly arn: string,

/**
* the api id of the GraphQL Api
*/
readonly apiId: string,
}

/**
* An AppSync GraphQL API
*
* @resource AWS::AppSync::GraphQLApi
*/
export class GraphQLApi extends Construct {
export class GraphQLApi extends GraphQLApiBase {
/**
* Import a GraphQL API given an arn
* @param scope scope
* @param id id
* @param graphQLApiId the apiId of the api
*/
public static fromGraphQLApiId(scope: Construct, id: string, graphQLApiId: string): IGraphQLApi {
const generatedArn = Stack.of(scope).formatArn({
service: 'appsync',
resource: `apis/${graphQLApiId}`,
});
return GraphQLApi.fromGraphQLApiAttributes(scope, id, { arn: generatedArn, apiId: graphQLApiId});
}

/**
* Import a GraphQL API given an arn
* @param scope scope
* @param id id
* @param graphQLApiArn the arn of the api
*/
public static fromGraphQLApiArn(scope: Construct, id: string, graphQLApiArn: string): IGraphQLApi {
const apiId = Fn.select(1, Fn.split('/', graphQLApiArn));
if (apiId == undefined || apiId == graphQLApiArn) {
throw Error('Arn does not contain a valid apiId');
}
return GraphQLApi.fromGraphQLApiAttributes(scope, id, { arn: graphQLApiArn, apiId: apiId});
}

/**
* Import a GraphQL API through this function
* @param scope scope
* @param id id
* @param attrs GraphQL API Attributes of an API
*/
public static fromGraphQLApiAttributes(scope: Construct, id: string, attrs: GraphQLApiAttributes): IGraphQLApi {
class Import extends GraphQLApiBase {
public readonly apiId = attrs.apiId;
public readonly arn = attrs.arn;
constructor (s: Construct, i: string){
super(s, i);
}
}
return new Import(scope, id);
}
/**
* the id of the GraphQL API
*/
Expand All @@ -315,6 +372,8 @@ export class GraphQLApi extends Construct {
public readonly arn: string;
/**
* the URL of the endpoint created by AppSync
*
* @attribute
*/
public readonly graphQlUrl: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about the casing here (and everywhere really)... how does this translate to Python where we do snake casing?

does GraphQL get broken down into graph_q_l?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i think in this case would be best to change all of them to graphqlUrl for props

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ this would be a breaking change tho so not sure if thats the best move for something so small..

@MrArnoldPalmer thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it needs to happen in this PR, but fixing the casing sounds like something we need to do regardless if it requires code that is not idiomatic for Python developers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i think a chore might be better

/**
Expand Down Expand Up @@ -418,72 +477,6 @@ export class GraphQLApi extends Construct {
});
}

/**
* add a new dummy data source to this API
* @param name The name of the data source
* @param description The description of the data source
*/
public addNoneDataSource(name: string, description: string): NoneDataSource {
return new NoneDataSource(this, `${name}DS`, {
api: this,
description,
name,
});
}

/**
* add a new DynamoDB data source to this API
* @param name The name of the data source
* @param description The description of the data source
* @param table The DynamoDB table backing this data source [disable-awslint:ref-via-interface]
*/
public addDynamoDbDataSource(
name: string,
description: string,
table: ITable,
): DynamoDbDataSource {
return new DynamoDbDataSource(this, `${name}DS`, {
api: this,
description,
name,
table,
});
}

/**
* add a new http data source to this API
* @param name The name of the data source
* @param description The description of the data source
* @param endpoint The http endpoint
*/
public addHttpDataSource(name: string, description: string, endpoint: string): HttpDataSource {
return new HttpDataSource(this, `${name}DS`, {
api: this,
description,
endpoint,
name,
});
}

/**
* add a new Lambda data source to this API
* @param name The name of the data source
* @param description The description of the data source
* @param lambdaFunction The Lambda function to call to interact with this data source
*/
public addLambdaDataSource(
name: string,
description: string,
lambdaFunction: IFunction,
): LambdaDataSource {
return new LambdaDataSource(this, `${name}DS`, {
api: this,
description,
name,
lambdaFunction,
});
}

/**
* Adds an IAM policy statement associated with this GraphQLApi to an IAM
* principal's policy.
Expand Down Expand Up @@ -597,6 +590,15 @@ export class GraphQLApi extends Construct {
}
}

/**
* Add schema dependency if not imported
* @param construct the construct that has a dependency
*/
public addSchemaDependency( construct: CfnResource ): boolean {
construct.addDependsOn(this.schema);
return true;
Comment on lines +601 to +602
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this method? it seems like it's substituting one API call (addDependsOn) with another (addSchemaDependency). any reason users can't just do it directly?

does it require validation? what's valid as a construct to add

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there needs to be a dependency add for resolvers and the schema, but i was not comfortable passing schema's around in imports so essentially i needed a way to allow for adding a dependency only if the api wasn't imported.

this basically was my workaround.. if anything i can make this function protected

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make it internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, I've thought about this a little more and I think that it might be good to offer a public api to add a dependency.. if the graphql api is in another stack, it will be deployed before the imported stack so the dependency doesn't matter too much?

but in the case that it does we should have a public api to allow for adding dependencies? if not i dont see why a public api is a net bad

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it enables use cases that users cannot achieve, then I agree.
this method calls construct.addDependsOn(schema) and returns true. can users just make that api call instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shivlaks its just really annoying for users to have to do after creating each resolver.

The only reason its public as of right now is that I need IGraphQLApi to have some capability to reference a schema, I can't make it protected or the function will be inaccessible to the Resolver class.

}

private formatOpenIdConnectConfig(
config: OpenIdConnectConfig,
): CfnGraphQLApi.OpenIDConnectConfigProperty {
Expand Down Expand Up @@ -666,4 +668,4 @@ export class GraphQLApi extends Construct {
const authModes = props.authorizationConfig?.additionalAuthorizationModes;
return authModes ? this.formatAdditionalAuthorizationModes(authModes) : undefined;
}
}
}
Loading