1270 lines
40 KiB
Markdown
1270 lines
40 KiB
Markdown
# 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](#configuration--setup)
|
|
2. [Core Utility Layer](#core-utility-layer)
|
|
3. [CRUD Operations Patterns](#crud-operations-patterns)
|
|
4. [Advanced Features](#advanced-features)
|
|
5. [API Endpoint Patterns](#api-endpoint-patterns)
|
|
6. [TypeScript Integration](#typescript-integration)
|
|
7. [Best Practices](#best-practices)
|
|
8. [Real-World Examples](#real-world-examples)
|
|
|
|
## Configuration & Setup
|
|
|
|
### Environment Variables
|
|
|
|
Create the following environment variables in your `.env` file:
|
|
|
|
```env
|
|
NUXT_NOCODB_URL=https://your-nocodb-instance.com
|
|
NUXT_NOCODB_TOKEN=your-api-token-here
|
|
```
|
|
|
|
### Nuxt Configuration
|
|
|
|
Configure NocoDB in your `nuxt.config.ts`:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
export const getEntities = async () =>
|
|
$fetch<EntityResponse<YourEntityType>>(createTableUrl(Table.YourEntity), {
|
|
headers: {
|
|
"xc-token": getNocoDbConfiguration().token,
|
|
},
|
|
params: {
|
|
limit: 1000,
|
|
},
|
|
});
|
|
```
|
|
|
|
#### Single Record Retrieval
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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`:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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.
|