@@ -3,10 +3,21 @@ require('dotenv').config(); |
| 3 | | 3 | |
| 4 | const express = require('express'); | 4 | const express = require('express'); |
| 5 | const cors = require('cors'); | 5 | const cors = require('cors'); |
| 6 | -const fs = require('fs').promises; | | |
| 7 | const path = require('path'); | 6 | const path = require('path'); |
| 8 | const axios = require('axios'); | 7 | const axios = require('axios'); |
| 9 | | 8 | |
| | 9 | +// Import our new database module |
| | 10 | +const { |
| | 11 | + initializeDatabase, |
| | 12 | + getAllRestaurants, |
| | 13 | + getNearbyRestaurants, |
| | 14 | + getRestaurantByPlaceId, |
| | 15 | + addRestaurant, |
| | 16 | + addRating, |
| | 17 | + getRestaurantRatings, |
| | 18 | + migrateFromJSON |
| | 19 | +} = require('./database'); |
| | 20 | + |
| 10 | const app = express(); | 21 | const app = express(); |
| 11 | const PORT = process.env.PORT || 3000; | 22 | const PORT = process.env.PORT || 3000; |
| 12 | | 23 | |
@@ -14,64 +25,6 @@ const PORT = process.env.PORT || 3000; |
| 14 | app.use(cors()); | 25 | app.use(cors()); |
| 15 | app.use(express.json()); | 26 | app.use(express.json()); |
| 16 | | 27 | |
| 17 | -// Simple file-based storage | | |
| 18 | -const DATA_DIR = path.join(__dirname, 'db'); | | |
| 19 | -const RESTAURANTS_FILE = path.join(DATA_DIR, 'restaurants.json'); | | |
| 20 | -const RATINGS_FILE = path.join(DATA_DIR, 'ratings.json'); | | |
| 21 | - | | |
| 22 | -// Initialize data files | | |
| 23 | -async function initializeData() { | | |
| 24 | - try { | | |
| 25 | - await fs.mkdir(DATA_DIR, { recursive: true }); | | |
| 26 | - | | |
| 27 | - try { | | |
| 28 | - await fs.access(RESTAURANTS_FILE); | | |
| 29 | - } catch { | | |
| 30 | - await fs.writeFile(RESTAURANTS_FILE, JSON.stringify([])); | | |
| 31 | - } | | |
| 32 | - | | |
| 33 | - try { | | |
| 34 | - await fs.access(RATINGS_FILE); | | |
| 35 | - } catch { | | |
| 36 | - await fs.writeFile(RATINGS_FILE, JSON.stringify([])); | | |
| 37 | - } | | |
| 38 | - } catch (error) { | | |
| 39 | - console.error('Error initializing data files:', error); | | |
| 40 | - } | | |
| 41 | -} | | |
| 42 | - | | |
| 43 | -// Helper functions | | |
| 44 | -async function getRestaurants() { | | |
| 45 | - const data = await fs.readFile(RESTAURANTS_FILE, 'utf8'); | | |
| 46 | - return JSON.parse(data); | | |
| 47 | -} | | |
| 48 | - | | |
| 49 | -async function saveRestaurants(restaurants) { | | |
| 50 | - await fs.writeFile(RESTAURANTS_FILE, JSON.stringify(restaurants, null, 2)); | | |
| 51 | -} | | |
| 52 | - | | |
| 53 | -async function getRatings() { | | |
| 54 | - const data = await fs.readFile(RATINGS_FILE, 'utf8'); | | |
| 55 | - return JSON.parse(data); | | |
| 56 | -} | | |
| 57 | - | | |
| 58 | -async function saveRatings(ratings) { | | |
| 59 | - await fs.writeFile(RATINGS_FILE, JSON.stringify(ratings, null, 2)); | | |
| 60 | -} | | |
| 61 | - | | |
| 62 | -// Calculate distance between two coordinates (in km) | | |
| 63 | -function calculateDistance(lat1, lon1, lat2, lon2) { | | |
| 64 | - const R = 6371; // Radius of the Earth in km | | |
| 65 | - const dLat = (lat2 - lat1) * Math.PI / 180; | | |
| 66 | - const dLon = (lon2 - lon1) * Math.PI / 180; | | |
| 67 | - const a = | | |
| 68 | - Math.sin(dLat/2) * Math.sin(dLat/2) + | | |
| 69 | - Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * | | |
| 70 | - Math.sin(dLon/2) * Math.sin(dLon/2); | | |
| 71 | - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); | | |
| 72 | - return R * c; | | |
| 73 | -} | | |
| 74 | - | | |
| 75 | // Test Google Places API | 28 | // Test Google Places API |
| 76 | app.get('/api/test-google', async (req, res) => { | 29 | app.get('/api/test-google', async (req, res) => { |
| 77 | if (!process.env.GOOGLE_PLACES_API_KEY) { | 30 | if (!process.env.GOOGLE_PLACES_API_KEY) { |
@@ -113,8 +66,11 @@ app.get('/api/restaurants/nearby', async (req, res) => { |
| 113 | return res.status(400).json({ error: 'Latitude and longitude are required' }); | 66 | return res.status(400).json({ error: 'Latitude and longitude are required' }); |
| 114 | } | 67 | } |
| 115 | | 68 | |
| 116 | - const restaurants = await getRestaurants(); | 69 | + const restaurants = await getNearbyRestaurants( |
| 117 | - const ratings = await getRatings(); | 70 | + parseFloat(lat), |
| | 71 | + parseFloat(lng), |
| | 72 | + parseFloat(radius) |
| | 73 | + ); |
| 118 | | 74 | |
| 119 | // If we have no restaurants, try to fetch some from OpenStreetMap | 75 | // If we have no restaurants, try to fetch some from OpenStreetMap |
| 120 | if (restaurants.length === 0) { | 76 | if (restaurants.length === 0) { |
@@ -139,46 +95,31 @@ app.get('/api/restaurants/nearby', async (req, res) => { |
| 139 | | 95 | |
| 140 | const places = response.data.elements | 96 | const places = response.data.elements |
| 141 | .filter(place => place.tags && place.tags.name) | 97 | .filter(place => place.tags && place.tags.name) |
| 142 | - .slice(0, 5) // Just add first 5 automatically | 98 | + .slice(0, 5); // Just add first 5 automatically |
| 143 | - .map((place, index) => ({ | | |
| 144 | - id: index + 1, | | |
| 145 | - place_id: `osm_${place.id}`, | | |
| 146 | - name: place.tags.name, | | |
| 147 | - address: [ | | |
| 148 | - place.tags['addr:street'], | | |
| 149 | - place.tags['addr:city'] | | |
| 150 | - ].filter(Boolean).join(', ') || 'Address not available', | | |
| 151 | - latitude: place.lat, | | |
| 152 | - longitude: place.lon, | | |
| 153 | - created_at: new Date().toISOString() | | |
| 154 | - })); | | |
| 155 | | 99 | |
| 156 | - if (places.length > 0) { | 100 | + for (const place of places) { |
| 157 | - await saveRestaurants(places); | 101 | + try { |
| 158 | - restaurants.push(...places); | 102 | + const newRestaurant = await addRestaurant({ |
| | 103 | + place_id: `osm_${place.id}`, |
| | 104 | + name: place.tags.name, |
| | 105 | + address: [ |
| | 106 | + place.tags['addr:street'], |
| | 107 | + place.tags['addr:city'] |
| | 108 | + ].filter(Boolean).join(', ') || 'Address not available', |
| | 109 | + latitude: place.lat, |
| | 110 | + longitude: place.lon |
| | 111 | + }); |
| | 112 | + restaurants.push(newRestaurant); |
| | 113 | + } catch (err) { |
| | 114 | + console.error('Error adding restaurant from OSM:', err); |
| | 115 | + } |
| 159 | } | 116 | } |
| 160 | } catch (apiError) { | 117 | } catch (apiError) { |
| 161 | console.error('Failed to fetch from OpenStreetMap:', apiError.message); | 118 | console.error('Failed to fetch from OpenStreetMap:', apiError.message); |
| 162 | } | 119 | } |
| 163 | } | 120 | } |
| 164 | - | | |
| 165 | - // Filter restaurants within radius and calculate ratings | | |
| 166 | - const nearbyRestaurants = restaurants | | |
| 167 | - .filter(r => calculateDistance(parseFloat(lat), parseFloat(lng), r.latitude, r.longitude) <= radius) | | |
| 168 | - .map(restaurant => { | | |
| 169 | - const restaurantRatings = ratings.filter(r => r.restaurant_id === restaurant.id); | | |
| 170 | - const average_rating = restaurantRatings.length > 0 | | |
| 171 | - ? restaurantRatings.reduce((sum, r) => sum + r.rating, 0) / restaurantRatings.length | | |
| 172 | - : null; | | |
| 173 | - | | |
| 174 | - return { | | |
| 175 | - ...restaurant, | | |
| 176 | - average_rating, | | |
| 177 | - total_ratings: restaurantRatings.length | | |
| 178 | - }; | | |
| 179 | - }); | | |
| 180 | | 121 | |
| 181 | - res.json(nearbyRestaurants); | 122 | + res.json(restaurants); |
| 182 | } catch (error) { | 123 | } catch (error) { |
| 183 | console.error('Error fetching restaurants:', error); | 124 | console.error('Error fetching restaurants:', error); |
| 184 | res.status(500).json({ error: 'Failed to fetch restaurants' }); | 125 | res.status(500).json({ error: 'Failed to fetch restaurants' }); |
@@ -190,26 +131,19 @@ app.post('/api/restaurants', async (req, res) => { |
| 190 | try { | 131 | try { |
| 191 | const { place_id, name, address, latitude, longitude } = req.body; | 132 | const { place_id, name, address, latitude, longitude } = req.body; |
| 192 | | 133 | |
| 193 | - const restaurants = await getRestaurants(); | | |
| 194 | - | | |
| 195 | // Check if restaurant already exists | 134 | // Check if restaurant already exists |
| 196 | - const existing = restaurants.find(r => r.place_id === place_id); | 135 | + const existing = await getRestaurantByPlaceId(place_id); |
| 197 | if (existing) { | 136 | if (existing) { |
| 198 | return res.json(existing); | 137 | return res.json(existing); |
| 199 | } | 138 | } |
| 200 | | 139 | |
| 201 | - const newRestaurant = { | 140 | + const newRestaurant = await addRestaurant({ |
| 202 | - id: restaurants.length + 1, | | |
| 203 | place_id, | 141 | place_id, |
| 204 | name, | 142 | name, |
| 205 | address, | 143 | address, |
| 206 | latitude, | 144 | latitude, |
| 207 | - longitude, | 145 | + longitude |
| 208 | - created_at: new Date().toISOString() | 146 | + }); |
| 209 | - }; | | |
| 210 | - | | |
| 211 | - restaurants.push(newRestaurant); | | |
| 212 | - await saveRestaurants(restaurants); | | |
| 213 | | 147 | |
| 214 | res.json(newRestaurant); | 148 | res.json(newRestaurant); |
| 215 | } catch (error) { | 149 | } catch (error) { |
@@ -237,23 +171,8 @@ app.post('/api/restaurants/:id/ratings', async (req, res) => { |
| 237 | } | 171 | } |
| 238 | } | 172 | } |
| 239 | | 173 | |
| 240 | - const ratings = await getRatings(); | 174 | + const result = await addRating(parseInt(id), rating, review); |
| 241 | - | 175 | + res.json(result); |
| 242 | - const newRating = { | | |
| 243 | - id: ratings.length + 1, | | |
| 244 | - restaurant_id: parseInt(id), | | |
| 245 | - rating, | | |
| 246 | - review: review || null, | | |
| 247 | - created_at: new Date().toISOString() | | |
| 248 | - }; | | |
| 249 | - | | |
| 250 | - ratings.push(newRating); | | |
| 251 | - await saveRatings(ratings); | | |
| 252 | - | | |
| 253 | - res.json({ | | |
| 254 | - id: newRating.id, | | |
| 255 | - message: 'Toast rating added successfully! 🍞' | | |
| 256 | - }); | | |
| 257 | } catch (error) { | 176 | } catch (error) { |
| 258 | console.error('Error adding rating:', error); | 177 | console.error('Error adding rating:', error); |
| 259 | res.status(500).json({ error: 'Failed to add rating' }); | 178 | res.status(500).json({ error: 'Failed to add rating' }); |
@@ -264,13 +183,8 @@ app.post('/api/restaurants/:id/ratings', async (req, res) => { |
| 264 | app.get('/api/restaurants/:id/ratings', async (req, res) => { | 183 | app.get('/api/restaurants/:id/ratings', async (req, res) => { |
| 265 | try { | 184 | try { |
| 266 | const { id } = req.params; | 185 | const { id } = req.params; |
| 267 | - const ratings = await getRatings(); | 186 | + const ratings = await getRestaurantRatings(parseInt(id)); |
| 268 | - | 187 | + res.json(ratings); |
| 269 | - const restaurantRatings = ratings | | |
| 270 | - .filter(r => r.restaurant_id === parseInt(id)) | | |
| 271 | - .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); | | |
| 272 | - | | |
| 273 | - res.json(restaurantRatings); | | |
| 274 | } catch (error) { | 188 | } catch (error) { |
| 275 | console.error('Error fetching ratings:', error); | 189 | console.error('Error fetching ratings:', error); |
| 276 | res.status(500).json({ error: 'Failed to fetch ratings' }); | 190 | res.status(500).json({ error: 'Failed to fetch ratings' }); |
@@ -416,22 +330,21 @@ app.post('/api/seed', async (req, res) => { |
| 416 | { place_id: 'seed_4', name: 'Crispy Corner', address: '400 Crunch St', latitude: parseFloat(lat) - 0.005, longitude: parseFloat(lng) - 0.005 } | 330 | { place_id: 'seed_4', name: 'Crispy Corner', address: '400 Crunch St', latitude: parseFloat(lat) - 0.005, longitude: parseFloat(lng) - 0.005 } |
| 417 | ]; | 331 | ]; |
| 418 | | 332 | |
| 419 | - const restaurants = await getRestaurants(); | | |
| 420 | let added = 0; | 333 | let added = 0; |
| 421 | | 334 | |
| 422 | for (const seedRestaurant of seedRestaurants) { | 335 | for (const seedRestaurant of seedRestaurants) { |
| 423 | - if (!restaurants.find(r => r.place_id === seedRestaurant.place_id)) { | 336 | + try { |
| 424 | - restaurants.push({ | 337 | + await addRestaurant(seedRestaurant); |
| 425 | - id: restaurants.length + 1, | | |
| 426 | - ...seedRestaurant, | | |
| 427 | - created_at: new Date().toISOString() | | |
| 428 | - }); | | |
| 429 | added++; | 338 | added++; |
| | 339 | + } catch (err) { |
| | 340 | + if (err.message.includes('UNIQUE constraint failed')) { |
| | 341 | + console.log(`Seed restaurant already exists: ${seedRestaurant.name}`); |
| | 342 | + } else { |
| | 343 | + console.error(`Failed to add seed restaurant:`, err); |
| | 344 | + } |
| 430 | } | 345 | } |
| 431 | } | 346 | } |
| 432 | | 347 | |
| 433 | - await saveRestaurants(restaurants); | | |
| 434 | - | | |
| 435 | res.json({ message: `Seeded ${added} restaurants with toast! 🍞` }); | 348 | res.json({ message: `Seeded ${added} restaurants with toast! 🍞` }); |
| 436 | } catch (error) { | 349 | } catch (error) { |
| 437 | console.error('Error seeding data:', error); | 350 | console.error('Error seeding data:', error); |
@@ -439,9 +352,30 @@ app.post('/api/seed', async (req, res) => { |
| 439 | } | 352 | } |
| 440 | }); | 353 | }); |
| 441 | | 354 | |
| 442 | -// Initialize data files and start server | 355 | +// Migration endpoint (one-time use) |
| 443 | -initializeData().then(() => { | 356 | +app.post('/api/migrate', async (req, res) => { |
| 444 | - app.listen(PORT, () => { | 357 | + try { |
| 445 | - console.log(`LocalToast backend running on http://localhost:${PORT} 🍞`); | 358 | + await migrateFromJSON(); |
| 446 | - }); | 359 | + res.json({ message: 'Migration completed successfully!' }); |
| 447 | -}); | 360 | + } catch (error) { |
| | 361 | + console.error('Migration error:', error); |
| | 362 | + res.status(500).json({ error: 'Migration failed' }); |
| | 363 | + } |
| | 364 | +}); |
| | 365 | + |
| | 366 | +// Initialize database and start server |
| | 367 | +initializeDatabase() |
| | 368 | + .then(() => { |
| | 369 | + app.listen(PORT, () => { |
| | 370 | + console.log(`LocalToast backend running on http://localhost:${PORT} 🍞`); |
| | 371 | + console.log('Database: SQLite'); |
| | 372 | + |
| | 373 | + // Offer to migrate on first run |
| | 374 | + console.log('\nIf you have existing JSON data, run:'); |
| | 375 | + console.log(`curl -X POST http://localhost:${PORT}/api/migrate`); |
| | 376 | + }); |
| | 377 | + }) |
| | 378 | + .catch(err => { |
| | 379 | + console.error('Failed to initialize database:', err); |
| | 380 | + process.exit(1); |
| | 381 | + }); |