From 07e9b9eaf8c4f0c75459c550a8e20c32a12d801a Mon Sep 17 00:00:00 2001 From: estrattonbailey Date: Thu, 24 Jun 2021 16:57:33 -0500 Subject: [PATCH 1/3] fix: merge options Avoids accidental overwrites when using instances and augmenting functionality at call-time. --- index.ts | 10 ++++- lib/merge.ts | 39 +++++++++++++++++ test/index.js | 1 + test/merge.test.ts | 102 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 lib/merge.ts create mode 100644 test/merge.test.ts diff --git a/index.ts b/index.ts index a757b31..2b10bbf 100644 --- a/index.ts +++ b/index.ts @@ -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 + | any[] + export type GretchResponse = | { url: string @@ -158,6 +166,6 @@ export function create (defaultOpts: GretchOptions = {}) { T = DefaultGretchResponse, A = DefaultGretchError > (url: string, opts: GretchOptions = {}): GretchInstance { - return gretch(url, { ...defaultOpts, ...opts }) + return gretch(url, merge(defaultOpts, opts)) } } diff --git a/lib/merge.ts b/lib/merge.ts new file mode 100644 index 0000000..a10534e --- /dev/null +++ b/lib/merge.ts @@ -0,0 +1,39 @@ +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) { + c[k] = [...(a[k] || []), ...v] + } else { + c[k] = merge(a[k], v) + } + } else { + c[k] = v + } + } + + return c +} diff --git a/test/index.js b/test/index.js index 61370c7..545ae87 100644 --- a/test/index.js +++ b/test/index.js @@ -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) diff --git a/test/merge.test.ts b/test/merge.test.ts new file mode 100644 index 0000000..bbb0893 --- /dev/null +++ b/test/merge.test.ts @@ -0,0 +1,102 @@ +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 values', () => { + const o = merge( + { + timeout: 100, + retry: { + attempts: 3 + } + }, + { + timeout: 200, + retry: false + } + ) + + assert.equal(o.timeout, 200) + assert.equal(o.retry, false) + }) + + 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') + }) +} From 0e0550da701672983508f97f3575168b5e796e3f Mon Sep 17 00:00:00 2001 From: estrattonbailey Date: Thu, 24 Jun 2021 17:20:58 -0500 Subject: [PATCH 2/3] feat(hooks): support multiple hooks Provides optional array syntax when defining hooks, which allows users to define more than one before or after hook. --- README.md | 23 ++++++++++++++++------- index.ts | 15 +++++++++++---- lib/merge.ts | 6 ++++-- test/index.test.ts | 19 ++++++++++++++----- test/merge.test.ts | 18 +++++++++++++----- 5 files changed, 58 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 8f44eae..f37d6d1 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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() ``` diff --git a/index.ts b/index.ts index 2b10bbf..10bd751 100644 --- a/index.ts +++ b/index.ts @@ -34,9 +34,15 @@ export type GretchResponse = 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 = { @@ -97,7 +103,7 @@ export function gretch ( 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 @@ -152,7 +158,8 @@ export function gretch ( response } - if (hooks.after) hooks.after(res, opts) + if (hooks.after) + [].concat(hooks.after).forEach(hook => hook({ ...res }, opts)) return res } diff --git a/lib/merge.ts b/lib/merge.ts index a10534e..a08bf2c 100644 --- a/lib/merge.ts +++ b/lib/merge.ts @@ -25,10 +25,12 @@ export function merge ( headersToObj(new Headers(a[k])), headersToObj(new Headers(v)) ) - } else if (v.pop) { + } else if (v.pop && a[k].pop) { c[k] = [...(a[k] || []), ...v] - } else { + } else if (typeof a[k] === 'object' && !a[k].pop) { c[k] = merge(a[k], v) + } else { + c[k] = v } } else { c[k] = v diff --git a/test/index.test.ts b/test/index.test.ts index fbe285d..0685c49 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -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() diff --git a/test/merge.test.ts b/test/merge.test.ts index bbb0893..e43a3a0 100644 --- a/test/merge.test.ts +++ b/test/merge.test.ts @@ -51,22 +51,30 @@ export default (test, assert) => { assert.equal(o.headers['x-out'], 'out') }) - test('overwrites values', () => { + test('overwrites mixed values', () => { const o = merge( { timeout: 100, - retry: { - attempts: 3 + retry: false, + hooks: { + after () {} } }, { timeout: 200, - retry: false + retry: { + attempts: 3 + }, + hooks: { + after: [() => {}] + } } ) assert.equal(o.timeout, 200) - assert.equal(o.retry, false) + // @ts-ignore + assert.equal(o.retry.attempts, 3) + assert(Array.isArray(o.hooks.after)) }) test('merges hooks', () => { From f4211e5b171f63116ed1f07c98f671dfcd225fff Mon Sep 17 00:00:00 2001 From: estrattonbailey Date: Thu, 19 Aug 2021 09:27:45 -0500 Subject: [PATCH 3/3] fix: cjs export --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bd2fde4..236bd75 100644 --- a/package.json +++ b/package.json @@ -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",