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

402 lines
14 KiB
JavaScript

const database = require('../config/database');
const { v4: uuidv4 } = require('uuid');
class CustomTemplate {
constructor(data = {}) {
this.id = data.id;
this.type = data.type;
this.title = data.title;
this.description = data.description;
this.icon = data.icon;
this.category = data.category;
this.gradient = data.gradient;
this.border = data.border;
this.text = data.text;
this.subtext = data.subtext;
this.complexity = data.complexity;
this.business_rules = data.business_rules;
this.technical_requirements = data.technical_requirements;
this.approved = data.approved;
this.usage_count = data.usage_count;
this.created_by_user_session = data.created_by_user_session;
this.created_at = data.created_at;
this.updated_at = data.updated_at;
this.is_custom = data.is_custom ?? false;
// Admin approval workflow fields
this.status = data.status || 'pending';
this.admin_notes = data.admin_notes;
this.admin_reviewed_at = data.admin_reviewed_at;
this.admin_reviewed_by = data.admin_reviewed_by;
this.canonical_template_id = data.canonical_template_id;
this.similarity_score = data.similarity_score;
this.user_id = data.user_id;
}
static async getById(id) {
const result = await database.query('SELECT * FROM custom_templates WHERE id = $1', [id]);
return result.rows.length ? new CustomTemplate(result.rows[0]) : null;
}
// Get custom template by ID with features
static async getByIdWithFeatures(id) {
const templateQuery = `
SELECT * FROM custom_templates
WHERE id = $1
`;
const featuresQuery = `
SELECT * FROM custom_features
WHERE template_id = $1
ORDER BY created_at DESC
`;
const [templateResult, featuresResult] = await Promise.all([
database.query(templateQuery, [id]),
database.query(featuresQuery, [id])
]);
if (templateResult.rows.length === 0) {
return null;
}
const template = new CustomTemplate(templateResult.rows[0]);
template.features = featuresResult.rows;
return template;
}
// Check for duplicate custom templates based on title, type, category, and user_id
static async checkForDuplicate(templateData) {
const normalizedTitle = (templateData.title || '').toLowerCase();
console.log('[CustomTemplate.checkForDuplicate] Checking for duplicates:', {
type: templateData.type,
title: templateData.title,
normalizedTitle,
category: templateData.category,
user_id: templateData.user_id
});
// Check for exact type match (globally unique)
const typeQuery = `
SELECT id, title, type, category, user_id FROM custom_templates
WHERE type = $1
`;
const typeResult = await database.query(typeQuery, [templateData.type]);
if (typeResult.rows.length > 0) {
console.log('[CustomTemplate.checkForDuplicate] Found duplicate by type:', typeResult.rows[0]);
return typeResult.rows[0];
}
// Check for same title for same user (category-agnostic)
if (templateData.user_id) {
const titleQuery = `
SELECT id, title, type, category, user_id FROM custom_templates
WHERE LOWER(title) = LOWER($1) AND user_id = $2
`;
const titleParams = [templateData.title, templateData.user_id];
console.log('[CustomTemplate.checkForDuplicate] title check params:', titleParams);
const titleResult = await database.query(titleQuery, titleParams);
if (titleResult.rows.length > 0) {
const row = titleResult.rows[0];
const titleMatch = (row.title || '').toLowerCase() === normalizedTitle;
console.log('[CustomTemplate.checkForDuplicate] Found duplicate by title+user:', {
id: row.id,
title: row.title,
type: row.type,
category: row.category,
user_id: row.user_id,
titleMatch
});
return titleResult.rows[0];
}
}
// Also check if main templates already have the same title (case-insensitive)
const mainTitleQuery = `
SELECT id, title, type, category FROM templates
WHERE is_active = true AND LOWER(title) = LOWER($1)
LIMIT 1
`;
const mainTitleParams = [templateData.title];
console.log('[CustomTemplate.checkForDuplicate] main title check params:', mainTitleParams);
const mainTitleResult = await database.query(mainTitleQuery, mainTitleParams);
if (mainTitleResult.rows.length > 0) {
const row = mainTitleResult.rows[0];
const titleMatch = (row.title || '').toLowerCase() === normalizedTitle;
console.log('[CustomTemplate.checkForDuplicate] Found duplicate title in main templates:', {
id: row.id,
title: row.title,
type: row.type,
category: row.category,
titleMatch
});
return mainTitleResult.rows[0];
}
console.log('[CustomTemplate.checkForDuplicate] No duplicates found');
return null;
}
// Check if template type exists in main templates table
static async checkTypeInMainTemplates(type) {
const query = `
SELECT id, title, type FROM templates
WHERE type = $1 AND is_active = true
`;
const result = await database.query(query, [type]);
return result.rows.length > 0 ? result.rows[0] : null;
}
static async create(data) {
const id = uuidv4();
console.log('[CustomTemplate.create] start - id:', id);
const query = `
INSERT INTO custom_templates (
id, type, title, description, icon, category, gradient, border, text, subtext,
complexity, business_rules, technical_requirements, approved, usage_count,
created_by_user_session, status, admin_notes, admin_reviewed_at,
admin_reviewed_by, canonical_template_id, similarity_score, is_custom, user_id
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24)
RETURNING *
`;
const values = [
id,
data.type,
data.title,
data.description || null,
data.icon || null,
data.category,
data.gradient || null,
data.border || null,
data.text || null,
data.subtext || null,
data.complexity,
data.business_rules || null,
data.technical_requirements || null,
data.approved ?? false,
data.usage_count ?? 1,
data.created_by_user_session || null,
data.status || 'pending',
data.admin_notes || null,
data.admin_reviewed_at || null,
data.admin_reviewed_by || null,
data.canonical_template_id || null,
data.similarity_score || null,
data.is_custom ?? false,
data.user_id || null,
];
console.log('[CustomTemplate.create] values prepared (truncated):', {
id: values[0],
type: values[1],
title: values[2],
is_custom: values[22],
user_id: values[23]
});
const result = await database.query(query, values);
console.log('[CustomTemplate.create] insert done - row id:', result.rows[0]?.id, 'user_id:', result.rows[0]?.user_id);
const customTemplate = new CustomTemplate(result.rows[0]);
// Automatically trigger tech stack analysis for new custom template
try {
console.log(`🤖 [CustomTemplate.create] Triggering auto tech stack analysis for custom template: ${customTemplate.title}`);
// Use dynamic import to avoid circular dependency
const autoTechStackAnalyzer = require('../services/auto_tech_stack_analyzer');
autoTechStackAnalyzer.queueForAnalysis(customTemplate.id, 'custom', 1); // High priority for new templates
} catch (error) {
console.error(`⚠️ [CustomTemplate.create] Failed to queue tech stack analysis:`, error.message);
// Don't fail template creation if auto-analysis fails
}
return customTemplate;
}
static async update(id, updates) {
const fields = [];
const values = [];
let idx = 1;
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'
];
for (const k of allowed) {
if (updates[k] !== undefined) {
fields.push(`${k} = $${idx++}`);
values.push(updates[k]);
}
}
if (fields.length === 0) return await CustomTemplate.getById(id);
const query = `UPDATE custom_templates SET ${fields.join(', ')}, updated_at = NOW() WHERE id = $${idx} RETURNING *`;
values.push(id);
const result = await database.query(query, values);
const updatedTemplate = result.rows.length ? new CustomTemplate(result.rows[0]) : null;
// Automatically trigger tech stack analysis for updated custom template
if (updatedTemplate) {
try {
console.log(`🤖 [CustomTemplate.update] Triggering auto tech stack analysis for updated custom template: ${updatedTemplate.title}`);
// Use dynamic import to avoid circular dependency
const autoTechStackAnalyzer = require('../services/auto_tech_stack_analyzer');
autoTechStackAnalyzer.queueForAnalysis(updatedTemplate.id, 'custom', 2); // Normal priority for updates
} catch (error) {
console.error(`⚠️ [CustomTemplate.update] Failed to queue tech stack analysis:`, error.message);
// Don't fail template update if auto-analysis fails
}
}
return updatedTemplate;
}
static async delete(id) {
const result = await database.query('DELETE FROM custom_templates WHERE id = $1', [id]);
return result.rowCount > 0;
}
// Admin workflow methods
static async getPendingTemplates(limit = 50, offset = 0) {
const query = `
SELECT * FROM custom_templates
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT $1 OFFSET $2
`;
const result = await database.query(query, [limit, offset]);
return result.rows.map(r => new CustomTemplate(r));
}
static async getTemplatesByStatus(status, limit = 50, offset = 0) {
const query = `
SELECT * FROM custom_templates
WHERE status = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`;
const result = await database.query(query, [status, limit, offset]);
return result.rows.map(r => new CustomTemplate(r));
}
// Get custom templates created by a specific user session
static async getByCreatorSession(sessionKey, limit = 100, offset = 0, status = null) {
if (!sessionKey) return [];
let query = `
SELECT * FROM custom_templates
WHERE created_by_user_session = $1
`;
const values = [sessionKey];
if (status) {
query += ` AND status = $2`;
values.push(status);
}
query += ` ORDER BY created_at DESC LIMIT ${status ? '$3' : '$2'} OFFSET ${status ? '$4' : '$3'}`;
values.push(limit, offset);
const result = await database.query(query, values);
return result.rows.map(r => new CustomTemplate(r));
}
static async getTemplateStats() {
const query = `
SELECT
status,
COUNT(*) as count
FROM custom_templates
GROUP BY status
`;
const result = await database.query(query);
return result.rows;
}
// Get custom templates by authenticated user id
static async getByUserId(userId, limit = 100, offset = 0, status = null) {
if (!userId) return [];
let query = `
SELECT * FROM custom_templates
WHERE user_id = $1
`;
const values = [userId];
if (status) {
query += ` AND status = $2`;
values.push(status);
}
query += ` ORDER BY created_at DESC LIMIT ${status ? '$3' : '$2'} OFFSET ${status ? '$4' : '$3'}`;
values.push(limit, offset);
const result = await database.query(query, values);
return result.rows.map(r => new CustomTemplate(r));
}
static async reviewTemplate(id, reviewData) {
const { status, admin_notes, canonical_template_id, admin_reviewed_by } = reviewData;
const updates = {
status,
admin_notes,
admin_reviewed_at: new Date(),
admin_reviewed_by
};
// Maintain the legacy boolean flag alongside the status for easier filtering
if (status === 'approved') {
updates.approved = true;
} else if (status === 'rejected' || status === 'duplicate') {
updates.approved = false;
}
if (canonical_template_id) {
updates.canonical_template_id = canonical_template_id;
}
return await CustomTemplate.update(id, updates);
}
// Get all custom templates
static async getAll(limit = 100, offset = 0) {
const query = `
SELECT * FROM custom_templates
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`;
const result = await database.query(query, [limit, offset]);
return result.rows.map(r => new CustomTemplate(r));
}
// Search custom templates
static async search(searchTerm, limit = 20) {
const query = `
SELECT * FROM custom_templates
WHERE (title ILIKE $1 OR description ILIKE $1 OR category ILIKE $1)
ORDER BY usage_count DESC, created_at DESC
LIMIT $2
`;
const result = await database.query(query, [`%${searchTerm}%`, limit]);
return result.rows.map(r => new CustomTemplate(r));
}
// Get statistics for admin dashboard
static async getStats() {
const query = `
SELECT
status,
COUNT(*) as count
FROM custom_templates
GROUP BY status
`;
const result = await database.query(query);
return result.rows.map(row => ({
status: row.status,
count: parseInt(row.count) || 0
}));
}
// Alias for getAll method to match admin route expectations
static async getAllTemplates(limit = 50, offset = 0) {
return await CustomTemplate.getAll(limit, offset);
}
}
module.exports = CustomTemplate;