@empathyco/x-adapter 8.0.0-alpha.9 → 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 CHANGED
@@ -1,13 +1,691 @@
1
1
  # x-adapter
2
2
 
3
- `x-adapter` is a library of utils to ease the communication with any API. Some features that it
4
- provides:
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
- ### Contributing
12
+ <br>
13
+
14
+ ## Tech Stack
15
+
16
+ [![TypeScript](https://img.shields.io/badge/-typescript-3178C6?logo=typescript&logoColor=white&style=for-the-badge)](https://www.typescriptlang.org/)
17
+ [![Jest](https://img.shields.io/badge/-jest-C21325?logo=jest&logoColor=white&style=for-the-badge)](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
- To start contributing to the project, please take a look to our
13
- [Contributing Guide](../../.github/CONTRIBUTING.md).
691
+ [empathyco/x License](https://github.com/empathyco/x/blob/main/packages/x-adapter/LICENSE)