diff --git a/api/scripts/deploy-dev.sh b/api/scripts/deploy-dev.sh index 416d90ed63..08fcb8a492 100755 --- a/api/scripts/deploy-dev.sh +++ b/api/scripts/deploy-dev.sh @@ -45,7 +45,13 @@ eval "$rsync_command" exit_code=$? # Run unraid-api restart on remote host -ssh root@"${server_name}" "unraid-api restart" +dev=${DEV:-true} + +if [ "$dev" = true ]; then + ssh root@"${server_name}" "INTROSPECTION=true unraid-api restart" +else + ssh root@"${server_name}" "unraid-api restart" +fi # Play built-in sound based on the operating system if [[ "$OSTYPE" == "darwin"* ]]; then diff --git a/api/src/unraid-api/graph/graph.module.ts b/api/src/unraid-api/graph/graph.module.ts index 6f87b27835..6c63d28d59 100644 --- a/api/src/unraid-api/graph/graph.module.ts +++ b/api/src/unraid-api/graph/graph.module.ts @@ -1,3 +1,9 @@ +import type { ApolloDriverConfig } from '@nestjs/apollo'; +import { ApolloDriver } from '@nestjs/apollo'; +import { Module } from '@nestjs/common'; +import { GraphQLModule } from '@nestjs/graphql'; + +import { NoUnusedVariablesRule, print } from 'graphql'; import { DateTimeResolver, JSONResolver, @@ -5,21 +11,19 @@ import { URLResolver, UUIDResolver, } from 'graphql-scalars'; -import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long'; -import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'; -import { ApolloDriver, type ApolloDriverConfig } from '@nestjs/apollo'; -import { Module } from '@nestjs/common'; -import { GraphQLModule } from '@nestjs/graphql'; -import { ResolversModule } from './resolvers/resolvers.module'; + import { GRAPHQL_INTROSPECTION } from '@app/environment'; +import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long'; import { typeDefs } from '@app/graphql/schema/index'; -import { NoUnusedVariablesRule, print } from 'graphql'; +import { idPrefixPlugin } from '@app/unraid-api/graph/id-prefix-plugin'; + +import { ConnectResolver } from './connect/connect.resolver'; +import { ConnectService } from './connect/connect.service'; import { NetworkResolver } from './network/network.resolver'; +import { ResolversModule } from './resolvers/resolvers.module'; +import { sandboxPlugin } from './sandbox-plugin'; import { ServicesResolver } from './services/services.resolver'; import { SharesResolver } from './shares/shares.resolver'; -import { ConnectResolver } from './connect/connect.resolver'; -import { ConnectService } from './connect/connect.service'; -import { idPrefixPlugin } from '@app/unraid-api/graph/id-prefix-plugin'; @Module({ imports: [ @@ -33,9 +37,7 @@ import { idPrefixPlugin } from '@app/unraid-api/graph/id-prefix-plugin'; extra, }), playground: false, - plugins: GRAPHQL_INTROSPECTION - ? [ApolloServerPluginLandingPageLocalDefault(), idPrefixPlugin] - : [idPrefixPlugin], + plugins: GRAPHQL_INTROSPECTION ? [sandboxPlugin, idPrefixPlugin] : [idPrefixPlugin], subscriptions: { 'graphql-ws': { path: '/graphql', @@ -55,12 +57,6 @@ import { idPrefixPlugin } from '@app/unraid-api/graph/id-prefix-plugin'; // schema: schema }), ], - providers: [ - NetworkResolver, - ServicesResolver, - SharesResolver, - ConnectResolver, - ConnectService, - ], + providers: [NetworkResolver, ServicesResolver, SharesResolver, ConnectResolver, ConnectService], }) export class GraphModule {} diff --git a/api/src/unraid-api/graph/sandbox-plugin.ts b/api/src/unraid-api/graph/sandbox-plugin.ts new file mode 100644 index 0000000000..7b2817554d --- /dev/null +++ b/api/src/unraid-api/graph/sandbox-plugin.ts @@ -0,0 +1,82 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +import type { ApolloServerPlugin, GraphQLServerContext, GraphQLServerListener } from '@apollo/server'; + +/** The initial query displayed in the Apollo sandbox */ +const initialDocument = `query ExampleQuery { + notifications { + id + overview { + unread { + info + warning + alert + total + } + archive { + info + warning + alert + total + } + } + } + }`; + +/** helper for raising precondition failure errors during an http request. */ +const preconditionFailed = (preconditionName: string) => { + throw new HttpException(`Precondition failed: ${preconditionName} `, HttpStatus.PRECONDITION_FAILED); +}; + +/** + * Renders the sandbox page for the GraphQL server with Apollo Server landing page configuration. + * + * @param service - The GraphQL server context object + * @returns Promise that resolves to an Apollo `LandingPage`, or throws a precondition failed error + * @throws {Error} When downstream plugin components from apollo are unavailable. This should never happen. + * + * @remarks + * This function configures and renders the Apollo Server landing page with: + * - Disabled footer + * - Enabled cookies + * - Initial document state + * - Shared headers containing CSRF token + */ +async function renderSandboxPage(service: GraphQLServerContext) { + const { getters } = await import('@app/store'); + const { ApolloServerPluginLandingPageLocalDefault } = await import( + '@apollo/server/plugin/landingPage/default' + ); + const plugin = ApolloServerPluginLandingPageLocalDefault({ + footer: false, + includeCookies: true, + document: initialDocument, + embed: { + initialState: { + sharedHeaders: { + 'x-csrf-token': getters.emhttp().var.csrfToken, + }, + }, + }, + }); + if (!plugin.serverWillStart) return preconditionFailed('serverWillStart'); + const serverListener = await plugin.serverWillStart(service); + + if (!serverListener) return preconditionFailed('serverListener'); + if (!serverListener.renderLandingPage) return preconditionFailed('renderLandingPage'); + return serverListener.renderLandingPage(); +} + +/** + * Apollo plugin to render the GraphQL Sandbox page on-demand based on current server state. + * + * Usually, the `ApolloServerPluginLandingPageLocalDefault` plugin configures its + * parameters once, during server startup. This plugin defers the configuration + * and rendering to request-time instead of server startup. + */ +export const sandboxPlugin: ApolloServerPlugin = { + serverWillStart: async (service) => + ({ + renderLandingPage: () => renderSandboxPage(service), + }) satisfies GraphQLServerListener, +};