repos / starfx

a micro-mvc framework for react apps
git clone https://github.com/neurosnap/starfx.git

starfx / src / store / slice
Eric Bower  ·  2025-06-06

table.ts

  1import { createSelector } from "reselect";
  2import type { AnyState, IdProp } from "../../types.js";
  3import type { BaseSchema } from "../types.js";
  4
  5interface PropId {
  6  id: IdProp;
  7}
  8
  9interface PropIds {
 10  ids: IdProp[];
 11}
 12
 13interface PatchEntity<T> {
 14  [key: string]: Partial<T[keyof T]>;
 15}
 16
 17const excludesFalse = <T>(n?: T): n is T => Boolean(n);
 18
 19function mustSelectEntity<Entity extends AnyState = AnyState>(
 20  defaultEntity: Entity | (() => Entity),
 21) {
 22  const isFn = typeof defaultEntity === "function";
 23
 24  return function selectEntity<S extends AnyState = AnyState>(
 25    selectById: (s: S, p: PropId) => Entity | undefined,
 26  ) {
 27    return (state: S, { id }: PropId): Entity => {
 28      if (isFn) {
 29        const entity = defaultEntity as () => Entity;
 30        return selectById(state, { id }) || entity();
 31      }
 32
 33      return selectById(state, { id }) || (defaultEntity as Entity);
 34    };
 35  };
 36}
 37
 38function tableSelectors<
 39  Entity extends AnyState = AnyState,
 40  S extends AnyState = AnyState,
 41>(
 42  selectTable: (s: S) => Record<IdProp, Entity>,
 43  empty?: Entity | (() => Entity) | undefined,
 44) {
 45  const must = empty ? mustSelectEntity(empty) : null;
 46  const tableAsList = (data: Record<IdProp, Entity>): Entity[] =>
 47    Object.values(data).filter(excludesFalse);
 48  const findById = (data: Record<IdProp, Entity>, { id }: PropId) => data[id];
 49  const findByIds = (
 50    data: Record<IdProp, Entity>,
 51    { ids }: PropIds,
 52  ): Entity[] => ids.map((id) => data[id]).filter(excludesFalse);
 53  const selectById = (
 54    state: S,
 55    { id }: PropId,
 56  ): typeof empty extends undefined ? Entity | undefined : Entity => {
 57    const data = selectTable(state);
 58    return findById(data, { id });
 59  };
 60
 61  const sbi = must ? must(selectById) : selectById;
 62
 63  return {
 64    findById: must ? must(findById) : findById,
 65    findByIds,
 66    tableAsList,
 67    selectTable,
 68    selectTableAsList: createSelector(selectTable, (data): Entity[] =>
 69      tableAsList(data),
 70    ),
 71    selectById: sbi,
 72    selectByIds: createSelector(
 73      selectTable,
 74      (_: S, p: PropIds) => p.ids,
 75      (data, ids) => findByIds(data, { ids }),
 76    ),
 77  };
 78}
 79
 80export interface TableOutput<
 81  Entity extends AnyState,
 82  S extends AnyState,
 83  Empty extends Entity | undefined = Entity | undefined,
 84> extends BaseSchema<Record<IdProp, Entity>> {
 85  schema: "table";
 86  initialState: Record<IdProp, Entity>;
 87  empty: Empty;
 88  add: (e: Record<IdProp, Entity>) => (s: S) => void;
 89  set: (e: Record<IdProp, Entity>) => (s: S) => void;
 90  remove: (ids: IdProp[]) => (s: S) => void;
 91  patch: (e: PatchEntity<Record<IdProp, Entity>>) => (s: S) => void;
 92  merge: (e: PatchEntity<Record<IdProp, Entity>>) => (s: S) => void;
 93  reset: () => (s: S) => void;
 94  findById: (d: Record<IdProp, Entity>, { id }: PropId) => Empty;
 95  findByIds: (d: Record<IdProp, Entity>, { ids }: PropIds) => Entity[];
 96  tableAsList: (d: Record<IdProp, Entity>) => Entity[];
 97  selectTable: (s: S) => Record<IdProp, Entity>;
 98  selectTableAsList: (state: S) => Entity[];
 99  selectById: (s: S, p: PropId) => Empty;
100  selectByIds: (s: S, p: PropIds) => Entity[];
101}
102
103export function createTable<
104  Entity extends AnyState = AnyState,
105  S extends AnyState = AnyState,
106>(p: {
107  name: keyof S;
108  initialState?: Record<IdProp, Entity>;
109  empty: Entity | (() => Entity);
110}): TableOutput<Entity, S, Entity>;
111export function createTable<
112  Entity extends AnyState = AnyState,
113  S extends AnyState = AnyState,
114>(p: {
115  name: keyof S;
116  initialState?: Record<IdProp, Entity>;
117  empty?: Entity | (() => Entity);
118}): TableOutput<Entity, S, Entity | undefined>;
119export function createTable<
120  Entity extends AnyState = AnyState,
121  S extends AnyState = AnyState,
122>({
123  name,
124  empty,
125  initialState = {},
126}: {
127  name: keyof S;
128  initialState?: Record<IdProp, Entity>;
129  empty?: Entity | (() => Entity);
130}): TableOutput<Entity, S, Entity | undefined> {
131  const selectors = tableSelectors<Entity, S>((s: S) => s[name], empty);
132
133  return {
134    schema: "table",
135    name: name as string,
136    initialState,
137    empty: typeof empty === "function" ? empty() : empty,
138    add: (entities) => (s) => {
139      const state = selectors.selectTable(s);
140      Object.keys(entities).forEach((id) => {
141        state[id] = entities[id];
142      });
143    },
144    set: (entities) => (s) => {
145      // deno-lint-ignore no-explicit-any
146      (s as any)[name] = entities;
147    },
148    remove: (ids) => (s) => {
149      const state = selectors.selectTable(s);
150      ids.forEach((id) => {
151        delete state[id];
152      });
153    },
154    patch: (entities) => (s) => {
155      const state = selectors.selectTable(s);
156      Object.keys(entities).forEach((id) => {
157        state[id] = { ...state[id], ...entities[id] };
158      });
159    },
160    merge: (entities) => (s) => {
161      const state = selectors.selectTable(s);
162      Object.keys(entities).forEach((id) => {
163        const entity = entities[id];
164        Object.keys(entity).forEach((prop) => {
165          const val = entity[prop];
166          if (Array.isArray(val)) {
167            // deno-lint-ignore no-explicit-any
168            const list = val as any[];
169            // deno-lint-ignore no-explicit-any
170            (state as any)[id][prop].push(...list);
171          } else {
172            // deno-lint-ignore no-explicit-any
173            (state as any)[id][prop] = entities[id][prop];
174          }
175        });
176      });
177    },
178    reset: () => (s) => {
179      // deno-lint-ignore no-explicit-any
180      (s as any)[name] = initialState;
181    },
182    ...selectors,
183  };
184}
185
186export function table<
187  Entity extends AnyState = AnyState,
188  S extends AnyState = AnyState,
189>(p: {
190  initialState?: Record<IdProp, Entity>;
191  empty: Entity | (() => Entity);
192}): (n: string) => TableOutput<Entity, S, Entity>;
193export function table<
194  Entity extends AnyState = AnyState,
195  S extends AnyState = AnyState,
196>(p?: {
197  initialState?: Record<IdProp, Entity>;
198  empty?: Entity | (() => Entity);
199}): (n: string) => TableOutput<Entity, S, Entity | undefined>;
200export function table<
201  Entity extends AnyState = AnyState,
202  S extends AnyState = AnyState,
203>({
204  initialState,
205  empty,
206}: {
207  initialState?: Record<IdProp, Entity>;
208  empty?: Entity | (() => Entity);
209} = {}): (n: string) => TableOutput<Entity, S, Entity | undefined> {
210  return (name: string) => createTable<Entity>({ name, empty, initialState });
211}