@graphql-tools/url-loader
Version:
A set of utils for faster development of GraphQL tools
629 lines (628 loc) • 29.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UrlLoader = exports.SubscriptionProtocol = void 0;
const tslib_1 = require("tslib");
/* eslint-disable no-case-declarations */
/// <reference lib="dom" />
const graphql_1 = require("@graphql-tools/graphql");
const utils_1 = require("@graphql-tools/utils");
const wrap_1 = require("@graphql-tools/wrap");
const graphql_ws_1 = require("graphql-ws");
const isomorphic_ws_1 = tslib_1.__importDefault(require("isomorphic-ws"));
const extract_files_1 = require("extract-files");
const value_or_promise_1 = require("value-or-promise");
const graphql_live_query_1 = require("@n1ru4l/graphql-live-query");
const defaultAsyncFetch_js_1 = require("./defaultAsyncFetch.js");
const defaultSyncFetch_js_1 = require("./defaultSyncFetch.js");
const handleMultipartMixedResponse_js_1 = require("./handleMultipartMixedResponse.js");
const handleEventStreamResponse_js_1 = require("./event-stream/handleEventStreamResponse.js");
const addCancelToResponseStream_js_1 = require("./event-stream/addCancelToResponseStream.js");
const fetch_1 = require("@whatwg-node/fetch");
const utils_js_1 = require("./utils.js");
const asyncImport = (moduleName) => Promise.resolve().then(() => tslib_1.__importStar(require(moduleName)));
const syncImport = (moduleName) => require(moduleName);
var SubscriptionProtocol;
(function (SubscriptionProtocol) {
SubscriptionProtocol["WS"] = "WS";
/**
* Use legacy web socket protocol `graphql-ws` instead of the more current standard `graphql-transport-ws`
*/
SubscriptionProtocol["LEGACY_WS"] = "LEGACY_WS";
/**
* Use SSE for subscription instead of WebSocket
*/
SubscriptionProtocol["SSE"] = "SSE";
/**
* Use `graphql-sse` for subscriptions
*/
SubscriptionProtocol["GRAPHQL_SSE"] = "GRAPHQL_SSE";
})(SubscriptionProtocol = exports.SubscriptionProtocol || (exports.SubscriptionProtocol = {}));
function isCompatibleUri(uri) {
try {
// eslint-disable-next-line no-new
new URL(uri);
return true;
}
catch (_a) {
return false;
}
}
/**
* This loader loads a schema from a URL. The loaded schema is a fully-executable,
* remote schema since it's created using [@graphql-tools/wrap](/docs/remote-schemas).
*
* ```
* const schema = await loadSchema('http://localhost:3000/graphql', {
* loaders: [
* new UrlLoader(),
* ]
* });
* ```
*/
class UrlLoader {
createFormDataFromVariables({ query, variables, operationName, extensions, }) {
const vars = Object.assign({}, variables);
const { clone, files } = (0, extract_files_1.extractFiles)(vars, 'variables', ((v) => (0, extract_files_1.isExtractableFile)(v) ||
(v === null || v === void 0 ? void 0 : v.promise) ||
(0, utils_1.isAsyncIterable)(v) ||
(v === null || v === void 0 ? void 0 : v.then) ||
typeof (v === null || v === void 0 ? void 0 : v.arrayBuffer) === 'function'));
if (files.size === 0) {
return JSON.stringify({
query,
variables,
operationName,
extensions,
});
}
const map = {};
const uploads = [];
let currIndex = 0;
for (const [file, curr] of files) {
map[currIndex] = curr;
uploads[currIndex] = file;
currIndex++;
}
const form = new fetch_1.FormData();
form.append('operations', JSON.stringify({
query,
variables: clone,
operationName,
extensions,
}));
form.append('map', JSON.stringify(map));
function handleUpload(upload, i) {
const indexStr = i.toString();
if (upload != null) {
const filename = upload.filename || upload.name || upload.path || `blob-${indexStr}`;
if ((0, utils_js_1.isPromiseLike)(upload)) {
return upload.then((resolvedUpload) => handleUpload(resolvedUpload, i));
// If Blob
}
else if ((0, utils_js_1.isBlob)(upload)) {
form.append(indexStr, upload, filename);
}
else if ((0, utils_js_1.isGraphQLUpload)(upload)) {
const stream = upload.createReadStream();
const chunks = [];
return Promise.resolve().then(async () => {
for await (const chunk of stream) {
if (chunk) {
chunks.push(...chunk);
}
}
const blobPart = new Uint8Array(chunks);
form.append(indexStr, new fetch_1.File([blobPart], filename, { type: upload.mimetype }), filename);
});
}
else {
form.append(indexStr, new fetch_1.File([upload], filename), filename);
}
}
}
return value_or_promise_1.ValueOrPromise.all(uploads.map((upload, i) => new value_or_promise_1.ValueOrPromise(() => handleUpload(upload, i))))
.then(() => form)
.resolve();
}
prepareGETUrl({ baseUrl, query, variables, operationName, extensions, }) {
const HTTP_URL = switchProtocols(baseUrl, {
wss: 'https',
ws: 'http',
});
const dummyHostname = 'https://dummyhostname.com';
const validUrl = HTTP_URL.startsWith('http')
? HTTP_URL
: HTTP_URL.startsWith('/')
? `${dummyHostname}${HTTP_URL}`
: `${dummyHostname}/${HTTP_URL}`;
const urlObj = new URL(validUrl);
urlObj.searchParams.set('query', query);
if (variables && Object.keys(variables).length > 0) {
urlObj.searchParams.set('variables', JSON.stringify(variables));
}
if (operationName) {
urlObj.searchParams.set('operationName', operationName);
}
if (extensions) {
urlObj.searchParams.set('extensions', JSON.stringify(extensions));
}
const finalUrl = urlObj.toString().replace(dummyHostname, '');
return finalUrl;
}
buildHTTPExecutor(initialEndpoint, fetch, options) {
const defaultMethod = this.getDefaultMethodFromOptions(options === null || options === void 0 ? void 0 : options.method, 'POST');
const HTTP_URL = switchProtocols(initialEndpoint, {
wss: 'https',
ws: 'http',
});
const executor = (request) => {
var _a, _b, _c;
const controller = (0, addCancelToResponseStream_js_1.cancelNeeded)() ? new fetch_1.AbortController() : undefined;
let method = defaultMethod;
const operationAst = (0, utils_1.getOperationASTFromRequest)(request);
const operationType = operationAst.operation;
if ((options === null || options === void 0 ? void 0 : options.useGETForQueries) && operationType === 'query') {
method = 'GET';
}
let accept = 'application/json, multipart/mixed';
// @ts-expect-error Uses graphql-js so it doesn't like us
if (operationType === 'subscription' || (0, graphql_live_query_1.isLiveQueryOperationDefinitionNode)(operationAst)) {
method = 'GET';
accept = 'text/event-stream';
}
else if ((_a = operationAst.directives) === null || _a === void 0 ? void 0 : _a.some(({ name }) => name.value === 'defer' || name.value === 'stream')) {
accept += ', multipart/mixed';
}
const endpoint = ((_b = request.extensions) === null || _b === void 0 ? void 0 : _b.endpoint) || HTTP_URL;
const headers = Object.assign({
accept,
}, options === null || options === void 0 ? void 0 : options.headers, ((_c = request.extensions) === null || _c === void 0 ? void 0 : _c.headers) || {});
const query = (0, graphql_1.print)(request.document);
const requestBody = {
query,
variables: request.variables,
operationName: request.operationName,
extensions: request.extensions,
};
let timeoutId;
if (options === null || options === void 0 ? void 0 : options.timeout) {
timeoutId = setTimeout(() => {
if (!(controller === null || controller === void 0 ? void 0 : controller.signal.aborted)) {
controller === null || controller === void 0 ? void 0 : controller.abort();
}
}, options.timeout);
}
const credentials = (options === null || options === void 0 ? void 0 : options.credentials) !== 'disable' ? (options === null || options === void 0 ? void 0 : options.credentials) || 'same-origin' : null;
return new value_or_promise_1.ValueOrPromise(() => {
switch (method) {
case 'GET':
const finalUrl = this.prepareGETUrl({
baseUrl: endpoint,
...requestBody,
});
return fetch(finalUrl, {
method: 'GET',
...(credentials != null ? { credentials } : {}),
headers,
signal: controller === null || controller === void 0 ? void 0 : controller.signal,
});
case 'POST':
if (options === null || options === void 0 ? void 0 : options.multipart) {
return new value_or_promise_1.ValueOrPromise(() => this.createFormDataFromVariables(requestBody))
.then(body => fetch(endpoint, {
method: 'POST',
...(credentials != null ? { credentials } : {}),
body,
headers: {
...headers,
...(typeof body === 'string' ? { 'content-type': 'application/json' } : {}),
},
signal: controller === null || controller === void 0 ? void 0 : controller.signal,
}))
.resolve();
}
else {
return fetch(endpoint, {
method: 'POST',
...(credentials != null ? { credentials } : {}),
body: JSON.stringify(requestBody),
headers: {
'content-type': 'application/json',
...headers,
},
signal: controller === null || controller === void 0 ? void 0 : controller.signal,
});
}
}
})
.then((fetchResult) => {
if (timeoutId != null) {
clearTimeout(timeoutId);
}
// Retry should respect HTTP Errors
if ((options === null || options === void 0 ? void 0 : options.retry) != null && !fetchResult.status.toString().startsWith('2')) {
throw new Error(fetchResult.statusText || `HTTP Error: ${fetchResult.status}`);
}
const contentType = fetchResult.headers.get('content-type');
if (contentType === null || contentType === void 0 ? void 0 : contentType.includes('text/event-stream')) {
return (0, handleEventStreamResponse_js_1.handleEventStreamResponse)(fetchResult, controller);
}
else if (contentType === null || contentType === void 0 ? void 0 : contentType.includes('multipart/mixed')) {
return (0, handleMultipartMixedResponse_js_1.handleMultipartMixedResponse)(fetchResult, controller);
}
return fetchResult.text();
})
.then(result => {
if (typeof result === 'string') {
if (result) {
return JSON.parse(result);
}
}
else {
return result;
}
})
.resolve();
};
if ((options === null || options === void 0 ? void 0 : options.retry) != null) {
return function retryExecutor(request) {
let result;
let error;
let attempt = 0;
function retryAttempt() {
attempt++;
if (attempt > options.retry) {
if (result != null) {
return result;
}
if (error != null) {
throw error;
}
throw new Error('No result');
}
return new value_or_promise_1.ValueOrPromise(() => executor(request))
.then(res => {
var _a;
result = res;
if ((_a = result === null || result === void 0 ? void 0 : result.errors) === null || _a === void 0 ? void 0 : _a.length) {
return retryAttempt();
}
return result;
})
.catch((e) => {
error = e;
return retryAttempt();
})
.resolve();
}
return retryAttempt();
};
}
return executor;
}
buildWSExecutor(subscriptionsEndpoint, webSocketImpl, connectionParams) {
const WS_URL = switchProtocols(subscriptionsEndpoint, {
https: 'wss',
http: 'ws',
});
const subscriptionClient = (0, graphql_ws_1.createClient)({
url: WS_URL,
webSocketImpl,
connectionParams,
lazy: true,
});
return ({ document, variables, operationName, extensions }) => {
const query = (0, graphql_1.print)(document);
return (0, utils_1.observableToAsyncIterable)({
subscribe: observer => {
const unsubscribe = subscriptionClient.subscribe({
query,
variables: variables,
operationName,
extensions,
},
// @ts-expect-error Uses graphql-js so it doesn't like us
observer);
return {
unsubscribe,
};
},
});
};
}
buildWSLegacyExecutor(subscriptionsEndpoint, WebSocketImpl, options) {
const WS_URL = switchProtocols(subscriptionsEndpoint, {
https: 'wss',
http: 'ws',
});
const observerById = new Map();
let websocket = null;
const ensureWebsocket = () => {
websocket = new WebSocketImpl(WS_URL, 'graphql-ws', {
followRedirects: true,
headers: options === null || options === void 0 ? void 0 : options.headers,
rejectUnauthorized: false,
skipUTF8Validation: true,
});
websocket.onopen = () => {
let payload = {};
switch (typeof (options === null || options === void 0 ? void 0 : options.connectionParams)) {
case 'function':
payload = options === null || options === void 0 ? void 0 : options.connectionParams();
break;
case 'object':
payload = options === null || options === void 0 ? void 0 : options.connectionParams;
break;
}
websocket.send(JSON.stringify({
type: utils_js_1.LEGACY_WS.CONNECTION_INIT,
payload,
}));
};
};
const cleanupWebsocket = () => {
if (websocket != null && observerById.size === 0) {
websocket.send(JSON.stringify({
type: utils_js_1.LEGACY_WS.CONNECTION_TERMINATE,
}));
websocket.terminate();
websocket = null;
}
};
return function legacyExecutor(request) {
const id = Date.now().toString();
return (0, utils_1.observableToAsyncIterable)({
subscribe(observer) {
ensureWebsocket();
if (websocket == null) {
throw new Error(`WebSocket connection is not found!`);
}
websocket.onmessage = event => {
const data = JSON.parse(event.data.toString('utf-8'));
switch (data.type) {
case utils_js_1.LEGACY_WS.CONNECTION_ACK: {
if (websocket == null) {
throw new Error(`WebSocket connection is not found!`);
}
websocket.send(JSON.stringify({
type: utils_js_1.LEGACY_WS.START,
id,
payload: {
query: (0, graphql_1.print)(request.document),
variables: request.variables,
operationName: request.operationName,
},
}));
break;
}
case utils_js_1.LEGACY_WS.CONNECTION_ERROR: {
observer.error(data.payload);
break;
}
case utils_js_1.LEGACY_WS.CONNECTION_KEEP_ALIVE: {
break;
}
case utils_js_1.LEGACY_WS.DATA: {
observer.next(data.payload);
break;
}
case utils_js_1.LEGACY_WS.COMPLETE: {
if (websocket == null) {
throw new Error(`WebSocket connection is not found!`);
}
websocket.send(JSON.stringify({
type: utils_js_1.LEGACY_WS.CONNECTION_TERMINATE,
}));
observer.complete();
cleanupWebsocket();
break;
}
}
};
return {
unsubscribe: () => {
websocket === null || websocket === void 0 ? void 0 : websocket.send(JSON.stringify({
type: utils_js_1.LEGACY_WS.STOP,
id,
}));
cleanupWebsocket();
},
};
},
});
};
}
getFetch(customFetch, importFn) {
if (customFetch) {
if (typeof customFetch === 'string') {
const [moduleName, fetchFnName] = customFetch.split('#');
return new value_or_promise_1.ValueOrPromise(() => importFn(moduleName))
.then(module => (fetchFnName ? module[fetchFnName] : module))
.resolve();
}
else if (typeof customFetch === 'function') {
return customFetch;
}
}
if (importFn === asyncImport) {
return defaultAsyncFetch_js_1.defaultAsyncFetch;
}
else {
return defaultSyncFetch_js_1.defaultSyncFetch;
}
}
getDefaultMethodFromOptions(method, defaultMethod) {
if (method) {
defaultMethod = method;
}
return defaultMethod;
}
getWebSocketImpl(importFn, options) {
if (typeof (options === null || options === void 0 ? void 0 : options.webSocketImpl) === 'string') {
const [moduleName, webSocketImplName] = options.webSocketImpl.split('#');
return new value_or_promise_1.ValueOrPromise(() => importFn(moduleName))
.then(importedModule => (webSocketImplName ? importedModule[webSocketImplName] : importedModule))
.resolve();
}
else {
const websocketImpl = (options === null || options === void 0 ? void 0 : options.webSocketImpl) || isomorphic_ws_1.default;
return websocketImpl;
}
}
buildSubscriptionExecutor(subscriptionsEndpoint, fetch, importFn, options) {
if ((options === null || options === void 0 ? void 0 : options.subscriptionsProtocol) === SubscriptionProtocol.SSE) {
return this.buildHTTPExecutor(subscriptionsEndpoint, fetch, options);
}
else if ((options === null || options === void 0 ? void 0 : options.subscriptionsProtocol) === SubscriptionProtocol.GRAPHQL_SSE) {
if (!(options === null || options === void 0 ? void 0 : options.subscriptionsEndpoint)) {
// when no custom subscriptions endpoint is specified,
// graphql-sse is recommended to be used on `/graphql/stream`
subscriptionsEndpoint += '/stream';
}
return this.buildHTTPExecutor(subscriptionsEndpoint, fetch, options);
}
else {
const webSocketImpl$ = new value_or_promise_1.ValueOrPromise(() => this.getWebSocketImpl(importFn, options));
const executor$ = webSocketImpl$.then(webSocketImpl => {
if ((options === null || options === void 0 ? void 0 : options.subscriptionsProtocol) === SubscriptionProtocol.LEGACY_WS) {
return this.buildWSLegacyExecutor(subscriptionsEndpoint, webSocketImpl, options);
}
else {
return this.buildWSExecutor(subscriptionsEndpoint, webSocketImpl, options === null || options === void 0 ? void 0 : options.connectionParams);
}
});
return request => executor$.then(executor => executor(request)).resolve();
}
}
getExecutor(endpoint, importFn, options) {
const fetch$ = new value_or_promise_1.ValueOrPromise(() => this.getFetch(options === null || options === void 0 ? void 0 : options.customFetch, importFn));
const httpExecutor$ = fetch$.then(fetch => {
return this.buildHTTPExecutor(endpoint, fetch, options);
});
if ((options === null || options === void 0 ? void 0 : options.subscriptionsEndpoint) != null || (options === null || options === void 0 ? void 0 : options.subscriptionsProtocol) !== SubscriptionProtocol.SSE) {
const subscriptionExecutor$ = fetch$.then(fetch => {
const subscriptionsEndpoint = (options === null || options === void 0 ? void 0 : options.subscriptionsEndpoint) || endpoint;
return this.buildSubscriptionExecutor(subscriptionsEndpoint, fetch, importFn, options);
});
// eslint-disable-next-line no-inner-declarations
function getExecutorByRequest(request) {
const operationAst = (0, utils_1.getOperationASTFromRequest)(request);
if (operationAst.operation === 'subscription' ||
// @ts-expect-error Uses graphql-js so it doesn't like us
(0, graphql_live_query_1.isLiveQueryOperationDefinitionNode)(operationAst, request.variables)) {
return subscriptionExecutor$;
}
else {
return httpExecutor$;
}
}
return request => getExecutorByRequest(request)
.then(executor => executor(request))
.resolve();
}
else {
return request => httpExecutor$.then(executor => executor(request)).resolve();
}
}
getExecutorAsync(endpoint, options) {
return this.getExecutor(endpoint, asyncImport, options);
}
getExecutorSync(endpoint, options) {
return this.getExecutor(endpoint, syncImport, options);
}
handleSDL(pointer, fetch, options) {
const defaultMethod = this.getDefaultMethodFromOptions(options === null || options === void 0 ? void 0 : options.method, 'GET');
return new value_or_promise_1.ValueOrPromise(() => fetch(pointer, {
method: defaultMethod,
headers: options.headers,
}))
.then(response => response.text())
.then(schemaString => (0, utils_1.parseGraphQLSDL)(pointer, schemaString, options))
.resolve();
}
async load(pointer, options) {
if (!isCompatibleUri(pointer)) {
return [];
}
let source = {
location: pointer,
};
let executor;
if ((options === null || options === void 0 ? void 0 : options.handleAsSDL) || pointer.endsWith('.graphql') || pointer.endsWith('.graphqls')) {
const fetch = await this.getFetch(options === null || options === void 0 ? void 0 : options.customFetch, asyncImport);
source = await this.handleSDL(pointer, fetch, options);
if (!source.schema && !source.document && !source.rawSDL) {
throw new Error(`Invalid SDL response`);
}
source.schema =
source.schema ||
(source.document
? (0, graphql_1.buildASTSchema)(source.document, options)
: source.rawSDL
? (0, graphql_1.buildSchema)(source.rawSDL, options)
: undefined);
}
else {
executor = this.getExecutorAsync(pointer, options);
source.schema = await (0, wrap_1.introspectSchema)(executor, {}, options);
}
if (!source.schema) {
throw new Error(`Invalid introspected schema`);
}
if (options === null || options === void 0 ? void 0 : options.endpoint) {
executor = this.getExecutorAsync(options.endpoint, options);
}
if (executor) {
source.schema = (0, wrap_1.wrapSchema)({
schema: source.schema,
executor,
batch: options === null || options === void 0 ? void 0 : options.batch,
});
}
return [source];
}
loadSync(pointer, options) {
if (!isCompatibleUri(pointer)) {
return [];
}
let source = {
location: pointer,
};
let executor;
if ((options === null || options === void 0 ? void 0 : options.handleAsSDL) || pointer.endsWith('.graphql') || pointer.endsWith('.graphqls')) {
const fetch = this.getFetch(options === null || options === void 0 ? void 0 : options.customFetch, syncImport);
source = this.handleSDL(pointer, fetch, options);
if (!source.schema && !source.document && !source.rawSDL) {
throw new Error(`Invalid SDL response`);
}
source.schema =
source.schema ||
(source.document
? (0, graphql_1.buildASTSchema)(source.document, options)
: source.rawSDL
? (0, graphql_1.buildSchema)(source.rawSDL, options)
: undefined);
}
else {
executor = this.getExecutorSync(pointer, options);
source.schema = (0, wrap_1.introspectSchema)(executor, {}, options);
}
if (!source.schema) {
throw new Error(`Invalid introspected schema`);
}
if (options === null || options === void 0 ? void 0 : options.endpoint) {
executor = this.getExecutorSync(options.endpoint, options);
}
if (executor) {
source.schema = (0, wrap_1.wrapSchema)({
schema: source.schema,
executor,
});
}
return [source];
}
}
exports.UrlLoader = UrlLoader;
function switchProtocols(pointer, protocolMap) {
return Object.entries(protocolMap).reduce((prev, [source, target]) => prev.replace(`${source}://`, `${target}://`).replace(`${source}:\\`, `${target}:\\`), pointer);
}