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:
@ -24,6 +24,7 @@ export const query = gql`
|
||||
export const variables = {
|
||||
input: {
|
||||
object: {
|
||||
icon: 'IconPlus',
|
||||
labelPlural: 'View Filters',
|
||||
labelSingular: 'View Filter',
|
||||
nameSingular: 'viewFilter',
|
||||
@ -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 });
|
||||
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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) ?? ''],
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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()),
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
@ -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>;
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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),
|
||||
}));
|
||||
@ -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,
|
||||
}));
|
||||
Reference in New Issue
Block a user