Erster Schritt weg von Supabase hin zu "läuft auf jedem PostgreSQL".
Gemessen sitzt die Kopplung nicht dort, wo der Begriff "Supabase-Projekt"
sie vermuten lässt: das Schema ist reines PostgreSQL, und von 58 RLS-Policies
rufen nur fünf auth.uid() direkt auf. Die übrigen 53 gehen über is_hr_user().
Diese eine Funktion ist die Brücke — wird sie umgelegt, folgt der Rest.
Die Migration legt sie um. app_current_user_id() liest jetzt zuerst
current_setting('app.user_id') und fällt nur ersatzweise auf auth.uid()
zurück. Deshalb plpgsql statt language sql: eine SQL-Funktion wird beim
Anlegen geparst, und auth.uid() gibt es auf einem gewöhnlichen PostgreSQL
nicht — die Migration liesse sich dort gar nicht erst anwenden. Der
Ausnahmeblock fängt das ab, und damit läuft dieselbe Migration auf beiden
Systemen. Der Rückfall verschwindet mit der Abschlussmigration.
Dazu app_users als Nachfolger von auth.users, external_id ist die oid des
Anbieters statt der E-Mail: eine Namensänderung darf kein zweites Konto
erzeugen.
Die neue Zugriffsschicht ist Kysely auf einem pg-Pool. Was daran zählt, ist
nicht der Query-Builder, sondern was er verhindert:
- Die Kysely-Instanz wird nicht exportiert. Wer abfragen will, geht durch
withUser() — und das öffnet immer eine Transaktion.
- set_config(..., true) ist transaktionslokal. Ohne das dritte Argument
bliebe die Kennung an der gepoolten Verbindung kleben und die nächste
Anfrage liefe im Namen der vorherigen Person. In einer Personaldatenbank.
- Eine ESLint-Regel verbietet den Import von pg und von lib/db/pool
ausserhalb von lib/db. Nachgewiesen: eine Testdatei mit beiden Importen
erzeugt zwei Fehler.
- Einen privilegierten Zugang gibt es nicht mehr. asSystem() benutzt
dieselbe Rolle ohne BYPASSRLS; was ohne angemeldete Person laufen darf,
muss als SECURITY-DEFINER-Funktion in der Datenbank stehen.
tests/integration/session-context.test.ts läuft gegen einen Pool mit genau
einer Verbindung — sonst träfe er die Lücke mal und mal nicht. Er prüft, dass
nach Commit *und* nach Rollback nichts an der Verbindung zurückbleibt, und
belegt in einer Gegenprobe, dass eine Einstellung ohne Transaktion tatsächlich
hängen bleibt. Ein Sicherheitstest, der sich mangels DATABASE_URL selbst
überspringt, wäre schlimmer als keiner: in der CI schlägt schon das Fehlen
des Verbindungsstrings fehl.
Beim Schreiben der Migration stellte sich heraus, dass die Policies
hire_drafts_owner und saved_reports_owner heissen, nicht _own. Mit dem
geratenen Namen hätte drop policy nichts getroffen und create policy wäre mit
"already exists" abgebrochen.
Typecheck, Lint und 182 Tests sind grün. Die Anwendung läuft unverändert
weiter — sie benutzt die neue Schicht noch nicht.
98 lines
3.0 KiB
YAML
98 lines
3.0 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [master, main]
|
|
pull_request:
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
check:
|
|
name: Lint, Typen, Tests, Build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
|
|
- run: npm ci
|
|
|
|
- name: Lint
|
|
run: npm run lint
|
|
|
|
- name: Typecheck
|
|
run: npm run typecheck
|
|
|
|
# Catches the drift that `tsc` cannot: lib/supabase/types.ts is
|
|
# hand-written, so a migration adding a column leaves it silently stale.
|
|
- name: Schema/Typen-Abgleich
|
|
run: npm run types:check
|
|
|
|
- name: Unit- und Komponententests
|
|
run: npm test
|
|
|
|
- name: Build
|
|
run: npm run build
|
|
env:
|
|
# Read at module scope by the Supabase browser client, so the build
|
|
# needs them present — never the real project's values.
|
|
NEXT_PUBLIC_SUPABASE_URL: http://127.0.0.1:54321
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: build-time-placeholder
|
|
|
|
integration:
|
|
name: Integrationstests (echtes Postgres)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
|
|
- run: npm ci
|
|
|
|
- uses: supabase/setup-cli@v1
|
|
with:
|
|
version: latest
|
|
|
|
# Applies every migration to a fresh database — which also means a
|
|
# migration that cannot be replayed from scratch fails here rather than
|
|
# on a restore or a new environment.
|
|
- name: Supabase starten
|
|
run: supabase start
|
|
|
|
# `-o env` emits API_URL / ANON_KEY / SERVICE_ROLE_KEY / DB_URL; the app
|
|
# expects them under its own names. DATABASE_URL ist der direkte
|
|
# Postgres-Zugang — den braucht die neue Zugriffsschicht (lib/db) und
|
|
# vor allem der Nachweis zum Sitzungskontext.
|
|
- name: Testumgebung schreiben
|
|
run: |
|
|
supabase status -o env \
|
|
--override-name api.url=NEXT_PUBLIC_SUPABASE_URL \
|
|
--override-name auth.anon_key=NEXT_PUBLIC_SUPABASE_ANON_KEY \
|
|
--override-name auth.service_role_key=SUPABASE_SERVICE_ROLE_KEY \
|
|
--override-name db.url=DATABASE_URL \
|
|
| grep -E '^(NEXT_PUBLIC_SUPABASE_URL|NEXT_PUBLIC_SUPABASE_ANON_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL)=' \
|
|
| tr -d '"' > .env.test.local
|
|
# Lokal läuft Postgres ohne TLS; ohne das versucht `pg` es trotzdem.
|
|
echo "DATABASE_SSL=false" >> .env.test.local
|
|
grep -q '^DATABASE_URL=' .env.test.local \
|
|
|| { echo "DATABASE_URL wurde nicht geschrieben — der Sitzungskontext-Nachweis liefe ins Leere."; exit 1; }
|
|
|
|
- name: Seed
|
|
run: node --env-file=.env.test.local supabase/seed.ts
|
|
|
|
- name: Integrationstests
|
|
run: npm run test:integration
|
|
|
|
- name: Supabase-Logs bei Fehlschlag
|
|
if: failure()
|
|
run: supabase status && docker ps -a
|