monacousa-portal/nocodb-implementation-guide.md

40 KiB

Comprehensive NocoDB Integration Implementation Guide

This guide provides a complete implementation reference for integrating NocoDB with a Nuxt.js application, based on production patterns and best practices.

Table of Contents

  1. Configuration & Setup
  2. Core Utility Layer
  3. CRUD Operations Patterns
  4. Advanced Features
  5. API Endpoint Patterns
  6. TypeScript Integration
  7. Best Practices
  8. Real-World Examples

Configuration & Setup

Environment Variables

Create the following environment variables in your .env file:

NUXT_NOCODB_URL=https://your-nocodb-instance.com
NUXT_NOCODB_TOKEN=your-api-token-here

Nuxt Configuration

Configure NocoDB in your nuxt.config.ts:

export default defineNuxtConfig({
  runtimeConfig: {
    nocodb: {
      url: process.env.NUXT_NOCODB_URL || "",
      token: process.env.NUXT_NOCODB_TOKEN || "",
    },
    // Other runtime config...
  },
})

Core Utility Layer

Base Configuration Functions

Create server/utils/nocodb.ts with the following foundation:

import type { YourEntityType, YourEntityResponse, ExpenseFilters } 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 {
  YourEntity = "your-table-id-here",
  AnotherEntity = "another-table-id-here",
  // Add all your table IDs
}

/**
 * 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;
};

export const getNocoDbConfiguration = () => {
  const config = useRuntimeConfig().nocodb;
  console.log('[nocodb] Configuration URL:', config.url);
  return config;
};

export const createTableUrl = (table: Table) => {
  const url = `${getNocoDbConfiguration().url}/api/v2/tables/${table}/records`;
  console.log('[nocodb] Table URL:', url);
  return url;
};

CRUD Operations Patterns

READ Operations

Basic List Fetching

export const getEntities = async () =>
  $fetch<EntityResponse<YourEntityType>>(createTableUrl(Table.YourEntity), {
    headers: {
      "xc-token": getNocoDbConfiguration().token,
    },
    params: {
      limit: 1000,
    },
  });

Single Record Retrieval

export const getEntityById = async (id: string) => {
  console.log('[nocodb.getEntityById] Fetching entity ID:', id);
  
  const result = await $fetch<YourEntityType>(`${createTableUrl(Table.YourEntity)}/${id}`, {
    headers: {
      "xc-token": getNocoDbConfiguration().token,
    },
  });
  
  console.log('[nocodb.getEntityById] Successfully retrieved entity:', result.Id);
  return result;
};

Advanced Filtering with Dynamic Parameters

const buildEntityParams = (filters?: YourFiltersType) => {
  const params: any = { limit: 1000 };
  
  // Build filter conditions
  if (filters?.startDate && filters?.endDate) {
    const startDate = filters.startDate.includes('T') ? filters.startDate.split('T')[0] : filters.startDate;
    const endDate = filters.endDate.includes('T') ? filters.endDate.split('T')[0] : filters.endDate;
    params.where = `(DateField,gte,${startDate})~and(DateField,lte,${endDate})`;
  } else if (filters?.startDate) {
    const startDate = filters.startDate.includes('T') ? filters.startDate.split('T')[0] : filters.startDate;
    params.where = `(DateField,gte,${startDate})`;
  } else if (filters?.endDate) {
    const endDate = filters.endDate.includes('T') ? filters.endDate.split('T')[0] : filters.endDate;
    params.where = `(DateField,lte,${endDate})`;
  }
  
  // Add additional filters
  if (filters?.category) {
    const categoryFilter = `(Category,eq,${encodeURIComponent(filters.category)})`;
    params.where = params.where ? `${params.where}~and${categoryFilter}` : categoryFilter;
  }
  
  // Sort by date descending (newest first)
  params.sort = '-DateField';
  
  return params;
};

export const getEntitiesFiltered = async (filters?: YourFiltersType) => {
  console.log('[nocodb.getEntitiesFiltered] Fetching with filters...', filters);
  
  const startTime = Date.now();
  
  try {
    const result = await $fetch<EntityResponse<YourEntityType>>(createTableUrl(Table.YourEntity), {
      headers: { "xc-token": getNocoDbConfiguration().token },
      params: buildEntityParams(filters)
    });

    console.log('[nocodb.getEntitiesFiltered] Successfully fetched entities, count:', result.list?.length || 0);
    console.log('[nocodb.getEntitiesFiltered] Request duration:', Date.now() - startTime, 'ms');
    
    return result;
  } catch (error: any) {
    console.error('[nocodb.getEntitiesFiltered] Error fetching entities:', error);
    throw error;
  }
};

CREATE Operations

export const createEntity = async (data: Partial<YourEntityType>) => {
  console.log('[nocodb.createEntity] Creating entity with fields:', Object.keys(data));
  
  // Create a clean data object that matches the entity schema
  const cleanData: Record<string, any> = {};

  // Only include fields that are part of the entity schema
  const allowedFields = [
    "Field1",
    "Field2", 
    "Field3",
    // Add your actual field names here
  ];

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

  // Fix date formatting for PostgreSQL
  if (cleanData['DateField']) {
    cleanData['DateField'] = convertDateFormat(cleanData['DateField']);
  }

  console.log('[nocodb.createEntity] Clean data fields:', Object.keys(cleanData));
  const url = createTableUrl(Table.YourEntity);

  try {
    const result = await $fetch<YourEntityType>(url, {
      method: "POST",
      headers: {
        "xc-token": getNocoDbConfiguration().token,
      },
      body: cleanData,
    });
    console.log('[nocodb.createEntity] Created entity with ID:', result.Id);
    return result;
  } catch (error) {
    console.error('[nocodb.createEntity] Create failed:', error);
    console.error('[nocodb.createEntity] Error details:', error instanceof Error ? error.message : 'Unknown error');
    throw error;
  }
};

UPDATE Operations with Retry Logic

export const updateEntity = async (id: string, data: Partial<YourEntityType>, retryCount = 0): Promise<YourEntityType> => {
  console.log('[nocodb.updateEntity] Updating entity:', id, 'Retry:', retryCount);
  console.log('[nocodb.updateEntity] Data fields:', Object.keys(data));
  
  // First, try to verify the record exists
  if (retryCount === 0) {
    try {
      console.log('[nocodb.updateEntity] Verifying record exists...');
      const existingRecord = await getEntityById(id);
      console.log('[nocodb.updateEntity] Record exists with ID:', existingRecord.Id);
    } catch (verifyError: any) {
      console.error('[nocodb.updateEntity] Failed to verify record:', verifyError);
      if (verifyError.statusCode === 404 || verifyError.status === 404) {
        console.error('[nocodb.updateEntity] Record verification failed - record not found');
      }
    }
  }
  
  // Create a clean data object
  const cleanData: Record<string, any> = {};

  // Only include fields that are part of the entity schema
  const allowedFields = [
    "Field1",
    "Field2", 
    "Field3",
    // Add your actual field names here
  ];

  // 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.updateEntity] Converting undefined to null for field: ${field}`);
      } else {
        cleanData[field] = value;
      }
    }
  }

  // Fix date formatting for PostgreSQL
  if (cleanData['DateField']) {
    cleanData['DateField'] = convertDateFormat(cleanData['DateField']);
  }

  console.log('[nocodb.updateEntity] Clean data fields:', Object.keys(cleanData));
  
  // PATCH requires ID in the body (not in URL)
  cleanData.Id = parseInt(id);
  
  const url = createTableUrl(Table.YourEntity);

  try {
    console.log('[nocodb.updateEntity] Sending PATCH request');
    
    const result = await $fetch<YourEntityType>(url, {
      method: "PATCH",
      headers: {
        "xc-token": getNocoDbConfiguration().token,
        "Content-Type": "application/json"
      },
      body: cleanData
    });
    console.log('[nocodb.updateEntity] Update successful for ID:', id);
    return result;
  } catch (error: any) {
    console.error('[nocodb.updateEntity] Update failed:', error);
    console.error('[nocodb.updateEntity] 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.updateEntity] 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 updateEntity(id, data, retryCount + 1);
    }
    
    throw error;
  }
};

DELETE Operations

export const deleteEntity = async (id: string) => {
  const startTime = Date.now();
  console.log('[nocodb.deleteEntity] =========================');
  console.log('[nocodb.deleteEntity] DELETE operation started at:', new Date().toISOString());
  console.log('[nocodb.deleteEntity] Target ID:', id);
  
  const url = createTableUrl(Table.YourEntity);
  console.log('[nocodb.deleteEntity] URL:', url);
  
  const requestBody = {
    "Id": parseInt(id)
  };
  
  console.log('[nocodb.deleteEntity] 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.deleteEntity] DELETE successful');
    console.log('[nocodb.deleteEntity] Duration:', Date.now() - startTime, 'ms');
    
    return result;
  } catch (error: any) {
    console.error('[nocodb.deleteEntity] DELETE FAILED');
    console.error('[nocodb.deleteEntity] Error type:', error.constructor.name);
    console.error('[nocodb.deleteEntity] Error message:', error.message);
    console.error('[nocodb.deleteEntity] Duration:', Date.now() - startTime, 'ms');
    throw error;
  }
};

Advanced Features

Relationship Management

Fetching Linked Records

export const getEntityWithRelations = async (id: string) => {
  console.log('[nocodb.getEntityWithRelations] Fetching entity with relations:', id);
  
  try {
    // First fetch the basic entity data
    const result = await $fetch<YourEntityType>(`${createTableUrl(Table.YourEntity)}/${id}`, {
      headers: {
        "xc-token": getNocoDbConfiguration().token,
      },
      params: {
        fields: '*'
      }
    });
    
    console.log('[nocodb.getEntityWithRelations] Successfully fetched entity:', result.Id);
    
    // Now fetch and populate the related records
    if (result['RelatedField']) {
      // Handle case where related field is a count (number)
      if (typeof result['RelatedField'] === 'number' && result['RelatedField'] > 0) {
        const relatedCount = result['RelatedField'] as number;
        console.log(`[nocodb.getEntityWithRelations] Entity has ${relatedCount} related records`);
        
        // Fetch the linked records using the links API
        try {
          const config = getNocoDbConfiguration();
          const tableId = "your-table-id";
          const fieldId = "your-relation-field-id";
          
          const linkUrl = `${config.url}/api/v2/tables/${tableId}/links/${fieldId}/records/${result.Id}`;
          console.log(`[nocodb.getEntityWithRelations] Fetching linked records from: ${linkUrl}`);
          
          const linkedResponse = await $fetch<any>(linkUrl, {
            headers: {
              "xc-token": config.token,
            },
            params: {
              limit: 100
            }
          });
          
          if (linkedResponse && linkedResponse.list && Array.isArray(linkedResponse.list)) {
            // The links API returns limited data, so we need to fetch full records
            const fullRelatedDetails = await Promise.all(
              linkedResponse.list.map(async (linkedRecord: any) => {
                try {
                  const recordId = linkedRecord.Id || linkedRecord.id;
                  if (recordId) {
                    const fullDetails = await getRelatedEntityById(recordId.toString());
                    return fullDetails;
                  }
                  return linkedRecord;
                } catch (error) {
                  console.error(`[nocodb.getEntityWithRelations] Failed to fetch full details for record ${linkedRecord.Id}:`, error);
                  return linkedRecord;
                }
              })
            );
            
            result['RelatedField'] = fullRelatedDetails;
            console.log(`[nocodb.getEntityWithRelations] Successfully fetched full details for ${fullRelatedDetails.length} related records`);
          }
        } catch (linkError) {
          console.error(`[nocodb.getEntityWithRelations] Failed to fetch linked records:`, linkError);
        }
      }
    }
    
    return result;
  } catch (error: any) {
    console.error('[nocodb.getEntityWithRelations] Error fetching entity with relations:', error);
    throw error;
  }
};

Linking/Unlinking Records

export const linkRecords = async (parentTableId: string, parentId: string, childTableId: string, childId: string, fieldId: string) => {
  console.log('[nocodb.linkRecords] Linking records:', { parentId, childId, fieldId });
  
  const config = getNocoDbConfiguration();
  const linkUrl = `${config.url}/api/v2/tables/${parentTableId}/links/${fieldId}/records/${parentId}`;
  
  try {
    const result = await $fetch(linkUrl, {
      method: "POST",
      headers: {
        "xc-token": config.token,
        "Content-Type": "application/json"
      },
      body: {
        Id: parseInt(childId)
      }
    });
    
    console.log('[nocodb.linkRecords] Successfully linked records');
    return result;
  } catch (error: any) {
    console.error('[nocodb.linkRecords] Failed to link records:', error);
    throw error;
  }
};

export const unlinkRecords = async (parentTableId: string, parentId: string, childTableId: string, childId: string, fieldId: string) => {
  console.log('[nocodb.unlinkRecords] Unlinking records:', { parentId, childId, fieldId });
  
  const config = getNocoDbConfiguration();
  const unlinkUrl = `${config.url}/api/v2/tables/${parentTableId}/links/${fieldId}/records/${parentId}`;
  
  try {
    const result = await $fetch(unlinkUrl, {
      method: "DELETE",
      headers: {
        "xc-token": config.token,
        "Content-Type": "application/json"
      },
      body: {
        Id: parseInt(childId)
      }
    });
    
    console.log('[nocodb.unlinkRecords] Successfully unlinked records');
    return result;
  } catch (error: any) {
    console.error('[nocodb.unlinkRecords] Failed to unlink records:', error);
    throw error;
  }
};

Sorting and Advanced Querying

export const getEntitiesWithSorting = async () => {
  try {
    const result = await $fetch<EntityResponse<YourEntityType>>(createTableUrl(Table.YourEntity), {
      headers: {
        "xc-token": getNocoDbConfiguration().token,
      },
      params: {
        limit: 1000,
        fields: '*'
      },
    });

    // Apply custom sorting logic if needed
    if (result.list && Array.isArray(result.list)) {
      result.list.sort((a, b) => {
        // Example: Sort by a field with letter and number parts
        const fieldA = a['SortField'] || '';
        const fieldB = b['SortField'] || '';
        
        // Extract letter and number parts
        const matchA = fieldA.match(/^([A-Za-z]+)(\d+)$/);
        const matchB = fieldB.match(/^([A-Za-z]+)(\d+)$/);
        
        if (matchA && matchB) {
          const [, letterA, numberA] = matchA;
          const [, letterB, numberB] = matchB;
          
          // First sort by letter
          const letterCompare = letterA.localeCompare(letterB);
          if (letterCompare !== 0) {
            return letterCompare;
          }
          
          // Then sort by number within the same letter group
          return parseInt(numberA) - parseInt(numberB);
        }
        
        // Fallback to string comparison
        return fieldA.localeCompare(fieldB);
      });
    }
    
    return result;
  } catch (error: any) {
    console.error('[nocodb.getEntitiesWithSorting] Error:', error);
    throw error;
  }
};

API Endpoint Patterns

Standard Endpoint Structure

// server/api/get-entities.ts
import { getEntities } from "../utils/nocodb";
import { requireAuth } from "../utils/auth";

export default defineEventHandler(async (event) => {
  console.log('[get-entities] Request received');

  // Check authentication (adapt to your auth system)
  await requireAuth(event);

  try {
    console.log('[get-entities] Fetching entities...');
    const entities = await getEntities();
    console.log('[get-entities] Successfully fetched entities, count:', entities.list?.length || 0);
    return entities;
  } catch (error) {
    console.error('[get-entities] Error occurred:', error);
    console.error('[get-entities] Error details:', error instanceof Error ? error.message : 'Unknown error');
    throw error;
  }
});

POST Endpoint with Validation

// server/api/create-entity.ts
import { createEntity } from "../utils/nocodb";
import { requireAuth } from "../utils/auth";

export default defineEventHandler(async (event) => {
  console.log('[create-entity] Request received');

  await requireAuth(event);

  try {
    const body = await readBody(event);
    console.log('[create-entity] Request body fields:', body ? Object.keys(body) : 'none');
    
    if (!body || Object.keys(body).length === 0) {
      console.error('[create-entity] No data provided');
      throw createError({ statusCode: 400, statusMessage: "No data provided" });
    }
    
    // Validate required fields
    const requiredFields = ['Field1', 'Field2']; // Add your required fields
    const missingFields = requiredFields.filter(field => !body[field]);
    
    if (missingFields.length > 0) {
      throw createError({ 
        statusCode: 400, 
        statusMessage: `Missing required fields: ${missingFields.join(', ')}` 
      });
    }
    
    console.log('[create-entity] Creating new entity with fields:', Object.keys(body));
    const createdEntity = await createEntity(body);
    console.log('[create-entity] Successfully created entity with ID:', createdEntity.Id);
    
    return createdEntity;
  } catch (error) {
    console.error('[create-entity] Error occurred:', error);
    console.error('[create-entity] Error stack:', error instanceof Error ? error.stack : 'No stack trace');
    
    if (error instanceof Error) {
      throw createError({ statusCode: 500, statusMessage: error.message });
    } else {
      throw createError({
        statusCode: 500,
        statusMessage: "An unexpected error occurred",
      });
    }
  }
});

TypeScript Integration

Entity Type Definitions

Create comprehensive types in utils/types.ts:

// Enums for type safety
export enum EntityStatus {
  Active = 'Active',
  Inactive = 'Inactive',
  Pending = 'Pending'
}

export enum EntityCategory {
  TypeA = 'Type A',
  TypeB = 'Type B',
  TypeC = 'Type C'
}

// Main entity interface
export interface YourEntityType {
  Id: number;
  Name: string;
  Description?: string;
  Status: EntityStatus;
  Category: EntityCategory;
  DateCreated: string;
  DateModified?: string;
  // Add your specific fields
  
  // Computed fields (added by API processing)
  DisplayName?: string;
  FormattedDate?: string;
  
  // Related entities
  RelatedEntities?: RelatedEntityType[];
  
  // File attachments
  Attachments?: FileAttachment[];
}

export interface RelatedEntityType {
  Id: number;
  Name: string;
  Type: string;
}

export interface FileAttachment {
  id: string;
  url: string;
  signedUrl: string;
  title: string;
  mimetype: string;
  size: number;
  width?: number;
  height?: number;
  thumbnails?: {
    tiny: { signedUrl: string };
    small: { signedUrl: string };
    card_cover: { signedUrl: string };
  };
}

// Filter interfaces
export interface EntityFilters {
  startDate?: string;
  endDate?: string;
  status?: EntityStatus;
  category?: EntityCategory;
  searchTerm?: string;
}

// Response interfaces
export interface EntityResponse {
  list: YourEntityType[];
  PageInfo: PageInfo;
}

export interface PageInfo {
  pageSize: number;
  totalRows: number;
  isFirstPage: boolean;
  isLastPage: boolean;
  page: number;
}

Generic Helper Functions

// Generic function for any entity type
export const getGenericEntity = async <T>(table: Table, id: string): Promise<T> => {
  console.log(`[nocodb.getGenericEntity] Fetching ${table} ID: ${id}`);
  
  const result = await $fetch<T>(`${createTableUrl(table)}/${id}`, {
    headers: {
      "xc-token": getNocoDbConfiguration().token,
    },
  });
  
  console.log(`[nocodb.getGenericEntity] Successfully retrieved ${table}:`, (result as any).Id);
  return result;
};

// Generic list function
export const getGenericEntityList = async <T>(table: Table, params?: any): Promise<EntityResponse<T>> => {
  console.log(`[nocodb.getGenericEntityList] Fetching ${table} list with params:`, params);
  
  const result = await $fetch<EntityResponse<T>>(createTableUrl(table), {
    headers: {
      "xc-token": getNocoDbConfiguration().token,
    },
    params: {
      limit: 1000,
      ...params
    }
  });
  
  console.log(`[nocodb.getGenericEntityList] Successfully fetched ${table} list, count:`, result.list?.length || 0);
  return result;
};

Best Practices

1. Comprehensive Logging

// Always include comprehensive logging for debugging
const logOperation = (operation: string, entityType: string, data?: any) => {
  console.log(`[nocodb.${operation}] =========================`);
  console.log(`[nocodb.${operation}] Operation: ${operation.toUpperCase()}`);
  console.log(`[nocodb.${operation}] Entity: ${entityType}`);
  console.log(`[nocodb.${operation}] Timestamp: ${new Date().toISOString()}`);
  if (data) {
    console.log(`[nocodb.${operation}] Data:`, JSON.stringify(data, null, 2));
  }
  console.log(`[nocodb.${operation}] =========================`);
};

2. Error Handling Strategy

// Centralized error handling
export const handleNocoDbError = (error: any, operation: string, entityType: string) => {
  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;
};

3. Performance Optimization

// Cache frequently accessed data
const entityCache = new Map<string, { data: any; timestamp: number }>();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

export const getCachedEntity = async (id: string): Promise<YourEntityType | null> => {
  const cacheKey = `entity-${id}`;
  const cached = entityCache.get(cacheKey);
  
  if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
    console.log('[nocodb.getCachedEntity] Cache hit for ID:', id);
    return cached.data;
  }
  
  try {
    const entity = await getEntityById(id);
    entityCache.set(cacheKey, { data: entity, timestamp: Date.now() });
    console.log('[nocodb.getCachedEntity] Cache updated for ID:', id);
    return entity;
  } catch (error) {
    console.error('[nocodb.getCachedEntity] Failed to fetch and cache entity:', error);
    return null;
  }
};

// Clear cache when entity is updated
export const clearEntityCache = (id: string) => {
  const cacheKey = `entity-${id}`;
  entityCache.delete(cacheKey);
  console.log('[nocodb.clearEntityCache] Cache cleared for ID:', id);
};

4. Data Validation

// Input validation functions
export const validateEntityData = (data: Partial<YourEntityType>): string[] => {
  const errors: string[] = [];
  
  // Required field validation
  if (!data.Name || data.Name.trim().length === 0) {
    errors.push('Name is required');
  }
  
  // Format validation
  if (data.Email && !isValidEmail(data.Email)) {
    errors.push('Invalid email format');
  }
  
  // Range validation
  if (data.Age && (data.Age < 0 || data.Age > 150)) {
    errors.push('Age must be between 0 and 150');
  }
  
  return errors;
};

const isValidEmail = (email: string): boolean => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
};

5. Batch Operations

// Batch create multiple entities
export const createEntitiesBatch = async (entities: Partial<YourEntityType>[]): Promise<YourEntityType[]> => {
  console.log('[nocodb.createEntitiesBatch] Creating batch of', entities.length, 'entities');
  
  const results: YourEntityType[] = [];
  const errors: string[] = [];
  
  for (let i = 0; i < entities.length; i++) {
    try {
      const result = await createEntity(entities[i]);
      results.push(result);
      console.log(`[nocodb.createEntitiesBatch] Created entity ${i + 1}/${entities.length}:`, result.Id);
    } catch (error) {
      const errorMsg = `Failed to create entity ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`;
      errors.push(errorMsg);
      console.error('[nocodb.createEntitiesBatch]', errorMsg);
    }
  }
  
  if (errors.length > 0) {
    console.warn('[nocodb.createEntitiesBatch] Batch completed with errors:', errors);
  }
  
  console.log('[nocodb.createEntitiesBatch] Batch completed. Created:', results.length, 'Errors:', errors.length);
  return results;
};

Real-World Examples

Example 1: Interest Management System

Based on the actual implementation patterns from your codebase:

// Interest entity with comprehensive field management
export interface Interest {
  Id: number;
  "Full Name": string;
  "Yacht Name": string;
  Length: string;
  "Email Address": string;
  "Sales Process Level": InterestSalesProcessLevel;
  "Phone Number": string;
  "EOI Status": EOIStatus;
  "Berth Info Sent Status": BerthInfoSentStatus;
  "Contract Status": ContractStatus;
  "Date Added": string;
  "Created At": string;
  // Signature links
  "Signature Link Client"?: string;
  "Signature Link CC"?: string;
  "Signature Link Developer"?: string;
  // Document ID
  "documensoID"?: string;
}

export const updateInterestWithValidation = async (id: string, data: Partial<Interest>): Promise<Interest> => {
  console.log('[nocodb.updateInterestWithValidation] Updating interest:', id);
  
  // Validate the data before processing
  const validationErrors = validateInterestData(data);
  if (validationErrors.length > 0) {
    throw createError({
      statusCode: 400,
      statusMessage: `Validation failed: ${validationErrors.join(', ')}`
    });
  }
  
  // Clean and filter data
  const cleanData: Record<string, any> = {};
  
  const allowedFields = [
    "Full Name", "Yacht Name", "Length", "Address", "Email Address",
    "Sales Process Level", "Phone Number", "Extra Comments",
    "Berth Size Desired", "Date Added", "Width", "Depth",
    "Source", "Contact Method Preferred", "Lead Category",
    "EOI Status", "Berth Info Sent Status", "Contract Sent Status",
    "Deposit 10% Status", "Contract Status",
    "Signature Link Client", "Signature Link CC", "Signature Link Developer",
    "documensoID"
  ];
  
  for (const field of allowedFields) {
    if (field in data) {
      const value = (data as any)[field];
      cleanData[field] = value === undefined ? null : value;
    }
  }
  
  // Handle date formatting
  if (cleanData['Date Added']) {
    cleanData['Date Added'] = convertDateFormat(cleanData['Date Added']);
  }
  
  // Include ID for PATCH request
  cleanData.Id = parseInt(id);
  
  try {
    const result = await $fetch<Interest>(createTableUrl(Table.Interest), {
      method: "PATCH",
      headers: {
        "xc-token": getNocoDbConfiguration().token,
        "Content-Type": "application/json"
      },
      body: cleanData
    });
    
    console.log('[nocodb.updateInterestWithValidation] Successfully updated interest:', result.Id);
    return result;
  } catch (error: any) {
    console.error('[nocodb.updateInterestWithValidation] Update failed:', error);
    throw handleNocoDbError(error, 'updateInterestWithValidation', 'Interest');
  }
};

const validateInterestData = (data: Partial<Interest>): string[] => {
  const errors: string[] = [];
  
  if (data["Full Name"] && data["Full Name"].trim().length === 0) {
    errors.push("Full Name cannot be empty");
  }
  
  if (data["Email Address"] && !isValidEmail(data["Email Address"])) {
    errors.push("Invalid email address format");
  }
  
  if (data["Phone Number"] && data["Phone Number"].length < 10) {
    errors.push("Phone number must be at least 10 digits");
  }
  
  return errors;
};

Example 2: Berth Management with Relationships

// Berth entity with interested parties relationship
export const getBerthWithInterestedParties = async (id: string): Promise<Berth> => {
  console.log('[nocodb.getBerthWithInterestedParties] Fetching berth:', id);
  
  try {
    // Fetch basic berth data
    const berth = await $fetch<Berth>(`${createTableUrl(Table.Berth)}/${id}`, {
      headers: {
        "xc-token": getNocoDbConfiguration().token,
      }
    });
    
    // Handle interested parties relationship
    if (berth['Interested Parties']) {
      if (typeof berth['Interested Parties'] === 'number' && berth['Interested Parties'] > 0) {
        const partyCount = berth['Interested Parties'] as number;
        console.log(`[nocodb.getBerthWithInterestedParties] Fetching ${partyCount} interested parties`);
        
        try {
          const config = getNocoDbConfiguration();
          const berthsTableId = "mczgos9hr3oa9qc";
          const interestedPartiesFieldId = "c7q2z2rb27c1cb5";
          
          const linkUrl = `${config.url}/api/v2/tables/${berthsTableId}/links/${interestedPartiesFieldId}/records/${berth.Id}`;
          
          const linkedResponse = await $fetch<any>(linkUrl, {
            headers: { "xc-token": config.token },
            params: { limit: 100 }
          });
          
          if (linkedResponse?.list?.length > 0) {
            const fullPartyDetails = await Promise.all(
              linkedResponse.list.map(async (party: any) => {
                try {
                  const partyId = party.Id || party.id;
                  return partyId ? await getInterestById(partyId.toString()) : party;
                } catch (error) {
                  console.error(`[nocodb.getBerthWithInterestedParties] Failed to fetch party ${party.Id}:`, error);
                  return party;
                }
              })
            );
            
            berth['Interested Parties'] = fullPartyDetails;
            console.log(`[nocodb.getBerthWithInterestedParties] Successfully populated ${fullPartyDetails.length} interested parties`);
          }
        } catch (linkError) {
          console.error(`[nocodb.getBerthWithInterestedParties] Failed to fetch linked parties:`, linkError);
          // Provide fallback data
          berth['Interested Parties'] = Array.from({ length: partyCount }, (_, i) => ({
            Id: i + 1,
            'Full Name': `Party ${i + 1}`,
            'Sales Process Level': null,
            'EOI Status': null,
            'Contract Status': null
          })) as any;
        }
      }
    }
    
    return berth;
  } catch (error: any) {
    console.error('[nocodb.getBerthWithInterestedParties] Error fetching berth:', error);
    throw handleNocoDbError(error, 'getBerthWithInterestedParties', 'Berth');
  }
};

Example 3: Expense Tracking with Currency Conversion

// Advanced expense management with filtering and post-processing
export const getExpensesWithProcessing = async (filters?: ExpenseFilters) => {
  console.log('[nocodb.getExpensesWithProcessing] Fetching expenses with filters:', filters);
  const startTime = Date.now();
  
  try {
    const result = await $fetch<ExpensesResponse>(createTableUrl(Table.Expense), {
      headers: { "xc-token": getNocoDbConfiguration().token },
      params: buildExpenseParams(filters)
    });
    
    console.log('[nocodb.getExpensesWithProcessing] Fetched', result.list?.length || 0, 'expenses');
    
    // Post-process expenses with currency conversion
    if (result.list?.length > 0) {
      // Add computed price numbers
      result.list = result.list.map(expense => ({
        ...expense,
        PriceNumber: parseFloat(expense.Price?.replace(/[€$,]/g, '') || '0') || 0
      }));
      
      // Apply currency conversion (integrate with your currency service)
      const { processExpenseWithCurrency } = await import('@/server/utils/currency');
      const processedExpenses = await Promise.all(
        result.list.map(expense => processExpenseWithCurrency(expense))
      );
      
      // Calculate summary statistics
      const summary = {
        totalCount: processedExpenses.length,
        totalUSD: processedExpenses.reduce((sum, e) => sum + (e.PriceUSD || e.PriceNumber || 0), 0),
        totalEUR: processedExpenses.reduce((sum, e) => sum + (e.PriceEUR || e.PriceNumber || 0), 0),
        uniquePayers: [...new Set(processedExpenses.map(e => e.Payer))].length,
        currencies: [...new Set(processedExpenses.map(e => e.Currency || e.currency))].filter(Boolean)
      };
      
      console.log('[nocodb.getExpensesWithProcessing] Processing completed in', Date.now() - startTime, 'ms');
      
      return {
        list: processedExpenses,
        PageInfo: result.PageInfo,
        summary
      };
    }
    
    return result;
  } catch (error: any) {
    console.error('[nocodb.getExpensesWithProcessing] Error:', error);
    throw handleNocoDbError(error, 'getExpensesWithProcessing', 'Expense');
  }
};

const buildExpenseParams = (filters?: ExpenseFilters) => {
  const params: any = { limit: 1000, sort: '-Time' };
  
  if (filters?.startDate && filters?.endDate) {
    const start = filters.startDate.includes('T') ? filters.startDate.split('T')[0] : filters.startDate;
    const end = filters.endDate.includes('T') ? filters.endDate.split('T')[0] : filters.endDate;
    params.where = `(Time,gte,${start})~and(Time,lte,${end})`;
  }
  
  if (filters?.payer) {
    const payerFilter = `(Payer,eq,${encodeURIComponent(filters.payer)})`;
    params.where = params.where ? `${params.where}~and${payerFilter}` : payerFilter;
  }
  
  if (filters?.category) {
    const categoryFilter = `(Category,eq,${encodeURIComponent(filters.category)})`;
    params.where = params.where ? `${params.where}~and${categoryFilter}` : categoryFilter;
  }
  
  return params;
};

Example 4: File Attachment Handling

// Handle file attachments in NocoDB
export const updateEntityWithAttachment = async (id: string, attachmentData: any) => {
  console.log('[nocodb.updateEntityWithAttachment] Adding attachment to entity:', id);
  
  try {
    // Get existing entity
    const entity = await getEntityById(id);
    const existingAttachments = entity['Attachments'] || [];
    
    // Add new attachment to array
    const updatedAttachments = [...existingAttachments, attachmentData];
    
    // Update entity with new attachments
    return await updateEntity(id, {
      'Attachments': updatedAttachments
    });
  } catch (error) {
    console.error('[nocodb.updateEntityWithAttachment] Failed to add attachment:', error);
    throw error;
  }
};

Conclusion

This implementation guide provides a comprehensive foundation for integrating NocoDB with Nuxt.js applications. The patterns shown here are based on production code and include:

  • Robust error handling with retry mechanisms
  • Type safety with comprehensive TypeScript interfaces
  • Performance optimization through caching and efficient queries
  • Security through field whitelisting and data validation
  • Maintainability through consistent logging and error reporting
  • Scalability through generic functions and batch operations

Use these patterns as a foundation and adapt them to your specific use cases. The modular approach allows you to implement only the features you need while maintaining consistency across your application.

Key Takeaways

  1. Always validate and sanitize data before sending to NocoDB
  2. Use comprehensive logging for debugging and monitoring
  3. Implement retry logic for network operations
  4. Handle relationships carefully using NocoDB's links API
  5. Cache frequently accessed data to improve performance
  6. Use TypeScript interfaces for type safety and better developer experience
  7. Follow consistent naming conventions for maintainability

This guide should serve as a complete reference for implementing robust NocoDB integrations in your applications.