server.js•7.27 kB
#!/usr/bin/env node
/**
* Express server to serve the Airtable bases web page
* Keeps API key secure on the server side
* Compatible with both local development and Vercel deployment
*/
import express from 'express';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { AirtableService } from './dist/airtableService.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
// Get API key from environment or config
const API_KEY = process.env.AIRTABLE_API_KEY || '';
// Middleware for parsing JSON bodies (must be before routes)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// API routes (must be before static files)
// API endpoint to fetch bases
app.get('/api/bases', async (req, res) => {
try {
const service = new AirtableService(API_KEY);
const response = await service.listBases();
res.json({
success: true,
bases: response.bases,
total: response.bases.length,
});
} catch (error) {
console.error('Error fetching bases:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
// API endpoint to fetch tables for a specific base
app.get('/api/bases/:baseId/tables', async (req, res) => {
try {
const { baseId } = req.params;
const service = new AirtableService(API_KEY);
const response = await service.getBaseSchema(baseId);
res.json({
success: true,
tables: response.tables,
total: response.tables.length,
});
} catch (error) {
console.error('Error fetching tables:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
// API endpoint to fetch records from a specific table
app.get('/api/bases/:baseId/tables/:tableId/records', async (req, res) => {
try {
const { baseId, tableId } = req.params;
const { maxRecords = 100 } = req.query;
const service = new AirtableService(API_KEY);
const records = await service.listRecords(baseId, tableId, {
maxRecords: parseInt(maxRecords, 10),
});
res.json({
success: true,
records: records,
total: records.length,
});
} catch (error) {
console.error('Error fetching records:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
// API endpoint to fetch comments for a record
// Comments are stored in the custom Comments table in VuPTT_Comments base
app.get('/api/bases/:baseId/tables/:tableId/records/:recordId/comments', async (req, res) => {
try {
if (!API_KEY) {
return res.status(500).json({
success: false,
error: 'AIRTABLE_API_KEY environment variable is not set',
});
}
const { baseId, tableId, recordId } = req.params;
// Comments are stored in the custom Comments table
const COMMENTS_BASE_ID = 'apptnDpTBFKNhQmJw';
const COMMENTS_TABLE_ID = 'tblzDXKlDXH6nBnh9';
const service = new AirtableService(API_KEY);
// Filter comments by base_id, table_id, and record_id
const filterFormula = `AND({base_id} = "${baseId}", {table_id} = "${tableId}", {record_id} = "${recordId}")`;
const records = await service.listRecords(COMMENTS_BASE_ID, COMMENTS_TABLE_ID, {
filterByFormula: filterFormula,
});
// Transform records to match the expected comment format
// Note: Airtable records include createdTime as metadata, but our type doesn't capture it
// We'll use the record ID timestamp or current time as fallback
const comments = records.map(record => {
// Try to extract createdTime from record metadata if available
// Airtable API returns createdTime, but our schema might not capture it
const createdTime = record.createdTime || new Date().toISOString();
return {
id: record.id,
text: record.fields.comment || '',
author: {
name: record.fields.user || 'Anonymous',
email: record.fields.user || '',
},
createdTime: createdTime,
parentCommentId: null, // Not supported in custom table yet
};
});
// Sort by createdTime descending (newest first)
comments.sort((a, b) => {
const timeA = new Date(a.createdTime).getTime();
const timeB = new Date(b.createdTime).getTime();
return timeB - timeA;
});
res.json({
success: true,
comments: comments,
offset: null,
});
} catch (error) {
console.error('Error fetching comments:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
// API endpoint to create a comment on a record
// Comments are stored in the custom Comments table in VuPTT_Comments base
app.post('/api/bases/:baseId/tables/:tableId/records/:recordId/comments', async (req, res) => {
try {
if (!API_KEY) {
return res.status(500).json({
success: false,
error: 'AIRTABLE_API_KEY environment variable is not set',
});
}
const { baseId, tableId, recordId } = req.params;
const { text, user } = req.body; // user is optional
if (!text || text.trim().length === 0) {
return res.status(400).json({
success: false,
error: 'Comment text is required',
});
}
// Comments are stored in the custom Comments table
const COMMENTS_BASE_ID = 'apptnDpTBFKNhQmJw';
const COMMENTS_TABLE_ID = 'tblzDXKlDXH6nBnh9';
const service = new AirtableService(API_KEY);
// Create record in Comments table
const record = await service.createRecord(COMMENTS_BASE_ID, COMMENTS_TABLE_ID, {
base_id: baseId,
table_id: tableId,
record_id: recordId,
user: user || '', // Can be empty
comment: text.trim(),
});
// Transform to match expected comment format
// Airtable API returns createdTime as metadata
const createdTime = record.createdTime || new Date().toISOString();
const comment = {
id: record.id,
text: record.fields.comment || text.trim(),
author: {
name: record.fields.user || 'Anonymous',
email: record.fields.user || '',
},
createdTime: createdTime,
parentCommentId: null,
};
res.json({
success: true,
comment: comment,
});
} catch (error) {
console.error('Error creating comment:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
// Serve static files from public directory (after API routes)
app.use(express.static(join(__dirname, 'public')));
// Export for Vercel (serverless function)
export default app;
// Start server locally (not on Vercel)
if (process.env.VERCEL !== '1') {
app.listen(PORT, () => {
console.log(`🚀 Server running at http://localhost:${PORT}`);
console.log(`📊 View your Airtable bases at http://localhost:${PORT}`);
});
}