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
JavaScript
;
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