Api keys and webhook migration to core (#13011)

TODO: check Zapier trigger records work as expected

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
nitin
2025-07-09 20:33:54 +05:30
committed by GitHub
parent 18792f9f74
commit 484c267aa6
113 changed files with 4563 additions and 1060 deletions
@@ -0,0 +1,67 @@
import { WEBHOOK_EMPTY_OPERATION } from '~/pages/settings/developers/webhooks/constants/WebhookEmptyOperation';
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
import { addEmptyOperationIfNecessary } from '../addEmptyOperationIfNecessary';
describe('addEmptyOperationIfNecessary', () => {
it('should add empty operation when no wildcard or null object operations exist', () => {
const operations: WebhookOperationType[] = [
{ object: 'person', action: 'created' },
{ object: 'company', action: 'updated' },
];
const result = addEmptyOperationIfNecessary(operations);
expect(result).toEqual([
{ object: 'person', action: 'created' },
{ object: 'company', action: 'updated' },
WEBHOOK_EMPTY_OPERATION,
]);
});
it('should not add empty operation when wildcard operation exists', () => {
const operations: WebhookOperationType[] = [
{ object: '*', action: '*' },
{ object: 'person', action: 'created' },
];
const result = addEmptyOperationIfNecessary(operations);
expect(result).toEqual([
{ object: '*', action: '*' },
{ object: 'person', action: 'created' },
]);
});
it('should not add empty operation when null object operation exists', () => {
const operations: WebhookOperationType[] = [
{ object: 'person', action: 'created' },
{ object: null, action: 'test' },
];
const result = addEmptyOperationIfNecessary(operations);
expect(result).toEqual([
{ object: 'person', action: 'created' },
{ object: null, action: 'test' },
]);
});
it('should handle empty array by adding empty operation', () => {
const operations: WebhookOperationType[] = [];
const result = addEmptyOperationIfNecessary(operations);
expect(result).toEqual([WEBHOOK_EMPTY_OPERATION]);
});
it('should not modify original array', () => {
const operations: WebhookOperationType[] = [
{ object: 'person', action: 'created' },
];
const originalLength = operations.length;
addEmptyOperationIfNecessary(operations);
expect(operations.length).toBe(originalLength);
});
});
@@ -0,0 +1,47 @@
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
import { cleanAndFormatOperations } from '../cleanAndFormatOperations';
describe('cleanAndFormatOperations', () => {
it('should filter out operations with null object values', () => {
const operations: WebhookOperationType[] = [
{ object: 'person', action: 'created' },
{ object: null, action: 'test' },
{ object: 'person', action: 'updated' },
];
const result = cleanAndFormatOperations(operations);
expect(result).toEqual(['person.created', 'person.updated']);
});
it('should remove duplicate operations', () => {
const operations: WebhookOperationType[] = [
{ object: 'person', action: 'created' },
{ object: 'person', action: 'created' },
{ object: 'company', action: 'updated' },
];
const result = cleanAndFormatOperations(operations);
expect(result).toEqual(['person.created', 'company.updated']);
});
it('should handle empty array', () => {
const operations: WebhookOperationType[] = [];
const result = cleanAndFormatOperations(operations);
expect(result).toEqual([]);
});
it('should handle wildcard operations', () => {
const operations: WebhookOperationType[] = [
{ object: '*', action: '*' },
{ object: 'person', action: 'created' },
];
const result = cleanAndFormatOperations(operations);
expect(result).toEqual(['*.*', 'person.created']);
});
});
@@ -0,0 +1,71 @@
import { WebhookFormValues } from '@/settings/developers/validation-schemas/webhookFormSchema';
import {
createWebhookCreateInput,
createWebhookUpdateInput,
} from '../createWebhookInput';
describe('createWebhookInput', () => {
const mockFormValues: WebhookFormValues = {
targetUrl: ' https://test.com/webhook ',
description: 'Test webhook',
operations: [
{ object: 'person', action: 'created' },
{ object: 'person', action: 'created' }, // duplicate
{ object: 'company', action: 'updated' },
{ object: null, action: 'test' }, // should be filtered out
],
secret: 'test-secret',
};
describe('createWebhookCreateInput', () => {
it('should create input for webhook creation', () => {
const result = createWebhookCreateInput(mockFormValues);
expect(result).toEqual({
targetUrl: 'https://test.com/webhook',
operations: ['person.created', 'company.updated'],
description: 'Test webhook',
secret: 'test-secret',
});
});
it('should trim targetUrl', () => {
const formValues: WebhookFormValues = {
...mockFormValues,
targetUrl: ' https://example.com ',
};
const result = createWebhookCreateInput(formValues);
expect(result.targetUrl).toBe('https://example.com');
});
});
describe('createWebhookUpdateInput', () => {
it('should create input for webhook update with id', () => {
const webhookId = 'test-webhook-id';
const result = createWebhookUpdateInput(mockFormValues, webhookId);
expect(result).toEqual({
id: 'test-webhook-id',
targetUrl: 'https://test.com/webhook',
operations: ['person.created', 'company.updated'],
description: 'Test webhook',
secret: 'test-secret',
});
});
it('should trim targetUrl and include id', () => {
const formValues: WebhookFormValues = {
...mockFormValues,
targetUrl: ' https://example.com ',
};
const webhookId = 'test-webhook-id';
const result = createWebhookUpdateInput(formValues, webhookId);
expect(result.targetUrl).toBe('https://example.com');
expect(result.id).toBe('test-webhook-id');
});
});
});
@@ -0,0 +1,42 @@
import { parseOperationsFromStrings } from '../parseOperationsFromStrings';
describe('parseOperationsFromStrings', () => {
it('should parse operation strings into object/action pairs', () => {
const operations = ['person.created', 'company.updated', 'lead.deleted'];
const result = parseOperationsFromStrings(operations);
expect(result).toEqual([
{ object: 'person', action: 'created' },
{ object: 'company', action: 'updated' },
{ object: 'lead', action: 'deleted' },
]);
});
it('should handle wildcard operations', () => {
const operations = ['*.*', 'person.created'];
const result = parseOperationsFromStrings(operations);
expect(result).toEqual([
{ object: '*', action: '*' },
{ object: 'person', action: 'created' },
]);
});
it('should handle empty array', () => {
const operations: string[] = [];
const result = parseOperationsFromStrings(operations);
expect(result).toEqual([]);
});
it('should handle operations with multiple dots by taking first two parts', () => {
const operations = ['person.created.test'];
const result = parseOperationsFromStrings(operations);
expect(result).toEqual([{ object: 'person', action: 'created' }]);
});
});
@@ -0,0 +1,14 @@
import { WEBHOOK_EMPTY_OPERATION } from '~/pages/settings/developers/webhooks/constants/WebhookEmptyOperation';
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
export const addEmptyOperationIfNecessary = (
newOperations: WebhookOperationType[],
): WebhookOperationType[] => {
if (
!newOperations.some((op) => op.object === '*' && op.action === '*') &&
!newOperations.some((op) => op.object === null)
) {
return [...newOperations, WEBHOOK_EMPTY_OPERATION];
}
return newOperations;
};
@@ -0,0 +1,15 @@
import { isDefined } from 'twenty-shared/utils';
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
export const cleanAndFormatOperations = (
operations: WebhookOperationType[],
) => {
return Array.from(
new Set(
operations
.filter((op) => isDefined(op.object) && isDefined(op.action))
.map((op) => `${op.object}.${op.action}`),
),
);
};
@@ -0,0 +1,28 @@
import { WebhookFormValues } from '@/settings/developers/validation-schemas/webhookFormSchema';
import { cleanAndFormatOperations } from './cleanAndFormatOperations';
export const createWebhookCreateInput = (formValues: WebhookFormValues) => {
const cleanedOperations = cleanAndFormatOperations(formValues.operations);
return {
targetUrl: formValues.targetUrl.trim(),
operations: cleanedOperations,
description: formValues.description,
secret: formValues.secret,
};
};
export const createWebhookUpdateInput = (
formValues: WebhookFormValues,
webhookId: string,
) => {
const cleanedOperations = cleanAndFormatOperations(formValues.operations);
return {
id: webhookId,
targetUrl: formValues.targetUrl.trim(),
operations: cleanedOperations,
description: formValues.description,
secret: formValues.secret,
};
};
@@ -2,8 +2,6 @@ import { isNonEmptyString } from '@sniptt/guards';
import { DateTime } from 'luxon';
import { NEVER_EXPIRE_DELTA_IN_YEARS } from '@/settings/developers/constants/NeverExpireDeltaInYears';
import { ApiFieldItem } from '@/settings/developers/types/api-key/ApiFieldItem';
import { ApiKey } from '@/settings/developers/types/api-key/ApiKey';
import { beautifyDateDiff } from '~/utils/date-utils';
export const doesNeverExpire = (expiresAt: string) => {
@@ -28,16 +26,3 @@ export const formatExpiration = (
}
return withExpiresMention ? `Expires in ${dateDiff}` : `In ${dateDiff}`;
};
export const formatExpirations = (
apiKeys: Array<Pick<ApiKey, 'id' | 'name' | 'expiresAt'>>,
): ApiFieldItem[] => {
return apiKeys.map(({ id, name, expiresAt }) => {
return {
id,
name,
expiration: formatExpiration(expiresAt || null),
type: 'internal',
};
});
};
@@ -0,0 +1,10 @@
import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType';
export const parseOperationsFromStrings = (
operations: string[],
): WebhookOperationType[] => {
return operations.map((op: string) => {
const [object, action] = op.split('.');
return { object, action };
});
};