Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ dist
dist-ssr
*.local

# Prisma client, generated by `npm run db:generate`
backend/src/generated

# Environment variables
.env
.env.*
Expand Down
108 changes: 105 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,22 @@ How the API stays fast when the catalog grows (cursors, indexes, text search, li
In the repository now:

- React, TypeScript and Vite for the frontend
- ESLint and Prettier for linting and formatting
- A backend package in `backend/` with TypeScript and Prisma, set up as an npm workspace. The database models come in a later issue.
- ESLint and Prettier for linting and formatting, for both the frontend and the backend
- Vitest for testing

Planned:

- A GraphQL backend in Node and TypeScript
- PostgreSQL, installed directly on the VM (not in Docker), accessed with Prisma
- PostgreSQL 18, installed directly on the VM and on our own machines (not in Docker), accessed with Prisma

### Why PostgreSQL

The data model in [docs/DATA_MODEL.md](docs/DATA_MODEL.md) is relational: apparatus have groups, groups have elements and vaults, and coaches have gymnasts that have routines. Routines contain elements, and an element can be in many routines. A relational database stores these relations directly and keeps them consistent.

- **Relations and constraints.** Foreign keys stop a group from pointing to an apparatus that does not exist, or a routine from pointing to a gymnast that does not exist. A unique constraint on gymnast, apparatus and routine name gives constraint 6. Rules that compare several rows, such as "all elements in a routine belong to the routine's apparatus" (constraints 7 and 8), cannot be written as a simple constraint. They are checked in the backend and can be moved to triggers later if needed.
- **Search, filter and sort over the whole data set.** This must happen on the server and not only on what the client has loaded. PostgreSQL does it with indexes and `ORDER BY`, and it has full text search for the search field. Cursor pagination uses an index on the sort columns, so a page is fast whatever the size of the catalog.
- **Prisma.** Prisma supports PostgreSQL well. It gives us migrations, a way to seed the catalog data, and TypeScript types for the queries.
- **Prisma.** Prisma supports PostgreSQL well. It gives us migrations, a way to seed the catalog data, and TypeScript types for the queries. We chose it over writing SQL by hand because the tables are described in one readable schema file, the migrations are generated from that file, and a query that does not match the database is caught as a type error before the code runs.
- **Runs directly on the VM.** PostgreSQL is a standard package for the virtual machine, so it does not need Docker.

We also considered MongoDB and SQLite:
Expand Down Expand Up @@ -109,10 +110,111 @@ Other commands:
| `npm run format` | Formats all files with Prettier |
| `npm run test` | Runs the tests with Vitest |

`npm ci` installs both the frontend and the backend, because the backend is an npm workspace. The database commands are described in the next section.

## Database (local setup)

The backend uses PostgreSQL 18. Everyone installs it directly on their own machine, without Docker, and everyone uses version 18 so that migrations behave the same for all of us.

### 1. Install PostgreSQL 18

**Mac**, with one of these:

- **Postgres.app** (recommended): download it from [postgresapp.com](https://postgresapp.com/), move it to Applications and open it. Choose PostgreSQL 18 if you are asked, and click _Initialize_. To use `psql` in the terminal, run this once and open a new terminal:

```bash
sudo mkdir -p /etc/paths.d && echo /Applications/Postgres.app/Contents/Versions/latest/bin | sudo tee /etc/paths.d/postgresapp
```

- **Homebrew:**

```bash
brew install postgresql@18
brew services start postgresql@18
```

**Windows:** download the installer from EDB through [postgresql.org/download/windows](https://www.postgresql.org/download/windows/) and choose version 18. Keep the default port 5432, and write down the password you choose for the `postgres` user. `psql` is available as _SQL Shell (psql)_ in the Start menu.

### 2. Create a user and a database

Open `psql` as the administrator:

- Mac: `psql postgres`
- Windows: _SQL Shell (psql)_, log in as `postgres`

Then create a user and a database, both called `cop`. Choose your own password:

```sql
CREATE USER cop WITH PASSWORD 'choose-a-password' CREATEDB;
CREATE DATABASE cop OWNER cop;
```

The user needs `CREATEDB` because `prisma migrate dev` creates a temporary _shadow database_ to check the migrations, and deletes it again afterwards.

### 3. Connect the backend to the database

Copy the example file and fill in your password:

```bash
cp backend/.env.example backend/.env
```

On Windows: `copy backend\.env.example backend\.env`.

Open `backend/.env` and replace `your-password` in `DATABASE_URL`. The format is `postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public`. The file is ignored by Git, so your password is never committed.

### 4. Install and run

From the repository root:

```bash
npm ci
npm run db:generate
```

| Command | What it does |
| --------------------- | ------------------------------------------------------------------------------ |
| `npm run db:generate` | Generates the Prisma client from `backend/prisma/schema.prisma` |
| `npm run db:migrate` | Creates a migration from changes in the schema and applies it to your database |
| `npm run db:deploy` | Applies existing migrations without creating new ones. Used on the server |
| `npm run db:studio` | Opens Prisma Studio in the browser, to look at the data in the database |

### 5. Check the connection

To check that the backend can reach your database:

1. Make sure PostgreSQL is running. In Postgres.app the server should show as running. With Homebrew, run `brew services start postgresql@18`. On Windows the service starts automatically.
2. Open a terminal in the repository root and go to the backend folder:

```bash
cd backend
```

3. Run:

```bash
npx prisma migrate status
```

4. Look for this line in the output. If it is there, the connection works:

```
Datasource "db": PostgreSQL database "cop", schema "public" at "localhost:5432"
```

5. Go back to the repository root:

```bash
cd ..
```

On a fresh setup the output also says "No migration found in prisma/migrations" and "The current database is not managed by Prisma Migrate". That is the expected result and not an error: there are no tables or migrations yet, because the models are added in a later issue. If the connection fails, Prisma prints an error such as "Can't reach database server" or "Authentication failed" instead.

## Project structure

| Folder | Contents |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `backend/` | The backend package: TypeScript, Prisma and the draft GraphQL schema |
| `data/` | The catalog data extracted from the Code of Points: JSON files and one image per element |
| `docs/` | The specification: user interface plans, data model, example data and AI usage. [docs/EXTRACTION.md](docs/EXTRACTION.md) explains how `data/` was made |
| `public/` | Static files served as they are, such as the favicon |
Expand Down
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copy this file to .env and fill in your own values. .env is not committed.
#
# Format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public
DATABASE_URL="postgresql://cop:your-password@localhost:5432/cop?schema=public"
26 changes: 26 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "backend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:deploy": "prisma migrate deploy",
"prisma:studio": "prisma studio"
},
"engines": {
"node": ">=24"
},
"devDependencies": {
"@types/node": "^24.19.2",
"dotenv": "^17.4.2",
"prisma": "7.10.0",
"tsx": "^4.23.15",
"typescript": "~6.0.2"
},
"dependencies": {
"@prisma/client": "7.10.0"
}
}
14 changes: 14 additions & 0 deletions backend/prisma.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Prisma configuration. Prisma 7 reads the database URL from here, not from
// schema.prisma. DATABASE_URL is loaded from backend/.env.
import 'dotenv/config'
import { defineConfig } from 'prisma/config'

export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: process.env['DATABASE_URL'],
},
})
11 changes: 11 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Prisma schema for the backend. The models are added in a later issue.
// Docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}

datasource db {
provider = "postgresql"
}
20 changes: 20 additions & 0 deletions backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es2023",
"lib": ["ES2023"],
"types": ["node"],
"module": "nodenext",
"moduleResolution": "nodenext",
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"strict": true,
"skipLibCheck": true,
"noEmit": true,

/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "prisma.config.ts"]
}
20 changes: 20 additions & 0 deletions docs/AI_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,23 @@
GraphQL concept as it was introduced, and checked that the schema parses
with the `graphql` library. Part of the reasoning in the error handling
section comes from Claude's suggestions, which the team accepted.

## Database choice and backend setup (`backend/`, README)

- **Tools:** Claude for the discussion and the decisions, Claude Code for the
setup.
- **Background:** The database was not chosen before the first delivery. The
reviews of the first delivery suggested a database, and the team discussed
that feedback.
- **What the team did:** Discussed the options with Claude, using the
suggestion from the reviews as a starting point. Answered a questionnaire
that Claude made, so that every decision was made before any setup started:
PostgreSQL with Prisma, npm as the only package manager, the backend as its
own npm workspace in `backend/`, PostgreSQL installed directly on each
machine without Docker, and everyone on PostgreSQL 18. The team then wrote
the task description for Claude Code based on those decisions.
- **What Claude did:** In the discussion, Claude explained the options and
their trade-offs and made the questionnaire. Claude Code then set up the
workspace, TypeScript, Prisma and the ESLint configuration for the backend,
wrote the "Database (local setup)" section in the README, and checked that
the install, lint, build and the connection to a local database work.
10 changes: 9 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import eslintConfigPrettier from 'eslint-config-prettier/flat'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
globalIgnores(['dist']),
globalIgnores(['dist', 'backend/src/generated']),
{
files: ['**/*.{ts,tsx}'],
ignores: ['backend/**'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
Expand All @@ -20,5 +21,12 @@ export default defineConfig([
globals: globals.browser,
},
},
{
files: ['backend/**/*.ts'],
extends: [js.configs.recommended, tseslint.configs.recommended],
languageOptions: {
globals: globals.node,
},
},
eslintConfigPrettier,
])
Loading