@@ -3,10 +3,21 @@ require('dotenv').config(); |
| 3 | 3 | |
| 4 | 4 | const express = require('express'); |
| 5 | 5 | const cors = require('cors'); |
| 6 | | -const fs = require('fs').promises; |
| 7 | 6 | const path = require('path'); |
| 8 | 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 | 21 | const app = express(); |
| 11 | 22 | const PORT = process.env.PORT || 3000; |
| 12 | 23 | |
@@ -14,64 +25,6 @@ const PORT = process.env.PORT || 3000; |
| 14 | 25 | app.use(cors()); |
| 15 | 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 | 28 | // Test Google Places API |
| 76 | 29 | app.get('/api/test-google', async (req, res) => { |
| 77 | 30 | if (!process.env.GOOGLE_PLACES_API_KEY) { |
@@ -113,8 +66,11 @@ app.get('/api/restaurants/nearby', async (req, res) => { |
| 113 | 66 | return res.status(400).json({ error: 'Latitude and longitude are required' }); |
| 114 | 67 | } |
| 115 | 68 | |
| 116 | | - const restaurants = await getRestaurants(); |
| 117 | | - const ratings = await getRatings(); |
| 69 | + const restaurants = await getNearbyRestaurants( |
| 70 | + parseFloat(lat), |
| 71 | + parseFloat(lng), |
| 72 | + parseFloat(radius) |
| 73 | + ); |
| 118 | 74 | |
| 119 | 75 | // If we have no restaurants, try to fetch some from OpenStreetMap |
| 120 | 76 | if (restaurants.length === 0) { |
@@ -139,46 +95,31 @@ app.get('/api/restaurants/nearby', async (req, res) => { |
| 139 | 95 | |
| 140 | 96 | const places = response.data.elements |
| 141 | 97 | .filter(place => place.tags && place.tags.name) |
| 142 | | - .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 | | - })); |
| 98 | + .slice(0, 5); // Just add first 5 automatically |
| 155 | 99 | |
| 156 | | - if (places.length > 0) { |
| 157 | | - await saveRestaurants(places); |
| 158 | | - restaurants.push(...places); |
| 100 | + for (const place of places) { |
| 101 | + try { |
| 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 | 117 | } catch (apiError) { |
| 161 | 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 | 123 | } catch (error) { |
| 183 | 124 | console.error('Error fetching restaurants:', error); |
| 184 | 125 | res.status(500).json({ error: 'Failed to fetch restaurants' }); |
@@ -190,26 +131,19 @@ app.post('/api/restaurants', async (req, res) => { |
| 190 | 131 | try { |
| 191 | 132 | const { place_id, name, address, latitude, longitude } = req.body; |
| 192 | 133 | |
| 193 | | - const restaurants = await getRestaurants(); |
| 194 | | - |
| 195 | 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 | 136 | if (existing) { |
| 198 | 137 | return res.json(existing); |
| 199 | 138 | } |
| 200 | 139 | |
| 201 | | - const newRestaurant = { |
| 202 | | - id: restaurants.length + 1, |
| 140 | + const newRestaurant = await addRestaurant({ |
| 203 | 141 | place_id, |
| 204 | 142 | name, |
| 205 | 143 | address, |
| 206 | 144 | latitude, |
| 207 | | - longitude, |
| 208 | | - created_at: new Date().toISOString() |
| 209 | | - }; |
| 210 | | - |
| 211 | | - restaurants.push(newRestaurant); |
| 212 | | - await saveRestaurants(restaurants); |
| 145 | + longitude |
| 146 | + }); |
| 213 | 147 | |
| 214 | 148 | res.json(newRestaurant); |
| 215 | 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(); |
| 241 | | - |
| 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 | | - }); |
| 174 | + const result = await addRating(parseInt(id), rating, review); |
| 175 | + res.json(result); |
| 257 | 176 | } catch (error) { |
| 258 | 177 | console.error('Error adding rating:', error); |
| 259 | 178 | res.status(500).json({ error: 'Failed to add rating' }); |
@@ -264,13 +183,8 @@ app.post('/api/restaurants/:id/ratings', async (req, res) => { |
| 264 | 183 | app.get('/api/restaurants/:id/ratings', async (req, res) => { |
| 265 | 184 | try { |
| 266 | 185 | const { id } = req.params; |
| 267 | | - const ratings = await getRatings(); |
| 268 | | - |
| 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); |
| 186 | + const ratings = await getRestaurantRatings(parseInt(id)); |
| 187 | + res.json(ratings); |
| 274 | 188 | } catch (error) { |
| 275 | 189 | console.error('Error fetching ratings:', error); |
| 276 | 190 | res.status(500).json({ error: 'Failed to fetch ratings' }); |
@@ -416,22 +330,21 @@ app.post('/api/seed', async (req, res) => { |
| 416 | 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 | 333 | let added = 0; |
| 421 | 334 | |
| 422 | 335 | for (const seedRestaurant of seedRestaurants) { |
| 423 | | - if (!restaurants.find(r => r.place_id === seedRestaurant.place_id)) { |
| 424 | | - restaurants.push({ |
| 425 | | - id: restaurants.length + 1, |
| 426 | | - ...seedRestaurant, |
| 427 | | - created_at: new Date().toISOString() |
| 428 | | - }); |
| 336 | + try { |
| 337 | + await addRestaurant(seedRestaurant); |
| 429 | 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 | 348 | res.json({ message: `Seeded ${added} restaurants with toast! 🍞` }); |
| 436 | 349 | } catch (error) { |
| 437 | 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 |
| 443 | | -initializeData().then(() => { |
| 444 | | - app.listen(PORT, () => { |
| 445 | | - console.log(`LocalToast backend running on http://localhost:${PORT} 🍞`); |
| 446 | | - }); |
| 447 | | -}); |
| 355 | +// Migration endpoint (one-time use) |
| 356 | +app.post('/api/migrate', async (req, res) => { |
| 357 | + try { |
| 358 | + await migrateFromJSON(); |
| 359 | + res.json({ message: 'Migration completed successfully!' }); |
| 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 | + }); |