@empathyco/x-adapter 8.0.0-alpha.8 → 8.0.0
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/README.md +683 -5
- package/dist/cjs/http-clients/fetch.http-client.js +2 -2
- package/dist/cjs/http-clients/fetch.http-client.js.map +1 -1
- package/dist/cjs/mappers/schema-mapper.factory.js +3 -4
- package/dist/cjs/mappers/schema-mapper.factory.js.map +1 -1
- package/dist/cjs/schemas/types.js.map +1 -1
- package/dist/cjs/schemas/utils.js +7 -5
- package/dist/cjs/schemas/utils.js.map +1 -1
- package/dist/cjs/utils/index.js +0 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/interpolate.js +3 -3
- package/dist/cjs/utils/interpolate.js.map +1 -1
- package/dist/esm/http-clients/fetch.http-client.js +3 -3
- package/dist/esm/http-clients/fetch.http-client.js.map +1 -1
- package/dist/esm/mappers/schema-mapper.factory.js +4 -5
- package/dist/esm/mappers/schema-mapper.factory.js.map +1 -1
- package/dist/esm/schemas/types.js.map +1 -1
- package/dist/esm/schemas/utils.js +7 -5
- package/dist/esm/schemas/utils.js.map +1 -1
- package/dist/esm/utils/index.js +0 -1
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/interpolate.js +3 -3
- package/dist/esm/utils/interpolate.js.map +1 -1
- package/dist/types/endpoint-adapter/types.d.ts +1 -1
- package/dist/types/http-clients/types.d.ts +1 -1
- package/dist/types/mappers/schema-mapper.factory.d.ts +2 -2
- package/dist/types/mappers/types.d.ts +1 -1
- package/dist/types/schemas/types.d.ts +14 -12
- package/dist/types/schemas/utils.d.ts +1 -1
- package/dist/types/utils/index.d.ts +0 -1
- package/package.json +18 -14
- package/dist/cjs/utils/extract-value.js +0 -28
- package/dist/cjs/utils/extract-value.js.map +0 -1
- package/dist/esm/utils/extract-value.js +0 -24
- package/dist/esm/utils/extract-value.js.map +0 -1
- package/dist/types/utils/extract-value.d.ts +0 -12
package/README.md
CHANGED
|
@@ -1,13 +1,691 @@
|
|
|
1
1
|
# x-adapter
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
**Empathy Adapter** is a library of utils to ease the communication with any API.
|
|
4
|
+
|
|
5
|
+
Some features that it provides:
|
|
5
6
|
|
|
6
7
|
- Create an API request function based on a simple configuration.
|
|
8
|
+
- Allow to configure several endpoints by extending the initial configuration.
|
|
7
9
|
- Allow to configure the response/request mapping.
|
|
8
10
|
- Create mapping functions based on Schemas.
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
<br>
|
|
13
|
+
|
|
14
|
+
## Tech Stack
|
|
15
|
+
|
|
16
|
+
[](https://www.typescriptlang.org/)
|
|
17
|
+
[](https://jestjs.io/)
|
|
18
|
+
|
|
19
|
+
<br>
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
# or pnpm or yarn
|
|
25
|
+
npm install @empathyco/x-adapter
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you use this package together with
|
|
29
|
+
[x-components](https://github.com/empathyco/x/tree/main/packages/x-components), you should
|
|
30
|
+
additionally install the
|
|
31
|
+
[@empathyco/x-types](https://github.com/empathyco/x/tree/main/packages/search-types) package and
|
|
32
|
+
take the advantage of it in your project development.
|
|
33
|
+
|
|
34
|
+
<br>
|
|
35
|
+
|
|
36
|
+
## Configuration & Usage
|
|
37
|
+
|
|
38
|
+
An `API Adapter` is a collection of `EndpointAdapters`, one for each endpoint of the API you want to
|
|
39
|
+
consume. Each `EndpointAdapter` is an asynchronous function that performs a request with the given
|
|
40
|
+
data, and returns a response promise with the requested data. Internally, it usually has to
|
|
41
|
+
transform the request data so the API can understand it, and the response data so your app
|
|
42
|
+
understands it as well.
|
|
43
|
+
|
|
44
|
+
<br>
|
|
45
|
+
|
|
46
|
+
### Implement your own adapter
|
|
47
|
+
|
|
48
|
+
To create an `EndpointAdapter` you can use the `endpointAdapterFactory` function. This function will
|
|
49
|
+
receive an `EndpointAdapterOptions` object containing all the needed data to perform and map a
|
|
50
|
+
request, and return a function that when invoked will trigger the request. The options that can be
|
|
51
|
+
configured are:
|
|
52
|
+
|
|
53
|
+
- `endpoint`: The URL that the `httpClient` uses. It can be either a string or a mapper function
|
|
54
|
+
that dynamically generates the URL string using the request data.
|
|
55
|
+
- `httpClient`: A function that will receive the endpoint and request options such as the parameters
|
|
56
|
+
and will perform the request, returning a promise with the unprocessed response data.
|
|
57
|
+
- `defaultRequestOptions`: Default values for the endpoint configuration. You can use it to define
|
|
58
|
+
if a request is cancelable, a unique id to identify it, anything but the `endpoint` can be set.
|
|
59
|
+
Check
|
|
60
|
+
[EndpointAdapterOptions](https://github.com/empathyco/x/blob/main/packages/x-adapter/src/endpoint-adapter/types.ts)
|
|
61
|
+
to see the available options.
|
|
62
|
+
- `requestMapper`: A function to transform the unprocessed request into parameters the API can
|
|
63
|
+
understand.
|
|
64
|
+
- `responseMapper`: A function to transform the API response into data that your project can
|
|
65
|
+
understand.
|
|
66
|
+
|
|
67
|
+
<br>
|
|
68
|
+
|
|
69
|
+
#### Basic adapter implementation
|
|
70
|
+
|
|
71
|
+
In this example we have a simple request mapper that will add a `q` parameter to the endpoint's url
|
|
72
|
+
to perform the request. If you check the function call below, you will see the query parameter
|
|
73
|
+
passed.
|
|
74
|
+
|
|
75
|
+
###### Types definition
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// API models
|
|
79
|
+
interface ApiRequest {
|
|
80
|
+
q?: string;
|
|
81
|
+
id?: number;
|
|
82
|
+
}
|
|
83
|
+
interface ApiSearchResponse {
|
|
84
|
+
products: ApiProduct[];
|
|
85
|
+
total: number;
|
|
86
|
+
}
|
|
87
|
+
interface ApiProduct {
|
|
88
|
+
id: number;
|
|
89
|
+
title: string;
|
|
90
|
+
price: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// App models
|
|
94
|
+
interface AppSearchRequest {
|
|
95
|
+
query: string;
|
|
96
|
+
}
|
|
97
|
+
interface AppSearchResponse {
|
|
98
|
+
products: AppProduct[];
|
|
99
|
+
total: number;
|
|
100
|
+
}
|
|
101
|
+
interface AppProduct {
|
|
102
|
+
id: string;
|
|
103
|
+
name: string;
|
|
104
|
+
price: number;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
###### Adapter's factory function implementation
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { endpointAdapterFactory } from '@empathyco/x-adapter';
|
|
112
|
+
|
|
113
|
+
export const searchProducts = endpointAdapterFactory({
|
|
114
|
+
endpoint: 'https://dummyjson.com/products/search',
|
|
115
|
+
requestMapper({ query }: Readonly<AppSearchRequest>): ApiRequest {
|
|
116
|
+
return {
|
|
117
|
+
q: query // the request will be triggered as https://dummyjson.com/products/search?q=phone
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
responseMapper({ products, total }: Readonly<ApiSearchResponse>): AppSearchResponse {
|
|
121
|
+
return {
|
|
122
|
+
products: products.map(product => {
|
|
123
|
+
return {
|
|
124
|
+
id: product.id.toString(),
|
|
125
|
+
name: product.title,
|
|
126
|
+
price: product.price
|
|
127
|
+
};
|
|
128
|
+
}),
|
|
129
|
+
total: total
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Function call
|
|
135
|
+
async function searchOnClick() {
|
|
136
|
+
const response = await searchProducts({ query: 'phone' });
|
|
137
|
+
console.log('products', response.products);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
<br>
|
|
142
|
+
|
|
143
|
+
#### Using a dynamic endpoint
|
|
144
|
+
|
|
145
|
+
If you need to generate an endpoint url dynamically, you can add parameters inside curly brackets to
|
|
146
|
+
the endpoint string. By default, these parameters will be replaced using the request data. If a
|
|
147
|
+
parameter is not found inside the request, an empty string will be used.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
export const getItemById = endpointAdapterFactory({
|
|
151
|
+
endpoint: 'https://dummyjson.com/{section}/{id}'
|
|
152
|
+
// ... rest of options to configure
|
|
153
|
+
});
|
|
154
|
+
getItemById({ section: 'products', id: 1 }); // 'https://dummyjson.com/products/1'
|
|
155
|
+
getItemById({ section: 'quotes', id: 3 }); // 'https://dummyjson.com/quotes/3'
|
|
156
|
+
getItemById({ section: 'quotes' }); // 'https://dummyjson.com/quotes/'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For more complex use cases, you can use a mapper function. This function receives the request, and
|
|
160
|
+
must return the URL string.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
export const getProductById = endpointAdapterFactory({
|
|
164
|
+
endpoint: ({ id }: GetProductByIdRequest) => 'https://dummyjson.com/products/' + id
|
|
165
|
+
// ... rest of options to configure
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Additionally, you can also overwrite your adapter's endpoint definition using the
|
|
170
|
+
`RequestOptions.endpoint` parameter in the function call. Take into account that your
|
|
171
|
+
`responseMapper` definition should be agnostic enough to support the change:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
export const getItemById = endpointAdapterFactory({
|
|
175
|
+
endpoint: 'https://dummyjson.com/quotes/{id}',
|
|
176
|
+
// ... rest of options to configure
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// You would pass the new endpoint in the function call
|
|
180
|
+
getItemById({ id: 1 }, { endpoint: 'https://dummyjson.com/products/{id}');
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
<br>
|
|
184
|
+
|
|
185
|
+
#### Using the httpClient function
|
|
186
|
+
|
|
187
|
+
Every adapter created using `endpointAdapterFactory` uses the `Fetch API` by default to perform the
|
|
188
|
+
requests. But you can use your own `HttpClient` as part of the configurable
|
|
189
|
+
`EndpointAdapterOptions`. A `HttpClient` is a function that accepts two parameters: the `endpoint`
|
|
190
|
+
string, and an additional
|
|
191
|
+
[`options`](https://github.com/empathyco/x/blob/main/packages/x-adapter/src/http-clients/types.ts)
|
|
192
|
+
object to make the request with.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// HTTP Client
|
|
196
|
+
const axiosHttpClient: HttpClient = (endpoint, options) =>
|
|
197
|
+
axios.get(endpoint, { params: options?.parameters }).then(response => response.data);
|
|
198
|
+
|
|
199
|
+
// Request Mapper
|
|
200
|
+
const customRequestMapper: Mapper<AppSearchRequest, ApiRequest> = ({ query }) => {
|
|
201
|
+
return {
|
|
202
|
+
q: query
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Response Mapper
|
|
207
|
+
const customResponseMapper: Mapper<ApiSearchResponse, AppSearchResponse> = ({
|
|
208
|
+
products,
|
|
209
|
+
total
|
|
210
|
+
}) => {
|
|
211
|
+
return {
|
|
212
|
+
products: products.map(product => {
|
|
213
|
+
return {
|
|
214
|
+
id: product.id.toString(),
|
|
215
|
+
name: product.title,
|
|
216
|
+
price: product.price
|
|
217
|
+
};
|
|
218
|
+
}),
|
|
219
|
+
total: total
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Adapter factory function implementation
|
|
224
|
+
export const searchProducts = endpointAdapterFactory({
|
|
225
|
+
endpoint: 'https://dummyjson.com/products/search',
|
|
226
|
+
httpClient: axiosHttpClient,
|
|
227
|
+
requestMapper: customRequestMapper,
|
|
228
|
+
responseMapper: customResponseMapper
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
<br>
|
|
233
|
+
|
|
234
|
+
### Implement your own adapter using schemas
|
|
235
|
+
|
|
236
|
+
Sometimes the transformations you will need to do in the mappers are just renaming parameters. What
|
|
237
|
+
the API calls `q` might be called `query` in your request. To ease this transformations,
|
|
238
|
+
`@empathyco/x-adapter` allows to create mappers using schemas.
|
|
239
|
+
|
|
240
|
+
A schema is just a dictionary where the key is the desired parameter name, and the value is the path
|
|
241
|
+
of the source object that has the desired value or a simple mapper function if you need to transform
|
|
242
|
+
the value somehow.
|
|
243
|
+
|
|
244
|
+
###### Types definition
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// API models
|
|
248
|
+
interface ApiUserRequest {
|
|
249
|
+
q: string;
|
|
250
|
+
}
|
|
251
|
+
interface ApiUserResponse {
|
|
252
|
+
users: ApiUser[];
|
|
253
|
+
total: number;
|
|
254
|
+
}
|
|
255
|
+
interface ApiUser {
|
|
256
|
+
id: number;
|
|
257
|
+
firstName: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// App models
|
|
261
|
+
interface AppUserRequest {
|
|
262
|
+
query: string;
|
|
263
|
+
}
|
|
264
|
+
interface AppUserResponse {
|
|
265
|
+
people: AppUser[];
|
|
266
|
+
total: number;
|
|
267
|
+
}
|
|
268
|
+
interface AppUser {
|
|
269
|
+
id: string;
|
|
270
|
+
name: string;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
###### Schema's mapper factory function implementation
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
// Map both the request and the response to connect your model with the API you are working with.
|
|
278
|
+
const userSearchRequestMapper = schemaMapperFactory<AppUserRequest, ApiUserRequest>({
|
|
279
|
+
q: 'query'
|
|
280
|
+
});
|
|
281
|
+
const userSearchResponseMapper = schemaMapperFactory<ApiUserResponse, AppUserResponse>({
|
|
282
|
+
people: ({ users }) =>
|
|
283
|
+
users.map(user => {
|
|
284
|
+
return {
|
|
285
|
+
id: user.id.toString(),
|
|
286
|
+
name: user.firstName
|
|
287
|
+
};
|
|
288
|
+
}),
|
|
289
|
+
total: 'total'
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Use the mappers in the Endpoint's adapter factory function
|
|
293
|
+
export const searchUsers = endpointAdapterFactory({
|
|
294
|
+
endpoint: 'https://dummyjson.com/users/search',
|
|
295
|
+
requestMapper: userSearchRequestMapper,
|
|
296
|
+
responseMapper: userSearchResponseMapper
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
<br>
|
|
301
|
+
|
|
302
|
+
#### Create more complex models with SubSchemas
|
|
303
|
+
|
|
304
|
+
When you are creating adapters for different APIs you might find the case that you have to map the
|
|
305
|
+
same model in different places. To help you with that, schemas allows to use `SubSchemas`. To use
|
|
306
|
+
them you just have to provide with the `Path` of the data to map, and the `Schema` to apply to it.
|
|
307
|
+
|
|
308
|
+
###### Types definition
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
// API models
|
|
312
|
+
interface ApiRequest {
|
|
313
|
+
q: string;
|
|
314
|
+
}
|
|
315
|
+
interface ApiResponse {
|
|
316
|
+
users: ApiUser[];
|
|
317
|
+
total: number;
|
|
318
|
+
}
|
|
319
|
+
interface ApiUser {
|
|
320
|
+
id: number;
|
|
321
|
+
email: string;
|
|
322
|
+
phone: string;
|
|
323
|
+
address: ApiAddress;
|
|
324
|
+
company: ApiAddress;
|
|
325
|
+
}
|
|
326
|
+
interface ApiAddress {
|
|
327
|
+
address: string;
|
|
328
|
+
city: string;
|
|
329
|
+
postalCode: string;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// App models
|
|
333
|
+
interface AppRequest {
|
|
334
|
+
query: string;
|
|
335
|
+
}
|
|
336
|
+
interface AppResponse {
|
|
337
|
+
people: AppUser[];
|
|
338
|
+
count: number;
|
|
339
|
+
}
|
|
340
|
+
interface AppUser {
|
|
341
|
+
id: number;
|
|
342
|
+
contact: {
|
|
343
|
+
email: string;
|
|
344
|
+
phone: string;
|
|
345
|
+
homeAddress: AppAddress;
|
|
346
|
+
companyAddress: AppAddress;
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
interface AppAddress {
|
|
350
|
+
displayName: string;
|
|
351
|
+
postalCode: number;
|
|
352
|
+
city: string;
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
###### Schemas and SubSchemas implementation
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
// Address Schema definition
|
|
360
|
+
const addressSchema: Schema<ApiAddress, AppUserAddress> = {
|
|
361
|
+
displayName: source => `${source.address}, ${source.postalCode} - ${source.city}`,
|
|
362
|
+
city: 'city',
|
|
363
|
+
postalCode: source => parseInt(source.postalCode)
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// User Schema definition with a subSchema
|
|
367
|
+
const userSchema: Schema<ApiUser, AppUser> = {
|
|
368
|
+
id: 'id',
|
|
369
|
+
contact: {
|
|
370
|
+
email: source => source.email.toLowerCase(),
|
|
371
|
+
phone: 'phone',
|
|
372
|
+
homeAddress: {
|
|
373
|
+
$subSchema: addressSchema,
|
|
374
|
+
$path: 'address'
|
|
375
|
+
},
|
|
376
|
+
companyAddress: {
|
|
377
|
+
$subSchema: addressSchema,
|
|
378
|
+
$path: 'address'
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
###### Schema's mapper factory function implementation with subSchemas
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
// Response mapper with user's subSchema implemented
|
|
388
|
+
const responseMapper = schemaMapperFactory<ApiSearchUsersResponse, AppSearchUsersResponse>({
|
|
389
|
+
people: {
|
|
390
|
+
$subSchema: userSchema,
|
|
391
|
+
$path: 'users'
|
|
392
|
+
},
|
|
393
|
+
count: 'total'
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const requestMapper = schemaMapperFactory<SearchUsersRequest, ApiSearchUsersRequest>({
|
|
397
|
+
q: 'query'
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Endpoint Adapter Factory function implementation
|
|
401
|
+
export const searchUsersWithContactInfo = endpointAdapterFactory({
|
|
402
|
+
endpoint: 'https://dummyjson.com/users/search',
|
|
403
|
+
requestMapper,
|
|
404
|
+
responseMapper
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
<br>
|
|
409
|
+
|
|
410
|
+
#### Using a mutable schema
|
|
411
|
+
|
|
412
|
+
This feature lets you have some default schemas, and modify or extend them for some concrete
|
|
413
|
+
implementations. To do so, you can use the `createMutableSchema` function, passing a `Source` and
|
|
414
|
+
`Target` type parameters to map your models. This function will return a `MutableSchema` that apart
|
|
415
|
+
from the mapping information will also contain some methods to create new schemas or modify the
|
|
416
|
+
current one.
|
|
417
|
+
|
|
418
|
+
In the example below we will create a `MutableSchema` to have a default object that will be reused
|
|
419
|
+
for different endpoint calls.
|
|
420
|
+
|
|
421
|
+
###### Types definition and MutableSchema
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
// API models
|
|
425
|
+
export interface ApiBaseObject {
|
|
426
|
+
id: number;
|
|
427
|
+
body: string;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// APP models
|
|
431
|
+
export interface AppBaseObject {
|
|
432
|
+
id: string;
|
|
433
|
+
text: string;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Mutable Schema
|
|
437
|
+
export const baseObjectSchema = createMutableSchema<ApiBaseObject, AppBaseObject>({
|
|
438
|
+
id: ({ id }) => id.toString(),
|
|
439
|
+
text: 'body'
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Once we have the `MutableSchema`, we can use the following methods to fit our different APIs needs:
|
|
444
|
+
|
|
445
|
+
- `$extends`: Creates a new `MutableSchema` based on the original one. The original remains
|
|
446
|
+
unchanged. This can be useful if we need to create a new `EndpointAdapter` with models based on
|
|
447
|
+
another API.
|
|
448
|
+
- `$override`: Merges/modifies the original `MutableSchema` partially, so the change will affect to
|
|
449
|
+
all the `EndpointAdapter`(s) that are using it. It can be used to change the structure of our
|
|
450
|
+
request/response mappers, or to add them new fields. Useful for clients with few differences in
|
|
451
|
+
their APIs. For example, you can create a library with a default adapter and use this library from
|
|
452
|
+
the customer projects overriding only the needed field (e.g. retrieve the images from `pictures`
|
|
453
|
+
instead of `images` in a products API).
|
|
454
|
+
- `$replace`: Replaces completely the original `MutableSchema` by a new one, it won't exist anymore.
|
|
455
|
+
The change will affect to all the `EndpointAdapter`(s) that were using it. Useful for clients with
|
|
456
|
+
a completely different API/response to the standard you have been working with.
|
|
457
|
+
|
|
458
|
+
###### Extend a MutableSchema to reuse it in two different endpoints with more fields
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
import { ApiBaseObject, AppBaseObject, baseObjectSchema } from '@/base-types';
|
|
462
|
+
|
|
463
|
+
// Api models
|
|
464
|
+
interface ApiPost extends ApiBaseObject {
|
|
465
|
+
title: string;
|
|
466
|
+
}
|
|
467
|
+
interface ApiPostsResponse {
|
|
468
|
+
posts: ApiPost[];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
interface ApiComment extends ApiBaseObject {
|
|
472
|
+
postId: number;
|
|
473
|
+
}
|
|
474
|
+
interface ApiCommentsResponse {
|
|
475
|
+
comments: ApiComment[];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// App models
|
|
479
|
+
interface AppPost extends AppBaseObject {
|
|
480
|
+
postTitle: string;
|
|
481
|
+
}
|
|
482
|
+
interface AppPostsResponse {
|
|
483
|
+
posts: AppPost[];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
interface AppComment extends AppBaseObject {
|
|
487
|
+
postId: number;
|
|
488
|
+
}
|
|
489
|
+
interface AppCommentsResponse {
|
|
490
|
+
comments: AppComment[];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Extend for posts endpoint
|
|
494
|
+
const postSchema = baseObjectSchema.$extends<ApiPost, AppPost>({
|
|
495
|
+
postTitle: 'title'
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const postsResponse = schemaMapperFactory<ApiPostsResponse, AppPostsResponse>({
|
|
499
|
+
posts: {
|
|
500
|
+
$subSchema: postSchema,
|
|
501
|
+
$path: 'posts'
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
export const searchPosts = endpointAdapterFactory({
|
|
506
|
+
endpoint: 'https://dummyjson.com/posts',
|
|
507
|
+
responseMapper: postsResponse
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Extend for comments endpoint
|
|
511
|
+
const commentSchema = baseObjectSchema.$extends<ApiComment, AppComment>({
|
|
512
|
+
postId: 'postId'
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const commentsResponse = schemaMapperFactory<ApiCommentsResponse, AppCommentsResponse>({
|
|
516
|
+
comments: {
|
|
517
|
+
$subSchema: commentSchema,
|
|
518
|
+
$path: 'comments'
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
export const searchComments = endpointAdapterFactory({
|
|
523
|
+
endpoint: 'https://dummyjson.com/comments',
|
|
524
|
+
responseMapper: commentsResponse
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
###### Override a MutableSchema to add more fields
|
|
529
|
+
|
|
530
|
+
As said above, the suitable context for using the `override` method would be a project with an API
|
|
531
|
+
that doesn't differ too much against the one used in our "base project". That means we can reuse
|
|
532
|
+
most of the types and schemas definitions, so we would only add a few new fields from the new API.
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
import { ApiBaseObject, AppBaseObject, baseObjectSchema } from '@/base-types';
|
|
536
|
+
|
|
537
|
+
// Api models
|
|
538
|
+
interface ApiTodo {
|
|
539
|
+
completed: boolean;
|
|
540
|
+
todo: string;
|
|
541
|
+
userId: number;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
interface ApiTodosResponse {
|
|
545
|
+
todos: ApiBaseObject[];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// App models
|
|
549
|
+
interface AppTodo {
|
|
550
|
+
completed: boolean;
|
|
551
|
+
text: string;
|
|
552
|
+
userId: string;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
interface AppTodosResponse {
|
|
556
|
+
todos: AppBaseObject[];
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Response mapper
|
|
560
|
+
const todosResponse = schemaMapperFactory<ApiTodosResponse, AppTodosResponse>({
|
|
561
|
+
todos: {
|
|
562
|
+
$subSchema: baseObjectSchema,
|
|
563
|
+
$path: 'todos'
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Endpoint Adapter
|
|
568
|
+
export const searchTodos = endpointAdapterFactory({
|
|
569
|
+
endpoint: 'https://dummyjson.com/todos',
|
|
570
|
+
responseMapper: todosResponse
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Override the original Schema. The Schema changes to map: 'id', 'completed', 'text' and 'userId''
|
|
574
|
+
baseObjectSchema.$override<ApiTodo, AppTodo>({
|
|
575
|
+
completed: 'completed',
|
|
576
|
+
text: 'todo',
|
|
577
|
+
userId: ({ userId }) => userId.toString()
|
|
578
|
+
});
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
###### Replace a MutableSchema to completely change it
|
|
582
|
+
|
|
583
|
+
In this case we are facing too many differences between API responses. We don't need to write a
|
|
584
|
+
whole adapter from scratch, as there are other parts of the API that aren't changing so much, but we
|
|
585
|
+
should replace some `endpointAdapter`'s schemas.
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
import { ApiBaseObject, AppBaseObject, baseObjectSchema } from '@/base-types';
|
|
589
|
+
|
|
590
|
+
// Api models
|
|
591
|
+
interface ApiQuote {
|
|
592
|
+
id: number;
|
|
593
|
+
quote: string;
|
|
594
|
+
author: string;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
interface ApiQuotesResponse {
|
|
598
|
+
quotes: ApiBaseObject[];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// App models
|
|
602
|
+
interface AppQuote {
|
|
603
|
+
quoteId: string;
|
|
604
|
+
quote: string;
|
|
605
|
+
author: string;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
interface AppQuotesResponse {
|
|
609
|
+
quotes: AppBaseObject[];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Response mapper
|
|
613
|
+
const quotesResponse = schemaMapperFactory<ApiQuotesResponse, AppQuotesResponse>({
|
|
614
|
+
quotes: {
|
|
615
|
+
$subSchema: baseObjectSchema,
|
|
616
|
+
$path: 'quotes'
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Endpoint Adapter
|
|
621
|
+
export const searchQuotes = endpointAdapterFactory({
|
|
622
|
+
endpoint: 'https://dummyjson.com/quotes',
|
|
623
|
+
responseMapper: quotesResponse
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Replace the original Schema
|
|
627
|
+
baseObjectSchema.$replace<ApiQuote, AppQuote>({
|
|
628
|
+
quoteId: ({ id }) => id.toString(),
|
|
629
|
+
quote: 'quote',
|
|
630
|
+
author: 'author'
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
<br>
|
|
635
|
+
|
|
636
|
+
### Extend an adapter that uses schemas
|
|
637
|
+
|
|
638
|
+
Imagine you have a new setup and that you can reuse most of the stuff you have developed. Probably
|
|
639
|
+
you have built an adapter instance as a configuration object that contains all of your
|
|
640
|
+
`EndpointAdapter` calls, so you only need to extend the endpoint you need to change.
|
|
641
|
+
|
|
642
|
+
```ts
|
|
643
|
+
export const adapter = {
|
|
644
|
+
searchItem: getItemById,
|
|
645
|
+
searchList: searchComments
|
|
646
|
+
// Any endpoint adapter you are using to communicate with your API
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
adapter.searchList = searchComments.extends({
|
|
650
|
+
endpoint: 'https://dummyjson.com/comments/',
|
|
651
|
+
defaultRequestOptions: {
|
|
652
|
+
// If you need to send an id, a header...
|
|
653
|
+
},
|
|
654
|
+
defaultRequestOptions: {
|
|
655
|
+
parameters: {
|
|
656
|
+
limit: 10,
|
|
657
|
+
skip: 10
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
For further detail, you can check the
|
|
664
|
+
[x-platform-adapter](https://github.com/empathyco/x/tree/main/packages/x-adapter-platform) package.
|
|
665
|
+
It is a whole adapter implementation using this `x-adapter` library to suit the
|
|
666
|
+
[Search Platform API](https://docs.empathy.co/develop-empathy-platform/api-reference/search-api.html)
|
|
667
|
+
needs.
|
|
668
|
+
|
|
669
|
+
## Test
|
|
670
|
+
|
|
671
|
+
**Empathy Adapter** features are tested using [Jest](https://jestjs.io/). You will find a
|
|
672
|
+
`__tests__` folder inside each of the project's sections.
|
|
673
|
+
|
|
674
|
+
```
|
|
675
|
+
npm run test
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
## Changelog
|
|
679
|
+
|
|
680
|
+
[Changelog summary](https://github.com/empathyco/x/blob/main/packages/x-adapter/CHANGELOG.md)
|
|
681
|
+
|
|
682
|
+
## Contributing
|
|
683
|
+
|
|
684
|
+
To start contributing to the project, please take a look at our
|
|
685
|
+
**[Contributing Guide](https://github.com/empathyco/x/blob/main/.github/CONTRIBUTING.md).** Take in
|
|
686
|
+
account that `x-adapter` is developed using [Typescript](https://www.typescriptlang.org/), so we
|
|
687
|
+
recommend you to check it out.
|
|
688
|
+
|
|
689
|
+
## License
|
|
11
690
|
|
|
12
|
-
|
|
13
|
-
[Contributing Guide](../../.github/CONTRIBUTING.md).
|
|
691
|
+
[empathyco/x License](https://github.com/empathyco/x/blob/main/packages/x-adapter/LICENSE)
|
|
@@ -16,8 +16,8 @@ const utils_1 = require("./utils");
|
|
|
16
16
|
const fetchHttpClient = (endpoint, { id = endpoint, cancelable = true, parameters = {}, properties, sendParamsInBody = false } = {}) => {
|
|
17
17
|
const signal = cancelable ? { signal: abortAndGetNewAbortSignal(id) } : {};
|
|
18
18
|
const flatParameters = (0, x_utils_1.flatObject)(parameters);
|
|
19
|
-
const url = sendParamsInBody ? endpoint : (0, utils_1.buildUrl)(endpoint, flatParameters);
|
|
20
|
-
const bodyParameters = sendParamsInBody ? { body: JSON.stringify(parameters) } : {};
|
|
19
|
+
const url = sendParamsInBody ? endpoint : (0, utils_1.buildUrl)(endpoint, (0, x_utils_1.cleanEmpty)(flatParameters));
|
|
20
|
+
const bodyParameters = sendParamsInBody ? { body: JSON.stringify((0, x_utils_1.cleanEmpty)(parameters)) } : {};
|
|
21
21
|
return fetch(url, {
|
|
22
22
|
...properties,
|
|
23
23
|
...bodyParameters,
|