const { query } = require('../config/db'); /** * Получить канал со всеми связанными настройками (style + schedule). */ async function getFullChannel(channelId, userId = null) { const filter = userId ? `AND user_id=$2` : ''; const params = userId ? [channelId, userId] : [channelId]; const { rows } = await query( `SELECT c.*, to_jsonb(s.*) - 'channel_id' - 'updated_at' AS style, to_jsonb(sch.*) - 'channel_id' - 'updated_at' AS schedule FROM channels c LEFT JOIN channel_style s ON s.channel_id = c.id LEFT JOIN channel_schedule sch ON sch.channel_id = c.id WHERE c.id=$1 ${filter}`, params ); return rows[0] || null; } async function listChannels(userId) { const { rows } = await query( `SELECT c.*, to_jsonb(s.*) - 'channel_id' - 'updated_at' AS style, to_jsonb(sch.*) - 'channel_id' - 'updated_at' AS schedule FROM channels c LEFT JOIN channel_style s ON s.channel_id = c.id LEFT JOIN channel_schedule sch ON sch.channel_id = c.id WHERE c.user_id=$1 ORDER BY c.created_at DESC`, [userId] ); return rows; } /** * Создать канал — заполняет 3 таблицы транзакционно. */ async function createChannel(userId, data) { const { name, tg_channel_id, tg_username, bot_token, niche, audience, goal, language, region, style = {}, schedule = {}, } = data; if (!name) throw new Error('name is required'); const client = await require('../config/db').query; // INSERT channel const { rows: chRows } = await query( `INSERT INTO channels (user_id, name, tg_channel_id, tg_username, bot_token, niche, audience, goal, language, region) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`, [ userId, name, tg_channel_id || null, tg_username || null, bot_token || null, niche || null, audience || null, goal || 'educational', language || 'ru', region || 'ru', ] ); const channel = chRows[0]; // INSERT style await query( `INSERT INTO channel_style (channel_id, tone, tone_custom, formality, humor, post_length, structure, emoji_level, hashtags_mode, cta_mode, example_posts, banned_words, banned_topics, expertise) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`, [ channel.id, style.tone || 'friendly', style.tone_custom || null, style.formality || 'informal', style.humor || 'moderate', style.post_length || 'medium', style.structure || 'mixed', style.emoji_level || 'moderate', style.hashtags_mode || 'end', style.cta_mode || 'sometimes', JSON.stringify(style.example_posts || []), JSON.stringify(style.banned_words || []), JSON.stringify(style.banned_topics || []), JSON.stringify(style.expertise || []), ] ); // INSERT schedule await query( `INSERT INTO channel_schedule (channel_id, posts_per_day, time_slots, timezone, rubrics, sources, auto_publish) VALUES ($1,$2,$3,$4,$5,$6,$7)`, [ channel.id, schedule.posts_per_day || 1, JSON.stringify(schedule.time_slots || []), schedule.timezone || 'Europe/Moscow', JSON.stringify(schedule.rubrics || []), JSON.stringify(schedule.sources || []), schedule.auto_publish || false, ] ); return getFullChannel(channel.id); } /** * Обновить канал (только то что передано). */ async function updateChannel(channelId, userId, data) { const { style, schedule, ...channelFields } = data; // обновить channels if (Object.keys(channelFields).length) { const fields = ['name', 'tg_channel_id', 'tg_username', 'bot_token', 'niche', 'audience', 'goal', 'language', 'region', 'is_active', 'vk_access_token', 'ai_style_prompt', 'image_quality', 'auto_draft_enabled', 'auto_draft_count', 'auto_draft_time']; const updates = fields.filter(f => channelFields[f] !== undefined); if (updates.length) { const setClauses = updates.map((f, i) => `${f}=$${i + 1}`).join(', '); const values = updates.map(f => channelFields[f]); values.push(channelId, userId); await query( `UPDATE channels SET ${setClauses}, updated_at=NOW() WHERE id=$${values.length - 1} AND user_id=$${values.length}`, values ); } } // обновить style if (style && Object.keys(style).length) { const fields = ['tone', 'tone_custom', 'formality', 'humor', 'post_length', 'structure', 'emoji_level', 'hashtags_mode', 'cta_mode', 'example_posts', 'banned_words', 'banned_topics', 'expertise', 'image_enabled', 'image_style', 'image_palette', 'image_custom_colors', 'image_prompt_instructions']; const updates = fields.filter(f => style[f] !== undefined); if (updates.length) { const setClauses = updates.map((f, i) => { const isJson = ['example_posts', 'banned_words', 'banned_topics', 'expertise'].includes(f); return `${f}=$${i + 1}${isJson ? '::jsonb' : ''}`; }).join(', '); const values = updates.map(f => { const isJson = ['example_posts', 'banned_words', 'banned_topics', 'expertise'].includes(f); return isJson ? JSON.stringify(style[f]) : style[f]; }); values.push(channelId); await query( `UPDATE channel_style SET ${setClauses}, updated_at=NOW() WHERE channel_id=$${values.length}`, values ); } } // обновить schedule if (schedule && Object.keys(schedule).length) { const fields = ['posts_per_day', 'time_slots', 'timezone', 'rubrics', 'sources', 'auto_publish']; const updates = fields.filter(f => schedule[f] !== undefined); if (updates.length) { const setClauses = updates.map((f, i) => { const isJson = ['time_slots', 'rubrics', 'sources'].includes(f); return `${f}=$${i + 1}${isJson ? '::jsonb' : ''}`; }).join(', '); const values = updates.map(f => { const isJson = ['time_slots', 'rubrics', 'sources'].includes(f); return isJson ? JSON.stringify(schedule[f]) : schedule[f]; }); values.push(channelId); await query( `UPDATE channel_schedule SET ${setClauses}, updated_at=NOW() WHERE channel_id=$${values.length}`, values ); } } return getFullChannel(channelId, userId); } async function deleteChannel(channelId, userId) { await query(`DELETE FROM channels WHERE id=$1 AND user_id=$2`, [channelId, userId]); return { ok: true }; } module.exports = { getChannel: getFullChannel, // алиас для обратной совместимости getFullChannel, listChannels, createChannel, updateChannel, deleteChannel, };