Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Backend
BACKEND_PORT=8000

# Frontend
FRONTEND_PORT=3000

# Database
DATABASE_URL=postgresql+psycopg://paform:paform@postgres:5432/paform

# CORS
CORS_ALLOW_ORIGINS=https://localhost:3000,https://paform.local

# Hygraph
HYGRAPH_ENDPOINT=
HYGRAPH_TOKEN=
HYGRAPH_WEBHOOK_SECRET=
135 changes: 66 additions & 69 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,84 +1,81 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
branches: [ "**" ]
push:
branches: [ "main" ]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
pre-commit:
backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.5.21"
enable-cache: true
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }}
- name: Install backend deps
run: pip install -U pip && pip install -e ./backend[dev]
- name: Lint (ruff/black)
run: |
ruff check backend
black --check backend
- name: Tests + coverage
run: pytest backend/tests -q --cov=backend --cov-report=xml
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: backend-coverage
path: coverage.xml

- name: Install dependencies and run pre-commit
run: |
cd backend
uv sync --dev
source .venv/bin/activate
cd ..
pre-commit run --all-files

backend-tests:
frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./backend

steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.5.21"
enable-cache: true
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20" }
- name: Cache pnpm
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: corepack enable
- run: pnpm install --frozen-lockfile
working-directory: frontend
- run: pnpm run lint
working-directory: frontend
- run: pnpm run build
working-directory: frontend

- name: Install dependencies
run: |
uv sync --dev
source .venv/bin/activate

- name: Test with pytest
run: |
source .venv/bin/activate
pytest

frontend-tests:
openapi-and-docs:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./frontend

needs: [backend]
steps:
- uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: ./frontend/package-lock.json

- name: Install dependencies
run: npm ci
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- name: Generate OpenAPI snapshot
run: python backend/scripts/generate_openapi.py
- name: Build docs (mkdocs)
run: |
pip install mkdocs mkdocs-material
mkdocs build --strict

- name: Lint
run: npm run lint

- name: Build
run: npm run build
docker-images:
runs-on: ubuntu-latest
needs: [backend, frontend]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Build backend image
run: docker build -t ghcr.io/${{ github.repository }}/backend:latest -f backend/Dockerfile .
- name: Build frontend image
run: docker build -t ghcr.io/${{ github.repository }}/frontend:latest -f frontend/Dockerfile .
101 changes: 14 additions & 87 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,89 +1,16 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 # Use the ref you want to point at
hooks:
- id: trailing-whitespace
- id: check-ast
- id: check-builtin-literals
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: debug-statements
- id: end-of-file-fixer
- id: mixed-line-ending
args: [--fix=lf]
- id: check-byte-order-marker
- id: check-merge-conflict
- id: check-symlinks
- id: detect-private-key
- id: requirements-txt-fixer
- id: check-yaml
args: [--unsafe]
- id: check-toml

- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.6.12
hooks:
- id: uv-lock

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.11.2'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
types_or: [python, jupyter]
- id: ruff-format
types_or: [python, jupyter]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
hooks:
- id: mypy
entry: python3 -m mypy --config-file backend/pyproject.toml
language: system
types: [python]
exclude: 'tests'

- repo: https://github.com/crate-ci/typos
rev: v1 # v1.19.0
hooks:
- id: typos
args: []

- repo: https://github.com/nbQA-dev/nbQA
rev: 1.9.1
hooks:
- id: nbqa-ruff
args: [--fix, --exit-non-zero-on-fix]

- repo: local
hooks:
- id: prettier-js-format
name: prettier-js-format
entry: npm run format:fix
files: 'app/'
language: node
types: [javascript]
additional_dependencies:
- npm
- prettier

- repo: local
hooks:
- id: nextjs-lint
name: Next.js Lint
entry: npm run lint
language: system
types: [javascript, jsx, tsx]
pass_filenames: false

ci:
autofix_commit_msg: |
[pre-commit.ci] Add auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
autofix_prs: true
autoupdate_branch: ''
autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate'
autoupdate_schedule: weekly
skip: [mypy, prettier-js-format, nextjs-lint]
submodules: false
rev: v0.6.8
hooks: [{ id: ruff, args: ["--fix"] }]
- repo: https://github.com/psf/black
rev: 24.8.0
hooks: [{ id: black }]
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks: [{ id: isort }]
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.41.0
hooks: [{ id: markdownlint }]

default_language_version:
python: python3.11
2 changes: 1 addition & 1 deletion backend/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ RUN pip install --no-cache-dir --upgrade pip

COPY . .

RUN pip install --no-cache-dir -e .
RUN pip install --no-cache-dir -e .[dev]

ARG BACKEND_PORT
ARG FRONTEND_PORT
Expand Down
49 changes: 3 additions & 46 deletions backend/api/routes_sync.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,5 @@
"""Sync endpoints for Hygraph webhooks (H4)."""

from __future__ import annotations

import logging
from typing import Any, Dict

from fastapi import APIRouter, Depends, Header, HTTPException, Request

from api.config import Settings
from services.hygraph_service import HygraphService


logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/sync", tags=["sync"])


def get_hygraph_service() -> HygraphService:
settings = Settings()
# Using hygraph_token as webhook secret in MVP; adjust if separate secret provided
return HygraphService(webhook_secret=settings.hygraph_token)


@router.post("/hygraph")
async def sync_hygraph(
request: Request,
x_hygraph_signature: str | None = Header(default=None),
x_hygraph_event_id: str | None = Header(default=None),
service: HygraphService = Depends(get_hygraph_service),
) -> Dict[str, Any]:
try:
raw = await request.body()
if not x_hygraph_signature or not service.verify_signature(raw, x_hygraph_signature):
raise HTTPException(status_code=401, detail="invalid signature")

event = await request.json()
event_id = x_hygraph_event_id or event.get("id") or "unknown"
if not service.is_idempotent(event_id):
return {"status": "duplicate", "event_id": event_id}

return service.sync(event)
except HTTPException:
raise
except Exception as e:
logger.exception("Error syncing hygraph")
raise HTTPException(status_code=500, detail=str(e)) from e
from fastapi import APIRouter

router = APIRouter()

# TODO: Implement sync routes for Hygraph webhooks and manual pulls.
7 changes: 2 additions & 5 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ build-backend = "hatchling.build"
[tool.hatch.build]
packages = ["api", "services"]

[dependency-groups]
[project.optional-dependencies]
dev = [
"codecov>=2.1.13",
"mypy>=1.14.1",
Expand All @@ -34,6 +34,7 @@ dev = [
"pytest-cov>=6.0.0",
"pytest-mock>=3.14.0",
"ruff>=0.9.2",
"httpx>=0.27.0",
]
docs = [
"jinja2>=3.1.6", # Pinning version to address vulnerability GHSA-cpwx-vrp4-4pq7
Expand All @@ -47,10 +48,6 @@ docs = [
"pymdown-extensions>=10.0.0",
]

# Default dependency groups to be installed
[tool.uv]
default-groups = ["dev", "docs"]

[tool.ruff]
line-length = 88
target-version = "py312"
Expand Down
22 changes: 22 additions & 0 deletions backend/scripts/generate_openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# backend/scripts/generate_openapi.py
import json
import os
import sys

# Add the backend directory to the Python path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from api.main import app # adjust import to your app entrypoint
from fastapi.openapi.utils import get_openapi

schema = get_openapi(title=app.title, version=app.version, routes=app.routes)
# Optional: stable sort for dicts/lists to reduce churn
text = json.dumps(schema, indent=2, sort_keys=True)
out = os.path.join(os.path.dirname(__file__), "..", "..", "docs", "API_SPEC.md")
os.makedirs(os.path.dirname(out), exist_ok=True)
with open(out, "w") as f:
f.write("# API Specification (OpenAPI)\n\n")
f.write("```json\n")
f.write(text)
f.write("\n```\n")
print(f"Wrote OpenAPI -> {out}")
1 change: 1 addition & 0 deletions backend/services/hygraph_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# TODO: Implement Hygraph client for GraphQL queries with pagination.
Loading
Loading