const axios = require('axios'); const config = require('../config'); const logger = require('../utils/logger'); class NocoDBService { constructor() { this.apiUrl = config.nocodb.apiUrl; this.apiToken = config.nocodb.apiToken; this.projectId = config.nocodb.projectId; this.timeout = 10000; // 10 seconds // Create axios instance with defaults this.client = axios.create({ baseURL: this.apiUrl, timeout: this.timeout, headers: { 'xc-token': this.apiToken, 'Content-Type': 'application/json' } }); // Add response interceptor for error handling this.client.interceptors.response.use( response => response, error => { logger.error('NocoDB API Error:', { message: error.message, url: error.config?.url, method: error.config?.method, status: error.response?.status, data: error.response?.data }); throw error; } ); } // Build table URL getTableUrl(tableId) { return `/db/data/v1/${this.projectId}/${tableId}`; } // Get all records from a table async getAll(tableId, params = {}) { const url = this.getTableUrl(tableId); const response = await this.client.get(url, { params }); return response.data; } // Get ALL records from a table using pagination async getAllPaginated(tableId, params = {}) { try { let allRecords = []; let offset = 0; const limit = params.limit || 100; let hasMore = true; while (hasMore) { const response = await this.getAll(tableId, { ...params, limit: limit, offset: offset }); const records = response.list || []; allRecords = allRecords.concat(records); // Check if there are more records hasMore = records.length === limit; offset += limit; // Safety check to prevent infinite loops if (offset > 10000) { logger.warn(`Reached maximum offset limit while fetching records from table ${tableId}`); break; } } logger.info(`Fetched ${allRecords.length} total records from table ${tableId}`); return { list: allRecords, pageInfo: { totalRows: allRecords.length, page: 1, pageSize: allRecords.length, isFirstPage: true, isLastPage: true } }; } catch (error) { logger.error('Error fetching paginated records:', error); throw error; } } // Get single record async getById(tableId, recordId) { const url = `${this.getTableUrl(tableId)}/${recordId}`; const response = await this.client.get(url); return response.data; } // Create record async create(sheetId, data) { try { // Explicitly remove any ID field to prevent NocoDB conflicts const cleanData = { ...data }; // Remove all possible ID field variations delete cleanData.ID; delete cleanData.id; delete cleanData.Id; delete cleanData.iD; // Remove any undefined values to prevent issues Object.keys(cleanData).forEach(key => { if (cleanData[key] === undefined) { delete cleanData[key]; } }); logger.info(`Creating record in sheet ${sheetId}`); logger.info(`Data being sent to NocoDB:`, JSON.stringify(cleanData, null, 2)); const url = this.getTableUrl(sheetId); const response = await this.client.post(url, cleanData); logger.info(`Create response status: ${response.status}`); logger.info(`Create response:`, JSON.stringify(response.data, null, 2)); return response.data; } catch (error) { logger.error('Error creating record:', error); logger.error('Error response data:', JSON.stringify(error.response?.data, null, 2)); logger.error('Error response status:', error.response?.status); logger.error('Error response headers:', JSON.stringify(error.response?.headers, null, 2)); logger.error('Request URL:', error.config?.url); logger.error('Request method:', error.config?.method); logger.error('Request data:', JSON.stringify(error.config?.data, null, 2)); throw error; } } // Update record async update(tableId, recordId, data) { const url = `${this.getTableUrl(tableId)}/${recordId}`; const response = await this.client.patch(url, data); return response.data; } // Delete record async delete(tableId, recordId) { const url = `${this.getTableUrl(tableId)}/${recordId}`; const response = await this.client.delete(url); return response.data; } // Get locations with proper filtering async getLocations(params = {}) { // For locations, we want all records by default, so use getAllPaginated // unless specific limit/offset are provided if (!params.limit && !params.offset) { return this.getAllPaginated(config.nocodb.tableId, params); } const defaultParams = { limit: 1000, offset: 0, ...params }; return this.getAll(config.nocodb.tableId, defaultParams); } // Get user by email async getUserByEmail(email) { if (!config.nocodb.loginSheetId) { throw new Error('Login sheet not configured'); } const response = await this.getAll(config.nocodb.loginSheetId, { where: `(Email,eq,${email})`, limit: 1 }); return response.list?.[0] || null; } // Get latest settings async getLatestSettings() { if (!config.nocodb.settingsSheetId) { return null; } const response = await this.getAll(config.nocodb.settingsSheetId, { sort: '-created_at', limit: 1 }); return response.list?.[0] || null; } // Get settings with walk sheet data async getWalkSheetSettings() { if (!config.nocodb.settingsSheetId) { return null; } const response = await this.getAll(config.nocodb.settingsSheetId, { sort: '-created_at', limit: 20 }); // Find first row with walk sheet data const settings = response.list?.find(row => row.walk_sheet_title || row.walk_sheet_subtitle || row.walk_sheet_footer || row.qr_code_1_url || row.qr_code_2_url || row.qr_code_3_url ) || response.list?.[0]; return settings || null; } } // Export singleton instance module.exports = new NocoDBService();