Passed
Push — feature/data-request-hooks ( 13824f )
by Tristan
06:12
created

indexCrudReducer.ts ➔ reducer   C

Complexity

Conditions 6

Size

Total Lines 100
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 81
dl 0
loc 100
c 0
b 0
f 0
rs 6.6884
cc 6

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
import { FetchError } from "../../helpers/httpRequests";
2
import {
3
  deleteProperty,
4
  filterObjectProps,
5
  getId,
6
  hasKey,
7
  mapToObjectTrans,
8
} from "../../helpers/queries";
9
import { ResourceStatus } from "./types";
10
11
export enum ActionTypes {
12
  indexStart = "indexStart",
13
  indexFulfill = "indexFulfill",
14
  indexReject = "indexReject",
15
16
  createStart = "createStart",
17
  createFulfill = "createFulfill",
18
  createReject = "createReject",
19
20
  updateStart = "updateStart",
21
  updateFulfill = "updateFulfill",
22
  updateReject = "updateReject",
23
24
  deleteStart = "deleteStart",
25
  deleteFulfill = "deleteFulfill",
26
  deleteReject = "deleteReject",
27
}
28
29
export type IndexStartAction = { type: ActionTypes.indexStart };
30
export type IndexFulfillAction<T> = {
31
  type: ActionTypes.indexFulfill;
32
  payload: T[];
33
};
34
export type IndexRejectAction = {
35
  type: ActionTypes.indexReject;
36
  payload: Error | FetchError;
37
};
38
39
export type CreateStartAction<T> = {
40
  type: ActionTypes.createStart;
41
  meta: { item: T };
42
};
43
export type CreateFulfillAction<T> = {
44
  type: ActionTypes.createFulfill;
45
  payload: T;
46
  meta: { item: T };
47
};
48
export type CreateRejectAction<T> = {
49
  type: ActionTypes.createReject;
50
  payload: Error | FetchError;
51
  meta: { item: T };
52
};
53
54
export type UpdateStartAction<T> = {
55
  type: ActionTypes.updateStart;
56
  meta: { id: number; item: T };
57
};
58
export type UpdateFulfillAction<T> = {
59
  type: ActionTypes.updateFulfill;
60
  payload: T;
61
  meta: { id: number; item: T };
62
};
63
export type UpdateRejectAction<T> = {
64
  type: ActionTypes.updateReject;
65
  payload: Error | FetchError;
66
  meta: { id: number; item: T };
67
};
68
69
export type DeleteStartAction = {
70
  type: ActionTypes.deleteStart;
71
  meta: { id: number };
72
};
73
export type DeleteFulfillAction = {
74
  type: ActionTypes.deleteFulfill;
75
  meta: { id: number };
76
};
77
export type DeleteRejectAction = {
78
  type: ActionTypes.deleteReject;
79
  payload: Error | FetchError;
80
  meta: { id: number };
81
};
82
export type AsyncAction<T> =
83
  | IndexStartAction
84
  | IndexFulfillAction<T>
85
  | IndexRejectAction
86
  | CreateStartAction<T>
87
  | CreateFulfillAction<T>
88
  | CreateRejectAction<T>
89
  | UpdateStartAction<T>
90
  | UpdateFulfillAction<T>
91
  | UpdateRejectAction<T>
92
  | DeleteStartAction
93
  | DeleteFulfillAction
94
  | DeleteRejectAction;
95
96
export interface ResourceState<T> {
97
  indexMeta: {
98
    status: ResourceStatus;
99
    pendingCount: number;
100
    error: Error | FetchError | undefined;
101
  };
102
  createMeta: {
103
    status: ResourceStatus;
104
    pendingCount: number;
105
    error: Error | FetchError | undefined; // Only stores the most recent error;
106
  };
107
  values: {
108
    [id: string]: {
109
      value: T;
110
      error: Error | FetchError | undefined;
111
      status: ResourceStatus;
112
      pendingCount: number;
113
    };
114
  };
115
}
116
117
export function initializeState<T extends { id: number }>(
118
  items: T[],
119
): ResourceState<T> {
120
  return {
121
    indexMeta: {
122
      status: "initial",
123
      pendingCount: 0,
124
      error: undefined,
125
    },
126
    createMeta: {
127
      status: "initial",
128
      pendingCount: 0,
129
      error: undefined,
130
    },
131
    values: mapToObjectTrans(items, getId, (item) => ({
132
      value: item,
133
      error: undefined,
134
      status: "initial",
135
      pendingCount: 0,
136
    })),
137
  };
138
}
139
140
type StateValues<T> = ResourceState<T>["values"];
141
142
/**
143
 * Decrement the number if it above zero, else return 0.
144
 * @param num
145
 */
146
function decrement(num: number): number {
147
  return num <= 0 ? 0 : num - 1;
148
}
149
150
function mergeItem<T extends { id: number }>(
151
  values: StateValues<T>,
152
  item: T,
153
): StateValues<T> {
154
  if (hasKey(values, item.id)) {
155
    // We leave the status and count as is, in case an update or delete is in progress for this item.
156
    return {
157
      ...values,
158
      [item.id]: {
159
        ...values[item.id],
160
        value: item,
161
        error: undefined,
162
      },
163
    };
164
  }
165
  return {
166
    ...values,
167
    [item.id]: {
168
      value: item,
169
      error: undefined,
170
      status: "fulfilled",
171
      pendingCount: 0,
172
    },
173
  };
174
}
175
function mergeIndexPayload<T extends { id: number }>(
176
  values: StateValues<T>,
177
  payload: T[],
178
): StateValues<T> {
179
  // Update or create a values entry for each item in the payload.
180
  const newValues = payload.reduce(mergeItem, values);
181
  // Delete any values entries that don't exist in the new payload.
182
  const payloadIds = payload.map(getId);
183
  return filterObjectProps(newValues, (item) =>
184
    payloadIds.includes(item.value.id),
185
  );
186
}
187
188
function mergeCreatePayload<T extends { id: number }>(
189
  values: StateValues<T>,
190
  payload: T,
191
): StateValues<T> {
192
  if (hasKey(values, payload.id)) {
193
    // Something has gone wrong if an existing item has the same id as the newly created one.
194
    // TODO: But should we throw the error, or just update the value and pretend everything's ok?
195
    throw new Error(
196
      "Cannot create new item as an existing item shares the same id. Try refreshing the whole index.",
197
    );
198
  }
199
  return {
200
    ...values,
201
    [payload.id]: {
202
      value: payload,
203
      error: undefined,
204
      status: "fulfilled",
205
      pendingCount: 0,
206
    },
207
  };
208
}
209
210
function mergeUpdateStart<T extends { id: number }>(
211
  values: StateValues<T>,
212
  action: UpdateStartAction<T>,
213
): StateValues<T> {
214
  if (!hasKey(values, action.meta.id)) {
215
    throw new Error(
216
      "Cannot update an item that doesn't exist yet. Maybe you tried to update an item after deleting it?",
217
    );
218
  }
219
  return {
220
    ...values,
221
    [action.meta.id]: {
222
      // TODO: if we wanted to do an optimistic update, we could save action.payload.item here.
223
      // But we would need some way to reverse it if it failed.
224
      ...values[action.meta.id],
225
      status: "pending",
226
      pendingCount: values[action.meta.id].pendingCount + 1,
227
      error: undefined,
228
    },
229
  };
230
}
231
function mergeUpdateFulfill<T extends { id: number }>(
232
  values: StateValues<T>,
233
  action: UpdateFulfillAction<T>,
234
): StateValues<T> {
235
  if (!hasKey(values, action.meta.id)) {
236
    throw new Error(
237
      "Cannot update an item that doesn't exist yet. Maybe you tried to update an item after deleting it?",
238
    );
239
  }
240
  return {
241
    ...values,
242
    [action.meta.id]: {
243
      value: action.payload,
244
      status:
245
        values[action.meta.id].pendingCount === 1 ? "fulfilled" : "pending",
246
      pendingCount: decrement(values[action.meta.id].pendingCount),
247
      error: undefined,
248
    },
249
  };
250
}
251
function mergeUpdateReject<T extends { id: number }>(
252
  values: StateValues<T>,
253
  action: UpdateRejectAction<T>,
254
): StateValues<T> {
255
  if (!hasKey(values, action.meta.id)) {
256
    // In this case, the request has already errored, so don't throw an error.
257
    // Simply leave the state as is.
258
    return values;
259
  }
260
  return {
261
    ...values,
262
    [action.meta.id]: {
263
      ...values[action.meta.id],
264
      status: values[action.meta.id].pendingCount === 1 ? "error" : "pending",
265
      pendingCount: decrement(values[action.meta.id].pendingCount),
266
      error: action.payload,
267
    },
268
  };
269
}
270
function mergeDeleteStart<T extends { id: number }>(
271
  values: StateValues<T>,
272
  action: DeleteStartAction,
273
): StateValues<T> {
274
  if (!hasKey(values, action.meta.id)) {
275
    // If the item already doesn't exist, nothing needs to be done.
276
    return values;
277
  }
278
  return {
279
    ...values,
280
    [action.meta.id]: {
281
      ...values[action.meta.id],
282
      status: "pending",
283
      pendingCount: values[action.meta.id].pendingCount + 1,
284
      error: undefined,
285
    },
286
  };
287
}
288
function mergeDeleteFulfill<T extends { id: number }>(
289
  values: StateValues<T>,
290
  action: DeleteFulfillAction,
291
): StateValues<T> {
292
  return deleteProperty(values, action.meta.id);
293
}
294
function mergeDeleteReject<T extends { id: number }>(
295
  values: StateValues<T>,
296
  action: DeleteRejectAction,
297
): StateValues<T> {
298
  if (!hasKey(values, action.meta.id)) {
299
    // If the item already doesn't exist, nothing needs to be done, despite the error.
300
    return values;
301
  }
302
  return {
303
    ...values,
304
    [action.meta.id]: {
305
      ...values[action.meta.id],
306
      status: values[action.meta.id].pendingCount === 1 ? "error" : "pending",
307
      pendingCount: decrement(values[action.meta.id].pendingCount),
308
      error: action.payload,
309
    },
310
  };
311
}
312
313
export function reducer<T extends { id: number }>(
314
  state: ResourceState<T>,
315
  action: AsyncAction<T>,
316
): ResourceState<T> {
317
  switch (action.type) {
318
    case ActionTypes.indexStart:
319
      return {
320
        ...state,
321
        indexMeta: {
322
          ...state.indexMeta,
323
          status: "pending",
324
          pendingCount: state.indexMeta.pendingCount + 1,
325
          error: undefined,
326
        },
327
      };
328
    case ActionTypes.indexFulfill:
329
      return {
330
        ...state,
331
        indexMeta: {
332
          ...state.indexMeta,
333
          status: state.indexMeta.pendingCount === 1 ? "fulfilled" : "pending",
334
          pendingCount: decrement(state.indexMeta.pendingCount),
335
          error: undefined,
336
        },
337
        values: mergeIndexPayload(state.values, action.payload),
338
      };
339
    case ActionTypes.indexReject:
340
      return {
341
        ...state,
342
        indexMeta: {
343
          ...state.indexMeta,
344
          status: state.indexMeta.pendingCount === 1 ? "rejected" : "pending",
345
          pendingCount: decrement(state.indexMeta.pendingCount),
346
          error: action.payload,
347
        },
348
      };
349
    case ActionTypes.createStart:
350
      // TODO: We could add an optimistic update here.
351
      return {
352
        ...state,
353
        createMeta: {
354
          ...state.createMeta,
355
          status: "pending",
356
          pendingCount: state.createMeta.pendingCount + 1,
357
          error: undefined,
358
        },
359
      };
360
    case ActionTypes.createFulfill:
361
      return {
362
        ...state,
363
        createMeta: {
364
          status: state.createMeta.pendingCount === 1 ? "fulfilled" : "pending",
365
          pendingCount: decrement(state.createMeta.pendingCount),
366
          error: undefined,
367
        },
368
        values: mergeCreatePayload(state.values, action.payload),
369
      };
370
    case ActionTypes.createReject:
371
      return {
372
        ...state,
373
        createMeta: {
374
          status: state.createMeta.pendingCount === 1 ? "rejected" : "pending",
375
          pendingCount: decrement(state.createMeta.pendingCount),
376
          error: action.payload,
377
        },
378
      };
379
    case ActionTypes.updateStart:
380
      return {
381
        ...state,
382
        values: mergeUpdateStart(state.values, action),
383
      };
384
    case ActionTypes.updateFulfill:
385
      return {
386
        ...state,
387
        values: mergeUpdateFulfill(state.values, action),
388
      };
389
    case ActionTypes.updateReject:
390
      return {
391
        ...state,
392
        values: mergeUpdateReject(state.values, action),
393
      };
394
    case ActionTypes.deleteStart:
395
      return {
396
        ...state,
397
        values: mergeDeleteStart(state.values, action),
398
      };
399
    case ActionTypes.deleteFulfill:
400
      return {
401
        ...state,
402
        values: mergeDeleteFulfill(state.values, action),
403
      };
404
    case ActionTypes.deleteReject:
405
      return {
406
        ...state,
407
        values: mergeDeleteReject(state.values, action),
408
      };
409
410
    default:
411
      return state;
412
  }
413
}
414
415
export default reducer;
416