From 9cddaa788279e36cbeee2af131ec4e64c0d6e05c Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Fri, 12 Jan 2024 05:35:07 -0500 Subject: [PATCH] feat: Adds Setup CLI Command to Configure GraphQL Trusted Documents (#9800) Co-authored-by: Tobbe Lundberg --- docs/docs/cli-commands.md | 34 ++- docs/docs/graphql/trusted-documents.md | 5 + .../__codemod_tests__/grapqlTransform.test.ts | 13 + .../alreadySetUp/input/graphql.js | 32 +++ .../alreadySetUp/output/graphql.js | 32 +++ .../__testfixtures__/graphql/input/graphql.js | 25 ++ .../graphql/output/graphql.js | 32 +++ .../__tests__/__fixtures__/toml/default.toml | 21 ++ .../__fixtures__/toml/fragments.toml | 23 ++ .../toml/fragments_no_space_equals.toml | 23 ++ .../toml/trusted_docs_already_setup.toml | 24 ++ .../toml/trusted_docs_commented_graphql.toml | 23 ++ .../trusted_docs_fragments_already_setup.toml | 24 ++ .../toml/trusted_docs_no_space_equals.toml | 23 ++ .../trustedDocuments.test.ts.snap | 113 +++++++++ .../__tests__/trustedDocuments.test.ts | 234 ++++++++++++++++++ .../trustedDocuments/graphqlTransform.ts | 64 +++++ .../trustedDocuments/trustedDocuments.ts | 25 ++ .../trustedDocumentsHandler.ts | 143 +++++++++++ .../cli/src/commands/setup/graphql/graphql.ts | 2 + 20 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__codemod_tests__/grapqlTransform.test.ts create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/alreadySetUp/input/graphql.js create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/alreadySetUp/output/graphql.js create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/graphql/input/graphql.js create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/graphql/output/graphql.js create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/default.toml create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/fragments.toml create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/fragments_no_space_equals.toml create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_already_setup.toml create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_commented_graphql.toml create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_fragments_already_setup.toml create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_no_space_equals.toml create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__snapshots__/trustedDocuments.test.ts.snap create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/trustedDocuments.test.ts create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/graphqlTransform.ts create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocuments.ts create mode 100644 packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts diff --git a/docs/docs/cli-commands.md b/docs/docs/cli-commands.md index 2337b3da8b1a..61ccb5038f75 100644 --- a/docs/docs/cli-commands.md +++ b/docs/docs/cli-commands.md @@ -2001,7 +2001,7 @@ It's the author of the npm package's responsibility to specify the correct compa ### setup graphql -This command creates the necessary files to support GraphQL features like fragments. +This command creates the necessary files to support GraphQL features like fragments and trusted documents. #### Usage @@ -2033,6 +2033,38 @@ Run `yarn rw setup graphql fragments` ✔ Add possibleTypes to the GraphQL cache config ``` +#### setup graphql trusted-documents + +This command creates the necessary configuration to start using [GraphQL Trusted Documents](./graphql/trusted-documents.md). + + +``` +yarn redwood setup graphql trusted-documents +``` + +#### Usage + +Run `yarn rw setup graphql trusted-documents` + +#### Example + +```bash +~/redwood-app$ yarn rw setup graphql trusted-documents +✔ Update Redwood Project Configuration to enable GraphQL Trusted Documents ... +✔ Generating Trusted Documents store ... +✔ Configuring the GraphQL Handler to use a Trusted Documents store ... +``` + + +If you have not setup the RedwoodJS server file, it will be setup: + +```bash +✔ Adding the experimental server file... +✔ Adding config to redwood.toml... +✔ Adding required api packages... +``` + + ### setup realtime This command creates the necessary files, installs the required packages, and provides examples to setup RedwoodJS Realtime from GraphQL live queries and subscriptions. See the Realtime docs for more information. diff --git a/docs/docs/graphql/trusted-documents.md b/docs/docs/graphql/trusted-documents.md index 9b67a7f0e02e..a8f1aaf98881 100644 --- a/docs/docs/graphql/trusted-documents.md +++ b/docs/docs/graphql/trusted-documents.md @@ -93,6 +93,11 @@ Thus preventing unwanted queries or GraphQl traversal attacks, ## Configure Trusted Documents +Below are instructions to manually configure Trusted Documents in your RedwoodJS project. + +Alternatively, you can use the `yarn redwood setup graphql trusted-documents` [CLI setup command](../cli-commands.md#setup-graphql-trusted-docs). + + ### Configure redwood.toml Setting `trustedDocuments` to true will diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__codemod_tests__/grapqlTransform.test.ts b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__codemod_tests__/grapqlTransform.test.ts new file mode 100644 index 000000000000..a44dd63ff138 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__codemod_tests__/grapqlTransform.test.ts @@ -0,0 +1,13 @@ +describe('trusted-documents graphql handler transform', () => { + test('Default handler', async () => { + await matchFolderTransform('graphqlTransform', 'graphql', { + useJsCodeshift: true, + }) + }) + + test('Handler with the store already set up', async () => { + await matchFolderTransform('graphqlTransform', 'alreadySetUp', { + useJsCodeshift: true, + }) + }) +}) diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/alreadySetUp/input/graphql.js b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/alreadySetUp/input/graphql.js new file mode 100644 index 000000000000..ceb92dbf9d5b --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/alreadySetUp/input/graphql.js @@ -0,0 +1,32 @@ +import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api' +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { cookieName, getCurrentUser } from 'src/lib/auth' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +import { store } from 'src/lib/trustedDocumentsStore' + +const authDecoder = createAuthDecoder(cookieName) + +export const handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, + + trustedDocuments: { + store + }, +}) diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/alreadySetUp/output/graphql.js b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/alreadySetUp/output/graphql.js new file mode 100644 index 000000000000..ceb92dbf9d5b --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/alreadySetUp/output/graphql.js @@ -0,0 +1,32 @@ +import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api' +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { cookieName, getCurrentUser } from 'src/lib/auth' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +import { store } from 'src/lib/trustedDocumentsStore' + +const authDecoder = createAuthDecoder(cookieName) + +export const handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, + + trustedDocuments: { + store + }, +}) diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/graphql/input/graphql.js b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/graphql/input/graphql.js new file mode 100644 index 000000000000..e9c53e285fad --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/graphql/input/graphql.js @@ -0,0 +1,25 @@ +import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api' +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { cookieName, getCurrentUser } from 'src/lib/auth' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +const authDecoder = createAuthDecoder(cookieName) + +export const handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +}) diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/graphql/output/graphql.js b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/graphql/output/graphql.js new file mode 100644 index 000000000000..ceb92dbf9d5b --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__testfixtures__/graphql/output/graphql.js @@ -0,0 +1,32 @@ +import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api' +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { cookieName, getCurrentUser } from 'src/lib/auth' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +import { store } from 'src/lib/trustedDocumentsStore' + +const authDecoder = createAuthDecoder(cookieName) + +export const handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, + + trustedDocuments: { + store + }, +}) diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/default.toml b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/default.toml new file mode 100644 index 000000000000..5ec850926377 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/default.toml @@ -0,0 +1,21 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "${API_DEV_PORT:8911}" +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/fragments.toml b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/fragments.toml new file mode 100644 index 000000000000..5fb1d209abb0 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/fragments.toml @@ -0,0 +1,23 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "${API_DEV_PORT:8911}" +[graphql] + fragments = true +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/fragments_no_space_equals.toml b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/fragments_no_space_equals.toml new file mode 100644 index 000000000000..149af9b99d53 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/fragments_no_space_equals.toml @@ -0,0 +1,23 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "${API_DEV_PORT:8911}" +[graphql] + fragments=true +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_already_setup.toml b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_already_setup.toml new file mode 100644 index 000000000000..6d0fa706cd91 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_already_setup.toml @@ -0,0 +1,24 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "${API_DEV_PORT:8911}" +[graphql] + trustedDocuments = true + fragments = true +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_commented_graphql.toml b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_commented_graphql.toml new file mode 100644 index 000000000000..ad3aa27c57aa --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_commented_graphql.toml @@ -0,0 +1,23 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "${API_DEV_PORT:8911}" +# [graphql] +# trustedDocuments = true +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_fragments_already_setup.toml b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_fragments_already_setup.toml new file mode 100644 index 000000000000..6d0fa706cd91 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_fragments_already_setup.toml @@ -0,0 +1,24 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "${API_DEV_PORT:8911}" +[graphql] + trustedDocuments = true + fragments = true +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_no_space_equals.toml b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_no_space_equals.toml new file mode 100644 index 000000000000..b07dfb46c621 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__fixtures__/toml/trusted_docs_no_space_equals.toml @@ -0,0 +1,23 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "${API_DEV_PORT:8911}" +[graphql] + trustedDocuments=true +[browser] + open = true +[notifications] + versionUpdates = ["latest"] diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__snapshots__/trustedDocuments.test.ts.snap b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__snapshots__/trustedDocuments.test.ts.snap new file mode 100644 index 000000000000..05ac34b420f2 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/__snapshots__/trustedDocuments.test.ts.snap @@ -0,0 +1,113 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Trusted documents setup Project toml configuration updates default toml where graphql fragments are already setup updates the toml file with graphql and trusted documents enabled and keeps fragments 1`] = ` +"# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run \`yarn rw dev\`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "\${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "\${API_DEV_PORT:8911}" +[graphql] + fragments = true + trustedDocuments = true +[browser] + open = true +[notifications] + versionUpdates = ["latest"] +" +`; + +exports[`Trusted documents setup Project toml configuration updates default toml where graphql fragments are already setup using no spaces updates the toml file with graphql and trusted documents enabled and keeps fragments 1`] = ` +"# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run \`yarn rw dev\`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "\${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "\${API_DEV_PORT:8911}" +[graphql] + fragments=true + trustedDocuments = true +[browser] + open = true +[notifications] + versionUpdates = ["latest"] +" +`; + +exports[`Trusted documents setup Project toml configuration updates default toml where no graphql or trusted documents is setup updates the toml file with graphql and trusted documents enabled 1`] = ` +"# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run \`yarn rw dev\`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "\${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "\${API_DEV_PORT:8911}" +[browser] + open = true +[notifications] + versionUpdates = ["latest"] + +[graphql] + trustedDocuments = true" +`; + +exports[`Trusted documents setup Project toml configuration updates toml where graphql section is commented out adds a new section with \`trustedDocuments = true\` 1`] = ` +"# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run \`yarn rw dev\`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Redwood App" + port = "\${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "\${API_DEV_PORT:8911}" +# [graphql] +# trustedDocuments = true +[browser] + open = true +[notifications] + versionUpdates = ["latest"] + +[graphql] + trustedDocuments = true" +`; diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/trustedDocuments.test.ts b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/trustedDocuments.test.ts new file mode 100644 index 000000000000..7e026ca59ab4 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/trustedDocuments.test.ts @@ -0,0 +1,234 @@ +let mockExecutedTaskTitles: Array = [] +let mockSkippedTaskTitles: Array = [] + +jest.mock('fs', () => require('memfs').fs) +jest.mock('node:fs', () => require('memfs').fs) +jest.mock('execa') +// The jscodeshift parts are tested by another test +jest.mock('../../fragments/runTransform', () => { + return { + runTransform: () => { + return {} + }, + } +}) + +jest.mock('listr2', () => { + return { + // Return a constructor function, since we're calling `new` on Listr + Listr: jest.fn().mockImplementation((tasks: Array) => { + return { + run: async () => { + mockExecutedTaskTitles = [] + mockSkippedTaskTitles = [] + + for (const task of tasks) { + const skip = + typeof task.skip === 'function' ? task.skip : () => task.skip + + if (skip()) { + mockSkippedTaskTitles.push(task.title) + } else { + mockExecutedTaskTitles.push(task.title) + await task.task() + } + } + }, + } + }), + } +}) + +import path from 'node:path' + +import { vol } from 'memfs' + +import { handler } from '../trustedDocumentsHandler' + +// Set up RWJS_CWD +let original_RWJS_CWD: string | undefined +const APP_PATH = '/redwood-app' + +const tomlFixtures: Record = {} + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + process.env.RWJS_CWD = APP_PATH + + const actualFs = jest.requireActual('fs') + const tomlFixturesPath = path.join(__dirname, '__fixtures__', 'toml') + + tomlFixtures.default = actualFs.readFileSync( + path.join(tomlFixturesPath, 'default.toml'), + 'utf-8' + ) + + tomlFixtures.fragments = actualFs.readFileSync( + path.join(tomlFixturesPath, 'fragments.toml'), + 'utf-8' + ) + + tomlFixtures.fragmentsNoSpaceEquals = actualFs.readFileSync( + path.join(tomlFixturesPath, 'fragments_no_space_equals.toml'), + 'utf-8' + ) + + tomlFixtures.trustedDocsAlreadySetup = actualFs.readFileSync( + path.join(tomlFixturesPath, 'trusted_docs_already_setup.toml'), + 'utf-8' + ) + + tomlFixtures.trustedDocsNoSpaceEquals = actualFs.readFileSync( + path.join(tomlFixturesPath, 'trusted_docs_no_space_equals.toml'), + 'utf-8' + ) + + tomlFixtures.trustedDocsFragmentsAlreadySetup = actualFs.readFileSync( + path.join(tomlFixturesPath, 'trusted_docs_fragments_already_setup.toml'), + 'utf-8' + ) + + tomlFixtures.trustedDocsCommentedGraphql = actualFs.readFileSync( + path.join(tomlFixturesPath, 'trusted_docs_commented_graphql.toml'), + 'utf-8' + ) +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD + jest.resetAllMocks() + jest.resetModules() +}) + +// Silence console.info +console.info = jest.fn() + +describe('Trusted documents setup', () => { + it('runs all tasks', async () => { + vol.fromJSON({ 'redwood.toml': '', 'api/src/functions/graphql.js': '' }, APP_PATH) + + await handler({ force: false }) + + expect(mockExecutedTaskTitles).toMatchInlineSnapshot(` + [ + "Update Redwood Project Configuration to enable GraphQL Trusted Documents ...", + "Generating Trusted Documents store ...", + "Configuring the GraphQL Handler to use a Trusted Documents store ...", + ] + `) + }) + + describe('Project toml configuration updates', () => { + describe('default toml where no graphql or trusted documents is setup', () => { + it('updates the toml file with graphql and trusted documents enabled', async () => { + vol.fromJSON( + { + 'redwood.toml': tomlFixtures.default, + 'api/src/functions/graphql.js': '', + }, + APP_PATH + ) + + await handler({ force: false }) + + expect(vol.toJSON()[APP_PATH + '/redwood.toml']).toMatchSnapshot() + }) + }) + describe('default toml where graphql fragments are already setup', () => { + it('updates the toml file with graphql and trusted documents enabled and keeps fragments', async () => { + vol.fromJSON( + { + 'redwood.toml': tomlFixtures.fragments, + 'api/src/functions/graphql.ts': '', + }, + APP_PATH + ) + + await handler({ force: false }) + + expect(vol.toJSON()[APP_PATH + '/redwood.toml']).toMatchSnapshot() + }) + }) + describe('default toml where graphql fragments are already setup using no spaces', () => { + it('updates the toml file with graphql and trusted documents enabled and keeps fragments', async () => { + vol.fromJSON( + { + 'redwood.toml': tomlFixtures.fragmentsNoSpaceEquals, + 'api/src/functions/graphql.js': '', + }, + APP_PATH + ) + + await handler({ force: false }) + + expect(vol.toJSON()[APP_PATH + '/redwood.toml']).toMatchSnapshot() + }) + }) + describe('default toml where graphql trusted documents are already setup', () => { + it('makes no changes as trusted documents are already setup', async () => { + vol.fromJSON( + { + 'redwood.toml': tomlFixtures.trustedDocsAlreadySetup, + 'api/src/functions/graphql.js': '', + }, + APP_PATH + ) + + await handler({ force: false }) + + expect(vol.toJSON()[APP_PATH + '/redwood.toml']).toEqual( + tomlFixtures.trustedDocsAlreadySetup + ) + }) + }) + describe('default toml where graphql trusted documents are already setup using no spaces', () => { + it('makes no changes as trusted documents are already setup', async () => { + vol.fromJSON( + { + 'redwood.toml': tomlFixtures.trustedDocsNoSpaceEquals, + 'api/src/functions/graphql.js': '', + }, + APP_PATH + ) + + await handler({ force: false }) + + expect(vol.toJSON()[APP_PATH + '/redwood.toml']).toEqual( + tomlFixtures.trustedDocsNoSpaceEquals + ) + }) + }) + describe('default toml where graphql trusted documents and fragments are already setup', () => { + it('makes no changes as trusted documents are already setup', async () => { + vol.fromJSON( + { + 'redwood.toml': tomlFixtures.trustedDocsFragmentsAlreadySetup, + 'api/src/functions/graphql.js': '', + }, + APP_PATH + ) + + await handler({ force: false }) + + expect(vol.toJSON()[APP_PATH + '/redwood.toml']).toEqual( + tomlFixtures.trustedDocsFragmentsAlreadySetup + ) + }) + }) + describe('toml where graphql section is commented out', () => { + it('adds a new section with `trustedDocuments = true`', async () => { + vol.fromJSON( + { + 'redwood.toml': tomlFixtures.trustedDocsCommentedGraphql, + 'api/src/functions/graphql.js': '', + }, + APP_PATH + ) + + await handler({ force: false }) + + expect(vol.toJSON()[APP_PATH + '/redwood.toml']).toMatchSnapshot() + }) + }) + }) +}) diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/graphqlTransform.ts b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/graphqlTransform.ts new file mode 100644 index 000000000000..7a3e636d75fb --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/graphqlTransform.ts @@ -0,0 +1,64 @@ +import type { FileInfo, API } from 'jscodeshift' + +export default function transform(file: FileInfo, api: API) { + const j = api.jscodeshift + const root = j(file.source) + + const allImports = root.find(j.ImportDeclaration) + + const hasStoreImport = allImports.some((i) => { + return i.get('source').value.value === 'src/lib/trustedDocumentsStore' + }) + + if (!hasStoreImport) { + allImports + .at(-1) + .insertAfter( + j.importDeclaration( + [j.importSpecifier(j.identifier('store'))], + j.literal('src/lib/trustedDocumentsStore') + ) + ) + } + + const createGraphQLHandlerCalls = root.find(j.CallExpression, { + callee: { + name: 'createGraphQLHandler', + }, + }) + + if (createGraphQLHandlerCalls.length === 0) { + throw new Error( + "Error updating your graphql handler function. You'll have to do it manually. " + + "(Couldn't find a call to `createGraphQLHandler`)" + ) + } + + const existingTrustedDocumentsProperty = createGraphQLHandlerCalls.find( + j.ObjectProperty, + { + key: { + name: 'trustedDocuments', + }, + } + ) + + if (existingTrustedDocumentsProperty.length === 0) { + const storeProperty = j.objectProperty( + j.identifier('store'), + j.identifier('store') + ) + storeProperty.shorthand = true + + createGraphQLHandlerCalls + .get(0) + .node.arguments[0].properties.push( + j.objectProperty( + j.identifier('trustedDocuments'), + j.objectExpression([storeProperty]) + ) + ) + } + + return root.toSource() +} diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocuments.ts b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocuments.ts new file mode 100644 index 000000000000..d561ce51e886 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocuments.ts @@ -0,0 +1,25 @@ +import type { Argv } from 'yargs' + +import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' + +export const command = 'trusted-documents' +export const description = 'Set up Trusted Documents for GraphQL' + +export function builder(yargs: Argv) { + return yargs.option('force', { + alias: 'f', + default: false, + description: 'Overwrite existing configuration', + type: 'boolean', + }) +} + +export async function handler({ force }: { force: boolean }) { + recordTelemetryAttributes({ + command: 'setup graphql trusted-documents', + force, + }) + + const { handler } = await import('./trustedDocumentsHandler.js') + return handler({ force }) +} diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts new file mode 100644 index 000000000000..fe93c5d6fc22 --- /dev/null +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts @@ -0,0 +1,143 @@ +import fs from 'node:fs' +import path from 'node:path' + +import toml from '@iarna/toml' +import execa from 'execa' +import { Listr } from 'listr2' +import { format } from 'prettier' + +import { prettierOptions } from '@redwoodjs/cli-helpers' +import { getConfigPath, getPaths, resolveFile } from '@redwoodjs/project-config' + +import { runTransform } from '../fragments/runTransform' + +function updateRedwoodToml(redwoodTomlPath: string) { + const originalTomlContent = fs.readFileSync(redwoodTomlPath, 'utf-8') + const redwoodTomlContent = fs.readFileSync(redwoodTomlPath, 'utf-8') + // Can't type toml.parse because this PR has not been included in a released yet + // https://github.com/iarna/iarna-toml/commit/5a89e6e65281e4544e23d3dbaf9e8428ed8140e9 + const redwoodTomlObject = toml.parse(redwoodTomlContent) as any + const hasExistingGraphqlSection = !!redwoodTomlObject?.graphql + + if (redwoodTomlObject?.graphql?.trustedDocuments) { + console.info( + 'GraphQL Trusted Documents are already enabled in your Redwood project.' + ) + + return { newConfig: undefined, trustedDocumentsExists: true } + } + + let newTomlContent = + originalTomlContent.replace(/\n$/, '') + + '\n\n[graphql]\n trustedDocuments = true' + + if (hasExistingGraphqlSection) { + const existingGraphqlSetting = Object.keys(redwoodTomlObject.graphql) + + let inGraphqlSection = false + let indentation = '' + let lastGraphqlSettingIndex = 0 + + const tomlLines = originalTomlContent.split('\n') + tomlLines.forEach((line, index) => { + if (line.startsWith('[graphql]')) { + inGraphqlSection = true + lastGraphqlSettingIndex = index + } else { + if (/^\s*\[/.test(line)) { + inGraphqlSection = false + } + } + + if (inGraphqlSection) { + const matches = line.match( + new RegExp(`^(\\s*)(${existingGraphqlSetting})\\s*=`, 'i') + ) + + if (matches) { + indentation = matches[1] + } + + if (/^\s*\w+\s*=/.test(line)) { + lastGraphqlSettingIndex = index + } + } + }) + + tomlLines.splice( + lastGraphqlSettingIndex + 1, + 0, + `${indentation}trustedDocuments = true` + ) + + newTomlContent = tomlLines.join('\n') + } + + return { newConfig: newTomlContent, trustedDocumentsExists: false } +} + +export async function handler({ force }: { force: boolean }) { + const tasks = new Listr( + [ + { + title: + 'Update Redwood Project Configuration to enable GraphQL Trusted Documents ...', + task: () => { + const redwoodTomlPath = getConfigPath() + + const { newConfig, trustedDocumentsExists } = + updateRedwoodToml(redwoodTomlPath) + + if (newConfig && (force || !trustedDocumentsExists)) { + fs.writeFileSync(redwoodTomlPath, newConfig, 'utf-8') + } + }, + }, + { + title: 'Generating Trusted Documents store ...', + task: () => { + execa.commandSync('yarn redwood generate types', { stdio: 'ignore' }) + }, + }, + { + title: + 'Configuring the GraphQL Handler to use a Trusted Documents store ...', + task: async () => { + const graphqlPath = resolveFile( + path.join(getPaths().api.functions, 'graphql') + ) + + if (!graphqlPath) { + throw new Error('Could not find a GraphQL handler in your project.') + } + + const transformResult = await runTransform({ + transformPath: path.join(__dirname, 'graphqlTransform.js'), + targetPaths: [graphqlPath], + }) + + if (transformResult.error) { + throw new Error(transformResult.error) + } + + const source = fs.readFileSync(graphqlPath, 'utf-8') + + const prettifiedApp = format(source, { + ...prettierOptions(), + parser: 'babel-ts', + }) + + fs.writeFileSync(graphqlPath, prettifiedApp, 'utf-8') + }, + }, + ], + { rendererOptions: { collapseSubtasks: false } } + ) + + try { + await tasks.run() + } catch (e: any) { + console.error(e.message) + process.exit(e?.exitCode || 1) + } +} diff --git a/packages/cli/src/commands/setup/graphql/graphql.ts b/packages/cli/src/commands/setup/graphql/graphql.ts index aca51785336d..2a4c810bd703 100644 --- a/packages/cli/src/commands/setup/graphql/graphql.ts +++ b/packages/cli/src/commands/setup/graphql/graphql.ts @@ -2,12 +2,14 @@ import terminalLink from 'terminal-link' import type { Argv } from 'yargs' import * as fragmentsCommand from './features/fragments/fragments' +import * as trustedDocumentsCommand from './features/trustedDocuments/trustedDocuments' export const command = 'graphql ' export const description = 'Set up GraphQL feature support' export function builder(yargs: Argv) { return yargs .command(fragmentsCommand) + .command(trustedDocumentsCommand) .epilogue( `Also see the ${terminalLink( 'Redwood CLI Reference',