feat: enhance template tooling with pagination and flexible retrieval

- Add pagination support to all template search/list tools
  - Consistent response format with total, limit, offset, hasMore
  - Support for customizable limits (1-100) and offsets

- Add new list_templates tool for browsing all templates
  - Returns minimal data (id, name, views, node count)
  - Supports sorting by views, created_at, or name
  - Efficient for discovering available templates

- Enhance get_template with flexible response modes
  - nodes_only: Just list of node types (minimal tokens)
  - structure: Nodes with positions and connections
  - full: Complete workflow JSON (default)

- Update database_statistics to show template count
  - Shows total templates, average/min/max views
  - Provides complete database overview

- Add count methods to repository for pagination
  - getSearchCount, getNodeTemplatesCount, getTaskTemplatesCount
  - Enables accurate pagination info

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-09-14 15:04:17 +02:00
parent f35097ed46
commit e7895d2e01
5 changed files with 339 additions and 65 deletions

Binary file not shown.

View File

@@ -725,21 +725,32 @@ export class N8NDocumentationMCPServer {
case 'get_node_as_tool_info': case 'get_node_as_tool_info':
this.validateToolParams(name, args, ['nodeType']); this.validateToolParams(name, args, ['nodeType']);
return this.getNodeAsToolInfo(args.nodeType); return this.getNodeAsToolInfo(args.nodeType);
case 'list_templates':
// No required params
const listLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
const listOffset = Math.max(Number(args.offset) || 0, 0);
const sortBy = args.sortBy || 'views';
return this.listTemplates(listLimit, listOffset, sortBy);
case 'list_node_templates': case 'list_node_templates':
this.validateToolParams(name, args, ['nodeTypes']); this.validateToolParams(name, args, ['nodeTypes']);
const templateLimit = args.limit !== undefined ? Number(args.limit) || 10 : 10; const templateLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
return this.listNodeTemplates(args.nodeTypes, templateLimit); const templateOffset = Math.max(Number(args.offset) || 0, 0);
return this.listNodeTemplates(args.nodeTypes, templateLimit, templateOffset);
case 'get_template': case 'get_template':
this.validateToolParams(name, args, ['templateId']); this.validateToolParams(name, args, ['templateId']);
const templateId = Number(args.templateId); const templateId = Number(args.templateId);
return this.getTemplate(templateId); const mode = args.mode || 'full';
return this.getTemplate(templateId, mode);
case 'search_templates': case 'search_templates':
this.validateToolParams(name, args, ['query']); this.validateToolParams(name, args, ['query']);
const searchLimit = args.limit !== undefined ? Number(args.limit) || 20 : 20; const searchLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
return this.searchTemplates(args.query, searchLimit); const searchOffset = Math.max(Number(args.offset) || 0, 0);
return this.searchTemplates(args.query, searchLimit, searchOffset);
case 'get_templates_for_task': case 'get_templates_for_task':
this.validateToolParams(name, args, ['task']); this.validateToolParams(name, args, ['task']);
return this.getTemplatesForTask(args.task); const taskLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
const taskOffset = Math.max(Number(args.offset) || 0, 0);
return this.getTemplatesForTask(args.task, taskLimit, taskOffset);
case 'validate_workflow': case 'validate_workflow':
this.validateToolParams(name, args, ['workflow']); this.validateToolParams(name, args, ['workflow']);
return this.validateWorkflow(args.workflow, args.options); return this.validateWorkflow(args.workflow, args.options);
@@ -1594,8 +1605,19 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
GROUP BY package_name GROUP BY package_name
`).all() as any[]; `).all() as any[];
// Get template statistics
const templateStats = this.db!.prepare(`
SELECT
COUNT(*) as total_templates,
AVG(views) as avg_views,
MIN(views) as min_views,
MAX(views) as max_views
FROM templates
`).get() as any;
return { return {
totalNodes: stats.total, totalNodes: stats.total,
totalTemplates: templateStats.total_templates || 0,
statistics: { statistics: {
aiTools: stats.ai_tools, aiTools: stats.ai_tools,
triggers: stats.triggers, triggers: stats.triggers,
@@ -1604,6 +1626,12 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
documentationCoverage: Math.round((stats.with_docs / stats.total) * 100) + '%', documentationCoverage: Math.round((stats.with_docs / stats.total) * 100) + '%',
uniquePackages: stats.packages, uniquePackages: stats.packages,
uniqueCategories: stats.categories, uniqueCategories: stats.categories,
templates: {
total: templateStats.total_templates || 0,
avgViews: Math.round(templateStats.avg_views || 0),
minViews: templateStats.min_views || 0,
maxViews: templateStats.max_views || 0
}
}, },
packageBreakdown: packages.map(pkg => ({ packageBreakdown: packages.map(pkg => ({
package: pkg.package_name, package: pkg.package_name,
@@ -2300,76 +2328,95 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
} }
// Template-related methods // Template-related methods
private async listNodeTemplates(nodeTypes: string[], limit: number = 10): Promise<any> { private async listTemplates(limit: number = 10, offset: number = 0, sortBy: 'views' | 'created_at' | 'name' = 'views'): Promise<any> {
await this.ensureInitialized(); await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized'); if (!this.templateService) throw new Error('Template service not initialized');
const templates = await this.templateService.listNodeTemplates(nodeTypes, limit); const result = await this.templateService.listTemplates(limit, offset, sortBy);
if (templates.length === 0) { return {
...result,
tip: result.items.length > 0 ?
`Use get_template(templateId) to get full workflow details. Total: ${result.total} templates available.` :
"No templates found. Run 'npm run fetch:templates' to update template database"
};
}
private async listNodeTemplates(nodeTypes: string[], limit: number = 10, offset: number = 0): Promise<any> {
await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized');
const result = await this.templateService.listNodeTemplates(nodeTypes, limit, offset);
if (result.items.length === 0 && offset === 0) {
return { return {
...result,
message: `No templates found using nodes: ${nodeTypes.join(', ')}`, message: `No templates found using nodes: ${nodeTypes.join(', ')}`,
tip: "Try searching with more common nodes or run 'npm run fetch:templates' to update template database", tip: "Try searching with more common nodes or run 'npm run fetch:templates' to update template database"
templates: []
}; };
} }
return { return {
templates, ...result,
count: templates.length, tip: `Showing ${result.items.length} of ${result.total} templates. Use offset for pagination.`
tip: `Use get_template(templateId) to get the full workflow JSON for any template`
}; };
} }
private async getTemplate(templateId: number): Promise<any> { private async getTemplate(templateId: number, mode: 'nodes_only' | 'structure' | 'full' = 'full'): Promise<any> {
await this.ensureInitialized(); await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized'); if (!this.templateService) throw new Error('Template service not initialized');
const template = await this.templateService.getTemplate(templateId); const template = await this.templateService.getTemplate(templateId, mode);
if (!template) { if (!template) {
return { return {
error: `Template ${templateId} not found`, error: `Template ${templateId} not found`,
tip: "Use list_node_templates or search_templates to find available templates" tip: "Use list_templates, list_node_templates or search_templates to find available templates"
}; };
} }
const usage = mode === 'nodes_only' ? "Node list for quick overview" :
mode === 'structure' ? "Workflow structure without full details" :
"Complete workflow JSON ready to import into n8n";
return { return {
mode,
template, template,
usage: "Import this workflow JSON directly into n8n or use it as a reference for building workflows" usage
}; };
} }
private async searchTemplates(query: string, limit: number = 20): Promise<any> { private async searchTemplates(query: string, limit: number = 20, offset: number = 0): Promise<any> {
await this.ensureInitialized(); await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized'); if (!this.templateService) throw new Error('Template service not initialized');
const templates = await this.templateService.searchTemplates(query, limit); const result = await this.templateService.searchTemplates(query, limit, offset);
if (templates.length === 0) { if (result.items.length === 0 && offset === 0) {
return { return {
...result,
message: `No templates found matching: "${query}"`, message: `No templates found matching: "${query}"`,
tip: "Try different keywords or run 'npm run fetch:templates' to update template database", tip: "Try different keywords or run 'npm run fetch:templates' to update template database"
templates: []
}; };
} }
return { return {
templates, ...result,
count: templates.length, query,
query tip: `Found ${result.total} templates matching "${query}". Showing ${result.items.length}.`
}; };
} }
private async getTemplatesForTask(task: string): Promise<any> { private async getTemplatesForTask(task: string, limit: number = 10, offset: number = 0): Promise<any> {
await this.ensureInitialized(); await this.ensureInitialized();
if (!this.templateService) throw new Error('Template service not initialized'); if (!this.templateService) throw new Error('Template service not initialized');
const templates = await this.templateService.getTemplatesForTask(task); const result = await this.templateService.getTemplatesForTask(task, limit, offset);
const availableTasks = this.templateService.listAvailableTasks(); const availableTasks = this.templateService.listAvailableTasks();
if (templates.length === 0) { if (result.items.length === 0 && offset === 0) {
return { return {
...result,
message: `No templates found for task: ${task}`, message: `No templates found for task: ${task}`,
availableTasks, availableTasks,
tip: "Try a different task or use search_templates for custom searches" tip: "Try a different task or use search_templates for custom searches"
@@ -2377,10 +2424,10 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
} }
return { return {
...result,
task, task,
templates, description: this.getTaskDescription(task),
count: templates.length, tip: `${result.total} templates available for ${task}. Showing ${result.items.length}.`
description: this.getTaskDescription(task)
}; };
} }

View File

@@ -323,9 +323,37 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
required: ['nodeType'], required: ['nodeType'],
}, },
}, },
{
name: 'list_templates',
description: `List all templates with minimal data (id, name, views, node count). Use for browsing available templates.`,
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of results (1-100). Default 10.',
default: 10,
minimum: 1,
maximum: 100,
},
offset: {
type: 'number',
description: 'Pagination offset. Default 0.',
default: 0,
minimum: 0,
},
sortBy: {
type: 'string',
enum: ['views', 'created_at', 'name'],
description: 'Sort field. Default: views (popularity).',
default: 'views',
},
},
},
},
{ {
name: 'list_node_templates', name: 'list_node_templates',
description: `Find templates using specific nodes. 399 community workflows. Use FULL types: "n8n-nodes-base.httpRequest".`, description: `Find templates using specific nodes. Returns paginated results. Use FULL types: "n8n-nodes-base.httpRequest".`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@@ -338,6 +366,14 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
type: 'number', type: 'number',
description: 'Maximum number of templates to return. Default 10.', description: 'Maximum number of templates to return. Default 10.',
default: 10, default: 10,
minimum: 1,
maximum: 100,
},
offset: {
type: 'number',
description: 'Pagination offset. Default 0.',
default: 0,
minimum: 0,
}, },
}, },
required: ['nodeTypes'], required: ['nodeTypes'],
@@ -345,7 +381,7 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
}, },
{ {
name: 'get_template', name: 'get_template',
description: `Get complete workflow JSON by ID. Ready to import. IDs from list_node_templates or search_templates.`, description: `Get template by ID. Use mode to control response size: nodes_only (minimal), structure (nodes+connections), full (complete workflow).`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@@ -353,13 +389,19 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
type: 'number', type: 'number',
description: 'The template ID to retrieve', description: 'The template ID to retrieve',
}, },
mode: {
type: 'string',
enum: ['nodes_only', 'structure', 'full'],
description: 'Response detail level. nodes_only: just node list, structure: nodes+connections, full: complete workflow JSON.',
default: 'full',
},
}, },
required: ['templateId'], required: ['templateId'],
}, },
}, },
{ {
name: 'search_templates', name: 'search_templates',
description: `Search templates by name/description keywords. NOT for node types! For nodes use list_node_templates. Example: "chatbot".`, description: `Search templates by name/description keywords. Returns paginated results. NOT for node types! For nodes use list_node_templates.`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@@ -371,6 +413,14 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
type: 'number', type: 'number',
description: 'Maximum number of results. Default 20.', description: 'Maximum number of results. Default 20.',
default: 20, default: 20,
minimum: 1,
maximum: 100,
},
offset: {
type: 'number',
description: 'Pagination offset. Default 0.',
default: 0,
minimum: 0,
}, },
}, },
required: ['query'], required: ['query'],
@@ -378,7 +428,7 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
}, },
{ {
name: 'get_templates_for_task', name: 'get_templates_for_task',
description: `Curated templates by task: ai_automation, data_sync, webhooks, email, slack, data_transform, files, scheduling, api, database.`, description: `Curated templates by task. Returns paginated results sorted by popularity.`,
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@@ -398,6 +448,19 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
], ],
description: 'The type of task to get templates for', description: 'The type of task to get templates for',
}, },
limit: {
type: 'number',
description: 'Maximum number of results. Default 10.',
default: 10,
minimum: 1,
maximum: 100,
},
offset: {
type: 'number',
description: 'Pagination offset. Default 0.',
default: 0,
minimum: 0,
},
}, },
required: ['task'], required: ['task'],
}, },

View File

@@ -173,17 +173,17 @@ export class TemplateRepository {
/** /**
* Get templates that use specific node types * Get templates that use specific node types
*/ */
getTemplatesByNodes(nodeTypes: string[], limit: number = 10): StoredTemplate[] { getTemplatesByNodes(nodeTypes: string[], limit: number = 10, offset: number = 0): StoredTemplate[] {
// Build query for multiple node types // Build query for multiple node types
const conditions = nodeTypes.map(() => "nodes_used LIKE ?").join(" OR "); const conditions = nodeTypes.map(() => "nodes_used LIKE ?").join(" OR ");
const query = ` const query = `
SELECT * FROM templates SELECT * FROM templates
WHERE ${conditions} WHERE ${conditions}
ORDER BY views DESC, created_at DESC ORDER BY views DESC, created_at DESC
LIMIT ? LIMIT ? OFFSET ?
`; `;
const params = [...nodeTypes.map(n => `%"${n}"%`), limit]; const params = [...nodeTypes.map(n => `%"${n}"%`), limit, offset];
const results = this.db.prepare(query).all(...params) as StoredTemplate[]; const results = this.db.prepare(query).all(...params) as StoredTemplate[];
return results.map(t => this.decompressWorkflow(t)); return results.map(t => this.decompressWorkflow(t));
} }
@@ -232,13 +232,13 @@ export class TemplateRepository {
/** /**
* Search templates by name or description * Search templates by name or description
*/ */
searchTemplates(query: string, limit: number = 20): StoredTemplate[] { searchTemplates(query: string, limit: number = 20, offset: number = 0): StoredTemplate[] {
logger.debug(`Searching templates for: "${query}" (FTS5: ${this.hasFTS5Support})`); logger.debug(`Searching templates for: "${query}" (FTS5: ${this.hasFTS5Support})`);
// If FTS5 is not supported, go straight to LIKE search // If FTS5 is not supported, go straight to LIKE search
if (!this.hasFTS5Support) { if (!this.hasFTS5Support) {
logger.debug('Using LIKE search (FTS5 not available)'); logger.debug('Using LIKE search (FTS5 not available)');
return this.searchTemplatesLIKE(query, limit); return this.searchTemplatesLIKE(query, limit, offset);
} }
try { try {
@@ -255,8 +255,8 @@ export class TemplateRepository {
JOIN templates_fts ON t.id = templates_fts.rowid JOIN templates_fts ON t.id = templates_fts.rowid
WHERE templates_fts MATCH ? WHERE templates_fts MATCH ?
ORDER BY rank, t.views DESC ORDER BY rank, t.views DESC
LIMIT ? LIMIT ? OFFSET ?
`).all(ftsQuery, limit) as StoredTemplate[]; `).all(ftsQuery, limit, offset) as StoredTemplate[];
logger.debug(`FTS5 search returned ${results.length} results`); logger.debug(`FTS5 search returned ${results.length} results`);
return results.map(t => this.decompressWorkflow(t)); return results.map(t => this.decompressWorkflow(t));
@@ -267,14 +267,14 @@ export class TemplateRepository {
query: query, query: query,
ftsQuery: query.split(' ').map(term => `"${term}"`).join(' OR ') ftsQuery: query.split(' ').map(term => `"${term}"`).join(' OR ')
}); });
return this.searchTemplatesLIKE(query, limit); return this.searchTemplatesLIKE(query, limit, offset);
} }
} }
/** /**
* Fallback search using LIKE when FTS5 is not available * Fallback search using LIKE when FTS5 is not available
*/ */
private searchTemplatesLIKE(query: string, limit: number = 20): StoredTemplate[] { private searchTemplatesLIKE(query: string, limit: number = 20, offset: number = 0): StoredTemplate[] {
const likeQuery = `%${query}%`; const likeQuery = `%${query}%`;
logger.debug(`Using LIKE search with pattern: ${likeQuery}`); logger.debug(`Using LIKE search with pattern: ${likeQuery}`);
@@ -282,8 +282,8 @@ export class TemplateRepository {
SELECT * FROM templates SELECT * FROM templates
WHERE name LIKE ? OR description LIKE ? WHERE name LIKE ? OR description LIKE ?
ORDER BY views DESC, created_at DESC ORDER BY views DESC, created_at DESC
LIMIT ? LIMIT ? OFFSET ?
`).all(likeQuery, likeQuery, limit) as StoredTemplate[]; `).all(likeQuery, likeQuery, limit, offset) as StoredTemplate[];
logger.debug(`LIKE search returned ${results.length} results`); logger.debug(`LIKE search returned ${results.length} results`);
return results.map(t => this.decompressWorkflow(t)); return results.map(t => this.decompressWorkflow(t));
@@ -292,7 +292,7 @@ export class TemplateRepository {
/** /**
* Get templates for a specific task/use case * Get templates for a specific task/use case
*/ */
getTemplatesForTask(task: string): StoredTemplate[] { getTemplatesForTask(task: string, limit: number = 10, offset: number = 0): StoredTemplate[] {
// Map tasks to relevant node combinations // Map tasks to relevant node combinations
const taskNodeMap: Record<string, string[]> = { const taskNodeMap: Record<string, string[]> = {
'ai_automation': ['@n8n/n8n-nodes-langchain.openAi', '@n8n/n8n-nodes-langchain.agent', 'n8n-nodes-base.openAi'], 'ai_automation': ['@n8n/n8n-nodes-langchain.openAi', '@n8n/n8n-nodes-langchain.agent', 'n8n-nodes-base.openAi'],
@@ -312,18 +312,21 @@ export class TemplateRepository {
return []; return [];
} }
return this.getTemplatesByNodes(nodes, 10); return this.getTemplatesByNodes(nodes, limit, offset);
} }
/** /**
* Get all templates with limit * Get all templates with limit
*/ */
getAllTemplates(limit: number = 10): StoredTemplate[] { getAllTemplates(limit: number = 10, offset: number = 0, sortBy: 'views' | 'created_at' | 'name' = 'views'): StoredTemplate[] {
const orderClause = sortBy === 'name' ? 'name ASC' :
sortBy === 'created_at' ? 'created_at DESC' :
'views DESC, created_at DESC';
const results = this.db.prepare(` const results = this.db.prepare(`
SELECT * FROM templates SELECT * FROM templates
ORDER BY views DESC, created_at DESC ORDER BY ${orderClause}
LIMIT ? LIMIT ? OFFSET ?
`).all(limit) as StoredTemplate[]; `).all(limit, offset) as StoredTemplate[];
return results.map(t => this.decompressWorkflow(t)); return results.map(t => this.decompressWorkflow(t));
} }
@@ -335,6 +338,77 @@ export class TemplateRepository {
return result.count; return result.count;
} }
/**
* Get count for search results
*/
getSearchCount(query: string): number {
if (!this.hasFTS5Support) {
const likeQuery = `%${query}%`;
const result = this.db.prepare(`
SELECT COUNT(*) as count FROM templates
WHERE name LIKE ? OR description LIKE ?
`).get(likeQuery, likeQuery) as { count: number };
return result.count;
}
try {
const ftsQuery = query.split(' ').map(term => {
const escaped = term.replace(/"/g, '""');
return `"${escaped}"`;
}).join(' OR ');
const result = this.db.prepare(`
SELECT COUNT(*) as count FROM templates t
JOIN templates_fts ON t.id = templates_fts.rowid
WHERE templates_fts MATCH ?
`).get(ftsQuery) as { count: number };
return result.count;
} catch {
const likeQuery = `%${query}%`;
const result = this.db.prepare(`
SELECT COUNT(*) as count FROM templates
WHERE name LIKE ? OR description LIKE ?
`).get(likeQuery, likeQuery) as { count: number };
return result.count;
}
}
/**
* Get count for node templates
*/
getNodeTemplatesCount(nodeTypes: string[]): number {
const conditions = nodeTypes.map(() => "nodes_used LIKE ?").join(" OR ");
const query = `SELECT COUNT(*) as count FROM templates WHERE ${conditions}`;
const params = nodeTypes.map(n => `%"${n}"%`);
const result = this.db.prepare(query).get(...params) as { count: number };
return result.count;
}
/**
* Get count for task templates
*/
getTaskTemplatesCount(task: string): number {
const taskNodeMap: Record<string, string[]> = {
'ai_automation': ['@n8n/n8n-nodes-langchain.openAi', '@n8n/n8n-nodes-langchain.agent', 'n8n-nodes-base.openAi'],
'data_sync': ['n8n-nodes-base.googleSheets', 'n8n-nodes-base.postgres', 'n8n-nodes-base.mysql'],
'webhook_processing': ['n8n-nodes-base.webhook', 'n8n-nodes-base.httpRequest'],
'email_automation': ['n8n-nodes-base.gmail', 'n8n-nodes-base.emailSend', 'n8n-nodes-base.emailReadImap'],
'slack_integration': ['n8n-nodes-base.slack', 'n8n-nodes-base.slackTrigger'],
'data_transformation': ['n8n-nodes-base.code', 'n8n-nodes-base.set', 'n8n-nodes-base.merge'],
'file_processing': ['n8n-nodes-base.readBinaryFile', 'n8n-nodes-base.writeBinaryFile', 'n8n-nodes-base.googleDrive'],
'scheduling': ['n8n-nodes-base.scheduleTrigger', 'n8n-nodes-base.cron'],
'api_integration': ['n8n-nodes-base.httpRequest', 'n8n-nodes-base.graphql'],
'database_operations': ['n8n-nodes-base.postgres', 'n8n-nodes-base.mysql', 'n8n-nodes-base.mongodb']
};
const nodes = taskNodeMap[task];
if (!nodes) {
return 0;
}
return this.getNodeTemplatesCount(nodes);
}
/** /**
* Get all existing template IDs for comparison * Get all existing template IDs for comparison
* Used in update mode to skip already fetched templates * Used in update mode to skip already fetched templates

View File

@@ -21,6 +21,21 @@ export interface TemplateWithWorkflow extends TemplateInfo {
workflow: any; workflow: any;
} }
export interface PaginatedResponse<T> {
items: T[];
total: number;
limit: number;
offset: number;
hasMore: boolean;
}
export interface TemplateMinimal {
id: number;
name: string;
views: number;
nodeCount: number;
}
export class TemplateService { export class TemplateService {
private repository: TemplateRepository; private repository: TemplateRepository;
@@ -31,40 +46,115 @@ export class TemplateService {
/** /**
* List templates that use specific node types * List templates that use specific node types
*/ */
async listNodeTemplates(nodeTypes: string[], limit: number = 10): Promise<TemplateInfo[]> { async listNodeTemplates(nodeTypes: string[], limit: number = 10, offset: number = 0): Promise<PaginatedResponse<TemplateInfo>> {
const templates = this.repository.getTemplatesByNodes(nodeTypes, limit); const templates = this.repository.getTemplatesByNodes(nodeTypes, limit, offset);
return templates.map(this.formatTemplateInfo); const total = this.repository.getNodeTemplatesCount(nodeTypes);
return {
items: templates.map(this.formatTemplateInfo),
total,
limit,
offset,
hasMore: offset + limit < total
};
} }
/** /**
* Get a specific template with full workflow * Get a specific template with different detail levels
*/ */
async getTemplate(templateId: number): Promise<TemplateWithWorkflow | null> { async getTemplate(templateId: number, mode: 'nodes_only' | 'structure' | 'full' = 'full'): Promise<any> {
const template = this.repository.getTemplate(templateId); const template = this.repository.getTemplate(templateId);
if (!template) { if (!template) {
return null; return null;
} }
const workflow = JSON.parse(template.workflow_json || '{}');
if (mode === 'nodes_only') {
return {
id: template.id,
name: template.name,
nodes: workflow.nodes?.map((n: any) => ({
type: n.type,
name: n.name
})) || []
};
}
if (mode === 'structure') {
return {
id: template.id,
name: template.name,
nodes: workflow.nodes?.map((n: any) => ({
id: n.id,
type: n.type,
name: n.name,
position: n.position
})) || [],
connections: workflow.connections || {}
};
}
// Full mode
return { return {
...this.formatTemplateInfo(template), ...this.formatTemplateInfo(template),
workflow: JSON.parse(template.workflow_json || '{}') workflow
}; };
} }
/** /**
* Search templates by query * Search templates by query
*/ */
async searchTemplates(query: string, limit: number = 20): Promise<TemplateInfo[]> { async searchTemplates(query: string, limit: number = 20, offset: number = 0): Promise<PaginatedResponse<TemplateInfo>> {
const templates = this.repository.searchTemplates(query, limit); const templates = this.repository.searchTemplates(query, limit, offset);
return templates.map(this.formatTemplateInfo); const total = this.repository.getSearchCount(query);
return {
items: templates.map(this.formatTemplateInfo),
total,
limit,
offset,
hasMore: offset + limit < total
};
} }
/** /**
* Get templates for a specific task * Get templates for a specific task
*/ */
async getTemplatesForTask(task: string): Promise<TemplateInfo[]> { async getTemplatesForTask(task: string, limit: number = 10, offset: number = 0): Promise<PaginatedResponse<TemplateInfo>> {
const templates = this.repository.getTemplatesForTask(task); const templates = this.repository.getTemplatesForTask(task, limit, offset);
return templates.map(this.formatTemplateInfo); const total = this.repository.getTaskTemplatesCount(task);
return {
items: templates.map(this.formatTemplateInfo),
total,
limit,
offset,
hasMore: offset + limit < total
};
}
/**
* List all templates with minimal data
*/
async listTemplates(limit: number = 10, offset: number = 0, sortBy: 'views' | 'created_at' | 'name' = 'views'): Promise<PaginatedResponse<TemplateMinimal>> {
const templates = this.repository.getAllTemplates(limit, offset, sortBy);
const total = this.repository.getTemplateCount();
const items = templates.map(t => ({
id: t.id,
name: t.name,
views: t.views,
nodeCount: JSON.parse(t.nodes_used).length
}));
return {
items,
total,
limit,
offset,
hasMore: offset + limit < total
};
} }
/** /**