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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# VM defaults: copy to server/.env on the VM and replace the password placeholder.
# For a backend running locally through SSH tunnel, use 127.0.0.1:5433 instead.
# Never put real passwords or secrets in this file or commit server/.env.
DATABASE_URL=postgresql://tapfinder_app:REPLACE_WITH_PASSWORD@localhost:5432/tapfinder
DATABASE_URL=postgresql://tapfinder_app:REPLACE_WITH_PASSWORD@localhost:5432/tapfinder_dev
PORT=4000
# Keep the API local until a reverse proxy (for example, Nginx) is configured.
HOST=127.0.0.1
105 changes: 91 additions & 14 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Backend og database

Backenden er et TypeScript-prosjekt med Apollo Server (GraphQL) og PostgreSQL.
Målet er at både backend og PostgreSQL kjører direkte på prosjektets VM, uten
Docker. Backendoppsettet er foreløpig et skjelett med helsesjekker; Tapfinders
domene-API og tabeller kommer når gruppa har bestemt datamodellen.
Både backend og PostgreSQL kjøres direkte på prosjektets VM, uten Docker.
Dette oppsettet inneholder første venue-skjema, GraphQL-lesespørringer og et
idempotent importscript for et kuratert Geoapify-datasett.

## Krav og npm-oppsett

Expand All @@ -25,10 +25,12 @@ lockfila med `npm --prefix server install`, og `package.json` og
etter at ny kode er hentet inn. Dette skjer ikke automatisk ved Git-push før
automatisk utrulling er satt opp.

## Database på VM-en
## Utviklingsdatabase på VM-en

Kjør dette på VM-en der PostgreSQL er installert. Opprett applikasjonsrollen
uten å skrive passordet inn i SQL-kommandoen:
uten å skrive passordet inn i SQL-kommandoen. Hvis `tapfinder_app` allerede
finnes fra det tidligere databaseoppsettet, behold rollen og hopp over
`CREATE ROLE` og `\password`:

```bash
sudo -u postgres psql
Expand All @@ -39,7 +41,7 @@ I `psql`:
```sql
CREATE ROLE tapfinder_app LOGIN;
\password tapfinder_app
CREATE DATABASE tapfinder OWNER tapfinder_app;
CREATE DATABASE tapfinder_dev OWNER tapfinder_app;
\q
```

Expand All @@ -48,16 +50,17 @@ localhost på VM-en. Ikke åpne port 5432 mot internett.

## Miljøvariabler

Backend leser tilkoblingen fra `DATABASE_URL`. `.env.example` i repo-roten er en
mal uten hemmeligheter. På VM-en kopierer du den til `server/.env` og legger inn
det faktiske databasepassordet der. `server/.env` er ignorert av Git og må ikke
committes eller deles.
Backend og migreringsverktøy leser tilkoblingen fra `DATABASE_URL`. `.env.example`
i repo-roten er en mal uten hemmeligheter. På VM-en kopierer du den til
`server/.env` og legger inn det faktiske databasepassordet der. `server/.env` er
ignorert av Git og må ikke committes. Del passordet med gruppa gjennom en avtalt
privat kanal, ikke Git.

På VM-en, der backend og PostgreSQL kjører på samme maskin, skal adressen bruke
`localhost` og port `5432`:

```env
DATABASE_URL=postgresql://tapfinder_app:PASSORD@127.0.0.1:5432/tapfinder
DATABASE_URL=postgresql://tapfinder_app:PASSORD@127.0.0.1:5432/tapfinder_dev
PORT=4000
HOST=127.0.0.1
```
Expand Down Expand Up @@ -129,11 +132,85 @@ npm --prefix server run migrate:create -- add-your-change-name
```

Review migrasjonen sammen med backendendringen i pull request. Før migrasjoner
kjøres mot VM-databasen, ta backup og sørg for at riktig `DATABASE_URL` er satt
i VM-ens `server/.env`. Kjør deretter:
kjøres på VM-en, kontroller at VM-ens `server/.env` peker til `tapfinder_dev`.
Før migrering av en database med viktige data: ta backup. Kjør deretter:

```bash
npm --prefix server run migrate:up
```

Ikke kjør migrasjoner mot VM-databasen før endringen er godkjent.
Migreringen `create_venues` oppretter `venues` og `venue_sources`. Interne venue-
ID-er lages av databasen. Geoapify-ID-en lagres i `venue_sources`, med unik
kombinasjon av leverandør og ekstern ID, slik at importen kan kjøres flere ganger
uten duplikater.

## Venue-felter og Geoapify-import

Første versjon inneholder `id`, `name`, `type`, `city`, `area`, `address`,
`latitude`, `longitude`, `openingHours`, `tags` og `websiteUrl`. Image, beskrivelse,
prisnivå, vurdering og antall anmeldelser er utelatt inntil dere har datakilder
for dem. `websiteUrl` kan være `null`. `openingHours` og `tags` er alltid lister;
ukjente verdier representeres som `[]`.

Importer bare data dere har tillatelse til å lagre og distribuere. Behold
nødvendig Geoapify/OpenStreetMap-attribusjon i produktet og kontroller gjeldende
vilkår før dere committer et datasett til Git.

Seed-scriptet leser `server/data/venues.json`. Filen er opprettet som en tom liste
og skal fylles med normaliserte poster. Format:

```json
[
{
"sourceId": "Geoapify- eller OSM-ID",
"name": "Eksempelbar",
"type": "BAR",
"city": "Trondheim",
"area": "Sentrum",
"address": "Eksempelveien 1",
"latitude": 63.4305,
"longitude": 10.3951,
"openingHours": [
{ "day": "FRIDAY", "opens": "18:00", "closes": "02:00", "closed": false }
],
"tags": ["bar"],
"websiteUrl": null
}
]
```

Dette er kun et format-eksempel, ikke et virkelig sted. `sourceId` må være stabil
og unik hos Geoapify/OSM. `openingHours` må konverteres fra Geoapifys
åpningstidstekst til ukedag/intervallformat før import; bruk `[]` hvis data
mangler eller ikke er korrekt tolket. `tags` skal være deres normaliserte tagger.
Fyll ut `server/data/venues.json` med de verifiserte postene før seed-kjøring.

Kjør importen fra repoets rot på VM-en etter at migreringen er på plass:

```bash
npm --prefix server run seed:venues
```

Scriptet avviser andre databaser enn `tapfinder_dev`, validerer feltene og gjør
alle endringene i én transaksjon. Kjør det gjerne igjen etter at datasettet er
oppdatert; samme Geoapify-ID oppdaterer samme venue.

GraphQL Sandbox kan kontrollere dataene med:

```graphql
query {
venues {
id
name
type
city
area
address
latitude
longitude
openingHours { day opens closes closed }
tags
websiteUrl
}
}
```
1 change: 1 addition & 0 deletions server/data/venues.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
77 changes: 77 additions & 0 deletions server/migrations/1791904375317_create-venues.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;

/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
pgm.createTable('venues', {
id: {
type: 'uuid',
primaryKey: true,
default: pgm.func('gen_random_uuid()'),
},
name: { type: 'text', notNull: true },
type: { type: 'text', notNull: true },
city: { type: 'text', notNull: true },
area: { type: 'text', notNull: true },
address: { type: 'text', notNull: true },
latitude: { type: 'double precision', notNull: true },
longitude: { type: 'double precision', notNull: true },
opening_hours: {
type: 'jsonb',
notNull: true,
default: pgm.func("'[]'::jsonb"),
},
tags: { type: 'text[]', notNull: true, default: pgm.func("'{}'::text[]") },
website_url: { type: 'text' },
created_at: {
type: 'timestamptz',
notNull: true,
default: pgm.func('current_timestamp'),
},
updated_at: {
type: 'timestamptz',
notNull: true,
default: pgm.func('current_timestamp'),
},
});

pgm.createTable('venue_sources', {
id: { type: 'bigserial', primaryKey: true },
venue_id: {
type: 'uuid',
notNull: true,
references: 'venues',
onDelete: 'CASCADE',
},
provider: { type: 'text', notNull: true },
external_id: { type: 'text', notNull: true },
fetched_at: {
type: 'timestamptz',
notNull: true,
default: pgm.func('current_timestamp'),
},
raw_data: { type: 'jsonb' },
});

pgm.addConstraint('venue_sources', 'venue_sources_provider_external_id_unique', {
unique: ['provider', 'external_id'],
});
pgm.createIndex('venues', 'city', { name: 'venues_city_idx' });
pgm.createIndex('venues', 'type', { name: 'venues_type_idx' });
};

/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
pgm.dropTable('venue_sources');
pgm.dropTable('venues');
};
3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"start": "node dist/index.js",
"migrate:create": "node-pg-migrate create",
"migrate:up": "node-pg-migrate up",
"migrate:down": "node-pg-migrate down"
"migrate:down": "node-pg-migrate down",
"seed:venues": "tsx scripts/seed-venues.ts"
},
"dependencies": {
"@apollo/server": "^5.0.0",
Expand Down
Loading