mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
perf(core): Cache webhooks (#6825)
* refactor: Initial setup * Refactor for clarity * Comments to clarify * More replacements * Simplify with `fullPath` * Fix tests * Implement remaining methods * chore: Fix misresolved conflicts * Simplify syntax * Reduce diff * Minor cleanup * Fix lint * Inject dependency * Improve typings * Remove unused method * Restore method * Add comment * Rename in test * Restore comments * Clean up dynamic webhook handling * Clean up tests * Remove redundant `cache` prefix * fix: Correct `uniquePath` for dynamic webhooks
This commit is contained in:
127
packages/cli/src/services/webhook.service.ts
Normal file
127
packages/cli/src/services/webhook.service.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { WebhookRepository } from '@/databases/repositories';
|
||||
import { Service } from 'typedi';
|
||||
import { CacheService } from './cache.service';
|
||||
import type { WebhookEntity } from '@/databases/entities/WebhookEntity';
|
||||
import type { IHttpRequestMethods } from 'n8n-workflow';
|
||||
import type { DeepPartial } from 'typeorm';
|
||||
|
||||
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: DeepPartial<WebhookEntity>) {
|
||||
return this.webhookRepository.create(data);
|
||||
}
|
||||
|
||||
async deleteWorkflowWebhooks(workflowId: string) {
|
||||
const webhooks = await this.webhookRepository.findBy({ workflowId });
|
||||
|
||||
return this.deleteWebhooks(webhooks);
|
||||
}
|
||||
|
||||
async deleteInstanceWebhooks() {
|
||||
const webhooks = await this.webhookRepository.find();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user