Skip to content

(SP: 3) [Database][Backend] Blog DB schema + Sanity → PostgreSQL data migration#390

Merged
ViktorSvertoka merged 4 commits into
developfrom
sl/feat/blog-admin
Mar 4, 2026
Merged

(SP: 3) [Database][Backend] Blog DB schema + Sanity → PostgreSQL data migration#390
ViktorSvertoka merged 4 commits into
developfrom
sl/feat/blog-admin

Conversation

@LesiaUKR
Copy link
Copy Markdown
Collaborator

@LesiaUKR LesiaUKR commented Mar 4, 2026

Closes #384, #385

Summary

  • Adds FK indexes on blog_posts.author_id and blog_post_categories.category_id for query performance
  • One-time migration script transfers all Sanity blog content to PostgreSQL, re-uploads images from Sanity CDN to Cloudinary
  • Portable Text → Tiptap JSON conversion for all 3 locales (uk/en/pl)
  • 4 categories, 3 authors, 21 posts migrated successfully

Test plan

  • Run migration script — 21 posts inserted, 0 skipped
  • All images load from res.cloudinary.com URLs (verified in DBeaver)
  • Spot-check posts: title/body in all 3 locales present and correct
  • Category translations present (4 categories × 3 locales = 12 rows)
  • Delete migration script after PR merge

Summary by CodeRabbit

  • New Features

    • Blog content migration framework implemented to facilitate seamless content synchronization and management
    • Infrastructure additions support multi-language blog content and metadata handling
  • Performance

    • Database indices added for blog posts and categories to optimize query performance
    • Schema improvements enhance system efficiency for blog operations

LesiaUKR added 4 commits March 3, 2026 22:40
…ript

- Add FK indexes on blog_posts.author_id and blog_post_categories.category_id
- Add one-time migration script: fetches Sanity data via REST API,
  re-uploads images to Cloudinary, converts Portable Text → Tiptap JSON,
  inserts into 7 blog tables (4 categories, 3 authors, 21 posts)
- Drizzle migration 0028 for index changes

Closes #384, #385
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
devlovers-net Ignored Ignored Mar 4, 2026 10:26am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

The PR adds database indices to optimize blog post lookups by author and category, and introduces a Sanity-to-PostgreSQL migration script that transfers blog content (categories, authors, posts, translations) with Cloudinary image uploads and Tiptap JSON body conversion.

Changes

Cohort / File(s) Summary
Database Schema & Indices
frontend/db/schema/blog.ts, frontend/drizzle/0028_bitter_wrecking_crew.sql, frontend/drizzle/meta/_journal.json
Adds btree indices on blog_posts.author_id and blog_post_categories.category_id for query optimization; updates migration tracking metadata.
Data Migration Script
frontend/db/seed-blog-migration.ts
Introduces one-time Sanity CMS to PostgreSQL migration script with idempotent inserts, Cloudinary image uploads, and Tiptap JSON body conversion for UK/EN/PL locales.

Sequence Diagram

sequenceDiagram
    participant MigrationScript as Migration Script
    participant SanityAPI as Sanity REST API
    participant Transformer as Text/Image Transformer
    participant Cloudinary as Cloudinary API
    participant Database as PostgreSQL

    MigrationScript->>SanityAPI: Fetch categories
    SanityAPI-->>MigrationScript: Category data
    MigrationScript->>Database: Insert categories + translations

    MigrationScript->>SanityAPI: Fetch authors
    SanityAPI-->>MigrationScript: Author data + images
    MigrationScript->>Cloudinary: Upload author images
    Cloudinary-->>MigrationScript: Image URL + PublicId
    MigrationScript->>Database: Insert authors + translations + image refs

    MigrationScript->>SanityAPI: Fetch posts + content
    SanityAPI-->>MigrationScript: Post data (3 locales)
    MigrationScript->>Cloudinary: Upload post main images
    Cloudinary-->>MigrationScript: Image URL + PublicId
    MigrationScript->>Transformer: Convert PortableText to Tiptap JSON
    Transformer-->>MigrationScript: Tiptap bodies (uk/en/pl)
    MigrationScript->>Database: Insert posts + translations + categories junction
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 From Sanity's halls to Postgres we leap,
With images soaring through Cloudinary's deep,
Indices bloom like carrots so fine,
Locales and bodies in Tiptap align,
The blog schema hops toward its design! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main changes: adding blog database schema and a one-time Sanity-to-PostgreSQL data migration.
Linked Issues check ✅ Passed The PR successfully implements all schema requirements from issue #384 (7 tables, composite PKs, relations exports, drizzle migration) and includes a comprehensive data migration script addressing issue #385, with verified test results showing 21 posts migrated across 3 locales.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issues: blog schema definition, drizzle migrations, and the sanity-to-postgresql migration script with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sl/feat/blog-admin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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: 2

🧹 Nitpick comments (1)
frontend/db/seed-blog-migration.ts (1)

76-86: Consider adding a timeout to prevent indefinite hangs.

The fetch calls lack a timeout. If the Sanity API becomes unresponsive, the migration script could hang indefinitely. For a one-time migration script this is acceptable, but adding an AbortController with a timeout would make the script more robust.

💡 Optional: Add timeout to fetch
 async function sanityFetch<T>(query: string): Promise<T> {
   const url = `${SANITY_API}?query=${encodeURIComponent(query)}`;
-  const res = await fetch(url);
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 30000);
+  const res = await fetch(url, { signal: controller.signal });
+  clearTimeout(timeoutId);
 
   if (!res.ok) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/db/seed-blog-migration.ts` around lines 76 - 86, The sanityFetch<T>
function can hang because fetch has no timeout; update sanityFetch to create an
AbortController, set a short timeout (e.g. configurable constant like
SANITY_FETCH_TIMEOUT) that calls controller.abort(), pass controller.signal into
fetch(url, { signal }), clear the timeout on success, and throw a clear error
when aborted so callers know the request timed out; ensure the function still
returns json.result as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/db/seed-blog-migration.ts`:
- Around line 579-588: The migration is double-serializing the `body` JSONB
column by calling JSON.stringify before passing it to Drizzle; update the
`db.insert(blogPostTranslations).values(...)` call so the `body` value is passed
as a plain object (or null) instead of a string (e.g., use `body` directly or
`body ?? null`), leaving Drizzle to handle JSON serialization for the schema's
`jsonb('body')` field.
- Around line 407-421: The insert is JSON-double-serializing socialMedia by
calling JSON.stringify before inserting into the jsonb column; update the
blogAuthors insert to pass the plain JS value (socialMedia) instead of
JSON.stringify(socialMedia) so Drizzle/Postgres can handle serialization (look
for the socialMedia variable and the db.insert(blogAuthors).values(...) block);
also audit similar patterns like the body field in blogPostTranslations and
remove JSON.stringify there as well so jsonb columns receive native
objects/arrays rather than stringified JSON.

---

Nitpick comments:
In `@frontend/db/seed-blog-migration.ts`:
- Around line 76-86: The sanityFetch<T> function can hang because fetch has no
timeout; update sanityFetch to create an AbortController, set a short timeout
(e.g. configurable constant like SANITY_FETCH_TIMEOUT) that calls
controller.abort(), pass controller.signal into fetch(url, { signal }), clear
the timeout on success, and throw a clear error when aborted so callers know the
request timed out; ensure the function still returns json.result as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 96eaac99-0fd7-4ad2-9dfa-6bd54cdec5b5

📥 Commits

Reviewing files that changed from the base of the PR and between 635499e and 97fd677.

📒 Files selected for processing (5)
  • frontend/db/schema/blog.ts
  • frontend/db/seed-blog-migration.ts
  • frontend/drizzle/0028_bitter_wrecking_crew.sql
  • frontend/drizzle/meta/0028_snapshot.json
  • frontend/drizzle/meta/_journal.json

Comment on lines +407 to +421
const socialMedia = (author.socialMedia || [])
.filter(s => s.platform && s.url)
.map(s => ({ platform: s.platform!, url: s.url! }));

const [row] = await db
.insert(blogAuthors)
.values({
slug,
imageUrl,
imagePublicId,
socialMedia: JSON.stringify(socialMedia),
displayOrder: 0,
})
.onConflictDoNothing()
.returning();
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

🧩 Analysis chain

🏁 Script executed:

# Find and examine the schema file for blogAuthors table
find frontend/db -name "*.ts" -type f | xargs grep -l "blogAuthors\|social_media" | head -10

Repository: DevLoversTeam/devlovers.net

Length of output: 132


🏁 Script executed:

# Look at the schema definition
grep -n "socialMedia\|social_media" frontend/db/schema.ts -B2 -A2

Repository: DevLoversTeam/devlovers.net

Length of output: 125


🏁 Script executed:

# Check how other jsonb columns are inserted in the codebase
rg "jsonb\(" frontend/db/schema -A3

Repository: DevLoversTeam/devlovers.net

Length of output: 4333


🏁 Script executed:

# Search for other insert operations to jsonb columns
rg "\.values\(" frontend/db/seed-blog-migration.ts -B5 -A5 | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 1933


🏁 Script executed:

# Search for how tags (which is also jsonb) is used to confirm pattern
rg "tags:" frontend/db/schema/blog.ts -B2 -A2

Repository: DevLoversTeam/devlovers.net

Length of output: 318


🏁 Script executed:

# Look for all JSON.stringify usages in seed file to see if there's a pattern
rg "JSON.stringify" frontend/db/seed-blog-migration.ts -B3 -A1

Repository: DevLoversTeam/devlovers.net

Length of output: 327


🏁 Script executed:

# Check the body column definition in blogPostTranslations schema
rg "body:" frontend/db/schema/blog.ts -B1 -A1

Repository: DevLoversTeam/devlovers.net

Length of output: 136


🏁 Script executed:

# Check if there's a type annotation on the jsonb columns to understand expected format
rg "socialMedia:|body:" frontend/db/schema/blog.ts -A3

Repository: DevLoversTeam/devlovers.net

Length of output: 379


🏁 Script executed:

# Check the actual Drizzle configuration and types for these jsonb columns
cat frontend/db/schema/blog.ts | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 3129


🏁 Script executed:

# Look for $type annotations on jsonb columns that might indicate expected format
rg "\$type" frontend/db/schema/blog.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

# Check the actual Drizzle ORM setup/config to understand the environment
find frontend -name "*.ts" -type f | xargs grep -l "drizzle\|db\." | grep -E "config|setup|index" | head -5

Repository: DevLoversTeam/devlovers.net

Length of output: 118


🏁 Script executed:

# Check the Drizzle config and DB index
cat frontend/db/index.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 2161


🏁 Script executed:

# Look for any documentation or examples of how Drizzle ORM handles jsonb in this project
cat frontend/drizzle.config.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 282


🏁 Script executed:

# Search for how data is being read/queried to see what format is expected
rg "\.select\(\)" frontend/db/seed-blog-migration.ts -A5 | head -40

Repository: DevLoversTeam/devlovers.net

Length of output: 53


Remove JSON.stringify() on the socialMedia JSONB column.

The socialMedia field is defined as jsonb('social_media') with a default value of [] (not '[]'). This indicates the schema expects a JavaScript object/array, not a stringified value. When using Drizzle ORM with PostgreSQL, jsonb columns handle serialization internally—passing JSON.stringify(socialMedia) causes the data to be stored as a string literal (e.g., "[{...}]") instead of a JSON object (e.g., [{...}]), resulting in double-serialization when retrieved and queried.

Note: The body field in blogPostTranslations has the same pattern; consider reviewing that as well for consistency.

🔧 Suggested fix
     const [row] = await db
       .insert(blogAuthors)
       .values({
         slug,
         imageUrl,
         imagePublicId,
-        socialMedia: JSON.stringify(socialMedia),
+        socialMedia,
         displayOrder: 0,
       })
📝 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 socialMedia = (author.socialMedia || [])
.filter(s => s.platform && s.url)
.map(s => ({ platform: s.platform!, url: s.url! }));
const [row] = await db
.insert(blogAuthors)
.values({
slug,
imageUrl,
imagePublicId,
socialMedia: JSON.stringify(socialMedia),
displayOrder: 0,
})
.onConflictDoNothing()
.returning();
const socialMedia = (author.socialMedia || [])
.filter(s => s.platform && s.url)
.map(s => ({ platform: s.platform!, url: s.url! }));
const [row] = await db
.insert(blogAuthors)
.values({
slug,
imageUrl,
imagePublicId,
socialMedia,
displayOrder: 0,
})
.onConflictDoNothing()
.returning();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/db/seed-blog-migration.ts` around lines 407 - 421, The insert is
JSON-double-serializing socialMedia by calling JSON.stringify before inserting
into the jsonb column; update the blogAuthors insert to pass the plain JS value
(socialMedia) instead of JSON.stringify(socialMedia) so Drizzle/Postgres can
handle serialization (look for the socialMedia variable and the
db.insert(blogAuthors).values(...) block); also audit similar patterns like the
body field in blogPostTranslations and remove JSON.stringify there as well so
jsonb columns receive native objects/arrays rather than stringified JSON.

Comment on lines +579 to +588
await db
.insert(blogPostTranslations)
.values({
postId: row.id,
locale,
title,
body: body ? JSON.stringify(body) : null,
})
.onConflictDoNothing();
}
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

Same potential double-serialization issue with body JSONB column.

Similar to the socialMedia field, the body column is defined as jsonb('body') in the schema. Drizzle ORM should handle the serialization automatically.

🔧 Suggested fix
       await db
         .insert(blogPostTranslations)
         .values({
           postId: row.id,
           locale,
           title,
-          body: body ? JSON.stringify(body) : null,
+          body,
         })
         .onConflictDoNothing();
📝 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
await db
.insert(blogPostTranslations)
.values({
postId: row.id,
locale,
title,
body: body ? JSON.stringify(body) : null,
})
.onConflictDoNothing();
}
await db
.insert(blogPostTranslations)
.values({
postId: row.id,
locale,
title,
body,
})
.onConflictDoNothing();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/db/seed-blog-migration.ts` around lines 579 - 588, The migration is
double-serializing the `body` JSONB column by calling JSON.stringify before
passing it to Drizzle; update the `db.insert(blogPostTranslations).values(...)`
call so the `body` value is passed as a plain object (or null) instead of a
string (e.g., use `body` directly or `body ?? null`), leaving Drizzle to handle
JSON serialization for the schema's `jsonb('body')` field.

@ViktorSvertoka ViktorSvertoka merged commit efa85ef into develop Mar 4, 2026
8 checks passed
@ViktorSvertoka ViktorSvertoka deleted the sl/feat/blog-admin branch March 4, 2026 22:14
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.

2 participants