ops: add dockerfile, makefile and cli tools
This commit is contained in:
172
AGENTS.md
Normal file
172
AGENTS.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# malago
|
||||
|
||||
personal anime tracking platform. go/htmx rewrite.
|
||||
|
||||
## stack
|
||||
|
||||
- go standard library (`net/http`)
|
||||
- htmx + templ templates
|
||||
- sqlite + sqlc
|
||||
- tailwind (dark theme)
|
||||
- jikan api (myanimelist)
|
||||
|
||||
## structure
|
||||
|
||||
```
|
||||
cmd/server/ main entry
|
||||
internal/
|
||||
auth/ sessions, passwords
|
||||
database/ sqlc generated, migrations
|
||||
handlers/ http handlers by domain
|
||||
middleware/ auth, logging
|
||||
jikan/ api client
|
||||
templates/ templ components
|
||||
migrations/ sql files
|
||||
static/ css, js
|
||||
```
|
||||
|
||||
## go patterns
|
||||
|
||||
### errors
|
||||
|
||||
always handle errors explicitly. wrap with context using `fmt.Errorf`:
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch user: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
use `errors.Is` and `errors.As` to check wrapped errors.
|
||||
|
||||
### early returns
|
||||
|
||||
reduce nesting. check errors first, return early:
|
||||
|
||||
```go
|
||||
func getUser(id string) (*User, error) {
|
||||
if id == "" {
|
||||
return nil, ErrInvalidID
|
||||
}
|
||||
|
||||
user, err := db.FindUser(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
```
|
||||
|
||||
### defer for cleanup
|
||||
|
||||
always close resources with defer:
|
||||
|
||||
```go
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
```
|
||||
|
||||
### interfaces
|
||||
|
||||
accept interfaces, return structs. keep interfaces small:
|
||||
|
||||
```go
|
||||
type Reader interface {
|
||||
Read(p []byte) (n int, err error)
|
||||
}
|
||||
```
|
||||
|
||||
### naming
|
||||
|
||||
- short, lowercase package names: `auth`, `jikan`, `db`
|
||||
- `MixedCaps` for exports, `mixedCaps` for internal
|
||||
- getters: `Owner()` not `GetOwner()`
|
||||
- interfaces: single method = method name + `er` suffix (`Reader`, `Writer`)
|
||||
|
||||
### zero values
|
||||
|
||||
design structs so zero value is useful:
|
||||
|
||||
```go
|
||||
var buf bytes.Buffer // ready to use, no init needed
|
||||
buf.WriteString("hello")
|
||||
```
|
||||
|
||||
### composition over inheritance
|
||||
|
||||
embed types to compose behavior:
|
||||
|
||||
```go
|
||||
type Handler struct {
|
||||
db *database.Queries
|
||||
jikan *jikan.Client
|
||||
logger *slog.Logger
|
||||
}
|
||||
```
|
||||
|
||||
## htmx
|
||||
|
||||
check for htmx requests:
|
||||
|
||||
```go
|
||||
func isHTMX(r *http.Request) bool {
|
||||
return r.Header.Get("HX-Request") == "true"
|
||||
}
|
||||
```
|
||||
|
||||
return partials for htmx, full pages otherwise. use `hx-swap-oob` for multiple updates. trigger toasts with `HX-Trigger` header.
|
||||
|
||||
## templ
|
||||
|
||||
render components directly to response:
|
||||
|
||||
```go
|
||||
func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
|
||||
templates.HomePage(data).Render(r.Context(), w)
|
||||
}
|
||||
```
|
||||
|
||||
pass data explicitly. keep components focused. use layouts.
|
||||
|
||||
## database
|
||||
|
||||
sqlc generates type-safe queries. always use parameterized queries.
|
||||
|
||||
tables: `user`, `session`, `account`, `anime`, `watch_list_entry`
|
||||
|
||||
watch statuses: `watching`, `completed`, `on_hold`, `dropped`, `plan_to_watch`
|
||||
|
||||
## jikan api
|
||||
|
||||
rate limit: 3 req/sec max. stagger batch requests. cache in local db.
|
||||
|
||||
## commands
|
||||
|
||||
```bash
|
||||
make dev # hot reload (air)
|
||||
make build # binary
|
||||
make test # tests
|
||||
make migrate # run migrations
|
||||
make sqlc # generate code
|
||||
make create-user # cli user creation
|
||||
```
|
||||
|
||||
## env
|
||||
|
||||
```bash
|
||||
DATABASE_FILE=malago.db
|
||||
SESSION_SECRET=min_32_chars_random
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
## avoid
|
||||
|
||||
- panics in handlers
|
||||
- forgetting `defer resp.Body.Close()`
|
||||
- unstaggered jikan requests (429 errors)
|
||||
- globals for config/state
|
||||
- large monolithic templates
|
||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM golang:1.22-bullseye AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Enable CGO for sqlite3
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
# Build the server and the CLI tools
|
||||
RUN go build -o main_server ./cmd/server
|
||||
RUN go build -o create_user ./cmd/create-user
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Required for sqlite
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /app/main_server .
|
||||
COPY --from=builder /app/create_user .
|
||||
COPY --from=builder /app/static ./static
|
||||
COPY --from=builder /app/migrations ./migrations
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE 3000
|
||||
|
||||
# Run the server
|
||||
CMD ["./main_server"]
|
||||
26
Makefile
Normal file
26
Makefile
Normal file
@@ -0,0 +1,26 @@
|
||||
.PHONY: dev build test migrate sqlc create-user
|
||||
|
||||
dev:
|
||||
air
|
||||
|
||||
build:
|
||||
go build -o main_server ./cmd/server
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
migrate:
|
||||
sqlite3 malago.db < migrations/001_init.sql
|
||||
|
||||
sqlc:
|
||||
sqlc generate
|
||||
|
||||
templ:
|
||||
templ generate
|
||||
|
||||
create-user:
|
||||
@if [ -z "$(EMAIL)" ] || [ -z "$(PASSWORD)" ]; then \
|
||||
echo "Usage: make create-user EMAIL=your@email.com PASSWORD=yourpassword"; \
|
||||
else \
|
||||
go run ./cmd/create-user -email=$(EMAIL) -password=$(PASSWORD); \
|
||||
fi
|
||||
46
cmd/create-user/main.go
Normal file
46
cmd/create-user/main.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"malago/internal/auth"
|
||||
"malago/internal/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
email := flag.String("email", "", "Email/Username for the new user")
|
||||
password := flag.String("password", "", "Password for the new user")
|
||||
flag.Parse()
|
||||
|
||||
if *email == "" || *password == "" {
|
||||
fmt.Println("Usage: make create-user EMAIL=user@example.com PASSWORD=secret")
|
||||
fmt.Println("Or : go run ./cmd/create-user -email=user@example.com -password=secret")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", "malago.db")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
queries := database.New(db)
|
||||
authService := auth.NewService(queries)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Try to create the user
|
||||
user, err := authService.RegisterUser(ctx, *email, *password)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create user: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully created user: %s (ID: %s)\n", user.Username, user.ID)
|
||||
}
|
||||
13
sqlc.yaml
Normal file
13
sqlc.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: "2"
|
||||
sql:
|
||||
- engine: "sqlite"
|
||||
queries: "internal/database/queries.sql"
|
||||
schema: "migrations/"
|
||||
gen:
|
||||
go:
|
||||
package: "database"
|
||||
out: "internal/database"
|
||||
emit_json_tags: true
|
||||
emit_prepared_queries: false
|
||||
emit_interface: true
|
||||
emit_exact_table_names: false
|
||||
Reference in New Issue
Block a user