Skip to content

Commit

Permalink
Add the GraphQL API demo use case
Browse files Browse the repository at this point in the history
- Added the files for a new use case that is a demo of setting up a simple GraphQL API using AWS AppSync and RDS Aurora.
- Edited the main README file and added the information about the new use case.
- Revised the README files and simplified them if it was possible.
  • Loading branch information
habedi committed Dec 3, 2024
1 parent 6693bb4 commit 0fb4c17
Show file tree
Hide file tree
Showing 19 changed files with 1,127 additions and 22 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ max_line_length = 100
# Markdown files
[*.md]
trim_trailing_whitespace = false # Don't remove trailing whitespace in Markdown files
max_line_length = 150 # Allow longer lines in Markdown files
# Bash scripts
[*.sh]
Expand Down
26 changes: 12 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
# Learning Terraform with AWS
# Terraform Playground

[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![CodeFactor](https://www.codefactor.io/repository/github/habedi/learning-terraform/badge)](https://www.codefactor.io/repository/github/habedi/learning-terraform)
[![CodeFactor](https://www.codefactor.io/repository/github/habedi/terraform-playground/badge)](https://www.codefactor.io/repository/github/habedi/terraform-playground)

The [use-cases](use-cases/) directory contains a collection of use cases where [Terraform](https://www.terraform.io/)
is used to provision resources in [AWS](https://aws.amazon.com/).

See the README file in each use case directory for more information about the use case.
This repository contains a collection of use cases where [Terraform](https://www.terraform.io/) is used to provision resources
in [AWS](https://aws.amazon.com/) to set up environments for various tasks.

The list of currently implemented use cases:

| Index | Task |
|-------|----------------------------------------------------------------------|
| 1 | [Use Case 1: Provision a server](modules/use-case-1/) |
| 2 | [Use Case 2: Provision a server and a database](modules/use-case-2/) |
| Index | Task | Complexity |
|-------|-------------------------------------------------------------------------------|------------|
| 1 | [Provision a Server](use-cases/use-case-1/) | Simple |
| 2 | [Provision a Server and a Database](use-cases/use-case-2/) | Simple |
| 3 | [Set up a GraphQL API with AWS AppSync and RDS Aurora](use-cases/use-case-3/) | Complex |

## Installing Poetry
## Poetry

Optionally, you can use [Poetry](https://python-poetry.org/) to manage the Python dependencies
(if you use Python for scripting, etc.).
You can use [Poetry](https://python-poetry.org/) to manage the Python dependencies (if you use Python for scripting, etc.).

```bash
pip install poetry
pipx install poetry # or uv tool install poetry
```
5 changes: 2 additions & 3 deletions use-cases/use-case-1/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Use Case 1: Provision a server in AWS
# Provision a Server in AWS

This use case provisions a single EC2 instance in AWS.
The instance is created in a new VPC with a single public subnet and a security group that allows SSH access from the
public internet.
The instance is created in a new VPC with a single public subnet and a security group that allows SSH access from the public internet.
8 changes: 3 additions & 5 deletions use-cases/use-case-2/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# Use Case 2: Provision a server and a database in AWS
# Provision a Server and a Database in AWS

This use case provisions an EC2 instance and an RDS MySQL database instance in AWS.
The EC2 instance is created in a new VPC with a single public subnet and a security group that allows SSH access from
the public internet.
The EC2 instance is created in a new VPC with a single public subnet and a security group that allows SSH access from the public internet.
The RDS MySQL database instance is created in the same VPC as the EC2 instance, however, in a private subnet.
The security group of the RDS MySQL database instance allows inbound traffic only from the security group of the EC2
instance.
The security group of the RDS MySQL database instance allows inbound traffic only from the security group of the EC2 instance.
So, the ECT instance can connect to the RDS MySQL database instance.
90 changes: 90 additions & 0 deletions use-cases/use-case-3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Set Up a GraphQL API with AWS AppSync and RDS Aurora Serverless

This use case is a simple demo that shows how to create a [GraphQL API](https://graphql.org/) using [AWS AppSync](https://aws.amazon.com/appsync/) and
an [RDS Aurora Serverless database](https://aws.amazon.com/rds/aurora/serverless/) as the data source.

You can start and stop the demo by running the commands below in the terminal.
The commands must be run in the directory where the `main.tf` file is located.

#### Prepare the Environment

```bash
terraform init
```

#### Create the Execution Plan

```bash
terraform plan -out=plan.tfplan
```

#### Apply the Plan

```bash
terraform apply -parallelism=4
```

#### Clean Up

```bash
terraform destroy
```

### Customizing the Demo

Change the values for the variables in the [variables.tf](variables.tf) file to customize the demo (for example, region, database name, etc.).

### Accessing the API

After the `apply` command is run without errors, you should be able to access the API via
the [AppSync console](https://us-east-1.console.aws.amazon.com/appsync/home?region=us-east-1#/).
The console will show the URL for the GraphQL endpoint, which includes an API named `roles-api`.
You can also access and invoke the API using the AWS CLI, the AWS SDKs, etc.

Example queries, mutations, and subscriptions to interact with the API:

```graphql
mutation MyMutation1 {
createRole(id: "3", name: "ROLE NAME") {
id
name
description
}
updateRole(id: "3", name: "DUMMY_USER", description: "Dummy User") {
id
name
description
}
}

query Query {
listRoles {
id
name
description
}
}

mutation MyMutation2 {
deleteRole(id: "3") {
id
}
}

subscription MySubscription {
onDeleteRole {
description
id
name
}
}
```

### API Schema and Resolvers

The schema and resolvers for the API are defined in the [schema](assets/apis/roles/schema.graphql) file and [resolvers](assets/apis/roles/resolvers/)
directory, respectively.

### Cleanup

After you are done with the demo, you can clean up the resources by running the `destroy` Terraform command.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "2018-05-29",
"statements": [
"insert into core_model.roles (id, name, description) values (:id::integer, :name::text, :description::text) returning id, name, description"
],
"variableMap": {
":id": $util.toJson($ctx.args.id),
":name": $util.toJson($ctx.args.name),
":description": $util.toJson($ctx.args.description)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#if ($ctx.error)
$util.error($ctx.error.message, $ctx.error.type)
#else
#if ($ctx.result.length == 0)
null
#else
$utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])
#end
#end
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Sends a request to the attached data source for deleteRole
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the request
*/
export function request(ctx) {
// Extract the ID argument from the mutation input
const {id} = ctx.args;

// Construct the SQL query
const sqlQuery = `
delete
from core_model.roles
where id = :id::integer
returning id, name, description
`;

// Log the constructed query
console.log(`Executing query: ${sqlQuery} with id=${id}`);

// Return the SQL query with parameters
return {
version: "2018-05-29",
statements: [sqlQuery],
variableMap: {
":id": id,
},
};
}

/**
* Returns the resolver result for deleteRole
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the result
*/
export function response(ctx) {
// Log the raw database response
console.log(`Raw response from data source: ${JSON.stringify(ctx.result)}`);

// Parse the SQL statement results
const sqlStatementResults = JSON.parse(ctx.result).sqlStatementResults;

if (!sqlStatementResults || sqlStatementResults.length === 0) {
console.log("No SQL statement results found.");
return null; // No rows deleted
}

// Extract records from the first statement result
const records = sqlStatementResults[0].records;

if (!records || records.length === 0) {
console.log("No records found in the database response.");
return null; // No rows deleted
}

// Map the first record to the GraphQL schema
const deletedRole = {
id: records[0][0]?.longValue, // Assuming the first column is `id`
name: records[0][1]?.stringValue, // Assuming the second column is `name`
description: records[0][2]?.stringValue, // Assuming the third column is `description`
};

// Log the processed role
console.log(`Deleted role: ${JSON.stringify(deletedRole)}`);

return deletedRole;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"version": "2018-05-29",
"statements": [
"select id, name, description from core_model.roles where id = :id::integer",
],
"variableMap": {
":id": $util.toJson($ctx.args.id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#if ($ctx.error)
$util.error($ctx.error.message, $ctx.error.type)
#else
#if ($ctx.result.length == 0)
null
#else
$utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])
#end
#end
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Sends a request to the attached data source for listRoles
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the request
*/
export function request(ctx) {
// SQL query to fetch all roles
const sqlQuery = "select id, name, description from core_model.roles";

// Log the constructed query
console.log(`Executing query: ${sqlQuery}`);

// Return the query to be executed
return {
version: "2018-05-29",
statements: [sqlQuery],
};
}

/**
* Returns the resolver result for listRoles
* @param {import('@aws-appsync/utils').Context} ctx the context
* @returns {*} the result
*/
export function response(ctx) {
// Log the raw database response
console.log(`Raw response from data source: ${JSON.stringify(ctx.result)}`);

// Parse the SQL statement results
const sqlStatementResults = JSON.parse(ctx.result).sqlStatementResults;

if (!sqlStatementResults || sqlStatementResults.length === 0) {
console.log("No SQL statement results found.");
return [];
}

// Extract records from the first statement result
const records = sqlStatementResults[0].records;

if (!records || records.length === 0) {
console.log("No records found in the database response.");
return [];
}

// Map records to the GraphQL schema
const roles = records.map(row => {
return {
id: row[0]?.longValue, // Assuming the first column is `id`
name: row[1]?.stringValue, // Assuming the second column is `name`
description: row[2]?.stringValue, // Assuming the third column is `description`
};
});

// Log the processed roles
console.log(`Processed roles: ${JSON.stringify(roles)}`);

return roles;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "2018-05-29",
"statements": [
"update core_model.roles set name = :name::text, description = :description::text where id = :id::integer returning id, name, description"
],
"variableMap": {
":id": $util.toJson($ctx.args.id),
":name": $util.toJson($ctx.args.name),
":description": $util.toJson($ctx.args.description)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#if ($ctx.error)
$util.error($ctx.error.message, $ctx.error.type)
#else
#if ($ctx.result.length == 0)
null
#else
$utils.toJson($utils.rds.toJsonObject($ctx.result)[0][0])
#end
#end
25 changes: 25 additions & 0 deletions use-cases/use-case-3/assets/apis/roles/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type Role {
id: ID! # The unique identifier for the role
name: String # The name of the role
description: String # The description of the role
}

# Queries
type Query {
getRole(id: ID!): Role # Fetch a role by its ID
listRoles: [Role] # List all roles
}

# Mutations
type Mutation {
createRole(id: ID!, name: String, description: String): Role # Create a new role
updateRole(id: ID!, name: String!, description: String!): Role # Update an existing role
deleteRole(id: ID!): Role # Delete a role
}

# Subscription (Optional)
type Subscription {
onCreateRole: Role @aws_subscribe(mutations: ["createRole"]) # Listen for role creation
onUpdateRole: Role @aws_subscribe(mutations: ["updateRole"]) # Listen for role updates
onDeleteRole: Role @aws_subscribe(mutations: ["deleteRole"]) # Listen for role deletion
}
3 changes: 3 additions & 0 deletions use-cases/use-case-3/assets/db/db.sql.gz
Git LFS file not shown
Loading

0 comments on commit 0fb4c17

Please sign in to comment.