seed.ts
1 import Database from "better-sqlite3"; 2 import { eq } from "drizzle-orm"; 3 import { drizzle } from "drizzle-orm/better-sqlite3"; 4 import { migrate } from "drizzle-orm/better-sqlite3/migrator"; 5 import path from "path"; 6 import { fileURLToPath } from "url"; 7 import * as schema from "../app/db/schema"; 8 import { 9 UserRole, 10 CourseStatus, 11 LessonProgressStatus, 12 QuestionType, 13 TeamMemberRole, 14 } from "../app/db/schema"; 15 16 const __filename = fileURLToPath(import.meta.url); 17 const __dirname = path.dirname(__filename); 18 const migrationsFolder = path.resolve(__dirname, "../drizzle"); 19 20 const sqlite = new Database("data.db"); 21 sqlite.pragma("journal_mode = WAL"); 22 sqlite.pragma("foreign_keys = ON"); 23 24 const db = drizzle(sqlite, { schema }); 25 26 // ─── Helpers ─── 27 28 function daysAgo(n: number): string { 29 const d = new Date(); 30 d.setDate(d.getDate() - n); 31 return d.toISOString(); 32 } 33 34 function slugify(title: string): string { 35 return title 36 .toLowerCase() 37 .replace(/[^a-z0-9]+/g, "-") 38 .replace(/(^-|-$)/g, ""); 39 } 40 41 // ─── Seed Data ─── 42 43 async function seed() { 44 console.log("Seeding database..."); 45 46 // Drop and recreate tables for a clean seed 47 sqlite.exec(` 48 DROP TABLE IF EXISTS video_watch_events; 49 DROP TABLE IF EXISTS quiz_answers; 50 DROP TABLE IF EXISTS quiz_attempts; 51 DROP TABLE IF EXISTS quiz_options; 52 DROP TABLE IF EXISTS quiz_questions; 53 DROP TABLE IF EXISTS quizzes; 54 DROP TABLE IF EXISTS lesson_progress; 55 DROP TABLE IF EXISTS coupons; 56 DROP TABLE IF EXISTS team_members; 57 DROP TABLE IF EXISTS teams; 58 DROP TABLE IF EXISTS purchases; 59 DROP TABLE IF EXISTS enrollments; 60 DROP TABLE IF EXISTS lessons; 61 DROP TABLE IF EXISTS modules; 62 DROP TABLE IF EXISTS courses; 63 DROP TABLE IF EXISTS categories; 64 DROP TABLE IF EXISTS users; 65 DROP TABLE IF EXISTS __drizzle_migrations; 66 `); 67 68 // Create tables using the same Drizzle migrations as the live database 69 migrate(db, { migrationsFolder }); 70 71 console.log("Tables created."); 72 73 // ─── Users ─── 74 // 1 admin, 2 instructors, 5 students 75 76 const [admin] = db 77 .insert(schema.users) 78 .values({ 79 name: "Alex Rivera", 80 email: "alex.rivera@ralph.dev", 81 role: UserRole.Admin, 82 avatarUrl: "https://api.dicebear.com/9.x/avataaars/svg?seed=alex", 83 createdAt: daysAgo(120), 84 }) 85 .returning() 86 .all(); 87 88 const [instructor1] = db 89 .insert(schema.users) 90 .values({ 91 name: "Sarah Chen", 92 email: "sarah.chen@ralph.dev", 93 role: UserRole.Instructor, 94 avatarUrl: "https://api.dicebear.com/9.x/avataaars/svg?seed=sarah", 95 bio: "Senior TypeScript engineer with 10 years of experience building large-scale web applications. Previously at Stripe and Vercel. Passionate about type safety and developer tooling.", 96 createdAt: daysAgo(100), 97 }) 98 .returning() 99 .all(); 100 101 const [instructor2] = db 102 .insert(schema.users) 103 .values({ 104 name: "Marcus Johnson", 105 email: "marcus.johnson@ralph.dev", 106 role: UserRole.Instructor, 107 avatarUrl: "https://api.dicebear.com/9.x/avataaars/svg?seed=marcus", 108 bio: "Full-stack developer and API architect specializing in Node.js and cloud infrastructure. Has built and scaled APIs serving millions of requests daily. Conference speaker and open-source contributor.", 109 createdAt: daysAgo(95), 110 }) 111 .returning() 112 .all(); 113 114 const students = db 115 .insert(schema.users) 116 .values([ 117 { 118 name: "Emma Wilson", 119 email: "emma.wilson@student.dev", 120 role: UserRole.Student, 121 avatarUrl: "https://api.dicebear.com/9.x/avataaars/svg?seed=emma", 122 createdAt: daysAgo(60), 123 }, 124 { 125 name: "James Park", 126 email: "james.park@student.dev", 127 role: UserRole.Student, 128 avatarUrl: "https://api.dicebear.com/9.x/avataaars/svg?seed=james", 129 createdAt: daysAgo(55), 130 }, 131 { 132 name: "Olivia Martinez", 133 email: "olivia.martinez@student.dev", 134 role: UserRole.Student, 135 avatarUrl: "https://api.dicebear.com/9.x/avataaars/svg?seed=olivia", 136 createdAt: daysAgo(45), 137 }, 138 { 139 name: "Liam Thompson", 140 email: "liam.thompson@student.dev", 141 role: UserRole.Student, 142 avatarUrl: "https://api.dicebear.com/9.x/avataaars/svg?seed=liam", 143 createdAt: daysAgo(30), 144 }, 145 { 146 name: "Sophia Davis", 147 email: "sophia.davis@student.dev", 148 role: UserRole.Student, 149 avatarUrl: "https://api.dicebear.com/9.x/avataaars/svg?seed=sophia", 150 createdAt: daysAgo(20), 151 }, 152 ]) 153 .returning() 154 .all(); 155 156 const [bossy] = db 157 .insert(schema.users) 158 .values({ 159 name: "Bossy McBossface", 160 email: "bossy.mcbossface@student.dev", 161 role: UserRole.Student, 162 avatarUrl: "https://api.dicebear.com/9.x/avataaars/svg?seed=bossy", 163 createdAt: daysAgo(40), 164 }) 165 .returning() 166 .all(); 167 168 console.log( 169 `Created ${1 + 2 + students.length + 1} users (1 admin, 2 instructors, ${students.length + 1} students).` 170 ); 171 172 // ─── Categories ─── 173 174 const categoriesData = db 175 .insert(schema.categories) 176 .values([ 177 { name: "Programming", slug: "programming" }, 178 { name: "Design", slug: "design" }, 179 { name: "Data Science", slug: "data-science" }, 180 { name: "DevOps", slug: "devops" }, 181 { name: "Marketing", slug: "marketing" }, 182 ]) 183 .returning() 184 .all(); 185 186 const catBySlug = Object.fromEntries(categoriesData.map((c) => [c.slug, c])); 187 188 console.log(`Created ${categoriesData.length} categories.`); 189 190 // ─── Course 1: Introduction to TypeScript (Sarah Chen) ─── 191 192 const [course1] = db 193 .insert(schema.courses) 194 .values({ 195 title: "Introduction to TypeScript", 196 slug: "introduction-to-typescript", 197 description: 198 "Master TypeScript from the ground up. Learn type annotations, interfaces, generics, and advanced patterns that will make your JavaScript code safer and more maintainable. Includes hands-on projects and real-world examples.", 199 salesCopy: `## Why TypeScript? 200 201 If you've been writing JavaScript and wondering why your code breaks in production with cryptic "undefined is not a function" errors, TypeScript is the answer you've been looking for. 202 203 TypeScript adds a powerful type system on top of JavaScript that catches bugs before they ever reach your users. It's not just about finding errors — it's about writing code with confidence, knowing that your editor understands your code as well as you do. 204 205 ## What You'll Learn 206 207 This course takes you from zero TypeScript knowledge to confidently using advanced patterns in real projects. We start with the basics — type annotations, interfaces, and simple generics — and build up to discriminated unions, mapped types, conditional types, and template literal types. 208 209 Every concept is taught through practical examples. You won't just learn what a generic is — you'll learn when and why to use one, and how to constrain them for maximum type safety. 210 211 ### Course Highlights 212 213 - **19 lessons** across 5 modules, from setup to advanced patterns 214 - **Hands-on quizzes** to test your understanding as you go 215 - **Real-world React examples** showing TypeScript in production code 216 - **Error handling patterns** using Result types and discriminated unions 217 218 ## Who Is This Course For? 219 220 This course is perfect for JavaScript developers who want to level up their code quality. Whether you're working on a personal project or a large team codebase, TypeScript will make your development experience faster, safer, and more enjoyable. 221 222 No prior TypeScript experience required — just a solid understanding of JavaScript fundamentals. 223 224 ## What Makes This Course Different 225 226 Unlike courses that just show you syntax, this course focuses on *thinking in types*. You'll learn to design your types first and let them guide your implementation, catching entire categories of bugs at compile time instead of runtime. 227 228 By the end of this course, you'll understand why TypeScript has become the default choice for serious JavaScript development.`, 229 instructorId: instructor1.id, 230 categoryId: catBySlug["programming"].id, 231 status: CourseStatus.Published, 232 coverImageUrl: "/images/course-typescript.svg", 233 price: 4999, 234 createdAt: daysAgo(90), 235 updatedAt: daysAgo(10), 236 }) 237 .returning() 238 .all(); 239 240 // Course 1 modules and lessons 241 const c1Modules = [ 242 { 243 title: "Getting Started with TypeScript", 244 lessons: [ 245 { 246 title: "What is TypeScript?", 247 duration: 8, 248 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 249 githubRepoUrl: 250 "https://github.com/total-typescript/ts-intro-what-is-ts", 251 content: `## What is TypeScript? 252 253 TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It adds optional static typing and class-based object-oriented programming to the language. 254 255 ### Why TypeScript? 256 257 - Catch errors at compile time instead of runtime 258 - Better IDE support with autocompletion 259 - Easier to refactor large codebases 260 - Self-documenting code through types`, 261 }, 262 { 263 title: "Installing and Configuring TypeScript", 264 duration: 12, 265 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 266 content: `## Setting Up TypeScript 267 268 Let's get TypeScript installed and configured in your development environment. 269 270 ### Installation 271 272 \`\`\`bash 273 npm install -g typescript 274 tsc --version 275 \`\`\` 276 277 ### tsconfig.json 278 279 The \`tsconfig.json\` file configures the TypeScript compiler options for your project.`, 280 }, 281 { 282 title: "Your First TypeScript Program", 283 duration: 15, 284 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 285 githubRepoUrl: 286 "https://github.com/total-typescript/ts-intro-first-program", 287 content: `## Hello, TypeScript! 288 289 Let's write our first TypeScript program and see the compilation process in action. 290 291 \`\`\`typescript 292 function greet(name: string): string { 293 return \\\`Hello, \\\${name}!\\\`; 294 } 295 296 console.log(greet('World')); 297 \`\`\``, 298 }, 299 ], 300 }, 301 { 302 title: "Type System Fundamentals", 303 lessons: [ 304 { 305 title: "Primitive Types", 306 duration: 10, 307 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 308 content: `## Primitive Types 309 310 TypeScript supports the same primitive types as JavaScript, plus a few extras. 311 312 - \`string\` — text values 313 - \`number\` — numeric values (integer and float) 314 - \`boolean\` — true/false 315 - \`null\` and \`undefined\` 316 - \`symbol\` and \`bigint\``, 317 }, 318 { 319 title: "Arrays and Tuples", 320 duration: 12, 321 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 322 content: `## Arrays and Tuples 323 324 Learn how to type arrays and fixed-length tuples in TypeScript. 325 326 \`\`\`typescript 327 const numbers: number[] = [1, 2, 3]; 328 const pair: [string, number] = ['age', 25]; 329 \`\`\``, 330 }, 331 { 332 title: "Type Aliases and Interfaces", 333 duration: 18, 334 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 335 content: `## Type Aliases vs Interfaces 336 337 Both type aliases and interfaces let you define custom types, but they have subtle differences. 338 339 ### Type Alias 340 341 \`\`\`typescript 342 type User = { 343 name: string; 344 age: number; 345 }; 346 \`\`\` 347 348 ### Interface 349 350 \`\`\`typescript 351 interface User { 352 name: string; 353 age: number; 354 } 355 \`\`\``, 356 }, 357 { 358 title: "Union and Intersection Types", 359 duration: 14, 360 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 361 content: `## Union and Intersection Types 362 363 Combine types in powerful ways using unions (\`|\`) and intersections (\`&\`). 364 365 \`\`\`typescript 366 type StringOrNumber = string | number; 367 type Named = { name: string }; 368 type Aged = { age: number }; 369 type Person = Named & Aged; 370 \`\`\``, 371 }, 372 ], 373 }, 374 { 375 title: "Functions and Generics", 376 lessons: [ 377 { 378 title: "Function Types", 379 duration: 11, 380 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 381 content: `## Typing Functions 382 383 TypeScript lets you type function parameters, return values, and even the function itself. 384 385 \`\`\`typescript 386 function add(a: number, b: number): number { 387 return a + b; 388 } 389 390 const multiply: (a: number, b: number) => number = (a, b) => a * b; 391 \`\`\``, 392 }, 393 { 394 title: "Generics Basics", 395 duration: 20, 396 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 397 githubRepoUrl: 398 "https://github.com/total-typescript/ts-generics-basics", 399 content: `## Introduction to Generics 400 401 Generics let you write reusable code that works with multiple types while maintaining type safety. 402 403 \`\`\`typescript 404 function identity<T>(value: T): T { 405 return value; 406 } 407 408 const str = identity('hello'); // string 409 const num = identity(42); // number 410 \`\`\``, 411 }, 412 { 413 title: "Generic Constraints", 414 duration: 16, 415 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 416 content: `## Constraining Generics 417 418 Use \`extends\` to limit what types a generic can accept. 419 420 \`\`\`typescript 421 function getLength<T extends { length: number }>(item: T): number { 422 return item.length; 423 } 424 425 getLength('hello'); // OK 426 getLength([1, 2, 3]); // OK 427 // getLength(42); // Error! 428 \`\`\``, 429 }, 430 { 431 title: "Utility Types", 432 duration: 15, 433 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 434 content: `## Built-in Utility Types 435 436 TypeScript provides several utility types for common type transformations. 437 438 - \`Partial<T>\` — makes all properties optional 439 - \`Required<T>\` — makes all properties required 440 - \`Pick<T, K>\` — selects specific properties 441 - \`Omit<T, K>\` — excludes specific properties 442 - \`Record<K, V>\` — creates an object type with keys K and values V`, 443 }, 444 ], 445 }, 446 { 447 title: "Advanced Patterns", 448 lessons: [ 449 { 450 title: "Discriminated Unions", 451 duration: 14, 452 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 453 content: `## Discriminated Unions 454 455 A pattern that combines union types with literal types to create type-safe tagged unions. 456 457 \`\`\`typescript 458 type Shape = 459 | { kind: 'circle'; radius: number } 460 | { kind: 'rectangle'; width: number; height: number }; 461 462 function area(shape: Shape): number { 463 switch (shape.kind) { 464 case 'circle': return Math.PI * shape.radius ** 2; 465 case 'rectangle': return shape.width * shape.height; 466 } 467 } 468 \`\`\``, 469 }, 470 { 471 title: "Type Guards and Narrowing", 472 duration: 13, 473 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 474 content: `## Type Guards 475 476 Type guards are expressions that narrow a type within a conditional block. 477 478 \`\`\`typescript 479 function isString(value: unknown): value is string { 480 return typeof value === 'string'; 481 } 482 483 function process(value: string | number) { 484 if (isString(value)) { 485 console.log(value.toUpperCase()); // string 486 } else { 487 console.log(value.toFixed(2)); // number 488 } 489 } 490 \`\`\``, 491 }, 492 { 493 title: "Mapped Types", 494 duration: 17, 495 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 496 content: `## Mapped Types 497 498 Create new types by transforming each property of an existing type. 499 500 \`\`\`typescript 501 type Readonly<T> = { 502 readonly [K in keyof T]: T[K]; 503 }; 504 505 type Optional<T> = { 506 [K in keyof T]?: T[K]; 507 }; 508 \`\`\``, 509 }, 510 { 511 title: "Conditional Types", 512 duration: 19, 513 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 514 content: `## Conditional Types 515 516 Types that depend on a condition, similar to ternary expressions but at the type level. 517 518 \`\`\`typescript 519 type IsString<T> = T extends string ? true : false; 520 521 type A = IsString<'hello'>; // true 522 type B = IsString<42>; // false 523 \`\`\``, 524 }, 525 { 526 title: "Template Literal Types", 527 duration: 10, 528 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 529 content: `## Template Literal Types 530 531 Construct string types using template literal syntax. 532 533 \`\`\`typescript 534 type Color = 'red' | 'blue' | 'green'; 535 type CSSProperty = \\\`color-\\\${Color}\\\`; 536 // 'color-red' | 'color-blue' | 'color-green' 537 \`\`\``, 538 }, 539 ], 540 }, 541 { 542 title: "Real-World TypeScript", 543 lessons: [ 544 { 545 title: "TypeScript with React", 546 duration: 22, 547 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 548 githubRepoUrl: 549 "https://github.com/total-typescript/ts-react-examples", 550 content: `## TypeScript + React 551 552 Learn how to use TypeScript effectively in React applications. 553 554 \`\`\`typescript 555 interface ButtonProps { 556 label: string; 557 onClick: () => void; 558 variant?: 'primary' | 'secondary'; 559 } 560 561 function Button({ label, onClick, variant = 'primary' }: ButtonProps) { 562 return <button onClick={onClick} className={variant}>{label}</button>; 563 } 564 \`\`\``, 565 }, 566 { 567 title: "Error Handling Patterns", 568 duration: 14, 569 videoUrl: "https://www.youtube.com/watch?v=zQnBQ4tB3ZA", 570 content: `## Error Handling in TypeScript 571 572 Strategies for handling errors in a type-safe way. 573 574 \`\`\`typescript 575 type Result<T, E = Error> = 576 | { ok: true; value: T } 577 | { ok: false; error: E }; 578 579 function divide(a: number, b: number): Result<number> { 580 if (b === 0) return { ok: false, error: new Error('Division by zero') }; 581 return { ok: true, value: a / b }; 582 } 583 \`\`\``, 584 }, 585 { 586 title: "Course Wrap-Up and Next Steps", 587 duration: 8, 588 content: `## Congratulations! 589 590 You've completed the Introduction to TypeScript course. Here's what we covered: 591 592 - TypeScript fundamentals and type system 593 - Functions, generics, and utility types 594 - Advanced patterns like discriminated unions and mapped types 595 - Real-world usage with React 596 597 ### Next Steps 598 599 Practice by converting an existing JavaScript project to TypeScript. Start with strict mode enabled and work through the errors one by one.`, 600 }, 601 ], 602 }, 603 ]; 604 605 const course1LessonIds: number[] = []; 606 607 for (let mi = 0; mi < c1Modules.length; mi++) { 608 const modData = c1Modules[mi]; 609 const [mod] = db 610 .insert(schema.modules) 611 .values({ 612 courseId: course1.id, 613 title: modData.title, 614 position: mi + 1, 615 createdAt: daysAgo(90 - mi), 616 }) 617 .returning() 618 .all(); 619 620 for (let li = 0; li < modData.lessons.length; li++) { 621 const lessonData = modData.lessons[li]; 622 const [lesson] = db 623 .insert(schema.lessons) 624 .values({ 625 moduleId: mod.id, 626 title: lessonData.title, 627 content: lessonData.content, 628 videoUrl: lessonData.videoUrl ?? null, 629 githubRepoUrl: 630 ("githubRepoUrl" in lessonData ? lessonData.githubRepoUrl : null) ?? 631 null, 632 position: li + 1, 633 durationMinutes: lessonData.duration, 634 createdAt: daysAgo(90 - mi), 635 }) 636 .returning() 637 .all(); 638 course1LessonIds.push(lesson.id); 639 } 640 } 641 642 console.log( 643 `Created course "${course1.title}" with ${c1Modules.length} modules and ${course1LessonIds.length} lessons.` 644 ); 645 646 // ─── Course 2: Building REST APIs with Node.js (Marcus Johnson) ─── 647 648 const [course2] = db 649 .insert(schema.courses) 650 .values({ 651 title: "Building REST APIs with Node.js", 652 slug: "building-rest-apis-with-nodejs", 653 description: 654 "Learn to build production-ready REST APIs using Node.js and Express. Covers routing, middleware, authentication, database integration, error handling, testing, and deployment best practices.", 655 salesCopy: `## Build APIs That Actually Work in Production 656 657 Most API tutorials teach you how to return JSON from an endpoint. This course teaches you how to build APIs that handle real traffic, real users, and real problems — the kind you'll face on the job. 658 659 From your first Express route to deploying a production-ready API, you'll learn every layer of the stack: routing, middleware, validation, authentication, database integration, testing, and deployment. 660 661 ## What You'll Build 662 663 Throughout this course, you'll build a complete REST API from scratch. Not a toy project — a properly structured API with authentication, input validation, error handling, pagination, and tests. 664 665 ### Topics Covered 666 667 - **Express fundamentals** — routing, middleware chains, request/response lifecycle 668 - **Input validation with Zod** — never trust user input, validate everything 669 - **Database integration** — Drizzle ORM with SQLite, CRUD operations, transactions 670 - **JWT authentication** — secure your endpoints with industry-standard tokens 671 - **Security hardening** — rate limiting, CORS, security headers with Helmet 672 - **Testing** — unit tests with Vitest, integration tests with Supertest 673 - **Deployment** — environment config, process management, CI/CD basics 674 675 ## Who Should Take This Course? 676 677 This course is designed for developers who know JavaScript and want to build backend services. If you've built frontends but never created your own API, this is the perfect next step. 678 679 You should be comfortable with JavaScript basics — functions, async/await, and working with objects. No backend experience required. 680 681 ## Why Node.js for APIs? 682 683 Node.js lets you use the same language on both frontend and backend. Its non-blocking I/O model handles concurrent requests efficiently, and the npm ecosystem gives you battle-tested libraries for every common backend task. 684 685 Express is the most widely-used Node.js web framework for a reason — it's minimal, flexible, and has a massive community. The patterns you learn here will transfer to any Node.js framework. 686 687 ## 20 Lessons, 5 Modules, Zero Fluff 688 689 Every lesson is focused and practical. No 45-minute lectures where 40 minutes are filler. Each lesson teaches one concept, shows you how to implement it, and moves on.`, 690 instructorId: instructor2.id, 691 categoryId: catBySlug["programming"].id, 692 status: CourseStatus.Published, 693 coverImageUrl: "/images/course-nodejs.svg", 694 price: 5999, 695 createdAt: daysAgo(75), 696 updatedAt: daysAgo(5), 697 }) 698 .returning() 699 .all(); 700 701 const c2Modules = [ 702 { 703 title: "API Fundamentals", 704 lessons: [ 705 { 706 title: "What is a REST API?", 707 duration: 10, 708 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 709 content: `## REST API Fundamentals 710 711 REST (Representational State Transfer) is an architectural style for designing networked applications. RESTful APIs use HTTP methods to perform CRUD operations on resources. 712 713 ### Key Principles 714 715 - Stateless communication 716 - Resource-based URLs 717 - Standard HTTP methods (GET, POST, PUT, DELETE) 718 - JSON as the data format`, 719 }, 720 { 721 title: "Setting Up Express", 722 duration: 15, 723 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 724 githubRepoUrl: 725 "https://github.com/total-typescript/rest-api-express-setup", 726 content: `## Express.js Setup 727 728 Express is the most popular Node.js web framework for building APIs. 729 730 \`\`\`javascript 731 import express from 'express'; 732 733 const app = express(); 734 app.use(express.json()); 735 736 app.get('/api/health', (req, res) => { 737 res.json({ status: 'ok' }); 738 }); 739 740 app.listen(3000, () => console.log('Server running on port 3000')); 741 \`\`\``, 742 }, 743 { 744 title: "HTTP Methods and Status Codes", 745 duration: 12, 746 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 747 content: `## HTTP Methods 748 749 - **GET** — Retrieve resources (200 OK) 750 - **POST** — Create resources (201 Created) 751 - **PUT** — Update resources (200 OK) 752 - **DELETE** — Remove resources (204 No Content) 753 754 ### Common Status Codes 755 756 - 200 OK, 201 Created, 204 No Content 757 - 400 Bad Request, 401 Unauthorized, 404 Not Found 758 - 500 Internal Server Error`, 759 }, 760 { 761 title: "Request and Response Objects", 762 duration: 14, 763 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 764 content: `## Working with Request & Response 765 766 Express provides rich request and response objects for handling HTTP communication. 767 768 \`\`\`javascript 769 app.post('/api/users', (req, res) => { 770 const { name, email } = req.body; 771 // ... create user 772 res.status(201).json({ id: 1, name, email }); 773 }); 774 \`\`\``, 775 }, 776 ], 777 }, 778 { 779 title: "Routing and Middleware", 780 lessons: [ 781 { 782 title: "Express Router", 783 duration: 13, 784 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 785 content: `## Organizing Routes 786 787 Use Express Router to organize your API endpoints into logical groups. 788 789 \`\`\`javascript 790 import { Router } from 'express'; 791 792 const userRouter = Router(); 793 userRouter.get('/', getUsers); 794 userRouter.get('/:id', getUserById); 795 userRouter.post('/', createUser); 796 797 app.use('/api/users', userRouter); 798 \`\`\``, 799 }, 800 { 801 title: "Custom Middleware", 802 duration: 16, 803 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 804 content: `## Middleware in Express 805 806 Middleware functions have access to the request, response, and next function in the request-response cycle. 807 808 \`\`\`javascript 809 function logger(req, res, next) { 810 console.log(\\\`\\\${req.method} \\\${req.url}\\\`); 811 next(); 812 } 813 814 function authenticate(req, res, next) { 815 const token = req.headers.authorization; 816 if (!token) return res.status(401).json({ error: 'Unauthorized' }); 817 next(); 818 } 819 \`\`\``, 820 }, 821 { 822 title: "Error Handling Middleware", 823 duration: 11, 824 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 825 content: `## Centralized Error Handling 826 827 Express supports error-handling middleware with four parameters. 828 829 \`\`\`javascript 830 app.use((err, req, res, next) => { 831 console.error(err.stack); 832 res.status(err.status || 500).json({ 833 error: err.message || 'Internal Server Error' 834 }); 835 }); 836 \`\`\``, 837 }, 838 { 839 title: "Validation with Zod", 840 duration: 18, 841 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 842 content: `## Request Validation 843 844 Use Zod to validate request bodies, query parameters, and URL parameters. 845 846 \`\`\`javascript 847 import { z } from 'zod'; 848 849 const CreateUserSchema = z.object({ 850 name: z.string().min(1), 851 email: z.string().email(), 852 age: z.number().int().positive().optional() 853 }); 854 855 app.post('/api/users', (req, res) => { 856 const result = CreateUserSchema.safeParse(req.body); 857 if (!result.success) return res.status(400).json(result.error); 858 // ... create user with result.data 859 }); 860 \`\`\``, 861 }, 862 ], 863 }, 864 { 865 title: "Database Integration", 866 lessons: [ 867 { 868 title: "Connecting to a Database", 869 duration: 14, 870 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 871 content: `## Database Setup 872 873 Learn how to connect your API to a database using an ORM. 874 875 \`\`\`javascript 876 import { drizzle } from 'drizzle-orm/better-sqlite3'; 877 import Database from 'better-sqlite3'; 878 879 const sqlite = new Database('app.db'); 880 const db = drizzle(sqlite); 881 \`\`\``, 882 }, 883 { 884 title: "CRUD Operations", 885 duration: 20, 886 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 887 githubRepoUrl: 888 "https://github.com/total-typescript/rest-api-crud-operations", 889 content: `## Building CRUD Endpoints 890 891 Implement Create, Read, Update, Delete operations for your API resources. 892 893 \`\`\`javascript 894 // Create 895 app.post('/api/posts', async (req, res) => { 896 const post = await db.insert(posts).values(req.body).returning(); 897 res.status(201).json(post); 898 }); 899 900 // Read 901 app.get('/api/posts/:id', async (req, res) => { 902 const post = await db.select().from(posts).where(eq(posts.id, req.params.id)); 903 if (!post) return res.status(404).json({ error: 'Not found' }); 904 res.json(post); 905 }); 906 \`\`\``, 907 }, 908 { 909 title: "Pagination and Filtering", 910 duration: 15, 911 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 912 content: `## Pagination 913 914 Implement cursor-based and offset-based pagination for list endpoints. 915 916 \`\`\`javascript 917 app.get('/api/posts', async (req, res) => { 918 const page = parseInt(req.query.page) || 1; 919 const limit = parseInt(req.query.limit) || 10; 920 const offset = (page - 1) * limit; 921 922 const results = await db.select().from(posts) 923 .limit(limit).offset(offset); 924 res.json({ data: results, page, limit }); 925 }); 926 \`\`\``, 927 }, 928 { 929 title: "Transactions", 930 duration: 12, 931 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 932 content: `## Database Transactions 933 934 Use transactions to ensure data consistency when multiple operations must succeed or fail together. 935 936 \`\`\`javascript 937 await db.transaction(async (tx) => { 938 const [order] = await tx.insert(orders).values({ userId, total }).returning(); 939 for (const item of items) { 940 await tx.insert(orderItems).values({ orderId: order.id, ...item }); 941 } 942 }); 943 \`\`\``, 944 }, 945 ], 946 }, 947 { 948 title: "Authentication and Security", 949 lessons: [ 950 { 951 title: "JWT Authentication", 952 duration: 22, 953 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 954 content: `## JSON Web Tokens 955 956 Implement JWT-based authentication for your API. 957 958 \`\`\`javascript 959 import jwt from 'jsonwebtoken'; 960 961 app.post('/api/login', async (req, res) => { 962 const user = await findUser(req.body.email); 963 const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { 964 expiresIn: '7d' 965 }); 966 res.json({ token }); 967 }); 968 \`\`\``, 969 }, 970 { 971 title: "Rate Limiting", 972 duration: 10, 973 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 974 content: `## Rate Limiting 975 976 Protect your API from abuse by limiting the number of requests per client. 977 978 \`\`\`javascript 979 import rateLimit from 'express-rate-limit'; 980 981 const limiter = rateLimit({ 982 windowMs: 15 * 60 * 1000, // 15 minutes 983 max: 100 // limit each IP to 100 requests per window 984 }); 985 986 app.use('/api/', limiter); 987 \`\`\``, 988 }, 989 { 990 title: "CORS and Security Headers", 991 duration: 11, 992 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 993 content: `## CORS Configuration 994 995 Configure Cross-Origin Resource Sharing for your API. 996 997 \`\`\`javascript 998 import cors from 'cors'; 999 import helmet from 'helmet'; 1000 1001 app.use(cors({ origin: 'https://yourapp.com' })); 1002 app.use(helmet()); 1003 \`\`\``, 1004 }, 1005 ], 1006 }, 1007 { 1008 title: "Testing and Deployment", 1009 lessons: [ 1010 { 1011 title: "Unit Testing API Routes", 1012 duration: 18, 1013 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 1014 content: `## Testing with Vitest and Supertest 1015 1016 Write tests for your API endpoints using Vitest and Supertest. 1017 1018 \`\`\`javascript 1019 import { describe, it, expect } from 'vitest'; 1020 import request from 'supertest'; 1021 import app from '../app'; 1022 1023 describe('GET /api/users', () => { 1024 it('returns a list of users', async () => { 1025 const res = await request(app).get('/api/users'); 1026 expect(res.status).toBe(200); 1027 expect(res.body).toBeInstanceOf(Array); 1028 }); 1029 }); 1030 \`\`\``, 1031 }, 1032 { 1033 title: "Integration Testing", 1034 duration: 16, 1035 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 1036 content: `## Integration Tests 1037 1038 Test complete request flows including database interactions. 1039 1040 \`\`\`javascript 1041 describe('User CRUD', () => { 1042 it('creates and retrieves a user', async () => { 1043 const createRes = await request(app) 1044 .post('/api/users') 1045 .send({ name: 'Test', email: 'test@test.com' }); 1046 expect(createRes.status).toBe(201); 1047 1048 const getRes = await request(app) 1049 .get(\\\`/api/users/\\\${createRes.body.id}\\\`); 1050 expect(getRes.body.name).toBe('Test'); 1051 }); 1052 }); 1053 \`\`\``, 1054 }, 1055 { 1056 title: "Environment Variables and Config", 1057 duration: 9, 1058 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 1059 content: `## Configuration Management 1060 1061 Manage environment-specific settings with environment variables. 1062 1063 \`\`\`javascript 1064 const config = { 1065 port: process.env.PORT || 3000, 1066 dbUrl: process.env.DATABASE_URL || 'sqlite:app.db', 1067 jwtSecret: process.env.JWT_SECRET || 'dev-secret' 1068 }; 1069 \`\`\``, 1070 }, 1071 { 1072 title: "Deploying Your API", 1073 duration: 14, 1074 videoUrl: "https://www.youtube.com/watch?v=lsMQRaeKNDk", 1075 content: `## Deployment 1076 1077 Deploy your Node.js API to production. We'll cover various hosting options and best practices. 1078 1079 ### Deployment Checklist 1080 1081 - Set NODE_ENV=production 1082 - Use a process manager (PM2) 1083 - Set up logging and monitoring 1084 - Configure HTTPS 1085 - Set up CI/CD pipeline`, 1086 }, 1087 { 1088 title: "Course Wrap-Up", 1089 duration: 7, 1090 content: `## Congratulations! 1091 1092 You've completed the Building REST APIs course. You now have the skills to build, test, and deploy production-ready APIs with Node.js. 1093 1094 ### Key Takeaways 1095 1096 - RESTful design principles 1097 - Express routing and middleware 1098 - Database integration and transactions 1099 - Authentication and security 1100 - Testing and deployment`, 1101 }, 1102 ], 1103 }, 1104 ]; 1105 1106 const course2LessonIds: number[] = []; 1107 1108 for (let mi = 0; mi < c2Modules.length; mi++) { 1109 const modData = c2Modules[mi]; 1110 const [mod] = db 1111 .insert(schema.modules) 1112 .values({ 1113 courseId: course2.id, 1114 title: modData.title, 1115 position: mi + 1, 1116 createdAt: daysAgo(75 - mi), 1117 }) 1118 .returning() 1119 .all(); 1120 1121 for (let li = 0; li < modData.lessons.length; li++) { 1122 const lessonData = modData.lessons[li]; 1123 const [lesson] = db 1124 .insert(schema.lessons) 1125 .values({ 1126 moduleId: mod.id, 1127 title: lessonData.title, 1128 content: lessonData.content, 1129 videoUrl: lessonData.videoUrl ?? null, 1130 githubRepoUrl: 1131 ("githubRepoUrl" in lessonData ? lessonData.githubRepoUrl : null) ?? 1132 null, 1133 position: li + 1, 1134 durationMinutes: lessonData.duration, 1135 createdAt: daysAgo(75 - mi), 1136 }) 1137 .returning() 1138 .all(); 1139 course2LessonIds.push(lesson.id); 1140 } 1141 } 1142 1143 console.log( 1144 `Created course "${course2.title}" with ${c2Modules.length} modules and ${course2LessonIds.length} lessons.` 1145 ); 1146 1147 // ─── Quizzes ─── 1148 // Add quizzes to some lessons in both courses 1149 1150 // Quiz 1: TypeScript Basics Quiz (attached to "Your First TypeScript Program", lesson 3 of course 1) 1151 const [quiz1] = db 1152 .insert(schema.quizzes) 1153 .values({ 1154 lessonId: course1LessonIds[2], // "Your First TypeScript Program" 1155 title: "TypeScript Basics Quiz", 1156 passingScore: 0.7, 1157 }) 1158 .returning() 1159 .all(); 1160 1161 const quiz1Questions = [ 1162 { 1163 text: "What does TypeScript compile to?", 1164 type: QuestionType.MultipleChoice, 1165 options: [ 1166 { text: "JavaScript", correct: true }, 1167 { text: "WebAssembly", correct: false }, 1168 { text: "Java bytecode", correct: false }, 1169 { text: "Machine code", correct: false }, 1170 ], 1171 }, 1172 { 1173 text: "TypeScript is a superset of JavaScript.", 1174 type: QuestionType.TrueFalse, 1175 options: [ 1176 { text: "True", correct: true }, 1177 { text: "False", correct: false }, 1178 ], 1179 }, 1180 { 1181 text: "Which file configures the TypeScript compiler?", 1182 type: QuestionType.MultipleChoice, 1183 options: [ 1184 { text: "tsconfig.json", correct: true }, 1185 { text: "package.json", correct: false }, 1186 { text: "typescript.config.js", correct: false }, 1187 { text: ".tsrc", correct: false }, 1188 ], 1189 }, 1190 ]; 1191 1192 const quiz1OptionIds: { 1193 questionId: number; 1194 optionId: number; 1195 correct: boolean; 1196 }[] = []; 1197 1198 for (let qi = 0; qi < quiz1Questions.length; qi++) { 1199 const q = quiz1Questions[qi]; 1200 const [question] = db 1201 .insert(schema.quizQuestions) 1202 .values({ 1203 quizId: quiz1.id, 1204 questionText: q.text, 1205 questionType: q.type, 1206 position: qi + 1, 1207 }) 1208 .returning() 1209 .all(); 1210 1211 for (const opt of q.options) { 1212 const [option] = db 1213 .insert(schema.quizOptions) 1214 .values({ 1215 questionId: question.id, 1216 optionText: opt.text, 1217 isCorrect: opt.correct, 1218 }) 1219 .returning() 1220 .all(); 1221 quiz1OptionIds.push({ 1222 questionId: question.id, 1223 optionId: option.id, 1224 correct: opt.correct, 1225 }); 1226 } 1227 } 1228 1229 // Quiz 2: Generics Quiz (attached to "Generics Basics", lesson index 5 in course 1) 1230 const [quiz2] = db 1231 .insert(schema.quizzes) 1232 .values({ 1233 lessonId: course1LessonIds[7], // "Generics Basics" (module 3, lesson 2) 1234 title: "Generics Knowledge Check", 1235 passingScore: 0.6, 1236 }) 1237 .returning() 1238 .all(); 1239 1240 const quiz2Questions = [ 1241 { 1242 text: "What is the primary benefit of generics?", 1243 type: QuestionType.MultipleChoice, 1244 options: [ 1245 { text: "Code reusability with type safety", correct: true }, 1246 { text: "Faster execution speed", correct: false }, 1247 { text: "Smaller bundle size", correct: false }, 1248 { text: "Better error messages", correct: false }, 1249 ], 1250 }, 1251 { 1252 text: "Generic type parameters can be constrained using the 'extends' keyword.", 1253 type: QuestionType.TrueFalse, 1254 options: [ 1255 { text: "True", correct: true }, 1256 { text: "False", correct: false }, 1257 ], 1258 }, 1259 ]; 1260 1261 const quiz2OptionIds: { 1262 questionId: number; 1263 optionId: number; 1264 correct: boolean; 1265 }[] = []; 1266 1267 for (let qi = 0; qi < quiz2Questions.length; qi++) { 1268 const q = quiz2Questions[qi]; 1269 const [question] = db 1270 .insert(schema.quizQuestions) 1271 .values({ 1272 quizId: quiz2.id, 1273 questionText: q.text, 1274 questionType: q.type, 1275 position: qi + 1, 1276 }) 1277 .returning() 1278 .all(); 1279 1280 for (const opt of q.options) { 1281 const [option] = db 1282 .insert(schema.quizOptions) 1283 .values({ 1284 questionId: question.id, 1285 optionText: opt.text, 1286 isCorrect: opt.correct, 1287 }) 1288 .returning() 1289 .all(); 1290 quiz2OptionIds.push({ 1291 questionId: question.id, 1292 optionId: option.id, 1293 correct: opt.correct, 1294 }); 1295 } 1296 } 1297 1298 // Quiz 3: REST API Basics (attached to "HTTP Methods and Status Codes", lesson index 2 in course 2) 1299 const [quiz3] = db 1300 .insert(schema.quizzes) 1301 .values({ 1302 lessonId: course2LessonIds[2], // "HTTP Methods and Status Codes" 1303 title: "HTTP Methods Quiz", 1304 passingScore: 0.7, 1305 }) 1306 .returning() 1307 .all(); 1308 1309 const quiz3Questions = [ 1310 { 1311 text: "Which HTTP method is used to create a new resource?", 1312 type: QuestionType.MultipleChoice, 1313 options: [ 1314 { text: "POST", correct: true }, 1315 { text: "GET", correct: false }, 1316 { text: "PUT", correct: false }, 1317 { text: "PATCH", correct: false }, 1318 ], 1319 }, 1320 { 1321 text: "A 404 status code means the server encountered an internal error.", 1322 type: QuestionType.TrueFalse, 1323 options: [ 1324 { text: "True", correct: false }, 1325 { text: "False", correct: true }, 1326 ], 1327 }, 1328 { 1329 text: "Which status code indicates successful resource creation?", 1330 type: QuestionType.MultipleChoice, 1331 options: [ 1332 { text: "201 Created", correct: true }, 1333 { text: "200 OK", correct: false }, 1334 { text: "204 No Content", correct: false }, 1335 { text: "202 Accepted", correct: false }, 1336 ], 1337 }, 1338 ]; 1339 1340 const quiz3OptionIds: { 1341 questionId: number; 1342 optionId: number; 1343 correct: boolean; 1344 }[] = []; 1345 1346 for (let qi = 0; qi < quiz3Questions.length; qi++) { 1347 const q = quiz3Questions[qi]; 1348 const [question] = db 1349 .insert(schema.quizQuestions) 1350 .values({ 1351 quizId: quiz3.id, 1352 questionText: q.text, 1353 questionType: q.type, 1354 position: qi + 1, 1355 }) 1356 .returning() 1357 .all(); 1358 1359 for (const opt of q.options) { 1360 const [option] = db 1361 .insert(schema.quizOptions) 1362 .values({ 1363 questionId: question.id, 1364 optionText: opt.text, 1365 isCorrect: opt.correct, 1366 }) 1367 .returning() 1368 .all(); 1369 quiz3OptionIds.push({ 1370 questionId: question.id, 1371 optionId: option.id, 1372 correct: opt.correct, 1373 }); 1374 } 1375 } 1376 1377 console.log("Created 3 quizzes with questions and options."); 1378 1379 // ─── Enrollments ─── 1380 // Varied enrollment patterns: 1381 // - Emma: enrolled in both courses (nearly complete in course 1, mid-way in course 2) 1382 // - James: enrolled in course 1 only (completed) 1383 // - Olivia: enrolled in both courses (just started course 1, mid-way in course 2) 1384 // - Liam: enrolled in course 2 only (just started, abandoned) 1385 // - Sophia: enrolled in course 1 only (recently enrolled, barely started) 1386 1387 db.insert(schema.enrollments) 1388 .values([ 1389 { userId: students[0].id, courseId: course1.id, enrolledAt: daysAgo(50) }, 1390 { userId: students[0].id, courseId: course2.id, enrolledAt: daysAgo(40) }, 1391 { 1392 userId: students[1].id, 1393 courseId: course1.id, 1394 enrolledAt: daysAgo(45), 1395 completedAt: daysAgo(10), 1396 }, 1397 { userId: students[2].id, courseId: course1.id, enrolledAt: daysAgo(35) }, 1398 { userId: students[2].id, courseId: course2.id, enrolledAt: daysAgo(30) }, 1399 { userId: students[3].id, courseId: course2.id, enrolledAt: daysAgo(25) }, 1400 { userId: students[4].id, courseId: course1.id, enrolledAt: daysAgo(15) }, 1401 ]) 1402 .run(); 1403 1404 console.log("Created 7 enrollments."); 1405 1406 // ─── Lesson Progress ─── 1407 1408 // Helper to mark lessons as complete 1409 function markComplete( 1410 userId: number, 1411 lessonId: number, 1412 daysAgoCompleted: number 1413 ) { 1414 db.insert(schema.lessonProgress) 1415 .values({ 1416 userId, 1417 lessonId, 1418 status: LessonProgressStatus.Completed, 1419 completedAt: daysAgo(daysAgoCompleted), 1420 }) 1421 .run(); 1422 } 1423 1424 function markInProgress(userId: number, lessonId: number) { 1425 db.insert(schema.lessonProgress) 1426 .values({ 1427 userId, 1428 lessonId, 1429 status: LessonProgressStatus.InProgress, 1430 }) 1431 .run(); 1432 } 1433 1434 // Emma (students[0]) — nearly complete in course 1 (17 of 19 lessons done) 1435 for (let i = 0; i < 17; i++) { 1436 markComplete(students[0].id, course1LessonIds[i], 50 - i); 1437 } 1438 markInProgress(students[0].id, course1LessonIds[17]); 1439 1440 // Emma — mid-way through course 2 (10 of 20 lessons done) 1441 for (let i = 0; i < 10; i++) { 1442 markComplete(students[0].id, course2LessonIds[i], 40 - i); 1443 } 1444 markInProgress(students[0].id, course2LessonIds[10]); 1445 1446 // James (students[1]) — completed all of course 1 1447 for (let i = 0; i < course1LessonIds.length; i++) { 1448 markComplete(students[1].id, course1LessonIds[i], 45 - i); 1449 } 1450 1451 // Olivia (students[2]) — just started course 1 (3 lessons done) 1452 for (let i = 0; i < 3; i++) { 1453 markComplete(students[2].id, course1LessonIds[i], 30 - i); 1454 } 1455 markInProgress(students[2].id, course1LessonIds[3]); 1456 1457 // Olivia — mid-way through course 2 (8 lessons done) 1458 for (let i = 0; i < 8; i++) { 1459 markComplete(students[2].id, course2LessonIds[i], 28 - i); 1460 } 1461 1462 // Liam (students[3]) — just started course 2, abandoned (2 lessons done) 1463 for (let i = 0; i < 2; i++) { 1464 markComplete(students[3].id, course2LessonIds[i], 22 - i); 1465 } 1466 1467 // Sophia (students[4]) — barely started course 1 (1 lesson done) 1468 markComplete(students[4].id, course1LessonIds[0], 12); 1469 markInProgress(students[4].id, course1LessonIds[1]); 1470 1471 console.log("Created lesson progress records."); 1472 1473 // ─── Quiz Attempts ─── 1474 1475 // Helper to record a quiz attempt with answers 1476 function recordQuizAttempt( 1477 userId: number, 1478 quizId: number, 1479 optionIds: { questionId: number; optionId: number; correct: boolean }[], 1480 selectedCorrectIndices: number[], // which questions (0-based) the student got right 1481 attemptDaysAgo: number 1482 ) { 1483 const totalQuestions = new Set(optionIds.map((o) => o.questionId)).size; 1484 const correctCount = selectedCorrectIndices.length; 1485 const score = correctCount / totalQuestions; 1486 1487 // Determine passing based on quiz passingScore (we'll just use 0.7 as default) 1488 const passed = score >= 0.7; 1489 1490 const [attempt] = db 1491 .insert(schema.quizAttempts) 1492 .values({ 1493 userId, 1494 quizId, 1495 score, 1496 passed, 1497 attemptedAt: daysAgo(attemptDaysAgo), 1498 }) 1499 .returning() 1500 .all(); 1501 1502 // Build answer selections 1503 const questionIds = [...new Set(optionIds.map((o) => o.questionId))]; 1504 for (let qi = 0; qi < questionIds.length; qi++) { 1505 const qId = questionIds[qi]; 1506 const qOptions = optionIds.filter((o) => o.questionId === qId); 1507 let selectedOption: (typeof qOptions)[0]; 1508 1509 if (selectedCorrectIndices.includes(qi)) { 1510 // Pick correct answer 1511 selectedOption = qOptions.find((o) => o.correct)!; 1512 } else { 1513 // Pick wrong answer 1514 selectedOption = qOptions.find((o) => !o.correct)!; 1515 } 1516 1517 db.insert(schema.quizAnswers) 1518 .values({ 1519 attemptId: attempt.id, 1520 questionId: qId, 1521 selectedOptionId: selectedOption.optionId, 1522 }) 1523 .run(); 1524 } 1525 } 1526 1527 // Emma — passed quiz 1 (3/3 correct) 1528 recordQuizAttempt(students[0].id, quiz1.id, quiz1OptionIds, [0, 1, 2], 35); 1529 1530 // Emma — passed quiz 2 (2/2 correct) 1531 recordQuizAttempt(students[0].id, quiz2.id, quiz2OptionIds, [0, 1], 30); 1532 1533 // Emma — passed quiz 3 (2/3 correct, just barely at 67% with 70% passing = fail, then retake) 1534 recordQuizAttempt(students[0].id, quiz3.id, quiz3OptionIds, [0, 2], 28); 1535 // Retake — all correct 1536 recordQuizAttempt(students[0].id, quiz3.id, quiz3OptionIds, [0, 1, 2], 27); 1537 1538 // James — passed quiz 1 (3/3 correct) 1539 recordQuizAttempt(students[1].id, quiz1.id, quiz1OptionIds, [0, 1, 2], 40); 1540 1541 // James — passed quiz 2 (2/2 correct) 1542 recordQuizAttempt(students[1].id, quiz2.id, quiz2OptionIds, [0, 1], 35); 1543 1544 // Olivia — failed quiz 1 first attempt (1/3 correct), then passed on retry (3/3) 1545 recordQuizAttempt(students[2].id, quiz1.id, quiz1OptionIds, [0], 25); 1546 recordQuizAttempt(students[2].id, quiz1.id, quiz1OptionIds, [0, 1, 2], 24); 1547 1548 // Olivia — passed quiz 3 (3/3 correct) 1549 recordQuizAttempt(students[2].id, quiz3.id, quiz3OptionIds, [0, 1, 2], 20); 1550 1551 // Sophia — failed quiz 1 (1/3 correct, hasn't retaken yet) 1552 recordQuizAttempt(students[4].id, quiz1.id, quiz1OptionIds, [1], 10); 1553 1554 console.log("Created quiz attempts and answers."); 1555 1556 // ─── Video Watch Events ─── 1557 // Sprinkle some realistic watch events 1558 1559 function addWatchEvent( 1560 userId: number, 1561 lessonId: number, 1562 eventType: string, 1563 positionSeconds: number, 1564 eventDaysAgo: number 1565 ) { 1566 db.insert(schema.videoWatchEvents) 1567 .values({ 1568 userId, 1569 lessonId, 1570 eventType, 1571 positionSeconds, 1572 createdAt: daysAgo(eventDaysAgo), 1573 }) 1574 .run(); 1575 } 1576 1577 // Emma watching course 1 lesson 1 (8 min video) 1578 addWatchEvent(students[0].id, course1LessonIds[0], "play", 0, 50); 1579 addWatchEvent(students[0].id, course1LessonIds[0], "pause", 180, 50); 1580 addWatchEvent(students[0].id, course1LessonIds[0], "play", 180, 49); 1581 addWatchEvent(students[0].id, course1LessonIds[0], "ended", 480, 49); 1582 1583 // James watching course 1 lesson 1 1584 addWatchEvent(students[1].id, course1LessonIds[0], "play", 0, 45); 1585 addWatchEvent(students[1].id, course1LessonIds[0], "ended", 480, 45); 1586 1587 // Liam started watching course 2 lesson 1 but stopped mid-way 1588 addWatchEvent(students[3].id, course2LessonIds[0], "play", 0, 22); 1589 addWatchEvent(students[3].id, course2LessonIds[0], "pause", 300, 22); 1590 addWatchEvent(students[3].id, course2LessonIds[0], "seek", 150, 21); 1591 addWatchEvent(students[3].id, course2LessonIds[0], "play", 150, 21); 1592 addWatchEvent(students[3].id, course2LessonIds[0], "pause", 360, 21); 1593 1594 console.log("Created video watch events."); 1595 1596 // ─── Purchases ─── 1597 // Individual purchases for enrolled students 1598 1599 const [purchase1] = db 1600 .insert(schema.purchases) 1601 .values({ 1602 userId: students[0].id, // Emma — bought course 1 individually 1603 courseId: course1.id, 1604 pricePaid: 4999, 1605 country: "US", 1606 createdAt: daysAgo(50), 1607 }) 1608 .returning() 1609 .all(); 1610 1611 db.insert(schema.purchases) 1612 .values({ 1613 userId: students[0].id, // Emma — bought course 2 individually 1614 courseId: course2.id, 1615 pricePaid: 5999, 1616 country: "US", 1617 createdAt: daysAgo(40), 1618 }) 1619 .run(); 1620 1621 db.insert(schema.purchases) 1622 .values({ 1623 userId: students[1].id, // James — bought course 1 with PPP discount (India) 1624 courseId: course1.id, 1625 pricePaid: 2500, 1626 country: "IN", 1627 createdAt: daysAgo(45), 1628 }) 1629 .run(); 1630 1631 db.insert(schema.purchases) 1632 .values({ 1633 userId: students[2].id, // Olivia — bought course 1 individually 1634 courseId: course1.id, 1635 pricePaid: 4999, 1636 country: "US", 1637 createdAt: daysAgo(35), 1638 }) 1639 .run(); 1640 1641 db.insert(schema.purchases) 1642 .values({ 1643 userId: students[4].id, // Sophia — bought course 1 individually 1644 courseId: course1.id, 1645 pricePaid: 4999, 1646 country: "US", 1647 createdAt: daysAgo(15), 1648 }) 1649 .run(); 1650 1651 console.log("Created 5 individual purchases."); 1652 1653 // ─── Teams, Team Members, and Coupons ─── 1654 // Bossy McBossface bought 5 team seats for course 2; Olivia and Liam redeemed coupons 1655 1656 const [team1] = db 1657 .insert(schema.teams) 1658 .values({ createdAt: daysAgo(30) }) 1659 .returning() 1660 .all(); 1661 1662 db.insert(schema.teamMembers) 1663 .values({ 1664 teamId: team1.id, 1665 userId: bossy.id, 1666 role: TeamMemberRole.Admin, 1667 createdAt: daysAgo(30), 1668 }) 1669 .run(); 1670 1671 // Team purchase by Bossy McBossface for course 2 (5 seats) 1672 const [teamPurchase] = db 1673 .insert(schema.purchases) 1674 .values({ 1675 userId: bossy.id, 1676 courseId: course2.id, 1677 pricePaid: 5999 * 5, 1678 country: "US", 1679 createdAt: daysAgo(30), 1680 }) 1681 .returning() 1682 .all(); 1683 1684 // Generate 5 coupons for the team purchase 1685 const couponCodes = [ 1686 "TEAM-NODEJS-A1B2C3", 1687 "TEAM-NODEJS-D4E5F6", 1688 "TEAM-NODEJS-G7H8I9", 1689 "TEAM-NODEJS-J0K1L2", 1690 "TEAM-NODEJS-M3N4O5", 1691 ]; 1692 1693 const seededCoupons = db 1694 .insert(schema.coupons) 1695 .values( 1696 couponCodes.map((code) => ({ 1697 teamId: team1.id, 1698 courseId: course2.id, 1699 code, 1700 purchaseId: teamPurchase.id, 1701 createdAt: daysAgo(30), 1702 })) 1703 ) 1704 .returning() 1705 .all(); 1706 1707 // Redeem 2 coupons: Olivia (students[2]) and Liam (students[3]) 1708 // Olivia already has an enrollment for course 2 from the enrollments section above 1709 db.update(schema.coupons) 1710 .set({ 1711 redeemedByUserId: students[2].id, 1712 redeemedAt: daysAgo(30), 1713 }) 1714 .where(eq(schema.coupons.id, seededCoupons[0].id)) 1715 .run(); 1716 1717 // Liam already has an enrollment for course 2 from the enrollments section above 1718 db.update(schema.coupons) 1719 .set({ 1720 redeemedByUserId: students[3].id, 1721 redeemedAt: daysAgo(25), 1722 }) 1723 .where(eq(schema.coupons.id, seededCoupons[1].id)) 1724 .run(); 1725 1726 console.log( 1727 `Created 1 team with Bossy McBossface as admin, 1 team purchase, and ${seededCoupons.length} coupons (2 redeemed, 3 available).` 1728 ); 1729 1730 console.log("\n✓ Seed complete!"); 1731 console.log(" Users: 9 (1 admin, 2 instructors, 6 students)"); 1732 console.log(" Categories: 5"); 1733 console.log( 1734 ` Courses: 2 (${course1LessonIds.length} + ${course2LessonIds.length} lessons)` 1735 ); 1736 console.log(" Quizzes: 3"); 1737 console.log(" Enrollments: 7"); 1738 console.log(" Purchases: 6 (5 individual + 1 team)"); 1739 console.log(" Teams: 1 (with 5 coupons)"); 1740 } 1741 1742 seed().catch(console.error);