Update n8n to 1.117.2 (#379)

This commit is contained in:
Romuald Członkowski
2025-10-28 08:55:20 +01:00
committed by GitHub
parent 18b8747005
commit 3f427f9528
9 changed files with 595 additions and 1530 deletions

View File

@@ -75,10 +75,15 @@ async function fetchTemplatesRobust() {
// Fetch detail
const detail = await fetcher.fetchTemplateDetail(template.id);
// Save immediately
repository.saveTemplate(template, detail);
saved++;
if (detail !== null) {
// Save immediately
repository.saveTemplate(template, detail);
saved++;
} else {
errors++;
console.error(`\n❌ Failed to fetch template ${template.id} (${template.name}) after retries`);
}
// Rate limiting
await new Promise(resolve => setTimeout(resolve, 200));

View File

@@ -40,7 +40,37 @@ export interface TemplateDetail {
export class TemplateFetcher {
private readonly baseUrl = 'https://api.n8n.io/api/templates';
private readonly pageSize = 250; // Maximum allowed by API
private readonly maxRetries = 3;
private readonly retryDelay = 1000; // 1 second base delay
/**
* Retry helper for API calls
*/
private async retryWithBackoff<T>(
fn: () => Promise<T>,
context: string,
maxRetries: number = this.maxRetries
): Promise<T | null> {
let lastError: any;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
if (attempt < maxRetries) {
const delay = this.retryDelay * attempt; // Exponential backoff
logger.warn(`${context} - Attempt ${attempt}/${maxRetries} failed, retrying in ${delay}ms...`);
await this.sleep(delay);
}
}
}
logger.error(`${context} - All ${maxRetries} attempts failed, skipping`, lastError);
return null;
}
/**
* Fetch all templates and filter to last 12 months
* This fetches ALL pages first, then applies date filter locally
@@ -73,93 +103,105 @@ export class TemplateFetcher {
let page = 1;
let hasMore = true;
let totalWorkflows = 0;
logger.info('Starting complete template fetch from n8n.io API');
while (hasMore) {
try {
const response = await axios.get(`${this.baseUrl}/search`, {
params: {
page,
rows: this.pageSize
// Note: sort_by parameter doesn't work, templates come in popularity order
}
});
const { workflows } = response.data;
totalWorkflows = response.data.totalWorkflows || totalWorkflows;
allTemplates.push(...workflows);
// Calculate total pages for better progress reporting
const totalPages = Math.ceil(totalWorkflows / this.pageSize);
if (progressCallback) {
// Enhanced progress with page information
progressCallback(allTemplates.length, totalWorkflows);
}
logger.debug(`Fetched page ${page}/${totalPages}: ${workflows.length} templates (total so far: ${allTemplates.length}/${totalWorkflows})`);
// Check if there are more pages
if (workflows.length < this.pageSize) {
hasMore = false;
}
const result = await this.retryWithBackoff(
async () => {
const response = await axios.get(`${this.baseUrl}/search`, {
params: {
page,
rows: this.pageSize
// Note: sort_by parameter doesn't work, templates come in popularity order
}
});
return response.data;
},
`Fetching templates page ${page}`
);
if (result === null) {
// All retries failed for this page, skip it and continue
logger.warn(`Skipping page ${page} after ${this.maxRetries} failed attempts`);
page++;
// Rate limiting - be nice to the API (slightly faster with 250 rows/page)
if (hasMore) {
await this.sleep(300); // 300ms between requests (was 500ms with 100 rows)
}
} catch (error) {
logger.error(`Error fetching templates page ${page}:`, error);
throw error;
continue;
}
const { workflows } = result;
totalWorkflows = result.totalWorkflows || totalWorkflows;
allTemplates.push(...workflows);
// Calculate total pages for better progress reporting
const totalPages = Math.ceil(totalWorkflows / this.pageSize);
if (progressCallback) {
// Enhanced progress with page information
progressCallback(allTemplates.length, totalWorkflows);
}
logger.debug(`Fetched page ${page}/${totalPages}: ${workflows.length} templates (total so far: ${allTemplates.length}/${totalWorkflows})`);
// Check if there are more pages
if (workflows.length < this.pageSize) {
hasMore = false;
}
page++;
// Rate limiting - be nice to the API (slightly faster with 250 rows/page)
if (hasMore) {
await this.sleep(300); // 300ms between requests (was 500ms with 100 rows)
}
}
logger.info(`Fetched all ${allTemplates.length} templates from n8n.io`);
return allTemplates;
}
async fetchTemplateDetail(workflowId: number): Promise<TemplateDetail> {
try {
const response = await axios.get(`${this.baseUrl}/workflows/${workflowId}`);
return response.data.workflow;
} catch (error) {
logger.error(`Error fetching template detail for ${workflowId}:`, error);
throw error;
}
async fetchTemplateDetail(workflowId: number): Promise<TemplateDetail | null> {
const result = await this.retryWithBackoff(
async () => {
const response = await axios.get(`${this.baseUrl}/workflows/${workflowId}`);
return response.data.workflow;
},
`Fetching template detail for workflow ${workflowId}`
);
return result;
}
async fetchAllTemplateDetails(
workflows: TemplateWorkflow[],
workflows: TemplateWorkflow[],
progressCallback?: (current: number, total: number) => void
): Promise<Map<number, TemplateDetail>> {
const details = new Map<number, TemplateDetail>();
let skipped = 0;
logger.info(`Fetching details for ${workflows.length} templates`);
for (let i = 0; i < workflows.length; i++) {
const workflow = workflows[i];
try {
const detail = await this.fetchTemplateDetail(workflow.id);
const detail = await this.fetchTemplateDetail(workflow.id);
if (detail !== null) {
details.set(workflow.id, detail);
if (progressCallback) {
progressCallback(i + 1, workflows.length);
}
// Rate limiting (conservative to avoid API throttling)
await this.sleep(150); // 150ms between requests
} catch (error) {
logger.error(`Failed to fetch details for workflow ${workflow.id}:`, error);
// Continue with other templates
} else {
skipped++;
logger.warn(`Skipped workflow ${workflow.id} after ${this.maxRetries} failed attempts`);
}
if (progressCallback) {
progressCallback(i + 1, workflows.length);
}
// Rate limiting (conservative to avoid API throttling)
await this.sleep(150); // 150ms between requests
}
logger.info(`Successfully fetched ${details.size} template details`);
logger.info(`Successfully fetched ${details.size} template details (${skipped} skipped)`);
return details;
}

View File

@@ -496,10 +496,17 @@ export class TemplateRepository {
// Count node usage
const nodeCount: Record<string, number> = {};
topNodes.forEach(t => {
const nodes = JSON.parse(t.nodes_used);
nodes.forEach((n: string) => {
nodeCount[n] = (nodeCount[n] || 0) + 1;
});
if (!t.nodes_used) return;
try {
const nodes = JSON.parse(t.nodes_used);
if (Array.isArray(nodes)) {
nodes.forEach((n: string) => {
nodeCount[n] = (nodeCount[n] || 0) + 1;
});
}
} catch (error) {
logger.warn(`Failed to parse nodes_used for template stats:`, error);
}
});
// Get top 10 most used nodes