Skip to content

Commit 2a78d77

Browse files
committed
Fix backend persistence crash and optimize order lookup
1 parent bef8c77 commit 2a78d77

3 files changed

Lines changed: 40 additions & 141 deletions

File tree

backend/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Backend API
22

3-
A minimal Node.js backend for BroCode Spot backed by a persistent SQLite database.
3+
A minimal Node.js backend for BroCode Spot backed by a persistent JSON file database.
44

55
## Start
66

@@ -14,8 +14,8 @@ Server starts at `http://localhost:4000` by default.
1414

1515
### Issue #26: Move from in-memory store to persistent DB
1616

17-
- Uses `node:sqlite` with a local database file at `backend/data/brocode.sqlite`.
18-
- You can override the location with `BROCODE_DB_PATH=/custom/path.sqlite npm run backend`.
17+
- Uses a local JSON database file at `backend/data/brocode.json`.
18+
- You can override the location with `BROCODE_DB_PATH=/custom/path.json npm run backend`.
1919
- On first start, seed data is inserted for users, spots, catalog items, and a sample order.
2020
- New orders are validated against DB data (known `spotId`, `userId`, `productId`) and item pricing is always derived from catalog prices in the database.
2121

backend/db.js

Lines changed: 23 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2-
import { dirname, resolve } from 'node:path';
32
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
3+
import { dirname, resolve } from 'node:path';
44

5-
const defaultDbPath = resolve(process.cwd(), 'backend', 'data', 'brocode.sqlite');
5+
const defaultDbPath = resolve(process.cwd(), 'backend', 'data', 'brocode.json');
66
const dbPath = process.env.BROCODE_DB_PATH ? resolve(process.env.BROCODE_DB_PATH) : defaultDbPath;
77

88
const dbDirectory = dirname(dbPath);
@@ -34,79 +34,6 @@ const verifyPassword = (password, storedPassword) => {
3434
return timingSafeEqual(candidateHash, expectedHash);
3535
};
3636

37-
db.exec(`
38-
CREATE TABLE IF NOT EXISTS users (
39-
id TEXT PRIMARY KEY,
40-
username TEXT NOT NULL UNIQUE,
41-
password TEXT NOT NULL,
42-
name TEXT NOT NULL,
43-
role TEXT NOT NULL
44-
);
45-
46-
CREATE TABLE IF NOT EXISTS spots (
47-
id TEXT PRIMARY KEY,
48-
location TEXT NOT NULL,
49-
date TEXT NOT NULL,
50-
host_user_id TEXT NOT NULL,
51-
FOREIGN KEY (host_user_id) REFERENCES users(id) ON DELETE CASCADE
52-
);
53-
54-
CREATE TABLE IF NOT EXISTS catalog_items (
55-
id TEXT PRIMARY KEY,
56-
category TEXT NOT NULL,
57-
name TEXT NOT NULL,
58-
price REAL NOT NULL
59-
);
60-
61-
CREATE TABLE IF NOT EXISTS orders (
62-
id TEXT PRIMARY KEY,
63-
spot_id TEXT NOT NULL,
64-
user_id TEXT NOT NULL,
65-
total_amount REAL NOT NULL,
66-
created_at TEXT NOT NULL,
67-
FOREIGN KEY (spot_id) REFERENCES spots(id),
68-
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
69-
);
70-
71-
CREATE TABLE IF NOT EXISTS order_items (
72-
id INTEGER PRIMARY KEY AUTOINCREMENT,
73-
order_id TEXT NOT NULL,
74-
product_id TEXT NOT NULL,
75-
name TEXT NOT NULL,
76-
quantity INTEGER NOT NULL,
77-
unit_price REAL NOT NULL,
78-
total REAL NOT NULL,
79-
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
80-
);
81-
`);
82-
83-
const hasUsers = db.prepare('SELECT COUNT(*) AS count FROM users').get().count > 0;
84-
85-
if (!hasUsers) {
86-
const insertUser = db.prepare('INSERT INTO users (id, username, password, name, role) VALUES (?, ?, ?, ?, ?)');
87-
insertUser.run('u-1', 'brocode', hashPassword('changeme'), 'Ram', 'admin');
88-
insertUser.run('u-2', 'dhanush', hashPassword('changeme'), 'Dhanush', 'user');
89-
90-
const insertSpot = db.prepare('INSERT INTO spots (id, location, date, host_user_id) VALUES (?, ?, ?, ?)');
91-
insertSpot.run('spot-2025-07-26', 'Attibele Toll Plaza', '2025-07-26T10:00:00.000Z', 'u-1');
92-
93-
const insertCatalogItem = db.prepare('INSERT INTO catalog_items (id, category, name, price) VALUES (?, ?, ?, ?)');
94-
insertCatalogItem.run('d-1', 'drinks', 'Brocode Beer', 180);
95-
insertCatalogItem.run('d-2', 'drinks', 'Kingfisher Beer', 170);
96-
insertCatalogItem.run('f-1', 'food', 'Beef Biriyani', 220);
97-
insertCatalogItem.run('f-2', 'food', 'Parotta', 30);
98-
insertCatalogItem.run('c-1', 'cigarettes', 'Marlboro', 25);
99-
insertCatalogItem.run('c-2', 'cigarettes', 'Classic', 20);
100-
101-
db.prepare(
102-
'INSERT INTO orders (id, spot_id, user_id, total_amount, created_at) VALUES (?, ?, ?, ?, ?)'
103-
).run('ord-1', 'spot-2025-07-26', 'u-2', 360, '2025-07-26T10:30:00.000Z');
104-
105-
db.prepare(
106-
`INSERT INTO order_items (order_id, product_id, name, quantity, unit_price, total)
107-
VALUES (?, ?, ?, ?, ?, ?)`
108-
).run('ord-1', 'd-1', 'Brocode Beer', 2, 180, 360);
109-
}
11037
const seedData = () => ({
11138
users: [
11239
{ id: 'u-1', username: 'brocode', password: hashPassword('changeme'), name: 'Ram', role: 'admin' },
@@ -196,42 +123,6 @@ const mapOrder = (order, items) => ({
196123
})),
197124
});
198125

199-
const fetchOrderItemsByOrderIds = (orderIds) => {
200-
if (orderIds.length === 0) return new Map();
201-
202-
const placeholders = orderIds.map(() => '?').join(',');
203-
const rows = db
204-
.prepare(
205-
`SELECT order_id, product_id, name, quantity, unit_price, total
206-
FROM order_items
207-
WHERE order_id IN (${placeholders})
208-
ORDER BY id ASC`
209-
)
210-
.all(...orderIds);
211-
212-
const itemsByOrderId = new Map();
213-
rows.forEach((row) => {
214-
if (!itemsByOrderId.has(row.order_id)) {
215-
itemsByOrderId.set(row.order_id, []);
216-
}
217-
itemsByOrderId.get(row.order_id).push(row);
218-
});
219-
220-
return itemsByOrderId;
221-
};
222-
223-
const getCatalogItemByIdStatement = db.prepare(
224-
'SELECT id, category, name, price FROM catalog_items WHERE id = ?'
225-
);
226-
227-
const userExistsStatement = db.prepare('SELECT 1 AS found FROM users WHERE id = ? LIMIT 1');
228-
const spotExistsStatement = db.prepare('SELECT 1 AS found FROM spots WHERE id = ? LIMIT 1');
229-
const getUserByUsernameStatement = db.prepare('SELECT id, username, password, name, role FROM users WHERE username = ?');
230-
const updateUserPasswordStatement = db.prepare('UPDATE users SET password = ? WHERE id = ?');
231-
const deleteUserByIdStatement = db.prepare('DELETE FROM users WHERE id = ?');
232-
const deleteOrdersByUserIdStatement = db.prepare('DELETE FROM orders WHERE user_id = ?');
233-
const deleteSpotsByHostUserIdStatement = db.prepare('DELETE FROM spots WHERE host_user_id = ?');
234-
235126
export const database = {
236127
getUserByCredentials(username, password) {
237128
const user = state.users.find((entry) => entry.username === username);
@@ -303,6 +194,20 @@ export const database = {
303194
});
304195
},
305196

197+
getOrderById(orderId) {
198+
const order = state.orders.find((entry) => entry.id === orderId);
199+
200+
if (!order) {
201+
return null;
202+
}
203+
204+
const orderItems = state.order_items
205+
.filter((item) => item.order_id === order.id)
206+
.sort((a, b) => a.id - b.id);
207+
208+
return mapOrder(order, orderItems);
209+
},
210+
306211
createOrder({ spotId, userId, items }) {
307212
const parsedItems = items.map((item) => {
308213
const quantity = Number(item.quantity || 0);
@@ -361,18 +266,15 @@ export const database = {
361266
},
362267

363268
deleteUserCompletely(userId) {
364-
db.exec('BEGIN');
269+
const hostedSpotIds = new Set(state.spots.filter((spot) => spot.host_user_id === userId).map((spot) => spot.id));
365270

366-
try {
367-
deleteOrdersByUserIdStatement.run(userId);
368-
deleteSpotsByHostUserIdStatement.run(userId);
369-
deleteUserByIdStatement.run(userId);
271+
state.users = state.users.filter((user) => user.id !== userId);
272+
state.orders = state.orders.filter((order) => order.user_id !== userId && !hostedSpotIds.has(order.spot_id));
273+
const activeOrderIds = new Set(state.orders.map((order) => order.id));
274+
state.order_items = state.order_items.filter((item) => activeOrderIds.has(item.order_id));
275+
state.spots = state.spots.filter((spot) => spot.host_user_id !== userId);
370276

371-
db.exec('COMMIT');
372-
} catch (error) {
373-
db.exec('ROLLBACK');
374-
throw error;
375-
}
277+
persist();
376278
},
377279

378280
getBillBySpotId(spotId) {

backend/server.js

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -179,28 +179,25 @@ const server = createServer(async (req, res) => {
179179
return;
180180
}
181181
if (method === 'GET' && path.startsWith('/api/orders/')) {
182-
console.log("I AM INSIDE ORDER ID ROUTE");
183-
const orderId = path.replace('/api/orders/', '');
182+
const orderId = path.replace('/api/orders/', '');
184183

185-
186-
const authHeader = req.headers.authorization;
187-
if (!authHeader) {
188-
sendJson(res, 401, { error: 'Unauthorized' });
189-
return;
190-
}
184+
const authHeader = req.headers.authorization;
185+
if (!authHeader) {
186+
sendJson(res, 401, { error: 'Unauthorized' });
187+
return;
188+
}
191189

192-
const orders = database.getOrders({});
193-
const order = orders.find(o => o.id === orderId);
190+
const order = database.getOrderById(orderId);
194191

195-
if (!order) {
196-
sendJson(res, 404, { error: `Order not found: ${orderId}` });
192+
if (!order) {
193+
sendJson(res, 404, { error: `Order not found: ${orderId}` });
194+
return;
195+
}
196+
197+
sendJson(res, 200, order);
197198
return;
198199
}
199200

200-
sendJson(res, 200, order);
201-
return;
202-
}
203-
204201
if (method === 'POST' && path === '/api/orders') {
205202
try {
206203
const { spotId, userId, items } = await readBody(req);
@@ -263,5 +260,5 @@ const server = createServer(async (req, res) => {
263260

264261
server.listen(port, () => {
265262
console.log(`Backend API running on http://localhost:${port}`);
266-
console.log(`Using SQLite database at: ${dbPath}`);
263+
console.log(`Using local database at: ${dbPath}`);
267264
});

0 commit comments

Comments
 (0)