Skip to content

Commit 2cbec1d

Browse files
authored
Merge pull request #28 from fuzziecoder/codex/fix-issue-#26-by-migrating-to-db
Codex-generated pull request
2 parents 0e08ae8 + 3b23ba1 commit 2cbec1d

3 files changed

Lines changed: 295 additions & 60 deletions

File tree

backend/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Backend API (Starter)
1+
# Backend API
22

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

55
## Start
66

@@ -10,6 +10,12 @@ npm run backend
1010

1111
Server starts at `http://localhost:4000` by default.
1212

13+
## Database
14+
15+
- Uses `node:sqlite` with a local database file at `backend/data/brocode.sqlite`.
16+
- You can override the location with `BROCODE_DB_PATH=/custom/path.sqlite npm run backend`.
17+
- On first start, seed data is inserted for users, spots, catalog items, and a sample order.
18+
1319
## Available endpoints
1420

1521
- `GET /api/health`
@@ -29,7 +35,3 @@ Server starts at `http://localhost:4000` by default.
2935
"password": "changeme"
3036
}
3137
```
32-
33-
## Note
34-
35-
Data is currently in-memory and resets whenever the process restarts. See `backend/store.js`.

backend/db.js

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { existsSync, mkdirSync } from 'node:fs';
2+
import { dirname, resolve } from 'node:path';
3+
import { randomUUID } from 'node:crypto';
4+
import { DatabaseSync } from 'node:sqlite';
5+
6+
const defaultDbPath = resolve(process.cwd(), 'backend', 'data', 'brocode.sqlite');
7+
const dbPath = process.env.BROCODE_DB_PATH ? resolve(process.env.BROCODE_DB_PATH) : defaultDbPath;
8+
9+
const dbDirectory = dirname(dbPath);
10+
if (!existsSync(dbDirectory)) {
11+
mkdirSync(dbDirectory, { recursive: true });
12+
}
13+
14+
const db = new DatabaseSync(dbPath);
15+
db.exec('PRAGMA journal_mode = WAL;');
16+
db.exec('PRAGMA foreign_keys = ON;');
17+
18+
db.exec(`
19+
CREATE TABLE IF NOT EXISTS users (
20+
id TEXT PRIMARY KEY,
21+
username TEXT NOT NULL UNIQUE,
22+
password TEXT NOT NULL,
23+
name TEXT NOT NULL,
24+
role TEXT NOT NULL
25+
);
26+
27+
CREATE TABLE IF NOT EXISTS spots (
28+
id TEXT PRIMARY KEY,
29+
location TEXT NOT NULL,
30+
date TEXT NOT NULL,
31+
host_user_id TEXT NOT NULL,
32+
FOREIGN KEY (host_user_id) REFERENCES users(id)
33+
);
34+
35+
CREATE TABLE IF NOT EXISTS catalog_items (
36+
id TEXT PRIMARY KEY,
37+
category TEXT NOT NULL,
38+
name TEXT NOT NULL,
39+
price REAL NOT NULL
40+
);
41+
42+
CREATE TABLE IF NOT EXISTS orders (
43+
id TEXT PRIMARY KEY,
44+
spot_id TEXT NOT NULL,
45+
user_id TEXT NOT NULL,
46+
total_amount REAL NOT NULL,
47+
created_at TEXT NOT NULL,
48+
FOREIGN KEY (spot_id) REFERENCES spots(id),
49+
FOREIGN KEY (user_id) REFERENCES users(id)
50+
);
51+
52+
CREATE TABLE IF NOT EXISTS order_items (
53+
id INTEGER PRIMARY KEY AUTOINCREMENT,
54+
order_id TEXT NOT NULL,
55+
product_id TEXT NOT NULL,
56+
name TEXT NOT NULL,
57+
quantity INTEGER NOT NULL,
58+
unit_price REAL NOT NULL,
59+
total REAL NOT NULL,
60+
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
61+
);
62+
`);
63+
64+
const hasUsers = db.prepare('SELECT COUNT(*) AS count FROM users').get().count > 0;
65+
66+
if (!hasUsers) {
67+
const insertUser = db.prepare('INSERT INTO users (id, username, password, name, role) VALUES (?, ?, ?, ?, ?)');
68+
insertUser.run('u-1', 'brocode', 'changeme', 'Ram', 'admin');
69+
insertUser.run('u-2', 'dhanush', 'changeme', 'Dhanush', 'user');
70+
71+
const insertSpot = db.prepare('INSERT INTO spots (id, location, date, host_user_id) VALUES (?, ?, ?, ?)');
72+
insertSpot.run('spot-2025-07-26', 'Attibele Toll Plaza', '2025-07-26T10:00:00.000Z', 'u-1');
73+
74+
const insertCatalogItem = db.prepare('INSERT INTO catalog_items (id, category, name, price) VALUES (?, ?, ?, ?)');
75+
insertCatalogItem.run('d-1', 'drinks', 'Brocode Beer', 180);
76+
insertCatalogItem.run('d-2', 'drinks', 'Kingfisher Beer', 170);
77+
insertCatalogItem.run('f-1', 'food', 'Beef Biriyani', 220);
78+
insertCatalogItem.run('f-2', 'food', 'Parotta', 30);
79+
insertCatalogItem.run('c-1', 'cigarettes', 'Marlboro', 25);
80+
insertCatalogItem.run('c-2', 'cigarettes', 'Classic', 20);
81+
82+
db.prepare(
83+
'INSERT INTO orders (id, spot_id, user_id, total_amount, created_at) VALUES (?, ?, ?, ?, ?)'
84+
).run('ord-1', 'spot-2025-07-26', 'u-2', 360, '2025-07-26T10:30:00.000Z');
85+
86+
db.prepare(
87+
`INSERT INTO order_items (order_id, product_id, name, quantity, unit_price, total)
88+
VALUES (?, ?, ?, ?, ?, ?)`
89+
).run('ord-1', 'd-1', 'Brocode Beer', 2, 180, 360);
90+
}
91+
92+
const mapOrder = (order, items) => ({
93+
id: order.id,
94+
spotId: order.spot_id,
95+
userId: order.user_id,
96+
totalAmount: order.total_amount,
97+
createdAt: order.created_at,
98+
items: items.map((item) => ({
99+
productId: item.product_id,
100+
name: item.name,
101+
quantity: item.quantity,
102+
unitPrice: item.unit_price,
103+
total: item.total,
104+
})),
105+
});
106+
107+
const fetchOrderItemsByOrderIds = (orderIds) => {
108+
if (orderIds.length === 0) return new Map();
109+
110+
const placeholders = orderIds.map(() => '?').join(',');
111+
const rows = db
112+
.prepare(
113+
`SELECT order_id, product_id, name, quantity, unit_price, total
114+
FROM order_items
115+
WHERE order_id IN (${placeholders})
116+
ORDER BY id ASC`
117+
)
118+
.all(...orderIds);
119+
120+
const itemsByOrderId = new Map();
121+
rows.forEach((row) => {
122+
if (!itemsByOrderId.has(row.order_id)) {
123+
itemsByOrderId.set(row.order_id, []);
124+
}
125+
itemsByOrderId.get(row.order_id).push(row);
126+
});
127+
128+
return itemsByOrderId;
129+
};
130+
131+
export const database = {
132+
getUserByCredentials(username, password) {
133+
const user = db
134+
.prepare('SELECT id, username, name, role FROM users WHERE username = ? AND password = ?')
135+
.get(username, password);
136+
137+
return user || null;
138+
},
139+
140+
getCatalog() {
141+
const rows = db.prepare('SELECT id, category, name, price FROM catalog_items ORDER BY category, id').all();
142+
return rows.reduce(
143+
(acc, row) => {
144+
if (!acc[row.category]) {
145+
acc[row.category] = [];
146+
}
147+
148+
acc[row.category].push({
149+
id: row.id,
150+
name: row.name,
151+
price: row.price,
152+
});
153+
154+
return acc;
155+
},
156+
{ drinks: [], food: [], cigarettes: [] }
157+
);
158+
},
159+
160+
getCatalogCategory(category) {
161+
const rows = db.prepare('SELECT id, name, price FROM catalog_items WHERE category = ? ORDER BY id').all(category);
162+
return rows;
163+
},
164+
165+
getSpots() {
166+
return db
167+
.prepare('SELECT id, location, date, host_user_id AS hostUserId FROM spots ORDER BY date DESC')
168+
.all();
169+
},
170+
171+
getOrders({ spotId, userId }) {
172+
const conditions = [];
173+
const values = [];
174+
175+
if (spotId) {
176+
conditions.push('spot_id = ?');
177+
values.push(spotId);
178+
}
179+
180+
if (userId) {
181+
conditions.push('user_id = ?');
182+
values.push(userId);
183+
}
184+
185+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
186+
const orders = db
187+
.prepare(
188+
`SELECT id, spot_id, user_id, total_amount, created_at
189+
FROM orders
190+
${whereClause}
191+
ORDER BY created_at DESC`
192+
)
193+
.all(...values);
194+
195+
const itemsByOrderId = fetchOrderItemsByOrderIds(orders.map((order) => order.id));
196+
197+
return orders.map((order) => mapOrder(order, itemsByOrderId.get(order.id) || []));
198+
},
199+
200+
createOrder({ spotId, userId, items }) {
201+
const parsedItems = items.map((item) => {
202+
const quantity = Number(item.quantity || 0);
203+
const unitPrice = Number(item.unitPrice || 0);
204+
205+
return {
206+
productId: item.productId,
207+
name: item.name,
208+
quantity,
209+
unitPrice,
210+
total: quantity * unitPrice,
211+
};
212+
});
213+
214+
const totalAmount = parsedItems.reduce((sum, item) => sum + item.total, 0);
215+
const orderId = randomUUID();
216+
const createdAt = new Date().toISOString();
217+
218+
const insertOrder = db.prepare(
219+
'INSERT INTO orders (id, spot_id, user_id, total_amount, created_at) VALUES (?, ?, ?, ?, ?)'
220+
);
221+
const insertOrderItem = db.prepare(
222+
'INSERT INTO order_items (order_id, product_id, name, quantity, unit_price, total) VALUES (?, ?, ?, ?, ?, ?)'
223+
);
224+
225+
db.exec('BEGIN');
226+
try {
227+
insertOrder.run(orderId, spotId, userId, totalAmount, createdAt);
228+
parsedItems.forEach((item) => {
229+
insertOrderItem.run(orderId, item.productId, item.name, item.quantity, item.unitPrice, item.total);
230+
});
231+
db.exec('COMMIT');
232+
} catch (error) {
233+
db.exec('ROLLBACK');
234+
throw error;
235+
}
236+
237+
return {
238+
id: orderId,
239+
spotId,
240+
userId,
241+
items: parsedItems,
242+
totalAmount,
243+
createdAt,
244+
};
245+
},
246+
247+
getBillBySpotId(spotId) {
248+
const summaryRows = db
249+
.prepare(
250+
`SELECT user_id, SUM(total_amount) AS total
251+
FROM orders
252+
WHERE spot_id = ?
253+
GROUP BY user_id`
254+
)
255+
.all(spotId);
256+
257+
const total = summaryRows.reduce((sum, row) => sum + row.total, 0);
258+
const userTotals = summaryRows.reduce((acc, row) => {
259+
acc[row.user_id] = row.total;
260+
return acc;
261+
}, {});
262+
263+
const orderCount = db.prepare('SELECT COUNT(*) AS count FROM orders WHERE spot_id = ?').get(spotId).count;
264+
265+
return {
266+
spotId,
267+
total,
268+
userTotals,
269+
orderCount,
270+
};
271+
},
272+
};
273+
274+
export { dbPath };

0 commit comments

Comments
 (0)