Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] (feat: api-gateway): Smithy-like syntax and Velocity-Template DSL for Api Gatewau #111

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 188 additions & 44 deletions examples/lib/game-score-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Core, Lambda, DynamoDB } from 'punchcard';
import { string, integer, Record, Minimum, optional, array, boolean, nothing, Maximum } from '@punchcard/shape';
import { Core, DynamoDB, Api } from 'punchcard';
import { string, integer, Record, Minimum, optional, array, boolean, nothing, Maximum, timestamp } from '@punchcard/shape';
import { Ok, Operation, Fail } from 'punchcard/lib/api';

import { VTL } from '@punchcard/shape-velocity-template/lib';

/**
* Create a new Punchcard Application.
Expand All @@ -11,6 +14,10 @@ export const app = new Core.App();
*/
const stack = app.stack('game-score-service');

class InternalServerError extends Record({
errorMessage: string
}) {}

/**
* Record of data to maintain a user's statistics for a game.
*/
Expand All @@ -22,7 +29,7 @@ class UserGameScore extends Record({
/**
* Title of the game played.
*/
gameTitle: string,
gameId: string,
/**
* Top score achieved on this game.
* Minimum: 0
Expand All @@ -43,10 +50,11 @@ class UserGameScore extends Record({
* Version of the DynamoDB record - use for optimistic locking.
*/
version: integer
}) {}
})
.Deriving(VTL.DSL) {}

namespace UserGameScore {
export class Key extends UserGameScore.Pick(['userId', 'gameTitle']) {}
export class Key extends UserGameScore.Pick(['userId', 'gameId']) {}
}
/**
* DynamoDB Table storing the User-Game statistics.
Expand All @@ -55,7 +63,122 @@ const UserScores = new DynamoDB.Table(stack, 'ScoreStore', {
data: UserGameScore,
key: {
partition: 'userId',
sort: 'gameTitle'
sort: 'gameId'
}
});

const endpoint = new Api.Endpoint();

export const GameService = new Api.Service({
serviceName: 'game-service',
});

class GetTimeRequest extends Record({
/**
* Length. `hi`.
*/
length: integer
}) {}

class GetTimeResponse extends Record({
currentTime: timestamp
}) {}
const GetTimeCall = new Api.Call({
input: GetTimeRequest,
output: GetTimeResponse,
endpoint,
}, async () =>
Ok(new GetTimeResponse({
currentTime: new Date()
}))
);

const GetTime = GameService.addOperation({
name: 'GetTime',
input: string
}, request => {
// transform the string request into a GetTimeRequest using VTL
const input = VTL.Of(GetTimeRequest, {
length: request.length
});

// call the GetTimeCall operation and pass the VTL output
const response = GetTimeCall.call(input);

// could just return here ...
// return response;

// ... or transform the response with VTL
return response.currentTime;
});

// /user/<userId>
const User = GameService.addChild({
name: 'user',
identifiers: {
userId: string
}
});

/**
* POST: /user
*/
class CreateUserRequest extends Record({
userName: string
}) {}
class CreateUserResponse extends Record({
userId: string
}) {}
const CreateUserHandler = new Api.Call({
input: string,
output: CreateUserResponse,
endpoint
}, async (request) => {
return Ok(new CreateUserResponse({
userId: `todo: ${request}`
}))
});
const CreateUser = User.onCreate(CreateUserRequest, request => CreateUserHandler.call(request.userName).userId);

// PUT: /user/<userId>
class UpdateUserRequest extends Record({
userId: string
}) {}
const UpdateUserHandler = new Api.Call({
endpoint,
input: UpdateUserRequest,
output: nothing
}, async () => Ok(null));
const UpdateUser = User.onUpdate(UpdateUserRequest, request => UpdateUserHandler.call(request));

/**
* GET: /user/<userId>
*/
class GetUserRequest extends Record({
userId: string
}) {}
class GetUserResponse extends Record({
userName: string
}) {}
const GetUserHandler = new Api.Call({
endpoint,
input: string,
output: GetUserResponse
}, async (userId) => {
// TODO: lookup user in DDB
return Ok(new GetUserResponse({
userName: `todo: ${userId}`
}));
});
const GetUser = User.onGet(GetUserRequest, request => GetUserHandler.call(request.userId));

/**
* /score/<scoreId>
*/
const Score = GameService.addChild({
name: 'score',
identifiers: {
scoreId: string
}
});

Expand All @@ -68,9 +191,9 @@ class SubmitScoreRequest extends Record({
*/
userId: string,
/**
* Title of the game played.
* Id of the game played.
*/
gameTitle: string,
gameId: string,
/**
* Did the player win or lose?
*/
Expand All @@ -83,32 +206,30 @@ class SubmitScoreRequest extends Record({
.apply(Minimum(0)),
}) { }

/**
* Lambda Function to submit a game score for a user.
*/
const submitScore = new Lambda.Function(stack, 'SubmitScore', {
/**
* Accepts a `SubmitScoreRequest`.
*/
request: SubmitScoreRequest,
/**
* Returns `nothing` (equiv. to `void`).
*/
response: nothing,
/**
* Needs read and write access to update user game scores.
*/
const SubmitScoreCall = new Api.Call({
endpoint,
input: SubmitScoreRequest,
output: nothing,
errors: {
InternalServerError
},
depends: UserScores.readWriteAccess()
}, async (request, highScores) => {
const key = new UserGameScore.Key({
userId: request.userId,
gameTitle: request.gameTitle
gameId: request.gameId
});
await update();
try {
await update();
return Ok(null);
} catch (err) {
return Fail('InternalServerError', new InternalServerError({
errorMessage: err.message
}));
}

async function update() {
const gameScore = await highScores.get(key);
console.log('got game score', gameScore);
if (gameScore) {
try {
/**
Expand Down Expand Up @@ -147,7 +268,7 @@ const submitScore = new Lambda.Function(stack, 'SubmitScore', {
* No record exists, so put the initial value.
*/
await highScores.put(new UserGameScore({
gameTitle: request.gameTitle,
gameId: request.gameId,
losses: request.victory ? 0 : 1,
wins: request.victory ? 1 : 0,
topScore: request.score,
Expand All @@ -173,30 +294,40 @@ const submitScore = new Lambda.Function(stack, 'SubmitScore', {
};
});

/**
* POST: /score {
* userId: string,
* gameId: string,
* victory: boolean,
* score: number
* }
*/
const SubmitScore = Score.onCreate(SubmitScoreRequest, request => SubmitScoreCall.call(request));

/**
* Global Secondary Index to lookup a game title's high scores.
*/
const HighScores = UserScores.globalIndex({
indexName: 'high-scores',
key: {
partition: 'gameTitle',
partition: 'gameId',
sort: 'topScore'
}
});

/**
* Helper class for creating and passing around keys of the High Score Index.
*/
class HighScoresKey extends UserGameScore.Pick(['gameTitle', 'topScore']) {}
class HighScoresKey extends UserGameScore.Pick(['gameId', 'topScore']) {}

/**
* A request for a game's high scores.
*/
class GetHighScoresRequest extends Record({
class ListHighScoresRequest extends Record({
/**
* Title of game to query for high scores.
* Id of game to query for high scores.
*/
gameTitle: string,
gameId: string,
/**
* Max number of results to return.
*
Expand All @@ -210,31 +341,37 @@ class GetHighScoresRequest extends Record({
.apply(Maximum(1000))
}) {}

/**
* Lambda Function to get High Scores for a given game.
*/
const getHighScores = new Lambda.Function(stack, 'GetTopN', {
const ListHighScoresHandler = new Api.Call({
endpoint,
input: ListHighScoresRequest,
/**
* Accepts a `GetHighScoresRequest`.
* Returns an array of `UserGameScore` objects.
*/
request: GetHighScoresRequest,
output: array(UserGameScore),
/**
* Returns an array of `UserGameScore` objects.
* Enumeration of errors returned by ListHighScores.
*/
response: array(UserGameScore),
errors: {
InternalServerError
},
/**
* We need reac access to the HighScores index to lookup results.
*/
depends: HighScores.readAccess(),
}, async (request, highScores) => {
const maxResults = request.maxResults === undefined ? 100 : request.maxResults;

return await query([]);
try {
return Ok(await query([]));
} catch (err) {
return Fail('InternalServerError', new InternalServerError({
errorMessage: err.message
}));
}

async function query(scores: UserGameScore[], LastEvaluatedKey?: HighScoresKey): Promise<UserGameScore[]> {
const numberToFetch = Math.min(maxResults - scores.length, 100);
const nextScores = await highScores.query({
gameTitle: request.gameTitle,
gameId: request.gameId,
topScore: _ => _.greaterThan(0)
}, {
Limit: numberToFetch,
Expand All @@ -250,4 +387,11 @@ const getHighScores = new Lambda.Function(stack, 'GetTopN', {

return await query(scores, nextScores.LastEvaluatedKey);
}
});
})

/**
* Lambda Function to get High Scores for a given game.
*
* GET: /score?gameId=<gameId>&maxResults=100
*/
const ListHighScores = Score.onList(ListHighScoresRequest, request => ListHighScoresHandler.call(request));
1 change: 1 addition & 0 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@punchcard/shape": "^0.1.0",
"@punchcard/shape-hive": "^0.1.0",
"@punchcard/shape-json": "^0.1.0",
"@punchcard/shape-velocity-template": "^0.1.0",
"uuid": "^3.3.2"
},
"devDependencies": {
Expand Down
Loading