/ scripts / seed.ts
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);