mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2026-06-11 09:47:54 +02:00
f53c482939
Story: https://linear.app/n8n/issue/PAY-1188 - Implement Redis hashes on the caching service, based on Micha's work in #7747, adapted from `node-cache-manager-ioredis-yet`. Optimize workflow ownership lookups and manual webhook lookups with Redis hashes. - Simplify the caching service by removing all currently unused methods and options: `enable`, `disable`, `getCache`, `keys`, `keyValues`, `refreshFunctionEach`, `refreshFunctionMany`, `refreshTtl`, etc. - Remove the flag `N8N_CACHE_ENABLED`. Currently some features on `master` are broken with caching disabled, and test webhooks now rely entirely on caching, for multi-main setup support. We originally introduced this flag to protect against excessive memory usage, but total cache usage is low enough that we decided to drop this setting. Apparently this flag was also never documented. - Overall caching service refactor: use generics, reduce branching, add discriminants for cache kinds for better type safety, type caching events, improve readability, remove outdated docs, etc. Also refactor and expand caching service tests. Follow-up to: https://github.com/n8n-io/n8n/pull/8176 --------- Co-authored-by: Michael Auerswald <michael.auerswald@gmail.com>
121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
import { WebhookRepository } from '@db/repositories/webhook.repository';
|
|
import { Service } from 'typedi';
|
|
import { CacheService } from '@/services/cache/cache.service';
|
|
import type { WebhookEntity } from '@db/entities/WebhookEntity';
|
|
import type { IHttpRequestMethods } from 'n8n-workflow';
|
|
|
|
type Method = NonNullable<IHttpRequestMethods>;
|
|
|
|
@Service()
|
|
export class WebhookService {
|
|
constructor(
|
|
private webhookRepository: WebhookRepository,
|
|
private cacheService: CacheService,
|
|
) {}
|
|
|
|
async populateCache() {
|
|
const allWebhooks = await this.webhookRepository.find();
|
|
|
|
if (!allWebhooks) return;
|
|
|
|
void this.cacheService.setMany(allWebhooks.map((w) => [w.cacheKey, w]));
|
|
}
|
|
|
|
private async findCached(method: Method, path: string) {
|
|
const cacheKey = `webhook:${method}-${path}`;
|
|
|
|
const cachedWebhook = await this.cacheService.get(cacheKey);
|
|
|
|
if (cachedWebhook) return this.webhookRepository.create(cachedWebhook);
|
|
|
|
let dbWebhook = await this.findStaticWebhook(method, path);
|
|
|
|
if (dbWebhook === null) {
|
|
dbWebhook = await this.findDynamicWebhook(method, path);
|
|
}
|
|
|
|
void this.cacheService.set(cacheKey, dbWebhook);
|
|
|
|
return dbWebhook;
|
|
}
|
|
|
|
/**
|
|
* Find a matching webhook with zero dynamic path segments, e.g. `<uuid>` or `user/profile`.
|
|
*/
|
|
private async findStaticWebhook(method: Method, path: string) {
|
|
return this.webhookRepository.findOneBy({ webhookPath: path, method });
|
|
}
|
|
|
|
/**
|
|
* Find a matching webhook with one or more dynamic path segments, e.g. `<uuid>/user/:id/posts`.
|
|
* It is mandatory for dynamic webhooks to have `<uuid>/` at the base.
|
|
*/
|
|
private async findDynamicWebhook(method: Method, path: string) {
|
|
const [uuidSegment, ...otherSegments] = path.split('/');
|
|
|
|
const dynamicWebhooks = await this.webhookRepository.findBy({
|
|
webhookId: uuidSegment,
|
|
method,
|
|
pathLength: otherSegments.length,
|
|
});
|
|
|
|
if (dynamicWebhooks.length === 0) return null;
|
|
|
|
const requestSegments = new Set(otherSegments);
|
|
|
|
const { webhook } = dynamicWebhooks.reduce<{
|
|
webhook: WebhookEntity | null;
|
|
maxMatches: number;
|
|
}>(
|
|
(acc, dw) => {
|
|
const allStaticSegmentsMatch = dw.staticSegments.every((s) => requestSegments.has(s));
|
|
|
|
if (allStaticSegmentsMatch && dw.staticSegments.length > acc.maxMatches) {
|
|
acc.maxMatches = dw.staticSegments.length;
|
|
acc.webhook = dw;
|
|
return acc;
|
|
} else if (dw.staticSegments.length === 0 && !acc.webhook) {
|
|
acc.webhook = dw; // edge case: if path is `:var`, match on anything
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{ webhook: null, maxMatches: 0 },
|
|
);
|
|
|
|
return webhook;
|
|
}
|
|
|
|
async findWebhook(method: Method, path: string) {
|
|
return this.findCached(method, path);
|
|
}
|
|
|
|
async storeWebhook(webhook: WebhookEntity) {
|
|
void this.cacheService.set(webhook.cacheKey, webhook);
|
|
|
|
return this.webhookRepository.insert(webhook);
|
|
}
|
|
|
|
createWebhook(data: Partial<WebhookEntity>) {
|
|
return this.webhookRepository.create(data);
|
|
}
|
|
|
|
async deleteWorkflowWebhooks(workflowId: string) {
|
|
const webhooks = await this.webhookRepository.findBy({ workflowId });
|
|
|
|
return this.deleteWebhooks(webhooks);
|
|
}
|
|
|
|
private async deleteWebhooks(webhooks: WebhookEntity[]) {
|
|
void this.cacheService.deleteMany(webhooks.map((w) => w.cacheKey));
|
|
|
|
return this.webhookRepository.remove(webhooks);
|
|
}
|
|
|
|
async getWebhookMethods(path: string) {
|
|
return this.webhookRepository
|
|
.find({ select: ['method'], where: { webhookPath: path } })
|
|
.then((rows) => rows.map((r) => r.method));
|
|
}
|
|
}
|