Building Full Stack Serverless Application With Amplify, Flutter, GraphQL, AWS CDK, and Typescript(PART 3)

Building Full Stack Serverless Application With Amplify, Flutter, GraphQL, AWS CDK, and Typescript(PART 3)

Hello, In the last 2 articles of this series, we looked at some basic concepts needed to understand graphql and appsync, and we also started creating our API.
PART 1

Part 2
In this article, we'll pick up from where we left off

Adding a DynamoDB Table

Now that the Lambda function has been configured we need to create a DynamoDB table and enable the Lambda function to access it.

To do so, we’ll add the following code below the GraphQL resolver definitions in lib/notes-cdk-app-stack.ts:

 // lib/notes-cdk-app-stack.ts
    const notesTable = new ddb.Table(this, "CDKNotesTable", {
      billingMode: ddb.BillingMode.PAY_PER_REQUEST,
      partitionKey: {
        name: "id",
        type: ddb.AttributeType.STRING,
      },
    });
    // enable the Lambda function to access the DynamoDB table (using IAM)
    notesTable.grantFullAccess(notesLambda);

    // Create an environment variable that we will use in the function code
    notesLambda.addEnvironment("NOTES_TABLE", notesTable.tableName);

Outputting everything

// Prints out the AppSync GraphQL endpoint to the terminal
    new cdk.CfnOutput(this, "GraphQLAPIURL", {
      value: api.graphqlUrl,
    });

    // Prints out the AppSync GraphQL API key to the terminal
    new cdk.CfnOutput(this, "GraphQLAPIKey", {
      value: api.apiKey || "",
    });

    // Prints out the stack region to the terminal
    new cdk.CfnOutput(this, "Stack Region", {
      value: this.region,
    });

Adding the Lambda function code

Finally, we need to add the function code that we will be using to map the GraphQL queries and mutations into DynamoDB operations.

We can use the event object that is passed into the function argument to find the fieldName of the operation that is invoking the function and then map the invocation to the proper DynamoDB operation.

For example, if the GraphQL operation that triggered the function is createNote, then the fieldName will be available at event.info.fieldName.

We can also access any arguments that are passed into the GraphQL operation by accessing the event.arguments object.

With this in mind, create the following files in the lambda-fns directory: main.ts, createNote.ts, deleteNote.ts, getNoteById.ts, listNotes.ts, updateNote.ts, and Note.ts.

Next, we’ll update these files with the code needed to interact with the DynamoDB table and perform various operations. main.ts

import createNote from "./createNote";
import deleteNote from "./deleteNote";
import getNoteById from "./getNoteById";
import listNotes from "./listNotes";
import updateNote from "./updateNote";
import Note from "./Note";

type AppSyncEvent = {
  info: {
    fieldName: string;
  };
  arguments: {
    noteId: string;
    note: Note;
  };
};

exports.handler = async (event: AppSyncEvent) => {
  switch (event.info.fieldName) {
    case "getNoteById":
      return await getNoteById(event.arguments.noteId);
    case "createNote":
      return await createNote(event.arguments.note);
    case "listNotes":
      return await listNotes();
    case "deleteNote":
      return await deleteNote(event.arguments.noteId);
    case "updateNote":
      return await updateNote(event.arguments.note);
    default:
      return null;
  }
};

Note.ts

type Note = {
  id: string;
  title: string;
  description: string;
  color: number;
  completed: boolean;
};

export default Note;

createNote.ts

const AWS = require("aws-sdk");
const docClient = new AWS.DynamoDB.DocumentClient();
import Note from "./Note";

async function createNote(note: Note) {
  const params = {
    TableName: process.env.NOTES_TABLE,
    Item: note,
  };
  try {
    await docClient.put(params).promise();
    return note;
  } catch (err) {
    console.log("DynamoDB error: ", err);
    return null;
  }
}
export default createNote;

updateNote.ts

// lambda-fns/updateNote.ts
const AWS = require("aws-sdk");
const docClient = new AWS.DynamoDB.DocumentClient();

type Params = {
  TableName: string | undefined;
  Key: string | {};
  ExpressionAttributeValues: any;
  ExpressionAttributeNames: any;
  UpdateExpression: string;
  ReturnValues: string;
};

async function updateNote(note: any) {
  let params: Params = {
    TableName: process.env.NOTES_TABLE,
    Key: {
      id: note.id,
    },
    ExpressionAttributeValues: {},
    ExpressionAttributeNames: {},
    UpdateExpression: "",
    ReturnValues: "UPDATED_NEW",
  };
  let prefix = "set ";
  let attributes = Object.keys(note);
  for (let i = 0; i < attributes.length; i++) {
    let attribute = attributes[i];
    if (attribute !== "id") {
      params["UpdateExpression"] +=
        prefix + "#" + attribute + " = :" + attribute;
      params["ExpressionAttributeValues"][":" + attribute] = note[attribute];
      params["ExpressionAttributeNames"]["#" + attribute] = attribute;
      prefix = ", ";
    }
  }
  console.log("params: ", params);
  try {
    await docClient.update(params).promise();
    return note;
  } catch (err) {
    console.log("DynamoDB error: ", err);
    return null;
  }
}

export default updateNote;

getNoteById

const AWS = require("aws-sdk");
const docClient = new AWS.DynamoDB.DocumentClient();

async function getNoteById(noteId: string) {
  const params = {
    TableName: process.env.NOTES_TABLE,
    Key: { id: noteId },
  };
  try {
    const { Item } = await docClient.get(params).promise();
    return Item;
  } catch (err) {
    console.log("DynamoDB error: ", err);
  }
}

export default getNoteById;

listNote.ts

const AWS = require("aws-sdk");
const docClient = new AWS.DynamoDB.DocumentClient();

async function listNotes() {
  const params = {
    TableName: process.env.NOTES_TABLE,
  };
  try {
    const data = await docClient.scan(params).promise();
    return data.Items;
  } catch (err) {
    console.log("DynamoDB error: ", err);
    return null;
  }
}

export default listNotes;

deleteNote.ts

const AWS = require("aws-sdk");
const docClient = new AWS.DynamoDB.DocumentClient();

async function deleteNote(noteId: string) {
  const params = {
    TableName: process.env.NOTES_TABLE,
    Key: {
      id: noteId,
    },
  };
  try {
    await docClient.delete(params).promise();
    return noteId;
  } catch (err) {
    console.log("DynamoDB error: ", err);
    return null;
  }
}

export default deleteNote;

Deploying and testing

Now we are ready to deploy. To do so, run the following command from your terminal:

npm run build && cdk deploy

Now that the updates have been deployed, visit the AppSync console and click on the API name to view the dashboard for your API.

Public Access

As we mentioned earlier, the API supports both public and private access.
To get all data from the database using the listNotes endpoint, all we need to do is use the API KEY which grants public access.

Screen Shot 2021-09-16 at 07.25.04.png

Screen Shot 2021-09-16 at 07.25.14.png If you try to do a mutation with the API KEY, you'll get an unauthorized errorType.

Screen Shot 2021-09-16 at 07.32.09.png

In-order to do a mutation or access private endpoints, we have to create a user in aws Cognito.

Screen Shot 2021-09-16 at 07.34.33.png

Next, click on Manage User Pools

Screen Shot 2021-09-16 at 07.34.56.png Next, select your application, click on users and groups in the left menu and then create a new user.

Screen Shot 2021-09-16 at 07.35.48.png

Screen Shot 2021-09-16 at 07.36.23.png You'll need the username and password in appsync.

Next, go back to appsync and select Cognito user pool as your authorization provider.

Screen Shot 2021-09-16 at 07.40.59.png Sign In and then perform a mutation.

Screen Shot 2021-09-16 at 07.44.26.png Here are all the endpoints.

mutation createNote {
  createNote(note: {id: 5, description: "this is a description", title: "this one", color: "3213"}) {
    id
    title
    description
    color
    createdOn
  }
  updateNote(note: {id: 5, description: "this is an updated description", title: "updated title", color: "3213"}) {
    id
    title
    description
    color
    createdOn
  }
  deleteNote(noteId: "5")
}

query listNotes {
  getNoteById(noteId: "5") {
    color
    createdOn
    description
    id
    title
  }
  listNotes {
    color
    createdOn
    description
    id
    title
  }
}

Subscriptions

One of the most powerful components of a GraphQL API and one of the things that AppSync makes really easy is enabling real-time updates via GraphQL subscriptions.

GraphQL subscriptions allow you to subscribe to mutations made against a GraphQL API. In our case, we may want to subscribe to changes like when a new note is created, when a note is deleted, or when a note is updated.

type Subscription {
  onCreateNote: Note @aws_subscribe(mutations: ["createNote"])
  onDeleteNote: String @aws_subscribe(mutations: ["deleteNote"])
  onUpdateNote: Note @aws_subscribe(mutations: ["updateNote"])
}

To test out the changes, open another window and visit the AppSync Query editor for the GraphQL API.

From here, you can test out the subscription by running a subscription in one editor and triggering a mutation in another:

Screen Shot 2021-09-16 at 08.17.54.png If you face the issue above, solve it like this. github.com/awslabs/aws-mobile-appsync-sdk-i..
The next step is about developing our mobile app using the amplify library and flutter framework, to consume the API we just created.

That's all for this article Folks.
Coming up next and last is flutter with Amplify. Stay tuned
Thanks for reading through.
If you loved it, there's a high chance, someone in your clique would love it too. Show a brother some love, by spreading the word, leaving a like or comment. See you in the next article Happy Coding ✌🏿