Pass zodSchema directly to entity hooks for safeParse validation

environments/review-entity-sto-fklt1b/deployments/2835
Alex Gleason 2023-03-13 16:23:11 -05:00
rodzic bced3d6632
commit d0ceac9987
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
3 zmienionych plików z 24 dodań i 31 usunięć

Wyświetl plik

@ -1,4 +1,5 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import z from 'zod';
import { getNextLink, getPrevLink } from 'soapbox/api'; import { getNextLink, getPrevLink } from 'soapbox/api';
import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks';
@ -17,9 +18,9 @@ type EntityPath = [
] ]
/** Additional options for the hook. */ /** Additional options for the hook. */
interface UseEntitiesOpts<TEntity> { interface UseEntitiesOpts<TEntity extends Entity> {
/** A parser function that returns the desired type, or undefined if validation fails. */ /** A zod schema to parse the API entities. */
parser?: (entity: unknown) => TEntity | undefined schema?: z.ZodType<TEntity, z.ZodTypeDef, any>
/** /**
* Time (milliseconds) until this query becomes stale and should be refetched. * 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. * It is 1 minute by default, and can be set to `Infinity` to opt-out of automatic fetching.
@ -41,8 +42,8 @@ function useEntities<TEntity extends Entity>(
const [entityType, listKey] = path; const [entityType, listKey] = path;
const defaultParser = (entity: unknown) => entity as TEntity; const defaultSchema = z.custom<TEntity>();
const parseEntity = opts.parser || defaultParser; const schema = opts.schema || defaultSchema;
const cache = useAppSelector(state => state.entities[entityType]); const cache = useAppSelector(state => state.entities[entityType]);
const list = cache?.lists[listKey]; const list = cache?.lists[listKey];
@ -51,9 +52,10 @@ function useEntities<TEntity extends Entity>(
const entities: readonly TEntity[] = entityIds ? ( const entities: readonly TEntity[] = entityIds ? (
Array.from(entityIds).reduce<TEntity[]>((result, id) => { Array.from(entityIds).reduce<TEntity[]>((result, id) => {
const entity = parseEntity(cache?.store[id] as unknown); // TODO: parse after fetch, not during render.
if (entity) { const entity = schema.safeParse(cache?.store[id]);
result.push(entity); if (entity.success) {
result.push(entity.data);
} }
return result; return result;
}, []) }, [])

Wyświetl plik

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import z from 'zod';
import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks';
@ -10,8 +11,8 @@ type EntityPath = [entityType: string, entityId: string]
/** Additional options for the hook. */ /** Additional options for the hook. */
interface UseEntityOpts<TEntity> { interface UseEntityOpts<TEntity> {
/** A parser function that returns the desired type, or undefined if validation fails. */ /** A zod schema to parse the API entity. */
parser?: (entity: unknown) => TEntity | undefined schema?: z.ZodType<TEntity, z.ZodTypeDef, any>
/** Whether to refetch this entity every time the hook mounts, even if it's already in the store. */ /** Whether to refetch this entity every time the hook mounts, even if it's already in the store. */
refetch?: boolean refetch?: boolean
} }
@ -26,10 +27,14 @@ function useEntity<TEntity extends Entity>(
const [entityType, entityId] = path; const [entityType, entityId] = path;
const defaultParser = (entity: unknown) => entity as TEntity; const defaultSchema = z.custom<TEntity>();
const parseEntity = opts.parser || defaultParser; 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 [isFetching, setIsFetching] = useState(false);
const isLoading = isFetching && !entity; const isLoading = isFetching && !entity;

Wyświetl plik

@ -3,7 +3,7 @@ import { groupSchema, Group } from 'soapbox/schemas/group';
import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship'; import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship';
function useGroups() { function useGroups() {
const { entities, ...result } = useEntities<Group>(['Group', ''], '/api/v1/groups', { parser: parseGroup }); const { entities, ...result } = useEntities<Group>(['Group', ''], '/api/v1/groups', { schema: groupSchema });
const { relationships } = useGroupRelationships(entities.map(entity => entity.id)); const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
const groups = entities.map((group) => ({ ...group, relationship: relationships[group.id] || null })); const groups = entities.map((group) => ({ ...group, relationship: relationships[group.id] || null }));
@ -15,7 +15,7 @@ function useGroups() {
} }
function useGroup(groupId: string, refetch = true) { function useGroup(groupId: string, refetch = true) {
const { entity: group, ...result } = useEntity<Group>(['Group', groupId], `/api/v1/groups/${groupId}`, { parser: parseGroup, refetch }); const { entity: group, ...result } = useEntity<Group>(['Group', groupId], `/api/v1/groups/${groupId}`, { schema: groupSchema, refetch });
const { entity: relationship } = useGroupRelationship(groupId); const { entity: relationship } = useGroupRelationship(groupId);
return { return {
@ -25,13 +25,13 @@ function useGroup(groupId: string, refetch = true) {
} }
function useGroupRelationship(groupId: string) { function useGroupRelationship(groupId: string) {
return useEntity<GroupRelationship>(['GroupRelationship', groupId], `/api/v1/groups/relationships?id[]=${groupId}`, { parser: parseGroupRelationship }); return useEntity<GroupRelationship>(['GroupRelationship', groupId], `/api/v1/groups/relationships?id[]=${groupId}`, { schema: groupRelationshipSchema });
} }
function useGroupRelationships(groupIds: string[]) { function useGroupRelationships(groupIds: string[]) {
const q = groupIds.map(id => `id[]=${id}`).join('&'); const q = groupIds.map(id => `id[]=${id}`).join('&');
const endpoint = groupIds.length ? `/api/v1/groups/relationships?${q}` : undefined; const endpoint = groupIds.length ? `/api/v1/groups/relationships?${q}` : undefined;
const { entities, ...result } = useEntities<GroupRelationship>(['GroupRelationship', q], endpoint, { parser: parseGroupRelationship }); const { entities, ...result } = useEntities<GroupRelationship>(['GroupRelationship', q], endpoint, { schema: groupRelationshipSchema });
const relationships = entities.reduce<Record<string, GroupRelationship>>((map, relationship) => { const relationships = entities.reduce<Record<string, GroupRelationship>>((map, relationship) => {
map[relationship.id] = 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 }; export { useGroup, useGroups };