n8n_node_librebooking/nodes/LibreBookingTrigger/LibreBookingTrigger.node.ts

353 lines
8.7 KiB
TypeScript

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<LibreBookingSession> {
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<void> {
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<ReservationData[]> {
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<any> {
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<INodeExecutionData[][] | null> {
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<string, string>) || {};
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<string, string> = {};
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);
}
}
}