zeroed-some/localtoast / 4f598fd

Browse files

migrate to sqlite

Authored by espadonne
SHA
4f598fd007097b135adeb1c20aafb50fa0d61481
Parents
28cf243
Tree
cff4bd6

3 changed files

StatusFile+-
A backend/src/database.js 263 0
A backend/src/db/localtoast.db bin
M backend/src/index.js 78 144
backend/src/database.jsadded
@@ -0,0 +1,263 @@
1
+const sqlite3 = require('sqlite3').verbose();
2
+const path = require('path');
3
+
4
+// Database file location
5
+const DB_PATH = process.env.NODE_ENV === 'production'
6
+  ? '/data/localtoast.db'  // Railway persistent volume
7
+  : path.join(__dirname, 'db', 'localtoast.db');
8
+
9
+// Create database connection
10
+const db = new sqlite3.Database(DB_PATH, (err) => {
11
+  if (err) {
12
+    console.error('Error opening database:', err);
13
+  } else {
14
+    console.log('Connected to SQLite database at:', DB_PATH);
15
+  }
16
+});
17
+
18
+// Enable foreign keys
19
+db.run('PRAGMA foreign_keys = ON');
20
+
21
+// Initialize database schema
22
+function initializeDatabase() {
23
+  return new Promise((resolve, reject) => {
24
+    db.serialize(() => {
25
+      // Create restaurants table
26
+      db.run(`
27
+        CREATE TABLE IF NOT EXISTS restaurants (
28
+          id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+          place_id TEXT UNIQUE NOT NULL,
30
+          name TEXT NOT NULL,
31
+          address TEXT NOT NULL,
32
+          latitude REAL NOT NULL,
33
+          longitude REAL NOT NULL,
34
+          created_at DATETIME DEFAULT CURRENT_TIMESTAMP
35
+        )
36
+      `, (err) => {
37
+        if (err) console.error('Error creating restaurants table:', err);
38
+      });
39
+
40
+      // Create ratings table
41
+      db.run(`
42
+        CREATE TABLE IF NOT EXISTS ratings (
43
+          id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+          restaurant_id INTEGER NOT NULL,
45
+          rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
46
+          review TEXT,
47
+          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
48
+          FOREIGN KEY (restaurant_id) REFERENCES restaurants (id)
49
+        )
50
+      `, (err) => {
51
+        if (err) console.error('Error creating ratings table:', err);
52
+      });
53
+
54
+      // Create indexes for better performance
55
+      db.run(`
56
+        CREATE INDEX IF NOT EXISTS idx_restaurants_location 
57
+        ON restaurants(latitude, longitude)
58
+      `);
59
+
60
+      db.run(`
61
+        CREATE INDEX IF NOT EXISTS idx_ratings_restaurant 
62
+        ON ratings(restaurant_id)
63
+      `, (err) => {
64
+        if (err) {
65
+          reject(err);
66
+        } else {
67
+          console.log('Database initialized successfully');
68
+          resolve();
69
+        }
70
+      });
71
+    });
72
+  });
73
+}
74
+
75
+// Database helper functions
76
+const dbHelpers = {
77
+  // Get all restaurants with ratings
78
+  getAllRestaurants: () => {
79
+    return new Promise((resolve, reject) => {
80
+      const query = `
81
+        SELECT 
82
+          r.*,
83
+          AVG(rt.rating) as average_rating,
84
+          COUNT(rt.id) as total_ratings
85
+        FROM restaurants r
86
+        LEFT JOIN ratings rt ON r.id = rt.restaurant_id
87
+        GROUP BY r.id
88
+      `;
89
+      
90
+      db.all(query, (err, rows) => {
91
+        if (err) reject(err);
92
+        else resolve(rows);
93
+      });
94
+    });
95
+  },
96
+
97
+  // Get restaurants within radius
98
+  getNearbyRestaurants: (lat, lng, radiusKm = 5) => {
99
+    return new Promise((resolve, reject) => {
100
+      // SQLite doesn't have built-in geospatial functions, so we'll use a bounding box
101
+      // This is an approximation but works well for small distances
102
+      const latDiff = radiusKm / 111; // 1 degree latitude ≈ 111 km
103
+      const lngDiff = radiusKm / (111 * Math.cos(lat * Math.PI / 180));
104
+      
105
+      const query = `
106
+        SELECT 
107
+          r.*,
108
+          AVG(rt.rating) as average_rating,
109
+          COUNT(rt.id) as total_ratings
110
+        FROM restaurants r
111
+        LEFT JOIN ratings rt ON r.id = rt.restaurant_id
112
+        WHERE 
113
+          r.latitude BETWEEN ? AND ?
114
+          AND r.longitude BETWEEN ? AND ?
115
+        GROUP BY r.id
116
+      `;
117
+      
118
+      db.all(query, [
119
+        lat - latDiff, lat + latDiff,
120
+        lng - lngDiff, lng + lngDiff
121
+      ], (err, rows) => {
122
+        if (err) reject(err);
123
+        else resolve(rows);
124
+      });
125
+    });
126
+  },
127
+
128
+  // Get restaurant by place_id
129
+  getRestaurantByPlaceId: (placeId) => {
130
+    return new Promise((resolve, reject) => {
131
+      db.get(
132
+        'SELECT * FROM restaurants WHERE place_id = ?',
133
+        [placeId],
134
+        (err, row) => {
135
+          if (err) reject(err);
136
+          else resolve(row);
137
+        }
138
+      );
139
+    });
140
+  },
141
+
142
+  // Add new restaurant
143
+  addRestaurant: (restaurant) => {
144
+    return new Promise((resolve, reject) => {
145
+      const { place_id, name, address, latitude, longitude } = restaurant;
146
+      
147
+      db.run(
148
+        `INSERT INTO restaurants (place_id, name, address, latitude, longitude)
149
+         VALUES (?, ?, ?, ?, ?)`,
150
+        [place_id, name, address, latitude, longitude],
151
+        function(err) {
152
+          if (err) {
153
+            reject(err);
154
+          } else {
155
+            // Get the inserted restaurant
156
+            db.get(
157
+              'SELECT * FROM restaurants WHERE id = ?',
158
+              [this.lastID],
159
+              (err, row) => {
160
+                if (err) reject(err);
161
+                else resolve(row);
162
+              }
163
+            );
164
+          }
165
+        }
166
+      );
167
+    });
168
+  },
169
+
170
+  // Add rating
171
+  addRating: (restaurantId, rating, review) => {
172
+    return new Promise((resolve, reject) => {
173
+      db.run(
174
+        `INSERT INTO ratings (restaurant_id, rating, review)
175
+         VALUES (?, ?, ?)`,
176
+        [restaurantId, rating, review],
177
+        function(err) {
178
+          if (err) {
179
+            reject(err);
180
+          } else {
181
+            resolve({ 
182
+              id: this.lastID, 
183
+              message: 'Toast rating added successfully! 🍞' 
184
+            });
185
+          }
186
+        }
187
+      );
188
+    });
189
+  },
190
+
191
+  // Get ratings for a restaurant
192
+  getRestaurantRatings: (restaurantId) => {
193
+    return new Promise((resolve, reject) => {
194
+      db.all(
195
+        `SELECT * FROM ratings 
196
+         WHERE restaurant_id = ? 
197
+         ORDER BY created_at DESC`,
198
+        [restaurantId],
199
+        (err, rows) => {
200
+          if (err) reject(err);
201
+          else resolve(rows);
202
+        }
203
+      );
204
+    });
205
+  },
206
+
207
+  // Migrate from JSON files (one-time migration)
208
+  migrateFromJSON: async () => {
209
+    const fs = require('fs').promises;
210
+    const oldRestaurantsPath = path.join(__dirname, 'db', 'restaurants.json');
211
+    const oldRatingsPath = path.join(__dirname, 'db', 'ratings.json');
212
+    
213
+    try {
214
+      // Check if JSON files exist
215
+      const restaurantsData = await fs.readFile(oldRestaurantsPath, 'utf8').catch(() => '[]');
216
+      const ratingsData = await fs.readFile(oldRatingsPath, 'utf8').catch(() => '[]');
217
+      
218
+      const restaurants = JSON.parse(restaurantsData);
219
+      const ratings = JSON.parse(ratingsData);
220
+      
221
+      console.log(`Migrating ${restaurants.length} restaurants and ${ratings.length} ratings...`);
222
+      
223
+      // Migrate restaurants
224
+      for (const restaurant of restaurants) {
225
+        try {
226
+          await dbHelpers.addRestaurant(restaurant);
227
+          console.log(`Migrated restaurant: ${restaurant.name}`);
228
+        } catch (err) {
229
+          if (err.message.includes('UNIQUE constraint failed')) {
230
+            console.log(`Restaurant already exists: ${restaurant.name}`);
231
+          } else {
232
+            console.error(`Failed to migrate restaurant ${restaurant.name}:`, err);
233
+          }
234
+        }
235
+      }
236
+      
237
+      // Migrate ratings
238
+      for (const rating of ratings) {
239
+        try {
240
+          await dbHelpers.addRating(rating.restaurant_id, rating.rating, rating.review);
241
+          console.log(`Migrated rating for restaurant ${rating.restaurant_id}`);
242
+        } catch (err) {
243
+          console.error(`Failed to migrate rating:`, err);
244
+        }
245
+      }
246
+      
247
+      console.log('Migration completed!');
248
+      
249
+      // Rename old files to .backup
250
+      await fs.rename(oldRestaurantsPath, oldRestaurantsPath + '.backup').catch(() => {});
251
+      await fs.rename(oldRatingsPath, oldRatingsPath + '.backup').catch(() => {});
252
+      
253
+    } catch (error) {
254
+      console.error('Migration error:', error);
255
+    }
256
+  }
257
+};
258
+
259
+module.exports = {
260
+  db,
261
+  initializeDatabase,
262
+  ...dbHelpers
263
+};
backend/src/db/localtoast.dbadded
Binary file changed.
backend/src/index.jsmodified
@@ -3,10 +3,21 @@ require('dotenv').config();
33
 
44
 const express = require('express');
55
 const cors = require('cors');
6
-const fs = require('fs').promises;
76
 const path = require('path');
87
 const axios = require('axios');
98
 
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
+
1021
 const app = express();
1122
 const PORT = process.env.PORT || 3000;
1223
 
@@ -14,64 +25,6 @@ const PORT = process.env.PORT || 3000;
1425
 app.use(cors());
1526
 app.use(express.json());
1627
 
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
-
7528
 // Test Google Places API
7629
 app.get('/api/test-google', async (req, res) => {
7730
   if (!process.env.GOOGLE_PLACES_API_KEY) {
@@ -113,8 +66,11 @@ app.get('/api/restaurants/nearby', async (req, res) => {
11366
       return res.status(400).json({ error: 'Latitude and longitude are required' });
11467
     }
11568
 
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
+    );
11874
     
11975
     // If we have no restaurants, try to fetch some from OpenStreetMap
12076
     if (restaurants.length === 0) {
@@ -139,46 +95,31 @@ app.get('/api/restaurants/nearby', async (req, res) => {
13995
         
14096
         const places = response.data.elements
14197
           .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
15599
         
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
+          }
159116
         }
160117
       } catch (apiError) {
161118
         console.error('Failed to fetch from OpenStreetMap:', apiError.message);
162119
       }
163120
     }
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
-      });
180121
 
181
-    res.json(nearbyRestaurants);
122
+    res.json(restaurants);
182123
   } catch (error) {
183124
     console.error('Error fetching restaurants:', error);
184125
     res.status(500).json({ error: 'Failed to fetch restaurants' });
@@ -190,26 +131,19 @@ app.post('/api/restaurants', async (req, res) => {
190131
   try {
191132
     const { place_id, name, address, latitude, longitude } = req.body;
192133
     
193
-    const restaurants = await getRestaurants();
194
-    
195134
     // Check if restaurant already exists
196
-    const existing = restaurants.find(r => r.place_id === place_id);
135
+    const existing = await getRestaurantByPlaceId(place_id);
197136
     if (existing) {
198137
       return res.json(existing);
199138
     }
200139
     
201
-    const newRestaurant = {
202
-      id: restaurants.length + 1,
140
+    const newRestaurant = await addRestaurant({
203141
       place_id,
204142
       name,
205143
       address,
206144
       latitude,
207
-      longitude,
208
-      created_at: new Date().toISOString()
209
-    };
210
-    
211
-    restaurants.push(newRestaurant);
212
-    await saveRestaurants(restaurants);
145
+      longitude
146
+    });
213147
     
214148
     res.json(newRestaurant);
215149
   } catch (error) {
@@ -237,23 +171,8 @@ app.post('/api/restaurants/:id/ratings', async (req, res) => {
237171
       }
238172
     }
239173
     
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);
257176
   } catch (error) {
258177
     console.error('Error adding rating:', error);
259178
     res.status(500).json({ error: 'Failed to add rating' });
@@ -264,13 +183,8 @@ app.post('/api/restaurants/:id/ratings', async (req, res) => {
264183
 app.get('/api/restaurants/:id/ratings', async (req, res) => {
265184
   try {
266185
     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);
274188
   } catch (error) {
275189
     console.error('Error fetching ratings:', error);
276190
     res.status(500).json({ error: 'Failed to fetch ratings' });
@@ -416,22 +330,21 @@ app.post('/api/seed', async (req, res) => {
416330
       { place_id: 'seed_4', name: 'Crispy Corner', address: '400 Crunch St', latitude: parseFloat(lat) - 0.005, longitude: parseFloat(lng) - 0.005 }
417331
     ];
418332
     
419
-    const restaurants = await getRestaurants();
420333
     let added = 0;
421334
     
422335
     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);
429338
         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
+        }
430345
       }
431346
     }
432347
     
433
-    await saveRestaurants(restaurants);
434
-    
435348
     res.json({ message: `Seeded ${added} restaurants with toast! 🍞` });
436349
   } catch (error) {
437350
     console.error('Error seeding data:', error);
@@ -439,9 +352,30 @@ app.post('/api/seed', async (req, res) => {
439352
   }
440353
 });
441354
 
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
+  });