-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from contactlab/next
Version 1.0.0
- Loading branch information
Showing
39 changed files
with
7,292 additions
and
4,907 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# http://editorconfig.org | ||
root = true | ||
|
||
[*] | ||
charset = utf-8 | ||
indent_style = space | ||
indent_size = 2 | ||
end_of_line = lf | ||
insert_final_newline = true | ||
trim_trailing_whitespace = true | ||
|
||
[*.md] | ||
insert_final_newline = false | ||
trim_trailing_whitespace = false |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
node_modules | ||
.vscode | ||
.nyc_output | ||
*.log* | ||
dist | ||
coverage | ||
lib |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
git-tag-version = false | ||
tag-version-prefix = | ||
save-exact = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"printWidth": 80, | ||
"singleQuote": true, | ||
"bracketSpacing": false, | ||
"jsxBracketSameLine": false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,6 @@ language: node_js | |
node_js: | ||
- "8" | ||
- "node" | ||
cache: yarn | ||
cache: | ||
directories: | ||
- "node_modules" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,88 +1,213 @@ | ||
# Appy [![Build Status](https://travis-ci.org/contactlab/appy.svg?branch=master)](https://travis-ci.org/contactlab/appy) | ||
# Appy | ||
|
||
Fetch API the Contactlab way | ||
[![Build Status](https://travis-ci.org/contactlab/appy.svg?branch=master)](https://travis-ci.org/contactlab/appy) | ||
|
||
This package is statically typed using Flow | ||
Fetch API the Contactlab way | ||
|
||
## Install | ||
|
||
```sh | ||
$ yarn add @contactlab/appy | ||
$ npm install @contactlab/appy | ||
|
||
# --- or --- | ||
|
||
$ yarn add @contactlab/appy | ||
``` | ||
|
||
## Use | ||
## Motivation | ||
|
||
Appy try to offer a better model for fething resources, using the standard global `fetch()` function as a "backbone" and some principles from Functional Programming paradigm. | ||
|
||
The model is built around the concepts of: | ||
|
||
### Request | ||
- asynchronous operations (`Task`) | ||
- which can fail for some reason (`Either`) | ||
- or return data with a specific shape that should be decoded/validated (`Decoder`). | ||
|
||
```js | ||
import {request} from '@contactlab/appy'; | ||
In order to achieve this, Appy intensely uses: | ||
|
||
const options = { | ||
// request options | ||
}; | ||
- [Typescript](https://www.typescriptlang.org) >= v2.9 | ||
- [`fp-ts`](https://github.com/gcanti/io-ts) | ||
- [`io-ts`](https://github.com/gcanti/fp-ts/) | ||
|
||
// request :: (String, String, ?Object) => Promise<Object> | ||
request('GET', 'https://my.api.com/me', options) | ||
.then(handleResponse) | ||
.catch(handleError) | ||
## API | ||
|
||
**Note:** every sub module/lib is exported into the main `index.ts` file for a comfortable use. | ||
|
||
### request | ||
|
||
```typescript | ||
import {get} from '@contactlab/appy'; | ||
// same as: | ||
// import {get} from '@contactlab/appy/lib/request'; | ||
|
||
get('http://jsonplaceholder.typicode.com/posts') | ||
.run() | ||
.then(result => | ||
result.fold(err => console.error(err), data => console.log(data)) | ||
); | ||
``` | ||
|
||
Request `options`: | ||
- There is no need to specify a `method` key, it is overridden by the method argument passed to `request()`. | ||
- The default `mode` is set to "cors". | ||
- The `body` content gets stringified, so objects are accepted | ||
This is a low level module: | ||
it uses the standard Web API Fetch function (`fetch`) in order to make a request to a resource | ||
and wraps it in a `TaskEither` monad. | ||
|
||
So, you can: | ||
|
||
`request` returns a Promise. | ||
For both Success and Reject cases the return type is: | ||
- use the standard, clean and widely supported api to make XHR; | ||
- "project" it into a declarative functional world where execution is lazy (`Task`); | ||
- handle "by design" the possibility of a failure with an explicit channel for errors (`Either`). | ||
|
||
```js | ||
Promise<{ | ||
status: string, | ||
payload: Object | {message: string} | ||
}> | ||
The module tries to be as more compliant as possible with the `fetch()` interface but with subtle differences: | ||
|
||
- request `method` is always explicit (no implicit "GET"); | ||
- accepted methods are definened by the `Method` union type; | ||
- `fetch`'s input is always a `USVString` (no `Request` objects allowed); | ||
- `Response` is mapped into a specific `AppyResponse<Mixed>` interface; | ||
- `AppyResponse` `headers` property is always a `HeadersMap` (alias for a map of string); | ||
- `AppyResponse` has a `body` property that is the result of parsing to JSON the string returned from `response.text()`; if it cannot be parsed as JSON, `body` value is just the string (both types of data are covered by the `Mixed` type). | ||
|
||
`RequestInit` configuration object instead remains the same. | ||
|
||
#### Exports | ||
|
||
See [here](src/request.ts) for the complete list of types. | ||
|
||
```typescript | ||
function request( | ||
m: Method, | ||
u: USVString, | ||
o?: RequestInit | ||
): TaskEither<AppyError, AppyResponse<Mixed>>; | ||
``` | ||
|
||
### Api | ||
```typescript | ||
function get( | ||
u: USVString, | ||
o?: RequestInit | ||
): TaskEither<AppyError, AppyResponse<Mixed>>; | ||
``` | ||
|
||
Internally uses the `request()`. | ||
```typescript | ||
function post( | ||
u: USVString, | ||
o?: RequestInit | ||
): TaskEither<AppyError, AppyResponse<Mixed>>; | ||
``` | ||
|
||
```js | ||
import {api} from '@contactlab/appy'; | ||
```typescript | ||
function put( | ||
u: USVString, | ||
o?: RequestInit | ||
): TaskEither<AppyError, AppyResponse<Mixed>>; | ||
``` | ||
|
||
const config = { | ||
baseUri: 'https://my.api.com', | ||
id: 'module_id', | ||
version: '1.0.0' | ||
} | ||
const options = { | ||
// request options | ||
}; | ||
```typescript | ||
function patch( | ||
u: USVString, | ||
o?: RequestInit | ||
): TaskEither<AppyError, AppyResponse<Mixed>>; | ||
``` | ||
|
||
// api :: (Object) => (String, String, String, ?Object) => Promise<Object> | ||
const myFetch = api(config); | ||
myFetch('GET', '/me', 'myToken', options) | ||
.then(handleResponse) | ||
.catch(handleError) | ||
```typescript | ||
function del( | ||
u: USVString, | ||
o?: RequestInit | ||
): TaskEither<AppyError, AppyResponse<Mixed>>; | ||
``` | ||
|
||
Request `options` as above apart from the headers: | ||
- If not overwritten by the headers passed in, `"Accept"` and `"Content-type"` are set to "application/json". | ||
- `"Authorization"` is set by the `token` argument. | ||
- `"Contactlab-ClientId"` is set by the `id` key of the configuration. | ||
- `"Contactlab-ClientVersion"` is set by the `version` key of the configuration. | ||
### api | ||
|
||
```typescript | ||
import * as t from 'io-ts'; | ||
import {api} from '@contactlab/appy'; | ||
// same as: | ||
// import {api} from '@contactlab/appy/lib/api'; | ||
|
||
const myApi = api({baseUri: 'http://jsonplaceholder.typicode.com'}); | ||
const token = 'secret'; | ||
const Posts = t.array( | ||
t.type({ | ||
userId: t.number, | ||
id: t.number, | ||
title: t.string, | ||
body: t.string | ||
}) | ||
); | ||
|
||
myApi | ||
.get('/posts', {token, decoder: Posts}) | ||
.run() | ||
.then(result => | ||
result.fold(err => console.error(err), data => console.log(data)) | ||
); | ||
``` | ||
|
||
`api` at the second call returns the `request` Promise type. | ||
This module is tailored on the needs of the Contactlab Frontend Team. | ||
It uses the "low-level" `request` module in order to interact with Contactlab's services (REST API). | ||
|
||
Unless a configuration error occurs, in that case the return type will be: | ||
So, it is a little more opinionated: | ||
|
||
```js | ||
Promise<{ | ||
error: string | ||
}> | ||
- the exposed function (`api`) is used to "load" some configuration and returns an object with methods; | ||
- the configuration has a required `baseUri` (string) key which will be prepended to every `uri`; | ||
- there are also 2 optional keys `id` (string) and `version` (string) which will be passed as request's `headers`: | ||
- `'Contactlab-ClientId': ${id}`, | ||
- `'Contactlab-ClientVersion': ${version}`; | ||
- the main `api` method is `request()` which uses under the hood the `request` module with some subtle differences: | ||
- the `options` parameter is mandatory and it is an extension of the `RequestInit` interface; | ||
- `options` has a required `token` (string) key which will be passed as request's `Authorization: Bearer ${token}` header; | ||
- `options` has a require `decoder` (`Decoder<Mixed, A>`) key which will be used to decode the service's JSON payload; | ||
- decoder errors are expressed with a `DecoderError` class which extends the `AppyError` tagged union type; | ||
- thus, the returned type of `api` methods is `TaskEither<ApiError, A>` | ||
- `headers` in `options` object can only be a map of strings (`{[k: string]: string}`); if you need to work with a `Header` object you have to transform it; | ||
- `options` is merged with a predefined object in order to set some default values: | ||
- `mode: 'cors'` | ||
- `headers: {'Accept': 'application/json', 'Content-type': 'application/json'}` | ||
|
||
#### Exports | ||
|
||
See [here](src/api.ts) for the complete list of types. | ||
|
||
```typescript | ||
interface ApiMethods { | ||
request: <A>(m: Method, u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>; | ||
|
||
get: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>; | ||
|
||
post: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>; | ||
|
||
put: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>; | ||
|
||
patch: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>; | ||
|
||
del: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>; | ||
} | ||
|
||
function api(c: ApiConfig): ApiMethods | ||
``` | ||
|
||
### Test | ||
## Examples | ||
|
||
You can find a couple of examples [here](examples). | ||
|
||
## About `fetch()` compatibility | ||
|
||
The Fetch API is available only on "modern" browsers: if you need to support legacy browsers (e.g. **Internet Explorer 11** or older) or you want to use it in a Nodejs script we recommend you the excellent [`isomorphic-fetch`](https://www.npmjs.com/package/isomorphic-fetch) package. | ||
|
||
## Contributing | ||
|
||
Opening issues is always welcome. | ||
|
||
Then, fork the repository or create a new branch, write your code and send a pull request. | ||
|
||
This project uses [Prettier](https://prettier.io/) (automatically applied as pre-commit hook), [TSLint](https://palantir.github.io/tslint/) and [Jest](https://facebook.github.io/jest/en/). | ||
|
||
Tests are run with: | ||
|
||
```sh | ||
$ yarn test | ||
``` | ||
$ npm test | ||
``` | ||
|
||
## License | ||
|
||
Released under the [Apache 2.0](LICENSE) license. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Examples | ||
|
||
In order to show the examples resultes go to project root and run | ||
|
||
```sh | ||
$ npx ts-node ./examples/${EXAMPLE_FILE_NAME} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/*tslint:disable:no-console*/ | ||
|
||
/** | ||
* Fetch a list of albums with a simple request, | ||
* take the first, | ||
* take the title, | ||
* create a new album with the title in upper-case | ||
* and print the result | ||
*/ | ||
|
||
import 'isomorphic-fetch'; | ||
import {get, post} from '../src/index'; | ||
|
||
interface Album { | ||
userId: number; | ||
id: number; | ||
title: string; | ||
} | ||
|
||
const URL = 'http://jsonplaceholder.typicode.com/albums'; | ||
|
||
// same as: request('GET', URL) | ||
get(URL) | ||
.map(data => (data.body as Album[])[0]) // this is not safe ;) | ||
.map(album => ({ | ||
id: 1001, | ||
userId: album.userId, | ||
title: album.title.toUpperCase() | ||
})) | ||
.chain(newAlbum => | ||
post(URL, { | ||
headers: {'Content-type': 'application/json'}, | ||
body: JSON.stringify(newAlbum) | ||
}) | ||
) | ||
.run() | ||
.then(result => | ||
result.fold(err => console.log(err), data => console.log(data.body)) | ||
); |
Oops, something went wrong.