From d0ceac99877f5e6723dd07068737c308a895b103 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 13 Mar 2023 16:23:11 -0500 Subject: [PATCH] Pass zodSchema directly to entity hooks for safeParse validation --- app/soapbox/entity-store/hooks/useEntities.ts | 18 ++++++++------- app/soapbox/entity-store/hooks/useEntity.ts | 15 ++++++++----- app/soapbox/hooks/useGroups.ts | 22 ++++--------------- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/app/soapbox/entity-store/hooks/useEntities.ts b/app/soapbox/entity-store/hooks/useEntities.ts index 909273ea3..fa7e26d62 100644 --- a/app/soapbox/entity-store/hooks/useEntities.ts +++ b/app/soapbox/entity-store/hooks/useEntities.ts @@ -1,4 +1,5 @@ import { useEffect } from 'react'; +import z from 'zod'; import { getNextLink, getPrevLink } from 'soapbox/api'; import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks'; @@ -17,9 +18,9 @@ type EntityPath = [ ] /** Additional options for the hook. */ -interface UseEntitiesOpts { - /** A parser function that returns the desired type, or undefined if validation fails. */ - parser?: (entity: unknown) => TEntity | undefined +interface UseEntitiesOpts { + /** A zod schema to parse the API entities. */ + schema?: z.ZodType /** * Time (milliseconds) until this query becomes stale and should be refetched. * It is 1 minute by default, and can be set to `Infinity` to opt-out of automatic fetching. @@ -41,8 +42,8 @@ function useEntities( const [entityType, listKey] = path; - const defaultParser = (entity: unknown) => entity as TEntity; - const parseEntity = opts.parser || defaultParser; + const defaultSchema = z.custom(); + const schema = opts.schema || defaultSchema; const cache = useAppSelector(state => state.entities[entityType]); const list = cache?.lists[listKey]; @@ -51,9 +52,10 @@ function useEntities( const entities: readonly TEntity[] = entityIds ? ( Array.from(entityIds).reduce((result, id) => { - const entity = parseEntity(cache?.store[id] as unknown); - if (entity) { - result.push(entity); + // TODO: parse after fetch, not during render. + const entity = schema.safeParse(cache?.store[id]); + if (entity.success) { + result.push(entity.data); } return result; }, []) diff --git a/app/soapbox/entity-store/hooks/useEntity.ts b/app/soapbox/entity-store/hooks/useEntity.ts index d0bae8630..d242c9b4e 100644 --- a/app/soapbox/entity-store/hooks/useEntity.ts +++ b/app/soapbox/entity-store/hooks/useEntity.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import z from 'zod'; import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks'; @@ -10,8 +11,8 @@ type EntityPath = [entityType: string, entityId: string] /** Additional options for the hook. */ interface UseEntityOpts { - /** A parser function that returns the desired type, or undefined if validation fails. */ - parser?: (entity: unknown) => TEntity | undefined + /** A zod schema to parse the API entity. */ + schema?: z.ZodType /** Whether to refetch this entity every time the hook mounts, even if it's already in the store. */ refetch?: boolean } @@ -26,10 +27,14 @@ function useEntity( const [entityType, entityId] = path; - const defaultParser = (entity: unknown) => entity as TEntity; - const parseEntity = opts.parser || defaultParser; + const defaultSchema = z.custom(); + const schema = opts.schema || defaultSchema; - const entity = useAppSelector(state => parseEntity(state.entities[entityType]?.store[entityId])); + const entity = useAppSelector(state => { + // TODO: parse after fetch, not during render. + const result = schema.safeParse(state.entities[entityType]?.store[entityId]); + return result.success ? result.data : undefined; + }); const [isFetching, setIsFetching] = useState(false); const isLoading = isFetching && !entity; diff --git a/app/soapbox/hooks/useGroups.ts b/app/soapbox/hooks/useGroups.ts index 65437675b..7873c4f5d 100644 --- a/app/soapbox/hooks/useGroups.ts +++ b/app/soapbox/hooks/useGroups.ts @@ -3,7 +3,7 @@ import { groupSchema, Group } from 'soapbox/schemas/group'; import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship'; function useGroups() { - const { entities, ...result } = useEntities(['Group', ''], '/api/v1/groups', { parser: parseGroup }); + const { entities, ...result } = useEntities(['Group', ''], '/api/v1/groups', { schema: groupSchema }); const { relationships } = useGroupRelationships(entities.map(entity => entity.id)); const groups = entities.map((group) => ({ ...group, relationship: relationships[group.id] || null })); @@ -15,7 +15,7 @@ function useGroups() { } function useGroup(groupId: string, refetch = true) { - const { entity: group, ...result } = useEntity(['Group', groupId], `/api/v1/groups/${groupId}`, { parser: parseGroup, refetch }); + const { entity: group, ...result } = useEntity(['Group', groupId], `/api/v1/groups/${groupId}`, { schema: groupSchema, refetch }); const { entity: relationship } = useGroupRelationship(groupId); return { @@ -25,13 +25,13 @@ function useGroup(groupId: string, refetch = true) { } function useGroupRelationship(groupId: string) { - return useEntity(['GroupRelationship', groupId], `/api/v1/groups/relationships?id[]=${groupId}`, { parser: parseGroupRelationship }); + return useEntity(['GroupRelationship', groupId], `/api/v1/groups/relationships?id[]=${groupId}`, { schema: groupRelationshipSchema }); } function useGroupRelationships(groupIds: string[]) { const q = groupIds.map(id => `id[]=${id}`).join('&'); const endpoint = groupIds.length ? `/api/v1/groups/relationships?${q}` : undefined; - const { entities, ...result } = useEntities(['GroupRelationship', q], endpoint, { parser: parseGroupRelationship }); + const { entities, ...result } = useEntities(['GroupRelationship', q], endpoint, { schema: groupRelationshipSchema }); const relationships = entities.reduce>((map, relationship) => { map[relationship.id] = relationship; @@ -44,18 +44,4 @@ function useGroupRelationships(groupIds: string[]) { }; } -const parseGroup = (entity: unknown) => { - const result = groupSchema.safeParse(entity); - if (result.success) { - return result.data; - } -}; - -const parseGroupRelationship = (entity: unknown) => { - const result = groupRelationshipSchema.safeParse(entity); - if (result.success) { - return result.data; - } -}; - export { useGroup, useGroups }; \ No newline at end of file