@@ -207,7 +207,8 @@ export enum AllMetadataName {
|
||||
viewField = 'viewField',
|
||||
viewFilter = 'viewFilter',
|
||||
viewFilterGroup = 'viewFilterGroup',
|
||||
viewGroup = 'viewGroup'
|
||||
viewGroup = 'viewGroup',
|
||||
webhook = 'webhook'
|
||||
}
|
||||
|
||||
export type Analytics = {
|
||||
@@ -1148,6 +1149,7 @@ export type CreateViewSortInput = {
|
||||
|
||||
export type CreateWebhookInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
operations: Array<Scalars['String']>;
|
||||
secret?: InputMaybe<Scalars['String']>;
|
||||
targetUrl: Scalars['String'];
|
||||
@@ -1287,10 +1289,6 @@ export type DeleteViewGroupInput = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DeleteWebhookInput = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DeleteWorkflowVersionStepInput = {
|
||||
/** Step to delete ID */
|
||||
stepId: Scalars['String'];
|
||||
@@ -1691,10 +1689,6 @@ export type GetServerlessFunctionSourceCodeInput = {
|
||||
version?: Scalars['String'];
|
||||
};
|
||||
|
||||
export type GetWebhookInput = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
/** Order by options for graph widgets */
|
||||
export enum GraphOrderBy {
|
||||
FIELD_ASC = 'FIELD_ASC',
|
||||
@@ -2075,7 +2069,7 @@ export type Mutation = {
|
||||
deleteTwoFactorAuthenticationMethod: DeleteTwoFactorAuthenticationMethodOutput;
|
||||
deleteUser: User;
|
||||
deleteUserFromWorkspace: UserWorkspace;
|
||||
deleteWebhook: Scalars['Boolean'];
|
||||
deleteWebhook: Webhook;
|
||||
deleteWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
deleteWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deleteWorkspaceInvitation: Scalars['String'];
|
||||
@@ -2165,7 +2159,7 @@ export type Mutation = {
|
||||
updatePasswordViaResetToken: InvalidatePasswordOutput;
|
||||
updateSkill: Skill;
|
||||
updateUserEmail: Scalars['Boolean'];
|
||||
updateWebhook?: Maybe<Webhook>;
|
||||
updateWebhook: Webhook;
|
||||
updateWorkflowRunStep: WorkflowAction;
|
||||
updateWorkflowVersionPositions: Scalars['Boolean'];
|
||||
updateWorkflowVersionStep: WorkflowAction;
|
||||
@@ -2592,7 +2586,7 @@ export type MutationDeleteUserFromWorkspaceArgs = {
|
||||
|
||||
|
||||
export type MutationDeleteWebhookArgs = {
|
||||
input: DeleteWebhookInput;
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
@@ -4017,7 +4011,7 @@ export type QueryValidatePasswordResetTokenArgs = {
|
||||
|
||||
|
||||
export type QueryWebhookArgs = {
|
||||
input: GetWebhookInput;
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type QueueJob = {
|
||||
@@ -5001,8 +4995,14 @@ export type UpdateViewSortInput = {
|
||||
};
|
||||
|
||||
export type UpdateWebhookInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
/** The id of the webhook to update */
|
||||
id: Scalars['UUID'];
|
||||
/** The webhook fields to update */
|
||||
update: UpdateWebhookInputUpdates;
|
||||
};
|
||||
|
||||
export type UpdateWebhookInputUpdates = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
operations?: InputMaybe<Array<Scalars['String']>>;
|
||||
secret?: InputMaybe<Scalars['String']>;
|
||||
targetUrl?: InputMaybe<Scalars['String']>;
|
||||
@@ -5246,6 +5246,7 @@ export enum ViewVisibility {
|
||||
|
||||
export type Webhook = {
|
||||
__typename?: 'Webhook';
|
||||
applicationId: Scalars['UUID'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
@@ -6345,11 +6346,11 @@ export type CreateWebhookMutationVariables = Exact<{
|
||||
export type CreateWebhookMutation = { __typename?: 'Mutation', createWebhook: { __typename?: 'Webhook', id: string, targetUrl: string, operations: Array<string>, description?: string | null, secret: string } };
|
||||
|
||||
export type DeleteWebhookMutationVariables = Exact<{
|
||||
input: DeleteWebhookInput;
|
||||
id: Scalars['UUID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteWebhookMutation = { __typename?: 'Mutation', deleteWebhook: boolean };
|
||||
export type DeleteWebhookMutation = { __typename?: 'Mutation', deleteWebhook: { __typename?: 'Webhook', id: string, targetUrl: string, operations: Array<string>, description?: string | null, secret: string } };
|
||||
|
||||
export type RevokeApiKeyMutationVariables = Exact<{
|
||||
input: RevokeApiKeyInput;
|
||||
@@ -6370,7 +6371,7 @@ export type UpdateWebhookMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateWebhookMutation = { __typename?: 'Mutation', updateWebhook?: { __typename?: 'Webhook', id: string, targetUrl: string, operations: Array<string>, description?: string | null, secret: string } | null };
|
||||
export type UpdateWebhookMutation = { __typename?: 'Mutation', updateWebhook: { __typename?: 'Webhook', id: string, targetUrl: string, operations: Array<string>, description?: string | null, secret: string } };
|
||||
|
||||
export type GetApiKeyQueryVariables = Exact<{
|
||||
input: GetApiKeyInput;
|
||||
@@ -6385,7 +6386,7 @@ export type GetApiKeysQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
export type GetApiKeysQuery = { __typename?: 'Query', apiKeys: Array<{ __typename?: 'ApiKey', id: string, name: string, expiresAt: string, revokedAt?: string | null, role: { __typename?: 'Role', id: string, label: string, icon?: string | null } }> };
|
||||
|
||||
export type GetWebhookQueryVariables = Exact<{
|
||||
input: GetWebhookInput;
|
||||
id: Scalars['UUID'];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -11989,10 +11990,12 @@ export type CreateWebhookMutationHookResult = ReturnType<typeof useCreateWebhook
|
||||
export type CreateWebhookMutationResult = Apollo.MutationResult<CreateWebhookMutation>;
|
||||
export type CreateWebhookMutationOptions = Apollo.BaseMutationOptions<CreateWebhookMutation, CreateWebhookMutationVariables>;
|
||||
export const DeleteWebhookDocument = gql`
|
||||
mutation DeleteWebhook($input: DeleteWebhookInput!) {
|
||||
deleteWebhook(input: $input)
|
||||
mutation DeleteWebhook($id: UUID!) {
|
||||
deleteWebhook(id: $id) {
|
||||
...WebhookFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
${WebhookFragmentFragmentDoc}`;
|
||||
export type DeleteWebhookMutationFn = Apollo.MutationFunction<DeleteWebhookMutation, DeleteWebhookMutationVariables>;
|
||||
|
||||
/**
|
||||
@@ -12008,7 +12011,7 @@ export type DeleteWebhookMutationFn = Apollo.MutationFunction<DeleteWebhookMutat
|
||||
* @example
|
||||
* const [deleteWebhookMutation, { data, loading, error }] = useDeleteWebhookMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
@@ -12189,8 +12192,8 @@ export type GetApiKeysQueryHookResult = ReturnType<typeof useGetApiKeysQuery>;
|
||||
export type GetApiKeysLazyQueryHookResult = ReturnType<typeof useGetApiKeysLazyQuery>;
|
||||
export type GetApiKeysQueryResult = Apollo.QueryResult<GetApiKeysQuery, GetApiKeysQueryVariables>;
|
||||
export const GetWebhookDocument = gql`
|
||||
query GetWebhook($input: GetWebhookInput!) {
|
||||
webhook(input: $input) {
|
||||
query GetWebhook($id: UUID!) {
|
||||
webhook(id: $id) {
|
||||
...WebhookFragment
|
||||
}
|
||||
}
|
||||
@@ -12208,7 +12211,7 @@ export const GetWebhookDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useGetWebhookQuery({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -207,7 +207,8 @@ export enum AllMetadataName {
|
||||
viewField = 'viewField',
|
||||
viewFilter = 'viewFilter',
|
||||
viewFilterGroup = 'viewFilterGroup',
|
||||
viewGroup = 'viewGroup'
|
||||
viewGroup = 'viewGroup',
|
||||
webhook = 'webhook'
|
||||
}
|
||||
|
||||
export type Analytics = {
|
||||
@@ -1115,6 +1116,7 @@ export type CreateViewSortInput = {
|
||||
|
||||
export type CreateWebhookInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
operations: Array<Scalars['String']>;
|
||||
secret?: InputMaybe<Scalars['String']>;
|
||||
targetUrl: Scalars['String'];
|
||||
@@ -1254,10 +1256,6 @@ export type DeleteViewGroupInput = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DeleteWebhookInput = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DeleteWorkflowVersionStepInput = {
|
||||
/** Step to delete ID */
|
||||
stepId: Scalars['String'];
|
||||
@@ -1658,10 +1656,6 @@ export type GetServerlessFunctionSourceCodeInput = {
|
||||
version?: Scalars['String'];
|
||||
};
|
||||
|
||||
export type GetWebhookInput = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
/** Order by options for graph widgets */
|
||||
export enum GraphOrderBy {
|
||||
FIELD_ASC = 'FIELD_ASC',
|
||||
@@ -2033,7 +2027,7 @@ export type Mutation = {
|
||||
deleteTwoFactorAuthenticationMethod: DeleteTwoFactorAuthenticationMethodOutput;
|
||||
deleteUser: User;
|
||||
deleteUserFromWorkspace: UserWorkspace;
|
||||
deleteWebhook: Scalars['Boolean'];
|
||||
deleteWebhook: Webhook;
|
||||
deleteWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
deleteWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deleteWorkspaceInvitation: Scalars['String'];
|
||||
@@ -2118,7 +2112,7 @@ export type Mutation = {
|
||||
updatePageLayoutWithTabsAndWidgets: PageLayout;
|
||||
updatePasswordViaResetToken: InvalidatePasswordOutput;
|
||||
updateUserEmail: Scalars['Boolean'];
|
||||
updateWebhook?: Maybe<Webhook>;
|
||||
updateWebhook: Webhook;
|
||||
updateWorkflowRunStep: WorkflowAction;
|
||||
updateWorkflowVersionPositions: Scalars['Boolean'];
|
||||
updateWorkflowVersionStep: WorkflowAction;
|
||||
@@ -2500,7 +2494,7 @@ export type MutationDeleteUserFromWorkspaceArgs = {
|
||||
|
||||
|
||||
export type MutationDeleteWebhookArgs = {
|
||||
input: DeleteWebhookInput;
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
@@ -3820,7 +3814,7 @@ export type QueryValidatePasswordResetTokenArgs = {
|
||||
|
||||
|
||||
export type QueryWebhookArgs = {
|
||||
input: GetWebhookInput;
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type QueueJob = {
|
||||
@@ -4770,8 +4764,14 @@ export type UpdateViewSortInput = {
|
||||
};
|
||||
|
||||
export type UpdateWebhookInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
/** The id of the webhook to update */
|
||||
id: Scalars['UUID'];
|
||||
/** The webhook fields to update */
|
||||
update: UpdateWebhookInputUpdates;
|
||||
};
|
||||
|
||||
export type UpdateWebhookInputUpdates = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
operations?: InputMaybe<Array<Scalars['String']>>;
|
||||
secret?: InputMaybe<Scalars['String']>;
|
||||
targetUrl?: InputMaybe<Scalars['String']>;
|
||||
@@ -5015,6 +5015,7 @@ export enum ViewVisibility {
|
||||
|
||||
export type Webhook = {
|
||||
__typename?: 'Webhook';
|
||||
applicationId: Scalars['UUID'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
|
||||
+1
@@ -45,6 +45,7 @@ export const useMetadataErrorHandler = () => {
|
||||
commandMenuItem: t`command menu item`,
|
||||
frontComponent: t`front component`,
|
||||
navigationMenuItem: t`navigation menu item`,
|
||||
webhook: t`webhook`,
|
||||
} as const satisfies Record<AllMetadataName, string>;
|
||||
|
||||
const handleMetadataError = (
|
||||
|
||||
+6
-2
@@ -1,7 +1,11 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { WEBHOOK_FRAGMENT } from '@/settings/developers/graphql/fragments/webhookFragment';
|
||||
|
||||
export const DELETE_WEBHOOK = gql`
|
||||
mutation DeleteWebhook($input: DeleteWebhookInput!) {
|
||||
deleteWebhook(input: $input)
|
||||
mutation DeleteWebhook($id: UUID!) {
|
||||
deleteWebhook(id: $id) {
|
||||
...WebhookFragment
|
||||
}
|
||||
}
|
||||
${WEBHOOK_FRAGMENT}
|
||||
`;
|
||||
|
||||
@@ -2,8 +2,8 @@ import gql from 'graphql-tag';
|
||||
import { WEBHOOK_FRAGMENT } from '@/settings/developers/graphql/fragments/webhookFragment';
|
||||
|
||||
export const GET_WEBHOOK = gql`
|
||||
query GetWebhook($input: GetWebhookInput!) {
|
||||
webhook(input: $input) {
|
||||
query GetWebhook($id: UUID!) {
|
||||
webhook(id: $id) {
|
||||
...WebhookFragment
|
||||
}
|
||||
}
|
||||
|
||||
+17
-21
@@ -61,11 +61,13 @@ const createSuccessfulUpdateMock = (webhookId: string, webhookData = {}) => ({
|
||||
variables: {
|
||||
input: {
|
||||
id: webhookId,
|
||||
targetUrl: 'https://updated.com/webhook',
|
||||
operations: ['person.updated'],
|
||||
description: 'Updated webhook',
|
||||
secret: 'updated-secret',
|
||||
...webhookData,
|
||||
update: {
|
||||
targetUrl: 'https://updated.com/webhook',
|
||||
operations: ['person.updated'],
|
||||
description: 'Updated webhook',
|
||||
secret: 'updated-secret',
|
||||
...webhookData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -87,16 +89,12 @@ const createSuccessfulDeleteMock = (webhookId: string) => ({
|
||||
request: {
|
||||
query: DELETE_WEBHOOK,
|
||||
variables: {
|
||||
input: {
|
||||
id: webhookId,
|
||||
},
|
||||
id: webhookId,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
deleteWebhook: {
|
||||
id: webhookId,
|
||||
},
|
||||
deleteWebhook: createMockWebhookData({ id: webhookId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -105,9 +103,7 @@ const createGetWebhookMock = (webhookId: string, webhookData = {}) => ({
|
||||
request: {
|
||||
query: GET_WEBHOOK,
|
||||
variables: {
|
||||
input: {
|
||||
id: webhookId,
|
||||
},
|
||||
id: webhookId,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
@@ -329,10 +325,12 @@ describe('useWebhookForm', () => {
|
||||
variables: {
|
||||
input: {
|
||||
id: webhookId,
|
||||
targetUrl: 'https://test.com/webhook',
|
||||
operations: ['person.created'],
|
||||
description: 'Test webhook',
|
||||
secret: 'test-secret',
|
||||
update: {
|
||||
targetUrl: 'https://test.com/webhook',
|
||||
operations: ['person.created'],
|
||||
description: 'Test webhook',
|
||||
secret: 'test-secret',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -464,9 +462,7 @@ describe('useWebhookForm', () => {
|
||||
request: {
|
||||
query: DELETE_WEBHOOK,
|
||||
variables: {
|
||||
input: {
|
||||
id: webhookId,
|
||||
},
|
||||
id: webhookId,
|
||||
},
|
||||
},
|
||||
error: new Error('Deletion failed'),
|
||||
|
||||
@@ -55,7 +55,7 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
|
||||
const { loading, error } = useGetWebhookQuery({
|
||||
skip: isCreationMode || !webhookId,
|
||||
variables: {
|
||||
input: { id: webhookId || '' },
|
||||
id: webhookId || '',
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
const webhook = data.webhook;
|
||||
@@ -119,8 +119,10 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const input = createWebhookUpdateInput(formValues, webhookId);
|
||||
const { data } = await updateWebhook({ variables: { input } });
|
||||
const input = createWebhookUpdateInput(formValues);
|
||||
const { data } = await updateWebhook({
|
||||
variables: { input: { id: webhookId, update: input } },
|
||||
});
|
||||
const updatedWebhook = data?.updateWebhook;
|
||||
|
||||
formConfig.reset(formValues);
|
||||
@@ -182,7 +184,7 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => {
|
||||
|
||||
try {
|
||||
await deleteWebhook({
|
||||
variables: { input: { id: webhookId } },
|
||||
variables: { id: webhookId },
|
||||
});
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Webhook deleted successfully`,
|
||||
|
||||
+3
-7
@@ -1,8 +1,8 @@
|
||||
import { type WebhookFormValues } from '@/settings/developers/validation-schemas/webhookFormSchema';
|
||||
import {
|
||||
createWebhookCreateInput,
|
||||
createWebhookUpdateInput,
|
||||
} from '@/settings/developers/utils/createWebhookInput';
|
||||
import { type WebhookFormValues } from '@/settings/developers/validation-schemas/webhookFormSchema';
|
||||
|
||||
describe('createWebhookInput', () => {
|
||||
const mockFormValues: WebhookFormValues = {
|
||||
@@ -43,11 +43,9 @@ describe('createWebhookInput', () => {
|
||||
|
||||
describe('createWebhookUpdateInput', () => {
|
||||
it('should create input for webhook update with id', () => {
|
||||
const webhookId = 'test-webhook-id';
|
||||
const result = createWebhookUpdateInput(mockFormValues, webhookId);
|
||||
const result = createWebhookUpdateInput(mockFormValues);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'test-webhook-id',
|
||||
targetUrl: 'https://test.com/webhook',
|
||||
operations: ['person.created', 'company.updated'],
|
||||
description: 'Test webhook',
|
||||
@@ -60,12 +58,10 @@ describe('createWebhookInput', () => {
|
||||
...mockFormValues,
|
||||
targetUrl: ' https://example.com ',
|
||||
};
|
||||
const webhookId = 'test-webhook-id';
|
||||
|
||||
const result = createWebhookUpdateInput(formValues, webhookId);
|
||||
const result = createWebhookUpdateInput(formValues);
|
||||
|
||||
expect(result.targetUrl).toBe('https://example.com');
|
||||
expect(result.id).toBe('test-webhook-id');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,14 +12,10 @@ export const createWebhookCreateInput = (formValues: WebhookFormValues) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createWebhookUpdateInput = (
|
||||
formValues: WebhookFormValues,
|
||||
webhookId: string,
|
||||
) => {
|
||||
export const createWebhookUpdateInput = (formValues: WebhookFormValues) => {
|
||||
const cleanedOperations = cleanAndFormatOperations(formValues.operations);
|
||||
|
||||
return {
|
||||
id: webhookId,
|
||||
targetUrl: formValues.targetUrl.trim(),
|
||||
operations: cleanedOperations,
|
||||
description: formValues.description,
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import { PostgresCredentialsEntity } from 'src/engine/core-modules/postgres-cred
|
||||
import { PublicDomainEntity } from 'src/engine/core-modules/public-domain/public-domain.entity';
|
||||
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WebhookEntity } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddUniversalToWebhook1769517102605 implements MigrationInterface {
|
||||
name = 'AddUniversalToWebhook1769517102605';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" ADD "universalIdentifier" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" ADD "applicationId" uuid`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_d48d713d01cc3c81bad1f39795" ON "core"."webhook" ("workspaceId", "universalIdentifier") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" ADD CONSTRAINT "FK_e755f49a9ef74b36e27932f7a6c" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" DROP CONSTRAINT "FK_e755f49a9ef74b36e27932f7a6c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_d48d713d01cc3c81bad1f39795"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" DROP COLUMN "applicationId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" DROP COLUMN "universalIdentifier"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
import { makeWebhookUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1769525557511-makeWebhookUniversalIdentifierAndApplicationIdNotNull.util';
|
||||
|
||||
export class MakeWebhookUnivesralIdentiferAndApplicationIdNotNull1769525557511
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'MakeWebhookUnivesralIdentiferAndApplicationIdNotNull1769525557511';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const savepointName =
|
||||
'sp_make_webhook_universal_identifier_and_application_id_not_null';
|
||||
|
||||
try {
|
||||
await queryRunner.query(`SAVEPOINT ${savepointName}`);
|
||||
|
||||
await makeWebhookUniversalIdentifierAndApplicationIdNotNullQueries(
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
|
||||
} catch (e) {
|
||||
try {
|
||||
await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
||||
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
|
||||
} catch (rollbackError) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'Failed to rollback to savepoint in MakeWebhookUnivesralIdentiferAndApplicationIdNotNull1769525557511',
|
||||
rollbackError,
|
||||
);
|
||||
throw rollbackError;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'Swallowing MakeWebhookUnivesralIdentiferAndApplicationIdNotNull1769525557511 error',
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" DROP CONSTRAINT "FK_e755f49a9ef74b36e27932f7a6c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_d48d713d01cc3c81bad1f39795"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" ALTER COLUMN "applicationId" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" ALTER COLUMN "universalIdentifier" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_d48d713d01cc3c81bad1f39795" ON "core"."webhook" ("workspaceId", "universalIdentifier") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" ADD CONSTRAINT "FK_e755f49a9ef74b36e27932f7a6c" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
export const makeWebhookUniversalIdentifierAndApplicationIdNotNullQueries =
|
||||
async (queryRunner: QueryRunner): Promise<void> => {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" DROP CONSTRAINT "FK_e755f49a9ef74b36e27932f7a6c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "core"."IDX_d48d713d01cc3c81bad1f39795"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" ALTER COLUMN "universalIdentifier" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" ALTER COLUMN "applicationId" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_d48d713d01cc3c81bad1f39795" ON "core"."webhook" ("workspaceId", "universalIdentifier") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."webhook" ADD CONSTRAINT "FK_e755f49a9ef74b36e27932f7a6c" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -16,8 +16,8 @@ import { CreateAuditLogFromInternalEvent } from 'src/engine/core-modules/audit/j
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { CallWebhookJobsJob } from 'src/engine/core-modules/webhook/jobs/call-webhook-jobs.job';
|
||||
import { WorkspaceEventBatchForWebhook } from 'src/engine/core-modules/webhook/types/workspace-event-batch-for-webhook.type';
|
||||
import { CallWebhookJobsJob } from 'src/engine/metadata-modules/webhook/jobs/call-webhook-jobs.job';
|
||||
import { WorkspaceEventBatchForWebhook } from 'src/engine/metadata-modules/webhook/types/workspace-event-batch-for-webhook.type';
|
||||
import { CallDatabaseEventTriggerJobsJob } from 'src/engine/metadata-modules/database-event-trigger/jobs/call-database-event-trigger-jobs.job';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import { WorkspaceEventEmitterService } from 'src/engine/workspace-event-emitter/workspace-event-emitter.service';
|
||||
|
||||
@@ -53,7 +53,7 @@ import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.mod
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { WebhookModule } from 'src/engine/core-modules/webhook/webhook.module';
|
||||
import { WebhookModule } from 'src/engine/metadata-modules/webhook/webhook.module';
|
||||
import { WorkflowApiModule } from 'src/engine/core-modules/workflow/workflow-api.module';
|
||||
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
|
||||
@@ -15,7 +15,7 @@ import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user
|
||||
import { UpdateWorkspaceMemberEmailJob } from 'src/engine/core-modules/user/jobs/update-workspace-member-email.job';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { WebhookJobModule } from 'src/engine/core-modules/webhook/jobs/webhook-job.module';
|
||||
import { WebhookJobModule } from 'src/engine/metadata-modules/webhook/jobs/webhook-job.module';
|
||||
import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsUrl } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class CreateWebhookInput {
|
||||
@Field()
|
||||
@IsNotEmpty()
|
||||
@IsUrl()
|
||||
targetUrl: string;
|
||||
|
||||
@Field(() => [String])
|
||||
operations: string[];
|
||||
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
secret?: string;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class DeleteWebhookInput {
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
id: string;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class GetWebhookInput {
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
id: string;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateWebhookInput {
|
||||
@Field(() => UUIDScalarType)
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
id: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
targetUrl?: string;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
operations?: string[];
|
||||
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
secret?: string;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { ToolModule } from 'src/engine/core-modules/tool/tool.module';
|
||||
import { CallWebhookJobsJob } from 'src/engine/core-modules/webhook/jobs/call-webhook-jobs.job';
|
||||
import { CallWebhookJob } from 'src/engine/core-modules/webhook/jobs/call-webhook.job';
|
||||
import { WebhookModule } from 'src/engine/core-modules/webhook/webhook.module';
|
||||
|
||||
@Module({
|
||||
imports: [AuditModule, WebhookModule, MetricsModule, ToolModule],
|
||||
providers: [CallWebhookJobsJob, CallWebhookJob],
|
||||
})
|
||||
export class WebhookJobModule {}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
@Index('IDX_WEBHOOK_WORKSPACE_ID', ['workspaceId'])
|
||||
@Entity({ name: 'webhook', schema: 'core' })
|
||||
@ObjectType('Webhook')
|
||||
export class WebhookEntity extends WorkspaceRelatedEntity {
|
||||
@IDField(() => UUIDScalarType)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Field()
|
||||
@Column()
|
||||
targetUrl: string;
|
||||
|
||||
@Field(() => [String])
|
||||
@Column('text', { array: true, default: ['*.*'] })
|
||||
operations: string[];
|
||||
|
||||
@Field({ nullable: true })
|
||||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Field()
|
||||
@Column()
|
||||
secret: string;
|
||||
|
||||
@Field()
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@Field()
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@DeleteDateColumn({ type: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { WebhookEntity } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||
import { WebhookResolver } from 'src/engine/core-modules/webhook/webhook.resolver';
|
||||
import { WebhookService } from 'src/engine/core-modules/webhook/webhook.service';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
import { WebhookController } from './controllers/webhook.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WebhookEntity]),
|
||||
AuthModule,
|
||||
PermissionsModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
],
|
||||
providers: [WebhookService, WebhookResolver],
|
||||
controllers: [WebhookController],
|
||||
exports: [WebhookService, TypeOrmModule],
|
||||
})
|
||||
export class WebhookModule {}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { CreateWebhookInput } from 'src/engine/core-modules/webhook/dtos/create-webhook.dto';
|
||||
import { DeleteWebhookInput } from 'src/engine/core-modules/webhook/dtos/delete-webhook.dto';
|
||||
import { GetWebhookInput } from 'src/engine/core-modules/webhook/dtos/get-webhook.dto';
|
||||
import { UpdateWebhookInput } from 'src/engine/core-modules/webhook/dtos/update-webhook.dto';
|
||||
import { webhookGraphqlApiExceptionHandler } from 'src/engine/core-modules/webhook/utils/webhook-graphql-api-exception-handler.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
import { WebhookEntity } from './webhook.entity';
|
||||
import { WebhookService } from './webhook.service';
|
||||
|
||||
@Resolver(() => WebhookEntity)
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS),
|
||||
)
|
||||
export class WebhookResolver {
|
||||
constructor(private readonly webhookService: WebhookService) {}
|
||||
|
||||
@Query(() => [WebhookEntity])
|
||||
async webhooks(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookEntity[]> {
|
||||
return this.webhookService.findByWorkspaceId(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => WebhookEntity, { nullable: true })
|
||||
async webhook(
|
||||
@Args('input') input: GetWebhookInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookEntity | null> {
|
||||
return this.webhookService.findById(input.id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => WebhookEntity)
|
||||
async createWebhook(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@Args('input') input: CreateWebhookInput,
|
||||
): Promise<WebhookEntity> {
|
||||
try {
|
||||
return await this.webhookService.create({
|
||||
targetUrl: input.targetUrl,
|
||||
operations: input.operations,
|
||||
description: input.description,
|
||||
secret: input.secret,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
} catch (error) {
|
||||
webhookGraphqlApiExceptionHandler(error);
|
||||
throw error; // This line will never be reached but satisfies TypeScript
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => WebhookEntity, { nullable: true })
|
||||
async updateWebhook(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@Args('input') input: UpdateWebhookInput,
|
||||
): Promise<WebhookEntity | null> {
|
||||
try {
|
||||
const updateData: QueryDeepPartialEntity<WebhookEntity> = {};
|
||||
|
||||
if (input.targetUrl !== undefined) updateData.targetUrl = input.targetUrl;
|
||||
if (input.operations !== undefined)
|
||||
updateData.operations = input.operations;
|
||||
if (input.description !== undefined)
|
||||
updateData.description = input.description;
|
||||
if (input.secret !== undefined) updateData.secret = input.secret;
|
||||
|
||||
return await this.webhookService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
updateData,
|
||||
);
|
||||
} catch (error) {
|
||||
webhookGraphqlApiExceptionHandler(error);
|
||||
throw error; // This line will never be reached but satisfies TypeScript
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async deleteWebhook(
|
||||
@Args('input') input: DeleteWebhookInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<boolean> {
|
||||
const result = await this.webhookService.delete(input.id, workspace.id);
|
||||
|
||||
return result !== null;
|
||||
}
|
||||
}
|
||||
@@ -1,420 +0,0 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { ArrayContains, IsNull } from 'typeorm';
|
||||
|
||||
import { WebhookEntity } from './webhook.entity';
|
||||
import { WebhookException, WebhookExceptionCode } from './webhook.exception';
|
||||
import { WebhookService } from './webhook.service';
|
||||
|
||||
describe('WebhookService', () => {
|
||||
let service: WebhookService;
|
||||
let mockWebhookRepository: any;
|
||||
|
||||
const mockWorkspaceId = 'workspace-123';
|
||||
const mockWebhookId = 'webhook-456';
|
||||
|
||||
const mockWebhook: WebhookEntity = {
|
||||
id: mockWebhookId,
|
||||
targetUrl: 'https://example.com/webhook',
|
||||
secret: 'webhook-secret',
|
||||
operations: ['create', 'update'],
|
||||
workspaceId: mockWorkspaceId,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-01'),
|
||||
deletedAt: undefined,
|
||||
workspace: {} as any,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockWebhookRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
WebhookService,
|
||||
{
|
||||
provide: getRepositoryToken(WebhookEntity),
|
||||
useValue: mockWebhookRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<WebhookService>(WebhookService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('normalizeTargetUrl', () => {
|
||||
it('should normalize valid URLs', () => {
|
||||
const result = (service as any).normalizeTargetUrl(
|
||||
'https://example.com/webhook',
|
||||
);
|
||||
|
||||
expect(result).toBe('https://example.com/webhook');
|
||||
});
|
||||
|
||||
it('should return original string if invalid URL', () => {
|
||||
const invalidUrl = 'not-a-url';
|
||||
const result = (service as any).normalizeTargetUrl(invalidUrl);
|
||||
|
||||
expect(result).toBe(invalidUrl);
|
||||
});
|
||||
|
||||
it('should normalize URL with trailing slash', () => {
|
||||
const result = (service as any).normalizeTargetUrl(
|
||||
'https://example.com/webhook/',
|
||||
);
|
||||
|
||||
expect(result).toBe('https://example.com/webhook/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTargetUrl', () => {
|
||||
it('should validate HTTPS URLs', () => {
|
||||
const result = (service as any).validateTargetUrl(
|
||||
'https://example.com/webhook',
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate HTTP URLs', () => {
|
||||
const result = (service as any).validateTargetUrl(
|
||||
'http://example.com/webhook',
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid URLs', () => {
|
||||
const result = (service as any).validateTargetUrl('not-a-url');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject non-HTTP protocols', () => {
|
||||
const result = (service as any).validateTargetUrl(
|
||||
'ftp://example.com/webhook',
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByWorkspaceId', () => {
|
||||
it('should find all webhooks for a workspace', async () => {
|
||||
const mockWebhooks = [
|
||||
mockWebhook,
|
||||
{ ...mockWebhook, id: 'another-webhook' },
|
||||
];
|
||||
|
||||
mockWebhookRepository.find.mockResolvedValue(mockWebhooks);
|
||||
|
||||
const result = await service.findByWorkspaceId(mockWorkspaceId);
|
||||
|
||||
expect(mockWebhookRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
workspaceId: mockWorkspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(mockWebhooks);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByOperations', () => {
|
||||
it('should find webhooks by operations using ArrayContains', async () => {
|
||||
const operations = ['create', 'update'];
|
||||
const mockWebhooks = [mockWebhook];
|
||||
|
||||
mockWebhookRepository.find.mockResolvedValue(mockWebhooks);
|
||||
|
||||
const result = await service.findByOperations(
|
||||
mockWorkspaceId,
|
||||
operations,
|
||||
);
|
||||
|
||||
expect(mockWebhookRepository.find).toHaveBeenCalledWith({
|
||||
where: operations.map((operation) => ({
|
||||
workspaceId: mockWorkspaceId,
|
||||
operations: ArrayContains([operation]),
|
||||
deletedAt: IsNull(),
|
||||
})),
|
||||
});
|
||||
expect(result).toEqual(mockWebhooks);
|
||||
});
|
||||
|
||||
it('should handle single operation', async () => {
|
||||
const operations = ['create'];
|
||||
|
||||
mockWebhookRepository.find.mockResolvedValue([mockWebhook]);
|
||||
|
||||
const result = await service.findByOperations(
|
||||
mockWorkspaceId,
|
||||
operations,
|
||||
);
|
||||
|
||||
expect(mockWebhookRepository.find).toHaveBeenCalledWith({
|
||||
where: [
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
operations: ArrayContains(['create']),
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result).toEqual([mockWebhook]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should find a webhook by ID and workspace ID', async () => {
|
||||
mockWebhookRepository.findOne.mockResolvedValue(mockWebhook);
|
||||
|
||||
const result = await service.findById(mockWebhookId, mockWorkspaceId);
|
||||
|
||||
expect(mockWebhookRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: mockWebhookId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(mockWebhook);
|
||||
});
|
||||
|
||||
it('should return null if webhook not found', async () => {
|
||||
mockWebhookRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await service.findById('non-existent', mockWorkspaceId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create and save a webhook with valid target URL', async () => {
|
||||
const webhookData = {
|
||||
targetUrl: 'https://example.com/webhook',
|
||||
secret: 'webhook-secret',
|
||||
operations: ['create', 'update'],
|
||||
workspaceId: mockWorkspaceId,
|
||||
};
|
||||
|
||||
mockWebhookRepository.create.mockReturnValue(mockWebhook);
|
||||
mockWebhookRepository.save.mockResolvedValue(mockWebhook);
|
||||
|
||||
const result = await service.create(webhookData);
|
||||
|
||||
expect(mockWebhookRepository.create).toHaveBeenCalledWith({
|
||||
...webhookData,
|
||||
targetUrl: 'https://example.com/webhook',
|
||||
secret: 'webhook-secret',
|
||||
});
|
||||
expect(mockWebhookRepository.save).toHaveBeenCalledWith(mockWebhook);
|
||||
expect(result).toEqual(mockWebhook);
|
||||
});
|
||||
|
||||
it('should throw WebhookException for invalid target URL', async () => {
|
||||
const webhookData = {
|
||||
targetUrl: 'invalid-url',
|
||||
operations: ['create'],
|
||||
workspaceId: mockWorkspaceId,
|
||||
};
|
||||
|
||||
await expect(service.create(webhookData)).rejects.toThrow(
|
||||
WebhookException,
|
||||
);
|
||||
|
||||
await expect(service.create(webhookData)).rejects.toMatchObject({
|
||||
code: WebhookExceptionCode.INVALID_TARGET_URL,
|
||||
});
|
||||
|
||||
expect(mockWebhookRepository.create).not.toHaveBeenCalled();
|
||||
expect(mockWebhookRepository.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw WebhookException for webhook data without target URL', async () => {
|
||||
const webhookData = {
|
||||
operations: ['create'],
|
||||
workspaceId: mockWorkspaceId,
|
||||
};
|
||||
|
||||
await expect(service.create(webhookData)).rejects.toThrow(
|
||||
WebhookException,
|
||||
);
|
||||
|
||||
await expect(service.create(webhookData)).rejects.toMatchObject({
|
||||
code: WebhookExceptionCode.INVALID_TARGET_URL,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update an existing webhook', async () => {
|
||||
const updateData = { targetUrl: 'https://updated.example.com/webhook' };
|
||||
const updatedWebhook = { ...mockWebhook, ...updateData };
|
||||
|
||||
mockWebhookRepository.findOne
|
||||
.mockResolvedValueOnce(mockWebhook)
|
||||
.mockResolvedValueOnce(updatedWebhook);
|
||||
mockWebhookRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.update(
|
||||
mockWebhookId,
|
||||
mockWorkspaceId,
|
||||
updateData,
|
||||
);
|
||||
|
||||
expect(mockWebhookRepository.update).toHaveBeenCalledWith(
|
||||
mockWebhookId,
|
||||
updateData,
|
||||
);
|
||||
expect(result).toEqual(updatedWebhook);
|
||||
});
|
||||
|
||||
it('should return null if webhook to update does not exist', async () => {
|
||||
mockWebhookRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await service.update('non-existent', mockWorkspaceId, {
|
||||
targetUrl: 'https://updated.example.com',
|
||||
});
|
||||
|
||||
expect(mockWebhookRepository.update).not.toHaveBeenCalled();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw WebhookException for invalid target URL during update', async () => {
|
||||
const updateData = { targetUrl: 'invalid-url' };
|
||||
|
||||
mockWebhookRepository.findOne.mockResolvedValue(mockWebhook);
|
||||
|
||||
await expect(
|
||||
service.update(mockWebhookId, mockWorkspaceId, updateData),
|
||||
).rejects.toThrow(WebhookException);
|
||||
|
||||
await expect(
|
||||
service.update(mockWebhookId, mockWorkspaceId, updateData),
|
||||
).rejects.toMatchObject({
|
||||
code: WebhookExceptionCode.INVALID_TARGET_URL,
|
||||
});
|
||||
|
||||
expect(mockWebhookRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update without target URL validation if targetUrl not in updateData', async () => {
|
||||
const updateData = { operations: ['create', 'update', 'delete'] };
|
||||
const updatedWebhook = { ...mockWebhook, ...updateData };
|
||||
|
||||
mockWebhookRepository.findOne
|
||||
.mockResolvedValueOnce(mockWebhook)
|
||||
.mockResolvedValueOnce(updatedWebhook);
|
||||
mockWebhookRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.update(
|
||||
mockWebhookId,
|
||||
mockWorkspaceId,
|
||||
updateData,
|
||||
);
|
||||
|
||||
expect(mockWebhookRepository.update).toHaveBeenCalledWith(
|
||||
mockWebhookId,
|
||||
updateData,
|
||||
);
|
||||
expect(result).toEqual(updatedWebhook);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should soft delete a webhook', async () => {
|
||||
mockWebhookRepository.findOne.mockResolvedValue(mockWebhook);
|
||||
mockWebhookRepository.softDelete.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.delete(mockWebhookId, mockWorkspaceId);
|
||||
|
||||
expect(mockWebhookRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: mockWebhookId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
expect(mockWebhookRepository.softDelete).toHaveBeenCalledWith(
|
||||
mockWebhookId,
|
||||
);
|
||||
expect(result).toEqual(mockWebhook);
|
||||
});
|
||||
|
||||
it('should return null if webhook to delete does not exist', async () => {
|
||||
mockWebhookRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await service.delete('non-existent', mockWorkspaceId);
|
||||
|
||||
expect(mockWebhookRepository.softDelete).not.toHaveBeenCalled();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle URLs with query parameters', async () => {
|
||||
const webhookData = {
|
||||
targetUrl: 'https://example.com/webhook?param=value',
|
||||
operations: ['create'],
|
||||
workspaceId: mockWorkspaceId,
|
||||
};
|
||||
|
||||
const normalizedWebhook = {
|
||||
...mockWebhook,
|
||||
targetUrl: 'https://example.com/webhook?param=value',
|
||||
};
|
||||
|
||||
mockWebhookRepository.create.mockReturnValue(normalizedWebhook);
|
||||
mockWebhookRepository.save.mockResolvedValue(normalizedWebhook);
|
||||
|
||||
const result = await service.create(webhookData);
|
||||
|
||||
expect(result.targetUrl).toBe('https://example.com/webhook?param=value');
|
||||
});
|
||||
|
||||
it('should handle URLs with fragments', async () => {
|
||||
const webhookData = {
|
||||
targetUrl: 'https://example.com/webhook#section',
|
||||
operations: ['create'],
|
||||
workspaceId: mockWorkspaceId,
|
||||
};
|
||||
|
||||
const normalizedWebhook = {
|
||||
...mockWebhook,
|
||||
targetUrl: 'https://example.com/webhook#section',
|
||||
};
|
||||
|
||||
mockWebhookRepository.create.mockReturnValue(normalizedWebhook);
|
||||
mockWebhookRepository.save.mockResolvedValue(normalizedWebhook);
|
||||
|
||||
const result = await service.create(webhookData);
|
||||
|
||||
expect(result.targetUrl).toBe('https://example.com/webhook#section');
|
||||
});
|
||||
|
||||
it('should handle empty operations array', async () => {
|
||||
await service.findByOperations(mockWorkspaceId, []);
|
||||
|
||||
expect(mockWebhookRepository.find).toHaveBeenCalledWith({
|
||||
where: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ArrayContains, IsNull, Repository } from 'typeorm';
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
import { WebhookEntity } from './webhook.entity';
|
||||
import { WebhookException, WebhookExceptionCode } from './webhook.exception';
|
||||
|
||||
@Injectable()
|
||||
export class WebhookService {
|
||||
constructor(
|
||||
@InjectRepository(WebhookEntity)
|
||||
private readonly webhookRepository: Repository<WebhookEntity>,
|
||||
) {}
|
||||
|
||||
private normalizeTargetUrl(targetUrl: string): string {
|
||||
try {
|
||||
const url = new URL(targetUrl);
|
||||
|
||||
return url.toString();
|
||||
} catch {
|
||||
return targetUrl;
|
||||
}
|
||||
}
|
||||
|
||||
private validateTargetUrl(targetUrl: string): boolean {
|
||||
try {
|
||||
const url = new URL(targetUrl);
|
||||
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async findByWorkspaceId(workspaceId: string): Promise<WebhookEntity[]> {
|
||||
return this.webhookRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByOperations(
|
||||
workspaceId: string,
|
||||
operations: string[],
|
||||
): Promise<WebhookEntity[]> {
|
||||
return this.webhookRepository.find({
|
||||
where: operations.map((operation) => ({
|
||||
workspaceId,
|
||||
operations: ArrayContains([operation]),
|
||||
deletedAt: IsNull(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<WebhookEntity | null> {
|
||||
const webhook = await this.webhookRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
deletedAt: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
return webhook || null;
|
||||
}
|
||||
|
||||
async create(webhookData: Partial<WebhookEntity>): Promise<WebhookEntity> {
|
||||
const normalizedTargetUrl = this.normalizeTargetUrl(
|
||||
webhookData.targetUrl || '',
|
||||
);
|
||||
|
||||
if (!this.validateTargetUrl(normalizedTargetUrl)) {
|
||||
throw new WebhookException(
|
||||
'Invalid target URL provided',
|
||||
WebhookExceptionCode.INVALID_TARGET_URL,
|
||||
{ userFriendlyMessage: msg`Please provide a valid HTTP or HTTPS URL.` },
|
||||
);
|
||||
}
|
||||
|
||||
const webhook = this.webhookRepository.create({
|
||||
...webhookData,
|
||||
targetUrl: normalizedTargetUrl,
|
||||
secret: webhookData.secret,
|
||||
});
|
||||
|
||||
return this.webhookRepository.save(webhook);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
updateData: QueryDeepPartialEntity<WebhookEntity>,
|
||||
): Promise<WebhookEntity | null> {
|
||||
const webhook = await this.findById(id, workspaceId);
|
||||
|
||||
if (!webhook) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDefined(updateData.targetUrl)) {
|
||||
const normalizedTargetUrl = this.normalizeTargetUrl(
|
||||
updateData.targetUrl as string,
|
||||
);
|
||||
|
||||
if (!this.validateTargetUrl(normalizedTargetUrl)) {
|
||||
throw new WebhookException(
|
||||
'Invalid target URL provided',
|
||||
WebhookExceptionCode.INVALID_TARGET_URL,
|
||||
{
|
||||
userFriendlyMessage: msg`Please provide a valid HTTP or HTTPS URL.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
updateData.targetUrl = normalizedTargetUrl;
|
||||
}
|
||||
|
||||
await this.webhookRepository.update(id, updateData);
|
||||
|
||||
return this.findById(id, workspaceId);
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<WebhookEntity | null> {
|
||||
const webhook = await this.findById(id, workspaceId);
|
||||
|
||||
if (!webhook) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.webhookRepository.softDelete(id);
|
||||
|
||||
return webhook;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import { PostgresCredentialsEntity } from 'src/engine/core-modules/postgres-cred
|
||||
import { PublicDomainEntity } from 'src/engine/core-modules/public-domain/public-domain.entity';
|
||||
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WebhookEntity } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import {
|
||||
DEFAULT_FAST_MODEL,
|
||||
|
||||
+4
@@ -179,6 +179,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
|
||||
propertiesToCompare: [...FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
webhook: {
|
||||
propertiesToCompare: ['targetUrl', 'operations', 'description', 'secret'],
|
||||
propertiesToStringify: ['operations'],
|
||||
},
|
||||
} as const satisfies {
|
||||
[P in AllMetadataName]: OneFlatEntityConfiguration<P>;
|
||||
};
|
||||
|
||||
+2
@@ -8,6 +8,7 @@ import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
@@ -51,4 +52,5 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
|
||||
agent: AgentEntity,
|
||||
commandMenuItem: CommandMenuItemEntity,
|
||||
navigationMenuItem: NavigationMenuItemEntity,
|
||||
webhook: WebhookEntity,
|
||||
} as const satisfies Record<AllMetadataName, EntityTarget<ObjectLiteral>>;
|
||||
|
||||
+7
@@ -478,6 +478,13 @@ export const ALL_METADATA_RELATIONS = {
|
||||
},
|
||||
oneToMany: {},
|
||||
},
|
||||
webhook: {
|
||||
manyToOne: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
},
|
||||
oneToMany: {},
|
||||
},
|
||||
} as const satisfies MetadataRelationsProperties;
|
||||
|
||||
// Note: satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
|
||||
|
||||
+1
@@ -90,4 +90,5 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
|
||||
objectMetadata: true,
|
||||
},
|
||||
frontComponent: {},
|
||||
webhook: {},
|
||||
} as const satisfies MetadataRequiredForValidation;
|
||||
|
||||
+15
@@ -6,6 +6,7 @@ import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/typ
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type';
|
||||
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
@@ -144,6 +145,11 @@ import {
|
||||
type DeleteViewAction,
|
||||
type UpdateViewAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/types/workspace-migration-view-action.type';
|
||||
import {
|
||||
type CreateWebhookAction,
|
||||
type DeleteWebhookAction,
|
||||
type UpdateWebhookAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type';
|
||||
|
||||
export type AllFlatEntityTypesByMetadataName = {
|
||||
fieldMetadata: {
|
||||
@@ -362,4 +368,13 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
flatEntity: FlatFrontComponent;
|
||||
entity: MetadataEntity<'frontComponent'>;
|
||||
};
|
||||
webhook: {
|
||||
actions: {
|
||||
create: CreateWebhookAction;
|
||||
update: UpdateWebhookAction;
|
||||
delete: DeleteWebhookAction;
|
||||
};
|
||||
flatEntity: FlatWebhook;
|
||||
entity: MetadataEntity<'webhook'>;
|
||||
};
|
||||
};
|
||||
|
||||
+2
@@ -162,3 +162,5 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
"view",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for webhook 1`] = `[]`;
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
|
||||
"skill",
|
||||
"view",
|
||||
"viewFilterGroup",
|
||||
"webhook",
|
||||
"fieldMetadata",
|
||||
"role",
|
||||
"serverlessFunction",
|
||||
|
||||
+8
@@ -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)[];
|
||||
+16
@@ -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 {}
|
||||
+43
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
@@ -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>;
|
||||
+4
@@ -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>;
|
||||
+41
@@ -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,
|
||||
};
|
||||
};
|
||||
+31
@@ -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;
|
||||
};
|
||||
+17
@@ -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,
|
||||
});
|
||||
+41
@@ -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(),
|
||||
};
|
||||
};
|
||||
+20
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { WebhookModule } from 'src/engine/metadata-modules/webhook/webhook.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
||||
import { RouteTriggerModule } from 'src/engine/metadata-modules/route-trigger/route-trigger.module';
|
||||
@@ -43,6 +44,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
||||
RouteTriggerModule,
|
||||
CronTriggerModule,
|
||||
DatabaseEventTriggerModule,
|
||||
WebhookModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [
|
||||
@@ -60,6 +62,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
||||
ViewModule,
|
||||
RoleModule,
|
||||
PermissionsModule,
|
||||
WebhookModule,
|
||||
],
|
||||
})
|
||||
export class MetadataEngineModule {}
|
||||
|
||||
+25
-24
@@ -13,20 +13,16 @@ import {
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
|
||||
import { CreateWebhookInput } from 'src/engine/core-modules/webhook/dtos/create-webhook.dto';
|
||||
import { UpdateWebhookInput } from 'src/engine/core-modules/webhook/dtos/update-webhook.dto';
|
||||
import { type WebhookEntity } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||
import { WebhookService } from 'src/engine/core-modules/webhook/webhook.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/create-webhook.input';
|
||||
import { UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input';
|
||||
import { type WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhook.dto';
|
||||
import { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service';
|
||||
|
||||
/**
|
||||
* rest/webhooks is deprecated, use rest/metadata/webhooks instead
|
||||
* rest/webhooks will be removed in the future
|
||||
*/
|
||||
@Controller(['rest/webhooks', 'rest/metadata/webhooks'])
|
||||
@UseGuards(
|
||||
JwtAuthGuard,
|
||||
@@ -40,15 +36,15 @@ export class WebhookController {
|
||||
@Get()
|
||||
async findAll(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookEntity[]> {
|
||||
return this.webhookService.findByWorkspaceId(workspace.id);
|
||||
): Promise<WebhookDTO[]> {
|
||||
return this.webhookService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookEntity | null> {
|
||||
): Promise<WebhookDTO | null> {
|
||||
return this.webhookService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@@ -56,23 +52,28 @@ export class WebhookController {
|
||||
async create(
|
||||
@Body() createWebhookDto: CreateWebhookInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookEntity> {
|
||||
return this.webhookService.create({
|
||||
targetUrl: createWebhookDto.targetUrl,
|
||||
operations: createWebhookDto.operations || ['*.*'],
|
||||
description: createWebhookDto.description,
|
||||
secret: createWebhookDto.secret,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
): Promise<WebhookDTO> {
|
||||
return this.webhookService.create(createWebhookDto, workspace.id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateWebhookDto: UpdateWebhookInput,
|
||||
@Body()
|
||||
updateWebhookDto: {
|
||||
targetUrl?: string;
|
||||
operations?: string[];
|
||||
description?: string;
|
||||
secret?: string;
|
||||
},
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookEntity | null> {
|
||||
return this.webhookService.update(id, workspace.id, updateWebhookDto);
|
||||
): Promise<WebhookDTO> {
|
||||
const input: UpdateWebhookInput = {
|
||||
id,
|
||||
update: updateWebhookDto,
|
||||
};
|
||||
|
||||
return this.webhookService.update(input, workspace.id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@@ -80,8 +81,8 @@ export class WebhookController {
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<boolean> {
|
||||
const result = await this.webhookService.delete(id, workspace.id);
|
||||
await this.webhookService.delete(id, workspace.id);
|
||||
|
||||
return result !== null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class CreateWebhookInput {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
id?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
targetUrl: string;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [String])
|
||||
operations: string[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
secret?: string;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateWebhookInputUpdates {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
targetUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Field(() => [String], { nullable: true })
|
||||
operations?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateWebhookInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType, {
|
||||
description: 'The id of the webhook to update',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@Type(() => UpdateWebhookInputUpdates)
|
||||
@ValidateNested()
|
||||
@Field(() => UpdateWebhookInputUpdates, {
|
||||
description: 'The webhook fields to update',
|
||||
})
|
||||
update: UpdateWebhookInputUpdates;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('Webhook')
|
||||
export class WebhookDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
targetUrl: string;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [String])
|
||||
operations: string[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
secret: string;
|
||||
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
applicationId: string;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
createdAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@Field()
|
||||
updatedAt: Date;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Index('IDX_WEBHOOK_WORKSPACE_ID', ['workspaceId'])
|
||||
@Entity({ name: 'webhook', schema: 'core' })
|
||||
export class WebhookEntity
|
||||
extends SyncableEntity
|
||||
implements Required<WebhookEntity>
|
||||
{
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
targetUrl: string;
|
||||
|
||||
@Column('text', { array: true, default: ['*.*'] })
|
||||
operations: string[];
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@Column({ nullable: false })
|
||||
secret: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@DeleteDateColumn({ type: 'timestamptz' })
|
||||
deletedAt: Date | null;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
type CallHandler,
|
||||
type ExecutionContext,
|
||||
Injectable,
|
||||
type NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { type Observable, catchError } from 'rxjs';
|
||||
|
||||
import { webhookGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/webhook/utils/webhook-graphql-api-exception-handler.util';
|
||||
|
||||
@Injectable()
|
||||
export class WebhookGraphqlApiExceptionInterceptor implements NestInterceptor {
|
||||
intercept(
|
||||
_context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): Observable<unknown> {
|
||||
return next.handle().pipe(catchError(webhookGraphqlApiExceptionHandler));
|
||||
}
|
||||
}
|
||||
+21
-13
@@ -1,6 +1,8 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chunk from 'lodash.chunk';
|
||||
import { ArrayContains, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import type { ObjectRecordEvent } from 'twenty-shared/database-events';
|
||||
|
||||
@@ -12,10 +14,10 @@ import { MessageQueueService } from 'src/engine/core-modules/message-queue/servi
|
||||
import {
|
||||
CallWebhookJob,
|
||||
type CallWebhookJobData,
|
||||
} from 'src/engine/core-modules/webhook/jobs/call-webhook.job';
|
||||
import { WebhookService } from 'src/engine/core-modules/webhook/webhook.service';
|
||||
} from 'src/engine/metadata-modules/webhook/jobs/call-webhook.job';
|
||||
import { transformEventBatchToWebhookEvents } from 'src/engine/metadata-modules/webhook/utils/transform-event-batch-to-webhook-events';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import { transformEventBatchToWebhookEvents } from 'src/engine/core-modules/webhook/utils/transform-event-batch-to-webhook-events';
|
||||
|
||||
const WEBHOOK_JOBS_CHUNK_SIZE = 20;
|
||||
|
||||
@@ -25,7 +27,8 @@ export class CallWebhookJobsJob {
|
||||
constructor(
|
||||
@InjectMessageQueue(MessageQueue.webhookQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly webhookService: WebhookService,
|
||||
@InjectRepository(WebhookEntity)
|
||||
private readonly webhookRepository: Repository<WebhookEntity>,
|
||||
) {}
|
||||
|
||||
@Process(CallWebhookJobsJob.name)
|
||||
@@ -39,15 +42,20 @@ export class CallWebhookJobsJob {
|
||||
|
||||
const [nameSingular, operation] = workspaceEventBatch.name.split('.');
|
||||
|
||||
const webhooks = await this.webhookService.findByOperations(
|
||||
workspaceEventBatch.workspaceId,
|
||||
[
|
||||
`${nameSingular}.${operation}`,
|
||||
`*.${operation}`,
|
||||
`${nameSingular}.*`,
|
||||
'*.*',
|
||||
],
|
||||
);
|
||||
const operations = [
|
||||
`${nameSingular}.${operation}`,
|
||||
`*.${operation}`,
|
||||
`${nameSingular}.*`,
|
||||
'*.*',
|
||||
];
|
||||
|
||||
const webhooks = await this.webhookRepository.find({
|
||||
where: operations.map((op) => ({
|
||||
workspaceId: workspaceEventBatch.workspaceId,
|
||||
operations: ArrayContains([op]),
|
||||
deletedAt: IsNull(),
|
||||
})),
|
||||
});
|
||||
|
||||
const webhookEvents = transformEventBatchToWebhookEvents({
|
||||
workspaceEventBatch,
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { ToolModule } from 'src/engine/core-modules/tool/tool.module';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { CallWebhookJobsJob } from 'src/engine/metadata-modules/webhook/jobs/call-webhook-jobs.job';
|
||||
import { CallWebhookJob } from 'src/engine/metadata-modules/webhook/jobs/call-webhook.job';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WebhookEntity]),
|
||||
AuditModule,
|
||||
MetricsModule,
|
||||
ToolModule,
|
||||
],
|
||||
providers: [CallWebhookJobsJob, CallWebhookJob],
|
||||
})
|
||||
export class WebhookJobModule {}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import type { ObjectRecordEvent } from 'twenty-shared/database-events';
|
||||
|
||||
import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import type { WebhookEntity } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||
import { transformEventBatchToWebhookEvents } from 'src/engine/core-modules/webhook/utils/transform-event-batch-to-webhook-events';
|
||||
import type { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { transformEventBatchToWebhookEvents } from 'src/engine/metadata-modules/webhook/utils/transform-event-batch-to-webhook-events';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
const mockObjectMetadata: FlatObjectMetadata = {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { ObjectRecordEvent } from 'twenty-shared/database-events';
|
||||
|
||||
import { transformEventToWebhookEvent } from 'src/engine/core-modules/webhook/utils/transform-event-to-webhook-event';
|
||||
import { transformEventToWebhookEvent } from 'src/engine/metadata-modules/webhook/utils/transform-event-to-webhook-event';
|
||||
|
||||
describe('transformEventToWebhookEvent', () => {
|
||||
it('should transform event to webhook event', () => {
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export const generateWebhookSecret = (): string => {
|
||||
return randomBytes(32).toString('hex');
|
||||
};
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import type { ObjectRecordEvent } from 'twenty-shared/database-events';
|
||||
|
||||
import { type WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { type CallWebhookJobData } from 'src/engine/metadata-modules/webhook/jobs/call-webhook.job';
|
||||
import { transformEventToWebhookEvent } from 'src/engine/metadata-modules/webhook/utils/transform-event-to-webhook-event';
|
||||
import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
import { type CallWebhookJobData } from 'src/engine/core-modules/webhook/jobs/call-webhook.job';
|
||||
import { type WebhookEntity } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||
import { transformEventToWebhookEvent } from 'src/engine/core-modules/webhook/utils/transform-event-to-webhook-event';
|
||||
|
||||
export const transformEventBatchToWebhookEvents = ({
|
||||
workspaceEventBatch,
|
||||
+7
-5
@@ -1,23 +1,25 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
WebhookException,
|
||||
WebhookExceptionCode,
|
||||
} from 'src/engine/core-modules/webhook/webhook.exception';
|
||||
} from 'src/engine/metadata-modules/webhook/webhook.exception';
|
||||
|
||||
export const webhookGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof WebhookException) {
|
||||
switch (error.code) {
|
||||
case WebhookExceptionCode.WEBHOOK_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
throw new NotFoundError(error);
|
||||
case WebhookExceptionCode.INVALID_WEBHOOK_INPUT:
|
||||
case WebhookExceptionCode.INVALID_TARGET_URL:
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
throw new UserInputError(error);
|
||||
case WebhookExceptionCode.WEBHOOK_ALREADY_EXISTS:
|
||||
throw new ConflictError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
+7
-1
@@ -6,6 +6,8 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export enum WebhookExceptionCode {
|
||||
WEBHOOK_NOT_FOUND = 'WEBHOOK_NOT_FOUND',
|
||||
WEBHOOK_ALREADY_EXISTS = 'WEBHOOK_ALREADY_EXISTS',
|
||||
INVALID_WEBHOOK_INPUT = 'INVALID_WEBHOOK_INPUT',
|
||||
INVALID_TARGET_URL = 'INVALID_TARGET_URL',
|
||||
}
|
||||
|
||||
@@ -13,8 +15,12 @@ const getWebhookExceptionUserFriendlyMessage = (code: WebhookExceptionCode) => {
|
||||
switch (code) {
|
||||
case WebhookExceptionCode.WEBHOOK_NOT_FOUND:
|
||||
return msg`Webhook not found.`;
|
||||
case WebhookExceptionCode.WEBHOOK_ALREADY_EXISTS:
|
||||
return msg`A webhook with this configuration already exists.`;
|
||||
case WebhookExceptionCode.INVALID_WEBHOOK_INPUT:
|
||||
return msg`Invalid webhook input.`;
|
||||
case WebhookExceptionCode.INVALID_TARGET_URL:
|
||||
return msg`Invalid target URL.`;
|
||||
return msg`Invalid target URL. Please provide a valid HTTP or HTTPS URL.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { FlatWebhookModule } from 'src/engine/metadata-modules/flat-webhook/flat-webhook.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WebhookController } from 'src/engine/metadata-modules/webhook/controllers/webhook.controller';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { WebhookGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/webhook/interceptors/webhook-graphql-api-exception.interceptor';
|
||||
import { WebhookResolver } from 'src/engine/metadata-modules/webhook/webhook.resolver';
|
||||
import { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WebhookEntity]),
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
ApplicationModule,
|
||||
AuthModule,
|
||||
PermissionsModule,
|
||||
FlatWebhookModule,
|
||||
],
|
||||
controllers: [WebhookController],
|
||||
providers: [
|
||||
WebhookService,
|
||||
WebhookResolver,
|
||||
WebhookGraphqlApiExceptionInterceptor,
|
||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
exports: [WebhookService],
|
||||
})
|
||||
export class WebhookModule {}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/create-webhook.input';
|
||||
import { UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input';
|
||||
import { WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhook.dto';
|
||||
import { WebhookGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/webhook/interceptors/webhook-graphql-api-exception.interceptor';
|
||||
import { WebhookService } from 'src/engine/metadata-modules/webhook/webhook.service';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(
|
||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||
WebhookGraphqlApiExceptionInterceptor,
|
||||
)
|
||||
@Resolver(() => WebhookDTO)
|
||||
export class WebhookResolver {
|
||||
constructor(private readonly webhookService: WebhookService) {}
|
||||
|
||||
@Query(() => [WebhookDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS))
|
||||
async webhooks(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookDTO[]> {
|
||||
return await this.webhookService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => WebhookDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS))
|
||||
async webhook(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookDTO | null> {
|
||||
return await this.webhookService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => WebhookDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS))
|
||||
async createWebhook(
|
||||
@Args('input') input: CreateWebhookInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookDTO> {
|
||||
return await this.webhookService.create(input, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => WebhookDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS))
|
||||
async updateWebhook(
|
||||
@Args('input') input: UpdateWebhookInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookDTO> {
|
||||
return await this.webhookService.update(input, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => WebhookDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.API_KEYS_AND_WEBHOOKS))
|
||||
async deleteWebhook(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<WebhookDTO> {
|
||||
return await this.webhookService.delete(id, workspace.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { fromCreateWebhookInputToFlatWebhookToCreate } from 'src/engine/metadata-modules/flat-webhook/utils/from-create-webhook-input-to-flat-webhook-to-create.util';
|
||||
import { fromDeleteWebhookInputToFlatWebhookOrThrow } from 'src/engine/metadata-modules/flat-webhook/utils/from-delete-webhook-input-to-flat-webhook-or-throw.util';
|
||||
import { fromFlatWebhookToWebhookDto } from 'src/engine/metadata-modules/flat-webhook/utils/from-flat-webhook-to-webhook-dto.util';
|
||||
import { fromUpdateWebhookInputToFlatWebhookToUpdateOrThrow } from 'src/engine/metadata-modules/flat-webhook/utils/from-update-webhook-input-to-flat-webhook-to-update-or-throw.util';
|
||||
import { fromWebhookEntityToFlatWebhook } from 'src/engine/metadata-modules/flat-webhook/utils/from-webhook-entity-to-flat-webhook.util';
|
||||
import { type CreateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/create-webhook.input';
|
||||
import { type UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input';
|
||||
import { type WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhook.dto';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class WebhookService {
|
||||
constructor(
|
||||
@InjectRepository(WebhookEntity)
|
||||
private readonly webhookRepository: Repository<WebhookEntity>,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
private normalizeTargetUrl(targetUrl: string): string {
|
||||
try {
|
||||
const url = new URL(targetUrl);
|
||||
|
||||
return url.toString();
|
||||
} catch {
|
||||
return targetUrl;
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(workspaceId: string): Promise<WebhookDTO[]> {
|
||||
const webhooks = await this.webhookRepository.find({
|
||||
where: { workspaceId, deletedAt: IsNull() },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
|
||||
return webhooks
|
||||
.map(fromWebhookEntityToFlatWebhook)
|
||||
.map(fromFlatWebhookToWebhookDto);
|
||||
}
|
||||
|
||||
async findById(id: string, workspaceId: string): Promise<WebhookDTO | null> {
|
||||
const webhook = await this.webhookRepository.findOne({
|
||||
where: { id, workspaceId, deletedAt: IsNull() },
|
||||
});
|
||||
|
||||
if (!isDefined(webhook)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fromFlatWebhookToWebhookDto(fromWebhookEntityToFlatWebhook(webhook));
|
||||
}
|
||||
|
||||
async create(
|
||||
input: CreateWebhookInput,
|
||||
workspaceId: string,
|
||||
): Promise<WebhookDTO> {
|
||||
const normalizedTargetUrl = this.normalizeTargetUrl(input.targetUrl);
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const flatWebhookToCreate = fromCreateWebhookInputToFlatWebhookToCreate({
|
||||
createWebhookInput: {
|
||||
...input,
|
||||
targetUrl: normalizedTargetUrl,
|
||||
},
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
webhook: {
|
||||
flatEntityToCreate: [flatWebhookToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while creating webhook',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatWebhookMaps: recomputedFlatWebhookMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatWebhookMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatWebhookToWebhookDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: flatWebhookToCreate.id,
|
||||
flatEntityMaps: recomputedFlatWebhookMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async update(
|
||||
input: UpdateWebhookInput,
|
||||
workspaceId: string,
|
||||
): Promise<WebhookDTO> {
|
||||
const normalizedInput = {
|
||||
...input,
|
||||
update: {
|
||||
...input.update,
|
||||
...(isDefined(input.update.targetUrl) && {
|
||||
targetUrl: this.normalizeTargetUrl(input.update.targetUrl),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const { flatWebhookMaps: existingFlatWebhookMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatWebhookMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatWebhookToUpdate =
|
||||
fromUpdateWebhookInputToFlatWebhookToUpdateOrThrow({
|
||||
flatWebhookMaps: existingFlatWebhookMaps,
|
||||
updateWebhookInput: normalizedInput,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
webhook: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [flatWebhookToUpdate],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while updating webhook',
|
||||
);
|
||||
}
|
||||
|
||||
const { flatWebhookMaps: recomputedFlatWebhookMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatWebhookMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatWebhookToWebhookDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: input.id,
|
||||
flatEntityMaps: recomputedFlatWebhookMaps,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<WebhookDTO> {
|
||||
const { flatWebhookMaps: existingFlatWebhookMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatWebhookMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatWebhookToDelete = fromDeleteWebhookInputToFlatWebhookOrThrow({
|
||||
flatWebhookMaps: existingFlatWebhookMaps,
|
||||
webhookId: id,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
webhook: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [flatWebhookToDelete],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(validateAndBuildResult)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while deleting webhook',
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatWebhookToWebhookDto(flatWebhookToDelete);
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
|
||||
flatRowLevelPermissionPredicateGroupMaps:
|
||||
'flat-maps:row-level-permission-predicate-group',
|
||||
flatFrontComponentMaps: 'flat-maps:front-component',
|
||||
flatWebhookMaps: 'flat-maps:webhook',
|
||||
flatWorkspaceMemberMaps: 'flat-maps:workspace-member',
|
||||
serverlessFunctionLayerMaps: 'cache:serverless-function-layer',
|
||||
applicationVariableMaps: 'cache:application-variable',
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { combineFilters, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type SerializableAuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { type FlatWorkspaceMemberMaps } from 'src/engine/core-modules/user/types/flat-workspace-member-maps.type';
|
||||
import { transformEventToWebhookEvent } from 'src/engine/core-modules/webhook/utils/transform-event-to-webhook-event';
|
||||
import { transformEventToWebhookEvent } from 'src/engine/metadata-modules/webhook/utils/transform-event-to-webhook-event';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
+40
@@ -38,6 +38,7 @@ import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engi
|
||||
import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service';
|
||||
import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service';
|
||||
import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service';
|
||||
import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationBuildOrchestratorService {
|
||||
@@ -66,6 +67,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
private readonly workspaceMigrationRowLevelPermissionPredicateActionsBuilderService: WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService,
|
||||
private readonly workspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService: WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
|
||||
private readonly workspaceMigrationFrontComponentActionsBuilderService: WorkspaceMigrationFrontComponentActionsBuilderService,
|
||||
private readonly workspaceMigrationWebhookActionsBuilderService: WorkspaceMigrationWebhookActionsBuilderService,
|
||||
) {}
|
||||
|
||||
private setupOptimisticCache({
|
||||
@@ -168,6 +170,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatFrontComponentMaps,
|
||||
flatWebhookMaps,
|
||||
} = fromToAllFlatEntityMaps;
|
||||
|
||||
if (isDefined(flatObjectMetadataMaps)) {
|
||||
@@ -1049,6 +1052,37 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(flatWebhookMaps)) {
|
||||
const { from: fromFlatWebhookMaps, to: toFlatWebhookMaps } =
|
||||
flatWebhookMaps;
|
||||
|
||||
const webhookResult =
|
||||
await this.workspaceMigrationWebhookActionsBuilderService.validateAndBuild(
|
||||
{
|
||||
additionalCacheDataMaps,
|
||||
from: fromFlatWebhookMaps,
|
||||
to: toFlatWebhookMaps,
|
||||
buildOptions,
|
||||
dependencyOptimisticFlatEntityMaps: undefined,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
|
||||
{
|
||||
allFlatEntityMaps: optimisticAllFlatEntityMaps,
|
||||
flatEntityMapsAndRelatedFlatEntityMaps:
|
||||
webhookResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (webhookResult.status === 'fail') {
|
||||
orchestratorFailureReport.webhook.push(...webhookResult.errors);
|
||||
} else {
|
||||
orchestratorActionsReport.webhook = webhookResult.actions;
|
||||
}
|
||||
}
|
||||
|
||||
const allErrors = Object.values(orchestratorFailureReport);
|
||||
|
||||
if (allErrors.some((report) => report.length > 0)) {
|
||||
@@ -1200,6 +1234,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
...aggregatedOrchestratorActionsReport.frontComponent.create,
|
||||
...aggregatedOrchestratorActionsReport.frontComponent.update,
|
||||
///
|
||||
|
||||
// Webhooks
|
||||
...aggregatedOrchestratorActionsReport.webhook.delete,
|
||||
...aggregatedOrchestratorActionsReport.webhook.create,
|
||||
...aggregatedOrchestratorActionsReport.webhook.update,
|
||||
///
|
||||
],
|
||||
workspaceId,
|
||||
},
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { type BaseCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type';
|
||||
import { type BaseDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type';
|
||||
import { type BaseUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type';
|
||||
|
||||
export type CreateWebhookAction = BaseCreateWorkspaceMigrationAction<'webhook'>;
|
||||
|
||||
export type UpdateWebhookAction = BaseUpdateWorkspaceMigrationAction<'webhook'>;
|
||||
|
||||
export type DeleteWebhookAction = BaseDeleteWorkspaceMigrationAction<'webhook'>;
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
|
||||
import { UpdateWebhookAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type';
|
||||
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
|
||||
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
|
||||
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
|
||||
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-result.type';
|
||||
import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-webhook-validator.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationWebhookActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
|
||||
typeof ALL_METADATA_NAME.webhook
|
||||
> {
|
||||
constructor(
|
||||
private readonly flatWebhookValidatorService: FlatWebhookValidatorService,
|
||||
) {
|
||||
super(ALL_METADATA_NAME.webhook);
|
||||
}
|
||||
|
||||
protected validateFlatEntityCreation(
|
||||
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.webhook>,
|
||||
): FlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.webhook,
|
||||
'create'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatWebhookValidatorService.validateFlatWebhookCreation(args);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatWebhookToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'create',
|
||||
metadataName: 'webhook',
|
||||
flatEntity: flatWebhookToValidate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityDeletion(
|
||||
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.webhook>,
|
||||
): FlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.webhook,
|
||||
'delete'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatWebhookValidatorService.validateFlatWebhookDeletion(args);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatWebhookToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'delete',
|
||||
metadataName: 'webhook',
|
||||
universalIdentifier: flatWebhookToValidate.universalIdentifier,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityUpdate(
|
||||
args: FlatEntityUpdateValidationArgs<typeof ALL_METADATA_NAME.webhook>,
|
||||
): FlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.webhook,
|
||||
'update'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatWebhookValidatorService.validateFlatWebhookUpdate(args);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityId, flatEntityUpdates } = args;
|
||||
|
||||
const updateWebhookAction: UpdateWebhookAction = {
|
||||
type: 'update',
|
||||
metadataName: 'webhook',
|
||||
entityId: flatEntityId,
|
||||
updates: flatEntityUpdates,
|
||||
};
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: updateWebhookAction,
|
||||
};
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { WebhookExceptionCode } from 'src/engine/metadata-modules/webhook/webhook.exception';
|
||||
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/utils/find-flat-entity-property-update.util';
|
||||
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
|
||||
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
|
||||
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class FlatWebhookValidatorService {
|
||||
private validateTargetUrl(targetUrl: string): boolean {
|
||||
try {
|
||||
const url = new URL(targetUrl);
|
||||
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public validateFlatWebhookCreation({
|
||||
flatEntityToValidate: flatWebhook,
|
||||
}: FlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.webhook
|
||||
>): FailedFlatEntityValidation<'webhook', 'create'> {
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatWebhook.id,
|
||||
universalIdentifier: flatWebhook.universalIdentifier,
|
||||
targetUrl: flatWebhook.targetUrl,
|
||||
},
|
||||
metadataName: 'webhook',
|
||||
type: 'create',
|
||||
});
|
||||
|
||||
if (!isNonEmptyString(flatWebhook.targetUrl)) {
|
||||
validationResult.errors.push({
|
||||
code: WebhookExceptionCode.INVALID_WEBHOOK_INPUT,
|
||||
message: t`Target URL is required`,
|
||||
userFriendlyMessage: msg`Target URL is required`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.validateTargetUrl(flatWebhook.targetUrl)) {
|
||||
validationResult.errors.push({
|
||||
code: WebhookExceptionCode.INVALID_TARGET_URL,
|
||||
message: t`Invalid target URL provided`,
|
||||
userFriendlyMessage: msg`Please provide a valid HTTP or HTTPS URL`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
public validateFlatWebhookDeletion({
|
||||
flatEntityToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatWebhookMaps: optimisticFlatWebhookMaps,
|
||||
},
|
||||
}: FlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.webhook
|
||||
>): FailedFlatEntityValidation<'webhook', 'delete'> {
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatEntityToValidate.id,
|
||||
universalIdentifier: flatEntityToValidate.universalIdentifier,
|
||||
targetUrl: flatEntityToValidate.targetUrl,
|
||||
},
|
||||
metadataName: 'webhook',
|
||||
type: 'delete',
|
||||
});
|
||||
|
||||
const existingWebhook = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatEntityToValidate.id,
|
||||
flatEntityMaps: optimisticFlatWebhookMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(existingWebhook)) {
|
||||
validationResult.errors.push({
|
||||
code: WebhookExceptionCode.WEBHOOK_NOT_FOUND,
|
||||
message: t`Webhook not found`,
|
||||
userFriendlyMessage: msg`Webhook not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
public validateFlatWebhookUpdate({
|
||||
flatEntityId,
|
||||
flatEntityUpdates,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatWebhookMaps: optimisticFlatWebhookMaps,
|
||||
},
|
||||
}: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.webhook
|
||||
>): FailedFlatEntityValidation<'webhook', 'update'> {
|
||||
const fromFlatWebhook = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId,
|
||||
flatEntityMaps: optimisticFlatWebhookMaps,
|
||||
});
|
||||
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
id: flatEntityId,
|
||||
universalIdentifier: fromFlatWebhook?.universalIdentifier,
|
||||
},
|
||||
metadataName: 'webhook',
|
||||
type: 'update',
|
||||
});
|
||||
|
||||
if (!isDefined(fromFlatWebhook)) {
|
||||
validationResult.errors.push({
|
||||
code: WebhookExceptionCode.WEBHOOK_NOT_FOUND,
|
||||
message: t`Webhook not found`,
|
||||
userFriendlyMessage: msg`Webhook not found`,
|
||||
});
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const targetUrlUpdate = findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates,
|
||||
property: 'targetUrl',
|
||||
});
|
||||
|
||||
if (
|
||||
isDefined(targetUrlUpdate) &&
|
||||
!this.validateTargetUrl(targetUrlUpdate.to)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: WebhookExceptionCode.INVALID_TARGET_URL,
|
||||
message: t`Invalid target URL provided`,
|
||||
userFriendlyMessage: msg`Please provide a valid HTTP or HTTPS URL`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
+3
@@ -27,6 +27,7 @@ import { FlatViewFilterGroupValidatorService } from 'src/engine/workspace-manage
|
||||
import { FlatViewFilterValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-filter-validator.service';
|
||||
import { FlatViewGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-group-validator.service';
|
||||
import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-validator.service';
|
||||
import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-webhook-validator.service';
|
||||
|
||||
@Module({
|
||||
imports: [FeatureFlagModule],
|
||||
@@ -57,6 +58,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
|
||||
FlatRowLevelPermissionPredicateValidatorService,
|
||||
FlatRowLevelPermissionPredicateGroupValidatorService,
|
||||
FlatFrontComponentValidatorService,
|
||||
FlatWebhookValidatorService,
|
||||
],
|
||||
exports: [
|
||||
FlatViewValidatorService,
|
||||
@@ -84,6 +86,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
|
||||
FlatRowLevelPermissionPredicateValidatorService,
|
||||
FlatRowLevelPermissionPredicateGroupValidatorService,
|
||||
FlatFrontComponentValidatorService,
|
||||
FlatWebhookValidatorService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceMigrationBuilderValidatorsModule {}
|
||||
|
||||
+3
@@ -22,6 +22,7 @@ import { WorkspaceMigrationServerlessFunctionActionsBuilderService } from 'src/e
|
||||
import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service';
|
||||
import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service';
|
||||
import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service';
|
||||
import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service';
|
||||
import { WorkspaceMigrationViewFilterGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter-group/workspace-migration-view-filter-group-actions-builder.service';
|
||||
import { WorkspaceMigrationViewFilterActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-filter/workspace-migration-view-filter-actions-builder.service';
|
||||
import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/workspace-migration-view-group-actions-builder.service';
|
||||
@@ -56,6 +57,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService,
|
||||
WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
|
||||
WorkspaceMigrationFrontComponentActionsBuilderService,
|
||||
WorkspaceMigrationWebhookActionsBuilderService,
|
||||
],
|
||||
exports: [
|
||||
WorkspaceMigrationViewActionsBuilderService,
|
||||
@@ -83,6 +85,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
|
||||
FlatFieldMetadataTypeValidatorService,
|
||||
WorkspaceMigrationFrontComponentActionsBuilderService,
|
||||
WorkspaceMigrationWebhookActionsBuilderService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceMigrationBuilderModule {}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { CreateWebhookAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateWebhookActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'create',
|
||||
'webhook',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<CreateWebhookAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId } = context;
|
||||
const { flatEntity } = action;
|
||||
|
||||
const webhookRepository =
|
||||
queryRunner.manager.getRepository<WebhookEntity>(WebhookEntity);
|
||||
|
||||
await webhookRepository.insert({
|
||||
...flatEntity,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<CreateWebhookAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { DeleteWebhookAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteWebhookActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'delete',
|
||||
'webhook',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<DeleteWebhookAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId, allFlatEntityMaps } = context;
|
||||
const { universalIdentifier } = action;
|
||||
|
||||
const flatWebhook = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: allFlatEntityMaps.flatWebhookMaps,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
const webhookRepository =
|
||||
queryRunner.manager.getRepository<WebhookEntity>(WebhookEntity);
|
||||
|
||||
await webhookRepository.delete({
|
||||
id: flatWebhook.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<DeleteWebhookAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { UpdateWebhookAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type';
|
||||
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateWebhookActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'update',
|
||||
'webhook',
|
||||
) {
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerArgs<UpdateWebhookAction>,
|
||||
): Promise<void> {
|
||||
const { action, queryRunner, workspaceId } = context;
|
||||
const { entityId, updates } = action;
|
||||
|
||||
const webhookRepository =
|
||||
queryRunner.manager.getRepository<WebhookEntity>(WebhookEntity);
|
||||
|
||||
await webhookRepository.update(
|
||||
{ id: entityId, workspaceId },
|
||||
fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
|
||||
updates,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerArgs<UpdateWebhookAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+7
@@ -58,6 +58,9 @@ import { CreateFrontComponentActionHandlerService } from 'src/engine/workspace-m
|
||||
import { UpdateFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/update-front-component-action-handler.service';
|
||||
import { DeleteFrontComponentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/front-component/services/delete-front-component-action-handler.service';
|
||||
import { UpdateSkillActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/skill/services/update-skill-action-handler.service';
|
||||
import { CreateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service';
|
||||
import { DeleteWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/delete-webhook-action-handler.service';
|
||||
import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/update-webhook-action-handler.service';
|
||||
import { CreateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/create-view-field-action-handler.service';
|
||||
import { DeleteViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/delete-view-field-action-handler.service';
|
||||
import { UpdateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view-field/services/update-view-field-action-handler.service';
|
||||
@@ -173,6 +176,10 @@ import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/
|
||||
CreateFrontComponentActionHandlerService,
|
||||
UpdateFrontComponentActionHandlerService,
|
||||
DeleteFrontComponentActionHandlerService,
|
||||
|
||||
CreateWebhookActionHandlerService,
|
||||
UpdateWebhookActionHandlerService,
|
||||
DeleteWebhookActionHandlerService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
|
||||
|
||||
+2
-1
@@ -71,7 +71,8 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = <
|
||||
case 'pageLayoutWidget':
|
||||
case 'pageLayoutTab':
|
||||
case 'commandMenuItem':
|
||||
case 'frontComponent': {
|
||||
case 'frontComponent':
|
||||
case 'webhook': {
|
||||
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: action.flatEntity,
|
||||
flatEntityAndRelatedMapsToMutate: allFlatEntityMaps,
|
||||
|
||||
+2
-1
@@ -48,7 +48,8 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = <
|
||||
case 'pageLayoutWidget':
|
||||
case 'pageLayoutTab':
|
||||
case 'commandMenuItem':
|
||||
case 'frontComponent': {
|
||||
case 'frontComponent':
|
||||
case 'webhook': {
|
||||
const flatEntityToDelete = findFlatEntityByUniversalIdentifierOrThrow<
|
||||
MetadataFlatEntity<typeof action.metadataName>
|
||||
>({
|
||||
|
||||
+2
-1
@@ -70,7 +70,8 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = <
|
||||
case 'pageLayoutWidget':
|
||||
case 'pageLayoutTab':
|
||||
case 'commandMenuItem':
|
||||
case 'frontComponent': {
|
||||
case 'frontComponent':
|
||||
case 'webhook': {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
|
||||
action.metadataName,
|
||||
);
|
||||
|
||||
+17
-9
@@ -11,6 +11,8 @@ import {
|
||||
import { makeAdminPanelAPIRequest } from 'test/integration/twenty-config/utils/make-admin-panel-api-request.util';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { type UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input';
|
||||
|
||||
const CREATE_CONFIG_VARIABLE_MUTATION = gql`
|
||||
mutation CreateDatabaseConfigVariable($key: String!, $value: JSON!) {
|
||||
createDatabaseConfigVariable(key: $key, value: $value)
|
||||
@@ -139,12 +141,14 @@ describe('webhooksResolver (e2e)', () => {
|
||||
|
||||
createdWebhookId = createdWebhookData.id;
|
||||
|
||||
const updateInput = {
|
||||
const updateInput: UpdateWebhookInput = {
|
||||
id: createdWebhookData.id,
|
||||
targetUrl: 'https://updated.com/webhook',
|
||||
operations: ['person.updated', 'company.created'],
|
||||
description: 'Updated webhook',
|
||||
secret: 'updated-secret',
|
||||
update: {
|
||||
targetUrl: 'https://updated.com/webhook',
|
||||
operations: ['person.updated', 'company.created'],
|
||||
description: 'Updated webhook',
|
||||
secret: 'updated-secret',
|
||||
},
|
||||
};
|
||||
|
||||
const updateResponse = await updateWebhook(updateInput);
|
||||
@@ -156,10 +160,14 @@ describe('webhooksResolver (e2e)', () => {
|
||||
const updatedWebhookData = updateResponse.body.data.updateWebhook;
|
||||
|
||||
expect(updatedWebhookData.id).toBe(createdWebhookData.id);
|
||||
expect(updatedWebhookData.targetUrl).toBe(updateInput.targetUrl);
|
||||
expect(updatedWebhookData.operations).toEqual(updateInput.operations);
|
||||
expect(updatedWebhookData.description).toBe(updateInput.description);
|
||||
expect(updatedWebhookData.secret).toBe(updateInput.secret);
|
||||
expect(updatedWebhookData.targetUrl).toBe(updateInput.update.targetUrl);
|
||||
expect(updatedWebhookData.operations).toEqual(
|
||||
updateInput.update.operations,
|
||||
);
|
||||
expect(updatedWebhookData.description).toBe(
|
||||
updateInput.update.description,
|
||||
);
|
||||
expect(updatedWebhookData.secret).toBe(updateInput.update.secret);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,14 +17,20 @@ const CREATE_WEBHOOK_MUTATION = gql`
|
||||
`;
|
||||
|
||||
const DELETE_WEBHOOK_MUTATION = gql`
|
||||
mutation DeleteWebhook($input: DeleteWebhookInput!) {
|
||||
deleteWebhook(input: $input)
|
||||
mutation DeleteWebhook($id: UUID!) {
|
||||
deleteWebhook(id: $id) {
|
||||
id
|
||||
targetUrl
|
||||
operations
|
||||
description
|
||||
secret
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const GET_WEBHOOK_QUERY = gql`
|
||||
query GetWebhook($input: GetWebhookInput!) {
|
||||
webhook(input: $input) {
|
||||
query GetWebhook($id: UUID!) {
|
||||
webhook(id: $id) {
|
||||
id
|
||||
targetUrl
|
||||
operations
|
||||
@@ -81,14 +87,14 @@ export const createWebhook = (input: WebhookInput) => {
|
||||
export const deleteWebhook = (id: string) => {
|
||||
return makeMetadataAPIRequest({
|
||||
query: DELETE_WEBHOOK_MUTATION,
|
||||
variables: { input: { id } },
|
||||
variables: { id },
|
||||
});
|
||||
};
|
||||
|
||||
export const getWebhook = (id: string) => {
|
||||
return makeMetadataAPIRequest({
|
||||
query: GET_WEBHOOK_QUERY,
|
||||
variables: { input: { id } },
|
||||
variables: { id },
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -23,4 +23,5 @@ export const ALL_METADATA_NAME = {
|
||||
commandMenuItem: 'commandMenuItem',
|
||||
navigationMenuItem: 'navigationMenuItem',
|
||||
frontComponent: 'frontComponent',
|
||||
webhook: 'webhook',
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user