@graphql-tools/url-loader
Version:
A set of utils for faster development of GraphQL tools
278 lines (277 loc) • 11.2 kB
JavaScript
import { buildASTSchema, buildSchema } from 'graphql';
import WebSocket from 'isomorphic-ws';
import { ValueOrPromise } from 'value-or-promise';
import { buildGraphQLWSExecutor } from '@graphql-tools/executor-graphql-ws';
import { buildHTTPExecutor, isLiveQueryOperationDefinitionNode, } from '@graphql-tools/executor-http';
import { buildWSLegacyExecutor } from '@graphql-tools/executor-legacy-ws';
import { getOperationASTFromRequest, parseGraphQLSDL, } from '@graphql-tools/utils';
import { schemaFromExecutor, wrapSchema } from '@graphql-tools/wrap';
import { defaultAsyncFetch } from './defaultAsyncFetch.js';
import { defaultSyncFetch } from './defaultSyncFetch.js';
const asyncImport = (moduleName) => import(`${moduleName}`);
// eslint-disable-next-line @typescript-eslint/no-require-imports
const syncImport = (moduleName) => require(`${moduleName}`);
export 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 || (SubscriptionProtocol = {}));
const acceptableProtocols = ['http:', 'https:', 'ws:', 'wss:'];
function isCompatibleUri(uri) {
try {
const url = new URL(uri);
return acceptableProtocols.includes(url.protocol);
}
catch {
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(),
* ]
* });
* ```
*/
export class UrlLoader {
buildHTTPExecutor(initialEndpoint, fetchFn, options) {
const HTTP_URL = switchProtocols(initialEndpoint, {
wss: 'https',
ws: 'http',
});
return buildHTTPExecutor({
endpoint: HTTP_URL,
fetch: fetchFn,
...options,
});
}
buildWSExecutor(subscriptionsEndpoint, webSocketImpl, connectionParams) {
const WS_URL = switchProtocols(subscriptionsEndpoint, {
https: 'wss',
http: 'ws',
});
return buildGraphQLWSExecutor({
url: WS_URL,
webSocketImpl,
connectionParams,
});
}
buildWSLegacyExecutor(subscriptionsEndpoint, WebSocketImpl, options) {
const WS_URL = switchProtocols(subscriptionsEndpoint, {
https: 'wss',
http: 'ws',
});
return buildWSLegacyExecutor(WS_URL, WebSocketImpl, options);
}
getFetch(customFetch, importFn) {
if (customFetch) {
if (typeof customFetch === 'string') {
const [moduleName, fetchFnName] = customFetch.split('#');
return new ValueOrPromise(() => importFn(moduleName))
.then(module => (fetchFnName ? module[fetchFnName] : module))
.resolve();
}
else if (typeof customFetch === 'function') {
return customFetch;
}
}
if (importFn === asyncImport) {
return defaultAsyncFetch;
}
else {
return defaultSyncFetch;
}
}
getDefaultMethodFromOptions(method, defaultMethod) {
if (method) {
defaultMethod = method;
}
return defaultMethod;
}
getWebSocketImpl(importFn, options) {
if (typeof options?.webSocketImpl === 'string') {
const [moduleName, webSocketImplName] = options.webSocketImpl.split('#');
return new ValueOrPromise(() => importFn(moduleName))
.then(importedModule => webSocketImplName ? importedModule[webSocketImplName] : importedModule)
.resolve();
}
else {
const websocketImpl = options?.webSocketImpl || WebSocket;
return websocketImpl;
}
}
buildSubscriptionExecutor(subscriptionsEndpoint, fetch, importFn, options) {
if (options?.subscriptionsProtocol === SubscriptionProtocol.SSE) {
return this.buildHTTPExecutor(subscriptionsEndpoint, fetch, options);
}
else if (options?.subscriptionsProtocol === SubscriptionProtocol.GRAPHQL_SSE) {
if (!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 ValueOrPromise(() => this.getWebSocketImpl(importFn, options));
const executor$ = webSocketImpl$.then(webSocketImpl => {
if (options?.subscriptionsProtocol === SubscriptionProtocol.LEGACY_WS) {
return this.buildWSLegacyExecutor(subscriptionsEndpoint, webSocketImpl, options);
}
else {
return this.buildWSExecutor(subscriptionsEndpoint, webSocketImpl, options?.connectionParams);
}
});
return request => executor$.then(executor => executor(request)).resolve();
}
}
getExecutor(endpoint, importFn, options) {
const fetch$ = new ValueOrPromise(() => this.getFetch(options?.customFetch, importFn));
const httpExecutor$ = fetch$.then(fetch => {
return this.buildHTTPExecutor(endpoint, fetch, options);
});
if (options?.subscriptionsEndpoint != null ||
options?.subscriptionsProtocol !== SubscriptionProtocol.SSE) {
const subscriptionExecutor$ = fetch$.then(fetch => {
const subscriptionsEndpoint = options?.subscriptionsEndpoint || endpoint;
return this.buildSubscriptionExecutor(subscriptionsEndpoint, fetch, importFn, options);
});
function getExecutorByRequest(request) {
request.operationType =
request.operationType || getOperationASTFromRequest(request)?.operation;
if (request.operationType === 'subscription' &&
isLiveQueryOperationDefinitionNode(getOperationASTFromRequest(request))) {
request.operationType = 'subscription';
}
if (request.operationType === 'subscription') {
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?.method, 'GET');
return new ValueOrPromise(() => fetch(pointer, {
method: defaultMethod,
headers: typeof options?.headers === 'function' ? options.headers() : options?.headers,
}))
.then(response => response.text())
.then(schemaString => parseGraphQLSDL(pointer, schemaString, options))
.resolve();
}
async load(pointer, options) {
if (!isCompatibleUri(pointer)) {
return [];
}
let source = {
location: pointer,
};
let executor;
if (options?.handleAsSDL || pointer.endsWith('.graphql') || pointer.endsWith('.graphqls')) {
const fetch = await this.getFetch(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
? buildASTSchema(source.document, options)
: source.rawSDL
? buildSchema(source.rawSDL, options)
: undefined);
}
else {
executor = this.getExecutorAsync(pointer, options);
source.schema = await schemaFromExecutor(executor, {}, options);
}
if (!source.schema) {
throw new Error(`Invalid introspected schema`);
}
if (options?.endpoint) {
executor = this.getExecutorAsync(options.endpoint, options);
}
if (executor) {
source.schema = wrapSchema({
schema: source.schema,
executor,
batch: options?.batch,
});
}
return [source];
}
loadSync(pointer, options) {
if (!isCompatibleUri(pointer)) {
return [];
}
let source = {
location: pointer,
};
let executor;
if (options?.handleAsSDL || pointer.endsWith('.graphql') || pointer.endsWith('.graphqls')) {
const fetch = this.getFetch(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
? buildASTSchema(source.document, options)
: source.rawSDL
? buildSchema(source.rawSDL, options)
: undefined);
}
else {
executor = this.getExecutorSync(pointer, options);
source.schema = schemaFromExecutor(executor, {}, options);
}
if (!source.schema) {
throw new Error(`Invalid introspected schema`);
}
if (options?.endpoint) {
executor = this.getExecutorSync(options.endpoint, options);
}
if (executor) {
source.schema = wrapSchema({
schema: source.schema,
executor,
});
}
return [source];
}
}
function switchProtocols(pointer, protocolMap) {
return Object.entries(protocolMap).reduce((prev, [source, target]) => prev.replace(`${source}://`, `${target}://`).replace(`${source}:\\`, `${target}:\\`), pointer);
}