402 lines
14 KiB
JavaScript
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;
|