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:
+67
@@ -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);
|
||||
});
|
||||
});
|
||||
+47
@@ -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']);
|
||||
});
|
||||
});
|
||||
+71
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
+42
@@ -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' }]);
|
||||
});
|
||||
});
|
||||
+14
@@ -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;
|
||||
};
|
||||
+15
@@ -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',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
+10
@@ -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 };
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user