EntityStore: consolidate types, fix type of "path"

This commit is contained in:
Alex Gleason 2023-03-22 16:06:10 -05:00
parent d2fd9e0387
commit 8f67d2c76f
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
8 changed files with 68 additions and 37 deletions

View file

@ -3,4 +3,30 @@ import type z from 'zod';
type EntitySchema<TEntity extends Entity = Entity> = z.ZodType<TEntity, z.ZodTypeDef, any>; type EntitySchema<TEntity extends Entity = Entity> = z.ZodType<TEntity, z.ZodTypeDef, any>;
export type { EntitySchema }; /**
* Tells us where to find/store the entity in the cache.
* This value is accepted in hooks, but needs to be parsed into an `EntitiesPath`
* before being passed to the store.
*/
type ExpandedEntitiesPath = [
/** Name of the entity type for use in the global cache, eg `'Notification'`. */
entityType: string,
/**
* Name of a particular index of this entity type.
* Multiple params get combined into one string with a `:` separator.
*/
...listKeys: string[],
]
/** Used to look up an entity in a list. */
type EntitiesPath = [entityType: string, listKey: string]
/** Used to look up a single entity by its ID. */
type EntityPath = [entityType: string, entityId: string]
export type {
EntitySchema,
ExpandedEntitiesPath,
EntitiesPath,
EntityPath,
};

View file

@ -4,10 +4,11 @@ import { useAppDispatch } from 'soapbox/hooks';
import { importEntities } from '../actions'; import { importEntities } from '../actions';
import type { Entity } from '../types'; import { parseEntitiesPath } from './utils';
import type { EntitySchema } from './types';
import type { Entity } from '../types';
import type { EntitySchema, ExpandedEntitiesPath } from './types';
type EntityPath = [entityType: string, listKey?: string]
type CreateFn<Params, Result> = (params: Params) => Promise<Result> | Result; type CreateFn<Params, Result> = (params: Params) => Promise<Result> | Result;
interface UseCreateEntityOpts<TEntity extends Entity = Entity> { interface UseCreateEntityOpts<TEntity extends Entity = Entity> {
@ -30,11 +31,13 @@ interface EntityCallbacks<TEntity extends Entity = Entity, Error = unknown> {
} }
function useCreateEntity<TEntity extends Entity = Entity, Params = any, Result = unknown>( function useCreateEntity<TEntity extends Entity = Entity, Params = any, Result = unknown>(
path: EntityPath, expandedPath: ExpandedEntitiesPath,
createFn: CreateFn<Params, Result>, createFn: CreateFn<Params, Result>,
opts: UseCreateEntityOpts<TEntity> = {}, opts: UseCreateEntityOpts<TEntity> = {},
) { ) {
const path = parseEntitiesPath(expandedPath);
const [entityType, listKey] = path; const [entityType, listKey] = path;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
return async function createEntity( return async function createEntity(

View file

@ -2,15 +2,20 @@ import { useAppDispatch } from 'soapbox/hooks';
import { dismissEntities } from '../actions'; import { dismissEntities } from '../actions';
type EntityPath = [entityType: string, listKey: string] import { parseEntitiesPath } from './utils';
import type { ExpandedEntitiesPath } from './types';
type DismissFn<T> = (entityId: string) => Promise<T> | T; type DismissFn<T> = (entityId: string) => Promise<T> | T;
/** /**
* Removes an entity from a specific list. * Removes an entity from a specific list.
* To remove an entity globally from all lists, see `useDeleteEntity`. * To remove an entity globally from all lists, see `useDeleteEntity`.
*/ */
function useDismissEntity<T = unknown>(path: EntityPath, dismissFn: DismissFn<T>) { function useDismissEntity<T = unknown>(expandedPath: ExpandedEntitiesPath, dismissFn: DismissFn<T>) {
const path = parseEntitiesPath(expandedPath);
const [entityType, listKey] = path; const [entityType, listKey] = path;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
// TODO: optimistic dismissing // TODO: optimistic dismissing

View file

@ -7,21 +7,11 @@ import { filteredArray } from 'soapbox/schemas/utils';
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions'; import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions';
import type { Entity, EntityListState } from '../types'; import { parseEntitiesPath } from './utils';
import type { EntitySchema } from './types';
import type { RootState } from 'soapbox/store';
/** Tells us where to find/store the entity in the cache. */ import type { Entity, EntityListState } from '../types';
type EntityPath = [ import type { EntitiesPath, EntitySchema, ExpandedEntitiesPath } from './types';
/** Name of the entity type for use in the global cache, eg `'Notification'`. */ import type { RootState } from 'soapbox/store';
entityType: string,
/**
* Name of a particular index of this entity type.
* Multiple params get combined into one string with a `:` separator.
* You can use empty-string (`''`) if you don't need separate lists.
*/
...listKeys: string[],
]
/** Additional options for the hook. */ /** Additional options for the hook. */
interface UseEntitiesOpts<TEntity extends Entity> { interface UseEntitiesOpts<TEntity extends Entity> {
@ -39,7 +29,7 @@ interface UseEntitiesOpts<TEntity extends Entity> {
/** A hook for fetching and displaying API entities. */ /** A hook for fetching and displaying API entities. */
function useEntities<TEntity extends Entity>( function useEntities<TEntity extends Entity>(
/** Tells us where to find/store the entity in the cache. */ /** Tells us where to find/store the entity in the cache. */
path: EntityPath, expandedPath: ExpandedEntitiesPath,
/** API route to GET, eg `'/api/v1/notifications'`. If undefined, nothing will be fetched. */ /** API route to GET, eg `'/api/v1/notifications'`. If undefined, nothing will be fetched. */
endpoint: string | undefined, endpoint: string | undefined,
/** Additional options for the hook. */ /** Additional options for the hook. */
@ -49,8 +39,8 @@ function useEntities<TEntity extends Entity>(
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const getState = useGetState(); const getState = useGetState();
const [entityType, ...listKeys] = path; const path = parseEntitiesPath(expandedPath);
const listKey = listKeys.join(':'); const [entityType, listKey] = path;
const entities = useAppSelector(state => selectEntities<TEntity>(state, path)); const entities = useAppSelector(state => selectEntities<TEntity>(state, path));
@ -128,10 +118,10 @@ function useEntities<TEntity extends Entity>(
} }
/** Get cache at path from Redux. */ /** Get cache at path from Redux. */
const selectCache = (state: RootState, path: EntityPath) => state.entities[path[0]]; const selectCache = (state: RootState, path: EntitiesPath) => state.entities[path[0]];
/** Get list at path from Redux. */ /** Get list at path from Redux. */
const selectList = (state: RootState, path: EntityPath) => { const selectList = (state: RootState, path: EntitiesPath) => {
const [, ...listKeys] = path; const [, ...listKeys] = path;
const listKey = listKeys.join(':'); const listKey = listKeys.join(':');
@ -139,18 +129,18 @@ const selectList = (state: RootState, path: EntityPath) => {
}; };
/** Select a particular item from a list state. */ /** Select a particular item from a list state. */
function selectListState<K extends keyof EntityListState>(state: RootState, path: EntityPath, key: K) { function selectListState<K extends keyof EntityListState>(state: RootState, path: EntitiesPath, key: K) {
const listState = selectList(state, path)?.state; const listState = selectList(state, path)?.state;
return listState ? listState[key] : undefined; return listState ? listState[key] : undefined;
} }
/** Hook to get a particular item from a list state. */ /** Hook to get a particular item from a list state. */
function useListState<K extends keyof EntityListState>(path: EntityPath, key: K) { function useListState<K extends keyof EntityListState>(path: EntitiesPath, key: K) {
return useAppSelector(state => selectListState(state, path, key)); return useAppSelector(state => selectListState(state, path, key));
} }
/** Get list of entities from Redux. */ /** Get list of entities from Redux. */
function selectEntities<TEntity extends Entity>(state: RootState, path: EntityPath): readonly TEntity[] { function selectEntities<TEntity extends Entity>(state: RootState, path: EntitiesPath): readonly TEntity[] {
const cache = selectCache(state, path); const cache = selectCache(state, path);
const list = selectList(state, path); const list = selectList(state, path);

View file

@ -6,9 +6,7 @@ import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { importEntities } from '../actions'; import { importEntities } from '../actions';
import type { Entity } from '../types'; import type { Entity } from '../types';
import type { EntitySchema } from './types'; import type { EntitySchema, EntityPath } from './types';
type EntityPath = [entityType: string, entityId: string]
/** Additional options for the hook. */ /** Additional options for the hook. */
interface UseEntityOpts<TEntity extends Entity> { interface UseEntityOpts<TEntity extends Entity> {

View file

@ -2,11 +2,10 @@ import { useApi } from 'soapbox/hooks';
import { useCreateEntity } from './useCreateEntity'; import { useCreateEntity } from './useCreateEntity';
import { useDeleteEntity } from './useDeleteEntity'; import { useDeleteEntity } from './useDeleteEntity';
import { parseEntitiesPath } from './utils';
import type { Entity } from '../types'; import type { Entity } from '../types';
import type { EntitySchema } from './types'; import type { EntitySchema, ExpandedEntitiesPath } from './types';
type EntityPath = [entityType: string, listKey?: string]
interface UseEntityActionsOpts<TEntity extends Entity = Entity> { interface UseEntityActionsOpts<TEntity extends Entity = Entity> {
schema?: EntitySchema<TEntity> schema?: EntitySchema<TEntity>
@ -18,11 +17,12 @@ interface EntityActionEndpoints {
} }
function useEntityActions<TEntity extends Entity = Entity, Params = any>( function useEntityActions<TEntity extends Entity = Entity, Params = any>(
path: EntityPath, expandedPath: ExpandedEntitiesPath,
endpoints: EntityActionEndpoints, endpoints: EntityActionEndpoints,
opts: UseEntityActionsOpts<TEntity> = {}, opts: UseEntityActionsOpts<TEntity> = {},
) { ) {
const api = useApi(); const api = useApi();
const path = parseEntitiesPath(expandedPath);
const [entityType] = path; const [entityType] = path;
const deleteEntity = useDeleteEntity(entityType, (entityId) => { const deleteEntity = useDeleteEntity(entityType, (entityId) => {

View file

@ -0,0 +1,9 @@
import type { EntitiesPath, ExpandedEntitiesPath } from './types';
function parseEntitiesPath(expandedPath: ExpandedEntitiesPath): EntitiesPath {
const [entityType, ...listKeys] = expandedPath;
const listKey = (listKeys || []).join(':');
return [entityType, listKey];
}
export { parseEntitiesPath };

View file

@ -7,7 +7,7 @@ import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/grou
function useGroups() { function useGroups() {
const { entities, ...result } = useEntities<Group>( const { entities, ...result } = useEntities<Group>(
[Entities.GROUPS, ''], [Entities.GROUPS],
'/api/v1/groups', '/api/v1/groups',
{ schema: groupSchema }, { schema: groupSchema },
); );