Spaces:
Runtime error
Runtime error
| import express from 'express'; | |
| import path from 'path'; | |
| import fs from 'fs'; | |
| import crypto from 'crypto'; | |
| import { fileURLToPath } from 'url'; | |
| import { memberQueryValidator, candidateQueryValidator } from '../middleware/validator.js'; | |
| import { scraperLimiter } from '../middleware/rateLimiter.js'; | |
| import { | |
| fetchFromAppwrite, | |
| getMemberData as fetchMemberFromAppwrite, | |
| getCandidateData as fetchCandidateFromAppwrite, | |
| getStats as getAppwriteStats | |
| } from '../services/appwriteDataFetcher.js'; | |
| import candidateService from '../services/candidateService.js'; | |
| import prsService from '../services/prsService.js'; | |
| import cacheService from '../services/cacheService.js'; | |
| import browserPool from '../services/browserPool.js'; | |
| import fileStorage from '../utils/fileStorage.js'; | |
| import imageProxy from '../services/imageProxy.js'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| const router = express.Router(); | |
| // ============================================================================ | |
| // APPWRITE RESPONSE LOGGER | |
| // ============================================================================ | |
| function logAppwriteResponse(functionName, params, response) { | |
| const logEntry = { | |
| timestamp: new Date().toISOString(), | |
| functionName, | |
| params, | |
| response, | |
| responseSize: JSON.stringify(response).length | |
| }; | |
| const logPath = path.join(__dirname, '..', 'appwriteresponse.txt'); | |
| const logLine = `\n${'='.repeat(80)}\n${JSON.stringify(logEntry, null, 2)}\n`; | |
| fs.appendFileSync(logPath, logLine, 'utf8'); | |
| console.log(`[APPWRITE] ${functionName} - Response logged to appwriteresponse.txt`); | |
| } | |
| // ============================================================================ | |
| // CONSTANTS | |
| // ============================================================================ | |
| const CONFIG = { | |
| TIMEOUTS: { | |
| APPWRITE_PRIMARY: 20_000, | |
| APPWRITE_SECONDARY: 25_000, | |
| LOCAL_PRS: 25_000, | |
| CANDIDATE: 10_000, | |
| }, | |
| DATA_PATHS: { | |
| ASSEMBLY: 'india_assembly.geojson', | |
| PARLIAMENTARY: 'india_parliamentary.geojson', | |
| ALL_DATA: 'all_data.json', | |
| RAJYA_SABHA: 'rajya_sabha.json', | |
| }, | |
| STORAGE_MAX_AGE: 24 * 60 * 60 * 1000, | |
| }; | |
| // ============================================================================ | |
| // UTILITY FUNCTIONS | |
| // ============================================================================ | |
| const generateRequestId = () => crypto.randomBytes(8).toString('hex'); | |
| const loadStaticFile = (filename) => { | |
| const filepath = path.join(__dirname, '..', 'data', | |
| filename.includes('geojson') ? 'geojson' : '', filename); | |
| if (!fs.existsSync(filepath)) { | |
| throw new Error(`File not found: ${filename}`); | |
| } | |
| return JSON.parse(fs.readFileSync(filepath, 'utf8')); | |
| }; | |
| const withCache = async (cacheType, cacheKey, loader) => { | |
| let data = await cacheService.get(cacheType, cacheKey); | |
| if (!data) { | |
| data = await loader(); | |
| cacheService.set(cacheType, cacheKey, data); | |
| } | |
| return data; | |
| }; | |
| const createTimeoutPromise = (ms, errorMsg) => | |
| new Promise((_, reject) => | |
| setTimeout(() => reject(new Error(errorMsg)), ms) | |
| ); | |
| // ============================================================================ | |
| // SMART DATA MERGER | |
| // ============================================================================ | |
| function needsUpdate(value) { | |
| return !value || | |
| value === 'N/A' || | |
| value === 'Unknown' || | |
| value === 'null' || | |
| value === 'undefined' || | |
| value === '' || | |
| String(value).trim() === ''; | |
| } | |
| function smartMergeField(target, source, field, targetLocation = null) { | |
| let currentValue; | |
| let targetObj = target; | |
| if (targetLocation === 'personal') { | |
| if (!target.personal) target.personal = {}; | |
| currentValue = target.personal[field]; | |
| targetObj = target.personal; | |
| } else if (targetLocation === 'performance') { | |
| if (!target.performance) target.performance = {}; | |
| currentValue = target.performance[field]; | |
| targetObj = target.performance; | |
| } else { | |
| currentValue = target[field]; | |
| } | |
| const sourceValue = source[field]; | |
| if (needsUpdate(currentValue) && !needsUpdate(sourceValue)) { | |
| targetObj[field] = sourceValue; | |
| return true; | |
| } | |
| return false; | |
| } | |
| // ============================================================================ | |
| // DATA VALIDATION | |
| // ============================================================================ | |
| function validateAppwritePrsData(data) { | |
| return data?.found || (data?.name && data?.party && data.party !== 'Unknown'); | |
| } | |
| function validateLocalPrsData(data) { | |
| return data?.found && data?.html && data.html.length > 0; | |
| } | |
| function validateCandidateData(data) { | |
| return data && !data.error && (data.candidate || data.movableAssets || data.criminalCases); | |
| } | |
| // ============================================================================ | |
| // DATA MERGING FUNCTIONS | |
| // ============================================================================ | |
| function mergeAppwritePrs(target, data) { | |
| if (data.name) target.name = data.name; | |
| if (data.imageUrl && !target.imageUrl) target.imageUrl = data.imageUrl; | |
| if (data.state) target.state = data.state; | |
| if (data.constituency) target.constituency = data.constituency; | |
| if (data.party) target.party = data.party; | |
| if (!target.personal) target.personal = {}; | |
| const personalFields = ['age', 'gender', 'education', 'termStart', 'termEnd', 'noOfTerm', 'membership']; | |
| personalFields.forEach(field => { | |
| if (data[field] && needsUpdate(target.personal[field])) { | |
| target.personal[field] = data[field]; | |
| } | |
| }); | |
| if (!target.performance) target.performance = {}; | |
| const performanceFields = [ | |
| 'attendance', 'natAttendance', 'stateAttendance', | |
| 'debates', 'natDebates', 'stateDebates', | |
| 'questions', 'natQuestions', 'stateQuestions', | |
| 'pmb', 'natPMB', 'statePMB' | |
| ]; | |
| performanceFields.forEach(field => { | |
| if (data[field] && needsUpdate(target.performance[field])) { | |
| target.performance[field] = data[field]; | |
| } | |
| }); | |
| if (data.attendanceTable) target.attendanceTable = data.attendanceTable; | |
| if (data.debatesTable) target.debatesTable = data.debatesTable; | |
| if (data.questionsTable) target.questionsTable = data.questionsTable; | |
| } | |
| function mergeLocalPrs(target, data) { | |
| Object.assign(target, { | |
| html: data.html || '', | |
| imageUrl: data.imageUrl || target.imageUrl || '', | |
| state: data.state || target.state || 'Unknown', | |
| constituency: data.constituency || target.constituency || 'Unknown', | |
| party: data.party || target.party || 'Unknown', | |
| }); | |
| if (data.performance) { | |
| target.performance = { ...target.performance, ...data.performance }; | |
| } | |
| if (data.personal) { | |
| target.personal = { ...target.personal, ...data.personal }; | |
| } | |
| } | |
| function mergeCandidate(target, data) { | |
| // Set encrypted meow and bhaw at top level | |
| if (data.meow) target.meow = data.meow; | |
| if (data.bhaw) target.bhaw = data.bhaw; | |
| // Clean and proxy the candidate data | |
| const cleanedCandidateData = { ...data }; | |
| // Remove unwanted fields | |
| delete cleanedCandidateData.searchUrl; | |
| delete cleanedCandidateData.timestamp; | |
| delete cleanedCandidateData.fetchedAt; | |
| delete cleanedCandidateData.metadata; | |
| delete cleanedCandidateData.assetLink; | |
| // Proxy image URLs in candidate data | |
| if (cleanedCandidateData.imageUrl) { | |
| const baseUrl = target._req ? `${target._req.protocol}://${target._req.get('host')}` : ''; | |
| const proxyUrl = imageProxy.createProxyUrl(cleanedCandidateData.imageUrl, baseUrl); | |
| if (proxyUrl) { | |
| cleanedCandidateData.imageUrl = proxyUrl; | |
| cleanedCandidateData._imageProxied = true; | |
| } | |
| } | |
| // Proxy image URL in nested candidate object | |
| if (cleanedCandidateData.candidate && cleanedCandidateData.candidate.imageUrl) { | |
| const baseUrl = target._req ? `${target._req.protocol}://${target._req.get('host')}` : ''; | |
| const proxyUrl = imageProxy.createProxyUrl(cleanedCandidateData.candidate.imageUrl, baseUrl); | |
| if (proxyUrl) { | |
| cleanedCandidateData.candidate.imageUrl = proxyUrl; | |
| cleanedCandidateData.candidate._imageProxied = true; | |
| } | |
| } | |
| target.candidateData = cleanedCandidateData; | |
| if (!target.personal) target.personal = {}; | |
| if (data.candidate) { | |
| if (needsUpdate(target.personal.age)) { | |
| target.personal.age = data.candidate.age; | |
| } | |
| if (needsUpdate(target.personal.education)) { | |
| target.personal.education = data.candidate.education; | |
| } | |
| if (needsUpdate(target.personal.gender)) { | |
| target.personal.gender = data.candidate.gender; | |
| } | |
| } | |
| const financialFields = [ | |
| 'assets', 'liabilities', 'immovableAssets', | |
| 'movableAssets', 'incomeTax', 'criminalCases', 'aiSummary', 'summary' | |
| ]; | |
| financialFields.forEach(field => { | |
| if (data[field]) target[field] = data[field]; | |
| }); | |
| } | |
| // ============================================================================ | |
| // MAIN DATA FETCHER | |
| // ============================================================================ | |
| async function fetchAllData(params, requestId) { | |
| const { name, type, constituency, party, state, meow, bhaw } = params; | |
| const startTime = Date.now(); | |
| const results = { | |
| appwritePrimary: null, | |
| localPrs: null, | |
| candidate: null, | |
| }; | |
| const errors = {}; | |
| let fromStorage = { prs: false, candidate: false }; | |
| console.log(`[${requestId}] Fetching all data sources`, { name, type }); | |
| // ======================================== | |
| // CHECK STORAGE FIRST | |
| // ======================================== | |
| try { | |
| const storedPrs = await fileStorage.getPrsData(name, type); | |
| if (storedPrs.found) { | |
| const age = Date.now() - new Date(storedPrs.data.timestamp).getTime(); | |
| if (age < CONFIG.STORAGE_MAX_AGE) { | |
| results.appwritePrimary = storedPrs.data.data; | |
| fromStorage.prs = true; | |
| console.log(`[${requestId}] Using stored PRS data (age: ${Math.round(age / 1000 / 60)} minutes)`); | |
| } | |
| } | |
| const storedCandidate = await fileStorage.getCandidateData(name); | |
| if (storedCandidate.found) { | |
| const age = Date.now() - new Date(storedCandidate.data.timestamp).getTime(); | |
| if (age < CONFIG.STORAGE_MAX_AGE) { | |
| results.candidate = storedCandidate.data.data; | |
| fromStorage.candidate = true; | |
| console.log(`[${requestId}] Using stored candidate data (age: ${Math.round(age / 1000 / 60)} minutes)`); | |
| } | |
| } | |
| } catch (error) { | |
| console.error(`[${requestId}] Storage check failed:`, error); | |
| } | |
| // ======================================== | |
| // FETCH FROM SOURCES (if not in storage) | |
| // ======================================== | |
| const fetchPromises = []; | |
| // ๐ด APPWRITE CALL #1: Primary data fetch | |
| if (!results.appwritePrimary) { | |
| fetchPromises.push( | |
| Promise.race([ | |
| fetchMemberFromAppwrite(name, type, constituency, state), | |
| createTimeoutPromise(CONFIG.TIMEOUTS.APPWRITE_PRIMARY, 'Appwrite primary timeout') | |
| ]) | |
| .then(data => { | |
| // ๐ LOG APPWRITE RESPONSE | |
| logAppwriteResponse('fetchMemberFromAppwrite (PRIMARY)', { name, type, constituency, state }, data); | |
| if (validateAppwritePrsData(data)) { | |
| results.appwritePrimary = data; | |
| fileStorage.savePrsData(name, type, data, { | |
| constituency, | |
| state, | |
| source: 'appwrite-sdk' | |
| }); | |
| console.log(`[${requestId}] โ Appwrite SDK data fetched:`, { | |
| name: data.name, | |
| party: data.party, | |
| attendance: data.attendance, | |
| age: data.age | |
| }); | |
| } | |
| }) | |
| .catch(err => { | |
| errors.appwritePrimary = err.message; | |
| console.error(`[${requestId}] โ Appwrite SDK failed:`, err); | |
| }) | |
| ); | |
| } | |
| if (!results.localPrs) { | |
| fetchPromises.push( | |
| Promise.race([ | |
| prsService.getMemberData(name, type, constituency, party), | |
| createTimeoutPromise(CONFIG.TIMEOUTS.LOCAL_PRS, 'Local PRS timeout') | |
| ]) | |
| .then(data => { | |
| if (validateLocalPrsData(data)) { | |
| results.localPrs = data; | |
| fileStorage.savePrsData(name, type, data, { | |
| constituency, | |
| party, | |
| source: 'local-prs' | |
| }); | |
| console.log(`[${requestId}] โ Local PRS data fetched`); | |
| } | |
| }) | |
| .catch(err => { | |
| errors.localPrs = err.message; | |
| console.error(`[${requestId}] โ Local PRS failed:`, err); | |
| }) | |
| ); | |
| } | |
| if (!results.candidate) { | |
| fetchPromises.push( | |
| Promise.race([ | |
| candidateService.getCandidateData(name, constituency || '', party || '', meow, bhaw), | |
| createTimeoutPromise(CONFIG.TIMEOUTS.CANDIDATE, 'Candidate timeout') | |
| ]) | |
| .then(result => { | |
| const data = result?.data || result; | |
| if (validateCandidateData(data)) { | |
| results.candidate = data; | |
| fileStorage.saveCandidateData(name, data, { | |
| constituency, | |
| party, | |
| source: 'appwrite-candidate' | |
| }); | |
| console.log(`[${requestId}] โ Candidate data fetched`); | |
| } | |
| }) | |
| .catch(err => { | |
| errors.candidate = err.message; | |
| console.error(`[${requestId}] โ Candidate fetch failed:`, err); | |
| }) | |
| ); | |
| } | |
| await Promise.allSettled(fetchPromises); | |
| const duration = Date.now() - startTime; | |
| console.log(`[${requestId}] Initial fetch completed in ${duration}ms`, { | |
| appwritePrimary: !!results.appwritePrimary, | |
| localPrs: !!results.localPrs, | |
| candidate: !!results.candidate, | |
| }); | |
| return { results, errors, fromStorage, duration }; | |
| } | |
| // ============================================================================ | |
| // SECONDARY APPWRITE CALL | |
| // ============================================================================ | |
| async function fetchSecondaryData(params, currentData, requestId) { | |
| const { name, type, constituency, state } = params; | |
| console.log(`[${requestId}] ๐ Making secondary Appwrite SDK call to fill missing data`); | |
| try { | |
| // ๐ด APPWRITE CALL #2: Secondary data fetch | |
| const secondaryData = await Promise.race([ | |
| fetchMemberFromAppwrite(name, type, constituency, state), | |
| createTimeoutPromise(CONFIG.TIMEOUTS.APPWRITE_SECONDARY, 'Secondary Appwrite timeout') | |
| ]); | |
| // ๐ LOG APPWRITE RESPONSE | |
| logAppwriteResponse('fetchMemberFromAppwrite (SECONDARY)', { name, type, constituency, state }, secondaryData); | |
| if (!validateAppwritePrsData(secondaryData)) { | |
| console.warn(`[${requestId}] Secondary call returned no valid data`); | |
| return null; | |
| } | |
| console.log(`[${requestId}] Secondary data received:`, { | |
| name: secondaryData.name, | |
| attendance: secondaryData.attendance, | |
| age: secondaryData.age, | |
| education: secondaryData.education, | |
| debates: secondaryData.debates, | |
| questions: secondaryData.questions, | |
| attendanceTable: secondaryData.attendanceTable ? 'Present' : 'Missing', | |
| debatesTable: secondaryData.debatesTable ? 'Present' : 'Missing', | |
| questionsTable: secondaryData.questionsTable ? 'Present' : 'Missing', | |
| }); | |
| const updates = {}; | |
| const personalFields = ['age', 'gender', 'education', 'termStart', 'termEnd', 'noOfTerm', 'membership']; | |
| personalFields.forEach(field => { | |
| if (smartMergeField(currentData, secondaryData, field, 'personal')) { | |
| if (!updates.personal) updates.personal = {}; | |
| updates.personal[field] = secondaryData[field]; | |
| } | |
| }); | |
| const performanceFields = [ | |
| 'attendance', 'natAttendance', 'stateAttendance', | |
| 'debates', 'natDebates', 'stateDebates', | |
| 'questions', 'natQuestions', 'stateQuestions', | |
| 'pmb', 'natPMB', 'statePMB' | |
| ]; | |
| performanceFields.forEach(field => { | |
| if (smartMergeField(currentData, secondaryData, field, 'performance')) { | |
| if (!updates.performance) updates.performance = {}; | |
| updates.performance[field] = secondaryData[field]; | |
| } | |
| }); | |
| const topLevelFields = ['imageUrl', 'state', 'constituency', 'party', 'attendanceTable', 'debatesTable', 'questionsTable']; | |
| topLevelFields.forEach(field => { | |
| if (smartMergeField(currentData, secondaryData, field)) { | |
| updates[field] = secondaryData[field]; | |
| } | |
| }); | |
| if (secondaryData.attendanceTable && !currentData.attendanceTable) { | |
| currentData.attendanceTable = secondaryData.attendanceTable; | |
| updates.attendanceTable = secondaryData.attendanceTable; | |
| } | |
| if (secondaryData.debatesTable && !currentData.debatesTable) { | |
| currentData.debatesTable = secondaryData.debatesTable; | |
| updates.debatesTable = secondaryData.debatesTable; | |
| } | |
| if (secondaryData.questionsTable && !currentData.questionsTable) { | |
| currentData.questionsTable = secondaryData.questionsTable; | |
| updates.questionsTable = secondaryData.questionsTable; | |
| } | |
| if (Object.keys(updates).length > 0) { | |
| console.log(`[${requestId}] โ Secondary call filled missing fields:`, Object.keys(updates)); | |
| return { | |
| updated: true, | |
| fields: Object.keys(updates), | |
| data: updates | |
| }; | |
| } else { | |
| console.log(`[${requestId}] Secondary call made but no new data needed`); | |
| return { updated: false }; | |
| } | |
| } catch (error) { | |
| console.error(`[${requestId}] โ Secondary Appwrite SDK call failed:`, error); | |
| return null; | |
| } | |
| } | |
| // ============================================================================ | |
| // BUILD FINAL RESPONSE | |
| // ============================================================================ | |
| function buildResponse(results, fromStorage, req) { | |
| const merged = { | |
| found: true, | |
| fromStorage, | |
| sources: { | |
| appwritePrimary: !!results.appwritePrimary, | |
| localPrs: !!results.localPrs, | |
| candidate: !!results.candidate, | |
| } | |
| }; | |
| merged._baseUrl = `${req.protocol}://${req.get('host')}`; | |
| if (results.candidate) { | |
| mergeCandidate(merged, results.candidate); | |
| } | |
| if (results.appwritePrimary) { | |
| mergeAppwritePrs(merged, results.appwritePrimary); | |
| } else if (results.localPrs) { | |
| mergeLocalPrs(merged, results.localPrs); | |
| } | |
| // Proxy top-level imageUrl | |
| if (merged.imageUrl) { | |
| const baseUrl = req ? `${req.protocol}://${req.get('host')}` : ''; | |
| const proxyUrl = imageProxy.createProxyUrl(merged.imageUrl, baseUrl); | |
| if (proxyUrl) { | |
| merged.imageUrl = proxyUrl; | |
| merged._imageProxied = true; | |
| } | |
| } | |
| // Proxy imageUrl in candidateData | |
| if (merged.candidateData && merged.candidateData.imageUrl) { | |
| const baseUrl = req ? `${req.protocol}://${req.get('host')}` : ''; | |
| const proxyUrl = imageProxy.createProxyUrl(merged.candidateData.imageUrl, baseUrl); | |
| if (proxyUrl) { | |
| merged.candidateData.imageUrl = proxyUrl; | |
| merged.candidateData._imageProxied = true; | |
| } | |
| } | |
| // Proxy imageUrl in nested candidate object | |
| if (merged.candidateData && merged.candidateData.candidate && merged.candidateData.candidate.imageUrl) { | |
| const baseUrl = req ? `${req.protocol}://${req.get('host')}` : ''; | |
| const proxyUrl = imageProxy.createProxyUrl(merged.candidateData.candidate.imageUrl, baseUrl); | |
| if (proxyUrl) { | |
| merged.candidateData.candidate.imageUrl = proxyUrl; | |
| merged.candidateData.candidate._imageProxied = true; | |
| } | |
| } | |
| return merged; | |
| } | |
| // ============================================================================ | |
| // ROUTES | |
| // ============================================================================ | |
| router.get('/image/:imageId', async (req, res) => { | |
| const { imageId } = req.params; | |
| const requestId = req.sessionId || generateRequestId(); | |
| try { | |
| const cached = await cacheService.get('image', imageId); | |
| if (cached) { | |
| console.log(`[${requestId}] Image served from cache: ${imageId}`); | |
| res.set({ | |
| 'Content-Type': cached.contentType || 'image/jpeg', | |
| 'Cache-Control': 'public, max-age=26400', | |
| 'X-Source': 'cache', | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Methods': 'GET', | |
| 'Cross-Origin-Resource-Policy': 'cross-origin' | |
| }); | |
| return res.send(cached.buffer); | |
| } | |
| const actualUrl = imageProxy.getActualUrl(imageId); | |
| if (!actualUrl) { | |
| console.warn(`[${requestId}] Invalid image ID: ${imageId}`); | |
| return res.status(404).json({ error: 'Image not found' }); | |
| } | |
| console.log(`[${requestId}] Fetching image: ${imageId}`); | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 10000); | |
| const response = await fetch(actualUrl, { | |
| signal: controller.signal, | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (compatible; FixKaro/1.0)', | |
| } | |
| }); | |
| clearTimeout(timeout); | |
| if (!response.ok) { | |
| throw new Error(`Failed to fetch image: ${response.status}`); | |
| } | |
| const contentType = response.headers.get('content-type') || 'image/jpeg'; | |
| const arrayBuffer = await response.arrayBuffer(); | |
| const buffer = Buffer.from(arrayBuffer); | |
| await cacheService.set('image', imageId, { | |
| buffer, | |
| contentType, | |
| cachedAt: new Date().toISOString() | |
| }); | |
| console.log(`[${requestId}] โ Image fetched and cached: ${imageId} (${(buffer.length / 1024).toFixed(2)} KB)`); | |
| res.set({ | |
| 'Content-Type': contentType, | |
| 'Cache-Control': 'public, max-age=86400', | |
| 'Content-Length': buffer.length, | |
| 'X-Source': 'proxy' | |
| }); | |
| res.send(buffer); | |
| } catch (error) { | |
| if (error.name === 'AbortError') { | |
| console.warn(`[${requestId}] Image fetch timeout: ${imageId}`); | |
| return res.status(504).json({ error: 'Image fetch timeout' }); | |
| } | |
| console.error(`[${requestId}] Image proxy error: ${imageId}`, error); | |
| res.status(500).json({ error: 'Failed to fetch image' }); | |
| } | |
| }); | |
| router.get('/constituencies', async (req, res) => { | |
| try { | |
| const { type = 'assembly' } = req.query; | |
| const filename = type === 'assembly' | |
| ? CONFIG.DATA_PATHS.ASSEMBLY | |
| : CONFIG.DATA_PATHS.PARLIAMENTARY; | |
| const data = await withCache('geojson', type, () => loadStaticFile(filename)); | |
| res.json(data); | |
| } catch (error) { | |
| console.error('Constituencies error:', error); | |
| res.status(error.message.includes('not found') ? 404 : 500) | |
| .json({ error: error.message }); | |
| } | |
| }); | |
| router.get('/image-proxy/stats', (req, res) => { | |
| res.json({ | |
| imageProxy: imageProxy.getStats(), | |
| imageCache: cacheService.getStats().image || { hits: 0, misses: 0 }, | |
| timestamp: new Date().toISOString() | |
| }); | |
| }); | |
| router.get('/all-data', async (req, res) => { | |
| try { | |
| const data = await withCache('geojson', 'all_data', | |
| () => loadStaticFile(CONFIG.DATA_PATHS.ALL_DATA)); | |
| res.json(data); | |
| } catch (error) { | |
| console.error('All-data error:', error); | |
| res.status(500).json({ error: 'Failed to load data' }); | |
| } | |
| }); | |
| router.get('/rajya-sabha', async (req, res) => { | |
| try { | |
| const data = await withCache('geojson', 'rajya_sabha', | |
| () => loadStaticFile(CONFIG.DATA_PATHS.RAJYA_SABHA)); | |
| res.json(data); | |
| } catch (error) { | |
| console.error('Rajya Sabha error:', error); | |
| res.status(500).json({ error: 'Failed to load data' }); | |
| } | |
| }); | |
| // ============================================================================ | |
| // MAIN PRS ENDPOINT | |
| // ============================================================================ | |
| router.get('/prs', scraperLimiter, memberQueryValidator, async (req, res) => { | |
| const requestId = req.sessionId || generateRequestId(); | |
| const params = req.query; | |
| const { name, type } = params; | |
| const overallStartTime = Date.now(); | |
| try { | |
| const { results, errors, fromStorage, duration } = await fetchAllData(params, requestId); | |
| const hasData = results.appwritePrimary || results.localPrs || results.candidate; | |
| if (!hasData) { | |
| console.warn(`[${requestId}] No data found from any source`, { name, type }); | |
| fileStorage.saveAnalytics('prs_request', { | |
| name, | |
| type, | |
| duration, | |
| success: false, | |
| errors: Object.keys(errors), | |
| }); | |
| return res.status(404).json({ | |
| found: false, | |
| error: 'Member not found in any database', | |
| name, | |
| type, | |
| details: errors, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| } | |
| const responseData = buildResponse(results, fromStorage, req); | |
| console.log(`[${requestId}] Initial response built:`, { | |
| hasPrsData: !!(results.appwritePrimary || results.localPrs), | |
| hasCandidateData: !!results.candidate, | |
| personalFields: Object.keys(responseData.personal || {}), | |
| performanceFields: Object.keys(responseData.performance || {}), | |
| }); | |
| // ASYNC SECONDARY FETCH - Fire and forget | |
| fetchSecondaryData(params, responseData, requestId) | |
| .then(secondaryResult => { | |
| if (secondaryResult?.updated) { | |
| console.log(`[${requestId}] ๐ Background update completed`); | |
| // Store result in cache for polling | |
| cacheService.set('poll', requestId, { | |
| secondaryDataMerged: true, | |
| mergedFields: secondaryResult.fields, | |
| ...secondaryResult.data // Spread the nested updates (personal, performance, etc.) | |
| }, 5 * 60 * 1000); // 5 minutes TTL | |
| } | |
| }) | |
| .catch(err => console.error(`[${requestId}] Background fetch failed:`, err)); | |
| const totalDuration = Date.now() - overallStartTime; | |
| fileStorage.saveAnalytics('prs_request', { | |
| name, | |
| type, | |
| sources: responseData.sources, | |
| fromStorage, | |
| secondaryCallMade: true, // It's always attempted now | |
| duration: totalDuration, | |
| success: true, | |
| }); | |
| console.log(`[${requestId}] โ PRS initial response sent (secondary fetch in background):`, { | |
| name, | |
| sources: responseData.sources, | |
| totalDuration: `${totalDuration}ms`, | |
| }); | |
| return res.json({ | |
| ...responseData, | |
| bothComplete: false, // Signal that more data might come | |
| requestId, // Crucial for polling | |
| timing: { | |
| total: totalDuration, | |
| primary: duration, | |
| secondary: 0 // Async | |
| }, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| } catch (error) { | |
| console.error(`[${requestId}] โ Unexpected error in PRS endpoint:`, error); | |
| res.status(500).json({ | |
| error: 'Internal server error', | |
| message: error.message, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| } | |
| }); | |
| // ============================================================================ | |
| // CANDIDATE ENDPOINT | |
| // ============================================================================ | |
| router.get('/candidate', scraperLimiter, candidateQueryValidator, async (req, res) => { | |
| const requestId = req.sessionId || generateRequestId(); | |
| try { | |
| const { name, constituency, party, meow, bhaw } = req.query; | |
| console.log(`[${requestId}] Candidate request:`, { name }); | |
| const result = await candidateService.getCandidateData( | |
| name, | |
| constituency, | |
| party, | |
| meow, | |
| bhaw | |
| ); | |
| console.log(`[${requestId}] Result is:`, result); | |
| res.json({ | |
| data: result.data || result, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| } catch (error) { | |
| console.error(`[${requestId}] โ Candidate fetch error:`, error); | |
| res.status(500).json({ | |
| error: 'Failed to fetch candidate data', | |
| timestamp: new Date().toISOString(), | |
| }); | |
| } | |
| }); | |
| // ============================================================================ | |
| // DEBUG & ADMIN ENDPOINTS | |
| // ============================================================================ | |
| router.get('/test-appwrite-sdk', async (req, res) => { | |
| try { | |
| const { name = 'ANITA SUBHADARSHINI', type = 'MP' } = req.query; | |
| const requestId = generateRequestId(); | |
| console.log(`[${requestId}] Testing Appwrite SDK:`, { name, type }); | |
| // ๐ด APPWRITE CALL #3: Test endpoint | |
| const result = await fetchMemberFromAppwrite(name, type); | |
| // ๐ LOG APPWRITE RESPONSE | |
| logAppwriteResponse('fetchMemberFromAppwrite (TEST)', { name, type }, result); | |
| console.log("Result:", result); | |
| res.json({ | |
| success: true, | |
| result, | |
| stats: getAppwriteStats(), | |
| timestamp: new Date().toISOString(), | |
| }); | |
| } catch (error) { | |
| res.status(500).json({ | |
| success: false, | |
| error: error.message, | |
| stack: error.stack, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| } | |
| }); | |
| router.get('/debug-full-flow', async (req, res) => { | |
| const { name, type } = req.query; | |
| if (!name || !type) { | |
| return res.status(400).json({ error: 'name and type are required' }); | |
| } | |
| try { | |
| const requestId = generateRequestId(); | |
| // ๐ด APPWRITE CALL #4: Debug primary | |
| const primary = await fetchMemberFromAppwrite(name, type); | |
| logAppwriteResponse('fetchMemberFromAppwrite (DEBUG-PRIMARY)', { name, type }, primary); | |
| const mockResponse = { | |
| name, | |
| type, | |
| personal: {}, | |
| performance: {} | |
| }; | |
| mergeAppwritePrs(mockResponse, primary); | |
| const secondary = await fetchMemberFromAppwrite(name, type); | |
| logAppwriteResponse('fetchMemberFromAppwrite (DEBUG-SECONDARY)', { name, type }, secondary); | |
| const beforeMerge = JSON.parse(JSON.stringify(mockResponse)); | |
| const secondaryResult = await fetchSecondaryData( | |
| { name, type }, | |
| mockResponse, | |
| requestId | |
| ); | |
| res.json({ | |
| primary, | |
| secondary, | |
| beforeMerge, | |
| afterMerge: mockResponse, | |
| secondaryResult, | |
| fieldsUpdated: secondaryResult?.fields || [], | |
| }); | |
| } catch (error) { | |
| res.status(500).json({ | |
| error: error.message, | |
| stack: error.stack | |
| }); | |
| } | |
| }); | |
| router.get('/health', (req, res) => { | |
| const memUsage = process.memoryUsage(); | |
| res.status(200).json({ | |
| status: 'healthy', | |
| uptime: process.uptime(), | |
| memory: { | |
| used: (memUsage.heapUsed / 1024 / 1024).toFixed(2) + ' MB', | |
| total: (memUsage.heapTotal / 1024 / 1024).toFixed(2) + ' MB', | |
| percentage: ((memUsage.heapUsed / memUsage.heapTotal) * 100).toFixed(2) + '%', | |
| }, | |
| services: { | |
| browser: browserPool.getStats(), | |
| cache: cacheService.getStats(), | |
| appwrite: getAppwriteStats(), | |
| candidate: candidateService.getStats(), | |
| }, | |
| storage: fileStorage.getStats(), | |
| timestamp: new Date().toISOString(), | |
| }); | |
| }); | |
| router.post('/cache/clear', (req, res) => { | |
| const { type } = req.query; | |
| cacheService.flush(type); | |
| candidateService.clearCache(); | |
| console.log('Cache cleared:', { type: type || 'all' }); | |
| res.json({ | |
| message: `Cache cleared: ${type || 'all'}`, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| }); | |
| router.get('/welcome', (req, res) => { | |
| console.log('Welcome endpoint accessed'); | |
| res.json({ | |
| message: "Welcome to the FixKaro Web API!", | |
| version: "3.0", | |
| features: [ | |
| "Appwrite Node SDK integration", | |
| "Dual SDK calls for complete data", | |
| "Smart field-level merging", | |
| "File-based caching with 24h TTL", | |
| "Progressive data loading", | |
| "Comprehensive logging" | |
| ], | |
| timestamp: new Date().toISOString(), | |
| }); | |
| }); | |
| console.log('โ API routes initialized with Appwrite SDK dual-call support'); | |
| // ============================================================================ | |
| // POLLING ENDPOINT | |
| // ============================================================================ | |
| router.get('/prs/poll/:requestId', async (req, res) => { | |
| const { requestId } = req.params; | |
| const data = await cacheService.get('poll', requestId); | |
| if (data) { | |
| res.json({ ready: true, data }); | |
| } else { | |
| res.json({ ready: false }); | |
| } | |
| }); | |
| export default router; |