Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@apollo/datasource-rest

Package Overview
Dependencies
Maintainers
4
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@apollo/datasource-rest - npm Package Compare versions

Comparing version 4.3.2 to 5.0.0

15

dist/HTTPCache.d.ts

@@ -0,4 +1,10 @@

/// <reference types="node" />
import type { Options as HttpCacheSemanticsOptions } from 'http-cache-semantics';
import type { Fetcher, FetcherResponse, FetcherRequestInit } from '@apollo/utils.fetcher';
import { KeyValueCache } from '@apollo/utils.keyvaluecache';
import type { CacheOptions, RequestOptions } from './RESTDataSource';
import type { CacheOptions, RequestOptions, ValueOrPromise } from './RESTDataSource';
interface ResponseWithCacheWritePromise {
response: FetcherResponse;
cacheWritePromise?: Promise<void>;
}
export declare class HTTPCache {

@@ -10,6 +16,9 @@ private keyValueCache;

cacheKey?: string;
cacheOptions?: CacheOptions | ((url: string, response: FetcherResponse, request: RequestOptions) => CacheOptions | undefined);
}): Promise<FetcherResponse>;
cacheOptions?: CacheOptions | ((url: string, response: FetcherResponse, request: RequestOptions) => ValueOrPromise<CacheOptions | undefined>);
httpCacheSemanticsCachePolicyOptions?: HttpCacheSemanticsOptions;
}): Promise<ResponseWithCacheWritePromise>;
private storeResponseAndReturnClone;
private readResponseAndWriteToCache;
}
export {};
//# sourceMappingURL=HTTPCache.d.ts.map

86

dist/HTTPCache.js

@@ -43,6 +43,9 @@ "use strict";

const cacheKey = (_b = cache === null || cache === void 0 ? void 0 : cache.cacheKey) !== null && _b !== void 0 ? _b : urlString;
if (requestOpts.method === 'HEAD') {
return { response: await this.httpFetch(urlString, requestOpts) };
}
const entry = await this.keyValueCache.get(cacheKey);
if (!entry) {
const response = await this.httpFetch(urlString, requestOpts);
const policy = new http_cache_semantics_1.default(policyRequestFrom(urlString, requestOpts), policyResponseFrom(response));
const policy = new http_cache_semantics_1.default(policyRequestFrom(urlString, requestOpts), policyResponseFrom(response), cache === null || cache === void 0 ? void 0 : cache.httpCacheSemanticsCachePolicyOptions);
return this.storeResponseAndReturnClone(urlString, response, requestOpts, policy, cacheKey, cache === null || cache === void 0 ? void 0 : cache.cacheOptions);

@@ -52,2 +55,3 @@ }

const policy = http_cache_semantics_1.default.fromObject(policyRaw);
const urlFromPolicy = policy._url;
policy._url = undefined;

@@ -58,7 +62,9 @@ if ((ttlOverride && policy.age() < ttlOverride) ||

const headers = policy.responseHeaders();
return new node_fetch_1.Response(body, {
url: policy._url,
status: policy._status,
headers,
});
return {
response: new node_fetch_1.Response(body, {
url: urlFromPolicy,
status: policy._status,
headers: cachePolicyHeadersToNodeFetchHeadersInit(headers),
}),
};
}

@@ -69,3 +75,3 @@ else {

...requestOpts,
headers: revalidationHeaders,
headers: cachePolicyHeadersToFetcherHeadersInit(revalidationHeaders),
};

@@ -77,3 +83,3 @@ const revalidationResponse = await this.httpFetch(urlString, revalidationRequest);

status: revalidatedPolicy._status,
headers: revalidatedPolicy.responseHeaders(),
headers: cachePolicyHeadersToNodeFetchHeadersInit(revalidatedPolicy.responseHeaders()),
}), requestOpts, revalidatedPolicy, cacheKey, cache === null || cache === void 0 ? void 0 : cache.cacheOptions);

@@ -84,3 +90,3 @@ }

if (typeof cacheOptions === 'function') {
cacheOptions = cacheOptions(url, response, request);
cacheOptions = await cacheOptions(url, response, request);
}

@@ -90,3 +96,3 @@ let ttlOverride = cacheOptions === null || cacheOptions === void 0 ? void 0 : cacheOptions.ttl;

!(request.method === 'GET' && policy.storable())) {
return response;
return { response };
}

@@ -97,6 +103,19 @@ let ttl = ttlOverride === undefined

if (ttl <= 0)
return response;
return { response };
if (canBeRevalidated(response)) {
ttl *= 2;
}
const returnedResponse = response.clone();
return {
response: returnedResponse,
cacheWritePromise: this.readResponseAndWriteToCache({
response,
policy,
ttl,
ttlOverride,
cacheKey,
}),
};
}
async readResponseAndWriteToCache({ response, policy, ttl, ttlOverride, cacheKey, }) {
const body = await response.text();

@@ -111,8 +130,2 @@ const entry = JSON.stringify({

});
return new node_fetch_1.Response(body, {
url: response.url,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers),
});
}

@@ -122,3 +135,3 @@ }

function canBeRevalidated(response) {
return response.headers.has('ETag');
return response.headers.has('ETag') || response.headers.has('Last-Modified');
}

@@ -136,5 +149,40 @@ function policyRequestFrom(url, request) {

status: response.status,
headers: Object.fromEntries(response.headers),
headers: response.headers instanceof node_fetch_1.Headers
? nodeFetchHeadersToCachePolicyHeaders(response.headers)
: Object.fromEntries(response.headers),
};
}
function nodeFetchHeadersToCachePolicyHeaders(headers) {
const cachePolicyHeaders = Object.create(null);
for (const [name, values] of Object.entries(headers.raw())) {
cachePolicyHeaders[name] = values.length === 1 ? values[0] : values;
}
return cachePolicyHeaders;
}
function cachePolicyHeadersToNodeFetchHeadersInit(headers) {
const headerList = [];
for (const [name, value] of Object.entries(headers)) {
if (typeof value === 'string') {
headerList.push([name, value]);
}
else if (value) {
for (const subValue of value) {
headerList.push([name, subValue]);
}
}
}
return headerList;
}
function cachePolicyHeadersToFetcherHeadersInit(headers) {
const headerRecord = Object.create(null);
for (const [name, value] of Object.entries(headers)) {
if (typeof value === 'string') {
headerRecord[name] = value;
}
else if (value) {
headerRecord[name] = value.join(', ');
}
}
return headerRecord;
}
//# sourceMappingURL=HTTPCache.js.map

@@ -1,2 +0,2 @@

export { RESTDataSource, RequestOptions, WillSendRequestOptions, } from './RESTDataSource';
export { RESTDataSource, RequestOptions, AugmentedRequest, } from './RESTDataSource';
//# sourceMappingURL=index.d.ts.map

@@ -1,15 +0,19 @@

import { HTTPCache } from './HTTPCache';
import { GraphQLError } from 'graphql';
/// <reference types="node" />
import type { Fetcher, FetcherRequestInit, FetcherResponse } from '@apollo/utils.fetcher';
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
import type { Fetcher, FetcherRequestInit, FetcherResponse } from '@apollo/utils.fetcher';
import type { WithRequired } from '@apollo/utils.withrequired';
declare type ValueOrPromise<T> = T | Promise<T>;
declare type URLSearchParamsInit = ConstructorParameters<typeof URLSearchParams>[0];
export declare type RequestOptions = FetcherRequestInit & {
params?: URLSearchParamsInit;
cacheOptions?: CacheOptions | ((url: string, response: FetcherResponse, request: RequestOptions) => CacheOptions | undefined);
import { GraphQLError } from 'graphql';
import { HTTPCache } from './HTTPCache';
import type { Options as HttpCacheSemanticsOptions } from 'http-cache-semantics';
export type ValueOrPromise<T> = T | Promise<T>;
export type RequestOptions = FetcherRequestInit & {
params?: Record<string, string | undefined> | URLSearchParams;
cacheKey?: string;
cacheOptions?: CacheOptions | ((url: string, response: FetcherResponse, request: RequestOptions) => Promise<CacheOptions | undefined>);
httpCacheSemanticsCachePolicyOptions?: HttpCacheSemanticsOptions;
};
export declare type WillSendRequestOptions = Omit<WithRequired<RequestOptions, 'headers'>, 'params'> & {
params: URLSearchParams;
};
export interface HeadRequest extends RequestOptions {
method?: 'HEAD';
body?: never;
}
export interface GetRequest extends RequestOptions {

@@ -19,2 +23,3 @@ method?: 'GET';

}
export type RequestWithoutBody = HeadRequest | GetRequest;
export interface RequestWithBody extends Omit<RequestOptions, 'body'> {

@@ -24,2 +29,6 @@ method?: 'POST' | 'PUT' | 'PATCH' | 'DELETE';

}
type DataSourceRequest = RequestWithoutBody | RequestWithBody;
export type AugmentedRequest = (Omit<WithRequired<RequestWithoutBody, 'headers'>, 'params'> | Omit<WithRequired<RequestWithBody, 'headers'>, 'params'>) & {
params: URLSearchParams;
};
export interface CacheOptions {

@@ -32,16 +41,53 @@ ttl?: number;

}
export interface RequestDeduplicationResult {
policy: RequestDeduplicationPolicy;
deduplicatedAgainstPreviousRequest: boolean;
}
export interface HTTPCacheResult {
cacheWritePromise: Promise<void> | undefined;
}
export interface DataSourceFetchResult<TResult> {
parsedBody: TResult;
response: FetcherResponse;
requestDeduplication: RequestDeduplicationResult;
httpCache: HTTPCacheResult;
}
export type RequestDeduplicationPolicy = {
policy: 'deduplicate-during-request-lifetime';
deduplicationKey: string;
} | {
policy: 'deduplicate-until-invalidated';
deduplicationKey: string;
} | {
policy: 'do-not-deduplicate';
invalidateDeduplicationKeys?: string[];
};
export declare abstract class RESTDataSource {
httpCache: HTTPCache;
memoizedResults: Map<string, Promise<any>>;
protected httpCache: HTTPCache;
protected deduplicationPromises: Map<string, Promise<any>>;
baseURL?: string;
memoizeGetRequests: boolean;
constructor(config?: DataSourceConfig);
protected cacheKeyFor(url: URL, _request: RequestOptions): string;
protected willSendRequest?(requestOpts: WillSendRequestOptions): ValueOrPromise<void>;
protected resolveURL(path: string, _request: RequestOptions): ValueOrPromise<URL>;
protected cacheOptionsFor?(url: string, response: FetcherResponse, request: FetcherRequestInit): CacheOptions | undefined;
protected didReceiveResponse<TResult = any>(response: FetcherResponse, _request: RequestOptions): Promise<TResult>;
protected cacheKeyFor(url: URL, request: RequestOptions): string;
protected requestDeduplicationPolicyFor(url: URL, request: RequestOptions): RequestDeduplicationPolicy;
protected willSendRequest?(path: string, requestOpts: AugmentedRequest): ValueOrPromise<void>;
protected resolveURL(path: string, _request: AugmentedRequest): ValueOrPromise<URL>;
protected cacheOptionsFor?(url: string, response: FetcherResponse, request: FetcherRequestInit): ValueOrPromise<CacheOptions | undefined>;
protected didEncounterError(error: Error, _request: RequestOptions): void;
protected parseBody(response: FetcherResponse): Promise<object | string>;
protected errorFromResponse(response: FetcherResponse): Promise<GraphQLError>;
private cloneDataSourceFetchResult;
protected cloneParsedBody<TResult>(parsedBody: TResult): any;
protected shouldJSONSerializeBody(body: RequestWithBody['body']): boolean;
protected throwIfResponseIsError(options: {
url: URL;
request: RequestOptions;
response: FetcherResponse;
parsedBody: unknown;
}): Promise<void>;
protected errorFromResponse({ response, parsedBody, }: {
url: URL;
request: RequestOptions;
response: FetcherResponse;
parsedBody: unknown;
}): Promise<GraphQLError>;
protected head(path: string, request?: HeadRequest): Promise<FetcherResponse>;
protected get<TResult = any>(path: string, request?: GetRequest): Promise<TResult>;

@@ -52,12 +98,8 @@ protected post<TResult = any>(path: string, request?: RequestWithBody): Promise<TResult>;

protected delete<TResult = any>(path: string, request?: RequestWithBody): Promise<TResult>;
private fetch;
private urlSearchParamsFromRecord;
fetch<TResult>(path: string, incomingRequest?: DataSourceRequest): Promise<DataSourceFetchResult<TResult>>;
protected catchCacheWritePromiseErrors(cacheWritePromise: Promise<void>): void;
protected trace<TResult>(url: URL, request: RequestOptions, fn: () => Promise<TResult>): Promise<TResult>;
}
export declare class AuthenticationError extends GraphQLError {
constructor(message: string);
}
export declare class ForbiddenError extends GraphQLError {
constructor(message: string);
}
export {};
//# sourceMappingURL=RESTDataSource.d.ts.map
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ForbiddenError = exports.AuthenticationError = exports.RESTDataSource = void 0;
exports.RESTDataSource = void 0;
const graphql_1 = require("graphql");
const lodash_isplainobject_1 = __importDefault(require("lodash.isplainobject"));
const HTTPCache_1 = require("./HTTPCache");
const graphql_1 = require("graphql");
const NODE_ENV = process.env.NODE_ENV;
class RESTDataSource {
constructor(config) {
this.memoizedResults = new Map();
this.memoizeGetRequests = true;
this.deduplicationPromises = new Map();
this.httpCache = new HTTPCache_1.HTTPCache(config === null || config === void 0 ? void 0 : config.cache, config === null || config === void 0 ? void 0 : config.fetch);
}
cacheKeyFor(url, _request) {
return url.toString();
cacheKeyFor(url, request) {
var _a, _b;
return (_a = request.cacheKey) !== null && _a !== void 0 ? _a : `${(_b = request.method) !== null && _b !== void 0 ? _b : 'GET'} ${url}`;
}
resolveURL(path, _request) {
if (path.startsWith('/')) {
path = path.slice(1);
requestDeduplicationPolicyFor(url, request) {
var _a;
const method = (_a = request.method) !== null && _a !== void 0 ? _a : 'GET';
const cacheKey = this.cacheKeyFor(url, request);
if (['GET', 'HEAD'].includes(method)) {
return {
policy: 'deduplicate-during-request-lifetime',
deduplicationKey: cacheKey,
};
}
const baseURL = this.baseURL;
if (baseURL) {
const normalizedBaseURL = baseURL.endsWith('/')
? baseURL
: baseURL.concat('/');
return new URL(path, normalizedBaseURL);
}
else {
return new URL(path);
return {
policy: 'do-not-deduplicate',
invalidateDeduplicationKeys: [
this.cacheKeyFor(url, { ...request, method: 'GET' }),
this.cacheKeyFor(url, { ...request, method: 'HEAD' }),
],
};
}
}
async didReceiveResponse(response, _request) {
if (response.ok) {
return this.parseBody(response);
}
else {
throw await this.errorFromResponse(response);
}
resolveURL(path, _request) {
return new URL(path, this.baseURL);
}

@@ -56,108 +60,201 @@ didEncounterError(error, _request) {

}
async errorFromResponse(response) {
const message = `${response.status}: ${response.statusText}`;
let error;
if (response.status === 401) {
error = new AuthenticationError(message);
cloneDataSourceFetchResult(dataSourceFetchResult, requestDeduplicationResult) {
return {
...dataSourceFetchResult,
requestDeduplication: requestDeduplicationResult,
parsedBody: this.cloneParsedBody(dataSourceFetchResult.parsedBody),
};
}
cloneParsedBody(parsedBody) {
if (typeof parsedBody === 'string') {
return parsedBody;
}
else if (response.status === 403) {
error = new ForbiddenError(message);
}
else {
error = new graphql_1.GraphQLError(message);
return JSON.parse(JSON.stringify(parsedBody));
}
const body = await this.parseBody(response);
Object.assign(error.extensions, {
response: {
url: response.url,
status: response.status,
statusText: response.statusText,
body,
}
shouldJSONSerializeBody(body) {
var _a;
return !!((Array.isArray(body) ||
(0, lodash_isplainobject_1.default)(body) ||
(body &&
typeof body === 'object' &&
'toJSON' in body &&
typeof body.toJSON === 'function' &&
!(body instanceof Buffer) &&
((_a = body.constructor) === null || _a === void 0 ? void 0 : _a.name) !== 'FormData')));
}
async throwIfResponseIsError(options) {
if (options.response.ok) {
return;
}
throw await this.errorFromResponse(options);
}
async errorFromResponse({ response, parsedBody, }) {
return new graphql_1.GraphQLError(`${response.status}: ${response.statusText}`, {
extensions: {
...(response.status === 401
? { code: 'UNAUTHENTICATED' }
: response.status === 403
? { code: 'FORBIDDEN' }
: {}),
response: {
url: response.url,
status: response.status,
statusText: response.statusText,
body: parsedBody,
},
},
});
return error;
}
async head(path, request) {
return (await this.fetch(path, { method: 'HEAD', ...request })).response;
}
async get(path, request) {
return this.fetch(path, { method: 'GET', ...request });
return (await this.fetch(path, {
method: 'GET',
...request,
})).parsedBody;
}
async post(path, request) {
return this.fetch(path, { method: 'POST', ...request });
return (await this.fetch(path, {
method: 'POST',
...request,
})).parsedBody;
}
async patch(path, request) {
return this.fetch(path, { method: 'PATCH', ...request });
return (await this.fetch(path, {
method: 'PATCH',
...request,
})).parsedBody;
}
async put(path, request) {
return this.fetch(path, { method: 'PUT', ...request });
return (await this.fetch(path, {
method: 'PUT',
...request,
})).parsedBody;
}
async delete(path, request) {
return this.fetch(path, { method: 'DELETE', ...request });
return (await this.fetch(path, {
method: 'DELETE',
...request,
})).parsedBody;
}
async fetch(path, request) {
var _a;
const modifiedRequest = {
...request,
params: request.params instanceof URLSearchParams
? request.params
: new URLSearchParams(request.params),
headers: (_a = request.headers) !== null && _a !== void 0 ? _a : Object.create(null),
body: undefined,
urlSearchParamsFromRecord(params) {
const usp = new URLSearchParams();
if (params) {
for (const [name, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
usp.set(name, value);
}
}
}
return usp;
}
async fetch(path, incomingRequest = {}) {
var _a, _b;
const augmentedRequest = {
...incomingRequest,
params: incomingRequest.params instanceof URLSearchParams
? incomingRequest.params
: this.urlSearchParamsFromRecord(incomingRequest.params),
headers: (_a = incomingRequest.headers) !== null && _a !== void 0 ? _a : Object.create(null),
};
if (!augmentedRequest.method)
augmentedRequest.method = 'GET';
if (this.willSendRequest) {
await this.willSendRequest(modifiedRequest);
await this.willSendRequest(path, augmentedRequest);
}
const url = await this.resolveURL(path, modifiedRequest);
for (const [name, value] of modifiedRequest.params) {
const url = await this.resolveURL(path, augmentedRequest);
for (const [name, value] of augmentedRequest.params) {
url.searchParams.append(name, value);
}
if (request.body !== undefined &&
request.body !== null &&
(request.body.constructor === Object ||
Array.isArray(request.body) ||
(request.body.toJSON &&
typeof request.body.toJSON === 'function'))) {
modifiedRequest.body = JSON.stringify(request.body);
if (!modifiedRequest.headers) {
modifiedRequest.headers = { 'content-type': 'application/json' };
if (this.shouldJSONSerializeBody(augmentedRequest.body)) {
augmentedRequest.body = JSON.stringify(augmentedRequest.body);
if (!augmentedRequest.headers) {
augmentedRequest.headers = { 'content-type': 'application/json' };
}
else if (!modifiedRequest.headers['content-type']) {
modifiedRequest.headers['content-type'] = 'application/json';
else if (!augmentedRequest.headers['content-type']) {
augmentedRequest.headers['content-type'] = 'application/json';
}
}
const cacheKey = this.cacheKeyFor(url, modifiedRequest);
const outgoingRequest = augmentedRequest;
const performRequest = async () => {
return this.trace(url, modifiedRequest, async () => {
return this.trace(url, outgoingRequest, async () => {
var _a;
const cacheOptions = modifiedRequest.cacheOptions
? modifiedRequest.cacheOptions
const cacheKey = this.cacheKeyFor(url, outgoingRequest);
const cacheOptions = outgoingRequest.cacheOptions
? outgoingRequest.cacheOptions
: (_a = this.cacheOptionsFor) === null || _a === void 0 ? void 0 : _a.bind(this);
try {
const response = await this.httpCache.fetch(url, modifiedRequest, {
const { response, cacheWritePromise } = await this.httpCache.fetch(url, outgoingRequest, {
cacheKey,
cacheOptions,
httpCacheSemanticsCachePolicyOptions: outgoingRequest.httpCacheSemanticsCachePolicyOptions,
});
return await this.didReceiveResponse(response, modifiedRequest);
if (cacheWritePromise) {
this.catchCacheWritePromiseErrors(cacheWritePromise);
}
const parsedBody = await this.parseBody(response);
await this.throwIfResponseIsError({
url,
request: outgoingRequest,
response,
parsedBody,
});
return {
parsedBody: parsedBody,
response,
httpCache: {
cacheWritePromise,
},
};
}
catch (error) {
this.didEncounterError(error, modifiedRequest);
this.didEncounterError(error, outgoingRequest);
throw error;
}
});
};
if (this.memoizeGetRequests) {
if (request.method === 'GET') {
let promise = this.memoizedResults.get(cacheKey);
if (promise)
return promise;
promise = performRequest();
this.memoizedResults.set(cacheKey, promise);
return promise;
const policy = this.requestDeduplicationPolicyFor(url, outgoingRequest);
if (policy.policy === 'deduplicate-during-request-lifetime' ||
policy.policy === 'deduplicate-until-invalidated') {
const previousRequestPromise = this.deduplicationPromises.get(policy.deduplicationKey);
if (previousRequestPromise)
return previousRequestPromise.then((result) => this.cloneDataSourceFetchResult(result, {
policy,
deduplicatedAgainstPreviousRequest: true,
}));
const thisRequestPromise = performRequest();
this.deduplicationPromises.set(policy.deduplicationKey, thisRequestPromise);
try {
return this.cloneDataSourceFetchResult(await thisRequestPromise, {
policy,
deduplicatedAgainstPreviousRequest: false,
});
}
else {
this.memoizedResults.delete(cacheKey);
return performRequest();
finally {
if (policy.policy === 'deduplicate-during-request-lifetime') {
this.deduplicationPromises.delete(policy.deduplicationKey);
}
}
}
else {
return performRequest();
for (const key of (_b = policy.invalidateDeduplicationKeys) !== null && _b !== void 0 ? _b : []) {
this.deduplicationPromises.delete(key);
}
return {
...(await performRequest()),
requestDeduplication: {
policy,
deduplicatedAgainstPreviousRequest: false,
},
};
}
}
catchCacheWritePromiseErrors(cacheWritePromise) {
cacheWritePromise.catch((e) => {
console.error(`Error writing from RESTDataSource to cache: ${e}`);
});
}
async trace(url, request, fn) {

@@ -181,16 +278,2 @@ if (NODE_ENV === 'development') {

exports.RESTDataSource = RESTDataSource;
class AuthenticationError extends graphql_1.GraphQLError {
constructor(message) {
super(message, { extensions: { code: 'UNAUTHENTICATED' } });
this.name = 'AuthenticationError';
}
}
exports.AuthenticationError = AuthenticationError;
class ForbiddenError extends graphql_1.GraphQLError {
constructor(message) {
super(message, { extensions: { code: 'FORBIDDEN' } });
this.name = 'ForbiddenError';
}
}
exports.ForbiddenError = ForbiddenError;
//# sourceMappingURL=RESTDataSource.js.map
{
"name": "@apollo/datasource-rest",
"description": "REST DataSource for Apollo Server v4",
"version": "4.3.2",
"version": "5.0.0",
"author": "Apollo <packages@apollographql.com>",

@@ -34,23 +34,26 @@ "license": "MIT",

"devDependencies": {
"@apollo/server": "4.0.0",
"@apollo/utils.withrequired": "1.0.0",
"@changesets/changelog-github": "0.4.7",
"@changesets/cli": "2.25.0",
"@types/http-cache-semantics": "4.0.1",
"@types/jest": "29.1.2",
"@types/node": "14.18.32",
"cspell": "6.12.0",
"@apollo/server": "4.3.0",
"@apollo/utils.withrequired": "2.0.0",
"@changesets/changelog-github": "0.4.8",
"@changesets/cli": "2.26.0",
"@types/jest": "29.2.5",
"@types/lodash.isplainobject": "4.0.7",
"@types/node": "14.18.36",
"cspell": "6.18.1",
"form-data": "4.0.0",
"graphql": "16.6.0",
"jest": "29.1.2",
"jest-junit": "14.0.1",
"jest": "29.3.1",
"jest-junit": "15.0.0",
"nock": "13.2.9",
"prettier": "2.7.1",
"prettier": "2.8.2",
"ts-jest": "29.0.3",
"ts-node": "10.9.1",
"typescript": "4.8.4"
"typescript": "4.9.4"
},
"dependencies": {
"@apollo/utils.fetcher": "^1.0.0",
"@apollo/utils.keyvaluecache": "1.0.1",
"@apollo/utils.fetcher": "^2.0.0",
"@apollo/utils.keyvaluecache": "^2.0.0",
"@types/http-cache-semantics": "^4.0.1",
"http-cache-semantics": "^4.1.0",
"lodash.isplainobject": "^4.0.6",
"node-fetch": "^2.6.7"

@@ -62,5 +65,5 @@ },

"volta": {
"node": "16.18.0",
"npm": "8.19.2"
"node": "18.13.0",
"npm": "9.2.0"
}
}

@@ -5,5 +5,12 @@ # Apollo REST Data Source

RESTDataSource wraps an implementation of the DOM-style Fetch API such as `node-fetch` and adds the following features:
- Two layers of caching:
+ An in-memory "request deduplication" feature which by default avoids sending the same GET (or HEAD) request multiple times in parallel.
+ An "HTTP cache" which provides browser-style caching in a (potentially shared) `KeyValueCache` which observes standard HTTP caching headers.
- Convenience features such as the ability to specify an un-serialized object as a JSON request body and an easy way to specify URL search parameters
- Error handling
## Documentation
View the [Apollo Server documentation for data sources](https://www.apollographql.com/docs/apollo-server/features/data-sources/) for more details.
View the [Apollo Server documentation for RESTDataSource](https://www.apollographql.com/docs/apollo-server/data/fetching-rest) for more high-level details and examples.

@@ -20,3 +27,3 @@ ## Usage

Your implementation of these methods can call on convenience methods built into the [`RESTDataSource`](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) class to perform HTTP requests, while making it easy to build up query parameters, parse JSON results, and handle errors.
Your implementation of these methods can call convenience methods built into the [`RESTDataSource`](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) class to perform HTTP requests, while making it easy to build up query parameters, parse JSON results, and handle errors.

@@ -27,6 +34,3 @@ ```javascript

class MoviesAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://movies-api.example.com/';
}
override baseURL = 'https://movies-api.example.com/';

@@ -40,3 +44,3 @@ async getMovie(id) {

params: {
per_page: limit,
per_page: limit.toString(), // all params entries should be strings
order_by: 'most_viewed',

@@ -51,14 +55,16 @@ },

### API Reference
To see the all the properties and functions that can be overridden, the [source code](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) is always the best option.
`RESTDataSource` is designed to be subclassed in order to create an API for use by the rest of your server. Many of its methods are protected. These consist of HTTP fetching methods (`fetch`, `get`, `put`, `post`, `patch`, `delete`, and `head`) which your API can call, and other methods that can be overridden to customize behavior.
This README lists all the protected methods. In practice, if you're looking to customize behavior by overriding methods, reading the [source code](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) is the best option.
#### Properties
##### `baseURL`
Optional value to use for all the REST calls. If it is set in your class implementation, this base URL is used as the prefix for all calls. If it is not set, then the value passed to the REST call is exactly the value used.
Optional value to use for all the REST calls. If it is set in your class implementation, this base URL is used as the prefix for all calls. If it is not set, then the value passed to the REST call is exactly the value used. See also `resolveURL`.
```js title="baseURL.js"
class MoviesAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://movies-api.example.com/';
}
override baseURL = 'https://movies-api.example.com/';
// GET

@@ -73,21 +79,80 @@ async getMovie(id) {

##### `memoizeGetRequests`
By default, `RESTDataSource` caches all outgoing GET **requests** in a separate memoized cache from the regular response cache. It makes the assumption that all responses from HTTP GET calls are cacheable by their URL.
If a request is made with the same cache key (URL by default) but with an HTTP method other than GET, the cached request is then cleared.
`RESTDataSource` interprets the string passed to methods such as `this.get()` as an URL in exactly the same way that a browser interprets a link on a web page whose address is the same as `this.baseURL`. This may lead to slightly surprising behavior if `this.baseURL` has a non-empty path component:
If you would like to disable the GET request cache, set the `memoizeGetRequests` property to `false`. You might want to do this if your API is not actually cacheable or your data changes over time.
- If the string passed to a method such as `this.get()` starts with a slash, then it is resolved relative to the *host* of the base URL, not to the full base URL. That is, if `this.baseURL` is `https://foo.com/a/b/c/`, then `this.get('d')` resolves to `https://foo.com/a/b/c/d`, but `this.get('/d')` resolves to `https://foo.com/d`.
- If the base URL has a path element and does not end in a slash, then the given path replaces the last element of the path. That is, if `baseURL` is `https://foo.com/a/b/c`, `this.get('d')` resolves to `https://foo.com/a/b/d`
```js title="memoizeGetRequests.js"
class MoviesAPI extends RESTDataSource {
constructor() {
super();
// Defaults to true
this.memoizeGetRequests = false;
In practice, this means that you should usually set `this.baseURL` to the common prefix of all URLs you want to access *including a trailing slash*, and you should pass paths *without a leading slash* to methods such as `this.get()`.
If a resource's path starts with something that looks like an URL because it contains a colon and you want it to be added on to the full base URL after its path (so you can't pass it as `this.get('/foo:bar')`), you can pass a path starting with `./`, like `this.get('./foo:bar')`.
##### `httpCache`
This is an internal object that adds HTTP-header-sensitive caching to HTTP fetching. Its exact API is internal to this package and may change between versions.
#### Overridable methods
##### `cacheKeyFor`
By default, `RESTDatasource` uses the `cacheKey` option from the request as the cache key, or the request method and full request URL otherwise when saving information about the request to the `KeyValueCache`. Override this method to remove query parameters or compute a custom cache key.
For example, you could use this to use header fields or the HTTP method as part of the cache key. Even though we do validate header fields and don't serve responses from cache when they don't match, new responses overwrite old ones with different header fields. (For the HTTP method, this might be a positive thing, as you may want a `POST /foo` request to stop a previously cached `GET /foo` from being returned.)
##### `requestDeduplicationPolicyFor`
By default, `RESTDataSource` de-duplicates all **concurrent** outgoing **`GET` (or `HEAD`) requests** in an in-memory cache, separate from the `KeyValueCache` used for the HTTP response cache. It makes the assumption that two `GET` (or two `HEAD`) requests to the same URL made in parallel can share the same response. When the request returns, its response is delivered to each caller that requested the same URL concurrently, and then it is removed from the cache.
If a request is made with the same cache key (method + URL by default) but with an HTTP method other than `GET` or `HEAD`, deduplication of the in-flight request is invalidated: the next parallel `GET` (or `HEAD`) request for the same URL will make a new request.
You can configure this behavior in several ways:
- You can change which requests are de-deduplicated and which are not.
- You can tell `RESTDataSource` to de-duplicate a request against new requests that start after it completes, not just overlapping requests. (This was the poorly-documented behavior of `RESTDataSource` prior to v5.0.0.)
- You can control the "deduplication key" independently from the `KeyValueCache` cache key.
You do this by overriding the `requestDeduplicationPolicyFor` method in your class. This method takes an URL and a request, and returns a policy object with one of three forms:
- `{policy: 'deduplicate-during-request-lifetime', deduplicationKey: string}`: This is the default behavior for `GET` requests. If a request with the same deduplication key is in progress, share its result. Otherwise, start a request, allow other requests to de-duplicate against it while it is running, and forget about it once the request returns successfully.
- `{policy: 'deduplicate-until-invalidated', deduplicationKey: string}`: This was the default behavior for `GET` requests in versions prior to v5. If a request with the same deduplication key is in progress, share its result. Otherwise, start a request and allow other requests to de-duplicate against it while it is running. All future requests with policy `deduplicate-during-request-lifetime` or `deduplicate-until-invalidated` with the same `deduplicationKey` will share the same result until a request is started with policy `do-not-deduplicate` and a matching entry in `invalidateDeduplicationKeys`.
- `{ policy: 'do-not-deduplicate'; invalidateDeduplicationKeys?: string[] }`: This is the default behavior for non-`GET` requests. Always run an actual HTTP request and don't allow other requests to de-duplicate against it. Additionally, invalidate any listed keys immediately: new requests with that `deduplicationKey` will not match any requests that currently exist in the request cache.
The default implementation of this method is:
```ts
protected requestDeduplicationPolicyFor(
url: URL,
request: RequestOptions,
): RequestDeduplicationPolicy {
const method = request.method ?? 'GET';
// Start with the cache key that is used for the shared header-sensitive
// cache. Note that its default implementation does not include the HTTP
// method, so if a subclass overrides this and allows non-GET/HEADs to be
// de-duplicated it will be important for it to include (at least!) the
// method in the deduplication key, so we're explicitly adding GET/HEAD here.
const cacheKey = this.cacheKeyFor(url, request);
if (['GET', 'HEAD'].includes(method)) {
return {
policy: 'deduplicate-during-request-lifetime',
deduplicationKey: `${method} ${cacheKey}`,
};
} else {
return {
policy: 'do-not-deduplicate',
// Always invalidate GETs and HEADs when a different method is seen on the same
// cache key (ie, URL), as per standard HTTP semantics. (We don't have
// to invalidate the key with this HTTP method because we never write
// it.)
invalidateDeduplicationKeys: [
this.cacheKeyFor(url, { ...request, method: 'GET' }),
this.cacheKeyFor(url, { ...request, method: 'HEAD' }),
],
};
}
```
// Outgoing requests are never cached, however the response cache is still enabled
async getMovie(id) {
return this.get(
`https://movies-api.example.com/movies/${encodeURIComponent(id)}` // path
);
To fully disable de-duplication, just always return `do-not-duplicate`. (This does not affect the HTTP header-sensitive cache.)
```ts
class MoviesAPI extends RESTDataSource {
protected override requestDeduplicationPolicyFor() {
return { policy: 'do-not-deduplicate' } as const;
}

@@ -97,15 +162,30 @@ }

#### Methods
##### `willSendRequest`
This method is invoked at the beginning of processing each request. It's called with the `path` and `request` provided to `fetch`, with a guaranteed non-empty `headers` and `params` objects. If a `Promise` is returned from this method it will wait until the promise is completed to continue executing the request. See the [intercepting fetches](#intercepting-fetches) section for usage examples.
##### `cacheKeyFor`
By default, `RESTDatasource` uses the full request URL as the cache key. Override this method to remove query parameters or compute a custom cache key.
##### `resolveURL`
For example, you could use this to use header fields as part of the cache key. Even though we do validate header fields and don't serve responses from cache when they don't match, new responses overwrite old ones with different header fields.
In some cases, you'll want to set the URL based on the environment or other contextual values rather than simply resolving against `this.baseURL`. To do this, you can override `resolveURL`:
##### `willSendRequest`
This method is invoked just before the fetch call is made. If a `Promise` is returned from this method it will wait until the promise is completed to continue executing the request.
```ts
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
class PersonalizationAPI extends RESTDataSource {
override async resolveURL(path: string, _request: AugmentedRequest) {
if (!this.baseURL) {
const addresses = await resolveSrv(path.split("/")[1] + ".service.consul");
this.baseURL = addresses[0];
}
return super.resolveURL(path);
}
}
```
##### `cacheOptionsFor`
Allows setting the `CacheOptions` to be used for each request/response in the HTTPCache. This is separate from the request-only cache. You can use this to set the TTL.
Allows setting the `CacheOptions` to be used for each request/response in the `HTTPCache`. This is separate from the request-only cache. You can use this to set the TTL to a value in seconds. If you return `{ttl: 0}`, the response will not be stored. If you return a positive number for `ttl` and the operation returns a 2xx status code, then the response *will* be cached, regardless of HTTP headers: make sure this is what you intended! (There is currently no way to say "only cache responses that should be cached according to HTTP headers, but change the TTL to something specific".) Note that if you do not specify `ttl` here, only `GET` requests are cached.
You can also specify `cacheOptions` as part of the "request" in any call to `get()`, `post()`, etc. Note that specifically `head()` calls are not cached at all, so this will have no effect for `HEAD` requests. This can either be an object such as `{ttl: 1}`, or a function returning that object. If `cacheOptions` is provided, `cacheOptionsFor` is not called (ie, `this.cacheOptionsFor` is effectively the default value of `cacheOptions`).
The `cacheOptions` function and `cacheOptionsFor` method may be async.
```javascript

@@ -119,20 +199,45 @@ override cacheOptionsFor() {

##### `didReceiveResponse`
By default, this method checks if the response was returned successfully and parses the response into the result object. If the response had an error, it detects which type of HTTP error and throws the error result.
If you override this behavior, be sure to implement the proper error handling.
##### `didEncounterError`
By default, this method just throws the `error` it was given. If you override this method, you can choose to either perform some additional logic and still throw, or to swallow the error by not throwing the error result.
##### `parseBody`
This method is called with the HTTP response and should read the body and parse it into an appropriate format. By default, it checks to see if the `Content-Type` header starts with `application/json` or ends with `+json` (just looking at the header as a string without using a Content-Type parser) and returns `response.json()` if it does or `response.text()` if it does not. If you want to read the body in a different way, override this. This method should read the response fully; if it does not, it could cause a memory leak inside the HTTP cache. If you override this, you may want to override `cloneParsedBody` as well.
##### `cloneParsedBody`
This method is used to clone a body (for use by the request deduplication feature so that multiple callers get distinct return values that can be separately mutated). If your `parseBody` returns values other than basic JSON objects, you might want to override this method too. You can also change this method to return its argument without cloning if your code that uses this class is OK with the values returned from deduplicated requests sharing state.
##### `shouldJSONSerializeBody`
By default, this method returns `true` if the request body is:
- a plain object or an array
- an object with a `toJSON` method (which isn't a `Buffer` or an instance of a class named `FormData`)
You can override this method in order to serialize other objects such as custom classes as JSON.
##### `throwIfResponseIsError`
After the body is parsed, this method checks a condition (by default, if the HTTP status is 4xx or 5xx) and throws an error created with `errorFromResponse` if the condition is met.
##### `errorFromResponse`
Creates an error based on the response.
##### `catchCacheWritePromiseErrors`
This class writes to the shared HTTP-header-sensitive cache in the background (ie, the write is not awaited as part of the HTTP fetch). It passes the `Promise` associated with that cache write to this method. By default, this method adds a `catch` handler to the `Promise` which writes any errors to `console.error`. You could use this to do different error handling, or to do no error handling if you trust all callers to use the `fetch` method and await `httpCache.cacheWritePromise`.
##### `trace`
This method wraps the entire processing of a single request; if the `NODE_ENV` environment variable is equal to `development`, it logs the request method, URL, and duration. You can override this to provide observability in a different manner.
### HTTP Methods
The `get` method on the [`RESTDataSource`](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) makes an HTTP `GET` request. Similarly, there are methods built-in to allow for `POST`, `PUT`, `PATCH`, and `DELETE` requests.
The `get` method on the [`RESTDataSource`](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) makes an HTTP `GET` request and returns its parsed body. Similarly, there are methods built-in to allow for `POST`, `PUT`, `PATCH`, `DELETE`, and `HEAD` requests. (The `head` method returns the full `FetcherResponse` rather than the body because `HEAD` responses do not have bodies.)
```javascript
class MoviesAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://movies-api.example.com/';
}
override baseURL = 'https://movies-api.example.com/';

@@ -172,4 +277,6 @@ // an example making an HTTP POST request

All of the HTTP helper functions (`get`, `put`, `post`, `patch`, and `delete`) accept a second parameter for setting the `body`, `headers`, `params`, and `cacheOptions`.
All of the HTTP helper functions (`get`, `put`, `post`, `patch`, `delete`, and `head`) accept a second parameter for setting the `body`, `headers`, `params`, `cacheKey`, and `cacheOptions` (and other Fetch API options).
Alternatively, you can use the `fetch` method. The return value of this method is a `DataSourceFetchResult`, which contains `parsedBody`, `response`, and some other fields with metadata about how the operation interacted with the cache.
### Intercepting fetches

@@ -183,3 +290,3 @@

class PersonalizationAPI extends RESTDataSource {
willSendRequest(request) {
willSendRequest(path, request) {
request.headers['authorization'] = this.context.token;

@@ -194,3 +301,3 @@ }

class PersonalizationAPI extends RESTDataSource {
willSendRequest(request) {
willSendRequest(path, request) {
request.params.set('api_key', this.context.token);

@@ -201,5 +308,5 @@ }

If you're using TypeScript, you can use the `RequestOptions` type to define the `willSendRequest` signature:
If you're using TypeScript, you can use the `AugmentedRequest` type to define the `willSendRequest` signature:
```ts
import { RESTDataSource, WillSendRequestOptions } from '@apollo/datasource-rest';
import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';

@@ -209,7 +316,9 @@ class PersonalizationAPI extends RESTDataSource {

constructor(private token: string) {
super();
private token: string;
constructor(options: { cache: KeyValueCache; token: string}) {
super(options);
this.token = options.token;
}
override willSendRequest(request: WillSendRequestOptions) {
override willSendRequest(_path: string, request: AugmentedRequest) {
request.headers['authorization'] = this.token;

@@ -220,26 +329,7 @@ }

### Resolving URLs dynamically
In some cases, you'll want to set the URL based on the environment or other contextual values. To do this, you can override `resolveURL`:
### Integration with Apollo Server
```ts
class PersonalizationAPI extends RESTDataSource {
constructor(private token: string) {
super();
}
To give resolvers access to data sources, you create and return them from your `context` function. (The following example uses the Apollo Server 4 API.)
override async resolveURL(path: string) {
if (!this.baseURL) {
const addresses = await resolveSrv(path.split("/")[1] + ".service.consul");
this.baseURL = addresses[0];
}
return super.resolveURL(path);
}
}
```
### Accessing data sources from resolvers
To give resolvers access to data sources, you pass them as options to the `ApolloServer` constructor:
```ts

@@ -284,7 +374,1 @@ interface MyContext {

```
### Implementing custom metrics
By overriding `trace` method, it's possible to implement custom metrics for request timing.
See the original method [implementation](https://github.com/apollographql/datasource-rest/tree/main/src/RESTDataSource.ts) or the reference.

@@ -1,3 +0,8 @@

import fetch, { Response } from 'node-fetch';
import nodeFetch, {
Response as NodeFetchResponse,
Headers as NodeFetchHeaders,
type HeadersInit as NodeFetchHeadersInit,
} from 'node-fetch';
import CachePolicy from 'http-cache-semantics';
import type { Options as HttpCacheSemanticsOptions } from 'http-cache-semantics';
import type {

@@ -13,4 +18,23 @@ Fetcher,

} from '@apollo/utils.keyvaluecache';
import type { CacheOptions, RequestOptions } from './RESTDataSource';
import type {
CacheOptions,
RequestOptions,
ValueOrPromise,
} from './RESTDataSource';
// We want to use a couple internal properties of CachePolicy. (We could get
// `_url` and `_status` off of the serialized CachePolicyObject, but `age()` is
// just missing from `@types/http-cache-semantics` for now.) So we just cast to
// this interface for now.
interface SneakyCachePolicy extends CachePolicy {
_url: string | undefined;
_status: number;
age(): number;
}
interface ResponseWithCacheWritePromise {
response: FetcherResponse;
cacheWritePromise?: Promise<void>;
}
export class HTTPCache {

@@ -22,3 +46,3 @@ private keyValueCache: KeyValueCache;

keyValueCache: KeyValueCache = new InMemoryLRUCache(),
httpFetch: Fetcher = fetch,
httpFetch: Fetcher = nodeFetch,
) {

@@ -43,5 +67,6 @@ this.keyValueCache = new PrefixingKeyValueCache(

request: RequestOptions,
) => CacheOptions | undefined);
) => ValueOrPromise<CacheOptions | undefined>);
httpCacheSemanticsCachePolicyOptions?: HttpCacheSemanticsOptions;
},
): Promise<FetcherResponse> {
): Promise<ResponseWithCacheWritePromise> {
const urlString = url.toString();

@@ -51,4 +76,15 @@ requestOpts.method = requestOpts.method ?? 'GET';

// Bypass the cache altogether for HEAD requests. Caching them might be fine
// to do, but for now this is just a pragmatic choice for timeliness without
// fully understanding the interplay between GET and HEAD requests (i.e.
// refreshing headers with HEAD requests, responding to HEADs with cached
// and valid GETs, etc.)
if (requestOpts.method === 'HEAD') {
return { response: await this.httpFetch(urlString, requestOpts) };
}
const entry = await this.keyValueCache.get(cacheKey);
if (!entry) {
// There's nothing in our cache. Fetch the URL and save it to the cache if
// we're allowed.
const response = await this.httpFetch(urlString, requestOpts);

@@ -59,3 +95,4 @@

policyResponseFrom(response),
);
cache?.httpCacheSemanticsCachePolicyOptions,
) as SneakyCachePolicy;

@@ -74,4 +111,7 @@ return this.storeResponseAndReturnClone(

const policy = CachePolicy.fromObject(policyRaw);
// Remove url from the policy, because otherwise it would never match a request with a custom cache key
const policy = CachePolicy.fromObject(policyRaw) as SneakyCachePolicy;
// Remove url from the policy, because otherwise it would never match a
// request with a custom cache key (ie, we want users to be able to tell us
// that two requests should be treated as the same even if the URL differs).
const urlFromPolicy = policy._url;
policy._url = undefined;

@@ -86,9 +126,30 @@

) {
// Either the cache entry was created with an explicit TTL override (ie,
// `ttl` returned from `cacheOptionsFor`) and we're within that TTL, or
// the cache entry was not created with an explicit TTL override and the
// header-based cache policy says we can safely use the cached response.
const headers = policy.responseHeaders();
return new Response(body, {
url: policy._url,
status: policy._status,
headers,
});
return {
response: new NodeFetchResponse(body, {
url: urlFromPolicy,
status: policy._status,
headers: cachePolicyHeadersToNodeFetchHeadersInit(headers),
}),
};
} else {
// We aren't sure that we're allowed to use the cached response, so we are
// going to actually do a fetch. However, we may have one extra trick up
// our sleeve. If the cached response contained an `etag` or
// `last-modified` header, then we can add an appropriate `if-none-match`
// or `if-modified-since` header to the request. If what we're fetching
// hasn't changed, then the server can return a small 304 response instead
// of a large 200, and we can use the body from our cache. This logic is
// implemented inside `policy.revalidationHeaders`; we support it by
// setting a larger KeyValueCache TTL for responses with these headers
// (see `canBeRevalidated`). (If the cached response doesn't have those
// headers, we'll just end up fetching the normal request here.)
//
// Note that even if we end up able to reuse the cached body here, we
// still re-write to the cache, because we might need to update the TTL or
// other aspects of the cache policy based on the headers we got back.
const revalidationHeaders = policy.revalidationHeaders(

@@ -99,3 +160,3 @@ policyRequestFrom(urlString, requestOpts),

...requestOpts,
headers: revalidationHeaders,
headers: cachePolicyHeadersToFetcherHeadersInit(revalidationHeaders),
};

@@ -110,11 +171,16 @@ const revalidationResponse = await this.httpFetch(

policyResponseFrom(revalidationResponse),
);
) as unknown as { policy: SneakyCachePolicy; modified: boolean };
return this.storeResponseAndReturnClone(
urlString,
new Response(modified ? await revalidationResponse.text() : body, {
url: revalidatedPolicy._url,
status: revalidatedPolicy._status,
headers: revalidatedPolicy.responseHeaders(),
}),
new NodeFetchResponse(
modified ? await revalidationResponse.text() : body,
{
url: revalidatedPolicy._url,
status: revalidatedPolicy._status,
headers: cachePolicyHeadersToNodeFetchHeadersInit(
revalidatedPolicy.responseHeaders(),
),
},
),
requestOpts,

@@ -132,3 +198,3 @@ revalidatedPolicy,

request: RequestOptions,
policy: CachePolicy,
policy: SneakyCachePolicy,
cacheKey: string,

@@ -141,6 +207,6 @@ cacheOptions?:

request: RequestOptions,
) => CacheOptions | undefined),
): Promise<FetcherResponse> {
) => ValueOrPromise<CacheOptions | undefined>),
): Promise<ResponseWithCacheWritePromise> {
if (typeof cacheOptions === 'function') {
cacheOptions = cacheOptions(url, response, request);
cacheOptions = await cacheOptions(url, response, request);
}

@@ -156,3 +222,3 @@

) {
return response;
return { response };
}

@@ -164,6 +230,8 @@

: ttlOverride;
if (ttl <= 0) return response;
if (ttl <= 0) return { response };
// If a response can be revalidated, we don't want to remove it from the cache right after it expires.
// We may be able to use better heuristics here, but for now we'll take the max-age times 2.
// If a response can be revalidated, we don't want to remove it from the
// cache right after it expires. (See the comment above the call to
// `revalidationHeaders` for details.) We may be able to use better
// heuristics here, but for now we'll take the max-age times 2.
if (canBeRevalidated(response)) {

@@ -173,2 +241,43 @@ ttl *= 2;

// Clone the response and return it. In the background, read the original
// response and write it to the cache. The caller is responsible for
// `await`ing or `catch`ing `cacheWritePromise`. (By default, RESTDataSource
// `catch`es it with `console.log`.)
//
// When you clone a response, you're generally expected (at least by
// node-fetch: https://github.com/node-fetch/node-fetch/issues/151) to read
// both bodies in parallel; if you only read one of them and ignore the
// other, the one you're reading might start blocking once the second one's
// buffer fills. We don't think this is a real problem here: we do
// immediately read from the one we're writing to the cache, and if the
// caller doesn't bother to read its response, the only real downside is
// that we won't ever write to the cache, which seems maybe OK for an
// "ignored" body. (It could perhaps lead to a memory leak, but the answer
// there is to make sure your parseBody override does consume the response.)
const returnedResponse = response.clone();
return {
response: returnedResponse,
cacheWritePromise: this.readResponseAndWriteToCache({
response,
policy,
ttl,
ttlOverride,
cacheKey,
}),
};
}
private async readResponseAndWriteToCache({
response,
policy,
ttl,
ttlOverride,
cacheKey,
}: {
response: FetcherResponse;
policy: CachePolicy;
ttl: number | null | undefined;
ttlOverride: number | undefined;
cacheKey: string;
}): Promise<void> {
const body = await response.text();

@@ -184,13 +293,2 @@ const entry = JSON.stringify({

});
// We have to clone the response before returning it because the
// body can only be used once.
// To avoid https://github.com/bitinn/node-fetch/issues/151, we don't use
// response.clone() but create a new response from the consumed body
return new Response(body, {
url: response.url,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers),
});
}

@@ -200,3 +298,3 @@ }

function canBeRevalidated(response: FetcherResponse): boolean {
return response.headers.has('ETag');
return response.headers.has('ETag') || response.headers.has('Last-Modified');
}

@@ -215,4 +313,69 @@

status: response.status,
headers: Object.fromEntries(response.headers),
headers:
response.headers instanceof NodeFetchHeaders
? nodeFetchHeadersToCachePolicyHeaders(response.headers)
: Object.fromEntries(response.headers),
};
}
// In the special case that these headers come from node-fetch, uses
// node-fetch's `raw()` method (which returns a `Record<string, string[]>`) to
// create our CachePolicy.Headers. Note that while we could theoretically just
// return `headers.raw()` here (it does match the typing of
// CachePolicy.Headers), `http-cache-semantics` sadly does expect most of the
// headers it pays attention to to only show up once (see eg
// https://github.com/kornelski/http-cache-semantics/issues/28). We want to
// preserve the multiplicity of other headers that CachePolicy doesn't parse
// (like set-cookie) because we store the CachePolicy in the cache, but not the
// interesting ones that we hope were singletons, so this function
// de-singletonizes singleton response headers.
function nodeFetchHeadersToCachePolicyHeaders(
headers: NodeFetchHeaders,
): CachePolicy.Headers {
const cachePolicyHeaders = Object.create(null);
for (const [name, values] of Object.entries(headers.raw())) {
cachePolicyHeaders[name] = values.length === 1 ? values[0] : values;
}
return cachePolicyHeaders;
}
// CachePolicy.Headers can store header values as string or string-array (for
// duplicate headers). Convert it to "list of pairs", which is a valid
// `node-fetch` constructor argument and will preserve the separation of
// duplicate headers.
function cachePolicyHeadersToNodeFetchHeadersInit(
headers: CachePolicy.Headers,
): NodeFetchHeadersInit {
const headerList = [];
for (const [name, value] of Object.entries(headers)) {
if (typeof value === 'string') {
headerList.push([name, value]);
} else if (value) {
for (const subValue of value) {
headerList.push([name, subValue]);
}
}
}
return headerList;
}
// CachePolicy.Headers can store header values as string or string-array (for
// duplicate headers). Convert it to "Record of strings", which is all we allow
// for HeadersInit in Fetcher. (Perhaps we should expand that definition in
// `@apollo/utils.fetcher` to allow HeadersInit to be `string[][]` too; then we
// could use the same function as cachePolicyHeadersToNodeFetchHeadersInit here
// which would let us more properly support duplicate headers in *requests* if
// using node-fetch.)
function cachePolicyHeadersToFetcherHeadersInit(
headers: CachePolicy.Headers,
): Record<string, string> {
const headerRecord = Object.create(null);
for (const [name, value] of Object.entries(headers)) {
if (typeof value === 'string') {
headerRecord[name] = value;
} else if (value) {
headerRecord[name] = value.join(', ');
}
}
return headerRecord;
}
export {
RESTDataSource,
RequestOptions,
WillSendRequestOptions,
AugmentedRequest,
} from './RESTDataSource';

@@ -1,4 +0,1 @@

import { HTTPCache } from './HTTPCache';
import { GraphQLError } from 'graphql';
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
import type {

@@ -9,11 +6,34 @@ Fetcher,

} from '@apollo/utils.fetcher';
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
import type { WithRequired } from '@apollo/utils.withrequired';
import { GraphQLError } from 'graphql';
import isPlainObject from 'lodash.isplainobject';
import { HTTPCache } from './HTTPCache';
import type { Options as HttpCacheSemanticsOptions } from 'http-cache-semantics';
type ValueOrPromise<T> = T | Promise<T>;
export type ValueOrPromise<T> = T | Promise<T>;
// URLSearchParams is globally available in Node / coming from @types/node
type URLSearchParamsInit = ConstructorParameters<typeof URLSearchParams>[0];
export type RequestOptions = FetcherRequestInit & {
params?: URLSearchParamsInit;
/**
* URL search parameters can be provided either as a record object (in which
* case keys with `undefined` values are ignored) or as an URLSearchParams
* object. If you want to specify a parameter multiple times, use
* URLSearchParams with its "array of two-element arrays" constructor form.
* (The URLSearchParams object is globally available in Node, and provided to
* TypeScript by @types/node.)
*/
params?: Record<string, string | undefined> | URLSearchParams;
/**
* The default implementation of `cacheKeyFor` returns this value if it is
* provided. This is used both as part of the request deduplication key and as
* the key in the shared HTTP-header-sensitive cache.
*/
cacheKey?: string;
/**
* This can be a `CacheOptions` object or a (possibly async) function
* returning such an object. The details of what its fields mean are
* documented under `CacheOptions`. The function is called after a real HTTP
* request is made (and is not called if a response from the cache can be
* returned). If this is provided, the `cacheOptionsFor` hook is not called.
*/
cacheOptions?:

@@ -25,11 +45,15 @@ | CacheOptions

request: RequestOptions,
) => CacheOptions | undefined);
) => Promise<CacheOptions | undefined>);
/**
* If provided, this is passed through as the third argument to `new
* CachePolicy()` from the `http-cache-semantics` npm package as part of the
* HTTP-header-sensitive cache.
*/
httpCacheSemanticsCachePolicyOptions?: HttpCacheSemanticsOptions;
};
export type WillSendRequestOptions = Omit<
WithRequired<RequestOptions, 'headers'>,
'params'
> & {
params: URLSearchParams;
};
export interface HeadRequest extends RequestOptions {
method?: 'HEAD';
body?: never;
}

@@ -41,2 +65,4 @@ export interface GetRequest extends RequestOptions {

export type RequestWithoutBody = HeadRequest | GetRequest;
export interface RequestWithBody extends Omit<RequestOptions, 'body'> {

@@ -47,5 +73,30 @@ method?: 'POST' | 'PUT' | 'PATCH' | 'DELETE';

type DataSourceRequest = GetRequest | RequestWithBody;
type DataSourceRequest = RequestWithoutBody | RequestWithBody;
// While tempting, this union can't be reduced / factored out to just
// Omit<WithRequired<RequestWithBody | RequestWithBody, 'headers'>, 'params'> & { params: URLSearchParams }
// TS loses its ability to discriminate against the method (and its consequential `body` type)
/**
* This type is for convenience w.r.t. the `willSendRequest` and `resolveURL`
* hooks to ensure that headers and params are always present, even if they're
* empty.
*/
export type AugmentedRequest = (
| Omit<WithRequired<RequestWithoutBody, 'headers'>, 'params'>
| Omit<WithRequired<RequestWithBody, 'headers'>, 'params'>
) & {
params: URLSearchParams;
};
export interface CacheOptions {
/**
* This sets the TTL used in the shared cache to a value in seconds. If this
* is 0, the response will not be stored. If this is a positive number and
* the operation returns a 2xx status code, then the response *will* be
* cached, regardless of HTTP headers or method: make sure this is what you
* intended! (There is currently no way to say "only cache responses that
* should be cached according to HTTP headers, but change the TTL to something
* specific".) Note that if this is not provided, only `GET` requests are
* cached.
*/
ttl?: number;

@@ -61,7 +112,51 @@ }

export interface RequestDeduplicationResult {
policy: RequestDeduplicationPolicy;
deduplicatedAgainstPreviousRequest: boolean;
}
export interface HTTPCacheResult {
// This is primarily returned so that tests can be deterministic.
cacheWritePromise: Promise<void> | undefined;
}
export interface DataSourceFetchResult<TResult> {
parsedBody: TResult;
response: FetcherResponse;
requestDeduplication: RequestDeduplicationResult;
httpCache: HTTPCacheResult;
}
// RESTDataSource has two layers of caching. The first layer is purely in-memory
// within a single RESTDataSource object and is called "request deduplication".
// It is primarily designed so that multiple identical GET requests started
// concurrently can share one real HTTP GET; it does not observe HTTP response
// headers. (The second layer uses a potentially shared KeyValueCache for
// storage and does observe HTTP response headers.) To configure request
// deduplication, override requestDeduplicationPolicyFor.
export type RequestDeduplicationPolicy =
// If a request with the same deduplication key is in progress, share its
// result. Otherwise, start a request, allow other requests to de-duplicate
// against it while it is running, and forget about it once the request returns
// successfully.
| { policy: 'deduplicate-during-request-lifetime'; deduplicationKey: string }
// If a request with the same deduplication key is in progress, share its
// result. Otherwise, start a request and allow other requests to de-duplicate
// against it while it is running. All future requests with policy
// `deduplicate-during-request-lifetime` or `deduplicate-until-invalidated`
// with the same `deduplicationKey` will share the same result until a request
// is started with policy `do-not-deduplicate` and a matching entry in
// `invalidateDeduplicationKeys`.
| { policy: 'deduplicate-until-invalidated'; deduplicationKey: string }
// Always run an actual HTTP request and don't allow other requests to
// de-duplicate against it. Additionally, invalidate any listed keys
// immediately: new requests with that deduplicationKey will not match any
// requests that current exist. (The invalidation feature is used so that
// doing (say) `DELETE /path` invalidates any result for `GET /path` within
// the deduplication store.)
| { policy: 'do-not-deduplicate'; invalidateDeduplicationKeys?: string[] };
export abstract class RESTDataSource {
httpCache: HTTPCache;
memoizedResults = new Map<string, Promise<any>>();
protected httpCache: HTTPCache;
protected deduplicationPromises = new Map<string, Promise<any>>();
baseURL?: string;
memoizeGetRequests: boolean = true;

@@ -72,13 +167,64 @@ constructor(config?: DataSourceConfig) {

// By default, we use the full request URL as the cache key.
// You can override this to remove query parameters or compute a cache key in any way that makes sense.
// For example, you could use this to take Vary header fields into account.
// Although we do validate header fields and don't serve responses from cache when they don't match,
// new responses overwrite old ones with different vary header fields.
protected cacheKeyFor(url: URL, _request: RequestOptions): string {
return url.toString();
// By default, we use `cacheKey` from the request if provided, or the full
// request URL. You can override this to remove query parameters or compute a
// cache key in any way that makes sense. For example, you could use this to
// take header fields into account (the kinds of fields you expect to show up
// in Vary in the response). Although we do parse Vary in responses so that we
// won't return a cache entry whose Vary-ed header field doesn't match, new
// responses can overwrite old ones with different Vary-ed header fields if
// you don't take the header into account in the cache key.
protected cacheKeyFor(url: URL, request: RequestOptions): string {
return request.cacheKey ?? `${request.method ?? 'GET'} ${url}`;
}
/**
* Calculates the deduplication policy for the request.
*
* By default, GET requests have the policy
* `deduplicate-during-request-lifetime` with deduplication key `GET
* ${cacheKey}`, and all other requests have the policy `do-not-deduplicate`
* and invalidate `GET ${cacheKey}`, where `cacheKey` is the value returned by
* `cacheKeyFor` (and is the same cache key used in the HTTP-header-sensitive
* shared cache).
*
* Note that the default cache key only contains the URL (not the method,
* headers, body, etc), so if you send multiple GET requests that differ only
* in headers (etc), or if you change your policy to allow non-GET requests to
* be deduplicated, you may want to put more information into the cache key or
* be careful to keep the HTTP method in the deduplication key.
*/
protected requestDeduplicationPolicyFor(
url: URL,
request: RequestOptions,
): RequestDeduplicationPolicy {
const method = request.method ?? 'GET';
// Start with the cache key that is used for the shared header-sensitive
// cache. Note that its default implementation does not include the HTTP
// method, so if a subclass overrides this and allows non-GET/HEADs to be
// de-duplicated it will be important for it to include (at least!) the
// method in the deduplication key, so we're explicitly adding GET/HEAD here.
const cacheKey = this.cacheKeyFor(url, request);
if (['GET', 'HEAD'].includes(method)) {
return {
policy: 'deduplicate-during-request-lifetime',
deduplicationKey: cacheKey,
};
} else {
return {
policy: 'do-not-deduplicate',
// Always invalidate GETs and HEADs when a different method is seen on
// the same cache key (ie, URL), as per standard HTTP semantics. (We
// don't have to invalidate the key with this HTTP method because we
// never write it.)
invalidateDeduplicationKeys: [
this.cacheKeyFor(url, { ...request, method: 'GET' }),
this.cacheKeyFor(url, { ...request, method: 'HEAD' }),
],
};
}
}
protected willSendRequest?(
requestOpts: WillSendRequestOptions,
path: string,
requestOpts: AugmentedRequest,
): ValueOrPromise<void>;

@@ -88,16 +234,5 @@

path: string,
_request: RequestOptions,
_request: AugmentedRequest,
): ValueOrPromise<URL> {
if (path.startsWith('/')) {
path = path.slice(1);
}
const baseURL = this.baseURL;
if (baseURL) {
const normalizedBaseURL = baseURL.endsWith('/')
? baseURL
: baseURL.concat('/');
return new URL(path, normalizedBaseURL);
} else {
return new URL(path);
}
return new URL(path, this.baseURL);
}

@@ -109,15 +244,4 @@

request: FetcherRequestInit,
): CacheOptions | undefined;
): ValueOrPromise<CacheOptions | undefined>;
protected async didReceiveResponse<TResult = any>(
response: FetcherResponse,
_request: RequestOptions,
): Promise<TResult> {
if (response.ok) {
return this.parseBody(response) as any as Promise<TResult>;
} else {
throw await this.errorFromResponse(response);
}
}
protected didEncounterError(error: Error, _request: RequestOptions) {

@@ -127,2 +251,10 @@ throw error;

// Reads the body of the response and returns it in parsed form. If you want
// to process data in some other way (eg, reading binary data), override this
// method. It's important that the body always read in full (otherwise the
// clone of this response that is being read to write to the HTTPCache could
// block and lead to a memory leak).
//
// If you override this to return interesting new mutable data types, override
// cloneParsedBody too.
protected parseBody(response: FetcherResponse): Promise<object | string> {

@@ -146,26 +278,86 @@ const contentType = response.headers.get('Content-Type');

protected async errorFromResponse(response: FetcherResponse) {
const message = `${response.status}: ${response.statusText}`;
private cloneDataSourceFetchResult<TResult>(
dataSourceFetchResult: Omit<
DataSourceFetchResult<TResult>,
'requestDeduplication'
>,
requestDeduplicationResult: RequestDeduplicationResult,
): DataSourceFetchResult<TResult> {
return {
...dataSourceFetchResult,
requestDeduplication: requestDeduplicationResult,
parsedBody: this.cloneParsedBody(dataSourceFetchResult.parsedBody),
};
}
let error: GraphQLError;
if (response.status === 401) {
error = new AuthenticationError(message);
} else if (response.status === 403) {
error = new ForbiddenError(message);
protected cloneParsedBody<TResult>(parsedBody: TResult) {
if (typeof parsedBody === 'string') {
return parsedBody;
} else {
error = new GraphQLError(message);
return JSON.parse(JSON.stringify(parsedBody));
}
}
const body = await this.parseBody(response);
protected shouldJSONSerializeBody(body: RequestWithBody['body']): boolean {
return !!(
// We accept arbitrary objects and arrays as body and serialize them as JSON.
(
Array.isArray(body) ||
isPlainObject(body) ||
// We serialize any objects that have a toJSON method (except Buffers or things that look like FormData)
(body &&
typeof body === 'object' &&
'toJSON' in body &&
typeof (body as any).toJSON === 'function' &&
!(body instanceof Buffer) &&
// XXX this is a bit of a hacky check for FormData-like objects (in
// case a FormData implementation has a toJSON method on it)
(body as any).constructor?.name !== 'FormData')
)
);
}
Object.assign(error.extensions, {
response: {
url: response.url,
status: response.status,
statusText: response.statusText,
body,
protected async throwIfResponseIsError(options: {
url: URL;
request: RequestOptions;
response: FetcherResponse;
parsedBody: unknown;
}) {
if (options.response.ok) {
return;
}
throw await this.errorFromResponse(options);
}
protected async errorFromResponse({
response,
parsedBody,
}: {
url: URL;
request: RequestOptions;
response: FetcherResponse;
parsedBody: unknown;
}) {
return new GraphQLError(`${response.status}: ${response.statusText}`, {
extensions: {
...(response.status === 401
? { code: 'UNAUTHENTICATED' }
: response.status === 403
? { code: 'FORBIDDEN' }
: {}),
response: {
url: response.url,
status: response.status,
statusText: response.statusText,
body: parsedBody,
},
},
});
}
return error;
protected async head(
path: string,
request?: HeadRequest,
): Promise<FetcherResponse> {
return (await this.fetch(path, { method: 'HEAD', ...request })).response;
}

@@ -177,3 +369,8 @@

): Promise<TResult> {
return this.fetch<TResult>(path, { method: 'GET', ...request });
return (
await this.fetch<TResult>(path, {
method: 'GET',
...request,
})
).parsedBody;
}

@@ -185,3 +382,8 @@

): Promise<TResult> {
return this.fetch<TResult>(path, { method: 'POST', ...request });
return (
await this.fetch<TResult>(path, {
method: 'POST',
...request,
})
).parsedBody;
}

@@ -193,3 +395,8 @@

): Promise<TResult> {
return this.fetch<TResult>(path, { method: 'PATCH', ...request });
return (
await this.fetch<TResult>(path, {
method: 'PATCH',
...request,
})
).parsedBody;
}

@@ -201,3 +408,8 @@

): Promise<TResult> {
return this.fetch<TResult>(path, { method: 'PUT', ...request });
return (
await this.fetch<TResult>(path, {
method: 'PUT',
...request,
})
).parsedBody;
}

@@ -209,64 +421,110 @@

): Promise<TResult> {
return this.fetch<TResult>(path, { method: 'DELETE', ...request });
return (
await this.fetch<TResult>(path, {
method: 'DELETE',
...request,
})
).parsedBody;
}
private async fetch<TResult>(
private urlSearchParamsFromRecord(
params: Record<string, string | undefined> | undefined,
): URLSearchParams {
const usp = new URLSearchParams();
if (params) {
for (const [name, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
usp.set(name, value);
}
}
}
return usp;
}
public async fetch<TResult>(
path: string,
request: DataSourceRequest,
): Promise<TResult> {
const modifiedRequest: WillSendRequestOptions = {
...request,
incomingRequest: DataSourceRequest = {},
): Promise<DataSourceFetchResult<TResult>> {
const augmentedRequest: AugmentedRequest = {
...incomingRequest,
// guarantee params and headers objects before calling `willSendRequest` for convenience
params:
request.params instanceof URLSearchParams
? request.params
: new URLSearchParams(request.params),
headers: request.headers ?? Object.create(null),
body: undefined,
incomingRequest.params instanceof URLSearchParams
? incomingRequest.params
: this.urlSearchParamsFromRecord(incomingRequest.params),
headers: incomingRequest.headers ?? Object.create(null),
};
// Default to GET in the case that `fetch` is called directly with no method
// provided. Our other request methods all provide one.
if (!augmentedRequest.method) augmentedRequest.method = 'GET';
if (this.willSendRequest) {
await this.willSendRequest(modifiedRequest);
await this.willSendRequest(path, augmentedRequest);
}
const url = await this.resolveURL(path, modifiedRequest);
const url = await this.resolveURL(path, augmentedRequest);
// Append params to existing params in the path
for (const [name, value] of modifiedRequest.params as URLSearchParams) {
for (const [name, value] of augmentedRequest.params as URLSearchParams) {
url.searchParams.append(name, value);
}
// We accept arbitrary objects and arrays as body and serialize them as JSON
if (
request.body !== undefined &&
request.body !== null &&
(request.body.constructor === Object ||
Array.isArray(request.body) ||
((request.body as any).toJSON &&
typeof (request.body as any).toJSON === 'function'))
) {
modifiedRequest.body = JSON.stringify(request.body);
if (this.shouldJSONSerializeBody(augmentedRequest.body)) {
augmentedRequest.body = JSON.stringify(augmentedRequest.body);
// If Content-Type header has not been previously set, set to application/json
if (!modifiedRequest.headers) {
modifiedRequest.headers = { 'content-type': 'application/json' };
} else if (!modifiedRequest.headers['content-type']) {
modifiedRequest.headers['content-type'] = 'application/json';
if (!augmentedRequest.headers) {
augmentedRequest.headers = { 'content-type': 'application/json' };
} else if (!augmentedRequest.headers['content-type']) {
augmentedRequest.headers['content-type'] = 'application/json';
}
}
const cacheKey = this.cacheKeyFor(url, modifiedRequest);
// At this point we know the `body` is a `string`, `Buffer`, or `undefined`
// (not possibly an `object`).
const outgoingRequest = augmentedRequest as WithRequired<
RequestOptions,
'method'
>;
const performRequest = async () => {
return this.trace(url, modifiedRequest, async () => {
const cacheOptions = modifiedRequest.cacheOptions
? modifiedRequest.cacheOptions
return this.trace(url, outgoingRequest, async () => {
const cacheKey = this.cacheKeyFor(url, outgoingRequest);
const cacheOptions = outgoingRequest.cacheOptions
? outgoingRequest.cacheOptions
: this.cacheOptionsFor?.bind(this);
try {
const response = await this.httpCache.fetch(url, modifiedRequest, {
cacheKey,
cacheOptions,
const { response, cacheWritePromise } = await this.httpCache.fetch(
url,
outgoingRequest,
{
cacheKey,
cacheOptions,
httpCacheSemanticsCachePolicyOptions:
outgoingRequest.httpCacheSemanticsCachePolicyOptions,
},
);
if (cacheWritePromise) {
this.catchCacheWritePromiseErrors(cacheWritePromise);
}
const parsedBody = await this.parseBody(response);
await this.throwIfResponseIsError({
url,
request: outgoingRequest,
response,
parsedBody,
});
return await this.didReceiveResponse(response, modifiedRequest);
return {
parsedBody: parsedBody as any as TResult,
response,
httpCache: {
cacheWritePromise,
},
};
} catch (error) {
this.didEncounterError(error as Error, modifiedRequest);
this.didEncounterError(error as Error, outgoingRequest);
throw error;
}

@@ -278,19 +536,63 @@ });

// Disabling the request cache does not disable the response cache
if (this.memoizeGetRequests) {
if (request.method === 'GET') {
let promise = this.memoizedResults.get(cacheKey);
if (promise) return promise;
const policy = this.requestDeduplicationPolicyFor(url, outgoingRequest);
if (
policy.policy === 'deduplicate-during-request-lifetime' ||
policy.policy === 'deduplicate-until-invalidated'
) {
const previousRequestPromise = this.deduplicationPromises.get(
policy.deduplicationKey,
);
if (previousRequestPromise)
return previousRequestPromise.then((result) =>
this.cloneDataSourceFetchResult(result, {
policy,
deduplicatedAgainstPreviousRequest: true,
}),
);
promise = performRequest();
this.memoizedResults.set(cacheKey, promise);
return promise;
} else {
this.memoizedResults.delete(cacheKey);
return performRequest();
const thisRequestPromise = performRequest();
this.deduplicationPromises.set(
policy.deduplicationKey,
thisRequestPromise,
);
try {
// The request promise needs to be awaited here rather than just
// returned. This ensures that the request completes before it's removed
// from the cache. Additionally, the use of finally here guarantees the
// deduplication cache is cleared in the event of an error during the
// request.
//
// Note: we could try to get fancy and only clone if no de-duplication
// happened (and we're "deduplicate-during-request-lifetime") but we
// haven't quite bothered yet.
return this.cloneDataSourceFetchResult(await thisRequestPromise, {
policy,
deduplicatedAgainstPreviousRequest: false,
});
} finally {
if (policy.policy === 'deduplicate-during-request-lifetime') {
this.deduplicationPromises.delete(policy.deduplicationKey);
}
}
} else {
return performRequest();
for (const key of policy.invalidateDeduplicationKeys ?? []) {
this.deduplicationPromises.delete(key);
}
return {
...(await performRequest()),
requestDeduplication: {
policy,
deduplicatedAgainstPreviousRequest: false,
},
};
}
}
// Override this method to handle these errors in a different way.
protected catchCacheWritePromiseErrors(cacheWritePromise: Promise<void>) {
cacheWritePromise.catch((e) => {
console.error(`Error writing from RESTDataSource to cache: ${e}`);
});
}
protected async trace<TResult>(

@@ -316,15 +618,1 @@ url: URL,

}
export class AuthenticationError extends GraphQLError {
constructor(message: string) {
super(message, { extensions: { code: 'UNAUTHENTICATED' } });
this.name = 'AuthenticationError';
}
}
export class ForbiddenError extends GraphQLError {
constructor(message: string) {
super(message, { extensions: { code: 'FORBIDDEN' } });
this.name = 'ForbiddenError';
}
}

@@ -25,6 +25,3 @@ {

"baseUrl": ".",
"paths": {
"*" : ["types/*"]
}
}
}

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

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc