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:
parent
0e85cb40bc
commit
adf226a38a
|
|
@ -1,4 +1,4 @@
|
||||||
import { getNocoDbConfiguration } from "../utils/nocodb";
|
import { getBerthById } from "../utils/nocodb";
|
||||||
import { requireAuth } from "../utils/auth";
|
import { requireAuth } from "../utils/auth";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
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);
|
console.log('[get-berth-by-id] Fetching berth with ID:', berthId);
|
||||||
|
const berth = await getBerthById(berthId as string);
|
||||||
// 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.*'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[get-berth-by-id] Successfully fetched berth:', berthId);
|
console.log('[get-berth-by-id] Successfully fetched berth:', berthId);
|
||||||
|
|
||||||
return berth;
|
return berth;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getNocoDbConfiguration } from "../utils/nocodb";
|
import { getBerths } from "../utils/nocodb";
|
||||||
import { requireAuth } from "../utils/auth";
|
import { requireAuth } from "../utils/auth";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|
@ -8,54 +8,9 @@ export default defineEventHandler(async (event) => {
|
||||||
await requireAuth(event);
|
await requireAuth(event);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = getNocoDbConfiguration();
|
|
||||||
const berthsTableId = "mczgos9hr3oa9qc";
|
|
||||||
|
|
||||||
console.log('[get-berths] Fetching berths...');
|
console.log('[get-berths] Fetching berths...');
|
||||||
const berths = await $fetch<{ list: any[] }>(`${config.url}/api/v2/tables/${berthsTableId}/records`, {
|
const berths = await getBerths();
|
||||||
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'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[get-berths] Successfully fetched berths, count:', berths.list?.length || 0);
|
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;
|
return berths;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[get-berths] Error occurred:', error);
|
console.error('[get-berths] Error occurred:', error);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,17 +1,5 @@
|
||||||
import { withBerthQueue } from '~/server/utils/operation-lock';
|
import { updateBerth } from "../utils/nocodb";
|
||||||
import { getNocoDbConfiguration } from '~/server/utils/nocodb';
|
import { requireAuth } from "../utils/auth";
|
||||||
import { requireAuth } from '~/server/utils/auth';
|
|
||||||
import {
|
|
||||||
BerthStatus,
|
|
||||||
BerthArea,
|
|
||||||
SidePontoon,
|
|
||||||
MooringType,
|
|
||||||
CleatType,
|
|
||||||
CleatCapacity,
|
|
||||||
BollardType,
|
|
||||||
BollardCapacity,
|
|
||||||
Access
|
|
||||||
} from '~/utils/types';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
console.log('[update-berth] Request received');
|
console.log('[update-berth] Request received');
|
||||||
|
|
@ -22,90 +10,28 @@ export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const { berthId, updates } = body;
|
const { berthId, updates } = body;
|
||||||
console.log('[update-berth] Request body:', { berthId, updates });
|
|
||||||
|
if (!berthId) {
|
||||||
if (!berthId || !updates) {
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: "berthId and updates object are required"
|
statusMessage: "Berth ID is required"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate enum fields
|
if (!updates || typeof updates !== 'object') {
|
||||||
const validEnumFields: Record<string, string[]> = {
|
throw createError({
|
||||||
'Status': Object.values(BerthStatus),
|
statusCode: 400,
|
||||||
'Area': Object.values(BerthArea),
|
statusMessage: "Updates object is required"
|
||||||
'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,
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('[update-berth] Error occurred:', error);
|
console.error('[update-berth] Error occurred:', error);
|
||||||
console.error('[update-berth] Error details:', error instanceof Error ? error.message : 'Unknown error');
|
console.error('[update-berth] Error details:', error instanceof Error ? error.message : 'Unknown error');
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { Interest, Berth } from "@/utils/types";
|
||||||
|
|
||||||
export interface PageInfo {
|
export interface PageInfo {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
totalRows: number;
|
totalRows: number;
|
||||||
|
|
@ -11,8 +13,14 @@ export interface InterestsResponse {
|
||||||
PageInfo: PageInfo;
|
PageInfo: PageInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BerthsResponse {
|
||||||
|
list: Berth[];
|
||||||
|
PageInfo: PageInfo;
|
||||||
|
}
|
||||||
|
|
||||||
export enum Table {
|
export enum Table {
|
||||||
Interest = "mbs9hjauug4eseo",
|
Interest = "mbs9hjauug4eseo",
|
||||||
|
Berth = "mczgos9hr3oa9qc",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -414,3 +422,146 @@ export const getInterestByFieldAsync = async (fieldName: string, value: any): Pr
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue