import { Logger } from '@n8n/backend-common'; import { WorkflowPublishedVersionRepository, type PublishedWorkflowDataForExecution, type WorkflowEntity, type WorkflowHistory, } from '@n8n/db'; import { Service } from '@n8n/di'; import { ensureError } from '@n8n/utils/errors/ensure-error'; import { CacheService } from '@/services/cache/cache.service'; export type { PublishedWorkflowDataForExecution }; export type PublishedWorkflowData = { workflow: WorkflowEntity; publishedVersion: WorkflowHistory; }; const NO_EXPIRY = 0; const cacheKey = (workflowId: string) => `workflow-published-data:${workflowId}`; @Service() export class WorkflowPublishedDataService { constructor( private readonly logger: Logger, private readonly workflowPublishedVersionRepository: WorkflowPublishedVersionRepository, private readonly cacheService: CacheService, ) { this.logger = this.logger.scoped('poll-trigger'); } /** * Resolves a workflow's published version from the database: the workflow * entity and the `WorkflowHistory` row that the `workflow_published_version` * mapping currently points at, or `null` when there is no published version. */ async getPublishedWorkflowData(workflowId: string): Promise { const record = await this.workflowPublishedVersionRepository.getPublishedVersionWithRelations(workflowId); if (!record?.publishedVersion || !record.workflow) { return null; } return { workflow: record.workflow, publishedVersion: record.publishedVersion }; } async getPublishedWorkflowDataForExecution( workflowId: string, ): Promise { return await this.workflowPublishedVersionRepository.getPublishedVersionForExecution( workflowId, ); } /** * Get the published workflow data from the cache, falling back to the database if not found. * The cache is not refreshed here on a miss. Only the publication applier should refresh the * cache to ensure it never disagrees with the database. The cached value is a small execution * payload, not a full WorkflowEntity with relations. */ async getCachedPublishedWorkflowDataForExecution( workflowId: string, ): Promise { try { const cached = await this.cacheService.get( cacheKey(workflowId), ); if (cached) return cached; } catch (error) { this.logger.warn('Failed to read published-version cache; falling back to the database', { workflowId, error: ensureError(error).message, }); } return await this.getPublishedWorkflowDataForExecution(workflowId); } /** * Drops the cached entry. */ async invalidateCache(workflowId: string): Promise { await this.cacheService.delete(cacheKey(workflowId)); } /** * Repopulates the cached entry from current database state. * If there is no published version, the cache entry is deleted. * This should only be called by the publication applier to ensure the cache never disagrees with the database. */ async refreshCache(workflowId: string): Promise { const key = cacheKey(workflowId); const data = await this.getPublishedWorkflowDataForExecution(workflowId); if (data) { await this.cacheService.set(key, data, NO_EXPIRY); } else { await this.cacheService.delete(key); } } }