Changed: DB Params

This commit is contained in:
2025-03-20 12:35:13 +01:00
parent 8640a12439
commit b71b3d12ca
822 changed files with 134218 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
FROM pierrezemb/gostatic
COPY ./public/ /srv/http/
ENTRYPOINT ["/goStatic", "-port", "8080"]

View File

@@ -0,0 +1,25 @@
## Tasks
### generate
```sh
templ generate
```
### deploy
requires: generate
dir: cdk
```sh
cdk deploy
```
### deploy-hotswap
requires: generate
dir: cdk
```sh
cdk deploy --hotswap
```

11851
templ/examples/counter/assets/css/bulma.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v16/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/assets/favicon/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/assets/favicon/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

19
templ/examples/counter/cdk/.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# go.sum should be committed
!go.sum
# CDK asset staging directory
.cdk.staging
cdk.out

View File

@@ -0,0 +1,47 @@
{
"app": "go mod download && go run stack.go",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"go.mod",
"go.sum",
"**/*test.go"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true
}
}

View File

@@ -0,0 +1,136 @@
package main
import (
"fmt"
"github.com/aws/aws-cdk-go/awscdk/v2"
"github.com/aws/aws-cdk-go/awscdk/v2/awscloudfront"
"github.com/aws/aws-cdk-go/awscdk/v2/awscloudfrontorigins"
"github.com/aws/aws-cdk-go/awscdk/v2/awsdynamodb"
"github.com/aws/aws-cdk-go/awscdk/v2/awsiam"
"github.com/aws/aws-cdk-go/awscdk/v2/awslambda"
"github.com/aws/aws-cdk-go/awscdk/v2/awss3"
"github.com/aws/aws-cdk-go/awscdk/v2/awss3deployment"
awslambdago "github.com/aws/aws-cdk-go/awscdklambdagoalpha/v2"
"github.com/aws/constructs-go/constructs/v10"
"github.com/aws/jsii-runtime-go"
)
type CounterStackProps struct {
awscdk.StackProps
}
func NewCounterStack(scope constructs.Construct, id string, props *CounterStackProps) awscdk.Stack {
var sprops awscdk.StackProps
if props != nil {
sprops = props.StackProps
}
stack := awscdk.NewStack(scope, &id, &sprops)
// Create a global count database.
db := awsdynamodb.NewTable(stack, jsii.String("count"), &awsdynamodb.TableProps{
PartitionKey: &awsdynamodb.Attribute{
Name: jsii.String("_pk"),
Type: awsdynamodb.AttributeType_STRING,
},
BillingMode: awsdynamodb.BillingMode_PAY_PER_REQUEST,
// Change this for production systems.
RemovalPolicy: awscdk.RemovalPolicy_DESTROY,
TimeToLiveAttribute: jsii.String("_ttl"),
})
// Strip the binary, and remove the deprecated Lambda SDK RPC code for performance.
// These options are not required, but make cold start faster.
bundlingOptions := &awslambdago.BundlingOptions{
GoBuildFlags: &[]*string{jsii.String(`-ldflags "-s -w" -tags lambda.norpc`)},
}
f := awslambdago.NewGoFunction(stack, jsii.String("handler"), &awslambdago.GoFunctionProps{
Runtime: awslambda.Runtime_PROVIDED_AL2(),
MemorySize: jsii.Number(1024),
Architecture: awslambda.Architecture_ARM_64(),
Entry: jsii.String("../lambda"),
Bundling: bundlingOptions,
Environment: &map[string]*string{
"TABLE_NAME": db.TableName(),
},
})
// Grant DB access.
db.GrantReadWriteData(f)
// Add a Function URL.
lambdaURL := f.AddFunctionUrl(&awslambda.FunctionUrlOptions{
AuthType: awslambda.FunctionUrlAuthType_NONE,
})
awscdk.NewCfnOutput(stack, jsii.String("lambdaFunctionUrl"), &awscdk.CfnOutputProps{
ExportName: jsii.String("lambdaFunctionUrl"),
Value: lambdaURL.Url(),
})
assetsBucket := awss3.NewBucket(stack, jsii.String("assets"), &awss3.BucketProps{
BlockPublicAccess: awss3.BlockPublicAccess_BLOCK_ALL(),
Encryption: awss3.BucketEncryption_S3_MANAGED,
EnforceSSL: jsii.Bool(true),
RemovalPolicy: awscdk.RemovalPolicy_DESTROY,
Versioned: jsii.Bool(false),
})
// Allow CloudFront to read from the bucket.
cfOAI := awscloudfront.NewOriginAccessIdentity(stack, jsii.String("cfnOriginAccessIdentity"), &awscloudfront.OriginAccessIdentityProps{})
cfs := awsiam.NewPolicyStatement(&awsiam.PolicyStatementProps{})
cfs.AddActions(jsii.String("s3:GetBucket*"))
cfs.AddActions(jsii.String("s3:GetObject*"))
cfs.AddActions(jsii.String("s3:List*"))
cfs.AddResources(assetsBucket.BucketArn())
cfs.AddResources(jsii.String(fmt.Sprintf("%v/*", *assetsBucket.BucketArn())))
cfs.AddCanonicalUserPrincipal(cfOAI.CloudFrontOriginAccessIdentityS3CanonicalUserId())
assetsBucket.AddToResourcePolicy(cfs)
// Add a CloudFront distribution to route between the public directory and the Lambda function URL.
lambdaURLDomain := awscdk.Fn_Select(jsii.Number(2), awscdk.Fn_Split(jsii.String("/"), lambdaURL.Url(), nil))
lambdaOrigin := awscloudfrontorigins.NewHttpOrigin(lambdaURLDomain, &awscloudfrontorigins.HttpOriginProps{
ProtocolPolicy: awscloudfront.OriginProtocolPolicy_HTTPS_ONLY,
})
cf := awscloudfront.NewDistribution(stack, jsii.String("customerFacing"), &awscloudfront.DistributionProps{
DefaultBehavior: &awscloudfront.BehaviorOptions{
AllowedMethods: awscloudfront.AllowedMethods_ALLOW_ALL(),
Origin: lambdaOrigin,
CachedMethods: awscloudfront.CachedMethods_CACHE_GET_HEAD(),
OriginRequestPolicy: awscloudfront.OriginRequestPolicy_ALL_VIEWER_EXCEPT_HOST_HEADER(),
CachePolicy: awscloudfront.CachePolicy_CACHING_DISABLED(),
ViewerProtocolPolicy: awscloudfront.ViewerProtocolPolicy_REDIRECT_TO_HTTPS,
},
PriceClass: awscloudfront.PriceClass_PRICE_CLASS_100,
})
// Add /assets* to the distribution backed by S3.
assetsOrigin := awscloudfrontorigins.NewS3Origin(assetsBucket, &awscloudfrontorigins.S3OriginProps{
// Get content from the / directory in the bucket.
OriginPath: jsii.String("/"),
OriginAccessIdentity: cfOAI,
})
cf.AddBehavior(jsii.String("/assets*"), assetsOrigin, nil)
// Export the domain.
awscdk.NewCfnOutput(stack, jsii.String("cloudFrontDomain"), &awscdk.CfnOutputProps{
ExportName: jsii.String("cloudfrontDomain"),
Value: cf.DomainName(),
})
// Deploy the contents of the ./assets directory to the S3 bucket.
awss3deployment.NewBucketDeployment(stack, jsii.String("assetsDeployment"), &awss3deployment.BucketDeploymentProps{
DestinationBucket: assetsBucket,
Sources: &[]awss3deployment.ISource{
awss3deployment.Source_Asset(jsii.String("../assets"), nil),
},
DestinationKeyPrefix: jsii.String("assets"),
Distribution: cf,
DistributionPaths: jsii.Strings("/assets*"),
})
return stack
}
func main() {
defer jsii.Close()
app := awscdk.NewApp(nil)
NewCounterStack(app, "CounterStack", &CounterStackProps{})
app.Synth(nil)
}

View File

@@ -0,0 +1,62 @@
package components
import "strconv"
css border() {
border: 1px solid #eeeeee;
border-radius: 4px;
margin: 10px;
padding-top: 30px;
padding-bottom: 30px;
}
templ counts(global, session int) {
<form id="countsForm" action="/" method="POST" hx-post="/" hx-select="#countsForm" hx-swap="outerHTML">
<div class="columns">
<div class={ "column", "has-text-centered", "is-primary", border }>
<h1 class="title is-size-1 has-text-centered">{ strconv.Itoa(global) }</h1>
<p class="subtitle has-text-centered">Global</p>
<div><button class="button is-primary" type="submit" name="global" value="global">+1</button></div>
</div>
<div class={ "column", "has-text-centered", border }>
<h1 class="title is-size-1 has-text-centered">{ strconv.Itoa(session) }</h1>
<p class="subtitle has-text-centered">Session</p>
<div><button class="button is-secondary" type="submit" name="session" value="session">+1</button></div>
</div>
</div>
</form>
}
templ Page(global, session int) {
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Counts</title>
<link rel="stylesheet" href="/assets/css/bulma.min.css"/>
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png"/>
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon/favicon-32x32.png"/>
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png"/>
<link rel="manifest" href="/assets/favicon/site.webmanifest"/>
<script src="/assets/js/htmx.min.js"></script>
</head>
<body class="bg-gray-100">
<header class="hero is-primary">
<div class="hero-body">
<div class="container">
<h1 class="title">Counts</h1>
</div>
</div>
</header>
<section class="section">
<div class="container">
<div class="columns is-centered">
<div class="column is-half">
@counts(global, session)
</div>
</div>
</div>
</section>
</body>
</html>
}

View File

@@ -0,0 +1,163 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "strconv"
func border() templ.CSSClass {
templ_7745c5c3_CSSBuilder := templruntime.GetBuilder()
templ_7745c5c3_CSSBuilder.WriteString(`border:1px solid #eeeeee;`)
templ_7745c5c3_CSSBuilder.WriteString(`border-radius:4px;`)
templ_7745c5c3_CSSBuilder.WriteString(`margin:10px;`)
templ_7745c5c3_CSSBuilder.WriteString(`padding-top:30px;`)
templ_7745c5c3_CSSBuilder.WriteString(`padding-bottom:30px;`)
templ_7745c5c3_CSSID := templ.CSSID(`border`, templ_7745c5c3_CSSBuilder.String())
return templ.ComponentCSSClass{
ID: templ_7745c5c3_CSSID,
Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`),
}
}
func counts(global, session int) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form id=\"countsForm\" action=\"/\" method=\"POST\" hx-post=\"/\" hx-select=\"#countsForm\" hx-swap=\"outerHTML\"><div class=\"columns\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 = []any{"column", "has-text-centered", "is-primary", border}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/examples/counter/components/components.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><h1 class=\"title is-size-1 has-text-centered\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(global))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/examples/counter/components/components.templ`, Line: 17, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</h1><p class=\"subtitle has-text-centered\">Global</p><div><button class=\"button is-primary\" type=\"submit\" name=\"global\" value=\"global\">+1</button></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 = []any{"column", "has-text-centered", border}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/examples/counter/components/components.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"><h1 class=\"title is-size-1 has-text-centered\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(session))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/examples/counter/components/components.templ`, Line: 22, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h1><p class=\"subtitle has-text-centered\">Session</p><div><button class=\"button is-secondary\" type=\"submit\" name=\"session\" value=\"session\">+1</button></div></div></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func Page(global, session int) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Counts</title><link rel=\"stylesheet\" href=\"/assets/css/bulma.min.css\"><link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/assets/favicon/apple-touch-icon.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/assets/favicon/favicon-32x32.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/assets/favicon/favicon-16x16.png\"><link rel=\"manifest\" href=\"/assets/favicon/site.webmanifest\"><script src=\"/assets/js/htmx.min.js\"></script></head><body class=\"bg-gray-100\"><header class=\"hero is-primary\"><div class=\"hero-body\"><div class=\"container\"><h1 class=\"title\">Counts</h1></div></div></header><section class=\"section\"><div class=\"container\"><div class=\"columns is-centered\"><div class=\"column is-half\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = counts(global, session).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></div></div></section></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -0,0 +1,189 @@
package db
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
type OptionsFunc func(*CountStore)
func WithClient(client *dynamodb.Client) func(*CountStore) {
return func(ms *CountStore) {
ms.db = client
}
}
func NewCountStore(tableName, region string, options ...OptionsFunc) (s *CountStore, err error) {
s = &CountStore{
tableName: tableName,
}
for _, o := range options {
o(s)
}
if s.db == nil {
cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region))
if err != nil {
return s, err
}
s.db = dynamodb.NewFromConfig(cfg)
}
return
}
type CountStore struct {
db *dynamodb.Client
tableName string
}
func stripEmpty(strings []string) (op []string) {
for _, s := range strings {
if s != "" {
op = append(op, s)
}
}
return
}
type countRecord struct {
PK string `dynamodbav:"_pk"`
Count int `dynamodbav:"count"`
}
func (s CountStore) BatchGet(ctx context.Context, ids ...string) (counts []int, err error) {
nonEmptyIDs := stripEmpty(ids)
if len(nonEmptyIDs) == 0 {
return nil, nil
}
// Make DynamoDB keys.
ris := make(map[string]types.KeysAndAttributes)
for _, id := range nonEmptyIDs {
ri := ris[s.tableName]
ri.Keys = append(ris[s.tableName].Keys, map[string]types.AttributeValue{
"_pk": &types.AttributeValueMemberS{
Value: id,
},
})
ri.ConsistentRead = aws.Bool(true)
ris[s.tableName] = ri
}
// Execute the batch request.
var batchResponses []map[string]types.AttributeValue
// DynamoDB might not process everything, so we need a loop.
var unprocessedAttempts int
for {
var bgio *dynamodb.BatchGetItemOutput
bgio, err = s.db.BatchGetItem(ctx, &dynamodb.BatchGetItemInput{
RequestItems: ris,
})
if err != nil {
return
}
for _, responses := range bgio.Responses {
batchResponses = append(batchResponses, responses...)
}
if len(bgio.UnprocessedKeys) > 0 {
ris = bgio.UnprocessedKeys
unprocessedAttempts++
if unprocessedAttempts > 3 {
err = fmt.Errorf("countstore: exceeded three attempts to get all counts")
return
}
continue
}
break
}
// Process the responses into structs.
crs := []countRecord{}
err = attributevalue.UnmarshalListOfMaps(batchResponses, &crs)
if err != nil {
err = fmt.Errorf("countstore: failed to unmarshal result of BatchGet: %w", err)
return
}
// Match up the inputs to the records.
idToCount := make(map[string]int, len(ids))
for _, cr := range crs {
idToCount[cr.PK] = cr.Count
}
// Create the output in the right order.
// Missing values are defaulted to zero.
for _, id := range ids {
counts = append(counts, idToCount[id])
}
return
}
func (s CountStore) Get(ctx context.Context, id string) (count int, err error) {
if id == "" {
return
}
gio, err := s.db.GetItem(ctx, &dynamodb.GetItemInput{
Key: map[string]types.AttributeValue{
"_pk": &types.AttributeValueMemberS{
Value: id,
},
},
TableName: &s.tableName,
ConsistentRead: aws.Bool(true),
})
if err != nil || gio.Item == nil {
return
}
var cr countRecord
err = attributevalue.UnmarshalMap(gio.Item, &cr)
if err != nil {
return 0, fmt.Errorf("countstore: failed to process result of Get: %w", err)
}
count = cr.Count
return
}
func (s CountStore) Increment(ctx context.Context, id string) (count int, err error) {
if id == "" {
return
}
uio, err := s.db.UpdateItem(ctx, &dynamodb.UpdateItemInput{
Key: map[string]types.AttributeValue{
"_pk": &types.AttributeValueMemberS{
Value: id,
},
},
TableName: &s.tableName,
UpdateExpression: aws.String("SET #c = if_not_exists(#c, :zero) + :one"),
ExpressionAttributeNames: map[string]string{
"#c": "count",
},
ExpressionAttributeValues: map[string]types.AttributeValue{
":zero": &types.AttributeValueMemberN{Value: "0"},
":one": &types.AttributeValueMemberN{Value: "1"},
},
ReturnValues: types.ReturnValueAllNew,
})
if err != nil {
return
}
// Parse the response.
var cr countRecord
err = attributevalue.UnmarshalMap(uio.Attributes, &cr)
if err != nil {
return 0, fmt.Errorf("countstore: failed to process result of Increment: %w", err)
}
count = cr.Count
return
}

View File

@@ -0,0 +1,53 @@
module github.com/a-h/templ/examples/counter
go 1.23
toolchain go1.23.3
require (
github.com/a-h/templ v0.2.234-0.20230427112944-80f0dc03a8a8
github.com/akrylysov/algnhsa v1.1.0
github.com/aws/aws-cdk-go/awscdk/v2 v2.147.3
github.com/aws/aws-cdk-go/awscdklambdagoalpha/v2 v2.147.3-alpha.0
github.com/aws/aws-sdk-go-v2 v1.30.1
github.com/aws/aws-sdk-go-v2/config v1.27.24
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.7
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.1
github.com/aws/constructs-go/constructs/v10 v10.3.0
github.com/aws/jsii-runtime-go v1.101.0
github.com/segmentio/ksuid v1.0.4
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
)
require (
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/aws/aws-lambda-go v1.47.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.24 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.202 // indirect
github.com/cdklabs/awscdk-asset-kubectl-go/kubectlv20/v2 v2.1.2 // indirect
github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.0.3 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/yuin/goldmark v1.7.4 // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/tools v0.24.0 // indirect
)
replace github.com/a-h/templ => ../../

View File

@@ -0,0 +1,109 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/akrylysov/algnhsa v1.1.0 h1:G0SoP16tMRyiism7VNc3JFA0wq/cVgEkp/ExMVnc6PQ=
github.com/akrylysov/algnhsa v1.1.0/go.mod h1:+bOweRs/WBu5awl+ifCoSYAuKVPAmoTk8XOMrZ1xwiw=
github.com/aws/aws-cdk-go/awscdk/v2 v2.147.3 h1:7Wbi5d1f+RGn6fg9YzzMjD5D/rc/62zuFOtmJIed+B0=
github.com/aws/aws-cdk-go/awscdk/v2 v2.147.3/go.mod h1:WF3lt7ah4wNktbClICIBbKdITtCqyCrPBQl3nkaLug4=
github.com/aws/aws-cdk-go/awscdklambdagoalpha/v2 v2.147.3-alpha.0 h1:j+eT+oCMXTQZL+9ERI1FBDxkFApHPZTYZhtj9zalvlo=
github.com/aws/aws-cdk-go/awscdklambdagoalpha/v2 v2.147.3-alpha.0/go.mod h1:uUP2KJ0yXDOlAl5VmSN36cSpw5Ajv10JVhPl1M7WJ6A=
github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI=
github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
github.com/aws/aws-sdk-go-v2 v1.30.1 h1:4y/5Dvfrhd1MxRDD77SrfsDaj8kUkkljU7XE83NPV+o=
github.com/aws/aws-sdk-go-v2 v1.30.1/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/config v1.27.24 h1:NM9XicZ5o1CBU/MZaHwFtimRpWx9ohAUAqkG6AqSqPo=
github.com/aws/aws-sdk-go-v2/config v1.27.24/go.mod h1:aXzi6QJTuQRVVusAO8/NxpdTeTyr/wRcybdDtfUwJSs=
github.com/aws/aws-sdk-go-v2/credentials v1.17.24 h1:YclAsrnb1/GTQNt2nzv+756Iw4mF8AOzcDfweWwwm/M=
github.com/aws/aws-sdk-go-v2/credentials v1.17.24/go.mod h1:Hld7tmnAkoBQdTMNYZGzztzKRdA4fCdn9L83LOoigac=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.7 h1:pPhmvNKbgb9l5VHcPmMx9g+FHtRbY+ba2J6GefXQGEI=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.7/go.mod h1:OZU7QRvIYXhKry99PttkDTQyN8yCo8RzYjhIKHdQXoo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 h1:Aznqksmd6Rfv2HQN9cpqIV/lQRMaIpJkLLaJ1ZI76no=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9/go.mod h1:WQr3MY7AxGNxaqAtsDWn+fBxmd4XvLkzeqQ8P1VM0/w=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 h1:5SAoZ4jYpGH4721ZNoS1znQrhOfZinOhc4XuTXx/nVc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13/go.mod h1:+rdA6ZLpaSeM7tSg/B0IEDinCIBJGmW8rKDFkYpP04g=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 h1:WIijqeaAO7TYFLbhsZmi2rgLEAtWOC1LhxCAVTJlSKw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13/go.mod h1:i+kbfa76PQbWw/ULoWnp51EYVWH4ENln76fLQE3lXT8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.1 h1:Szwz1vpZkvfhFMJ0X5uUECgHeUmPAxk1UGqAVs/pARw=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.1/go.mod h1:b4wouGyJlzkr2HAvPrDGgYNp1EtmlXOkzhEOvl0c0FQ=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.1 h1:jfkCLx62YWL6bSOkT7aEDKNAX3OwWomlThCxQNBPvbY=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.1/go.mod h1:dLPiMfhRZhblwOeKqdNde7K9jl/pMuIGCGAwC6vQOIo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.14 h1:X1J0Kd17n1PeXeoArNXlvnKewCyMvhVQh7iNMy6oi3s=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.14/go.mod h1:VYMN7l7dxp6xtQRjqIau6d7QAbmPG+yJ75GtCy70f18=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 h1:I9zMeF107l0rJrpnHpjEiiTSCKYAIw8mALiXcPsGBiA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15/go.mod h1:9xWJ3Q/S6Ojusz1UIkfycgD1mGirJfLLKqq3LPT7WN8=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 h1:p1GahKIjyMDZtiKoIn0/jAj/TkMzfzndDv5+zi2Mhgc=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.1/go.mod h1:/vWdhoIoYA5hYoPZ6fm7Sv4d8701PiG5VKe8/pPJL60=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 h1:ORnrOK0C4WmYV/uYt3koHEWBLYsRDwk2Np+eEoyV4Z0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2/go.mod h1:xyFHA4zGxgYkdD73VeezHt3vSKEG9EmFnGwoKlP00u4=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 h1:+woJ607dllHJQtsnJLi52ycuqHMwlW+Wqm2Ppsfp4nQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.1/go.mod h1:jiNR3JqT15Dm+QWq2SRgh0x0bCNSRP2L25+CqPNpJlQ=
github.com/aws/constructs-go/constructs/v10 v10.3.0 h1:LsjBIMiaDX/vqrXWhzTquBJ9pPdi02/H+z1DCwg0PEM=
github.com/aws/constructs-go/constructs/v10 v10.3.0/go.mod h1:GgzwIwoRJ2UYsr3SU+JhAl+gq5j39bEMYf8ev3J+s9s=
github.com/aws/jsii-runtime-go v1.101.0 h1:x4rWNWRz7uDhVN0qSO7T6cG0VAhQ9300s5DjWUrXmWY=
github.com/aws/jsii-runtime-go v1.101.0/go.mod h1:4L4Qmve/HSwM5hXV5ZowR2gBNb9zqkUtycaaN6aZ3mg=
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.202 h1:VixXB9DnHN8oP7pXipq8GVFPjWCOdeNxIaS/ZyUwTkI=
github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.202/go.mod h1:iPUti/SWjA3XAS3CpnLciFjS8TN9Y+8mdZgDfSgcyus=
github.com/cdklabs/awscdk-asset-kubectl-go/kubectlv20/v2 v2.1.2 h1:k+WD+6cERd59Mao84v0QtRrcdZuuSMfzlEmuIypKnVs=
github.com/cdklabs/awscdk-asset-kubectl-go/kubectlv20/v2 v2.1.2/go.mod h1:CvFHBo0qcg8LUkJqIxQtP1rD/sNGv9bX3L2vHT2FUAo=
github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.0.3 h1:8NLWOIVaxAtpUXv5reojlAeDP7R8yswm9mDONf7F/3o=
github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.0.3/go.mod h1:ZjFqfhYpCLzh4z7ChcHCrkXfqCuEiRlNApDfJd6plts=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,81 @@
package handlers
import (
"context"
"net/http"
"github.com/a-h/templ/examples/counter/components"
"github.com/a-h/templ/examples/counter/services"
"github.com/a-h/templ/examples/counter/session"
"golang.org/x/exp/slog"
)
type CountService interface {
Increment(ctx context.Context, it services.IncrementType, sessionID string) (counts services.Counts, err error)
Get(ctx context.Context, sessionID string) (counts services.Counts, err error)
}
func New(log *slog.Logger, cs CountService) *DefaultHandler {
return &DefaultHandler{
Log: log,
CountService: cs,
}
}
type DefaultHandler struct {
Log *slog.Logger
CountService CountService
}
func (h *DefaultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
h.Post(w, r)
return
}
h.Get(w, r)
}
func (h *DefaultHandler) Get(w http.ResponseWriter, r *http.Request) {
var props ViewProps
var err error
props.Counts, err = h.CountService.Get(r.Context(), session.ID(r))
if err != nil {
h.Log.Error("failed to get counts", slog.Any("error", err))
http.Error(w, "failed to get counts", http.StatusInternalServerError)
return
}
h.View(w, r, props)
}
func (h *DefaultHandler) Post(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// Decide the action to take based on the button that was pressed.
var it services.IncrementType
if r.Form.Has("global") {
it = services.IncrementTypeGlobal
}
if r.Form.Has("session") {
it = services.IncrementTypeSession
}
counts, err := h.CountService.Increment(r.Context(), it, session.ID(r))
if err != nil {
h.Log.Error("failed to increment", slog.Any("error", err))
http.Error(w, "failed to increment", http.StatusInternalServerError)
return
}
// Display the view.
h.View(w, r, ViewProps{
Counts: counts,
})
}
type ViewProps struct {
Counts services.Counts
}
func (h *DefaultHandler) View(w http.ResponseWriter, r *http.Request, props ViewProps) {
components.Page(props.Counts.Global, props.Counts.Session).Render(r.Context(), w)
}

View File

@@ -0,0 +1,30 @@
package main
import (
"os"
"github.com/a-h/templ/examples/counter/db"
"github.com/a-h/templ/examples/counter/handlers"
"github.com/a-h/templ/examples/counter/services"
"github.com/a-h/templ/examples/counter/session"
"github.com/akrylysov/algnhsa"
"golang.org/x/exp/slog"
)
func main() {
// Create handlers.
log := slog.New(slog.NewJSONHandler(os.Stderr, nil))
s, err := db.NewCountStore(os.Getenv("TABLE_NAME"), os.Getenv("AWS_REGION"))
if err != nil {
log.Error("failed to create store", slog.Any("error", err))
os.Exit(1)
}
cs := services.NewCount(log, s)
h := handlers.New(log, cs)
// Add session middleware.
sh := session.NewMiddleware(h)
// Start Lambda.
algnhsa.ListenAndServe(sh, nil)
}

View File

@@ -0,0 +1,43 @@
package main
import (
"fmt"
"net/http"
"os"
"time"
"github.com/a-h/templ/examples/counter/db"
"github.com/a-h/templ/examples/counter/handlers"
"github.com/a-h/templ/examples/counter/services"
"github.com/a-h/templ/examples/counter/session"
"golang.org/x/exp/slog"
)
func main() {
log := slog.New(slog.NewJSONHandler(os.Stderr, nil))
s, err := db.NewCountStore(os.Getenv("TABLE_NAME"), os.Getenv("AWS_REGION"))
if err != nil {
log.Error("failed to create store", slog.Any("error", err))
os.Exit(1)
}
cs := services.NewCount(log, s)
h := handlers.New(log, cs)
var secureFlag = true
if os.Getenv("SECURE_FLAG") == "false" {
secureFlag = false
}
// Add session middleware.
sh := session.NewMiddleware(h, session.WithSecure(secureFlag))
server := &http.Server{
Addr: "localhost:9000",
Handler: sh,
ReadTimeout: time.Second * 10,
WriteTimeout: time.Second * 10,
}
fmt.Printf("Listening on %v\n", server.Addr)
server.ListenAndServe()
}

View File

@@ -0,0 +1,84 @@
package services
import (
"context"
"errors"
"fmt"
"sync"
"github.com/a-h/templ/examples/counter/db"
"golang.org/x/exp/slog"
)
type Counts struct {
Global int
Session int
}
type IncrementType int
const (
IncrementTypeUnknown IncrementType = iota
IncrementTypeGlobal
IncrementTypeSession
)
var ErrUnknownIncrementType error = errors.New("unknown increment type")
func NewCount(log *slog.Logger, cs *db.CountStore) Count {
return Count{
Log: log,
CountStore: cs,
}
}
type Count struct {
Log *slog.Logger
CountStore *db.CountStore
}
func (cs Count) Increment(ctx context.Context, it IncrementType, sessionID string) (counts Counts, err error) {
// Work out which operations to do.
var global, session func(ctx context.Context, id string) (count int, err error)
switch it {
case IncrementTypeGlobal:
global = cs.CountStore.Increment
session = cs.CountStore.Get
case IncrementTypeSession:
global = cs.CountStore.Get
session = cs.CountStore.Increment
default:
return counts, ErrUnknownIncrementType
}
// Run the operations in parallel.
var wg sync.WaitGroup
wg.Add(2)
errs := make([]error, 2)
go func() {
defer wg.Done()
counts.Global, errs[0] = global(ctx, "global")
}()
go func() {
defer wg.Done()
counts.Session, errs[1] = session(ctx, sessionID)
}()
wg.Wait()
return counts, errors.Join(errs...)
}
func (cs Count) Get(ctx context.Context, sessionID string) (counts Counts, err error) {
globalAndSessionCounts, err := cs.CountStore.BatchGet(ctx, "global", sessionID)
if err != nil {
err = fmt.Errorf("countservice: failed to get counts: %w", err)
return
}
if len(globalAndSessionCounts) != 2 {
err = fmt.Errorf("countservice: unexpected counts returned, expected 2, got %d", len(globalAndSessionCounts))
return
}
counts.Global = globalAndSessionCounts[0]
counts.Session = globalAndSessionCounts[1]
return
}

View File

@@ -0,0 +1,56 @@
package session
import (
"net/http"
"github.com/segmentio/ksuid"
)
type MiddlewareOpts func(*Middleware)
func NewMiddleware(next http.Handler, opts ...MiddlewareOpts) http.Handler {
mw := Middleware{
Next: next,
Secure: true,
HTTPOnly: true,
}
for _, opt := range opts {
opt(&mw)
}
return mw
}
func WithSecure(secure bool) MiddlewareOpts {
return func(m *Middleware) {
m.Secure = secure
}
}
func WithHTTPOnly(httpOnly bool) MiddlewareOpts {
return func(m *Middleware) {
m.HTTPOnly = httpOnly
}
}
type Middleware struct {
Next http.Handler
Secure bool
HTTPOnly bool
}
func ID(r *http.Request) (id string) {
cookie, err := r.Cookie("sessionID")
if err != nil {
return
}
return cookie.Value
}
func (mw Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
id := ID(r)
if id == "" {
id = ksuid.New().String()
http.SetCookie(w, &http.Cookie{Name: "sessionID", Value: id, Secure: mw.Secure, HttpOnly: mw.HTTPOnly})
}
mw.Next.ServeHTTP(w, r)
}