diff --git a/src/RRNLError.js b/src/RRNLError.ts similarity index 90% rename from src/RRNLError.js rename to src/RRNLError.ts index d158587..743f590 100644 --- a/src/RRNLError.js +++ b/src/RRNLError.ts @@ -1,8 +1,7 @@ -/* @flow */ - export default class RRNLError extends Error { constructor(msg: string) { super(msg); this.name = 'RRNLError'; } -} + +} \ No newline at end of file diff --git a/src/RelayNetworkLayer.js b/src/RelayNetworkLayer.js deleted file mode 100644 index 016583f..0000000 --- a/src/RelayNetworkLayer.js +++ /dev/null @@ -1,106 +0,0 @@ -/* @flow */ - -import { Network } from 'relay-runtime'; -import RelayRequest from './RelayRequest'; -import fetchWithMiddleware from './fetchWithMiddleware'; -import type { - Middleware, - MiddlewareSync, - MiddlewareRaw, - FetchFunction, - FetchHookFunction, - SubscribeFunction, - RNLExecuteFunction, -} from './definition'; - -export type RelayNetworkLayerOpts = {| - subscribeFn?: SubscribeFunction, - beforeFetch?: FetchHookFunction, - noThrow?: boolean, -|}; - -export default class RelayNetworkLayer { - _middlewares: Middleware[]; - _rawMiddlewares: MiddlewareRaw[]; - _middlewaresSync: RNLExecuteFunction[]; - execute: RNLExecuteFunction; - executeWithEvents: any; - +fetchFn: FetchFunction; - +subscribeFn: ?SubscribeFunction; - +noThrow: boolean; - - constructor( - middlewares: Array, - opts?: RelayNetworkLayerOpts - ) { - this._middlewares = []; - this._rawMiddlewares = []; - this._middlewaresSync = []; - this.noThrow = false; - - const mws = Array.isArray(middlewares) ? (middlewares: any) : [middlewares]; - mws.forEach((mw) => { - if (mw) { - if (mw.execute) { - this._middlewaresSync.push(mw.execute); - } else if (mw.isRawMiddleware) { - this._rawMiddlewares.push(mw); - } else { - this._middlewares.push(mw); - } - } - }); - - if (opts) { - this.subscribeFn = opts.subscribeFn; - this.noThrow = opts.noThrow === true; - - // TODO deprecate - if (opts.beforeFetch) { - this._middlewaresSync.push((opts.beforeFetch: any)); - } - } - - this.fetchFn = (operation, variables, cacheConfig, uploadables) => { - for (let i = 0; i < this._middlewaresSync.length; i++) { - const res = this._middlewaresSync[i](operation, variables, cacheConfig, uploadables); - if (res) return res; - } - - return { - subscribe: (sink) => { - const req = new RelayRequest(operation, variables, cacheConfig, uploadables); - const res = fetchWithMiddleware( - req, - this._middlewares, - this._rawMiddlewares, - this.noThrow - ); - - res - .then( - (value) => { - sink.next(value); - sink.complete(); - }, - (error) => { - if (error && error.name && error.name === 'AbortError') { - sink.complete(); - } else sink.error(error); - } - ) - // avoid unhandled promise rejection error - .catch(() => {}); - - return () => { - req.cancel(); - }; - }, - }; - }; - - const network = Network.create(this.fetchFn, this.subscribeFn); - this.execute = network.execute; - this.executeWithEvents = network.executeWithEvents; - } -} diff --git a/src/RelayNetworkLayer.ts b/src/RelayNetworkLayer.ts new file mode 100644 index 0000000..c4cf3ad --- /dev/null +++ b/src/RelayNetworkLayer.ts @@ -0,0 +1,80 @@ +import { Network } from "relay-runtime"; +import RelayRequest from "./RelayRequest"; +import fetchWithMiddleware from "./fetchWithMiddleware"; +import type { Middleware, MiddlewareSync, MiddlewareRaw, FetchFunction, FetchHookFunction, SubscribeFunction, RNLExecuteFunction } from "./definition"; +export type RelayNetworkLayerOpts = { + subscribeFn?: SubscribeFunction; + beforeFetch?: FetchHookFunction; + noThrow?: boolean; +}; +export default class RelayNetworkLayer { + _middlewares: Middleware[]; + _rawMiddlewares: MiddlewareRaw[]; + _middlewaresSync: RNLExecuteFunction[]; + execute: RNLExecuteFunction; + executeWithEvents: any; + readonly fetchFn: FetchFunction; + readonly subscribeFn: SubscribeFunction | null | undefined; + readonly noThrow: boolean; + + constructor(middlewares: Array<(Middleware | null | undefined) | MiddlewareSync | MiddlewareRaw>, opts?: RelayNetworkLayerOpts) { + this._middlewares = []; + this._rawMiddlewares = []; + this._middlewaresSync = []; + this.noThrow = false; + const mws = Array.isArray(middlewares) ? (middlewares as any) : [middlewares]; + mws.forEach(mw => { + if (mw) { + if (mw.execute) { + this._middlewaresSync.push(mw.execute); + } else if (mw.isRawMiddleware) { + this._rawMiddlewares.push(mw); + } else { + this._middlewares.push(mw); + } + } + }); + + if (opts) { + this.subscribeFn = opts.subscribeFn; + this.noThrow = opts.noThrow === true; + + // TODO deprecate + if (opts.beforeFetch) { + this._middlewaresSync.push((opts.beforeFetch as any)); + } + } + + this.fetchFn = (operation, variables, cacheConfig, uploadables) => { + for (let i = 0; i < this._middlewaresSync.length; i++) { + const res = this._middlewaresSync[i](operation, variables, cacheConfig, uploadables); + + if (res) return res; + } + + return { + subscribe: sink => { + const req = new RelayRequest(operation, variables, cacheConfig, uploadables); + const res = fetchWithMiddleware(req, this._middlewares, this._rawMiddlewares, this.noThrow); + res.then(value => { + sink.next(value); + sink.complete(); + }, error => { + if (error && error.name && error.name === 'AbortError') { + sink.complete(); + } else sink.error(error); + }) // avoid unhandled promise rejection error + .catch(() => {}); + return () => { + req.cancel(); + }; + } + }; + }; + + const network = Network.create(this.fetchFn, this.subscribeFn); + this.execute = network.execute; + this.executeWithEvents = network.executeWithEvents; + } + +} \ No newline at end of file diff --git a/src/RelayRequest.js b/src/RelayRequest.ts similarity index 70% rename from src/RelayRequest.js rename to src/RelayRequest.ts index 8bbd244..cff22d8 100644 --- a/src/RelayRequest.js +++ b/src/RelayRequest.ts @@ -1,46 +1,34 @@ -/* @flow */ +import { Class } from "utility-types"; +import type { ConcreteBatch, Variables, CacheConfig, UploadableMap, FetchOpts } from "./definition"; +import RRNLError from "./RRNLError"; -import type { ConcreteBatch, Variables, CacheConfig, UploadableMap, FetchOpts } from './definition'; -import RRNLError from './RRNLError'; - -function getFormDataInterface(): ?Class { - return (typeof window !== 'undefined' && window.FormData) || (global && global.FormData); +function getFormDataInterface(): Class | null | undefined { + return typeof window !== 'undefined' && window.FormData || global && global.FormData; } export default class RelayRequest { static lastGenId: number; id: string; fetchOpts: FetchOpts; - operation: ConcreteBatch; variables: Variables; cacheConfig: CacheConfig; - uploadables: ?UploadableMap; - controller: ?window.AbortController; - - constructor( - operation: ConcreteBatch, - variables: Variables, - cacheConfig: CacheConfig, - uploadables?: ?UploadableMap - ) { + uploadables: UploadableMap | null | undefined; + controller: window.AbortController | null | undefined; + + constructor(operation: ConcreteBatch, variables: Variables, cacheConfig: CacheConfig, uploadables?: UploadableMap | null | undefined) { this.operation = operation; this.variables = variables; this.cacheConfig = cacheConfig; this.uploadables = uploadables; - this.id = this.operation.id || this.operation.name || this._generateID(); - const fetchOpts: FetchOpts = { method: 'POST', headers: {}, - body: this.prepareBody(), + body: this.prepareBody() }; - - this.controller = - typeof window !== 'undefined' && window.AbortController ? new window.AbortController() : null; + this.controller = typeof window !== 'undefined' && window.AbortController ? new window.AbortController() : null; if (this.controller) fetchOpts.signal = this.controller.signal; - this.fetchOpts = fetchOpts; } @@ -49,9 +37,13 @@ export default class RelayRequest { } prepareBody(): string | FormData { - const { uploadables } = this; + const { + uploadables + } = this; + if (uploadables) { const _FormData_ = getFormDataInterface(); + if (!_FormData_) { throw new RRNLError('Uploading files without `FormData` interface does not supported.'); } @@ -60,20 +52,18 @@ export default class RelayRequest { formData.append('id', this.getID()); formData.append('query', this.getQueryString()); formData.append('variables', JSON.stringify(this.getVariables())); - - Object.keys(uploadables).forEach((key) => { + Object.keys(uploadables).forEach(key => { if (Object.prototype.hasOwnProperty.call(uploadables, key)) { formData.append(key, uploadables[key]); } }); - return formData; } return JSON.stringify({ id: this.getID(), query: this.getQueryString(), - variables: this.getVariables(), + variables: this.getVariables() }); } @@ -85,6 +75,7 @@ export default class RelayRequest { if (!this.constructor.lastGenId) { this.constructor.lastGenId = 0; } + this.constructor.lastGenId += 1; return this.constructor.lastGenId.toString(); } @@ -103,6 +94,7 @@ export default class RelayRequest { isFormData(): boolean { const _FormData_ = getFormDataInterface(); + return !!_FormData_ && this.fetchOpts.body instanceof _FormData_; } @@ -111,14 +103,17 @@ export default class RelayRequest { this.controller.abort(); return true; } + return false; } clone(): RelayRequest { - // $FlowFixMe const newRequest = Object.assign(Object.create(Object.getPrototypeOf(this)), this); - newRequest.fetchOpts = { ...this.fetchOpts }; - newRequest.fetchOpts.headers = { ...this.fetchOpts.headers }; - return (newRequest: any); + newRequest.fetchOpts = { ...this.fetchOpts + }; + newRequest.fetchOpts.headers = { ...this.fetchOpts.headers + }; + return (newRequest as any); } + } diff --git a/src/RelayRequestBatch.js b/src/RelayRequestBatch.ts similarity index 58% rename from src/RelayRequestBatch.js rename to src/RelayRequestBatch.ts index bb80c06..bd518f0 100644 --- a/src/RelayRequestBatch.js +++ b/src/RelayRequestBatch.ts @@ -1,11 +1,8 @@ -/* @flow */ - -import type { FetchOpts, Variables } from './definition'; -import type RelayRequest from './RelayRequest'; -import RRNLError from './RRNLError'; - +import { $Shape } from "utility-types"; +import type { FetchOpts, Variables } from "./definition"; +import type RelayRequest from "./RelayRequest"; +import RRNLError from "./RRNLError"; export type Requests = RelayRequest[]; - export default class RelayRequestBatch { fetchOpts: $Shape; requests: Requests; @@ -15,18 +12,17 @@ export default class RelayRequestBatch { this.fetchOpts = { method: 'POST', headers: {}, - body: this.prepareBody(), + body: this.prepareBody() }; } - setFetchOption(name: string, value: mixed) { + setFetchOption(name: string, value: unknown) { this.fetchOpts[name] = value; } - setFetchOptions(opts: Object) { - this.fetchOpts = { - ...this.fetchOpts, - ...opts, + setFetchOptions(opts: Record) { + this.fetchOpts = { ...this.fetchOpts, + ...opts }; } @@ -34,15 +30,16 @@ export default class RelayRequestBatch { if (!this.fetchOpts.body) { this.fetchOpts.body = this.prepareBody(); } - return (this.fetchOpts.body: any) || ''; + + return (this.fetchOpts.body as any) || ''; } prepareBody(): string { - return `[${this.requests.map((r) => r.getBody()).join(',')}]`; + return `[${this.requests.map(r => r.getBody()).join(',')}]`; } getIds(): string[] { - return this.requests.map((r) => r.getID()); + return this.requests.map(r => r.getID()); } getID(): string { @@ -58,11 +55,12 @@ export default class RelayRequestBatch { } clone(): RelayRequestBatch { - // $FlowFixMe const newRequest = Object.assign(Object.create(Object.getPrototypeOf(this)), this); - newRequest.fetchOpts = { ...this.fetchOpts }; - newRequest.fetchOpts.headers = { ...this.fetchOpts.headers }; - return (newRequest: any); + newRequest.fetchOpts = { ...this.fetchOpts + }; + newRequest.fetchOpts.headers = { ...this.fetchOpts.headers + }; + return (newRequest as any); } getVariables(): Variables { @@ -72,4 +70,5 @@ export default class RelayRequestBatch { getQueryString(): string { return this.prepareBody(); } + } diff --git a/src/RelayResponse.js b/src/RelayResponse.ts similarity index 53% rename from src/RelayResponse.js rename to src/RelayResponse.ts index 63ee5b3..ff8a30b 100644 --- a/src/RelayResponse.js +++ b/src/RelayResponse.ts @@ -1,20 +1,16 @@ -/* @flow */ - -import type { FetchResponse, GraphQLResponseErrors, PayloadData } from './definition'; - +import type { FetchResponse, GraphQLResponseErrors, PayloadData } from "./definition"; export default class RelayResponse { _res: any; // response from low-level method, eg. fetch - data: ?PayloadData; - errors: ?GraphQLResponseErrors; - + data: PayloadData | null | undefined; + errors: GraphQLResponseErrors | null | undefined; ok: any; status: number; - statusText: ?string; - headers: ?{ [name: string]: string }; - url: ?string; - text: ?string; - json: mixed; + statusText: string | null | undefined; + headers: Record | null | undefined; + url: string | null | undefined; + text: string | null | undefined; + json: unknown; static async createFromFetch(res: FetchResponse): Promise { const r = new RelayResponse(); @@ -33,38 +29,34 @@ export default class RelayResponse { return r; } - static async createFromGraphQL(res: { errors?: any, data?: any }) { + static async createFromGraphQL(res: { + errors?: any; + data?: any; + }) { const r = new RelayResponse(); r._res = res; r.ok = true; r.status = 200; r.data = res.data; r.errors = res.errors; - return r; } - processJsonData(json: mixed) { + processJsonData(json: unknown) { this.json = json; + if (json) { - if (json.data) this.data = (json.data: any); - if (json.errors) this.errors = (json.errors: any); + if (json.data) this.data = (json.data as any); + if (json.errors) this.errors = (json.errors as any); } } clone(): RelayResponse { - // $FlowFixMe return Object.assign(Object.create(Object.getPrototypeOf(this)), this); } toString(): string { - return [ - `Response:`, - ` Url: ${this.url || ''}`, - ` Status code: ${this.status || ''}`, - ` Status text: ${this.statusText || ''}`, - ` Response headers: ${JSON.stringify(this.headers) || ''}`, - ` Response body: ${JSON.stringify(this.json) || ''}`, - ].join('\n'); + return [`Response:`, ` Url: ${this.url || ''}`, ` Status code: ${this.status || ''}`, ` Status text: ${this.statusText || ''}`, ` Response headers: ${JSON.stringify(this.headers) || ''}`, ` Response body: ${JSON.stringify(this.json) || ''}`].join('\n'); } + } diff --git a/src/__mocks__/mockReq.js b/src/__mocks__/mockReq.ts similarity index 72% rename from src/__mocks__/mockReq.js rename to src/__mocks__/mockReq.ts index 22ab611..5e64fab 100644 --- a/src/__mocks__/mockReq.js +++ b/src/__mocks__/mockReq.ts @@ -1,25 +1,21 @@ -/* @flow */ /* eslint-disable import/prefer-default-export, no-param-reassign */ - -import type RelayNetworkLayer from '../RelayNetworkLayer'; -import type RelayResponse from '../RelayResponse'; -import type { CacheConfig } from '../definition'; - +import type RelayNetworkLayer from "../RelayNetworkLayer"; +import type RelayResponse from "../RelayResponse"; +import type { CacheConfig } from "../definition"; type ReqData = { - query?: string, - variables?: Object, - cacheConfig?: CacheConfig, - files?: any, + query?: string; + variables?: Record; + cacheConfig?: CacheConfig; + files?: any; }; - type ReqId = string; class MockReq { reqid: ReqId; reqData: ReqData; error: Error; - payload: Object; - cacheConfig: Object; + payload: Record; + cacheConfig: Record; constructor(reqid?: ReqId, reqData?: ReqData = {}) { this.reqid = reqid || Math.random().toString(); @@ -38,7 +34,7 @@ class MockReq { return `debugname${this.reqid}`; } - getVariables(): Object { + getVariables(): Record { return this.reqData.variables || {}; } @@ -50,7 +46,7 @@ class MockReq { this.error = err; } - resolve(resp: Object) { + resolve(resp: Record) { this.payload = resp; } @@ -60,52 +56,54 @@ class MockReq { const operation = ({ id: this.getID(), text, - operationKind, - }: any); + operationKind + } as any); const variables = this.getVariables() || {}; const cacheConfig = this.reqData.cacheConfig || {}; const uploadables = this.getFiles(); - - const res = (rnl.fetchFn(operation, variables, cacheConfig, uploadables): any); - + const res = (rnl.fetchFn(operation, variables, cacheConfig, uploadables) as any); const promise = new Promise((resolve, reject) => { res.subscribe({ complete: () => {}, - error: (error) => reject(error), - next: (value) => resolve(value), + error: error => reject(error), + next: value => resolve(value) }); }); - // avoid unhandled rejection in tests promise.catch(() => {}); - // but allow to read rejected response return promise; } + } export function mockReq(reqid?: ReqId | number, data?: ReqData): MockReq { return new MockReq(reqid ? reqid.toString() : undefined, data); } - export function mockMutationReq(reqid?: ReqId | number, data?: ReqData): MockReq { return new MockReq(reqid ? reqid.toString() : undefined, { query: 'mutation {}', - ...data, + ...data }); } - export function mockFormDataReq(reqid?: ReqId | number, data?: ReqData): MockReq { return new MockReq(reqid ? reqid.toString() : undefined, { - files: { file1: 'data' }, - ...data, + files: { + file1: 'data' + }, + ...data }); } - export function mockReqWithSize(reqid: ReqId | number, size: number): MockReq { - return mockReq(reqid, { query: `{${'x'.repeat(size)}}` }); + return mockReq(reqid, { + query: `{${'x'.repeat(size)}}` + }); } - export function mockReqWithFiles(reqid: ReqId | number): MockReq { - return mockReq(reqid, { files: { file1: 'data', file2: 'data' } }); -} + return mockReq(reqid, { + files: { + file1: 'data', + file2: 'data' + } + }); +} \ No newline at end of file diff --git a/src/__tests__/RelayNetworkLayer-test.js b/src/__tests__/RelayNetworkLayer-test.ts similarity index 60% rename from src/__tests__/RelayNetworkLayer-test.js rename to src/__tests__/RelayNetworkLayer-test.ts index feb671a..1735da7 100644 --- a/src/__tests__/RelayNetworkLayer-test.js +++ b/src/__tests__/RelayNetworkLayer-test.ts @@ -1,123 +1,114 @@ -/* @flow */ - -import fetchMock from 'fetch-mock'; -import RelayNetworkLayer from '../RelayNetworkLayer'; - +import fetchMock from "fetch-mock"; +import RelayNetworkLayer from "../RelayNetworkLayer"; fetchMock.mock({ matcher: '*', response: { - data: {}, - }, + data: {} + } }); - const mockOperation: any = { kind: 'Batch', - fragment: {}, + fragment: {} }; - describe('RelayNetworkLayer', () => { it('should call middlewares', async () => { - const mw1: any = jest.fn((next) => next); - const mw2: any = jest.fn((next) => next); - + const mw1: any = jest.fn(next => next); + const mw2: any = jest.fn(next => next); const network = new RelayNetworkLayer([null, mw1, undefined, mw2]); - await (network.execute(mockOperation, {}, {}): any).toPromise(); + await (network.execute(mockOperation, {}, {}) as any).toPromise(); expect(mw1).toHaveBeenCalled(); expect(mw2).toHaveBeenCalled(); }); - describe('sync middleware', () => { it('should return payload from sync middleware, without calling async middlewares', async () => { - const asyncMW: any = jest.fn((next) => next); - + const asyncMW: any = jest.fn(next => next); const syncMW = { - execute: () => ({ data: {} }), + execute: () => ({ + data: {} + }) }; const network = new RelayNetworkLayer([syncMW, asyncMW]); - await (network.execute(mockOperation, {}, {}): any).toPromise(); + await (network.execute(mockOperation, {}, {}) as any).toPromise(); expect(asyncMW).not.toHaveBeenCalled(); }); - it('should call async middlewares, if sync middleware returns undefined', async () => { - const asyncMW: any = jest.fn((next) => next); - + const asyncMW: any = jest.fn(next => next); const syncMW = { - execute: () => undefined, + execute: () => undefined }; - const network = new RelayNetworkLayer([syncMW, asyncMW]); - await (network.execute(mockOperation, {}, {}): any).toPromise(); + await (network.execute(mockOperation, {}, {}) as any).toPromise(); expect(asyncMW).toHaveBeenCalled(); }); }); - describe('beforeFetch option', () => { it('should return payload from beforeFetch, without calling async middlewares', async () => { - const asyncMW: any = jest.fn((next) => next); - + const asyncMW: any = jest.fn(next => next); const network = new RelayNetworkLayer([asyncMW], { - beforeFetch: () => ({ data: {} }), + beforeFetch: () => ({ + data: {} + }) }); - await (network.execute(mockOperation, {}, {}): any).toPromise(); + await (network.execute(mockOperation, {}, {}) as any).toPromise(); expect(asyncMW).not.toHaveBeenCalled(); }); - it('should call async middlewares, if beforeFetch returns undefined', async () => { - const asyncMW: any = jest.fn((next) => next); - + const asyncMW: any = jest.fn(next => next); const network = new RelayNetworkLayer([asyncMW], { - beforeFetch: () => undefined, + beforeFetch: () => undefined }); - await (network.execute(mockOperation, {}, {}): any).toPromise(); + await (network.execute(mockOperation, {}, {}) as any).toPromise(); expect(asyncMW).toHaveBeenCalled(); }); }); - it('should correctly call raw middlewares', async () => { fetchMock.mock({ matcher: '/graphql', response: { status: 200, body: { - data: { text: 'response' }, + data: { + text: 'response' + } }, - sendAsJson: true, + sendAsJson: true }, - method: 'POST', + method: 'POST' }); - const regularMiddleware: any = (next) => async (req) => { - (req: any).fetchOpts.headers.reqId += ':regular'; + const regularMiddleware: any = next => async req => { + (req as any).fetchOpts.headers.reqId += ':regular'; const res: any = await next(req); res.data.text += ':regular'; return res; }; const createRawMiddleware = (id: number): any => { - const rawMiddleware = (next) => async (req) => { - (req: any).fetchOpts.headers.reqId += `:raw${id}`; + const rawMiddleware = next => async req => { + (req as any).fetchOpts.headers.reqId += `:raw${id}`; const res: any = await next(req); const parentJsonFN = res.json; + res.json = async () => { const json = await parentJsonFN.bind(res)(); json.data.text += `:raw${id}`; return json; }; + return res; }; + rawMiddleware.isRawMiddleware = true; return rawMiddleware; }; // rawMiddlewares should be called the last - const network = new RelayNetworkLayer([ - createRawMiddleware(1), - createRawMiddleware(2), - regularMiddleware, - ]); + const network = new RelayNetworkLayer([createRawMiddleware(1), createRawMiddleware(2), regularMiddleware]); const observable: any = network.execute(mockOperation, {}, {}); const result = await observable.toPromise(); expect(fetchMock.lastOptions().headers.reqId).toEqual('undefined:regular:raw1:raw2'); - expect(result.data).toEqual({ text: 'undefined:raw2:raw1:regular' }); + expect(result.data).toEqual({ + text: 'undefined:raw2:raw1:regular' + }); }); -}); +}); \ No newline at end of file diff --git a/src/__tests__/fetchWithMiddleware-test.js b/src/__tests__/fetchWithMiddleware-test.ts similarity index 58% rename from src/__tests__/fetchWithMiddleware-test.js rename to src/__tests__/fetchWithMiddleware-test.ts index ae81dda..f44c8b2 100644 --- a/src/__tests__/fetchWithMiddleware-test.js +++ b/src/__tests__/fetchWithMiddleware-test.ts @@ -1,67 +1,67 @@ -/* @flow */ /* eslint-disable no-param-reassign */ - -import fetchMock from 'fetch-mock'; -import fetchWithMiddleware from '../fetchWithMiddleware'; -import RelayRequest from '../RelayRequest'; -import RelayResponse from '../RelayResponse'; - +import fetchMock from "fetch-mock"; +import fetchWithMiddleware from "../fetchWithMiddleware"; +import RelayRequest from "../RelayRequest"; +import RelayResponse from "../RelayResponse"; describe('fetchWithMiddleware', () => { beforeEach(() => { fetchMock.restore(); }); - it('should make a successfull request without middlewares', async () => { - fetchMock.post('/graphql', { id: 1, data: { user: 123 } }); - const req = new RelayRequest(({}: any), {}, {}, null); + fetchMock.post('/graphql', { + id: 1, + data: { + user: 123 + } + }); + const req = new RelayRequest(({} as any), {}, {}, null); const res = await fetchWithMiddleware(req, [], []); - expect(res.data).toEqual({ user: 123 }); + expect(res.data).toEqual({ + user: 123 + }); }); - it('should make a successfull request with middlewares', async () => { - const numPlus5 = (next) => async (req) => { - (req: any).fetchOpts.headers.reqId += ':mw1'; + const numPlus5 = next => async req => { + (req as any).fetchOpts.headers.reqId += ':mw1'; const res: any = await next(req); res.data.text += ':mw1'; return res; }; - const numMultiply10 = (next) => async (req) => { - (req: any).fetchOpts.headers.reqId += ':mw2'; + + const numMultiply10 = next => async req => { + (req as any).fetchOpts.headers.reqId += ':mw2'; const res: any = await next(req); res.data.text += ':mw2'; return res; }; - fetchMock.post('/graphql', { id: 1, data: { text: 'response' } }); - const req = new RelayRequest(({}: any), {}, {}, null); + fetchMock.post('/graphql', { + id: 1, + data: { + text: 'response' + } + }); + const req = new RelayRequest(({} as any), {}, {}, null); req.fetchOpts.headers = { - reqId: 'request', + reqId: 'request' }; - - const res: any = await fetchWithMiddleware( - req, - [ - numPlus5, - numMultiply10, // should be last, when changing request - // should be first, when changing response - ], - [] - ); + const res: any = await fetchWithMiddleware(req, [numPlus5, numMultiply10 // should be last, when changing request + // should be first, when changing response + ], []); expect(res.data.text).toEqual('response:mw2:mw1'); expect(fetchMock.lastOptions().headers.reqId).toEqual('request:mw1:mw2'); }); - it('should fail correctly on network failure', async () => { fetchMock.mock({ matcher: '/graphql', response: { - throws: new Error('Network connection error'), + throws: new Error('Network connection error') }, - method: 'POST', + method: 'POST' }); - const req = new RelayRequest(({}: any), {}, {}, null); - + const req = new RelayRequest(({} as any), {}, {}, null); expect.assertions(2); + try { await fetchWithMiddleware(req, [], []); } catch (e) { @@ -69,22 +69,23 @@ describe('fetchWithMiddleware', () => { expect(e.toString()).toMatch('Network connection error'); } }); - it('should handle error response', async () => { fetchMock.mock({ matcher: '/graphql', response: { status: 200, body: { - errors: [{ location: 1, message: 'major error' }], - }, + errors: [{ + location: 1, + message: 'major error' + }] + } }, - method: 'POST', + method: 'POST' }); - - const req = new RelayRequest(({}: any), {}, {}, null); - + const req = new RelayRequest(({} as any), {}, {}, null); expect.assertions(2); + try { await fetchWithMiddleware(req, [], []); } catch (e) { @@ -92,39 +93,40 @@ describe('fetchWithMiddleware', () => { expect(e.toString()).toMatch('major error'); } }); - it('should not throw if noThrow set', async () => { fetchMock.mock({ matcher: '/graphql', response: { status: 200, body: { - errors: [{ location: 1, message: 'major error' }], - }, + errors: [{ + location: 1, + message: 'major error' + }] + } }, - method: 'POST', + method: 'POST' }); - - const req = new RelayRequest(({}: any), {}, {}, null); - + const req = new RelayRequest(({} as any), {}, {}, null); expect.assertions(1); const res = await fetchWithMiddleware(req, [], [], true); - expect(res.errors).toEqual([{ location: 1, message: 'major error' }]); + expect(res.errors).toEqual([{ + location: 1, + message: 'major error' + }]); }); - it('should handle server non-2xx errors', async () => { fetchMock.mock({ matcher: '/graphql', response: { status: 500, - body: 'Something went completely wrong.', + body: 'Something went completely wrong.' }, - method: 'POST', + method: 'POST' }); - - const req = new RelayRequest(({}: any), {}, {}, null); - + const req = new RelayRequest(({} as any), {}, {}, null); expect.assertions(2); + try { await fetchWithMiddleware(req, [], []); } catch (e) { @@ -132,21 +134,19 @@ describe('fetchWithMiddleware', () => { expect(e.toString()).toMatch('Something went completely wrong'); } }); - it('should fail on missing `data` property', async () => { fetchMock.mock({ matcher: '/graphql', response: { status: 200, body: {}, - sendAsJson: true, + sendAsJson: true }, - method: 'POST', + method: 'POST' }); - - const req = new RelayRequest(({}: any), {}, {}, null); - + const req = new RelayRequest(({} as any), {}, {}, null); expect.assertions(2); + try { await fetchWithMiddleware(req, [], []); } catch (e) { @@ -154,16 +154,16 @@ describe('fetchWithMiddleware', () => { expect(e.toString()).toMatch('Server return empty response.data'); } }); - it('should fail correctly with a response from a middleware cache', async () => { - const middleware = () => async () => - RelayResponse.createFromGraphQL({ - errors: [{ message: 'A GraphQL error occurred' }], - }); - - const req = new RelayRequest(({}: any), {}, {}, null); + const middleware = () => async () => RelayResponse.createFromGraphQL({ + errors: [{ + message: 'A GraphQL error occurred' + }] + }); + const req = new RelayRequest(({} as any), {}, {}, null); expect.hasAssertions(); + try { await fetchWithMiddleware(req, [middleware], []); } catch (e) { @@ -171,4 +171,4 @@ describe('fetchWithMiddleware', () => { expect(e.toString()).toMatch('A GraphQL error occurred'); } }); -}); +}); \ No newline at end of file diff --git a/src/createRequestError.js b/src/createRequestError.js deleted file mode 100644 index bd76ad3..0000000 --- a/src/createRequestError.js +++ /dev/null @@ -1,89 +0,0 @@ -/* @flow */ - -import RelayRequest from './RelayRequest'; -import RRNLError from './RRNLError'; -import type { GraphQLResponseErrors, RelayRequestAny } from './definition'; -import type RelayResponse from './RelayResponse'; - -export class RRNLRequestError extends RRNLError { - req: RelayRequestAny; - res: ?RelayResponse; - - constructor(msg: string) { - super(msg); - this.name = 'RRNLRequestError'; - } -} - -/** - * Formats an error response from GraphQL server request. - */ -export function formatGraphQLErrors(request: RelayRequest, errors: GraphQLResponseErrors): string { - const CONTEXT_BEFORE = 20; - const CONTEXT_LENGTH = 60; - - if (!request.getQueryString) { - return errors.join('\n'); - } - - let queryLines = []; - const queryString = request.getQueryString(); - if (queryString) { - // When using persisted query, queryString is an empty string. - queryLines = queryString.split('\n'); - } - - return errors - .map(({ locations, message }, ii) => { - const prefix = `${ii + 1}. `; - const indent = ' '.repeat(prefix.length); - - // custom errors thrown in graphql-server may not have locations - const locationMessage = - locations && queryLines.length - ? '\n' + - locations - .map(({ column, line }) => { - const queryLine = queryLines[line - 1]; - const offset = Math.min(column - 1, CONTEXT_BEFORE); - return [ - queryLine.substr(column - 1 - offset, CONTEXT_LENGTH), - `${' '.repeat(Math.max(offset, 0))}^^^`, - ] - .map((messageLine) => indent + messageLine) - .join('\n'); - }) - .join('\n') - : ''; - return prefix + message + locationMessage; - }) - .join('\n'); -} - -export function createRequestError(req: RelayRequestAny, res?: RelayResponse) { - let errorReason = ''; - - if (!res) { - errorReason = 'Server return empty response.'; - } else if (res.errors) { - if (req instanceof RelayRequest) { - errorReason = formatGraphQLErrors(req, res.errors); - } else { - errorReason = JSON.stringify(res.errors); - } - } else if (!res.json) { - errorReason = - (res.text ? res.text : `Server return empty response with Status Code: ${res.status}.`) + - (res ? `\n\n${res.toString()}` : ''); - } else if (!res.data) { - errorReason = 'Server return empty response.data.\n\n' + res.toString(); - } - - const error = new RRNLRequestError( - `Relay request for \`${req.getID()}\` failed by the following reasons:\n\n${errorReason}` - ); - - error.req = req; - error.res = res; - return error; -} diff --git a/src/createRequestError.ts b/src/createRequestError.ts new file mode 100644 index 0000000..e714eba --- /dev/null +++ b/src/createRequestError.ts @@ -0,0 +1,74 @@ +import RelayRequest from "./RelayRequest"; +import RRNLError from "./RRNLError"; +import type { GraphQLResponseErrors, RelayRequestAny } from "./definition"; +import type RelayResponse from "./RelayResponse"; +export class RRNLRequestError extends RRNLError { + req: RelayRequestAny; + res: RelayResponse | null | undefined; + + constructor(msg: string) { + super(msg); + this.name = 'RRNLRequestError'; + } + +} + +/** + * Formats an error response from GraphQL server request. + */ +export function formatGraphQLErrors(request: RelayRequest, errors: GraphQLResponseErrors): string { + const CONTEXT_BEFORE = 20; + const CONTEXT_LENGTH = 60; + + if (!request.getQueryString) { + return errors.join('\n'); + } + + let queryLines = []; + const queryString = request.getQueryString(); + + if (queryString) { + // When using persisted query, queryString is an empty string. + queryLines = queryString.split('\n'); + } + + return errors.map(({ + locations, + message + }, ii) => { + const prefix = `${ii + 1}. `; + const indent = ' '.repeat(prefix.length); + // custom errors thrown in graphql-server may not have locations + const locationMessage = locations && queryLines.length ? '\n' + locations.map(({ + column, + line + }) => { + const queryLine = queryLines[line - 1]; + const offset = Math.min(column - 1, CONTEXT_BEFORE); + return [queryLine.substr(column - 1 - offset, CONTEXT_LENGTH), `${' '.repeat(Math.max(offset, 0))}^^^`].map(messageLine => indent + messageLine).join('\n'); + }).join('\n') : ''; + return prefix + message + locationMessage; + }).join('\n'); +} +export function createRequestError(req: RelayRequestAny, res?: RelayResponse) { + let errorReason = ''; + + if (!res) { + errorReason = 'Server return empty response.'; + } else if (res.errors) { + if (req instanceof RelayRequest) { + errorReason = formatGraphQLErrors(req, res.errors); + } else { + errorReason = JSON.stringify(res.errors); + } + } else if (!res.json) { + errorReason = (res.text ? res.text : `Server return empty response with Status Code: ${res.status}.`) + (res ? `\n\n${res.toString()}` : ''); + } else if (!res.data) { + errorReason = 'Server return empty response.data.\n\n' + res.toString(); + } + + const error = new RRNLRequestError(`Relay request for \`${req.getID()}\` failed by the following reasons:\n\n${errorReason}`); + error.req = req; + error.res = res; + return error; +} \ No newline at end of file diff --git a/src/definition.js b/src/definition.js deleted file mode 100644 index 066923d..0000000 --- a/src/definition.js +++ /dev/null @@ -1,136 +0,0 @@ -/* @flow */ - -import type RelayRequest from './RelayRequest'; -import type RelayRequestBatch from './RelayRequestBatch'; -import type RelayResponse from './RelayResponse'; - -export type RelayRequestAny = RelayRequest | RelayRequestBatch; -export type MiddlewareNextFn = (req: RelayRequestAny) => Promise; -export type Middleware = (next: MiddlewareNextFn) => MiddlewareNextFn; -export type MiddlewareRawNextFn = (req: RelayRequestAny) => Promise; - -export type MiddlewareRaw = { - isRawMiddleware: true, - [[call]]: (next: MiddlewareRawNextFn) => MiddlewareRawNextFn, -}; - -export type MiddlewareSync = {| - execute: ( - operation: ConcreteBatch, - variables: Variables, - cacheConfig: CacheConfig, - uploadables: ?UploadableMap - ) => ?ObservableFromValue, -|}; - -export type FetchOpts = { - url?: string, - method: 'POST' | 'GET', - headers: { [name: string]: string }, - body: string | FormData, - // Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests - credentials?: 'same-origin' | 'include' | 'omit', - mode?: 'cors' | 'websocket' | 'navigate' | 'no-cors' | 'same-origin', - cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached', - redirect?: 'follow' | 'error' | 'manual', - signal?: window.AbortSignal, - [name: string]: mixed, -}; - -export type FetchResponse = Response; - -export type GraphQLResponseErrors = Array<{ - message: string, - locations?: Array<{ - column: number, - line: number, - }>, - stack?: Array, -}>; - -export type GraphQLResponse = { - data?: any, - errors?: GraphQLResponseErrors, -}; - -export type RRNLResponseObject = { - ok: any, - status: number, - statusText: string, - headers: { [name: string]: string }, - url: string, - payload: ?GraphQLResponse, -}; - -export type RNLExecuteFunction = ( - operation: ConcreteBatch, - variables: Variables, - cacheConfig: CacheConfig, - uploadables?: ?UploadableMap -) => RelayObservable; - -// /////////////////////////// -// Relay Modern re-exports -// /////////////////////////// - -export type Variables = { [name: string]: any }; -export type ConcreteBatch = { - kind: 'Batch', - fragment: any, - id: ?string, - metadata: { [key: string]: mixed }, - name: string, - query: any, - text: ?string, - operationKind: string, -}; -export type CacheConfig = { - force?: ?boolean, - poll?: ?number, - rerunParamExperimental?: ?any, - skipBatch?: ?boolean, -}; -export type Disposable = { dispose(): void }; -export type Uploadable = File | Blob; -export type UploadableMap = { [key: string]: Uploadable }; -export type PayloadData = { [key: string]: mixed }; -export type QueryPayload = - | {| - data?: ?PayloadData, - errors?: Array, - rerunVariables?: Variables, - |} - | RelayResponse; - -export type UnsubscribeFunction = () => void; -export type Sink = { - next: (value: T) => void, - complete: () => void, - error: (value: T) => void, -}; -// this is workaround should be class from relay-runtime/network/RelayObservable.js -export type RelayObservable = { - subscribe: (sink: Sink) => UnsubscribeFunction, -}; -// Note: This should accept Subscribable instead of RelayObservable, -// however Flow cannot yet distinguish it from T. -export type ObservableFromValue = RelayObservable | Promise | T; -export type FetchFunction = ( - operation: ConcreteBatch, - variables: Variables, - cacheConfig: CacheConfig, - uploadables: ?UploadableMap -) => ObservableFromValue; -export type FetchHookFunction = ( - operation: ConcreteBatch, - variables: Variables, - cacheConfig: CacheConfig, - uploadables: ?UploadableMap -) => void | ObservableFromValue; -// See SubscribeFunction type declaration in relay-runtime/network/RelayNetworkTypes.js -export type SubscribeFunction = ( - operation: ConcreteBatch, - variables: Variables, - cacheConfig: CacheConfig, - observer: any -) => RelayObservable | Disposable; diff --git a/src/definition.ts b/src/definition.ts new file mode 100644 index 0000000..89da010 --- /dev/null +++ b/src/definition.ts @@ -0,0 +1,96 @@ +import type RelayRequest from "./RelayRequest"; +import type RelayRequestBatch from "./RelayRequestBatch"; +import type RelayResponse from "./RelayResponse"; +export type RelayRequestAny = RelayRequest | RelayRequestBatch; +export type MiddlewareNextFn = (req: RelayRequestAny) => Promise; +export type Middleware = (next: MiddlewareNextFn) => MiddlewareNextFn; +export type MiddlewareRawNextFn = (req: RelayRequestAny) => Promise; +export type MiddlewareRaw = { + isRawMiddleware: true; +}; +export type MiddlewareSync = { + execute: (operation: ConcreteBatch, variables: Variables, cacheConfig: CacheConfig, uploadables: UploadableMap | null | undefined) => ObservableFromValue | null | undefined; +}; +export type FetchOpts = { + url?: string; + method: "POST" | "GET"; + headers: Record; + body: string | FormData; + // Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests + credentials?: "same-origin" | "include" | "omit"; + mode?: "cors" | "websocket" | "navigate" | "no-cors" | "same-origin"; + cache?: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached"; + redirect?: "follow" | "error" | "manual"; + signal?: window.AbortSignal; + [name: string]: unknown; +}; +export type FetchResponse = Response; +export type GraphQLResponseErrors = Array<{ + message: string; + locations?: Array<{ + column: number; + line: number; + }>; + stack?: Array; +}>; +export type GraphQLResponse = { + data?: any; + errors?: GraphQLResponseErrors; +}; +export type RRNLResponseObject = { + ok: any; + status: number; + statusText: string; + headers: Record; + url: string; + payload: GraphQLResponse | null | undefined; +}; +export type RNLExecuteFunction = (operation: ConcreteBatch, variables: Variables, cacheConfig: CacheConfig, uploadables?: UploadableMap | null | undefined) => RelayObservable; +// /////////////////////////// +// Relay Modern re-exports +// /////////////////////////// +export type Variables = Record; +export type ConcreteBatch = { + kind: "Batch"; + fragment: any; + id: string | null | undefined; + metadata: Record; + name: string; + query: any; + text: string | null | undefined; + operationKind: string; +}; +export type CacheConfig = { + force?: boolean | null | undefined; + poll?: number | null | undefined; + rerunParamExperimental?: any | null | undefined; + skipBatch?: boolean | null | undefined; +}; +export type Disposable = { + dispose(): void; +}; +export type Uploadable = File | Blob; +export type UploadableMap = Record; +export type PayloadData = Record; +export type QueryPayload = { + data?: PayloadData | null | undefined; + errors?: Array; + rerunVariables?: Variables; +} | RelayResponse; +export type UnsubscribeFunction = () => void; +export type Sink = { + next: (value: T) => void; + complete: () => void; + error: (value: T) => void; +}; +// this is workaround should be class from relay-runtime/network/RelayObservable.js +export type RelayObservable = { + subscribe: (sink: Sink) => UnsubscribeFunction; +}; +// Note: This should accept Subscribable instead of RelayObservable, +// however Flow cannot yet distinguish it from T. +export type ObservableFromValue = RelayObservable | Promise | T; +export type FetchFunction = (operation: ConcreteBatch, variables: Variables, cacheConfig: CacheConfig, uploadables: UploadableMap | null | undefined) => ObservableFromValue; +export type FetchHookFunction = (operation: ConcreteBatch, variables: Variables, cacheConfig: CacheConfig, uploadables: UploadableMap | null | undefined) => void | ObservableFromValue; +// See SubscribeFunction type declaration in relay-runtime/network/RelayNetworkTypes.js +export type SubscribeFunction = (operation: ConcreteBatch, variables: Variables, cacheConfig: CacheConfig, observer: any) => RelayObservable | Disposable; \ No newline at end of file diff --git a/src/express-middleware/graphqlBatchHTTPWrapper.js b/src/express-middleware/graphqlBatchHTTPWrapper.js deleted file mode 100644 index c2cf51d..0000000 --- a/src/express-middleware/graphqlBatchHTTPWrapper.js +++ /dev/null @@ -1,71 +0,0 @@ -/* @flow */ - -type ExpressMiddleware = (req: any, res: any) => any; - -export default function (graphqlHTTPMiddleware: ExpressMiddleware): ExpressMiddleware { - return (req, res) => { - const subResponses = []; - return Promise.all( - req.body.map( - (data) => - new Promise((resolve) => { - const subRequest = { - __proto__: req.__proto__, // eslint-disable-line - ...req, - body: data, - }; - const subResponse = { - ...res, - status(st) { - this.statusCode = st; - return this; - }, - set() { - return this; - }, - send(payload) { - resolve({ status: this.statusCode, id: data.id, payload }); - }, - - // support express-graphql@0.5.2 - setHeader() { - return this; - }, - header() {}, - write(payload) { - this.payload = payload; - }, - end(payload) { - // support express-graphql@0.5.4 - if (payload) { - this.payload = payload; - } - resolve({ - status: this.statusCode, - id: data.id, - payload: this.payload, - }); - }, - }; - subResponses.push(subResponse); - graphqlHTTPMiddleware(subRequest, subResponse); - }) - ) - ) - .then((responses) => { - let response = ''; - responses.forEach(({ status, id, payload }, idx) => { - if (status) { - res.status(status); - } - const comma = responses.length - 1 > idx ? ',' : ''; - response += `{ "id":"${id}", "payload":${payload} }${comma}`; - }); - res.set('Content-Type', 'application/json'); - res.send(`[${response}]`); - }) - .catch((err) => { - res.status(500).send({ error: err.message }); - }); - }; -} diff --git a/src/express-middleware/graphqlBatchHTTPWrapper.ts b/src/express-middleware/graphqlBatchHTTPWrapper.ts new file mode 100644 index 0000000..c993f5f --- /dev/null +++ b/src/express-middleware/graphqlBatchHTTPWrapper.ts @@ -0,0 +1,80 @@ +type ExpressMiddleware = (req: any, res: any) => any; +export default function (graphqlHTTPMiddleware: ExpressMiddleware): ExpressMiddleware { + return (req, res) => { + const subResponses = []; + return Promise.all(req.body.map(data => new Promise(resolve => { + const subRequest = { + __proto__: req.__proto__, + // eslint-disable-line + ...req, + body: data + }; + const subResponse = { ...res, + + status(st) { + this.statusCode = st; + return this; + }, + + set() { + return this; + }, + + send(payload) { + resolve({ + status: this.statusCode, + id: data.id, + payload + }); + }, + + // support express-graphql@0.5.2 + setHeader() { + return this; + }, + + header() {}, + + write(payload) { + this.payload = payload; + }, + + end(payload) { + // support express-graphql@0.5.4 + if (payload) { + this.payload = payload; + } + + resolve({ + status: this.statusCode, + id: data.id, + payload: this.payload + }); + } + + }; + subResponses.push(subResponse); + graphqlHTTPMiddleware(subRequest, subResponse); + }))).then(responses => { + let response = ''; + responses.forEach(({ + status, + id, + payload + }, idx) => { + if (status) { + res.status(status); + } + + const comma = responses.length - 1 > idx ? ',' : ''; + response += `{ "id":"${id}", "payload":${payload} }${comma}`; + }); + res.set('Content-Type', 'application/json'); + res.send(`[${response}]`); + }).catch(err => { + res.status(500).send({ + error: err.message + }); + }); + }; +} \ No newline at end of file diff --git a/src/fetchWithMiddleware.js b/src/fetchWithMiddleware.ts similarity index 61% rename from src/fetchWithMiddleware.js rename to src/fetchWithMiddleware.ts index 04f4abe..1eafa08 100644 --- a/src/fetchWithMiddleware.js +++ b/src/fetchWithMiddleware.ts @@ -1,61 +1,46 @@ -/* @flow */ /* eslint-disable no-param-reassign, prefer-const */ - -import { createRequestError } from './createRequestError'; -import RelayResponse from './RelayResponse'; -import type { - Middleware, - MiddlewareNextFn, - RelayRequestAny, - MiddlewareRaw, - MiddlewareRawNextFn, - FetchResponse, -} from './definition'; +import { createRequestError } from "./createRequestError"; +import RelayResponse from "./RelayResponse"; +import type { Middleware, MiddlewareNextFn, RelayRequestAny, MiddlewareRaw, MiddlewareRawNextFn, FetchResponse } from "./definition"; function runFetch(req: RelayRequestAny): Promise { - let { url } = req.fetchOpts; + let { + url + } = req.fetchOpts; if (!url) url = '/graphql'; - if (!req.fetchOpts.headers.Accept) req.fetchOpts.headers.Accept = '*/*'; + if (!req.fetchOpts.headers['Content-Type'] && !req.isFormData()) { req.fetchOpts.headers['Content-Type'] = 'application/json'; } - return fetch(url, (req.fetchOpts: any)); + return fetch(url, (req.fetchOpts as any)); } // convert fetch response to RelayResponse object -const convertResponse: (next: MiddlewareRawNextFn) => MiddlewareNextFn = (next) => async (req) => { +const convertResponse: (next: MiddlewareRawNextFn) => MiddlewareNextFn = next => async req => { const resFromFetch = await next(req); - const res = await RelayResponse.createFromFetch(resFromFetch); + if (res.status && res.status >= 400) { throw createRequestError(req, res); } + return res; }; -export default function fetchWithMiddleware( - req: RelayRequestAny, - middlewares: Middleware[], // works with RelayResponse - rawFetchMiddlewares: MiddlewareRaw[], // works with raw fetch response - noThrow?: boolean -): Promise { - // $FlowFixMe - const wrappedFetch: MiddlewareNextFn = compose( - ...middlewares, - convertResponse, - ...rawFetchMiddlewares - )((runFetch: any)); - - return wrappedFetch(req).then((res) => { +export default function fetchWithMiddleware(req: RelayRequestAny, middlewares: Middleware[], // works with RelayResponse +rawFetchMiddlewares: MiddlewareRaw[], // works with raw fetch response +noThrow?: boolean): Promise { + const wrappedFetch: MiddlewareNextFn = compose(...middlewares, convertResponse, ...rawFetchMiddlewares)((runFetch as any)); + return wrappedFetch(req).then(res => { if (!noThrow && (!res || res.errors || !res.data)) { throw createRequestError(req, res); } + return res; }); } - /** * Composes single-argument functions from right to left. The rightmost * function can take multiple arguments as it provides the signature for @@ -66,13 +51,13 @@ export default function fetchWithMiddleware( * from right to left. For example, compose(f, g, h) is identical to doing * (...args) => f(g(h(...args))). */ + function compose(...funcs) { if (funcs.length === 0) { - return (arg) => arg; + return arg => arg; } else { const last = funcs[funcs.length - 1]; const rest = funcs.slice(0, -1); - // $FlowFixMe - Suppress error about promise not being callable - return (...args) => rest.reduceRight((composed, f) => f((composed: any)), last(...args)); + return (...args) => rest.reduceRight((composed, f) => f((composed as any)), last(...args)); } } diff --git a/src/index.js b/src/index.js deleted file mode 100644 index a6f3ae5..0000000 --- a/src/index.js +++ /dev/null @@ -1,48 +0,0 @@ -/* @flow */ - -import RelayNetworkLayer from './RelayNetworkLayer'; -import batchMiddleware, { RRNLBatchMiddlewareError } from './middlewares/batch'; -import legacyBatchMiddleware from './middlewares/legacyBatch'; -import retryMiddleware, { RRNLRetryMiddlewareError } from './middlewares/retry'; -import urlMiddleware from './middlewares/url'; -import authMiddleware, { RRNLAuthMiddlewareError } from './middlewares/auth'; -import perfMiddleware from './middlewares/perf'; -import loggerMiddleware from './middlewares/logger'; -import persistedQueriesMiddleware from './middlewares/persistedQueries'; -import errorMiddleware from './middlewares/error'; -import cacheMiddleware from './middlewares/cache'; -import progressMiddleware from './middlewares/progress'; -import uploadMiddleware from './middlewares/upload'; -import graphqlBatchHTTPWrapper from './express-middleware/graphqlBatchHTTPWrapper'; -import RelayNetworkLayerRequest from './RelayRequest'; -import RelayNetworkLayerRequestBatch from './RelayRequestBatch'; -import RelayNetworkLayerResponse from './RelayResponse'; -import { createRequestError, formatGraphQLErrors, RRNLRequestError } from './createRequestError'; -import RRNLError from './RRNLError'; - -export { - RelayNetworkLayer, - RelayNetworkLayerRequest, - RelayNetworkLayerRequestBatch, - RelayNetworkLayerResponse, - batchMiddleware, - legacyBatchMiddleware, - retryMiddleware, - urlMiddleware, - authMiddleware, - perfMiddleware, - loggerMiddleware, - persistedQueriesMiddleware, - errorMiddleware, - cacheMiddleware, - progressMiddleware, - uploadMiddleware, - graphqlBatchHTTPWrapper, - createRequestError, - formatGraphQLErrors, - RRNLError, - RRNLRequestError, - RRNLRetryMiddlewareError, - RRNLAuthMiddlewareError, - RRNLBatchMiddlewareError, -}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..766c6b9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,20 @@ +import RelayNetworkLayer from "./RelayNetworkLayer"; +import batchMiddleware, { RRNLBatchMiddlewareError } from "./middlewares/batch"; +import legacyBatchMiddleware from "./middlewares/legacyBatch"; +import retryMiddleware, { RRNLRetryMiddlewareError } from "./middlewares/retry"; +import urlMiddleware from "./middlewares/url"; +import authMiddleware, { RRNLAuthMiddlewareError } from "./middlewares/auth"; +import perfMiddleware from "./middlewares/perf"; +import loggerMiddleware from "./middlewares/logger"; +import persistedQueriesMiddleware from "./middlewares/persistedQueries"; +import errorMiddleware from "./middlewares/error"; +import cacheMiddleware from "./middlewares/cache"; +import progressMiddleware from "./middlewares/progress"; +import uploadMiddleware from "./middlewares/upload"; +import graphqlBatchHTTPWrapper from "./express-middleware/graphqlBatchHTTPWrapper"; +import RelayNetworkLayerRequest from "./RelayRequest"; +import RelayNetworkLayerRequestBatch from "./RelayRequestBatch"; +import RelayNetworkLayerResponse from "./RelayResponse"; +import { createRequestError, formatGraphQLErrors, RRNLRequestError } from "./createRequestError"; +import RRNLError from "./RRNLError"; +export { RelayNetworkLayer, RelayNetworkLayerRequest, RelayNetworkLayerRequestBatch, RelayNetworkLayerResponse, batchMiddleware, legacyBatchMiddleware, retryMiddleware, urlMiddleware, authMiddleware, perfMiddleware, loggerMiddleware, persistedQueriesMiddleware, errorMiddleware, cacheMiddleware, progressMiddleware, uploadMiddleware, graphqlBatchHTTPWrapper, createRequestError, formatGraphQLErrors, RRNLError, RRNLRequestError, RRNLRetryMiddlewareError, RRNLAuthMiddlewareError, RRNLBatchMiddlewareError }; \ No newline at end of file diff --git a/src/middlewares/auth.js b/src/middlewares/auth.ts similarity index 65% rename from src/middlewares/auth.js rename to src/middlewares/auth.ts index 9e9036b..b4fa51a 100644 --- a/src/middlewares/auth.js +++ b/src/middlewares/auth.ts @@ -1,40 +1,33 @@ -/* @flow */ /* eslint-disable no-param-reassign, arrow-body-style, dot-notation */ - -import { isFunction } from '../utils'; -import type RelayResponse from '../RelayResponse'; -import type { Middleware, RelayRequestAny } from '../definition'; -import RRNLError from '../RRNLError'; - +import { isFunction } from "../utils"; +import type RelayResponse from "../RelayResponse"; +import type { Middleware, RelayRequestAny } from "../definition"; +import RRNLError from "../RRNLError"; export class RRNLAuthMiddlewareError extends RRNLError { constructor(msg: string) { super(msg); this.name = 'RRNLAuthMiddlewareError'; } -} - -export type AuthMiddlewareOpts = {| - token?: string | Promise | ((req: RelayRequestAny) => string | Promise), - tokenRefreshPromise?: (req: RelayRequestAny, res: RelayResponse) => string | Promise, - allowEmptyToken?: boolean, - prefix?: string, - header?: string, -|}; +} +export type AuthMiddlewareOpts = { + token?: string | Promise | ((req: RelayRequestAny) => string | Promise); + tokenRefreshPromise?: (req: RelayRequestAny, res: RelayResponse) => string | Promise; + allowEmptyToken?: boolean; + prefix?: string; + header?: string; +}; export default function authMiddleware(opts?: AuthMiddlewareOpts): Middleware { const { token: tokenOrThunk, tokenRefreshPromise, allowEmptyToken = false, prefix = 'Bearer ', - header = 'Authorization', + header = 'Authorization' } = opts || {}; - let tokenRefreshInProgress = null; - - return (next) => async (req) => { + return next => async req => { try { - // $FlowFixMe const token = await (isFunction(tokenOrThunk) ? tokenOrThunk(req) : tokenOrThunk); if (!token && tokenRefreshPromise && !allowEmptyToken) { @@ -44,30 +37,30 @@ export default function authMiddleware(opts?: AuthMiddlewareOpts): Middleware { if (token) { req.fetchOpts.headers[header] = `${prefix}${token}`; } + const res = await next(req); return res; } catch (e) { if (e && tokenRefreshPromise) { - if (e.message === 'Empty token' || (e.res && e.res.status === 401)) { + if (e.message === 'Empty token' || e.res && e.res.status === 401) { if (tokenRefreshPromise) { if (!tokenRefreshInProgress) { - tokenRefreshInProgress = Promise.resolve(tokenRefreshPromise(req, e.res)) - .then((newToken) => { - tokenRefreshInProgress = null; - return newToken; - }) - .catch((err) => { - tokenRefreshInProgress = null; - throw err; - }); + tokenRefreshInProgress = Promise.resolve(tokenRefreshPromise(req, e.res)).then(newToken => { + tokenRefreshInProgress = null; + return newToken; + }).catch(err => { + tokenRefreshInProgress = null; + throw err; + }); } - return tokenRefreshInProgress.then((newToken) => { + return tokenRefreshInProgress.then(newToken => { if (!newToken && !allowEmptyToken) { throw new RRNLAuthMiddlewareError('Empty token'); } const newReq = req.clone(); + if (newToken) { newReq.fetchOpts.headers[header] = `${prefix}${newToken}`; } else { diff --git a/src/middlewares/batch.js b/src/middlewares/batch.ts similarity index 62% rename from src/middlewares/batch.js rename to src/middlewares/batch.ts index 1367ef5..22b5879 100644 --- a/src/middlewares/batch.js +++ b/src/middlewares/batch.ts @@ -1,64 +1,55 @@ -/* @flow */ -/* eslint-disable no-param-reassign */ - -import { isFunction } from '../utils'; -import RelayRequestBatch from '../RelayRequestBatch'; -import RelayRequest from '../RelayRequest'; -import type RelayResponse from '../RelayResponse'; -import type { Middleware, FetchOpts } from '../definition'; -import RRNLError from '../RRNLError'; +import { $PropertyType } from "utility-types"; +/* eslint-disable no-param-reassign */ +import { isFunction } from "../utils"; +import RelayRequestBatch from "../RelayRequestBatch"; +import RelayRequest from "../RelayRequest"; +import type RelayResponse from "../RelayResponse"; +import type { Middleware, FetchOpts } from "../definition"; +import RRNLError from "../RRNLError"; // Max out at roughly 100kb (express-graphql imposed max) const DEFAULT_BATCH_SIZE = 102400; - -type Headers = { [name: string]: string }; - -export type BatchMiddlewareOpts = {| - batchUrl?: - | string - | Promise - | ((requestList: RequestWrapper[]) => string | Promise), - batchTimeout?: number, - maxBatchSize?: number, - allowMutations?: boolean, - method?: 'POST' | 'GET', - headers?: Headers | Promise | ((req: RelayRequestBatch) => Headers | Promise), +type Headers = Record; +export type BatchMiddlewareOpts = { + batchUrl?: string | Promise | ((requestList: RequestWrapper[]) => string | Promise); + batchTimeout?: number; + maxBatchSize?: number; + allowMutations?: boolean; + method?: "POST" | "GET"; + headers?: Headers | Promise | ((req: RelayRequestBatch) => Headers | Promise); // Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests - credentials?: $PropertyType, - mode?: $PropertyType, - cache?: $PropertyType, - redirect?: $PropertyType, -|}; - -export type RequestWrapper = {| - req: RelayRequest, - completeOk: (res: Object) => void, - completeErr: (e: Error) => void, - done: boolean, - duplicates: Array, -|}; - + credentials?: $PropertyType; + mode?: $PropertyType; + cache?: $PropertyType; + redirect?: $PropertyType; +}; +export type RequestWrapper = { + req: RelayRequest; + completeOk: (res: Record) => void; + completeErr: (e: Error) => void; + done: boolean; + duplicates: Array; +}; type Batcher = { - bodySize: number, - requestList: RequestWrapper[], - acceptRequests: boolean, + bodySize: number; + requestList: RequestWrapper[]; + acceptRequests: boolean; }; - export class RRNLBatchMiddlewareError extends RRNLError { constructor(msg: string) { super(msg); this.name = 'RRNLBatchMiddlewareError'; } -} +} export default function batchMiddleware(options?: BatchMiddlewareOpts): Middleware { const opts = options || {}; const batchTimeout = opts.batchTimeout || 0; // 0 is the same as nextTick in nodeJS + const allowMutations = opts.allowMutations || false; const batchUrl = opts.batchUrl || '/graphql/batch'; const maxBatchSize = opts.maxBatchSize || DEFAULT_BATCH_SIZE; const singleton = {}; - const fetchOpts = {}; if (opts.method) fetchOpts.method = opts.method; if (opts.credentials) fetchOpts.credentials = opts.credentials; @@ -66,17 +57,14 @@ export default function batchMiddleware(options?: BatchMiddlewareOpts): Middlewa if (opts.cache) fetchOpts.cache = opts.cache; if (opts.redirect) fetchOpts.redirect = opts.redirect; if (opts.headers) fetchOpts.headersOrThunk = opts.headers; - - return (next) => (req) => { + return next => req => { // do not batch mutations unless allowMutations = true if (req.isMutation() && !allowMutations) { return next(req); } if (!(req instanceof RelayRequest)) { - throw new RRNLBatchMiddlewareError( - 'Relay batch middleware accepts only simple RelayRequest. Did you add batchMiddleware twice?' - ); + throw new RRNLBatchMiddlewareError('Relay batch middleware accepts only simple RelayRequest. Did you add batchMiddleware twice?'); } // req with FormData can not be batched @@ -94,15 +82,17 @@ export default function batchMiddleware(options?: BatchMiddlewareOpts): Middlewa batchUrl, singleton, maxBatchSize, - fetchOpts, + fetchOpts }); }; } function passThroughBatch(req: RelayRequest, next, opts) { - const { singleton } = opts; + const { + singleton + } = opts; + const bodyLength = (req.getBody() as any).length; - const bodyLength = (req.getBody(): any).length; if (!bodyLength) { return next(req); } @@ -117,30 +107,27 @@ function passThroughBatch(req: RelayRequest, next, opts) { // +1 accounts for tailing comma after joining singleton.batcher.bodySize += bodyLength + 1; - // queue request return new Promise((resolve, reject) => { - const { requestList } = singleton.batcher; - + const { + requestList + } = singleton.batcher; const requestWrapper: RequestWrapper = { req, - completeOk: (res) => { + completeOk: res => { requestWrapper.done = true; resolve(res); - requestWrapper.duplicates.forEach((r) => r.completeOk(res)); + requestWrapper.duplicates.forEach(r => r.completeOk(res)); }, - completeErr: (err) => { + completeErr: err => { requestWrapper.done = true; reject(err); - requestWrapper.duplicates.forEach((r) => r.completeErr(err)); + requestWrapper.duplicates.forEach(r => r.completeErr(err)); }, done: false, - duplicates: [], + duplicates: [] }; - - const duplicateIndex = requestList.findIndex( - (wrapper) => req.getBody() === wrapper.req.getBody() - ); + const duplicateIndex = requestList.findIndex(wrapper => req.getBody() === wrapper.req.getBody()); if (duplicateIndex !== -1) { /* @@ -160,24 +147,21 @@ function passThroughBatch(req: RelayRequest, next, opts) { function prepareNewBatcher(next, opts): Batcher { const batcher: Batcher = { - bodySize: 2, // account for '[]' + bodySize: 2, + // account for '[]' requestList: [], - acceptRequests: true, + acceptRequests: true }; - setTimeout(() => { batcher.acceptRequests = false; - sendRequests(batcher.requestList, next, opts) - .then(() => finalizeUncompleted(batcher.requestList)) - .catch((e) => { - if (e && e.name === 'AbortError') { - finalizeCanceled(batcher.requestList, e); - } else { - finalizeUncompleted(batcher.requestList); - } - }); + sendRequests(batcher.requestList, next, opts).then(() => finalizeUncompleted(batcher.requestList)).catch(e => { + if (e && e.name === 'AbortError') { + finalizeCanceled(batcher.requestList, e); + } else { + finalizeUncompleted(batcher.requestList); + } + }); }, opts.batchTimeout); - return batcher; } @@ -185,49 +169,45 @@ async function sendRequests(requestList: RequestWrapper[], next, opts) { if (requestList.length === 1) { // SEND AS SINGLE QUERY const wrapper = requestList[0]; - const res = await next(wrapper.req); wrapper.completeOk(res); - wrapper.duplicates.forEach((r) => r.completeOk(res)); + wrapper.duplicates.forEach(r => r.completeOk(res)); return res; } else if (requestList.length > 1) { // SEND AS BATCHED QUERY - - const batchRequest = new RelayRequestBatch(requestList.map((wrapper) => wrapper.req)); - // $FlowFixMe + const batchRequest = new RelayRequestBatch(requestList.map(wrapper => wrapper.req)); const url = await (isFunction(opts.batchUrl) ? opts.batchUrl(requestList) : opts.batchUrl); batchRequest.setFetchOption('url', url); - - const { headersOrThunk, ...fetchOpts } = opts.fetchOpts; + const { + headersOrThunk, + ...fetchOpts + } = opts.fetchOpts; batchRequest.setFetchOptions(fetchOpts); if (headersOrThunk) { - const headers = await (isFunction(headersOrThunk) - ? (headersOrThunk: any)(batchRequest) - : headersOrThunk); + const headers = await (isFunction(headersOrThunk) ? (headersOrThunk as any)(batchRequest) : headersOrThunk); batchRequest.setFetchOption('headers', headers); } try { const batchResponse = await next(batchRequest); + if (!batchResponse || !Array.isArray(batchResponse.json)) { - throw new RRNLBatchMiddlewareError( - 'Wrong response from server. Did your server support batch request?' - ); + throw new RRNLBatchMiddlewareError('Wrong response from server. Did your server support batch request?'); } batchResponse.json.forEach((payload: any, index) => { if (!payload) return; const request = requestList[index]; + if (request) { const res = createSingleResponse(batchResponse, payload); request.completeOk(res); } }); - return batchResponse; } catch (e) { - requestList.forEach((request) => request.completeErr(e)); + requestList.forEach(request => request.completeErr(e)); } } @@ -236,19 +216,14 @@ async function sendRequests(requestList: RequestWrapper[], next, opts) { // check that server returns responses for all requests function finalizeCanceled(requestList: RequestWrapper[], error: Error) { - requestList.forEach((request) => request.completeErr(error)); + requestList.forEach(request => request.completeErr(error)); } // check that server returns responses for all requests function finalizeUncompleted(requestList: RequestWrapper[]) { requestList.forEach((request, index) => { if (!request.done) { - request.completeErr( - new RRNLBatchMiddlewareError( - `Server does not return response for request at index ${index}.\n` + - `Response should have an array with ${requestList.length} item(s).` - ) - ); + request.completeErr(new RRNLBatchMiddlewareError(`Server does not return response for request at index ${index}.\n` + `Response should have an array with ${requestList.length} item(s).`)); } }); } diff --git a/src/middlewares/cache.js b/src/middlewares/cache.ts similarity index 67% rename from src/middlewares/cache.js rename to src/middlewares/cache.ts index 7e81758..fc4eee2 100644 --- a/src/middlewares/cache.js +++ b/src/middlewares/cache.ts @@ -1,20 +1,16 @@ -/* @flow */ - -import { QueryResponseCache } from 'relay-runtime'; -import type { Middleware } from '../definition'; -import { isFunction } from '../utils'; - -type CacheMiddlewareOpts = {| - size?: number, - ttl?: number, - onInit?: (cache: QueryResponseCache) => any, - allowMutations?: boolean, - allowFormData?: boolean, - clearOnMutation?: boolean, - cacheErrors?: boolean, - updateTTLOnGet?: boolean, -|}; - +import { QueryResponseCache } from "relay-runtime"; +import type { Middleware } from "../definition"; +import { isFunction } from "../utils"; +type CacheMiddlewareOpts = { + size?: number; + ttl?: number; + onInit?: (cache: QueryResponseCache) => any; + allowMutations?: boolean; + allowFormData?: boolean; + clearOnMutation?: boolean; + cacheErrors?: boolean; + updateTTLOnGet?: boolean; +}; export default function cacheMiddleware(opts?: CacheMiddlewareOpts): Middleware { const { size, @@ -24,22 +20,25 @@ export default function cacheMiddleware(opts?: CacheMiddlewareOpts): Middleware allowFormData, clearOnMutation, cacheErrors, - updateTTLOnGet, + updateTTLOnGet } = opts || {}; const cache = new QueryResponseCache({ - size: size || 100, // 100 requests - ttl: ttl || 15 * 60 * 1000, // 15 minutes + size: size || 100, + // 100 requests + ttl: ttl || 15 * 60 * 1000 // 15 minutes + }); if (isFunction(onInit)) { onInit(cache); } - return (next) => async (req) => { + return next => async req => { if (req.isMutation()) { if (clearOnMutation) { cache.clear(); } + if (!allowMutations) { return next(req); } @@ -54,17 +53,18 @@ export default function cacheMiddleware(opts?: CacheMiddlewareOpts): Middleware const variables = req.getVariables(); const res = await next(req); - if (!res.errors || (res.errors && cacheErrors)) { + if (!res.errors || res.errors && cacheErrors) { cache.set(queryId, variables, res); } + return res; } try { const queryId = req.getID(); const variables = req.getVariables(); - const cachedRes = cache.get(queryId, variables); + if (cachedRes) { if (updateTTLOnGet) { cache.set(queryId, variables, cachedRes); @@ -74,7 +74,8 @@ export default function cacheMiddleware(opts?: CacheMiddlewareOpts): Middleware } const res = await next(req); - if (!res.errors || (res.errors && cacheErrors)) { + + if (!res.errors || res.errors && cacheErrors) { cache.set(queryId, variables, res); } @@ -86,4 +87,4 @@ export default function cacheMiddleware(opts?: CacheMiddlewareOpts): Middleware return next(req); }; -} +} \ No newline at end of file diff --git a/src/middlewares/error.js b/src/middlewares/error.ts similarity index 72% rename from src/middlewares/error.js rename to src/middlewares/error.ts index cca0645..89fa1b3 100644 --- a/src/middlewares/error.js +++ b/src/middlewares/error.ts @@ -1,30 +1,29 @@ -/* @flow */ /* eslint-disable no-console */ - -import RelayRequest from '../RelayRequest'; -import RelayRequestBatch from '../RelayRequestBatch'; -import type RelayResponse from '../RelayResponse'; -import type { Middleware, GraphQLResponseErrors, RelayRequestAny } from '../definition'; - -export type GqlErrorMiddlewareOpts = {| - logger?: Function, - prefix?: string, - disableServerMiddlewareTip?: boolean, -|}; - +import RelayRequest from "../RelayRequest"; +import RelayRequestBatch from "../RelayRequestBatch"; +import type RelayResponse from "../RelayResponse"; +import type { Middleware, GraphQLResponseErrors, RelayRequestAny } from "../definition"; +export type GqlErrorMiddlewareOpts = { + logger?: (...args: Array) => any; + prefix?: string; + disableServerMiddlewareTip?: boolean; +}; export default function errorMiddleware(options?: GqlErrorMiddlewareOpts): Middleware { const opts = options || {}; const logger = opts.logger || console.error.bind(console); const prefix = opts.prefix || '[RELAY-NETWORK] GRAPHQL SERVER ERROR:\n\n'; const disableServerMiddlewareTip = opts.disableServerMiddlewareTip || false; - function displayErrors( - errors: GraphQLResponseErrors, - reqRes: { req: RelayRequestAny, res: RelayResponse } - ) { - return errors.forEach((error) => { - const { message, stack, ...rest } = error; - + function displayErrors(errors: GraphQLResponseErrors, reqRes: { + req: RelayRequestAny; + res: RelayResponse; + }) { + return errors.forEach(error => { + const { + message, + stack, + ...rest + } = error; let msg = `${prefix}`; const fmt = []; @@ -53,17 +52,23 @@ export default function errorMiddleware(options?: GqlErrorMiddlewareOpts): Middl }); } - return (next) => (req) => { - return next(req).then((res) => { + return next => req => { + return next(req).then(res => { if (req instanceof RelayRequest) { if (Array.isArray(res.errors)) { - displayErrors(res.errors, { req, res }); + displayErrors(res.errors, { + req, + res + }); } } else if (req instanceof RelayRequestBatch) { if (Array.isArray(res.json)) { res.json.forEach((payload: any) => { if (Array.isArray(payload.errors)) { - displayErrors(payload.errors, { req, res }); + displayErrors(payload.errors, { + req, + res + }); } }); } @@ -90,4 +95,4 @@ function noticeAbsentStack() { }); app.use('/graphql', graphQLMiddleware);`; -} +} \ No newline at end of file diff --git a/src/middlewares/legacyBatch.js b/src/middlewares/legacyBatch.ts similarity index 64% rename from src/middlewares/legacyBatch.js rename to src/middlewares/legacyBatch.ts index 1fcd2b0..71d7526 100644 --- a/src/middlewares/legacyBatch.js +++ b/src/middlewares/legacyBatch.ts @@ -1,58 +1,49 @@ -/* @flow */ -/* eslint-disable no-param-reassign */ - -import { isFunction } from '../utils'; -import RelayRequestBatch from '../RelayRequestBatch'; -import RelayRequest from '../RelayRequest'; -import type RelayResponse from '../RelayResponse'; -import type { Middleware, FetchOpts } from '../definition'; -import { RRNLBatchMiddlewareError } from './batch'; +import { $PropertyType } from "utility-types"; +/* eslint-disable no-param-reassign */ +import { isFunction } from "../utils"; +import RelayRequestBatch from "../RelayRequestBatch"; +import RelayRequest from "../RelayRequest"; +import type RelayResponse from "../RelayResponse"; +import type { Middleware, FetchOpts } from "../definition"; +import { RRNLBatchMiddlewareError } from "./batch"; // Max out at roughly 100kb (express-graphql imposed max) const DEFAULT_BATCH_SIZE = 102400; - -type Headers = { [name: string]: string }; - -export type BatchMiddlewareOpts = {| - batchUrl?: string | Promise | ((requestMap: BatchRequestMap) => string | Promise), - batchTimeout?: number, - maxBatchSize?: number, - allowMutations?: boolean, - method?: 'POST' | 'GET', - headers?: Headers | Promise | ((req: RelayRequestBatch) => Headers | Promise), +type Headers = Record; +export type BatchMiddlewareOpts = { + batchUrl?: string | Promise | ((requestMap: BatchRequestMap) => string | Promise); + batchTimeout?: number; + maxBatchSize?: number; + allowMutations?: boolean; + method?: "POST" | "GET"; + headers?: Headers | Promise | ((req: RelayRequestBatch) => Headers | Promise); // Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests - credentials?: $PropertyType, - mode?: $PropertyType, - cache?: $PropertyType, - redirect?: $PropertyType, -|}; - -export type BatchRequestMap = { - [reqId: string]: RequestWrapper, + credentials?: $PropertyType; + mode?: $PropertyType; + cache?: $PropertyType; + redirect?: $PropertyType; +}; +export type BatchRequestMap = Record; +export type RequestWrapper = { + req: RelayRequest; + completeOk: (res: Record) => void; + completeErr: (e: Error) => void; + done: boolean; + duplicates: Array; }; - -export type RequestWrapper = {| - req: RelayRequest, - completeOk: (res: Object) => void, - completeErr: (e: Error) => void, - done: boolean, - duplicates: Array, -|}; - type Batcher = { - bodySize: number, - requestMap: BatchRequestMap, - acceptRequests: boolean, + bodySize: number; + requestMap: BatchRequestMap; + acceptRequests: boolean; }; - export default function legacyBatchMiddleware(options?: BatchMiddlewareOpts): Middleware { const opts = options || {}; const batchTimeout = opts.batchTimeout || 0; // 0 is the same as nextTick in nodeJS + const allowMutations = opts.allowMutations || false; const batchUrl = opts.batchUrl || '/graphql/batch'; const maxBatchSize = opts.maxBatchSize || DEFAULT_BATCH_SIZE; const singleton = {}; - const fetchOpts = {}; if (opts.method) fetchOpts.method = opts.method; if (opts.credentials) fetchOpts.credentials = opts.credentials; @@ -60,17 +51,14 @@ export default function legacyBatchMiddleware(options?: BatchMiddlewareOpts): Mi if (opts.cache) fetchOpts.cache = opts.cache; if (opts.redirect) fetchOpts.redirect = opts.redirect; if (opts.headers) fetchOpts.headersOrThunk = opts.headers; - - return (next) => (req) => { + return next => req => { // do not batch mutations unless allowMutations = true if (req.isMutation() && !allowMutations) { return next(req); } if (!(req instanceof RelayRequest)) { - throw new RRNLBatchMiddlewareError( - 'Relay batch middleware accepts only simple RelayRequest. Did you add batchMiddleware twice?' - ); + throw new RRNLBatchMiddlewareError('Relay batch middleware accepts only simple RelayRequest. Did you add batchMiddleware twice?'); } // req with FormData can not be batched @@ -83,15 +71,17 @@ export default function legacyBatchMiddleware(options?: BatchMiddlewareOpts): Mi batchUrl, singleton, maxBatchSize, - fetchOpts, + fetchOpts }); }; } function passThroughBatch(req: RelayRequest, next, opts) { - const { singleton } = opts; + const { + singleton + } = opts; + const bodyLength = (req.fetchOpts.body as any).length; - const bodyLength = (req.fetchOpts.body: any).length; if (!bodyLength) { return next(req); } @@ -106,26 +96,26 @@ function passThroughBatch(req: RelayRequest, next, opts) { // +1 accounts for tailing comma after joining singleton.batcher.bodySize += bodyLength + 1; - // queue request return new Promise((resolve, reject) => { const relayReqId = req.getID(); - const { requestMap } = singleton.batcher; - + const { + requestMap + } = singleton.batcher; const requestWrapper: RequestWrapper = { req, - completeOk: (res) => { + completeOk: res => { requestWrapper.done = true; resolve(res); - requestWrapper.duplicates.forEach((r) => r.completeOk(res)); + requestWrapper.duplicates.forEach(r => r.completeOk(res)); }, - completeErr: (err) => { + completeErr: err => { requestWrapper.done = true; reject(err); - requestWrapper.duplicates.forEach((r) => r.completeErr(err)); + requestWrapper.duplicates.forEach(r => r.completeErr(err)); }, done: false, - duplicates: [], + duplicates: [] }; if (requestMap[relayReqId]) { @@ -146,18 +136,15 @@ function passThroughBatch(req: RelayRequest, next, opts) { function prepareNewBatcher(next, opts): Batcher { const batcher: Batcher = { - bodySize: 2, // account for '[]' + bodySize: 2, + // account for '[]' requestMap: {}, - acceptRequests: true, + acceptRequests: true }; - setTimeout(() => { batcher.acceptRequests = false; - sendRequests(batcher.requestMap, next, opts) - .then(() => finalizeUncompleted(batcher.requestMap)) - .catch(() => finalizeUncompleted(batcher.requestMap)); + sendRequests(batcher.requestMap, next, opts).then(() => finalizeUncompleted(batcher.requestMap)).catch(() => finalizeUncompleted(batcher.requestMap)); }, opts.batchTimeout); - return batcher; } @@ -167,49 +154,45 @@ async function sendRequests(requestMap: BatchRequestMap, next, opts) { if (ids.length === 1) { // SEND AS SINGLE QUERY const request = requestMap[ids[0]]; - const res = await next(request.req); request.completeOk(res); - request.duplicates.forEach((r) => r.completeOk(res)); + request.duplicates.forEach(r => r.completeOk(res)); return res; } else if (ids.length > 1) { // SEND AS BATCHED QUERY - - const batchRequest = new RelayRequestBatch(ids.map((id) => requestMap[id].req)); - // $FlowFixMe + const batchRequest = new RelayRequestBatch(ids.map(id => requestMap[id].req)); const url = await (isFunction(opts.batchUrl) ? opts.batchUrl(requestMap) : opts.batchUrl); batchRequest.setFetchOption('url', url); - - const { headersOrThunk, ...fetchOpts } = opts.fetchOpts; + const { + headersOrThunk, + ...fetchOpts + } = opts.fetchOpts; batchRequest.setFetchOptions(fetchOpts); if (headersOrThunk) { - const headers = await (isFunction(headersOrThunk) - ? (headersOrThunk: any)(batchRequest) - : headersOrThunk); + const headers = await (isFunction(headersOrThunk) ? (headersOrThunk as any)(batchRequest) : headersOrThunk); batchRequest.setFetchOption('headers', headers); } try { const batchResponse = await next(batchRequest); + if (!batchResponse || !Array.isArray(batchResponse.json)) { - throw new RRNLBatchMiddlewareError( - 'Wrong response from server. Did your server support batch request?' - ); + throw new RRNLBatchMiddlewareError('Wrong response from server. Did your server support batch request?'); } batchResponse.json.forEach((payload: any) => { if (!payload) return; const request = requestMap[payload.id]; + if (request) { const res = createSingleResponse(batchResponse, payload); request.completeOk(res); } }); - return batchResponse; } catch (e) { - ids.forEach((id) => { + ids.forEach(id => { requestMap[id].completeErr(e); }); } @@ -220,15 +203,11 @@ async function sendRequests(requestMap: BatchRequestMap, next, opts) { // check that server returns responses for all requests function finalizeUncompleted(requestMap: BatchRequestMap) { - Object.keys(requestMap).forEach((id) => { + Object.keys(requestMap).forEach(id => { const request = requestMap[id]; + if (!request.done) { - request.completeErr( - new RRNLBatchMiddlewareError( - `Server does not return response for request with id ${id} \n` + - `Response should have following shape { "id": "${id}", "data": {} }` - ) - ); + request.completeErr(new RRNLBatchMiddlewareError(`Server does not return response for request with id ${id} \n` + `Response should have following shape { "id": "${id}", "data": {} }`)); } }); } diff --git a/src/middlewares/logger.js b/src/middlewares/logger.js deleted file mode 100644 index 60118ea..0000000 --- a/src/middlewares/logger.js +++ /dev/null @@ -1,56 +0,0 @@ -/* @flow */ -/* eslint-disable no-console */ - -import RelayRequest from '../RelayRequest'; -import RelayRequestBatch from '../RelayRequestBatch'; -import type { Middleware } from '../definition'; - -export type LoggerMiddlewareOpts = {| - logger?: Function, -|}; - -export default function loggerMiddleware(opts?: LoggerMiddlewareOpts): Middleware { - const logger = (opts && opts.logger) || console.log.bind(console, '[RELAY-NETWORK]'); - - return (next) => (req) => { - const start = new Date().getTime(); - - logger(`Run ${req.getID()}`, req); - return next(req).then( - (res) => { - const end = new Date().getTime(); - - let queryId; - let queryData; - if (req instanceof RelayRequest) { - queryId = req.getID(); - queryData = { - query: req.getQueryString(), - variables: req.getVariables(), - }; - } else if (req instanceof RelayRequestBatch) { - queryId = req.getID(); - queryData = { - requestList: req.requests, - responseList: res.json, - }; - } else { - queryId = 'CustomRequest'; - queryData = {}; - } - - logger(`Done ${queryId} in ${end - start}ms`, { ...queryData, req, res }); - if (res.status !== 200) { - logger(`Status ${res.status}: ${res.statusText || ''} for ${queryId}`); - } - return res; - }, - (error) => { - if (error && error.name && error.name === 'AbortError') { - logger(`Cancelled ${req.getID()}`); - } - throw error; - } - ); - }; -} diff --git a/src/middlewares/logger.ts b/src/middlewares/logger.ts new file mode 100644 index 0000000..1aae58e --- /dev/null +++ b/src/middlewares/logger.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ +import RelayRequest from "../RelayRequest"; +import RelayRequestBatch from "../RelayRequestBatch"; +import type { Middleware } from "../definition"; +export type LoggerMiddlewareOpts = { + logger?: (...args: Array) => any; +}; +export default function loggerMiddleware(opts?: LoggerMiddlewareOpts): Middleware { + const logger = opts && opts.logger || console.log.bind(console, '[RELAY-NETWORK]'); + return next => req => { + const start = new Date().getTime(); + logger(`Run ${req.getID()}`, req); + return next(req).then(res => { + const end = new Date().getTime(); + let queryId; + let queryData; + + if (req instanceof RelayRequest) { + queryId = req.getID(); + queryData = { + query: req.getQueryString(), + variables: req.getVariables() + }; + } else if (req instanceof RelayRequestBatch) { + queryId = req.getID(); + queryData = { + requestList: req.requests, + responseList: res.json + }; + } else { + queryId = 'CustomRequest'; + queryData = {}; + } + + logger(`Done ${queryId} in ${end - start}ms`, { ...queryData, + req, + res + }); + + if (res.status !== 200) { + logger(`Status ${res.status}: ${res.statusText || ''} for ${queryId}`); + } + + return res; + }, error => { + if (error && error.name && error.name === 'AbortError') { + logger(`Cancelled ${req.getID()}`); + } + + throw error; + }); + }; +} \ No newline at end of file diff --git a/src/middlewares/perf.js b/src/middlewares/perf.ts similarity index 50% rename from src/middlewares/perf.js rename to src/middlewares/perf.ts index 8e7d318..05d2dd6 100644 --- a/src/middlewares/perf.js +++ b/src/middlewares/perf.ts @@ -1,22 +1,16 @@ -/* @flow */ /* eslint-disable no-console */ - -import type { Middleware } from '../definition'; - -export type PerfMiddlewareOpts = {| - logger?: Function, -|}; - +import type { Middleware } from "../definition"; +export type PerfMiddlewareOpts = { + logger?: (...args: Array) => any; +}; export default function performanceMiddleware(opts?: PerfMiddlewareOpts): Middleware { - const logger = (opts && opts.logger) || console.log.bind(console, '[RELAY-NETWORK]'); - - return (next) => (req) => { + const logger = opts && opts.logger || console.log.bind(console, '[RELAY-NETWORK]'); + return next => req => { const start = new Date().getTime(); - - return next(req).then((res) => { + return next(req).then(res => { const end = new Date().getTime(); logger(`[${end - start}ms] ${req.getID()}`, req, res); return res; }); }; -} +} \ No newline at end of file diff --git a/src/middlewares/persistedQueries.js b/src/middlewares/persistedQueries.ts similarity index 63% rename from src/middlewares/persistedQueries.js rename to src/middlewares/persistedQueries.ts index 3485717..4d2fa26 100644 --- a/src/middlewares/persistedQueries.js +++ b/src/middlewares/persistedQueries.ts @@ -1,20 +1,15 @@ -// @flow /* eslint-disable no-console */ - -import type { Middleware, RelayRequestAny, MiddlewareNextFn } from '../definition'; -import type RelayResponse from '../RelayResponse'; - -type PersistedQueriesMiddlewareOptions = {| hash: string |}; - -async function makePersistedQueryRequestWithFallback( - o: { - req: RelayRequestAny, - next: MiddlewareNextFn, - options?: PersistedQueriesMiddlewareOptions, - }, - original = false, - hasRunFallback: boolean = false -): Promise { +import type { Middleware, RelayRequestAny, MiddlewareNextFn } from "../definition"; +import type RelayResponse from "../RelayResponse"; +type PersistedQueriesMiddlewareOptions = { + hash: string; +}; + +async function makePersistedQueryRequestWithFallback(o: { + req: RelayRequestAny; + next: MiddlewareNextFn; + options?: PersistedQueriesMiddlewareOptions; +}, original = false, hasRunFallback: boolean = false): Promise { const makeFallback = async (prevError: Error) => { if (hasRunFallback) { throw prevError; @@ -29,9 +24,11 @@ async function makePersistedQueryRequestWithFallback( // process it // If the backend rejects it we fallback to the original request (which has the text query) const persistedQueriesReq = JSON.parse(JSON.stringify(o.req)); - - const { cacheID, id, text: queryText } = persistedQueriesReq.operation; - + const { + cacheID, + id, + text: queryText + } = persistedQueriesReq.operation; const queryId = id || cacheID; if (!queryId && (!o.options?.hash || !queryText)) { @@ -43,9 +40,7 @@ async function makePersistedQueryRequestWithFallback( delete body.query; body.doc_id = queryId; persistedQueriesReq.fetchOpts.body = JSON.stringify(body); - delete persistedQueriesReq.operation.text; - return await o.next(original ? o.req : persistedQueriesReq); } catch (e) { return makeFallback(e); @@ -55,13 +50,10 @@ async function makePersistedQueryRequestWithFallback( return makeRequest(); } -export default function persistedQueriesMiddleware( - options?: PersistedQueriesMiddlewareOptions -): Middleware { - return (next) => (req) => - makePersistedQueryRequestWithFallback({ - req, - next, - options, - }); -} +export default function persistedQueriesMiddleware(options?: PersistedQueriesMiddlewareOptions): Middleware { + return next => req => makePersistedQueryRequestWithFallback({ + req, + next, + options + }); +} \ No newline at end of file diff --git a/src/middlewares/progress.js b/src/middlewares/progress.ts similarity index 50% rename from src/middlewares/progress.js rename to src/middlewares/progress.ts index 5aaf8a3..a1e9c6f 100644 --- a/src/middlewares/progress.js +++ b/src/middlewares/progress.ts @@ -1,44 +1,46 @@ -/* @flow */ /* eslint-disable no-await-in-loop */ - -import type { - MiddlewareRaw, - RelayRequestAny, - FetchResponse, - MiddlewareRawNextFn, -} from '../definition'; - +import type { MiddlewareRaw, RelayRequestAny, FetchResponse, MiddlewareRawNextFn } from "../definition"; export type ProgressOpts = { - sizeHeader?: string, - onProgress: (runningTotal: number, totalSize: ?number) => any, + sizeHeader?: string; + onProgress: (runningTotal: number, totalSize: number | null | undefined) => any; }; function createProgressHandler(opts: ProgressOpts) { - const { onProgress, sizeHeader = 'Content-Length' } = opts || {}; - + const { + onProgress, + sizeHeader = 'Content-Length' + } = opts || {}; return async (res: FetchResponse) => { - const { body, headers } = res; + const { + body, + headers + } = res; if (!body) { return; } const totalResponseSize = headers.get(sizeHeader); - let totalSize = null; + if (totalResponseSize !== null) { totalSize = parseInt(totalResponseSize, 10); } const reader = body.getReader(); - let completed = false; let runningTotal = 0; - do { - const step: { value: ?any, done: boolean } = await reader.read(); - const { done, value } = step; - const length = (value && value.length) || 0; + do { + const step: { + value: any | null | undefined; + done: boolean; + } = await reader.read(); + const { + done, + value + } = step; + const length = value && value.length || 0; completed = done; if (!completed) { @@ -52,14 +54,12 @@ function createProgressHandler(opts: ProgressOpts) { export default function progressMiddleware(opts: ProgressOpts): MiddlewareRaw { const progressHandler = createProgressHandler(opts); - const mw = - (next: MiddlewareRawNextFn) => - async (req: RelayRequestAny): Promise => { - const res: FetchResponse = await next(req); - progressHandler(res.clone()); - return res; - }; + const mw = (next: MiddlewareRawNextFn) => async (req: RelayRequestAny): Promise => { + const res: FetchResponse = await next(req); + progressHandler(res.clone()); + return res; + }; mw.isRawMiddleware = true; - return (mw: any); -} + return (mw as any); +} \ No newline at end of file diff --git a/src/middlewares/retry.js b/src/middlewares/retry.ts similarity index 66% rename from src/middlewares/retry.js rename to src/middlewares/retry.ts index 926f5ed..cd10895 100644 --- a/src/middlewares/retry.js +++ b/src/middlewares/retry.ts @@ -1,41 +1,32 @@ -/* @flow */ /* eslint-disable no-console */ - -import type { Middleware, RelayRequestAny, MiddlewareNextFn } from '../definition'; -import type RelayResponse from '../RelayResponse'; -import { isFunction } from '../utils'; -import RRNLError from '../RRNLError'; - +import type { Middleware, RelayRequestAny, MiddlewareNextFn } from "../definition"; +import type RelayResponse from "../RelayResponse"; +import { isFunction } from "../utils"; +import RRNLError from "../RRNLError"; export type RetryAfterFn = (attempt: number) => number | false; export type TimeoutAfterFn = (attempt: number) => number; -export type ForceRetryFn = (runNow: Function, delay: number) => any; -export type AbortFn = (msg: ?string) => any; - -export type BeforeRetryCb = (meta: {| - forceRetry: Function, - abort: AbortFn, - delay: number, - attempt: number, - lastError: ?Error, - req: RelayRequestAny, -|}) => any; - -export type StatusCheckFn = ( - statusCode: number, - req: RelayRequestAny, - res: RelayResponse -) => boolean; - -export type RetryMiddlewareOpts = {| - fetchTimeout?: number | TimeoutAfterFn, - retryDelays?: number[] | RetryAfterFn, - statusCodes?: number[] | false | StatusCheckFn, - logger?: Function | false, - allowMutations?: boolean, - allowFormData?: boolean, - forceRetry?: ForceRetryFn | false, // DEPRECATED in favor `beforeRetry` - beforeRetry?: BeforeRetryCb | false, -|}; +export type ForceRetryFn = (runNow: (...args: Array) => any, delay: number) => any; +export type AbortFn = (msg: string | null | undefined) => any; +export type BeforeRetryCb = (meta: { + forceRetry: (...args: Array) => any; + abort: AbortFn; + delay: number; + attempt: number; + lastError: Error | null | undefined; + req: RelayRequestAny; +}) => any; +export type StatusCheckFn = (statusCode: number, req: RelayRequestAny, res: RelayResponse) => boolean; +export type RetryMiddlewareOpts = { + fetchTimeout?: number | TimeoutAfterFn; + retryDelays?: number[] | RetryAfterFn; + statusCodes?: number[] | false | StatusCheckFn; + logger?: ((...args: Array) => any) | false; + allowMutations?: boolean; + allowFormData?: boolean; + forceRetry?: ForceRetryFn | false; + // DEPRECATED in favor `beforeRetry` + beforeRetry?: BeforeRetryCb | false; +}; function noopFn() {} @@ -44,27 +35,29 @@ export class RRNLRetryMiddlewareError extends RRNLError { super(msg); this.name = 'RRNLRetryMiddlewareError'; } -} +} export default function retryMiddleware(options?: RetryMiddlewareOpts): Middleware { const opts = options || {}; const timeout = opts.fetchTimeout || 15000; const retryDelays = opts.retryDelays || [1000, 3000]; const statusCodes = opts.statusCodes || false; - const logger = - opts.logger === false ? () => {} : opts.logger || console.log.bind(console, '[RELAY-NETWORK]'); + const logger = opts.logger === false ? () => {} : opts.logger || console.log.bind(console, '[RELAY-NETWORK]'); const allowMutations = opts.allowMutations || false; const allowFormData = opts.allowFormData || false; const forceRetryFn = opts.forceRetry || false; // DEPRECATED in favor `beforeRetry` + const beforeRetry = opts.beforeRetry || false; let retryAfterMs: RetryAfterFn = () => false; + if (retryDelays) { if (Array.isArray(retryDelays)) { - retryAfterMs = (attempt) => { + retryAfterMs = attempt => { if (retryDelays.length >= attempt) { return retryDelays[attempt]; } + return false; }; } else if (isFunction(retryDelays)) { @@ -73,6 +66,7 @@ export default function retryMiddleware(options?: RetryMiddlewareOpts): Middlewa } let timeoutAfterMs: TimeoutAfterFn; + if (typeof timeout === 'number') { timeoutAfterMs = () => timeout; } else { @@ -82,6 +76,7 @@ export default function retryMiddleware(options?: RetryMiddlewareOpts): Middlewa let retryOnStatusCode: StatusCheckFn = (status, req, res) => { return res.status < 200 || res.status > 300; }; + if (statusCodes) { if (Array.isArray(statusCodes)) { retryOnStatusCode = (status, req, res) => statusCodes.indexOf(res.status) !== -1; @@ -90,7 +85,7 @@ export default function retryMiddleware(options?: RetryMiddlewareOpts): Middlewa } } - return (next) => (req) => { + return next => req => { if (req.isMutation() && !allowMutations) { return next(req); } @@ -107,33 +102,30 @@ export default function retryMiddleware(options?: RetryMiddlewareOpts): Middlewa retryOnStatusCode, forceRetryFn, beforeRetry, - logger, + logger }); }; } -async function makeRetriableRequest( - o: { - req: RelayRequestAny, - next: MiddlewareNextFn, - timeoutAfterMs: TimeoutAfterFn, - retryAfterMs: RetryAfterFn, - retryOnStatusCode: StatusCheckFn, - forceRetryFn: ForceRetryFn | false, - beforeRetry: BeforeRetryCb | false, - logger: Function, - }, - delay: number = 0, - attempt: number = 0, - lastError: ?Error = null -): Promise { +async function makeRetriableRequest(o: { + req: RelayRequestAny; + next: MiddlewareNextFn; + timeoutAfterMs: TimeoutAfterFn; + retryAfterMs: RetryAfterFn; + retryOnStatusCode: StatusCheckFn; + forceRetryFn: ForceRetryFn | false; + beforeRetry: BeforeRetryCb | false; + logger: (...args: Array) => any; +}, delay: number = 0, attempt: number = 0, lastError: Error | null | undefined = null): Promise { const makeRetry = async (prevError: Error) => { const retryDelay = o.retryAfterMs(attempt); + if (retryDelay) { o.logger(prevError.message); o.logger(`will retry in ${retryDelay} milliseconds`); return makeRetriableRequest(o, retryDelay, attempt + 1, prevError); } + throw prevError; }; @@ -152,9 +144,7 @@ async function makeRetriableRequest( // response with invalid statusCode if (e && e.res && o.retryOnStatusCode(e.res.status, o.req, e.res)) { - const err = new RRNLRetryMiddlewareError( - `Wrong response status ${e.res.status}, retrying...` - ); + const err = new RRNLRetryMiddlewareError(`Wrong response status ${e.res.status}, retrying...`); return makeRetry(err); } @@ -167,7 +157,11 @@ async function makeRetriableRequest( return makeRequest(); } else { // second and all further calls should be delayed - const { promise, forceExec, abort } = delayedExecution(makeRequest, delay); + const { + promise, + forceExec, + abort + } = delayedExecution(makeRequest, delay); if (o.forceRetryFn) { // DEPRECATED in favor `beforeRetry` @@ -181,7 +175,7 @@ async function makeRetriableRequest( attempt, delay, lastError, - req: o.req, + req: o.req }); } @@ -193,13 +187,10 @@ async function makeRetriableRequest( * This function delays execution of some function for some period of time. * Returns a promise, with ability to run execution immidiately, or abort it. */ -export function delayedExecution( - execFn: () => Promise, - delay: number = 0 -): { - forceExec: () => any, - abort: () => any, - promise: Promise, +export function delayedExecution(execFn: () => Promise, delay: number = 0): { + forceExec: () => any; + abort: () => any; + promise: Promise; } { let abort = noopFn; let forceExec = noopFn; @@ -208,18 +199,19 @@ export function delayedExecution( return { abort, forceExec, - promise: execFn(), + promise: execFn() }; } const promise = new Promise((resolve, reject) => { let delayId; - abort = (msg) => { + abort = msg => { if (delayId) { clearTimeout(delayId); delayId = null; } + reject(new RRNLRetryMiddlewareError(msg || 'Aborted in beforeRetry() callback')); }; @@ -235,8 +227,11 @@ export function delayedExecution( resolve(execFn()); }, delay); }); - - return { forceExec, abort, promise }; + return { + forceExec, + abort, + promise + }; } /* @@ -244,11 +239,7 @@ export function delayedExecution( * if Promise completed in this period, then returns its result * if not - returns other Promise from onTimeout() callback */ -export function promiseWithTimeout( - promise: Promise, - timeoutMS: number, - onTimeout: () => Promise -): Promise { +export function promiseWithTimeout(promise: Promise, timeoutMS: number, onTimeout: () => Promise): Promise { if (!timeoutMS) { return promise; } @@ -257,15 +248,12 @@ export function promiseWithTimeout( const timeoutId = setTimeout(() => { onTimeout().then(resolve).catch(reject); }, timeoutMS); - - promise - .then((res) => { - clearTimeout(timeoutId); - resolve(res); - }) - .catch((err) => { - clearTimeout(timeoutId); - reject(err); - }); + promise.then(res => { + clearTimeout(timeoutId); + resolve(res); + }).catch(err => { + clearTimeout(timeoutId); + reject(err); + }); }); -} +} \ No newline at end of file diff --git a/src/middlewares/upload.js b/src/middlewares/upload.ts similarity index 69% rename from src/middlewares/upload.js rename to src/middlewares/upload.ts index 3d8d99f..fc7aede 100644 --- a/src/middlewares/upload.js +++ b/src/middlewares/upload.ts @@ -1,47 +1,40 @@ -/* @flow */ - -import { extractFiles } from 'extract-files'; - -import type { Middleware } from '../definition'; -import RelayRequestBatch from '../RelayRequestBatch'; - +import { extractFiles } from "extract-files"; +import type { Middleware } from "../definition"; +import RelayRequestBatch from "../RelayRequestBatch"; export default function uploadMiddleware(): Middleware { - return (next) => async (req) => { + return next => async req => { if (req instanceof RelayRequestBatch) { throw new Error('RelayRequestBatch is not supported'); } const operations = { query: req.operation.text, - variables: req.variables, + variables: req.variables }; - - const { clone: extractedOperations, files } = extractFiles(operations); + const { + clone: extractedOperations, + files + } = extractFiles(operations); if (files.size) { const formData = new FormData(); - formData.append('operations', JSON.stringify(extractedOperations)); - const pathMap = {}; let i = 0; - files.forEach((paths) => { + files.forEach(paths => { pathMap[++i] = paths; }); formData.append('map', JSON.stringify(pathMap)); - i = 0; files.forEach((paths, file) => { formData.append(++i, file, file.name); }); - req.fetchOpts.method = 'POST'; req.fetchOpts.body = formData; delete req.fetchOpts.headers['Content-Type']; } const res = await next(req); - return res; }; -} +} \ No newline at end of file diff --git a/src/middlewares/url.js b/src/middlewares/url.ts similarity index 52% rename from src/middlewares/url.js rename to src/middlewares/url.ts index a8ea6ff..193f1dc 100644 --- a/src/middlewares/url.js +++ b/src/middlewares/url.ts @@ -1,35 +1,37 @@ -/* @flow */ -/* eslint-disable no-param-reassign */ - -import { isFunction } from '../utils'; -import type RelayRequest from '../RelayRequest'; -import type { Middleware, FetchOpts } from '../definition'; - -type Headers = { [name: string]: string }; +import { $PropertyType } from "utility-types"; +/* eslint-disable no-param-reassign */ +import { isFunction } from "../utils"; +import type RelayRequest from "../RelayRequest"; +import type { Middleware, FetchOpts } from "../definition"; +type Headers = Record; export type UrlMiddlewareOpts = { - url: string | Promise | ((req: RelayRequest) => string | Promise), - method?: 'POST' | 'GET', - headers?: Headers | Promise | ((req: RelayRequest) => Headers | Promise), + url: string | Promise | ((req: RelayRequest) => string | Promise); + method?: "POST" | "GET"; + headers?: Headers | Promise | ((req: RelayRequest) => Headers | Promise); // Avaliable request modes in fetch options. For details see https://fetch.spec.whatwg.org/#requests - credentials?: $PropertyType, - mode?: $PropertyType, - cache?: $PropertyType, - redirect?: $PropertyType, + credentials?: $PropertyType; + mode?: $PropertyType; + cache?: $PropertyType; + redirect?: $PropertyType; }; - export default function urlMiddleware(opts?: UrlMiddlewareOpts): Middleware { - const { url, headers, method = 'POST', credentials, mode, cache, redirect } = opts || {}; + const { + url, + headers, + method = 'POST', + credentials, + mode, + cache, + redirect + } = opts || {}; const urlOrThunk: any = url || '/graphql'; const headersOrThunk: any = headers; - - return (next) => async (req) => { + return next => async req => { req.fetchOpts.url = await (isFunction(urlOrThunk) ? urlOrThunk(req) : urlOrThunk); if (headersOrThunk) { - req.fetchOpts.headers = await (isFunction(headersOrThunk) - ? headersOrThunk(req) - : headersOrThunk); + req.fetchOpts.headers = await (isFunction(headersOrThunk) ? headersOrThunk(req) : headersOrThunk); } if (method) req.fetchOpts.method = method; @@ -37,7 +39,6 @@ export default function urlMiddleware(opts?: UrlMiddlewareOpts): Middleware { if (mode) req.fetchOpts.mode = mode; if (cache) req.fetchOpts.cache = cache; if (redirect) req.fetchOpts.redirect = redirect; - return next(req); }; -} +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.ts similarity index 54% rename from src/utils.js rename to src/utils.ts index 4b642b0..e44f1ed 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -1,6 +1,4 @@ -/* @flow */ /* eslint-disable */ - -export function isFunction(obj: any): boolean %checks { +export function isFunction(obj: any): boolean { return !!(obj && obj.constructor && obj.call && obj.apply); -} +} \ No newline at end of file