Skip to content

Commit

Permalink
Merge pull request #23 from truework/beta
Browse files Browse the repository at this point in the history
[Beta] multiple hooks, deep merge options
  • Loading branch information
estrattonbailey committed Aug 19, 2021
2 parents e9e3d14 + f4211e5 commit 89c72c9
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 19 deletions.
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ const { url, status, response } = await gretch(
are good for code that needs to run on every request, like adding tracking
headers and logging errors.

Hooks should be defined as an array. That way you can compose multiple hooks
per-request, and define and merge default hooks when [creating
instances](#creating-instances).

#### `before`

The `before` hook runs just prior to the request being made. You can even modify
Expand All @@ -227,24 +231,29 @@ object, and the full options object.
```js
const response = await gretch('/api/user/12', {
hooks: {
before (request, options) {
request.headers.set('Tracking-ID', 'abcde')
}
before: [
(request, options) => {
request.headers.set('Tracking-ID', 'abcde')
}
]
}
}).json()
```

#### `after`

The `after` hook has the opportunity to read the `gretchen` response. It
The `after` runs after the request has resolved and any body interface methods
have been called. It has the opportunity to read the `gretchen` response. It
_cannot_ modify it. This is mostly useful for logging.

```js
const response = await gretch('/api/user/12', {
hooks: {
after ({ url, status, data, error }) {
sentry.captureMessage(`${url} returned ${status}`)
}
after: [
({ url, status, data, error }, options) => {
sentry.captureMessage(`${url} returned ${status}`)
}
]
}
}).json()
```
Expand Down
25 changes: 20 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ import {
} from './lib/handleRetry'
import { handleTimeout } from './lib/handleTimeout'
import { normalizeURL } from './lib/utils'
import { merge } from './lib/merge'

export type DefaultGretchResponse = any
export type DefaultGretchError = any

export type MergeableObject =
| {
[k: string]: MergeableObject
}
| Partial<GretchOptions>
| any[]

export type GretchResponse<T = DefaultGretchResponse, A = DefaultGretchError> =
| {
url: string
Expand All @@ -26,9 +34,15 @@ export type GretchResponse<T = DefaultGretchResponse, A = DefaultGretchError> =
response: Response
}

export type GretchBeforeHook = (request: Request, opts: GretchOptions) => void
export type GretchAfterHook = (
response: GretchResponse,
opts: GretchOptions
) => void

export type GretchHooks = {
before?: (request: Request, opts: GretchOptions) => void
after?: (response: GretchResponse, opts: GretchOptions) => void
before?: GretchBeforeHook | GretchBeforeHook[]
after?: GretchAfterHook | GretchAfterHook[]
}

export type GretchOptions = {
Expand Down Expand Up @@ -89,7 +103,7 @@ export function gretch<T = DefaultGretchResponse, A = DefaultGretchError> (
baseURL !== undefined ? normalizeURL(url, { baseURL }) : url
const request = new Request(normalizedURL, options)

if (hooks.before) hooks.before(request, opts)
if (hooks.before) [].concat(hooks.before).forEach(hook => hook(request, opts))

const fetcher = () =>
timeout
Expand Down Expand Up @@ -144,7 +158,8 @@ export function gretch<T = DefaultGretchResponse, A = DefaultGretchError> (
response
}

if (hooks.after) hooks.after(res, opts)
if (hooks.after)
[].concat(hooks.after).forEach(hook => hook({ ...res }, opts))

return res
}
Expand All @@ -158,6 +173,6 @@ export function create (defaultOpts: GretchOptions = {}) {
T = DefaultGretchResponse,
A = DefaultGretchError
> (url: string, opts: GretchOptions = {}): GretchInstance<T, A> {
return gretch(url, { ...defaultOpts, ...opts })
return gretch(url, merge(defaultOpts, opts))
}
}
41 changes: 41 additions & 0 deletions lib/merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { GretchOptions, MergeableObject } from '../index'

function headersToObj (headers: Headers) {
let o = {}

headers.forEach((v, k) => {
o[k] = v
})

return o
}

export function merge (
a: MergeableObject = {},
b: MergeableObject = {}
): GretchOptions {
let c = { ...a }

for (const k of Object.keys(b)) {
const v = b[k]

if (typeof v === 'object') {
if (k === 'headers') {
c[k] = merge(
headersToObj(new Headers(a[k])),
headersToObj(new Headers(v))
)
} else if (v.pop && a[k].pop) {
c[k] = [...(a[k] || []), ...v]
} else if (typeof a[k] === 'object' && !a[k].pop) {
c[k] = merge(a[k], v)
} else {
c[k] = v
}
} else {
c[k] = v
}
}

return c
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "1.2.0",
"description": "Making fetch happen in Typescript.",
"source": "index.ts",
"main": "dist/gretchen.cjs.js",
"modern": "dist/gretchen.esm.js",
"main": "dist/gretchen.js",
"modern": "dist/gretchen.modern.js",
"module": "dist/gretchen.esm.js",
"unpkg": "dist/gretchen.iife.js",
"types": "dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require('./utils.test').default(test, assert)
require('./handleRetry.test').default(test, assert)
require('./handleTimeout.test').default(test, assert)
require('./index.test').default(test, assert)
require('./merge.test').default(test, assert)

process.on('unhandledRejection', e => {
console.error(e)
Expand Down
19 changes: 14 additions & 5 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,16 +267,25 @@ export default (test, assert) => {
await gretch(`http://127.0.0.1:${port}`, {
timeout: 50000,
hooks: {
before (request) {
before (request, opts) {
assert(request.url)
assert(opts.timeout)
hooks++
},
after ({ status }) {
hooks++
}
after: [
(response, opts) => {
assert(response.status)
assert(opts.timeout)
hooks++
},
() => {
hooks++
}
]
}
}).json()

assert(hooks === 2)
assert(hooks === 3)

server.close()

Expand Down
110 changes: 110 additions & 0 deletions test/merge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { merge } from '../lib/merge'

export default (test, assert) => {
test('merges primitives', () => {
const o = merge(
{
str: 'in',
bool: false,
int: 0,
arr: ['in'],
obj: {
prop: 'in'
}
},
{
str: 'out',
bool: true,
int: 1,
arr: ['out'],
obj: {
prop: 'out'
}
}
)

assert.equal(o.str, 'out')
assert.equal(o.bool, true)
assert.equal(o.int, 1)
assert.deepEqual(o.arr, ['in', 'out'])
assert.equal(o.obj.prop, 'out')
})

test('merges headers', () => {
const o = merge(
{
headers: new Headers({
'X-In': 'in',
'X-Header': 'in'
})
},
{
headers: {
'X-Out': 'out',
'X-Header': 'out'
}
}
)

assert.equal(o.headers['x-header'], 'out')
assert.equal(o.headers['x-in'], 'in')
assert.equal(o.headers['x-out'], 'out')
})

test('overwrites mixed values', () => {
const o = merge(
{
timeout: 100,
retry: false,
hooks: {
after () {}
}
},
{
timeout: 200,
retry: {
attempts: 3
},
hooks: {
after: [() => {}]
}
}
)

assert.equal(o.timeout, 200)
// @ts-ignore
assert.equal(o.retry.attempts, 3)
assert(Array.isArray(o.hooks.after))
})

test('merges hooks', () => {
const o = merge(
{
hooks: {
before () {}
}
},
{
hooks: {
after () {}
}
}
)

assert(typeof o.hooks.before === 'function')
assert(typeof o.hooks.after === 'function')
})

test('clones reference object', () => {
const defaults = {
prop: 'default'
}

const o = merge(defaults, {
prop: 'out'
})

assert.equal(defaults.prop, 'default')
assert.equal(o.prop, 'out')
})
}

0 comments on commit 89c72c9

Please sign in to comment.