Browse Source

refactor RouteFunction behaviour, document it

tags/v1.0.0-alpha.1
ojizero 1 year ago
parent
commit
ec5804bc5d
No account linked to committer's email address
6 changed files with 127 additions and 52 deletions
  1. 4
    4
      .npmignore
  2. 90
    4
      README.md
  3. 1
    1
      examples/sample-elasticsearch-client/index.js
  4. 1
    1
      package.json
  5. 27
    38
      src/method.ts
  6. 4
    4
      test/method.spec.ts

+ 4
- 4
.npmignore View File

@@ -1,4 +1,4 @@
./src
./test
./examples
./tsconfig*.json
src
test
examples
tsconfig*.json

+ 90
- 4
README.md View File

@@ -16,7 +16,7 @@ Instead of worrying about how to consume some HTTP API, we should focus on the b

So instead of having the focus be centered around how you make the request like the following,

```javascript
```js
import request from 'request' // or who knows what you wanna use ¯\_(ツ)_/¯

function myApiWrapper (arg1, arg2) {
@@ -38,7 +38,7 @@ function myApiWrapper (arg1, arg2) {

The above boilerplate where you worry about whether you're using `request` or `request-promise` or whatnot, and you worry about how to resolve your response and what it looks like, get completely abstracted and unified into,

```javascript
```js
import portal from '@ojizero/portal'

const portalBase = portal({ baseUrl: 'https://some.api' })
@@ -80,6 +80,8 @@ const client = portal({ baseUrl: 'some.base.url' }) // Initial configuration can
export const someGetMethod = client.route({ path: '/some/path' })
// Get method with path variables
export const someGetMethodWithParam = client.route({ path: '/some/path/:withInnerVariable' })
// Post method with path variables
export const somePostMethodWithParam = client.route({ path: '/some/path/:withInnerVariable', method: 'POST' })

/// NOTE: ideally this wouldn't be a module level instance but this is to simplify this example 😬

@@ -90,6 +92,7 @@ import * as YourAPIClient from 'your-client-module'

const someGetMethodPromise = YourAPIClient.someGetMethod() // GET http://some.base.url/some/path
const someGetMethodWithParamPromise = YourAPIClient.someGetMethodWithParam(5) // GET http://some.base.url/some/path/5
const somePostMethodWithParamPromise = YourAPIClient.someGetMethodWithParam(5, { payload: { some: 'payload' } }) // POST http://some.base.url/some/path/5 { some: 'payload' }
```

Examples can be found in the [`examples`](./examples) folder
@@ -219,6 +222,62 @@ The Accept header value of the request, defaults to `application/json`.

Additional headers to always be added to requests to the given route, defaults to `{}`.

###### Usage example

```js
/* --- snip --- */

const getRoute = portalObject.route({ path: '/some/base/path' })
await getRoute() // GET /some/base/path
await getRoute({ queryString: { a: 10 } }) // GET /some/base/path?a=10

/* --- snip --- */

const postRoute = portalObject.route({ path: '/some/base/path', method: 'POST' })
await postRoute({ payload: { some: 'payload' } }) // POST /some/base/path { some: 'payload' }

/* --- snip --- */
```

###### RouteFunction

Type: `(...args: any[]): Promise<Response>`

A function that takes any number of arguments and performs the HTTP request.

The only special argument is the last one, which can be an object hodling any request options that can be used by the underlying client, as well as the payload (under the key `payload`) and query string (under the key `queryString`) objects if needed.

Example calls would be

```js
getMethod(arg1, arg2, /*...,*/ argn)
getMethod(arg1, arg2, /*...,*/ argn, { queryString: { q: 'hello' } })
postMethod(arg1, arg2, /*...,*/ argn, { payload: { q: 'hello' } })
```

All calls to a RouteFunction produce a promise that resolves to an oject of [`Response`](######response) type.

###### Response

An object with the following attributes,

- `status`, which is an object holding the HTTP status both as a message and a code.
- `body`, any response body returns from the HTTP request
- `headers`, HTTP headers returned for the response
- `_rawResponse`, the raw underlying HTTP response object (from the underlying client library)

```ts
export interface Response {
status: {
code?: number,
word?: string,
},
body: any,
headers: IncomingHttpHeaders,
_rawResponse: RawResponse,
}
```

##### resource

A function (representing a ResourceFactory), and can be used to generate client resources. A resource is a basic CRUD API for a given route, providing a default set of APIs for `list`, `get`, `edit`, `add`, and `delete`, those APIs correspond to the following HTTP calls,
@@ -231,6 +290,8 @@ A function (representing a ResourceFactory), and can be used to generate client

It takes a [`ResourceConfig`](######resourceconfig) object as input and returns a ResourceFactory function. When calling the ResourceFactory, the result is an object with the list of CRUD operations (and any additional ones defined in the resource config) as function defined on it.

The result from using the resource factory, is an object with the enabled default resource method, and any other extra ones as its keys, and each of those keys poiting to the `RouteFunction` used to execute the API call.

###### ResourceConfig

An object with the following attributes,
@@ -247,10 +308,35 @@ The list of routes to enable, by default all basic CRUD APIs are enabled.

A JSON with string keys mapping to corresponding [MethodSpec](######methodspec). Each key will be added an added method defined by it's corresponding spec. Defaults to `{}`.

###### Usage example

```js
/* --- snip --- */

const apiResource = portalObject.resource({ baseRoute: '/some/base/path' })
/**
* apiResource has the following methods on it
* list, get, edit, add, set, del, delete
* corresponding the default set of APIs in an HTTP resource
*/

await apiResource.list() // GET /some/base/path
await apiResource.get(someId) // GET /some/base/path/:someId
// ... etc

/* --- snip --- */
```

Each method defined on the resource object follows the type [RouteFunction](######routefunction).

##### _client

The underlying client instance used by the two previous factories. This is exposed only for transparncy but is not intended to be used ideally by anyone external.

##### Usage of the portal object

RouteFunction

## Status

This is still a work in progress :D any help is appreciated
@@ -263,9 +349,9 @@ This is still a work in progress :D any help is appreciated
- [ ] Support non JSON payload
- [ ] Get `onError: resolve` to work
- [ ] Support simplified form for validation
- [ ] Finalize behvaiour of genreator method functions
- [x] Finalize behvaiour of genreator method functions
- [x] What to do with their arguments (ambiguity of options/payload/querystring in args)
- [ ] Document internal method factory behvaiour
- [x] Document RouteFunction behvaiour
- [ ] ...

## License

+ 1
- 1
examples/sample-elasticsearch-client/index.js View File

@@ -60,7 +60,7 @@ if (require.main === module) {
delete response._rawResponse // This is a HUGE object
console.log({ createIndex: { stringifiedResponse: JSON.stringify(response) } })

response = await client.addDocument('test-index', 'test-type', 'test-id', { a: { test: 'document' } })
response = await client.addDocument('test-index', 'test-type', 'test-id', { payload: { a: { test: 'document' } } })
delete response._rawResponse // This is a HUGE object
console.log({ addDocument: { stringifiedResponse: JSON.stringify(response) } })


+ 1
- 1
package.json View File

@@ -1,6 +1,6 @@
{
"name": "@ojizero/portal",
"version": "1.0.0-alpha.0",
"version": "1.0.0-alpha.1",
"description": "HTTP API clients ... simplified.",
"main": "lib/index.js",
"publishConfig": {

+ 27
- 38
src/method.ts View File

@@ -1,4 +1,4 @@
import { Client, Response, RequestConfig } from './client'
import { Client, Response, RequestConfig as ClientRequestConfig } from './client'

import defaults from 'lodash.defaultsdeep'
import {
@@ -22,9 +22,19 @@ export interface MethodSpec {
headers?: OutgoingHttpHeaders,
}

export type RequestConfig = ClientRequestConfig & {
path?: any[] | { [p: string]: any },
payload?: any,
queryString?: { [q: string]: any },
}
export type RouteFunction = (...args: any[]) => Promise<Response>
export type MethodFactory = (spec: MethodSpec) => RouteFunction

function isRequestConfig (arg: any): arg is RequestConfig {
return typeof arg === 'object'
}


export function methodGenerator (client: Client): MethodFactory {
return function methodFactory (spec: MethodSpec): RouteFunction {
const {
@@ -40,7 +50,7 @@ export function methodGenerator (client: Client): MethodFactory {

const method = _method.toUpperCase()

const defaultOptions: RequestConfig = {
const defaultOptions: ClientRequestConfig = {
headers: {
'Accept': accept,
'Content-Type': contentType,
@@ -51,11 +61,12 @@ export function methodGenerator (client: Client): MethodFactory {
return async function (...args: any[]): Promise<Response> {
let length = args.length

let query
let payload
let options
let options: RequestConfig = {}
let query: any
let payload: any
// let pathParams

if (typeof args[length - 1] === 'object') {
if (isRequestConfig(args[length - 1])) {
// If the last argument it an
// object use it as options
options = args[length - 1]
@@ -63,40 +74,18 @@ export function methodGenerator (client: Client): MethodFactory {
length -= 1
}

if (typeof args[length - 1] === 'object') {
// If the second to last argument it an
// object use it as a payload
payload = args[length - 1]
args = args.slice(0, length - 1)
length -= 1
} else if ((!!body) && typeof options !== 'undefined') {
// If paylaod is expected and options is defined use
// the last argument as payload instead of options
payload = options
options = undefined
if ('queryString' in options) {
query = options.queryString
delete options.queryString
}

if (typeof args[length - 1] === 'object') {
// If the third to last argument it an
// object use it as a query string
query = args[length - 1]
args = args.slice(0, length - 1)
length -= 1
} else if ((!!queryString) && (!!body)) {
// Both query string and body are expected
if (method === 'GET') {
// For GET requests give precedence to query string
query = payload
payload = undefined
} else {
// For everything else give precedence to payload
query = undefined
}
} else if ((!!queryString) && (!body)) {
// Query string is expected but body isn't
query = payload
payload = undefined
if ('payload' in options) {
payload = options.payload
delete options.payload
}
// if ('path' in options) {
// pathParams = options.path
// delete options.path
// }

ensureValidData(params, args, 'Parameters')
ensureValidData(body, payload, 'Payload')

+ 4
- 4
test/method.spec.ts View File

@@ -125,7 +125,7 @@ describe('Method', async () => {
methodFunction = methodGenerator(mockPostMethodNoParams)
const payload = { some: { mock: 'payload '} }

await methodFunction(payload)
await methodFunction({ payload })

expect(client.request)
.to.have.been.calledOnceWithExactly(
@@ -145,7 +145,7 @@ describe('Method', async () => {
methodFunction = methodGenerator(mockPostMethodWithParams)
const payload = { some: { mock: 'payload '} }

await methodFunction(10, payload)
await methodFunction(10, { payload })

expect(client.request)
.to.have.been.calledOnceWithExactly(
@@ -169,9 +169,9 @@ describe('Method', async () => {

it('passes query string arguments if specified', async () => {
methodFunction = methodGenerator(mockGetMethodWithQueryString)
const query = { some_arg: 'a-string' }
const queryString = { some_arg: 'a-string' }

await methodFunction(10, query)
await methodFunction(10, { queryString })

expect(client.request)
.to.have.been.calledOnceWithExactly(

Loading…
Cancel
Save