BlogEditor/backend/routes/posts.js

733 lines
27 KiB
JavaScript

import express from 'express'
import { pool } from '../config/database.js'
import slugify from 'slugify'
import logger from '../utils/logger.js'
const router = express.Router()
const MAX_EXTERNAL_URL_LENGTH = 2048
function isValidExternalUrl(url) {
if (typeof url !== 'string') return false
const trimmed = url.trim()
if (!trimmed || trimmed.length > MAX_EXTERNAL_URL_LENGTH) return false
return trimmed.startsWith('http://') || trimmed.startsWith('https://')
}
// Get all posts for current user
// Note: authenticateToken middleware is applied at server level, so req.user is available
router.get('/', async (req, res) => {
try {
logger.transaction('FETCH_POSTS', { userId: req.user.id })
const query = 'SELECT id, title, slug, status, content_type, external_url, thumbnail_url, excerpt, language, post_group_id, created_at, updated_at FROM posts WHERE user_id = $1 ORDER BY updated_at DESC'
logger.db('SELECT', query, [req.user.id])
const result = await pool.query(query, [req.user.id])
logger.transaction('FETCH_POSTS_SUCCESS', {
userId: req.user.id,
count: result.rows.length
})
res.json(result.rows)
} catch (error) {
logger.error('POSTS', 'Error fetching posts', error)
res.status(500).json({ message: 'Failed to fetch posts', error: error.message })
}
})
// Get single post by ID
router.get('/:id', async (req, res) => {
try {
logger.transaction('FETCH_POST_BY_ID', {
postId: req.params.id,
userId: req.user.id
})
const query = 'SELECT * FROM posts WHERE id = $1 AND user_id = $2'
logger.db('SELECT', query, [req.params.id, req.user.id])
const result = await pool.query(query, [req.params.id, req.user.id])
if (result.rows.length === 0) {
logger.warn('POSTS', 'Post not found', {
postId: req.params.id,
userId: req.user.id
})
return res.status(404).json({ message: 'Post not found' })
}
logger.transaction('FETCH_POST_BY_ID_SUCCESS', {
postId: req.params.id,
userId: req.user.id
})
res.json(result.rows[0])
} catch (error) {
logger.error('POSTS', 'Error fetching post', error)
res.status(500).json({ message: 'Failed to fetch post', error: error.message })
}
})
// Get post by slug (public)
router.get('/slug/:slug', async (req, res) => {
try {
logger.transaction('FETCH_POST_BY_SLUG', { slug: req.params.slug })
const query = 'SELECT * FROM posts WHERE slug = $1 AND status = $2'
logger.db('SELECT', query, [req.params.slug, 'published'])
const result = await pool.query(query, [req.params.slug, 'published'])
if (result.rows.length === 0) {
logger.warn('POSTS', 'Post not found by slug', { slug: req.params.slug })
return res.status(404).json({ message: 'Post not found' })
}
logger.transaction('FETCH_POST_BY_SLUG_SUCCESS', {
slug: req.params.slug,
postId: result.rows[0].id
})
res.json(result.rows[0])
} catch (error) {
logger.error('POSTS', 'Error fetching post by slug', error)
res.status(500).json({ message: 'Failed to fetch post', error: error.message })
}
})
// Create post
router.post('/', async (req, res) => {
try {
const { title, content_json, content_type, external_url, status, thumbnail_url, excerpt, language } = req.body
logger.transaction('CREATE_POST', {
userId: req.user.id,
title: title?.substring(0, 50),
status: status || 'draft',
content_type: content_type || 'tiptap',
language: language || 'en'
})
const isLinkPost = content_type === 'link'
if (isLinkPost) {
if (!title || typeof title !== 'string' || !title.trim()) {
return res.status(400).json({ message: 'Title is required' })
}
if (!external_url || !isValidExternalUrl(external_url)) {
return res.status(400).json({ message: 'Valid external URL is required (http:// or https://, max 2048 characters)' })
}
} else {
if (!title || !content_json) {
logger.warn('POSTS', 'Missing required fields', {
hasTitle: !!title,
hasContent: !!content_json
})
return res.status(400).json({ message: 'Title and content are required' })
}
}
const slug = slugify(title, { lower: true, strict: true }) + '-' + Date.now()
const postStatus = status || 'draft'
const contentType = isLinkPost ? 'link' : (content_type || 'tiptap')
const contentJson = isLinkPost ? {} : content_json
const externalUrl = isLinkPost ? external_url.trim() : null
const postLanguage = language || 'en'
const thumbnailUrl = thumbnail_url && typeof thumbnail_url === 'string' ? thumbnail_url.trim() || null : null
const excerptVal = excerpt != null && typeof excerpt === 'string' ? excerpt.trim().slice(0, 500) || null : null
if (postStatus === 'published') {
if (!thumbnailUrl) {
return res.status(400).json({ message: 'Thumbnail is required to publish. Add a post thumbnail first.' })
}
if (!excerptVal) {
return res.status(400).json({ message: 'List description (excerpt) is required to publish.' })
}
}
const query = `INSERT INTO posts (user_id, title, content_json, slug, status, content_type, external_url, thumbnail_url, excerpt, language)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *`
logger.db('INSERT', query, [req.user.id, title, '[content_json]', slug, postStatus, contentType, externalUrl, thumbnailUrl, excerptVal, postLanguage])
const result = await pool.query(query, [
req.user.id,
title,
JSON.stringify(contentJson),
slug,
postStatus,
contentType,
externalUrl,
thumbnailUrl,
excerptVal,
postLanguage
])
logger.transaction('CREATE_POST_SUCCESS', {
postId: result.rows[0].id,
userId: req.user.id,
slug: result.rows[0].slug,
language: postLanguage
})
res.status(201).json(result.rows[0])
} catch (error) {
logger.error('POSTS', 'Error creating post', error)
res.status(500).json({ message: 'Failed to create post', error: error.message })
}
})
// Update post
router.put('/:id', async (req, res) => {
try {
const { title, content_json, content_type, external_url, status, thumbnail_url, excerpt, language } = req.body
logger.transaction('UPDATE_POST', {
postId: req.params.id,
userId: req.user.id,
updates: {
title: title !== undefined,
content: content_json !== undefined,
status: status !== undefined,
content_type: content_type !== undefined,
external_url: external_url !== undefined,
thumbnail_url: thumbnail_url !== undefined,
excerpt: excerpt !== undefined,
language: language !== undefined
}
})
// Check if post exists and belongs to user (fetch thumbnail/excerpt when publishing)
const checkQuery = status === 'published'
? 'SELECT id, thumbnail_url, excerpt FROM posts WHERE id = $1 AND user_id = $2'
: 'SELECT id FROM posts WHERE id = $1 AND user_id = $2'
logger.db('SELECT', checkQuery, [req.params.id, req.user.id])
const existingResult = await pool.query(checkQuery, [req.params.id, req.user.id])
const existingPost = existingResult.rows[0]
if (!existingPost) {
logger.warn('POSTS', 'Post not found for update', {
postId: req.params.id,
userId: req.user.id
})
return res.status(404).json({ message: 'Post not found' })
}
const isLinkUpdate = content_type === 'link'
if (external_url !== undefined && isLinkUpdate && !isValidExternalUrl(external_url)) {
return res.status(400).json({ message: 'Valid external URL is required (http:// or https://, max 2048 characters)' })
}
if (content_type === 'link' && external_url === undefined) {
return res.status(400).json({ message: 'external_url is required when content_type is link' })
}
// When publishing, require thumbnail and excerpt (use existing if not in body)
if (status === 'published') {
const finalThumbnail = thumbnail_url !== undefined
? (thumbnail_url && typeof thumbnail_url === 'string' ? thumbnail_url.trim() || null : null)
: (existingPost.thumbnail_url ?? null)
const finalExcerpt = excerpt !== undefined
? (excerpt != null && typeof excerpt === 'string' ? excerpt.trim().slice(0, 500) || null : null)
: (existingPost.excerpt ?? null)
if (!finalThumbnail) {
return res.status(400).json({ message: 'Thumbnail is required to publish. Add a post thumbnail first.' })
}
if (!finalExcerpt) {
return res.status(400).json({ message: 'List description (excerpt) is required to publish.' })
}
}
// Build update query dynamically
const updates = []
const values = []
let paramCount = 1
if (title !== undefined) {
updates.push(`title = $${paramCount++}`)
values.push(title)
}
if (content_json !== undefined) {
updates.push(`content_json = $${paramCount++}`)
values.push(JSON.stringify(content_json))
}
if (status !== undefined) {
updates.push(`status = $${paramCount++}`)
values.push(status)
}
if (content_type !== undefined) {
updates.push(`content_type = $${paramCount++}`)
values.push(content_type)
}
// When content_type is set to non-link, clear external_url; when link, set URL
if (content_type !== undefined) {
updates.push(`external_url = $${paramCount++}`)
values.push(content_type === 'link' && external_url !== undefined ? external_url.trim() : null)
} else if (external_url !== undefined) {
updates.push(`external_url = $${paramCount++}`)
values.push(external_url.trim())
}
// Update slug if title changed
if (title !== undefined) {
const slug = slugify(title, { lower: true, strict: true }) + '-' + Date.now()
updates.push(`slug = $${paramCount++}`)
values.push(slug)
}
if (thumbnail_url !== undefined) {
updates.push(`thumbnail_url = $${paramCount++}`)
values.push(thumbnail_url && typeof thumbnail_url === 'string' ? thumbnail_url.trim() || null : null)
}
if (excerpt !== undefined) {
updates.push(`excerpt = $${paramCount++}`)
values.push(excerpt != null && typeof excerpt === 'string' ? excerpt.trim().slice(0, 500) || null : null)
}
if (language !== undefined) {
updates.push(`language = $${paramCount++}`)
values.push(language || 'en')
}
updates.push(`updated_at = NOW()`)
values.push(req.params.id, req.user.id)
const updateQuery = `UPDATE posts SET ${updates.join(', ')} WHERE id = $${paramCount} AND user_id = $${paramCount + 1} RETURNING *`
logger.db('UPDATE', updateQuery, values)
const result = await pool.query(updateQuery, values)
logger.transaction('UPDATE_POST_SUCCESS', {
postId: req.params.id,
userId: req.user.id
})
res.json(result.rows[0])
} catch (error) {
logger.error('POSTS', 'Error updating post', error)
res.status(500).json({ message: 'Failed to update post', error: error.message })
}
})
// Delete post
router.delete('/:id', async (req, res) => {
try {
logger.transaction('DELETE_POST', {
postId: req.params.id,
userId: req.user.id
})
const query = 'DELETE FROM posts WHERE id = $1 AND user_id = $2 RETURNING id'
logger.db('DELETE', query, [req.params.id, req.user.id])
const result = await pool.query(query, [req.params.id, req.user.id])
if (result.rows.length === 0) {
logger.warn('POSTS', 'Post not found for deletion', {
postId: req.params.id,
userId: req.user.id
})
return res.status(404).json({ message: 'Post not found' })
}
logger.transaction('DELETE_POST_SUCCESS', {
postId: req.params.id,
userId: req.user.id
})
res.json({ message: 'Post deleted successfully' })
} catch (error) {
logger.error('POSTS', 'Error deleting post', error)
res.status(500).json({ message: 'Failed to delete post', error: error.message })
}
})
// Get post group by post_group_id (returns both EN and HI versions)
router.get('/group/:post_group_id', async (req, res) => {
try {
logger.transaction('FETCH_POST_GROUP', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
const query = 'SELECT * FROM posts WHERE post_group_id = $1 AND user_id = $2 ORDER BY language'
logger.db('SELECT', query, [req.params.post_group_id, req.user.id])
const result = await pool.query(query, [req.params.post_group_id, req.user.id])
if (result.rows.length === 0) {
logger.warn('POSTS', 'Post group not found', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
return res.status(404).json({ message: 'Post group not found' })
}
// Organize by language
const posts = {
post_group_id: req.params.post_group_id,
english: result.rows.find(p => p.language === 'en') || null,
hindi: result.rows.find(p => p.language === 'hi') || null
}
logger.transaction('FETCH_POST_GROUP_SUCCESS', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
res.json(posts)
} catch (error) {
logger.error('POSTS', 'Error fetching post group', error)
res.status(500).json({ message: 'Failed to fetch post group', error: error.message })
}
})
// Create dual-language post (creates both EN and HI posts together)
router.post('/dual-language', async (req, res) => {
try {
const {
// English fields
title_en, excerpt_en, content_json_en, external_url_en, status_en,
// Hindi fields
title_hi, excerpt_hi, content_json_hi, external_url_hi, status_hi,
// Shared fields
content_type, thumbnail_url, status
} = req.body
// Support both old format (status) and new format (status_en/status_hi)
const enStatus = status_en || status || 'draft'
const hiStatus = status_hi || status || 'draft'
logger.transaction('CREATE_DUAL_LANGUAGE_POST', {
userId: req.user.id,
status_en: enStatus,
status_hi: hiStatus,
content_type: content_type || 'tiptap'
})
const isLinkPost = content_type === 'link'
// Validation for publishing English
if (enStatus === 'published') {
if (!title_en?.trim() || !excerpt_en?.trim()) {
return res.status(400).json({ message: 'English title and excerpt required to publish' })
}
if (isLinkPost && !external_url_en?.trim()) {
return res.status(400).json({ message: 'English URL required for link posts' })
}
if (isLinkPost && !isValidExternalUrl(external_url_en)) {
return res.status(400).json({ message: 'Valid English external URL is required' })
}
if (!isLinkPost && !content_json_en) {
return res.status(400).json({ message: 'English content required' })
}
if (!thumbnail_url?.trim()) {
return res.status(400).json({ message: 'Thumbnail required to publish' })
}
}
// Validation for publishing Hindi
if (hiStatus === 'published') {
if (!title_hi?.trim() || !excerpt_hi?.trim()) {
return res.status(400).json({ message: 'Hindi title and excerpt required to publish' })
}
if (isLinkPost && !external_url_hi?.trim()) {
return res.status(400).json({ message: 'Hindi URL required for link posts' })
}
if (isLinkPost && !isValidExternalUrl(external_url_hi)) {
return res.status(400).json({ message: 'Valid Hindi external URL is required' })
}
if (!isLinkPost && !content_json_hi) {
return res.status(400).json({ message: 'Hindi content required' })
}
if (!thumbnail_url?.trim()) {
return res.status(400).json({ message: 'Thumbnail required to publish' })
}
}
// Generate shared post_group_id using crypto
const { randomUUID } = await import('crypto')
const postGroupId = randomUUID()
const contentType = isLinkPost ? 'link' : (content_type || 'tiptap')
const thumbnailUrlVal = thumbnail_url && typeof thumbnail_url === 'string' ? thumbnail_url.trim() || null : null
// Create English post
const slugEn = slugify(title_en || 'untitled', { lower: true, strict: true }) + '-' + Date.now()
const excerptEnVal = excerpt_en != null && typeof excerpt_en === 'string' ? excerpt_en.trim().slice(0, 500) || null : null
const contentEnVal = isLinkPost ? {} : (content_json_en || {})
const externalUrlEnVal = isLinkPost && external_url_en ? external_url_en.trim() : null
const enQuery = `INSERT INTO posts (user_id, title, content_json, slug, status, content_type, external_url, thumbnail_url, excerpt, language, post_group_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *`
logger.db('INSERT', enQuery, [req.user.id, title_en, '[content_json]', slugEn, enStatus, contentType, externalUrlEnVal, thumbnailUrlVal, excerptEnVal, 'en', postGroupId])
const enResult = await pool.query(enQuery, [
req.user.id,
title_en || 'Untitled',
JSON.stringify(contentEnVal),
slugEn,
enStatus,
contentType,
externalUrlEnVal,
thumbnailUrlVal,
excerptEnVal,
'en',
postGroupId
])
// Create Hindi post
const slugHi = slugify(title_hi || 'untitled', { lower: true, strict: true }) + '-' + Date.now() + '-hi'
const excerptHiVal = excerpt_hi != null && typeof excerpt_hi === 'string' ? excerpt_hi.trim().slice(0, 500) || null : null
const contentHiVal = isLinkPost ? {} : (content_json_hi || {})
const externalUrlHiVal = isLinkPost && external_url_hi ? external_url_hi.trim() : null
const hiQuery = `INSERT INTO posts (user_id, title, content_json, slug, status, content_type, external_url, thumbnail_url, excerpt, language, post_group_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *`
logger.db('INSERT', hiQuery, [req.user.id, title_hi, '[content_json]', slugHi, hiStatus, contentType, externalUrlHiVal, thumbnailUrlVal, excerptHiVal, 'hi', postGroupId])
const hiResult = await pool.query(hiQuery, [
req.user.id,
title_hi || 'शीर्षकहीन',
JSON.stringify(contentHiVal),
slugHi,
hiStatus,
contentType,
externalUrlHiVal,
thumbnailUrlVal,
excerptHiVal,
'hi',
postGroupId
])
logger.transaction('CREATE_DUAL_LANGUAGE_POST_SUCCESS', {
postGroupId: postGroupId,
userId: req.user.id,
englishId: enResult.rows[0].id,
hindiId: hiResult.rows[0].id
})
res.status(201).json({
post_group_id: postGroupId,
english: enResult.rows[0],
hindi: hiResult.rows[0]
})
} catch (error) {
logger.error('POSTS', 'Error creating dual-language post', error)
res.status(500).json({ message: 'Failed to create posts', error: error.message })
}
})
// Update dual-language post (updates both EN and HI posts)
router.put('/dual-language/:post_group_id', async (req, res) => {
try {
const {
// English fields
title_en, excerpt_en, content_json_en, external_url_en, status_en,
// Hindi fields
title_hi, excerpt_hi, content_json_hi, external_url_hi, status_hi,
// Shared fields
content_type, thumbnail_url, status
} = req.body
// Support both old format (status) and new format (status_en/status_hi)
const enStatus = status_en !== undefined ? status_en : status
const hiStatus = status_hi !== undefined ? status_hi : status
logger.transaction('UPDATE_DUAL_LANGUAGE_POST', {
postGroupId: req.params.post_group_id,
userId: req.user.id,
status_en: enStatus,
status_hi: hiStatus
})
// Check if posts exist and belong to user
const checkQuery = 'SELECT id, language, thumbnail_url, excerpt, title FROM posts WHERE post_group_id = $1 AND user_id = $2'
logger.db('SELECT', checkQuery, [req.params.post_group_id, req.user.id])
const existingResult = await pool.query(checkQuery, [req.params.post_group_id, req.user.id])
if (existingResult.rows.length === 0) {
logger.warn('POSTS', 'Post group not found for update', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
return res.status(404).json({ message: 'Post group not found' })
}
const existingEn = existingResult.rows.find(p => p.language === 'en')
const existingHi = existingResult.rows.find(p => p.language === 'hi')
const isLinkUpdate = content_type === 'link'
// Validation for publishing English
if (enStatus === 'published') {
const finalTitleEn = title_en !== undefined ? title_en : existingEn?.title
const finalExcerptEn = excerpt_en !== undefined ? excerpt_en : existingEn?.excerpt
if (!finalTitleEn?.trim() || !finalExcerptEn?.trim()) {
return res.status(400).json({ message: 'English title and excerpt required to publish' })
}
if (isLinkUpdate) {
const finalUrlEn = external_url_en !== undefined ? external_url_en : null
if (!finalUrlEn?.trim() || !isValidExternalUrl(finalUrlEn)) {
return res.status(400).json({ message: 'Valid English URL required for link posts' })
}
} else if (content_json_en === undefined && !existingEn?.content_json) {
return res.status(400).json({ message: 'English content required' })
}
const finalThumbnail = thumbnail_url !== undefined ? thumbnail_url : existingEn?.thumbnail_url
if (!finalThumbnail?.trim()) {
return res.status(400).json({ message: 'Thumbnail required to publish' })
}
}
// Validation for publishing Hindi
if (hiStatus === 'published') {
const finalTitleHi = title_hi !== undefined ? title_hi : existingHi?.title
const finalExcerptHi = excerpt_hi !== undefined ? excerpt_hi : existingHi?.excerpt
if (!finalTitleHi?.trim() || !finalExcerptHi?.trim()) {
return res.status(400).json({ message: 'Hindi title and excerpt required to publish' })
}
if (isLinkUpdate) {
const finalUrlHi = external_url_hi !== undefined ? external_url_hi : null
if (!finalUrlHi?.trim() || !isValidExternalUrl(finalUrlHi)) {
return res.status(400).json({ message: 'Valid Hindi URL required for link posts' })
}
} else if (content_json_hi === undefined && !existingHi?.content_json) {
return res.status(400).json({ message: 'Hindi content required' })
}
const finalThumbnail = thumbnail_url !== undefined ? thumbnail_url : existingHi?.thumbnail_url
if (!finalThumbnail?.trim()) {
return res.status(400).json({ message: 'Thumbnail required to publish' })
}
}
// Update English post
if (existingEn) {
const updatesEn = []
const valuesEn = []
let paramCount = 1
if (title_en !== undefined) {
updatesEn.push(`title = $${paramCount++}`)
valuesEn.push(title_en || 'Untitled')
// Update slug if title changed
const slugEn = slugify(title_en || 'untitled', { lower: true, strict: true }) + '-' + Date.now()
updatesEn.push(`slug = $${paramCount++}`)
valuesEn.push(slugEn)
}
if (content_json_en !== undefined) {
updatesEn.push(`content_json = $${paramCount++}`)
valuesEn.push(JSON.stringify(content_json_en))
}
if (enStatus !== undefined) {
updatesEn.push(`status = $${paramCount++}`)
valuesEn.push(enStatus)
}
if (content_type !== undefined) {
updatesEn.push(`content_type = $${paramCount++}`)
valuesEn.push(content_type)
}
if (external_url_en !== undefined) {
updatesEn.push(`external_url = $${paramCount++}`)
valuesEn.push(external_url_en?.trim() || null)
}
if (thumbnail_url !== undefined) {
updatesEn.push(`thumbnail_url = $${paramCount++}`)
valuesEn.push(thumbnail_url?.trim() || null)
}
if (excerpt_en !== undefined) {
updatesEn.push(`excerpt = $${paramCount++}`)
valuesEn.push(excerpt_en?.trim()?.slice(0, 500) || null)
}
if (updatesEn.length > 0) {
updatesEn.push(`updated_at = NOW()`)
valuesEn.push(existingEn.id)
const updateQueryEn = `UPDATE posts SET ${updatesEn.join(', ')} WHERE id = $${paramCount} RETURNING *`
logger.db('UPDATE', updateQueryEn, valuesEn)
await pool.query(updateQueryEn, valuesEn)
}
}
// Update Hindi post
if (existingHi) {
const updatesHi = []
const valuesHi = []
let paramCount = 1
if (title_hi !== undefined) {
updatesHi.push(`title = $${paramCount++}`)
valuesHi.push(title_hi || 'शीर्षकहीन')
// Update slug if title changed
const slugHi = slugify(title_hi || 'untitled', { lower: true, strict: true }) + '-' + Date.now() + '-hi'
updatesHi.push(`slug = $${paramCount++}`)
valuesHi.push(slugHi)
}
if (content_json_hi !== undefined) {
updatesHi.push(`content_json = $${paramCount++}`)
valuesHi.push(JSON.stringify(content_json_hi))
}
if (hiStatus !== undefined) {
updatesHi.push(`status = $${paramCount++}`)
valuesHi.push(hiStatus)
}
if (content_type !== undefined) {
updatesHi.push(`content_type = $${paramCount++}`)
valuesHi.push(content_type)
}
if (external_url_hi !== undefined) {
updatesHi.push(`external_url = $${paramCount++}`)
valuesHi.push(external_url_hi?.trim() || null)
}
if (thumbnail_url !== undefined) {
updatesHi.push(`thumbnail_url = $${paramCount++}`)
valuesHi.push(thumbnail_url?.trim() || null)
}
if (excerpt_hi !== undefined) {
updatesHi.push(`excerpt = $${paramCount++}`)
valuesHi.push(excerpt_hi?.trim()?.slice(0, 500) || null)
}
if (updatesHi.length > 0) {
updatesHi.push(`updated_at = NOW()`)
valuesHi.push(existingHi.id)
const updateQueryHi = `UPDATE posts SET ${updatesHi.join(', ')} WHERE id = $${paramCount} RETURNING *`
logger.db('UPDATE', updateQueryHi, valuesHi)
await pool.query(updateQueryHi, valuesHi)
}
}
// Fetch updated posts
const result = await pool.query('SELECT * FROM posts WHERE post_group_id = $1 AND user_id = $2 ORDER BY language',
[req.params.post_group_id, req.user.id])
logger.transaction('UPDATE_DUAL_LANGUAGE_POST_SUCCESS', {
postGroupId: req.params.post_group_id,
userId: req.user.id
})
res.json({
post_group_id: req.params.post_group_id,
english: result.rows.find(p => p.language === 'en') || null,
hindi: result.rows.find(p => p.language === 'hi') || null
})
} catch (error) {
logger.error('POSTS', 'Error updating dual-language post', error)
res.status(500).json({ message: 'Failed to update posts', error: error.message })
}
})
export default router