733 lines
27 KiB
JavaScript
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
|