commit 1af67a06d169e27feed1b024a9eb9beb1843e17e Author: Sebastian Zell Date: Sun Jan 25 14:43:42 2026 +0100 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4874e04 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,55 @@ +# Git +.git +.gitignore + +# Node.js +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build-Ausgabe (wird im Container neu gebaut) +dist + +# Test-Dateien +test +*.test.ts +*.spec.ts +coverage + +# IDE und Editor +.idea +.vscode +*.swp +*.swo +*~ + +# OS-spezifische Dateien +.DS_Store +Thumbs.db + +# Dokumentation (nicht im Container benötigt) +*.md +!README.md +docs + +# Beispiel-Workflows (werden als Volume gemountet) +workflows + +# Umgebungsvariablen +.env +.env.* + +# Archive +*.tar.gz +*.zip + +# Logs +*.log +logs + +# Temporäre Dateien +tmp +temp +.tmp +.cache diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..47bd35c --- /dev/null +++ b/.env.docker @@ -0,0 +1,62 @@ +# Docker Umgebungsvariablen für n8n mit LibreBooking Node +# Kopiere diese Datei nach .env und passe die Werte an + +# ============================================ +# n8n Basis-Konfiguration +# ============================================ + +# Host und Port +N8N_HOST=localhost +N8N_PORT=5678 +N8N_PROTOCOL=http + +# Webhook URL (für externe Webhooks) +# Für Produktion: https://your-domain.com/ +WEBHOOK_URL=http://localhost:5678/ + +# ============================================ +# Authentifizierung (für Produktion aktivieren!) +# ============================================ + +N8N_BASIC_AUTH_ACTIVE=false +N8N_BASIC_AUTH_USER=admin +N8N_BASIC_AUTH_PASSWORD=changeme_secure_password + +# ============================================ +# Zeitzone +# ============================================ + +TZ=Europe/Berlin + +# ============================================ +# Logging +# ============================================ + +# Mögliche Werte: silent, error, warn, info, debug +N8N_LOG_LEVEL=info + +# ============================================ +# PostgreSQL (optional, für Produktion empfohlen) +# Aktivieren mit: docker-compose --profile with-postgres up -d +# ============================================ + +POSTGRES_USER=n8n +POSTGRES_PASSWORD=n8n_secure_password +POSTGRES_DB=n8n + +# Wenn PostgreSQL aktiv, diese Variable in docker-compose.yml hinzufügen: +# DB_TYPE=postgresdb +# DB_POSTGRESDB_HOST=postgres +# DB_POSTGRESDB_PORT=5432 +# DB_POSTGRESDB_DATABASE=${POSTGRES_DB} +# DB_POSTGRESDB_USER=${POSTGRES_USER} +# DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD} + +# ============================================ +# LibreBooking Konfiguration (Optional) +# Diese können auch direkt in n8n als Credentials angelegt werden +# ============================================ + +# LIBREBOOKING_URL=https://booking.example.com +# LIBREBOOKING_USER=api_user +# LIBREBOOKING_PASSWORD=api_password diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..71bdef4 --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# n8n LibreBooking Node - Umgebungsvariablen +# +# Kopiere diese Datei nach .env und passe die Werte an: +# cp .env.example .env +# + +# n8n Authentifizierung +# WICHTIG: Ändere diese Werte für Produktion! +N8N_BASIC_AUTH_USER=admin +N8N_BASIC_AUTH_PASSWORD=changeme + +# Webhook-URL (für Produktion anpassen) +# Beispiel: https://n8n.deine-domain.de/ +WEBHOOK_URL=http://localhost:5678/ + +# Zeitzone +TZ=Europe/Berlin + +# Log-Level (debug, info, warn, error) +N8N_LOG_LEVEL=info + +# Optional: Datenbank (Standard: SQLite) +# DB_TYPE=postgresdb +# DB_POSTGRESDB_HOST=localhost +# DB_POSTGRESDB_PORT=5432 +# DB_POSTGRESDB_DATABASE=n8n +# DB_POSTGRESDB_USER=n8n +# DB_POSTGRESDB_PASSWORD=password + +# Optional: Executions +# EXECUTIONS_DATA_SAVE_ON_ERROR=all +# EXECUTIONS_DATA_SAVE_ON_SUCCESS=all +# EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80ccd10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.env +*.log + +# Generated PDFs +*.pdf diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..1a02188 --- /dev/null +++ b/.npmignore @@ -0,0 +1,70 @@ +# Source files (nur dist wird veröffentlicht) +*.ts +!*.d.ts +tsconfig.json + +# Git +.git +.gitignore +.gitattributes + +# Tests +test/ +*.test.ts +*.spec.ts +coverage/ +jest.config.js + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Entwicklung +.eslintrc.js +.eslintrc.json +.prettierrc +.prettierrc.json +.editorconfig +.vscode/ +.idea/ + +# Dokumentation (README bleibt) +CONTRIBUTING.md +CHANGELOG.md +INSTALLATION.md +SCHNELLSTART.md +ARCHIV-INFO.md +docs/ + +# Beispiele +workflows/ +examples/ + +# Skripte +install.sh +install.ps1 + +# OS-spezifisch +.DS_Store +Thumbs.db + +# Archive +*.tar.gz +*.zip + +# Logs und temp +*.log +logs/ +tmp/ +temp/ +.tmp/ +.cache/ + +# Umgebungsvariablen +.env +.env.* +!.env.example + +# node_modules (sowieso ignoriert, aber sicherheitshalber) +node_modules/ diff --git a/ARCHIV-INFO.md b/ARCHIV-INFO.md new file mode 100644 index 0000000..f6070b2 --- /dev/null +++ b/ARCHIV-INFO.md @@ -0,0 +1,123 @@ +# LibreBooking n8n Node - Archiv-Information + +Dieses Archiv enthält den vollständigen LibreBooking n8n Node. + +## Archiv entpacken + +### Linux/macOS + +```bash +# .tar.gz Archiv entpacken +tar -xzf n8n-nodes-librebooking.tar.gz +cd n8n-nodes-librebooking +``` + +### Windows + +```powershell +# .zip Archiv entpacken +Expand-Archive -Path n8n-nodes-librebooking.zip -DestinationPath . +cd n8n-nodes-librebooking +``` + +Oder: Rechtsklick → "Alle extrahieren..." + +## Installation + +### Schnellste Methode (Linux/Mac) + +```bash +chmod +x install.sh +./install.sh +n8n start +``` + +### Schnellste Methode (Windows) + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\install.ps1 +n8n start +``` + +### Mit Docker + +```bash +docker-compose up -d +# Browser öffnen: http://localhost:5678 +``` + +## Enthaltene Dateien + +``` +n8n-nodes-librebooking/ +├── credentials/ # API-Credentials Definition +├── nodes/ # Node-Implementierungen +│ ├── LibreBooking/ # Haupt-Node +│ └── LibreBookingTrigger/ # Trigger-Node +├── custom-nodes/ # Für Docker-Integration (eigenständig) +│ ├── credentials/ +│ ├── nodes/ +│ ├── package.json +│ └── README.md +├── workflows/ # Beispiel-Workflows +├── test/ # Test-Scripts +├── Dockerfile # Docker Image Definition +├── Dockerfile.custom-nodes # Für Custom Nodes Integration +├── docker-compose.yml # Docker Compose Konfiguration +├── docker-compose.override.yml # Override für bestehende Installationen +├── docker-compose.example.yml # Vollständiges Beispiel +├── install.sh # Installations-Skript (Linux/Mac) +├── install.ps1 # Installations-Skript (Windows) +├── install-docker.sh # Docker-Integration Skript +├── nginx.conf # Reverse Proxy Beispiel +├── .env.docker # Docker Umgebungsvariablen +├── package.json # npm Paket-Definition +├── tsconfig.json # TypeScript Konfiguration +├── README.md # Hauptdokumentation +├── INSTALLATION.md # Detaillierte Installationsanleitung +├── DOCKER-INTEGRATION.md # Docker-Integration Anleitung +├── SCHNELLSTART.md # Kurzanleitung +├── SCHNELLSTART-DOCKER.md # Docker Kurzanleitung +├── CHANGELOG.md # Versionshistorie +├── CONTRIBUTING.md # Entwickler-Anleitung +└── LICENSE # MIT Lizenz +``` + +## Docker-Integration (NEU) + +Für bestehende n8n Docker-Installationen: + +```bash +# Automatisch +./install-docker.sh -p /pfad/zu/n8n + +# Oder manuell +cp -r custom-nodes /pfad/zu/n8n/ +cd /pfad/zu/n8n/custom-nodes && npm install && npm run build +docker-compose restart n8n +``` + +📖 Siehe **DOCKER-INTEGRATION.md** für ausführliche Anleitung. + +## Dokumentation + +- **README.md** - Übersicht und Schnellstart +- **INSTALLATION.md** - Detaillierte Installationsanleitung +- **DOCKER-INTEGRATION.md** - Anleitung für bestehende Docker-Installationen +- **SCHNELLSTART.md** - Ultra-Kurzanleitung für Experten +- **SCHNELLSTART-DOCKER.md** - Docker-Kurzanleitung +- **CONTRIBUTING.md** - Anleitung für Entwickler + +## Support + +Bei Fragen oder Problemen: +- GitHub Issues: https://github.com/your-org/n8n-nodes-librebooking/issues + +## Lizenz + +MIT License - siehe LICENSE Datei + +--- + +*LibreBooking n8n Node v1.0.0* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1f1293b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,109 @@ +# Changelog + +Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert. + +Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/), +und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). + +## [Unreleased] + +### Geplant +- Webhook-basierter Trigger (falls von LibreBooking unterstützt) +- Batch-Operationen für mehrere Reservierungen +- Erweiterte Filteroptionen + +## [1.0.0] - 2026-01-25 + +### Hinzugefügt + +#### LibreBooking Node +- **Reservierung (Reservation)** + - Alle Reservierungen abrufen (GetAll) + - Reservierung nach Referenznummer abrufen (Get) + - Neue Reservierung erstellen (Create) + - Reservierung aktualisieren (Update) + - Reservierung löschen (Delete) + - Reservierung genehmigen (Approve) + - Check-In durchführen (CheckIn) + - Check-Out durchführen (CheckOut) + +- **Ressource (Resource)** + - Alle Ressourcen abrufen (GetAll) + - Ressource nach ID abrufen (Get) + - Verfügbarkeit prüfen (GetAvailability) + - Status abrufen (GetStatus) + - Neue Ressource erstellen (Create) + - Ressource aktualisieren (Update) + - Ressource löschen (Delete) + +- **Zeitplan (Schedule)** + - Alle Zeitpläne abrufen (GetAll) + - Zeitplan nach ID abrufen (Get) + - Slots abrufen (GetSlots) + +- **Benutzer (User)** + - Alle Benutzer abrufen (GetAll) + - Benutzer nach ID abrufen (Get) + - Neuen Benutzer erstellen (Create) + - Benutzer aktualisieren (Update) + - Benutzer löschen (Delete) + +- **Konto (Account)** + - Eigenes Konto abrufen (Get) + - Konto aktualisieren (Update) + - Passwort ändern (ChangePassword) + +- **Gruppe (Group)** + - Alle Gruppen abrufen (GetAll) + - Gruppe nach ID abrufen (Get) + - Neue Gruppe erstellen (Create) + - Gruppe aktualisieren (Update) + - Gruppe löschen (Delete) + +- **Zubehör (Accessory)** + - Alles Zubehör abrufen (GetAll) + - Zubehör nach ID abrufen (Get) + - Neues Zubehör erstellen (Create) + - Zubehör aktualisieren (Update) + - Zubehör löschen (Delete) + +- **Attribut (Attribute)** + - Attributkategorien abrufen (GetCategories) + - Attribute nach Kategorie abrufen (GetByCategory) + +#### LibreBooking Trigger Node +- Polling-basierter Trigger für Reservierungs-Events +- Event-Typen: + - Neue Reservierung + - Geänderte Reservierung + - Alle Reservierungen +- Filter nach Ressource, Zeitplan und Benutzer +- Konfigurierbares Zeitfenster (7-90 Tage) +- Deduplizierung von Events + +#### Credentials +- LibreBooking API Credentials mit Session-basierter Authentifizierung +- Automatische Token-Verwaltung +- Verbindungstest integriert + +#### Dokumentation +- Vollständige README.md auf Deutsch +- Detaillierte INSTALLATION.md +- Beispiel-Workflows +- API-Dokumentation mit allen Operationen + +#### Entwickler-Tools +- Docker-Support mit Dockerfile und docker-compose +- Installations-Skripte für Linux/Mac und Windows +- Test-Suite für API-Verbindung +- ESLint und Prettier Konfiguration + +### Sicherheit +- Keine Speicherung von Passwörtern im Klartext +- Session-basierte Authentifizierung +- Automatisches Sign-Out nach Operationen + +--- + +[Unreleased]: https://github.com/DEIN-REPO/n8n-nodes-librebooking/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/DEIN-REPO/n8n-nodes-librebooking/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..07bd3e9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,238 @@ +# Beitragen zum LibreBooking n8n Node + +Vielen Dank für dein Interesse, zu diesem Projekt beizutragen! 🎉 + +## Inhaltsverzeichnis + +- [Code of Conduct](#code-of-conduct) +- [Wie kann ich beitragen?](#wie-kann-ich-beitragen) +- [Entwicklungsumgebung einrichten](#entwicklungsumgebung-einrichten) +- [Code-Richtlinien](#code-richtlinien) +- [Pull Request Prozess](#pull-request-prozess) +- [Bug Reports](#bug-reports) +- [Feature Requests](#feature-requests) + +## Code of Conduct + +Dieses Projekt folgt einem [Code of Conduct](CODE_OF_CONDUCT.md). Mit deiner Teilnahme erklärst du dich einverstanden, diesen einzuhalten. + +## Wie kann ich beitragen? + +### Bugs melden + +- Überprüfe zunächst, ob der Bug bereits gemeldet wurde +- Erstelle ein Issue mit einer klaren Beschreibung +- Füge Schritte zur Reproduktion hinzu +- Gib deine Umgebung an (OS, Node.js Version, n8n Version) + +### Features vorschlagen + +- Erstelle ein Issue mit dem Label "enhancement" +- Beschreibe den Use Case +- Erkläre, warum diese Funktion nützlich wäre + +### Code beitragen + +1. Forke das Repository +2. Erstelle einen Feature-Branch +3. Implementiere deine Änderungen +4. Schreibe Tests (falls möglich) +5. Erstelle einen Pull Request + +## Entwicklungsumgebung einrichten + +### Voraussetzungen + +- Node.js 18.x oder höher +- npm 8.x oder höher +- n8n (global installiert) +- Git + +### Setup + +```bash +# Repository klonen +git clone https://github.com/DEIN-REPO/n8n-nodes-librebooking.git +cd n8n-nodes-librebooking + +# Dependencies installieren +npm install + +# Build ausführen +npm run build + +# Für Entwicklung: Watch-Modus +npm run dev +``` + +### Lokales Testen + +```bash +# Node mit n8n verlinken +npm link + +# In n8n-Verzeichnis verlinken +cd $(npm root -g)/n8n +npm link n8n-nodes-librebooking + +# n8n starten +n8n start +``` + +### Mit Docker testen + +```bash +docker-compose up --build +``` + +## Code-Richtlinien + +### TypeScript + +- Verwende strenge Typisierung (`strict: true`) +- Vermeide `any` wo möglich +- Dokumentiere komplexe Funktionen mit JSDoc + +### Formatierung + +```bash +# Code formatieren +npm run format + +# Linting prüfen +npm run lint + +# Linting mit automatischer Korrektur +npm run lintfix +``` + +### Commit Messages + +Wir folgen [Conventional Commits](https://www.conventionalcommits.org/): + +``` +feat: Neue Funktion hinzugefügt +fix: Bug behoben +docs: Dokumentation aktualisiert +style: Formatierung geändert (kein Code) +refactor: Code umstrukturiert +test: Tests hinzugefügt/geändert +chore: Build-Prozess/Tools geändert +``` + +Beispiele: +``` +feat(reservation): Check-In Operation hinzugefügt +fix(auth): Session-Token wird jetzt korrekt erneuert +docs: Installationsanleitung aktualisiert +``` + +### Projektstruktur + +``` +n8n-nodes-librebooking/ +├── credentials/ # Credential-Definitionen +│ └── LibreBookingApi.credentials.ts +├── nodes/ # Node-Definitionen +│ ├── LibreBooking/ +│ │ ├── LibreBooking.node.ts +│ │ └── librebooking.svg +│ └── LibreBookingTrigger/ +│ ├── LibreBookingTrigger.node.ts +│ └── librebooking.svg +├── test/ # Tests +├── workflows/ # Beispiel-Workflows +├── dist/ # Kompilierte Dateien (generiert) +└── package.json +``` + +## Pull Request Prozess + +1. **Branch erstellen:** + ```bash + git checkout -b feature/meine-funktion + ``` + +2. **Änderungen implementieren:** + - Halte dich an die Code-Richtlinien + - Aktualisiere die Dokumentation + - Füge Tests hinzu (falls sinnvoll) + +3. **Testen:** + ```bash + npm run lint + npm run build + # Manuell in n8n testen + ``` + +4. **Commit und Push:** + ```bash + git add . + git commit -m "feat: Meine neue Funktion" + git push origin feature/meine-funktion + ``` + +5. **Pull Request erstellen:** + - Beschreibe deine Änderungen + - Referenziere relevante Issues + - Warte auf Review + +### PR Checkliste + +- [ ] Code folgt den Richtlinien +- [ ] Linting/Formatting bestanden +- [ ] Build erfolgreich +- [ ] Dokumentation aktualisiert +- [ ] CHANGELOG.md aktualisiert +- [ ] Keine Secrets/Credentials im Code + +## Bug Reports + +### Template + +```markdown +## Beschreibung +[Klare Beschreibung des Bugs] + +## Schritte zur Reproduktion +1. ... +2. ... +3. ... + +## Erwartetes Verhalten +[Was sollte passieren?] + +## Tatsächliches Verhalten +[Was passiert stattdessen?] + +## Umgebung +- OS: [z.B. Ubuntu 22.04] +- Node.js: [z.B. 20.10.0] +- n8n: [z.B. 1.20.0] +- LibreBooking: [z.B. 2.8.5] + +## Logs/Screenshots +[Falls vorhanden] +``` + +## Feature Requests + +### Template + +```markdown +## Beschreibung +[Beschreibe die gewünschte Funktion] + +## Use Case +[Warum wird diese Funktion benötigt?] + +## Vorgeschlagene Lösung +[Falls du eine Idee hast] + +## Alternativen +[Andere Möglichkeiten, die du in Betracht gezogen hast] +``` + +--- + +Vielen Dank für deinen Beitrag! 🙏 diff --git a/DOCKER-INTEGRATION.md b/DOCKER-INTEGRATION.md new file mode 100644 index 0000000..390d0dc --- /dev/null +++ b/DOCKER-INTEGRATION.md @@ -0,0 +1,592 @@ +# Docker-Integration für LibreBooking n8n Node + +Diese Anleitung beschreibt die Integration des LibreBooking Nodes in eine **bestehende n8n Docker-Installation**. + +## Inhaltsverzeichnis + +- [Voraussetzungen](#voraussetzungen) +- [Methode 1: Automatische Integration mit Skript](#methode-1-automatische-integration-mit-skript) +- [Methode 2: Manuelle Integration](#methode-2-manuelle-integration) +- [Methode 3: Integration in bestehende docker-compose.yml](#methode-3-integration-in-bestehende-docker-composeyml) +- [Methode 4: Dockerfile erweitern](#methode-4-dockerfile-erweitern) +- [Verifizierung der Installation](#verifizierung-der-installation) +- [Troubleshooting](#troubleshooting) +- [Updates und Wartung](#updates-und-wartung) + +--- + +## Voraussetzungen + +### System-Anforderungen + +- **Docker** Version 20.10 oder höher +- **Docker Compose** v2.0+ (Plugin) oder docker-compose v1.29+ +- **Laufende n8n Docker-Installation** +- **Zugriff auf das Dateisystem** des Docker-Hosts + +### Prüfen der Voraussetzungen + +```bash +# Docker Version prüfen +docker --version +# Docker version 24.0.x, build xxxxx + +# Docker Compose Version prüfen +docker compose version +# Docker Compose version v2.x.x + +# Oder für ältere Versionen: +docker-compose --version +# docker-compose version 1.29.x, build xxxxx + +# n8n Container Status prüfen +docker ps | grep n8n +``` + +--- + +## Methode 1: Automatische Integration mit Skript + +Die einfachste Methode für die Integration in eine bestehende Installation. + +### Schritt 1: Skript ausführbar machen + +```bash +chmod +x install-docker.sh +``` + +### Schritt 2: Skript ausführen + +```bash +# Im aktuellen Verzeichnis (wenn dort n8n läuft) +./install-docker.sh + +# Oder mit Pfad zur n8n Installation +./install-docker.sh -p /pfad/zu/n8n + +# Mit Überschreiben bestehender Dateien +./install-docker.sh -f -p /pfad/zu/n8n +``` + +### Skript-Optionen + +| Option | Beschreibung | +|--------|--------------| +| `-p, --path PATH` | Pfad zur n8n Docker-Installation | +| `-b, --build` | Node im Container bauen | +| `-f, --force` | Bestehende Installation überschreiben | +| `-h, --help` | Hilfe anzeigen | + +### Was das Skript tut + +1. Prüft Docker und Docker Compose Installation +2. Prüft ob n8n Container läuft +3. Kopiert `custom-nodes/` Verzeichnis +4. Erstellt/aktualisiert `docker-compose.override.yml` +5. Setzt korrekte Berechtigungen (UID 1000) +6. Startet Container bei Bedarf neu + +--- + +## Methode 2: Manuelle Integration + +Für mehr Kontrolle oder spezielle Setups. + +### Schritt 1: Custom Nodes Verzeichnis kopieren + +```bash +# Zum n8n Verzeichnis wechseln +cd /pfad/zu/ihrer/n8n/installation + +# Custom Nodes kopieren +cp -r /pfad/zu/librebooking_n8n_node/custom-nodes ./custom-nodes +``` + +### Schritt 2: Dependencies installieren und bauen + +```bash +cd custom-nodes + +# Node.js Dependencies installieren +npm install + +# TypeScript kompilieren +npm run build +``` + +### Schritt 3: docker-compose.override.yml erstellen + +Erstellen Sie eine `docker-compose.override.yml` Datei: + +```yaml +version: '3.8' + +services: + n8n: + volumes: + # LibreBooking Custom Node einbinden + - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + + environment: + # Custom Nodes Pfad + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + # Community Nodes aktivieren + - N8N_COMMUNITY_NODES_ENABLED=true +``` + +### Schritt 4: Berechtigungen setzen + +```bash +# n8n läuft als "node" User mit UID 1000 +sudo chown -R 1000:1000 custom-nodes/ +``` + +### Schritt 5: Container neustarten + +```bash +# Mit docker-compose +docker-compose restart n8n + +# Oder mit Docker Compose Plugin +docker compose restart n8n + +# Bei Problemen: Container komplett neu erstellen +docker-compose down && docker-compose up -d +``` + +--- + +## Methode 3: Integration in bestehende docker-compose.yml + +Wenn Sie keine Override-Datei verwenden möchten. + +### Bestehende docker-compose.yml erweitern + +Fügen Sie folgende Einträge zu Ihrem n8n Service hinzu: + +```yaml +version: '3.8' + +services: + n8n: + image: n8nio/n8n:latest + # ... Ihre bestehende Konfiguration ... + + volumes: + # Bestehende Volumes beibehalten + - n8n_data:/home/node/.n8n + + # LibreBooking Node hinzufügen + - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + + environment: + # Bestehende Umgebungsvariablen beibehalten + - N8N_HOST=0.0.0.0 + - N8N_PORT=5678 + + # Custom Nodes Konfiguration hinzufügen + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true +``` + +### Vollständiges Beispiel + +Siehe `docker-compose.example.yml` für eine vollständige Konfiguration mit: +- PostgreSQL Datenbank (optional) +- Redis Queue (optional) +- Health Checks +- Alle Umgebungsvariablen + +--- + +## Methode 4: Dockerfile erweitern + +Für produktive Deployments oder wenn Sie ein eigenes Image benötigen. + +### Einfaches Dockerfile + +```dockerfile +# Dockerfile.custom-nodes +ARG N8N_VERSION=latest +FROM n8nio/n8n:${N8N_VERSION} + +USER root + +# Custom Node Verzeichnis erstellen +RUN mkdir -p /home/node/.n8n/custom/n8n-nodes-librebooking && \ + chown -R node:node /home/node/.n8n/custom + +WORKDIR /home/node/.n8n/custom/n8n-nodes-librebooking + +# Dateien kopieren +COPY --chown=node:node custom-nodes/ ./ + +# Dependencies installieren und bauen +RUN npm install && npm run build + +USER node +WORKDIR /home/node + +ENV N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom +ENV N8N_COMMUNITY_NODES_ENABLED=true + +EXPOSE 5678 +CMD ["n8n", "start"] +``` + +### Image bauen und verwenden + +```bash +# Image bauen +docker build -f Dockerfile.custom-nodes -t n8n-librebooking . + +# Container starten +docker run -d \ + --name n8n \ + -p 5678:5678 \ + -v n8n_data:/home/node/.n8n \ + n8n-librebooking +``` + +### Mit docker-compose + +```yaml +version: '3.8' + +services: + n8n: + build: + context: . + dockerfile: Dockerfile.custom-nodes + args: + N8N_VERSION: latest + # ... weitere Konfiguration +``` + +### Multi-Stage Build (Optimiert) + +```dockerfile +# Build Stage +FROM node:18-alpine AS builder +WORKDIR /build +COPY custom-nodes/ ./ +RUN npm install && npm run build + +# Production Stage +ARG N8N_VERSION=latest +FROM n8nio/n8n:${N8N_VERSION} + +USER root +RUN mkdir -p /home/node/.n8n/custom/n8n-nodes-librebooking && \ + chown -R node:node /home/node/.n8n/custom + +# Nur gebaute Dateien kopieren +COPY --from=builder --chown=node:node /build/dist /home/node/.n8n/custom/n8n-nodes-librebooking/dist +COPY --from=builder --chown=node:node /build/package.json /home/node/.n8n/custom/n8n-nodes-librebooking/ + +USER node + +ENV N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom +ENV N8N_COMMUNITY_NODES_ENABLED=true + +CMD ["n8n", "start"] +``` + +--- + +## Verifizierung der Installation + +### 1. Container-Logs prüfen + +```bash +# Logs anzeigen +docker logs n8n 2>&1 | grep -i "librebooking\|custom\|node" + +# Live Logs verfolgen +docker logs -f n8n +``` + +### 2. In n8n prüfen + +1. Öffnen Sie n8n im Browser (z.B. `http://localhost:5678`) +2. Erstellen Sie einen neuen Workflow +3. Fügen Sie einen neuen Node hinzu +4. Suchen Sie nach "LibreBooking" - zwei Nodes sollten erscheinen: + - **LibreBooking** - Hauptnode für alle Operationen + - **LibreBooking Trigger** - Trigger für neue Reservierungen + +### 3. Node-Verzeichnis im Container prüfen + +```bash +# In Container einloggen +docker exec -it n8n /bin/sh + +# Custom Nodes Verzeichnis prüfen +ls -la /home/node/.n8n/custom/ + +# LibreBooking Node prüfen +ls -la /home/node/.n8n/custom/n8n-nodes-librebooking/dist/ +``` + +### 4. Umgebungsvariablen prüfen + +```bash +docker exec n8n env | grep N8N_CUSTOM +# N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + +docker exec n8n env | grep COMMUNITY +# N8N_COMMUNITY_NODES_ENABLED=true +``` + +--- + +## Troubleshooting + +### Problem: Node wird nicht erkannt + +**Symptom:** LibreBooking Node erscheint nicht in der Node-Suche + +**Lösungen:** + +1. **Pfad prüfen:** + ```bash + docker exec n8n ls -la /home/node/.n8n/custom/n8n-nodes-librebooking/ + ``` + +2. **Umgebungsvariablen prüfen:** + ```bash + docker exec n8n env | grep -E "N8N_CUSTOM|COMMUNITY" + ``` + +3. **Container komplett neu starten:** + ```bash + docker-compose down + docker-compose up -d + ``` + +4. **Logs auf Fehler prüfen:** + ```bash + docker logs n8n 2>&1 | grep -i error + ``` + +### Problem: Permissions-Fehler + +**Symptom:** `EACCES: permission denied` oder ähnliche Fehler + +**Lösungen:** + +1. **Berechtigungen setzen:** + ```bash + sudo chown -R 1000:1000 custom-nodes/ + sudo chmod -R 755 custom-nodes/ + ``` + +2. **Volume mit korrektem User mounten:** + ```yaml + services: + n8n: + user: "1000:1000" + ``` + +3. **SELinux Context (falls aktiviert):** + ```bash + sudo chcon -R -t svirt_sandbox_file_t custom-nodes/ + ``` + +### Problem: Container startet nicht + +**Symptom:** Container crashed oder startet nicht + +**Lösungen:** + +1. **Logs prüfen:** + ```bash + docker logs n8n + ``` + +2. **Ohne Override starten (zum Testen):** + ```bash + mv docker-compose.override.yml docker-compose.override.yml.bak + docker-compose up -d + ``` + +3. **Volume-Konflikte prüfen:** + ```bash + docker volume ls + docker volume inspect n8n_data + ``` + +### Problem: Node wird geladen aber Fehler bei Ausführung + +**Symptom:** Node ist sichtbar aber Ausführung schlägt fehl + +**Lösungen:** + +1. **Build prüfen:** + ```bash + docker exec n8n ls -la /home/node/.n8n/custom/n8n-nodes-librebooking/dist/ + ``` + +2. **Neu bauen:** + ```bash + cd custom-nodes + npm run rebuild + docker-compose restart n8n + ``` + +3. **Dependencies prüfen:** + ```bash + docker exec -it n8n /bin/sh + cd /home/node/.n8n/custom/n8n-nodes-librebooking + npm ls + ``` + +### Problem: TypeScript Build schlägt fehl + +**Symptom:** `tsc` Fehler beim Bauen + +**Lösungen:** + +1. **Node.js Version prüfen:** + ```bash + node --version # Sollte 18+ sein + npm --version + ``` + +2. **Clean Build:** + ```bash + cd custom-nodes + rm -rf node_modules dist package-lock.json + npm install + npm run build + ``` + +### Problem: Webhook URL nicht erreichbar + +**Symptom:** LibreBooking kann Webhooks nicht senden + +**Lösungen:** + +1. **WEBHOOK_URL Umgebungsvariable setzen:** + ```yaml + environment: + - WEBHOOK_URL=https://ihre-domain.com/ + ``` + +2. **Netzwerk-Konfiguration prüfen:** + ```bash + docker network inspect $(docker network ls -q) + ``` + +--- + +## Updates und Wartung + +### Node aktualisieren + +```bash +# Neue Version herunterladen +cd /pfad/zu/neuem/librebooking_n8n_node + +# Custom Nodes ersetzen +rm -rf /pfad/zu/n8n/custom-nodes +cp -r custom-nodes /pfad/zu/n8n/custom-nodes + +# Neu bauen +cd /pfad/zu/n8n/custom-nodes +npm install +npm run build + +# Container neustarten +cd /pfad/zu/n8n +docker-compose restart n8n +``` + +### n8n Version aktualisieren + +```bash +# Image aktualisieren +docker pull n8nio/n8n:latest + +# Container neu erstellen +docker-compose down +docker-compose up -d +``` + +### Backup erstellen + +```bash +# Docker Volumes sichern +docker run --rm \ + -v n8n_data:/source:ro \ + -v $(pwd)/backup:/backup \ + alpine tar cvzf /backup/n8n_data_backup.tar.gz -C /source . + +# Custom Nodes sichern +tar cvzf custom-nodes-backup.tar.gz custom-nodes/ +``` + +### Logs rotieren + +```yaml +services: + n8n: + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" +``` + +--- + +## Erweiterte Konfiguration + +### Kubernetes Deployment + +Für Kubernetes-Umgebungen kann ein ConfigMap oder PersistentVolume verwendet werden: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: librebooking-node +data: + # Node-Dateien als ConfigMap +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: n8n +spec: + template: + spec: + containers: + - name: n8n + volumeMounts: + - name: custom-nodes + mountPath: /home/node/.n8n/custom/n8n-nodes-librebooking + volumes: + - name: custom-nodes + configMap: + name: librebooking-node +``` + +### Mit Reverse Proxy (Nginx) + +Siehe `nginx.conf` für eine vollständige Nginx-Konfiguration mit: +- SSL/TLS Termination +- WebSocket Support +- Optimierte Timeouts für Webhooks + +--- + +## Hilfe und Support + +- **GitHub Issues:** [Repository Issues](https://github.com/ihr-repo/librebooking-n8n-node/issues) +- **n8n Community:** [community.n8n.io](https://community.n8n.io) +- **LibreBooking Dokumentation:** [LibreBooking Wiki](https://github.com/effgarces/BookedScheduler/wiki) + +--- + +*Letzte Aktualisierung: Januar 2026* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fa2c457 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# Dockerfile für n8n mit LibreBooking Node +# Basiert auf dem offiziellen n8n Docker Image + +FROM n8nio/n8n:latest + +# Als Root-Benutzer für Installation +USER root + +# Arbeitsverzeichnis für den Custom Node +WORKDIR /home/node/.n8n/custom + +# Kopiere Node-Dateien +COPY package*.json ./ +COPY tsconfig.json ./ +COPY index.ts ./ +COPY credentials/ ./credentials/ +COPY nodes/ ./nodes/ + +# Installiere Dependencies und baue den Node +RUN npm install && \ + npm run build && \ + chown -R node:node /home/node/.n8n + +# Zurück zum node-Benutzer +USER node + +# Arbeitsverzeichnis auf n8n Standard setzen +WORKDIR /home/node + +# n8n wird automatisch den Custom Node laden +ENV N8N_CUSTOM_EXTENSIONS="/home/node/.n8n/custom" + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget -q --spider http://localhost:5678/healthz || exit 1 + +# Standard n8n Port +EXPOSE 5678 + +# Startbefehl +CMD ["n8n", "start"] diff --git a/Dockerfile.custom-nodes b/Dockerfile.custom-nodes new file mode 100644 index 0000000..1463d62 --- /dev/null +++ b/Dockerfile.custom-nodes @@ -0,0 +1,48 @@ +# Dockerfile für Custom Nodes Integration +# Verwendet das offizielle n8n Image und fügt den LibreBooking Node hinzu +# +# Build: docker build -f Dockerfile.custom-nodes -t n8n-librebooking . +# Run: docker run -p 5678:5678 n8n-librebooking + +ARG N8N_VERSION=latest +FROM n8nio/n8n:${N8N_VERSION} + +# Wechsle zu root für Installationen +USER root + +# Erstelle Custom Nodes Verzeichnis +RUN mkdir -p /home/node/.n8n/custom/n8n-nodes-librebooking && \ + chown -R node:node /home/node/.n8n/custom + +# Arbeitsverzeichnis setzen +WORKDIR /home/node/.n8n/custom/n8n-nodes-librebooking + +# Kopiere Custom Node Dateien +COPY --chown=node:node custom-nodes/package.json . +COPY --chown=node:node custom-nodes/tsconfig.json . +COPY --chown=node:node custom-nodes/index.ts . +COPY --chown=node:node custom-nodes/credentials/ ./credentials/ +COPY --chown=node:node custom-nodes/nodes/ ./nodes/ + +# Installiere Dependencies und baue den Node +RUN npm install && npm run build + +# Wechsle zurück zum node User +USER node + +# Arbeitsverzeichnis für n8n setzen +WORKDIR /home/node + +# Umgebungsvariablen +ENV N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom \ + N8N_COMMUNITY_NODES_ENABLED=true + +# n8n Port +EXPOSE 5678 + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD wget -qO- http://localhost:5678/healthz || exit 1 + +# Startbefehl +CMD ["n8n", "start"] diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000..ba46884 --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,553 @@ +# Installationsanleitung - LibreBooking n8n Node + +Diese Anleitung beschreibt alle verfügbaren Methoden zur Installation des LibreBooking n8n Nodes. + +## Inhaltsverzeichnis + +- [Voraussetzungen](#voraussetzungen) +- [Installation aus Git-Archiv](#installation-aus-git-archiv) +- [Methode 1: Automatische Installation mit Skript](#methode-1-automatische-installation-mit-skript) +- [Methode 2: Manuelle Installation mit npm](#methode-2-manuelle-installation-mit-npm) +- [Methode 3: Installation aus npm Registry](#methode-3-installation-aus-npm-registry) +- [Methode 4: Docker Installation](#methode-4-docker-installation) +- [Methode 5: n8n Community Nodes](#methode-5-n8n-community-nodes) +- [Verifizierung der Installation](#verifizierung-der-installation) +- [Troubleshooting](#troubleshooting) +- [Deinstallation](#deinstallation) + +--- + +## Voraussetzungen + +### Systemanforderungen + +| Komponente | Mindestversion | Empfohlen | +|------------|---------------|-----------| +| Node.js | 18.x | 20.x LTS | +| npm | 8.x | 10.x | +| n8n | 1.0.0 | Neueste | + +### Node.js installieren + +**Linux (Ubuntu/Debian):** +```bash +# Mit NodeSource Repository (empfohlen) +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt-get install -y nodejs + +# Version prüfen +node --version +npm --version +``` + +**macOS:** +```bash +# Mit Homebrew +brew install node@20 + +# Version prüfen +node --version +npm --version +``` + +**Windows:** +1. Lade Node.js von https://nodejs.org/ herunter +2. Wähle die LTS-Version (20.x) +3. Führe den Installer aus +4. Öffne PowerShell und prüfe: `node --version` + +### n8n installieren + +```bash +# Global installieren +npm install -g n8n + +# Installation prüfen +n8n --version +``` + +--- + +## Installation aus Git-Archiv + +### Archiv herunterladen + +1. **GitHub Release herunterladen:** + ```bash + # .tar.gz für Linux/Mac + wget https://github.com/DEIN-REPO/n8n-nodes-librebooking/releases/latest/download/n8n-nodes-librebooking.tar.gz + + # .zip für Windows + # Über Browser herunterladen + ``` + +2. **Oder direkt von Git:** + ```bash + git clone https://github.com/DEIN-REPO/n8n-nodes-librebooking.git + cd n8n-nodes-librebooking + ``` + +### Archiv entpacken + +**Linux/macOS:** +```bash +# .tar.gz entpacken +tar -xzf n8n-nodes-librebooking.tar.gz +cd n8n-nodes-librebooking +``` + +**Windows (PowerShell):** +```powershell +# .zip entpacken +Expand-Archive -Path n8n-nodes-librebooking.zip -DestinationPath . +cd n8n-nodes-librebooking +``` + +--- + +## Methode 1: Automatische Installation mit Skript + +Die einfachste Methode für die meisten Benutzer. + +### Linux/macOS + +```bash +# In das Verzeichnis wechseln +cd n8n-nodes-librebooking + +# Skript ausführbar machen (falls nötig) +chmod +x install.sh + +# Installation starten +./install.sh +``` + +**Optionen:** +```bash +./install.sh # Standard-Installation mit npm link +./install.sh --no-link # Nur Build, ohne npm link +./install.sh --global # Globale Installation +./install.sh --help # Hilfe anzeigen +``` + +### Windows (PowerShell) + +```powershell +# In das Verzeichnis wechseln +cd n8n-nodes-librebooking + +# Skript ausführen (evtl. Ausführungsrichtlinie anpassen) +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\install.ps1 +``` + +**Optionen:** +```powershell +.\install.ps1 # Standard-Installation mit npm link +.\install.ps1 -NoLink # Nur Build, ohne npm link +.\install.ps1 -Global # Globale Installation +.\install.ps1 -Help # Hilfe anzeigen +``` + +--- + +## Methode 2: Manuelle Installation mit npm + +Für Benutzer, die mehr Kontrolle über den Installationsprozess möchten. + +### Schritt 1: Dependencies installieren + +```bash +cd n8n-nodes-librebooking +npm install +``` + +### Schritt 2: TypeScript kompilieren + +```bash +npm run build +``` + +### Schritt 3: Node verlinken + +```bash +# Node global verfügbar machen +npm link + +# Mit n8n verlinken (optional, falls n8n global installiert ist) +cd $(npm root -g)/n8n +npm link n8n-nodes-librebooking +``` + +### Schritt 4: n8n neu starten + +```bash +# n8n stoppen (falls läuft) +# Ctrl+C oder: +pkill -f n8n + +# n8n starten +n8n start +``` + +--- + +## Methode 3: Installation aus npm Registry + +> **Hinweis:** Diese Methode ist für eine zukünftige Veröffentlichung auf npm vorgesehen. + +```bash +# Global installieren +npm install -g n8n-nodes-librebooking + +# Oder lokal in einem Projekt +npm install n8n-nodes-librebooking +``` + +Nach der Veröffentlichung auf npm wird diese Methode die einfachste sein. + +--- + +## Methode 4: Docker Installation + +### Voraussetzungen für Docker + +- Docker 20.x oder höher +- Docker Compose v2.x (empfohlen) + +**Docker installieren:** +- Linux: https://docs.docker.com/engine/install/ +- macOS: https://docs.docker.com/desktop/mac/install/ +- Windows: https://docs.docker.com/desktop/windows/install/ + +### Mit docker-compose (empfohlen) + +```bash +cd n8n-nodes-librebooking + +# Umgebungsvariablen konfigurieren (optional) +cp .env.example .env +# .env Datei bearbeiten + +# Container bauen und starten +docker-compose up -d + +# Logs anzeigen +docker-compose logs -f + +# Status prüfen +docker-compose ps +``` + +**Umgebungsvariablen (`.env`):** +```env +# n8n Authentifizierung +N8N_BASIC_AUTH_USER=admin +N8N_BASIC_AUTH_PASSWORD=sicheres-passwort-hier + +# Webhook-URL (für Produktion) +WEBHOOK_URL=https://n8n.deine-domain.de/ + +# Zeitzone +TZ=Europe/Berlin + +# Log-Level (debug, info, warn, error) +N8N_LOG_LEVEL=info +``` + +**Nützliche docker-compose Befehle:** +```bash +# Stoppen +docker-compose down + +# Neu bauen (nach Änderungen) +docker-compose build --no-cache + +# Neustart +docker-compose restart + +# Logs eines bestimmten Services +docker-compose logs -f n8n + +# In Container Shell +docker-compose exec n8n sh +``` + +### Mit Docker direkt + +```bash +cd n8n-nodes-librebooking + +# Image bauen +docker build -t n8n-librebooking . + +# Container starten +docker run -d \ + --name n8n-librebooking \ + -p 5678:5678 \ + -e N8N_BASIC_AUTH_ACTIVE=true \ + -e N8N_BASIC_AUTH_USER=admin \ + -e N8N_BASIC_AUTH_PASSWORD=changeme \ + -e GENERIC_TIMEZONE=Europe/Berlin \ + -v n8n_data:/home/node/.n8n \ + n8n-librebooking +``` + +**Container verwalten:** +```bash +# Logs anzeigen +docker logs -f n8n-librebooking + +# Stoppen +docker stop n8n-librebooking + +# Starten +docker start n8n-librebooking + +# Entfernen +docker rm -f n8n-librebooking + +# Image entfernen +docker rmi n8n-librebooking +``` + +### Volumes und Konfiguration + +**Wichtige Volumes:** + +| Volume/Pfad | Beschreibung | +|-------------|--------------| +| `/home/node/.n8n` | n8n Datenverzeichnis (Workflows, Credentials) | +| `/home/node/.n8n/custom` | Custom Nodes | +| `/home/node/workflows` | Beispiel-Workflows (read-only) | + +**Daten sichern:** +```bash +# Mit docker-compose +docker-compose exec n8n tar -czf /tmp/backup.tar.gz /home/node/.n8n +docker cp n8n-librebooking:/tmp/backup.tar.gz ./backup.tar.gz + +# Ohne docker-compose +docker cp n8n-librebooking:/home/node/.n8n ./n8n-backup +``` + +**Daten wiederherstellen:** +```bash +docker cp ./n8n-backup/. n8n-librebooking:/home/node/.n8n/ +docker-compose restart +``` + +--- + +## Methode 5: n8n Community Nodes + +> **Hinweis:** Diese Methode wird verfügbar sein, sobald der Node im n8n Community Node Repository veröffentlicht ist. + +1. Öffne n8n im Browser +2. Gehe zu **Settings** → **Community Nodes** +3. Klicke auf **Install a community node** +4. Gib ein: `n8n-nodes-librebooking` +5. Klicke auf **Install** +6. Starte n8n neu + +--- + +## Verifizierung der Installation + +### 1. n8n starten + +```bash +# Lokal +n8n start + +# Mit Docker +docker-compose up -d +``` + +### 2. Browser öffnen + +Öffne http://localhost:5678 im Browser. + +### 3. Node suchen + +1. Erstelle einen neuen Workflow +2. Klicke auf das **+** Symbol +3. Suche nach "LibreBooking" +4. Du solltest zwei Nodes sehen: + - **LibreBooking** (für API-Operationen) + - **LibreBooking Trigger** (für Events) + +### 4. Credentials einrichten + +1. Klicke auf einen LibreBooking Node +2. Unter "Credentials" klicke auf **Create New** +3. Wähle **LibreBooking API** +4. Fülle aus: + - **URL:** Deine LibreBooking URL (z.B. `https://booking.example.com/Web/Services`) + - **Username:** Dein Admin-Benutzername + - **Password:** Dein Passwort +5. Klicke auf **Save** +6. Teste die Verbindung + +--- + +## Troubleshooting + +### Häufige Probleme + +#### Node wird nicht angezeigt + +**Problem:** Der LibreBooking Node erscheint nicht in n8n. + +**Lösungen:** +```bash +# 1. Prüfe, ob der Build erfolgreich war +ls -la dist/ + +# 2. Prüfe npm link Status +npm ls -g --depth=0 | grep librebooking + +# 3. n8n Custom Extensions Pfad prüfen +echo $N8N_CUSTOM_EXTENSIONS + +# 4. n8n komplett neu starten +pkill -f n8n +n8n start +``` + +#### Build-Fehler + +**Problem:** `npm run build` schlägt fehl. + +**Lösungen:** +```bash +# Node.js Version prüfen +node --version # Sollte >= 18 sein + +# node_modules löschen und neu installieren +rm -rf node_modules +npm install + +# TypeScript-Fehler anzeigen +npx tsc --noEmit +``` + +#### npm link Probleme + +**Problem:** `npm link` funktioniert nicht. + +**Lösungen:** +```bash +# Als Admin/Root ausführen (Linux/Mac) +sudo npm link + +# Windows: PowerShell als Administrator starten + +# Alternativer Pfad für Custom Nodes +export N8N_CUSTOM_EXTENSIONS=$(pwd) +n8n start +``` + +#### Docker-Probleme + +**Problem:** Container startet nicht. + +**Lösungen:** +```bash +# Logs prüfen +docker-compose logs n8n + +# Container-Status prüfen +docker-compose ps + +# Neu bauen +docker-compose build --no-cache +docker-compose up -d +``` + +#### Credential-Fehler + +**Problem:** "Authentication failed" bei Verbindung. + +**Lösungen:** +1. Prüfe LibreBooking URL (inkl. `/Web/Services`) +2. Prüfe Benutzername und Passwort +3. Prüfe ob API in LibreBooking aktiviert ist: + - Admin → Konfiguration → API aktivieren +4. Teste API manuell: + ```bash + curl -X POST "https://dein-server/Web/Services/Authentication/Authenticate" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"password"}' + ``` + +### Logs und Debugging + +**n8n Logs aktivieren:** +```bash +# Umgebungsvariable setzen +export N8N_LOG_LEVEL=debug +n8n start + +# Mit Docker +docker-compose exec n8n sh -c "N8N_LOG_LEVEL=debug n8n start" +``` + +**Node-spezifische Logs:** +In n8n Workflow-Ausführungen werden Details angezeigt unter "Execution Data". + +--- + +## Deinstallation + +### npm link entfernen + +```bash +# Im Projektverzeichnis +npm unlink + +# Global entfernen +npm unlink -g n8n-nodes-librebooking +``` + +### Global installiertes Paket entfernen + +```bash +npm uninstall -g n8n-nodes-librebooking +``` + +### Docker entfernen + +```bash +# Container und Volumes entfernen +docker-compose down -v + +# Images entfernen +docker rmi n8n-librebooking +docker rmi n8nio/n8n +``` + +### Projektverzeichnis löschen + +```bash +# Verzeichnis löschen +rm -rf n8n-nodes-librebooking + +# Archiv löschen +rm n8n-nodes-librebooking.tar.gz +rm n8n-nodes-librebooking.zip +``` + +--- + +## Support + +Bei Fragen oder Problemen: + +1. **GitHub Issues:** [Hier Issues erstellen](https://github.com/DEIN-REPO/n8n-nodes-librebooking/issues) +2. **Dokumentation:** Siehe [README.md](README.md) +3. **LibreBooking API:** https://www.bookedscheduler.com/help/api/ + +--- + +*Letzte Aktualisierung: Januar 2026* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..840fafe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 LibreBooking n8n Node Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec38613 --- /dev/null +++ b/README.md @@ -0,0 +1,618 @@ +# n8n-nodes-librebooking + +Ein vollständiger n8n Node für die Integration mit [LibreBooking](https://librebooking.org/) - einer Open-Source Ressourcen- und Raumbuchungslösung. + + +[![npm version](https://img.shields.io/npm/v/n8n-nodes-librebooking.svg?style=flat-square)](https://www.npmjs.com/package/n8n-nodes-librebooking) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) +[![n8n Community Node](https://img.shields.io/badge/n8n-Community%20Node-orange?style=flat-square)](https://n8n.io) +[![LibreBooking](https://img.shields.io/badge/LibreBooking-Integration-blue?style=flat-square)](https://librebooking.org) +[![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](https://nodejs.org) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?style=flat-square)](https://www.typescriptlang.org) + +--- + +## ⚡ Schnellstart + +### Option 1: Mit Installations-Skript (empfohlen) + +```bash +# Archiv entpacken +tar -xzf n8n-nodes-librebooking.tar.gz +cd n8n-nodes-librebooking + +# Installieren (Linux/Mac) +./install.sh + +# n8n starten +n8n start +``` + +### Option 2: Mit Docker + +```bash +# Archiv entpacken und starten +tar -xzf n8n-nodes-librebooking.tar.gz +cd n8n-nodes-librebooking +docker-compose up -d + +# Browser öffnen: http://localhost:5678 +``` + +### Option 3: Manuell mit npm + +```bash +cd n8n-nodes-librebooking +npm install && npm run build && npm link +n8n start +``` + +📖 **Detaillierte Anleitung:** Siehe [INSTALLATION.md](INSTALLATION.md) + +--- + +## 📋 Inhaltsverzeichnis + +- [Schnellstart](#-schnellstart) +- [Funktionen](#-funktionen) +- [Installation](#-installation) +- [Konfiguration](#️-konfiguration) +- [Operationen](#-operationen) +- [Beispiele](#-beispiele) +- [Trigger Node](#-trigger-node) +- [Troubleshooting](#-troubleshooting) +- [Entwicklung](#-entwicklung) +- [Lizenz](#-lizenz) + +--- + +## 🚀 Funktionen + +### Regular Node (LibreBooking) +- **Reservierungen**: Erstellen, Abrufen, Aktualisieren, Löschen, Genehmigen, Check-In/Check-Out +- **Ressourcen**: Verwalten von Räumen, Equipment und anderen buchbaren Ressourcen +- **Zeitpläne**: Abrufen von Zeitplänen und verfügbaren Slots +- **Benutzer**: Vollständige Benutzerverwaltung (Admin-Rechte erforderlich) +- **Konten**: Eigenes Konto verwalten +- **Gruppen**: Benutzergruppen mit Rollen und Berechtigungen verwalten +- **Zubehör**: Zubehörteile abrufen +- **Attribute**: Benutzerdefinierte Felder verwalten + +### Trigger Node (LibreBooking Trigger) +- Polling-basierter Trigger für Reservierungs-Events +- Erkennung neuer Reservierungen +- Erkennung geänderter Reservierungen +- Konfigurierbare Filter (Ressource, Zeitplan, Benutzer) +- Deduplizierung von Events + +--- + +## 📦 Installation + +### Über npm (empfohlen) + +```bash +npm install n8n-nodes-librebooking +``` + +### Manuelle Installation + +1. Laden Sie das Paket herunter oder klonen Sie das Repository: + ```bash + git clone https://github.com/your-org/n8n-nodes-librebooking.git + ``` + +2. Wechseln Sie ins Verzeichnis und installieren Sie die Abhängigkeiten: + ```bash + cd n8n-nodes-librebooking + npm install + ``` + +3. Kompilieren Sie das Projekt: + ```bash + npm run build + ``` + +4. Verlinken Sie das Paket für lokale Entwicklung: + ```bash + npm link + ``` + +5. Verlinken Sie es in Ihrem n8n Custom-Nodes-Verzeichnis: + ```bash + cd ~/.n8n/custom + npm link n8n-nodes-librebooking + ``` + +6. Starten Sie n8n neu: + ```bash + n8n start + ``` + +### Docker-Installation + +Fügen Sie in Ihrem Docker-Compose oder Dockerfile hinzu: + +```dockerfile +RUN npm install -g n8n-nodes-librebooking +``` + +Oder über Umgebungsvariable: +```yaml +environment: + - N8N_CUSTOM_EXTENSIONS=n8n-nodes-librebooking +``` + +### Integration in bestehende Docker-Installation + +Für bestehende n8n Docker-Installationen gibt es mehrere Integrationsmethoden: + +#### Schnellste Methode: Automatisches Skript + +```bash +# Ins n8n Verzeichnis wechseln +cd /pfad/zu/ihrer/n8n/installation + +# Skript ausführen +./install-docker.sh +``` + +#### Manuelle Methode + +```bash +# 1. Custom Nodes kopieren +cp -r custom-nodes /pfad/zu/n8n/ +cd /pfad/zu/n8n/custom-nodes && npm install && npm run build + +# 2. docker-compose.override.yml erstellen +cat > docker-compose.override.yml << 'EOF' +version: '3.8' +services: + n8n: + volumes: + - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + environment: + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true +EOF + +# 3. Container neustarten +docker-compose restart n8n +``` + +#### Eigenes Docker-Image bauen + +```bash +docker build -f Dockerfile.custom-nodes -t n8n-librebooking . +docker run -d -p 5678:5678 n8n-librebooking +``` + +📖 **Ausführliche Docker-Anleitung:** Siehe [DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md) + +🚀 **Docker-Schnellstart:** Siehe [SCHNELLSTART-DOCKER.md](SCHNELLSTART-DOCKER.md) + +--- + +## ⚙️ Konfiguration + +### Credentials einrichten + +1. Öffnen Sie n8n und gehen Sie zu **Settings** → **Credentials** +2. Klicken Sie auf **Add Credential** und wählen Sie **LibreBooking API** +3. Füllen Sie die folgenden Felder aus: + +| Feld | Beschreibung | Beispiel | +|------|-------------|----------| +| **LibreBooking URL** | Die Basis-URL Ihrer LibreBooking-Installation | `https://booking.example.com` | +| **Benutzername** | Ihr LibreBooking-Login (E-Mail oder Benutzername) | `admin@example.com` | +| **Passwort** | Ihr LibreBooking-Passwort | `•••••••••` | + +> ⚠️ **Wichtig**: Die URL sollte **ohne** `/Web/Services` angegeben werden! + +### API aktivieren + +Stellen Sie sicher, dass die API in Ihrer LibreBooking-Installation aktiviert ist: + +1. Öffnen Sie die `config.php` Ihrer LibreBooking-Installation +2. Suchen Sie nach `$conf['settings']['api']['enabled']` +3. Setzen Sie den Wert auf `true` + +```php +$conf['settings']['api']['enabled'] = 'true'; +``` + +### Credential-Test + +Nach dem Speichern der Credentials können Sie diese testen: +- Klicken Sie auf **Test** um die Verbindung zu überprüfen +- Bei erfolgreicher Verbindung wird eine Bestätigung angezeigt + +--- + +## 📖 Operationen + +### Reservierungen + +| Operation | Beschreibung | Admin-Rechte | +|-----------|-------------|--------------| +| **Alle Abrufen** | Liste aller Reservierungen mit optionalen Filtern | ❌ | +| **Abrufen** | Einzelne Reservierung per Referenznummer | ❌ | +| **Erstellen** | Neue Reservierung anlegen | ❌ | +| **Aktualisieren** | Bestehende Reservierung ändern | ❌ | +| **Löschen** | Reservierung entfernen | ❌ | +| **Genehmigen** | Ausstehende Reservierung genehmigen | ❌* | +| **Check-In** | In Reservierung einchecken | ❌ | +| **Check-Out** | Aus Reservierung auschecken | ❌ | + +*Erfordert Genehmigungsrechte + +#### Filter für "Alle Abrufen" +- `userId` - Nach Benutzer filtern +- `resourceId` - Nach Ressource filtern +- `scheduleId` - Nach Zeitplan filtern +- `startDateTime` - Startzeit (ISO 8601) +- `endDateTime` - Endzeit (ISO 8601) + +#### Zusätzliche Felder für "Erstellen/Aktualisieren" +- `description` - Beschreibung +- `participants` - Teilnehmer (Benutzer-IDs) +- `invitees` - Eingeladene (Benutzer-IDs) +- `resources` - Zusätzliche Ressourcen +- `allowParticipation` - Teilnahme erlauben + +### Ressourcen + +| Operation | Beschreibung | Admin-Rechte | +|-----------|-------------|--------------| +| **Alle Abrufen** | Liste aller Ressourcen | ❌ | +| **Abrufen** | Einzelne Ressource per ID | ❌ | +| **Verfügbarkeit Prüfen** | Verfügbarkeit einer/aller Ressourcen | ❌ | +| **Gruppen Abrufen** | Ressourcen-Gruppenstruktur | ❌ | +| **Typen Abrufen** | Verfügbare Ressourcen-Typen | ❌ | +| **Status Abrufen** | Verfügbare Status-Werte | ❌ | +| **Erstellen** | Neue Ressource anlegen | ✅ | +| **Aktualisieren** | Ressource ändern | ✅ | +| **Löschen** | Ressource entfernen | ✅ | + +#### Ressourcen-Optionen +- `location` - Standort +- `contact` - Kontaktinformation +- `description` - Beschreibung +- `maxParticipants` - Maximale Teilnehmerzahl +- `requiresApproval` - Genehmigung erforderlich +- `allowMultiday` - Mehrtägige Buchungen +- `requiresCheckIn` - Check-In erforderlich +- `autoReleaseMinutes` - Auto-Release nach X Minuten +- `color` - Anzeigefarbe (Hex) +- `statusId` - Status (0=Versteckt, 1=Verfügbar, 2=Nicht verfügbar) + +### Zeitpläne + +| Operation | Beschreibung | Admin-Rechte | +|-----------|-------------|--------------| +| **Alle Abrufen** | Liste aller Zeitpläne | ❌ | +| **Abrufen** | Einzelner Zeitplan mit Perioden | ❌ | +| **Slots Abrufen** | Verfügbare Slots eines Zeitplans | ❌ | + +### Benutzer (Admin) + +| Operation | Beschreibung | Admin-Rechte | +|-----------|-------------|--------------| +| **Alle Abrufen** | Liste aller Benutzer | ❌ | +| **Abrufen** | Einzelner Benutzer per ID | ❌ | +| **Erstellen** | Neuen Benutzer anlegen | ✅ | +| **Aktualisieren** | Benutzer ändern | ✅ | +| **Passwort Ändern** | Benutzer-Passwort setzen | ✅ | +| **Löschen** | Benutzer entfernen | ✅ | + +### Konten + +| Operation | Beschreibung | Admin-Rechte | +|-----------|-------------|--------------| +| **Abrufen** | Eigene Kontoinformationen | ❌ | +| **Erstellen** | Neues Konto (Registrierung) | ❌ | +| **Aktualisieren** | Eigenes Konto ändern | ❌ | +| **Passwort Ändern** | Eigenes Passwort ändern | ❌ | + +### Gruppen + +| Operation | Beschreibung | Admin-Rechte | +|-----------|-------------|--------------| +| **Alle Abrufen** | Liste aller Gruppen | ❌ | +| **Abrufen** | Einzelne Gruppe | ❌ | +| **Erstellen** | Neue Gruppe anlegen | ✅ | +| **Aktualisieren** | Gruppe ändern | ✅ | +| **Löschen** | Gruppe entfernen | ✅ | +| **Rollen Ändern** | Gruppenrollen setzen | ✅ | +| **Berechtigungen Ändern** | Ressourcen-Berechtigungen | ✅ | +| **Benutzer Ändern** | Gruppenmitglieder | ✅ | + +#### Rollen-IDs +- `1` - Gruppenadministrator +- `2` - Anwendungsadministrator +- `3` - Ressourcenadministrator +- `4` - Zeitplanadministrator + +### Zubehör + +| Operation | Beschreibung | Admin-Rechte | +|-----------|-------------|--------------| +| **Alle Abrufen** | Liste aller Zubehörteile | ❌ | +| **Abrufen** | Einzelnes Zubehörteil | ❌ | + +### Attribute + +| Operation | Beschreibung | Admin-Rechte | +|-----------|-------------|--------------| +| **Abrufen** | Einzelnes Attribut | ❌ | +| **Nach Kategorie Abrufen** | Alle Attribute einer Kategorie | ❌ | +| **Erstellen** | Neues Attribut anlegen | ✅ | +| **Aktualisieren** | Attribut ändern | ✅ | +| **Löschen** | Attribut entfernen | ✅ | + +#### Attribut-Kategorien +- `1` - Reservierung +- `2` - Benutzer +- `4` - Ressource +- `5` - Ressourcen-Typ + +#### Attribut-Typen +- `1` - Einzeilig (Text) +- `2` - Mehrzeilig (Textarea) +- `3` - Auswahlliste +- `4` - Checkbox +- `5` - Datum/Zeit + +--- + +## 💡 Beispiele + +### Beispiel 1: Alle Reservierungen der nächsten Woche abrufen + +```json +{ + "nodes": [ + { + "parameters": { + "resource": "reservation", + "operation": "getAll", + "filters": { + "startDateTime": "={{ $now.toISO() }}", + "endDateTime": "={{ $now.plus({days: 7}).toISO() }}" + } + }, + "name": "LibreBooking", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [250, 300], + "credentials": { + "libreBookingApi": { + "id": "1", + "name": "LibreBooking" + } + } + } + ] +} +``` + +### Beispiel 2: Neue Reservierung erstellen + +```json +{ + "parameters": { + "resource": "reservation", + "operation": "create", + "resourceId": 1, + "startDateTime": "2026-01-26T10:00:00", + "endDateTime": "2026-01-26T11:00:00", + "title": "Team Meeting", + "additionalFields": { + "description": "Wöchentliches Team-Meeting", + "participants": "2,3,4" + } + } +} +``` + +### Beispiel 3: Verfügbarkeit prüfen + +```json +{ + "parameters": { + "resource": "resource", + "operation": "getAvailability", + "resourceIdOptional": 1, + "availabilityDateTime": "2026-01-26T14:00:00" + } +} +``` + +### Beispiel 4: Benutzer mit Filter suchen + +```json +{ + "parameters": { + "resource": "user", + "operation": "getAll", + "userFilters": { + "organization": "Marketing", + "lastName": "Müller" + } + } +} +``` + +--- + +## ⚡ Trigger Node + +Der **LibreBooking Trigger** ist ein Polling-basierter Trigger, der auf neue oder geänderte Reservierungen reagiert. + +### Konfiguration + +| Parameter | Beschreibung | +|-----------|-------------| +| **Event** | Art des Events (Neue/Geänderte/Alle Reservierungen) | +| **Filter** | Optional: Ressource, Zeitplan, Benutzer | +| **Zeitfenster** | Überwachungszeitraum (7/14/30/90 Tage) | +| **Detaillierte Daten** | Vollständige Reservierungsdetails abrufen | + +### Event-Typen + +- **Neue Reservierung**: Wird nur bei neuen Reservierungen ausgelöst +- **Geänderte Reservierung**: Wird bei Änderungen an bestehenden Reservierungen ausgelöst +- **Alle Reservierungen**: Wird bei neuen und geänderten Reservierungen ausgelöst + +### Beispiel-Workflow: Benachrichtigung bei neuer Reservierung + +``` +[LibreBooking Trigger] → [IF: Ressource = 1] → [Slack: Nachricht senden] +``` + +### Deduplizierung + +Der Trigger speichert Informationen über bereits verarbeitete Reservierungen und verhindert so doppelte Ausführungen. Bei Änderungen (Titel, Zeit, etc.) wird eine Reservierung als "geändert" erkannt. + +--- + +## 🔧 Troubleshooting + +### Häufige Fehler + +#### "Authentifizierung fehlgeschlagen" +- Überprüfen Sie die LibreBooking-URL (ohne `/Web/Services`) +- Stellen Sie sicher, dass Benutzername und Passwort korrekt sind +- Prüfen Sie, ob die API in LibreBooking aktiviert ist + +#### "Zugriff verweigert" (403) +- Die Operation erfordert Administrator-Rechte +- Verwenden Sie einen Admin-Account oder wählen Sie eine andere Operation + +#### "Nicht gefunden" (404) +- Die angegebene ID (Ressource, Benutzer, etc.) existiert nicht +- Überprüfen Sie die Referenznummer bei Reservierungen + +#### "Session abgelaufen" +- Der Session-Token ist abgelaufen +- Führen Sie den Workflow erneut aus + +### API-Limitierungen + +- LibreBooking hat keine dokumentierten Rate-Limits +- Bei vielen Anfragen empfehlen wir Pausen zwischen den Operationen +- Der Node authentifiziert sich bei jeder Ausführung neu und meldet sich am Ende ab + +### Debug-Tipps + +1. Aktivieren Sie die n8n-Logs für detaillierte Fehlermeldungen: + ```bash + export N8N_LOG_LEVEL=debug + n8n start + ``` + +2. Testen Sie die API direkt im Browser: + ``` + https://your-librebooking.com/Web/Services/index.php + ``` + +3. Überprüfen Sie die Zeitzonen-Einstellungen in LibreBooking und n8n + +--- + +## 🛠 Entwicklung + +### Voraussetzungen + +- Node.js 18.17.0 oder höher +- npm 9.x oder höher +- n8n (für Tests) + +### Setup + +```bash +# Repository klonen +git clone https://github.com/your-org/n8n-nodes-librebooking.git +cd n8n-nodes-librebooking + +# Abhängigkeiten installieren +npm install + +# TypeScript kompilieren +npm run build + +# Für Entwicklung (Watch-Modus) +npm run dev +``` + +### Projektstruktur + +``` +n8n-nodes-librebooking/ +├── credentials/ +│ └── LibreBookingApi.credentials.ts +├── nodes/ +│ ├── LibreBooking/ +│ │ ├── LibreBooking.node.ts +│ │ └── librebooking.svg +│ └── LibreBookingTrigger/ +│ ├── LibreBookingTrigger.node.ts +│ └── librebooking.svg +├── workflows/ +│ └── example-workflows.json +├── test/ +│ └── test-api.ts +├── package.json +├── tsconfig.json +└── README.md +``` + +### Tests ausführen + +```bash +# API-Test mit echten Credentials +npm test +``` + +### Linting + +```bash +npm run lint +npm run lint:fix +``` + +### Build für Produktion + +```bash +npm run build +``` + +--- + +## 📄 Lizenz + +MIT License - siehe [LICENSE](LICENSE) Datei + +--- + +## 🤝 Beitragen + +Beiträge sind willkommen! Bitte öffnen Sie einen Issue oder Pull Request. + +1. Fork des Repositories +2. Feature-Branch erstellen (`git checkout -b feature/AmazingFeature`) +3. Änderungen committen (`git commit -m 'Add AmazingFeature'`) +4. Branch pushen (`git push origin feature/AmazingFeature`) +5. Pull Request öffnen + +--- + +## 📞 Support + +- **Issues**: [GitHub Issues](https://github.com/your-org/n8n-nodes-librebooking/issues) +- **LibreBooking Dokumentation**: [https://librebooking.org/docs](https://librebooking.org/docs) +- **n8n Community**: [https://community.n8n.io](https://community.n8n.io) + +--- + +**Erstellt mit ❤️ für die n8n und LibreBooking Community** diff --git a/SCHNELLSTART-DOCKER.md b/SCHNELLSTART-DOCKER.md new file mode 100644 index 0000000..a36c6d8 --- /dev/null +++ b/SCHNELLSTART-DOCKER.md @@ -0,0 +1,103 @@ +# Schnellstart: Docker-Integration + +Ultra-kurze Anleitung für erfahrene Docker-Nutzer. + +--- + +## Automatische Installation (Empfohlen) + +```bash +# Ins n8n Verzeichnis wechseln +cd /pfad/zu/deiner/n8n/installation + +# Skript ausführen +./install-docker.sh + +# Oder mit Pfad +./install-docker.sh -p /opt/n8n +``` + +--- + +## Manuelle Installation (3 Schritte) + +### 1. Custom Nodes kopieren + +```bash +cp -r custom-nodes /pfad/zu/n8n/ +cd /pfad/zu/n8n/custom-nodes +npm install && npm run build +``` + +### 2. Override-Datei erstellen + +```bash +cat > docker-compose.override.yml << 'EOF' +version: '3.8' +services: + n8n: + volumes: + - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + environment: + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true +EOF +``` + +### 3. Neustarten + +```bash +docker-compose restart n8n +``` + +--- + +## Neues Setup mit Docker + +```bash +# Beispiel-Konfiguration verwenden +cp docker-compose.example.yml docker-compose.yml +cp .env.docker .env + +# .env anpassen, dann starten +docker-compose up -d +``` + +--- + +## Eigenes Image bauen + +```bash +docker build -f Dockerfile.custom-nodes -t n8n-librebooking . +docker run -d -p 5678:5678 n8n-librebooking +``` + +--- + +## Verifizierung + +```bash +# Node prüfen +docker exec n8n ls /home/node/.n8n/custom/n8n-nodes-librebooking/dist/ + +# In n8n: Nach "LibreBooking" suchen +``` + +--- + +## Bei Problemen + +```bash +# Berechtigungen +sudo chown -R 1000:1000 custom-nodes/ + +# Logs +docker logs n8n | grep -i error + +# Neustart +docker-compose down && docker-compose up -d +``` + +--- + +📖 **Ausführliche Anleitung:** [DOCKER-INTEGRATION.md](DOCKER-INTEGRATION.md) diff --git a/SCHNELLSTART.md b/SCHNELLSTART.md new file mode 100644 index 0000000..a30325f --- /dev/null +++ b/SCHNELLSTART.md @@ -0,0 +1,60 @@ +# Schnellstart - LibreBooking n8n Node + +Diese Anleitung ist für erfahrene Benutzer, die schnell loslegen möchten. + +## Option A: Mit Skript (empfohlen) + +```bash +# Archiv entpacken +tar -xzf n8n-nodes-librebooking.tar.gz +cd n8n-nodes-librebooking + +# Installieren +./install.sh + +# n8n starten +n8n start +``` + +## Option B: Mit Docker + +```bash +# Archiv entpacken +tar -xzf n8n-nodes-librebooking.tar.gz +cd n8n-nodes-librebooking + +# Container starten +docker-compose up -d + +# Browser öffnen +open http://localhost:5678 +``` + +## Option C: Manuell + +```bash +tar -xzf n8n-nodes-librebooking.tar.gz +cd n8n-nodes-librebooking +npm install +npm run build +npm link +n8n start +``` + +## Credentials einrichten + +1. Öffne http://localhost:5678 +2. Erstelle neuen Workflow +3. Füge "LibreBooking" Node hinzu +4. Erstelle neue Credentials: + - **URL:** `https://dein-server/Web/Services` + - **Username:** Admin-Benutzer + - **Password:** Passwort + +## Fertig! + +Der LibreBooking Node ist jetzt verfügbar. + +--- + +Für detaillierte Anleitungen siehe [INSTALLATION.md](INSTALLATION.md) diff --git a/credentials/LibreBookingApi.credentials.ts b/credentials/LibreBookingApi.credentials.ts new file mode 100644 index 0000000..ca0de1c --- /dev/null +++ b/credentials/LibreBookingApi.credentials.ts @@ -0,0 +1,65 @@ +import { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +/** + * LibreBooking API Credentials + * + * LibreBooking verwendet Session-basierte Authentifizierung. + * Der Node holt bei jeder Ausführung einen neuen Session-Token. + */ +export class LibreBookingApi implements ICredentialType { + name = 'libreBookingApi'; + displayName = 'LibreBooking API'; + documentationUrl = 'https://librebooking.org/docs/api'; + + properties: INodeProperties[] = [ + { + displayName: 'LibreBooking URL', + name: 'url', + type: 'string', + default: '', + placeholder: 'https://booking.example.com', + required: true, + description: 'Die Basis-URL Ihrer LibreBooking-Installation (ohne /Web/Services)', + }, + { + displayName: 'Benutzername', + name: 'username', + type: 'string', + default: '', + required: true, + description: 'Ihr LibreBooking-Benutzername oder E-Mail-Adresse', + }, + { + displayName: 'Passwort', + name: 'password', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + required: true, + description: 'Ihr LibreBooking-Passwort', + }, + ]; + + // Test-Request um die Credentials zu validieren + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.url}}', + url: '/Web/Services/index.php/Authentication/Authenticate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: { + username: '={{$credentials.username}}', + password: '={{$credentials.password}}', + }, + }, + }; +} diff --git a/custom-nodes/README.md b/custom-nodes/README.md new file mode 100644 index 0000000..867cd96 --- /dev/null +++ b/custom-nodes/README.md @@ -0,0 +1,42 @@ +# LibreBooking n8n Node - Custom Nodes Version + +Diese Version ist speziell für die Integration in bestehende n8n Docker-Installationen optimiert. + +## Schnellinstallation + +### 1. Verzeichnis in n8n Custom Nodes kopieren + +```bash +cp -r custom-nodes /pfad/zu/n8n/.n8n/custom/n8n-nodes-librebooking +``` + +### 2. Dependencies installieren und bauen + +```bash +cd /pfad/zu/n8n/.n8n/custom/n8n-nodes-librebooking +npm install +npm run build +``` + +### 3. n8n neustarten + +```bash +docker-compose restart n8n +``` + +## Enthaltene Dateien + +- `credentials/` - API Credentials Definition +- `nodes/` - LibreBooking und LibreBookingTrigger Nodes +- `package.json` - Vereinfachte Package-Konfiguration +- `tsconfig.json` - TypeScript Konfiguration + +## Weitere Informationen + +Siehe die ausführliche Dokumentation: +- `DOCKER-INTEGRATION.md` - Detaillierte Docker-Anleitung +- `README.md` - Vollständige Dokumentation + +## Lizenz + +MIT License diff --git a/custom-nodes/credentials/LibreBookingApi.credentials.ts b/custom-nodes/credentials/LibreBookingApi.credentials.ts new file mode 100644 index 0000000..ca0de1c --- /dev/null +++ b/custom-nodes/credentials/LibreBookingApi.credentials.ts @@ -0,0 +1,65 @@ +import { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +/** + * LibreBooking API Credentials + * + * LibreBooking verwendet Session-basierte Authentifizierung. + * Der Node holt bei jeder Ausführung einen neuen Session-Token. + */ +export class LibreBookingApi implements ICredentialType { + name = 'libreBookingApi'; + displayName = 'LibreBooking API'; + documentationUrl = 'https://librebooking.org/docs/api'; + + properties: INodeProperties[] = [ + { + displayName: 'LibreBooking URL', + name: 'url', + type: 'string', + default: '', + placeholder: 'https://booking.example.com', + required: true, + description: 'Die Basis-URL Ihrer LibreBooking-Installation (ohne /Web/Services)', + }, + { + displayName: 'Benutzername', + name: 'username', + type: 'string', + default: '', + required: true, + description: 'Ihr LibreBooking-Benutzername oder E-Mail-Adresse', + }, + { + displayName: 'Passwort', + name: 'password', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + required: true, + description: 'Ihr LibreBooking-Passwort', + }, + ]; + + // Test-Request um die Credentials zu validieren + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.url}}', + url: '/Web/Services/index.php/Authentication/Authenticate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: { + username: '={{$credentials.username}}', + password: '={{$credentials.password}}', + }, + }, + }; +} diff --git a/custom-nodes/index.ts b/custom-nodes/index.ts new file mode 100644 index 0000000..13abd91 --- /dev/null +++ b/custom-nodes/index.ts @@ -0,0 +1,4 @@ +// LibreBooking n8n Node - Entry Point +export { LibreBooking } from './nodes/LibreBooking/LibreBooking.node'; +export { LibreBookingTrigger } from './nodes/LibreBookingTrigger/LibreBookingTrigger.node'; +export { LibreBookingApi } from './credentials/LibreBookingApi.credentials'; diff --git a/custom-nodes/nodes/LibreBooking/LibreBooking.node.ts b/custom-nodes/nodes/LibreBooking/LibreBooking.node.ts new file mode 100644 index 0000000..675b4ad --- /dev/null +++ b/custom-nodes/nodes/LibreBooking/LibreBooking.node.ts @@ -0,0 +1,1203 @@ +import { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + IHttpRequestMethods, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +/** + * Authentifizierung bei LibreBooking + */ +async function authenticate( + executeFunctions: IExecuteFunctions, + baseUrl: string, + username: string, + password: string, +): Promise { + try { + const response = await executeFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`, + headers: { 'Content-Type': 'application/json' }, + body: { username, password }, + json: true, + }); + + if (!response.isAuthenticated) { + throw new NodeOperationError( + executeFunctions.getNode(), + 'Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Credentials.', + ); + } + + return { + sessionToken: response.sessionToken, + userId: response.userId, + sessionExpires: response.sessionExpires, + }; + } catch (error: any) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Authentifizierung fehlgeschlagen', + description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.', + }); + } +} + +/** + * Abmeldung von LibreBooking + */ +async function signOut( + executeFunctions: IExecuteFunctions, + baseUrl: string, + session: LibreBookingSession, +): Promise { + try { + await executeFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`, + headers: { 'Content-Type': 'application/json' }, + body: { + userId: session.userId, + sessionToken: session.sessionToken, + }, + json: true, + }); + } catch (error) { + // Ignoriere SignOut-Fehler + } +} + +/** + * API-Request mit Session-Authentifizierung + */ +async function makeApiRequest( + executeFunctions: IExecuteFunctions, + baseUrl: string, + session: LibreBookingSession, + method: IHttpRequestMethods, + endpoint: string, + body?: any, + qs?: any, +): Promise { + const options: any = { + method, + url: `${baseUrl}/Web/Services/index.php${endpoint}`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + json: true, + }; + + if (body && Object.keys(body).length > 0) { + options.body = body; + } + + if (qs && Object.keys(qs).length > 0) { + options.qs = qs; + } + + try { + return await executeFunctions.helpers.httpRequest(options); + } catch (error: any) { + if (error.statusCode === 401) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Authentifizierung abgelaufen', + description: 'Der Session-Token ist abgelaufen. Bitte erneut ausführen.', + }); + } else if (error.statusCode === 403) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Zugriff verweigert', + description: 'Sie haben keine Berechtigung für diese Operation. Admin-Rechte erforderlich?', + }); + } else if (error.statusCode === 404) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Nicht gefunden', + description: 'Die angeforderte Ressource wurde nicht gefunden.', + }); + } + throw new NodeApiError(executeFunctions.getNode(), error, { + message: `API-Fehler: ${error.message}`, + }); + } +} + +/** + * Hilfsfunktion: String zu Array von Zahlen + */ +function parseIdList(value: string | undefined): number[] { + if (!value || value.trim() === '') return []; + return value.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id)); +} + +/** + * LibreBooking n8n Node + * + * Vollständige Integration für die LibreBooking API. + * Unterstützt alle wichtigen Ressourcen und Operationen. + */ +export class LibreBooking implements INodeType { + description: INodeTypeDescription = { + displayName: 'LibreBooking', + name: 'libreBooking', + icon: 'file:librebooking.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Verwalten Sie Reservierungen, Ressourcen, Benutzer und mehr mit LibreBooking', + defaults: { + name: 'LibreBooking', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'libreBookingApi', + required: true, + }, + ], + properties: [ + // ===================================================== + // RESOURCE SELECTOR + // ===================================================== + { + displayName: 'Ressource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Reservierung', + value: 'reservation', + description: 'Reservierungen verwalten', + }, + { + name: 'Ressource', + value: 'resource', + description: 'Ressourcen (Räume, Equipment) verwalten', + }, + { + name: 'Zeitplan', + value: 'schedule', + description: 'Zeitpläne abrufen', + }, + { + name: 'Benutzer', + value: 'user', + description: 'Benutzer verwalten (Admin-Rechte erforderlich)', + }, + { + name: 'Konto', + value: 'account', + description: 'Eigenes Konto verwalten', + }, + { + name: 'Gruppe', + value: 'group', + description: 'Benutzergruppen verwalten', + }, + { + name: 'Zubehör', + value: 'accessory', + description: 'Zubehör abrufen', + }, + { + name: 'Attribut', + value: 'attribute', + description: 'Benutzerdefinierte Attribute verwalten', + }, + ], + default: 'reservation', + }, + + // ===================================================== + // RESERVATION OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['reservation'], + }, + }, + options: [ + { name: 'Erstellen', value: 'create', description: 'Neue Reservierung erstellen', action: 'Reservierung erstellen' }, + { name: 'Abrufen', value: 'get', description: 'Reservierung abrufen', action: 'Reservierung abrufen' }, + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Reservierungen abrufen', action: 'Alle Reservierungen abrufen' }, + { name: 'Aktualisieren', value: 'update', description: 'Reservierung aktualisieren', action: 'Reservierung aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Reservierung löschen', action: 'Reservierung löschen' }, + { name: 'Genehmigen', value: 'approve', description: 'Ausstehende Reservierung genehmigen', action: 'Reservierung genehmigen' }, + { name: 'Check-In', value: 'checkIn', description: 'In Reservierung einchecken', action: 'In Reservierung einchecken' }, + { name: 'Check-Out', value: 'checkOut', description: 'Aus Reservierung auschecken', action: 'Aus Reservierung auschecken' }, + ], + default: 'getAll', + }, + + // ===================================================== + // RESOURCE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['resource'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Ressourcen abrufen', action: 'Alle Ressourcen abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Ressource abrufen', action: 'Ressource abrufen' }, + { name: 'Verfügbarkeit Prüfen', value: 'getAvailability', description: 'Verfügbarkeit von Ressourcen prüfen', action: 'Verfügbarkeit prüfen' }, + { name: 'Gruppen Abrufen', value: 'getGroups', description: 'Ressourcen-Gruppen abrufen', action: 'Ressourcen-Gruppen abrufen' }, + { name: 'Typen Abrufen', value: 'getTypes', description: 'Ressourcen-Typen abrufen', action: 'Ressourcen-Typen abrufen' }, + { name: 'Status Abrufen', value: 'getStatuses', description: 'Verfügbare Status abrufen', action: 'Status abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neue Ressource erstellen (Admin)', action: 'Ressource erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Ressource aktualisieren (Admin)', action: 'Ressource aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Ressource löschen (Admin)', action: 'Ressource löschen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // SCHEDULE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['schedule'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Zeitpläne abrufen', action: 'Alle Zeitpläne abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Zeitplan abrufen', action: 'Zeitplan abrufen' }, + { name: 'Slots Abrufen', value: 'getSlots', description: 'Verfügbare Slots abrufen', action: 'Slots abrufen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // USER OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['user'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Benutzer abrufen', action: 'Alle Benutzer abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Benutzer abrufen', action: 'Benutzer abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neuen Benutzer erstellen (Admin)', action: 'Benutzer erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Benutzer aktualisieren (Admin)', action: 'Benutzer aktualisieren' }, + { name: 'Passwort Ändern', value: 'updatePassword', description: 'Benutzer-Passwort ändern (Admin)', action: 'Passwort ändern' }, + { name: 'Löschen', value: 'delete', description: 'Benutzer löschen (Admin)', action: 'Benutzer löschen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ACCOUNT OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['account'] } }, + options: [ + { name: 'Abrufen', value: 'get', description: 'Eigene Kontoinformationen abrufen', action: 'Konto abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neues Konto erstellen (Registrierung)', action: 'Konto erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Eigenes Konto aktualisieren', action: 'Konto aktualisieren' }, + { name: 'Passwort Ändern', value: 'updatePassword', description: 'Eigenes Passwort ändern', action: 'Passwort ändern' }, + ], + default: 'get', + }, + + // ===================================================== + // GROUP OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['group'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Gruppen abrufen', action: 'Alle Gruppen abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Gruppe abrufen', action: 'Gruppe abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neue Gruppe erstellen (Admin)', action: 'Gruppe erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Gruppe aktualisieren (Admin)', action: 'Gruppe aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Gruppe löschen (Admin)', action: 'Gruppe löschen' }, + { name: 'Rollen Ändern', value: 'changeRoles', description: 'Gruppenrollen ändern (Admin)', action: 'Rollen ändern' }, + { name: 'Berechtigungen Ändern', value: 'changePermissions', description: 'Gruppenberechtigungen ändern (Admin)', action: 'Berechtigungen ändern' }, + { name: 'Benutzer Ändern', value: 'changeUsers', description: 'Gruppenbenutzer ändern (Admin)', action: 'Benutzer ändern' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ACCESSORY OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['accessory'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Zubehörteile abrufen', action: 'Alle Zubehörteile abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Zubehörteil abrufen', action: 'Zubehörteil abrufen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ATTRIBUTE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['attribute'] } }, + options: [ + { name: 'Abrufen', value: 'get', description: 'Attribut abrufen', action: 'Attribut abrufen' }, + { name: 'Nach Kategorie Abrufen', value: 'getByCategory', description: 'Attribute einer Kategorie abrufen', action: 'Attribute nach Kategorie abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neues Attribut erstellen (Admin)', action: 'Attribut erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Attribut aktualisieren (Admin)', action: 'Attribut aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Attribut löschen (Admin)', action: 'Attribut löschen' }, + ], + default: 'getByCategory', + }, + + // ===================================================== + // RESERVATION PARAMETERS + // ===================================================== + { + displayName: 'Referenznummer', + name: 'referenceNumber', + type: 'string', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['get', 'update', 'delete', 'approve', 'checkIn', 'checkOut'] } }, + default: '', + description: 'Die eindeutige Referenznummer der Reservierung', + }, + { + displayName: 'Ressourcen-ID', + name: 'resourceId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create'] } }, + default: 1, + description: 'Die ID der zu reservierenden Ressource', + }, + { + displayName: 'Startzeit', + name: 'startDateTime', + type: 'dateTime', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Startzeitpunkt der Reservierung (ISO 8601 Format)', + }, + { + displayName: 'Endzeit', + name: 'endDateTime', + type: 'dateTime', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Endzeitpunkt der Reservierung (ISO 8601 Format)', + }, + { + displayName: 'Titel', + name: 'title', + type: 'string', + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Titel der Reservierung', + }, + { + displayName: 'Aktualisierungsbereich', + name: 'updateScope', + type: 'options', + displayOptions: { show: { resource: ['reservation'], operation: ['update', 'delete'] } }, + options: [ + { name: 'Nur Diese', value: 'this', description: 'Nur diese Instanz ändern' }, + { name: 'Zukünftige', value: 'future', description: 'Diese und alle zukünftigen Instanzen ändern' }, + { name: 'Alle', value: 'full', description: 'Alle Instanzen der Serie ändern' }, + ], + default: 'this', + }, + { + displayName: 'Zusätzliche Felder', + name: 'additionalFields', + type: 'collection', + placeholder: 'Feld hinzufügen', + default: {}, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Beschreibung', name: 'description', type: 'string', default: '' }, + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + { displayName: 'Zusätzliche Ressourcen', name: 'resources', type: 'string', default: '', description: 'Komma-getrennte Liste' }, + { displayName: 'Teilnehmer', name: 'participants', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' }, + { displayName: 'Eingeladene', name: 'invitees', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' }, + { displayName: 'Teilnahme Erlauben', name: 'allowParticipation', type: 'boolean', default: true }, + { displayName: 'Nutzungsbedingungen Akzeptiert', name: 'termsAccepted', type: 'boolean', default: true }, + ], + }, + { + displayName: 'Filter', + name: 'filters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['reservation'], operation: ['getAll'] } }, + options: [ + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' }, + { displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' }, + { displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' }, + ], + }, + + // ===================================================== + // RESOURCE PARAMETERS + // ===================================================== + { + displayName: 'Ressourcen-ID', + name: 'resourceIdParam', + type: 'number', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['get', 'update', 'delete'] } }, + default: 1, + }, + { + displayName: 'Ressourcen-ID (Optional)', + name: 'resourceIdOptional', + type: 'number', + displayOptions: { show: { resource: ['resource'], operation: ['getAvailability'] } }, + default: '', + }, + { + displayName: 'Datum/Zeit', + name: 'availabilityDateTime', + type: 'dateTime', + displayOptions: { show: { resource: ['resource'], operation: ['getAvailability'] } }, + default: '', + }, + { + displayName: 'Ressourcen-Name', + name: 'resourceName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Zeitplan-ID', + name: 'scheduleIdForResource', + type: 'number', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['create'] } }, + default: 1, + }, + { + displayName: 'Ressourcen-Optionen', + name: 'resourceOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['resource'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Standort', name: 'location', type: 'string', default: '' }, + { displayName: 'Kontakt', name: 'contact', type: 'string', default: '' }, + { displayName: 'Beschreibung', name: 'description', type: 'string', default: '' }, + { displayName: 'Notizen', name: 'notes', type: 'string', default: '' }, + { displayName: 'Max. Teilnehmer', name: 'maxParticipants', type: 'number', default: 0 }, + { displayName: 'Genehmigung Erforderlich', name: 'requiresApproval', type: 'boolean', default: false }, + { displayName: 'Mehrtägig Erlauben', name: 'allowMultiday', type: 'boolean', default: false }, + { displayName: 'Check-In Erforderlich', name: 'requiresCheckIn', type: 'boolean', default: false }, + { displayName: 'Auto-Release Minuten', name: 'autoReleaseMinutes', type: 'number', default: 0 }, + { displayName: 'Farbe', name: 'color', type: 'string', default: '' }, + { displayName: 'Status-ID', name: 'statusId', type: 'options', options: [{ name: 'Versteckt', value: 0 }, { name: 'Verfügbar', value: 1 }, { name: 'Nicht Verfügbar', value: 2 }], default: 1 }, + ], + }, + + // ===================================================== + // SCHEDULE PARAMETERS + // ===================================================== + { + displayName: 'Zeitplan-ID', + name: 'scheduleId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['schedule'], operation: ['get', 'getSlots'] } }, + default: 1, + }, + { + displayName: 'Slots-Filter', + name: 'slotsFilters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['schedule'], operation: ['getSlots'] } }, + options: [ + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' }, + { displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' }, + ], + }, + + // ===================================================== + // USER PARAMETERS + // ===================================================== + { + displayName: 'Benutzer-ID', + name: 'userId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['get', 'update', 'updatePassword', 'delete'] } }, + default: 1, + }, + { + displayName: 'E-Mail', + name: 'emailAddress', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + default: '', + }, + { + displayName: 'Benutzername', + name: 'userName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + default: '', + }, + { + displayName: 'Passwort', + name: 'password', + type: 'string', + typeOptions: { password: true }, + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'updatePassword'] } }, + default: '', + }, + { + displayName: 'Vorname', + name: 'firstName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Nachname', + name: 'lastName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Benutzer-Filter', + name: 'userFilters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['getAll'] } }, + options: [ + { displayName: 'Benutzername', name: 'username', type: 'string', default: '' }, + { displayName: 'E-Mail', name: 'email', type: 'string', default: '' }, + { displayName: 'Vorname', name: 'firstName', type: 'string', default: '' }, + { displayName: 'Nachname', name: 'lastName', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + ], + }, + { + displayName: 'Benutzer-Optionen', + name: 'userOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Zeitzone', name: 'timezone', type: 'string', default: 'Europe/Berlin' }, + { displayName: 'Sprache', name: 'language', type: 'string', default: 'de_de' }, + { displayName: 'Telefon', name: 'phone', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + { displayName: 'Position', name: 'position', type: 'string', default: '' }, + { displayName: 'Gruppen', name: 'groups', type: 'string', default: '', description: 'Komma-getrennte Gruppen-IDs' }, + ], + }, + + // ===================================================== + // ACCOUNT PARAMETERS + // ===================================================== + { + displayName: 'Benutzer-ID', + name: 'accountUserId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['account'], operation: ['get', 'update', 'updatePassword'] } }, + default: '', + }, + { + displayName: 'Account-Daten', + name: 'accountData', + type: 'collection', + placeholder: 'Feld hinzufügen', + default: {}, + displayOptions: { show: { resource: ['account'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'E-Mail', name: 'emailAddress', type: 'string', default: '' }, + { displayName: 'Benutzername', name: 'userName', type: 'string', default: '' }, + { displayName: 'Passwort', name: 'password', type: 'string', typeOptions: { password: true }, default: '' }, + { displayName: 'Vorname', name: 'firstName', type: 'string', default: '' }, + { displayName: 'Nachname', name: 'lastName', type: 'string', default: '' }, + { displayName: 'Zeitzone', name: 'timezone', type: 'string', default: 'Europe/Berlin' }, + { displayName: 'Sprache', name: 'language', type: 'string', default: 'de_de' }, + { displayName: 'Telefon', name: 'phone', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + { displayName: 'Position', name: 'position', type: 'string', default: '' }, + { displayName: 'AGB Akzeptiert', name: 'acceptTermsOfService', type: 'boolean', default: true }, + ], + }, + { + displayName: 'Passwort-Änderung', + name: 'passwordChange', + type: 'fixedCollection', + default: {}, + displayOptions: { show: { resource: ['account'], operation: ['updatePassword'] } }, + options: [ + { + name: 'passwords', + displayName: 'Passwörter', + values: [ + { displayName: 'Aktuelles Passwort', name: 'currentPassword', type: 'string', typeOptions: { password: true }, default: '' }, + { displayName: 'Neues Passwort', name: 'newPassword', type: 'string', typeOptions: { password: true }, default: '' }, + ], + }, + ], + }, + + // ===================================================== + // GROUP PARAMETERS + // ===================================================== + { + displayName: 'Gruppen-ID', + name: 'groupId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['group'], operation: ['get', 'update', 'delete', 'changeRoles', 'changePermissions', 'changeUsers'] } }, + default: 1, + }, + { + displayName: 'Gruppen-Name', + name: 'groupName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Standard-Gruppe', + name: 'isDefault', + type: 'boolean', + displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } }, + default: false, + }, + { + displayName: 'Rollen-IDs', + name: 'roleIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changeRoles'] } }, + default: '', + description: '1=Gruppenadmin, 2=App-Admin, 3=Ressourcen-Admin, 4=Zeitplan-Admin', + }, + { + displayName: 'Ressourcen-IDs', + name: 'permissionResourceIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changePermissions'] } }, + default: '', + }, + { + displayName: 'Benutzer-IDs', + name: 'groupUserIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changeUsers'] } }, + default: '', + }, + + // ===================================================== + // ACCESSORY PARAMETERS + // ===================================================== + { + displayName: 'Zubehör-ID', + name: 'accessoryId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['accessory'], operation: ['get'] } }, + default: 1, + }, + + // ===================================================== + // ATTRIBUTE PARAMETERS + // ===================================================== + { + displayName: 'Attribut-ID', + name: 'attributeId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['get', 'update', 'delete'] } }, + default: 1, + }, + { + displayName: 'Kategorie-ID', + name: 'categoryId', + type: 'options', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['getByCategory', 'create'] } }, + options: [ + { name: 'Reservierung', value: 1 }, + { name: 'Benutzer', value: 2 }, + { name: 'Ressource', value: 4 }, + { name: 'Ressourcen-Typ', value: 5 }, + ], + default: 1, + }, + { + displayName: 'Attribut-Label', + name: 'attributeLabel', + type: 'string', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Attribut-Typ', + name: 'attributeType', + type: 'options', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + options: [ + { name: 'Einzeilig', value: 1 }, + { name: 'Mehrzeilig', value: 2 }, + { name: 'Auswahlliste', value: 3 }, + { name: 'Checkbox', value: 4 }, + { name: 'Datum/Zeit', value: 5 }, + ], + default: 1, + }, + { + displayName: 'Attribut-Optionen', + name: 'attributeOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Erforderlich', name: 'required', type: 'boolean', default: false }, + { displayName: 'Nur Admin', name: 'adminOnly', type: 'boolean', default: false }, + { displayName: 'Privat', name: 'isPrivate', type: 'boolean', default: false }, + { displayName: 'Sortierung', name: 'sortOrder', type: 'number', default: 0 }, + { displayName: 'Regex-Validierung', name: 'regex', type: 'string', default: '' }, + { displayName: 'Mögliche Werte', name: 'possibleValues', type: 'string', default: '', description: 'Komma-getrennt' }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const credentials = await this.getCredentials('libreBookingApi'); + const baseUrl = (credentials.url as string).replace(/\/$/, ''); + const username = credentials.username as string; + const pw = credentials.password as string; + + const session = await authenticate(this, baseUrl, username, pw); + + try { + for (let i = 0; i < items.length; i++) { + try { + const resource = this.getNodeParameter('resource', i) as string; + const operation = this.getNodeParameter('operation', i) as string; + let responseData: any; + + // RESERVATION + if (resource === 'reservation') { + if (operation === 'getAll') { + const filters = this.getNodeParameter('filters', i, {}) as any; + const qs: any = {}; + if (filters.userId) qs.userId = filters.userId; + if (filters.resourceId) qs.resourceId = filters.resourceId; + if (filters.scheduleId) qs.scheduleId = filters.scheduleId; + if (filters.startDateTime) qs.startDateTime = filters.startDateTime; + if (filters.endDateTime) qs.endDateTime = filters.endDateTime; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Reservations/', undefined, qs); + } else if (operation === 'get') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Reservations/${referenceNumber}`); + } else if (operation === 'create') { + const resourceId = this.getNodeParameter('resourceId', i) as number; + const startDateTime = this.getNodeParameter('startDateTime', i) as string; + const endDateTime = this.getNodeParameter('endDateTime', i) as string; + const title = this.getNodeParameter('title', i, '') as string; + const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any; + const body: any = { resourceId, startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() }; + if (title) body.title = title; + if (additionalFields.description) body.description = additionalFields.description; + if (additionalFields.userId) body.userId = additionalFields.userId; + if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources); + if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants); + if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees); + if (additionalFields.allowParticipation !== undefined) body.allowParticipation = additionalFields.allowParticipation; + if (additionalFields.termsAccepted !== undefined) body.termsAccepted = additionalFields.termsAccepted; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Reservations/', body); + } else if (operation === 'update') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + const startDateTime = this.getNodeParameter('startDateTime', i) as string; + const endDateTime = this.getNodeParameter('endDateTime', i) as string; + const title = this.getNodeParameter('title', i, '') as string; + const updateScope = this.getNodeParameter('updateScope', i, 'this') as string; + const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any; + const body: any = { startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() }; + if (title) body.title = title; + if (additionalFields.description) body.description = additionalFields.description; + if (additionalFields.resourceId) body.resourceId = additionalFields.resourceId; + if (additionalFields.userId) body.userId = additionalFields.userId; + if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources); + if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants); + if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}?updateScope=${updateScope}`, body); + } else if (operation === 'delete') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + const updateScope = this.getNodeParameter('updateScope', i, 'this') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Reservations/${referenceNumber}?updateScope=${updateScope}`); + } else if (operation === 'approve') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/Approval`); + } else if (operation === 'checkIn') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/CheckIn`); + } else if (operation === 'checkOut') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/CheckOut`); + } + } + + // RESOURCE + else if (resource === 'resource') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/'); + } else if (operation === 'get') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Resources/${resourceIdParam}`); + } else if (operation === 'getAvailability') { + const resourceIdOptional = this.getNodeParameter('resourceIdOptional', i, '') as number | ''; + const availabilityDateTime = this.getNodeParameter('availabilityDateTime', i, '') as string; + let endpoint = '/Resources/Availability'; + if (resourceIdOptional) endpoint = `/Resources/${resourceIdOptional}/Availability`; + const qs: any = {}; + if (availabilityDateTime) qs.dateTime = new Date(availabilityDateTime).toISOString(); + responseData = await makeApiRequest(this, baseUrl, session, 'GET', endpoint, undefined, qs); + } else if (operation === 'getGroups') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Groups'); + } else if (operation === 'getTypes') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Types'); + } else if (operation === 'getStatuses') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Status'); + } else if (operation === 'create') { + const resourceName = this.getNodeParameter('resourceName', i) as string; + const scheduleIdForResource = this.getNodeParameter('scheduleIdForResource', i) as number; + const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any; + const body: any = { name: resourceName, scheduleId: scheduleIdForResource }; + if (resourceOptions.location) body.location = resourceOptions.location; + if (resourceOptions.contact) body.contact = resourceOptions.contact; + if (resourceOptions.description) body.description = resourceOptions.description; + if (resourceOptions.notes) body.notes = resourceOptions.notes; + if (resourceOptions.maxParticipants) body.maxParticipants = resourceOptions.maxParticipants; + if (resourceOptions.requiresApproval !== undefined) body.requiresApproval = resourceOptions.requiresApproval; + if (resourceOptions.allowMultiday !== undefined) body.allowMultiday = resourceOptions.allowMultiday; + if (resourceOptions.requiresCheckIn !== undefined) body.requiresCheckIn = resourceOptions.requiresCheckIn; + if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes; + if (resourceOptions.color) body.color = resourceOptions.color; + if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Resources/', body); + } else if (operation === 'update') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + const resourceName = this.getNodeParameter('resourceName', i) as string; + const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any; + const body: any = { name: resourceName }; + if (resourceOptions.location) body.location = resourceOptions.location; + if (resourceOptions.contact) body.contact = resourceOptions.contact; + if (resourceOptions.description) body.description = resourceOptions.description; + if (resourceOptions.notes) body.notes = resourceOptions.notes; + if (resourceOptions.maxParticipants) body.maxParticipants = resourceOptions.maxParticipants; + if (resourceOptions.requiresApproval !== undefined) body.requiresApproval = resourceOptions.requiresApproval; + if (resourceOptions.allowMultiday !== undefined) body.allowMultiday = resourceOptions.allowMultiday; + if (resourceOptions.requiresCheckIn !== undefined) body.requiresCheckIn = resourceOptions.requiresCheckIn; + if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes; + if (resourceOptions.color) body.color = resourceOptions.color; + if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Resources/${resourceIdParam}`, body); + } else if (operation === 'delete') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Resources/${resourceIdParam}`); + } + } + + // SCHEDULE + else if (resource === 'schedule') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Schedules/'); + } else if (operation === 'get') { + const scheduleId = this.getNodeParameter('scheduleId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Schedules/${scheduleId}`); + } else if (operation === 'getSlots') { + const scheduleId = this.getNodeParameter('scheduleId', i) as number; + const slotsFilters = this.getNodeParameter('slotsFilters', i, {}) as any; + const qs: any = {}; + if (slotsFilters.resourceId) qs.resourceId = slotsFilters.resourceId; + if (slotsFilters.startDateTime) qs.startDateTime = new Date(slotsFilters.startDateTime).toISOString(); + if (slotsFilters.endDateTime) qs.endDateTime = new Date(slotsFilters.endDateTime).toISOString(); + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Schedules/${scheduleId}/Slots`, undefined, qs); + } + } + + // USER + else if (resource === 'user') { + if (operation === 'getAll') { + const userFilters = this.getNodeParameter('userFilters', i, {}) as any; + const qs: any = {}; + if (userFilters.username) qs.username = userFilters.username; + if (userFilters.email) qs.email = userFilters.email; + if (userFilters.firstName) qs.firstName = userFilters.firstName; + if (userFilters.lastName) qs.lastName = userFilters.lastName; + if (userFilters.organization) qs.organization = userFilters.organization; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Users/', undefined, qs); + } else if (operation === 'get') { + const userId = this.getNodeParameter('userId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Users/${userId}`); + } else if (operation === 'create') { + const emailAddress = this.getNodeParameter('emailAddress', i) as string; + const userName = this.getNodeParameter('userName', i) as string; + const pw = this.getNodeParameter('password', i) as string; + const firstName = this.getNodeParameter('firstName', i) as string; + const lastName = this.getNodeParameter('lastName', i) as string; + const userOptions = this.getNodeParameter('userOptions', i, {}) as any; + const body: any = { emailAddress, userName, password: pw, firstName, lastName }; + if (userOptions.timezone) body.timezone = userOptions.timezone; + if (userOptions.language) body.language = userOptions.language; + if (userOptions.phone) body.phone = userOptions.phone; + if (userOptions.organization) body.organization = userOptions.organization; + if (userOptions.position) body.position = userOptions.position; + if (userOptions.groups) body.groups = parseIdList(userOptions.groups); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Users/', body); + } else if (operation === 'update') { + const userId = this.getNodeParameter('userId', i) as number; + const firstName = this.getNodeParameter('firstName', i) as string; + const lastName = this.getNodeParameter('lastName', i) as string; + const userOptions = this.getNodeParameter('userOptions', i, {}) as any; + const body: any = { firstName, lastName }; + if (userOptions.timezone) body.timezone = userOptions.timezone; + if (userOptions.language) body.language = userOptions.language; + if (userOptions.phone) body.phone = userOptions.phone; + if (userOptions.organization) body.organization = userOptions.organization; + if (userOptions.position) body.position = userOptions.position; + if (userOptions.groups) body.groups = parseIdList(userOptions.groups); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}`, body); + } else if (operation === 'updatePassword') { + const userId = this.getNodeParameter('userId', i) as number; + const pw = this.getNodeParameter('password', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}/Password`, { password: pw }); + } else if (operation === 'delete') { + const userId = this.getNodeParameter('userId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Users/${userId}`); + } + } + + // ACCOUNT + else if (resource === 'account') { + if (operation === 'get') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Accounts/${accountUserId}`); + } else if (operation === 'create') { + const accountData = this.getNodeParameter('accountData', i, {}) as any; + const body: any = {}; + if (accountData.emailAddress) body.emailAddress = accountData.emailAddress; + if (accountData.userName) body.userName = accountData.userName; + if (accountData.password) body.password = accountData.password; + if (accountData.firstName) body.firstName = accountData.firstName; + if (accountData.lastName) body.lastName = accountData.lastName; + if (accountData.timezone) body.timezone = accountData.timezone; + if (accountData.language) body.language = accountData.language; + if (accountData.phone) body.phone = accountData.phone; + if (accountData.organization) body.organization = accountData.organization; + if (accountData.position) body.position = accountData.position; + if (accountData.acceptTermsOfService !== undefined) body.acceptTermsOfService = accountData.acceptTermsOfService; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Accounts/', body); + } else if (operation === 'update') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + const accountData = this.getNodeParameter('accountData', i, {}) as any; + const body: any = {}; + if (accountData.emailAddress) body.emailAddress = accountData.emailAddress; + if (accountData.userName) body.userName = accountData.userName; + if (accountData.firstName) body.firstName = accountData.firstName; + if (accountData.lastName) body.lastName = accountData.lastName; + if (accountData.timezone) body.timezone = accountData.timezone; + if (accountData.language) body.language = accountData.language; + if (accountData.phone) body.phone = accountData.phone; + if (accountData.organization) body.organization = accountData.organization; + if (accountData.position) body.position = accountData.position; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Accounts/${accountUserId}`, body); + } else if (operation === 'updatePassword') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + const passwordChange = this.getNodeParameter('passwordChange', i, {}) as any; + const passwords = passwordChange.passwords || {}; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Accounts/${accountUserId}/Password`, { + currentPassword: passwords.currentPassword, + newPassword: passwords.newPassword, + }); + } + } + + // GROUP + else if (resource === 'group') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Groups/'); + } else if (operation === 'get') { + const groupId = this.getNodeParameter('groupId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Groups/${groupId}`); + } else if (operation === 'create') { + const groupName = this.getNodeParameter('groupName', i) as string; + const isDefault = this.getNodeParameter('isDefault', i, false) as boolean; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Groups/', { name: groupName, isDefault }); + } else if (operation === 'update') { + const groupId = this.getNodeParameter('groupId', i) as number; + const groupName = this.getNodeParameter('groupName', i) as string; + const isDefault = this.getNodeParameter('isDefault', i, false) as boolean; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}`, { name: groupName, isDefault }); + } else if (operation === 'delete') { + const groupId = this.getNodeParameter('groupId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Groups/${groupId}`); + } else if (operation === 'changeRoles') { + const groupId = this.getNodeParameter('groupId', i) as number; + const roleIds = this.getNodeParameter('roleIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Roles`, { roleIds: parseIdList(roleIds) }); + } else if (operation === 'changePermissions') { + const groupId = this.getNodeParameter('groupId', i) as number; + const permissionResourceIds = this.getNodeParameter('permissionResourceIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Permissions`, { resourceIds: parseIdList(permissionResourceIds) }); + } else if (operation === 'changeUsers') { + const groupId = this.getNodeParameter('groupId', i) as number; + const groupUserIds = this.getNodeParameter('groupUserIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Users`, { userIds: parseIdList(groupUserIds) }); + } + } + + // ACCESSORY + else if (resource === 'accessory') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Accessories/'); + } else if (operation === 'get') { + const accessoryId = this.getNodeParameter('accessoryId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Accessories/${accessoryId}`); + } + } + + // ATTRIBUTE + else if (resource === 'attribute') { + if (operation === 'get') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Attributes/${attributeId}`); + } else if (operation === 'getByCategory') { + const categoryId = this.getNodeParameter('categoryId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Attributes/Category/${categoryId}`); + } else if (operation === 'create') { + const attributeLabel = this.getNodeParameter('attributeLabel', i) as string; + const attributeType = this.getNodeParameter('attributeType', i) as number; + const categoryId = this.getNodeParameter('categoryId', i) as number; + const attributeOptions = this.getNodeParameter('attributeOptions', i, {}) as any; + const body: any = { label: attributeLabel, type: attributeType, categoryId }; + if (attributeOptions.required !== undefined) body.required = attributeOptions.required; + if (attributeOptions.adminOnly !== undefined) body.adminOnly = attributeOptions.adminOnly; + if (attributeOptions.isPrivate !== undefined) body.isPrivate = attributeOptions.isPrivate; + if (attributeOptions.sortOrder !== undefined) body.sortOrder = attributeOptions.sortOrder; + if (attributeOptions.regex) body.regex = attributeOptions.regex; + if (attributeOptions.possibleValues) body.possibleValues = attributeOptions.possibleValues.split(',').map((v: string) => v.trim()); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Attributes/', body); + } else if (operation === 'update') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + const attributeLabel = this.getNodeParameter('attributeLabel', i) as string; + const attributeType = this.getNodeParameter('attributeType', i) as number; + const attributeOptions = this.getNodeParameter('attributeOptions', i, {}) as any; + const body: any = { label: attributeLabel, type: attributeType }; + if (attributeOptions.required !== undefined) body.required = attributeOptions.required; + if (attributeOptions.adminOnly !== undefined) body.adminOnly = attributeOptions.adminOnly; + if (attributeOptions.isPrivate !== undefined) body.isPrivate = attributeOptions.isPrivate; + if (attributeOptions.sortOrder !== undefined) body.sortOrder = attributeOptions.sortOrder; + if (attributeOptions.regex) body.regex = attributeOptions.regex; + if (attributeOptions.possibleValues) body.possibleValues = attributeOptions.possibleValues.split(',').map((v: string) => v.trim()); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Attributes/${attributeId}`, body); + } else if (operation === 'delete') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Attributes/${attributeId}`); + } + } + + // Process response + if (responseData) { + if (Array.isArray(responseData)) { + returnData.push(...responseData.map(item => ({ json: item }))); + } else if (responseData.reservations) { + returnData.push(...responseData.reservations.map((item: any) => ({ json: item }))); + } else if (responseData.resources) { + returnData.push(...responseData.resources.map((item: any) => ({ json: item }))); + } else if (responseData.schedules) { + returnData.push(...responseData.schedules.map((item: any) => ({ json: item }))); + } else if (responseData.users) { + returnData.push(...responseData.users.map((item: any) => ({ json: item }))); + } else if (responseData.groups) { + returnData.push(...responseData.groups.map((item: any) => ({ json: item }))); + } else if (responseData.accessories) { + returnData.push(...responseData.accessories.map((item: any) => ({ json: item }))); + } else if (responseData.attributes) { + returnData.push(...responseData.attributes.map((item: any) => ({ json: item }))); + } else { + returnData.push({ json: responseData }); + } + } + + } catch (error: any) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw error; + } + } + } finally { + await signOut(this, baseUrl, session); + } + + return [returnData]; + } +} diff --git a/custom-nodes/nodes/LibreBooking/librebooking.svg b/custom-nodes/nodes/LibreBooking/librebooking.svg new file mode 100644 index 0000000..81306a9 --- /dev/null +++ b/custom-nodes/nodes/LibreBooking/librebooking.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-nodes/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts b/custom-nodes/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts new file mode 100644 index 0000000..5e2e9ae --- /dev/null +++ b/custom-nodes/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts @@ -0,0 +1,352 @@ +import { + INodeType, + INodeTypeDescription, + IPollFunctions, + INodeExecutionData, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +interface ReservationData { + referenceNumber: string; + startDate: string; + endDate: string; + title: string; + resourceId: number; + userId: number; + [key: string]: any; +} + +/** + * Authentifizierung bei LibreBooking + */ +async function authenticateTrigger( + pollFunctions: IPollFunctions, + baseUrl: string, + username: string, + password: string, +): Promise { + try { + const response = await pollFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`, + headers: { 'Content-Type': 'application/json' }, + body: { username, password }, + json: true, + }); + + if (!response.isAuthenticated) { + throw new NodeOperationError( + pollFunctions.getNode(), + 'Authentifizierung fehlgeschlagen', + ); + } + + return { + sessionToken: response.sessionToken, + userId: response.userId, + sessionExpires: response.sessionExpires, + }; + } catch (error: any) { + throw new NodeApiError(pollFunctions.getNode(), error, { + message: 'Authentifizierung fehlgeschlagen', + }); + } +} + +/** + * Abmeldung + */ +async function signOutTrigger( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, +): Promise { + try { + await pollFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`, + headers: { 'Content-Type': 'application/json' }, + body: { + userId: session.userId, + sessionToken: session.sessionToken, + }, + json: true, + }); + } catch (error) { + // Ignoriere SignOut-Fehler + } +} + +/** + * Reservierungen abrufen + */ +async function getReservations( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, + startDateTime: string, + endDateTime: string, + filters: any, +): Promise { + const qs: any = { + startDateTime, + endDateTime, + }; + + if (filters.resourceId) qs.resourceId = filters.resourceId; + if (filters.scheduleId) qs.scheduleId = filters.scheduleId; + if (filters.userId) qs.userId = filters.userId; + + const response = await pollFunctions.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/Web/Services/index.php/Reservations/`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + qs, + json: true, + }); + + return response.reservations || []; +} + +/** + * Detaillierte Reservierungsdaten abrufen + */ +async function getReservationDetails( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, + referenceNumber: string, +): Promise { + const response = await pollFunctions.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + json: true, + }); + + return response; +} + +/** + * Zeitfenster berechnen + */ +function getTimeWindow(timeWindow: string): { start: string; end: string } { + const now = new Date(); + const start = now.toISOString(); + + let endDate = new Date(now); + switch (timeWindow) { + case '7days': + endDate.setDate(endDate.getDate() + 7); + break; + case '14days': + endDate.setDate(endDate.getDate() + 14); + break; + case '30days': + endDate.setDate(endDate.getDate() + 30); + break; + case '90days': + endDate.setDate(endDate.getDate() + 90); + break; + default: + endDate.setDate(endDate.getDate() + 14); + } + + return { + start, + end: endDate.toISOString(), + }; +} + +/** + * Eindeutigen Schlüssel für Reservierung generieren + */ +function getReservationKey(reservation: ReservationData): string { + return `${reservation.referenceNumber}_${reservation.startDate}_${reservation.endDate}_${reservation.title || ''}`; +} + +/** + * LibreBooking Trigger Node + */ +export class LibreBookingTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'LibreBooking Trigger', + name: 'libreBookingTrigger', + icon: 'file:librebooking.svg', + group: ['trigger'], + version: 1, + description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst', + subtitle: '={{$parameter["event"]}}', + defaults: { + name: 'LibreBooking Trigger', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'libreBookingApi', + required: true, + }, + ], + polling: true, + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { name: 'Neue Reservierung', value: 'newReservation', description: 'Wird bei neuen Reservierungen ausgelöst' }, + { name: 'Geänderte Reservierung', value: 'updatedReservation', description: 'Wird bei geänderten Reservierungen ausgelöst' }, + { name: 'Alle Reservierungen', value: 'allReservations', description: 'Wird bei neuen und geänderten Reservierungen ausgelöst' }, + ], + default: 'newReservation', + }, + { + displayName: 'Filter', + name: 'filters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + options: [ + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' }, + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + ], + }, + { + displayName: 'Zeitfenster', + name: 'timeWindow', + type: 'options', + options: [ + { name: 'Nächste 7 Tage', value: '7days' }, + { name: 'Nächste 14 Tage', value: '14days' }, + { name: 'Nächste 30 Tage', value: '30days' }, + { name: 'Nächste 90 Tage', value: '90days' }, + ], + default: '14days', + }, + { + displayName: 'Optionen', + name: 'options', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + options: [ + { displayName: 'Detaillierte Daten Abrufen', name: 'fetchDetails', type: 'boolean', default: false }, + ], + }, + ], + }; + + async poll(this: IPollFunctions): Promise { + const credentials = await this.getCredentials('libreBookingApi'); + const baseUrl = (credentials.url as string).replace(/\/$/, ''); + const username = credentials.username as string; + const password = credentials.password as string; + + const event = this.getNodeParameter('event') as string; + const filters = this.getNodeParameter('filters', {}) as any; + const timeWindow = this.getNodeParameter('timeWindow', '14days') as string; + const options = this.getNodeParameter('options', {}) as any; + + const workflowStaticData = this.getWorkflowStaticData('node'); + const previousReservations = (workflowStaticData.reservations as Record) || {}; + + let session: LibreBookingSession; + try { + session = await authenticateTrigger(this, baseUrl, username, password); + } catch (error) { + throw error; + } + + try { + const { start, end } = getTimeWindow(timeWindow); + + const reservations = await getReservations( + this, + baseUrl, + session, + start, + end, + filters, + ); + + const returnData: INodeExecutionData[] = []; + const currentReservations: Record = {}; + + for (const reservation of reservations) { + const refNumber = reservation.referenceNumber; + const reservationKey = getReservationKey(reservation); + currentReservations[refNumber] = reservationKey; + + const isNew = !previousReservations[refNumber]; + const isUpdated = previousReservations[refNumber] && previousReservations[refNumber] !== reservationKey; + + let shouldTrigger = false; + let eventType = ''; + + if (event === 'newReservation' && isNew) { + shouldTrigger = true; + eventType = 'new'; + } else if (event === 'updatedReservation' && isUpdated) { + shouldTrigger = true; + eventType = 'updated'; + } else if (event === 'allReservations' && (isNew || isUpdated)) { + shouldTrigger = true; + eventType = isNew ? 'new' : 'updated'; + } + + if (shouldTrigger) { + let reservationData = reservation; + + if (options.fetchDetails) { + try { + reservationData = await getReservationDetails( + this, + baseUrl, + session, + refNumber, + ); + } catch (error) { + reservationData = reservation; + } + } + + returnData.push({ + json: { + ...reservationData, + _eventType: eventType, + _triggeredAt: new Date().toISOString(), + }, + }); + } + } + + workflowStaticData.reservations = currentReservations; + + if (returnData.length === 0) { + return null; + } + + return [returnData]; + + } finally { + await signOutTrigger(this, baseUrl, session); + } + } +} diff --git a/custom-nodes/nodes/LibreBookingTrigger/librebooking.svg b/custom-nodes/nodes/LibreBookingTrigger/librebooking.svg new file mode 100644 index 0000000..81306a9 --- /dev/null +++ b/custom-nodes/nodes/LibreBookingTrigger/librebooking.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/custom-nodes/package.json b/custom-nodes/package.json new file mode 100644 index 0000000..17c52f3 --- /dev/null +++ b/custom-nodes/package.json @@ -0,0 +1,35 @@ +{ + "name": "n8n-nodes-librebooking", + "version": "1.0.0", + "description": "LibreBooking n8n Community Node - Custom Nodes Version für Docker Integration", + "keywords": [ + "n8n-community-node-package", + "librebooking", + "booking", + "reservation" + ], + "main": "dist/index.js", + "scripts": { + "build": "tsc && npm run copy-icons", + "copy-icons": "mkdir -p dist/nodes/LibreBooking dist/nodes/LibreBookingTrigger && cp nodes/LibreBooking/*.svg dist/nodes/LibreBooking/ && cp nodes/LibreBookingTrigger/*.svg dist/nodes/LibreBookingTrigger/", + "clean": "rm -rf dist", + "rebuild": "npm run clean && npm run build" + }, + "n8n": { + "n8nNodesApiVersion": 1, + "credentials": [ + "dist/credentials/LibreBookingApi.credentials.js" + ], + "nodes": [ + "dist/nodes/LibreBooking/LibreBooking.node.js", + "dist/nodes/LibreBookingTrigger/LibreBookingTrigger.node.js" + ] + }, + "devDependencies": { + "typescript": "^5.3.0" + }, + "peerDependencies": { + "n8n-workflow": ">=1.0.0" + }, + "license": "MIT" +} diff --git a/custom-nodes/tsconfig.json b/custom-nodes/tsconfig.json new file mode 100644 index 0000000..88c33d7 --- /dev/null +++ b/custom-nodes/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["ES2019", "ES2020.Promise"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": [ + "nodes/**/*.ts", + "credentials/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "test" + ] +} diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..10e4bb4 --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,131 @@ +# Vollständige Docker Compose Beispiel-Konfiguration +# n8n mit LibreBooking Node - Ready to Use +# +# Verwendung: +# 1. cp docker-compose.example.yml docker-compose.yml +# 2. cp .env.docker .env +# 3. .env Datei anpassen +# 4. docker-compose up -d + +version: '3.8' + +services: + n8n: + # Verwende das vorgefertigte Image mit LibreBooking Node + build: + context: . + dockerfile: Dockerfile.custom-nodes + args: + N8N_VERSION: latest + + # Oder nutze das offizielle Image mit Volume-Mount: + # image: n8nio/n8n:latest + + container_name: n8n-librebooking + restart: unless-stopped + + ports: + - "${N8N_PORT:-5678}:5678" + + environment: + # Basis-Konfiguration + - N8N_HOST=${N8N_HOST:-localhost} + - N8N_PORT=5678 + - N8N_PROTOCOL=${N8N_PROTOCOL:-http} + - WEBHOOK_URL=${WEBHOOK_URL:-http://localhost:5678/} + + # Authentifizierung (aktivieren für Produktion!) + - N8N_BASIC_AUTH_ACTIVE=${N8N_BASIC_AUTH_ACTIVE:-false} + - N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER:-admin} + - N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD:-changeme} + + # Custom Nodes + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true + + # Zeitzone + - GENERIC_TIMEZONE=${TZ:-Europe/Berlin} + - TZ=${TZ:-Europe/Berlin} + + # Logging + - N8N_LOG_LEVEL=${N8N_LOG_LEVEL:-info} + + # Execution Settings + - EXECUTIONS_DATA_SAVE_ON_ERROR=all + - EXECUTIONS_DATA_SAVE_ON_SUCCESS=all + - EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true + + volumes: + # Persistente n8n Daten + - n8n_data:/home/node/.n8n + + # Custom Nodes (wenn nicht im Image gebaut) + # - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:5678/healthz"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + networks: + - n8n_network + + # Optional: PostgreSQL Datenbank für n8n + # Für Produktion empfohlen statt SQLite + postgres: + image: postgres:15-alpine + container_name: n8n-postgres + restart: unless-stopped + profiles: + - with-postgres + + environment: + - POSTGRES_USER=${POSTGRES_USER:-n8n} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-n8n_password} + - POSTGRES_DB=${POSTGRES_DB:-n8n} + + volumes: + - postgres_data:/var/lib/postgresql/data + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-n8n}"] + interval: 10s + timeout: 5s + retries: 5 + + networks: + - n8n_network + + # Optional: Redis für Queue Mode + redis: + image: redis:7-alpine + container_name: n8n-redis + restart: unless-stopped + profiles: + - with-redis + + volumes: + - redis_data:/data + + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + networks: + - n8n_network + +volumes: + n8n_data: + driver: local + postgres_data: + driver: local + redis_data: + driver: local + +networks: + n8n_network: + driver: bridge diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..1c915ec --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,31 @@ +# Docker Compose Override für bestehende n8n Installationen +# Diese Datei erweitert eine bestehende docker-compose.yml um den LibreBooking Node +# +# Verwendung: +# 1. Diese Datei in das Verzeichnis mit der bestehenden docker-compose.yml kopieren +# 2. custom-nodes Verzeichnis in das gleiche Verzeichnis kopieren +# 3. docker-compose up -d ausführen +# +# HINWEIS: Diese Datei wird automatisch mit der bestehenden docker-compose.yml zusammengeführt + +version: '3.8' + +services: + n8n: + # Zusätzliche Volumes für Custom Nodes + volumes: + # LibreBooking Custom Node - Variante 1: Vorgebauter Node + - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + + # Alternative: Nur das dist-Verzeichnis (wenn bereits gebaut) + # - ./custom-nodes/dist:/home/node/.n8n/custom/n8n-nodes-librebooking/dist:ro + # - ./custom-nodes/package.json:/home/node/.n8n/custom/n8n-nodes-librebooking/package.json:ro + + # Zusätzliche Umgebungsvariablen + environment: + # Pfad für Custom Nodes (normalerweise bereits gesetzt) + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + # Aktiviert erweiterte Node-Typen + - N8N_COMMUNITY_NODES_ENABLED=true + # Optional: Debug-Logging für Node-Entwicklung + # - N8N_LOG_LEVEL=debug diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a37c5b8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,65 @@ +# Docker Compose für n8n mit LibreBooking Node +# +# Verwendung: +# docker-compose up -d # Im Hintergrund starten +# docker-compose logs -f # Logs anzeigen +# docker-compose down # Stoppen und entfernen +# docker-compose build --no-cache # Neu bauen + +version: '3.8' + +services: + n8n: + build: + context: . + dockerfile: Dockerfile + container_name: n8n-librebooking + restart: unless-stopped + ports: + - "5678:5678" + environment: + # Basis-Konfiguration + - N8N_BASIC_AUTH_ACTIVE=true + - N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER:-admin} + - N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD:-changeme} + + # Webhook-URL (für Produktionsumgebung anpassen) + - WEBHOOK_URL=${WEBHOOK_URL:-http://localhost:5678/} + + # Timezone + - GENERIC_TIMEZONE=${TZ:-Europe/Berlin} + - TZ=${TZ:-Europe/Berlin} + + # Custom Extensions + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + + # Optional: Logging + - N8N_LOG_LEVEL=${N8N_LOG_LEVEL:-info} + + # Optional: Executions + - EXECUTIONS_DATA_SAVE_ON_ERROR=all + - EXECUTIONS_DATA_SAVE_ON_SUCCESS=all + - EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true + volumes: + # Persistente Daten + - n8n_data:/home/node/.n8n + # Workflow-Dateien (optional) + - ./workflows:/home/node/workflows:ro + networks: + - n8n-network + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:5678/healthz"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + +volumes: + n8n_data: + driver: local + name: n8n-librebooking-data + +networks: + n8n-network: + driver: bridge + name: n8n-librebooking-network diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..c94c56e --- /dev/null +++ b/index.ts @@ -0,0 +1,6 @@ +// n8n-nodes-librebooking +// Entry point for the LibreBooking n8n node package + +export * from './nodes/LibreBooking/LibreBooking.node'; +export * from './nodes/LibreBookingTrigger/LibreBookingTrigger.node'; +export * from './credentials/LibreBookingApi.credentials'; diff --git a/install-docker.sh b/install-docker.sh new file mode 100755 index 0000000..705fe6c --- /dev/null +++ b/install-docker.sh @@ -0,0 +1,349 @@ +#!/bin/bash + +# ============================================================================= +# LibreBooking n8n Node - Docker Integration Script +# ============================================================================= +# Automatische Integration des LibreBooking Nodes in bestehende n8n Docker-Installationen +# +# Verwendung: ./install-docker.sh [OPTIONS] +# +# Optionen: +# -p, --path PATH Pfad zur n8n Docker-Installation (Standard: aktuelles Verzeichnis) +# -b, --build Node im Container bauen statt vorgebaut kopieren +# -f, --force Bestehende Installation überschreiben +# -h, --help Diese Hilfe anzeigen +# +# ============================================================================= + +set -e + +# Farben für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Standardwerte +N8N_PATH="." +FORCE=false +BUILD_IN_CONTAINER=false +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Hilfsfunktionen +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[✓]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[!]${NC} $1" +} + +print_error() { + echo -e "${RED}[FEHLER]${NC} $1" +} + +show_help() { + echo "LibreBooking n8n Node - Docker Integration Script" + echo "" + echo "Verwendung: $0 [OPTIONS]" + echo "" + echo "Optionen:" + echo " -p, --path PATH Pfad zur n8n Docker-Installation" + echo " -b, --build Node im Container bauen" + echo " -f, --force Bestehende Installation überschreiben" + echo " -h, --help Diese Hilfe anzeigen" + echo "" + echo "Beispiele:" + echo " $0 # Installation im aktuellen Verzeichnis" + echo " $0 -p /opt/n8n # Installation in /opt/n8n" + echo " $0 -f -p /home/user/n8n # Installation mit Überschreiben" + exit 0 +} + +# Argumente parsen +while [[ $# -gt 0 ]]; do + case $1 in + -p|--path) + N8N_PATH="$2" + shift 2 + ;; + -b|--build) + BUILD_IN_CONTAINER=true + shift + ;; + -f|--force) + FORCE=true + shift + ;; + -h|--help) + show_help + ;; + *) + print_error "Unbekannte Option: $1" + show_help + ;; + esac +done + +echo "" +echo "=========================================" +echo " LibreBooking n8n Node - Docker Setup" +echo "=========================================" +echo "" + +# ============================================================================= +# Voraussetzungen prüfen +# ============================================================================= + +print_info "Prüfe Voraussetzungen..." + +# Docker prüfen +if ! command -v docker &> /dev/null; then + print_error "Docker ist nicht installiert!" + echo " Bitte installieren Sie Docker: https://docs.docker.com/get-docker/" + exit 1 +fi +print_success "Docker gefunden: $(docker --version)" + +# Docker Compose prüfen +if command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" + print_success "docker-compose gefunden: $(docker-compose --version)" +elif docker compose version &> /dev/null; then + COMPOSE_CMD="docker compose" + print_success "docker compose gefunden: $(docker compose version)" +else + print_error "Docker Compose ist nicht installiert!" + echo " Bitte installieren Sie Docker Compose" + exit 1 +fi + +# Zielpfad prüfen +if [ ! -d "$N8N_PATH" ]; then + print_error "Verzeichnis existiert nicht: $N8N_PATH" + exit 1 +fi + +N8N_PATH=$(cd "$N8N_PATH" && pwd) +print_info "Zielverzeichnis: $N8N_PATH" + +# docker-compose.yml prüfen +if [ ! -f "$N8N_PATH/docker-compose.yml" ] && [ ! -f "$N8N_PATH/docker-compose.yaml" ]; then + print_warning "Keine docker-compose.yml gefunden in $N8N_PATH" + echo "" + read -p "Soll eine Beispiel-Konfiguration erstellt werden? (j/n): " CREATE_EXAMPLE + if [[ "$CREATE_EXAMPLE" =~ ^[jJyY]$ ]]; then + print_info "Kopiere Beispiel-Konfiguration..." + cp "$SCRIPT_DIR/docker-compose.example.yml" "$N8N_PATH/docker-compose.yml" + cp "$SCRIPT_DIR/.env.docker" "$N8N_PATH/.env" + print_success "Beispiel-Konfiguration erstellt" + print_warning "Bitte $N8N_PATH/.env anpassen!" + else + print_error "Abbruch: Keine docker-compose.yml vorhanden" + exit 1 + fi +fi + +# ============================================================================= +# n8n Status prüfen +# ============================================================================= + +print_info "Prüfe n8n Container Status..." + +cd "$N8N_PATH" + +N8N_RUNNING=false +if $COMPOSE_CMD ps 2>/dev/null | grep -q "n8n.*Up"; then + N8N_RUNNING=true + print_success "n8n Container läuft" +else + print_warning "n8n Container läuft nicht (wird später gestartet)" +fi + +# ============================================================================= +# Custom Nodes Verzeichnis vorbereiten +# ============================================================================= + +print_info "Bereite Custom Nodes Verzeichnis vor..." + +CUSTOM_NODES_DIR="$N8N_PATH/custom-nodes" + +# Prüfen ob bereits installiert +if [ -d "$CUSTOM_NODES_DIR" ]; then + if [ "$FORCE" = true ]; then + print_warning "Bestehendes custom-nodes Verzeichnis wird überschrieben" + rm -rf "$CUSTOM_NODES_DIR" + else + print_warning "custom-nodes Verzeichnis existiert bereits" + read -p "Überschreiben? (j/n): " OVERWRITE + if [[ "$OVERWRITE" =~ ^[jJyY]$ ]]; then + rm -rf "$CUSTOM_NODES_DIR" + else + print_info "Behalte bestehendes Verzeichnis" + fi + fi +fi + +# Custom Nodes kopieren +if [ ! -d "$CUSTOM_NODES_DIR" ]; then + cp -r "$SCRIPT_DIR/custom-nodes" "$CUSTOM_NODES_DIR" + print_success "Custom Nodes kopiert nach: $CUSTOM_NODES_DIR" +fi + +# ============================================================================= +# Node bauen (wenn nicht bereits gebaut) +# ============================================================================= + +if [ ! -d "$CUSTOM_NODES_DIR/dist" ] || [ "$BUILD_IN_CONTAINER" = true ]; then + print_info "Baue LibreBooking Node..." + + cd "$CUSTOM_NODES_DIR" + + # Prüfen ob node/npm vorhanden + if command -v npm &> /dev/null; then + npm install 2>/dev/null || print_warning "npm install hatte Warnungen" + npm run build 2>/dev/null || { + print_warning "Build fehlgeschlagen, versuche alternativen Ansatz..." + # Manueller Build + npx tsc 2>/dev/null || true + mkdir -p dist/nodes/LibreBooking dist/nodes/LibreBookingTrigger + cp nodes/LibreBooking/*.svg dist/nodes/LibreBooking/ 2>/dev/null || true + cp nodes/LibreBookingTrigger/*.svg dist/nodes/LibreBookingTrigger/ 2>/dev/null || true + } + print_success "Node erfolgreich gebaut" + else + print_warning "npm nicht gefunden - Node wird beim Container-Start gebaut" + print_info "Erstelle Build-Skript für Container..." + cat > "$CUSTOM_NODES_DIR/build.sh" << 'BUILDEOF' +#!/bin/sh +cd /home/node/.n8n/custom/n8n-nodes-librebooking +npm install +npm run build +BUILDEOF + chmod +x "$CUSTOM_NODES_DIR/build.sh" + fi + + cd "$N8N_PATH" +fi + +# ============================================================================= +# Docker Compose Override erstellen/aktualisieren +# ============================================================================= + +print_info "Erstelle docker-compose.override.yml..." + +OVERRIDE_FILE="$N8N_PATH/docker-compose.override.yml" + +if [ -f "$OVERRIDE_FILE" ]; then + print_warning "docker-compose.override.yml existiert bereits" + + # Prüfen ob LibreBooking bereits konfiguriert + if grep -q "n8n-nodes-librebooking" "$OVERRIDE_FILE"; then + print_success "LibreBooking bereits in override.yml konfiguriert" + else + print_info "Füge LibreBooking Konfiguration hinzu..." + # Backup erstellen + cp "$OVERRIDE_FILE" "${OVERRIDE_FILE}.backup" + + # Hinzufügen (vereinfacht - für komplexe Fälle manuell anpassen) + cat >> "$OVERRIDE_FILE" << 'OVERRIDEEOF' + +# LibreBooking Node (automatisch hinzugefügt) +# Falls Konflikte auftreten, bitte manuell anpassen +# Siehe DOCKER-INTEGRATION.md für Details +OVERRIDEEOF + print_warning "Bitte $OVERRIDE_FILE manuell prüfen!" + fi +else + # Neue Override-Datei erstellen + cat > "$OVERRIDE_FILE" << 'OVERRIDEEOF' +# Docker Compose Override für LibreBooking n8n Node +# Automatisch generiert von install-docker.sh + +version: '3.8' + +services: + n8n: + volumes: + # LibreBooking Custom Node + - ./custom-nodes:/home/node/.n8n/custom/n8n-nodes-librebooking:ro + + environment: + # Custom Nodes aktivieren + - N8N_CUSTOM_EXTENSIONS=/home/node/.n8n/custom + - N8N_COMMUNITY_NODES_ENABLED=true +OVERRIDEEOF + print_success "docker-compose.override.yml erstellt" +fi + +# ============================================================================= +# Berechtigungen setzen +# ============================================================================= + +print_info "Setze Berechtigungen..." + +# n8n läuft oft als node User mit UID 1000 +if [ "$(id -u)" = "0" ]; then + chown -R 1000:1000 "$CUSTOM_NODES_DIR" 2>/dev/null || print_warning "Konnte Berechtigungen nicht setzen" +else + # Versuche mit sudo falls verfügbar + if command -v sudo &> /dev/null; then + sudo chown -R 1000:1000 "$CUSTOM_NODES_DIR" 2>/dev/null || print_warning "Konnte Berechtigungen nicht setzen (sudo fehlgeschlagen)" + fi +fi + +print_success "Berechtigungen konfiguriert" + +# ============================================================================= +# Container neustarten +# ============================================================================= + +echo "" +read -p "Soll der n8n Container jetzt neu gestartet werden? (j/n): " RESTART + +if [[ "$RESTART" =~ ^[jJyY]$ ]]; then + print_info "Starte n8n Container neu..." + + if [ "$N8N_RUNNING" = true ]; then + $COMPOSE_CMD restart n8n 2>/dev/null || $COMPOSE_CMD restart + else + $COMPOSE_CMD up -d n8n 2>/dev/null || $COMPOSE_CMD up -d + fi + + # Warten bis Container bereit + print_info "Warte auf Container-Start..." + sleep 5 + + # Status prüfen + if $COMPOSE_CMD ps | grep -q "n8n.*Up"; then + print_success "n8n Container läuft!" + else + print_warning "Container-Status unklar - bitte manuell prüfen" + fi +else + print_info "Container nicht neu gestartet" + echo " Führen Sie später aus: cd $N8N_PATH && $COMPOSE_CMD restart" +fi + +# ============================================================================= +# Abschluss +# ============================================================================= + +echo "" +echo "=========================================" +print_success "Installation abgeschlossen!" +echo "=========================================" +echo "" +echo "Nächste Schritte:" +echo "1. Öffnen Sie n8n in Ihrem Browser" +echo "2. Gehen Sie zu: Settings > Community Nodes" +echo "3. Der 'LibreBooking' Node sollte sichtbar sein" +echo "4. Erstellen Sie neue Credentials für LibreBooking" +echo "" +echo "Bei Problemen siehe: DOCKER-INTEGRATION.md" +echo "" diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..3fc8ca4 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,250 @@ +# +# LibreBooking n8n Node - Installations-Skript für Windows (PowerShell) +# +# Verwendung: +# .\install.ps1 +# +# Optionen: +# -NoLink Überspringt npm link (nur Build) +# -Global Installiert global statt npm link +# -Help Zeigt diese Hilfe an +# + +param( + [switch]$NoLink, + [switch]$Global, + [switch]$Help +) + +# Konfiguration +$MIN_NODE_VERSION = 18 +$ErrorActionPreference = "Stop" + +# Funktionen +function Write-ColorOutput { + param( + [string]$Message, + [string]$Color = "White" + ) + Write-Host $Message -ForegroundColor $Color +} + +function Write-Header { + Write-ColorOutput "" "Blue" + Write-ColorOutput "=============================================" "Blue" + Write-ColorOutput " LibreBooking n8n Node Installer" "Blue" + Write-ColorOutput "=============================================" "Blue" + Write-ColorOutput "" "Blue" +} + +function Write-Success { + param([string]$Message) + Write-ColorOutput "✓ $Message" "Green" +} + +function Write-Warning-Msg { + param([string]$Message) + Write-ColorOutput "⚠ $Message" "Yellow" +} + +function Write-Error-Msg { + param([string]$Message) + Write-ColorOutput "✗ $Message" "Red" +} + +function Write-Info { + param([string]$Message) + Write-ColorOutput "ℹ $Message" "Cyan" +} + +function Show-Help { + Write-Host "Verwendung: .\install.ps1 [OPTIONEN]" + Write-Host "" + Write-Host "Optionen:" + Write-Host " -NoLink Überspringt npm link (nur Build)" + Write-Host " -Global Installiert global mit npm install -g" + Write-Host " -Help Zeigt diese Hilfe an" + Write-Host "" + Write-Host "Beispiele:" + Write-Host " .\install.ps1 # Standard-Installation mit npm link" + Write-Host " .\install.ps1 -NoLink # Nur Dependencies installieren und Build" + Write-Host " .\install.ps1 -Global # Globale Installation" + exit 0 +} + +function Test-Command { + param([string]$Command) + try { + $null = Get-Command $Command -ErrorAction Stop + return $true + } + catch { + return $false + } +} + +function Get-NodeVersion { + $version = (node -v) -replace 'v', '' + return [int]($version.Split('.')[0]) +} + +# Hilfe anzeigen +if ($Help) { + Show-Help +} + +# Start +Write-Header + +# 1. Node.js prüfen +Write-Host "" +Write-Info "Prüfe Voraussetzungen..." +Write-Host "" + +if (-not (Test-Command "node")) { + Write-Error-Msg "Node.js ist nicht installiert!" + Write-Host " Bitte installiere Node.js v$MIN_NODE_VERSION oder höher:" + Write-Host " https://nodejs.org/" + exit 1 +} + +$nodeVersion = Get-NodeVersion +if ($nodeVersion -lt $MIN_NODE_VERSION) { + Write-Error-Msg "Node.js Version $nodeVersion ist zu alt!" + Write-Host " Mindestens Version $MIN_NODE_VERSION benötigt." + exit 1 +} + +$nodeFullVersion = (node -v) -replace 'v', '' +Write-Success "Node.js v$nodeFullVersion gefunden" + +# 2. npm prüfen +if (-not (Test-Command "npm")) { + Write-Error-Msg "npm ist nicht installiert!" + exit 1 +} + +$npmVersion = npm -v +Write-Success "npm v$npmVersion gefunden" + +# 3. n8n prüfen (optional) +if (Test-Command "n8n") { + try { + $n8nVersion = n8n --version 2>$null + Write-Success "n8n $n8nVersion gefunden" + } + catch { + Write-Success "n8n installiert" + } +} +else { + Write-Warning-Msg "n8n ist nicht global installiert." + Write-Host " Für npm link muss n8n global installiert sein:" + Write-Host " npm install -g n8n" + Write-Host "" + + if (-not $NoLink -and -not $Global) { + $response = Read-Host "Möchtest du trotzdem fortfahren? (j/N)" + if ($response -notmatch '^[Jj]$') { + exit 1 + } + } +} + +# 4. Dependencies installieren +Write-Host "" +Write-Info "Installiere Dependencies..." + +try { + npm install + Write-Success "Dependencies installiert" +} +catch { + Write-Error-Msg "Fehler bei npm install: $_" + exit 1 +} + +# 5. TypeScript kompilieren +Write-Host "" +Write-Info "Kompiliere TypeScript..." + +try { + npm run build + Write-Success "Build erfolgreich" +} +catch { + Write-Error-Msg "Fehler beim Build: $_" + exit 1 +} + +# 6. npm link oder global install +if ($Global) { + Write-Host "" + Write-Info "Installiere global..." + + try { + npm install -g . + Write-Success "Global installiert" + } + catch { + Write-Error-Msg "Fehler bei globaler Installation: $_" + exit 1 + } +} +elseif (-not $NoLink) { + Write-Host "" + Write-Info "Verlinke mit npm link..." + + try { + npm link + Write-Success "npm link erfolgreich" + + # Versuche mit n8n zu verlinken + if (Test-Command "n8n") { + $n8nPath = Join-Path (npm root -g) "n8n" + if (Test-Path $n8nPath) { + Write-Info "Verlinke mit n8n..." + Push-Location $n8nPath + try { + npm link n8n-nodes-librebooking 2>$null + Write-Success "Mit n8n verlinkt" + } + catch { + Write-Warning-Msg "Konnte nicht automatisch mit n8n verlinken" + } + finally { + Pop-Location + } + } + } + } + catch { + Write-Error-Msg "Fehler bei npm link: $_" + exit 1 + } +} + +# Abschluss +Write-Host "" +Write-ColorOutput "=============================================" "Green" +Write-ColorOutput " Installation erfolgreich abgeschlossen!" "Green" +Write-ColorOutput "=============================================" "Green" +Write-Host "" +Write-Host "Nächste Schritte:" +Write-Host "" + +if ($NoLink) { + Write-Host " 1. Führe 'npm link' aus, um den Node zu verlinken" + Write-Host " 2. Starte n8n neu: n8n start" +} +else { + Write-Host " 1. Starte n8n neu: n8n start" + Write-Host " (oder mit Docker: docker-compose restart)" +} + +Write-Host "" +Write-Host " 2. Öffne n8n im Browser: http://localhost:5678" +Write-Host " 3. Der LibreBooking Node sollte verfügbar sein" +Write-Host "" +Write-Host "Bei Problemen siehe INSTALLATION.md oder README.md" +Write-Host "" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..69b459c --- /dev/null +++ b/install.sh @@ -0,0 +1,209 @@ +#!/bin/bash +# +# LibreBooking n8n Node - Installations-Skript für Linux/Mac +# +# Verwendung: +# chmod +x install.sh +# ./install.sh +# +# Optionen: +# --no-link Überspringt npm link (nur Build) +# --global Installiert global statt npm link +# --help Zeigt diese Hilfe an +# + +set -e + +# Farben für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Konfiguration +MIN_NODE_VERSION=18 +REQUIRED_NPM_VERSION=8 + +# Optionen +SKIP_LINK=false +GLOBAL_INSTALL=false + +# Funktionen +print_header() { + echo -e "${BLUE}" + echo "=============================================" + echo " LibreBooking n8n Node Installer" + echo "=============================================" + echo -e "${NC}" +} + +print_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠ $1${NC}" +} + +print_error() { + echo -e "${RED}✗ $1${NC}" +} + +print_info() { + echo -e "${BLUE}ℹ $1${NC}" +} + +show_help() { + echo "Verwendung: ./install.sh [OPTIONEN]" + echo "" + echo "Optionen:" + echo " --no-link Überspringt npm link (nur Build)" + echo " --global Installiert global mit npm install -g" + echo " --help Zeigt diese Hilfe an" + echo "" + echo "Beispiele:" + echo " ./install.sh # Standard-Installation mit npm link" + echo " ./install.sh --no-link # Nur Dependencies installieren und Build" + echo " ./install.sh --global # Globale Installation" + exit 0 +} + +check_command() { + if command -v $1 &> /dev/null; then + return 0 + else + return 1 + fi +} + +get_node_version() { + node -v | sed 's/v//' | cut -d. -f1 +} + +get_npm_version() { + npm -v | cut -d. -f1 +} + +# Parameter verarbeiten +for arg in "$@"; do + case $arg in + --no-link) + SKIP_LINK=true + ;; + --global) + GLOBAL_INSTALL=true + ;; + --help|-h) + show_help + ;; + *) + print_error "Unbekannte Option: $arg" + show_help + ;; + esac +done + +# Start +print_header + +# 1. Node.js prüfen +echo "" +print_info "Prüfe Voraussetzungen..." +echo "" + +if ! check_command node; then + print_error "Node.js ist nicht installiert!" + echo " Bitte installiere Node.js v${MIN_NODE_VERSION} oder höher:" + echo " https://nodejs.org/" + exit 1 +fi + +NODE_VERSION=$(get_node_version) +if [ "$NODE_VERSION" -lt "$MIN_NODE_VERSION" ]; then + print_error "Node.js Version $NODE_VERSION ist zu alt!" + echo " Mindestens Version ${MIN_NODE_VERSION} benötigt." + exit 1 +fi +print_success "Node.js v$(node -v | sed 's/v//') gefunden" + +# 2. npm prüfen +if ! check_command npm; then + print_error "npm ist nicht installiert!" + exit 1 +fi +print_success "npm v$(npm -v) gefunden" + +# 3. n8n prüfen (optional, aber empfohlen) +if check_command n8n; then + print_success "n8n $(n8n --version 2>/dev/null || echo 'installiert') gefunden" +else + print_warning "n8n ist nicht global installiert." + echo " Für npm link muss n8n global installiert sein:" + echo " npm install -g n8n" + echo "" + if [ "$SKIP_LINK" = false ] && [ "$GLOBAL_INSTALL" = false ]; then + read -p "Möchtest du trotzdem fortfahren? (j/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Jj]$ ]]; then + exit 1 + fi + fi +fi + +# 4. Dependencies installieren +echo "" +print_info "Installiere Dependencies..." +npm install +print_success "Dependencies installiert" + +# 5. TypeScript kompilieren +echo "" +print_info "Kompiliere TypeScript..." +npm run build +print_success "Build erfolgreich" + +# 6. npm link oder global install +if [ "$GLOBAL_INSTALL" = true ]; then + echo "" + print_info "Installiere global..." + npm install -g . + print_success "Global installiert" +elif [ "$SKIP_LINK" = false ]; then + echo "" + print_info "Verlinke mit npm link..." + npm link + print_success "npm link erfolgreich" + + # Prüfen ob n8n vorhanden und linken + if check_command n8n; then + N8N_PATH=$(npm root -g)/n8n + if [ -d "$N8N_PATH" ]; then + print_info "Verlinke mit n8n..." + cd "$N8N_PATH" 2>/dev/null && npm link n8n-nodes-librebooking 2>/dev/null && cd - > /dev/null + print_success "Mit n8n verlinkt" + fi + fi +fi + +# Abschluss +echo "" +echo -e "${GREEN}=============================================${NC}" +echo -e "${GREEN} Installation erfolgreich abgeschlossen!${NC}" +echo -e "${GREEN}=============================================${NC}" +echo "" +echo "Nächste Schritte:" +echo "" +if [ "$SKIP_LINK" = true ]; then + echo " 1. Führe 'npm link' aus, um den Node zu verlinken" + echo " 2. Starte n8n neu: n8n start" +else + echo " 1. Starte n8n neu: n8n start" + echo " (oder mit Docker: docker-compose restart)" +fi +echo "" +echo " 2. Öffne n8n im Browser: http://localhost:5678" +echo " 3. Der LibreBooking Node sollte verfügbar sein" +echo "" +echo "Bei Problemen siehe INSTALLATION.md oder README.md" +echo "" diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..6251781 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,122 @@ +# Nginx Reverse Proxy Konfiguration für n8n mit LibreBooking +# Beispielkonfiguration für HTTPS Zugang +# +# Verwendung: +# 1. Diese Datei nach /etc/nginx/sites-available/n8n kopieren +# 2. ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/ +# 3. SSL-Zertifikate einrichten (z.B. mit Certbot) +# 4. nginx -t && systemctl reload nginx + +# Upstream für n8n +upstream n8n_backend { + server 127.0.0.1:5678; + keepalive 32; +} + +# HTTP -> HTTPS Redirect +server { + listen 80; + listen [::]:80; + server_name n8n.example.com; + + # ACME Challenge für Let's Encrypt + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +# HTTPS Server +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name n8n.example.com; + + # SSL-Zertifikate (Let's Encrypt Beispiel) + ssl_certificate /etc/letsencrypt/live/n8n.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/privkey.pem; + + # SSL-Einstellungen (moderne Konfiguration) + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + + # HSTS + add_header Strict-Transport-Security "max-age=63072000" always; + + # Logging + access_log /var/log/nginx/n8n_access.log; + error_log /var/log/nginx/n8n_error.log; + + # Proxy-Einstellungen + location / { + proxy_pass http://n8n_backend; + proxy_http_version 1.1; + + # WebSocket Support (wichtig für n8n Editor) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Standard Proxy Headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # Timeouts (erhöht für lange Workflow-Ausführungen) + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + # Buffer-Einstellungen + proxy_buffering off; + proxy_buffer_size 4k; + + # Client-Upload Limit (anpassen nach Bedarf) + client_max_body_size 50M; + } + + # Webhook-spezifische Einstellungen + location /webhook/ { + proxy_pass http://n8n_backend; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Erhöhte Timeouts für Webhooks + proxy_connect_timeout 60s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + + proxy_buffering off; + client_max_body_size 100M; + } + + # Health Check Endpoint + location /healthz { + proxy_pass http://n8n_backend/healthz; + proxy_http_version 1.1; + proxy_set_header Host $host; + access_log off; + } +} + +# Optional: Monitoring/Metrics Server Block +# server { +# listen 127.0.0.1:9090; +# server_name localhost; +# +# location /nginx_status { +# stub_status on; +# allow 127.0.0.1; +# deny all; +# } +# } diff --git a/nodes/LibreBooking/LibreBooking.node.ts b/nodes/LibreBooking/LibreBooking.node.ts new file mode 100644 index 0000000..675b4ad --- /dev/null +++ b/nodes/LibreBooking/LibreBooking.node.ts @@ -0,0 +1,1203 @@ +import { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + IHttpRequestMethods, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +/** + * Authentifizierung bei LibreBooking + */ +async function authenticate( + executeFunctions: IExecuteFunctions, + baseUrl: string, + username: string, + password: string, +): Promise { + try { + const response = await executeFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`, + headers: { 'Content-Type': 'application/json' }, + body: { username, password }, + json: true, + }); + + if (!response.isAuthenticated) { + throw new NodeOperationError( + executeFunctions.getNode(), + 'Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihre Credentials.', + ); + } + + return { + sessionToken: response.sessionToken, + userId: response.userId, + sessionExpires: response.sessionExpires, + }; + } catch (error: any) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Authentifizierung fehlgeschlagen', + description: 'Überprüfen Sie die LibreBooking URL und Ihre Zugangsdaten.', + }); + } +} + +/** + * Abmeldung von LibreBooking + */ +async function signOut( + executeFunctions: IExecuteFunctions, + baseUrl: string, + session: LibreBookingSession, +): Promise { + try { + await executeFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`, + headers: { 'Content-Type': 'application/json' }, + body: { + userId: session.userId, + sessionToken: session.sessionToken, + }, + json: true, + }); + } catch (error) { + // Ignoriere SignOut-Fehler + } +} + +/** + * API-Request mit Session-Authentifizierung + */ +async function makeApiRequest( + executeFunctions: IExecuteFunctions, + baseUrl: string, + session: LibreBookingSession, + method: IHttpRequestMethods, + endpoint: string, + body?: any, + qs?: any, +): Promise { + const options: any = { + method, + url: `${baseUrl}/Web/Services/index.php${endpoint}`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + json: true, + }; + + if (body && Object.keys(body).length > 0) { + options.body = body; + } + + if (qs && Object.keys(qs).length > 0) { + options.qs = qs; + } + + try { + return await executeFunctions.helpers.httpRequest(options); + } catch (error: any) { + if (error.statusCode === 401) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Authentifizierung abgelaufen', + description: 'Der Session-Token ist abgelaufen. Bitte erneut ausführen.', + }); + } else if (error.statusCode === 403) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Zugriff verweigert', + description: 'Sie haben keine Berechtigung für diese Operation. Admin-Rechte erforderlich?', + }); + } else if (error.statusCode === 404) { + throw new NodeApiError(executeFunctions.getNode(), error, { + message: 'Nicht gefunden', + description: 'Die angeforderte Ressource wurde nicht gefunden.', + }); + } + throw new NodeApiError(executeFunctions.getNode(), error, { + message: `API-Fehler: ${error.message}`, + }); + } +} + +/** + * Hilfsfunktion: String zu Array von Zahlen + */ +function parseIdList(value: string | undefined): number[] { + if (!value || value.trim() === '') return []; + return value.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id)); +} + +/** + * LibreBooking n8n Node + * + * Vollständige Integration für die LibreBooking API. + * Unterstützt alle wichtigen Ressourcen und Operationen. + */ +export class LibreBooking implements INodeType { + description: INodeTypeDescription = { + displayName: 'LibreBooking', + name: 'libreBooking', + icon: 'file:librebooking.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Verwalten Sie Reservierungen, Ressourcen, Benutzer und mehr mit LibreBooking', + defaults: { + name: 'LibreBooking', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'libreBookingApi', + required: true, + }, + ], + properties: [ + // ===================================================== + // RESOURCE SELECTOR + // ===================================================== + { + displayName: 'Ressource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Reservierung', + value: 'reservation', + description: 'Reservierungen verwalten', + }, + { + name: 'Ressource', + value: 'resource', + description: 'Ressourcen (Räume, Equipment) verwalten', + }, + { + name: 'Zeitplan', + value: 'schedule', + description: 'Zeitpläne abrufen', + }, + { + name: 'Benutzer', + value: 'user', + description: 'Benutzer verwalten (Admin-Rechte erforderlich)', + }, + { + name: 'Konto', + value: 'account', + description: 'Eigenes Konto verwalten', + }, + { + name: 'Gruppe', + value: 'group', + description: 'Benutzergruppen verwalten', + }, + { + name: 'Zubehör', + value: 'accessory', + description: 'Zubehör abrufen', + }, + { + name: 'Attribut', + value: 'attribute', + description: 'Benutzerdefinierte Attribute verwalten', + }, + ], + default: 'reservation', + }, + + // ===================================================== + // RESERVATION OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['reservation'], + }, + }, + options: [ + { name: 'Erstellen', value: 'create', description: 'Neue Reservierung erstellen', action: 'Reservierung erstellen' }, + { name: 'Abrufen', value: 'get', description: 'Reservierung abrufen', action: 'Reservierung abrufen' }, + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Reservierungen abrufen', action: 'Alle Reservierungen abrufen' }, + { name: 'Aktualisieren', value: 'update', description: 'Reservierung aktualisieren', action: 'Reservierung aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Reservierung löschen', action: 'Reservierung löschen' }, + { name: 'Genehmigen', value: 'approve', description: 'Ausstehende Reservierung genehmigen', action: 'Reservierung genehmigen' }, + { name: 'Check-In', value: 'checkIn', description: 'In Reservierung einchecken', action: 'In Reservierung einchecken' }, + { name: 'Check-Out', value: 'checkOut', description: 'Aus Reservierung auschecken', action: 'Aus Reservierung auschecken' }, + ], + default: 'getAll', + }, + + // ===================================================== + // RESOURCE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['resource'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Ressourcen abrufen', action: 'Alle Ressourcen abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Ressource abrufen', action: 'Ressource abrufen' }, + { name: 'Verfügbarkeit Prüfen', value: 'getAvailability', description: 'Verfügbarkeit von Ressourcen prüfen', action: 'Verfügbarkeit prüfen' }, + { name: 'Gruppen Abrufen', value: 'getGroups', description: 'Ressourcen-Gruppen abrufen', action: 'Ressourcen-Gruppen abrufen' }, + { name: 'Typen Abrufen', value: 'getTypes', description: 'Ressourcen-Typen abrufen', action: 'Ressourcen-Typen abrufen' }, + { name: 'Status Abrufen', value: 'getStatuses', description: 'Verfügbare Status abrufen', action: 'Status abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neue Ressource erstellen (Admin)', action: 'Ressource erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Ressource aktualisieren (Admin)', action: 'Ressource aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Ressource löschen (Admin)', action: 'Ressource löschen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // SCHEDULE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['schedule'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Zeitpläne abrufen', action: 'Alle Zeitpläne abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Zeitplan abrufen', action: 'Zeitplan abrufen' }, + { name: 'Slots Abrufen', value: 'getSlots', description: 'Verfügbare Slots abrufen', action: 'Slots abrufen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // USER OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['user'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Benutzer abrufen', action: 'Alle Benutzer abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Benutzer abrufen', action: 'Benutzer abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neuen Benutzer erstellen (Admin)', action: 'Benutzer erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Benutzer aktualisieren (Admin)', action: 'Benutzer aktualisieren' }, + { name: 'Passwort Ändern', value: 'updatePassword', description: 'Benutzer-Passwort ändern (Admin)', action: 'Passwort ändern' }, + { name: 'Löschen', value: 'delete', description: 'Benutzer löschen (Admin)', action: 'Benutzer löschen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ACCOUNT OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['account'] } }, + options: [ + { name: 'Abrufen', value: 'get', description: 'Eigene Kontoinformationen abrufen', action: 'Konto abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neues Konto erstellen (Registrierung)', action: 'Konto erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Eigenes Konto aktualisieren', action: 'Konto aktualisieren' }, + { name: 'Passwort Ändern', value: 'updatePassword', description: 'Eigenes Passwort ändern', action: 'Passwort ändern' }, + ], + default: 'get', + }, + + // ===================================================== + // GROUP OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['group'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Gruppen abrufen', action: 'Alle Gruppen abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Gruppe abrufen', action: 'Gruppe abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neue Gruppe erstellen (Admin)', action: 'Gruppe erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Gruppe aktualisieren (Admin)', action: 'Gruppe aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Gruppe löschen (Admin)', action: 'Gruppe löschen' }, + { name: 'Rollen Ändern', value: 'changeRoles', description: 'Gruppenrollen ändern (Admin)', action: 'Rollen ändern' }, + { name: 'Berechtigungen Ändern', value: 'changePermissions', description: 'Gruppenberechtigungen ändern (Admin)', action: 'Berechtigungen ändern' }, + { name: 'Benutzer Ändern', value: 'changeUsers', description: 'Gruppenbenutzer ändern (Admin)', action: 'Benutzer ändern' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ACCESSORY OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['accessory'] } }, + options: [ + { name: 'Alle Abrufen', value: 'getAll', description: 'Alle Zubehörteile abrufen', action: 'Alle Zubehörteile abrufen' }, + { name: 'Abrufen', value: 'get', description: 'Zubehörteil abrufen', action: 'Zubehörteil abrufen' }, + ], + default: 'getAll', + }, + + // ===================================================== + // ATTRIBUTE OPERATIONS + // ===================================================== + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['attribute'] } }, + options: [ + { name: 'Abrufen', value: 'get', description: 'Attribut abrufen', action: 'Attribut abrufen' }, + { name: 'Nach Kategorie Abrufen', value: 'getByCategory', description: 'Attribute einer Kategorie abrufen', action: 'Attribute nach Kategorie abrufen' }, + { name: 'Erstellen', value: 'create', description: 'Neues Attribut erstellen (Admin)', action: 'Attribut erstellen' }, + { name: 'Aktualisieren', value: 'update', description: 'Attribut aktualisieren (Admin)', action: 'Attribut aktualisieren' }, + { name: 'Löschen', value: 'delete', description: 'Attribut löschen (Admin)', action: 'Attribut löschen' }, + ], + default: 'getByCategory', + }, + + // ===================================================== + // RESERVATION PARAMETERS + // ===================================================== + { + displayName: 'Referenznummer', + name: 'referenceNumber', + type: 'string', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['get', 'update', 'delete', 'approve', 'checkIn', 'checkOut'] } }, + default: '', + description: 'Die eindeutige Referenznummer der Reservierung', + }, + { + displayName: 'Ressourcen-ID', + name: 'resourceId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create'] } }, + default: 1, + description: 'Die ID der zu reservierenden Ressource', + }, + { + displayName: 'Startzeit', + name: 'startDateTime', + type: 'dateTime', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Startzeitpunkt der Reservierung (ISO 8601 Format)', + }, + { + displayName: 'Endzeit', + name: 'endDateTime', + type: 'dateTime', + required: true, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Endzeitpunkt der Reservierung (ISO 8601 Format)', + }, + { + displayName: 'Titel', + name: 'title', + type: 'string', + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + default: '', + description: 'Titel der Reservierung', + }, + { + displayName: 'Aktualisierungsbereich', + name: 'updateScope', + type: 'options', + displayOptions: { show: { resource: ['reservation'], operation: ['update', 'delete'] } }, + options: [ + { name: 'Nur Diese', value: 'this', description: 'Nur diese Instanz ändern' }, + { name: 'Zukünftige', value: 'future', description: 'Diese und alle zukünftigen Instanzen ändern' }, + { name: 'Alle', value: 'full', description: 'Alle Instanzen der Serie ändern' }, + ], + default: 'this', + }, + { + displayName: 'Zusätzliche Felder', + name: 'additionalFields', + type: 'collection', + placeholder: 'Feld hinzufügen', + default: {}, + displayOptions: { show: { resource: ['reservation'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Beschreibung', name: 'description', type: 'string', default: '' }, + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + { displayName: 'Zusätzliche Ressourcen', name: 'resources', type: 'string', default: '', description: 'Komma-getrennte Liste' }, + { displayName: 'Teilnehmer', name: 'participants', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' }, + { displayName: 'Eingeladene', name: 'invitees', type: 'string', default: '', description: 'Komma-getrennte Benutzer-IDs' }, + { displayName: 'Teilnahme Erlauben', name: 'allowParticipation', type: 'boolean', default: true }, + { displayName: 'Nutzungsbedingungen Akzeptiert', name: 'termsAccepted', type: 'boolean', default: true }, + ], + }, + { + displayName: 'Filter', + name: 'filters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['reservation'], operation: ['getAll'] } }, + options: [ + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' }, + { displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' }, + { displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' }, + ], + }, + + // ===================================================== + // RESOURCE PARAMETERS + // ===================================================== + { + displayName: 'Ressourcen-ID', + name: 'resourceIdParam', + type: 'number', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['get', 'update', 'delete'] } }, + default: 1, + }, + { + displayName: 'Ressourcen-ID (Optional)', + name: 'resourceIdOptional', + type: 'number', + displayOptions: { show: { resource: ['resource'], operation: ['getAvailability'] } }, + default: '', + }, + { + displayName: 'Datum/Zeit', + name: 'availabilityDateTime', + type: 'dateTime', + displayOptions: { show: { resource: ['resource'], operation: ['getAvailability'] } }, + default: '', + }, + { + displayName: 'Ressourcen-Name', + name: 'resourceName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Zeitplan-ID', + name: 'scheduleIdForResource', + type: 'number', + required: true, + displayOptions: { show: { resource: ['resource'], operation: ['create'] } }, + default: 1, + }, + { + displayName: 'Ressourcen-Optionen', + name: 'resourceOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['resource'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Standort', name: 'location', type: 'string', default: '' }, + { displayName: 'Kontakt', name: 'contact', type: 'string', default: '' }, + { displayName: 'Beschreibung', name: 'description', type: 'string', default: '' }, + { displayName: 'Notizen', name: 'notes', type: 'string', default: '' }, + { displayName: 'Max. Teilnehmer', name: 'maxParticipants', type: 'number', default: 0 }, + { displayName: 'Genehmigung Erforderlich', name: 'requiresApproval', type: 'boolean', default: false }, + { displayName: 'Mehrtägig Erlauben', name: 'allowMultiday', type: 'boolean', default: false }, + { displayName: 'Check-In Erforderlich', name: 'requiresCheckIn', type: 'boolean', default: false }, + { displayName: 'Auto-Release Minuten', name: 'autoReleaseMinutes', type: 'number', default: 0 }, + { displayName: 'Farbe', name: 'color', type: 'string', default: '' }, + { displayName: 'Status-ID', name: 'statusId', type: 'options', options: [{ name: 'Versteckt', value: 0 }, { name: 'Verfügbar', value: 1 }, { name: 'Nicht Verfügbar', value: 2 }], default: 1 }, + ], + }, + + // ===================================================== + // SCHEDULE PARAMETERS + // ===================================================== + { + displayName: 'Zeitplan-ID', + name: 'scheduleId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['schedule'], operation: ['get', 'getSlots'] } }, + default: 1, + }, + { + displayName: 'Slots-Filter', + name: 'slotsFilters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['schedule'], operation: ['getSlots'] } }, + options: [ + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Startzeit', name: 'startDateTime', type: 'dateTime', default: '' }, + { displayName: 'Endzeit', name: 'endDateTime', type: 'dateTime', default: '' }, + ], + }, + + // ===================================================== + // USER PARAMETERS + // ===================================================== + { + displayName: 'Benutzer-ID', + name: 'userId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['get', 'update', 'updatePassword', 'delete'] } }, + default: 1, + }, + { + displayName: 'E-Mail', + name: 'emailAddress', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + default: '', + }, + { + displayName: 'Benutzername', + name: 'userName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + default: '', + }, + { + displayName: 'Passwort', + name: 'password', + type: 'string', + typeOptions: { password: true }, + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'updatePassword'] } }, + default: '', + }, + { + displayName: 'Vorname', + name: 'firstName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Nachname', + name: 'lastName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Benutzer-Filter', + name: 'userFilters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['getAll'] } }, + options: [ + { displayName: 'Benutzername', name: 'username', type: 'string', default: '' }, + { displayName: 'E-Mail', name: 'email', type: 'string', default: '' }, + { displayName: 'Vorname', name: 'firstName', type: 'string', default: '' }, + { displayName: 'Nachname', name: 'lastName', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + ], + }, + { + displayName: 'Benutzer-Optionen', + name: 'userOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Zeitzone', name: 'timezone', type: 'string', default: 'Europe/Berlin' }, + { displayName: 'Sprache', name: 'language', type: 'string', default: 'de_de' }, + { displayName: 'Telefon', name: 'phone', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + { displayName: 'Position', name: 'position', type: 'string', default: '' }, + { displayName: 'Gruppen', name: 'groups', type: 'string', default: '', description: 'Komma-getrennte Gruppen-IDs' }, + ], + }, + + // ===================================================== + // ACCOUNT PARAMETERS + // ===================================================== + { + displayName: 'Benutzer-ID', + name: 'accountUserId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['account'], operation: ['get', 'update', 'updatePassword'] } }, + default: '', + }, + { + displayName: 'Account-Daten', + name: 'accountData', + type: 'collection', + placeholder: 'Feld hinzufügen', + default: {}, + displayOptions: { show: { resource: ['account'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'E-Mail', name: 'emailAddress', type: 'string', default: '' }, + { displayName: 'Benutzername', name: 'userName', type: 'string', default: '' }, + { displayName: 'Passwort', name: 'password', type: 'string', typeOptions: { password: true }, default: '' }, + { displayName: 'Vorname', name: 'firstName', type: 'string', default: '' }, + { displayName: 'Nachname', name: 'lastName', type: 'string', default: '' }, + { displayName: 'Zeitzone', name: 'timezone', type: 'string', default: 'Europe/Berlin' }, + { displayName: 'Sprache', name: 'language', type: 'string', default: 'de_de' }, + { displayName: 'Telefon', name: 'phone', type: 'string', default: '' }, + { displayName: 'Organisation', name: 'organization', type: 'string', default: '' }, + { displayName: 'Position', name: 'position', type: 'string', default: '' }, + { displayName: 'AGB Akzeptiert', name: 'acceptTermsOfService', type: 'boolean', default: true }, + ], + }, + { + displayName: 'Passwort-Änderung', + name: 'passwordChange', + type: 'fixedCollection', + default: {}, + displayOptions: { show: { resource: ['account'], operation: ['updatePassword'] } }, + options: [ + { + name: 'passwords', + displayName: 'Passwörter', + values: [ + { displayName: 'Aktuelles Passwort', name: 'currentPassword', type: 'string', typeOptions: { password: true }, default: '' }, + { displayName: 'Neues Passwort', name: 'newPassword', type: 'string', typeOptions: { password: true }, default: '' }, + ], + }, + ], + }, + + // ===================================================== + // GROUP PARAMETERS + // ===================================================== + { + displayName: 'Gruppen-ID', + name: 'groupId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['group'], operation: ['get', 'update', 'delete', 'changeRoles', 'changePermissions', 'changeUsers'] } }, + default: 1, + }, + { + displayName: 'Gruppen-Name', + name: 'groupName', + type: 'string', + required: true, + displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Standard-Gruppe', + name: 'isDefault', + type: 'boolean', + displayOptions: { show: { resource: ['group'], operation: ['create', 'update'] } }, + default: false, + }, + { + displayName: 'Rollen-IDs', + name: 'roleIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changeRoles'] } }, + default: '', + description: '1=Gruppenadmin, 2=App-Admin, 3=Ressourcen-Admin, 4=Zeitplan-Admin', + }, + { + displayName: 'Ressourcen-IDs', + name: 'permissionResourceIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changePermissions'] } }, + default: '', + }, + { + displayName: 'Benutzer-IDs', + name: 'groupUserIds', + type: 'string', + displayOptions: { show: { resource: ['group'], operation: ['changeUsers'] } }, + default: '', + }, + + // ===================================================== + // ACCESSORY PARAMETERS + // ===================================================== + { + displayName: 'Zubehör-ID', + name: 'accessoryId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['accessory'], operation: ['get'] } }, + default: 1, + }, + + // ===================================================== + // ATTRIBUTE PARAMETERS + // ===================================================== + { + displayName: 'Attribut-ID', + name: 'attributeId', + type: 'number', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['get', 'update', 'delete'] } }, + default: 1, + }, + { + displayName: 'Kategorie-ID', + name: 'categoryId', + type: 'options', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['getByCategory', 'create'] } }, + options: [ + { name: 'Reservierung', value: 1 }, + { name: 'Benutzer', value: 2 }, + { name: 'Ressource', value: 4 }, + { name: 'Ressourcen-Typ', value: 5 }, + ], + default: 1, + }, + { + displayName: 'Attribut-Label', + name: 'attributeLabel', + type: 'string', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + default: '', + }, + { + displayName: 'Attribut-Typ', + name: 'attributeType', + type: 'options', + required: true, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + options: [ + { name: 'Einzeilig', value: 1 }, + { name: 'Mehrzeilig', value: 2 }, + { name: 'Auswahlliste', value: 3 }, + { name: 'Checkbox', value: 4 }, + { name: 'Datum/Zeit', value: 5 }, + ], + default: 1, + }, + { + displayName: 'Attribut-Optionen', + name: 'attributeOptions', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + displayOptions: { show: { resource: ['attribute'], operation: ['create', 'update'] } }, + options: [ + { displayName: 'Erforderlich', name: 'required', type: 'boolean', default: false }, + { displayName: 'Nur Admin', name: 'adminOnly', type: 'boolean', default: false }, + { displayName: 'Privat', name: 'isPrivate', type: 'boolean', default: false }, + { displayName: 'Sortierung', name: 'sortOrder', type: 'number', default: 0 }, + { displayName: 'Regex-Validierung', name: 'regex', type: 'string', default: '' }, + { displayName: 'Mögliche Werte', name: 'possibleValues', type: 'string', default: '', description: 'Komma-getrennt' }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const credentials = await this.getCredentials('libreBookingApi'); + const baseUrl = (credentials.url as string).replace(/\/$/, ''); + const username = credentials.username as string; + const pw = credentials.password as string; + + const session = await authenticate(this, baseUrl, username, pw); + + try { + for (let i = 0; i < items.length; i++) { + try { + const resource = this.getNodeParameter('resource', i) as string; + const operation = this.getNodeParameter('operation', i) as string; + let responseData: any; + + // RESERVATION + if (resource === 'reservation') { + if (operation === 'getAll') { + const filters = this.getNodeParameter('filters', i, {}) as any; + const qs: any = {}; + if (filters.userId) qs.userId = filters.userId; + if (filters.resourceId) qs.resourceId = filters.resourceId; + if (filters.scheduleId) qs.scheduleId = filters.scheduleId; + if (filters.startDateTime) qs.startDateTime = filters.startDateTime; + if (filters.endDateTime) qs.endDateTime = filters.endDateTime; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Reservations/', undefined, qs); + } else if (operation === 'get') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Reservations/${referenceNumber}`); + } else if (operation === 'create') { + const resourceId = this.getNodeParameter('resourceId', i) as number; + const startDateTime = this.getNodeParameter('startDateTime', i) as string; + const endDateTime = this.getNodeParameter('endDateTime', i) as string; + const title = this.getNodeParameter('title', i, '') as string; + const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any; + const body: any = { resourceId, startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() }; + if (title) body.title = title; + if (additionalFields.description) body.description = additionalFields.description; + if (additionalFields.userId) body.userId = additionalFields.userId; + if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources); + if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants); + if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees); + if (additionalFields.allowParticipation !== undefined) body.allowParticipation = additionalFields.allowParticipation; + if (additionalFields.termsAccepted !== undefined) body.termsAccepted = additionalFields.termsAccepted; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Reservations/', body); + } else if (operation === 'update') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + const startDateTime = this.getNodeParameter('startDateTime', i) as string; + const endDateTime = this.getNodeParameter('endDateTime', i) as string; + const title = this.getNodeParameter('title', i, '') as string; + const updateScope = this.getNodeParameter('updateScope', i, 'this') as string; + const additionalFields = this.getNodeParameter('additionalFields', i, {}) as any; + const body: any = { startDateTime: new Date(startDateTime).toISOString(), endDateTime: new Date(endDateTime).toISOString() }; + if (title) body.title = title; + if (additionalFields.description) body.description = additionalFields.description; + if (additionalFields.resourceId) body.resourceId = additionalFields.resourceId; + if (additionalFields.userId) body.userId = additionalFields.userId; + if (additionalFields.resources) body.resources = parseIdList(additionalFields.resources); + if (additionalFields.participants) body.participants = parseIdList(additionalFields.participants); + if (additionalFields.invitees) body.invitees = parseIdList(additionalFields.invitees); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}?updateScope=${updateScope}`, body); + } else if (operation === 'delete') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + const updateScope = this.getNodeParameter('updateScope', i, 'this') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Reservations/${referenceNumber}?updateScope=${updateScope}`); + } else if (operation === 'approve') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/Approval`); + } else if (operation === 'checkIn') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/CheckIn`); + } else if (operation === 'checkOut') { + const referenceNumber = this.getNodeParameter('referenceNumber', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Reservations/${referenceNumber}/CheckOut`); + } + } + + // RESOURCE + else if (resource === 'resource') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/'); + } else if (operation === 'get') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Resources/${resourceIdParam}`); + } else if (operation === 'getAvailability') { + const resourceIdOptional = this.getNodeParameter('resourceIdOptional', i, '') as number | ''; + const availabilityDateTime = this.getNodeParameter('availabilityDateTime', i, '') as string; + let endpoint = '/Resources/Availability'; + if (resourceIdOptional) endpoint = `/Resources/${resourceIdOptional}/Availability`; + const qs: any = {}; + if (availabilityDateTime) qs.dateTime = new Date(availabilityDateTime).toISOString(); + responseData = await makeApiRequest(this, baseUrl, session, 'GET', endpoint, undefined, qs); + } else if (operation === 'getGroups') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Groups'); + } else if (operation === 'getTypes') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Types'); + } else if (operation === 'getStatuses') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Resources/Status'); + } else if (operation === 'create') { + const resourceName = this.getNodeParameter('resourceName', i) as string; + const scheduleIdForResource = this.getNodeParameter('scheduleIdForResource', i) as number; + const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any; + const body: any = { name: resourceName, scheduleId: scheduleIdForResource }; + if (resourceOptions.location) body.location = resourceOptions.location; + if (resourceOptions.contact) body.contact = resourceOptions.contact; + if (resourceOptions.description) body.description = resourceOptions.description; + if (resourceOptions.notes) body.notes = resourceOptions.notes; + if (resourceOptions.maxParticipants) body.maxParticipants = resourceOptions.maxParticipants; + if (resourceOptions.requiresApproval !== undefined) body.requiresApproval = resourceOptions.requiresApproval; + if (resourceOptions.allowMultiday !== undefined) body.allowMultiday = resourceOptions.allowMultiday; + if (resourceOptions.requiresCheckIn !== undefined) body.requiresCheckIn = resourceOptions.requiresCheckIn; + if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes; + if (resourceOptions.color) body.color = resourceOptions.color; + if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Resources/', body); + } else if (operation === 'update') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + const resourceName = this.getNodeParameter('resourceName', i) as string; + const resourceOptions = this.getNodeParameter('resourceOptions', i, {}) as any; + const body: any = { name: resourceName }; + if (resourceOptions.location) body.location = resourceOptions.location; + if (resourceOptions.contact) body.contact = resourceOptions.contact; + if (resourceOptions.description) body.description = resourceOptions.description; + if (resourceOptions.notes) body.notes = resourceOptions.notes; + if (resourceOptions.maxParticipants) body.maxParticipants = resourceOptions.maxParticipants; + if (resourceOptions.requiresApproval !== undefined) body.requiresApproval = resourceOptions.requiresApproval; + if (resourceOptions.allowMultiday !== undefined) body.allowMultiday = resourceOptions.allowMultiday; + if (resourceOptions.requiresCheckIn !== undefined) body.requiresCheckIn = resourceOptions.requiresCheckIn; + if (resourceOptions.autoReleaseMinutes) body.autoReleaseMinutes = resourceOptions.autoReleaseMinutes; + if (resourceOptions.color) body.color = resourceOptions.color; + if (resourceOptions.statusId !== undefined) body.statusId = resourceOptions.statusId; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Resources/${resourceIdParam}`, body); + } else if (operation === 'delete') { + const resourceIdParam = this.getNodeParameter('resourceIdParam', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Resources/${resourceIdParam}`); + } + } + + // SCHEDULE + else if (resource === 'schedule') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Schedules/'); + } else if (operation === 'get') { + const scheduleId = this.getNodeParameter('scheduleId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Schedules/${scheduleId}`); + } else if (operation === 'getSlots') { + const scheduleId = this.getNodeParameter('scheduleId', i) as number; + const slotsFilters = this.getNodeParameter('slotsFilters', i, {}) as any; + const qs: any = {}; + if (slotsFilters.resourceId) qs.resourceId = slotsFilters.resourceId; + if (slotsFilters.startDateTime) qs.startDateTime = new Date(slotsFilters.startDateTime).toISOString(); + if (slotsFilters.endDateTime) qs.endDateTime = new Date(slotsFilters.endDateTime).toISOString(); + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Schedules/${scheduleId}/Slots`, undefined, qs); + } + } + + // USER + else if (resource === 'user') { + if (operation === 'getAll') { + const userFilters = this.getNodeParameter('userFilters', i, {}) as any; + const qs: any = {}; + if (userFilters.username) qs.username = userFilters.username; + if (userFilters.email) qs.email = userFilters.email; + if (userFilters.firstName) qs.firstName = userFilters.firstName; + if (userFilters.lastName) qs.lastName = userFilters.lastName; + if (userFilters.organization) qs.organization = userFilters.organization; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Users/', undefined, qs); + } else if (operation === 'get') { + const userId = this.getNodeParameter('userId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Users/${userId}`); + } else if (operation === 'create') { + const emailAddress = this.getNodeParameter('emailAddress', i) as string; + const userName = this.getNodeParameter('userName', i) as string; + const pw = this.getNodeParameter('password', i) as string; + const firstName = this.getNodeParameter('firstName', i) as string; + const lastName = this.getNodeParameter('lastName', i) as string; + const userOptions = this.getNodeParameter('userOptions', i, {}) as any; + const body: any = { emailAddress, userName, password: pw, firstName, lastName }; + if (userOptions.timezone) body.timezone = userOptions.timezone; + if (userOptions.language) body.language = userOptions.language; + if (userOptions.phone) body.phone = userOptions.phone; + if (userOptions.organization) body.organization = userOptions.organization; + if (userOptions.position) body.position = userOptions.position; + if (userOptions.groups) body.groups = parseIdList(userOptions.groups); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Users/', body); + } else if (operation === 'update') { + const userId = this.getNodeParameter('userId', i) as number; + const firstName = this.getNodeParameter('firstName', i) as string; + const lastName = this.getNodeParameter('lastName', i) as string; + const userOptions = this.getNodeParameter('userOptions', i, {}) as any; + const body: any = { firstName, lastName }; + if (userOptions.timezone) body.timezone = userOptions.timezone; + if (userOptions.language) body.language = userOptions.language; + if (userOptions.phone) body.phone = userOptions.phone; + if (userOptions.organization) body.organization = userOptions.organization; + if (userOptions.position) body.position = userOptions.position; + if (userOptions.groups) body.groups = parseIdList(userOptions.groups); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}`, body); + } else if (operation === 'updatePassword') { + const userId = this.getNodeParameter('userId', i) as number; + const pw = this.getNodeParameter('password', i) as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Users/${userId}/Password`, { password: pw }); + } else if (operation === 'delete') { + const userId = this.getNodeParameter('userId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Users/${userId}`); + } + } + + // ACCOUNT + else if (resource === 'account') { + if (operation === 'get') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Accounts/${accountUserId}`); + } else if (operation === 'create') { + const accountData = this.getNodeParameter('accountData', i, {}) as any; + const body: any = {}; + if (accountData.emailAddress) body.emailAddress = accountData.emailAddress; + if (accountData.userName) body.userName = accountData.userName; + if (accountData.password) body.password = accountData.password; + if (accountData.firstName) body.firstName = accountData.firstName; + if (accountData.lastName) body.lastName = accountData.lastName; + if (accountData.timezone) body.timezone = accountData.timezone; + if (accountData.language) body.language = accountData.language; + if (accountData.phone) body.phone = accountData.phone; + if (accountData.organization) body.organization = accountData.organization; + if (accountData.position) body.position = accountData.position; + if (accountData.acceptTermsOfService !== undefined) body.acceptTermsOfService = accountData.acceptTermsOfService; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Accounts/', body); + } else if (operation === 'update') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + const accountData = this.getNodeParameter('accountData', i, {}) as any; + const body: any = {}; + if (accountData.emailAddress) body.emailAddress = accountData.emailAddress; + if (accountData.userName) body.userName = accountData.userName; + if (accountData.firstName) body.firstName = accountData.firstName; + if (accountData.lastName) body.lastName = accountData.lastName; + if (accountData.timezone) body.timezone = accountData.timezone; + if (accountData.language) body.language = accountData.language; + if (accountData.phone) body.phone = accountData.phone; + if (accountData.organization) body.organization = accountData.organization; + if (accountData.position) body.position = accountData.position; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Accounts/${accountUserId}`, body); + } else if (operation === 'updatePassword') { + const accountUserId = this.getNodeParameter('accountUserId', i) as number; + const passwordChange = this.getNodeParameter('passwordChange', i, {}) as any; + const passwords = passwordChange.passwords || {}; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Accounts/${accountUserId}/Password`, { + currentPassword: passwords.currentPassword, + newPassword: passwords.newPassword, + }); + } + } + + // GROUP + else if (resource === 'group') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Groups/'); + } else if (operation === 'get') { + const groupId = this.getNodeParameter('groupId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Groups/${groupId}`); + } else if (operation === 'create') { + const groupName = this.getNodeParameter('groupName', i) as string; + const isDefault = this.getNodeParameter('isDefault', i, false) as boolean; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Groups/', { name: groupName, isDefault }); + } else if (operation === 'update') { + const groupId = this.getNodeParameter('groupId', i) as number; + const groupName = this.getNodeParameter('groupName', i) as string; + const isDefault = this.getNodeParameter('isDefault', i, false) as boolean; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}`, { name: groupName, isDefault }); + } else if (operation === 'delete') { + const groupId = this.getNodeParameter('groupId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Groups/${groupId}`); + } else if (operation === 'changeRoles') { + const groupId = this.getNodeParameter('groupId', i) as number; + const roleIds = this.getNodeParameter('roleIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Roles`, { roleIds: parseIdList(roleIds) }); + } else if (operation === 'changePermissions') { + const groupId = this.getNodeParameter('groupId', i) as number; + const permissionResourceIds = this.getNodeParameter('permissionResourceIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Permissions`, { resourceIds: parseIdList(permissionResourceIds) }); + } else if (operation === 'changeUsers') { + const groupId = this.getNodeParameter('groupId', i) as number; + const groupUserIds = this.getNodeParameter('groupUserIds', i, '') as string; + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Groups/${groupId}/Users`, { userIds: parseIdList(groupUserIds) }); + } + } + + // ACCESSORY + else if (resource === 'accessory') { + if (operation === 'getAll') { + responseData = await makeApiRequest(this, baseUrl, session, 'GET', '/Accessories/'); + } else if (operation === 'get') { + const accessoryId = this.getNodeParameter('accessoryId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Accessories/${accessoryId}`); + } + } + + // ATTRIBUTE + else if (resource === 'attribute') { + if (operation === 'get') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Attributes/${attributeId}`); + } else if (operation === 'getByCategory') { + const categoryId = this.getNodeParameter('categoryId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'GET', `/Attributes/Category/${categoryId}`); + } else if (operation === 'create') { + const attributeLabel = this.getNodeParameter('attributeLabel', i) as string; + const attributeType = this.getNodeParameter('attributeType', i) as number; + const categoryId = this.getNodeParameter('categoryId', i) as number; + const attributeOptions = this.getNodeParameter('attributeOptions', i, {}) as any; + const body: any = { label: attributeLabel, type: attributeType, categoryId }; + if (attributeOptions.required !== undefined) body.required = attributeOptions.required; + if (attributeOptions.adminOnly !== undefined) body.adminOnly = attributeOptions.adminOnly; + if (attributeOptions.isPrivate !== undefined) body.isPrivate = attributeOptions.isPrivate; + if (attributeOptions.sortOrder !== undefined) body.sortOrder = attributeOptions.sortOrder; + if (attributeOptions.regex) body.regex = attributeOptions.regex; + if (attributeOptions.possibleValues) body.possibleValues = attributeOptions.possibleValues.split(',').map((v: string) => v.trim()); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', '/Attributes/', body); + } else if (operation === 'update') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + const attributeLabel = this.getNodeParameter('attributeLabel', i) as string; + const attributeType = this.getNodeParameter('attributeType', i) as number; + const attributeOptions = this.getNodeParameter('attributeOptions', i, {}) as any; + const body: any = { label: attributeLabel, type: attributeType }; + if (attributeOptions.required !== undefined) body.required = attributeOptions.required; + if (attributeOptions.adminOnly !== undefined) body.adminOnly = attributeOptions.adminOnly; + if (attributeOptions.isPrivate !== undefined) body.isPrivate = attributeOptions.isPrivate; + if (attributeOptions.sortOrder !== undefined) body.sortOrder = attributeOptions.sortOrder; + if (attributeOptions.regex) body.regex = attributeOptions.regex; + if (attributeOptions.possibleValues) body.possibleValues = attributeOptions.possibleValues.split(',').map((v: string) => v.trim()); + responseData = await makeApiRequest(this, baseUrl, session, 'POST', `/Attributes/${attributeId}`, body); + } else if (operation === 'delete') { + const attributeId = this.getNodeParameter('attributeId', i) as number; + responseData = await makeApiRequest(this, baseUrl, session, 'DELETE', `/Attributes/${attributeId}`); + } + } + + // Process response + if (responseData) { + if (Array.isArray(responseData)) { + returnData.push(...responseData.map(item => ({ json: item }))); + } else if (responseData.reservations) { + returnData.push(...responseData.reservations.map((item: any) => ({ json: item }))); + } else if (responseData.resources) { + returnData.push(...responseData.resources.map((item: any) => ({ json: item }))); + } else if (responseData.schedules) { + returnData.push(...responseData.schedules.map((item: any) => ({ json: item }))); + } else if (responseData.users) { + returnData.push(...responseData.users.map((item: any) => ({ json: item }))); + } else if (responseData.groups) { + returnData.push(...responseData.groups.map((item: any) => ({ json: item }))); + } else if (responseData.accessories) { + returnData.push(...responseData.accessories.map((item: any) => ({ json: item }))); + } else if (responseData.attributes) { + returnData.push(...responseData.attributes.map((item: any) => ({ json: item }))); + } else { + returnData.push({ json: responseData }); + } + } + + } catch (error: any) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw error; + } + } + } finally { + await signOut(this, baseUrl, session); + } + + return [returnData]; + } +} diff --git a/nodes/LibreBooking/librebooking.svg b/nodes/LibreBooking/librebooking.svg new file mode 100644 index 0000000..81306a9 --- /dev/null +++ b/nodes/LibreBooking/librebooking.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts b/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts new file mode 100644 index 0000000..5e2e9ae --- /dev/null +++ b/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts @@ -0,0 +1,352 @@ +import { + INodeType, + INodeTypeDescription, + IPollFunctions, + INodeExecutionData, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +interface ReservationData { + referenceNumber: string; + startDate: string; + endDate: string; + title: string; + resourceId: number; + userId: number; + [key: string]: any; +} + +/** + * Authentifizierung bei LibreBooking + */ +async function authenticateTrigger( + pollFunctions: IPollFunctions, + baseUrl: string, + username: string, + password: string, +): Promise { + try { + const response = await pollFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/Authenticate`, + headers: { 'Content-Type': 'application/json' }, + body: { username, password }, + json: true, + }); + + if (!response.isAuthenticated) { + throw new NodeOperationError( + pollFunctions.getNode(), + 'Authentifizierung fehlgeschlagen', + ); + } + + return { + sessionToken: response.sessionToken, + userId: response.userId, + sessionExpires: response.sessionExpires, + }; + } catch (error: any) { + throw new NodeApiError(pollFunctions.getNode(), error, { + message: 'Authentifizierung fehlgeschlagen', + }); + } +} + +/** + * Abmeldung + */ +async function signOutTrigger( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, +): Promise { + try { + await pollFunctions.helpers.httpRequest({ + method: 'POST', + url: `${baseUrl}/Web/Services/index.php/Authentication/SignOut`, + headers: { 'Content-Type': 'application/json' }, + body: { + userId: session.userId, + sessionToken: session.sessionToken, + }, + json: true, + }); + } catch (error) { + // Ignoriere SignOut-Fehler + } +} + +/** + * Reservierungen abrufen + */ +async function getReservations( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, + startDateTime: string, + endDateTime: string, + filters: any, +): Promise { + const qs: any = { + startDateTime, + endDateTime, + }; + + if (filters.resourceId) qs.resourceId = filters.resourceId; + if (filters.scheduleId) qs.scheduleId = filters.scheduleId; + if (filters.userId) qs.userId = filters.userId; + + const response = await pollFunctions.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/Web/Services/index.php/Reservations/`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + qs, + json: true, + }); + + return response.reservations || []; +} + +/** + * Detaillierte Reservierungsdaten abrufen + */ +async function getReservationDetails( + pollFunctions: IPollFunctions, + baseUrl: string, + session: LibreBookingSession, + referenceNumber: string, +): Promise { + const response = await pollFunctions.helpers.httpRequest({ + method: 'GET', + url: `${baseUrl}/Web/Services/index.php/Reservations/${referenceNumber}`, + headers: { + 'Content-Type': 'application/json', + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + }, + json: true, + }); + + return response; +} + +/** + * Zeitfenster berechnen + */ +function getTimeWindow(timeWindow: string): { start: string; end: string } { + const now = new Date(); + const start = now.toISOString(); + + let endDate = new Date(now); + switch (timeWindow) { + case '7days': + endDate.setDate(endDate.getDate() + 7); + break; + case '14days': + endDate.setDate(endDate.getDate() + 14); + break; + case '30days': + endDate.setDate(endDate.getDate() + 30); + break; + case '90days': + endDate.setDate(endDate.getDate() + 90); + break; + default: + endDate.setDate(endDate.getDate() + 14); + } + + return { + start, + end: endDate.toISOString(), + }; +} + +/** + * Eindeutigen Schlüssel für Reservierung generieren + */ +function getReservationKey(reservation: ReservationData): string { + return `${reservation.referenceNumber}_${reservation.startDate}_${reservation.endDate}_${reservation.title || ''}`; +} + +/** + * LibreBooking Trigger Node + */ +export class LibreBookingTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'LibreBooking Trigger', + name: 'libreBookingTrigger', + icon: 'file:librebooking.svg', + group: ['trigger'], + version: 1, + description: 'Wird bei neuen oder geänderten Reservierungen in LibreBooking ausgelöst', + subtitle: '={{$parameter["event"]}}', + defaults: { + name: 'LibreBooking Trigger', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'libreBookingApi', + required: true, + }, + ], + polling: true, + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { name: 'Neue Reservierung', value: 'newReservation', description: 'Wird bei neuen Reservierungen ausgelöst' }, + { name: 'Geänderte Reservierung', value: 'updatedReservation', description: 'Wird bei geänderten Reservierungen ausgelöst' }, + { name: 'Alle Reservierungen', value: 'allReservations', description: 'Wird bei neuen und geänderten Reservierungen ausgelöst' }, + ], + default: 'newReservation', + }, + { + displayName: 'Filter', + name: 'filters', + type: 'collection', + placeholder: 'Filter hinzufügen', + default: {}, + options: [ + { displayName: 'Ressourcen-ID', name: 'resourceId', type: 'number', default: '' }, + { displayName: 'Zeitplan-ID', name: 'scheduleId', type: 'number', default: '' }, + { displayName: 'Benutzer-ID', name: 'userId', type: 'number', default: '' }, + ], + }, + { + displayName: 'Zeitfenster', + name: 'timeWindow', + type: 'options', + options: [ + { name: 'Nächste 7 Tage', value: '7days' }, + { name: 'Nächste 14 Tage', value: '14days' }, + { name: 'Nächste 30 Tage', value: '30days' }, + { name: 'Nächste 90 Tage', value: '90days' }, + ], + default: '14days', + }, + { + displayName: 'Optionen', + name: 'options', + type: 'collection', + placeholder: 'Option hinzufügen', + default: {}, + options: [ + { displayName: 'Detaillierte Daten Abrufen', name: 'fetchDetails', type: 'boolean', default: false }, + ], + }, + ], + }; + + async poll(this: IPollFunctions): Promise { + const credentials = await this.getCredentials('libreBookingApi'); + const baseUrl = (credentials.url as string).replace(/\/$/, ''); + const username = credentials.username as string; + const password = credentials.password as string; + + const event = this.getNodeParameter('event') as string; + const filters = this.getNodeParameter('filters', {}) as any; + const timeWindow = this.getNodeParameter('timeWindow', '14days') as string; + const options = this.getNodeParameter('options', {}) as any; + + const workflowStaticData = this.getWorkflowStaticData('node'); + const previousReservations = (workflowStaticData.reservations as Record) || {}; + + let session: LibreBookingSession; + try { + session = await authenticateTrigger(this, baseUrl, username, password); + } catch (error) { + throw error; + } + + try { + const { start, end } = getTimeWindow(timeWindow); + + const reservations = await getReservations( + this, + baseUrl, + session, + start, + end, + filters, + ); + + const returnData: INodeExecutionData[] = []; + const currentReservations: Record = {}; + + for (const reservation of reservations) { + const refNumber = reservation.referenceNumber; + const reservationKey = getReservationKey(reservation); + currentReservations[refNumber] = reservationKey; + + const isNew = !previousReservations[refNumber]; + const isUpdated = previousReservations[refNumber] && previousReservations[refNumber] !== reservationKey; + + let shouldTrigger = false; + let eventType = ''; + + if (event === 'newReservation' && isNew) { + shouldTrigger = true; + eventType = 'new'; + } else if (event === 'updatedReservation' && isUpdated) { + shouldTrigger = true; + eventType = 'updated'; + } else if (event === 'allReservations' && (isNew || isUpdated)) { + shouldTrigger = true; + eventType = isNew ? 'new' : 'updated'; + } + + if (shouldTrigger) { + let reservationData = reservation; + + if (options.fetchDetails) { + try { + reservationData = await getReservationDetails( + this, + baseUrl, + session, + refNumber, + ); + } catch (error) { + reservationData = reservation; + } + } + + returnData.push({ + json: { + ...reservationData, + _eventType: eventType, + _triggeredAt: new Date().toISOString(), + }, + }); + } + } + + workflowStaticData.reservations = currentReservations; + + if (returnData.length === 0) { + return null; + } + + return [returnData]; + + } finally { + await signOutTrigger(this, baseUrl, session); + } + } +} diff --git a/nodes/LibreBookingTrigger/librebooking.svg b/nodes/LibreBookingTrigger/librebooking.svg new file mode 100644 index 0000000..81306a9 --- /dev/null +++ b/nodes/LibreBookingTrigger/librebooking.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b734aa9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3067 @@ +{ + "name": "n8n-nodes-librebooking", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "n8n-nodes-librebooking", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "eslint": "^8.54.0", + "n8n-workflow": "^1.20.0", + "prettier": "^3.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.2" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "n8n-workflow": "*" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@n8n_io/riot-tmpl": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@n8n_io/riot-tmpl/-/riot-tmpl-4.0.1.tgz", + "integrity": "sha512-/zdRbEfTFjsm1NqnpPQHgZTkTdbp5v3VUxGeMA9098sps8jRCTraQkc3AQstJgHUm7ylBXJcIVhnVeLUMWAfwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-riot": "^1.0.0" + } + }, + "node_modules/@n8n/errors": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@n8n/errors/-/errors-0.5.0.tgz", + "integrity": "sha512-0Vk1Eb3Uor+zeF/WVnuhFgJc51wEBTZNBlVQy3mvyr3sGmW86bP1jA7wmRsd0DZbswPwN0vNOl/TmkDTEopOtQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "callsites": "3.1.0" + } + }, + "node_modules/@n8n/tournament": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@n8n/tournament/-/tournament-1.0.6.tgz", + "integrity": "sha512-UGSxYXXVuOX0yL6HTLBStKYwLIa0+JmRKiSZSCMcM2s2Wax984KWT6XIA1TR/27i7yYpDk1MY14KsTPnuEp27A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@n8n_io/riot-tmpl": "^4.0.1", + "ast-types": "^0.16.1", + "esprima-next": "^5.8.4", + "recast": "^0.22.0" + }, + "engines": { + "node": ">=20.15", + "pnpm": ">=9.5" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-riot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-riot/-/eslint-config-riot-1.0.0.tgz", + "integrity": "sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima-next": { + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/esprima-next/-/esprima-next-5.8.4.tgz", + "integrity": "sha512-8nYVZ4ioIH4Msjb/XmhnBdz5WRRBaYqevKa1cv9nGJdCehMbzZCPNEEnqfLCZVetUVrUPEcb5IYyu1GG4hFqgg==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-base64": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", + "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonrepair": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.1.tgz", + "integrity": "sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw==", + "dev": true, + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, + "node_modules/jssha": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", + "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/n8n-workflow": { + "version": "1.120.7", + "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-1.120.7.tgz", + "integrity": "sha512-kTJVxns085po2Tcv9f4bJdKnngxyVnGjA+cQPsNTcZoxM+09R4+lxOWnH5aeAJkRxKEVYZ278/rwF5B6c/mnvg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@n8n/errors": "0.5.0", + "@n8n/tournament": "1.0.6", + "ast-types": "0.16.1", + "callsites": "3.1.0", + "esprima-next": "5.8.4", + "form-data": "4.0.0", + "jmespath": "0.16.0", + "js-base64": "3.7.2", + "jsonrepair": "3.13.1", + "jssha": "3.3.1", + "lodash": "4.17.21", + "luxon": "3.4.4", + "md5": "2.3.0", + "recast": "0.22.0", + "title-case": "3.0.3", + "transliteration": "2.3.5", + "xml2js": "0.6.2", + "zod": "3.25.67" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/recast": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.22.0.tgz", + "integrity": "sha512-5AAx+mujtXijsEavc5lWXBPQqrM4+Dl5qNH96N2aNeuJFUzpiiToKPsxQD/zAIJHspz7zz0maX0PCtCTFVlixQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/title-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", + "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/transliteration": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.3.5.tgz", + "integrity": "sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yargs": "^17.5.1" + }, + "bin": { + "slugify": "dist/bin/slugify", + "transliterate": "dist/bin/transliterate" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..498b5e3 --- /dev/null +++ b/package.json @@ -0,0 +1,82 @@ +{ + "name": "n8n-nodes-librebooking", + "version": "1.0.0", + "description": "n8n Node für LibreBooking - Ressourcen- und Reservierungsverwaltung", + "keywords": [ + "n8n-community-node-package", + "n8n", + "n8n-node", + "workflow", + "automation", + "librebooking", + "booking", + "reservation", + "resource-management", + "room-booking", + "raumbuchung", + "terminbuchung", + "open-source" + ], + "license": "MIT", + "homepage": "https://github.com/your-org/n8n-nodes-librebooking#readme", + "author": { + "name": "LibreBooking n8n Integration", + "email": "support@example.com" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/your-org/n8n-nodes-librebooking.git" + }, + "bugs": { + "url": "https://github.com/your-org/n8n-nodes-librebooking/issues" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc && npm run copy:icons", + "copy:icons": "cp nodes/LibreBooking/librebooking.svg dist/nodes/LibreBooking/ && cp nodes/LibreBookingTrigger/librebooking.svg dist/nodes/LibreBookingTrigger/", + "dev": "tsc --watch", + "clean": "rm -rf dist node_modules", + "rebuild": "npm run clean && npm install && npm run build", + "format": "prettier nodes credentials --write", + "lint": "eslint nodes credentials --ext .ts", + "lint:fix": "eslint nodes credentials --ext .ts --fix", + "prepack": "npm run build", + "prepublishOnly": "npm run lint && npm run build", + "postinstall": "echo 'Installation abgeschlossen. Führe npm run build aus.'", + "test": "ts-node test/test-api.ts", + "link:n8n": "npm link && echo 'Node verlinkt. Starte n8n neu mit: n8n start'", + "unlink": "npm unlink -g n8n-nodes-librebooking" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "n8n": { + "n8nNodesApiVersion": 1, + "credentials": [ + "dist/credentials/LibreBookingApi.credentials.js" + ], + "nodes": [ + "dist/nodes/LibreBooking/LibreBooking.node.js", + "dist/nodes/LibreBookingTrigger/LibreBookingTrigger.node.js" + ] + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "eslint": "^8.54.0", + "n8n-workflow": "^1.20.0", + "prettier": "^3.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.2" + }, + "peerDependencies": { + "n8n-workflow": "*" + }, + "engines": { + "node": ">=18.17.0" + } +} diff --git a/test/test-api.ts b/test/test-api.ts new file mode 100644 index 0000000..f1b5058 --- /dev/null +++ b/test/test-api.ts @@ -0,0 +1,325 @@ +/** + * LibreBooking API Test-Skript + * + * Testet die Authentifizierung und grundlegende API-Operationen + * mit den bereitgestellten Test-Credentials. + * + * Ausführen mit: npx ts-node test/test-api.ts + */ + +const https = require('https'); +const http = require('http'); + +// Test-Credentials +const TEST_CONFIG = { + url: 'https://librebooking.zell-cloud.de', + username: 'sebastian.zell@zell-aufmass.de', + password: 'wanUQ4uVqU6lfP', +}; + +interface LibreBookingSession { + sessionToken: string; + userId: number; + sessionExpires: string; +} + +/** + * HTTP/HTTPS Request Helper + */ +async function makeRequest( + url: string, + method: string, + headers: Record, + body?: any +): Promise { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const isHttps = urlObj.protocol === 'https:'; + const lib = isHttps ? https : http; + + const options = { + hostname: urlObj.hostname, + port: urlObj.port || (isHttps ? 443 : 80), + path: urlObj.pathname + urlObj.search, + method, + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + }; + + const req = lib.request(options, (res: any) => { + let data = ''; + res.on('data', (chunk: string) => (data += chunk)); + res.on('end', () => { + try { + const jsonData = JSON.parse(data); + resolve({ statusCode: res.statusCode, data: jsonData }); + } catch (e) { + resolve({ statusCode: res.statusCode, data }); + } + }); + }); + + req.on('error', reject); + + if (body) { + req.write(JSON.stringify(body)); + } + req.end(); + }); +} + +/** + * Authentifizierung testen + */ +async function testAuthentication(): Promise { + console.log('\n========================================'); + console.log('TEST 1: Authentifizierung'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Authentication/Authenticate`, + 'POST', + {}, + { + username: TEST_CONFIG.username, + password: TEST_CONFIG.password, + } + ); + + if (response.statusCode === 200 && response.data.isAuthenticated) { + console.log('✅ Authentifizierung erfolgreich!'); + console.log(` Session Token: ${response.data.sessionToken.substring(0, 20)}...`); + console.log(` User ID: ${response.data.userId}`); + console.log(` Session läuft ab: ${response.data.sessionExpires}`); + return { + sessionToken: response.data.sessionToken, + userId: response.data.userId, + sessionExpires: response.data.sessionExpires, + }; + } else { + console.log('❌ Authentifizierung fehlgeschlagen!'); + console.log(' Response:', JSON.stringify(response.data, null, 2)); + return null; + } + } catch (error: any) { + console.log('❌ Fehler bei der Authentifizierung:', error.message); + return null; + } +} + +/** + * Alle Reservierungen abrufen + */ +async function testGetReservations(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 2: Reservierungen abrufen'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Reservations/`, + 'GET', + { + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + } + ); + + if (response.statusCode === 200) { + const reservations = response.data.reservations || []; + console.log(`✅ ${reservations.length} Reservierung(en) gefunden`); + + if (reservations.length > 0) { + console.log('\n Erste 3 Reservierungen:'); + reservations.slice(0, 3).forEach((res: any, idx: number) => { + console.log(` ${idx + 1}. ${res.title || 'Ohne Titel'}`); + console.log(` Referenz: ${res.referenceNumber}`); + console.log(` Ressource: ${res.resourceName}`); + console.log(` Zeit: ${res.startDate} - ${res.endDate}`); + console.log(''); + }); + } + } else { + console.log(`❌ Fehler beim Abrufen: Status ${response.statusCode}`); + console.log(' Response:', JSON.stringify(response.data, null, 2)); + } + } catch (error: any) { + console.log('❌ Fehler:', error.message); + } +} + +/** + * Alle Ressourcen abrufen + */ +async function testGetResources(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 3: Ressourcen abrufen'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Resources/`, + 'GET', + { + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + } + ); + + if (response.statusCode === 200) { + const resources = response.data.resources || []; + console.log(`✅ ${resources.length} Ressource(n) gefunden`); + + resources.forEach((res: any, idx: number) => { + console.log(` ${idx + 1}. ${res.name} (ID: ${res.resourceId})`); + if (res.location) console.log(` Standort: ${res.location}`); + console.log(` Status: ${res.statusId === 1 ? 'Verfügbar' : res.statusId === 0 ? 'Versteckt' : 'Nicht verfügbar'}`); + }); + } else { + console.log(`❌ Fehler beim Abrufen: Status ${response.statusCode}`); + } + } catch (error: any) { + console.log('❌ Fehler:', error.message); + } +} + +/** + * Alle Zeitpläne abrufen + */ +async function testGetSchedules(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 4: Zeitpläne abrufen'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Schedules/`, + 'GET', + { + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + } + ); + + if (response.statusCode === 200) { + const schedules = response.data.schedules || []; + console.log(`✅ ${schedules.length} Zeitplan/Zeitpläne gefunden`); + + schedules.forEach((schedule: any, idx: number) => { + console.log(` ${idx + 1}. ${schedule.name} (ID: ${schedule.id})`); + console.log(` Zeitzone: ${schedule.timezone}`); + console.log(` Standard: ${schedule.isDefault ? 'Ja' : 'Nein'}`); + }); + } else { + console.log(`❌ Fehler beim Abrufen: Status ${response.statusCode}`); + } + } catch (error: any) { + console.log('❌ Fehler:', error.message); + } +} + +/** + * Alle Benutzer abrufen + */ +async function testGetUsers(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 5: Benutzer abrufen'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Users/`, + 'GET', + { + 'X-Booked-SessionToken': session.sessionToken, + 'X-Booked-UserId': session.userId.toString(), + } + ); + + if (response.statusCode === 200) { + const users = response.data.users || []; + console.log(`✅ ${users.length} Benutzer gefunden`); + + users.slice(0, 5).forEach((user: any, idx: number) => { + console.log(` ${idx + 1}. ${user.firstName} ${user.lastName}`); + console.log(` E-Mail: ${user.emailAddress}`); + console.log(` ID: ${user.id}`); + }); + + if (users.length > 5) { + console.log(` ... und ${users.length - 5} weitere`); + } + } else { + console.log(`❌ Fehler beim Abrufen: Status ${response.statusCode}`); + } + } catch (error: any) { + console.log('❌ Fehler:', error.message); + } +} + +/** + * Abmelden + */ +async function testSignOut(session: LibreBookingSession): Promise { + console.log('\n========================================'); + console.log('TEST 6: Abmelden'); + console.log('========================================'); + + try { + const response = await makeRequest( + `${TEST_CONFIG.url}/Web/Services/index.php/Authentication/SignOut`, + 'POST', + {}, + { + userId: session.userId, + sessionToken: session.sessionToken, + } + ); + + if (response.statusCode === 200 || response.statusCode === 204) { + console.log('✅ Erfolgreich abgemeldet'); + } else { + console.log(`⚠️ Abmeldung mit Status ${response.statusCode} abgeschlossen`); + } + } catch (error: any) { + console.log('⚠️ Abmeldung fehlgeschlagen (kann ignoriert werden):', error.message); + } +} + +/** + * Hauptfunktion + */ +async function runTests(): Promise { + console.log('\n📝 LibreBooking API Test'); + console.log('======================================'); + console.log(`URL: ${TEST_CONFIG.url}`); + console.log(`User: ${TEST_CONFIG.username}`); + console.log('======================================'); + + // Test 1: Authentifizierung + const session = await testAuthentication(); + + if (!session) { + console.log('\n❌ Tests abgebrochen - Authentifizierung fehlgeschlagen'); + process.exit(1); + } + + // Test 2-5: API-Endpunkte + await testGetReservations(session); + await testGetResources(session); + await testGetSchedules(session); + await testGetUsers(session); + + // Test 6: Abmelden + await testSignOut(session); + + console.log('\n========================================'); + console.log('✅ Alle Tests abgeschlossen!'); + console.log('========================================\n'); +} + +// Tests ausführen +runTests().catch(console.error); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..88c33d7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["ES2019", "ES2020.Promise"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": [ + "nodes/**/*.ts", + "credentials/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "test" + ] +} diff --git a/workflows/example-workflows.json b/workflows/example-workflows.json new file mode 100644 index 0000000..a815b81 --- /dev/null +++ b/workflows/example-workflows.json @@ -0,0 +1,265 @@ +{ + "name": "LibreBooking Beispiel-Workflows", + "description": "Sammlung von Beispiel-Workflows für den LibreBooking n8n Node", + "workflows": [ + { + "name": "1. Alle Reservierungen abrufen", + "description": "Ruft alle Reservierungen der nächsten 14 Tage ab", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "reservation", + "operation": "getAll", + "filters": {} + }, + "name": "Alle Reservierungen", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + } + ], + "connections": { + "Start": { + "main": [[{"node": "Alle Reservierungen", "type": "main", "index": 0}]] + } + } + }, + { + "name": "2. Neue Reservierung erstellen", + "description": "Erstellt eine neue Reservierung für morgen 10:00-11:00 Uhr", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "reservation", + "operation": "create", + "resourceId": 1, + "startDateTime": "={{ $now.plus({days: 1}).set({hour: 10, minute: 0, second: 0}).toISO() }}", + "endDateTime": "={{ $now.plus({days: 1}).set({hour: 11, minute: 0, second: 0}).toISO() }}", + "title": "Automatisch erstellte Reservierung", + "additionalFields": { + "description": "Diese Reservierung wurde automatisch über n8n erstellt" + } + }, + "name": "Reservierung erstellen", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + } + ], + "connections": { + "Start": { + "main": [[{"node": "Reservierung erstellen", "type": "main", "index": 0}]] + } + } + }, + { + "name": "3. Ressourcen-Verfügbarkeit prüfen", + "description": "Prüft die Verfügbarkeit aller Ressourcen", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "resource", + "operation": "getAvailability" + }, + "name": "Verfügbarkeit prüfen", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.available }}", + "value2": true + } + ] + } + }, + "name": "Nur Verfügbare", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [500, 300] + } + ], + "connections": { + "Start": { + "main": [[{"node": "Verfügbarkeit prüfen", "type": "main", "index": 0}]] + }, + "Verfügbarkeit prüfen": { + "main": [[{"node": "Nur Verfügbare", "type": "main", "index": 0}]] + } + } + }, + { + "name": "4. Benutzer-Übersicht", + "description": "Ruft alle Benutzer ab und formatiert sie", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "user", + "operation": "getAll", + "userFilters": {} + }, + "name": "Alle Benutzer", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "return {\n json: {\n name: `${$json.firstName} ${$json.lastName}`,\n email: $json.emailAddress,\n organization: $json.organization || 'Keine',\n lastLogin: $json.lastLogin\n }\n};" + }, + "name": "Formatieren", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [500, 300] + } + ], + "connections": { + "Start": { + "main": [[{"node": "Alle Benutzer", "type": "main", "index": 0}]] + }, + "Alle Benutzer": { + "main": [[{"node": "Formatieren", "type": "main", "index": 0}]] + } + } + }, + { + "name": "5. Trigger: Neue Reservierungen überwachen", + "description": "Trigger-Workflow der bei neuen Reservierungen auslöst", + "nodes": [ + { + "parameters": { + "event": "newReservation", + "filters": {}, + "timeWindow": "14days", + "options": { + "fetchDetails": true + } + }, + "name": "LibreBooking Trigger", + "type": "n8n-nodes-librebooking.libreBookingTrigger", + "typeVersion": 1, + "position": [100, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const reservation = $json;\nreturn {\n json: {\n message: `Neue Reservierung: ${reservation.title || 'Ohne Titel'}`,\n resource: reservation.resourceName,\n start: reservation.startDate,\n end: reservation.endDate,\n user: `${reservation.firstName} ${reservation.lastName}`\n }\n};" + }, + "name": "Nachricht formatieren", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [300, 300] + } + ], + "connections": { + "LibreBooking Trigger": { + "main": [[{"node": "Nachricht formatieren", "type": "main", "index": 0}]] + } + } + }, + { + "name": "6. Täglicher Reservierungsbericht", + "description": "Sendet täglich eine Übersicht der heutigen Reservierungen", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 8 * * *" + } + ] + } + }, + "name": "Täglich 8:00", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1, + "position": [100, 300] + }, + { + "parameters": { + "resource": "reservation", + "operation": "getAll", + "filters": { + "startDateTime": "={{ $now.startOf('day').toISO() }}", + "endDateTime": "={{ $now.endOf('day').toISO() }}" + } + }, + "name": "Heutige Reservierungen", + "type": "n8n-nodes-librebooking.libreBooking", + "typeVersion": 1, + "position": [300, 300], + "credentials": { + "libreBookingApi": "LibreBooking Account" + } + }, + { + "parameters": { + "aggregate": "aggregateAllItemData" + }, + "name": "Zusammenfassen", + "type": "n8n-nodes-base.aggregate", + "typeVersion": 1, + "position": [500, 300] + } + ], + "connections": { + "Täglich 8:00": { + "main": [[{"node": "Heutige Reservierungen", "type": "main", "index": 0}]] + }, + "Heutige Reservierungen": { + "main": [[{"node": "Zusammenfassen", "type": "main", "index": 0}]] + } + } + } + ] +} \ No newline at end of file