appsync-rds-todo icon indicating copy to clipboard operation
appsync-rds-todo copied to clipboard

This is a boilerplate AppSync-aurora project, that is deployed using the serverless framework with out of the box support for automated creation of a Serverless database cluster, lambdas, vpcs, securi...

AppSync RDS ToDo

appsync-rds-todo CI Dev

appsync-rds-todo CD develop

This is a boilerplate AppSync-aurora project, that is deployed using the serverless framework with out of the box support for automated creation of a Serverless database cluster, lambdas, vpcs, security groups, nat gateways, etc.

Local setup

Run the following command to setup the local database and run migrations

./scripts/setup-local.sh

This project exposes the following queries and mutations

Queries

type Query {
  notes(pagination: PaginationInput!, where: AWSJSON): PaginatedNotes!
  lists(pagination: PaginationInput!, where: AWSJSON): PaginatedLists!
  users(pagination: PaginationInput!, where: AWSJSON): PaginatedUsers!
}

Mutations

type Mutation {
  # create mutations
  createNote(input: CreateNoteRequest!): MutatedNote!
  createList(input: CreateListRequest!): MutatedList!
  createUser(input: CreateUserRequest!): MutatedUser!

  # update mutations
  updateList(input: UpdateListRequest!): MutatedList!
  updateNote(input: UpdateNoteRequest!): MutatedNote!
  updateUser(input: UpdateUserRequest!): MutatedUser!

  # delete mutations
  deleteList(id: ID!): MutatedList!
  deleteNote(id: ID!): MutatedNote!
  deleteUser(id: ID!): MutatedUser!
}

You can find the complete postman collection here: Collection

Highlights

  • Automated creation of resources

    This project handles creation of all resources. It uses an ApiKey for authentication for this project but to know more about how to integrate AppSync with cognito take a look at the following implementaions

    Creation of the following resources is automated

    • Serverless Aurora cluster
    • Subnets
    • Public Route Table
    • Private Route Table
    • Security Groups
    • VPC
    • Elastic IP
    • Secret
    • NAT Gateway
    • Internet Gateway
    • IAM Roles
    • Lambdas
    • Lambdas as datasources
    • RDS as a datasource
  • Support for running database migrations in the CD pipeline. If you've used a serverless cluster before you know how problematic this is. Take a look at the following files to get a better idea about how we did it

    • webpack.config.js
    • functions/database/migrate/index.js
    • scripts/post-deployment.js
  • All queries support a sequelizedWhere, which allows a highly-configurable queries

  • All mutations are resolved directly off of the database This project has out of the support for a camelCased GraphQL interface, whereas the database layer has snake_case table and column names. So how do we resolve mutations directly off of the database?

    • resolvers/mutations/createNote.req.vtl
    • resolvers/mutations/response.vtl
  • Max depth for GraphQL queries is restricted to 4

  • Support for paginated queries Nested pagination and fulfilling of deeply-nested queries is exhausting and overwhelming, take a look at how we've simplified this

    • utils/dbUtils.js
    • functions/queries/Notes/index.js
  • Support for serverless-webpack, which allows you to use the latest JavaScript functionality in your lambdas

  • Support for serverless-dotenv, which allows you to deploy easily to multiple environments

  • Out of the box support for sequelize

  • Out of the box CI/CD pipelines with support to innject environment variables using Github Secrets

    • .github/workflows/ci.yml
    • .github/workflows/cd.yml

Adding features on top of the template

What we've done here is built a base on top of which we can incrementally add new features easily.

Adding a new table

  • Create a new migration file
  • Increment the version in migrations/resources/ and add the necessary .sql
  • Create the sequelize model in the models/ folder

Adding a new query

  • Create a Lambda Take a look at the following lambda, you just need to change the model that you're passing to findAll functions/queries/Notes/index.js

    exports.handler = async (event, context, callback) =>
      logHandler(event, callback, async () => {
        try {
          return success(context.done || callback, await findAll(db[<new-model-you-created>], event));
        } catch (err) {
          return failure(context.fail || callback, err);
        }
      });
    
    
  • Add the Lambda in the resources/lambdas/functions.yml

    <name-of-function>:
      handler: <path-to-newly-created-function>.handler
      role: <either use `LambdaServiceRole` or create a new role as required>
      vpc:
        securityGroupIds:
          - !Ref ServerlessSecurityGroup
        subnetIds:
          - Ref: ServerlessPrivateSubnetA
          - Ref: ServerlessPrivateSubnetB
          - Ref: ServerlessPrivateSubnetC
    
  • Add the Lambda as a datasource in the resources/lambdas/datasources.yml

    - type: AWS_LAMBDA
      name: Lambda_<name-of-function>
      description: "<Proper description>"
      config:
        functionName: <name-of-the-function-as-in-the-functions.yml>
    
  • Add the query in resources/mapping-templates/queries.yml

    - type: Query
      field: <name-of-field-in-graphql-schema>
      request: "queries/query.req.vtl"
      response: "response.vtl"
      dataSource: <name-as-mentioned-in-the-lambdas/datasources.yml>
    

Adding a mutation

  • Create a new request resolver, based on the type of the mutation you can use one of the following templates

    • Create

      Similar to the one here createNote.req.vtl

          #set( $cols = [] )
          #set( $vals = [] )
          #foreach( $entry in $ctx.args.input.keySet() )
            #set( $regex = "([a-z])([A-Z]+)")
            #set( $replacement = "$1_$2")
            #set( $toSnake = $entry.replaceAll($regex, $replacement).toLowerCase() )
            #set( $discard = $cols.add("$toSnake") )
            #if( $util.isBoolean($ctx.args.input[$entry]) )
                #if( $ctx.args.input[$entry] )
                  #set( $discard = $vals.add("1") )
                #else
                  #set( $discard = $vals.add("0") )
                #end
            #else
                #set( $discard = $vals.add("'$ctx.args.input[$entry]'") )
            #end
          #end
      
          #set( $valStr = $vals.toString().replace("[","(").replace("]",")") )
          #set( $colStr = $cols.toString().replace("[","(").replace("]",")") )
          #if ( $valStr.substring(0, 1) != '(' )
            #set( $valStr = "($valStr)" )
          #end
          #if ( $colStr.substring(0, 1) != '(' )
            #set( $colStr = "($colStr)" )
          #end
          {
            "version": "2018-05-29",
            "statements":   ["INSERT INTO <name-of-table> $colStr VALUES $valStr", "SELECT * FROM <name-of-table> ORDER BY id DESC LIMIT 1"]
          }
      
    • Update

      Similar to the one here updateNote.req.vtl

          #set( $update = "" )
          #set( $equals = "=" )
          ## 1
          #foreach( $entry in $ctx.args.input.keySet() )
            ## 2
            #set( $cur = $ctx.args.input[$entry] )
            #set( $regex = "([a-z])([A-Z]+)")
            #set( $replacement = "$1_$2")
            #set( $toSnake = $entry.replaceAll($regex, $replacement).toLowerCase() )
            ## 3
            #if( $util.isBoolean($cur) )
                #if( $cur )
                  #set ( $cur = "1" )
                #else
                  #set ( $cur = "0" )
                #end
            #end
            ## 4
            #if ( $util.isNullOrEmpty($update) )
                #set($update = "$toSnake$equals'$cur'" )
            #else
                #set($update = "$update,$toSnake$equals'$cur'" )
            #end
          #end
          {
            "version": "2018-05-29",
            "statements":   ["UPDATE <name-of-table> SET $update WHERE id=$ctx.args.input.id", "SELECT * FROM <name-of-table> WHERE id=$ctx.args.input.id"]
          }
      
    • Delete

      Similar to the one here deleteNote.req.vtl

          {
            "version": "2018-05-29",
            "statements":   ["UPDATE <name-of-table> set deleted_at=NOW() WHERE id=$ctx.args.id", "SELECT * FROM <name-of-table> WHERE id=$ctx.args.id"]
          }
      
  • Add a mutation in resources/mapping-templates/mutations.yml

    - type: Mutation
      field: createNote
      request: "mutations/createNote.req.vtl"
      response: "mutations/response.vtl"
      dataSource: POSTGRES_RDS