apollo-datasource-http
Advanced tools
Comparing version 0.5.0 to 0.6.0
import { DataSource, DataSourceConfig } from 'apollo-datasource'; | ||
import { RequestError, NormalizedOptions, OptionsOfJSONResponseBody, Response, GotReturn as Request, PlainResponse } from 'got'; | ||
export declare type RequestOptions = OptionsOfJSONResponseBody | NormalizedOptions; | ||
import { Client, Pool } from 'undici'; | ||
import { DispatchOptions, ResponseData } from 'undici/types/dispatcher'; | ||
export declare type CacheTTLOptions = { | ||
requestCache?: { | ||
maxTtl: number; | ||
maxTtlIfError: number; | ||
}; | ||
}; | ||
export declare type RequestOptions = Omit<DispatchOptions, 'origin' | 'path' | 'method'> & CacheTTLOptions; | ||
declare type InternalRequestOptions = DispatchOptions & CacheTTLOptions; | ||
export declare type Response<TResult> = { | ||
body: TResult; | ||
} & Omit<ResponseData, 'body'>; | ||
export interface LRUOptions { | ||
@@ -9,28 +20,33 @@ readonly maxAge?: number; | ||
export interface HTTPDataSourceOptions { | ||
requestOptions?: Partial<RequestOptions>; | ||
pool?: Pool; | ||
requestOptions?: RequestOptions; | ||
clientOptions?: Client.Options; | ||
lru?: Partial<LRUOptions>; | ||
} | ||
export declare abstract class HTTPDataSource<TContext = any> extends DataSource { | ||
readonly baseURL: string; | ||
private readonly options?; | ||
private static readonly agents; | ||
baseURL?: string; | ||
context: TContext; | ||
private storageAdapter; | ||
private readonly pool; | ||
private readonly globalRequestOptions?; | ||
private readonly abortController; | ||
private readonly memoizedResults; | ||
constructor(options?: HTTPDataSourceOptions | undefined); | ||
constructor(baseURL: string, options?: HTTPDataSourceOptions | undefined); | ||
initialize(config: DataSourceConfig<TContext>): void; | ||
abort(): void; | ||
protected isResponseOk(response: PlainResponse): boolean; | ||
protected onCacheKeyCalculation(requestOptions: RequestOptions): string; | ||
protected isResponseOk(statusCode: number): boolean; | ||
protected isResponseCacheable<TResult = unknown>(requestOptions: InternalRequestOptions, response: Response<TResult>): boolean; | ||
protected onCacheKeyCalculation(requestOptions: InternalRequestOptions): string; | ||
protected onRequest?(requestOptions: RequestOptions): void; | ||
protected onResponse<TResult = unknown>(_request: Request, response: Response<TResult>): Response<TResult>; | ||
protected onError?(_error: RequestError): void; | ||
protected get<TResult = unknown>(url: string, requestOptions?: RequestOptions): Promise<Response<TResult>>; | ||
protected post<TResult = unknown>(url: string, requestOptions?: RequestOptions): Promise<Response<TResult>>; | ||
protected delete<TResult = unknown>(url: string, requestOptions?: RequestOptions): Promise<Response<TResult>>; | ||
protected put<TResult = unknown>(url: string, requestOptions?: RequestOptions): Promise<Response<TResult>>; | ||
protected onResponse<TResult = unknown>(response: Response<TResult>): Response<TResult>; | ||
protected onError?(_error: Error): void; | ||
protected get<TResult = unknown>(path: string, requestOptions?: RequestOptions): Promise<Response<TResult>>; | ||
protected post<TResult = unknown>(path: string, requestOptions?: RequestOptions): Promise<Response<TResult>>; | ||
protected delete<TResult = unknown>(path: string, requestOptions?: RequestOptions): Promise<Response<TResult>>; | ||
protected put<TResult = unknown>(path: string, requestOptions?: RequestOptions): Promise<Response<TResult>>; | ||
private performRequest; | ||
private request; | ||
} | ||
export {}; | ||
//# sourceMappingURL=http-data-source.d.ts.map |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -27,9 +8,9 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
const apollo_datasource_1 = require("apollo-datasource"); | ||
const got_1 = __importStar(require("got")); | ||
const undici_1 = require("undici"); | ||
const http_1 = require("http"); | ||
const quick_lru_1 = __importDefault(require("@alloc/quick-lru")); | ||
const secure_json_parse_1 = __importDefault(require("secure-json-parse")); | ||
const abort_controller_1 = __importDefault(require("abort-controller")); | ||
const agentkeepalive_1 = __importDefault(require("agentkeepalive")); | ||
const keyv_1 = __importDefault(require("keyv")); | ||
const apollo_server_errors_1 = require("apollo-server-errors"); | ||
const keyv_1 = __importDefault(require("keyv")); | ||
const { HttpsAgent } = agentkeepalive_1.default; | ||
function apolloKeyValueCacheToKeyv(cache) { | ||
@@ -57,5 +38,7 @@ return { | ||
} | ||
const cacheableStatusCodes = [200, 201, 202, 203, 206]; | ||
class HTTPDataSource extends apollo_datasource_1.DataSource { | ||
constructor(options) { | ||
constructor(baseURL, options) { | ||
super(); | ||
this.baseURL = baseURL; | ||
this.options = options; | ||
@@ -65,2 +48,4 @@ this.memoizedResults = new quick_lru_1.default({ | ||
}); | ||
this.pool = options?.pool ?? new undici_1.Pool(this.baseURL, options?.clientOptions); | ||
this.globalRequestOptions = options?.requestOptions; | ||
this.abortController = new abort_controller_1.default(); | ||
@@ -77,114 +62,113 @@ } | ||
} | ||
isResponseOk(response) { | ||
const { statusCode } = response; | ||
const limitStatusCode = response.request.options.followRedirect ? 299 : 399; | ||
return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304; | ||
isResponseOk(statusCode) { | ||
return (statusCode >= 200 && statusCode <= 399) || statusCode === 304; | ||
} | ||
isResponseCacheable(requestOptions, response) { | ||
return cacheableStatusCodes.indexOf(response.statusCode) > -1 && requestOptions.method === 'GET'; | ||
} | ||
onCacheKeyCalculation(requestOptions) { | ||
if (requestOptions.url) | ||
return requestOptions.url.toString(); | ||
throw new Error('No Cache key provided'); | ||
return requestOptions.origin + requestOptions.path; | ||
} | ||
onResponse(_request, response) { | ||
if (this.isResponseOk(response)) { | ||
onResponse(response) { | ||
if (this.isResponseOk(response.statusCode)) { | ||
return response; | ||
} | ||
throw new got_1.HTTPError(response); | ||
throw new apollo_server_errors_1.ApolloError(`Response code ${response.statusCode} (${http_1.STATUS_CODES[response.statusCode]})`, response.statusCode.toString()); | ||
} | ||
async get(url, requestOptions) { | ||
return await this.request(url, { | ||
async get(path, requestOptions) { | ||
return await this.request({ | ||
...requestOptions, | ||
method: 'GET', | ||
...requestOptions, | ||
path, | ||
origin: this.baseURL, | ||
}); | ||
} | ||
async post(url, requestOptions) { | ||
return await this.request(url, { | ||
async post(path, requestOptions) { | ||
return await this.request({ | ||
...requestOptions, | ||
method: 'POST', | ||
...requestOptions, | ||
path, | ||
origin: this.baseURL, | ||
}); | ||
} | ||
async delete(url, requestOptions) { | ||
return await this.request(url, { | ||
async delete(path, requestOptions) { | ||
return await this.request({ | ||
...requestOptions, | ||
method: 'DELETE', | ||
...requestOptions, | ||
path, | ||
origin: this.baseURL, | ||
}); | ||
} | ||
async put(url, requestOptions) { | ||
return await this.request(url, { | ||
async put(path, requestOptions) { | ||
return await this.request({ | ||
...requestOptions, | ||
method: 'PUT', | ||
...requestOptions, | ||
path, | ||
origin: this.baseURL, | ||
}); | ||
} | ||
async performRequest(options) { | ||
async performRequest(options, cacheKey) { | ||
this.onRequest?.(options); | ||
const cancelableRequest = got_1.default(options); | ||
const abort = () => { | ||
cancelableRequest.cancel('abortController'); | ||
}; | ||
this.abortController.signal.addEventListener('abort', abort); | ||
try { | ||
const response = await cancelableRequest; | ||
this.abortController.signal.removeEventListener('abort', abort); | ||
this.onResponse(response.request, response); | ||
const responseData = await this.pool.request(options); | ||
responseData.body.setEncoding('utf8'); | ||
let data = ''; | ||
for await (const chunk of responseData.body) { | ||
data += chunk; | ||
} | ||
let json; | ||
if (data) { | ||
json = secure_json_parse_1.default.parse(data); | ||
} | ||
const response = { | ||
...responseData, | ||
body: json, | ||
}; | ||
this.onResponse(response); | ||
if (options.requestCache && this.isResponseCacheable(options, response)) { | ||
this.storageAdapter.set(cacheKey, response, options.requestCache?.maxTtl); | ||
this.storageAdapter.set(`staleIfError:${cacheKey}`, response, options.requestCache?.maxTtlIfError); | ||
} | ||
return response; | ||
} | ||
catch (error) { | ||
let error_ = error; | ||
if (error instanceof got_1.HTTPError) { | ||
if (error.response.statusCode === 401) { | ||
const err = new apollo_server_errors_1.AuthenticationError(error.message); | ||
err.originalError = error; | ||
error_ = err; | ||
this.onError?.(error); | ||
if (options.requestCache) { | ||
const hasFallback = await this.storageAdapter.get(`staleIfError:${cacheKey}`); | ||
if (hasFallback) { | ||
return hasFallback; | ||
} | ||
else if (error.response.statusCode === 403) { | ||
const err = new apollo_server_errors_1.ForbiddenError(error.message); | ||
err.originalError = error; | ||
error_ = err; | ||
} | ||
else { | ||
const err = new apollo_server_errors_1.ApolloError(error.message, error.code); | ||
err.originalError = error; | ||
error_ = err; | ||
} | ||
} | ||
this.onError?.(error); | ||
this.abortController.signal.removeEventListener('abort', abort); | ||
throw error_; | ||
throw error; | ||
} | ||
} | ||
async request(path, requestOptions) { | ||
const options = got_1.default.mergeOptions({ | ||
cache: this.storageAdapter, | ||
path, | ||
responseType: 'json', | ||
throwHttpErrors: false, | ||
timeout: 5000, | ||
agent: HTTPDataSource.agents, | ||
prefixUrl: this.baseURL, | ||
}, { | ||
...this.options?.requestOptions, | ||
}, requestOptions); | ||
const cacheKey = this.onCacheKeyCalculation(options); | ||
async request(requestOptions) { | ||
const cacheKey = this.onCacheKeyCalculation(requestOptions); | ||
const ttlCacheEnabled = requestOptions.requestCache; | ||
if (requestOptions.method === 'GET' && ttlCacheEnabled) { | ||
const cachedResponse = await this.storageAdapter.get(cacheKey); | ||
if (cachedResponse) { | ||
return cachedResponse; | ||
} | ||
} | ||
const options = { | ||
...this.globalRequestOptions, | ||
...requestOptions, | ||
signal: this.abortController.signal, | ||
}; | ||
if (options.method === 'GET') { | ||
const cachedResponse = this.memoizedResults.get(cacheKey); | ||
if (cachedResponse) | ||
if (cachedResponse) { | ||
return cachedResponse; | ||
const response = await this.performRequest(options); | ||
this.memoizedResults.set(cacheKey, response); | ||
} | ||
const response = await this.performRequest(options, cacheKey); | ||
if (this.isResponseCacheable(options, response)) { | ||
this.memoizedResults.set(cacheKey, response); | ||
} | ||
return response; | ||
} | ||
return this.performRequest(options); | ||
return this.performRequest(options, cacheKey); | ||
} | ||
} | ||
exports.HTTPDataSource = HTTPDataSource; | ||
HTTPDataSource.agents = { | ||
http: new agentkeepalive_1.default({ | ||
keepAlive: true, | ||
scheduling: 'lifo', | ||
}), | ||
https: new HttpsAgent({ | ||
keepAlive: true, | ||
scheduling: 'lifo', | ||
}), | ||
}; | ||
//# sourceMappingURL=http-data-source.js.map |
@@ -1,3 +0,2 @@ | ||
export { HTTPDataSource, HTTPDataSourceOptions, LRUOptions, RequestOptions } from './http-data-source'; | ||
export { Response, CacheError, CancelError, MaxRedirectsError, ReadError, ParseError, UploadError, HTTPError, TimeoutError, RequestError, UnsupportedProtocolError, GotReturn as Request } from 'got'; | ||
export { HTTPDataSource, HTTPDataSourceOptions, LRUOptions, RequestOptions, Response } from './http-data-source'; | ||
//# sourceMappingURL=index.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.UnsupportedProtocolError = exports.RequestError = exports.TimeoutError = exports.HTTPError = exports.UploadError = exports.ParseError = exports.ReadError = exports.MaxRedirectsError = exports.CancelError = exports.CacheError = exports.HTTPDataSource = void 0; | ||
exports.HTTPDataSource = void 0; | ||
var http_data_source_1 = require("./http-data-source"); | ||
Object.defineProperty(exports, "HTTPDataSource", { enumerable: true, get: function () { return http_data_source_1.HTTPDataSource; } }); | ||
var got_1 = require("got"); | ||
Object.defineProperty(exports, "CacheError", { enumerable: true, get: function () { return got_1.CacheError; } }); | ||
Object.defineProperty(exports, "CancelError", { enumerable: true, get: function () { return got_1.CancelError; } }); | ||
Object.defineProperty(exports, "MaxRedirectsError", { enumerable: true, get: function () { return got_1.MaxRedirectsError; } }); | ||
Object.defineProperty(exports, "ReadError", { enumerable: true, get: function () { return got_1.ReadError; } }); | ||
Object.defineProperty(exports, "ParseError", { enumerable: true, get: function () { return got_1.ParseError; } }); | ||
Object.defineProperty(exports, "UploadError", { enumerable: true, get: function () { return got_1.UploadError; } }); | ||
Object.defineProperty(exports, "HTTPError", { enumerable: true, get: function () { return got_1.HTTPError; } }); | ||
Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return got_1.TimeoutError; } }); | ||
Object.defineProperty(exports, "RequestError", { enumerable: true, get: function () { return got_1.RequestError; } }); | ||
Object.defineProperty(exports, "UnsupportedProtocolError", { enumerable: true, get: function () { return got_1.UnsupportedProtocolError; } }); | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "apollo-datasource-http", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"author": "Dustin Deus <deusdustin@gmail.com>", | ||
@@ -54,9 +54,9 @@ "license": "MIT", | ||
"abort-controller": "^3.0.0", | ||
"agentkeepalive": "^4.1.4", | ||
"apollo-datasource": "^0.9.0", | ||
"apollo-server-caching": "^0.7.0", | ||
"apollo-server-errors": "^2.5.0", | ||
"got": "^11.8.2", | ||
"graphql": "^15.5.0", | ||
"keyv": "^4.0.3" | ||
"keyv": "^4.0.3", | ||
"secure-json-parse": "^2.4.0", | ||
"undici": "^4.1.0" | ||
}, | ||
@@ -63,0 +63,0 @@ "devDependencies": { |
@@ -5,13 +5,6 @@ # Apollo HTTP Data Source | ||
Optimized HTTP Data Source for Apollo Server | ||
Optimized JSON HTTP Data Source for Apollo Server | ||
- JSON by default | ||
- HTTP/1 [Keep-alive agents](https://github.com/node-modules/agentkeepalive) for socket reuse | ||
- HTTP/2 support (requires Node.js 15.10.0 or newer) | ||
- Uses [Got](https://github.com/sindresorhus/got) a modern HTTP Client shipped with: | ||
- Retry mechanism | ||
- Request cancellation | ||
- Timeout handling | ||
- RFC 7234 compliant HTTP caching | ||
- Request Deduplication and a Resource Cache | ||
- Uses [Undici](https://github.com/nodejs/undici) under the hood | ||
- Request Deduplication (LRU), Request Cache (TTL) and `stale-if-error` Cache (TTL) | ||
- Support [AbortController ](https://github.com/mysticatea/abort-controller) to manually cancel all running requests | ||
@@ -35,2 +28,6 @@ - Support for [Apollo Cache Storage backend](https://www.apollographql.com/docs/apollo-server/data/data-sources/#using-memcachedredis-as-a-cache-storage-backend) | ||
```ts | ||
// instantiate a pool outside of your hotpath | ||
const baseURL = 'https://movies-api.example.com' | ||
const pool = new Pool(baseURL) | ||
const server = new ApolloServer({ | ||
@@ -41,3 +38,3 @@ typeDefs, | ||
return { | ||
moviesAPI: new MoviesAPI(), | ||
moviesAPI: new MoviesAPI(baseURL, pool), | ||
} | ||
@@ -51,18 +48,21 @@ }, | ||
```ts | ||
import { HTTPDataSource } from "apollo-datasource-http"; | ||
import { Pool } from 'undici' | ||
import { HTTPDataSource } from 'apollo-datasource-http' | ||
const datasource = new (class MoviesAPI extends HTTPDataSource { | ||
constructor() { | ||
constructor(baseURL: string, pool: Pool) { | ||
// global client options | ||
super({ | ||
super(baseURL, { | ||
pool, | ||
clientOptions: { | ||
bodyTimeout: 100, | ||
headersTimeout: 100, | ||
}, | ||
requestOptions: { | ||
timeout: 2000, | ||
http2: true, | ||
headers: { | ||
"X-Client": "client", | ||
'X-Client': 'client', | ||
}, | ||
}, | ||
}); | ||
this.baseURL = "https://movies-api.example.com"; | ||
}) | ||
}) | ||
} | ||
@@ -72,15 +72,13 @@ onCacheKeyCalculation(requestOptions: RequestOptions): string { | ||
} | ||
onRequest(requestOptions: RequestOptions): void { | ||
// manipulate request | ||
// manipulate request before it is send | ||
} | ||
onResponse<TResult = unknown>( | ||
request: Request, | ||
response: Response<TResult>, | ||
): void { | ||
onResponse<TResult = unknown>(request: Request, response: Response<TResult>): void { | ||
// manipulate response or handle unsuccessful response in a different way | ||
return super.onResponse(request, response) | ||
} | ||
onError( | ||
error: RequestError | ||
): void { | ||
onError(error: RequestError): void { | ||
// log errors | ||
@@ -92,10 +90,9 @@ } | ||
headers: { | ||
"X-Foo": "bar", | ||
}, | ||
timeout: 3000, | ||
}); | ||
'X-Foo': 'bar', | ||
} | ||
}) | ||
} | ||
} | ||
})() | ||
// cancel all running requests e.g when request is closed prematurely | ||
// cancel all running requests e.g when the request is closed prematurely | ||
datasource.abort() | ||
@@ -113,2 +110,13 @@ ``` | ||
The http client throws for unsuccessful responses (statusCode >= 400). In case of an request error `onError` is executed. By default the error is rethrown as an instance of `ApolloError`. | ||
The http client throws for unsuccessful responses (statusCode >= 400). In case of an request error `onError` is executed. By default the error is rethrown in form of the original error. | ||
## Production checklist | ||
This setup is in use with Redis. If you use Redis ensure that limits are set: | ||
``` | ||
maxmemory 10mb | ||
maxmemory-policy allkeys-lru | ||
``` | ||
This will limit the cache to 10MB and removes the least recently used keys from the cache when the cache hits the limits. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
115
23193
225
2
+ Addedsecure-json-parse@^2.4.0
+ Addedundici@^4.1.0
+ Addedsecure-json-parse@2.7.0(transitive)
+ Addedundici@4.16.0(transitive)
- Removedagentkeepalive@^4.1.4
- Removedgot@^11.8.2
- Removed@sindresorhus/is@4.6.0(transitive)
- Removed@szmarczak/http-timer@4.0.6(transitive)
- Removed@types/cacheable-request@6.0.3(transitive)
- Removed@types/http-cache-semantics@4.0.4(transitive)
- Removed@types/keyv@3.1.4(transitive)
- Removed@types/node@22.7.4(transitive)
- Removed@types/responselike@1.0.3(transitive)
- Removedagentkeepalive@4.5.0(transitive)
- Removedcacheable-lookup@5.0.4(transitive)
- Removedcacheable-request@7.0.4(transitive)
- Removedclone-response@1.0.3(transitive)
- Removeddecompress-response@6.0.0(transitive)
- Removeddefer-to-connect@2.0.1(transitive)
- Removedend-of-stream@1.4.4(transitive)
- Removedget-stream@5.2.0(transitive)
- Removedgot@11.8.6(transitive)
- Removedhttp-cache-semantics@4.1.1(transitive)
- Removedhttp2-wrapper@1.0.3(transitive)
- Removedhumanize-ms@1.2.1(transitive)
- Removedlowercase-keys@2.0.0(transitive)
- Removedmimic-response@1.0.13.1.0(transitive)
- Removedms@2.1.3(transitive)
- Removednormalize-url@6.1.0(transitive)
- Removedonce@1.4.0(transitive)
- Removedp-cancelable@2.1.1(transitive)
- Removedpump@3.0.2(transitive)
- Removedquick-lru@5.1.1(transitive)
- Removedresolve-alpn@1.2.1(transitive)
- Removedresponselike@2.0.1(transitive)
- Removedundici-types@6.19.8(transitive)
- Removedwrappy@1.0.2(transitive)