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 = {
|
export const variables = {
|
||||||
input: {
|
input: {
|
||||||
object: {
|
object: {
|
||||||
|
icon: 'IconPlus',
|
||||||
labelPlural: 'View Filters',
|
labelPlural: 'View Filters',
|
||||||
labelSingular: 'View Filter',
|
labelSingular: 'View Filter',
|
||||||
nameSingular: 'viewFilter',
|
nameSingular: 'viewFilter',
|
||||||
@ -3,14 +3,14 @@ import { MockedProvider } from '@apollo/client/testing';
|
|||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { useCreateOneObjectRecordMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
|
import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
|
||||||
|
|
||||||
import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider';
|
import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider';
|
||||||
import {
|
import {
|
||||||
query,
|
query,
|
||||||
responseData,
|
responseData,
|
||||||
variables,
|
variables,
|
||||||
} from '../__mocks__/useCreateOneObjectRecordMetadataItem';
|
} from '../__mocks__/useCreateOneObjectMetadataItem';
|
||||||
|
|
||||||
const mocks = [
|
const mocks = [
|
||||||
{
|
{
|
||||||
@ -36,21 +36,19 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
|||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('useCreateOneObjectRecordMetadataItem', () => {
|
describe('useCreateOneObjectMetadataItem', () => {
|
||||||
it('should work as expected', async () => {
|
it('should work as expected', async () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(() => useCreateOneObjectMetadataItem(), {
|
||||||
() => useCreateOneObjectRecordMetadataItem(),
|
wrapper: Wrapper,
|
||||||
{
|
});
|
||||||
wrapper: Wrapper,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const res = await result.current.createOneObjectMetadataItem({
|
const res = await result.current.createOneObjectMetadataItem({
|
||||||
|
icon: 'IconPlus',
|
||||||
labelPlural: 'View Filters',
|
labelPlural: 'View Filters',
|
||||||
labelSingular: 'View Filter',
|
labelSingular: 'View Filter',
|
||||||
nameSingular: 'viewFilter',
|
|
||||||
namePlural: 'viewFilters',
|
namePlural: 'viewFilters',
|
||||||
|
nameSingular: 'viewFilter',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.data).toEqual({ createOneObject: responseData });
|
expect(res.data).toEqual({ createOneObject: responseData });
|
||||||
@ -103,28 +103,4 @@ describe('useObjectMetadataItemForSettings', () => {
|
|||||||
expect(res?.namePlural).toBe('opportunities');
|
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 { getOperationName } from '@apollo/client/utilities';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CreateObjectInput,
|
||||||
CreateOneObjectMetadataItemMutation,
|
CreateOneObjectMetadataItemMutation,
|
||||||
CreateOneObjectMetadataItemMutationVariables,
|
CreateOneObjectMetadataItemMutationVariables,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
@ -11,7 +12,7 @@ import { FIND_MANY_OBJECT_METADATA_ITEMS } from '../graphql/queries';
|
|||||||
|
|
||||||
import { useApolloMetadataClient } from './useApolloMetadataClient';
|
import { useApolloMetadataClient } from './useApolloMetadataClient';
|
||||||
|
|
||||||
export const useCreateOneObjectRecordMetadataItem = () => {
|
export const useCreateOneObjectMetadataItem = () => {
|
||||||
const apolloMetadataClient = useApolloMetadataClient();
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
|
||||||
const [mutate] = useMutation<
|
const [mutate] = useMutation<
|
||||||
@ -21,16 +22,10 @@ export const useCreateOneObjectRecordMetadataItem = () => {
|
|||||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createOneObjectMetadataItem = async (
|
const createOneObjectMetadataItem = async (input: CreateObjectInput) => {
|
||||||
input: CreateOneObjectMetadataItemMutationVariables['input']['object'],
|
|
||||||
) => {
|
|
||||||
return await mutate({
|
return await mutate({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: { object: input },
|
||||||
object: {
|
|
||||||
...input,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
awaitRefetchQueries: true,
|
awaitRefetchQueries: true,
|
||||||
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
|
refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''],
|
||||||
|
|||||||
@ -2,14 +2,8 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
|
||||||
import { formatObjectMetadataItemInput } from '../utils/formatObjectMetadataItemInput';
|
|
||||||
import { getObjectSlug } from '../utils/getObjectSlug';
|
import { getObjectSlug } from '../utils/getObjectSlug';
|
||||||
|
|
||||||
import { useCreateOneObjectRecordMetadataItem } from './useCreateOneObjectMetadataItem';
|
|
||||||
import { useDeleteOneObjectMetadataItem } from './useDeleteOneObjectMetadataItem';
|
|
||||||
import { useUpdateOneObjectMetadataItem } from './useUpdateOneObjectMetadataItem';
|
|
||||||
|
|
||||||
export const useObjectMetadataItemForSettings = () => {
|
export const useObjectMetadataItemForSettings = () => {
|
||||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
|
|
||||||
@ -36,65 +30,12 @@ export const useObjectMetadataItemForSettings = () => {
|
|||||||
(objectMetadataItem) => objectMetadataItem.namePlural === namePlural,
|
(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 {
|
return {
|
||||||
activateObjectMetadataItem,
|
|
||||||
activeObjectMetadataItems,
|
activeObjectMetadataItems,
|
||||||
createObjectMetadataItem,
|
|
||||||
inactiveObjectMetadataItems,
|
|
||||||
disableObjectMetadataItem,
|
|
||||||
editObjectMetadataItem,
|
|
||||||
eraseObjectMetadataItem,
|
|
||||||
findActiveObjectMetadataItemBySlug,
|
findActiveObjectMetadataItemBySlug,
|
||||||
findObjectMetadataItemById,
|
findObjectMetadataItemById,
|
||||||
findObjectMetadataItemByNamePlural,
|
findObjectMetadataItemByNamePlural,
|
||||||
|
inactiveObjectMetadataItems,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useMutation } from '@apollo/client';
|
|||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
UpdateObjectInput,
|
||||||
UpdateOneObjectMetadataItemMutation,
|
UpdateOneObjectMetadataItemMutation,
|
||||||
UpdateOneObjectMetadataItemMutationVariables,
|
UpdateOneObjectMetadataItemMutationVariables,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
@ -27,16 +28,7 @@ export const useUpdateOneObjectMetadataItem = () => {
|
|||||||
updatePayload,
|
updatePayload,
|
||||||
}: {
|
}: {
|
||||||
idToUpdate: UpdateOneObjectMetadataItemMutationVariables['idToUpdate'];
|
idToUpdate: UpdateOneObjectMetadataItemMutationVariables['idToUpdate'];
|
||||||
updatePayload: Pick<
|
updatePayload: UpdateObjectInput;
|
||||||
UpdateOneObjectMetadataItemMutationVariables['updatePayload'],
|
|
||||||
| 'description'
|
|
||||||
| 'icon'
|
|
||||||
| 'isActive'
|
|
||||||
| 'labelPlural'
|
|
||||||
| 'labelSingular'
|
|
||||||
| 'namePlural'
|
|
||||||
| 'nameSingular'
|
|
||||||
>;
|
|
||||||
}) => {
|
}) => {
|
||||||
return await mutate({
|
return await mutate({
|
||||||
variables: {
|
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,
|
||||||
|
}));
|
||||||
@ -1,12 +1,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
||||||
|
import { settingsCreateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsCreateObjectInputSchema';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { IconSettings } from '@/ui/display/icon';
|
import { IconSettings } from '@/ui/display/icon';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
@ -16,27 +19,22 @@ export const SettingsNewObject = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const { createObjectMetadataItem: createObject } =
|
const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem();
|
||||||
useObjectMetadataItemForSettings();
|
|
||||||
|
|
||||||
const [customFormValues, setCustomFormValues] = useState<{
|
const [formValues, setFormValues] = useState<{
|
||||||
description?: string;
|
description?: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
labelPlural: string;
|
labelPlural: string;
|
||||||
labelSingular: string;
|
labelSingular: string;
|
||||||
}>({ icon: 'IconListNumbers', labelPlural: '', labelSingular: '' });
|
}>({ icon: 'IconListNumbers', labelPlural: '', labelSingular: '' });
|
||||||
|
|
||||||
const canSave =
|
const canSave = !!formValues.labelPlural && !!formValues.labelSingular;
|
||||||
!!customFormValues.labelPlural && !!customFormValues.labelSingular;
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
const createdObject = await createObject({
|
const createdObject = await createOneObjectMetadataItem(
|
||||||
labelPlural: customFormValues.labelPlural,
|
settingsCreateObjectInputSchema.parse(formValues),
|
||||||
labelSingular: customFormValues.labelSingular,
|
);
|
||||||
description: customFormValues.description,
|
|
||||||
icon: customFormValues.icon,
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
createdObject.data?.createOneObject.isActive
|
createdObject.data?.createOneObject.isActive
|
||||||
@ -58,25 +56,26 @@ export const SettingsNewObject = () => {
|
|||||||
<SettingsHeaderContainer>
|
<SettingsHeaderContainer>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
links={[
|
links={[
|
||||||
{ children: 'Objects', href: '/settings/objects' },
|
{
|
||||||
|
children: 'Objects',
|
||||||
|
href: getSettingsPagePath(SettingsPath.Objects),
|
||||||
|
},
|
||||||
{ children: 'New' },
|
{ children: 'New' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<SaveAndCancelButtons
|
<SaveAndCancelButtons
|
||||||
isSaveDisabled={!canSave}
|
isSaveDisabled={!canSave}
|
||||||
onCancel={() => {
|
onCancel={() => navigate(getSettingsPagePath(SettingsPath.Objects))}
|
||||||
navigate('/settings/objects');
|
|
||||||
}}
|
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
/>
|
/>
|
||||||
</SettingsHeaderContainer>
|
</SettingsHeaderContainer>
|
||||||
<SettingsObjectFormSection
|
<SettingsObjectFormSection
|
||||||
icon={customFormValues.icon}
|
icon={formValues.icon}
|
||||||
singularName={customFormValues.labelSingular}
|
singularName={formValues.labelSingular}
|
||||||
pluralName={customFormValues.labelPlural}
|
pluralName={formValues.labelPlural}
|
||||||
description={customFormValues.description}
|
description={formValues.description}
|
||||||
onChange={(formValues) => {
|
onChange={(formValues) => {
|
||||||
setCustomFormValues((previousValues) => ({
|
setFormValues((previousValues) => ({
|
||||||
...previousValues,
|
...previousValues,
|
||||||
...formValues,
|
...formValues,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||||
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||||
@ -16,7 +17,9 @@ import {
|
|||||||
StyledObjectFieldTableRow,
|
StyledObjectFieldTableRow,
|
||||||
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
|
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
|
||||||
import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType';
|
import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { IconPlus, IconSettings } from '@/ui/display/icon';
|
import { IconPlus, IconSettings } from '@/ui/display/icon';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
@ -38,11 +41,9 @@ export const SettingsObjectDetail = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { objectSlug = '' } = useParams();
|
const { objectSlug = '' } = useParams();
|
||||||
const {
|
const { findActiveObjectMetadataItemBySlug } =
|
||||||
disableObjectMetadataItem,
|
useObjectMetadataItemForSettings();
|
||||||
editObjectMetadataItem,
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
findActiveObjectMetadataItemBySlug,
|
|
||||||
} = useObjectMetadataItemForSettings();
|
|
||||||
|
|
||||||
const activeObjectMetadataItem =
|
const activeObjectMetadataItem =
|
||||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||||
@ -64,8 +65,11 @@ export const SettingsObjectDetail = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleDisableObject = async () => {
|
const handleDisableObject = async () => {
|
||||||
await disableObjectMetadataItem(activeObjectMetadataItem);
|
await updateOneObjectMetadataItem({
|
||||||
navigate('/settings/objects');
|
idToUpdate: activeObjectMetadataItem.id,
|
||||||
|
updatePayload: { isActive: false },
|
||||||
|
});
|
||||||
|
navigate(getSettingsPagePath(SettingsPath.Objects));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => {
|
const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => {
|
||||||
@ -74,12 +78,13 @@ export const SettingsObjectDetail = () => {
|
|||||||
|
|
||||||
const handleSetLabelIdentifierField = (
|
const handleSetLabelIdentifierField = (
|
||||||
activeFieldMetadatItem: FieldMetadataItem,
|
activeFieldMetadatItem: FieldMetadataItem,
|
||||||
) => {
|
) =>
|
||||||
editObjectMetadataItem({
|
updateOneObjectMetadataItem({
|
||||||
...activeObjectMetadataItem,
|
idToUpdate: activeObjectMetadataItem.id,
|
||||||
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
|
updatePayload: {
|
||||||
|
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
|
|||||||
@ -2,13 +2,17 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||||
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
||||||
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
|
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
|
||||||
|
import { settingsUpdateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { IconArchive, IconSettings } from '@/ui/display/icon';
|
import { IconArchive, IconSettings } from '@/ui/display/icon';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
@ -22,11 +26,9 @@ export const SettingsObjectEdit = () => {
|
|||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const { objectSlug = '' } = useParams();
|
const { objectSlug = '' } = useParams();
|
||||||
const {
|
const { findActiveObjectMetadataItemBySlug } =
|
||||||
disableObjectMetadataItem,
|
useObjectMetadataItemForSettings();
|
||||||
editObjectMetadataItem,
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
findActiveObjectMetadataItemBySlug,
|
|
||||||
} = useObjectMetadataItemForSettings();
|
|
||||||
|
|
||||||
const activeObjectMetadataItem =
|
const activeObjectMetadataItem =
|
||||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||||
@ -76,7 +78,10 @@ export const SettingsObjectEdit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await editObjectMetadataItem(editedObjectMetadataItem);
|
await updateOneObjectMetadataItem({
|
||||||
|
idToUpdate: activeObjectMetadataItem.id,
|
||||||
|
updatePayload: settingsUpdateObjectInputSchema.parse(formValues),
|
||||||
|
});
|
||||||
|
|
||||||
navigate(`/settings/objects/${getObjectSlug(editedObjectMetadataItem)}`);
|
navigate(`/settings/objects/${getObjectSlug(editedObjectMetadataItem)}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -87,8 +92,11 @@ export const SettingsObjectEdit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDisable = async () => {
|
const handleDisable = async () => {
|
||||||
await disableObjectMetadataItem(activeObjectMetadataItem);
|
await updateOneObjectMetadataItem({
|
||||||
navigate('/settings/objects');
|
idToUpdate: activeObjectMetadataItem.id,
|
||||||
|
updatePayload: { isActive: false },
|
||||||
|
});
|
||||||
|
navigate(getSettingsPagePath(SettingsPath.Objects));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
|
||||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||||
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
@ -12,6 +14,8 @@ import {
|
|||||||
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
|
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
|
||||||
import { SettingsObjectCoverImage } from '@/settings/data-model/objects/SettingsObjectCoverImage';
|
import { SettingsObjectCoverImage } from '@/settings/data-model/objects/SettingsObjectCoverImage';
|
||||||
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/SettingsObjectInactiveMenuDropDown';
|
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/SettingsObjectInactiveMenuDropDown';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { IconChevronRight, IconPlus, IconSettings } from '@/ui/display/icon';
|
import { IconChevronRight, IconPlus, IconSettings } from '@/ui/display/icon';
|
||||||
import { H1Title } from '@/ui/display/typography/components/H1Title';
|
import { H1Title } from '@/ui/display/typography/components/H1Title';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
@ -34,12 +38,10 @@ export const SettingsObjects = () => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const {
|
const { activeObjectMetadataItems, inactiveObjectMetadataItems } =
|
||||||
activateObjectMetadataItem,
|
useObjectMetadataItemForSettings();
|
||||||
activeObjectMetadataItems,
|
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
|
||||||
inactiveObjectMetadataItems,
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
eraseObjectMetadataItem,
|
|
||||||
} = useObjectMetadataItemForSettings();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
@ -51,7 +53,9 @@ export const SettingsObjects = () => {
|
|||||||
title="Add object"
|
title="Add object"
|
||||||
accent="blue"
|
accent="blue"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => navigate('/settings/objects/new')}
|
onClick={() =>
|
||||||
|
navigate(getSettingsPagePath(SettingsPath.NewObject))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</SettingsHeaderContainer>
|
</SettingsHeaderContainer>
|
||||||
<div>
|
<div>
|
||||||
@ -101,13 +105,14 @@ export const SettingsObjects = () => {
|
|||||||
isCustomObject={inactiveObjectMetadataItem.isCustom}
|
isCustomObject={inactiveObjectMetadataItem.isCustom}
|
||||||
scopeKey={inactiveObjectMetadataItem.namePlural}
|
scopeKey={inactiveObjectMetadataItem.namePlural}
|
||||||
onActivate={() =>
|
onActivate={() =>
|
||||||
activateObjectMetadataItem(
|
updateOneObjectMetadataItem({
|
||||||
inactiveObjectMetadataItem,
|
idToUpdate: inactiveObjectMetadataItem.id,
|
||||||
)
|
updatePayload: { isActive: true },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
onErase={() =>
|
onErase={() =>
|
||||||
eraseObjectMetadataItem(
|
deleteOneObjectMetadataItem(
|
||||||
inactiveObjectMetadataItem,
|
inactiveObjectMetadataItem.id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { SafeParseError } from 'zod';
|
||||||
|
|
||||||
|
import { camelCaseStringSchema } from '../camelCaseStringSchema';
|
||||||
|
|
||||||
|
describe('camelCaseStringSchema', () => {
|
||||||
|
it('validates a camel case string', () => {
|
||||||
|
const result = camelCaseStringSchema.safeParse('camelCaseString');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails for non-camel case strings', () => {
|
||||||
|
const result = camelCaseStringSchema.safeParse('NotCamelCase');
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect((result as SafeParseError<string>).error.errors).toEqual([
|
||||||
|
{
|
||||||
|
code: 'custom',
|
||||||
|
message: 'String should be camel case',
|
||||||
|
path: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import camelCase from 'lodash.camelcase';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const camelCaseStringSchema = z
|
||||||
|
.string()
|
||||||
|
.refine((value) => camelCase(value) === value, {
|
||||||
|
message: 'String should be camel case',
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user