Skip to content

feat(q&a): localized questions, new schema and seeding#71

Merged
ViktorSvertoka merged 2 commits into
developfrom
feat/locale-questions
Dec 21, 2025
Merged

feat(q&a): localized questions, new schema and seeding#71
ViktorSvertoka merged 2 commits into
developfrom
feat/locale-questions

Conversation

@ViktorSvertoka
Copy link
Copy Markdown
Member

@ViktorSvertoka ViktorSvertoka commented Dec 21, 2025

Q&A Localization Support

Overview

This update introduces full localization support for Q&A content and improves the database structure for questions and categories.

Key Changes

Database & Schema

  • Extracted questions into a dedicated questions.ts schema
  • Migrated questions and categories to uuid identifiers
  • Added slug and locale fields for proper localization
  • Renamed nametitle for clearer semantics
  • Updated Drizzle ORM schema and generated migrations

Seeding & Data

  • Renamed seed script to seed-questions.ts
  • Updated package.json command:
    "seed:questions": "tsx db/seed-questions.ts"
  • Implemented localized seeding for:
    • categories (en, pl, uk)
    • questions with translated content
  • Added reusable slugify utility for consistent slugs

API & Frontend

  • Updated questions API to filter by category + locale
  • Fixed i18n routing and locale handling in proxy.ts
  • Updated TabsSection.tsx to:
    • read locale from URL
    • refetch questions on language change
    • prevent duplicated locale paths (e.g. /uk/uk/q&a)

Testing Instructions

  1. Run database seeds:

    npm run seed:categories
    npm run seed:questions
  2. Open Q&A page in different locales:

    • /uk/q&a
    • /en/q&a
    • /pl/q&a
  3. Switch language using the language toggle

  4. Verify that questions update correctly per locale

Results

  • Q&A content is fully localized
  • Clean and scalable questions schema
  • Stable i18n routing without duplicated locales
  • Ready for adding more languages in the future

Summary by CodeRabbit

  • New Features

    • Added search functionality to the Q&A section for finding questions.
    • Enabled multilingual support with locale-aware API calls and routing.
  • Improvements

    • Enhanced pagination with increased default result limit to 20 items (max 50).
    • Improved error handling and resilience for API requests.
    • Updated search input styling and positioning for better accessibility.
    • Optimized loading states and empty-state visuals.

✏️ Tip: You can customize this high-level summary in your review settings.

@netlify
Copy link
Copy Markdown

netlify Bot commented Dec 21, 2025

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit 9749470
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/694817a01ed0120008f35133
😎 Deploy Preview https://deploy-preview-71--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 21, 2025

Walkthrough

This PR refactors the question and category system to support localization and introduces a new database schema. Changes include: locale-aware API routes with pagination and search, modernized schema with UUID keys and composite constraints, restructured seed scripts separating category and question seeding, middleware updates to propagate locale headers, and component updates to handle locale-driven requests.

Changes

Cohort / File(s) Summary
Database Schema – Frontend (drizzle)
frontend/db/schema/categories.ts, frontend/db/schema/questions.ts, frontend/db/schema/index.ts
Replaces relational schema with locale-aware tables; categories use slug + locale composite key; new questions table with categorySlug, locale, and sortOrder; exports consolidated into index.
Database Schema – Migration
frontend/drizzle/schema.ts
Complete schema restructure with UUID primary keys; updates categories and questions definitions; introduces new translation tables (quizTranslations, quizAnswerTranslations, quizQuestionContent); adds new entities (quizzes, quizAttempts, quizAttemptAnswers); adds preferredLocale to user.
Seed Scripts
frontend/db/seed-categories.ts, frontend/db/seed-questions.ts, frontend/db/seed.ts, frontend/package.json
Replaces monolithic seed.ts with split seedCategories and seedQuestions; adds multilingual seed support with LOCALES; changes seed script from generic "seed" to "seed:questions"; implements batched inserts with onConflictDoNothing.
API Route Handler
frontend/app/api/questions/[category]/route.ts
Adds locale-aware filtering via query param or x-locale header; increases DEFAULT_LIMIT to 20; introduces search functionality with ilike; reworks pagination with bounded limit; replaces id-based ordering with sortOrder; removes category table dependency.
Frontend Component
frontend/components/q&a/TabsSection.tsx
Integrates useParams for locale reading; switches router.push to router.replace; adds mountedRef guard for debounce; includes locale in API fetch; resets pagination/search on category change; refactors search handlers inline.
Utilities & Middleware
frontend/proxy.ts, frontend/utils/slugify.ts
Adds slugify utility for URL-friendly string conversion; updates proxy locale parsing with stricter regex; defaults locale to 'uk'; propagates x-locale header in proxy flow.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Proxy
    participant APIRoute as API Route<br/>[category]
    participant DB as Database

    Client->>Proxy: GET /uk/q&a?category=...&search=...
    Proxy->>Proxy: Extract locale (uk)<br/>or default to 'uk'
    Proxy->>APIRoute: Forward + x-locale header
    
    APIRoute->>APIRoute: Read locale from<br/>query or header
    
    rect rgb(240, 250, 255)
        note right of APIRoute: Locale-aware Query
        APIRoute->>APIRoute: Build WHERE:<br/>categorySlug + locale
        APIRoute->>APIRoute: Apply search filter<br/>(ilike if present)
        APIRoute->>APIRoute: Order by sortOrder
    end
    
    APIRoute->>DB: SELECT count, rows<br/>WHERE categorySlug = slug<br/>AND locale = locale
    DB->>APIRoute: Return filtered results
    
    APIRoute->>APIRoute: Compute pagination<br/>(page, limit, total)
    APIRoute->>Client: { results, locale,<br/>page, totalPages }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • frontend/drizzle/schema.ts – Extensive table restructuring with new entities, translation tables, and foreign key relationships; requires careful verification of constraints and migrations
  • frontend/app/api/questions/[category]/route.ts – Logic density increased with locale handling, search filtering, and new pagination; verify error cases and boundary conditions
  • frontend/components/q&a/TabsSection.tsx – Multiple state/effect dependencies refactored; validate debounce logic, mounting guard, and locale reactivity
  • frontend/db/schema/ changes – Composite unique constraints and new table definitions; ensure alignment with drizzle schema
  • Cross-file consistency – Locale defaults ('uk'), schema alignment, and seed script logic flow require holistic verification

Possibly related PRs

  • PR #66 – Modifies frontend/proxy.ts with locale parsing and header propagation logic
  • PR #67 – Updates category seeding and data flow logic
  • PR #70 – Schema refactoring for categories/questions with locale-aware design

Suggested reviewers

  • AM1007

🐰 A schema took flight with locale in sight,
UUID keys gleaming, categories bright,
Questions now sorted with slugs and with grace,
Searching and seeding—each question in place!
The proxy now whispers of languages new,
Hopping through data for me and for you!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely captures the main changes: localization of questions, schema restructuring, and seeding implementation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/locale-questions

Comment @coderabbitai help to get the list of available commands and usage tips.

@ViktorSvertoka ViktorSvertoka merged commit 4bd9869 into develop Dec 21, 2025
3 of 4 checks passed
@ViktorSvertoka ViktorSvertoka deleted the feat/locale-questions branch December 21, 2025 15:52
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (8)
frontend/db/seed-questions.ts (2)

6-16: Consider stronger typing for answerBlocks to prevent data integrity issues.

The answerBlocks field is typed as unknown, which bypasses validation of the JSON structure being inserted into the database. This could lead to runtime errors if the data doesn't match the expected format used by the frontend.

🔎 Proposed type definition
+type AnswerBlock = {
+  // Define the expected structure based on frontend usage
+  // Example structure (adjust as needed):
+  type: 'text' | 'code' | 'list';
+  content: string;
+  language?: string;
+};
+
 type RawQuestion = {
   category: string;
   order?: number;
   translations: Record<
     string,
     {
       question: string;
-      answerBlocks: unknown;
+      answerBlocks: AnswerBlock[];
     }
   >;
 };

26-34: Consider adding validation for locale keys and required fields.

The script assumes all translations contain valid locale keys and required fields. Missing validation could lead to incomplete or malformed data in the database.

🔎 Proposed validation
+const VALID_LOCALES = ['en', 'pl', 'uk'] as const;
+
 const rows = data.flatMap(q =>
   Object.entries(q.translations).map(([locale, content]) => {
+    if (!VALID_LOCALES.includes(locale as any)) {
+      console.warn(`Skipping invalid locale "${locale}" for category "${q.category}"`);
+      return null;
+    }
+    if (!content.question || !content.answerBlocks) {
+      console.warn(`Missing required fields for locale "${locale}" in category "${q.category}"`);
+      return null;
+    }
     return {
       categorySlug: q.category,
       locale,
       sortOrder: q.order ?? 0,
       question: content.question,
       answerBlocks: content.answerBlocks,
     };
   })
- );
+ ).filter(Boolean);
frontend/db/seed-categories.ts (1)

7-7: Consider centralizing the LOCALES constant to avoid duplication.

The LOCALES constant is defined here and likely in other seed scripts (e.g., seed-questions.ts validation). Centralizing it in a shared config file would improve maintainability.

🔎 Proposed centralization

Create a new file frontend/config/locales.ts:

export const LOCALES = ['en', 'pl', 'uk'] as const;
export type Locale = typeof LOCALES[number];

Then import and use it in seed scripts:

+import { LOCALES } from '../config/locales';
-const LOCALES = ['en', 'pl', 'uk'] as const;
frontend/app/api/questions/[category]/route.ts (1)

37-39: Consider performance implications of ilike queries on large datasets.

The ilike operator performs case-insensitive pattern matching, which can be slow on large tables without proper indexing. Consider adding a database index on the question column or implementing full-text search for better performance.

Recommended database optimization:

  1. Add a GIN index for pattern matching:

    CREATE INDEX idx_questions_question_gin ON questions USING gin(question gin_trgm_ops);

    (Requires the pg_trgm extension)

  2. Alternatively, implement PostgreSQL full-text search:

    ALTER TABLE questions ADD COLUMN question_tsv tsvector;
    CREATE INDEX idx_questions_fts ON questions USING gin(question_tsv);
  3. Or limit search to the already-filtered result set by adding pagination before search filtering (trade-off: less accurate results).

frontend/utils/slugify.ts (1)

1-8: Implement consecutive hyphen deduplication, leading/trailing hyphen removal, and consider Unicode normalization for category names.

The current implementation produces incorrect slugs with leading or trailing hyphens when input starts or ends with special characters (e.g., "-test-name" → "-test-name"). The suggested improvements address this:

 export function slugify(value: string) {
   return value
     .toLowerCase()
     .trim()
     .replace(/\s+/g, '-')
     .replace(/\./g, '')
-    .replace(/[^a-z0-9-]/g, '');
+    .replace(/[^a-z0-9-]/g, '')
+    .replace(/-+/g, '-')
+    .replace(/^-+|-+$/g, '');
 }

For better Unicode handling (e.g., "Café" or non-Latin category names), consider using a dedicated library like slugify from npm.

frontend/components/q&a/TabsSection.tsx (2)

94-99: Missing response status check before parsing JSON.

If the API returns an error status (4xx/5xx), calling res.json() may throw or return unexpected data. Check res.ok before parsing.

🔎 Proposed fix
         const res = await fetch(
           `/api/questions/${active}?page=${currentPage}&limit=10&locale=${locale}${searchParam}`
         );
 
+        if (!res.ok) {
+          throw new Error(`HTTP ${res.status}`);
+        }
+
         const data: PaginatedResponse = await res.json();

167-170: Loading spinner may be invisible due to missing border color.

border-b-2 without a color class relies on the default border color, which may not be visible against certain backgrounds.

🔎 Proposed fix
             <div className="flex justify-center py-12">
-                <div className="animate-spin h-8 w-8 border-b-2" />
+                <div className="animate-spin h-8 w-8 border-b-2 border-gray-600 rounded-full" />
             </div>
frontend/drizzle/schema.ts (1)

201-211: Consider adding unique constraint on email.

The email column is notNull but lacks a unique constraint, which could allow duplicate user accounts. If email should be unique per user, add a unique constraint.

🔎 Proposed fix
 export const user = pgTable('user', {
   id: text('id').primaryKey().notNull(),
   name: text(),
-  email: text().notNull(),
+  email: text().notNull().unique(),
   emailVerified: timestamp('email_verified', { mode: 'string' }),
   image: text(),
   role: text().default('user'),
   points: integer().default(0),
   preferredLocale: varchar('preferred_locale', { length: 5 }).default('en'),
   createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
 });
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50c11d4 and fd81b0e.

📒 Files selected for processing (12)
  • frontend/app/api/questions/[category]/route.ts (1 hunks)
  • frontend/components/q&a/TabsSection.tsx (6 hunks)
  • frontend/db/schema/categories.ts (1 hunks)
  • frontend/db/schema/index.ts (1 hunks)
  • frontend/db/schema/questions.ts (1 hunks)
  • frontend/db/seed-categories.ts (1 hunks)
  • frontend/db/seed-questions.ts (1 hunks)
  • frontend/db/seed.ts (0 hunks)
  • frontend/drizzle/schema.ts (1 hunks)
  • frontend/package.json (1 hunks)
  • frontend/proxy.ts (4 hunks)
  • frontend/utils/slugify.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • frontend/db/seed.ts
🧰 Additional context used
🧬 Code graph analysis (6)
frontend/db/seed-questions.ts (2)
frontend/db/index.ts (1)
  • db (17-17)
frontend/db/schema/questions.ts (1)
  • questions (11-28)
frontend/db/schema/questions.ts (1)
frontend/drizzle/schema.ts (1)
  • questions (32-47)
frontend/db/schema/categories.ts (2)
frontend/db/seed.ts (1)
  • main (19-50)
frontend/drizzle/relations.ts (1)
  • category (4-9)
frontend/app/api/questions/[category]/route.ts (1)
frontend/db/schema/questions.ts (1)
  • questions (11-28)
frontend/db/seed-categories.ts (5)
frontend/data/category.ts (1)
  • categoryNames (1-9)
frontend/utils/slugify.ts (1)
  • slugify (1-8)
frontend/db/index.ts (1)
  • db (17-17)
frontend/db/schema/categories.ts (1)
  • categories (3-14)
frontend/drizzle/schema.ts (1)
  • categories (19-30)
frontend/drizzle/schema.ts (2)
frontend/db/schema/categories.ts (1)
  • categories (3-14)
frontend/db/schema/questions.ts (1)
  • questions (11-28)
🔇 Additional comments (6)
frontend/proxy.ts (1)

22-23: LGTM! Locale handling is now consistent and robust.

The updated regex pattern with lookahead (?=\/|$) correctly strips locale prefixes only when they appear at the start of the path. The consistent fallback to 'uk' across all extraction points and the new x-locale header propagation enable downstream handlers (like the questions API) to access locale information reliably.

Also applies to: 30-30, 40-40, 50-50, 60-60, 69-69

frontend/db/schema/index.ts (1)

2-2: LGTM! Schema export updated correctly.

The new questions schema is now properly exposed through the index barrel export.

frontend/package.json (1)

11-11: LGTM! Seed script renamed appropriately.

The focused seed:questions script aligns with the new dedicated seeding approach introduced in this PR.

frontend/db/seed-categories.ts (1)

9-23: LGTM! Efficient batched seeding with proper conflict handling.

The batched insert approach with onConflictDoNothing efficiently handles multi-locale category seeding and prevents duplicate entries via the schema's unique constraint on (slug, locale).

frontend/db/schema/categories.ts (1)

3-14: LGTM! Well-designed locale-aware schema.

The UUID-based schema with composite unique constraint on (slug, locale) properly supports multi-locale categories. The varchar lengths (50 for slug, 5 for locale) are appropriate for the use case.

frontend/app/api/questions/[category]/route.ts (1)

14-77: LGTM! Comprehensive error handling and locale-aware pagination.

The try-catch wrapper, default error response with proper status code, and inclusion of locale in responses provide good error resilience. The pagination logic with bounds checking (max 50 items, min 1) prevents abuse.

Comment on lines +25 to +28
const locale =
searchParams.get('locale') ||
req.headers.get('x-locale') ||
DEFAULT_LOCALE;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add validation for locale parameter to prevent invalid values.

The locale is accepted from query parameters or headers without validation. Invalid locale values could lead to unexpected behavior or empty result sets.

🔎 Proposed locale validation
+const VALID_LOCALES = ['en', 'pl', 'uk'] as const;
+
 const locale =
   searchParams.get('locale') ||
   req.headers.get('x-locale') ||
   DEFAULT_LOCALE;
+
+if (!VALID_LOCALES.includes(locale as any)) {
+  return NextResponse.json(
+    { error: `Invalid locale: ${locale}` },
+    { status: 400 }
+  );
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const locale =
searchParams.get('locale') ||
req.headers.get('x-locale') ||
DEFAULT_LOCALE;
const VALID_LOCALES = ['en', 'pl', 'uk'] as const;
const locale =
searchParams.get('locale') ||
req.headers.get('x-locale') ||
DEFAULT_LOCALE;
if (!VALID_LOCALES.includes(locale as any)) {
return NextResponse.json(
{ error: `Invalid locale: ${locale}` },
{ status: 400 }
);
}
🤖 Prompt for AI Agents
In frontend/app/api/questions/[category]/route.ts around lines 25 to 28, the
locale pulled from searchParams or x-locale header is used without validation;
validate the resolved locale against your allowed list (e.g. SUPPORTED_LOCALES
or a constant array including DEFAULT_LOCALE) and if the value is not in that
list, replace it with DEFAULT_LOCALE (or return a 400 Bad Request if you prefer
strict validation); implement the check immediately after computing locale and
ensure downstream code only sees a validated locale value.

Comment on lines +30 to +39
const search = searchParams.get('search')?.trim();

const baseCondition = and(
eq(questions.categorySlug, category.toLowerCase()),
eq(questions.locale, locale)
);

const baseCondition = eq(questions.categoryId, cat[0].id);
const whereCondition = search
? and(baseCondition, ilike(questions.question, `%${search}%`))
: baseCondition;
const whereCondition = search
? and(baseCondition, ilike(questions.question, `%${search}%`))
: baseCondition;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Escape SQL wildcard characters in the search parameter.

Drizzle ORM's ilike() function uses parameterized queries, which properly protects against SQL injection. However, user input containing % or _ characters will be interpreted as LIKE wildcards rather than literals, causing unexpected search behavior. Sanitize the search input by escaping these characters:

-const search = searchParams.get('search')?.trim();
+const search = searchParams.get('search')?.trim().replace(/[%_]/g, '\\$&');

This ensures that literal % and _ characters in user input are treated as text, not pattern wildcards.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
frontend/app/api/questions/[category]/route.ts lines 30-39: the search string
can contain '%' or '_' which LIKE/ILIKE treat as wildcards; before building the
ilike pattern, escape backslashes, percent and underscore in the trimmed search
input by prefixing them with a backslash (e.g., replace \ with \\ then % with \%
and _ with \_), then use that escaped value inside your `%${escaped}%` ilike
pattern so user-supplied literal % and _ are treated as text rather than
wildcards.

Comment on lines +32 to +35
const baseCondition = and(
eq(questions.categorySlug, category.toLowerCase()),
eq(questions.locale, locale)
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Validate and sanitize the category slug to prevent injection and errors.

The category parameter from the URL is lowercased but not validated. This could allow malicious or malformed input to reach the database query.

🔎 Proposed category validation
+// Add at the top of the function
+const categorySlugPattern = /^[a-z0-9-]+$/;
+
 const { category } = await ctx.params;
+
+if (!categorySlugPattern.test(category.toLowerCase())) {
+  return NextResponse.json(
+    { error: 'Invalid category format' },
+    { status: 400 }
+  );
+}
+
 const { searchParams } = new URL(req.url);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
frontend/app/api/questions/[category]/route.ts around lines 32 to 35: the
category slug from the URL is only lowercased and not validated, which can allow
malformed or malicious input into the DB query; validate and sanitize it before
use by decoding and trimming the input, enforce a strict pattern/length (e.g.,
allow only lowercase letters, numbers, hyphens, max length ~100) or similar
project-specific slug regex, reject invalid slugs with a 400 response (or
normalize acceptable characters), and only then use the sanitizedLowercasedSlug
in the query.

Comment on lines +28 to +30
const params = useParams();

const locale = params.locale as string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unsafe type assertion for locale parameter.

useParams() returns Params where values can be string | string[] | undefined. The direct cast as string bypasses type checking and could lead to runtime issues if locale is missing or malformed.

🔎 Proposed fix with validation
   const params = useParams();
-
-  const locale = params.locale as string;
+  const locale = typeof params.locale === 'string' ? params.locale : 'en';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const params = useParams();
const locale = params.locale as string;
const params = useParams();
const locale = typeof params.locale === 'string' ? params.locale : 'en';
🤖 Prompt for AI Agents
In frontend/components/q&a/TabsSection.tsx around lines 28 to 30, the code
unsafely asserts locale with `as string`; instead validate the value returned
from useParams() (it can be string | string[] | undefined) by checking typeof
params.locale === 'string' and that it is not empty before using it, handle
string[] by selecting a sensible element (e.g., first) or rejecting it, and
provide a safe fallback (default locale or early return/error handling) so no
direct type assertion is used.

Comment on lines +11 to +28
export const questions = pgTable(
'questions',
{
id: uuid('id').defaultRandom().primaryKey(),
categorySlug: varchar('category_slug', { length: 50 }).notNull(),
locale: varchar('locale', { length: 5 }).notNull(),
question: text('question').notNull(),
answerBlocks: jsonb('answer_blocks').notNull(),
sortOrder: integer('sort_order').default(0),
},
table => [
unique('questions_category_locale_order_unique').on(
table.categorySlug,
table.locale,
table.sortOrder
),
]
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Schema inconsistency: Two different questions table definitions exist.

This schema uses categorySlug (varchar) without a foreign key, while frontend/drizzle/schema.ts lines 32-47 defines a different questions table using categoryId (uuid) with a foreign key to categories. This creates ambiguity about which schema is authoritative and could cause migration/query issues.

Additionally, consider:

  1. Adding a foreign key constraint to categories(slug, locale) for referential integrity
  2. Making sortOrder explicitly .notNull() since it's part of a unique constraint—null values can bypass uniqueness checks
🔎 Proposed fix to add foreign key and make sortOrder notNull
+import { categories } from './categories';
+import { foreignKey } from 'drizzle-orm/pg-core';
+
 export const questions = pgTable(
   'questions',
   {
     id: uuid('id').defaultRandom().primaryKey(),
     categorySlug: varchar('category_slug', { length: 50 }).notNull(),
     locale: varchar('locale', { length: 5 }).notNull(),
     question: text('question').notNull(),
     answerBlocks: jsonb('answer_blocks').notNull(),
-    sortOrder: integer('sort_order').default(0),
+    sortOrder: integer('sort_order').default(0).notNull(),
   },
   table => [
     unique('questions_category_locale_order_unique').on(
       table.categorySlug,
       table.locale,
       table.sortOrder
     ),
+    foreignKey({
+      columns: [table.categorySlug, table.locale],
+      foreignColumns: [categories.slug, categories.locale],
+      name: 'questions_category_slug_locale_fk',
+    }).onDelete('cascade'),
   ]
 );

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/db/schema/questions.ts around lines 11 to 28 there is a conflicting
schema vs frontend/drizzle/schema.ts: this file uses categorySlug (varchar)
while the other file uses categoryId (uuid) with a FK to categories, and
sortOrder is nullable despite being part of a unique constraint; reconcile the
two by choosing one authoritative design (prefer using categoryId uuid
referencing categories.id for consistency with the other schema) or, if you must
keep categorySlug, add a proper foreign key to categories (categories.slug +
categories.locale) to enforce referential integrity, and make sortOrder
.notNull() so the unique index cannot be bypassed by NULLs; update the table
definition to match the chosen approach and remove the duplicate/ambiguous
schema elsewhere so both files agree.

Comment on lines +32 to +47
export const questions = pgTable(
'questions',
{
id: uuid('id').defaultRandom().primaryKey().notNull(),
question: text('question').notNull(),
answerBlocks: jsonb('answer_blocks').notNull(),
categoryId: uuid('category_id').notNull(),
},
table => [
foreignKey({
columns: [table.categoryId],
foreignColumns: [categories.id],
name: 'questions_category_id_categories_id_fk',
}).onDelete('cascade'),
]
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Explore file structure to locate the schema files
echo "=== Looking for schema files ==="
fd -type f -name "schema.ts" | head -20

echo -e "\n=== Looking for questions schema files ==="
fd -type f -path "*questions*" -name "*.ts" | head -20

echo -e "\n=== Checking frontend/drizzle/schema.ts exists ==="
test -f "frontend/drizzle/schema.ts" && echo "EXISTS" || echo "NOT FOUND"

echo -e "\n=== Checking frontend/db/schema/questions.ts exists ==="
test -f "frontend/db/schema/questions.ts" && echo "EXISTS" || echo "NOT FOUND"

Repository: DevLoversTeam/devlovers.net

Length of output: 629


🏁 Script executed:

#!/bin/bash
# Read both schema files to compare
echo "=== frontend/drizzle/schema.ts (lines around questions) ==="
cat -n frontend/drizzle/schema.ts | head -100

echo -e "\n=== frontend/db/schema/questions.ts ==="
cat -n frontend/db/schema/questions.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 4379


🏁 Script executed:

#!/bin/bash
# Check which schema files are imported/used
echo "=== Imports of drizzle schema.ts ==="
rg -l "from.*drizzle/schema" frontend --type ts

echo -e "\n=== Imports of db/schema/questions ==="
rg -l "from.*db/schema/questions" frontend --type ts

echo -e "\n=== All imports of 'questions' table ==="
rg "import.*questions" frontend --type ts -A 1

echo -e "\n=== Drizzle migrations folder ==="
ls -la frontend/drizzle/migrations 2>/dev/null | head -20 || echo "Migrations folder not found or empty"

echo -e "\n=== Check drizzle.config.ts ==="
cat frontend/drizzle.config.ts 2>/dev/null || cat drizzle.config.ts 2>/dev/null || echo "Config not found"

Repository: DevLoversTeam/devlovers.net

Length of output: 1165


🏁 Script executed:

#!/bin/bash
# Check db/schema/index.ts to see authoritative schema
echo "=== frontend/db/schema/index.ts ==="
cat -n frontend/db/schema/index.ts

echo -e "\n=== Check if frontend/drizzle/schema.ts is actually used anywhere ==="
rg "from.*frontend/drizzle/schema\|from ['\"].*drizzle/schema" frontend --type ts

echo -e "\n=== Check migrations in drizzle folder ==="
find frontend/drizzle -name "*.sql" 2>/dev/null | head -5

echo -e "\n=== Check drizzle folder structure ==="
ls -la frontend/drizzle/ 2>/dev/null

Repository: DevLoversTeam/devlovers.net

Length of output: 1393


🏁 Script executed:

#!/bin/bash
# Check the migrations to see which schema they match
echo "=== First migration (questions table) ==="
head -50 frontend/drizzle/0000_fair_quasar.sql

echo -e "\n=== Check if migrations match categoryId or categorySlug ==="
grep -n "category" frontend/drizzle/*.sql

echo -e "\n=== Verify which API is actually being used ==="
cat -n frontend/app/api/questions/\[category\]/route.ts | head -40

Repository: DevLoversTeam/devlovers.net

Length of output: 4389


Remove duplicate questions table definition from frontend/drizzle/schema.ts.

This file contains a conflicting questions schema that differs fundamentally from the authoritative definition in frontend/db/schema/questions.ts. The drizzle.config.ts explicitly configures migrations to use ./db/schema/index.ts, making frontend/db/schema/questions.ts the source of truth. The API route frontend/app/api/questions/[category]/route.ts imports and queries using the correct schema (with categorySlug, locale, and sortOrder columns), not the conflicting definition in frontend/drizzle/schema.ts. Remove the duplicate schema or reconcile it with the actual authoritative version.

Comment on lines +58 to +62
index('quiz_answers_question_display_order_idx').using(
'btree',
table.quizQuestionId.asc().nullsLast().op('int4_ops'),
table.displayOrder.asc().nullsLast().op('int4_ops')
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect operator class for UUID column in index.

quizQuestionId is a uuid column but uses int4_ops. This should be uuid_ops to match the column type.

🔎 Proposed fix
     index('quiz_answers_question_display_order_idx').using(
       'btree',
-      table.quizQuestionId.asc().nullsLast().op('int4_ops'),
+      table.quizQuestionId.asc().nullsLast().op('uuid_ops'),
       table.displayOrder.asc().nullsLast().op('int4_ops')
     ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
index('quiz_answers_question_display_order_idx').using(
'btree',
table.quizQuestionId.asc().nullsLast().op('int4_ops'),
table.displayOrder.asc().nullsLast().op('int4_ops')
),
index('quiz_answers_question_display_order_idx').using(
'btree',
table.quizQuestionId.asc().nullsLast().op('uuid_ops'),
table.displayOrder.asc().nullsLast().op('int4_ops')
),
🤖 Prompt for AI Agents
In frontend/drizzle/schema.ts around lines 58 to 62, the index definition uses
the wrong operator class for the uuid column quizQuestionId (int4_ops); update
the index to use uuid_ops for quizQuestionId while keeping displayOrder as
int4_ops (or remove explicit op for displayOrder if unnecessary), i.e., change
the operator class on the quizQuestionId entry to uuid_ops so the index matches
the column types.

Comment on lines +84 to +88
index('quiz_questions_quiz_display_order_idx').using(
'btree',
table.quizId.asc().nullsLast().op('int4_ops'),
table.displayOrder.asc().nullsLast().op('int4_ops')
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect operator class for UUID column in index.

quizId is a uuid column but uses int4_ops. Should be uuid_ops.

🔎 Proposed fix
     index('quiz_questions_quiz_display_order_idx').using(
       'btree',
-      table.quizId.asc().nullsLast().op('int4_ops'),
+      table.quizId.asc().nullsLast().op('uuid_ops'),
       table.displayOrder.asc().nullsLast().op('int4_ops')
     ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
index('quiz_questions_quiz_display_order_idx').using(
'btree',
table.quizId.asc().nullsLast().op('int4_ops'),
table.displayOrder.asc().nullsLast().op('int4_ops')
),
index('quiz_questions_quiz_display_order_idx').using(
'btree',
table.quizId.asc().nullsLast().op('uuid_ops'),
table.displayOrder.asc().nullsLast().op('int4_ops')
),
🤖 Prompt for AI Agents
In frontend/drizzle/schema.ts around lines 84 to 88, the index definition uses
the wrong operator class for the uuid column quizId (currently int4_ops); change
the operator class for table.quizId to 'uuid_ops' while keeping
table.displayOrder's operator class (int4_ops) intact so the index becomes
quizId.asc().nullsLast().op('uuid_ops') and
displayOrder.asc().nullsLast().op('int4_ops').

Comment on lines +182 to +192
index('quiz_attempts_quiz_percentage_completed_at_idx').using(
'btree',
table.quizId.asc().nullsLast().op('numeric_ops'),
table.percentage.asc().nullsLast().op('numeric_ops'),
table.completedAt.asc().nullsLast().op('uuid_ops')
),
index('quiz_attempts_user_completed_at_idx').using(
'btree',
table.userId.asc().nullsLast().op('text_ops'),
table.completedAt.asc().nullsLast().op('text_ops')
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Multiple incorrect operator classes in index definition.

  • Line 184: quizId (uuid) uses numeric_ops — should be uuid_ops
  • Line 186: completedAt (timestamp) uses uuid_ops — should be timestamptz_ops or omitted (btree default works)
🔎 Proposed fix
     index('quiz_attempts_quiz_percentage_completed_at_idx').using(
       'btree',
-      table.quizId.asc().nullsLast().op('numeric_ops'),
+      table.quizId.asc().nullsLast().op('uuid_ops'),
       table.percentage.asc().nullsLast().op('numeric_ops'),
-      table.completedAt.asc().nullsLast().op('uuid_ops')
+      table.completedAt.asc().nullsLast().op('timestamptz_ops')
     ),
🤖 Prompt for AI Agents
In frontend/drizzle/schema.ts around lines 182 to 192, the index operator
classes are incorrect: change table.quizId.op('numeric_ops') to
table.quizId.op('uuid_ops'), change table.completedAt.op('uuid_ops') to
table.completedAt.op('timestamptz_ops') (or remove the op to use btree default)
in the first index, and in the second index change
table.completedAt.op('text_ops') to table.completedAt.op('timestamptz_ops') (or
remove the op) while leaving table.userId.op('text_ops') as-is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant