mirror of
https://github.com/strapi/strapi.git
synced 2025-10-16 18:44:18 +00:00
235 lines
6.9 KiB
TypeScript
235 lines
6.9 KiB
TypeScript
import { isEmpty, mergeWith, isArray, isObject, isFunction } from 'lodash/fp';
|
|
import { ApolloServer, type ApolloServerPlugin, type ApolloServerOptions } from '@apollo/server';
|
|
import {
|
|
ApolloServerPluginLandingPageLocalDefault,
|
|
ApolloServerPluginLandingPageProductionDefault,
|
|
} from '@apollo/server/plugin/landingPage/default';
|
|
import { koaMiddleware } from '@as-integrations/koa';
|
|
import depthLimit from 'graphql-depth-limit';
|
|
import bodyParser from 'koa-bodyparser';
|
|
import cors from '@koa/cors';
|
|
|
|
import type { Core } from '@strapi/types';
|
|
import type { BaseContext, DefaultContextExtends, DefaultStateExtends } from 'koa';
|
|
|
|
import { formatGraphqlError } from './format-graphql-error';
|
|
|
|
const merge = mergeWith((a, b) => {
|
|
if (isArray(a) && isArray(b)) {
|
|
return a.concat(b);
|
|
}
|
|
});
|
|
|
|
export const determineLandingPage = (strapi: Core.Strapi) => {
|
|
const { config } = strapi.plugin('graphql');
|
|
const utils = strapi.plugin('graphql').service('utils');
|
|
|
|
/**
|
|
* configLanding page may be one of the following:
|
|
*
|
|
* - true: always use "playground" even in production
|
|
* - false: never show "playground" even in non-production
|
|
* - undefined: default Apollo behavior (hide playground on production)
|
|
* - a function that returns an Apollo plugin that implements renderLandingPage
|
|
** */
|
|
const configLandingPage = config('landingPage');
|
|
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
|
|
const localLanding = () => {
|
|
strapi.log.debug('Apollo landing page: local');
|
|
utils.playground.setEnabled(true);
|
|
return ApolloServerPluginLandingPageLocalDefault();
|
|
};
|
|
|
|
const prodLanding = () => {
|
|
strapi.log.debug('Apollo landing page: production');
|
|
utils.playground.setEnabled(false);
|
|
return ApolloServerPluginLandingPageProductionDefault();
|
|
};
|
|
|
|
const userLanding = (userFunction: (strapi?: Core.Strapi) => ApolloServerPlugin | boolean) => {
|
|
strapi.log.debug('Apollo landing page: from user-defined function...');
|
|
const result = userFunction(strapi);
|
|
if (result === true) {
|
|
return localLanding();
|
|
}
|
|
if (result === false) {
|
|
return prodLanding();
|
|
}
|
|
strapi.log.debug('Apollo landing page: user-defined');
|
|
return result;
|
|
};
|
|
|
|
// DEPRECATED, remove in Strapi v6
|
|
const playgroundAlways = config('playgroundAlways');
|
|
if (playgroundAlways !== undefined) {
|
|
strapi.log.warn(
|
|
'The graphql config playgroundAlways is deprecated. This will be removed in Strapi 6. Please use landingPage instead. '
|
|
);
|
|
}
|
|
if (playgroundAlways === false) {
|
|
strapi.log.warn(
|
|
'graphql config playgroundAlways:false has no effect, please use landingPage:false to disable Graphql Playground in all environments'
|
|
);
|
|
}
|
|
|
|
if (playgroundAlways || configLandingPage === true) {
|
|
return localLanding();
|
|
}
|
|
|
|
// if landing page has been disabled, use production
|
|
if (configLandingPage === false) {
|
|
return prodLanding();
|
|
}
|
|
|
|
// If user did not define any settings, use our defaults
|
|
if (configLandingPage === undefined) {
|
|
return isProduction ? prodLanding() : localLanding();
|
|
}
|
|
|
|
// if user provided a landing page function, return that
|
|
if (isFunction(configLandingPage)) {
|
|
return userLanding(configLandingPage);
|
|
}
|
|
|
|
// If no other setting could be found, default to production settings
|
|
strapi.log.warn(
|
|
'Your Graphql landing page has been disabled because there is a problem with your Graphql settings'
|
|
);
|
|
return prodLanding();
|
|
};
|
|
|
|
export async function bootstrap({ strapi }: { strapi: Core.Strapi }) {
|
|
// Generate the GraphQL schema for the content API
|
|
const schema = strapi.plugin('graphql').service('content-api').buildSchema();
|
|
|
|
if (isEmpty(schema)) {
|
|
strapi.log.warn('The GraphQL schema has not been generated because it is empty');
|
|
|
|
return;
|
|
}
|
|
|
|
const { config } = strapi.plugin('graphql');
|
|
|
|
const path: string = config('endpoint');
|
|
|
|
const landingPage = determineLandingPage(strapi);
|
|
|
|
type CustomOptions = {
|
|
cors: boolean;
|
|
uploads: boolean;
|
|
bodyParserConfig: boolean;
|
|
};
|
|
|
|
const defaultServerConfig: ApolloServerOptions<BaseContext> & CustomOptions = {
|
|
// Schema
|
|
schema,
|
|
|
|
// Validation
|
|
validationRules: [depthLimit(config('depthLimit') as number) as any],
|
|
|
|
// Errors
|
|
formatError: formatGraphqlError,
|
|
|
|
// Misc
|
|
cors: false,
|
|
uploads: false,
|
|
bodyParserConfig: true,
|
|
// send 400 http status instead of 200 for input validation errors
|
|
status400ForVariableCoercionErrors: true,
|
|
plugins: [landingPage],
|
|
|
|
cache: 'bounded' as const,
|
|
};
|
|
|
|
const serverConfig = merge(
|
|
defaultServerConfig,
|
|
config('apolloServer')
|
|
) as ApolloServerOptions<BaseContext> & CustomOptions;
|
|
|
|
// Create a new Apollo server
|
|
const server = new ApolloServer(serverConfig);
|
|
|
|
try {
|
|
// server.start() must be called before using server.applyMiddleware()
|
|
await server.start();
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
strapi.log.error('Failed to start the Apollo server', error.message);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
// Create the route handlers for Strapi
|
|
const handler: Core.MiddlewareHandler[] = [];
|
|
|
|
// add cors middleware
|
|
if (cors) {
|
|
handler.push(cors());
|
|
}
|
|
|
|
// add koa bodyparser middleware
|
|
if (isObject(serverConfig.bodyParserConfig)) {
|
|
handler.push(bodyParser(serverConfig.bodyParserConfig));
|
|
} else if (serverConfig.bodyParserConfig) {
|
|
handler.push(bodyParser());
|
|
} else {
|
|
strapi.log.debug('Body parser has been disabled for Apollo server');
|
|
}
|
|
|
|
// add the Strapi auth middleware
|
|
handler.push((ctx, next) => {
|
|
ctx.state.route = {
|
|
info: {
|
|
// Indicate it's a content API route
|
|
type: 'content-api',
|
|
},
|
|
};
|
|
|
|
const isPlaygroundRequest =
|
|
ctx.request.method === 'GET' &&
|
|
ctx.request.url === path && // Matches the GraphQL endpoint
|
|
strapi.plugin('graphql').service('utils').playground.isEnabled() && // Only allow if the Playground is enabled
|
|
ctx.request.header.accept?.includes('text/html'); // Specific to Playground UI loading
|
|
|
|
// Skip authentication for the GraphQL Playground UI
|
|
if (isPlaygroundRequest) {
|
|
return next();
|
|
}
|
|
|
|
return strapi.auth.authenticate(ctx, next);
|
|
});
|
|
|
|
// add the graphql server for koa
|
|
handler.push(
|
|
koaMiddleware<DefaultStateExtends, DefaultContextExtends>(server, {
|
|
// Initialize loaders for this request.
|
|
context: async ({ ctx }) => ({
|
|
state: ctx.state,
|
|
koaContext: ctx,
|
|
}),
|
|
})
|
|
);
|
|
|
|
// now that handlers are set up, add the graphql route to our apollo server
|
|
strapi.server.routes([
|
|
{
|
|
method: 'ALL',
|
|
path,
|
|
handler,
|
|
config: {
|
|
auth: false,
|
|
},
|
|
},
|
|
]);
|
|
|
|
// Register destroy behavior
|
|
// We're doing it here instead of exposing a destroy method to the strapi-server.js
|
|
// file since we need to have access to the ApolloServer instance
|
|
strapi.plugin('graphql').destroy = async () => {
|
|
await server.stop();
|
|
};
|
|
}
|