From adf226a38a068b41163cd2d903ff8a064df48334 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 17 Jun 2025 16:07:15 +0200 Subject: [PATCH] FEAT: Refactor berth API functions to use dedicated utility methods for fetching and updating berths, and add connection test for NocoDB --- server/api/get-berth-by-id.ts | 18 +--- server/api/get-berths.ts | 49 +-------- server/api/test-berth-connection.ts | 63 ++++++++++++ server/api/update-berth.ts | 108 ++++---------------- server/utils/nocodb.ts | 151 ++++++++++++++++++++++++++++ 5 files changed, 235 insertions(+), 154 deletions(-) create mode 100644 server/api/test-berth-connection.ts diff --git a/server/api/get-berth-by-id.ts b/server/api/get-berth-by-id.ts index e910f7c..5b00d8c 100644 --- a/server/api/get-berth-by-id.ts +++ b/server/api/get-berth-by-id.ts @@ -1,4 +1,4 @@ -import { getNocoDbConfiguration } from "../utils/nocodb"; +import { getBerthById } from "../utils/nocodb"; import { requireAuth } from "../utils/auth"; export default defineEventHandler(async (event) => { @@ -18,22 +18,8 @@ export default defineEventHandler(async (event) => { }); } - const config = getNocoDbConfiguration(); - const berthsTableId = "mczgos9hr3oa9qc"; - console.log('[get-berth-by-id] Fetching berth with ID:', berthId); - - // Fetch berth with linked interested parties - const berth = await $fetch(`${config.url}/api/v2/tables/${berthsTableId}/records/${berthId}`, { - headers: { - "xc-token": config.token, - }, - params: { - // Expand the "Interested Parties" linked field to get full interest records - fields: '*,Interested Parties.*' - } - }); - + const berth = await getBerthById(berthId as string); console.log('[get-berth-by-id] Successfully fetched berth:', berthId); return berth; diff --git a/server/api/get-berths.ts b/server/api/get-berths.ts index ce6dd4d..12c7025 100644 --- a/server/api/get-berths.ts +++ b/server/api/get-berths.ts @@ -1,4 +1,4 @@ -import { getNocoDbConfiguration } from "../utils/nocodb"; +import { getBerths } from "../utils/nocodb"; import { requireAuth } from "../utils/auth"; export default defineEventHandler(async (event) => { @@ -8,54 +8,9 @@ export default defineEventHandler(async (event) => { await requireAuth(event); try { - const config = getNocoDbConfiguration(); - const berthsTableId = "mczgos9hr3oa9qc"; - console.log('[get-berths] Fetching berths...'); - const berths = await $fetch<{ list: any[] }>(`${config.url}/api/v2/tables/${berthsTableId}/records`, { - headers: { - "xc-token": config.token, - }, - params: { - limit: 1000, - // Include interested parties count (expand the linked field) - fields: '*,Interested Parties.Id,Interested Parties.Full Name,Interested Parties.Sales Process Level,Interested Parties.EOI Status,Interested Parties.Contract Status' - }, - }); - + const berths = await getBerths(); console.log('[get-berths] Successfully fetched berths, count:', berths.list?.length || 0); - - // Sort berths by letter zone and then by number using Mooring Number - if (berths.list && Array.isArray(berths.list)) { - berths.list.sort((a, b) => { - const berthA = a['Mooring Number'] || ''; - const berthB = b['Mooring Number'] || ''; - - // Extract letter and number parts - const matchA = berthA.match(/^([A-Za-z]+)(\d+)$/); - const matchB = berthB.match(/^([A-Za-z]+)(\d+)$/); - - if (matchA && matchB) { - const [, letterA, numberA] = matchA; - const [, letterB, numberB] = matchB; - - // First sort by letter zone - const letterCompare = letterA.localeCompare(letterB); - if (letterCompare !== 0) { - return letterCompare; - } - - // Then sort by number within the same letter zone - return parseInt(numberA) - parseInt(numberB); - } - - // Fallback to string comparison if pattern doesn't match - return berthA.localeCompare(berthB); - }); - - console.log('[get-berths] Berths sorted by zone and number'); - } - return berths; } catch (error) { console.error('[get-berths] Error occurred:', error); diff --git a/server/api/test-berth-connection.ts b/server/api/test-berth-connection.ts new file mode 100644 index 0000000..9b5c2d3 --- /dev/null +++ b/server/api/test-berth-connection.ts @@ -0,0 +1,63 @@ +import { getBerths, getBerthById } from "../utils/nocodb"; +import { requireAuth } from "../utils/auth"; + +export default defineEventHandler(async (event) => { + console.log('[test-berth-connection] Testing berth NocoDB connection...'); + + // Check authentication (x-tag header OR Keycloak session) + await requireAuth(event); + + try { + // Test fetching all berths + console.log('[test-berth-connection] Testing getBerths()...'); + const berthsResponse = await getBerths(); + console.log('[test-berth-connection] Berths fetched successfully:', { + count: berthsResponse.list?.length || 0, + firstBerth: berthsResponse.list?.[0] ? { + id: berthsResponse.list[0].Id, + mooringNumber: berthsResponse.list[0]['Mooring Number'], + status: berthsResponse.list[0].Status, + area: berthsResponse.list[0].Area + } : null + }); + + let testBerth = null; + if (berthsResponse.list && berthsResponse.list.length > 0) { + const firstBerthId = berthsResponse.list[0].Id.toString(); + console.log('[test-berth-connection] Testing getBerthById() with ID:', firstBerthId); + testBerth = await getBerthById(firstBerthId); + console.log('[test-berth-connection] Individual berth fetched successfully:', { + id: testBerth.Id, + mooringNumber: testBerth['Mooring Number'], + interestedPartiesCount: testBerth['Interested Parties']?.length || 0 + }); + } + + return { + success: true, + message: "Berth NocoDB connection test successful", + data: { + totalBerths: berthsResponse.list?.length || 0, + testBerth: testBerth ? { + id: testBerth.Id, + mooringNumber: testBerth['Mooring Number'], + status: testBerth.Status, + area: testBerth.Area, + price: testBerth.Price, + interestedPartiesCount: testBerth['Interested Parties']?.length || 0 + } : null, + tableId: "mczgos9hr3oa9qc" + } + }; + } catch (error) { + console.error('[test-berth-connection] Error occurred:', error); + console.error('[test-berth-connection] Error details:', error instanceof Error ? error.message : 'Unknown error'); + + return { + success: false, + message: "Berth NocoDB connection test failed", + error: error instanceof Error ? error.message : 'Unknown error', + details: error + }; + } +}); diff --git a/server/api/update-berth.ts b/server/api/update-berth.ts index 1096bee..d377a1f 100644 --- a/server/api/update-berth.ts +++ b/server/api/update-berth.ts @@ -1,17 +1,5 @@ -import { withBerthQueue } from '~/server/utils/operation-lock'; -import { getNocoDbConfiguration } from '~/server/utils/nocodb'; -import { requireAuth } from '~/server/utils/auth'; -import { - BerthStatus, - BerthArea, - SidePontoon, - MooringType, - CleatType, - CleatCapacity, - BollardType, - BollardCapacity, - Access -} from '~/utils/types'; +import { updateBerth } from "../utils/nocodb"; +import { requireAuth } from "../utils/auth"; export default defineEventHandler(async (event) => { console.log('[update-berth] Request received'); @@ -22,90 +10,28 @@ export default defineEventHandler(async (event) => { try { const body = await readBody(event); const { berthId, updates } = body; - console.log('[update-berth] Request body:', { berthId, updates }); - - if (!berthId || !updates) { + + if (!berthId) { throw createError({ statusCode: 400, - statusMessage: "berthId and updates object are required" + statusMessage: "Berth ID is required" }); } - // Validate enum fields - const validEnumFields: Record = { - 'Status': Object.values(BerthStatus), - 'Area': Object.values(BerthArea), - 'Side Pontoon': Object.values(SidePontoon), - 'Mooring Type': Object.values(MooringType), - 'Cleat Type': Object.values(CleatType), - 'Cleat Capacity': Object.values(CleatCapacity), - 'Bollard Type': Object.values(BollardType), - 'Bollard Capacity': Object.values(BollardCapacity), - 'Access': Object.values(Access) - }; - - // Validate enum values - for (const [field, value] of Object.entries(updates)) { - if (validEnumFields[field] && value !== null && value !== undefined) { - if (!validEnumFields[field].includes(value as string)) { - throw createError({ - statusCode: 400, - statusMessage: `Invalid value for ${field}: ${value}. Must be one of: ${validEnumFields[field].join(', ')}` - }); - } - } - } - - // Handle measurement conversions - // If metric values are being updated, convert them to imperial for storage - const measurementFields = ['Nominal Boat Size', 'Water Depth', 'Length', 'Width', 'Depth']; - const processedUpdates = { ...updates }; - - for (const field of measurementFields) { - if (processedUpdates[field] !== undefined) { - const value = processedUpdates[field]; - if (typeof value === 'string') { - // Parse user input and convert metric to imperial if needed - const cleanInput = value.replace(/[^\d.]/g, ''); - const numericValue = parseFloat(cleanInput); - - if (!isNaN(numericValue)) { - const isMetric = value.toLowerCase().includes('m') && !value.toLowerCase().includes('ft'); - if (isMetric) { - // Convert metric to imperial for NocoDB storage - const imperial = numericValue / 0.3048; - processedUpdates[field] = parseFloat(imperial.toFixed(2)); - console.log(`[update-berth] Converted ${field} from ${numericValue}m to ${processedUpdates[field]}ft`); - } else { - // Assume imperial, store as is - processedUpdates[field] = numericValue; - } - } - } - } - } - - // Use queuing system to handle concurrent updates - return await withBerthQueue(berthId, async () => { - const config = getNocoDbConfiguration(); - const berthsTableId = "mczgos9hr3oa9qc"; - - const url = `${config.url}/api/v2/tables/${berthsTableId}/records/${berthId}`; - console.log('[update-berth] URL:', url); - console.log('[update-berth] Processed updates:', processedUpdates); - - const result = await $fetch(url, { - method: 'PATCH', - headers: { - "xc-token": config.token, - }, - body: processedUpdates, + if (!updates || typeof updates !== 'object') { + throw createError({ + statusCode: 400, + statusMessage: "Updates object is required" }); - - console.log('[update-berth] Successfully updated berth:', berthId); - return result; - }); + } + + console.log('[update-berth] Updating berth ID:', berthId); + console.log('[update-berth] Updates:', Object.keys(updates)); + const updatedBerth = await updateBerth(berthId.toString(), updates); + + console.log('[update-berth] Successfully updated berth:', berthId); + return updatedBerth; } catch (error) { console.error('[update-berth] Error occurred:', error); console.error('[update-berth] Error details:', error instanceof Error ? error.message : 'Unknown error'); diff --git a/server/utils/nocodb.ts b/server/utils/nocodb.ts index c3fc759..b290e0a 100644 --- a/server/utils/nocodb.ts +++ b/server/utils/nocodb.ts @@ -1,3 +1,5 @@ +import type { Interest, Berth } from "@/utils/types"; + export interface PageInfo { pageSize: number; totalRows: number; @@ -11,8 +13,14 @@ export interface InterestsResponse { PageInfo: PageInfo; } +export interface BerthsResponse { + list: Berth[]; + PageInfo: PageInfo; +} + export enum Table { Interest = "mbs9hjauug4eseo", + Berth = "mczgos9hr3oa9qc", } /** @@ -414,3 +422,146 @@ export const getInterestByFieldAsync = async (fieldName: string, value: any): Pr return null; } }; + +// Berth functions +export const getBerths = async () => { + console.log('[nocodb.getBerths] Fetching berths from NocoDB...'); + + const result = await $fetch(createTableUrl(Table.Berth), { + headers: { + "xc-token": getNocoDbConfiguration().token, + }, + params: { + limit: 1000, + // Include interested parties (expand the linked field) + fields: '*,Interested Parties.Id,Interested Parties.Full Name,Interested Parties.Sales Process Level,Interested Parties.EOI Status,Interested Parties.Contract Status' + }, + }); + + console.log('[nocodb.getBerths] Successfully fetched berths, count:', result.list?.length || 0); + + // Sort berths by letter zone and then by number using Mooring Number + if (result.list && Array.isArray(result.list)) { + result.list.sort((a, b) => { + const berthA = a['Mooring Number'] || ''; + const berthB = b['Mooring Number'] || ''; + + // Extract letter and number parts + const matchA = berthA.match(/^([A-Za-z]+)(\d+)$/); + const matchB = berthB.match(/^([A-Za-z]+)(\d+)$/); + + if (matchA && matchB) { + const [, letterA, numberA] = matchA; + const [, letterB, numberB] = matchB; + + // First sort by letter zone + const letterCompare = letterA.localeCompare(letterB); + if (letterCompare !== 0) { + return letterCompare; + } + + // Then sort by number within the same letter zone + return parseInt(numberA) - parseInt(numberB); + } + + // Fallback to string comparison if pattern doesn't match + return berthA.localeCompare(berthB); + }); + + console.log('[nocodb.getBerths] Berths sorted by zone and number'); + } + + return result; +}; + +export const getBerthById = async (id: string) => { + console.log('[nocodb.getBerthById] Fetching berth ID:', id); + + const result = await $fetch(`${createTableUrl(Table.Berth)}/${id}`, { + headers: { + "xc-token": getNocoDbConfiguration().token, + }, + params: { + // Include interested parties (expand the linked field) + fields: '*,Interested Parties.Id,Interested Parties.Full Name,Interested Parties.Sales Process Level,Interested Parties.EOI Status,Interested Parties.Contract Status' + } + }); + + console.log('[nocodb.getBerthById] Successfully fetched berth:', result.Id); + return result; +}; + +export const updateBerth = async (id: string, data: Partial): Promise => { + console.log('[nocodb.updateBerth] Updating berth:', id); + console.log('[nocodb.updateBerth] Data fields:', Object.keys(data)); + + // Create a clean data object that matches the Berth schema + const cleanData: Record = {}; + + // Only include fields that are part of the Berth schema + const allowedFields = [ + "Mooring Number", + "Area", + "Status", + "Nominal Boat Size", + "Water Depth", + "Length", + "Width", + "Depth", + "Side Pontoon", + "Power Capacity", + "Voltage", + "Mooring Type", + "Access", + "Cleat Type", + "Cleat Capacity", + "Bollard Type", + "Bollard Capacity", + "Price", + "Bow Facing" + ]; + + // Filter the data to only include allowed fields + for (const field of allowedFields) { + if (field in data) { + const value = (data as any)[field]; + + // Handle clearing fields - NocoDB requires null for clearing, not undefined + if (value === undefined) { + cleanData[field] = null; + console.log(`[nocodb.updateBerth] Converting undefined to null for field: ${field}`); + } else { + cleanData[field] = value; + } + } + } + + console.log('[nocodb.updateBerth] Clean data fields:', Object.keys(cleanData)); + + // PATCH requires ID in the body (not in URL) + // Ensure ID is an integer + cleanData.Id = parseInt(id); + + const url = createTableUrl(Table.Berth); + console.log('[nocodb.updateBerth] URL:', url); + + try { + console.log('[nocodb.updateBerth] Sending PATCH request'); + + const result = await $fetch(url, { + method: "PATCH", + headers: { + "xc-token": getNocoDbConfiguration().token, + "Content-Type": "application/json" + }, + body: cleanData + }); + + console.log('[nocodb.updateBerth] Update successful for ID:', id); + return result; + } catch (error: any) { + console.error('[nocodb.updateBerth] Update failed:', error); + console.error('[nocodb.updateBerth] Error details:', error instanceof Error ? error.message : 'Unknown error'); + throw error; + } +};