Skip to content

Commit

Permalink
Extends API capabilities to interact with Database (#7)
Browse files Browse the repository at this point in the history
* feat: Adds formal API path for playerHand
The `playerHand` path has been added to the legalBrawl API.
This will be the destination where POST requests are made by the game
client to submit, and in return receive an opposing player's selected
cards.

A model has been added to ensure that the body's JSON data is valid
before it reaches the Lambda (though this doesn't stop unsanitary
inputs, it's a matter of validation at a schema-level).

* feat: Adds handling for POST reqs to playerHand
A first-cut of handling logic for POST requests to `playerHand` has
been added. Currently, this only *submits* `playerHandInfo` payloads
into the `playerHands` database.
This change provides the capability for an HTTP POST request of a
specific format to be inserted into the `playerHands` dynamoDB database
using the `PutItem` API.

Known Bugs:
- There is currently an unhandled scenario where if an entry already
exists, it should return an error
- When marshalling the Golang struct to a DynamoDB struct using
`MarshalMap`, there doesn't seem to be any indication that the payload
coming in is bogus. For example, a field could be called "hand" rather
than "card". This causes the data for the unmatching field to be "Null".

* refactor: Reorganizes aws package

The `aws` package use by the `legalbrawlapi` module is getting
needlessly deep, and is causing circular dependencies. Because the API
gateway request body is being used by DynamoDB in `PutItem` requests
(among others).

This "flattening" of the structure should better support the
requirements that these two components have.

* refactor: Merges struct types

Throughout the API Gateway and DynamoDB code, there were similarities
that were suitable to merge into common struct types.

The benefit of this is that bespoke "conversion" logic isn't necessary
if HandInfo is a 1:1 to its DynamoDB counterpart. So in theory the
body coming in from the request can immediately be used in the `PutItem`
API call.

This results in (hopefully) maintable code, while also still being
terse.

* refactor: Changes type scopes for aws package to private

* chore: Tweaks HandleRequest function

* doc: Adds docstring to addHand method

* doc: Removes "bug" regarding duplicate hands.

Players may be using their existing hand for matchmaking. Since DynamoDB
doesn't duplicate entries anyway. This isn't a bug as far as we would
be concerned. This may change as requirements evolve.

* feat: Adds POST and GET to playerHands API

* feat: Adds timeout to playerHands GET request

* feat: Adds timeout to playerHands POST request

* feat: Adds PUT method to API

* feat: Adds validation to POST & PUT methods

NOTE: The requestValidatorOptions had to be broken out into individual
objects. Refer to: aws/aws-cdk#7613

* feat: Adds validation to GET method

* feat: Adds API-side validation of GET method

* feat: Adds API-side validation of POST and PUT request

* feat: Adds logic to PUT request

* feat: Adds preexisting player checks for POST request

* chore: Cleans up error codes, and messages for apigateway

* refactor: Removes an unnecessary dDBHandler client creation

* feat: Adds timeout to playerHands PUT request
  • Loading branch information
SamJC authored Nov 21, 2022
1 parent 09f68a9 commit 4a4ca30
Show file tree
Hide file tree
Showing 8 changed files with 848 additions and 57 deletions.
142 changes: 128 additions & 14 deletions backend/ops/src/stacks/backend-stack.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
import * as cdk from "aws-cdk-lib"
import * as path from "path"
import { Construct } from "constructs"
import { aws_apigateway as apigateway, Duration } from "aws-cdk-lib"
import { aws_dynamodb as dynamodb } from "aws-cdk-lib"
import { aws_apigateway as apigateway } from "aws-cdk-lib"
import { aws_lambda as lambda } from "aws-cdk-lib"
import { aws_iam as iam } from "aws-cdk-lib"
import { aws_lambda as lambda } from "aws-cdk-lib"
import { aws_secretsmanager as secretsmanager } from "aws-cdk-lib"
import { aws_ssm as ssm } from "aws-cdk-lib"
import { time } from "console"

export class BackendStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)

//Best Practices:
//https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/estimate-the-cost-of-a-dynamodb-table-for-on-demand-capacity.html?did=pg_card&trk=pg_card#estimate-the-cost-of-a-dynamodb-table-for-on-demand-capacity-best-practices
new dynamodb.Table(this, "gameDatabase", {
partitionKey: { name: "playerId", type: dynamodb.AttributeType.STRING },
sortKey: { name: "formationId", type: dynamodb.AttributeType.STRING },
const playerHandsTable = new dynamodb.Table(this, "playerHandsTable", {
partitionKey: { name: "version", type: dynamodb.AttributeType.STRING },
sortKey: { name: "playerId", type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PROVISIONED,
pointInTimeRecovery: false,
removalPolicy: cdk.RemovalPolicy.DESTROY,
tableName: "gameDatabase",
removalPolicy: cdk.RemovalPolicy.DESTROY, //Swap this to `retain` when we got something that is shaping up nicely
tableName: "playerHands",
})

//The Lambda to handle requests coming through the API Gateway
//Lambda would be written in golang
const lambdafn = new lambda.Function(this, "legalBrawlApiHandler", {
const lambdaFn = new lambda.Function(this, "legalBrawlApiHandler", {
architecture: lambda.Architecture.ARM_64,
runtime: lambda.Runtime.PROVIDED_AL2,
handler: "bootstrap",
code: lambda.Code.fromAsset(path.join(__dirname, "../../../src/dist")),
timeout: Duration.seconds(20),
environment: {
LEGAL_BRAWL_SECRET_NAME: "legalBrawl/prod/api/v1/",
PLAYER_HAND_TABLE_NAME: playerHandsTable.tableName,
//The card balancing version. This should later be controlled via some flag, release tag
//or higher-level environment var during release
PLAYER_HAND_VERSION: "1.0",
},
})

Expand All @@ -45,23 +49,58 @@ export class BackendStack extends cdk.Stack {
this,
"/legalBrawl/secret/arn/suffix"
)
lambdafn.addToRolePolicy(
lambdaFn.addToRolePolicy(
new iam.PolicyStatement({
actions: [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:UpdateItem",
],
resources: [
`${apiToken.secretArn}-${SecretArnSuffix}`,
playerHandsTable.tableArn,
],
resources: [`${apiToken.secretArn}-${SecretArnSuffix}`],
})
)

const api = new apigateway.LambdaRestApi(this, "legalBrawlApi", {
handler: lambdafn,
handler: lambdaFn,
//Setting proxy to `true` will forward *all* requests to Lambda
//TODO: Figure out what resources, and what methods we are looking to implement
proxy: false,
})

const apiIntegration = new apigateway.LambdaIntegration(lambdaFn)

//playerHand scheme
const playerHandModel = api.addModel("playerHand", {
contentType: `application/json`,
modelName: "playerHandv1",
schema: {
schema: apigateway.JsonSchemaVersion.DRAFT4,
title: "PlayerHand",
type: apigateway.JsonSchemaType.OBJECT,
required: ["handInfo"],
properties: {
handInfo: {
type: apigateway.JsonSchemaType.OBJECT,
properties: {
playerName: { type: apigateway.JsonSchemaType.STRING },
playerId: { type: apigateway.JsonSchemaType.STRING },
version: { type: apigateway.JsonSchemaType.STRING },
cards: {
type: apigateway.JsonSchemaType.ARRAY,
items: { type: apigateway.JsonSchemaType.INTEGER },
},
},
},
},
},
})

new ssm.StringParameter(this, "paramLegalBrawlApiUrl", {
parameterName: "/legalBrawl/api/url",
stringValue: api.url,
Expand All @@ -70,10 +109,64 @@ export class BackendStack extends cdk.Stack {
//b.s. example of API paths
const v1 = api.root.addResource("v1")
const formation = v1.addResource("formationId")
const formationPostMethod = formation.addMethod("POST", undefined, {
const playerHand = v1.addResource("playerHand")
const formationPostMethod = formation.addMethod("POST", apiIntegration, {
apiKeyRequired: false,
}) //Set apiKeyRequired to `true` when ready to start locking down the API

//Need to create individual validator objects, refer to: https://github.com/aws/aws-cdk/issues/7613
const playerHandPostValidator = new apigateway.RequestValidator(
this,
"playerHandPostValidator",
{
restApi: api,
requestValidatorName: "playerHandPostValidator",
validateRequestBody: true,
validateRequestParameters: false,
}
)
const playerHandGetValidator = new apigateway.RequestValidator(
this,
"playerHandGetValidator",
{
restApi: api,
requestValidatorName: "playerHandGetValidator",
validateRequestParameters: true,
}
)
const playerHandPutValidator = new apigateway.RequestValidator(
this,
"playerHandPutValidator",
{
restApi: api,
requestValidatorName: "playerHandPutValidator",
validateRequestBody: true,
validateRequestParameters: false,
}
)

const playerHandPost = playerHand.addMethod("POST", apiIntegration, {
apiKeyRequired: true,
requestModels: {
"application/json": playerHandModel,
},
requestValidator: playerHandPostValidator,
})
const playerHandGet = playerHand.addMethod("GET", apiIntegration, {
apiKeyRequired: true,
requestParameters: {
"method.request.querystring.playerId": true,
},
requestValidator: playerHandGetValidator,
})
const playerHandPut = playerHand.addMethod("PUT", apiIntegration, {
apiKeyRequired: true,
requestModels: {
"application/json": playerHandModel,
},
requestValidator: playerHandPutValidator,
})

const plan = api.addUsagePlan("legalBrawlUsagePlan", {
throttle: {
rateLimit: 10,
Expand All @@ -91,6 +184,27 @@ export class BackendStack extends cdk.Stack {
burstLimit: 2,
},
},
{
method: playerHandPost,
throttle: {
rateLimit: 10,
burstLimit: 2,
},
},
{
method: playerHandGet,
throttle: {
rateLimit: 10,
burstLimit: 2,
},
},
{
method: playerHandPut,
throttle: {
rateLimit: 10,
burstLimit: 2,
},
},
],
})

Expand Down
Loading

0 comments on commit 4a4ca30

Please sign in to comment.