375 lines
11 KiB
Plaintext
375 lines
11 KiB
Plaintext
const axios = require('axios');
|
|
|
|
class NocoDBService {
|
|
constructor() {
|
|
this.apiUrl = process.env.NOCODB_API_URL;
|
|
this.apiToken = process.env.NOCODB_API_TOKEN;
|
|
this.projectId = process.env.NOCODB_PROJECT_ID;
|
|
this.timeout = 10000;
|
|
|
|
// Table mapping with actual table IDs from NocoDB
|
|
this.tableIds = {
|
|
representatives: 'm3slxjt2t9fspvn',
|
|
emails: 'mclckn23dlsiuvj',
|
|
postalCodes: 'mfsefv20htd6jy1',
|
|
campaigns: 'mrbky41y7nahz98',
|
|
campaignEmails: 'mlij85ls403d7c2'
|
|
};
|
|
|
|
// Create axios instance with defaults like the map service
|
|
this.client = axios.create({
|
|
baseURL: this.apiUrl,
|
|
timeout: this.timeout,
|
|
headers: {
|
|
'xc-token': this.apiToken,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
// Add response interceptor for error handling
|
|
this.client.interceptors.response.use(
|
|
response => response,
|
|
error => {
|
|
console.error('NocoDB API Error:', {
|
|
message: error.message,
|
|
url: error.config?.url,
|
|
method: error.config?.method,
|
|
status: error.response?.status,
|
|
data: error.response?.data
|
|
});
|
|
throw error;
|
|
}
|
|
);
|
|
}
|
|
|
|
// Build table URL using table ID
|
|
getTableUrl(tableId) {
|
|
return `/db/data/v1/${this.projectId}/${tableId}`;
|
|
}
|
|
|
|
// Get all records from a table
|
|
async getAll(tableId, params = {}) {
|
|
const url = this.getTableUrl(tableId);
|
|
const response = await this.client.get(url, { params });
|
|
return response.data;
|
|
}
|
|
|
|
// Create record
|
|
async create(tableId, data) {
|
|
try {
|
|
// Clean data to prevent ID conflicts
|
|
const cleanData = { ...data };
|
|
delete cleanData.ID;
|
|
delete cleanData.id;
|
|
delete cleanData.Id;
|
|
|
|
// Remove undefined values
|
|
Object.keys(cleanData).forEach(key => {
|
|
if (cleanData[key] === undefined) {
|
|
delete cleanData[key];
|
|
}
|
|
});
|
|
|
|
const url = this.getTableUrl(tableId);
|
|
const response = await this.client.post(url, cleanData);
|
|
return response.data;
|
|
} catch (error) {
|
|
console.error('Error creating record:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async storeRepresentatives(postalCode, representatives) {
|
|
try {
|
|
const stored = [];
|
|
|
|
for (const rep of representatives) {
|
|
const record = {
|
|
postal_code: postalCode,
|
|
name: rep.name || '',
|
|
email: rep.email || '',
|
|
district_name: rep.district_name || '',
|
|
elected_office: rep.elected_office || '',
|
|
party_name: rep.party_name || '',
|
|
representative_set_name: rep.representative_set_name || '',
|
|
url: rep.url || '',
|
|
photo_url: rep.photo_url || '',
|
|
cached_at: new Date().toISOString()
|
|
};
|
|
|
|
const result = await this.create(this.tableIds.representatives, record);
|
|
stored.push(result);
|
|
}
|
|
|
|
return { success: true, count: stored.length };
|
|
} catch (error) {
|
|
// If we get a server error, don't throw - just log and return failure
|
|
if (error.response && error.response.status >= 500) {
|
|
console.log('NocoDB server unavailable, cannot cache representatives');
|
|
return { success: false, error: 'Server unavailable' };
|
|
}
|
|
|
|
console.log('Error storing representatives:', error.response?.data?.msg || error.message);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
async getRepresentativesByPostalCode(postalCode) {
|
|
try {
|
|
// Try to query with the most likely column name
|
|
const response = await this.getAll(this.tableIds.representatives, {
|
|
where: `(postal_code,eq,${postalCode})`
|
|
});
|
|
|
|
return response.list || [];
|
|
} catch (error) {
|
|
// If we get a 502 or other server error, just return empty array
|
|
if (error.response && (error.response.status === 502 || error.response.status >= 500)) {
|
|
console.log('NocoDB server unavailable (502/5xx error), returning empty cache result');
|
|
return [];
|
|
}
|
|
|
|
// For other errors like column not found, also return empty array
|
|
console.log('NocoDB cache error, returning empty array:', error.response?.data?.msg || error.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async clearRepresentativesByPostalCode(postalCode) {
|
|
try {
|
|
// Get existing records
|
|
const existing = await this.getRepresentativesByPostalCode(postalCode);
|
|
|
|
// Delete each record using client
|
|
for (const record of existing) {
|
|
const url = `${this.getTableUrl(this.tableIds.representatives)}/${record.Id}`;
|
|
await this.client.delete(url);
|
|
}
|
|
|
|
return { success: true, deleted: existing.length };
|
|
} catch (error) {
|
|
console.error('Error clearing representatives:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async logEmailSend(emailData) {
|
|
try {
|
|
const record = {
|
|
recipient_email: emailData.recipientEmail,
|
|
sender_name: emailData.senderName,
|
|
sender_email: emailData.senderEmail,
|
|
subject: emailData.subject,
|
|
postal_code: emailData.postalCode,
|
|
status: emailData.status,
|
|
sent_at: emailData.timestamp,
|
|
created_at: new Date().toISOString()
|
|
};
|
|
|
|
await this.create(this.tableIds.emails, record);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Error logging email:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getEmailLogs(filters = {}) {
|
|
try {
|
|
let whereClause = '';
|
|
const conditions = [];
|
|
|
|
if (filters.postalCode) {
|
|
conditions.push(`(postal_code,eq,${filters.postalCode})`);
|
|
}
|
|
if (filters.senderEmail) {
|
|
conditions.push(`(sender_email,eq,${filters.senderEmail})`);
|
|
}
|
|
if (filters.status) {
|
|
conditions.push(`(status,eq,${filters.status})`);
|
|
}
|
|
|
|
if (conditions.length > 0) {
|
|
whereClause = `?where=${conditions.join('~and')}`;
|
|
}
|
|
|
|
const params = {};
|
|
if (conditions.length > 0) {
|
|
params.where = conditions.join('~and');
|
|
}
|
|
params.sort = '-created_at';
|
|
|
|
const response = await this.getAll(this.tableIds.emails, params);
|
|
return response.list || [];
|
|
} catch (error) {
|
|
console.error('Error getting email logs:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async storePostalCodeInfo(postalCodeData) {
|
|
try {
|
|
const response = await this.create(this.tableIds.postalCodes, postalCodeData);
|
|
return response;
|
|
} catch (error) {
|
|
// Don't throw error for postal code caching failures
|
|
console.log('Postal code info storage failed:', error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Campaign management methods
|
|
async getAllCampaigns() {
|
|
try {
|
|
const response = await this.getAll(this.tableIds.campaigns, {
|
|
sort: '-created_at'
|
|
});
|
|
return response.list || [];
|
|
} catch (error) {
|
|
console.error('Get all campaigns failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getCampaignById(id) {
|
|
try {
|
|
const response = await this.getAll(this.tableIds.campaigns, {
|
|
where: `(id,eq,${id})`
|
|
});
|
|
return response.list && response.list.length > 0 ? response.list[0] : null;
|
|
} catch (error) {
|
|
console.error('Get campaign by ID failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getCampaignBySlug(slug) {
|
|
try {
|
|
const response = await this.getAll(this.tableIds.campaigns, {
|
|
where: `(slug,eq,${slug})`
|
|
});
|
|
return response.list && response.list.length > 0 ? response.list[0] : null;
|
|
} catch (error) {
|
|
console.error('Get campaign by slug failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async createCampaign(campaignData) {
|
|
try {
|
|
const response = await this.create(this.tableIds.campaigns, campaignData);
|
|
return response;
|
|
} catch (error) {
|
|
console.error('Create campaign failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async updateCampaign(id, updates) {
|
|
try {
|
|
// NocoDB update using direct API call
|
|
const url = `${this.getTableUrl(this.tableIds.campaigns)}/${id}`;
|
|
const response = await this.client.patch(url, updates);
|
|
return response.data;
|
|
} catch (error) {
|
|
console.error('Update campaign failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async deleteCampaign(id) {
|
|
try {
|
|
const url = `${this.getTableUrl(this.tableIds.campaigns)}/${id}`;
|
|
const response = await this.client.delete(url);
|
|
return response.data;
|
|
} catch (error) {
|
|
console.error('Delete campaign failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Campaign email tracking methods
|
|
async logCampaignEmail(emailData) {
|
|
try {
|
|
const response = await this.create(this.tableIds.campaignEmails, emailData);
|
|
return response;
|
|
} catch (error) {
|
|
console.error('Log campaign email failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getCampaignEmailCount(campaignId) {
|
|
try {
|
|
const response = await this.getAll(this.tableIds.campaignEmails, {
|
|
where: `(campaign_id,eq,${campaignId})`,
|
|
limit: 1000 // Get enough to count
|
|
});
|
|
return response.pageInfo ? response.pageInfo.totalRows : (response.list ? response.list.length : 0);
|
|
} catch (error) {
|
|
console.error('Get campaign email count failed:', error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
async getCampaignAnalytics(campaignId) {
|
|
try {
|
|
const response = await this.getAll(this.tableIds.campaignEmails, {
|
|
where: `(campaign_id,eq,${campaignId})`,
|
|
limit: 1000
|
|
});
|
|
|
|
const emails = response.list || [];
|
|
|
|
const analytics = {
|
|
totalEmails: emails.length,
|
|
smtpEmails: emails.filter(e => e.email_method === 'smtp').length,
|
|
mailtoClicks: emails.filter(e => e.email_method === 'mailto').length,
|
|
successfulEmails: emails.filter(e => e.status === 'sent' || e.status === 'clicked').length,
|
|
failedEmails: emails.filter(e => e.status === 'failed').length,
|
|
byLevel: {},
|
|
byDate: {},
|
|
recentEmails: emails.slice(0, 10).map(email => ({
|
|
timestamp: email.timestamp,
|
|
user_name: email.user_name,
|
|
recipient_name: email.recipient_name,
|
|
recipient_level: email.recipient_level,
|
|
email_method: email.email_method,
|
|
status: email.status
|
|
}))
|
|
};
|
|
|
|
// Group by government level
|
|
emails.forEach(email => {
|
|
const level = email.recipient_level || 'Other';
|
|
analytics.byLevel[level] = (analytics.byLevel[level] || 0) + 1;
|
|
});
|
|
|
|
// Group by date
|
|
emails.forEach(email => {
|
|
if (email.timestamp) {
|
|
const date = email.timestamp.split('T')[0]; // Get date part
|
|
analytics.byDate[date] = (analytics.byDate[date] || 0) + 1;
|
|
}
|
|
});
|
|
|
|
return analytics;
|
|
} catch (error) {
|
|
console.error('Get campaign analytics failed:', error);
|
|
return {
|
|
totalEmails: 0,
|
|
smtpEmails: 0,
|
|
mailtoClicks: 0,
|
|
successfulEmails: 0,
|
|
failedEmails: 0,
|
|
byLevel: {},
|
|
byDate: {},
|
|
recentEmails: []
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new NocoDBService(); |