Eric Bower
·
03 Jan 24
table.ts
1import { createSelector } from "../../deps.ts";
2import type { AnyState, IdProp } from "../../types.ts";
3import { BaseSchema } from "../types.ts";
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(
69 selectTable,
70 (data): Entity[] => tableAsList(data),
71 ),
72 selectById: sbi,
73 selectByIds: createSelector(
74 selectTable,
75 (_: S, p: PropIds) => p.ids,
76 (data, ids) => findByIds(data, { ids }),
77 ),
78 };
79}
80
81export interface TableOutput<
82 Entity extends AnyState,
83 S extends AnyState,
84 Empty extends Entity | undefined = Entity | undefined,
85> extends BaseSchema<Record<IdProp, Entity>> {
86 schema: "table";
87 initialState: Record<IdProp, Entity>;
88 empty: Empty;
89 add: (e: Record<IdProp, Entity>) => (s: S) => void;
90 set: (e: Record<IdProp, Entity>) => (s: S) => void;
91 remove: (ids: IdProp[]) => (s: S) => void;
92 patch: (e: PatchEntity<Record<IdProp, Entity>>) => (s: S) => void;
93 merge: (e: PatchEntity<Record<IdProp, Entity>>) => (s: S) => void;
94 reset: () => (s: S) => void;
95 findById: (
96 d: Record<IdProp, Entity>,
97 { id }: PropId,
98 ) => Empty;
99 findByIds: (d: Record<IdProp, Entity>, { ids }: PropIds) => Entity[];
100 tableAsList: (d: Record<IdProp, Entity>) => Entity[];
101 selectTable: (s: S) => Record<IdProp, Entity>;
102 selectTableAsList: (state: S) => Entity[];
103 selectById: (
104 s: S,
105 p: PropId,
106 ) => Empty;
107 selectByIds: (s: S, p: PropIds) => Entity[];
108}
109
110export function createTable<
111 Entity extends AnyState = AnyState,
112 S extends AnyState = AnyState,
113>(p: {
114 name: keyof S;
115 initialState?: Record<IdProp, Entity>;
116 empty: Entity | (() => Entity);
117}): TableOutput<Entity, S, Entity>;
118export function createTable<
119 Entity extends AnyState = AnyState,
120 S extends AnyState = AnyState,
121>(p: {
122 name: keyof S;
123 initialState?: Record<IdProp, Entity>;
124 empty?: Entity | (() => Entity);
125}): TableOutput<Entity, S, Entity | undefined>;
126export function createTable<
127 Entity extends AnyState = AnyState,
128 S extends AnyState = AnyState,
129>({
130 name,
131 empty,
132 initialState = {},
133}: {
134 name: keyof S;
135 initialState?: Record<IdProp, Entity>;
136 empty?: Entity | (() => Entity);
137}): TableOutput<Entity, S, Entity | undefined> {
138 const selectors = tableSelectors<Entity, S>((s: S) => s[name], empty);
139
140 return {
141 schema: "table",
142 name: name as string,
143 initialState,
144 empty: typeof empty === "function" ? empty() : empty,
145 add: (entities) => (s) => {
146 const state = selectors.selectTable(s);
147 Object.keys(entities).forEach((id) => {
148 state[id] = entities[id];
149 });
150 },
151 set: (entities) => (s) => {
152 // deno-lint-ignore no-explicit-any
153 (s as any)[name] = entities;
154 },
155 remove: (ids) => (s) => {
156 const state = selectors.selectTable(s);
157 ids.forEach((id) => {
158 delete state[id];
159 });
160 },
161 patch: (entities) => (s) => {
162 const state = selectors.selectTable(s);
163 Object.keys(entities).forEach((id) => {
164 state[id] = { ...state[id], ...entities[id] };
165 });
166 },
167 merge: (entities) => (s) => {
168 const state = selectors.selectTable(s);
169 Object.keys(entities).forEach((id) => {
170 const entity = entities[id];
171 Object.keys(entity).forEach((prop) => {
172 const val = entity[prop];
173 if (Array.isArray(val)) {
174 // deno-lint-ignore no-explicit-any
175 const list = val as any[];
176 // deno-lint-ignore no-explicit-any
177 (state as any)[id][prop].push(...list);
178 } else {
179 // deno-lint-ignore no-explicit-any
180 (state as any)[id][prop] = entities[id][prop];
181 }
182 });
183 });
184 },
185 reset: () => (s) => {
186 // deno-lint-ignore no-explicit-any
187 (s as any)[name] = initialState;
188 },
189 ...selectors,
190 };
191}
192
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>;
200export function table<
201 Entity extends AnyState = AnyState,
202 S extends AnyState = AnyState,
203>(p?: {
204 initialState?: Record<IdProp, Entity>;
205 empty?: Entity | (() => Entity);
206}): (n: string) => TableOutput<Entity, S, Entity | undefined>;
207export function table<
208 Entity extends AnyState = AnyState,
209 S extends AnyState = AnyState,
210>(
211 { initialState, empty }: {
212 initialState?: Record<IdProp, Entity>;
213 empty?: Entity | (() => Entity);
214 } = {},
215): (n: string) => TableOutput<Entity, S, Entity | undefined> {
216 return (name: string) => createTable<Entity>({ name, empty, initialState });
217}