Functional Web Apps
The revenge of dynamic web apps
Simon MacDonald
@macdonst
What is a dynamic web app?
-
Not static! HTML is rendered on-demand
-
Completely required for building an API
-
Database backed (full stack!)
Three-Tiered Architecture
Model
View
Controller
Traditional dynamic web app ecosystem
Rails Logical Architecture
Model
View
Controller
Rails Physical Architecture
Rails Physical Architecture
Traditional dynamic app problems
-
3 tier logical architecture is not the same as the physical infra
-
Difficult to deploy when infra is not versioned
-
Slow rolling deployment
-
Servers are difficult to scale horizontally
-
-
Slow to render HTML because traditional databases are slow
-
Maintenance cost is huge and not fun (patching, updating, debugging, etc)
-
Background jobs are required for performance but a clunky bolt on
-
pub/sub, queues, cron
-
📘Book Rec
⭐⭐⭐⭐⭐
Enter the
Pre-rendering HTML and served via CDN. Dynamic functionality initiated by JavaScript at runtime.
1
Totally static
Authortime build of all presentation/business logic delivered as static assets
2
Immutable deployment
Empowers the frontend web developers
3
Outsource backend
JAMstack logical architecture
JAMstack physical architecture
JAMstack
immutable deployment
JAMstack tradeoffs
-
Slow build
-
Dynamic is a second class citizen
-
Complex ecosystem of libraries, tooling and services need glue
The Rebound Effect
In conservation and energy economics, the rebound effect (or take-back effect) is the reduction in expected gains from new technologies that increase the efficiency of resource use, because of behavioral or other systemic responses. These responses diminish the beneficial effects of the new technology or other measures taken.
Rebound Effect
The expected gains from a new JavaScript framework is always less because of the amount of JavaScript being deployed on the client. This additional JavaScript diminishes the beneficial effects of the new technology and in some cases makes things worse.
The
-
HTML first (dynamic personalization and a11y is the priority)
-
Cloud function centric mode
-
On-demand database
-
Declarative deployment (explicitly defined Infra as Code)
FWA: A Different Approach
Functional Web App logical architecture
Functional Web App physical architecture
Functional Web App advantages
-
Power: build with a full-stack
-
Way more fun! Less maintaining, more shipping
-
Inclusive, fast, and accessible web consumer experience
But what about…
-
Coldstart *
-
Database story still emergent (DynamoDB, Cosmos, Planetscale, FaunaDB)
-
Declarative Infra-as-Code solutions are oft complex
-
All-in with managed services means all-in with a cloud vendor
* there are two solutions to coldstart for AWS Lambda: write small functions or pre-provision capacity
A growing ecosystem
-
AWS SAM
-
Azure Functions
-
Begin
-
Cloudflare
-
Deno Deploy
-
GCP
Functional Web Apps
in the wild
Static App → Functional Web App
-
A modern architectural pattern for dynamic apps:
-
HTML-first progressive enhancement
-
Cloud functions centered development model
-
Managed database built-in
-
Explicit declarative deployment (aka Infra-as-Code or IaC)
-
Architect
Cloudformation
@app
arc-fnh
@static
fingerprint true
folder public
@http
get /
get /components/*
get /players
get /players/add
post /players
post /players/:id/delete
post /players/delete
get /games
get /games/add
get /games/:id
post /games
post /games/:id
post /games/:id/delete
post /games/delete
get /import
post /import
get /invite/:id
get /seasons
get /seasons/add
post /seasons
post /seasons/delete
post /seasons/:id/delete
@events
find-spares
send-email
@scheduled
weekly-reminder cron(0 8 ? * WED *)
weekly-roster cron(0 10 ? * FRI *)
@plugins
arc-plugin-oauth
architect/plugin-lambda-invoker
@oauth
use-mock true
allow-list allow.mjs
@tables
players
email *String
games
gamedate *String
seasons
seasonID *String
invites
inviteID *String
expiresAt TTL
@tables-indexes
players
fulltime *String
name playersByFulltime
games
gamedate *String
name gamesByDate
seasons
seasonID *String
name seasonsByID
# invites
# email *String
# name invitesByEmail
@aws
# profile default
region us-west-2
architecture arm64
runtime nodejs16.x
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Description": "Exported by architect/package@8.0.3 on 2022-04-01T20:25:58.390Z",
"Resources": {
"Role": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Policies": [
{
"PolicyName": "ArcGlobalPolicy",
"PolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
},
{
"PolicyName": "ArcStaticBucketPolicy",
"PolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:PutObjectAcl",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
{
"Fn::Sub": [
"arn:aws:s3:::${bukkit}",
{
"bukkit": {
"Ref": "StaticBucket"
}
}
]
},
{
"Fn::Sub": [
"arn:aws:s3:::${bukkit}/*",
{
"bukkit": {
"Ref": "StaticBucket"
}
}
]
}
]
}
]
}
},
{
"PolicyName": "ArcDynamoPolicy",
"PolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:UpdateItem",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:DescribeStream",
"dynamodb:ListStreams"
],
"Resource": [
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}",
{
"tablename": {
"Ref": "PlayersTable"
}
}
]
},
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*",
{
"tablename": {
"Ref": "PlayersTable"
}
}
]
},
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/stream/*",
{
"tablename": {
"Ref": "PlayersTable"
}
}
]
},
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}",
{
"tablename": {
"Ref": "GamesTable"
}
}
]
},
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*",
{
"tablename": {
"Ref": "GamesTable"
}
}
]
},
{
"Fn::Sub": [
"arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/stream/*",
{
"tablename": {
"Ref": "GamesTable"
}
}
]
}
]
}
]
}
}
]
}
},
"PlayersParam": {
"Type": "AWS::SSM::Parameter",
"Properties": {
"Type": "String",
"Name": {
"Fn::Sub": [
"/${AWS::StackName}/tables/${tablename}",
{
"tablename": "players"
}
]
},
"Value": {
"Ref": "PlayersTable"
}
}
},
"GamesParam": {
"Type": "AWS::SSM::Parameter",
"Properties": {
"Type": "String",
"Name": {
"Fn::Sub": [
"/${AWS::StackName}/tables/${tablename}",
{
"tablename": "games"
}
]
},
"Value": {
"Ref": "GamesTable"
}
}
},
"StaticBucketParam": {
"Type": "AWS::SSM::Parameter",
"Properties": {
"Type": "String",
"Name": {
"Fn::Sub": [
"/${AWS::StackName}/static/${key}",
{
"key": "bucket"
}
]
},
"Value": {
"Ref": "StaticBucket"
}
}
},
"StaticFingerprintParam": {
"Type": "AWS::SSM::Parameter",
"Properties": {
"Type": "String",
"Name": {
"Fn::Sub": [
"/${AWS::StackName}/static/${key}",
{
"key": "fingerprint"
}
]
},
"Value": "true"
}
},
"ParameterStorePolicy": {
"Type": "AWS::IAM::Policy",
"DependsOn": "Role",
"Properties": {
"PolicyName": "ArcParameterStorePolicy",
"PolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetParametersByPath",
"ssm:GetParameter"
],
"Resource": {
"Fn::Sub": [
"arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}",
{}
]
}
},
{
"Effect": "Allow",
"Action": [
"ssm:GetParametersByPath",
"ssm:GetParameter"
],
"Resource": {
"Fn::Sub": [
"arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*",
{}
]
}
},
{
"Effect": "Allow",
"Action": [
"ssm:GetParametersByPath",
"ssm:GetParameter"
],
"Resource": {
"Fn::Sub": [
"arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*/*",
{}
]
}
}
]
},
"Roles": [
{
"Ref": "Role"
}
]
}
},
"HTTP": {
"Type": "AWS::Serverless::HttpApi",
"Properties": {
"StageName": "$default",
"DefinitionBody": {
"openapi": "3.0.1",
"info": {
"title": {
"Ref": "AWS::StackName"
}
},
"paths": {
"/games/add": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetGamesAddHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/players/add": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetPlayersAddHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/components/{proxy+}": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetComponentsCatchallHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/games/{id}": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetGamesIdHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
},
"post": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostGamesIdHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/auth": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetAuthHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/games": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetGamesHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
},
"post": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostGamesHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/login": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetLoginHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/players": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetPlayersHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
},
"post": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostPlayersHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetIndexHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/players/{id}/delete": {
"post": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostPlayersIdDeleteHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/games/{id}/delete": {
"post": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostGamesIdDeleteHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/players/{id}": {
"post": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostPlayersIdHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/logout": {
"post": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostLogoutHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/mock/auth/{part}": {
"x-amazon-apigateway-any-method": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "2.0",
"type": "aws_proxy",
"httpMethod": "POST",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AnyMockAuthPartHTTPLambda.Arn}/invocations"
},
"connectionType": "INTERNET"
}
}
},
"/_static/{proxy+}": {
"get": {
"x-amazon-apigateway-integration": {
"payloadFormatVersion": "1.0",
"type": "http_proxy",
"httpMethod": "GET",
"uri": {
"Fn::Sub": [
"http://${bukkit}.s3.${AWS::Region}.amazonaws.com/{proxy}",
{
"bukkit": {
"Ref": "StaticBucket"
}
}
]
},
"connectionType": "INTERNET",
"timeoutInMillis": 30000
}
}
}
}
}
}
},
"GetGamesAddHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-games-add",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"GetGamesAddHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/games/add",
"Method": "GET",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"GetPlayersAddHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-players-add",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"GetPlayersAddHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/players/add",
"Method": "GET",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"GetComponentsCatchallHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-components-catchall",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"GetComponentsCatchallHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/components/{proxy+}",
"Method": "GET",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"GetGamesIdHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-games-000id",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"GetGamesIdHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/games/{id}",
"Method": "GET",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"GetAuthHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/node_modules/arc-plugin-oauth/src/src/http/get-auth",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"GetAuthHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/auth",
"Method": "GET",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"GetGamesHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-games",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"GetGamesHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/games",
"Method": "GET",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"GetLoginHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/node_modules/arc-plugin-oauth/src/src/http/get-login",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"GetLoginHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/login",
"Method": "GET",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"GetPlayersHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-players",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"GetPlayersHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/players",
"Method": "GET",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"GetIndexHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/get-index",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_STATIC_SPA": false,
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"GetIndexHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/",
"Method": "GET",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"PostPlayersIdDeleteHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-players-000id-delete",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"PostPlayersIdDeleteHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/players/{id}/delete",
"Method": "POST",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"PostGamesIdDeleteHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-games-000id-delete",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"PostGamesIdDeleteHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/games/{id}/delete",
"Method": "POST",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"PostPlayersIdHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-players-000id",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"PostPlayersIdHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/players/{id}",
"Method": "POST",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"PostGamesIdHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-games-000id",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"PostGamesIdHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/games/{id}",
"Method": "POST",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"PostGamesHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-games",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"PostGamesHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/games",
"Method": "POST",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"PostLogoutHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/node_modules/arc-plugin-oauth/src/src/http/post-logout",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"PostLogoutHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/logout",
"Method": "POST",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"PostPlayersHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/src/http/post-players",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"PostPlayersHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/players",
"Method": "POST",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"AnyMockAuthPartHTTPLambda": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "index.handler",
"CodeUri": "/Users/simonmacdonald/Developer/macdonst/arc-fnh/node_modules/arc-plugin-oauth/src/src/http/get-mock-auth-000part",
"Runtime": "nodejs14.x",
"Architectures": [
"arm64"
],
"MemorySize": 1152,
"Timeout": 5,
"Environment": {
"Variables": {
"ARC_APP_NAME": "arc-fnh",
"ARC_ENV": "staging",
"ARC_ROLE": {
"Ref": "Role"
},
"ARC_SESSION_TABLE_NAME": "jwe",
"ARC_STACK_NAME": {
"Ref": "AWS::StackName"
},
"ARC_STATIC_BUCKET": {
"Ref": "StaticBucket"
},
"ARC_OAUTH_CLIENT_ID": "74528a8f82952e0346c9",
"ARC_OAUTH_CLIENT_SECRET": "3d54da78c796bc6bb59eda5fff1f9a1167b665e6",
"ARC_OAUTH_REDIRECT_URL": "https://bb2smqni8c.execute-api.us-west-2.amazonaws.com/auth",
"ARC_OAUTH_INCLUDE_PROPERTIES": "[\"login\"]",
"ARC_OAUTH_CUSTOM_AUTHORIZE": "",
"ARC_OAUTH_MATCH_PROPERTY": "login",
"ARC_OAUTH_AFTER_AUTH": "/",
"ARC_OAUTH_UN_AUTH_REDIRECT": "/login",
"ARC_OAUTH_USE_ALLOW_LIST": "true",
"ARC_OAUTH_ALLOW_LIST": "allow.mjs",
"ARC_OAUTH_TOKEN_URI": "https://github.com/login/oauth/access_token",
"ARC_OAUTH_USER_INFO_URI": "https://api.github.com/user"
}
},
"Role": {
"Fn::Sub": [
"arn:aws:iam::${AWS::AccountId}:role/${roleName}",
{
"roleName": {
"Ref": "Role"
}
}
]
},
"Events": {
"AnyMockAuthPartHTTPEvent": {
"Type": "HttpApi",
"Properties": {
"Path": "/mock/auth/{part}",
"Method": "ANY",
"ApiId": {
"Ref": "HTTP"
}
}
}
}
}
},
"PlayersTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"KeySchema": [
{
"AttributeName": "email",
"KeyType": "HASH"
}
],
"AttributeDefinitions": [
{
"AttributeName": "email",
"AttributeType": "S"
},
{
"AttributeName": "fulltime",
"AttributeType": "S"
}
],
"BillingMode": "PAY_PER_REQUEST",
"GlobalSecondaryIndexes": [
{
"IndexName": "playersByFulltime",
"KeySchema": [
{
"AttributeName": "fulltime",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
}
}
]
}
},
"GamesTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"KeySchema": [
{
"AttributeName": "gamedate",
"KeyType": "HASH"
}
],
"AttributeDefinitions": [
{
"AttributeName": "gamedate",
"AttributeType": "S"
}
],
"BillingMode": "PAY_PER_REQUEST",
"GlobalSecondaryIndexes": [
{
"IndexName": "gamesByDate",
"KeySchema": [
{
"AttributeName": "gamedate",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
}
}
]
}
},
"StaticBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"OwnershipControls": {
"Rules": [
{
"ObjectOwnership": "BucketOwnerEnforced"
}
]
},
"WebsiteConfiguration": {
"IndexDocument": "index.html",
"ErrorDocument": "404.html"
}
}
},
"StaticBucketPolicy": {
"Type": "AWS::S3::BucketPolicy",
"Properties": {
"Bucket": {
"Ref": "StaticBucket"
},
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:GetObject"
],
"Effect": "Allow",
"Principal": "*",
"Resource": [
{
"Fn::Sub": [
"arn:aws:s3:::${bukkit}/*",
{
"bukkit": {
"Ref": "StaticBucket"
}
}
]
}
],
"Sid": "PublicReadGetObject"
}
]
}
}
}
},
"Outputs": {
"API": {
"Description": "API Gateway (HTTP)",
"Value": {
"Fn::Sub": [
"https://${ApiId}.execute-api.${AWS::Region}.amazonaws.com",
{
"ApiId": {
"Ref": "HTTP"
}
}
]
}
},
"ApiId": {
"Description": "API ID (ApiId)",
"Value": {
"Ref": "HTTP"
}
},
"BucketURL": {
"Description": "Bucket URL",
"Value": {
"Fn::Sub": [
"http://${bukkit}.s3-website-${AWS::Region}.amazonaws.com",
{
"bukkit": {
"Ref": "StaticBucket"
}
}
]
}
}
}
}
GET /players
import arc from '@architect/functions'
import render from '@architect/views/render.mjs'
import { getFulltimePlayers, getSpares } from '@architect/shared/db/players.mjs'
import arcOauth from 'arc-plugin-oauth'
const auth = arcOauth.auth
export const handler = arc.http.async(auth, players)
async function players(req) {
const { type = 'fulltime' } = req.query
const players =
type === 'fulltime' ? await getFulltimePlayers() : await getSpares()
const initialState = { account: req.session?.account }
return {
html: render(
`
<form method="POST" action="/players/delete">
<hockey-page>
<hockey-action-buttons direction="row-reverse">
<hockey-button icon="delete">Delete</hockey-button>
<hockey-action-button action="/players/add" icon="plus" label="Add" type="link" variant="default"></hockey-action-button>
</hockey-action-buttons>
<enhance-table>
<enhance-thead>
<enhance-tr><enhance-th width="1rem"> </enhance-th><enhance-th>Name</enhance-th><enhance-th class="unseen">Email</enhance-th><enhance-th class="unseen">Phone</enhance-th><enhance-th>Position</enhance-th></enhance-tr>
</enhance-thead>
<enhance-tbody>
${players
.map(
(player) =>
`<enhance-tr>
<enhance-td>
<input type="checkbox" name="todelete" class="leading5-l pt-3 pb-3 pl-1 pr-1 radius2 shadow-2" value="${
player.email
}"/>
</enhance-td>
<enhance-td><a class="color-blue" href="/players/add?id=${
player.email
}">${player.name} ${
player.preferred === 'true'
? `<hockey-icon icon="star" style="width: 1rem; height: 1rem; display: inline;"></hockey-icon>`
: ''
}</a></enhance-td>
<enhance-td class="unseen">${player.email}</enhance-td>
<enhance-td class="unseen">${player.phone}</enhance-td>
<enhance-td class="capitalize">${
player.position
}</enhance-td>
</enhance-tr>`
)
.join('')}
</enhance-tbody>
</enhance-table>
</hockey-page>
</form>
`,
initialState
)
}
}
POST /players
import arc from '@architect/functions'
import { upsertPlayer } from '@architect/shared/db/players.mjs'
import arcOauth from 'arc-plugin-oauth'
const auth = arcOauth.auth
export const handler = arc.http.async(auth, http)
async function http(req) {
await upsertPlayer(req.body)
const location =
req.body.fulltime === 'true' ? '/players' : '/players?type=spares'
return {
location
}
}
Learn More
Check out https://github.com/architect/architect
Build your first app on https://begin.com
Read https://fwa.dev
Tell me how wrong I am
Thanks!
Functional Web Apps
By Simon MacDonald
Functional Web Apps
- 740