Skip to content

Commit 8d0d29c

Browse files
committed
Updating workshopt with strong contract structure, pictures for front and backend and ETL job
1 parent cdf3b31 commit 8d0d29c

24 files changed

+4015
-226
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env node
2+
3+
import { config } from 'dotenv';
4+
config();
5+
6+
import fs from 'fs';
7+
import path from 'path';
8+
import { pool, closePool } from '../config/database';
9+
10+
async function runMigration(migrationFile: string): Promise<void> {
11+
try {
12+
console.log(`Running migration: ${migrationFile}`);
13+
14+
const migrationPath = path.join(__dirname, 'migrations', migrationFile);
15+
16+
if (!fs.existsSync(migrationPath)) {
17+
throw new Error(`Migration file not found: ${migrationPath}`);
18+
}
19+
20+
const migrationSql = fs.readFileSync(migrationPath, 'utf8');
21+
22+
// Split the SQL into individual statements
23+
const statements = migrationSql
24+
.split(';')
25+
.map(statement => statement.trim())
26+
.filter(statement => statement.length > 0 && !statement.startsWith('--'));
27+
28+
const connection = await pool.getConnection();
29+
30+
try {
31+
for (const statement of statements) {
32+
console.log(`Executing: ${statement.substring(0, 50)}...`);
33+
await connection.execute(statement);
34+
}
35+
console.log(`✅ Migration ${migrationFile} completed successfully`);
36+
} finally {
37+
connection.release();
38+
}
39+
40+
} catch (error) {
41+
console.error(`❌ Migration ${migrationFile} failed:`, error);
42+
throw error;
43+
}
44+
}
45+
46+
async function main() {
47+
const migrationFile = process.argv[2];
48+
49+
if (!migrationFile) {
50+
console.log('Usage: npm run db:migrate <migration-file>');
51+
console.log('Example: npm run db:migrate add_image_url_to_products.sql');
52+
process.exit(1);
53+
}
54+
55+
try {
56+
await runMigration(migrationFile);
57+
} catch (error) {
58+
console.error('Migration failed:', error);
59+
process.exit(1);
60+
} finally {
61+
await closePool();
62+
}
63+
}
64+
65+
// Run if called directly
66+
if (require.main === module) {
67+
main();
68+
}
69+
70+
export { runMigration };
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- Migration: Add image_url column to products table
2+
-- This migration adds the image_url field to support product images
3+
4+
ALTER TABLE products
5+
ADD COLUMN image_url VARCHAR(500) NULL
6+
AFTER inventory_quantity;
7+
8+
-- Add some sample image URLs to existing products if they exist
9+
UPDATE products SET image_url = 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=500&h=500&fit=crop' WHERE name LIKE '%Laptop%' AND image_url IS NULL;
10+
UPDATE products SET image_url = 'https://images.unsplash.com/photo-1544947950-fa07a98d237f?w=500&h=500&fit=crop' WHERE name LIKE '%Novel%' AND image_url IS NULL;
11+
UPDATE products SET image_url = 'https://images.unsplash.com/photo-1578662996442-48f60103fc96?w=500&h=500&fit=crop' WHERE name LIKE '%Mixer%' AND image_url IS NULL;
12+
UPDATE products SET image_url = 'https://images.unsplash.com/photo-1542272604-787c3835535d?w=500&h=500&fit=crop' WHERE name LIKE '%Jeans%' AND image_url IS NULL;
13+
UPDATE products SET image_url = 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=500&h=500&fit=crop' WHERE name LIKE '%Dumbbell%' AND image_url IS NULL;
14+
UPDATE products SET image_url = 'https://images.unsplash.com/photo-1504148455328-c376907d081c?w=500&h=500&fit=crop' WHERE name LIKE '%Drill%' AND image_url IS NULL;

workshops/modernizr/backend/src/database/schema.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ CREATE TABLE IF NOT EXISTS products (
4343
description TEXT,
4444
price DECIMAL(10,2) NOT NULL,
4545
inventory_quantity INT NOT NULL DEFAULT 0,
46+
image_url VARCHAR(500) NULL,
4647
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
4748
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
4849

workshops/modernizr/backend/src/database/seed.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ async function seedProducts(): Promise<void> {
220220
description: 'High-performance laptop with advanced processor, 14-inch display, 16GB RAM, 512GB SSD. Perfect for professional work and creative projects.',
221221
price: 1999.99,
222222
inventory_quantity: 15,
223+
image_url: 'https://images.unsplash.com/photo-1541807084-5c52b6b3adef?w=600&h=600&fit=crop&auto=format',
223224
category_id: laptopCategoryId,
224225
seller_id: adminId
225226
},
@@ -228,6 +229,7 @@ async function seedProducts(): Promise<void> {
228229
description: 'A timeless story about the Jazz Age exploring themes of wealth, love, and the American Dream. A must-read classic.',
229230
price: 12.99,
230231
inventory_quantity: 50,
232+
image_url: 'https://images.unsplash.com/photo-1543002588-bfa74002ed7e?w=600&h=600&fit=crop&auto=format',
231233
category_id: booksCategoryId,
232234
seller_id: adminId
233235
},
@@ -236,6 +238,7 @@ async function seedProducts(): Promise<void> {
236238
description: 'Professional 5-quart stand mixer with 10 speeds. Includes dough hook, flat beater, and wire whip. Perfect for baking enthusiasts.',
237239
price: 299.99,
238240
inventory_quantity: 8,
241+
image_url: 'https://images.unsplash.com/photo-1578643463396-0997cb5328c1?w=600&h=600&fit=crop&auto=format',
239242
category_id: kitchenCategoryId,
240243
seller_id: adminId
241244
},
@@ -244,6 +247,7 @@ async function seedProducts(): Promise<void> {
244247
description: 'Classic straight-leg jeans in dark wash. Made from 100% cotton denim. Timeless style and comfortable fit.',
245248
price: 89.99,
246249
inventory_quantity: 25,
250+
image_url: 'https://images.unsplash.com/photo-1582552938357-32b906df40cb?w=600&h=600&fit=crop&auto=format',
247251
category_id: clothingCategoryId,
248252
seller_id: adminId
249253
},
@@ -252,6 +256,7 @@ async function seedProducts(): Promise<void> {
252256
description: 'Space-saving adjustable dumbbells with weight range from 5-50 lbs per dumbbell. Quick-change system for efficient workouts.',
253257
price: 449.99,
254258
inventory_quantity: 12,
259+
image_url: 'https://images.unsplash.com/photo-1586401100295-7a8096fd231a?w=600&h=600&fit=crop&auto=format',
255260
category_id: sportsCategoryId,
256261
seller_id: adminId
257262
},
@@ -260,6 +265,7 @@ async function seedProducts(): Promise<void> {
260265
description: '20V MAX cordless drill with 2 batteries, charger, and carrying case. 1/2-inch chuck, LED light, and 15 clutch settings.',
261266
price: 129.99,
262267
inventory_quantity: 20,
268+
image_url: 'https://images.unsplash.com/photo-1504148455328-c376907d081c?w=600&h=600&fit=crop&auto=format',
263269
category_id: gardenCategoryId,
264270
seller_id: adminId
265271
}
@@ -270,15 +276,16 @@ async function seedProducts(): Promise<void> {
270276
try {
271277
await pool.execute(
272278
`INSERT IGNORE INTO products
273-
(seller_id, category_id, name, description, price, inventory_quantity)
274-
VALUES (?, ?, ?, ?, ?, ?)`,
279+
(seller_id, category_id, name, description, price, inventory_quantity, image_url)
280+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
275281
[
276282
product.seller_id,
277283
product.category_id,
278284
product.name,
279285
product.description,
280286
product.price,
281-
product.inventory_quantity
287+
product.inventory_quantity,
288+
product.image_url || null
282289
]
283290
);
284291
console.log(`✓ Inserted product: ${product.name} (category_id: ${product.category_id})`);

workshops/modernizr/backend/src/models/Product.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Product {
66
description?: string;
77
price: number;
88
inventory_quantity: number;
9+
image_url?: string;
910
created_at: Date;
1011
updated_at: Date;
1112
}
@@ -22,6 +23,7 @@ export interface CreateProductRequest {
2223
category_id: number;
2324
price: number;
2425
inventory_quantity: number;
26+
image_url?: string;
2527
}
2628

2729
export interface UpdateProductRequest {
@@ -30,6 +32,7 @@ export interface UpdateProductRequest {
3032
category_id?: number;
3133
price?: number;
3234
inventory_quantity?: number;
35+
image_url?: string;
3336
}
3437

3538
export interface ProductSearchFilters {
@@ -83,6 +86,7 @@ export function toProductResponse(product: ProductWithDetails): any {
8386
description: product.description,
8487
price: Number(product.price),
8588
inventory_quantity: product.inventory_quantity,
89+
imageUrl: product.image_url, // Map database field to frontend field
8690
category: {
8791
id: product.category_id,
8892
name: product.category_name || 'Uncategorized'
@@ -126,6 +130,12 @@ export function validateCreateProductRequest(data: any): CreateProductRequest {
126130
errors.push('Inventory quantity cannot exceed 999,999');
127131
}
128132

133+
if (data.image_url && typeof data.image_url !== 'string') {
134+
errors.push('Image URL must be a string');
135+
} else if (data.image_url && data.image_url.length > 500) {
136+
errors.push('Image URL must be 500 characters or less');
137+
}
138+
129139
if (errors.length > 0) {
130140
throw new Error(errors.join(', '));
131141
}
@@ -136,6 +146,7 @@ export function validateCreateProductRequest(data: any): CreateProductRequest {
136146
category_id: Number(data.category_id),
137147
price: Number(data.price),
138148
inventory_quantity: Number(data.inventory_quantity),
149+
image_url: data.image_url?.trim() || undefined,
139150
};
140151
}
141152

@@ -191,6 +202,16 @@ export function validateUpdateProductRequest(data: any): UpdateProductRequest {
191202
}
192203
}
193204

205+
if (data.image_url !== undefined) {
206+
if (data.image_url !== null && typeof data.image_url !== 'string') {
207+
errors.push('Image URL must be a string or null');
208+
} else if (data.image_url && data.image_url.length > 500) {
209+
errors.push('Image URL must be 500 characters or less');
210+
} else {
211+
update.image_url = data.image_url?.trim() || undefined;
212+
}
213+
}
214+
194215
if (errors.length > 0) {
195216
throw new Error(errors.join(', '));
196217
}

workshops/modernizr/backend/src/repositories/OrderRepository.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class OrderRepository {
9898
p.description as product_description,
9999
p.price as current_price,
100100
p.inventory_quantity as product_inventory,
101+
p.image_url as product_image_url,
101102
p.created_at as product_created_at,
102103
p.updated_at as product_updated_at,
103104
c.name as category_name,
@@ -126,6 +127,7 @@ export class OrderRepository {
126127
description: row.product_description,
127128
price: parseFloat(row.current_price),
128129
inventory_quantity: row.product_inventory,
130+
image_url: row.product_image_url,
129131
category_name: row.category_name,
130132
seller_username: row.seller_username,
131133
seller_email: row.seller_email,
@@ -180,6 +182,7 @@ export class OrderRepository {
180182
p.description as product_description,
181183
p.price as current_price,
182184
p.inventory_quantity as product_inventory,
185+
p.image_url as product_image_url,
183186
p.created_at as product_created_at,
184187
p.updated_at as product_updated_at,
185188
c.name as category_name,
@@ -208,6 +211,7 @@ export class OrderRepository {
208211
description: row.product_description,
209212
price: parseFloat(row.current_price),
210213
inventory_quantity: row.product_inventory,
214+
image_url: row.product_image_url,
211215
category_name: row.category_name,
212216
seller_username: row.seller_username,
213217
seller_email: row.seller_email,
@@ -293,6 +297,7 @@ export class OrderRepository {
293297
p.description as product_description,
294298
p.price as current_price,
295299
p.inventory_quantity as product_inventory,
300+
p.image_url as product_image_url,
296301
p.created_at as product_created_at,
297302
p.updated_at as product_updated_at,
298303
c.name as category_name,
@@ -321,6 +326,7 @@ export class OrderRepository {
321326
description: row.product_description,
322327
price: parseFloat(row.current_price),
323328
inventory_quantity: row.product_inventory,
329+
image_url: row.product_image_url,
324330
category_name: row.category_name,
325331
seller_username: row.seller_username,
326332
seller_email: row.seller_email,

workshops/modernizr/backend/src/repositories/ProductRepository.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@ export class ProductRepository {
2929

3030
// Insert the product
3131
const [result] = await connection.execute(
32-
`INSERT INTO products (seller_id, category_id, name, description, price, inventory_quantity)
33-
VALUES (?, ?, ?, ?, ?, ?)`,
32+
`INSERT INTO products (seller_id, category_id, name, description, price, inventory_quantity, image_url)
33+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
3434
[
3535
sellerId,
3636
productData.category_id,
3737
productData.name,
3838
productData.description || null,
3939
productData.price,
40-
productData.inventory_quantity
40+
productData.inventory_quantity,
41+
productData.image_url || null
4142
]
4243
);
4344

@@ -70,6 +71,7 @@ export class ProductRepository {
7071
description,
7172
price,
7273
inventory_quantity,
74+
image_url,
7375
created_at,
7476
updated_at
7577
FROM products WHERE id = ?`,
@@ -94,7 +96,16 @@ export class ProductRepository {
9496
try {
9597
const [rows] = await connection.execute(
9698
`SELECT
97-
p.*,
99+
p.id,
100+
p.seller_id,
101+
p.category_id,
102+
p.name,
103+
p.description,
104+
p.price,
105+
p.inventory_quantity,
106+
p.image_url,
107+
p.created_at,
108+
p.updated_at,
98109
c.name as category_name,
99110
u.username as seller_username,
100111
u.email as seller_email
@@ -171,6 +182,11 @@ export class ProductRepository {
171182
updateValues.push(updateData.inventory_quantity);
172183
}
173184

185+
if (updateData.image_url !== undefined) {
186+
updateFields.push('image_url = ?');
187+
updateValues.push(updateData.image_url);
188+
}
189+
174190
if (updateFields.length === 0) {
175191
// No fields to update, return existing product
176192
return existingProduct;
@@ -291,6 +307,7 @@ export class ProductRepository {
291307
p.description,
292308
p.price,
293309
p.inventory_quantity,
310+
p.image_url,
294311
p.created_at,
295312
p.updated_at,
296313
c.name as category_name,

workshops/modernizr/backend/src/repositories/ShoppingCartRepository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export class ShoppingCartRepository {
7777
p.description as product_description,
7878
p.price as product_price,
7979
p.inventory_quantity as product_inventory,
80+
p.image_url as product_image_url,
8081
p.created_at as product_created_at,
8182
p.updated_at as product_updated_at,
8283
c.name as category_name,
@@ -106,6 +107,7 @@ export class ShoppingCartRepository {
106107
description: row.product_description,
107108
price: parseFloat(row.product_price),
108109
inventory_quantity: row.product_inventory,
110+
image_url: row.product_image_url,
109111
category_name: row.category_name,
110112
seller_username: row.seller_username,
111113
seller_email: row.seller_email,

0 commit comments

Comments
 (0)