Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 8 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ personal wantlist ("crate").

IT2810 (NTNU) Project 2. Group T19: `erikhfj`, `robertky`, `evenkkl`, `rachelks`.

**Status: M1 - spec and mockup.** `client/` renders mock data. `server/`
(PostgreSQL + GraphQL) arrives in M2, `e2e/` (Playwright) in M4.
**Status: M2 – PostgreSQL and GraphQL.** The frontend supports album and artist search, while the backend implements searching, filtering, sorting and pagination. Album details, reviews, crate functionality, simulated user identity and additional frontend controls are planned for later milestones. See the README for current implementation status.

## Commands

Expand All @@ -26,7 +25,7 @@ IT2810 (NTNU) Project 2. Group T19: `erikhfj`, `robertky`, `evenkkl`, `rachelks`
| `pnpm test` | Vitest, single run |

Run `pnpm format:check && pnpm lint && pnpm typecheck && pnpm test` before
opening a PR. CI enforces the same gates.
opening a PR. CI enforces the same gates, plus build and the client bundle budget.

## Architecture rules

Expand Down Expand Up @@ -77,12 +76,13 @@ requirements work - do not casually change them.
## Layout

```
client/ React + Vite + TypeScript (mock data at M1)
docs/ workflow, AI usage log (from #4)
client/ React + Vite + TypeScript + Apollo Client
server/ GraphQL Yoga + Drizzle + PostgreSQL, migrations, seeds, SQL checks
docs/ architecture, data model, UI specification, workflow, AI log, VM runbook
```

`server/` (GraphQL Yoga + Drizzle + PostgreSQL) and `e2e/` (Playwright) join
as they land.
`schema.graphql` is the shared API contract; `e2e/` (Playwright) is planned.
Local setup and environment variables are documented in `README.md`.

## Infrastructure

Expand All @@ -95,6 +95,4 @@ The finished app runs on the group VM: client under
- Apache `ProxyPass /graphql` to `127.0.0.1:3001`, client at `/var/www/html/project2`
- CI runs on a self-hosted runner on the same machine and deploys on push to `main`

Until M2 lands `server/`, the VM's `~/T19-Project-2` is a manual copy of an
early prototype rather than a git clone, so `crate-api` is running code that
is not in this repository.
CI deploys the tested commit to the VM, applies database migrations and restarts the API. See `docs/VM-setup.md` for deployment instructions and environment configuration.
210 changes: 84 additions & 126 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/AI-USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ the part that matters most.
| 2026-10-15 | robertky | Codex (ChatGPT) | Assisted me when I designed and implemented #7's batched synthetic catalog generator, its tests and local performance verification. I reviewed the generated data, when AI suggested something before using it. |
| 2026-10-16 | rachelks | Codex (ChatGPT) | Assisted with updating README and VM setup documentation for #48, clarifying implemented functionality and local development instructions. |
| 2026-10-16 | evenkkl | Claude | Assisted with reviewing PRs #50 and #51, checking the changes against the code and identifying areas for improvement. |
| 2026-10-16 | rachelks | Codex (ChatGPT) | Assisted with a final documentation review and cleanup for #48. |

## Notes

Expand Down
136 changes: 90 additions & 46 deletions docs/VM-setup.md
Original file line number Diff line number Diff line change
@@ -1,87 +1,131 @@
# VM setup

How to run the GraphQL API (`server/`) on the group VM and check that it works.
How to deploy the Crate application on the group VM and verify that it works.

## Run the API
## Deployment setup

`DATABASE_URL` must point at the `crate` database, for example in `server/.env`:
The VM runs PostgreSQL 18 (database `crate`, role `t19`), Node.js 24, pnpm,
Apache and the `crate-api.service` systemd service. The application is deployed
from `~/T19-Project-2`.

```
The repository root `.env` file must contain the database connection URL:

```dotenv
DATABASE_URL=postgres://t19:<password>@127.0.0.1:5432/crate
```

This file is gitignored and must not be committed. Unlike local development,
which uses `server/.env`, the VM service and deployment use the root `.env`.

To manually update the API, run these commands from `~/T19-Project-2`:

```bash
pnpm install
pnpm --filter @t19/server db:migrate # create the catalog tables
pnpm install --frozen-lockfile
export DATABASE_URL="$(sed -n 's/^DATABASE_URL=//p' .env | tail -n 1)"
pnpm --filter @t19/server db:migrate
pnpm --filter @t19/server build
pnpm --filter @t19/server start # http://localhost:3001/graphql
sudo /usr/bin/systemctl restart crate-api
```

The database and role must already exist before migrations are run. The API
runs on port 3001 and needs the repository's `schema.graphql` file at startup.

### Automated deployment

Pushes to `main` trigger `.github/workflows/ci.yml`. After formatting, linting,
type-checking, tests and builds pass, CI deploys the frontend to
`/var/www/html/project2`, updates the API in `~/T19-Project-2`, applies pending
migrations and restarts `crate-api.service`. Deployment does not seed the database.

Apache serves the frontend at `/project2/` and forwards `/graphql` requests to
the API on port 3001:

```apache
ProxyPass /graphql http://127.0.0.1:3001/graphql
ProxyPassReverse /graphql http://127.0.0.1:3001/graphql
```

### Verify a deployment

On the VM, check the deployed commit, service status and frontend response:

```bash
git -C ~/T19-Project-2 rev-parse HEAD
systemctl is-active crate-api
journalctl -u crate-api -n 50 --no-pager
curl -fsS -o /dev/null -w '%{http_code}\n' http://localhost/project2/
```

Then run the smoke tests below against the API and, if needed, through Apache.
You can also open <http://it2810-19.idi.ntnu.no/project2/> and try an album search.

### Optional sample data

To reset the database with the small sample catalog:
To reset a **development database** to the sample catalog:

```bash
pnpm --filter @t19/server db:seed
```

**Warning:** This replaces the existing catalog and deletes associated
reviews and crate items. Do not run it against a database containing data
you want to keep.
**Warning:** Seeding replaces the catalog and deletes associated tracks,
reviews and crate items. Do not run it on a database with data you want to keep.
The command requires the same `DATABASE_URL` environment variable as migrations.

## Frontend GraphQL endpoint

The frontend uses Apollo Client with `/graphql` as its default endpoint.
On the VM, Apache proxies `/graphql` to the API on port 3001; the frontend's
`/project2/` asset base does not change that endpoint.
The frontend uses Apollo Client and sends requests to `/graphql`. During local
development, Vite proxies these requests to the backend on port 3001. On the VM,
Apache handles the proxy instead. The frontend is served under `/project2/`.

For local development, run `pnpm --filter @t19/server dev` and `pnpm dev` in
separate terminals, after configuring the database and applying migrations.
Open <http://localhost:5173/project2/>. Vite proxies `/graphql` to
`http://127.0.0.1:3001`, so both processes must be running. To override the
frontend endpoint, set `VITE_GRAPHQL_URL` in `client/.env.local`.
For local setup instructions, see the [README](../README.md). To use another API
endpoint, set `VITE_GRAPHQL_URL` in `client/.env.local`.

## Smoke suite

Run on the VM after a deploy. Each command prints what to expect. The
expectations assume the sample data from `db:seed`.
Run these commands on the VM after deployment. The expected counts below assume
the sample catalog from `db:seed`; counts may differ if the database contains
other data. Check each response for GraphQL `errors`.

```bash
API=http://localhost:3001/graphql
gql() { curl -s "$API" -H 'content-type: application/json' -d "$1"; echo; }
```

1. The API answers. Expect `{"data":{"__typename":"Query"}}`.
To test Apache routing as well, set `API=http://localhost/graphql` and repeat.

```bash
gql '{"query":"{ __typename }"}'
```
1. **API response.** Expect `{"data":{"__typename":"Query"}}`.

2. Search is case-insensitive. Expect `totalCount` 4 for both.
```bash
gql '{"query":"{ __typename }"}'
```

```bash
gql '{"query":"{ a: searchAlbums(input:{query:\"blue\",sort:RELEVANCE},first:5){ totalCount } b: searchAlbums(input:{query:\"BLUE\",sort:RELEVANCE},first:5){ totalCount } }"}'
```
2. **Case-insensitive search.** With sample data, both `blue` and `BLUE` should
return `totalCount: 4`.

3. A single word also matches artist names, and artists and genres resolve.
Expect A Love Supreme and Blue Train, both by John Coltrane.
```bash
gql '{"query":"{ a: searchAlbums(input:{query:\"blue\",sort:RELEVANCE},first:5){ totalCount } b: searchAlbums(input:{query:\"BLUE\",sort:RELEVANCE},first:5){ totalCount } }"}'
```

```bash
gql '{"query":"{ searchAlbums(input:{query:\"coltrane\",sort:TITLE_ASC},first:5){ totalCount edges { node { title releaseYear artists { name } genres { name } } } } }"}'
```
3. **Artist search.** Searching for `coltrane` should return _A Love Supreme_
and _Blue Train_, both by John Coltrane.

4. Facets. With Rock (id 4) selected, expect `totalCount` 7. Genres combine
with AND, so `genreFacets` counts albums that are Rock and that genre:
Rock 7, Pop 3, Electronic 2. `decadeFacets` counts only Rock albums.
```bash
gql '{"query":"{ searchAlbums(input:{query:\"coltrane\",sort:TITLE_ASC},first:5){ totalCount edges { node { title releaseYear artists { name } genres { name } } } } }"}'
```

```bash
gql '{"query":"{ searchAlbums(input:{query:\"\",genreIds:[\"4\"],sort:TITLE_ASC},first:5){ totalCount genreFacets { genre { name } count } decadeFacets { decade count } } }"}'
```
4. **Genre filters and facets.** With Rock (ID `4`) selected, the sample catalog
should return `totalCount: 7`. Genre facets should show Rock `7`, Pop `3`
and Electronic `2`.

5. Paging. Expect `hasNextPage` true and an `endCursor`. Paste the cursor in as
`after` to get the next two albums, with no album repeated.
```bash
gql '{"query":"{ searchAlbums(input:{query:\"\",genreIds:[\"4\"],sort:TITLE_ASC},first:5){ totalCount genreFacets { genre { name } count } decadeFacets { decade count } } }"}'
```

```bash
gql '{"query":"{ searchAlbums(input:{query:\"\",sort:RELEASE_YEAR_DESC},first:2){ edges { node { title releaseYear } } pageInfo { hasNextPage endCursor } } }"}'
gql '{"query":"{ searchAlbums(input:{query:\"\",sort:RELEASE_YEAR_DESC},first:2,after:\"<endCursor>\"){ edges { node { title releaseYear } } pageInfo { hasNextPage endCursor } } }"}'
```
5. **Pagination.** The first query should return two albums, `hasNextPage: true`
and an `endCursor`. Replace `<endCursor>` in the second query with that value;
the next page should contain different albums.

```bash
gql '{"query":"{ searchAlbums(input:{query:\"\",sort:RELEASE_YEAR_DESC},first:2){ edges { node { title releaseYear } } pageInfo { hasNextPage endCursor } } }"}'
gql '{"query":"{ searchAlbums(input:{query:\"\",sort:RELEASE_YEAR_DESC},first:2,after:\"<endCursor>\"){ edges { node { title releaseYear } } pageInfo { hasNextPage endCursor } } }"}'
```
3 changes: 2 additions & 1 deletion docs/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ Then run:
pnpm format:check && pnpm lint && pnpm typecheck && pnpm test
```

All four must pass. CI runs the same thing and will reject the PR otherwise.
All four must pass. CI also runs `pnpm build` and checks the client bundle
budget; a failed gate blocks merging.

## Milestones

Expand Down
6 changes: 5 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ in M2 and M3. The API contract is `schema.graphql`, the data model is in
`docs/data-model.md`, and the planned interface is described in
`docs/ui-specification.md`.

At M2, search, filtering, sorting, pagination and DataLoaders are implemented in the backend. User identity, reviews, crate functionality and URL-based search state are planned for later milestones.

## 1. PostgreSQL + GraphQL Yoga

**Context:** The data is relational. Albums have many artists and genres, and
Expand Down Expand Up @@ -35,7 +37,7 @@ the next page continues from there (`first` + `after` in the schema). `OFFSET`
reads and throws away every skipped row, so it gets slower on later pages, and
it can repeat or skip rows when data changes between requests.

**Consequences:** Paging stays efficient on later pages. Paging only goes
**Consequences:** Keyset paging avoids scanning skipped rows, although counts and facets can still affect query performance. Paging only goes
forward, so there is no "jump to page N". Each sort mode needs its own cursor
logic, including nullable fields such as release year.

Expand All @@ -55,6 +57,8 @@ resolver loads every match into memory.
builder is the most complex server code, so it needs tests against a real
database.

The implemented search uses PostgreSQL full-text search with weighted album titles and artist names. Genre filters combine with AND, while decade filters combine with OR. Counts and facets cover matching results, not just the current page. The supported sort options are `RELEVANCE`, `TITLE_ASC` and `RELEASE_YEAR_DESC`; rating-based sorting is not yet implemented.

## 4. DataLoaders against N+1

**Context:** GraphQL resolves each field on its own. A page of 20 albums asking
Expand Down
21 changes: 15 additions & 6 deletions docs/data-model.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Crate data model

This document describes the data the application will need. `schema.graphql`
is the GraphQL contract built from this model. Database tables, resolvers and
queries are deliberately left to M2.
This document describes the model behind `schema.graphql`. The M2 database migrations implement the catalog and user-related tables. Currently, only album search and artist/genre relations are available through GraphQL resolvers. The user interactions described below are planned for later milestones.

## Core catalog

Expand All @@ -14,7 +12,14 @@ queries are deliberately left to M2.
- A **track** belongs to one album and has a title, track number and optional
duration.

The two many-to-many relationships will use join tables in PostgreSQL in M2.
The two many-to-many relationships use `album_artist` and `album_genre`, with
composite primary keys. Tracks have a unique `(album_id, track_number)`.
Foreign keys cascade on deletion. The seeds populate only the album, artist,
genre and join tables.

`album.search_vector` has a GIN index. SQL triggers refresh weighted title and
artist terms when titles, artist names or artist links change. Migration
`0001` also backfills existing albums.

## User-generated data

Expand All @@ -33,8 +38,10 @@ The two many-to-many relationships will use join tables in PostgreSQL in M2.
so repeated requests cannot accidentally switch the album back to the wrong
state.

The user identity and persistence of reviews and crate items are M2 work. The
schema already describes the operations the client will need. See
The tables exist, with unique `(user_id, album_id)` pairs for reviews and
crate items, a rating check of 1-5, and a trigger updating `review.updated_at`.
Identity transport, display-name derivation and review/crate resolvers are
not implemented. The schema describes the operations the client will need. See
[`docs/architecture.md`](./architecture.md) decision 5 for how the identity
travels and what happens when it is missing.

Expand All @@ -44,3 +51,5 @@ travels and what happens when it is missing.
choice. It returns a cursor-based connection so the client can load results in
pages. The connection also contains the total number of matches and genre
and decade facet counts for the filter UI.

Genres combine with AND, while decades combine with OR. Filtering, sorting and pagination are handled in SQL. See the README for supported sort options and pagination details.