UNPKG

n8n-nodes-nextcloud-tables

Version:

Production-Ready n8n Node für Nextcloud Tables - Vollständige API-Abdeckung mit erweiterten Filtern, Multi-Column-Sorting, CSV-Import und professioneller Datenvalidierung

664 lines 29.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RowHandler = void 0; const api_helper_1 = require("../helpers/api.helper"); class RowHandler { static async execute(context, operation, itemIndex) { switch (operation) { case 'getAll': return this.getAll(context, itemIndex); case 'get': return this.get(context, itemIndex); case 'create': return this.create(context, itemIndex); case 'createAIFriendly': return this.createAIFriendly(context, itemIndex); case 'getAllAIFriendly': return this.getAllAIFriendly(context, itemIndex); case 'update': return this.update(context, itemIndex); case 'updateAIFriendly': return this.updateAIFriendly(context, itemIndex); default: throw new Error(`Unbekannte Operation: ${operation}`); } } /** * Alle Zeilen abrufen */ static async getAll(context, itemIndex) { const nodeCollection = context.getNodeParameter('nodeCollection', itemIndex); const additionalOptions = context.getNodeParameter('additionalOptions', itemIndex, {}); const limit = additionalOptions.limit || 50; const offset = additionalOptions.offset || 0; let endpoint; let tableId; if (nodeCollection === 'tables') { tableId = api_helper_1.ApiHelper.getResourceId(context.getNodeParameter('tableId', itemIndex)); endpoint = `/tables/${tableId}/rows`; } else { const viewId = api_helper_1.ApiHelper.getResourceId(context.getNodeParameter('viewId', itemIndex)); endpoint = `/views/${viewId}/rows`; // Für Views müssen wir die Tabellen-ID ermitteln für Spalten-Info try { const view = await api_helper_1.ApiHelper.makeApiRequest(context, 'GET', `/views/${viewId}`); tableId = view.tableId; } catch (error) { // Fallback ohne Spalten-Info tableId = 0; } } // Basis Query-Parameter const queryParams = { limit: limit.toString(), offset: offset.toString(), }; // Erweiterte Filter, Sortierung und Suche if (additionalOptions.useFilters) { const filters = context.getNodeParameter('filters.filter', itemIndex, []); this.applyFilters(queryParams, filters); } if (additionalOptions.useSorting) { const sorting = context.getNodeParameter('sorting.sort', itemIndex, []); this.applySorting(queryParams, sorting); } if (additionalOptions.useSearch) { const search = context.getNodeParameter('search', itemIndex, {}); this.applySearch(queryParams, search); } try { const rows = await api_helper_1.ApiHelper.makeApiRequest(context, 'GET', endpoint, queryParams); // Optional: Spalten-Info laden für bessere Datenformatierung bei der Ausgabe let columns = []; if (tableId > 0) { columns = await api_helper_1.ApiHelper.getTableColumns(context, tableId); } // Client-seitige Nachbearbeitung falls erforderlich let processedRows = rows; // Client-seitige Filterung falls Server-API nicht alle Filter unterstützt if (additionalOptions.useFilters) { const filters = context.getNodeParameter('filters.filter', itemIndex, []); processedRows = this.applyClientSideFilters(processedRows, filters, columns); } // Client-seitige Suche falls Server-API nicht alle Suchoptionen unterstützt if (additionalOptions.useSearch) { const search = context.getNodeParameter('search', itemIndex, {}); processedRows = this.applyClientSideSearch(processedRows, search, columns); } // Zeilen für bessere Lesbarkeit formatieren return this.formatRowsForOutput(processedRows, columns); } catch (error) { api_helper_1.ApiHelper.handleApiError(error); } } /** * Wendet Filter auf Query-Parameter an */ static applyFilters(queryParams, filters) { if (!filters || filters.length === 0) { return; } // Nextcloud Tables API Filter-Format (falls unterstützt) const filterQueries = []; for (const filter of filters) { if (filter.columnId && filter.operator) { let filterQuery = ''; switch (filter.operator) { case 'equals': filterQuery = `${filter.columnId}=${encodeURIComponent(filter.value || '')}`; break; case 'not_equals': filterQuery = `${filter.columnId}!=${encodeURIComponent(filter.value || '')}`; break; case 'greater_than': filterQuery = `${filter.columnId}>${encodeURIComponent(filter.value || '')}`; break; case 'greater_equal': filterQuery = `${filter.columnId}>=${encodeURIComponent(filter.value || '')}`; break; case 'less_than': filterQuery = `${filter.columnId}<${encodeURIComponent(filter.value || '')}`; break; case 'less_equal': filterQuery = `${filter.columnId}<=${encodeURIComponent(filter.value || '')}`; break; case 'contains': filterQuery = `${filter.columnId}~${encodeURIComponent(filter.value || '')}`; break; case 'is_empty': filterQuery = `${filter.columnId}=null`; break; case 'is_not_empty': filterQuery = `${filter.columnId}!=null`; break; } if (filterQuery) { filterQueries.push(filterQuery); } } } if (filterQueries.length > 0) { queryParams['filter'] = filterQueries.join('&'); } } /** * Wendet Sortierung auf Query-Parameter an */ static applySorting(queryParams, sorting) { if (!sorting || sorting.length === 0) { return; } const sortQueries = []; for (const sort of sorting) { if (sort.columnId && sort.direction) { const direction = sort.direction === 'DESC' ? '-' : ''; sortQueries.push(`${direction}${sort.columnId}`); } } if (sortQueries.length > 0) { queryParams['sort'] = sortQueries.join(','); } } /** * Wendet Suche auf Query-Parameter an */ static applySearch(queryParams, search) { if (!search || !search.term) { return; } queryParams['search'] = encodeURIComponent(search.term); if (search.searchColumns && search.searchColumns.length > 0) { queryParams['searchColumns'] = search.searchColumns.join(','); } if (search.caseSensitive) { queryParams['caseSensitive'] = 'true'; } } /** * Client-seitige Filterung für erweiterte Filter-Optionen */ static applyClientSideFilters(rows, filters, columns) { if (!filters || filters.length === 0) { return rows; } return rows.filter(row => { return filters.every(filter => { if (!filter.columnId || !filter.operator) { return true; } const columnId = parseInt(filter.columnId, 10); const rowData = row.data?.find(d => d.columnId === columnId); const value = rowData?.value; const filterValue = filter.value; return this.evaluateFilter(value, filter.operator, filterValue); }); }); } /** * Client-seitige Suche für erweiterte Suchoptionen */ static applyClientSideSearch(rows, search, columns) { if (!search || !search.term) { return rows; } const searchTerm = search.caseSensitive ? search.term : search.term.toLowerCase(); const searchColumns = search.searchColumns || []; return rows.filter(row => { if (!row.data) { return false; } // Bestimme welche Spalten durchsucht werden sollen let columnsToSearch = columns; if (searchColumns.length > 0) { columnsToSearch = columns.filter(col => searchColumns.includes(col.id.toString())); } // Durchsuche nur Text-Spalten const textColumns = columnsToSearch.filter(col => col.type === 'text'); return textColumns.some(column => { const rowData = row.data.find(d => d.columnId === column.id); if (!rowData || !rowData.value) { return false; } const cellValue = search.caseSensitive ? String(rowData.value) : String(rowData.value).toLowerCase(); return cellValue.includes(searchTerm); }); }); } /** * Evaluiert eine Filter-Bedingung */ static evaluateFilter(value, operator, filterValue) { switch (operator) { case 'equals': return value == filterValue; case 'not_equals': return value != filterValue; case 'greater_than': return this.compareValues(value, filterValue) > 0; case 'greater_equal': return this.compareValues(value, filterValue) >= 0; case 'less_than': return this.compareValues(value, filterValue) < 0; case 'less_equal': return this.compareValues(value, filterValue) <= 0; case 'contains': return String(value || '').toLowerCase().includes(String(filterValue || '').toLowerCase()); case 'starts_with': return String(value || '').toLowerCase().startsWith(String(filterValue || '').toLowerCase()); case 'ends_with': return String(value || '').toLowerCase().endsWith(String(filterValue || '').toLowerCase()); case 'is_empty': return value === null || value === undefined || value === ''; case 'is_not_empty': return value !== null && value !== undefined && value !== ''; default: return true; } } /** * Vergleicht zwei Werte für numerische/alphanumerische Sortierung */ static compareValues(a, b) { // Numerischer Vergleich falls beide Zahlen sind const numA = parseFloat(a); const numB = parseFloat(b); if (!isNaN(numA) && !isNaN(numB)) { return numA - numB; } // Datum-Vergleich falls beide Daten sind const dateA = new Date(a); const dateB = new Date(b); if (!isNaN(dateA.getTime()) && !isNaN(dateB.getTime())) { return dateA.getTime() - dateB.getTime(); } // String-Vergleich return String(a || '').localeCompare(String(b || '')); } /** * Eine einzelne Zeile abrufen (über clientseitige Filterung) * Da die Nextcloud Tables API keinen direkten Einzelzeilen-Abruf unterstützt, * werden alle Zeilen abgerufen und dann gefiltert. */ static async get(context, itemIndex) { const tableId = api_helper_1.ApiHelper.getResourceId(context.getNodeParameter('tableId', itemIndex)); const rowId = api_helper_1.ApiHelper.validateRowId(context.getNodeParameter('rowId', itemIndex)); try { // Alle Zeilen der Tabelle abrufen const allRows = await api_helper_1.ApiHelper.makeApiRequest(context, 'GET', `/tables/${tableId}/rows`); // Die gewünschte Zeile finden const targetRow = allRows.find(row => row.id === rowId); if (!targetRow) { throw new Error(`Zeile mit ID ${rowId} wurde in Tabelle ${tableId} nicht gefunden`); } // Spalten-Info für bessere Formatierung laden const columns = await api_helper_1.ApiHelper.getTableColumns(context, tableId); return this.formatRowForOutput(targetRow, columns); } catch (error) { api_helper_1.ApiHelper.handleApiError(error); } } /** * Eine neue Zeile erstellen */ static async create(context, itemIndex) { const nodeCollection = context.getNodeParameter('nodeCollection', itemIndex); const dataArray = context.getNodeParameter('data.column', itemIndex, []); // Formatiere die Daten für die API const data = {}; for (const item of dataArray) { if (item.columnId && item.value !== undefined) { data[item.columnId] = item.value; } } let endpoint; let tableId; if (nodeCollection === 'tables') { tableId = api_helper_1.ApiHelper.getResourceId(context.getNodeParameter('tableId', itemIndex)); endpoint = `/tables/${tableId}/rows`; } else { const viewId = api_helper_1.ApiHelper.getResourceId(context.getNodeParameter('viewId', itemIndex)); endpoint = `/views/${viewId}/rows`; // Für Views müssen wir die Tabellen-ID ermitteln try { const view = await api_helper_1.ApiHelper.makeApiRequest(context, 'GET', `/views/${viewId}`); tableId = view.tableId; } catch (error) { // Fallback: Erstellen ohne Spalten-Info const formattedData = api_helper_1.ApiHelper.formatRowDataSimple(data); return await api_helper_1.ApiHelper.makeApiRequest(context, 'POST', endpoint, { data: formattedData }); } } // Spalten-Info laden für korrekte Datenformatierung const columns = await api_helper_1.ApiHelper.getTableColumns(context, tableId); // Erweiterte Formatierung mit Validierung const formatOptions = { validateSelections: true, dateTimeFormat: 'iso' }; try { const formattedData = api_helper_1.ApiHelper.formatRowData(data, columns, formatOptions); const result = await api_helper_1.ApiHelper.makeApiRequest(context, 'POST', endpoint, { data: formattedData }); return this.formatRowForOutput(result, columns); } catch (error) { api_helper_1.ApiHelper.handleApiError(error); } } /** * Eine Zeile aktualisieren */ static async update(context, itemIndex) { const tableId = api_helper_1.ApiHelper.getResourceId(context.getNodeParameter('tableId', itemIndex)); const rowId = api_helper_1.ApiHelper.validateRowId(context.getNodeParameter('rowId', itemIndex)); const dataArray = context.getNodeParameter('data.column', itemIndex, []); // Formatiere die Daten für die API const data = {}; for (const item of dataArray) { if (item.columnId && item.value !== undefined) { data[item.columnId] = item.value; } } // Spalten-Info laden für korrekte Datenformatierung const columns = await api_helper_1.ApiHelper.getTableColumns(context, tableId); // Erweiterte Formatierung mit Validierung const formatOptions = { validateSelections: true, dateTimeFormat: 'iso' }; try { const formattedData = api_helper_1.ApiHelper.formatRowData(data, columns, formatOptions); if (Object.keys(formattedData).length === 0) { throw new Error('Mindestens eine Spalte muss für die Aktualisierung angegeben werden'); } const result = await api_helper_1.ApiHelper.makeApiRequest(context, 'PUT', `/tables/${tableId}/rows/${rowId}`, { data: formattedData }); return this.formatRowForOutput(result, columns); } catch (error) { api_helper_1.ApiHelper.handleApiError(error); } } /** * Formatiert eine einzelne Zeile für bessere Ausgabe */ static formatRowForOutput(row, columns) { const formatted = { id: row.id, tableId: row.tableId, createdBy: row.createdBy, createdAt: row.createdAt, lastEditBy: row.lastEditBy, lastEditAt: row.lastEditAt, data: {} }; // Daten mit Spaltennamen anstatt IDs formatieren if (row.data && Array.isArray(row.data)) { for (const item of row.data) { const column = columns.find(col => col.id === item.columnId); const columnName = column?.title || `column_${item.columnId}`; formatted.data[columnName] = this.formatValueForOutput(item.value, column); } } return formatted; } /** * Formatiert mehrere Zeilen für bessere Ausgabe */ static formatRowsForOutput(rows, columns) { return rows.map(row => this.formatRowForOutput(row, columns)); } /** * Formatiert einen Wert für die Ausgabe basierend auf Spaltentyp */ static formatValueForOutput(value, column) { if (value === null || value === undefined) { return null; } if (!column) { return value; } // Formatierung basierend auf Spalten-Typ switch (column.type) { case 'datetime': // Datum formatieren if (typeof value === 'string' && value) { try { return new Date(value).toISOString(); } catch { return value; } } return value; case 'number': // Nummer formatieren if (typeof value === 'string' && value) { const num = parseFloat(value); return isNaN(num) ? value : num; } return value; case 'selection': // Selection-Werte sind normalerweise schon korrekt formatiert return value; default: return value; } } // ============================================== // AI-FRIENDLY METHODS - Optimiert für KI Agents // ============================================== /** * AI-Friendly Zeile erstellen * Alle Parameter sind durch fixedCollection gleichzeitig verfügbar */ static async createAIFriendly(context, itemIndex) { try { // Extrahiere Source-Konfiguration const sourceConfig = context.getNodeParameter('sourceConfig', itemIndex); if (!sourceConfig?.source) { throw new Error('Datenquelle-Konfiguration ist erforderlich'); } const { source } = sourceConfig; let endpoint; let tableId; // Bestimme API-Endpunkt basierend auf Quell-Typ if (source.type === 'table') { if (!source.tableId) { throw new Error('Tabellen-ID ist erforderlich wenn type="table"'); } tableId = parseInt(source.tableId); endpoint = `/tables/${tableId}/rows`; } else if (source.type === 'view') { if (!source.viewId) { throw new Error('View-ID ist erforderlich wenn type="view"'); } const viewId = parseInt(source.viewId); endpoint = `/views/${viewId}/rows`; // Für Views müssen wir die Tabellen-ID ermitteln const view = await api_helper_1.ApiHelper.makeApiRequest(context, 'GET', `/views/${viewId}`); tableId = view.tableId; } else { throw new Error(`Ungültiger Quell-Typ: ${source.type}`); } // Extrahiere Zeilen-Daten const rowDataConfig = context.getNodeParameter('rowDataConfig', itemIndex); if (!rowDataConfig?.data?.columns || rowDataConfig.data.columns.length === 0) { throw new Error('Mindestens eine Spalte mit Daten ist erforderlich'); } // Konvertiere AI-Friendly Format zu Nextcloud API Format const data = {}; for (const columnEntry of rowDataConfig.data.columns) { if (columnEntry.column?.columnId) { data[columnEntry.column.columnId] = columnEntry.column.value || ''; } } // Erstelle Zeile const response = await api_helper_1.ApiHelper.makeApiRequest(context, 'POST', endpoint, { data }); // Formatiere Ausgabe mit Spalten-Informationen const columns = await api_helper_1.ApiHelper.getTableColumns(context, tableId); return this.formatRowForOutput(response, columns); } catch (error) { api_helper_1.ApiHelper.handleApiError(error); } } /** * AI-Friendly Alle Zeilen abrufen mit erweiterten Optionen * Alle Filter/Sortierung/Suche Parameter gleichzeitig verfügbar */ static async getAllAIFriendly(context, itemIndex) { try { // Extrahiere Source-Konfiguration const sourceConfig = context.getNodeParameter('sourceConfig', itemIndex); if (!sourceConfig?.source) { throw new Error('Datenquelle-Konfiguration ist erforderlich'); } const { source } = sourceConfig; let endpoint; let tableId; // Bestimme API-Endpunkt basierend auf Quell-Typ if (source.type === 'table') { if (!source.tableId) { throw new Error('Tabellen-ID ist erforderlich wenn type="table"'); } tableId = parseInt(source.tableId); endpoint = `/tables/${tableId}/rows`; } else if (source.type === 'view') { if (!source.viewId) { throw new Error('View-ID ist erforderlich wenn type="view"'); } const viewId = parseInt(source.viewId); endpoint = `/views/${viewId}/rows`; // Für Views müssen wir die Tabellen-ID ermitteln const view = await api_helper_1.ApiHelper.makeApiRequest(context, 'GET', `/views/${viewId}`); tableId = view.tableId; } else { throw new Error(`Ungültiger Quell-Typ: ${source.type}`); } // Extrahiere Query-Optionen const queryConfig = context.getNodeParameter('queryConfig', itemIndex, {}); // Baue Query-Parameter const queryParams = {}; // Pagination if (queryConfig.query?.pagination?.settings) { const { limit, offset } = queryConfig.query.pagination.settings; if (limit !== undefined) queryParams.limit = limit.toString(); if (offset !== undefined) queryParams.offset = offset.toString(); } else { // Standard-Werte queryParams.limit = '50'; queryParams.offset = '0'; } // Filter if (queryConfig.query?.filters && queryConfig.query.filters.length > 0) { const filters = queryConfig.query.filters .map(f => f.filter) .filter(f => f?.columnId && f?.operator); this.applyFilters(queryParams, filters); } // Sortierung if (queryConfig.query?.sorting && queryConfig.query.sorting.length > 0) { const sorting = queryConfig.query.sorting .map(s => s.sort) .filter(s => s?.columnId && s?.direction); this.applySorting(queryParams, sorting); } // Suche if (queryConfig.query?.search?.settings?.term) { const searchSettings = queryConfig.query.search.settings; const search = { term: searchSettings.term, searchColumns: searchSettings.columns ? searchSettings.columns.split(',').map(id => id.trim()) : [], caseSensitive: searchSettings.caseSensitive }; this.applySearch(queryParams, search); } // Führe API-Abfrage aus - mit useQueryParams = true const rows = await api_helper_1.ApiHelper.makeApiRequest(context, 'GET', endpoint, queryParams, true // useQueryParams für GET-Request ); // Spalten-Info laden für bessere Datenformatierung const columns = await api_helper_1.ApiHelper.getTableColumns(context, tableId); // Client-seitige Nachbearbeitung falls erforderlich let processedRows = rows; // Client-seitige Filterung falls Server-API nicht alle Filter unterstützt if (queryConfig.query?.filters && queryConfig.query.filters.length > 0) { const filters = queryConfig.query.filters .map(f => f.filter) .filter(f => f?.columnId && f?.operator); processedRows = this.applyClientSideFilters(processedRows, filters, columns); } // Client-seitige Suche falls Server-API nicht alle Suchoptionen unterstützt if (queryConfig.query?.search?.settings?.term) { const searchSettings = queryConfig.query.search.settings; const search = { term: searchSettings.term, searchColumns: searchSettings.columns ? searchSettings.columns.split(',').map(id => id.trim()) : [], caseSensitive: searchSettings.caseSensitive }; processedRows = this.applyClientSideSearch(processedRows, search, columns); } // Zeilen für bessere Lesbarkeit formatieren return this.formatRowsForOutput(processedRows, columns); } catch (error) { api_helper_1.ApiHelper.handleApiError(error); } } /** * AI-Friendly Zeile aktualisieren * Alle Parameter durch fixedCollection gleichzeitig verfügbar */ static async updateAIFriendly(context, itemIndex) { try { // Extrahiere Update-Konfiguration const updateDataConfig = context.getNodeParameter('updateDataConfig', itemIndex); if (!updateDataConfig?.update) { throw new Error('Update-Konfiguration ist erforderlich'); } const { update } = updateDataConfig; if (!update.rowId) { throw new Error('Zeilen-ID ist erforderlich'); } if (!update.tableId) { throw new Error('Tabellen-ID ist erforderlich'); } if (!update.columns || update.columns.length === 0) { throw new Error('Mindestens eine Spalte mit Daten ist erforderlich'); } const tableId = parseInt(update.tableId); const rowId = parseInt(update.rowId); // Konvertiere AI-Friendly Format zu Nextcloud API Format const data = {}; for (const columnEntry of update.columns) { if (columnEntry.column?.columnId) { data[columnEntry.column.columnId] = columnEntry.column.value || ''; } } // Aktualisiere Zeile const endpoint = `/tables/${tableId}/rows/${rowId}`; const response = await api_helper_1.ApiHelper.makeApiRequest(context, 'PUT', endpoint, { data }); // Formatiere Ausgabe mit Spalten-Informationen const columns = await api_helper_1.ApiHelper.getTableColumns(context, tableId); return this.formatRowForOutput(response, columns); } catch (error) { api_helper_1.ApiHelper.handleApiError(error); } } } exports.RowHandler = RowHandler; //# sourceMappingURL=row.handler.js.map