klaim 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Makefile +39 -14
- package/README.md +262 -262
- package/deno.json +38 -38
- package/package.json +10 -10
- package/src/core/Cache.ts +72 -72
- package/src/core/Element.ts +150 -150
- package/src/core/Klaim.ts +222 -222
- package/src/core/Route.ts +165 -165
- package/src/tools/fetchWithCache.ts +30 -30
- package/tests/01.api.test.ts +60 -60
- package/tests/02.route.test.ts +186 -186
- package/tests/03.hook.test.ts +34 -34
- package/tests/04.klaim.test.ts +96 -96
- package/tests/05.cache.test.ts +46 -46
- package/tests/06.retry.test.ts +54 -54
- package/tests/07.validate.test.ts +61 -61
package/src/core/Klaim.ts
CHANGED
|
@@ -1,222 +1,222 @@
|
|
|
1
|
-
import fetchWithCache from "../tools/fetchWithCache";
|
|
2
|
-
|
|
3
|
-
import { Api } from "./Api";
|
|
4
|
-
import { ICallbackAfterArgs, ICallbackBeforeArgs } from "./Element";
|
|
5
|
-
import { Hook } from "./Hook";
|
|
6
|
-
import { Registry } from "./Registry";
|
|
7
|
-
import { Route, RouteMethod } from "./Route";
|
|
8
|
-
|
|
9
|
-
export type IArgs = Record<string, unknown>;
|
|
10
|
-
|
|
11
|
-
export type IBody = Record<string, unknown>;
|
|
12
|
-
|
|
13
|
-
export type IRouteReference = Record<
|
|
14
|
-
string,
|
|
15
|
-
<T>(args?: IArgs, body?: IBody) => Promise<T>
|
|
16
|
-
>;
|
|
17
|
-
|
|
18
|
-
export type IApiReference = Record<string, IRouteReference>;
|
|
19
|
-
|
|
20
|
-
export const Klaim: IApiReference = {};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Calls an API route
|
|
24
|
-
*
|
|
25
|
-
* @param api - The API to call
|
|
26
|
-
* @param route - The route to call
|
|
27
|
-
* @param args - The arguments to pass to the route
|
|
28
|
-
* @param body - The body to pass to the route
|
|
29
|
-
* @returns The response
|
|
30
|
-
*/
|
|
31
|
-
export async function callApi<T> (
|
|
32
|
-
api: Api,
|
|
33
|
-
route: Route,
|
|
34
|
-
args: IArgs = {},
|
|
35
|
-
body: IBody = {}
|
|
36
|
-
): Promise<T> {
|
|
37
|
-
let url = applyArgs(`${api.url}/${route.url}`, route, args);
|
|
38
|
-
|
|
39
|
-
let config: Record<string, unknown> = {};
|
|
40
|
-
|
|
41
|
-
if (body && route.method !== RouteMethod.GET) {
|
|
42
|
-
config.body = JSON.stringify(body);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
config.headers = {
|
|
46
|
-
"Content-Type": "application/json",
|
|
47
|
-
...api.headers,
|
|
48
|
-
...route.headers
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
config.method = route.method;
|
|
52
|
-
|
|
53
|
-
const {
|
|
54
|
-
beforeRoute,
|
|
55
|
-
beforeApi,
|
|
56
|
-
beforeUrl,
|
|
57
|
-
beforeConfig
|
|
58
|
-
} = applyBefore({ route, api, url, config });
|
|
59
|
-
url = beforeUrl;
|
|
60
|
-
config = beforeConfig;
|
|
61
|
-
api = Registry.updateApi(beforeApi);
|
|
62
|
-
route = Registry.updateRoute(beforeRoute);
|
|
63
|
-
|
|
64
|
-
let response = await fetchWithRetry(api, route, url, config);
|
|
65
|
-
|
|
66
|
-
if (route.schema && "validate" in route.schema) {
|
|
67
|
-
response = await route.schema.validate(response);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const {
|
|
71
|
-
afterRoute,
|
|
72
|
-
afterApi,
|
|
73
|
-
afterData
|
|
74
|
-
} = applyAfter({ route, api, response, data: response });
|
|
75
|
-
Registry.updateApi(afterApi);
|
|
76
|
-
Registry.updateRoute(afterRoute);
|
|
77
|
-
|
|
78
|
-
Hook.run(`${api.name}.${route.name}`);
|
|
79
|
-
|
|
80
|
-
return afterData as T;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Fetches data from the API
|
|
85
|
-
*
|
|
86
|
-
* @param withCache - Whether to use the cache
|
|
87
|
-
* @param url - The URL to fetch
|
|
88
|
-
* @param config - The fetch config
|
|
89
|
-
* @param api - The API
|
|
90
|
-
* @returns The response
|
|
91
|
-
*/
|
|
92
|
-
async function fetchData (
|
|
93
|
-
withCache: boolean,
|
|
94
|
-
url: string,
|
|
95
|
-
config: any,
|
|
96
|
-
api: any
|
|
97
|
-
): Promise<any> {
|
|
98
|
-
if (withCache) {
|
|
99
|
-
return await fetchWithCache(url, config, api.cache);
|
|
100
|
-
} else {
|
|
101
|
-
const rawResponse = await fetch(url, config);
|
|
102
|
-
return await rawResponse.json();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Fetches data with retries
|
|
108
|
-
*
|
|
109
|
-
* @param api - The API
|
|
110
|
-
* @param route - The route
|
|
111
|
-
* @param url - The URL to fetch
|
|
112
|
-
* @param config - The fetch config
|
|
113
|
-
* @returns The response
|
|
114
|
-
*/
|
|
115
|
-
async function fetchWithRetry (
|
|
116
|
-
api: Api,
|
|
117
|
-
route: Route,
|
|
118
|
-
url: string,
|
|
119
|
-
config: any
|
|
120
|
-
): Promise<any> {
|
|
121
|
-
const withCache = api.cache || route.cache;
|
|
122
|
-
const maxRetries = (route.retry || api.retry) || 0;
|
|
123
|
-
|
|
124
|
-
let response;
|
|
125
|
-
let attempt = 0;
|
|
126
|
-
let success = false;
|
|
127
|
-
const callCallback = route.callbacks?.call !== null
|
|
128
|
-
? route.callbacks?.call
|
|
129
|
-
: api.callbacks?.call;
|
|
130
|
-
|
|
131
|
-
while (attempt <= maxRetries && !success) {
|
|
132
|
-
if (callCallback) {
|
|
133
|
-
callCallback({});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
response = await fetchData(!!withCache, url, config, api);
|
|
138
|
-
success = true;
|
|
139
|
-
} catch (error: any) {
|
|
140
|
-
attempt++;
|
|
141
|
-
if (attempt > maxRetries) {
|
|
142
|
-
error.message
|
|
143
|
-
= `Failed to fetch ${url} after ${maxRetries} attempts`;
|
|
144
|
-
throw error;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return response;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Applies the arguments to the URL
|
|
154
|
-
*
|
|
155
|
-
* @param url - The URL to apply the arguments to
|
|
156
|
-
* @param route - The route to apply the arguments to
|
|
157
|
-
* @param args - The arguments to apply
|
|
158
|
-
* @returns The new URL
|
|
159
|
-
*/
|
|
160
|
-
function applyArgs (url: string, route: Route, args: IArgs): string {
|
|
161
|
-
let newUrl = url;
|
|
162
|
-
route.arguments.forEach(arg => {
|
|
163
|
-
const value = args[arg];
|
|
164
|
-
if (value === undefined) {
|
|
165
|
-
throw new Error(`Argument ${arg} is missing`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
newUrl = newUrl.replace(`[${arg}]`, <string> args[arg]);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
return newUrl;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Applies the before callback
|
|
176
|
-
*
|
|
177
|
-
* @param callbackArgs - The arguments to pass to the callback
|
|
178
|
-
* @param callbackArgs.route - The route
|
|
179
|
-
* @param callbackArgs.api - The API
|
|
180
|
-
* @param callbackArgs.url - The URL
|
|
181
|
-
* @param callbackArgs.config - The config
|
|
182
|
-
* @returns The new args
|
|
183
|
-
*/
|
|
184
|
-
function applyBefore ({ route, api, url, config }: ICallbackBeforeArgs): {
|
|
185
|
-
beforeRoute: Route;
|
|
186
|
-
beforeApi: Api;
|
|
187
|
-
beforeUrl: string;
|
|
188
|
-
beforeConfig: Record<string, unknown>;
|
|
189
|
-
} {
|
|
190
|
-
const beforeRes = route.callbacks.before?.({ route, api, url, config });
|
|
191
|
-
return {
|
|
192
|
-
beforeRoute: beforeRes?.route || route,
|
|
193
|
-
beforeApi: beforeRes?.api || api,
|
|
194
|
-
beforeUrl: beforeRes?.url || url,
|
|
195
|
-
beforeConfig: beforeRes?.config || config
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Applies the after callback
|
|
201
|
-
*
|
|
202
|
-
* @param callbackArgs - The arguments to pass to the callback
|
|
203
|
-
* @param callbackArgs.route - The route
|
|
204
|
-
* @param callbackArgs.api - The API
|
|
205
|
-
* @param callbackArgs.response - The response
|
|
206
|
-
* @param callbackArgs.data - The data
|
|
207
|
-
* @returns The new data
|
|
208
|
-
*/
|
|
209
|
-
function applyAfter ({ route, api, response, data }: ICallbackAfterArgs): {
|
|
210
|
-
afterRoute: Route;
|
|
211
|
-
afterApi: Api;
|
|
212
|
-
afterResponse: Response;
|
|
213
|
-
afterData: any;
|
|
214
|
-
} {
|
|
215
|
-
const afterRes = route.callbacks.after?.({ route, api, response, data });
|
|
216
|
-
return {
|
|
217
|
-
afterRoute: afterRes?.route || route,
|
|
218
|
-
afterApi: afterRes?.api || api,
|
|
219
|
-
afterResponse: afterRes?.response || response,
|
|
220
|
-
afterData: afterRes?.data || data
|
|
221
|
-
};
|
|
222
|
-
}
|
|
1
|
+
import fetchWithCache from "../tools/fetchWithCache";
|
|
2
|
+
|
|
3
|
+
import { Api } from "./Api";
|
|
4
|
+
import { ICallbackAfterArgs, ICallbackBeforeArgs } from "./Element";
|
|
5
|
+
import { Hook } from "./Hook";
|
|
6
|
+
import { Registry } from "./Registry";
|
|
7
|
+
import { Route, RouteMethod } from "./Route";
|
|
8
|
+
|
|
9
|
+
export type IArgs = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export type IBody = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
export type IRouteReference = Record<
|
|
14
|
+
string,
|
|
15
|
+
<T>(args?: IArgs, body?: IBody) => Promise<T>
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
export type IApiReference = Record<string, IRouteReference>;
|
|
19
|
+
|
|
20
|
+
export const Klaim: IApiReference = {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calls an API route
|
|
24
|
+
*
|
|
25
|
+
* @param api - The API to call
|
|
26
|
+
* @param route - The route to call
|
|
27
|
+
* @param args - The arguments to pass to the route
|
|
28
|
+
* @param body - The body to pass to the route
|
|
29
|
+
* @returns The response
|
|
30
|
+
*/
|
|
31
|
+
export async function callApi<T> (
|
|
32
|
+
api: Api,
|
|
33
|
+
route: Route,
|
|
34
|
+
args: IArgs = {},
|
|
35
|
+
body: IBody = {}
|
|
36
|
+
): Promise<T> {
|
|
37
|
+
let url = applyArgs(`${api.url}/${route.url}`, route, args);
|
|
38
|
+
|
|
39
|
+
let config: Record<string, unknown> = {};
|
|
40
|
+
|
|
41
|
+
if (body && route.method !== RouteMethod.GET) {
|
|
42
|
+
config.body = JSON.stringify(body);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
config.headers = {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
...api.headers,
|
|
48
|
+
...route.headers
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
config.method = route.method;
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
beforeRoute,
|
|
55
|
+
beforeApi,
|
|
56
|
+
beforeUrl,
|
|
57
|
+
beforeConfig
|
|
58
|
+
} = applyBefore({ route, api, url, config });
|
|
59
|
+
url = beforeUrl;
|
|
60
|
+
config = beforeConfig;
|
|
61
|
+
api = Registry.updateApi(beforeApi);
|
|
62
|
+
route = Registry.updateRoute(beforeRoute);
|
|
63
|
+
|
|
64
|
+
let response = await fetchWithRetry(api, route, url, config);
|
|
65
|
+
|
|
66
|
+
if (route.schema && "validate" in route.schema) {
|
|
67
|
+
response = await route.schema.validate(response);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
afterRoute,
|
|
72
|
+
afterApi,
|
|
73
|
+
afterData
|
|
74
|
+
} = applyAfter({ route, api, response, data: response });
|
|
75
|
+
Registry.updateApi(afterApi);
|
|
76
|
+
Registry.updateRoute(afterRoute);
|
|
77
|
+
|
|
78
|
+
Hook.run(`${api.name}.${route.name}`);
|
|
79
|
+
|
|
80
|
+
return afterData as T;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fetches data from the API
|
|
85
|
+
*
|
|
86
|
+
* @param withCache - Whether to use the cache
|
|
87
|
+
* @param url - The URL to fetch
|
|
88
|
+
* @param config - The fetch config
|
|
89
|
+
* @param api - The API
|
|
90
|
+
* @returns The response
|
|
91
|
+
*/
|
|
92
|
+
async function fetchData (
|
|
93
|
+
withCache: boolean,
|
|
94
|
+
url: string,
|
|
95
|
+
config: any,
|
|
96
|
+
api: any
|
|
97
|
+
): Promise<any> {
|
|
98
|
+
if (withCache) {
|
|
99
|
+
return await fetchWithCache(url, config, api.cache);
|
|
100
|
+
} else {
|
|
101
|
+
const rawResponse = await fetch(url, config);
|
|
102
|
+
return await rawResponse.json();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fetches data with retries
|
|
108
|
+
*
|
|
109
|
+
* @param api - The API
|
|
110
|
+
* @param route - The route
|
|
111
|
+
* @param url - The URL to fetch
|
|
112
|
+
* @param config - The fetch config
|
|
113
|
+
* @returns The response
|
|
114
|
+
*/
|
|
115
|
+
async function fetchWithRetry (
|
|
116
|
+
api: Api,
|
|
117
|
+
route: Route,
|
|
118
|
+
url: string,
|
|
119
|
+
config: any
|
|
120
|
+
): Promise<any> {
|
|
121
|
+
const withCache = api.cache || route.cache;
|
|
122
|
+
const maxRetries = (route.retry || api.retry) || 0;
|
|
123
|
+
|
|
124
|
+
let response;
|
|
125
|
+
let attempt = 0;
|
|
126
|
+
let success = false;
|
|
127
|
+
const callCallback = route.callbacks?.call !== null
|
|
128
|
+
? route.callbacks?.call
|
|
129
|
+
: api.callbacks?.call;
|
|
130
|
+
|
|
131
|
+
while (attempt <= maxRetries && !success) {
|
|
132
|
+
if (callCallback) {
|
|
133
|
+
callCallback({});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
response = await fetchData(!!withCache, url, config, api);
|
|
138
|
+
success = true;
|
|
139
|
+
} catch (error: any) {
|
|
140
|
+
attempt++;
|
|
141
|
+
if (attempt > maxRetries) {
|
|
142
|
+
error.message
|
|
143
|
+
= `Failed to fetch ${url} after ${maxRetries} attempts`;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return response;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Applies the arguments to the URL
|
|
154
|
+
*
|
|
155
|
+
* @param url - The URL to apply the arguments to
|
|
156
|
+
* @param route - The route to apply the arguments to
|
|
157
|
+
* @param args - The arguments to apply
|
|
158
|
+
* @returns The new URL
|
|
159
|
+
*/
|
|
160
|
+
function applyArgs (url: string, route: Route, args: IArgs): string {
|
|
161
|
+
let newUrl = url;
|
|
162
|
+
route.arguments.forEach(arg => {
|
|
163
|
+
const value = args[arg];
|
|
164
|
+
if (value === undefined) {
|
|
165
|
+
throw new Error(`Argument ${arg} is missing`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
newUrl = newUrl.replace(`[${arg}]`, <string> args[arg]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return newUrl;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Applies the before callback
|
|
176
|
+
*
|
|
177
|
+
* @param callbackArgs - The arguments to pass to the callback
|
|
178
|
+
* @param callbackArgs.route - The route
|
|
179
|
+
* @param callbackArgs.api - The API
|
|
180
|
+
* @param callbackArgs.url - The URL
|
|
181
|
+
* @param callbackArgs.config - The config
|
|
182
|
+
* @returns The new args
|
|
183
|
+
*/
|
|
184
|
+
function applyBefore ({ route, api, url, config }: ICallbackBeforeArgs): {
|
|
185
|
+
beforeRoute: Route;
|
|
186
|
+
beforeApi: Api;
|
|
187
|
+
beforeUrl: string;
|
|
188
|
+
beforeConfig: Record<string, unknown>;
|
|
189
|
+
} {
|
|
190
|
+
const beforeRes = route.callbacks.before?.({ route, api, url, config });
|
|
191
|
+
return {
|
|
192
|
+
beforeRoute: beforeRes?.route || route,
|
|
193
|
+
beforeApi: beforeRes?.api || api,
|
|
194
|
+
beforeUrl: beforeRes?.url || url,
|
|
195
|
+
beforeConfig: beforeRes?.config || config
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Applies the after callback
|
|
201
|
+
*
|
|
202
|
+
* @param callbackArgs - The arguments to pass to the callback
|
|
203
|
+
* @param callbackArgs.route - The route
|
|
204
|
+
* @param callbackArgs.api - The API
|
|
205
|
+
* @param callbackArgs.response - The response
|
|
206
|
+
* @param callbackArgs.data - The data
|
|
207
|
+
* @returns The new data
|
|
208
|
+
*/
|
|
209
|
+
function applyAfter ({ route, api, response, data }: ICallbackAfterArgs): {
|
|
210
|
+
afterRoute: Route;
|
|
211
|
+
afterApi: Api;
|
|
212
|
+
afterResponse: Response;
|
|
213
|
+
afterData: any;
|
|
214
|
+
} {
|
|
215
|
+
const afterRes = route.callbacks.after?.({ route, api, response, data });
|
|
216
|
+
return {
|
|
217
|
+
afterRoute: afterRes?.route || route,
|
|
218
|
+
afterApi: afterRes?.api || api,
|
|
219
|
+
afterResponse: afterRes?.response || response,
|
|
220
|
+
afterData: afterRes?.data || data
|
|
221
|
+
};
|
|
222
|
+
}
|