(SP: 3) [Database][Backend] Blog DB schema + Sanity → PostgreSQL data migration#390
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📝 WalkthroughWalkthroughThe 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
AbortControllerwith 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
📒 Files selected for processing (5)
frontend/db/schema/blog.tsfrontend/db/seed-blog-migration.tsfrontend/drizzle/0028_bitter_wrecking_crew.sqlfrontend/drizzle/meta/0028_snapshot.jsonfrontend/drizzle/meta/_journal.json
| 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(); |
There was a problem hiding this comment.
🧩 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 -10Repository: DevLoversTeam/devlovers.net
Length of output: 132
🏁 Script executed:
# Look at the schema definition
grep -n "socialMedia\|social_media" frontend/db/schema.ts -B2 -A2Repository: DevLoversTeam/devlovers.net
Length of output: 125
🏁 Script executed:
# Check how other jsonb columns are inserted in the codebase
rg "jsonb\(" frontend/db/schema -A3Repository: 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 -100Repository: 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 -A2Repository: 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 -A1Repository: 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 -A1Repository: 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 -A3Repository: 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 -100Repository: 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.tsRepository: 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 -5Repository: DevLoversTeam/devlovers.net
Length of output: 118
🏁 Script executed:
# Check the Drizzle config and DB index
cat frontend/db/index.tsRepository: 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.tsRepository: 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 -40Repository: 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.
| 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.
| await db | ||
| .insert(blogPostTranslations) | ||
| .values({ | ||
| postId: row.id, | ||
| locale, | ||
| title, | ||
| body: body ? JSON.stringify(body) : null, | ||
| }) | ||
| .onConflictDoNothing(); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
Closes #384, #385
Summary
blog_posts.author_idandblog_post_categories.category_idfor query performanceTest plan
res.cloudinary.comURLs (verified in DBeaver)Summary by CodeRabbit
New Features
Performance