refactor: validate objectMetadataItem with Zod on creation and update… (#4270)

* refactor: validate objectMetadataItem with Zod on creation and update & remove logic from useObjectMetadataItemForSettings

* refactor: review
This commit is contained in:
Thaïs
2024-03-05 07:32:30 -03:00
committed by GitHub
parent 0a2d8056bd
commit a9f4a66c4f
20 changed files with 332 additions and 189 deletions

View File

@ -24,6 +24,7 @@ export const query = gql`
export const variables = {
input: {
object: {
icon: 'IconPlus',
labelPlural: 'View Filters',
labelSingular: 'View Filter',
nameSingular: 'viewFilter',

View File

@ -3,14 +3,14 @@ import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { useCreateOneObjectRecordMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider';
import {
query,
responseData,
variables,
} from '../__mocks__/useCreateOneObjectRecordMetadataItem';
} from '../__mocks__/useCreateOneObjectMetadataItem';
const mocks = [
{
@ -36,21 +36,19 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
</RecoilRoot>
);
describe('useCreateOneObjectRecordMetadataItem', () => {
describe('useCreateOneObjectMetadataItem', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => useCreateOneObjectRecordMetadataItem(),
{
wrapper: Wrapper,
},
);
const { result } = renderHook(() => useCreateOneObjectMetadataItem(), {
wrapper: Wrapper,
});
await act(async () => {
const res = await result.current.createOneObjectMetadataItem({
icon: 'IconPlus',
labelPlural: 'View Filters',
labelSingular: 'View Filter',
nameSingular: 'viewFilter',
namePlural: 'viewFilters',
nameSingular: 'viewFilter',
});
expect(res.data).toEqual({ createOneObject: responseData });

View File

@ -103,28 +103,4 @@ describe('useObjectMetadataItemForSettings', () => {
expect(res?.namePlural).toBe('opportunities');
});
});
it('should editObjectMetadataItem', async () => {
const { result } = renderHook(
() => {
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
setMetadataItems(mockObjectMetadataItems);
return useObjectMetadataItemForSettings();
},
{
wrapper: Wrapper,
},
);
await act(async () => {
const res = await result.current.editObjectMetadataItem({
id: 'idToUpdate',
description: 'newDescription',
labelPlural: 'labelPlural',
labelSingular: 'labelSingular',
});
expect(res.data).toEqual({ updateOneObject: responseData });
});
});
});

View File

@ -2,6 +2,7 @@ import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import {
CreateObjectInput,
CreateOneObjectMetadataItemMutation,
CreateOneObjectMetadataItemMutationVariables,
} from '~/generated-metadata/graphql';
@ -11,7 +12,7 @@ import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useCreateOneObjectRecordMetadataItem = () => {
export const useCreateOneObjectMetadataItem = () => {
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
@ -21,16 +22,10 @@ export const useCreateOneObjectRecordMetadataItem = () => {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const createOneObjectMetadataItem = async (
input: CreateOneObjectMetadataItemMutationVariables['input']['object'],
) => {
const createOneObjectMetadataItem = async (input: CreateObjectInput) => {
return await mutate({
variables: {
input: {
object: {
...input,
},
},
input: { object: input },
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],

View File

@ -2,14 +2,8 @@ import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
import { formatObjectMetadataItemInput } from '../utils/formatObjectMetadataItemInput';
import { getObjectSlug } from '../utils/getObjectSlug';
import { useCreateOneObjectRecordMetadataItem } from './useCreateOneObjectMetadataItem';
import { useDeleteOneObjectMetadataItem } from './useDeleteOneObjectMetadataItem';
import { useUpdateOneObjectMetadataItem } from './useUpdateOneObjectMetadataItem';
export const useObjectMetadataItemForSettings = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
@ -36,65 +30,12 @@ export const useObjectMetadataItemForSettings = () => {
(objectMetadataItem) => objectMetadataItem.namePlural === namePlural,
);
const { createOneObjectMetadataItem } =
useCreateOneObjectRecordMetadataItem();
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
const createObjectMetadataItem = (
input: Pick<
ObjectMetadataItem,
'labelPlural' | 'labelSingular' | 'icon' | 'description'
>,
) => createOneObjectMetadataItem(formatObjectMetadataItemInput(input));
const editObjectMetadataItem = (
input: Pick<
ObjectMetadataItem,
| 'description'
| 'icon'
| 'id'
| 'labelIdentifierFieldMetadataId'
| 'labelPlural'
| 'labelSingular'
>,
) =>
updateOneObjectMetadataItem({
idToUpdate: input.id,
updatePayload: formatObjectMetadataItemInput(input),
});
const activateObjectMetadataItem = (
objectMetadataItem: Pick<ObjectMetadataItem, 'id'>,
) =>
updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: { isActive: true },
});
const disableObjectMetadataItem = (
objectMetadataItem: Pick<ObjectMetadataItem, 'id'>,
) =>
updateOneObjectMetadataItem({
idToUpdate: objectMetadataItem.id,
updatePayload: { isActive: false },
});
const eraseObjectMetadataItem = (
objectMetadataItem: Pick<ObjectMetadataItem, 'id'>,
) => deleteOneObjectMetadataItem(objectMetadataItem.id);
return {
activateObjectMetadataItem,
activeObjectMetadataItems,
createObjectMetadataItem,
inactiveObjectMetadataItems,
disableObjectMetadataItem,
editObjectMetadataItem,
eraseObjectMetadataItem,
findActiveObjectMetadataItemBySlug,
findObjectMetadataItemById,
findObjectMetadataItemByNamePlural,
inactiveObjectMetadataItems,
objectMetadataItems,
};
};

View File

@ -2,6 +2,7 @@ import { useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import {
UpdateObjectInput,
UpdateOneObjectMetadataItemMutation,
UpdateOneObjectMetadataItemMutationVariables,
} from '~/generated-metadata/graphql';
@ -27,16 +28,7 @@ export const useUpdateOneObjectMetadataItem = () => {
updatePayload,
}: {
idToUpdate: UpdateOneObjectMetadataItemMutationVariables['idToUpdate'];
updatePayload: Pick<
UpdateOneObjectMetadataItemMutationVariables['updatePayload'],
| 'description'
| 'icon'
| 'isActive'
| 'labelPlural'
| 'labelSingular'
| 'namePlural'
| 'nameSingular'
>;
updatePayload: UpdateObjectInput;
}) => {
return await mutate({
variables: {

View File

@ -1,23 +0,0 @@
import toCamelCase from 'lodash.camelcase';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
export const formatObjectMetadataItemInput = (
input: Pick<
ObjectMetadataItem,
| 'description'
| 'icon'
| 'labelIdentifierFieldMetadataId'
| 'labelPlural'
| 'labelSingular'
>,
) => ({
description: input.description?.trim() ?? null,
icon: input.icon,
labelIdentifierFieldMetadataId:
input.labelIdentifierFieldMetadataId?.trim() ?? null,
labelPlural: input.labelPlural.trim(),
labelSingular: input.labelSingular.trim(),
namePlural: toCamelCase(input.labelPlural.trim()),
nameSingular: toCamelCase(input.labelSingular.trim()),
});

View File

@ -0,0 +1,48 @@
import { SafeParseSuccess } from 'zod';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mockedCompanyObjectMetadataItem } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { objectMetadataItemSchema } from '../objectMetadataItemSchema';
describe('objectMetadataItemSchema', () => {
it('validates a valid object metadata item', () => {
// Given
const validObjectMetadataItem = mockedCompanyObjectMetadataItem;
// When
const result = objectMetadataItemSchema.safeParse(validObjectMetadataItem);
// Then
expect(result.success).toBe(true);
expect((result as SafeParseSuccess<ObjectMetadataItem>).data).toEqual(
validObjectMetadataItem,
);
});
it('fails for an invalid object metadata item', () => {
// Given
const invalidObjectMetadataItem = {
createdAt: 'invalid date',
dataSourceId: 'invalid uuid',
fields: 'not an array',
icon: 'invalid icon',
isActive: 'not a boolean',
isCustom: 'not a boolean',
isSystem: 'not a boolean',
labelPlural: 123,
labelSingular: 123,
namePlural: 'notCamelCase',
nameSingular: 'notCamelCase',
updatedAt: 'invalid date',
};
// When
const result = objectMetadataItemSchema.safeParse(
invalidObjectMetadataItem,
);
// Then
expect(result.success).toBe(false);
});
});

View File

@ -0,0 +1,6 @@
import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
// TODO: implement fieldMetadataItemSchema
export const fieldMetadataItemSchema: z.ZodType<FieldMetadataItem> = z.any();

View File

@ -0,0 +1,25 @@
import { z } from 'zod';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
export const objectMetadataItemSchema = z.object({
__typename: z.literal('object').optional(),
createdAt: z.string().datetime(),
dataSourceId: z.string().uuid(),
description: z.string().trim().nullable().optional(),
fields: z.array(fieldMetadataItemSchema),
icon: z.string().startsWith('Icon').trim(),
id: z.string().uuid(),
imageIdentifierFieldMetadataId: z.string().uuid().nullable(),
isActive: z.boolean(),
isCustom: z.boolean(),
isSystem: z.boolean(),
labelIdentifierFieldMetadataId: z.string().uuid().nullable(),
labelPlural: z.string().trim().min(1),
labelSingular: z.string().trim().min(1),
namePlural: camelCaseStringSchema,
nameSingular: camelCaseStringSchema,
updatedAt: z.string().datetime(),
}) satisfies z.ZodType<ObjectMetadataItem>;

View File

@ -0,0 +1,47 @@
import { SafeParseSuccess } from 'zod';
import { CreateObjectInput } from '~/generated-metadata/graphql';
import { settingsCreateObjectInputSchema } from '..//settingsCreateObjectInputSchema';
describe('settingsCreateObjectInputSchema', () => {
it('validates a valid input and adds name properties', () => {
// Given
const validInput = {
description: 'A valid description',
icon: 'IconPlus',
labelPlural: ' Labels ',
labelSingular: 'Label ',
};
// When
const result = settingsCreateObjectInputSchema.safeParse(validInput);
// Then
expect(result.success).toBe(true);
expect((result as SafeParseSuccess<CreateObjectInput>).data).toEqual({
description: validInput.description,
icon: validInput.icon,
labelPlural: 'Labels',
labelSingular: 'Label',
namePlural: 'labels',
nameSingular: 'label',
});
});
it('fails for an invalid input', () => {
// Given
const invalidInput = {
description: 123,
icon: true,
labelPlural: [],
labelSingular: {},
};
// When
const result = settingsCreateObjectInputSchema.safeParse(invalidInput);
// Then
expect(result.success).toBe(false);
});
});

View File

@ -0,0 +1,50 @@
import { SafeParseSuccess } from 'zod';
import { UpdateObjectInput } from '~/generated-metadata/graphql';
import { settingsUpdateObjectInputSchema } from '../settingsUpdateObjectInputSchema';
describe('settingsUpdateObjectInputSchema', () => {
it('validates a valid input and adds name properties', () => {
// Given
const validInput = {
description: 'A valid description',
icon: 'IconName',
labelPlural: 'Labels Plural ',
labelSingular: ' Label Singular',
labelIdentifierFieldMetadataId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
};
// When
const result = settingsUpdateObjectInputSchema.safeParse(validInput);
// Then
expect(result.success).toBe(true);
expect((result as SafeParseSuccess<UpdateObjectInput>).data).toEqual({
description: validInput.description,
icon: validInput.icon,
labelIdentifierFieldMetadataId: validInput.labelIdentifierFieldMetadataId,
labelPlural: 'Labels Plural',
labelSingular: 'Label Singular',
namePlural: 'labelsPlural',
nameSingular: 'labelSingular',
});
});
it('fails for an invalid input', () => {
// Given
const invalidInput = {
description: 123,
icon: true,
labelPlural: [],
labelSingular: {},
labelIdentifierFieldMetadataId: 'invalid uuid',
};
// When
const result = settingsUpdateObjectInputSchema.safeParse(invalidInput);
// Then
expect(result.success).toBe(false);
});
});

View File

@ -0,0 +1,17 @@
import camelCase from 'lodash.camelcase';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { CreateObjectInput } from '~/generated-metadata/graphql';
export const settingsCreateObjectInputSchema = objectMetadataItemSchema
.pick({
description: true,
icon: true,
labelPlural: true,
labelSingular: true,
})
.transform<CreateObjectInput>((value) => ({
...value,
nameSingular: camelCase(value.labelSingular),
namePlural: camelCase(value.labelPlural),
}));

View File

@ -0,0 +1,23 @@
import camelCase from 'lodash.camelcase';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { UpdateObjectInput } from '~/generated-metadata/graphql';
export const settingsUpdateObjectInputSchema = objectMetadataItemSchema
.pick({
description: true,
icon: true,
imageIdentifierFieldMetadataId: true,
isActive: true,
labelIdentifierFieldMetadataId: true,
labelPlural: true,
labelSingular: true,
})
.partial()
.transform<UpdateObjectInput>((value) => ({
...value,
nameSingular: value.labelSingular
? camelCase(value.labelSingular)
: undefined,
namePlural: value.labelPlural ? camelCase(value.labelPlural) : undefined,
}));