mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 06:22:04 +00:00
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:
BIN
data/nodes.db
BIN
data/nodes.db
Binary file not shown.
@@ -725,21 +725,32 @@ export class N8NDocumentationMCPServer {
|
||||
case 'get_node_as_tool_info':
|
||||
this.validateToolParams(name, 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':
|
||||
this.validateToolParams(name, args, ['nodeTypes']);
|
||||
const templateLimit = args.limit !== undefined ? Number(args.limit) || 10 : 10;
|
||||
return this.listNodeTemplates(args.nodeTypes, templateLimit);
|
||||
const templateLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 100);
|
||||
const templateOffset = Math.max(Number(args.offset) || 0, 0);
|
||||
return this.listNodeTemplates(args.nodeTypes, templateLimit, templateOffset);
|
||||
case 'get_template':
|
||||
this.validateToolParams(name, args, ['templateId']);
|
||||
const templateId = Number(args.templateId);
|
||||
return this.getTemplate(templateId);
|
||||
const mode = args.mode || 'full';
|
||||
return this.getTemplate(templateId, mode);
|
||||
case 'search_templates':
|
||||
this.validateToolParams(name, args, ['query']);
|
||||
const searchLimit = args.limit !== undefined ? Number(args.limit) || 20 : 20;
|
||||
return this.searchTemplates(args.query, searchLimit);
|
||||
const searchLimit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
|
||||
const searchOffset = Math.max(Number(args.offset) || 0, 0);
|
||||
return this.searchTemplates(args.query, searchLimit, searchOffset);
|
||||
case 'get_templates_for_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':
|
||||
this.validateToolParams(name, args, ['workflow']);
|
||||
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
|
||||
`).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 {
|
||||
totalNodes: stats.total,
|
||||
totalTemplates: templateStats.total_templates || 0,
|
||||
statistics: {
|
||||
aiTools: stats.ai_tools,
|
||||
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) + '%',
|
||||
uniquePackages: stats.packages,
|
||||
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 => ({
|
||||
package: pkg.package_name,
|
||||
@@ -2300,76 +2328,95 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
}
|
||||
|
||||
// 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();
|
||||
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 {
|
||||
...result,
|
||||
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",
|
||||
templates: []
|
||||
tip: "Try searching with more common nodes or run 'npm run fetch:templates' to update template database"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
templates,
|
||||
count: templates.length,
|
||||
tip: `Use get_template(templateId) to get the full workflow JSON for any template`
|
||||
...result,
|
||||
tip: `Showing ${result.items.length} of ${result.total} templates. Use offset for pagination.`
|
||||
};
|
||||
}
|
||||
|
||||
private async getTemplate(templateId: number): Promise<any> {
|
||||
private async getTemplate(templateId: number, mode: 'nodes_only' | 'structure' | 'full' = 'full'): Promise<any> {
|
||||
await this.ensureInitialized();
|
||||
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) {
|
||||
return {
|
||||
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 {
|
||||
mode,
|
||||
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();
|
||||
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 {
|
||||
...result,
|
||||
message: `No templates found matching: "${query}"`,
|
||||
tip: "Try different keywords or run 'npm run fetch:templates' to update template database",
|
||||
templates: []
|
||||
tip: "Try different keywords or run 'npm run fetch:templates' to update template database"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
templates,
|
||||
count: templates.length,
|
||||
query
|
||||
...result,
|
||||
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();
|
||||
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();
|
||||
|
||||
if (templates.length === 0) {
|
||||
if (result.items.length === 0 && offset === 0) {
|
||||
return {
|
||||
...result,
|
||||
message: `No templates found for task: ${task}`,
|
||||
availableTasks,
|
||||
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 {
|
||||
...result,
|
||||
task,
|
||||
templates,
|
||||
count: templates.length,
|
||||
description: this.getTaskDescription(task)
|
||||
description: this.getTaskDescription(task),
|
||||
tip: `${result.total} templates available for ${task}. Showing ${result.items.length}.`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -323,9 +323,37 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
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',
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -338,6 +366,14 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
type: 'number',
|
||||
description: 'Maximum number of templates to return. Default 10.',
|
||||
default: 10,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'Pagination offset. Default 0.',
|
||||
default: 0,
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
required: ['nodeTypes'],
|
||||
@@ -345,7 +381,7 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -353,13 +389,19 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
type: 'number',
|
||||
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'],
|
||||
},
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -371,6 +413,14 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
type: 'number',
|
||||
description: 'Maximum number of results. Default 20.',
|
||||
default: 20,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'Pagination offset. Default 0.',
|
||||
default: 0,
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
@@ -378,7 +428,7 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -398,6 +448,19 @@ export const n8nDocumentationToolsFinal: ToolDefinition[] = [
|
||||
],
|
||||
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'],
|
||||
},
|
||||
|
||||
@@ -173,17 +173,17 @@ export class TemplateRepository {
|
||||
/**
|
||||
* 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
|
||||
const conditions = nodeTypes.map(() => "nodes_used LIKE ?").join(" OR ");
|
||||
const query = `
|
||||
SELECT * FROM templates
|
||||
WHERE ${conditions}
|
||||
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[];
|
||||
return results.map(t => this.decompressWorkflow(t));
|
||||
}
|
||||
@@ -232,13 +232,13 @@ export class TemplateRepository {
|
||||
/**
|
||||
* 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})`);
|
||||
|
||||
// If FTS5 is not supported, go straight to LIKE search
|
||||
if (!this.hasFTS5Support) {
|
||||
logger.debug('Using LIKE search (FTS5 not available)');
|
||||
return this.searchTemplatesLIKE(query, limit);
|
||||
return this.searchTemplatesLIKE(query, limit, offset);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -255,8 +255,8 @@ export class TemplateRepository {
|
||||
JOIN templates_fts ON t.id = templates_fts.rowid
|
||||
WHERE templates_fts MATCH ?
|
||||
ORDER BY rank, t.views DESC
|
||||
LIMIT ?
|
||||
`).all(ftsQuery, limit) as StoredTemplate[];
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(ftsQuery, limit, offset) as StoredTemplate[];
|
||||
|
||||
logger.debug(`FTS5 search returned ${results.length} results`);
|
||||
return results.map(t => this.decompressWorkflow(t));
|
||||
@@ -267,14 +267,14 @@ export class TemplateRepository {
|
||||
query: query,
|
||||
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
|
||||
*/
|
||||
private searchTemplatesLIKE(query: string, limit: number = 20): StoredTemplate[] {
|
||||
private searchTemplatesLIKE(query: string, limit: number = 20, offset: number = 0): StoredTemplate[] {
|
||||
const likeQuery = `%${query}%`;
|
||||
logger.debug(`Using LIKE search with pattern: ${likeQuery}`);
|
||||
|
||||
@@ -282,8 +282,8 @@ export class TemplateRepository {
|
||||
SELECT * FROM templates
|
||||
WHERE name LIKE ? OR description LIKE ?
|
||||
ORDER BY views DESC, created_at DESC
|
||||
LIMIT ?
|
||||
`).all(likeQuery, likeQuery, limit) as StoredTemplate[];
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(likeQuery, likeQuery, limit, offset) as StoredTemplate[];
|
||||
|
||||
logger.debug(`LIKE search returned ${results.length} results`);
|
||||
return results.map(t => this.decompressWorkflow(t));
|
||||
@@ -292,7 +292,7 @@ export class TemplateRepository {
|
||||
/**
|
||||
* 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
|
||||
const taskNodeMap: Record<string, string[]> = {
|
||||
'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 this.getTemplatesByNodes(nodes, 10);
|
||||
return this.getTemplatesByNodes(nodes, limit, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(`
|
||||
SELECT * FROM templates
|
||||
ORDER BY views DESC, created_at DESC
|
||||
LIMIT ?
|
||||
`).all(limit) as StoredTemplate[];
|
||||
ORDER BY ${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(limit, offset) as StoredTemplate[];
|
||||
return results.map(t => this.decompressWorkflow(t));
|
||||
}
|
||||
|
||||
@@ -335,6 +338,77 @@ export class TemplateRepository {
|
||||
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
|
||||
* Used in update mode to skip already fetched templates
|
||||
|
||||
@@ -21,6 +21,21 @@ export interface TemplateWithWorkflow extends TemplateInfo {
|
||||
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 {
|
||||
private repository: TemplateRepository;
|
||||
|
||||
@@ -31,40 +46,115 @@ export class TemplateService {
|
||||
/**
|
||||
* List templates that use specific node types
|
||||
*/
|
||||
async listNodeTemplates(nodeTypes: string[], limit: number = 10): Promise<TemplateInfo[]> {
|
||||
const templates = this.repository.getTemplatesByNodes(nodeTypes, limit);
|
||||
return templates.map(this.formatTemplateInfo);
|
||||
async listNodeTemplates(nodeTypes: string[], limit: number = 10, offset: number = 0): Promise<PaginatedResponse<TemplateInfo>> {
|
||||
const templates = this.repository.getTemplatesByNodes(nodeTypes, limit, offset);
|
||||
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);
|
||||
if (!template) {
|
||||
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 {
|
||||
...this.formatTemplateInfo(template),
|
||||
workflow: JSON.parse(template.workflow_json || '{}')
|
||||
workflow
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search templates by query
|
||||
*/
|
||||
async searchTemplates(query: string, limit: number = 20): Promise<TemplateInfo[]> {
|
||||
const templates = this.repository.searchTemplates(query, limit);
|
||||
return templates.map(this.formatTemplateInfo);
|
||||
async searchTemplates(query: string, limit: number = 20, offset: number = 0): Promise<PaginatedResponse<TemplateInfo>> {
|
||||
const templates = this.repository.searchTemplates(query, limit, offset);
|
||||
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
|
||||
*/
|
||||
async getTemplatesForTask(task: string): Promise<TemplateInfo[]> {
|
||||
const templates = this.repository.getTemplatesForTask(task);
|
||||
return templates.map(this.formatTemplateInfo);
|
||||
async getTemplatesForTask(task: string, limit: number = 10, offset: number = 0): Promise<PaginatedResponse<TemplateInfo>> {
|
||||
const templates = this.repository.getTemplatesForTask(task, limit, offset);
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user