AWSTemplateFormatVersion: "2010-09-09" Parameters: userPoolId: Type: String Description: User Pool ID associated with this project Outputs: ChatQLApiId: Description: Unique AWS AppSync GraphQL API Identifier Value: !GetAtt chatQLApi.ApiId ChatQLApiUrl: Description: The Endpoint URL of your GraphQL API. Value: !GetAtt chatQLApi.GraphQLUrl Resources: conversationsTable: Type: "AWS::DynamoDB::Table" Properties: AttributeDefinitions: - AttributeName: "id" AttributeType: "S" KeySchema: - AttributeName: "id" KeyType: "HASH" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" messagesTable: Type: "AWS::DynamoDB::Table" Properties: AttributeDefinitions: - AttributeName: "conversationId" AttributeType: "S" - AttributeName: "createdAt" AttributeType: "S" KeySchema: - AttributeName: "conversationId" KeyType: "HASH" - AttributeName: "createdAt" KeyType: "RANGE" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" userConversationsTable: Type: "AWS::DynamoDB::Table" Properties: AttributeDefinitions: - AttributeName: "userId" AttributeType: "S" - AttributeName: "conversationId" AttributeType: "S" KeySchema: - AttributeName: "userId" KeyType: "HASH" - AttributeName: "conversationId" KeyType: "RANGE" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" GlobalSecondaryIndexes: - IndexName: "conversationId-index" KeySchema: - AttributeName: "conversationId" KeyType: "HASH" Projection: ProjectionType: "ALL" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" usersTable: Type: "AWS::DynamoDB::Table" Properties: AttributeDefinitions: - AttributeName: "cognitoId" AttributeType: "S" KeySchema: - AttributeName: "cognitoId" KeyType: "HASH" ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" awsAppSyncServiceRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "appsync.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" dynamodbAccessPolicy: Type: "AWS::IAM::Policy" Properties: PolicyName: "dynamodb-access" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "dynamodb:*" Resource: - !GetAtt conversationsTable.Arn - !GetAtt messagesTable.Arn - !GetAtt userConversationsTable.Arn - !GetAtt usersTable.Arn - !Sub '${userConversationsTable.Arn}/index/conversationId-index' Roles: - Ref: "awsAppSyncServiceRole" chatQLApi: Type: "AWS::AppSync::GraphQLApi" Properties: Name: "ChatQL" AuthenticationType: "AMAZON_COGNITO_USER_POOLS" UserPoolConfig: UserPoolId: !Ref userPoolId AwsRegion: !Ref "AWS::Region" DefaultAction: "ALLOW" conversationsTableDataSource: Type: "AWS::AppSync::DataSource" Properties: ApiId: !GetAtt chatQLApi.ApiId Name: "conversationsTableDataSource" Description: "conversationsTable DynamoDB data source" Type: "AMAZON_DYNAMODB" ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn DynamoDBConfig: TableName: !Ref conversationsTable AwsRegion: !Ref "AWS::Region" messagesTableDataSource: Type: "AWS::AppSync::DataSource" Properties: ApiId: !GetAtt chatQLApi.ApiId Name: "messagesTableDataSource" Description: "messagesTable DynamoDB data source" Type: "AMAZON_DYNAMODB" ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn DynamoDBConfig: TableName: !Ref messagesTable AwsRegion: !Ref "AWS::Region" userConversationsTableDataSource: Type: "AWS::AppSync::DataSource" Properties: ApiId: !GetAtt chatQLApi.ApiId Name: "userConversationsTableDataSource" Description: "userConversationsTable DynamoDB data source" Type: "AMAZON_DYNAMODB" ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn DynamoDBConfig: TableName: !Ref userConversationsTable AwsRegion: !Ref "AWS::Region" usersTableDataSource: Type: "AWS::AppSync::DataSource" Properties: ApiId: !GetAtt chatQLApi.ApiId Name: "usersTableDataSource" Description: "usersTable DynamoDB data source" Type: "AMAZON_DYNAMODB" ServiceRoleArn: !GetAtt awsAppSyncServiceRole.Arn DynamoDBConfig: TableName: !Ref usersTable AwsRegion: !Ref "AWS::Region" chatQLSchema: Type: "AWS::AppSync::GraphQLSchema" Properties: ApiId: !GetAtt chatQLApi.ApiId Definition: | schema { query: Query mutation: Mutation subscription: Subscription } type Conversation { # The Conversation's timestamp. createdAt: String # A unique identifier for the Conversation. id: ID! # The Conversation's messages. messages(after: String, first: Int): MessageConnection # The Conversation's name. name: String! } type Message { # The author object. Note: `authorId` is only available because we list it in `extraAttributes` in `Conversation.messages` author: User # The message content. content: String! # The id of the Conversation this message belongs to. This is the table primary key. conversationId: ID! # The message timestamp. This is also the table sort key. createdAt: String # Generated id for a message -- read-only id: ID! # Flag denoting if this message has been accepted by the server or not. isSent: Boolean recipient: User sender: String } type MessageConnection { messages: [Message] nextToken: String } type Mutation { # Create a Conversation. Use some of the cooked in template functions for UUID and DateTime. createConversation(createdAt: String, id: ID!, name: String!): Conversation # Create a message in a Conversation. createMessage(content: String, conversationId: ID!, createdAt: String!, id: ID!): Message # Put a single value of type 'User'. If an item does not exist with the same key the item will be created. If there exists an item at that key already, it will be updated. createUser(username: String!): User # Put a single value of type 'UserConversations'. If an item does not exist with the same key the item will be created. If there exists an item at that key already, it will be updated. createUserConversations(conversationId: ID!, userId: ID!): UserConversations } type Query { # Scan through all values of type 'Message'. Use the 'after' and 'before' arguments with the 'nextToken' returned by the 'MessageConnection' result to fetch pages. allMessage(after: String, conversationId: ID!, first: Int): [Message] # Scan through all values of type 'MessageConnection'. Use the 'after' and 'before' arguments with the 'nextToken' returned by the 'MessageConnectionConnection' result to fetch pages. allMessageConnection(after: String, conversationId: ID!, first: Int): MessageConnection allMessageFrom(after: String, conversationId: ID!, first: Int, sender: String!): [Message] # Scan through all values of type 'User'. Use the 'after' and 'before' arguments with the 'nextToken' returned by the 'UserConnection' result to fetch pages. allUser(after: String, first: Int): [User] # Get my user. me: User } type Subscription { # Subscribes to all new messages in a given Conversation. subscribeToNewMessage(conversationId: ID!): Message @aws_subscribe(mutations:["createMessage"]) subscribeToNewUCs(userId:ID!): UserConversations @aws_subscribe(mutations: ["createUserConversations"]) subscribeToNewUsers: User @aws_subscribe(mutations: ["createUser"]) } type User { # A unique identifier for the user. cognitoId: ID! # A user's enrolled Conversations. This is an interesting case. This is an interesting pagination case. conversations(after: String, first: Int): UserConverstationsConnection # Generated id for a user. read-only id: ID! # Get a users messages by querying a GSI on the Messages table. messages(after: String, first: Int): MessageConnection # The username username: String! # is the user registered? registered: Boolean } type UserConversations { associated: [UserConversations] conversation: Conversation conversationId: ID! user: User userId: ID! } type UserConverstationsConnection { nextToken: String userConversations: [UserConversations] } createConversationMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "Mutation" FieldName: "createConversation" DataSourceName: !GetAtt conversationsTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "PutItem", "key": { "id": { "S" : "${context.arguments.id}"} }, "attributeValues" : { "id": { "S": "${context.arguments.id}" }, "name": { "S": "${context.arguments.name}" } #if(${context.arguments.createdAt}) ,"createdAt": { "S": "${context.arguments.createdAt}"} #end } } ResponseMappingTemplate: | $utils.toJson($context.result) createMessageMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "Mutation" FieldName: "createMessage" DataSourceName: !GetAtt messagesTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "PutItem", "key" : { "conversationId" : { "S" : "${context.arguments.conversationId}" } }, "attributeValues" : { "conversationId": { "S": "${context.arguments.conversationId}" }, "content": { "S": "${context.arguments.content}" }, "createdAt": { "S": "${context.arguments.createdAt}" }, "sender": { "S": "${context.identity.sub}" }, "isSent": { "BOOL": true }, "id": { "S": "${context.arguments.id}" } } } ResponseMappingTemplate: | $utils.toJson($context.result) createUserConversationsMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "Mutation" FieldName: "createUserConversations" DataSourceName: !GetAtt userConversationsTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "PutItem", "key": { "userId": { "S" : "${context.arguments.userId}"}, "conversationId": { "S" : "${context.arguments.conversationId}"} }, "attributeValues" : { "userId": { "S": "${context.arguments.userId}" }, "conversationId": { "S": "${context.arguments.conversationId}" } } } ResponseMappingTemplate: | $utils.toJson($context.result) createUserMutationResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "Mutation" FieldName: "createUser" DataSourceName: !GetAtt usersTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "PutItem", "key": { "cognitoId": { "S" : "${context.identity.sub}"} }, "attributeValues" : { "cognitoId": { "S": "${context.identity.sub}" }, "username": { "S": "${context.arguments.username}" }, "id": { "S": "${context.identity.sub}" }, "registered": { "BOOL": true } } } ResponseMappingTemplate: | $utils.toJson($context.result) conversationUserConversationsResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "UserConversations" FieldName: "conversation" DataSourceName: !GetAtt conversationsTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "GetItem", "key" : { "id" : { "S" : "${context.source.conversationId}" } } } ResponseMappingTemplate: | $utils.toJson($context.result) meQueryResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "Query" FieldName: "me" DataSourceName: !GetAtt usersTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "GetItem", "key": { "cognitoId": { "S" : "${context.identity.sub}"} } } ResponseMappingTemplate: | $utils.toJson($context.result) allMessageConnectionQueryResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "Query" FieldName: "allMessageConnection" DataSourceName: !GetAtt messagesTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "Query", "query" : { "expression": "conversationId = :conversationId", "expressionValues" : { ":conversationId" : { "S" : "${context.arguments.conversationId}" } } }, "scanIndexForward": false, "limit": #if(${context.arguments.first}) ${context.arguments.first} #else 20 #end, "nextToken": #if(${context.arguments.after}) "${context.arguments.after}" #else null #end } ResponseMappingTemplate: | { "messages": $utils.toJson($context.result.items), "nextToken": #if(${context.result.nextToken}) "${context.result.nextToken}" #else null #end } associatedUserConversationsResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "UserConversations" FieldName: "associated" DataSourceName: !GetAtt userConversationsTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "Query", "query" : { "expression": "conversationId = :conversationId", "expressionValues" : { ":conversationId" : { "S" : "${context.source.conversationId}" } } }, "index": "conversationId-index" } ResponseMappingTemplate: | $util.toJson($context.result.items) messagesUserResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "User" FieldName: "messages" DataSourceName: !GetAtt messagesTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "Query", "query" : { "expression": "sender = :id", "expressionValues" : { ":id" : { "S" : "${context.source.cognitoId}" } } }, "index": "sender-conversationId-index", "scanIndexForward": false, "limit": #if(${context.arguments.first}) ${context.arguments.first} #else 20 #end, "nextToken": #if(${context.arguments.after}) "${context.arguments.after}" #else null #end } ResponseMappingTemplate: | { "messages": $utils.toJson($context.result.items), "nextToken": #if(${context.result.nextToken}) "${context.result.nextToken}" #else null #end } conversationsUserResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "User" FieldName: "conversations" DataSourceName: !GetAtt userConversationsTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "Query", "query" : { "expression": "userId = :userId", "expressionValues" : { ":userId" : { "S" : "${context.source.cognitoId}" } } }, "scanIndexForward": false, "limit": #if(${context.arguments.first}) ${context.arguments.first} #else 20 #end, "nextToken": #if(${context.arguments.after}) "${context.arguments.after}" #else null #end } ResponseMappingTemplate: | { "userConversations": $utils.toJson($context.result.items), "nextToken": #if(${context.result.nextToken}) "${context.result.nextToken}" #else null #end } allUserQueryResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "Query" FieldName: "allUser" DataSourceName: !GetAtt usersTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "Scan", "limit": #if(${context.arguments.first}) "${context.arguments.first}" #else 20 #end, "nextToken": #if(${context.arguments.after}) "${context.arguments.after}" #else null #end } ResponseMappingTemplate: | $utils.toJson($context.result.items) allMessageQueryResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "Query" FieldName: "allMessage" DataSourceName: !GetAtt messagesTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "Query", "query" : { "expression": "conversationId = :id", "expressionValues" : { ":id" : { "S" : "${context.arguments.conversationId}" } } }, "limit": #if(${context.arguments.first}) ${context.arguments.first} #else 20 #end, "nextToken": #if(${context.arguments.after}) "${context.arguments.after}" #else null #end } ResponseMappingTemplate: | $utils.toJson($context.result.items) allMessageFromQueryResolver: Type: "AWS::AppSync::Resolver" Properties: ApiId: !GetAtt chatQLApi.ApiId TypeName: "Query" FieldName: "allMessageFrom" DataSourceName: !GetAtt messagesTableDataSource.Name RequestMappingTemplate: | { "version" : "2017-02-28", "operation" : "Query", "query" : { "expression": "conversationId = :id and sender = :sender", "expressionValues" : { ":id" : { "S" : "${context.arguments.conversationId}" }, ":sender" : { "S" : "${context.arguments.sender}" } } }, "index" : "sender", "limit": #if(${context.arguments.first}) ${context.arguments.first} #else 20 #end, "nextToken": #if(${context.arguments.after}) "${context.arguments.after}" #else null #end } ResponseMappingTemplate: | $utils.toJson($context.result.items)