monacousa-portal/server/utils/nocodb.ts

385 lines
13 KiB
TypeScript

import type { Member, MembershipStatus, MemberResponse, NocoDBSettings } from '~/utils/types';
// Data normalization functions
export const normalizePersonName = (name: string): string => {
if (!name) return 'Unknown';
// Trim whitespace and normalize case
return name.trim()
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
};
// Pagination interface
export interface PageInfo {
pageSize: number;
totalRows: number;
isFirstPage: boolean;
isLastPage: boolean;
page: number;
}
// Response interfaces
export interface EntityResponse<T> {
list: T[];
PageInfo: PageInfo;
}
// Table ID enumeration - Replace with your actual table IDs
export enum Table {
Members = "members-table-id", // Will be configured via admin panel
}
/**
* Convert date from DD-MM-YYYY format to YYYY-MM-DD format for PostgreSQL
*/
const convertDateFormat = (dateString: string): string => {
if (!dateString) return dateString;
// If it's already in ISO format or contains 'T', return as is
if (dateString.includes('T') || dateString.match(/^\d{4}-\d{2}-\d{2}/)) {
return dateString;
}
// Handle DD-MM-YYYY format
const ddmmyyyyMatch = dateString.match(/^(\d{1,2})-(\d{1,2})-(\d{4})$/);
if (ddmmyyyyMatch) {
const [, day, month, year] = ddmmyyyyMatch;
const convertedDate = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
console.log(`[convertDateFormat] Converted ${dateString} to ${convertedDate}`);
return convertedDate;
}
// Handle DD/MM/YYYY format
const ddmmyyyySlashMatch = dateString.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
if (ddmmyyyySlashMatch) {
const [, day, month, year] = ddmmyyyySlashMatch;
const convertedDate = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
console.log(`[convertDateFormat] Converted ${dateString} to ${convertedDate}`);
return convertedDate;
}
console.warn(`[convertDateFormat] Could not parse date format: ${dateString}`);
return dateString;
};
// String data handling functions
export const parseStringBoolean = (value: string): boolean => {
return value === 'true';
};
export const formatBooleanAsString = (value: boolean): string => {
return value ? 'true' : 'false';
};
export const parseNationalities = (nationalityString: string): string[] => {
return nationalityString ? nationalityString.split(',').map(n => n.trim()).filter(n => n.length > 0) : [];
};
export const formatNationalitiesAsString = (nationalities: string[]): string => {
return nationalities.filter(n => n && n.trim()).join(',');
};
export const getNocoDbConfiguration = () => {
const config = useRuntimeConfig().nocodb;
// Use the new database URL
const updatedConfig = {
...config,
url: 'https://database.monacousa.org'
};
console.log('[nocodb] Configuration URL:', updatedConfig.url);
return updatedConfig;
};
export const createTableUrl = (table: Table) => {
const url = `${getNocoDbConfiguration().url}/api/v2/tables/${table}/records`;
console.log('[nocodb] Table URL:', url);
return url;
};
// CRUD operations for Members table
export const getMembers = async (): Promise<EntityResponse<Member>> => {
console.log('[nocodb.getMembers] Fetching all members...');
const startTime = Date.now();
try {
const result = await $fetch<EntityResponse<Member>>(createTableUrl(Table.Members), {
headers: {
"xc-token": getNocoDbConfiguration().token,
},
params: {
limit: 1000,
},
});
console.log('[nocodb.getMembers] Successfully fetched members, count:', result.list?.length || 0);
console.log('[nocodb.getMembers] Request duration:', Date.now() - startTime, 'ms');
return result;
} catch (error: any) {
console.error('[nocodb.getMembers] Error fetching members:', error);
throw error;
}
};
export const getMemberById = async (id: string): Promise<Member> => {
console.log('[nocodb.getMemberById] Fetching member ID:', id);
const result = await $fetch<Member>(`${createTableUrl(Table.Members)}/${id}`, {
headers: {
"xc-token": getNocoDbConfiguration().token,
},
});
console.log('[nocodb.getMemberById] Successfully retrieved member:', result.Id);
return result;
};
export const createMember = async (data: Partial<Member>): Promise<Member> => {
console.log('[nocodb.createMember] Creating member with fields:', Object.keys(data));
// Create a clean data object that matches the member schema
const cleanData: Record<string, any> = {};
// Only include fields that are part of the member schema
const allowedFields = [
"First Name",
"Last Name",
"Email",
"Phone",
"Current Year Dues Paid",
"Nationality",
"Date of Birth",
"Membership Date Paid",
"Payment Due Date",
"Membership Status",
"Address",
"Member Since"
];
// Filter the data to only include allowed fields
for (const field of allowedFields) {
if (field in data) {
cleanData[field] = (data as any)[field];
}
}
// Remove any computed or relation fields that shouldn't be sent
delete cleanData.Id;
delete cleanData.CreatedAt;
delete cleanData.UpdatedAt;
delete cleanData.FullName;
delete cleanData.FormattedPhone;
// Fix date formatting for PostgreSQL
if (cleanData['Date of Birth']) {
cleanData['Date of Birth'] = convertDateFormat(cleanData['Date of Birth']);
}
if (cleanData['Membership Date Paid']) {
cleanData['Membership Date Paid'] = convertDateFormat(cleanData['Membership Date Paid']);
}
if (cleanData['Payment Due Date']) {
cleanData['Payment Due Date'] = convertDateFormat(cleanData['Payment Due Date']);
}
console.log('[nocodb.createMember] Clean data fields:', Object.keys(cleanData));
const url = createTableUrl(Table.Members);
try {
const result = await $fetch<Member>(url, {
method: "POST",
headers: {
"xc-token": getNocoDbConfiguration().token,
},
body: cleanData,
});
console.log('[nocodb.createMember] Created member with ID:', result.Id);
return result;
} catch (error) {
console.error('[nocodb.createMember] Create failed:', error);
console.error('[nocodb.createMember] Error details:', error instanceof Error ? error.message : 'Unknown error');
throw error;
}
};
export const updateMember = async (id: string, data: Partial<Member>, retryCount = 0): Promise<Member> => {
console.log('[nocodb.updateMember] Updating member:', id, 'Retry:', retryCount);
console.log('[nocodb.updateMember] Data fields:', Object.keys(data));
// First, try to verify the record exists
if (retryCount === 0) {
try {
console.log('[nocodb.updateMember] Verifying record exists...');
const existingRecord = await getMemberById(id);
console.log('[nocodb.updateMember] Record exists with ID:', existingRecord.Id);
} catch (verifyError: any) {
console.error('[nocodb.updateMember] Failed to verify record:', verifyError);
if (verifyError.statusCode === 404 || verifyError.status === 404) {
console.error('[nocodb.updateMember] Record verification failed - record not found');
}
}
}
// Create a clean data object
const cleanData: Record<string, any> = {};
// Only include fields that are part of the member schema
const allowedFields = [
"First Name",
"Last Name",
"Email",
"Phone",
"Current Year Dues Paid",
"Nationality",
"Date of Birth",
"Membership Date Paid",
"Payment Due Date",
"Membership Status",
"Address",
"Member Since"
];
// 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.updateMember] Converting undefined to null for field: ${field}`);
} else {
cleanData[field] = value;
}
}
}
// Fix date formatting for PostgreSQL
if (cleanData['Date of Birth']) {
cleanData['Date of Birth'] = convertDateFormat(cleanData['Date of Birth']);
}
if (cleanData['Membership Date Paid']) {
cleanData['Membership Date Paid'] = convertDateFormat(cleanData['Membership Date Paid']);
}
if (cleanData['Payment Due Date']) {
cleanData['Payment Due Date'] = convertDateFormat(cleanData['Payment Due Date']);
}
console.log('[nocodb.updateMember] Clean data fields:', Object.keys(cleanData));
// PATCH requires ID in the body (not in URL)
cleanData.Id = parseInt(id);
const url = createTableUrl(Table.Members);
try {
console.log('[nocodb.updateMember] Sending PATCH request');
const result = await $fetch<Member>(url, {
method: "PATCH",
headers: {
"xc-token": getNocoDbConfiguration().token,
"Content-Type": "application/json"
},
body: cleanData
});
console.log('[nocodb.updateMember] Update successful for ID:', id);
return result;
} catch (error: any) {
console.error('[nocodb.updateMember] Update failed:', error);
console.error('[nocodb.updateMember] Error details:', error instanceof Error ? error.message : 'Unknown error');
// If it's a 404 error and we haven't retried too many times, wait and retry
if ((error.statusCode === 404 || error.status === 404) && retryCount < 3) {
console.error('[nocodb.updateMember] 404 Error - Record not found. This might be a sync delay.');
console.error(`Retrying in ${(retryCount + 1) * 1000}ms... (Attempt ${retryCount + 1}/3)`);
// Wait with exponential backoff
await new Promise(resolve => setTimeout(resolve, (retryCount + 1) * 1000));
// Retry the update
return updateMember(id, data, retryCount + 1);
}
throw error;
}
};
export const deleteMember = async (id: string) => {
const startTime = Date.now();
console.log('[nocodb.deleteMember] =========================');
console.log('[nocodb.deleteMember] DELETE operation started at:', new Date().toISOString());
console.log('[nocodb.deleteMember] Target ID:', id);
const url = createTableUrl(Table.Members);
console.log('[nocodb.deleteMember] URL:', url);
const requestBody = {
"Id": parseInt(id)
};
console.log('[nocodb.deleteMember] Request configuration:');
console.log(' Method: DELETE');
console.log(' URL:', url);
console.log(' Body:', JSON.stringify(requestBody, null, 2));
try {
const result = await $fetch(url, {
method: "DELETE",
headers: {
"xc-token": getNocoDbConfiguration().token,
"Content-Type": "application/json"
},
body: requestBody
});
console.log('[nocodb.deleteMember] DELETE successful');
console.log('[nocodb.deleteMember] Duration:', Date.now() - startTime, 'ms');
return result;
} catch (error: any) {
console.error('[nocodb.deleteMember] DELETE FAILED');
console.error('[nocodb.deleteMember] Error type:', error.constructor.name);
console.error('[nocodb.deleteMember] Error message:', error.message);
console.error('[nocodb.deleteMember] Duration:', Date.now() - startTime, 'ms');
throw error;
}
};
// Centralized error handling
export const handleNocoDbError = (error: any, operation: string, entityType: string = 'Member') => {
console.error(`[nocodb.${operation}] =========================`);
console.error(`[nocodb.${operation}] ERROR in ${operation} for ${entityType}`);
console.error(`[nocodb.${operation}] Error type:`, error.constructor?.name || 'Unknown');
console.error(`[nocodb.${operation}] Error status:`, error.statusCode || error.status || 'Unknown');
console.error(`[nocodb.${operation}] Error message:`, error.message || 'Unknown error');
console.error(`[nocodb.${operation}] Error data:`, error.data);
console.error(`[nocodb.${operation}] =========================`);
// Provide more specific error messages
if (error.statusCode === 401 || error.status === 401) {
throw createError({
statusCode: 401,
statusMessage: `Authentication failed when accessing ${entityType}. Please check your access permissions.`
});
} else if (error.statusCode === 403 || error.status === 403) {
throw createError({
statusCode: 403,
statusMessage: `Access denied to ${entityType}. This feature requires appropriate privileges.`
});
} else if (error.statusCode === 404 || error.status === 404) {
throw createError({
statusCode: 404,
statusMessage: `${entityType} not found. Please verify the record exists.`
});
} else if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT') {
throw createError({
statusCode: 503,
statusMessage: `${entityType} database is temporarily unavailable. Please try again in a moment.`
});
}
throw error;
};