codenuk_backend_mine/services/template-manager/src/routes/templates.js
2025-10-06 15:12:49 +05:30

1243 lines
49 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express');
const router = express.Router();
const Template = require('../models/template');
const CustomTemplate = require('../models/custom_template');
const Feature = require('../models/feature');
const CustomFeature = require('../models/custom_feature');
const AdminNotification = require('../models/admin_notification');
const database = require('../config/database');
// GET /api/templates - Get all templates grouped by category
router.get('/', async (req, res) => {
try {
console.log('📂 Fetching all templates by category...');
const templates = await Template.getAllByCategory();
res.json({
success: true,
data: templates,
message: `Found templates in ${Object.keys(templates).length} categories`
});
} catch (error) {
console.error('❌ Error fetching templates:', error.message);
res.status(500).json({
success: false,
error: 'Failed to fetch templates',
message: error.message
});
}
});
// GET /api/templates/stats - Get template statistics
router.get('/stats', async (req, res) => {
try {
console.log('📊 Fetching template statistics...');
const stats = await Template.getStats();
res.json({
success: true,
data: stats,
message: 'Template statistics retrieved successfully'
});
} catch (error) {
console.error('❌ Error fetching template stats:', error.message);
res.status(500).json({
success: false,
error: 'Failed to fetch template statistics',
message: error.message
});
}
});
// GET /api/templates/combined - Built-in templates + current user's custom templates (paginated)
// Query: userId (required for user customs), status (optional for customs), limit, offset
router.get('/combined', async (req, res) => {
try {
const userId = req.query.userId || req.query.userid || req.query.user_id || null;
const limit = parseInt(req.query.limit) || 6;
const offset = parseInt(req.query.offset) || 0;
const status = req.query.status || null; // optional filter for custom templates
// Fetch built-in (admin) templates grouped by category, then flatten
const defaultByCategory = await Template.getAllByCategory();
const adminTemplates = Object.values(defaultByCategory).flat().map(t => ({
id: t.id,
type: t.type,
title: t.title,
description: t.description,
icon: t.icon,
category: t.category,
gradient: t.gradient,
border: t.border,
text: t.text,
subtext: t.subtext,
created_at: t.created_at,
updated_at: t.updated_at,
is_custom: false,
source: 'admin'
}));
// Fetch current user's custom templates (if userId provided), else empty
let userCustomTemplates = [];
if (userId) {
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidV4Regex.test(userId)) {
return res.status(400).json({ success: false, error: 'Invalid userId', message: 'userId must be a valid UUID v4' });
}
const customs = await CustomTemplate.getByUserId(userId, 1000, 0, status);
userCustomTemplates = customs.map(ct => ({
id: ct.id,
type: ct.type,
title: ct.title,
description: ct.description,
icon: ct.icon,
category: ct.category,
gradient: ct.gradient,
border: ct.border,
text: ct.text,
subtext: ct.subtext,
created_at: ct.created_at,
updated_at: ct.updated_at,
is_custom: true,
status: ct.status,
user_id: ct.user_id,
source: 'user'
}));
}
// Combine and sort by created_at desc (fallback title)
const combined = [...adminTemplates, ...userCustomTemplates].sort((a, b) => {
const aTime = a.created_at ? new Date(a.created_at).getTime() : 0;
const bTime = b.created_at ? new Date(b.created_at).getTime() : 0;
if (aTime === bTime) return (a.title || '').localeCompare(b.title || '');
return bTime - aTime;
});
const total = combined.length;
const slice = combined.slice(offset, offset + limit);
const hasMore = offset + slice.length < total;
return res.json({
success: true,
data: slice,
count: slice.length,
pagination: { total, limit, offset, hasMore },
message: `Returned ${slice.length} of ${total} templates (combined admin + user)`
});
} catch (error) {
console.error('❌ Error fetching combined templates:', error.message);
return res.status(500).json({ success: false, error: 'Failed to fetch combined templates', message: error.message });
}
});
// GET /api/templates/merged - Get paginated, filtered templates (default + custom)
router.get('/merged', async (req, res) => {
try {
console.log('🚀 [MERGED-TEMPLATES] Starting template fetch operation...');
console.log('📋 [MERGED-TEMPLATES] Request parameters:', {
limit: req.query.limit || 'default: 10',
offset: req.query.offset || 'default: 0',
category: req.query.category || 'all categories',
search: req.query.search || 'no search query'
});
const limit = parseInt(req.query.limit) || 10;
const offset = parseInt(req.query.offset) || 0;
const categoryFilter = req.query.category || null;
const searchQuery = req.query.search ? req.query.search.toLowerCase() : null;
console.log("req.query __", req.query)
console.log('⚙️ [MERGED-TEMPLATES] Parsed parameters:', { limit, offset, categoryFilter, searchQuery });
// Get all default templates
console.log('🏗️ [MERGED-TEMPLATES] Fetching default templates by category...');
const defaultTemplatesByCat = await Template.getAllByCategory();
console.log('📊 [MERGED-TEMPLATES] Default templates by category structure:', Object.keys(defaultTemplatesByCat));
let defaultTemplates = [];
for (const cat in defaultTemplatesByCat) {
const catTemplates = defaultTemplatesByCat[cat];
console.log(`📁 [MERGED-TEMPLATES] Category "${cat}": ${catTemplates.length} templates`);
defaultTemplates = defaultTemplates.concat(catTemplates);
}
console.log('✅ [MERGED-TEMPLATES] Total default templates collected:', defaultTemplates.length);
console.log('🔍 [MERGED-TEMPLATES] Sample default template:', defaultTemplates[0] ? {
id: defaultTemplates[0].id,
title: defaultTemplates[0].title,
category: defaultTemplates[0].category,
type: defaultTemplates[0].type
} : 'No default templates found');
// Get all custom templates for the current user
console.log('🎨 [MERGED-TEMPLATES] Fetching custom templates...');
console.log('🔍 [MERGED-TEMPLATES] Request userId:', req.query.userId);
console.log('🔍 [MERGED-TEMPLATES] Request includeOthers:', req.query.includeOthers);
let customTemplates = [];
let userOwnCustomCount = 0;
let approvedOthersCustomCount = 0;
if (req.query.userId) {
// Validate UUID v4 for userId to avoid DB errors like "invalid input syntax for type uuid"
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidV4Regex.test(req.query.userId)) {
console.warn('⚠️ [MERGED-TEMPLATES] Invalid userId provided:', req.query.userId);
// Don't return error, just skip user-specific templates and continue with approved ones
console.log('⚠️ [MERGED-TEMPLATES] Continuing with approved templates only due to invalid userId');
customTemplates = await CustomTemplate.getTemplatesByStatus('approved');
approvedOthersCustomCount = customTemplates.length;
console.log('📈 [MERGED-TEMPLATES] Approved custom templates (invalid userId fallback):', approvedOthersCustomCount);
} else {
// Get ALL custom templates for this user (all statuses - approved, pending, rejected, etc.)
console.log('✅ [MERGED-TEMPLATES] Valid userId provided, fetching ALL user templates...');
customTemplates = await CustomTemplate.getByUserId(req.query.userId, 1000, 0);
userOwnCustomCount = customTemplates.length;
console.log('📈 [MERGED-TEMPLATES] ALL custom templates for THIS user:', userOwnCustomCount);
// Optionally include ALL custom templates from other users if explicitly requested
const includeOthers = String(req.query.includeOthers || '').toLowerCase() === 'true';
if (includeOthers) {
const allOtherCustomTemplates = await CustomTemplate.getAll(1000, 0);
const otherUsersTemplates = allOtherCustomTemplates.filter(t => t.user_id !== req.query.userId);
approvedOthersCustomCount = otherUsersTemplates.length;
console.log('📈 [MERGED-TEMPLATES] ALL custom templates from OTHER users (included by query):', approvedOthersCustomCount);
// Combine user's templates + all templates from others
customTemplates = [...customTemplates, ...otherUsersTemplates];
} else {
console.log(' [MERGED-TEMPLATES] Skipping custom templates from other users (includeOthers not set).');
}
}
} else {
// If no userId, get ALL custom templates regardless of status
console.log(' [MERGED-TEMPLATES] No userId provided, fetching ALL custom templates');
customTemplates = await CustomTemplate.getAll(1000, 0);
approvedOthersCustomCount = customTemplates.length;
console.log('📈 [MERGED-TEMPLATES] All custom templates (no user specified):', approvedOthersCustomCount);
}
console.log('📈 [MERGED-TEMPLATES] Totals → userOwn:', userOwnCustomCount, ', approvedOthers:', approvedOthersCustomCount, ', combinedCustoms:', customTemplates.length);
if (customTemplates.length > 0) {
console.log('🔍 [MERGED-TEMPLATES] Sample custom template:', {
id: customTemplates[0].id,
title: customTemplates[0].title,
category: customTemplates[0].category,
status: customTemplates[0].status
});
}
// Convert customs to standard template format and merge into flat array
console.log('🔄 [MERGED-TEMPLATES] Converting custom templates to standard format...');
const convertedCustomTemplates = customTemplates.map(customTemplate => ({
id: customTemplate.id,
type: customTemplate.type,
title: customTemplate.title,
description: customTemplate.description,
icon: customTemplate.icon,
category: customTemplate.category,
gradient: customTemplate.gradient,
border: customTemplate.border,
text: customTemplate.text,
subtext: customTemplate.subtext,
is_active: true,
created_at: customTemplate.created_at,
updated_at: customTemplate.updated_at,
is_custom: true,
complexity: customTemplate.complexity,
business_rules: customTemplate.business_rules,
technical_requirements: customTemplate.technical_requirements
}));
console.log('✅ [MERGED-TEMPLATES] Custom templates converted:', convertedCustomTemplates.length);
let allTemplates = defaultTemplates.concat(convertedCustomTemplates);
console.log('🔗 [MERGED-TEMPLATES] Combined templates total:', allTemplates.length);
// Apply category filter if specified
if (categoryFilter && categoryFilter !== 'all') {
console.log(`🎯 [MERGED-TEMPLATES] Applying category filter: "${categoryFilter}"`);
const beforeFilter = allTemplates.length;
allTemplates = allTemplates.filter(t => t.category === categoryFilter);
const afterFilter = allTemplates.length;
console.log(`📊 [MERGED-TEMPLATES] Category filter result: ${beforeFilter}${afterFilter} templates`);
}
// Apply search filter if specified
if (searchQuery) {
console.log(`🔍 [MERGED-TEMPLATES] Applying search filter: "${searchQuery}"`);
const beforeSearch = allTemplates.length;
allTemplates = allTemplates.filter(t =>
t.title.toLowerCase().includes(searchQuery) ||
t.description.toLowerCase().includes(searchQuery)
);
const afterSearch = allTemplates.length;
console.log(`📊 [MERGED-TEMPLATES] Search filter result: ${beforeSearch}${afterSearch} templates`);
}
// Sort by created_at descending
console.log('📅 [MERGED-TEMPLATES] Sorting templates by creation date...');
allTemplates.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
console.log('✅ [MERGED-TEMPLATES] Templates sorted successfully');
// Paginate
const total = allTemplates.length;
console.log('📊 [MERGED-TEMPLATES] Final template count before pagination:', total);
console.log('📄 [MERGED-TEMPLATES] Pagination parameters:', { offset, limit, total });
const paginatedTemplates = allTemplates.slice(offset, offset + limit);
console.log('📋 [MERGED-TEMPLATES] Paginated result:', {
requested: limit,
returned: paginatedTemplates.length,
startIndex: offset,
endIndex: offset + paginatedTemplates.length - 1
});
// Add feature counts to each template
console.log('🔢 [MERGED-TEMPLATES] Fetching feature counts for templates...');
// Separate default and custom templates for feature counting
const defaultTemplateIds = paginatedTemplates.filter(t => !t.is_custom).map(t => t.id);
const customTemplateIds = paginatedTemplates.filter(t => t.is_custom).map(t => t.id);
console.log('📊 [MERGED-TEMPLATES] Template ID breakdown:', {
defaultTemplates: defaultTemplateIds.length,
customTemplates: customTemplateIds.length
});
// Fetch feature counts for both types
let defaultFeatureCounts = {};
let customFeatureCounts = {};
if (defaultTemplateIds.length > 0) {
console.log('🔍 [MERGED-TEMPLATES] Fetching default template feature counts...');
defaultFeatureCounts = await Feature.countByTemplateIds(defaultTemplateIds);
console.log('✅ [MERGED-TEMPLATES] Default feature counts:', Object.keys(defaultFeatureCounts).length, 'templates');
}
if (customTemplateIds.length > 0) {
console.log('🔍 [MERGED-TEMPLATES] Fetching custom template feature counts...');
customFeatureCounts = await CustomFeature.countByTemplateIds(customTemplateIds);
console.log('✅ [MERGED-TEMPLATES] Custom feature counts:', Object.keys(customFeatureCounts).length, 'templates');
}
// Add feature counts to each template
const templatesWithFeatureCounts = paginatedTemplates.map(template => ({
...template,
feature_count: template.is_custom
? (customFeatureCounts[template.id] || 0)
: (defaultFeatureCounts[template.id] || 0)
}));
console.log('🎯 [MERGED-TEMPLATES] Feature counts added to all templates');
// Log sample of returned templates with feature counts
if (templatesWithFeatureCounts.length > 0) {
console.log('🔍 [MERGED-TEMPLATES] First template in result:', {
id: templatesWithFeatureCounts[0].id,
title: templatesWithFeatureCounts[0].title,
category: templatesWithFeatureCounts[0].category,
is_custom: templatesWithFeatureCounts[0].is_custom,
feature_count: templatesWithFeatureCounts[0].feature_count
});
if (templatesWithFeatureCounts.length > 1) {
console.log('🔍 [MERGED-TEMPLATES] Last template in result:', {
id: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].id,
title: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].title,
category: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].category,
is_custom: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].is_custom,
feature_count: templatesWithFeatureCounts[templatesWithFeatureCounts.length - 1].feature_count
});
}
}
const responseData = {
success: true,
data: templatesWithFeatureCounts,
pagination: {
total,
offset,
limit,
hasMore: offset + limit < total
},
message: `Found ${templatesWithFeatureCounts.length} templates (out of ${total}) with feature counts`
};
console.log('🎉 [MERGED-TEMPLATES] Response prepared successfully:', {
success: responseData.success,
dataCount: responseData.data.length,
pagination: responseData.pagination,
message: responseData.message,
sampleFeatureCounts: templatesWithFeatureCounts.slice(0, 3).map(t => ({
title: t.title,
feature_count: t.feature_count,
is_custom: t.is_custom
}))
});
res.json(responseData);
} catch (error) {
console.error('💥 [MERGED-TEMPLATES] Critical error occurred:', error.message);
console.error('📚 [MERGED-TEMPLATES] Error stack:', error.stack);
console.error('🔍 [MERGED-TEMPLATES] Error details:', {
name: error.name,
code: error.code,
sqlMessage: error.sqlMessage
});
res.status(500).json({
success: false,
error: 'Failed to fetch merged templates',
message: error.message
});
}
});
router.get('/all-templates-without-pagination', async (req, res) => {
try {
console.log('📂 [ALL-TEMPLATES] Fetching all templates with features and business rules...');
// Fetch templates (using your custom class methods)
const templatesQuery = 'SELECT * FROM templates WHERE is_active = true';
const customTemplatesQuery = 'SELECT * FROM custom_templates';
const [templatesResult, customTemplatesResult] = await Promise.all([
database.query(templatesQuery),
database.query(customTemplatesQuery)
]);
const templates = templatesResult.rows || [];
const customTemplates = customTemplatesResult.rows || [];
console.log(`📊 [ALL-TEMPLATES] Found ${templates.length} default templates and ${customTemplates.length} custom templates`);
// Merge both arrays
const allTemplates = [...templates, ...customTemplates];
// Sort by created_at (descending)
allTemplates.sort((a, b) => {
return new Date(b.created_at) - new Date(a.created_at);
});
// Fetch features and business rules for each template
console.log('🔍 [ALL-TEMPLATES] Fetching features and business rules for all templates...');
const templatesWithFeatures = await Promise.all(
allTemplates.map(async (template) => {
try {
// Check if this is a default template or custom template
const isCustomTemplate = !template.is_active; // custom templates don't have is_active field
let features = [];
let businessRules = {};
if (isCustomTemplate) {
// For custom templates, get features from custom_features table
const customFeaturesQuery = `
SELECT
cf.id,
cf.template_id,
cf.name,
cf.description,
cf.complexity,
cf.business_rules,
cf.technical_requirements,
'custom' as feature_type,
cf.created_at,
cf.updated_at,
cf.status,
cf.approved,
cf.usage_count,
0 as user_rating,
false as is_default,
true as created_by_user
FROM custom_features cf
WHERE cf.template_id = $1
ORDER BY cf.created_at DESC
`;
const customFeaturesResult = await database.query(customFeaturesQuery, [template.id]);
features = customFeaturesResult.rows || [];
// Extract business rules from custom features
features.forEach(feature => {
if (feature.business_rules) {
businessRules[feature.id] = feature.business_rules;
}
});
} else {
// For default templates, get features from template_features table
const defaultFeaturesQuery = `
SELECT
tf.*,
fbr.business_rules AS additional_business_rules
FROM template_features tf
LEFT JOIN feature_business_rules fbr
ON tf.template_id = fbr.template_id
AND (
fbr.feature_id = (tf.id::text)
OR fbr.feature_id = tf.feature_id
)
WHERE tf.template_id = $1
ORDER BY
CASE tf.feature_type
WHEN 'essential' THEN 1
WHEN 'suggested' THEN 2
WHEN 'custom' THEN 3
END,
tf.display_order,
tf.usage_count DESC,
tf.name
`;
const defaultFeaturesResult = await database.query(defaultFeaturesQuery, [template.id]);
features = defaultFeaturesResult.rows || [];
// Extract business rules from feature_business_rules table
features.forEach(feature => {
if (feature.additional_business_rules) {
businessRules[feature.id] = feature.additional_business_rules;
}
});
}
return {
...template,
features: features,
business_rules: businessRules,
feature_count: features.length,
is_custom: isCustomTemplate
};
} catch (featureError) {
console.error(`⚠️ [ALL-TEMPLATES] Error fetching features for template ${template.id}:`, featureError.message);
return {
...template,
features: [],
business_rules: {},
feature_count: 0,
is_custom: !template.is_active,
error: `Failed to fetch features: ${featureError.message}`
};
}
})
);
console.log(`✅ [ALL-TEMPLATES] Successfully processed ${templatesWithFeatures.length} templates with features and business rules`);
// Log sample data for debugging
if (templatesWithFeatures.length > 0) {
const sampleTemplate = templatesWithFeatures[0];
console.log('🔍 [ALL-TEMPLATES] Sample template data:', {
id: sampleTemplate.id,
title: sampleTemplate.title,
is_custom: sampleTemplate.is_custom,
feature_count: sampleTemplate.feature_count,
business_rules_count: Object.keys(sampleTemplate.business_rules || {}).length,
features_sample: sampleTemplate.features.slice(0, 2).map(f => ({
name: f.name,
type: f.feature_type,
has_business_rules: !!f.business_rules || !!f.additional_business_rules
}))
});
}
res.json({
success: true,
data: templatesWithFeatures,
message: `Found ${templatesWithFeatures.length} templates with features and business rules`,
summary: {
total_templates: templatesWithFeatures.length,
default_templates: templatesWithFeatures.filter(t => !t.is_custom).length,
custom_templates: templatesWithFeatures.filter(t => t.is_custom).length,
total_features: templatesWithFeatures.reduce((sum, t) => sum + t.feature_count, 0),
templates_with_business_rules: templatesWithFeatures.filter(t => Object.keys(t.business_rules || {}).length > 0).length
}
});
} catch (error) {
console.error('❌ Error fetching all templates without pagination:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch all templates without pagination',
message: error.message
});
}
});
// GET /api/templates/type/:type - Get template by type
router.get('/type/:type', async (req, res) => {
try {
const { type } = req.params;
console.log(`🔍 Fetching template by type: ${type}`);
const template = await Template.getByType(type);
if (!template) {
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with type ${type} does not exist`
});
}
// Get features for this template
const features = await Feature.getByTemplateId(template.id);
template.features = features;
res.json({
success: true,
data: template,
message: `Template ${template.title} retrieved successfully`
});
} catch (error) {
console.error('❌ Error fetching template by type:', error.message);
res.status(500).json({
success: false,
error: 'Failed to fetch template',
message: error.message
});
}
});
// GET /api/templates/:id - Get specific template with features (UUID constrained)
router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', async (req, res) => {
try {
const { id } = req.params;
console.log(`🔍 Fetching template: ${id}`);
// Extra guard: ensure UUID v4 to avoid DB errors if route matching misfires
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidV4Regex.test(id)) {
return res.status(400).json({
success: false,
error: 'Invalid template id',
message: 'id must be a valid UUID v4'
});
}
// First try to find in default templates
let template = await Template.getByIdWithFeatures(id);
let templateType = 'default';
// If not found in default templates, try custom templates
if (!template) {
const CustomTemplate = require('../models/custom_template');
template = await CustomTemplate.getByIdWithFeatures(id);
templateType = 'custom';
}
if (!template) {
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with ID ${id} does not exist`
});
}
// Add template type information to response
const responseData = {
...template,
template_type: templateType,
is_custom: templateType === 'custom'
};
res.json({
success: true,
data: responseData,
message: `Template ${template.title} retrieved successfully`
});
} catch (error) {
console.error('❌ Error fetching template:', error.message);
res.status(500).json({
success: false,
error: 'Failed to fetch template',
message: error.message
});
}
});
// GET /api/templates/:id/features - Get features for a template (UUID constrained)
router.get('/:id([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/features', async (req, res) => {
try {
const { id } = req.params;
console.log(`🎯 Fetching features for template: ${id}`);
// Check if template exists in either templates or custom_templates table
console.log(`🔍 Searching for template ID: ${id}`);
// First check templates table
const defaultTemplateCheck = await database.query(`
SELECT id, title, 'default' as template_type FROM templates WHERE id = $1 AND is_active = true
`, [id]);
console.log(`📊 Default templates found: ${defaultTemplateCheck.rows.length}`);
// Then check custom_templates table
const customTemplateCheck = await database.query(`
SELECT id, title, 'custom' as template_type FROM custom_templates WHERE id = $1
`, [id]);
console.log(`📊 Custom templates found: ${customTemplateCheck.rows.length}`);
// Combine results
const templateCheck = {
rows: [...defaultTemplateCheck.rows, ...customTemplateCheck.rows]
};
if (templateCheck.rows.length === 0) {
console.log(`❌ Template not found in either table: ${id}`);
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with ID ${id} does not exist in templates or custom_templates`
});
}
console.log(`✅ Template found: ${templateCheck.rows[0].title} (${templateCheck.rows[0].template_type})`);
// Fetch features from both tables for proper separation
console.log('📋 Fetching features from both template_features and custom_features tables');
// Get default/suggested features from template_features table
// Include aggregated business rules from feature_business_rules when available
const defaultFeaturesQuery = `
SELECT
tf.*,
fbr.business_rules AS additional_business_rules
FROM template_features tf
LEFT JOIN feature_business_rules fbr
ON tf.template_id = fbr.template_id
AND (
fbr.feature_id = (tf.id::text)
OR fbr.feature_id = tf.feature_id
)
WHERE tf.template_id = $1
ORDER BY
CASE tf.feature_type
WHEN 'essential' THEN 1
WHEN 'suggested' THEN 2
WHEN 'custom' THEN 3
END,
tf.display_order,
tf.usage_count DESC,
tf.name
`;
const defaultFeaturesResult = await database.query(defaultFeaturesQuery, [id]);
const defaultFeatures = defaultFeaturesResult.rows;
console.log(`📊 Found ${defaultFeatures.length} template features (all types)`);
console.log(`📋 Template features for ${id}:`, defaultFeatures.map(f => ({ name: f.name, type: f.feature_type, id: f.id })));
// Get custom features from custom_features table with business rules (if table exists)
// Some environments may not have run the feature_business_rules migration yet. Probe first.
const fbrExistsProbe = await database.query("SELECT to_regclass('public.feature_business_rules') AS tbl");
const hasFbrTable = !!(fbrExistsProbe.rows && fbrExistsProbe.rows[0] && fbrExistsProbe.rows[0].tbl);
const customFeaturesQuery = hasFbrTable
? `
SELECT
cf.id,
cf.template_id,
cf.name,
cf.description,
cf.complexity,
cf.business_rules,
cf.technical_requirements,
'custom' as feature_type,
cf.created_at,
cf.updated_at,
cf.status,
cf.approved,
cf.usage_count,
0 as user_rating,
false as is_default,
true as created_by_user,
fbr.business_rules as additional_business_rules
FROM custom_features cf
LEFT JOIN feature_business_rules fbr
ON cf.template_id = fbr.template_id
AND (
fbr.feature_id = (cf.id::text)
OR fbr.feature_id = ('custom_' || cf.id::text)
)
WHERE cf.template_id = $1
ORDER BY cf.created_at DESC
`
: `
SELECT
cf.id,
cf.template_id,
cf.name,
cf.description,
cf.complexity,
cf.business_rules,
cf.technical_requirements,
'custom' as feature_type,
cf.created_at,
cf.updated_at,
cf.status,
cf.approved,
cf.usage_count,
0 as user_rating,
false as is_default,
true as created_by_user,
NULL::jsonb as additional_business_rules
FROM custom_features cf
WHERE cf.template_id = $1
ORDER BY cf.created_at DESC
`;
const customFeaturesResult = await database.query(customFeaturesQuery, [id]);
const customFeatures = customFeaturesResult.rows;
console.log(`📊 Found ${customFeatures.length} custom features`);
// Combine both types of features
const features = [...defaultFeatures, ...customFeatures];
res.json({
success: true,
data: features,
count: features.length,
defaultFeaturesCount: defaultFeatures.length,
customFeaturesCount: customFeatures.length,
message: `Found ${defaultFeatures.length} default/suggested features and ${customFeatures.length} custom features`,
templateInfo: templateCheck.rows[0]
});
} catch (error) {
console.error('❌ Error fetching template features:', error.message);
res.status(500).json({
success: false,
error: 'Failed to fetch template features',
message: error.message
});
}
});
// POST /api/templates - Create new template
router.post('/', async (req, res) => {
try {
const templateData = req.body;
const debugPayload = {
raw: templateData,
normalized: {
title: (templateData.title || '').toLowerCase(),
type: templateData.type,
category: templateData.category,
isCustom: templateData.isCustom ?? templateData.is_custom ?? false,
user_id: templateData.user_id || templateData.userId || null
}
};
console.log('🏗️ Creating new template - incoming body:', JSON.stringify(debugPayload));
// Validate required fields
const requiredFields = ['type', 'title', 'category'];
for (const field of requiredFields) {
if (!templateData[field]) {
return res.status(400).json({
success: false,
error: 'Validation error',
message: `Field '${field}' is required`
});
}
}
// Check for duplicates in regular templates first
const existingTemplate = await Template.checkForDuplicate(templateData);
if (existingTemplate) {
const isTitleDuplicate = (existingTemplate.title || '').toLowerCase() === (templateData.title || '').toLowerCase();
const isTypeDuplicate = (existingTemplate.type || '') === (templateData.type || '');
console.log('[POST /api/templates] duplicate detected in main templates:', { existingTemplate, isTitleDuplicate, isTypeDuplicate });
const message = isTitleDuplicate
? `A template with this name already exists: "${existingTemplate.title}"`
: `A template with this type already exists: "${existingTemplate.title}" (type: ${existingTemplate.type})`;
return res.status(409).json({
success: false,
error: isTitleDuplicate ? 'Template name already exists' : 'Template type already exists',
message,
existing_template: {
id: existingTemplate.id,
title: existingTemplate.title,
type: existingTemplate.type,
category: existingTemplate.category
}
});
}
// If flagged as a custom template, store in custom_templates instead
if (templateData.isCustom === true || templateData.is_custom === true || templateData.source === 'custom') {
try {
const validComplexity = ['low', 'medium', 'high'];
const complexity = templateData.complexity || 'medium';
if (!validComplexity.includes(complexity)) {
return res.status(400).json({
success: false,
error: 'Invalid complexity',
message: `Complexity must be one of: ${validComplexity.join(', ')}`
});
}
// Check for duplicates in both regular and custom templates
const existingRegularTemplate = await CustomTemplate.checkTypeInMainTemplates(templateData.type);
if (existingRegularTemplate) {
return res.status(409).json({
success: false,
error: 'Template type already exists in main templates',
message: `A main template with type '${templateData.type}' already exists: "${existingRegularTemplate.title}"`,
existing_template: {
id: existingRegularTemplate.id,
title: existingRegularTemplate.title,
type: existingRegularTemplate.type,
source: 'main_templates'
}
});
}
const incomingUserId = templateData.user_id || templateData.userId || (req.user && (req.user.id || req.user.user_id)) || null;
// Check for duplicates in custom templates for this user
const duplicatePayload = {
type: templateData.type,
title: templateData.title,
category: templateData.category,
user_id: incomingUserId
};
console.log('[POST /api/templates - custom] duplicate payload:', duplicatePayload);
const existingCustomTemplate = await CustomTemplate.checkForDuplicate(duplicatePayload);
if (existingCustomTemplate) {
const isTitleDuplicate = (existingCustomTemplate.title || '').toLowerCase() === (templateData.title || '').toLowerCase();
const isTypeDuplicate = (existingCustomTemplate.type || '') === (templateData.type || '');
console.log('[POST /api/templates - custom] duplicate detected in custom/main:', { existingCustomTemplate, isTitleDuplicate, isTypeDuplicate });
const message = isTitleDuplicate
? `You already have a template with this name: "${existingCustomTemplate.title}"`
: `You already have a template with this type: "${existingCustomTemplate.title}" (type: ${existingCustomTemplate.type})`;
return res.status(409).json({
success: false,
error: isTitleDuplicate ? 'Template name already exists' : 'Template type already exists',
message,
existing_template: {
id: existingCustomTemplate.id,
title: existingCustomTemplate.title,
type: existingCustomTemplate.type,
category: existingCustomTemplate.category,
user_id: existingCustomTemplate.user_id,
source: 'custom_templates'
}
});
}
// Validate user_id format if provided
if (incomingUserId) {
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidV4Regex.test(incomingUserId)) {
return res.status(400).json({
success: false,
error: 'Invalid user_id',
message: 'user_id must be a valid UUID v4'
});
}
}
const isCustomValue = (templateData.is_custom !== undefined ? templateData.is_custom : (templateData.isCustom !== undefined ? templateData.isCustom : true));
const payloadToCreate = {
type: templateData.type,
title: templateData.title,
description: templateData.description,
icon: templateData.icon,
category: templateData.category,
gradient: templateData.gradient,
border: templateData.border,
text: templateData.text,
subtext: templateData.subtext,
complexity,
business_rules: templateData.business_rules,
technical_requirements: templateData.technical_requirements,
approved: false,
usage_count: 1,
created_by_user_session: templateData.created_by_user_session,
status: 'pending',
is_custom: isCustomValue,
user_id: incomingUserId
};
console.log('[Templates Route -> custom] user identification:', {
body_user_id: templateData.user_id,
body_userId: templateData.userId,
req_user: req.user ? (req.user.id || req.user.user_id) : null
});
console.log('[Templates Route -> custom] payload for create:', JSON.stringify(payloadToCreate));
const created = await CustomTemplate.create(payloadToCreate);
console.log('[Templates Route -> custom] created record summary:', { id: created.id, type: created.type, user_id: created.user_id, status: created.status });
// Create admin notification for new custom template
try {
console.log('[Templates Route -> custom] creating admin notification for template:', created.id, created.title);
const notif = await AdminNotification.notifyNewTemplate(created.id, created.title);
console.log('[Templates Route -> custom] admin notification created:', notif?.id);
} catch (notificationError) {
console.error('⚠️ Failed to create admin notification:', notificationError.message);
}
return res.status(201).json({
success: true,
data: created,
message: `Custom template '${created.title}' created successfully and submitted for admin review`
});
} catch (customErr) {
console.error('❌ Error creating custom template via templates route:', customErr.message);
return res.status(500).json({
success: false,
error: 'Failed to create custom template',
message: customErr.message
});
}
}
const template = await Template.create(templateData);
// Link back to custom_templates when approving from a custom
if (templateData.approved_from_custom) {
try {
const customId = templateData.approved_from_custom;
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (uuidV4Regex.test(customId)) {
await CustomTemplate.update(customId, {
approved: true,
status: 'approved',
canonical_template_id: template.id,
admin_reviewed_at: new Date(),
admin_reviewed_by: 'system_auto'
});
} else {
console.warn('[POST /api/templates] approved_from_custom is not a valid UUID v4');
}
} catch (linkErr) {
console.error('⚠️ Failed to set approved=true on custom_templates:', linkErr.message);
}
}
res.status(201).json({
success: true,
data: template,
message: `Template '${template.title}' created successfully`
});
} catch (error) {
console.error('❌ Error creating template:', error.message);
// Handle unique constraint violation
if (error.code === '23505') {
return res.status(409).json({
success: false,
error: 'Template already exists',
message: 'A template with this type already exists'
});
}
res.status(500).json({
success: false,
error: 'Failed to create template',
message: error.message
});
}
});
// POST /api/templates/approve-custom - Create main template and approve a custom template in one atomic flow
router.post('/approve-custom', async (req, res) => {
try {
const { custom_template_id, template } = req.body || {};
const customId = custom_template_id || req.body?.customTemplateId || req.body?.id;
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!customId || !uuidV4Regex.test(customId)) {
return res.status(400).json({ success: false, error: 'Invalid custom_template_id', message: 'Provide a valid UUID v4 for custom_template_id' });
}
// Load custom template to mirror missing fields if needed
const existingCustom = await CustomTemplate.getById(customId);
if (!existingCustom) {
return res.status(404).json({ success: false, error: 'Custom template not found', message: `No custom template with id ${customId}` });
}
const payload = {
type: template?.type || existingCustom.type,
title: template?.title || existingCustom.title,
description: template?.description ?? existingCustom.description,
icon: template?.icon ?? existingCustom.icon,
category: template?.category || existingCustom.category,
gradient: template?.gradient ?? existingCustom.gradient,
border: template?.border ?? existingCustom.border,
text: template?.text ?? existingCustom.text,
subtext: template?.subtext ?? existingCustom.subtext,
approved_from_custom: customId
};
// Create in main templates
const created = await Template.create(payload);
// Mark custom template as approved and link canonical_template_id
await CustomTemplate.update(customId, {
approved: true,
status: 'approved',
canonical_template_id: created.id,
admin_reviewed_at: new Date(),
admin_reviewed_by: (req.user && (req.user.username || req.user.email)) || 'admin'
});
return res.status(201).json({
success: true,
data: { template: created, custom_template_id: customId },
message: `Template '${created.title}' created and custom template approved`
});
} catch (error) {
console.error('❌ Error approving custom template:', error.message);
return res.status(500).json({ success: false, error: 'Failed to approve custom template', message: error.message });
}
});
// PUT /api/templates/:id - Update template or custom template based on isCustom flag
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const updateData = req.body;
const isCustomParam = (req.query.isCustom || req.query.is_custom || '').toString().toLowerCase();
const isCustom = isCustomParam === 'true' || isCustomParam === '1' || isCustomParam === 'yes';
console.log('📝 [PUT /api/templates/:id] start', { id, isCustom, bodyKeys: Object.keys(updateData || {}) });
if (isCustom) {
console.log('🔎 Looking up custom template by id');
const custom = await CustomTemplate.getById(id);
console.log('🔎 Lookup result (custom):', { found: !!custom });
if (!custom) {
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Custom template with ID ${id} does not exist`
});
}
// Validate allowed fields for custom templates to avoid no-op updates
const allowed = [
'title', 'description', 'icon', 'category', 'gradient', 'border', 'text', 'subtext',
'complexity', 'business_rules', 'technical_requirements', 'approved', 'usage_count',
'status', 'admin_notes', 'admin_reviewed_at', 'admin_reviewed_by',
'canonical_template_id', 'similarity_score', 'user_id'
];
const providedKeys = Object.keys(updateData || {});
const updatableKeys = providedKeys.filter(k => allowed.includes(k));
console.log('🧮 Update keys (custom):', { providedKeys, updatableKeys });
if (updatableKeys.length === 0) {
return res.status(400).json({
success: false,
error: 'No updatable fields',
message: 'Provide at least one updatable field'
});
}
console.log('📝 Updating custom template...');
const updated = await CustomTemplate.update(id, updateData);
console.log('📝 Update result (custom):', { updated: !!updated });
return res.json({
success: true,
data: updated,
message: `Custom template '${updated?.title || updated?.id}' updated successfully`
});
}
console.log('🔎 Looking up default template by id');
const template = await Template.getByIdWithFeatures(id);
console.log('🔎 Lookup result (default):', { found: !!template });
if (!template) {
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with ID ${id} does not exist`
});
}
console.log('📝 Updating default template...');
const updatedTemplate = await template.update(updateData);
console.log('📝 Update result (default):', { updated: !!updatedTemplate });
res.json({
success: true,
data: updatedTemplate,
message: `Template '${updatedTemplate.title}' updated successfully`
});
} catch (error) {
console.error('❌ Error updating template:', { message: error.message, stack: error.stack });
res.status(500).json({
success: false,
error: 'Failed to update template',
message: error.message
});
}
});
// DELETE /api/templates/:id - Delete template or custom template based on isCustom flag
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const isCustomParam = (req.query.isCustom || req.query.is_custom || '').toString().toLowerCase();
const isCustom = isCustomParam === 'true' || isCustomParam === '1' || isCustomParam === 'yes';
console.log('🗑️ [DELETE /api/templates/:id] start', { id, query: req.query, isCustomParam, isCustom });
if (isCustom) {
console.log('🔎 Looking up custom template by id');
const custom = await CustomTemplate.getById(id);
console.log('🔎 Lookup result (custom):', { found: !!custom });
if (!custom) {
console.warn('⚠️ Custom template not found', { id });
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Custom template with ID ${id} does not exist`
});
}
console.log('🗑️ Deleting custom template...');
const deleted = await CustomTemplate.delete(id);
console.log('🗑️ Delete result (custom):', { deleted });
if (!deleted) {
return res.status(500).json({
success: false,
error: 'Failed to delete template',
message: `Failed to delete custom template with ID ${id}`
});
}
return res.json({
success: true,
message: `Custom template '${custom.title || custom.id}' deleted successfully`
});
}
console.log('🔎 Looking up default template by id');
const template = await Template.getByIdWithFeatures(id);
console.log('🔎 Lookup result (default):', { found: !!template });
if (!template) {
console.warn('⚠️ Default template not found', { id });
return res.status(404).json({
success: false,
error: 'Template not found',
message: `Template with ID ${id} does not exist`
});
}
console.log('🗑️ Deleting default template...');
await Template.delete(id);
console.log('🗑️ Delete done (default)');
res.json({
success: true,
message: `Template '${template.title}' deleted successfully`
});
} catch (error) {
console.error('❌ Error deleting template:', { message: error.message, stack: error.stack });
res.status(500).json({
success: false,
error: 'Failed to delete template',
message: error.message
});
}
});
module.exports = router;