@subsquid/apollo-server-core
Version:
Core engine for Apollo GraphQL server
576 lines • 25.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isImplicitlyInstallablePlugin = exports.ApolloServerBase = void 0;
const mock_1 = require("@graphql-tools/mock");
const schema_1 = require("@graphql-tools/schema");
const loglevel_1 = __importDefault(require("loglevel"));
const graphql_1 = require("graphql");
const resolvable_1 = __importDefault(require("@josephg/resolvable"));
const utils_keyvaluecache_1 = require("@apollo/utils.keyvaluecache");
const schemaHash_1 = require("./utils/schemaHash");
const requestPipeline_1 = require("./requestPipeline");
const apollo_server_env_1 = require("apollo-server-env");
const apollo_tools_1 = require("@apollographql/apollo-tools");
const runHttpQuery_1 = require("./runHttpQuery");
const isNodeLike_1 = __importDefault(require("./utils/isNodeLike"));
const determineApolloConfig_1 = require("./determineApolloConfig");
const plugin_1 = require("./plugin");
const internalPlugin_1 = require("./internalPlugin");
const cachePolicy_1 = require("./cachePolicy");
const schemaManager_1 = require("./utils/schemaManager");
const uuid = __importStar(require("uuid"));
const UnboundedCache_1 = require("./utils/UnboundedCache");
const NoIntrospection = (context) => ({
Field(node) {
if (node.name.value === '__schema' || node.name.value === '__type') {
context.reportError(new graphql_1.GraphQLError('GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production', [node]));
}
},
});
class UnreachableCaseError extends Error {
constructor(val) {
super(`Unreachable case: ${val}`);
}
}
const recommendedCsrfPreventionRequestHeaders = [
'x-apollo-operation-name',
'apollo-require-preflight',
];
class ApolloServerBase {
constructor(config) {
var _a, _b;
this.graphqlPath = '/graphql';
this.requestOptions = Object.create(null);
this.plugins = [];
this.toDispose = new Set();
this.toDisposeLast = new Set();
this.drainServers = null;
this.landingPage = null;
if (!config)
throw new Error('ApolloServer requires options.');
this.config = {
...config,
nodeEnv: (_a = config.nodeEnv) !== null && _a !== void 0 ? _a : process.env.NODE_ENV,
};
const { context, resolvers, schema, modules, typeDefs, parseOptions = {}, introspection, plugins, gateway, apollo, stopOnTerminationSignals, mocks, mockEntireSchema, documentStore, csrfPrevention, ...requestOptions } = this.config;
if (config.logger) {
this.logger = config.logger;
}
else {
const loglevelLogger = loglevel_1.default.getLogger('apollo-server');
if (this.config.debug === true) {
loglevelLogger.setLevel(loglevel_1.default.levels.DEBUG);
}
else {
loglevelLogger.setLevel(loglevel_1.default.levels.INFO);
}
this.logger = loglevelLogger;
}
this.apolloConfig = (0, determineApolloConfig_1.determineApolloConfig)(apollo, this.logger);
if (gateway && (modules || schema || typeDefs || resolvers)) {
throw new Error('Cannot define both `gateway` and any of: `modules`, `schema`, `typeDefs`, or `resolvers`');
}
this.parseOptions = parseOptions;
this.context = context;
this.csrfPreventionRequestHeaders =
csrfPrevention === true
? recommendedCsrfPreventionRequestHeaders
: csrfPrevention === false
? null
: csrfPrevention === undefined
? null
: (_b = csrfPrevention.requestHeaders) !== null && _b !== void 0 ? _b : recommendedCsrfPreventionRequestHeaders;
const isDev = this.config.nodeEnv !== 'production';
this.stopOnTerminationSignals =
typeof stopOnTerminationSignals === 'boolean'
? stopOnTerminationSignals
: isNodeLike_1.default &&
this.config.nodeEnv !== 'test' &&
!this.serverlessFramework();
if ((typeof introspection === 'boolean' && !introspection) ||
(introspection === undefined && !isDev)) {
const noIntro = [NoIntrospection];
requestOptions.validationRules = requestOptions.validationRules
? requestOptions.validationRules.concat(noIntro)
: noIntro;
}
if (requestOptions.cache === 'bounded') {
requestOptions.cache = new utils_keyvaluecache_1.InMemoryLRUCache();
}
if (!requestOptions.cache) {
requestOptions.cache = new UnboundedCache_1.UnboundedCache();
if (!isDev &&
(requestOptions.persistedQueries === undefined ||
(requestOptions.persistedQueries &&
!requestOptions.persistedQueries.cache))) {
this.logger.warn('Persisted queries are enabled and are using an unbounded cache. Your server' +
' is vulnerable to denial of service attacks via memory exhaustion. ' +
'Set `cache: "bounded"` or `persistedQueries: false` in your ApolloServer ' +
'constructor, or see https://go.apollo.dev/s/cache-backends for other alternatives.');
}
}
if (requestOptions.persistedQueries !== false) {
const { cache: apqCache = requestOptions.cache, ...apqOtherOptions } = requestOptions.persistedQueries || Object.create(null);
requestOptions.persistedQueries = {
cache: new utils_keyvaluecache_1.PrefixingKeyValueCache(apqCache, requestPipeline_1.APQ_CACHE_PREFIX),
...apqOtherOptions,
};
}
else {
delete requestOptions.persistedQueries;
}
this.requestOptions = requestOptions;
this.ensurePluginInstantiation(plugins, isDev);
if (gateway) {
this.state = {
phase: 'initialized',
schemaManager: new schemaManager_1.SchemaManager({
gateway,
apolloConfig: this.apolloConfig,
schemaDerivedDataProvider: (schema) => this.generateSchemaDerivedData(schema),
logger: this.logger,
}),
};
}
else {
this.state = {
phase: 'initialized',
schemaManager: new schemaManager_1.SchemaManager({
apiSchema: this.maybeAddMocksToConstructedSchema(this.constructSchema()),
schemaDerivedDataProvider: (schema) => this.generateSchemaDerivedData(schema),
logger: this.logger,
}),
};
}
if (this.serverlessFramework()) {
this._start().catch((e) => this.logStartupError(e));
}
}
async start() {
if (this.serverlessFramework()) {
throw new Error('When using an ApolloServer subclass from a serverless framework ' +
"package, you don't need to call start(); just call createHandler().");
}
return await this._start();
}
async _start() {
var _a;
if (this.state.phase !== 'initialized') {
throw new Error(`called start() with surprising state ${this.state.phase}`);
}
const schemaManager = this.state.schemaManager;
const barrier = (0, resolvable_1.default)();
this.state = {
phase: 'starting',
barrier,
schemaManager,
};
try {
const executor = await schemaManager.start();
this.toDispose.add(async () => {
await schemaManager.stop();
});
if (executor) {
this.requestOptions.executor = executor;
}
const schemaDerivedData = schemaManager.getSchemaDerivedData();
const service = {
logger: this.logger,
schema: schemaDerivedData.schema,
schemaHash: schemaDerivedData.schemaHash,
apollo: this.apolloConfig,
serverlessFramework: this.serverlessFramework(),
};
if ((_a = this.requestOptions.persistedQueries) === null || _a === void 0 ? void 0 : _a.cache) {
service.persistedQueries = {
cache: this.requestOptions.persistedQueries.cache,
};
}
const taggedServerListeners = (await Promise.all(this.plugins.map(async (plugin) => ({
serverListener: plugin.serverWillStart && (await plugin.serverWillStart(service)),
installedImplicitly: isImplicitlyInstallablePlugin(plugin) &&
plugin.__internal_installed_implicitly__,
})))).filter((maybeTaggedServerListener) => typeof maybeTaggedServerListener.serverListener === 'object');
taggedServerListeners.forEach(({ serverListener: { schemaDidLoadOrUpdate } }) => {
if (schemaDidLoadOrUpdate) {
try {
schemaManager.onSchemaLoadOrUpdate(schemaDidLoadOrUpdate);
}
catch (e) {
if (e instanceof schemaManager_1.GatewayIsTooOldError) {
throw new Error([
`One of your plugins uses the 'schemaDidLoadOrUpdate' hook,`,
`but your gateway version is too old to support this hook.`,
`Please update your version of /gateway to at least 0.35.0.`,
].join(' '));
}
throw e;
}
}
});
const serverWillStops = taggedServerListeners.flatMap((l) => l.serverListener.serverWillStop
? [l.serverListener.serverWillStop]
: []);
if (serverWillStops.length) {
this.toDispose.add(async () => {
await Promise.all(serverWillStops.map((serverWillStop) => serverWillStop()));
});
}
const drainServerCallbacks = taggedServerListeners.flatMap((l) => l.serverListener.drainServer ? [l.serverListener.drainServer] : []);
if (drainServerCallbacks.length) {
this.drainServers = async () => {
await Promise.all(drainServerCallbacks.map((drainServer) => drainServer()));
};
}
let taggedServerListenersWithRenderLandingPage = taggedServerListeners.filter((l) => l.serverListener.renderLandingPage);
if (taggedServerListenersWithRenderLandingPage.length > 1) {
taggedServerListenersWithRenderLandingPage =
taggedServerListenersWithRenderLandingPage.filter((l) => !l.installedImplicitly);
}
if (taggedServerListenersWithRenderLandingPage.length > 1) {
throw Error('Only one plugin can implement renderLandingPage.');
}
else if (taggedServerListenersWithRenderLandingPage.length) {
this.landingPage = await taggedServerListenersWithRenderLandingPage[0]
.serverListener.renderLandingPage();
}
else {
this.landingPage = null;
}
this.state = {
phase: 'started',
schemaManager,
};
this.maybeRegisterTerminationSignalHandlers(['SIGINT', 'SIGTERM']);
}
catch (error) {
this.state = { phase: 'failed to start', error: error };
throw error;
}
finally {
barrier.resolve();
}
}
maybeRegisterTerminationSignalHandlers(signals) {
if (!this.stopOnTerminationSignals) {
return;
}
let receivedSignal = false;
const signalHandler = async (signal) => {
if (receivedSignal) {
return;
}
receivedSignal = true;
try {
await this.stop();
}
catch (e) {
this.logger.error(`stop() threw during ${signal} shutdown`);
this.logger.error(e);
process.exit(1);
}
process.kill(process.pid, signal);
};
signals.forEach((signal) => {
process.on(signal, signalHandler);
this.toDisposeLast.add(async () => {
process.removeListener(signal, signalHandler);
});
});
}
async _ensureStarted() {
while (true) {
switch (this.state.phase) {
case 'initialized':
throw new Error('You need to call `server.start()` before using your Apollo Server.');
case 'starting':
await this.state.barrier;
break;
case 'failed to start':
this.logStartupError(this.state.error);
throw new Error('This data graph is missing a valid configuration. More details may be available in the server logs.');
case 'started':
case 'draining':
return this.state.schemaManager.getSchemaDerivedData();
case 'stopping':
throw new Error('Cannot execute GraphQL operations while the server is stopping.');
case 'stopped':
throw new Error('Cannot execute GraphQL operations after the server has stopped.');
default:
throw new UnreachableCaseError(this.state);
}
}
}
async ensureStarted() {
await this._ensureStarted();
}
assertStarted(methodName) {
if (this.state.phase !== 'started' && this.state.phase !== 'draining') {
throw new Error('You must `await server.start()` before calling `server.' +
methodName +
'()`');
}
}
logStartupError(err) {
this.logger.error('An error occurred during Apollo Server startup. All GraphQL requests ' +
'will now fail. The startup error was: ' +
((err === null || err === void 0 ? void 0 : err.message) || err));
}
constructSchema() {
const { schema, modules, typeDefs, resolvers, parseOptions } = this.config;
if (schema) {
return schema;
}
if (modules) {
const { schema, errors } = (0, apollo_tools_1.buildServiceDefinition)(modules);
if (errors && errors.length > 0) {
throw new Error(errors.map((error) => error.message).join('\n\n'));
}
return schema;
}
if (!typeDefs) {
throw Error('Apollo Server requires either an existing schema, modules or typeDefs');
}
const augmentedTypeDefs = Array.isArray(typeDefs) ? typeDefs : [typeDefs];
return (0, schema_1.makeExecutableSchema)({
typeDefs: augmentedTypeDefs,
resolvers,
parseOptions,
});
}
maybeAddMocksToConstructedSchema(schema) {
const { mocks, mockEntireSchema } = this.config;
if (mocks === false) {
return schema;
}
if (!mocks && typeof mockEntireSchema === 'undefined') {
return schema;
}
return (0, mock_1.addMocksToSchema)({
schema,
mocks: mocks === true || typeof mocks === 'undefined' ? {} : mocks,
preserveResolvers: typeof mockEntireSchema === 'undefined' ? false : !mockEntireSchema,
});
}
generateSchemaDerivedData(schema) {
const schemaHash = (0, schemaHash_1.generateSchemaHash)(schema);
return {
schema,
schemaHash,
documentStore: this.config.documentStore === undefined
? new utils_keyvaluecache_1.InMemoryLRUCache()
: this.config.documentStore === null
? null
: new utils_keyvaluecache_1.PrefixingKeyValueCache(this.config.documentStore, `${uuid.v4()}:`),
};
}
async stop() {
var _a;
switch (this.state.phase) {
case 'initialized':
case 'starting':
case 'failed to start':
throw Error('apolloServer.stop() should only be called after `await apolloServer.start()` has succeeded');
case 'stopped':
if (this.state.stopError) {
throw this.state.stopError;
}
return;
case 'stopping':
case 'draining': {
await this.state.barrier;
const state = this.state;
if (state.phase !== 'stopped') {
throw Error(`Surprising post-stopping state ${state.phase}`);
}
if (state.stopError) {
throw state.stopError;
}
return;
}
case 'started':
break;
default:
throw new UnreachableCaseError(this.state);
}
const barrier = (0, resolvable_1.default)();
this.state = {
phase: 'draining',
schemaManager: this.state.schemaManager,
barrier,
};
try {
await ((_a = this.drainServers) === null || _a === void 0 ? void 0 : _a.call(this));
this.state = { phase: 'stopping', barrier };
await Promise.all([...this.toDispose].map((dispose) => dispose()));
await Promise.all([...this.toDisposeLast].map((dispose) => dispose()));
}
catch (stopError) {
this.state = { phase: 'stopped', stopError: stopError };
barrier.resolve();
throw stopError;
}
this.state = { phase: 'stopped', stopError: null };
}
serverlessFramework() {
return false;
}
ensurePluginInstantiation(userPlugins = [], isDev) {
this.plugins = userPlugins.map((plugin) => {
if (typeof plugin === 'function') {
return plugin();
}
return plugin;
});
const alreadyHavePluginWithInternalId = (id) => this.plugins.some((p) => (0, internalPlugin_1.pluginIsInternal)(p) && p.__internal_plugin_id__() === id);
{
if (!alreadyHavePluginWithInternalId('CacheControl')) {
this.plugins.push((0, plugin_1.ApolloServerPluginCacheControl)());
}
}
{
const alreadyHavePlugin = alreadyHavePluginWithInternalId('UsageReporting');
if (!alreadyHavePlugin && this.apolloConfig.key) {
if (this.apolloConfig.graphRef) {
this.plugins.unshift((0, plugin_1.ApolloServerPluginUsageReporting)());
}
else {
this.logger.warn('You have specified an Apollo key but have not specified a graph ref; usage ' +
'reporting is disabled. To enable usage reporting, set the `APOLLO_GRAPH_REF` ' +
'environment variable to `your-graph-id@your-graph-variant`. To disable this ' +
'warning, install `ApolloServerPluginUsageReportingDisabled`.');
}
}
}
{
const alreadyHavePlugin = alreadyHavePluginWithInternalId('SchemaReporting');
const enabledViaEnvVar = process.env.APOLLO_SCHEMA_REPORTING === 'true';
if (!alreadyHavePlugin && enabledViaEnvVar) {
if (this.apolloConfig.key) {
const options = {};
this.plugins.push((0, plugin_1.ApolloServerPluginSchemaReporting)(options));
}
else {
throw new Error("You've enabled schema reporting by setting the APOLLO_SCHEMA_REPORTING " +
'environment variable to true, but you also need to provide your ' +
'Apollo API key, via the APOLLO_KEY environment ' +
'variable or via `new ApolloServer({apollo: {key})');
}
}
}
{
const alreadyHavePlugin = alreadyHavePluginWithInternalId('InlineTrace');
if (!alreadyHavePlugin) {
this.plugins.push((0, plugin_1.ApolloServerPluginInlineTrace)({ __onlyIfSchemaIsFederated: true }));
}
}
const alreadyHavePlugin = alreadyHavePluginWithInternalId('LandingPageDisabled');
if (!alreadyHavePlugin) {
const plugin = isDev
? (0, plugin_1.ApolloServerPluginLandingPageLocalDefault)()
: (0, plugin_1.ApolloServerPluginLandingPageProductionDefault)();
if (!isImplicitlyInstallablePlugin(plugin)) {
throw Error('default landing page plugin should be implicitly installable?');
}
plugin.__internal_installed_implicitly__ = true;
this.plugins.push(plugin);
}
}
async graphQLServerOptions(integrationContextArgument) {
const { schema, schemaHash, documentStore } = await this._ensureStarted();
let context = this.context ? this.context : {};
try {
context =
typeof this.context === 'function'
? await this.context(integrationContextArgument || {})
: context;
}
catch (error) {
context = () => {
throw error;
};
}
return {
schema,
schemaHash,
logger: this.logger,
plugins: this.plugins,
documentStore,
dangerouslyDisableValidation: this.config.dangerouslyDisableValidation,
context,
parseOptions: this.parseOptions,
...this.requestOptions,
};
}
async executeOperation(request, integrationContextArgument) {
if (this.state.phase === 'initialized') {
await this._start();
}
const options = await this.graphQLServerOptions(integrationContextArgument);
if (typeof options.context === 'function') {
options.context = options.context();
}
else if (typeof options.context === 'object') {
options.context = (0, runHttpQuery_1.cloneObject)(options.context);
}
const requestCtx = {
logger: this.logger,
schema: options.schema,
schemaHash: options.schemaHash,
request: {
...request,
query: request.query && typeof request.query !== 'string'
? (0, graphql_1.print)(request.query)
: request.query,
},
context: options.context || Object.create(null),
cache: options.cache,
metrics: {},
response: {
http: {
headers: new apollo_server_env_1.Headers(),
},
},
debug: options.debug,
overallCachePolicy: (0, cachePolicy_1.newCachePolicy)(),
requestIsBatched: false,
};
return (0, requestPipeline_1.processGraphQLRequest)(options, requestCtx);
}
getLandingPage() {
this.assertStarted('getLandingPage');
return this.landingPage;
}
}
exports.ApolloServerBase = ApolloServerBase;
function isImplicitlyInstallablePlugin(p) {
return '__internal_installed_implicitly__' in p;
}
exports.isImplicitlyInstallablePlugin = isImplicitlyInstallablePlugin;
//# sourceMappingURL=ApolloServer.js.map