Compare commits

..

8 Commits

Author SHA1 Message Date
e7554fa2cc .gitea/ISSUE_TEMPLATE/feature_request.md aktualisiert
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 16s
Build and Push Docker Images / build-frontend (push) Successful in 16s
2026-01-22 17:42:28 +00:00
7c1fabf58a .gitea/ISSUE_TEMPLATE/bug_report.md aktualisiert
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 17s
Build and Push Docker Images / build-frontend (push) Successful in 16s
2026-01-22 17:40:58 +00:00
1203b6cbd1 .gitea/ISSUE_TEMPLATE/bug_report.md aktualisiert
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 16s
Build and Push Docker Images / build-frontend (push) Successful in 16s
2026-01-15 11:39:03 +00:00
525f2906fb .gitea/ISSUE_TEMPLATE/feature_request.md aktualisiert
Some checks failed
Build and Push Docker Images / build-frontend (push) Has been cancelled
Build and Push Docker Images / build-backend (push) Has been cancelled
2026-01-15 11:38:50 +00:00
b105382abf .gitea/ISSUE_TEMPLATE/bug_report.md aktualisiert
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 16s
Build and Push Docker Images / build-frontend (push) Successful in 15s
2026-01-15 11:38:00 +00:00
b1cdec7d17 Merge pull request 'Added feature request template' (#62) from dev into main
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 17s
Build and Push Docker Images / build-frontend (push) Successful in 16s
Reviewed-on: #62
2026-01-15 11:31:57 +00:00
f1d512b2e5 Merge pull request 'dev' (#61) from dev into main
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 17s
Build and Push Docker Images / build-frontend (push) Successful in 16s
Reviewed-on: #61
2026-01-15 11:29:15 +00:00
db21b43120 Merge pull request 'dev' (#40) from dev into main
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 18s
Build and Push Docker Images / build-frontend (push) Successful in 16s
Reviewed-on: #40
2026-01-08 22:21:06 +00:00
481 changed files with 9323 additions and 164364 deletions

View File

@@ -1,109 +0,0 @@
# FEDEO Selfhosting
DOMAIN=app.example.com
CONTACT_EMAIL=admin@deine-domain.de
DB_NAME=fedeo
DB_USER=fedeo
DB_PASSWORD=change-this-db-password
DATABASE_URL=postgres://fedeo:change-this-db-password@db:5432/fedeo
MINIO_ROOT_USER=fedeo-minio
MINIO_ROOT_PASSWORD=change-this-minio-password
MINIO_BUCKET=fedeo
HOST=0.0.0.0
PORT=3100
FEDEO_RUN_MIGRATIONS=true
COOKIE_SECRET=change-this-cookie-secret
JWT_SECRET=change-this-jwt-secret
ENCRYPTION_KEY=change-this-encryption-key
MAILER_SMTP_HOST=smtp.example.com
MAILER_SMTP_PORT=587
MAILER_SMTP_SSL=false
MAILER_SMTP_USER=mailer@example.com
MAILER_SMTP_PASS=change-this-mail-password
MAILER_FROM=FEDEO <no-reply@example.com>
# Desktop Push per Web Push. Schlüssel können mit
# `npx web-push generate-vapid-keys` erzeugt werden.
WEB_PUSH_PUBLIC_KEY=replace-this-web-push-public-key
WEB_PUSH_PRIVATE_KEY=replace-this-web-push-private-key
WEB_PUSH_SUBJECT=mailto:admin@example.com
S3_ENDPOINT=http://minio:9000
S3_REGION=eu-central-1
S3_ACCESS_KEY=fedeo-minio
S3_SECRET_KEY=change-this-minio-password
S3_BUCKET=fedeo
M2M_API_KEY=change-this-m2m-key
API_BASE_URL=https://app.example.com/backend
GOCARDLESS_BASE_URL=https://api.gocardless.com
GOCARDLESS_SECRET_ID=replace-this
GOCARDLESS_SECRET_KEY=replace-this
DOKUBOX_IMAP_HOST=imap.example.com
DOKUBOX_IMAP_PORT=993
DOKUBOX_IMAP_SECURE=true
DOKUBOX_IMAP_USER=dokubox@example.com
DOKUBOX_IMAP_PASSWORD=change-this-imap-password
OPENAI_API_KEY=replace-this
STIRLING_API_KEY=replace-this
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
# Optionaler Erststart-Bootstrap. Wenn gesetzt, werden Admin, Mandant,
# Admin-Rolle und Grunddaten beim Backend-Start idempotent angelegt.
FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
FEDEO_BOOTSTRAP_ADMIN_PASSWORD=change-this-admin-password
FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
# FEDEO Matrix-Kommunikation
#
# Diese Werte werden von docker-compose.yml und docker-compose.selfhost.yml
# gelesen, wenn das Profil "matrix" genutzt wird. Für produktive Systeme
# müssen alle Geheimnisse ersetzt werden.
MATRIX_SERVER_NAME=app.example.com
MATRIX_POSTGRES_DB=synapse
MATRIX_POSTGRES_USER=synapse
MATRIX_POSTGRES_PASSWORD=change-this-matrix-db-password
MATRIX_TURN_SHARED_SECRET=change-this-turn-secret
LIVEKIT_KEY=fedeo-livekit
LIVEKIT_SECRET=change-this-livekit-secret-please-replace
# Backend-Integration im Selfhost-Stack
MATRIX_HOMESERVER_URL=http://matrix-synapse:8008
MATRIX_RTC_HOST=app.example.com
MATRIX_RTC_JWT_URL=https://app.example.com/livekit/jwt
MATRIX_LIVEKIT_URL=wss://app.example.com/livekit/sfu
MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-synapse-homeserver-yaml
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
NUXT_PUBLIC_MATRIX_ELEMENT_URL=https://app.example.com/element
# Lokale Matrix-Entwicklung
MATRIX_DEV_SYNAPSE_PORT=8008
MATRIX_DEV_ELEMENT_PORT=8080
MATRIX_DEV_RTC_JWT_PORT=8081
MATRIX_DEV_LIVEKIT_PORT=7880
MATRIX_DEV_LIVEKIT_TCP_PORT=7881
MATRIX_DEV_LIVEKIT_RTC_MIN_PORT=50000
MATRIX_DEV_LIVEKIT_RTC_MAX_PORT=50100
MATRIX_DEV_LIVEKIT_NODE_IP=127.0.0.1
MATRIX_DEV_TURN_PORT=3478
MATRIX_DEV_TURN_MIN_PORT=49160
MATRIX_DEV_TURN_MAX_PORT=49200
# Lokale Backend-Integration gegen den Matrix-Entwicklungsstack
# MATRIX_HOMESERVER_URL=http://localhost:8008
# MATRIX_RTC_JWT_URL=http://localhost:8081
# MATRIX_LIVEKIT_URL=ws://localhost:7880
# MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-dev-synapse-homeserver-yaml
# NUXT_PUBLIC_MATRIX_ELEMENT_URL=http://localhost:8080

View File

@@ -2,37 +2,18 @@
name: 🐛 Bug Report
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
title: '[BUG] '
labels: bug
labels: Problem
assignees: ''
---
**Beschreibung**
Eine klare und prägnante Beschreibung des Fehlers.
**Reproduktion**
Schritte, um den Fehler zu reproduzieren:
Entweder:
1. Gehe zu '...'
2. Klicke auf '...'
3. Scrolle runter zu '...'
4. Siehe Fehler
Oder Link zur Seite
**Erwartetes Verhalten**
Eine klare Beschreibung dessen, was du erwartet hast.
**Screenshots**
Falls zutreffend, füge hier Screenshots oder Gifs hinzu, um das Problem zu verdeutlichen.
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
**Umgebung:**
- Betriebssystem: [z.B. Windows, macOS, Linux]
- Browser / Version (falls relevant): [z.B. Chrome 120]
- Projekt-Version: [z.B. v1.0.2]
**Zusätzlicher Kontext**
Füge hier alle anderen Informationen zum Problem hinzu.

View File

@@ -2,19 +2,16 @@
name: ✨ Feature Request
about: Schlage eine Idee für dieses Projekt vor.
title: '[FEATURE] '
labels: enhancement
labels: Funktionswunsch
assignees: ''
---
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
Eine klare Beschreibung des Problems (z.B. "Ich bin immer genervt, wenn...").
**Lösungsvorschlag**
Eine klare Beschreibung dessen, was du dir wünschst und wie es funktionieren soll.
**Alternativen**
Hast du über alternative Lösungen oder Workarounds nachgedacht?
**Zusätzlicher Kontext**
Hier ist Platz für weitere Informationen, Skizzen oder Beispiele von anderen Tools.

View File

@@ -1,5 +1,5 @@
name: Build and Push Docker Images
run-name: Build Backend, Frontend & Docs by @${{ github.actor }}
run-name: Build Backend & Frontend by @${{ github.actor }}
on: [push]
@@ -8,38 +8,12 @@ env:
# Wenn du die Gitea-interne Registry nutzt, ist es meist einfach der Hostname deiner Gitea-Instanz.
# Beispiel: gitea.deine-domain.de
REGISTRY_HOST: git.federspiel.tech
# Der Name des Repos (z.B. user/repo).
# Explizit in lowercase gesetzt, damit es exakt zu den Compose-Imagepfaden passt.
IMAGE_NAME: flfeders/fedeo
# Der Name des Repos (z.B. user/repo)
IMAGE_NAME: ${{ github.repository }}
ACTOR: flfeders
jobs:
# verify-docs-sync:
# runs-on: ubuntu-latest
# steps:
# - name: Check out repository code
# uses: actions/checkout@v3
#
# - name: Prüfe Node-Version
# uses: actions/setup-node@v4
# with:
# node-version: 20
#
# - name: Synchronisiere Funktionsdokumentation
# run: node docs/scripts/sync-funktionsdoku.mjs
#
# - name: Breche ab, wenn Doku nicht aktuell committed ist
# run: |
# if [ -n "$(git status --porcelain docs/)" ]; then
# echo "Die generierte Dokumentation ist nicht aktuell."
# echo "Bitte lokal ausführen: node docs/scripts/sync-funktionsdoku.mjs"
# echo "Danach die Änderungen committen."
# git status --short docs/
# exit 1
# fi
build-backend:
#needs: verify-docs-sync
runs-on: ubuntu-latest
steps:
- name: Check out repository code
@@ -72,7 +46,6 @@ jobs:
labels: ${{ steps.meta-backend.outputs.labels }}
build-frontend:
#needs: verify-docs-sync
runs-on: ubuntu-latest
steps:
- name: Check out repository code
@@ -101,37 +74,4 @@ jobs:
context: ./frontend # Hier wird der Ordner gewechselt (wie 'cd frontend')
push: true
tags: ${{ steps.meta-frontend.outputs.tags }}
labels: ${{ steps.meta-frontend.outputs.labels }}
build-docs:
needs: verify-docs-sync
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Log in to Docker Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY_HOST }}
username: ${{ env.ACTOR }}
password: ${{ vars.CI_TOKEN }}
- name: Extract metadata (tags, labels) for Docs
id: meta-docs
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}/docs
tags: |
type=ref,event=branch
type=ref,event=tag
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push Docs
uses: docker/build-push-action@v4
with:
context: .
file: ./docs-site/Dockerfile
push: true
tags: ${{ steps.meta-docs.outputs.tags }}
labels: ${{ steps.meta-docs.outputs.labels }}
labels: ${{ steps.meta-frontend.outputs.labels }}

7
.gitignore vendored
View File

@@ -1,7 +0,0 @@
.env
# Lokale Runtime-Daten und generierte Konfigurationen
matrix/postgres/
matrix/synapse/
matrix/dev/postgres/
matrix/dev/synapse/

8
.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/FEDEO.iml generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/FEDEO.iml" filepath="$PROJECT_DIR$/.idea/FEDEO.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

601
README.md
View File

@@ -1,508 +1,109 @@
# FEDEO Hosting Guide
Diese Anleitung beschreibt ein produktionsnahes Self-Hosting von FEDEO mit Docker Compose, Traefik, PostgreSQL und optionalem S3-kompatiblem Objektspeicher via MinIO.
## Architektur
Der Stack besteht aus:
# Docker Compose Setup
- `frontend`: Nuxt-Frontend auf Port `3000`
- `backend`: Node/Fastify-API auf Port `3100`
- `db`: PostgreSQL
- `traefik`: Reverse Proxy mit automatischen Let's-Encrypt-Zertifikaten
- optional `minio`: S3-kompatibler Objektspeicher fur Dateiuploads
## ENV Vars
Die Konfiguration erfolgt uber Umgebungsvariablen beziehungsweise eine `.env`-Datei im Deploy-Verzeichnis.
- DOMAIN
- PDF_LICENSE
- DB_PASS
- DB_USER
- CONTACT_EMAIL
## Voraussetzungen
Vor dem Deployment sollten folgende Punkte erfullt sein:
- Ein Linux-Server oder VPS mit offentlichen Ports `80` und `443`
- Docker Engine inkl. Compose Plugin
- Eine Domain, die auf den Server zeigt, z. B. `app.example.com`
- Optional: SMTP-Zugang fur E-Mails
- Optional: S3-Bucket oder MinIO fur Dateispeicher
Empfohlen:
- mindestens 2 vCPU
- mindestens 4 GB RAM
- SSD-Speicher fur PostgreSQL und Dateiuploads
## DNS und Netzwerk
Lege mindestens einen A- oder AAAA-Record an:
- `app.example.com -> <SERVER-IP>`
Traefik terminiert TLS direkt im Compose-Stack. Es ist kein zusatzlicher Reverse Proxy davor erforderlich.
## Benotigte Backend-Umgebungsvariablen
Das Backend erwartet mindestens diese Umgebungsvariablen:
- `COOKIE_SECRET`
- `JWT_SECRET`
- `PORT`
- `HOST`
- `DATABASE_URL`
- `S3_BUCKET`
- `ENCRYPTION_KEY`
- `MAILER_SMTP_HOST`
- `MAILER_SMTP_PORT`
- `MAILER_SMTP_SSL`
- `MAILER_SMTP_USER`
- `MAILER_SMTP_PASS`
- `MAILER_FROM`
- `S3_ENDPOINT`
- `S3_REGION`
- `S3_ACCESS_KEY`
- `S3_SECRET_KEY`
- `M2M_API_KEY`
- `API_BASE_URL`
- `GOCARDLESS_BASE_URL`
- `GOCARDLESS_SECRET_ID`
- `GOCARDLESS_SECRET_KEY`
- `DOKUBOX_IMAP_HOST`
- `DOKUBOX_IMAP_PORT`
- `DOKUBOX_IMAP_SECURE`
- `DOKUBOX_IMAP_USER`
- `DOKUBOX_IMAP_PASSWORD`
- `OPENAI_API_KEY`
- `STIRLING_API_KEY`
Minimal wichtige Werte fur den ersten Start:
- `HOST=0.0.0.0`
- `PORT=3100`
- `DATABASE_URL=postgres://fedeo:<starkes-passwort>@db:5432/fedeo`
- `API_BASE_URL=https://app.example.com/backend`
Wenn du MinIO verwendest, setze zusatzlich:
- `S3_ENDPOINT=http://minio:9000`
- `S3_REGION=eu-central-1`
- `S3_ACCESS_KEY=<MINIO_ROOT_USER>`
- `S3_SECRET_KEY=<MINIO_ROOT_PASSWORD>`
- `S3_BUCKET=fedeo`
## Deploy-Struktur
Deploye den Stack direkt aus einem geklonten Checkout dieses Repositories, weil die Selfhost-Compose-Datei die lokalen Build-Kontexte `./frontend` und `./backend` verwendet.
Beispiel:
```bash
git clone <DEIN-REPO-URL> /opt/fedeo
cd /opt/fedeo
```
Die Verzeichnisstruktur sollte dann mindestens so aussehen:
```text
/opt/fedeo/
docker-compose.selfhost.yml
.env
backend/
frontend/
traefik/
letsencrypt/
logs/
postgres/
minio/
```
Danach:
```bash
mkdir -p /opt/fedeo/traefik/letsencrypt
mkdir -p /opt/fedeo/traefik/logs
mkdir -p /opt/fedeo/postgres
mkdir -p /opt/fedeo/minio
touch /opt/fedeo/traefik/letsencrypt/acme.json
chmod 600 /opt/fedeo/traefik/letsencrypt/acme.json
```
Als Startpunkt kannst du die Beispielumgebung kopieren:
```bash
cp .env.example .env
```
Ersetze anschließend alle Platzhalter und passe mindestens `DOMAIN`, `CONTACT_EMAIL`, Datenbank-, Secret-, SMTP- und S3-Werte an.
## Beispiel `.env`
Diese Datei liegt neben der `docker-compose.yml`:
```env
DOMAIN=app.example.com
CONTACT_EMAIL=admin@deine-domain.de
DB_NAME=fedeo
DB_USER=fedeo
DB_PASSWORD=change-this-db-password
DATABASE_URL=postgres://fedeo:change-this-db-password@db:5432/fedeo
MINIO_ROOT_USER=fedeo-minio
MINIO_ROOT_PASSWORD=change-this-minio-password
MINIO_BUCKET=fedeo
HOST=0.0.0.0
PORT=3100
COOKIE_SECRET=change-this-cookie-secret
JWT_SECRET=change-this-jwt-secret
ENCRYPTION_KEY=change-this-encryption-key
MAILER_SMTP_HOST=smtp.example.com
MAILER_SMTP_PORT=587
MAILER_SMTP_SSL=false
MAILER_SMTP_USER=mailer@example.com
MAILER_SMTP_PASS=change-this-mail-password
MAILER_FROM=FEDEO <no-reply@example.com>
S3_ENDPOINT=http://minio:9000
S3_REGION=eu-central-1
S3_ACCESS_KEY=fedeo-minio
S3_SECRET_KEY=change-this-minio-password
S3_BUCKET=fedeo
M2M_API_KEY=change-this-m2m-key
API_BASE_URL=https://app.example.com/backend
GOCARDLESS_BASE_URL=https://api.gocardless.com
GOCARDLESS_SECRET_ID=replace-this
GOCARDLESS_SECRET_KEY=replace-this
DOKUBOX_IMAP_HOST=imap.example.com
DOKUBOX_IMAP_PORT=993
DOKUBOX_IMAP_SECURE=true
DOKUBOX_IMAP_USER=dokubox@example.com
DOKUBOX_IMAP_PASSWORD=change-this-imap-password
OPENAI_API_KEY=replace-this
STIRLING_API_KEY=replace-this
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
FEDEO_BOOTSTRAP_ADMIN_PASSWORD=change-this-admin-password
FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
MATRIX_SERVER_NAME=app.example.com
MATRIX_POSTGRES_DB=synapse
MATRIX_POSTGRES_USER=synapse
MATRIX_POSTGRES_PASSWORD=change-this-matrix-db-password
MATRIX_TURN_SHARED_SECRET=change-this-turn-secret
MATRIX_HOMESERVER_URL=http://matrix-synapse:8008
MATRIX_RTC_HOST=app.example.com
MATRIX_RTC_JWT_URL=https://app.example.com/livekit/jwt
MATRIX_LIVEKIT_URL=wss://app.example.com/livekit/sfu
MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-synapse-homeserver-yaml
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
LIVEKIT_KEY=fedeo-livekit
LIVEKIT_SECRET=change-this-livekit-secret-please-replace
NUXT_PUBLIC_MATRIX_ELEMENT_URL=https://app.example.com/element
```
Die `FEDEO_BOOTSTRAP_*`-Werte sind für den ersten Start gedacht. Wenn `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` gesetzt sind, legt das Backend idempotent einen Admin-Benutzer, einen ersten Mandanten, eine Administrator-Rolle und grundlegende Stammdaten an. Nach erfolgreichem Erstzugriff solltest du das Bootstrap-Passwort aus der `.env` entfernen oder ändern.
## Docker Compose mit optionalem S3 und Matrix
Die Selfhost-Konfiguration liegt in `docker-compose.selfhost.yml`. Sie startet MinIO standardmäßig mit. Wenn du stattdessen AWS S3, Hetzner Object Storage, Backblaze B2 S3 oder einen anderen externen S3-Dienst nutzen willst, kannst du die Services `minio` und `createbuckets` entfernen und nur die entsprechenden S3-Umgebungsvariablen auf den externen Anbieter zeigen lassen.
Der Matrix-Stack ist im Selfhost-Compose als optionales Profil `matrix` enthalten. Er umfasst Synapse, eine eigene PostgreSQL-Datenbank für Synapse, Redis, `.well-known/matrix`, coturn, LiveKit, den LiveKit-JWT-Service und Element Web. Das einfache Selfhost-Setup nutzt nur `DOMAIN`: Synapse läuft unter `https://DOMAIN/_matrix`, Matrix-Well-Known unter `https://DOMAIN/.well-known/matrix`, LiveKit unter `https://DOMAIN/livekit/sfu`, der JWT-Service unter `https://DOMAIN/livekit/jwt` und Element Web unter `https://DOMAIN/element`. Vor dem ersten Start musst du `matrix/synapse/homeserver.yaml` erzeugen und `matrix/selfhost/element-config.json` auf deine Domain anpassen.
Das Backend führt beim Containerstart standardmäßig `npm run migrate` aus. Setze `FEDEO_RUN_MIGRATIONS=false`, wenn du Migrationen bewusst manuell ausführen möchtest.
```yaml
## Docker Compose File
~~~
services:
traefik:
image: traefik:v2.11
container_name: fedeo-traefik
restart: unless-stopped
command:
- --api.insecure=false
- --api.dashboard=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
- --certificatesresolvers.letsencrypt.acme.email=${CONTACT_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --accesslog=true
- --accesslog.filepath=/logs/access.log
ports:
- "80:80"
- "443:443"
volumes:
- ./traefik/letsencrypt:/letsencrypt
- ./traefik/logs:/logs
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- web
db:
image: postgres:16
container_name: fedeo-db
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 10
networks:
- internal
minio:
image: minio/minio:latest
container_name: fedeo-minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- ./minio:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 10
networks:
- internal
createbuckets:
image: minio/mc:latest
container_name: fedeo-minio-init
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD};
mc mb --ignore-existing local/${MINIO_BUCKET};
mc anonymous set private local/${MINIO_BUCKET};
exit 0;
"
restart: "no"
networks:
- internal
backend:
build:
context: ./backend
container_name: fedeo-backend
restart: unless-stopped
depends_on:
db:
condition: service_healthy
minio:
condition: service_healthy
createbuckets:
condition: service_completed_successfully
environment:
NODE_ENV: production
HOST: ${HOST}
PORT: ${PORT}
COOKIE_SECRET: ${COOKIE_SECRET}
JWT_SECRET: ${JWT_SECRET}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
DATABASE_URL: ${DATABASE_URL}
MAILER_SMTP_HOST: ${MAILER_SMTP_HOST}
MAILER_SMTP_PORT: ${MAILER_SMTP_PORT}
MAILER_SMTP_SSL: ${MAILER_SMTP_SSL}
MAILER_SMTP_USER: ${MAILER_SMTP_USER}
MAILER_SMTP_PASS: ${MAILER_SMTP_PASS}
MAILER_FROM: ${MAILER_FROM}
S3_ENDPOINT: ${S3_ENDPOINT}
S3_REGION: ${S3_REGION}
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_BUCKET: ${S3_BUCKET}
M2M_API_KEY: ${M2M_API_KEY}
API_BASE_URL: ${API_BASE_URL}
GOCARDLESS_BASE_URL: ${GOCARDLESS_BASE_URL}
GOCARDLESS_SECRET_ID: ${GOCARDLESS_SECRET_ID}
GOCARDLESS_SECRET_KEY: ${GOCARDLESS_SECRET_KEY}
DOKUBOX_IMAP_HOST: ${DOKUBOX_IMAP_HOST}
DOKUBOX_IMAP_PORT: ${DOKUBOX_IMAP_PORT}
DOKUBOX_IMAP_SECURE: ${DOKUBOX_IMAP_SECURE}
DOKUBOX_IMAP_USER: ${DOKUBOX_IMAP_USER}
DOKUBOX_IMAP_PASSWORD: ${DOKUBOX_IMAP_PASSWORD}
OPENAI_API_KEY: ${OPENAI_API_KEY}
STIRLING_API_KEY: ${STIRLING_API_KEY}
labels:
- traefik.enable=true
- traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)
- traefik.http.routers.fedeo-backend.entrypoints=websecure
- traefik.http.routers.fedeo-backend.tls.certresolver=letsencrypt
- traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend
- traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-strip
- traefik.http.services.fedeo-backend.loadbalancer.server.port=3100
- traefik.docker.network=fedeo_web
networks:
- web
- internal
frontend:
build:
context: ./frontend
container_name: fedeo-frontend
restart: unless-stopped
depends_on:
- backend
environment:
NODE_ENV: production
NUXT_PUBLIC_API_BASE: https://${DOMAIN}/backend
NUXT_PUBLIC_PDF_LICENSE: ${NUXT_PUBLIC_PDF_LICENSE}
labels:
- traefik.enable=true
- traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`)
- traefik.http.routers.fedeo-frontend.entrypoints=websecure
- traefik.http.routers.fedeo-frontend.tls.certresolver=letsencrypt
- traefik.http.services.fedeo-frontend.loadbalancer.server.port=3000
- traefik.docker.network=fedeo_web
networks:
- web
frontend:
image: git.federspiel.tech/flfeders/fedeo/frontend:main
restart: always
environment:
- NUXT_PUBLIC_API_BASE=https://${DOMAIN}/backend
- NUXT_PUBLIC_PDF_LICENSE=${PDF_LICENSE}
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.port=3000"
# Middlewares
- "traefik.http.middlewares.fedeo-frontend-redirect-web-secure.redirectscheme.scheme=https"
# Web Entrypoint
- "traefik.http.routers.fedeo-frontend.middlewares=fedeo-frontend-redirect-web-secure"
- "traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
- "traefik.http.routers.fedeo-frontend.entrypoints=web"
# Web Secure Entrypoint
- "traefik.http.routers.fedeo-frontend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
- "traefik.http.routers.fedeo-frontend-secure.entrypoints=web-secured" #
- "traefik.http.routers.fedeo-frontend-secure.tls.certresolver=mytlschallenge"
backend:
image: git.federspiel.tech/flfeders/fedeo/backend:main
restart: always
environment:
- INFISICAL_CLIENT_ID=
- INFISICAL_CLIENT_SECRET=
- NODE_ENV=production
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.port=3100"
# Middlewares
- "traefik.http.middlewares.fedeo-backend-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend"
# Web Entrypoint
- "traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-redirect-web-secure"
- "traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
- "traefik.http.routers.fedeo-backend.entrypoints=web"
# Web Secure Entrypoint
- "traefik.http.routers.fedeo-backend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" #
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
# db:
# image: postgres
# restart: always
# shm_size: 128mb
# environment:
# POSTGRES_PASSWORD:
# POSTGRES_USER:
# POSTGRES_DB:
# volumes:
# - ./pg-data:/var/lib/postgresql/data
# ports:
# - "5432:5432"
traefik:
image: traefik:v2.11
restart: unless-stopped
container_name: traefik
command:
- "--api.insecure=false"
- "--api.dashboard=false"
- "--api.debug=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=traefik"
- "--entrypoints.web.address=:80"
- "--entrypoints.web-secured.address=:443"
- "--accesslog=true"
- "--accesslog.filepath=/logs/access.log"
- "--accesslog.bufferingsize=5000"
- "--accesslog.fields.defaultMode=keep"
- "--accesslog.fields.headers.defaultMode=keep"
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" #
- "--certificatesresolvers.mytlschallenge.acme.email=${CONTACT_EMAIL}"
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
ports:
- 80:80
- 443:443
volumes:
- "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS)
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/logs:/logs"
networks:
- traefik
networks:
web:
name: fedeo_web
driver: bridge
internal:
name: fedeo_internal
driver: bridge
```
## Externe S3-Provider statt MinIO
Wenn du keinen lokalen MinIO-Container betreiben willst:
1. Entferne die Services `minio` und `createbuckets` aus der Compose-Datei.
2. Entferne im Backend `depends_on` fur `minio` und `createbuckets`.
3. Trage in `.env` die Zugangsdaten des externen S3-Dienstes ein.
Beispiel fur die relevanten Werte:
```env
S3_ENDPOINT=https://s3.eu-central-1.amazonaws.com
S3_REGION=eu-central-1
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_BUCKET=fedeo
```
Hinweis: Das Backend nutzt `forcePathStyle: true`. Das funktioniert sauber mit MinIO und vielen S3-kompatiblen Providern. Bei reinem AWS S3 kann je nach Endpoint-Setup ein abweichendes Verhalten sinnvoll sein. Falls du AWS S3 einsetzen willst, sollte die S3-Initialisierung im Backend gegen den konkreten Zielprovider getestet werden.
## Start des Stacks
Im Deploy-Verzeichnis:
```bash
docker compose -f docker-compose.selfhost.yml build
docker compose -f docker-compose.selfhost.yml up -d
```
Mit Matrix-Profil:
```bash
docker compose -f docker-compose.selfhost.yml --profile matrix up -d
```
Synapse-Konfiguration vor dem ersten Matrix-Start erzeugen:
```bash
docker compose -f docker-compose.selfhost.yml --profile matrix run --rm \
-e SYNAPSE_SERVER_NAME="${MATRIX_SERVER_NAME}" \
-e SYNAPSE_REPORT_STATS=no \
matrix-synapse generate
```
Danach in `matrix/synapse/homeserver.yaml` mindestens Datenbank, Redis, `public_baseurl`, TURN und `registration_shared_secret` setzen. Der Wert von `registration_shared_secret` muss zusätzlich als `MATRIX_REGISTRATION_SHARED_SECRET` in die `.env`, damit FEDEO Matrix-Nutzer provisionieren kann.
Danach Status prufen:
```bash
docker compose -f docker-compose.selfhost.yml ps
docker compose -f docker-compose.selfhost.yml logs -f traefik
docker compose -f docker-compose.selfhost.yml logs -f backend
```
Wenn du Migrationen manuell ausführen möchtest:
```bash
docker compose -f docker-compose.selfhost.yml run --rm backend npm run migrate
```
## Funktionsprufung
Nach dem ersten Start sollten mindestens diese Checks erfolgreich sein:
```bash
curl -I https://app.example.com
curl https://app.example.com/backend/health
```
Erwartung:
- Frontend liefert `200` oder `302`
- Backend liefert JSON wie `{"status":"ok"}`
Wenn der Bootstrap aktiviert ist, kannst du dich danach mit `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` anmelden. Die Mandantensperre wird über `locked` gesteuert; `hasActiveLicense` wird nicht mehr für den Selfhost-Zugriff ausgewertet.
## Updates
Bei neuen Versionen:
```bash
git pull
docker compose -f docker-compose.selfhost.yml build
docker compose -f docker-compose.selfhost.yml up -d
```
Der Backend-Container wendet Datenbankmigrationen beim Start automatisch an. Bei kritischen Updates sollte vorher ein Backup von `./postgres` und `./minio` erstellt werden.
Falls du statt lokaler Builds vorgebaute Images verwenden willst, kannst du in der Compose-Datei `build:` durch passende `image:`-Eintrage ersetzen. Erst dann ist ein vorgelagertes `docker compose pull` sinnvoll.
## Backup-Empfehlung
Regelmassig sichern:
- `./postgres`
- `./minio` falls MinIO lokal genutzt wird
- `./matrix/postgres` falls Matrix lokal betrieben wird
- `./matrix/synapse` falls Matrix lokal betrieben wird
- `./traefik/letsencrypt/acme.json`
- deine `.env`
- deine dokumentierten Secret-Werte aus der `.env` oder deinem Secret-Management
## Bekannte Betriebsbesonderheiten
- Das Backend startet nur sauber, wenn alle Pflichtvariablen gesetzt sind.
- Ohne korrekt gesetzte S3-Secrets funktionieren Dateiuploads und dateibasierte Funktionen nicht.
- Fur die Frontend-PDF-Funktion wird eine gueltige `NUXT_PUBLIC_PDF_LICENSE` benotigt.
- PostgreSQL ist im Projekt vorgesehen; andere SQL-Datenbanken sind in dieser Compose-Datei nicht berucksichtigt.
## Optional: Nur mit bestehender externer Infrastruktur
Wenn bereits vorhanden:
- externer Reverse Proxy
- externer PostgreSQL-Server
- externer S3-Speicher
- externe Zertifikatsverwaltung
dann konnen `traefik`, `db` und `minio` aus dem Stack entfernt werden. In diesem Fall mussen die zugehorigen Hostnamen und Zugangsdaten in der `.env` beziehungsweise im Frontend-Environment auf die externe Infrastruktur zeigen.
traefik:
external: false
~~~

1
backend/.gitignore vendored
View File

@@ -3,4 +3,3 @@ node_modules
.env
/src/generated/prisma
/dist/

View File

@@ -1,3 +0,0 @@
{
"rules": []
}

View File

@@ -1,21 +1,11 @@
FROM node:20-bookworm-slim
FROM node:20-alpine
WORKDIR /usr/src/app
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
poppler-utils \
tesseract-ocr \
tesseract-ocr-deu \
tesseract-ocr-eng \
&& rm -rf /var/lib/apt/lists/*
# Package-Dateien
COPY package*.json ./
# Dev + Prod Dependencies (für TS-Build nötig).
# Sharp benötigt im Linux-Container native optionale Pakete, auch wenn das Lockfile auf macOS erzeugt wurde.
RUN npm install --include=optional \
&& npm install --include=optional --os=linux --cpu=x64 sharp
# Dev + Prod Dependencies (für TS-Build nötig)
RUN npm install
# Restlicher Sourcecode
COPY . .
@@ -26,5 +16,5 @@ RUN npm run build
# Port freigeben
EXPOSE 3100
# Migrationen ausführen und App starten
CMD ["sh", "./docker-entrypoint.sh"]
# Start der App
CMD ["node", "dist/src/index.js"]

View File

@@ -1,33 +1,13 @@
// src/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";
import { drizzle } from "drizzle-orm/node-postgres"
import { Pool } from "pg"
import {secrets} from "../src/utils/secrets";
import * as schema from "./schema"
console.log("[DB INIT] 1. Suche Connection String...");
const fallbackConnectionString = "postgres://postgres:wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu@fedeo-db-001.vpn.internal:5432/fedeo"
// Checken woher die URL kommt
let connectionString = process.env.DATABASE_URL || secrets.DATABASE_URL || fallbackConnectionString;
if (process.env.DATABASE_URL) {
console.log("[DB INIT] -> Gefunden in process.env.DATABASE_URL");
} else if (secrets.DATABASE_URL) {
console.log("[DB INIT] -> Gefunden in secrets.DATABASE_URL");
} else if (connectionString) {
console.log("[DB INIT] -> Nutze Fallback aus dem Projekt");
} else {
console.error("[DB INIT] ❌ KEIN CONNECTION STRING GEFUNDEN! .env nicht geladen?");
}
export const pool = new Pool({
connectionString,
max: 10,
});
connectionString: secrets.DATABASE_URL,
max: 10, // je nach Last
})
// TEST: Ist die DB wirklich da?
pool.query('SELECT NOW()')
.then(res => console.log(`[DB INIT] ✅ VERBINDUNG ERFOLGREICH! Zeit auf DB: ${res.rows[0].now}`))
.catch(err => console.error(`[DB INIT] ❌ VERBINDUNGSFEHLER:`, err.message));
export const db = drizzle(pool, { schema });
export const db = drizzle(pool , {schema})

View File

@@ -1,2 +0,0 @@
-- No-op migration: Datei war im Journal referenziert, aber fehlte im Repository.
SELECT 1;

View File

@@ -1,2 +0,0 @@
-- No-op migration: Datei war im Journal referenziert, aber fehlte im Repository.
SELECT 1;

View File

@@ -1,95 +0,0 @@
CREATE TABLE "m2m_api_keys" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"tenant_id" bigint NOT NULL,
"user_id" uuid NOT NULL,
"created_by" uuid,
"name" text NOT NULL,
"key_prefix" text NOT NULL,
"key_hash" text NOT NULL,
"active" boolean DEFAULT true NOT NULL,
"last_used_at" timestamp with time zone,
"expires_at" timestamp with time zone,
CONSTRAINT "m2m_api_keys_key_hash_unique" UNIQUE("key_hash")
);
--> statement-breakpoint
CREATE TABLE "serialtypes" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "serialtypes_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"name" text NOT NULL,
"intervall" text,
"icon" text,
"tenant" bigint NOT NULL,
"archived" boolean DEFAULT false NOT NULL,
"updated_at" timestamp with time zone,
"updated_by" uuid
);
--> statement-breakpoint
CREATE TABLE "serial_executions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant" bigint NOT NULL,
"execution_date" timestamp NOT NULL,
"status" text DEFAULT 'draft',
"created_by" text,
"created_at" timestamp DEFAULT now(),
"summary" text
);
--> statement-breakpoint
CREATE TABLE "public_links" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"token" text NOT NULL,
"tenant" integer NOT NULL,
"default_profile" uuid,
"is_protected" boolean DEFAULT false NOT NULL,
"pin_hash" text,
"config" jsonb DEFAULT '{}'::jsonb,
"name" text NOT NULL,
"description" text,
"active" boolean DEFAULT true NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
CONSTRAINT "public_links_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "wiki_pages" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" bigint NOT NULL,
"parent_id" uuid,
"title" text NOT NULL,
"content" jsonb,
"is_folder" boolean DEFAULT false NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"entity_type" text,
"entity_id" bigint,
"entity_uuid" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone,
"created_by" uuid,
"updated_by" uuid
);
--> statement-breakpoint
ALTER TABLE "projects" ALTER COLUMN "active_phase" SET DEFAULT 'Erstkontakt';--> statement-breakpoint
ALTER TABLE "createddocuments" ADD COLUMN "serialexecution" uuid;--> statement-breakpoint
ALTER TABLE "devices" ADD COLUMN "last_seen" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "devices" ADD COLUMN "last_debug_info" jsonb;--> statement-breakpoint
ALTER TABLE "files" ADD COLUMN "size" bigint;--> statement-breakpoint
ALTER TABLE "staff_time_events" ADD COLUMN "related_event_id" uuid;--> statement-breakpoint
ALTER TABLE "m2m_api_keys" ADD CONSTRAINT "m2m_api_keys_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "m2m_api_keys" ADD CONSTRAINT "m2m_api_keys_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "m2m_api_keys" ADD CONSTRAINT "m2m_api_keys_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_related_event_id_staff_time_events_id_fk" FOREIGN KEY ("related_event_id") REFERENCES "public"."staff_time_events"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "serialtypes" ADD CONSTRAINT "serialtypes_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "serialtypes" ADD CONSTRAINT "serialtypes_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "serial_executions" ADD CONSTRAINT "serial_executions_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "public_links" ADD CONSTRAINT "public_links_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "public_links" ADD CONSTRAINT "public_links_default_profile_auth_profiles_id_fk" FOREIGN KEY ("default_profile") REFERENCES "public"."auth_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_parent_id_wiki_pages_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."wiki_pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "wiki_pages_tenant_idx" ON "wiki_pages" USING btree ("tenant_id");--> statement-breakpoint
CREATE INDEX "wiki_pages_parent_idx" ON "wiki_pages" USING btree ("parent_id");--> statement-breakpoint
CREATE INDEX "wiki_pages_entity_int_idx" ON "wiki_pages" USING btree ("tenant_id","entity_type","entity_id");--> statement-breakpoint
CREATE INDEX "wiki_pages_entity_uuid_idx" ON "wiki_pages" USING btree ("tenant_id","entity_type","entity_uuid");--> statement-breakpoint
ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_serialexecution_serial_executions_id_fk" FOREIGN KEY ("serialexecution") REFERENCES "public"."serial_executions"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1 +0,0 @@
ALTER TABLE "services" ADD COLUMN "priceUpdateLocked" boolean DEFAULT false NOT NULL;

View File

@@ -1 +0,0 @@
ALTER TABLE "customers" ADD COLUMN "customTaxType" text;

View File

@@ -1,16 +0,0 @@
CREATE TABLE "contracttypes" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "contracttypes_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"tenant" bigint NOT NULL,
"name" text NOT NULL,
"description" text,
"paymentType" text,
"recurring" boolean DEFAULT false NOT NULL,
"archived" boolean DEFAULT false NOT NULL,
"updated_at" timestamp with time zone,
"updated_by" uuid
);
--> statement-breakpoint
ALTER TABLE "contracttypes" ADD CONSTRAINT "contracttypes_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "contracttypes" ADD CONSTRAINT "contracttypes_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1,3 +0,0 @@
ALTER TABLE "contracts" ADD COLUMN "contracttype" bigint;
--> statement-breakpoint
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_contracttype_contracttypes_id_fk" FOREIGN KEY ("contracttype") REFERENCES "public"."contracttypes"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1,3 +0,0 @@
ALTER TABLE "contracttypes" ADD COLUMN "billingInterval" text;
--> statement-breakpoint
ALTER TABLE "contracts" ADD COLUMN "billingInterval" text;

View File

@@ -1,16 +0,0 @@
CREATE TABLE "entitybankaccounts" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "entitybankaccounts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"tenant" bigint NOT NULL,
"iban_encrypted" jsonb NOT NULL,
"bic_encrypted" jsonb NOT NULL,
"bank_name_encrypted" jsonb NOT NULL,
"description" text,
"updated_at" timestamp with time zone,
"updated_by" uuid,
"archived" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
ALTER TABLE "entitybankaccounts" ADD CONSTRAINT "entitybankaccounts_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "entitybankaccounts" ADD CONSTRAINT "entitybankaccounts_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1,73 +0,0 @@
CREATE TABLE "customerspaces" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "customerspaces_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"name" text NOT NULL,
"type" text NOT NULL,
"tenant" bigint NOT NULL,
"customer" bigint NOT NULL,
"spaceNumber" text NOT NULL,
"parentSpace" bigint,
"infoData" jsonb DEFAULT '{"zip":"","city":"","streetNumber":""}'::jsonb NOT NULL,
"description" text,
"archived" boolean DEFAULT false NOT NULL,
"updated_at" timestamp with time zone,
"updated_by" uuid
);
--> statement-breakpoint
CREATE TABLE "customerinventoryitems" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "customerinventoryitems_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"name" text NOT NULL,
"description" text,
"tenant" bigint NOT NULL,
"customer" bigint NOT NULL,
"customerspace" bigint,
"customerInventoryId" text NOT NULL,
"serialNumber" text,
"quantity" bigint DEFAULT 0 NOT NULL,
"manufacturer" text,
"manufacturerNumber" text,
"purchaseDate" date,
"purchasePrice" double precision DEFAULT 0,
"currentValue" double precision,
"product" bigint,
"archived" boolean DEFAULT false NOT NULL,
"updated_at" timestamp with time zone,
"updated_by" uuid
);
--> statement-breakpoint
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_parentSpace_customerspaces_id_fk" FOREIGN KEY ("parentSpace") REFERENCES "public"."customerspaces"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_customerspace_customerspaces_id_fk" FOREIGN KEY ("customerspace") REFERENCES "public"."customerspaces"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_product_products_id_fk" FOREIGN KEY ("product") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
CREATE UNIQUE INDEX "customerinventoryitems_tenant_customerInventoryId_idx" ON "customerinventoryitems" USING btree ("tenant","customerInventoryId");
--> statement-breakpoint
ALTER TABLE "historyitems" ADD COLUMN "customerspace" bigint;
--> statement-breakpoint
ALTER TABLE "historyitems" ADD COLUMN "customerinventoryitem" bigint;
--> statement-breakpoint
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_customerspace_customerspaces_id_fk" FOREIGN KEY ("customerspace") REFERENCES "public"."customerspaces"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_customerinventoryitem_customerinventoryitems_id_fk" FOREIGN KEY ("customerinventoryitem") REFERENCES "public"."customerinventoryitems"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "tenants" ALTER COLUMN "numberRanges" SET DEFAULT '{"vendors":{"prefix":"","suffix":"","nextNumber":10000},"customers":{"prefix":"","suffix":"","nextNumber":10000},"products":{"prefix":"AT-","suffix":"","nextNumber":1000},"quotes":{"prefix":"AN-","suffix":"","nextNumber":1000},"confirmationOrders":{"prefix":"AB-","suffix":"","nextNumber":1000},"invoices":{"prefix":"RE-","suffix":"","nextNumber":1000},"spaces":{"prefix":"LP-","suffix":"","nextNumber":1000},"customerspaces":{"prefix":"KLP-","suffix":"","nextNumber":1000},"inventoryitems":{"prefix":"IA-","suffix":"","nextNumber":1000},"customerinventoryitems":{"prefix":"KIA-","suffix":"","nextNumber":1000},"projects":{"prefix":"PRJ-","suffix":"","nextNumber":1000},"costcentres":{"prefix":"KST-","suffix":"","nextNumber":1000}}'::jsonb;
--> statement-breakpoint
UPDATE "tenants"
SET "numberRanges" = COALESCE("numberRanges", '{}'::jsonb) || jsonb_build_object(
'customerspaces', COALESCE("numberRanges"->'customerspaces', '{"prefix":"KLP-","suffix":"","nextNumber":1000}'::jsonb),
'customerinventoryitems', COALESCE("numberRanges"->'customerinventoryitems', '{"prefix":"KIA-","suffix":"","nextNumber":1000}'::jsonb)
);

View File

@@ -1,3 +0,0 @@
ALTER TABLE "customerinventoryitems" ADD COLUMN "vendor" bigint;
--> statement-breakpoint
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_vendor_vendors_id_fk" FOREIGN KEY ("vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1,20 +0,0 @@
CREATE TABLE "memberrelations" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "memberrelations_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"tenant" bigint NOT NULL,
"type" text NOT NULL,
"billingInterval" text NOT NULL,
"billingAmount" double precision DEFAULT 0 NOT NULL,
"archived" boolean DEFAULT false NOT NULL,
"updated_at" timestamp with time zone,
"updated_by" uuid
);
--> statement-breakpoint
ALTER TABLE "customers" ADD COLUMN "memberrelation" bigint;
--> statement-breakpoint
ALTER TABLE "memberrelations" ADD CONSTRAINT "memberrelations_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "memberrelations" ADD CONSTRAINT "memberrelations_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "customers" ADD CONSTRAINT "customers_memberrelation_memberrelations_id_fk" FOREIGN KEY ("memberrelation") REFERENCES "public"."memberrelations"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1,4 +0,0 @@
ALTER TABLE "historyitems" ADD COLUMN "memberrelation" bigint;
--> statement-breakpoint
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_memberrelation_memberrelations_id_fk" FOREIGN KEY ("memberrelation") REFERENCES "public"."memberrelations"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1,33 +0,0 @@
ALTER TABLE "customers" ADD COLUMN IF NOT EXISTS "memberrelation" bigint;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'customers_memberrelation_memberrelations_id_fk'
) THEN
ALTER TABLE "customers"
ADD CONSTRAINT "customers_memberrelation_memberrelations_id_fk"
FOREIGN KEY ("memberrelation")
REFERENCES "public"."memberrelations"("id")
ON DELETE no action
ON UPDATE no action;
END IF;
END $$;
UPDATE "customers"
SET "memberrelation" = ("infoData"->>'memberrelation')::bigint
WHERE
"memberrelation" IS NULL
AND "type" = 'Mitglied'
AND jsonb_typeof(COALESCE("infoData", '{}'::jsonb)) = 'object'
AND COALESCE("infoData", '{}'::jsonb) ? 'memberrelation'
AND ("infoData"->>'memberrelation') ~ '^[0-9]+$';
UPDATE "customers"
SET "infoData" = COALESCE("infoData", '{}'::jsonb) - 'memberrelation'
WHERE
"type" = 'Mitglied'
AND jsonb_typeof(COALESCE("infoData", '{}'::jsonb)) = 'object'
AND COALESCE("infoData", '{}'::jsonb) ? 'memberrelation';

View File

@@ -1 +0,0 @@
-- Absichtlich leer: Die Objekte aus dieser generierten Migration existieren bereits in früheren Migrationen.

View File

@@ -1,3 +0,0 @@
ALTER TABLE "accounts" ADD COLUMN "accountChart" text DEFAULT 'skr03' NOT NULL;
--> statement-breakpoint
ALTER TABLE "tenants" ADD COLUMN "accountChart" text DEFAULT 'skr03' NOT NULL;

View File

@@ -1,3 +0,0 @@
ALTER TABLE "createddocuments"
ALTER COLUMN "customSurchargePercentage" TYPE double precision
USING "customSurchargePercentage"::double precision;

View File

@@ -1 +0,0 @@
ALTER TABLE "files" ADD COLUMN "extracted_text" text;

View File

@@ -1,2 +0,0 @@
ALTER TABLE "auth_users"
ADD COLUMN "is_admin" boolean NOT NULL DEFAULT false;

View File

@@ -1,2 +0,0 @@
ALTER TABLE "tasks"
ADD COLUMN "dependency_ids" jsonb NOT NULL DEFAULT '[]'::jsonb;

View File

@@ -1,2 +0,0 @@
ALTER TABLE "tenants"
ADD COLUMN "taxEvaluationPeriod" text DEFAULT 'monthly' NOT NULL;

View File

@@ -1,37 +0,0 @@
CREATE TABLE "branches" (
"id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"tenant" bigint NOT NULL,
"name" text NOT NULL,
"number" text,
"description" text,
"archived" boolean DEFAULT false NOT NULL,
"updated_at" timestamp with time zone,
"updated_by" uuid
);
--> statement-breakpoint
ALTER TABLE "branches" ADD CONSTRAINT "branches_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "branches" ADD CONSTRAINT "branches_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "costcentres" ADD COLUMN "branch" bigint;
--> statement-breakpoint
ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_branch_branches_id_fk" FOREIGN KEY ("branch") REFERENCES "public"."branches"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "auth_profiles" ADD COLUMN "branch_id" bigint;
--> statement-breakpoint
ALTER TABLE "auth_profiles" ADD CONSTRAINT "auth_profiles_branch_id_branches_id_fk" FOREIGN KEY ("branch_id") REFERENCES "public"."branches"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
CREATE TABLE "auth_profile_branches" (
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"profile_id" uuid NOT NULL,
"branch_id" bigint NOT NULL,
"created_by" uuid,
CONSTRAINT "auth_profile_branches_profile_id_branch_id_pk" PRIMARY KEY("profile_id","branch_id")
);
--> statement-breakpoint
ALTER TABLE "auth_profile_branches" ADD CONSTRAINT "auth_profile_branches_profile_id_auth_profiles_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."auth_profiles"("id") ON DELETE cascade ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "auth_profile_branches" ADD CONSTRAINT "auth_profile_branches_branch_id_branches_id_fk" FOREIGN KEY ("branch_id") REFERENCES "public"."branches"("id") ON DELETE cascade ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "auth_profile_branches" ADD CONSTRAINT "auth_profile_branches_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1,6 +0,0 @@
ALTER TABLE "statementallocations"
ADD COLUMN "booking_mode" text DEFAULT 'expense' NOT NULL,
ADD COLUMN "depreciation_months" integer,
ADD COLUMN "depreciation_start_date" text,
ADD COLUMN "depreciation_label" text,
ADD COLUMN "depreciation_group" text;

View File

@@ -1,3 +0,0 @@
ALTER TABLE "statementallocations"
ADD COLUMN "depreciation_method" text,
ADD COLUMN "residual_value" double precision;

View File

@@ -1,2 +0,0 @@
ALTER TABLE "products"
ADD COLUMN "supplierLink" text;

View File

@@ -1,31 +0,0 @@
CREATE TABLE "teams" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "teams_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"tenant" bigint NOT NULL,
"name" text NOT NULL,
"description" text,
"branch" bigint,
"archived" boolean DEFAULT false NOT NULL,
"updated_at" timestamp with time zone,
"updated_by" uuid
);
--> statement-breakpoint
CREATE TABLE "auth_profile_teams" (
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"profile_id" uuid NOT NULL,
"team_id" bigint NOT NULL,
"created_by" uuid,
CONSTRAINT "auth_profile_teams_profile_id_team_id_pk" PRIMARY KEY("profile_id","team_id")
);
--> statement-breakpoint
ALTER TABLE "teams" ADD CONSTRAINT "teams_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "teams" ADD CONSTRAINT "teams_branch_branches_id_fk" FOREIGN KEY ("branch") REFERENCES "public"."branches"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "teams" ADD CONSTRAINT "teams_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "auth_profile_teams" ADD CONSTRAINT "auth_profile_teams_profile_id_auth_profiles_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."auth_profiles"("id") ON DELETE cascade ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "auth_profile_teams" ADD CONSTRAINT "auth_profile_teams_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "auth_profile_teams" ADD CONSTRAINT "auth_profile_teams_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1 +0,0 @@
ALTER TABLE "events" ADD COLUMN "quick" boolean DEFAULT false NOT NULL;

View File

@@ -1,10 +0,0 @@
ALTER TABLE "statementallocations" ALTER COLUMN "bs_id" DROP NOT NULL;
ALTER TABLE "statementallocations" ADD COLUMN "manual_booking_date" text;
ALTER TABLE "statementallocations" ADD COLUMN "contra_account" bigint;
ALTER TABLE "statementallocations" ADD COLUMN "contra_ownaccount" uuid;
ALTER TABLE "statementallocations" ADD COLUMN "contra_customer" bigint;
ALTER TABLE "statementallocations" ADD COLUMN "contra_vendor" bigint;
ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_contra_account_accounts_id_fk" FOREIGN KEY ("contra_account") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_contra_ownaccount_ownaccounts_id_fk" FOREIGN KEY ("contra_ownaccount") REFERENCES "public"."ownaccounts"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_contra_customer_customers_id_fk" FOREIGN KEY ("contra_customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_contra_vendor_vendors_id_fk" FOREIGN KEY ("contra_vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1 +0,0 @@
ALTER TABLE "statementallocations" ADD COLUMN "datev_tax_key" text;

View File

@@ -1 +0,0 @@
ALTER TABLE "statementallocations" ADD COLUMN "manual_invoice_side" text;

View File

@@ -1,2 +0,0 @@
ALTER TABLE "costcentres" ADD COLUMN "parent_costcentre" uuid;
ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_parent_costcentre_costcentres_id_fk" FOREIGN KEY ("parent_costcentre") REFERENCES "public"."costcentres"("id") ON DELETE no action ON UPDATE no action;

View File

@@ -1,8 +0,0 @@
ALTER TABLE "events" ADD COLUMN "color" text;
UPDATE "events" AS e
SET "color" = COALESCE(t."calendarConfig"->'quickEntry'->>'color', '#2563eb')
FROM "tenants" AS t
WHERE e."tenant" = t."id"
AND e."quick" = true
AND e."color" IS NULL;

View File

@@ -1,2 +0,0 @@
ALTER TABLE "auth_profiles"
ADD COLUMN IF NOT EXISTS "availability_note" text;

View File

@@ -1,3 +0,0 @@
ALTER TABLE "historyitems" ADD COLUMN "contract" bigint;
--> statement-breakpoint
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_contract_contracts_id_fk" FOREIGN KEY ("contract") REFERENCES "public"."contracts"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -1 +0,0 @@
ALTER TABLE "contracts" ADD COLUMN "allowedContracttypes" jsonb DEFAULT '[]'::jsonb NOT NULL;

View File

@@ -1,53 +0,0 @@
CREATE TABLE "outgoingsepamandates" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "outgoingsepamandates_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"tenant" bigint NOT NULL,
"customer" bigint NOT NULL,
"bankaccount" bigint NOT NULL,
"reference" text NOT NULL,
"status" text DEFAULT 'Entwurf' NOT NULL,
"mandate_type" text DEFAULT 'CORE' NOT NULL,
"sequence_type" text DEFAULT 'RCUR' NOT NULL,
"signed_at" timestamp with time zone,
"valid_from" timestamp with time zone,
"valid_until" timestamp with time zone,
"default_mandate" boolean DEFAULT false NOT NULL,
"notes" text,
"archived" boolean DEFAULT false NOT NULL,
"updated_at" timestamp with time zone,
"updated_by" uuid
);
--> statement-breakpoint
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_bankaccount_entitybankaccounts_id_fk" FOREIGN KEY ("bankaccount") REFERENCES "public"."entitybankaccounts"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "contracts" ADD COLUMN "outgoingsepamandate" bigint;
--> statement-breakpoint
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_outgoingsepamandate_outgoingsepamandates_id_fk" FOREIGN KEY ("outgoingsepamandate") REFERENCES "public"."outgoingsepamandates"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "createddocuments" ADD COLUMN "outgoingsepamandate" bigint;
--> statement-breakpoint
ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_outgoingsepamandate_outgoingsepamandates_id_fk" FOREIGN KEY ("outgoingsepamandate") REFERENCES "public"."outgoingsepamandates"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "historyitems" ADD COLUMN "outgoingsepamandate" bigint;
--> statement-breakpoint
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_outgoingsepamandate_outgoingsepamandates_id_fk" FOREIGN KEY ("outgoingsepamandate") REFERENCES "public"."outgoingsepamandates"("id") ON DELETE cascade ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "tenants" ALTER COLUMN "numberRanges" SET DEFAULT '{"vendors":{"prefix":"","suffix":"","nextNumber":10000},"customers":{"prefix":"","suffix":"","nextNumber":10000},"products":{"prefix":"AT-","suffix":"","nextNumber":1000},"quotes":{"prefix":"AN-","suffix":"","nextNumber":1000},"costEstimates":{"prefix":"KS-","suffix":"","nextNumber":1000},"confirmationOrders":{"prefix":"AB-","suffix":"","nextNumber":1000},"invoices":{"prefix":"RE-","suffix":"","nextNumber":1000},"deliveryNotes":{"prefix":"LS-","suffix":"","nextNumber":1000},"packingSlips":{"prefix":"PS-","suffix":"","nextNumber":1000},"spaces":{"prefix":"LP-","suffix":"","nextNumber":1000},"customerspaces":{"prefix":"KLP-","suffix":"","nextNumber":1000},"inventoryitems":{"prefix":"IA-","suffix":"","nextNumber":1000},"customerinventoryitems":{"prefix":"KIA-","suffix":"","nextNumber":1000},"projects":{"prefix":"PRJ-","suffix":"","nextNumber":1000},"costcentres":{"prefix":"KST-","suffix":"","nextNumber":1000},"outgoingsepamandates":{"prefix":"SEPA-","suffix":"","nextNumber":1000}}'::jsonb;
--> statement-breakpoint
UPDATE "tenants"
SET "numberRanges" = COALESCE("numberRanges", '{}'::jsonb) || jsonb_build_object(
'outgoingsepamandates',
COALESCE("numberRanges"->'outgoingsepamandates', '{"prefix":"SEPA-","suffix":"","nextNumber":1000}'::jsonb)
);
--> statement-breakpoint
UPDATE "tenants"
SET "features" = COALESCE("features", '{}'::jsonb) || jsonb_build_object(
'outgoingsepamandates',
COALESCE("features"->'outgoingsepamandates', 'true'::jsonb)
);

View File

@@ -1,43 +0,0 @@
CREATE TABLE "communication_rooms" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" bigint NOT NULL,
"key" text NOT NULL,
"name" text NOT NULL,
"topic" text,
"type" text DEFAULT 'room' NOT NULL,
"entity_type" text,
"entity_id" bigint,
"entity_uuid" uuid,
"matrix_room_id" text,
"matrix_alias" text,
"parent_space_room_id" text,
"archived" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone,
"created_by" uuid,
"updated_by" uuid
);
ALTER TABLE "communication_rooms"
ADD CONSTRAINT "communication_rooms_tenant_id_tenants_id_fk"
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
ON DELETE cascade ON UPDATE no action;
ALTER TABLE "communication_rooms"
ADD CONSTRAINT "communication_rooms_created_by_auth_users_id_fk"
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
ON DELETE no action ON UPDATE no action;
ALTER TABLE "communication_rooms"
ADD CONSTRAINT "communication_rooms_updated_by_auth_users_id_fk"
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
ON DELETE no action ON UPDATE no action;
CREATE UNIQUE INDEX "communication_rooms_tenant_key_idx"
ON "communication_rooms" USING btree ("tenant_id", "key");
CREATE INDEX "communication_rooms_tenant_idx"
ON "communication_rooms" USING btree ("tenant_id");
CREATE INDEX "communication_rooms_entity_idx"
ON "communication_rooms" USING btree ("tenant_id", "entity_type", "entity_id", "entity_uuid");

View File

@@ -1,5 +0,0 @@
ALTER TABLE "events" ADD COLUMN "state" text DEFAULT 'Final' NOT NULL;
UPDATE "events"
SET "state" = 'Final'
WHERE "state" IS NULL;

View File

@@ -1 +0,0 @@
ALTER TABLE "events" ADD COLUMN "repeatInterval" text DEFAULT 'Keine Wiederholung' NOT NULL;

View File

@@ -1,46 +0,0 @@
CREATE TABLE "notification_push_subscriptions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" bigint NOT NULL,
"user_id" uuid NOT NULL,
"endpoint" text NOT NULL,
"p256dh" text NOT NULL,
"auth" text NOT NULL,
"user_agent" text,
"device_label" text,
"meta" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
"disabled_at" timestamp with time zone,
CONSTRAINT "notification_push_subscriptions_endpoint_key" UNIQUE("endpoint")
);
ALTER TABLE "notification_push_subscriptions"
ADD CONSTRAINT "notification_push_subscriptions_tenant_id_tenants_id_fk"
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
ON DELETE cascade ON UPDATE cascade;
ALTER TABLE "notification_push_subscriptions"
ADD CONSTRAINT "notification_push_subscriptions_user_id_auth_users_id_fk"
FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id")
ON DELETE cascade ON UPDATE cascade;
INSERT INTO "notifications_event_types" (
"event_key",
"display_name",
"description",
"category",
"severity",
"allowed_channels"
) VALUES
('system.test_push', 'Test-Push', 'Testet Desktop-Benachrichtigungen für den angemeldeten Nutzer.', 'system', 'info', '["inapp", "push"]'::jsonb),
('communication.message.new', 'Neue Chatnachricht', 'Benachrichtigt über relevante neue Chatnachrichten.', 'communication', 'info', '["inapp", "push"]'::jsonb),
('communication.call.started', 'Besprechung gestartet', 'Benachrichtigt Raumteilnehmer über gestartete Audio- oder Videoanrufe.', 'communication', 'info', '["inapp", "push"]'::jsonb),
('communication.call.missed', 'Verpasster Anruf', 'Benachrichtigt über verpasste Besprechungen.', 'communication', 'warning', '["inapp", "push"]'::jsonb),
('communication.room.invited', 'Raumeinladung', 'Benachrichtigt über Einladungen in Kommunikationsräume.', 'communication', 'info', '["inapp", "push"]'::jsonb)
ON CONFLICT ("event_key") DO UPDATE SET
"display_name" = EXCLUDED."display_name",
"description" = EXCLUDED."description",
"category" = EXCLUDED."category",
"severity" = EXCLUDED."severity",
"allowed_channels" = EXCLUDED."allowed_channels",
"is_active" = true;

View File

@@ -1,6 +0,0 @@
ALTER TABLE "filetags" ADD COLUMN "isSystemUsed" boolean DEFAULT false NOT NULL;
UPDATE "filetags"
SET "isSystemUsed" = true
WHERE COALESCE("createddocumenttype", '') <> ''
OR COALESCE("incomingDocumentType", '') <> '';

View File

@@ -1,2 +0,0 @@
ALTER TABLE "auth_profiles"
ADD COLUMN "calendar_subscription_token" text;

View File

@@ -1,2 +0,0 @@
ALTER TABLE "auth_profiles"
ADD COLUMN IF NOT EXISTS "availability_note" text;

View File

@@ -1,58 +0,0 @@
CREATE TABLE IF NOT EXISTS "communication_rooms" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" bigint NOT NULL,
"key" text NOT NULL,
"name" text NOT NULL,
"topic" text,
"type" text DEFAULT 'room' NOT NULL,
"entity_type" text,
"entity_id" bigint,
"entity_uuid" uuid,
"matrix_room_id" text,
"matrix_alias" text,
"parent_space_room_id" text,
"archived" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone,
"created_by" uuid,
"updated_by" uuid
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'communication_rooms_tenant_id_tenants_id_fk'
) THEN
ALTER TABLE "communication_rooms"
ADD CONSTRAINT "communication_rooms_tenant_id_tenants_id_fk"
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
ON DELETE cascade ON UPDATE no action;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'communication_rooms_created_by_auth_users_id_fk'
) THEN
ALTER TABLE "communication_rooms"
ADD CONSTRAINT "communication_rooms_created_by_auth_users_id_fk"
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
ON DELETE no action ON UPDATE no action;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'communication_rooms_updated_by_auth_users_id_fk'
) THEN
ALTER TABLE "communication_rooms"
ADD CONSTRAINT "communication_rooms_updated_by_auth_users_id_fk"
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
ON DELETE no action ON UPDATE no action;
END IF;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS "communication_rooms_tenant_key_idx"
ON "communication_rooms" USING btree ("tenant_id", "key");
CREATE INDEX IF NOT EXISTS "communication_rooms_tenant_idx"
ON "communication_rooms" USING btree ("tenant_id");
CREATE INDEX IF NOT EXISTS "communication_rooms_entity_idx"
ON "communication_rooms" USING btree ("tenant_id", "entity_type", "entity_id", "entity_uuid");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -36,279 +36,6 @@
"when": 1765716877146,
"tag": "0004_stormy_onslaught",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1771096926109,
"tag": "0005_green_shinobi_shaw",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1772000000000,
"tag": "0006_nifty_price_lock",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1772000100000,
"tag": "0007_bright_default_tax_type",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1773000000000,
"tag": "0008_quick_contracttypes",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1773000100000,
"tag": "0009_heavy_contract_contracttype",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1773000200000,
"tag": "0010_sudden_billing_interval",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1773000300000,
"tag": "0011_mighty_member_bankaccounts",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1773000400000,
"tag": "0012_shiny_customer_inventory",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1773000500000,
"tag": "0013_brisk_customer_inventory_vendor",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1773000600000,
"tag": "0014_smart_memberrelations",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1773000700000,
"tag": "0015_wise_memberrelation_history",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1773000800000,
"tag": "0016_fix_memberrelation_column_usage",
"breakpoints": true
},
{
"idx": 17,
"version": "7",
"when": 1771704862789,
"tag": "0017_slow_the_hood",
"breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1773000900000,
"tag": "0018_account_chart",
"breakpoints": true
},
{
"idx": 19,
"version": "7",
"when": 1773489600000,
"tag": "0019_custom_surcharge_percentage_decimal",
"breakpoints": true
},
{
"idx": 20,
"version": "7",
"when": 1773572400000,
"tag": "0020_file_extracted_text",
"breakpoints": true
},
{
"idx": 21,
"version": "7",
"when": 1773835200000,
"tag": "0021_admin_user_flag",
"breakpoints": true
},
{
"idx": 22,
"version": "7",
"when": 1773925200000,
"tag": "0022_task_dependencies",
"breakpoints": true
},
{
"idx": 23,
"version": "7",
"when": 1774080000000,
"tag": "0023_tax_evaluation_period",
"breakpoints": true
},
{
"idx": 24,
"version": "7",
"when": 1774393200000,
"tag": "0024_tenant_branches",
"breakpoints": true
},
{
"idx": 25,
"version": "7",
"when": 1774393201000,
"tag": "0025_statementallocation_depreciation",
"breakpoints": true
},
{
"idx": 26,
"version": "7",
"when": 1774393202000,
"tag": "0026_statementallocation_depreciation_method",
"breakpoints": true
},
{
"idx": 27,
"version": "7",
"when": 1774602000000,
"tag": "0027_product_supplier_link",
"breakpoints": true
},
{
"idx": 28,
"version": "7",
"when": 1776124800000,
"tag": "0028_teams",
"breakpoints": true
},
{
"idx": 29,
"version": "7",
"when": 1776211200000,
"tag": "0029_events_quick",
"breakpoints": true
},
{
"idx": 30,
"version": "7",
"when": 1776297600000,
"tag": "0030_manual_statementallocations",
"breakpoints": true
},
{
"idx": 31,
"version": "7",
"when": 1776298200000,
"tag": "0031_manual_statementallocations_tax_key",
"breakpoints": true
},
{
"idx": 32,
"version": "7",
"when": 1776298800000,
"tag": "0032_manual_statementallocations_invoice_side",
"breakpoints": true
},
{
"idx": 33,
"version": "7",
"when": 1777003200000,
"tag": "0033_costcentres_parent",
"breakpoints": true
},
{
"idx": 34,
"version": "7",
"when": 1777420800000,
"tag": "0034_events_color",
"breakpoints": true
},
{
"idx": 35,
"version": "7",
"when": 1778191200000,
"tag": "0035_contract_history",
"breakpoints": true
},
{
"idx": 36,
"version": "7",
"when": 1778194800000,
"tag": "0036_allowed_contracttypes",
"breakpoints": true
},
{
"idx": 37,
"version": "7",
"when": 1778840100000,
"tag": "0037_outgoing_sepa_mandates",
"breakpoints": true
},
{
"idx": 38,
"version": "7",
"when": 1779158400000,
"tag": "0038_events_state",
"breakpoints": true
},
{
"idx": 39,
"version": "7",
"when": 1779840000000,
"tag": "0039_events_repeat_interval",
"breakpoints": true
},
{
"idx": 40,
"version": "7",
"when": 1779141600000,
"tag": "0040_filetag_system_types",
"breakpoints": true
},
{
"idx": 41,
"version": "7",
"when": 1780149600000,
"tag": "0041_profile_calendar_subscription",
"breakpoints": true
},
{
"idx": 42,
"version": "7",
"when": 1780153200000,
"tag": "0042_profile_availability_note",
"breakpoints": true
},
{
"idx": 43,
"version": "7",
"when": 1780156800000,
"tag": "0043_communication_rooms",
"breakpoints": true
}
]
}
}

View File

@@ -16,7 +16,6 @@ export const accounts = pgTable("accounts", {
number: text("number").notNull(),
label: text("label").notNull(),
accountChart: text("accountChart").notNull().default("skr03"),
description: text("description"),
})

View File

@@ -1,30 +0,0 @@
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
import { authProfiles } from "./auth_profiles"
import { branches } from "./branches"
import { authUsers } from "./auth_users"
export const authProfileBranches = pgTable(
"auth_profile_branches",
{
created_at: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
profile_id: uuid("profile_id")
.notNull()
.references(() => authProfiles.id, { onDelete: "cascade" }),
branch_id: bigint("branch_id", { mode: "number" })
.notNull()
.references(() => branches.id, { onDelete: "cascade" }),
created_by: uuid("created_by").references(() => authUsers.id),
},
(table) => ({
primaryKey: [table.profile_id, table.branch_id],
})
)
export type AuthProfileBranch = typeof authProfileBranches.$inferSelect
export type NewAuthProfileBranch = typeof authProfileBranches.$inferInsert

View File

@@ -1,30 +0,0 @@
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
import { authProfiles } from "./auth_profiles"
import { teams } from "./teams"
import { authUsers } from "./auth_users"
export const authProfileTeams = pgTable(
"auth_profile_teams",
{
created_at: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
profile_id: uuid("profile_id")
.notNull()
.references(() => authProfiles.id, { onDelete: "cascade" }),
team_id: bigint("team_id", { mode: "number" })
.notNull()
.references(() => teams.id, { onDelete: "cascade" }),
created_by: uuid("created_by").references(() => authUsers.id),
},
(table) => ({
primaryKey: [table.profile_id, table.team_id],
})
)
export type AuthProfileTeam = typeof authProfileTeams.$inferSelect
export type NewAuthProfileTeam = typeof authProfileTeams.$inferInsert

View File

@@ -10,7 +10,6 @@ import {
jsonb,
} from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
import { branches } from "./branches"
export const authProfiles = pgTable("auth_profiles", {
id: uuid("id").primaryKey().defaultRandom(),
@@ -19,8 +18,6 @@ export const authProfiles = pgTable("auth_profiles", {
tenant_id: bigint("tenant_id", { mode: "number" }).notNull(),
branch_id: bigint("branch_id", { mode: "number" }).references(() => branches.id),
created_at: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
@@ -63,7 +60,6 @@ export const authProfiles = pgTable("auth_profiles", {
email: text("email"),
token_id: text("token_id"),
calendar_subscription_token: text("calendar_subscription_token"),
weekly_working_days: doublePrecision("weekly_working_days"),
@@ -75,7 +71,6 @@ export const authProfiles = pgTable("auth_profiles", {
contract_type: text("contract_type"),
position: text("position"),
qualification: text("qualification"),
availability_note: text("availability_note"),
address_street: text("address_street"),
address_zip: text("address_zip"),

View File

@@ -12,7 +12,6 @@ export const authUsers = pgTable("auth_users", {
multiTenant: boolean("multi_tenant").notNull().default(true),
must_change_password: boolean("must_change_password").notNull().default(false),
is_admin: boolean("is_admin").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),

View File

@@ -1,37 +0,0 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const branches = pgTable("branches", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
number: text("number"),
description: text("description"),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Branch = typeof branches.$inferSelect
export type NewBranch = typeof branches.$inferInsert

View File

@@ -1,57 +0,0 @@
import {
pgTable,
uuid,
bigint,
text,
timestamp,
boolean,
uniqueIndex,
index,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const communicationRooms = pgTable(
"communication_rooms",
{
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade" }),
key: text("key").notNull(),
name: text("name").notNull(),
topic: text("topic"),
type: text("type").notNull().default("room"),
entityType: text("entity_type"),
entityId: bigint("entity_id", { mode: "number" }),
entityUuid: uuid("entity_uuid"),
matrixRoomId: text("matrix_room_id"),
matrixAlias: text("matrix_alias"),
parentSpaceRoomId: text("parent_space_room_id"),
archived: boolean("archived").notNull().default(false),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }),
createdBy: uuid("created_by").references(() => authUsers.id),
updatedBy: uuid("updated_by").references(() => authUsers.id),
},
(table) => ({
tenantKeyIdx: uniqueIndex("communication_rooms_tenant_key_idx")
.on(table.tenantId, table.key),
tenantIdx: index("communication_rooms_tenant_idx")
.on(table.tenantId),
entityIdx: index("communication_rooms_entity_idx")
.on(table.tenantId, table.entityType, table.entityId, table.entityUuid),
})
)
export type CommunicationRoom = typeof communicationRooms.$inferSelect
export type NewCommunicationRoom = typeof communicationRooms.$inferInsert

View File

@@ -11,9 +11,7 @@ import {
import { tenants } from "./tenants"
import { customers } from "./customers"
import { contacts } from "./contacts"
import { contracttypes } from "./contracttypes"
import { authUsers } from "./auth_users"
import { outgoingsepamandates } from "./outgoingsepamandates"
export const contracts = pgTable(
"contracts",
@@ -50,10 +48,6 @@ export const contracts = pgTable(
contact: bigint("contact", { mode: "number" }).references(
() => contacts.id
),
contracttype: bigint("contracttype", { mode: "number" }).references(
() => contracttypes.id
),
allowedContracttypes: jsonb("allowedContracttypes").notNull().default([]),
bankingIban: text("bankingIban"),
bankingBIC: text("bankingBIC"),
@@ -61,12 +55,8 @@ export const contracts = pgTable(
bankingOwner: text("bankingOwner"),
sepaRef: text("sepaRef"),
sepaDate: timestamp("sepaDate", { withTimezone: true }),
outgoingsepamandate: bigint("outgoingsepamandate", { mode: "number" }).references(
() => outgoingsepamandates.id
),
paymentType: text("paymentType"),
billingInterval: text("billingInterval"),
invoiceDispatch: text("invoiceDispatch"),
ownFields: jsonb("ownFields").notNull().default({}),

View File

@@ -1,40 +0,0 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const contracttypes = pgTable("contracttypes", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
description: text("description"),
paymentType: text("paymentType"),
recurring: boolean("recurring").notNull().default(false),
billingInterval: text("billingInterval"),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type ContractType = typeof contracttypes.$inferSelect
export type NewContractType = typeof contracttypes.$inferInsert

View File

@@ -13,7 +13,6 @@ import { inventoryitems } from "./inventoryitems"
import { projects } from "./projects"
import { vehicles } from "./vehicles"
import { authUsers } from "./auth_users"
import { branches } from "./branches"
export const costcentres = pgTable("costcentres", {
id: uuid("id").primaryKey().defaultRandom(),
@@ -29,14 +28,10 @@ export const costcentres = pgTable("costcentres", {
number: text("number").notNull(),
name: text("name").notNull(),
parentCostcentre: uuid("parent_costcentre").references(() => costcentres.id),
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
project: bigint("project", { mode: "number" }).references(() => projects.id),
branch: bigint("branch", { mode: "number" }).references(() => branches.id),
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(
() => inventoryitems.id
),

View File

@@ -6,7 +6,6 @@ import {
jsonb,
boolean,
smallint,
doublePrecision,
uuid,
} from "drizzle-orm/pg-core"
@@ -19,7 +18,6 @@ import { projects } from "./projects"
import { plants } from "./plants"
import { authUsers } from "./auth_users"
import {serialExecutions} from "./serialexecutions";
import { outgoingsepamandates } from "./outgoingsepamandates"
export const createddocuments = pgTable("createddocuments", {
id: bigint("id", { mode: "number" })
@@ -98,7 +96,7 @@ export const createddocuments = pgTable("createddocuments", {
taxType: text("taxType"),
customSurchargePercentage: doublePrecision("customSurchargePercentage")
customSurchargePercentage: smallint("customSurchargePercentage")
.notNull()
.default(0),
@@ -119,10 +117,6 @@ export const createddocuments = pgTable("createddocuments", {
() => contracts.id
),
outgoingsepamandate: bigint("outgoingsepamandate", { mode: "number" }).references(
() => outgoingsepamandates.id
),
serialexecution: uuid("serialexecution").references(() => serialExecutions.id)
})

View File

@@ -1,66 +0,0 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
doublePrecision,
uuid,
date,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { customerspaces } from "./customerspaces"
import { products } from "./products"
import { vendors } from "./vendors"
import { authUsers } from "./auth_users"
export const customerinventoryitems = pgTable("customerinventoryitems", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
description: text("description"),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
customer: bigint("customer", { mode: "number" })
.notNull()
.references(() => customers.id),
customerspace: bigint("customerspace", { mode: "number" }).references(
() => customerspaces.id
),
customerInventoryId: text("customerInventoryId").notNull(),
serialNumber: text("serialNumber"),
quantity: bigint("quantity", { mode: "number" }).notNull().default(0),
manufacturer: text("manufacturer"),
manufacturerNumber: text("manufacturerNumber"),
purchaseDate: date("purchaseDate"),
purchasePrice: doublePrecision("purchasePrice").default(0),
currentValue: doublePrecision("currentValue"),
product: bigint("product", { mode: "number" }).references(() => products.id),
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type CustomerInventoryItem = typeof customerinventoryitems.$inferSelect
export type NewCustomerInventoryItem = typeof customerinventoryitems.$inferInsert

View File

@@ -10,7 +10,6 @@ import {
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { memberrelations } from "./memberrelations"
export const customers = pgTable(
"customers",
@@ -63,8 +62,6 @@ export const customers = pgTable(
updatedBy: uuid("updated_by").references(() => authUsers.id),
customPaymentType: text("custom_payment_type"), // ENUM payment_types separat?
customTaxType: text("customTaxType"),
memberrelation: bigint("memberrelation", { mode: "number" }).references(() => memberrelations.id),
}
)

View File

@@ -1,54 +0,0 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
jsonb,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { authUsers } from "./auth_users"
export const customerspaces = pgTable("customerspaces", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
type: text("type").notNull(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
customer: bigint("customer", { mode: "number" })
.notNull()
.references(() => customers.id),
space_number: text("spaceNumber").notNull(),
parentSpace: bigint("parentSpace", { mode: "number" }).references(
() => customerspaces.id
),
info_data: jsonb("infoData")
.notNull()
.default({ zip: "", city: "", streetNumber: "" }),
description: text("description"),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type CustomerSpace = typeof customerspaces.$inferSelect
export type NewCustomerSpace = typeof customerspaces.$inferInsert

View File

@@ -3,7 +3,7 @@ import {
uuid,
timestamp,
text,
bigint, jsonb,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
@@ -23,11 +23,6 @@ export const devices = pgTable("devices", {
password: text("password"),
externalId: text("externalId"),
lastSeen: timestamp("last_seen", { withTimezone: true }),
// Hier speichern wir den ganzen Payload (RSSI, Heap, IP, etc.)
lastDebugInfo: jsonb("last_debug_info"),
})
export type Device = typeof devices.$inferSelect

View File

@@ -1,39 +0,0 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
jsonb,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const entitybankaccounts = pgTable("entitybankaccounts", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
ibanEncrypted: jsonb("iban_encrypted").notNull(),
bicEncrypted: jsonb("bic_encrypted").notNull(),
bankNameEncrypted: jsonb("bank_name_encrypted").notNull(),
description: text("description"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
archived: boolean("archived").notNull().default(false),
})
export type EntityBankAccount = typeof entitybankaccounts.$inferSelect
export type NewEntityBankAccount = typeof entitybankaccounts.$inferInsert

View File

@@ -31,10 +31,6 @@ export const events = pgTable(
endDate: timestamp("endDate", { withTimezone: true }),
eventtype: text("eventtype").default("Umsetzung"),
quick: boolean("quick").notNull().default(false),
state: text("state").notNull().default("Final"),
color: text("color"),
repeatInterval: text("repeatInterval").notNull().default("Keine Wiederholung"),
project: bigint("project", { mode: "number" }), // FK follows when projects.ts exists

View File

@@ -66,7 +66,6 @@ export const files = pgTable("files", {
documentbox: uuid("documentbox").references(() => documentboxes.id),
name: text("name"),
extractedText: text("extracted_text"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
@@ -74,7 +73,6 @@ export const files = pgTable("files", {
createdBy: uuid("created_by").references(() => authUsers.id),
authProfile: uuid("auth_profile").references(() => authProfiles.id),
size: bigint("size", { mode: "number" }),
})
export type File = typeof files.$inferSelect

View File

@@ -26,8 +26,6 @@ export const filetags = pgTable("filetags", {
createdDocumentType: text("createddocumenttype").default(""),
incomingDocumentType: text("incomingDocumentType"),
isSystemUsed: boolean("isSystemUsed").notNull().default(false),
archived: boolean("archived").notNull().default(false),
})

View File

@@ -20,8 +20,6 @@ import { tasks } from "./tasks"
import { vehicles } from "./vehicles"
import { bankstatements } from "./bankstatements"
import { spaces } from "./spaces"
import { customerspaces } from "./customerspaces"
import { customerinventoryitems } from "./customerinventoryitems"
import { costcentres } from "./costcentres"
import { ownaccounts } from "./ownaccounts"
import { createddocuments } from "./createddocuments"
@@ -34,9 +32,6 @@ import { events } from "./events"
import { inventoryitemgroups } from "./inventoryitemgroups"
import { authUsers } from "./auth_users"
import {files} from "./files";
import { memberrelations } from "./memberrelations";
import { contracts } from "./contracts";
import { outgoingsepamandates } from "./outgoingsepamandates";
export const historyitems = pgTable("historyitems", {
id: bigint("id", { mode: "number" })
@@ -54,11 +49,6 @@ export const historyitems = pgTable("historyitems", {
{ onDelete: "cascade" }
),
contract: bigint("contract", { mode: "number" }).references(
() => contracts.id,
{ onDelete: "cascade" }
),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
@@ -109,17 +99,6 @@ export const historyitems = pgTable("historyitems", {
space: bigint("space", { mode: "number" }).references(() => spaces.id),
customerspace: bigint("customerspace", { mode: "number" }).references(() => customerspaces.id),
customerinventoryitem: bigint("customerinventoryitem", { mode: "number" }).references(() => customerinventoryitems.id),
memberrelation: bigint("memberrelation", { mode: "number" }).references(() => memberrelations.id),
outgoingsepamandate: bigint("outgoingsepamandate", { mode: "number" }).references(
() => outgoingsepamandates.id,
{ onDelete: "cascade" }
),
config: jsonb("config"),
projecttype: bigint("projecttype", { mode: "number" }).references(

View File

@@ -14,7 +14,7 @@ export const hourrates = pgTable("hourrates", {
name: text("name").notNull(),
purchase_price: doublePrecision("purchasePrice").notNull(),
purchasePrice: doublePrecision("purchasePrice").notNull(),
sellingPrice: doublePrecision("sellingPrice").notNull(),
archived: boolean("archived").notNull().default(false),

View File

@@ -1,7 +1,5 @@
export * from "./accounts"
export * from "./auth_profiles"
export * from "./auth_profile_branches"
export * from "./auth_profile_teams"
export * from "./auth_role_permisssions"
export * from "./auth_roles"
export * from "./auth_tenant_users"
@@ -10,26 +8,20 @@ export * from "./auth_users"
export * from "./bankaccounts"
export * from "./bankrequisitions"
export * from "./bankstatements"
export * from "./branches"
export * from "./checkexecutions"
export * from "./checks"
export * from "./citys"
export * from "./communication_rooms"
export * from "./contacts"
export * from "./contracts"
export * from "./contracttypes"
export * from "./costcentres"
export * from "./countrys"
export * from "./createddocuments"
export * from "./createdletters"
export * from "./customers"
export * from "./customerspaces"
export * from "./customerinventoryitems"
export * from "./devices"
export * from "./documentboxes"
export * from "./enums"
export * from "./events"
export * from "./entitybankaccounts"
export * from "./files"
export * from "./filetags"
export * from "./folders"
@@ -50,16 +42,12 @@ export * from "./incominginvoices"
export * from "./inventoryitemgroups"
export * from "./inventoryitems"
export * from "./letterheads"
export * from "./memberrelations"
export * from "./movements"
export * from "./m2m_api_keys"
export * from "./notifications_event_types"
export * from "./notifications_items"
export * from "./notifications_preferences"
export * from "./notifications_preferences_defaults"
export * from "./notification_push_subscriptions"
export * from "./ownaccounts"
export * from "./outgoingsepamandates"
export * from "./plants"
export * from "./productcategories"
export * from "./products"
@@ -73,7 +61,6 @@ export * from "./staff_time_entry_connects"
export * from "./staff_zeitstromtimestamps"
export * from "./statementallocations"
export * from "./tasks"
export * from "./teams"
export * from "./taxtypes"
export * from "./tenants"
export * from "./texttemplates"
@@ -84,5 +71,4 @@ export * from "./vendors"
export * from "./staff_time_events"
export * from "./serialtypes"
export * from "./serialexecutions"
export * from "./public_links"
export * from "./wikipages"
export * from "./public_links"

View File

@@ -1,48 +0,0 @@
import {
pgTable,
uuid,
bigint,
text,
timestamp,
boolean,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const m2mApiKeys = pgTable("m2m_api_keys", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade", onUpdate: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id, { onDelete: "cascade", onUpdate: "cascade" }),
createdBy: uuid("created_by").references(() => authUsers.id, {
onDelete: "set null",
onUpdate: "cascade",
}),
name: text("name").notNull(),
keyPrefix: text("key_prefix").notNull(),
keyHash: text("key_hash").notNull().unique(),
active: boolean("active").notNull().default(true),
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
expiresAt: timestamp("expires_at", { withTimezone: true }),
})
export type M2mApiKey = typeof m2mApiKeys.$inferSelect
export type NewM2mApiKey = typeof m2mApiKeys.$inferInsert

View File

@@ -1,39 +0,0 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
uuid,
doublePrecision,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const memberrelations = pgTable("memberrelations", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
type: text("type").notNull(),
billingInterval: text("billingInterval").notNull(),
billingAmount: doublePrecision("billingAmount").notNull().default(0),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type MemberRelation = typeof memberrelations.$inferSelect
export type NewMemberRelation = typeof memberrelations.$inferInsert

View File

@@ -1,50 +0,0 @@
import {
pgTable,
uuid,
bigint,
text,
jsonb,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const notificationPushSubscriptions = pgTable(
"notification_push_subscriptions",
{
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade", onUpdate: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id, { onDelete: "cascade", onUpdate: "cascade" }),
endpoint: text("endpoint").notNull(),
p256dh: text("p256dh").notNull(),
auth: text("auth").notNull(),
userAgent: text("user_agent"),
deviceLabel: text("device_label"),
meta: jsonb("meta"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
lastSeenAt: timestamp("last_seen_at", { withTimezone: true })
.notNull()
.defaultNow(),
disabledAt: timestamp("disabled_at", { withTimezone: true }),
},
(table) => ({
uniqueEndpoint: uniqueIndex("notification_push_subscriptions_endpoint_key").on(table.endpoint),
}),
)
export type NotificationPushSubscription =
typeof notificationPushSubscriptions.$inferSelect
export type NewNotificationPushSubscription =
typeof notificationPushSubscriptions.$inferInsert

View File

@@ -1,61 +0,0 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { entitybankaccounts } from "./entitybankaccounts"
import { authUsers } from "./auth_users"
export const outgoingsepamandates = pgTable("outgoingsepamandates", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
customer: bigint("customer", { mode: "number" })
.notNull()
.references(() => customers.id),
bankaccount: bigint("bankaccount", { mode: "number" })
.notNull()
.references(() => entitybankaccounts.id),
reference: text("reference").notNull(),
status: text("status").notNull().default("Entwurf"),
mandateType: text("mandate_type").notNull().default("CORE"),
sequenceType: text("sequence_type").notNull().default("RCUR"),
signedAt: timestamp("signed_at", { withTimezone: true }),
validFrom: timestamp("valid_from", { withTimezone: true }),
validUntil: timestamp("valid_until", { withTimezone: true }),
defaultMandate: boolean("default_mandate").notNull().default(false),
notes: text("notes"),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type OutgoingSepaMandate = typeof outgoingsepamandates.$inferSelect
export type NewOutgoingSepaMandate = typeof outgoingsepamandates.$inferInsert

View File

@@ -50,7 +50,6 @@ export const products = pgTable("products", {
vendor_allocation: jsonb("vendorAllocation").default([]),
article_number: text("articleNumber"),
supplier_link: text("supplierLink"),
barcodes: text("barcodes").array().notNull().default([]),

View File

@@ -54,7 +54,6 @@ export const services = pgTable("services", {
materialComposition: jsonb("materialComposition").notNull().default([]),
personalComposition: jsonb("personalComposition").notNull().default([]),
priceUpdateLocked: boolean("priceUpdateLocked").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),

View File

@@ -23,7 +23,9 @@ export const statementallocations = pgTable("statementallocations", {
id: uuid("id").primaryKey().defaultRandom(),
// foreign keys
bankstatement: integer("bs_id").references(() => bankstatements.id),
bankstatement: integer("bs_id")
.notNull()
.references(() => bankstatements.id),
createddocument: integer("cd_id").references(() => createddocuments.id),
@@ -32,7 +34,6 @@ export const statementallocations = pgTable("statementallocations", {
incominginvoice: bigint("ii_id", { mode: "number" }).references(
() => incominginvoices.id
),
manualInvoiceSide: text("manual_invoice_side"),
tenant: bigint("tenant", { mode: "number" })
.notNull()
@@ -42,43 +43,20 @@ export const statementallocations = pgTable("statementallocations", {
() => accounts.id
),
contraAccount: bigint("contra_account", { mode: "number" }).references(
() => accounts.id
),
created_at: timestamp("created_at", {
withTimezone: false,
}).defaultNow(),
ownaccount: uuid("ownaccount").references(() => ownaccounts.id),
contraOwnaccount: uuid("contra_ownaccount").references(() => ownaccounts.id),
description: text("description"),
manualBookingDate: text("manual_booking_date"),
datevTaxKey: text("datev_tax_key"),
bookingMode: text("booking_mode").notNull().default("expense"),
depreciationMonths: integer("depreciation_months"),
depreciationStartDate: text("depreciation_start_date"),
depreciationMethod: text("depreciation_method"),
depreciationLabel: text("depreciation_label"),
depreciationGroup: text("depreciation_group"),
residualValue: doublePrecision("residual_value"),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id
),
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
contraCustomer: bigint("contra_customer", { mode: "number" }).references(
() => customers.id
),
contraVendor: bigint("contra_vendor", { mode: "number" }).references(() => vendors.id),
updated_at: timestamp("updated_at", { withTimezone: true }),
updated_by: uuid("updated_by").references(() => authUsers.id),

View File

@@ -1,40 +0,0 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { branches } from "./branches"
export const teams = pgTable("teams", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
description: text("description"),
branch: bigint("branch", { mode: "number" })
.references(() => branches.id),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Team = typeof teams.$inferSelect
export type NewTeam = typeof teams.$inferInsert

View File

@@ -74,51 +74,6 @@ export const tenants = pgTable(
timeTracking: true,
planningBoard: true,
workingTimeTracking: true,
dashboard: true,
historyitems: true,
tasks: true,
wiki: true,
files: true,
createdletters: true,
documentboxes: true,
helpdesk: true,
email: true,
members: true,
customers: true,
vendors: true,
contactsList: true,
staffTime: true,
createDocument: true,
serialInvoice: true,
incomingInvoices: true,
outgoingsepamandates: true,
costcentres: true,
branches: true,
teams: true,
accounts: true,
ownaccounts: true,
banking: true,
spaces: true,
customerspaces: true,
customerinventoryitems: true,
inventoryitems: true,
inventoryitemgroups: true,
products: true,
productcategories: true,
services: true,
servicecategories: true,
memberrelations: true,
staffProfiles: true,
hourrates: true,
projecttypes: true,
contracttypes: true,
plants: true,
settingsNumberRanges: true,
settingsEmailAccounts: true,
settingsBanking: true,
settingsTexttemplates: true,
settingsTenant: true,
export: true,
}),
ownFields: jsonb("ownFields"),
@@ -130,20 +85,13 @@ export const tenants = pgTable(
customers: { prefix: "", suffix: "", nextNumber: 10000 },
products: { prefix: "AT-", suffix: "", nextNumber: 1000 },
quotes: { prefix: "AN-", suffix: "", nextNumber: 1000 },
costEstimates: { prefix: "KS-", suffix: "", nextNumber: 1000 },
confirmationOrders: { prefix: "AB-", suffix: "", nextNumber: 1000 },
invoices: { prefix: "RE-", suffix: "", nextNumber: 1000 },
deliveryNotes: { prefix: "LS-", suffix: "", nextNumber: 1000 },
packingSlips: { prefix: "PS-", suffix: "", nextNumber: 1000 },
spaces: { prefix: "LP-", suffix: "", nextNumber: 1000 },
customerspaces: { prefix: "KLP-", suffix: "", nextNumber: 1000 },
inventoryitems: { prefix: "IA-", suffix: "", nextNumber: 1000 },
customerinventoryitems: { prefix: "KIA-", suffix: "", nextNumber: 1000 },
projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 },
costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 },
outgoingsepamandates: { prefix: "SEPA-", suffix: "", nextNumber: 1000 },
}),
accountChart: text("accountChart").notNull().default("skr03"),
standardEmailForInvoices: text("standardEmailForInvoices"),
@@ -168,10 +116,6 @@ export const tenants = pgTable(
.notNull()
.default(14),
taxEvaluationPeriod: text("taxEvaluationPeriod")
.notNull()
.default("monthly"),
dokuboxEmailAddresses: jsonb("dokuboxEmailAddresses").default([]),
dokuboxkey: uuid("dokuboxkey").notNull().defaultRandom(),

View File

@@ -1,99 +0,0 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
integer,
index,
uuid,
AnyPgColumn
} from "drizzle-orm/pg-core"
import { relations } from "drizzle-orm"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const wikiPages = pgTable(
"wiki_pages",
{
// ID des Wiki-Eintrags selbst (neu = UUID)
id: uuid("id")
.primaryKey()
.defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade" }),
parentId: uuid("parent_id")
.references((): AnyPgColumn => wikiPages.id, { onDelete: "cascade" }),
title: text("title").notNull(),
content: jsonb("content"),
isFolder: boolean("is_folder").notNull().default(false),
sortOrder: integer("sort_order").notNull().default(0),
// --- POLYMORPHE BEZIEHUNG (Split) ---
// Art der Entität (z.B. 'customer', 'invoice', 'iot_device')
entityType: text("entity_type"),
// SPALTE 1: Für Legacy-Tabellen (BigInt)
// Nutzung: Wenn entityType='customer', wird hier die ID 1050 gespeichert
entityId: bigint("entity_id", { mode: "number" }),
// SPALTE 2: Für neue Tabellen (UUID)
// Nutzung: Wenn entityType='iot_device', wird hier die UUID gespeichert
entityUuid: uuid("entity_uuid"),
// ------------------------------------
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }),
createdBy: uuid("created_by").references(() => authUsers.id),
updatedBy: uuid("updated_by").references(() => authUsers.id),
},
(table) => ({
tenantIdx: index("wiki_pages_tenant_idx").on(table.tenantId),
parentIdx: index("wiki_pages_parent_idx").on(table.parentId),
// ZWEI separate Indexe für schnelle Lookups, je nachdem welche ID genutzt wird
// Fall 1: Suche nach Notizen für Kunde 1050
entityIntIdx: index("wiki_pages_entity_int_idx")
.on(table.tenantId, table.entityType, table.entityId),
// Fall 2: Suche nach Notizen für IoT-Device 550e84...
entityUuidIdx: index("wiki_pages_entity_uuid_idx")
.on(table.tenantId, table.entityType, table.entityUuid),
})
)
export const wikiPagesRelations = relations(wikiPages, ({ one, many }) => ({
tenant: one(tenants, {
fields: [wikiPages.tenantId],
references: [tenants.id],
}),
parent: one(wikiPages, {
fields: [wikiPages.parentId],
references: [wikiPages.id],
relationName: "parent_child",
}),
children: many(wikiPages, {
relationName: "parent_child",
}),
author: one(authUsers, {
fields: [wikiPages.createdBy],
references: [authUsers.id],
}),
}))
export type WikiPage = typeof wikiPages.$inferSelect
export type NewWikiPage = typeof wikiPages.$inferInsert

View File

@@ -1,7 +0,0 @@
set -e
if [ "${FEDEO_RUN_MIGRATIONS:-true}" = "true" ]; then
npm run migrate
fi
exec node dist/src/index.js

View File

@@ -1,14 +1,11 @@
import "dotenv/config"
import { defineConfig } from "drizzle-kit"
const fallbackDatabaseUrl = "postgres://postgres:wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu@fedeo-db-001.vpn.internal:5432/fedeo"
const databaseUrl = process.env.DATABASE_URL || fallbackDatabaseUrl
import {secrets} from "./src/utils/secrets";
export default defineConfig({
dialect: "postgresql",
schema: "./db/schema",
out: "./db/migrations",
dbCredentials: {
url: databaseUrl,
url: secrets.DATABASE_URL,
},
})
})

10423
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,16 +5,9 @@
"main": "index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"migrate": "tsx scripts/migrate.ts",
"fill": "ts-node src/webdav/fill-file-sizes.ts",
"dev:dav": "tsx watch src/webdav/server.ts",
"build": "tsc",
"start": "node dist/src/index.js",
"schema:index": "ts-node scripts/generate-schema-index.ts",
"bankcodes:update": "tsx scripts/generate-de-bank-codes.ts",
"members:import:csv": "tsx scripts/import-members-csv.ts",
"profiles:import:mitarbeiterliste": "tsx scripts/import-mitarbeiterliste.ts",
"accounts:import:skr42": "ts-node scripts/import-skr42-accounts.ts"
"schema:index": "ts-node scripts/generate-schema-index.ts"
},
"repository": {
"type": "git",
@@ -34,6 +27,7 @@
"@infisical/sdk": "^4.0.6",
"@mmote/niimbluelib": "^0.0.1-alpha.29",
"@prisma/client": "^6.15.0",
"@supabase/supabase-js": "^2.56.1",
"@zip.js/zip.js": "^2.7.73",
"archiver": "^7.0.1",
"axios": "^1.12.1",
@@ -54,8 +48,6 @@
"pg": "^8.16.3",
"pngjs": "^7.0.0",
"sharp": "^0.34.5",
"web-push": "^3.6.7",
"webdav-server": "^2.6.2",
"xmlbuilder": "^15.1.1",
"zpl-image": "^0.2.0",
"zpl-renderer-js": "^2.0.2"
@@ -64,7 +56,6 @@
"@types/bcrypt": "^6.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.3.0",
"@types/web-push": "^3.6.4",
"drizzle-kit": "^0.31.8",
"prisma": "^6.15.0",
"tsx": "^4.20.5",

View File

@@ -1,95 +0,0 @@
import fs from "node:fs/promises"
import path from "node:path"
import https from "node:https"
const DEFAULT_SOURCE_URL =
"https://www.bundesbank.de/resource/blob/602632/bec25ca5df1eb62fefadd8325dafe67c/472B63F073F071307366337C94F8C870/blz-aktuell-txt-data.txt"
const OUTPUT_NAME_FILE = path.resolve("src/utils/deBankCodes.ts")
const OUTPUT_BIC_FILE = path.resolve("src/utils/deBankBics.ts")
function fetchBuffer(url: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
https
.get(url, (res) => {
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return resolve(fetchBuffer(res.headers.location))
}
if (res.statusCode !== 200) {
return reject(new Error(`Download failed with status ${res.statusCode}`))
}
const chunks: Buffer[] = []
res.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
res.on("end", () => resolve(Buffer.concat(chunks)))
res.on("error", reject)
})
.on("error", reject)
})
}
function escapeTsString(value: string) {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
}
async function main() {
const source = process.env.BLZ_SOURCE_URL || DEFAULT_SOURCE_URL
const sourceFile = process.env.BLZ_SOURCE_FILE
let raw: Buffer
if (sourceFile) {
console.log(`Reading BLZ source file: ${sourceFile}`)
raw = await fs.readFile(sourceFile)
} else {
console.log(`Downloading BLZ source: ${source}`)
raw = await fetchBuffer(source)
}
const content = raw.toString("latin1")
const lines = content.split(/\r?\n/)
const nameMap = new Map<string, string>()
const bicMap = new Map<string, string>()
for (const line of lines) {
if (!line || line.length < 150) continue
const blz = line.slice(0, 8).trim()
const name = line.slice(9, 67).trim()
const bic = line.slice(139, 150).trim()
if (!/^\d{8}$/.test(blz) || !name) continue
if (!nameMap.has(blz)) nameMap.set(blz, name)
if (bic && !bicMap.has(blz)) bicMap.set(blz, bic)
}
const sortedNames = [...nameMap.entries()].sort(([a], [b]) => a.localeCompare(b))
const sortedBics = [...bicMap.entries()].sort(([a], [b]) => a.localeCompare(b))
const nameOutputLines = [
"// Lokale Bankleitzahl-zu-Institut Zuordnung (DE).",
"// Quelle: Deutsche Bundesbank, BLZ-Datei (vollstaendig).",
"export const DE_BANK_CODE_TO_NAME: Record<string, string> = {",
...sortedNames.map(([blz, name]) => ` "${blz}": "${escapeTsString(name)}",`),
"}",
"",
]
const bicOutputLines = [
"// Lokale Bankleitzahl-zu-BIC Zuordnung (DE).",
"// Quelle: Deutsche Bundesbank, BLZ-Datei (vollstaendig).",
"export const DE_BANK_CODE_TO_BIC: Record<string, string> = {",
...sortedBics.map(([blz, bic]) => ` "${blz}": "${escapeTsString(bic)}",`),
"}",
"",
]
await fs.writeFile(OUTPUT_NAME_FILE, nameOutputLines.join("\n"), "utf8")
await fs.writeFile(OUTPUT_BIC_FILE, bicOutputLines.join("\n"), "utf8")
console.log(`Wrote ${sortedNames.length} bank names to ${OUTPUT_NAME_FILE}`)
console.log(`Wrote ${sortedBics.length} bank BICs to ${OUTPUT_BIC_FILE}`)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

View File

@@ -1,270 +0,0 @@
import fs from "node:fs"
import path from "node:path"
import { and, eq } from "drizzle-orm"
import { db, pool } from "../db"
import { customers, entitybankaccounts } from "../db/schema"
import { decrypt, encrypt } from "../src/utils/crypt"
import { loadSecrets, secrets } from "../src/utils/secrets"
type CsvMemberRow = {
number: string
lastname: string
firstname: string
street: string
zip: string
city: string
birthdate: string
mobile: string
email: string
bankInstitute: string
iban: string
bic: string
date: string
memberStatus: string
}
const TENANT_ID = 38
const DEFAULT_CSV_PATH = "/Users/florianfederspiel/Downloads/Mitglieder Übersicht 2026_1.csv"
const args = process.argv.slice(2)
const dryRun = args.includes("--dry-run")
const csvArg = args.find((arg) => !arg.startsWith("--"))
const csvPath = csvArg || DEFAULT_CSV_PATH
function normalizeIban(value: string) {
return String(value || "").replace(/\s+/g, "").toUpperCase()
}
function parseGermanDate(value: string): string | null {
const v = String(value || "").trim()
if (!v) return null
const m = v.match(/^(\d{1,2})\.(\d{1,2})\.(\d{2}|\d{4})$/)
if (!m) return null
const day = m[1].padStart(2, "0")
const month = m[2].padStart(2, "0")
const yy = m[3]
const year = yy.length === 4 ? yy : (Number(yy) >= 70 ? `19${yy}` : `20${yy}`)
return `${year}-${month}-${day}`
}
function parseBoolFromStatus(value: string) {
const normalized = String(value || "").trim().toLowerCase()
return normalized !== "inaktiv"
}
function parseCsv(content: string): CsvMemberRow[] {
const lines = content
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l.length > 0)
if (!lines.length) return []
// Header:
// Nr;Name;Vorname;Straße, Hausnr.;PLZ;Wohnort;Geburtsdatum;Mobilfunknummer;Private Mail-Adresse;Kreditinstitut;IBAN;BIC;Datum;Mitgliedsstatus
const rows: CsvMemberRow[] = []
for (let i = 1; i < lines.length; i++) {
const cols = lines[i].split(";").map((v) => v.trim())
if (cols.length < 14) continue
const number = cols[0]
const lastname = cols[1]
const firstname = cols[2]
if (!number || !lastname || !firstname) continue
rows.push({
number,
lastname,
firstname,
street: cols[3] || "",
zip: cols[4] || "",
city: cols[5] || "",
birthdate: cols[6] || "",
mobile: cols[7] || "",
email: cols[8] || "",
bankInstitute: cols[9] || "",
iban: cols[10] || "",
bic: cols[11] || "",
date: cols[12] || "",
memberStatus: cols[13] || "",
})
}
return rows
}
async function loadBankAccountByIban(tenantId: number) {
const rows = await db
.select({
id: entitybankaccounts.id,
ibanEncrypted: entitybankaccounts.ibanEncrypted,
})
.from(entitybankaccounts)
.where(eq(entitybankaccounts.tenant, tenantId))
const map = new Map<string, number>()
for (const row of rows) {
try {
const iban = normalizeIban(decrypt(row.ibanEncrypted as any))
if (iban) map.set(iban, Number(row.id))
} catch {
// skip broken ciphertext rows
}
}
return map
}
async function main() {
if (!secrets.ENCRYPTION_KEY && process.env.ENCRYPTION_KEY) {
secrets.ENCRYPTION_KEY = process.env.ENCRYPTION_KEY
}
if (!secrets.ENCRYPTION_KEY && process.env.INFISICAL_CLIENT_ID && process.env.INFISICAL_CLIENT_SECRET) {
await loadSecrets()
}
if (!secrets.ENCRYPTION_KEY) {
throw new Error("ENCRYPTION_KEY fehlt. Bitte ENCRYPTION_KEY setzen oder Infisical-Zugang (INFISICAL_CLIENT_ID/INFISICAL_CLIENT_SECRET) bereitstellen.")
}
const absoluteCsvPath = path.resolve(csvPath)
if (!fs.existsSync(absoluteCsvPath)) {
throw new Error(`CSV nicht gefunden: ${absoluteCsvPath}`)
}
const raw = fs.readFileSync(absoluteCsvPath, "utf8")
const csvRows = parseCsv(raw)
if (!csvRows.length) {
throw new Error("Keine importierbaren Zeilen gefunden.")
}
const existingMembers = await db
.select()
.from(customers)
.where(and(eq(customers.tenant, TENANT_ID), eq(customers.type, "Mitglied")))
const memberByNumber = new Map(existingMembers.map((m) => [String(m.customerNumber), m]))
const bankAccountByIban = await loadBankAccountByIban(TENANT_ID)
let createdMembers = 0
let updatedMembers = 0
let createdBankAccounts = 0
let skippedNoIban = 0
for (const row of csvRows) {
const iban = normalizeIban(row.iban)
if (!iban) {
skippedNoIban += 1
continue
}
const fullName = `${row.firstname} ${row.lastname}`.trim()
const birthdate = parseGermanDate(row.birthdate)
const sepaSignedAt = parseGermanDate(row.date)
const active = parseBoolFromStatus(row.memberStatus)
let bankAccountId = bankAccountByIban.get(iban) || null
if (!bankAccountId) {
if (!dryRun) {
const [created] = await db
.insert(entitybankaccounts)
.values({
tenant: TENANT_ID,
ibanEncrypted: encrypt(iban),
bicEncrypted: encrypt(row.bic || "UNBEKANNT"),
bankNameEncrypted: encrypt(row.bankInstitute || "Unbekannt"),
description: "Import Mitglieder Uebersicht 2026_1",
})
.returning({ id: entitybankaccounts.id })
bankAccountId = created?.id || null
} else {
bankAccountId = -1
}
if (bankAccountId) {
bankAccountByIban.set(iban, bankAccountId)
createdBankAccounts += 1
}
}
const existing = memberByNumber.get(String(row.number))
const existingInfo = (existing?.infoData && typeof existing.infoData === "object")
? { ...(existing.infoData as Record<string, any>) }
: {}
const existingIds = Array.isArray(existingInfo.bankAccountIds) ? existingInfo.bankAccountIds : []
const mergedBankAccountIds = bankAccountId && !existingIds.includes(bankAccountId)
? [...existingIds, bankAccountId]
: existingIds
const infoData = {
...existingInfo,
street: row.street || existingInfo.street || "",
zip: row.zip || existingInfo.zip || "",
city: row.city || existingInfo.city || "",
phone: row.mobile || existingInfo.phone || "",
email: row.email || existingInfo.email || "",
birthdate: birthdate || existingInfo.birthdate || null,
hasSEPA: Boolean(sepaSignedAt || existingInfo.sepaSignedAt || existingInfo.hasSEPA),
sepaSignedAt: sepaSignedAt || existingInfo.sepaSignedAt || null,
bankAccountIds: mergedBankAccountIds,
}
const payload = {
tenant: TENANT_ID,
customerNumber: String(row.number),
type: "Mitglied",
isCompany: false,
firstname: row.firstname,
lastname: row.lastname,
name: fullName,
active,
infoData,
archived: false,
}
if (!existing) {
if (!dryRun) {
const [created] = await db.insert(customers).values(payload).returning()
if (created) memberByNumber.set(String(row.number), created)
}
createdMembers += 1
} else {
if (!dryRun) {
await db
.update(customers)
.set({
...payload,
updatedAt: new Date(),
})
.where(and(eq(customers.id, existing.id), eq(customers.tenant, TENANT_ID)))
}
updatedMembers += 1
}
}
console.log("")
console.log(`[IMPORT MEMBERS] Tenant: ${TENANT_ID}`)
console.log(`[IMPORT MEMBERS] CSV: ${absoluteCsvPath}`)
console.log(`[IMPORT MEMBERS] Dry-Run: ${dryRun ? "JA" : "NEIN"}`)
console.log(`[IMPORT MEMBERS] Zeilen: ${csvRows.length}`)
console.log(`[IMPORT MEMBERS] Mitglieder erstellt: ${createdMembers}`)
console.log(`[IMPORT MEMBERS] Mitglieder aktualisiert: ${updatedMembers}`)
console.log(`[IMPORT MEMBERS] Bankkonten erstellt: ${createdBankAccounts}`)
console.log(`[IMPORT MEMBERS] Ohne IBAN übersprungen: ${skippedNoIban}`)
console.log("")
}
main()
.catch((err) => {
console.error("[IMPORT MEMBERS] Fehler:", err)
process.exitCode = 1
})
.finally(async () => {
await pool.end()
})

Some files were not shown because too many files have changed in this diff Show More