Files
n8n-enterprise-unlocked/packages/cli/src/services/webhook.service.ts

121 lines
3.4 KiB
TypeScript

import { WebhookRepository } from '@db/repositories/webhook.repository';
import { Service } from 'typedi';
import { CacheService } from './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));
}
}