Introduce webhook v2 (#17456)

Migrate webhook to v2 entity
This commit is contained in:
Charles Bochet
2026-01-27 16:32:33 +01:00
committed by GitHub
parent 27e5b9797b
commit bc7791871f
83 changed files with 1574 additions and 975 deletions
@@ -0,0 +1,8 @@
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
export const FLAT_WEBHOOK_EDITABLE_PROPERTIES = [
'targetUrl',
'operations',
'description',
'secret',
] as const satisfies (keyof FlatWebhook)[];
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatWebhookMapCacheService } from 'src/engine/metadata-modules/flat-webhook/services/workspace-flat-webhook-map-cache.service';
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
@Module({
imports: [
TypeOrmModule.forFeature([WebhookEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatWebhookMapCacheService],
exports: [WorkspaceFlatWebhookMapCacheService],
})
export class FlatWebhookModule {}
@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatWebhookMaps } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook-maps.type';
import { fromWebhookEntityToFlatWebhook } from 'src/engine/metadata-modules/flat-webhook/utils/from-webhook-entity-to-flat-webhook.util';
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatWebhookMaps')
export class WorkspaceFlatWebhookMapCacheService extends WorkspaceCacheProvider<FlatWebhookMaps> {
constructor(
@InjectRepository(WebhookEntity)
private readonly webhookRepository: Repository<WebhookEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatWebhookMaps> {
const webhooks = await this.webhookRepository.find({
where: { workspaceId, deletedAt: IsNull() },
});
const flatWebhookMaps = createEmptyFlatEntityMaps();
for (const webhookEntity of webhooks) {
const flatWebhook = fromWebhookEntityToFlatWebhook(webhookEntity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatWebhook,
flatEntityMapsToMutate: flatWebhookMaps,
});
}
return flatWebhookMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
export type FlatWebhookMaps = FlatEntityMaps<FlatWebhook>;
@@ -0,0 +1,4 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
export type FlatWebhook = FlatEntityFrom<WebhookEntity>;
@@ -0,0 +1,41 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
import { type CreateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/create-webhook.input';
import { generateWebhookSecret } from 'src/engine/metadata-modules/webhook/utils/generate-webhook-secret.util';
export const fromCreateWebhookInputToFlatWebhookToCreate = ({
createWebhookInput,
workspaceId,
applicationId,
}: {
createWebhookInput: CreateWebhookInput;
workspaceId: string;
applicationId: string;
}): FlatWebhook => {
const now = new Date().toISOString();
const { targetUrl, description } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
createWebhookInput,
['targetUrl', 'description'],
);
const id = createWebhookInput.id ?? v4();
const secret = createWebhookInput.secret ?? generateWebhookSecret();
return {
id,
targetUrl,
operations: createWebhookInput.operations,
description: description ?? null,
secret,
workspaceId,
createdAt: now,
updatedAt: now,
deletedAt: null,
universalIdentifier: id,
applicationId,
};
};
@@ -0,0 +1,31 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
import {
WebhookException,
WebhookExceptionCode,
} from 'src/engine/metadata-modules/webhook/webhook.exception';
export const fromDeleteWebhookInputToFlatWebhookOrThrow = ({
flatWebhookMaps,
webhookId,
}: {
flatWebhookMaps: FlatEntityMaps<FlatWebhook>;
webhookId: string;
}): FlatWebhook => {
const existingFlatWebhook = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: webhookId,
flatEntityMaps: flatWebhookMaps,
});
if (!isDefined(existingFlatWebhook)) {
throw new WebhookException(
'Webhook not found',
WebhookExceptionCode.WEBHOOK_NOT_FOUND,
);
}
return existingFlatWebhook;
};
@@ -0,0 +1,17 @@
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
import { type WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhook.dto';
export const fromFlatWebhookToWebhookDto = (
flatWebhook: FlatWebhook,
): WebhookDTO => ({
id: flatWebhook.id,
targetUrl: flatWebhook.targetUrl,
operations: flatWebhook.operations,
description: flatWebhook.description,
secret: flatWebhook.secret,
workspaceId: flatWebhook.workspaceId,
applicationId: flatWebhook.applicationId,
createdAt: new Date(flatWebhook.createdAt),
updatedAt: new Date(flatWebhook.updatedAt),
deletedAt: flatWebhook.deletedAt ? new Date(flatWebhook.deletedAt) : null,
});
@@ -0,0 +1,41 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { FLAT_WEBHOOK_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-webhook/constants/flat-webhook-editable-properties.constant';
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
import { type UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input';
import {
WebhookException,
WebhookExceptionCode,
} from 'src/engine/metadata-modules/webhook/webhook.exception';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdateWebhookInputToFlatWebhookToUpdateOrThrow = ({
flatWebhookMaps,
updateWebhookInput,
}: {
flatWebhookMaps: FlatEntityMaps<FlatWebhook>;
updateWebhookInput: UpdateWebhookInput;
}): FlatWebhook => {
const existingFlatWebhook = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updateWebhookInput.id,
flatEntityMaps: flatWebhookMaps,
});
if (!isDefined(existingFlatWebhook)) {
throw new WebhookException(
'Webhook not found',
WebhookExceptionCode.WEBHOOK_NOT_FOUND,
);
}
return {
...mergeUpdateInExistingRecord({
existing: existingFlatWebhook,
properties: [...FLAT_WEBHOOK_EDITABLE_PROPERTIES],
update: updateWebhookInput.update,
}),
updatedAt: new Date().toISOString(),
};
};
@@ -0,0 +1,20 @@
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
import { type WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
export const fromWebhookEntityToFlatWebhook = (
webhookEntity: WebhookEntity,
): FlatWebhook => {
return {
id: webhookEntity.id,
targetUrl: webhookEntity.targetUrl,
operations: webhookEntity.operations,
description: webhookEntity.description,
secret: webhookEntity.secret,
workspaceId: webhookEntity.workspaceId,
universalIdentifier: webhookEntity.universalIdentifier,
applicationId: webhookEntity.applicationId,
createdAt: webhookEntity.createdAt.toISOString(),
updatedAt: webhookEntity.updatedAt.toISOString(),
deletedAt: webhookEntity.deletedAt?.toISOString() ?? null,
};
};