FEAT: Refactor berth API functions to use dedicated utility methods for fetching and updating berths, and add connection test for NocoDB

This commit is contained in:
Matt 2025-06-17 16:07:15 +02:00
parent 0e85cb40bc
commit adf226a38a
5 changed files with 235 additions and 154 deletions

View File

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

View File

@ -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);

View File

@ -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
};
}
});

View File

@ -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<string, string[]> = {
'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)) {
if (!updates || typeof updates !== 'object') {
throw createError({
statusCode: 400,
statusMessage: `Invalid value for ${field}: ${value}. Must be one of: ${validEnumFields[field].join(', ')}`
statusMessage: "Updates object is required"
});
}
}
}
// 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 };
console.log('[update-berth] Updating berth ID:', berthId);
console.log('[update-berth] Updates:', Object.keys(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,
});
const updatedBerth = await updateBerth(berthId.toString(), updates);
console.log('[update-berth] Successfully updated berth:', berthId);
return result;
});
return updatedBerth;
} catch (error) {
console.error('[update-berth] Error occurred:', error);
console.error('[update-berth] Error details:', error instanceof Error ? error.message : 'Unknown error');

View File

@ -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<BerthsResponse>(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<Berth>(`${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<Berth>): Promise<Berth> => {
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<string, any> = {};
// 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<Berth>(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;
}
};