Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Description

Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.

Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

- [ ] Test A
- [ ] Test B

# Performance (Optional)

If your change affects performance, please include benchmark results here:
```
http_req_duration.......: avg=1.23ms min=1.01ms med=1.15ms max=5.43ms p(90)=1.34ms p(95)=1.45ms
```

# Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes (`make test`)
- [ ] Any dependent changes have been merged and published in downstream modules
- [ ] I have run `make lint` and fixed any issues
- [ ] I have run `make migrate-up` (if applicable) and verified database changes
- [ ] I have run `make swagger` (if applicable) and updated API docs
- [ ] My commit messages follow [Conventional Commits](https://www.conventionalcommits.org/)
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@
*.swp

# OS
.DS_Store
.DS_Store

coverage.out

/bin

*.log
228 changes: 228 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# Contributing to Sal

Welcome to the Sal project! This document guides you through our development workflow, codebase structure, and standards.

## 🚀 Quick Start

1. **Prerequisites**:
* Go 1.22+
* Docker & Docker Compose
* Make

2. **Initial Setup**:
```bash
# 1. Start Database Container
make docker-up

# 2. Run Database Migrations
make migrate-up

# 3. Start the API Server
make run
```
The API will be available at `http://localhost:8000`.
Swagger UI: `http://localhost:8000/swagger/index.html`.

---

## 🛠️ Development Workflow

We use `make` to automate common tasks. Run these frequently!

- **`make run`**: Starts the API server locally.
- **`make test`**: Runs all unit tests. **Always run this before pushing.**
- **`make lint`**: Runs `golangci-lint` to check for style and potential bugs.
- **`make swagger`**: Regenerates Swagger documentation. Run this after changing API handlers.
- **`make migrate-up`**: Applies pending database migrations.

---

## 📂 Project Structure

- **`cmd/api`**: Entry point. Contains `main.go` and `server.go` (router setup).
- **`internal/handler`**: HTTP layer. Parses requests, validates input, calls business logic, sends responses.
- **`internal/repository`**: Data access layer. Executes SQL queries using `pgx`.
- **`internal/database`**: Database connection pool configuration.
- **`internal/config`**: Configuration loading from `.env`.
- **`internal/response`**: Helper utils for standard JSON responses.
- **`migrations/`**: SQL migration files (managed by `goose`).

---

## 🏗️ How to Add a New Feature

This guide walks you through implementing a feature from scratch.
**Scenario**: *"Allow a user to create a new organization."*

### Step 1: Database Migration
If your feature needs new tables or columns, start here.

1. Create a new migration file in `migrations/`.
2. Follow the naming convention: `YYYYMMDDHHMMSS_name.sql`.
3. Add `Up` and `Down` SQL commands.

```sql
-- migrations/20240220120000_create_orgs.sql
-- +goose Up
CREATE TABLE IF NOT EXISTS organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
owner_user_id UUID NOT NULL REFERENCES users(id)
);

-- +goose Down
DROP TABLE IF EXISTS organizations;
```
> **Run**: `make migrate-up` to apply changes.

### Step 2: Repository Layer
Implement the database logic in `internal/repository/`.

1. Define the struct matching your table.
2. Add a method to the Repository struct.

```go
// internal/repository/orgs.go
package repository

type Organization struct {
ID string `json:"id"`
Name string `json:"name"`
OwnerID string `json:"owner_id"`
}

func (r *OrganizationRepository) CreateOrg(ctx context.Context, org *Organization) error {
query := `INSERT INTO organizations (name, owner_user_id) VALUES ($1, $2) RETURNING id`
// We use r.db.Pool for queries
return r.db.Pool.QueryRow(ctx, query, org.Name, org.OwnerID).Scan(&org.ID)
}
```

### Step 3: Handler Layer
Implement the HTTP logic in `internal/handler/`.

1. Define the request payload struct with validation tags.
2. Create the handler function.
3. **Validation**: Use `validator` tags (e.g., `validate:"required,email"`).
4. **Response**: Use `response.JSON` or `response.Error`.

```go
// internal/handler/org.go
package handler

type CreateOrgRequest struct {
Name string `json:"name" validate:"required"`
}

// CreateOrg handles POST /api/v1/orgs
// ... (Add Swagger comments here) ...
func (h *AuthHandler) CreateOrg(w http.ResponseWriter, r *http.Request) {
// 1. Parse Body
var input CreateOrgRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
response.Error(w, http.StatusBadRequest, "Invalid Body")
return
}

// 2. Validate
if err := h.Validator.Struct(input); err != nil {
response.ValidationError(w, err)
return
}

// 3. Call Repository
org := &repository.Organization{
Name: input.Name,
OwnerID: r.Context().Value("user_id").(string),
}

if err := h.OrgRepo.CreateOrg(r.Context(), org); err != nil {
// Log the error internally here if you have a logger
response.Error(w, http.StatusInternalServerError, "Failed to create organization")
return
}

// 4. Send Success Response
response.JSON(w, http.StatusCreated, org)
}
```

### Step 4: Routing
Wire up your new handler in `cmd/api/server.go`.

```go
// cmd/api/server.go
func (s *Server) routes() {
s.Router.Route("/api/v1", func(r chi.Router) {
// Mount under /orgs
r.Post("/orgs", authHandler.CreateOrg)
})
}
```

### Step 5: Update Documentation
1. Add Swagger comments to your handler function (see `internal/handler/auth.go` for examples).
2. Run `make swagger`.
3. Check `docs/swagger.json` changes.

---

## 🚀 Performance Testing

You can measure the performance of your endpoints using `make benchmark`.

### Usage
```bash
# Test a specific endpoint
make benchmark ENDPOINT=/api/v1/health

# Test Authentication Flow (Register -> Login) with high load
make benchmark-auth
```

### Understanding Results
The output will show:
- **http_req_duration**: Total time for the request. Look at `p(95)` (95th percentile).
- **http_req_failed**: Should be 0%.

Please include these stats in your PR for performance-critical changes.

### Do I need to write a script for every endpoint?
**No!** You don't need to write a custom k6 script for every single endpoint.

1. **For simple GET requests:** Just use the generic command:
```bash
make benchmark ENDPOINT=/api/v1/beneficiaries
```
2. **For complex flows (POST/PUT):** You only need to write a custom script (like `scripts/k6-auth.js`) for **critical paths** like Login, Registration, or Checkout. You can copy an existing script and just change the payload.

---

## 📏 Coding Standards

### Configuration
- **Environment Variables**: Defined in `.env`.
- **Loading**: Add new variables to the `Config` struct in `internal/config/config.go`.
- **Usage**: Access via `s.Config.MyVar` in `server.go` and pass to handlers.

### Error Handling
- Use `response.Error(w, status, msg)` for standard errors.
- Use `response.ValidationError(w, err)` for validation errors.
- Do not expose internal DB errors to the client (e.g., "sql: no rows in result set"). Map them to user-friendly messages.

### Git Conventions
- We use **Conventional Commits**.
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation only
- `chore`: Maintain/cleanup
- **Example**: `feat(auth): add password reset flow`

---

## ✅ Before Submitting a PR
1. Run `make lint` to ensure code style.
2. Run `make test` to ensure no regressions.
3. Run `make swagger` if you changed APIs.
4. Fill out the Pull Request Template checklist.
38 changes: 30 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
# Environment variables
include .env
export

# Helpers
GO_CMD=go
GO_RUN=$(GO_CMD) run
GO_TEST=$(GO_CMD) test
GO_BUILD=$(GO_CMD) build
MIGRATE_CMD=$(GO_RUN) ./cmd/migrate/main.go
API_CMD=$(GO_RUN) ./cmd/api

# Database Connection (for psql)
DB_DSN=$(DATABASE_URL)
Expand All @@ -29,7 +26,7 @@ build: ## Build the API binary

run: ## Run the API locally
@echo "Starting API..."
$(GO_RUN) ./cmd/api
$(API_CMD)

test: ## Run unit tests with coverage
@echo "Running tests..."
Expand All @@ -38,7 +35,11 @@ test: ## Run unit tests with coverage

lint: ## Run golangci-lint
@echo "Running linter..."
golangci-lint run
$(shell go env GOPATH)/bin/golangci-lint run

clean: ## Clean build artifacts
rm -rf bin/ docs/ docs.go


# database
migrate-up: ## Apply all pending migrations
Expand Down Expand Up @@ -66,7 +67,28 @@ docker-logs: ## View container logs
# docs
docs-serve: ## Serve documentation locally (pkgsite)
@echo "Opening http://localhost:6060/github.com/off-by-2/sal"
pkgsite -http=:6060
$(shell go env GOPATH)/bin/pkgsite -http=:6060

docs-generate: ## Generate API Reference markdown (requires gomarkdoc)
gomarkdoc --output docs/reference.md ./...
$(shell go env GOPATH)/bin/gomarkdoc --output docs/reference.md $(shell go list ./...)

swagger: ## Generate Swagger docs
$(shell go env GOPATH)/bin/swag init -g cmd/api/main.go --output docs

# performance
benchmark: ## Run load test on an endpoint (Usage: make benchmark ENDPOINT=/health)
@if [ -z "$(ENDPOINT)" ]; then echo "Error: ENDPOINT is not set. Usage: make benchmark ENDPOINT=/health"; exit 1; fi
@echo "Running benchmark on http://host.docker.internal:8000$(ENDPOINT)..."
@docker run --rm -i \
-e ENDPOINT=$(ENDPOINT) \
-v $(PWD)/scripts/k6.js:/k6.js \
--add-host=host.docker.internal:host-gateway \
grafana/k6 run /k6.js

benchmark-auth: ## Run auth load test (Usage: make benchmark-auth)
@echo "Running auth benchmark..."
@docker run --rm -i \
-v $(PWD)/scripts/k6-auth.js:/auth.js \
--add-host=host.docker.internal:host-gateway \
grafana/k6 run /auth.js

18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ make run
make test
```

## 🚀 Before Opening a PR

Ensure your code is clean and tested:

```bash
# 1. Format and Lint
make lint

# 2. Run Tests
make test

# 3. Update Documentation (Swagger)
make swagger

# 4. Verify Build
make build
```

## Development Commands

We use `make` for common tasks:
Expand Down
Loading
Loading