1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | export type ActionType = string;
|
13 | export type NormalFSA<Payload: mixed | Error = typeof undefined, Meta: mixed = typeof undefined> = {|
|
14 | type: ActionType,
|
15 | payload: Payload,
|
16 | error: boolean,
|
17 | meta: Meta
|
18 | |};
|
19 | export type FluxStandardAction<Payload, Meta> = NormalFSA<Payload, Meta>;
|
20 | export type FSA<Payload, Meta> = NormalFSA<Payload, Meta>;
|
21 |
|
22 |
|
23 | export type PrimitiveType = null | string | boolean | number;
|
24 | export type JSONObject = ({ [key: string]: PrimitiveType | JSONObject | JSONArray | void });
|
25 | export type JSONArray = Array<PrimitiveType> | Array<JSONObject> | Array<JSONArray>;
|
26 | export type JSONType = JSONObject | JSONArray | PrimitiveType;
|
27 |
|
28 |
|
29 | export type ApiEndpointState = {
|
30 | +success: JSONType | null,
|
31 | +failure: JSONType | null,
|
32 | +timeout: JSONType | null,
|
33 | +mistake: JSONType | null,
|
34 | +isFetching: boolean,
|
35 | +lastUpdate: number | null,
|
36 | +lastResult: 'success' | 'failure' | 'timeout' | 'mistake' | null,
|
37 | };
|
38 | export type KeyedCollectionState =
|
39 | & ApiEndpointState
|
40 | & { +collection: { [string]: JSONObject } };
|
41 |
|
42 | export type ReducerState<Name: string> =
|
43 | & { +[string]: ApiEndpointState }
|
44 | & { +[Name]: ApiEndpointState };
|
45 | export type KeyedReducerState<Name: string> =
|
46 | & { +[string]: KeyedCollectionState }
|
47 | & { +[Name]: KeyedCollectionState };
|
48 |
|
49 | export type ParamKey = number | string;
|
50 | export type ParamValue = number | string | boolean;
|
51 | export type ParamsPair = [ParamKey, ParamValue];
|
52 | export type ParamsArray = Array<ParamsPair>;
|
53 | export type ParamsObject = { [ParamKey]: * };
|
54 |
|
55 |
|
56 |
|
57 | const queryPrefixRegexp = new RegExp(/^\?=/);
|
58 |
|
59 | const validateParamsPair = ([key, value]) => {
|
60 | if (typeof key !== 'string') {
|
61 | throw new Error(`Expected ParamsObject to have string keys, not "${typeof key}""`);
|
62 | }
|
63 | if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
64 | throw new Error(`Expected ParamsObject values to be strings, numbers, or booleans, not "${typeof value}"`);
|
65 | }
|
66 | };
|
67 |
|
68 | export const validateParamsObject = (params: ParamsObject) => {
|
69 | if (params && typeof params === 'object') {
|
70 | Object.entries(params).forEach(validateParamsPair);
|
71 | } else {
|
72 | throw new Error('Could not validate ParamsObject: Not an object');
|
73 | }
|
74 | };
|
75 |
|
76 | export const validateParamsArray = (params: ParamsArray) => {
|
77 | if (params && Array.isArray(params)) {
|
78 | params.forEach(validateParamsPair);
|
79 | } else {
|
80 | throw new Error('Could not validate ParamsArray: Not an array');
|
81 | }
|
82 | };
|
83 |
|
84 |
|
85 | const createParamsPairString = (paramsPair: ParamsPair): string => {
|
86 | if (Array.isArray(paramsPair) && paramsPair.length === 2) {
|
87 | if (typeof paramsPair[0] !== 'string') {
|
88 | throw new TypeError('paramsPair[0] must be a string');
|
89 | }
|
90 |
|
91 | if (!['number', 'string', 'boolean'].includes(typeof paramsPair[1])) {
|
92 | throw new TypeError('paramsPair[1] must be a number, string, or boolean');
|
93 | }
|
94 |
|
95 | return `${encodeURIComponent(String(paramsPair[0]))}=${encodeURIComponent(String(paramsPair[1]))}`;
|
96 | }
|
97 |
|
98 | throw new TypeError('paramsPair must be an array with two entries');
|
99 | };
|
100 |
|
101 |
|
102 |
|
103 | const restoreOriginalType = (text: string): * => {
|
104 | if (typeof text === 'string') {
|
105 | const decodedText = decodeURIComponent(text);
|
106 |
|
107 | if (!isNaN(decodedText)) {
|
108 |
|
109 |
|
110 |
|
111 | return parseFloat(decodedText);
|
112 | }
|
113 |
|
114 | if (decodedText === 'true' || decodedText === 'false') {
|
115 |
|
116 | return decodedText === 'true';
|
117 | }
|
118 |
|
119 |
|
120 | return decodedText;
|
121 | }
|
122 |
|
123 | throw new Error('Could not restore type: not a number, string, or boolean');
|
124 | };
|
125 |
|
126 |
|
127 | export const arrayToParamString = (paramsArray: ParamsArray = []): string => {
|
128 | if (Array.isArray(paramsArray)) {
|
129 | return paramsArray.map(createParamsPairString).join('&');
|
130 | }
|
131 |
|
132 | throw new TypeError('paramsArray must be an Array');
|
133 | };
|
134 |
|
135 |
|
136 | export const paramStringToArray = (paramsString: string = ''): ParamsArray => {
|
137 | if (typeof paramsString === 'string') {
|
138 | const cleanedPairs: Array<string> = paramsString.replace(queryPrefixRegexp, '').split('&');
|
139 |
|
140 | return cleanedPairs.map((param: string) => {
|
141 | const [key, value] = param.split('=');
|
142 |
|
143 | return [decodeURIComponent(key), restoreOriginalType(value)];
|
144 | });
|
145 | }
|
146 |
|
147 | throw new TypeError('paramsString must be a string');
|
148 | };
|
149 |
|
150 |
|
151 | export const objectToParamString = (paramsObject: ParamsObject = {}): string => {
|
152 | if (paramsObject && typeof paramsObject === 'object') {
|
153 | const entries = Object.keys(paramsObject)
|
154 | .map(key => [key, paramsObject[key]]);
|
155 |
|
156 | return arrayToParamString(entries);
|
157 | }
|
158 |
|
159 | throw new TypeError('paramsObject must be an Object');
|
160 | };
|
161 |
|
162 |
|
163 | export const paramStringToObject = (paramsString: string = '') => {
|
164 | const paramsArray: ParamsArray = paramStringToArray(paramsString);
|
165 |
|
166 | if (Array.isArray(paramsArray)) {
|
167 | const data = {};
|
168 |
|
169 | paramsArray.forEach(([key, value]: ParamsPair) => {
|
170 | data[key] = value;
|
171 | });
|
172 |
|
173 | return data;
|
174 | }
|
175 |
|
176 | throw new Error('Could not convert param string to object beacuse the intermediate Array was not created');
|
177 | };
|
178 |
|
179 | export const convertToParamString = (content: ParamsArray | ParamsObject = {}): string => {
|
180 | if (Array.isArray(content)) {
|
181 | return arrayToParamString(content);
|
182 | } else if (content !== null && typeof content === 'object') {
|
183 | return objectToParamString(content);
|
184 | }
|
185 |
|
186 | throw new Error('convertToParamString requires an array or an object as its first parameter');
|
187 | };
|
188 |
|
189 | export const convertQueryToString = (content: ParamsArray | ParamsObject = {}): string => {
|
190 | const convertedString = convertToParamString(content);
|
191 |
|
192 | if (typeof convertedString === 'string') {
|
193 | if (convertedString.length) {
|
194 | return `?${convertedString}`;
|
195 | }
|
196 |
|
197 |
|
198 |
|
199 | return '';
|
200 | }
|
201 |
|
202 | throw new Error('Query conversion failed');
|
203 | };
|
204 |
|
205 |
|
206 | export const getInit = () => {};
|
207 | export const getApiPath = () => '';
|
208 |
|
209 |
|
210 | export const initialEndpointState: ApiEndpointState = {
|
211 | success: null,
|
212 | failure: null,
|
213 | timeout: null,
|
214 | mistake: null,
|
215 | isFetching: false,
|
216 | lastUpdate: null,
|
217 | lastResult: null,
|
218 | };
|
219 | export const initialKeyedCollectionState: ApiEndpointState & KeyedCollectionState = {
|
220 | ...initialEndpointState,
|
221 | collection: {},
|
222 | };
|
223 |
|
224 | const rekeyObject = (key: string, obj: JSONObject) => {
|
225 | if (obj && typeof obj[key] === 'string') {
|
226 | return { [obj[key]]: obj };
|
227 | }
|
228 |
|
229 | console.trace(`Could not key response obect by key '${key}'`);
|
230 | console.error(JSON.stringify(obj, null, 2));
|
231 |
|
232 | return {};
|
233 | };
|
234 |
|
235 | export const handleKeyedCollection = (key: string, collection: { [string]: JSONObject } = {}, response: JSONObject | Array<JSONObject>) => {
|
236 | if (Array.isArray(response)) {
|
237 | return response.map(obj => rekeyObject(key, obj)).reduce((prev, next) => ({ ...prev, ...next }), collection);
|
238 | }
|
239 |
|
240 | return { ...collection, ...rekeyObject(key, response) };
|
241 | };
|
242 |
|
243 | export const handleRequest = <Name: string, State: ReducerState<Name>>(
|
244 | name: Name,
|
245 | state: State,
|
246 | ): State => (
|
247 | {
|
248 | ...state,
|
249 | [name]: {
|
250 | ...initialEndpointState,
|
251 | ...state[name],
|
252 | isFetching: true,
|
253 | },
|
254 | }
|
255 | );
|
256 |
|
257 | export const handleSuccess = <Name: string, State: ReducerState<Name>>(
|
258 | name: Name,
|
259 | state: State,
|
260 | action: FSA<*, *>,
|
261 | ): State => (
|
262 | {
|
263 | ...state,
|
264 | [name]: {
|
265 | ...initialEndpointState,
|
266 | ...state[name],
|
267 | success: action.payload || null,
|
268 | isFetching: false,
|
269 | lastUpdate: Date.now(),
|
270 | lastResult: 'success',
|
271 | },
|
272 | }
|
273 | );
|
274 |
|
275 | export const handleSuccessKeyed = <Name: string, State: KeyedReducerState<Name>>(
|
276 | name: Name,
|
277 | state: State,
|
278 | action: FSA<*, *>,
|
279 | key: string,
|
280 | ): State => (
|
281 | {
|
282 | ...state,
|
283 | [name]: {
|
284 | ...initialKeyedCollectionState,
|
285 | ...state[name],
|
286 | success: action.payload || null,
|
287 | isFetching: false,
|
288 | lastUpdate: Date.now(),
|
289 | lastResult: 'success',
|
290 | collection: handleKeyedCollection(key, state[name].collection, action.payload),
|
291 | },
|
292 | }
|
293 | );
|
294 |
|
295 | export const handleFailure = <Name: string, State: ReducerState<Name>>(
|
296 | name: Name,
|
297 | state: State,
|
298 | action: FSA<*, *>,
|
299 | ): State => (
|
300 | {
|
301 | ...state,
|
302 | [name]: {
|
303 | ...initialEndpointState,
|
304 | ...state[name],
|
305 | failure: action.payload || null,
|
306 | isFetching: false,
|
307 | lastUpdate: Date.now(),
|
308 | lastResult: 'failure',
|
309 | },
|
310 | }
|
311 | );
|
312 |
|
313 | export const handleTimeout = <Name: string, State: ReducerState<Name>>(
|
314 | name: Name,
|
315 | state: State,
|
316 | action: FSA<*, *>,
|
317 | ): State => (
|
318 | {
|
319 | ...state,
|
320 | [name]: {
|
321 | ...initialEndpointState,
|
322 | ...state[name],
|
323 | timeout: action.payload || null,
|
324 | isFetching: false,
|
325 | lastUpdate: Date.now(),
|
326 | lastResult: 'timeout',
|
327 | },
|
328 | }
|
329 | );
|
330 |
|
331 | export const handleMistake = <Name: string, State: ReducerState<Name>>(
|
332 | name: Name,
|
333 | state: State,
|
334 | action: FSA<*, *>,
|
335 | ): State => (
|
336 | {
|
337 | ...state,
|
338 | [name]: {
|
339 | ...initialEndpointState,
|
340 | ...state[name],
|
341 | mistake: action.payload || null,
|
342 | isFetching: false,
|
343 | lastUpdate: Date.now(),
|
344 | lastResult: 'mistake',
|
345 | },
|
346 | }
|
347 | );
|