Skip to content

Commit

Permalink
UI and GraphQL to create and list games (#61)
Browse files Browse the repository at this point in the history
* Workflow fix

* Can display and create game

* Codacy happiness

* Get DOM tests running under Jest

* Codacy happiness
  • Loading branch information
jarrod-lowe authored Aug 31, 2024
1 parent df93607 commit d139f0a
Show file tree
Hide file tree
Showing 23 changed files with 1,620 additions and 75 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/environment-main-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ jobs:
run: npm install -g esbuild

- name: Compile, check and test graphql
run: IN_PIPELINE=true make graphql
run: IN_PIPELINE=true ENVIRONMENT=${{ vars.ENVIRONMENT }} make graphql

- name: Test ui
run: IN_PIPELINE=true make ui-test
run: IN_PIPELINE=true ENVIRONMENT=${{ vars.ENVIRONMENT }} make ui-test

- name: Run codacy-coverage-reporter
uses: codacy/codacy-coverage-reporter-action@a38818475bb21847788496e9f0fddaa4e84955ba
Expand Down Expand Up @@ -67,4 +67,4 @@ jobs:
- name: Push UI
run: |
IN_PIPELINE=true make ui/.push-${{ vars.ENVIRONMENT }}
IN_PIPELINE=true ENVIRONMENT=${{ vars.ENVIRONMENT }} make ui/.push-${{ vars.ENVIRONMENT }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ graphql/node_modules
graphql/mutation/*/appsync.js
graphql/query/*/appsync.js
graphql/coverage/
graphql/environment.json

appsync/node_modules
appsync/.graphqlconfig.yml
Expand All @@ -51,6 +52,7 @@ ui/config/*
!ui/config/.emptyDir
ui/public/*
!ui/public/.emptyDir
!ui/public/style.css
ui/dist/*
!ui/dist/.emptyDir
ui/.push
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
default: all

ENVIRONMENT ?= dev
TERRAFORM_ENVIRONMENTS := aws github wildsea aws-dev wildsea-dev
TERRAFOM_VALIDATE := $(addsuffix /.validate,$(addprefix terraform/environment/, $(TERRAFORM_ENVIRONMENTS)))
TERRAFORM_MODULES := iac-roles oidc state-bucket wildsea
Expand Down
16 changes: 13 additions & 3 deletions appsync/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,25 @@ export type CreateGameInput = {
export type Game = {
__typename?: 'Game';
createdAt: Scalars['AWSDateTime']['output'];
description?: Maybe<Scalars['String']['output']>;
fireflyUserId: Scalars['ID']['output'];
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
gameDescription?: Maybe<Scalars['String']['output']>;
gameId: Scalars['ID']['output'];
gameName: Scalars['String']['output'];
players?: Maybe<Array<Scalars['ID']['output']>>;
privateNotes?: Maybe<Scalars['String']['output']>;
publicNotes?: Maybe<Scalars['String']['output']>;
type: Scalars['String']['output'];
updatedAt: Scalars['AWSDateTime']['output'];
};

export type GameSummary = {
__typename?: 'GameSummary';
gameDescription: Scalars['String']['output'];
gameId: Scalars['ID']['output'];
gameName: Scalars['String']['output'];
type: Scalars['String']['output'];
};

export type Mutation = {
__typename?: 'Mutation';
createGame: Game;
Expand All @@ -53,6 +62,7 @@ export type MutationCreateGameArgs = {
export type Query = {
__typename?: 'Query';
getGame: Game;
getGames?: Maybe<Array<GameSummary>>;
};


Expand Down
12 changes: 10 additions & 2 deletions appsync/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
export const getGameQuery = `
query getGame($id: ID!) {
getGame(id: $id) {
id name description publicNotes privateNotes fireflyUserId players createdAt updatedAt
gameId gameName gameDescription publicNotes privateNotes fireflyUserId players createdAt updatedAt type
}
}
`;

export const getGamesQuery = `
query getGames {
getGames {
gameId gameName gameDescription type
}
}
`;
Expand All @@ -13,7 +21,7 @@
export const createGameMutation = `
mutation createGame($input: CreateGameInput!) {
createGame(input: $input) {
id name description publicNotes privateNotes fireflyUserId players createdAt updatedAt
gameId gameName gameDescription publicNotes privateNotes fireflyUserId players createdAt updatedAt type
}
}
`;
Expand Down
45 changes: 25 additions & 20 deletions design/PLANNING.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,23 @@ Assistant:
2. **Game**
* `PK`: `GAME#<gameId>`
* `SK`: `METADATA`
* Other fields: `name`, `description`, `publicNotes`, `privateNotes`, `fireflyUserId`, `createdAt`, `updatedAt`, `GSI2PK`, `GSI2SK`
* `gameId`: gameId
* Other fields: `gameName`, `gameDescription`, `publicNotes`, `privateNotes`, `fireflyUserId`, `createdAt`, `updatedAt`, `GSI1PK`

3. **Player Sheet**
3. **GM Info**
* `PK`: `GAME#<gameId>`
* `SK`: `PLAYER#<userId>`
* `SK`: `PLAYER#GM#<userId>`
* `GSI1PK`: `USER#<userId>`
* `userId`: userId
* `gameId`: gameId
* gameName and gameDescription: Duplicated from game

4. **Player Sheet**
* `PK`: `GAME#<gameId>`
* `SK`: `PLAYER#PC#<userId>`
* `GSI1PK`: `USER#<userId>`
* `userId`: userId
* `gameId`: gameId
* `characterName` (String)
* `pronouns` (String)
* `bloodline` (String)
Expand All @@ -327,9 +339,10 @@ Assistant:
* Each map in the list represents an aspect and contains fields like `name` (String), `type` (String: "Trait", "Gear", or "Companion"), `track` (Map with fields like `length` (Number) and `currentValue` (Number)), `description` (String)
* `temporaryTracks` (List of Maps)
* Each map in the list represents a temporary track and contains fields like `name` (String), `type` (String: "Benefit", "Injury", or "Track"), `track` (Map with fields like `length` (Number) and `currentValue` (Number)), `description` (String)
* Other fields: `createdAt`, `updatedAt`, `GSI1PK`, `GSI1SK`
* Other fields: `createdAt`, `updatedAt`
* gameName and gameDescription: Duplicated from game

4. **Ship Sheet**
5. **Ship Sheet**
* `PK`: `GAME#<gameId>`
* `SK`: `SHIP#<shipId>`
* `name` (String)
Expand All @@ -353,23 +366,23 @@ Assistant:
* `passengers` (List of Strings)
* Other fields: `createdAt`, `updatedAt`

5. **Clock**
6. **Clock**
* `PK`: `GAME#<gameId>`
* `SK`: `CLOCK#<clockId>`
* Other fields: `name`, `lengths`, `visibility`, `gmNotes`, `createdAt`, `updatedAt`

6. **Dice Roll**
7. **Dice Roll**
* `PK`: `GAME#<gameId>`
* `SK`: `DICEROLL#<timeSortableGuid>`
* Other fields: `playerCharacterName`, `numRolled`, `numCut`, `result`, `twist`, `rollValues`, `createdAt`

7. **Seed Data**
8. **Seed Data**
* `PK`: `SEED#<type>`
* `SK`: `<id>`
* Other fields: vary based on the type, e.g., `name`, `description`, `trackLength`, etc.
* `<type>`'s include: `MIRE`, `EDGE`, `SKILL`, `LANGUAGE`, `RESOURCE`, `ASPECT`, `RATING`, `DESIGN`, `UNDERCREW`, `FITTINGTYPE`

8. **Level Name**
9. **Level Name**
* `PK`: `LEVELNAME#<type>`
* `SK`: `<numDots>`
* Other fields: `name`
Expand All @@ -384,18 +397,10 @@ No LSIs are required for this table design.
1. **GSI1: UserGameIndex**
* Partition Key: `GSI1PK` (String)
* Sort Key: `GSI1SK` (String)
* For Player Sheet records:
* For GM or Player Sheet records:
* `GSI1PK`: `USER#<userId>`
* `GSI1SK`: `GAME#<gameId>`
* This index allows us to query all games where the user is a player.

2. **GSI2: FireflyGameIndex**
* Partition Key: `GSI2PK` (String)
* Sort Key: `GSI2SK` (String)
* For Game records:
* `GSI2PK`: `USER#<fireflyUserId>`
* `GSI2SK`: `GAME#<gameId>`
* This index allows us to query all games where the user is the firefly.
* The GSI SK is `PK`
* This index allows us to query all games where the user is a firefly or player.

In this updated design, we have two generic record types:

Expand Down
8 changes: 6 additions & 2 deletions graphql/graphql.mk
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
graphql/%/appsync.js: graphql/node_modules graphql/%/appsync.ts appsync/graphql.ts appsync/schema.ts
graphql/%/appsync.js: graphql/node_modules graphql/%/appsync.ts appsync/graphql.ts appsync/schema.ts graphql/environment.json
cd graphql && \
esbuild $*/*.ts \
--bundle \
Expand All @@ -10,6 +10,10 @@ graphql/%/appsync.js: graphql/node_modules graphql/%/appsync.ts appsync/graphql.
--sources-content=false \
--outdir=$*

.PRECIOUS: graphql/environment.json
graphql/environment.json:
echo '{"name":"$(ENVIRONMENT)"}' >$@

graphql/node_modules: graphql/package.json
cd graphql && npm install && npm ci \

Expand All @@ -22,7 +26,7 @@ graphql: $(GRAPHQL_JS) appsync/graphql.ts appsync/schema.ts graphql-test
echo $(GRAPHQL_JS)

.PHONY: graphql-test
graphql-test: graphql/node_modules appsync/graphql.ts appsync/schema.ts
graphql-test: graphql/node_modules appsync/graphql.ts appsync/schema.ts graphql/environment.json
if [ -z "$(IN_PIPELINE)" ] ; then \
docker run --rm -it --user $$(id -u):$$(id -g) -v $(PWD):/app -w /app/graphql --entrypoint ./node_modules/jest/bin/jest.js node:20 --coverage ; \
else \
Expand Down
72 changes: 55 additions & 17 deletions graphql/mutation/createGame/appsync.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { util, Context, AppSyncIdentityCognito } from "@aws-appsync/utils";
import type {
DynamoDBPutItemRequest,
PutItemInputAttributeMap,
} from "@aws-appsync/utils/lib/resolver-return-types";
import type { PutItemInputAttributeMap } from "@aws-appsync/utils/lib/resolver-return-types";
import environment from "../../environment.json";
import { Game } from "../../../appsync/graphql";

/**
* A CreateGameInput creates a Game.
Expand All @@ -16,9 +15,7 @@ interface CreateGameInput {
description?: string;
}

export function request(
context: Context<{ input: CreateGameInput }>,
): DynamoDBPutItemRequest {
export function request(context: Context<{ input: CreateGameInput }>): unknown {
if (!context.identity) {
util.error("Unauthorized: Identity information is missing." as string);
}
Expand All @@ -31,25 +28,66 @@ export function request(
const input = context.arguments.input;
const id = util.autoId();
const timestamp = util.time.nowISO8601();
return {
operation: "PutItem",

context.stash.record = {
gameName: input.name,
gameDescription: input.description,
gameId: id,
fireflyUserId: identity.sub,
// players: no value yet
createdAt: timestamp,
updatedAt: timestamp,
type: "GAME",
};

const gameItem = {
key: util.dynamodb.toMapValues({ PK: "GAME#" + id, SK: "GAME" }),
operation: "PutItem",
table: "Wildsea-" + environment.name,
attributeValues: util.dynamodb.toMapValues(
context.stash.record,
) as PutItemInputAttributeMap,
};

const fireflyItem = {
key: util.dynamodb.toMapValues({
PK: "GAME#" + id,
SK: "PLAYER#GM#" + identity.sub,
}),
operation: "PutItem",
table: "Wildsea-" + environment.name,
attributeValues: util.dynamodb.toMapValues({
name: input.name,
description: input.description,
id: id,
fireflyUserId: identity.sub,
// players: no value yet
userId: identity.sub,
gameId: id,
gameName: input.name,
gameDescription: input.description,
GSI1PK: "USER#" + identity.sub,
createdAt: timestamp,
updatedAt: timestamp,
type: "CHARACTER",
}) as PutItemInputAttributeMap,
};

return {
operation: "TransactWriteItems",
transactItems: [gameItem, fireflyItem],
};
}

export function response(context: Context): unknown {
export function response(context: Context): Game | null {
if (context.error) {
util.appendError(context.error.message, context.error.type, context.result);
return;
return null;
}
return context.result;
return {
PK: context.result.keys[0].PK,
SK: context.result.keys[0].SK,
gameName: context.stash.record.gameName,
gameDescription: context.stash.record.gameDescription,
gameId: context.stash.record.gameId,
fireflyUserId: context.stash.record.fireflyUserId,
createdAt: context.stash.record.createdAt,
updatedAt: context.stash.record.updatedAt,
type: context.stash.record.type,
} as Game;
}
36 changes: 36 additions & 0 deletions graphql/query/getGames/appsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { util, Context, AppSyncIdentityCognito } from "@aws-appsync/utils";
import type { DynamoDBQueryRequest } from "@aws-appsync/utils/lib/resolver-return-types";
import type { GameSummary } from "../../../appsync/graphql";

export function request(context: Context): DynamoDBQueryRequest {
if (!context.identity) {
util.error("Unauthorized: Identity information is missing." as string);
}

const identity = context.identity as AppSyncIdentityCognito;
if (!identity.sub) {
util.error("Unauthorized: User ID is missing." as string);
}

return {
operation: "Query",
index: "GSI1",
query: {
expression: "#gsi1pk = :gsi1pk",
expressionNames: {
"#gsi1pk": "GSI1PK",
},
expressionValues: {
":gsi1pk": util.dynamodb.toString("USER#" + identity.sub),
},
},
};
}

export function response(context: Context): GameSummary[] {
if (context.error) {
util.appendError(context.error.message, context.error.type, context.result);
return [];
}
return context.result.items as GameSummary[];
}
15 changes: 12 additions & 3 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Mutation {

type Query {
getGame(id: ID!): Game! @aws_cognito_user_pools
getGames: [GameSummary!] @aws_cognito_user_pools
}

input CreateGameInput {
Expand All @@ -30,9 +31,9 @@ input CreateGameInput {
}

type Game @aws_cognito_user_pools {
id: ID!
name: String!
description: String
gameId: ID!
gameName: String!
gameDescription: String
publicNotes: String
privateNotes: String
fireflyUserId: ID!
Expand All @@ -42,4 +43,12 @@ type Game @aws_cognito_user_pools {
# clocks: [Clock!]!
createdAt: AWSDateTime!
updatedAt: AWSDateTime!
type: String!
}

type GameSummary @aws_cognito_user_pools {
gameId: ID!
gameName: String!
gameDescription: String!
type: String!
}
Loading

0 comments on commit d139f0a

Please sign in to comment.