UNPKG

apollo-datasource-http

Version:

[![CI](https://github.com/StarpTech/apollo-datasource-http/actions/workflows/ci.yml/badge.svg)](https://github.com/StarpTech/apollo-datasource-http/actions/workflows/ci.yml)

268 lines 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HTTPDataSource = exports.RequestError = void 0; const apollo_datasource_1 = require("apollo-datasource"); const undici_1 = require("undici"); const http_1 = require("http"); const quick_lru_1 = __importDefault(require("@alloc/quick-lru")); const zlib_1 = require("zlib"); const stream_to_promise_1 = __importDefault(require("stream-to-promise")); const apollo_server_errors_1 = require("apollo-server-errors"); const url_1 = require("url"); class RequestError extends Error { constructor(message, code, request, response) { super(message); this.message = message; this.code = code; this.request = request; this.response = response; this.name = 'RequestError'; } } exports.RequestError = RequestError; const statusCodeCacheableByDefault = new Set([200, 203]); class HTTPDataSource extends apollo_datasource_1.DataSource { constructor(baseURL, options) { var _a, _b, _c, _d, _e; super(); this.baseURL = baseURL; this.options = options; this.memoizedResults = new quick_lru_1.default({ maxSize: ((_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.lru) === null || _b === void 0 ? void 0 : _b.maxSize) ? this.options.lru.maxSize : 100, maxAge: (_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.lru) === null || _d === void 0 ? void 0 : _d.maxAge, }); this.pool = (_e = options === null || options === void 0 ? void 0 : options.pool) !== null && _e !== void 0 ? _e : new undici_1.Pool(this.baseURL, options === null || options === void 0 ? void 0 : options.clientOptions); this.globalRequestOptions = options === null || options === void 0 ? void 0 : options.requestOptions; this.logger = options === null || options === void 0 ? void 0 : options.logger; } buildQueryString(query) { const params = new url_1.URLSearchParams(); for (const key in query) { if (Object.prototype.hasOwnProperty.call(query, key)) { const value = query[key]; if (value !== undefined) { params.append(key, value.toString()); } } } params.sort(); return params.toString(); } initialize(config) { this.context = config.context; this.cache = config.cache; } isResponseOk(statusCode) { return statusCode >= 200 && statusCode <= 399; } isResponseCacheable(request, response) { return statusCodeCacheableByDefault.has(response.statusCode) && this.isRequestCacheable(request); } isRequestCacheable(request) { return request.method === 'GET'; } isRequestMemoizable(request) { return Boolean(request.memoize) && request.method === 'GET'; } onCacheKeyCalculation(request) { return request.origin + request.path; } onResponse(request, response) { if (this.isResponseOk(response.statusCode)) { return response; } throw new RequestError(`Response code ${response.statusCode} (${http_1.STATUS_CODES[response.statusCode.toString()]})`, response.statusCode, request, response); } async get(path, requestOptions) { return this.request({ headers: {}, query: {}, body: null, memoize: true, context: {}, ...requestOptions, method: 'GET', path, origin: this.baseURL, }); } async post(path, requestOptions) { return this.request({ headers: {}, query: {}, body: null, context: {}, ...requestOptions, method: 'POST', path, origin: this.baseURL, }); } async delete(path, requestOptions) { return this.request({ headers: {}, query: {}, body: null, context: {}, ...requestOptions, method: 'DELETE', path, origin: this.baseURL, }); } async put(path, requestOptions) { return this.request({ headers: {}, query: {}, body: null, context: {}, ...requestOptions, method: 'PUT', path, origin: this.baseURL, }); } async patch(path, requestOptions) { return this.request({ headers: {}, query: {}, body: null, context: {}, ...requestOptions, method: 'PATCH', path, origin: this.baseURL, }); } async performRequest(request, cacheKey) { var _a, _b, _c; try { if (request.body !== null && typeof request.body === 'object') { if (request.headers['content-type'] === undefined) { request.headers['content-type'] = 'application/json; charset=utf-8'; } request.body = JSON.stringify(request.body); } await ((_a = this.onRequest) === null || _a === void 0 ? void 0 : _a.call(this, request)); const requestOptions = { method: request.method, origin: request.origin, path: request.path, headers: request.headers, signal: request.signal, body: request.body, }; const responseData = await this.pool.request(requestOptions); const body = responseData.body; const headers = responseData.headers; let dataBuffer; switch (headers['content-encoding']) { case 'br': dataBuffer = await (0, stream_to_promise_1.default)(body.pipe((0, zlib_1.createBrotliDecompress)())); break; case 'gzip': case 'deflate': dataBuffer = await (0, stream_to_promise_1.default)(body.pipe((0, zlib_1.createUnzip)())); break; default: dataBuffer = await (0, stream_to_promise_1.default)(body); break; } let data = dataBuffer.toString('utf-8'); if (((_b = responseData.headers['content-type']) === null || _b === void 0 ? void 0 : _b.includes('application/json')) && data.length && typeof data === 'string') { data = JSON.parse(data); } const response = { isFromCache: false, memoized: false, ...responseData, body: data, }; this.onResponse(request, response); if (this.isRequestMemoizable(request)) { this.memoizedResults.set(cacheKey, response); } if (request.requestCache && this.isResponseCacheable(request, response)) { response.maxTtl = request.requestCache.maxTtl; const cachedResponse = JSON.stringify(response); this.cache .set(cacheKey, cachedResponse, { ttl: request.requestCache.maxTtl, }) .catch((err) => { var _a; return (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(err); }); this.cache .set(`staleIfError:${cacheKey}`, cachedResponse, { ttl: request.requestCache.maxTtl + request.requestCache.maxTtlIfError, }) .catch((err) => { var _a; return (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(err); }); } return response; } catch (error) { (_c = this.onError) === null || _c === void 0 ? void 0 : _c.call(this, error, request); if (request.requestCache) { const cacheItem = await this.cache.get(`staleIfError:${cacheKey}`); if (cacheItem) { const response = JSON.parse(cacheItem); response.isFromCache = true; return response; } } throw (0, apollo_server_errors_1.toApolloError)(error); } } async request(request) { var _a, _b; if (Object.keys(request.query).length > 0) { request.path = request.path + '?' + this.buildQueryString(request.query); } const cacheKey = this.onCacheKeyCalculation(request); const isRequestMemoizable = this.isRequestMemoizable(request); if (isRequestMemoizable) { if (this.memoizedResults.has(cacheKey)) { const response = await this.memoizedResults.get(cacheKey); response.memoized = true; response.isFromCache = false; return response; } } const headers = { ...(((_a = this.globalRequestOptions) === null || _a === void 0 ? void 0 : _a.headers) || {}), ...request.headers, }; const options = { ...request, ...this.globalRequestOptions, headers, }; const requestIsCacheable = this.isRequestCacheable(request); if (requestIsCacheable) { if (request.requestCache) { try { const cacheItem = await this.cache.get(cacheKey); if (cacheItem) { const cachedResponse = JSON.parse(cacheItem); cachedResponse.memoized = false; cachedResponse.isFromCache = true; return cachedResponse; } const response = this.performRequest(options, cacheKey); return response; } catch (error) { (_b = this.logger) === null || _b === void 0 ? void 0 : _b.error(`Cache item '${cacheKey}' could not be loaded: ${error.message}`); } } const response = this.performRequest(options, cacheKey); return response; } return this.performRequest(options, cacheKey); } } exports.HTTPDataSource = HTTPDataSource; //# sourceMappingURL=http-data-source.js.map