diff --git a/__fixtures__/fragment-test-project/.redwood/schema.graphql b/__fixtures__/fragment-test-project/.redwood/schema.graphql
new file mode 100644
index 000000000000..1cd2ea849879
--- /dev/null
+++ b/__fixtures__/fragment-test-project/.redwood/schema.graphql
@@ -0,0 +1,102 @@
+"""
+Use to check whether or not a user is authenticated and is associated
+with an optional set of roles.
+"""
+directive @requireAuth(roles: [String]) on FIELD_DEFINITION
+
+"""Use to skip authentication checks and allow public access."""
+directive @skipAuth on FIELD_DEFINITION
+
+scalar BigInt
+
+scalar Date
+
+scalar DateTime
+
+type Fruit implements Grocery {
+ id: ID!
+
+ """Seedless is only for fruits"""
+ isSeedless: Boolean
+ name: String!
+ nutrients: String
+ price: Int!
+ quantity: Int!
+ region: String!
+
+ """Ripeness is only for fruits"""
+ ripenessIndicators: String
+ stall: Stall!
+}
+
+union Groceries = Fruit | Vegetable
+
+interface Grocery {
+ id: ID!
+ name: String!
+ nutrients: String
+ price: Int!
+ quantity: Int!
+ region: String!
+ stall: Stall!
+}
+
+scalar JSON
+
+scalar JSONObject
+
+"""About the Redwood queries."""
+type Query {
+ fruitById(id: ID!): Fruit
+ fruits: [Fruit!]!
+ groceries: [Groceries!]!
+
+ """Fetches the Redwood root schema."""
+ redwood: Redwood
+ stallById(id: ID!): Stall
+ stalls: [Stall!]!
+ vegetableById(id: ID!): Vegetable
+ vegetables: [Vegetable!]!
+}
+
+"""
+The RedwoodJS Root Schema
+
+Defines details about RedwoodJS such as the current user and version information.
+"""
+type Redwood {
+ """The current user."""
+ currentUser: JSON
+
+ """The version of Prisma."""
+ prismaVersion: String
+
+ """The version of Redwood."""
+ version: String
+}
+
+type Stall {
+ fruits: [Fruit]
+ id: ID!
+ name: String!
+ stallNumber: String!
+ vegetables: [Vegetable]
+}
+
+scalar Time
+
+type Vegetable implements Grocery {
+ id: ID!
+
+ """Pickled is only for vegetables"""
+ isPickled: Boolean
+ name: String!
+ nutrients: String
+ price: Int!
+ quantity: Int!
+ region: String!
+ stall: Stall!
+
+ """Veggie Family is only for vegetables"""
+ vegetableFamily: String
+}
\ No newline at end of file
diff --git a/__fixtures__/fragment-test-project/README.md b/__fixtures__/fragment-test-project/README.md
new file mode 100644
index 000000000000..861af7d9e65e
--- /dev/null
+++ b/__fixtures__/fragment-test-project/README.md
@@ -0,0 +1,4 @@
+# Project to Test GraphQL Fragments
+
+* Has unions and interfaces to test: https://www.apollographql.com/docs/react/data/fragments/#using-fragments-with-unions-and-interfaces
+* Generates "possible types" for Apollo Client, see: https://www.apollographql.com/docs/react/data/fragments/#defining-possibletypes-manually
diff --git a/__fixtures__/fragment-test-project/api/db/schema.prisma b/__fixtures__/fragment-test-project/api/db/schema.prisma
new file mode 100644
index 000000000000..20b2df54078c
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/db/schema.prisma
@@ -0,0 +1,36 @@
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+ directUrl = env("DIRECT_URL")
+}
+
+generator client {
+ provider = "prisma-client-js"
+ binaryTargets = "native"
+}
+
+model Stall {
+ id String @id @default(cuid())
+ name String
+ stallNumber String @unique
+ produce Produce[]
+}
+
+model Produce {
+ id String @id @default(cuid())
+ name String @unique
+ quantity Int
+ price Int
+ nutrients String?
+ region String
+ /// Available only for fruits
+ isSeedless Boolean?
+ /// Available only for fruits
+ ripenessIndicators String?
+ /// Available only for vegetables
+ vegetableFamily String?
+ /// Available only for vegetables
+ isPickled Boolean?
+ stall Stall @relation(fields: [stallId], references: [id], onDelete: Cascade)
+ stallId String
+}
diff --git a/__fixtures__/fragment-test-project/api/jest.config.js b/__fixtures__/fragment-test-project/api/jest.config.js
new file mode 100644
index 000000000000..932fc82dce93
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/jest.config.js
@@ -0,0 +1,8 @@
+// More info at https://redwoodjs.com/docs/project-configuration-dev-test-build
+
+const config = {
+ rootDir: '../',
+ preset: '@redwoodjs/testing/config/jest/api',
+}
+
+module.exports = config
diff --git a/__fixtures__/fragment-test-project/api/package.json b/__fixtures__/fragment-test-project/api/package.json
new file mode 100644
index 000000000000..4a05d2980285
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "api",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@redwoodjs/api": "6.2.0",
+ "@redwoodjs/graphql-server": "6.2.0"
+ }
+}
diff --git a/__fixtures__/fragment-test-project/api/server.config.js b/__fixtures__/fragment-test-project/api/server.config.js
new file mode 100644
index 000000000000..73dca9225a3e
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/server.config.js
@@ -0,0 +1,52 @@
+/**
+ * This file allows you to configure the Fastify Server settings
+ * used by the RedwoodJS dev server.
+ *
+ * It also applies when running RedwoodJS with `yarn rw serve`.
+ *
+ * For the Fastify server options that you can set, see:
+ * https://www.fastify.io/docs/latest/Reference/Server/#factory
+ *
+ * Examples include: logger settings, timeouts, maximum payload limits, and more.
+ *
+ * Note: This configuration does not apply in a serverless deploy.
+ */
+
+/** @type {import('fastify').FastifyServerOptions} */
+const config = {
+ requestTimeout: 15_000,
+ logger: {
+ // Note: If running locally using `yarn rw serve` you may want to adjust
+ // the default non-development level to `info`
+ level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn',
+ },
+}
+
+/**
+ * You can also register Fastify plugins and additional routes for the API and Web sides
+ * in the configureFastify function.
+ *
+ * This function has access to the Fastify instance and options, such as the side
+ * (web, api, or proxy) that is being configured and other settings like the apiRootPath
+ * of the functions endpoint.
+ *
+ * Note: This configuration does not apply in a serverless deploy.
+ */
+
+/** @type {import('@redwoodjs/api-server/dist/types').FastifySideConfigFn} */
+const configureFastify = async (fastify, options) => {
+ if (options.side === 'api') {
+ fastify.log.trace({ custom: { options } }, 'Configuring api side')
+ }
+
+ if (options.side === 'web') {
+ fastify.log.trace({ custom: { options } }, 'Configuring web side')
+ }
+
+ return fastify
+}
+
+module.exports = {
+ config,
+ configureFastify,
+}
diff --git a/__fixtures__/fragment-test-project/api/src/.keep b/__fixtures__/fragment-test-project/api/src/.keep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts b/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts
new file mode 100644
index 000000000000..0f01aa367a85
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts
@@ -0,0 +1,18 @@
+import { mockRedwoodDirective, getDirectiveName } from '@redwoodjs/testing/api'
+
+import requireAuth from './requireAuth'
+
+describe('requireAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(requireAuth.schema).toBeTruthy()
+ expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
+ })
+
+ it('requireAuth has stub implementation. Should not throw when current user', () => {
+ // If you want to set values in context, pass it through e.g.
+ // mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
+ const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
+
+ expect(mockExecution).not.toThrowError()
+ })
+})
diff --git a/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.ts b/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.ts
new file mode 100644
index 000000000000..77b31a70ae07
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/directives/requireAuth/requireAuth.ts
@@ -0,0 +1,25 @@
+import gql from 'graphql-tag'
+
+import type { ValidatorDirectiveFunc } from '@redwoodjs/graphql-server'
+import { createValidatorDirective } from '@redwoodjs/graphql-server'
+
+import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
+
+export const schema = gql`
+ """
+ Use to check whether or not a user is authenticated and is associated
+ with an optional set of roles.
+ """
+ directive @requireAuth(roles: [String]) on FIELD_DEFINITION
+`
+
+type RequireAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
+
+const validate: RequireAuthValidate = ({ directiveArgs }) => {
+ const { roles } = directiveArgs
+ applicationRequireAuth({ roles })
+}
+
+const requireAuth = createValidatorDirective(schema, validate)
+
+export default requireAuth
diff --git a/__fixtures__/fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts b/__fixtures__/fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts
new file mode 100644
index 000000000000..88d99a56eab2
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts
@@ -0,0 +1,10 @@
+import { getDirectiveName } from '@redwoodjs/testing/api'
+
+import skipAuth from './skipAuth'
+
+describe('skipAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(skipAuth.schema).toBeTruthy()
+ expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
+ })
+})
diff --git a/__fixtures__/fragment-test-project/api/src/directives/skipAuth/skipAuth.ts b/__fixtures__/fragment-test-project/api/src/directives/skipAuth/skipAuth.ts
new file mode 100644
index 000000000000..e85b94ae8b89
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/directives/skipAuth/skipAuth.ts
@@ -0,0 +1,16 @@
+import gql from 'graphql-tag'
+
+import { createValidatorDirective } from '@redwoodjs/graphql-server'
+
+export const schema = gql`
+ """
+ Use to skip authentication checks and allow public access.
+ """
+ directive @skipAuth on FIELD_DEFINITION
+`
+
+const skipAuth = createValidatorDirective(schema, () => {
+ return
+})
+
+export default skipAuth
diff --git a/__fixtures__/fragment-test-project/api/src/functions/graphql.ts b/__fixtures__/fragment-test-project/api/src/functions/graphql.ts
new file mode 100644
index 000000000000..f395c3b0f852
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/functions/graphql.ts
@@ -0,0 +1,19 @@
+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 { db } from 'src/lib/db'
+import { logger } from 'src/lib/logger'
+
+export const handler = createGraphQLHandler({
+ loggerConfig: { logger, options: {} },
+ directives,
+ sdls,
+ services,
+ onException: () => {
+ // Disconnect from your database with an unhandled exception.
+ db.$disconnect()
+ },
+})
diff --git a/__fixtures__/fragment-test-project/api/src/graphql/.keep b/__fixtures__/fragment-test-project/api/src/graphql/.keep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/__fixtures__/fragment-test-project/api/src/graphql/groceries.sdl.ts b/__fixtures__/fragment-test-project/api/src/graphql/groceries.sdl.ts
new file mode 100644
index 000000000000..0870d7daeb54
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/graphql/groceries.sdl.ts
@@ -0,0 +1,49 @@
+export const schema = gql`
+ interface Grocery {
+ id: ID!
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ stall: Stall!
+ region: String!
+ }
+
+ type Fruit implements Grocery {
+ id: ID!
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ stall: Stall!
+ region: String!
+ "Seedless is only for fruits"
+ isSeedless: Boolean
+ "Ripeness is only for fruits"
+ ripenessIndicators: String
+ }
+
+ type Vegetable implements Grocery {
+ id: ID!
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ stall: Stall!
+ region: String!
+ "Veggie Family is only for vegetables"
+ vegetableFamily: String
+ "Pickled is only for vegetables"
+ isPickled: Boolean
+ }
+
+ union Groceries = Fruit | Vegetable
+
+ type Query {
+ groceries: [Groceries!]! @skipAuth
+ fruits: [Fruit!]! @skipAuth
+ fruitById(id: ID!): Fruit @skipAuth
+ vegetables: [Vegetable!]! @skipAuth
+ vegetableById(id: ID!): Vegetable @skipAuth
+ }
+`
diff --git a/__fixtures__/fragment-test-project/api/src/graphql/stalls.sdl.ts b/__fixtures__/fragment-test-project/api/src/graphql/stalls.sdl.ts
new file mode 100644
index 000000000000..ff159aa2df9a
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/graphql/stalls.sdl.ts
@@ -0,0 +1,14 @@
+export const schema = gql`
+ type Stall {
+ id: ID!
+ stallNumber: String!
+ name: String!
+ fruits: [Fruit]
+ vegetables: [Vegetable]
+ }
+
+ type Query {
+ stalls: [Stall!]! @skipAuth
+ stallById(id: ID!): Stall @skipAuth
+ }
+`
diff --git a/__fixtures__/fragment-test-project/api/src/lib/auth.ts b/__fixtures__/fragment-test-project/api/src/lib/auth.ts
new file mode 100644
index 000000000000..f98fe93a960c
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/lib/auth.ts
@@ -0,0 +1,25 @@
+/**
+ * Once you are ready to add authentication to your application
+ * you'll build out requireAuth() with real functionality. For
+ * now we just return `true` so that the calls in services
+ * have something to check against, simulating a logged
+ * in user that is allowed to access that service.
+ *
+ * See https://redwoodjs.com/docs/authentication for more info.
+ */
+export const isAuthenticated = () => {
+ return true
+}
+
+export const hasRole = ({ roles }) => {
+ return roles !== undefined
+}
+
+// This is used by the redwood directive
+// in ./api/src/directives/requireAuth
+
+// Roles are passed in by the requireAuth directive if you have auth setup
+// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
+export const requireAuth = ({ roles }) => {
+ return isAuthenticated()
+}
diff --git a/__fixtures__/fragment-test-project/api/src/lib/db.ts b/__fixtures__/fragment-test-project/api/src/lib/db.ts
new file mode 100644
index 000000000000..6743f0c11d9a
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/lib/db.ts
@@ -0,0 +1,21 @@
+// See https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/constructor
+// for options.
+
+import { PrismaClient } from '@prisma/client'
+
+import { emitLogLevels, handlePrismaLogging } from '@redwoodjs/api/logger'
+
+import { logger } from './logger'
+
+/*
+ * Instance of the Prisma Client
+ */
+export const db = new PrismaClient({
+ log: emitLogLevels(['info', 'warn', 'error', 'query']),
+})
+
+handlePrismaLogging({
+ db,
+ logger,
+ logLevels: ['info', 'warn', 'error', 'query'],
+})
diff --git a/__fixtures__/fragment-test-project/api/src/lib/logger.ts b/__fixtures__/fragment-test-project/api/src/lib/logger.ts
new file mode 100644
index 000000000000..150a309767c5
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/lib/logger.ts
@@ -0,0 +1,17 @@
+import { createLogger } from '@redwoodjs/api/logger'
+
+/**
+ * Creates a logger with RedwoodLoggerOptions
+ *
+ * These extend and override default LoggerOptions,
+ * can define a destination like a file or other supported pino log transport stream,
+ * and sets whether or not to show the logger configuration settings (defaults to false)
+ *
+ * @param RedwoodLoggerOptions
+ *
+ * RedwoodLoggerOptions have
+ * @param {options} LoggerOptions - defines how to log, such as redaction and format
+ * @param {string | DestinationStream} destination - defines where to log, such as a transport stream or file
+ * @param {boolean} showConfig - whether to display logger configuration on initialization
+ */
+export const logger = createLogger({})
diff --git a/__fixtures__/fragment-test-project/api/src/services/.keep b/__fixtures__/fragment-test-project/api/src/services/.keep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/__fixtures__/fragment-test-project/api/src/services/fruit.ts b/__fixtures__/fragment-test-project/api/src/services/fruit.ts
new file mode 100644
index 000000000000..b94ff9c3796d
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/fruit.ts
@@ -0,0 +1,29 @@
+import type { QueryResolvers } from 'types/graphql'
+
+import { db } from 'src/lib/db'
+import { logger } from 'src/lib/logger'
+
+const isFruit = { isSeedless: { not: null }, ripenessIndicators: { not: null } }
+
+export const fruits: QueryResolvers['fruits'] = async () => {
+ const result = await db.produce.findMany({
+ where: { ...isFruit },
+ include: { stall: true },
+ orderBy: { name: 'asc' },
+ })
+
+ logger.debug({ result }, 'frroooooots')
+
+ return result
+}
+
+export const fruitById: QueryResolvers['fruitById'] = async ({ id }) => {
+ const result = await db.produce.findUnique({
+ where: { id, ...isFruit },
+ include: { stall: true },
+ })
+
+ logger.debug({ result }, 'frroot')
+
+ return result
+}
diff --git a/__fixtures__/fragment-test-project/api/src/services/groceries.ts b/__fixtures__/fragment-test-project/api/src/services/groceries.ts
new file mode 100644
index 000000000000..8a34942d27a3
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/groceries.ts
@@ -0,0 +1,30 @@
+import { db } from 'src/lib/db'
+
+const isFruit = (grocery) => {
+ return grocery.isSeedless !== null && grocery.ripenessIndicators !== null
+}
+
+export const groceries = async () => {
+ const result = await db.produce.findMany({
+ include: { stall: true },
+ orderBy: { name: 'asc' },
+ })
+
+ const avail = result.map((grocery) => {
+ if (isFruit) {
+ return {
+ ...grocery,
+ __typename: 'Fruit',
+ __resolveType: 'Fruit',
+ }
+ } else {
+ return {
+ ...grocery,
+ __typename: 'Vegetable',
+ __resolveType: 'Vegetable',
+ }
+ }
+ })
+
+ return avail
+}
diff --git a/__fixtures__/fragment-test-project/api/src/services/stalls.ts b/__fixtures__/fragment-test-project/api/src/services/stalls.ts
new file mode 100644
index 000000000000..aa7d417fd10c
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/stalls.ts
@@ -0,0 +1,21 @@
+import type { QueryResolvers } from 'types/graphql'
+
+import { db } from 'src/lib/db'
+
+export const stalls: QueryResolvers['stalls'] = async () => {
+ const result = await db.stall.findMany({
+ include: { produce: true },
+ orderBy: { name: 'asc' },
+ })
+
+ return result
+}
+
+export const stallById: QueryResolvers['stallById'] = async ({ id }) => {
+ const result = await db.stall.findUnique({
+ where: { id },
+ include: { produce: true },
+ })
+
+ return result
+}
diff --git a/__fixtures__/fragment-test-project/api/src/services/vegetables.ts b/__fixtures__/fragment-test-project/api/src/services/vegetables.ts
new file mode 100644
index 000000000000..7caf39d47fea
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/src/services/vegetables.ts
@@ -0,0 +1,22 @@
+import type { QueryResolvers } from 'types/graphql'
+
+import { db } from 'src/lib/db'
+
+const isVegetable = { vegetableFamily: { not: null }, isPickled: { not: null } }
+
+export const vegetables: QueryResolvers['vegetables'] = async () => {
+ return await db.produce.findMany({
+ where: { ...isVegetable },
+ include: { stall: true },
+ orderBy: { name: 'asc' },
+ })
+}
+
+export const vegetableById: QueryResolvers['vegetableById'] = async ({
+ id,
+}) => {
+ return await db.produce.findUnique({
+ where: { id, ...isVegetable },
+ include: { stall: true },
+ })
+}
diff --git a/__fixtures__/fragment-test-project/api/tsconfig.json b/__fixtures__/fragment-test-project/api/tsconfig.json
new file mode 100644
index 000000000000..fcbbf9872e43
--- /dev/null
+++ b/__fixtures__/fragment-test-project/api/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "skipLibCheck": false,
+ "baseUrl": "./",
+ "rootDirs": [
+ "./src",
+ "../.redwood/types/mirror/api/src"
+ ],
+ "paths": {
+ "src/*": [
+ "./src/*",
+ "../.redwood/types/mirror/api/src/*"
+ ],
+ "types/*": ["./types/*", "../types/*"],
+ "@redwoodjs/testing": ["../node_modules/@redwoodjs/testing/api"]
+ },
+ "typeRoots": [
+ "../node_modules/@types",
+ "./node_modules/@types"
+ ],
+ "types": ["jest"],
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "src",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/api-*",
+ "../types"
+ ]
+}
diff --git a/__fixtures__/fragment-test-project/graphql.config.js b/__fixtures__/fragment-test-project/graphql.config.js
new file mode 100644
index 000000000000..e6c0ef53af71
--- /dev/null
+++ b/__fixtures__/fragment-test-project/graphql.config.js
@@ -0,0 +1,6 @@
+const { getPaths } = require('@redwoodjs/internal')
+
+module.exports = {
+ schema: getPaths().generated.schema,
+ documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}',
+}
diff --git a/__fixtures__/fragment-test-project/redwood.toml b/__fixtures__/fragment-test-project/redwood.toml
new file mode 100644
index 000000000000..11270a20be22
--- /dev/null
+++ b/__fixtures__/fragment-test-project/redwood.toml
@@ -0,0 +1,16 @@
+# 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 = 8910
+ apiUrl = "/.redwood/functions" # you can customise graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths
+ includeEnvironmentVariables = [] # any ENV vars that should be available to the web side, see https://redwoodjs.com/docs/environment-variables#web
+[api]
+ port = 8911
+[browser]
+ open = true
diff --git a/__fixtures__/fragment-test-project/scripts/.keep b/__fixtures__/fragment-test-project/scripts/.keep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/__fixtures__/fragment-test-project/web/.editorconfig b/__fixtures__/fragment-test-project/web/.editorconfig
new file mode 100644
index 000000000000..ae10a5cce3b2
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/.editorconfig
@@ -0,0 +1,10 @@
+# editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/__fixtures__/fragment-test-project/web/jest.config.js b/__fixtures__/fragment-test-project/web/jest.config.js
new file mode 100644
index 000000000000..0e54869ebdcb
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/jest.config.js
@@ -0,0 +1,8 @@
+// More info at https://redwoodjs.com/docs/project-configuration-dev-test-build
+
+const config = {
+ rootDir: '../',
+ preset: '@redwoodjs/testing/config/jest/web',
+}
+
+module.exports = config
diff --git a/__fixtures__/fragment-test-project/web/package.json b/__fixtures__/fragment-test-project/web/package.json
new file mode 100644
index 000000000000..1bffdb2f6902
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "web",
+ "version": "0.0.0",
+ "private": true,
+ "browserslist": {
+ "development": [
+ "last 1 version"
+ ],
+ "production": [
+ "defaults"
+ ]
+ },
+ "dependencies": {
+ "@redwoodjs/forms": "6.2.0",
+ "@redwoodjs/router": "6.2.0",
+ "@redwoodjs/web": "6.2.0",
+ "prop-types": "15.8.1",
+ "react": "18.2.0",
+ "react-dom": "18.2.0"
+ },
+ "devDependencies": {
+ "@redwoodjs/vite": "6.2.0"
+ }
+}
diff --git a/__fixtures__/fragment-test-project/web/src/App.tsx b/__fixtures__/fragment-test-project/web/src/App.tsx
new file mode 100644
index 000000000000..f60aa1dfc22a
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/App.tsx
@@ -0,0 +1,24 @@
+import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
+import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
+
+import introspection from 'src/graphql/possibleTypes'
+import FatalErrorPage from 'src/pages/FatalErrorPage'
+import Routes from 'src/Routes'
+
+const App = () => (
+
+
+
+
+
+
+
+)
+
+export default App
diff --git a/__fixtures__/fragment-test-project/web/src/Routes.tsx b/__fixtures__/fragment-test-project/web/src/Routes.tsx
new file mode 100644
index 000000000000..71fc2e0f7098
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/Routes.tsx
@@ -0,0 +1,21 @@
+// In this file, all Page components from 'src/pages` are auto-imported. Nested
+// directories are supported, and should be uppercase. Each subdirectory will be
+// prepended onto the component name.
+//
+// Examples:
+//
+// 'src/pages/HomePage/HomePage.js' -> HomePage
+// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
+
+import { Router, Route } from '@redwoodjs/router'
+
+const Routes = () => {
+ return (
+
+
+
+
+ )
+}
+
+export default Routes
diff --git a/__fixtures__/fragment-test-project/web/src/components/.keep b/__fixtures__/fragment-test-project/web/src/components/.keep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/__fixtures__/fragment-test-project/web/src/components/Card/Card.tsx b/__fixtures__/fragment-test-project/web/src/components/Card/Card.tsx
new file mode 100644
index 000000000000..8894a447b29c
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/Card/Card.tsx
@@ -0,0 +1,9 @@
+const Card = ({ children }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Card
diff --git a/__fixtures__/fragment-test-project/web/src/components/Fruit.tsx b/__fixtures__/fragment-test-project/web/src/components/Fruit.tsx
new file mode 100644
index 000000000000..8cdbb8f4bdd7
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/Fruit.tsx
@@ -0,0 +1,39 @@
+import type { Fruit } from 'types/graphql'
+
+import { registerFragment } from '@redwoodjs/web/apollo'
+
+import Card from 'src/components/Card/Card'
+import Stall from 'src/components/Stall'
+
+const { useRegisteredFragment } = registerFragment(
+ gql`
+ fragment Fruit_info on Fruit {
+ id
+ name
+ isSeedless
+ ripenessIndicators
+ stall {
+ ...Stall_info
+ }
+ }
+ `
+)
+
+const Fruit = ({ id }: { id: string }) => {
+ const { data: fruit, complete } = useRegisteredFragment(id)
+
+ console.log(fruit)
+
+ return (
+ complete && (
+
+
Fruit Name: {fruit.name}
+
Seeds? {fruit.isSeedless ? 'Yes' : 'No'}
+
Ripeness: {fruit.ripenessIndicators}
+
+
+ )
+ )
+}
+
+export default Fruit
diff --git a/__fixtures__/fragment-test-project/web/src/components/Produce.tsx b/__fixtures__/fragment-test-project/web/src/components/Produce.tsx
new file mode 100644
index 000000000000..fe3798c80578
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/Produce.tsx
@@ -0,0 +1,30 @@
+import type { Produce } from 'types/graphql'
+
+import { registerFragment } from '@redwoodjs/web/apollo'
+
+import Card from 'src/components/Card/Card'
+
+const { useRegisteredFragment } = registerFragment(
+ gql`
+ fragment Produce_info on Produce {
+ id
+ name
+ }
+ `
+)
+
+const Produce = ({ id }: { id: string }) => {
+ const { data, complete } = useRegisteredFragment(id)
+
+ console.log('>>>>>>>>>>>Produce', data)
+
+ return (
+ complete && (
+
+
Produce Name: {data.name}
+
+ )
+ )
+}
+
+export default Produce
diff --git a/__fixtures__/fragment-test-project/web/src/components/Stall.tsx b/__fixtures__/fragment-test-project/web/src/components/Stall.tsx
new file mode 100644
index 000000000000..3244714500bd
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/Stall.tsx
@@ -0,0 +1,28 @@
+import type { Stall } from 'types/graphql'
+
+import { registerFragment } from '@redwoodjs/web/apollo'
+
+const { useRegisteredFragment } = registerFragment(
+ gql`
+ fragment Stall_info on Stall {
+ id
+ name
+ }
+ `
+)
+
+const Stall = ({ id }: { id: string }) => {
+ const { data, complete } = useRegisteredFragment(id)
+
+ console.log(data)
+
+ return (
+ complete && (
+
+
Stall Name: {data.name}
+
+ )
+ )
+}
+
+export default Stall
diff --git a/__fixtures__/fragment-test-project/web/src/components/Vegetable.tsx b/__fixtures__/fragment-test-project/web/src/components/Vegetable.tsx
new file mode 100644
index 000000000000..461cb9377f48
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/components/Vegetable.tsx
@@ -0,0 +1,39 @@
+import type { Vegetable } from 'types/graphql'
+
+import { registerFragment } from '@redwoodjs/web/apollo'
+
+import Card from 'src/components/Card/Card'
+import Stall from 'src/components/Stall'
+
+const { useRegisteredFragment } = registerFragment(
+ gql`
+ fragment Vegetable_info on Vegetable {
+ id
+ name
+ vegetableFamily
+ isPickled
+ stall {
+ ...Stall_info
+ }
+ }
+ `
+)
+
+const Vegetable = ({ id }: { id: string }) => {
+ const { data: vegetable, complete } = useRegisteredFragment(id)
+
+ console.log(vegetable)
+
+ return (
+ complete && (
+
+
Vegetable Name: {vegetable.name}
+
Pickled? {vegetable.isPickled ? 'Yes' : 'No'}
+
Family: {vegetable.vegetableFamily}
+
+
+ )
+ )
+}
+
+export default Vegetable
diff --git a/__fixtures__/fragment-test-project/web/src/entry.client.tsx b/__fixtures__/fragment-test-project/web/src/entry.client.tsx
new file mode 100644
index 000000000000..ffee44f85869
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/entry.client.tsx
@@ -0,0 +1,17 @@
+import { hydrateRoot, createRoot } from 'react-dom/client'
+
+import App from './App'
+/**
+ * When `#redwood-app` isn't empty then it's very likely that you're using
+ * prerendering. So React attaches event listeners to the existing markup
+ * rather than replacing it.
+ * https://reactjs.org/docs/react-dom-client.html#hydrateroot
+ */
+const redwoodAppElement = document.getElementById('redwood-app')
+
+if (redwoodAppElement.children?.length > 0) {
+ hydrateRoot(redwoodAppElement, )
+} else {
+ const root = createRoot(redwoodAppElement)
+ root.render()
+}
diff --git a/__fixtures__/fragment-test-project/web/src/graphql/possibleTypes.ts b/__fixtures__/fragment-test-project/web/src/graphql/possibleTypes.ts
new file mode 100644
index 000000000000..79a376225982
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/graphql/possibleTypes.ts
@@ -0,0 +1,20 @@
+
+ export interface PossibleTypesResultData {
+ possibleTypes: {
+ [key: string]: string[]
+ }
+ }
+ const result: PossibleTypesResultData = {
+ "possibleTypes": {
+ "Groceries": [
+ "Fruit",
+ "Vegetable"
+ ],
+ "Grocery": [
+ "Fruit",
+ "Vegetable"
+ ]
+ }
+};
+ export default result;
+
\ No newline at end of file
diff --git a/__fixtures__/fragment-test-project/web/src/index.css b/__fixtures__/fragment-test-project/web/src/index.css
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/__fixtures__/fragment-test-project/web/src/index.html b/__fixtures__/fragment-test-project/web/src/index.html
new file mode 100644
index 000000000000..e240b8eb8c54
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__fixtures__/fragment-test-project/web/src/layouts/.keep b/__fixtures__/fragment-test-project/web/src/layouts/.keep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/__fixtures__/fragment-test-project/web/src/pages/FatalErrorPage/FatalErrorPage.tsx b/__fixtures__/fragment-test-project/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
new file mode 100644
index 000000000000..b2bb436f8ed0
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
@@ -0,0 +1,57 @@
+// This page will be rendered when an error makes it all the way to the top of the
+// application without being handled by a Javascript catch statement or React error
+// boundary.
+//
+// You can modify this page as you wish, but it is important to keep things simple to
+// avoid the possibility that it will cause its own error. If it does, Redwood will
+// still render a generic error page, but your users will prefer something a bit more
+// thoughtful :)
+
+// This import will be automatically removed when building for production
+import { DevFatalErrorPage } from '@redwoodjs/web/dist/components/DevFatalErrorPage'
+
+export default DevFatalErrorPage ||
+ (() => (
+
+
+
+
+
+
+)
diff --git a/__fixtures__/fragment-test-project/web/tsconfig.json b/__fixtures__/fragment-test-project/web/tsconfig.json
new file mode 100644
index 000000000000..8b5649abe5a4
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/tsconfig.json
@@ -0,0 +1,39 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "baseUrl": "./",
+ "skipLibCheck": false,
+ "rootDirs": [
+ "./src",
+ "../.redwood/types/mirror/web/src",
+ "../api/src",
+ "../.redwood/types/mirror/api/src"
+ ],
+ "paths": {
+ "src/*": [
+ "./src/*",
+ "../.redwood/types/mirror/web/src/*",
+ "../api/src/*",
+ "../.redwood/types/mirror/api/src/*"
+ ],
+ "$api/*": [ "../api/*" ],
+ "types/*": ["./types/*", "../types/*"],
+ "@redwoodjs/testing": ["../node_modules/@redwoodjs/testing/web"]
+ },
+ "typeRoots": ["../node_modules/@types", "./node_modules/@types"],
+ "types": ["jest", "@testing-library/jest-dom"],
+ "jsx": "preserve"
+ },
+ "include": [
+ "src",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types",
+ "./types"
+ ]
+}
diff --git a/__fixtures__/fragment-test-project/web/types/graphql.d.ts b/__fixtures__/fragment-test-project/web/types/graphql.d.ts
new file mode 100644
index 000000000000..04701b267972
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/types/graphql.d.ts
@@ -0,0 +1,123 @@
+import { Prisma } from "@prisma/client"
+export type Maybe = T | null;
+export type InputMaybe = Maybe;
+export type Exact = { [K in keyof T]: T[K] };
+export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
+export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
+/** All built-in and custom scalars, mapped to their actual values */
+export type Scalars = {
+ ID: string;
+ String: string;
+ Boolean: boolean;
+ Int: number;
+ Float: number;
+ BigInt: number;
+ Date: string;
+ DateTime: string;
+ JSON: Prisma.JsonValue;
+ JSONObject: Prisma.JsonObject;
+ Time: string;
+};
+
+export type Fruit = Grocery & {
+ __typename?: 'Fruit';
+ id: Scalars['ID'];
+ /** Seedless is only for fruits */
+ isSeedless?: Maybe;
+ name: Scalars['String'];
+ nutrients?: Maybe;
+ price: Scalars['Int'];
+ quantity: Scalars['Int'];
+ region: Scalars['String'];
+ /** Ripeness is only for fruits */
+ ripenessIndicators?: Maybe;
+ stall: Stall;
+};
+
+export type Groceries = Fruit | Vegetable;
+
+export type Grocery = {
+ id: Scalars['ID'];
+ name: Scalars['String'];
+ nutrients?: Maybe;
+ price: Scalars['Int'];
+ quantity: Scalars['Int'];
+ region: Scalars['String'];
+ stall: Stall;
+};
+
+/** About the Redwood queries. */
+export type Query = {
+ __typename?: 'Query';
+ fruitById?: Maybe;
+ fruits: Array;
+ groceries: Array;
+ /** Fetches the Redwood root schema. */
+ redwood?: Maybe;
+ stallById?: Maybe;
+ stalls: Array;
+ vegetableById?: Maybe;
+ vegetables: Array;
+};
+
+
+/** About the Redwood queries. */
+export type QueryfruitByIdArgs = {
+ id: Scalars['ID'];
+};
+
+
+/** About the Redwood queries. */
+export type QuerystallByIdArgs = {
+ id: Scalars['ID'];
+};
+
+
+/** About the Redwood queries. */
+export type QueryvegetableByIdArgs = {
+ id: Scalars['ID'];
+};
+
+/**
+ * The RedwoodJS Root Schema
+ *
+ * Defines details about RedwoodJS such as the current user and version information.
+ */
+export type Redwood = {
+ __typename?: 'Redwood';
+ /** The current user. */
+ currentUser?: Maybe;
+ /** The version of Prisma. */
+ prismaVersion?: Maybe;
+ /** The version of Redwood. */
+ version?: Maybe;
+};
+
+export type Stall = {
+ __typename?: 'Stall';
+ fruits?: Maybe>>;
+ id: Scalars['ID'];
+ name: Scalars['String'];
+ stallNumber: Scalars['String'];
+ vegetables?: Maybe>>;
+};
+
+export type Vegetable = Grocery & {
+ __typename?: 'Vegetable';
+ id: Scalars['ID'];
+ /** Pickled is only for vegetables */
+ isPickled?: Maybe;
+ name: Scalars['String'];
+ nutrients?: Maybe;
+ price: Scalars['Int'];
+ quantity: Scalars['Int'];
+ region: Scalars['String'];
+ stall: Stall;
+ /** Veggie Family is only for vegetables */
+ vegetableFamily?: Maybe;
+};
+
+export type GetGroceriesVariables = Exact<{ [key: string]: never; }>;
+
+
+export type GetGroceries = { __typename?: 'Query', groceries: Array<{ __typename?: 'Fruit', id: string, name: string, isSeedless?: boolean | null, ripenessIndicators?: string | null } | { __typename?: 'Vegetable', id: string, name: string, vegetableFamily?: string | null, isPickled?: boolean | null }> };
diff --git a/__fixtures__/fragment-test-project/web/types/possible-types.ts b/__fixtures__/fragment-test-project/web/types/possible-types.ts
new file mode 100644
index 000000000000..79a376225982
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/types/possible-types.ts
@@ -0,0 +1,20 @@
+
+ export interface PossibleTypesResultData {
+ possibleTypes: {
+ [key: string]: string[]
+ }
+ }
+ const result: PossibleTypesResultData = {
+ "possibleTypes": {
+ "Groceries": [
+ "Fruit",
+ "Vegetable"
+ ],
+ "Grocery": [
+ "Fruit",
+ "Vegetable"
+ ]
+ }
+};
+ export default result;
+
\ No newline at end of file
diff --git a/__fixtures__/fragment-test-project/web/vite.config.ts b/__fixtures__/fragment-test-project/web/vite.config.ts
new file mode 100644
index 000000000000..ddeb06d9a1cf
--- /dev/null
+++ b/__fixtures__/fragment-test-project/web/vite.config.ts
@@ -0,0 +1,15 @@
+import dns from 'dns';
+import type { UserConfig } from 'vite';
+import { defineConfig } from 'vite';
+
+// See: https://vitejs.dev/config/server-options.html#server-host
+// So that Vite will load on local instead of 127.0.0.1
+dns.setDefaultResultOrder('verbatim');
+import redwood from '@redwoodjs/vite';
+const viteConfig: UserConfig = {
+ plugins: [redwood()],
+ optimizeDeps: {
+ force: true
+ }
+};
+export default defineConfig(viteConfig);
\ No newline at end of file
diff --git a/__fixtures__/test-project/graphql.config.js b/__fixtures__/test-project/graphql.config.js
index 2da7862f6b57..e6c0ef53af71 100644
--- a/__fixtures__/test-project/graphql.config.js
+++ b/__fixtures__/test-project/graphql.config.js
@@ -2,4 +2,5 @@ const { getPaths } = require('@redwoodjs/internal')
module.exports = {
schema: getPaths().generated.schema,
+ documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}',
}
diff --git a/__fixtures__/test-project/web/package.json b/__fixtures__/test-project/web/package.json
index 9c4fb2bc8271..48c27b6c84c4 100644
--- a/__fixtures__/test-project/web/package.json
+++ b/__fixtures__/test-project/web/package.json
@@ -25,7 +25,7 @@
"@types/react": "18.2.14",
"@types/react-dom": "18.2.6",
"autoprefixer": "^10.4.15",
- "postcss": "^8.4.29",
+ "postcss": "^8.4.30",
"postcss-loader": "^7.3.3",
"prettier-plugin-tailwindcss": "0.4.1",
"tailwindcss": "^3.3.3"
diff --git a/__fixtures__/test-project/web/src/App.tsx b/__fixtures__/test-project/web/src/App.tsx
index 65419d60c7d6..cb77cb1e4322 100644
--- a/__fixtures__/test-project/web/src/App.tsx
+++ b/__fixtures__/test-project/web/src/App.tsx
@@ -1,6 +1,7 @@
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
+import possibleTypes from 'src/graphql/possibleTypes'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
@@ -13,7 +14,14 @@ const App = () => (
-
+
diff --git a/__fixtures__/test-project/web/src/graphql/possibleTypes.ts b/__fixtures__/test-project/web/src/graphql/possibleTypes.ts
new file mode 100644
index 000000000000..1dc34abfb3ab
--- /dev/null
+++ b/__fixtures__/test-project/web/src/graphql/possibleTypes.ts
@@ -0,0 +1,9 @@
+export interface PossibleTypesResultData {
+ possibleTypes: {
+ [key: string]: string[]
+ }
+}
+const result: PossibleTypesResultData = {
+ possibleTypes: {},
+}
+export default result
diff --git a/docs/docs/graphql.md b/docs/docs/graphql.md
index 7d1b0629d742..de494734e939 100644
--- a/docs/docs/graphql.md
+++ b/docs/docs/graphql.md
@@ -237,6 +237,49 @@ For example, if you have a query named `search` that supports [Apollo's offset p
}}>
```
+### Generate Possible Types
+
+
+In order to use [fragments](#fragments) with [unions](#unions) and interfaces in Apollo Client, you need to tell the client how to discriminate between the different types that implement or belong to a supertype.
+
+You pass a possibleTypes option to the InMemoryCache constructor to specify these relationships in your schema.
+
+This object maps the name of an interface or union type (the supertype) to the types that implement or belong to it (the subtypes).
+
+For example:
+
+```ts
+/// web/src/App.tsx
+
+
+```
+
+To make this easier to maintain, RedwoodJS GraphQL CodeGen automatically generates `possibleTypes` so you can simply assign it to the `graphQLClientConfig`:
+
+
+```ts
+import possibleTypes from 'src/graphql/possibleTypes'
+
+...
+/// web/src/App.tsx
+
+```
+
### Swapping out the RedwoodApolloProvider
As long as you're willing to do a bit of configuring yourself, you can swap out `RedwoodApolloProvider` with your GraphQL Client of choice. You'll just have to get to know a bit of the make up of the [RedwoodApolloProvider](https://github.com/redwoodjs/redwood/blob/main/packages/web/src/apollo/index.tsx#L71-L84); it's actually composed of a few more Providers and hooks:
@@ -806,6 +849,261 @@ type Mutation {
}
```
+See the [Directives](directives) section for complete information on RedwoodJS Directives.
+
+## Fragments
+
+[GraphQL fragments](https://graphql.org/learn/queries/#fragments) are reusable units of GraphQL queries that allow developers to define a set of fields that can be included in multiple queries. Fragments help improve code organization, reduce duplication, and make GraphQL queries more maintainable. They are particularly useful when you want to request the same set of fields on different parts of your data model or when you want to share query structures across multiple components or pages in your application.
+
+### What are Fragments?
+
+Here are some key points about GraphQL fragments:
+
+1. **Reusability**: Fragments allow you to define a set of fields once and reuse them in multiple queries. This reduces redundancy and makes your code more DRY (Don't Repeat Yourself).
+
+2. **Readability**: Fragments make queries more readable by separating the query structure from the actual query usage. This can lead to cleaner and more maintainable code.
+
+3. **Maintainability**: When you need to make changes to the requested fields, you only need to update the fragment definition in one place, and all queries using that fragment will automatically reflect the changes.
+
+### Basic Usage
+
+Here's a basic example of how you might use GraphQL fragments in developer documentation:
+
+Let's say you have a GraphQL schema representing books, and you want to create a fragment for retrieving basic book information like title, author, and publication year.
+
+
+```graphql
+# Define a GraphQL fragment for book information
+fragment BookInfo on Book {
+ id
+ title
+ author
+ publicationYear
+}
+
+# Example query using the BookInfo fragment
+query GetBookDetails($bookId: ID!) {
+ book(id: $bookId) {
+ ...BookInfo
+ description
+ # Include other fields specific to this query
+ }
+}
+```
+
+In this example:
+
+- We've defined a fragment called `BookInfo` that specifies the fields we want for book information.
+- In the `GetBookDetails` query, we use the `...BookInfo` spread syntax to include the fields defined in the fragment.
+- We also include additional fields specific to this query, such as `description`.
+
+By using the `BookInfo` fragment, you can maintain a consistent set of fields for book information across different parts of your application without duplicating the field selection in every query. This improves code maintainability and reduces the chance of errors.
+
+In developer documentation, you can explain the purpose of the fragment, provide examples like the one above, and encourage developers to use fragments to organize and reuse their GraphQL queries effectively.
+
+### Using Fragments in RedwoodJS
+
+RedwoodJS makes it easy to use fragments, especially with VS Code and Apollo GraphQL Client.
+
+First, RedwoodJS instructs the VS Code GraphQL Plugin where to look for fragments by configuring the `documents` attribute of your project's `graphql.config.js`:
+
+```js
+// graphql.config.js
+
+const { getPaths } = require('@redwoodjs/internal')
+
+module.exports = {
+ schema: getPaths().generated.schema,
+ documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}', // 👈 Tells VS Code plugin where to find fragments
+}
+```
+
+Second, RedwoodJS automatically creates the [fragmentRegistry](https://www.apollographql.com/docs/react/data/fragments/#registering-named-fragments-using-createfragmentregistry) needed for Apollo to know about the fragments in your project without needing to interpolate their declarations.
+
+Redwood exports ways to interact with fragments in the `@redwoodjs/web/apollo` package.
+
+```
+import { fragmentRegistry, registerFragment } from '@redwoodjs/web/apollo'
+```
+
+With `fragmentRegistry`, you can interact with the registry directly.
+
+With `registerFragment`, you can register a fragment with the registry and get back:
+
+ ```ts
+ { fragment, typename, getCacheKey, useRegisteredFragment }
+ ```
+
+which can then be used to work with the registered fragment.
+
+### registerFragment
+
+To register a fragment, you can simply register it with `registerFragment`.
+
+```ts
+import { registerFragment } from '@redwoodjs/web/apollo'
+
+registerFragment(
+ gql`
+ fragment BookInfo on Book {
+ id
+ title
+ author
+ publicationYear
+ }
+ `
+)
+```
+
+This makes the `BookInfo` available to use in your query:
+
+
+```ts
+import type { GetBookDetails } from 'types/graphql'
+
+import { useQuery } from '@redwoodjs/web'
+
+import BookInfo from 'src/components/BookInfo'
+
+const GET_BOOK_DETAILS = gql`
+ query GetBookDetails($bookId: ID!) {
+ book(id: $bookId) {
+ ...BookInfo
+ description
+ # Include other fields specific to this query
+ }
+ }
+
+...
+
+const { data, loading} = useQuery(GET_BOOK_DETAILS)
+
+```
+
+
+You can then access the book info from `data` and render:
+
+```ts
+{!loading && (
+
+
Title: {data.title}
+
by {data.author} ({data.publicationYear})<>
+
+)}
+```
+
+### fragment
+
+Access the original fragment you registered.
+
+```ts
+import { fragment } from '@redwoodjs/web/apollo'
+```
+
+### typename
+
+Access typename of fragment you registered.
+
+
+```ts
+import { typename } from '@redwoodjs/web/apollo'
+```
+
+For example, with
+
+```graphql
+# Define a GraphQL fragment for book information
+fragment BookInfo on Book {
+ id
+ title
+ author
+ publicationYear
+}
+
+the `typename` is `Book`.
+
+
+### useCache!!!
+
+### getCacheKey
+
+A helper function to create the cache key for the data associated with the fragment in Apollo cache.
+
+```ts
+import { getCacheKey } from '@redwoodjs/web/apollo'
+```
+
+For example, with
+
+```graphql
+# Define a GraphQL fragment for book information
+fragment BookInfo on Book {
+ id
+ title
+ author
+ publicationYear
+}
+```
+
+the `getCacheKey` is a function where `getCacheKey(42)` would return `Book:42`.
+
+### useRegisteredFragment
+
+```ts
+import { registerFragment } from '@redwoodjs/web/apollo'
+
+const { useRegisteredFragment } = registerFragment(
+...
+)
+```
+
+A helper function relies on Apollo's [`useFragment` hook](https://www.apollographql.com/docs/react/data/fragments/#usefragment) in Apollo cache.
+
+The useFragment hook represents a lightweight live binding into the Apollo Client Cache. It enables Apollo Client to broadcast specific fragment results to individual components. This hook returns an always-up-to-date view of whatever data the cache currently contains for a given fragment. useFragment never triggers network requests of its own.
+
+
+This means that once the Apollo Client Cache has loaded the data needed for the fragment, one can simply render the data for the fragment component with its id reference.
+
+Also, anywhere the fragment component is rendered will be updated with teh latest data if any of `useQuery` with uses the fragment received new data.
+
+```ts
+import type { Book } from 'types/graphql'
+
+import { registerFragment } from '@redwoodjs/web/apollo'
+
+const { useRegisteredFragment } = registerFragment(
+ gql`
+ fragment BookInfo on Book {
+ id
+ title
+ author
+ publicationYear
+ }
+ `
+)
+
+const Book = ({ id }: { id: string }) => {
+ const { data, complete } = useRegisteredFragment(id)
+
+ return (
+ complete && (
+
+
Title: {data.title}
+
by {data.author} ({data.publicationYear})<>
+
+ )
+ )
+}
+
+export default Book
+```
+
+:::note
+In order to use [fragments](#fragments) with [unions](#unions) and interfaces in Apollo Client, you need to tell the client how to discriminate between the different types that implement or belong to a supertype.
+
+Please see how to [generate possible types from fragments and union types](#generate-possible-types).
+:::
+
## Unions
Unions are abstract GraphQL types that enable a schema field to return one of multiple object types.
@@ -846,6 +1144,139 @@ query GetFavoriteTrees {
Redwood will automatically detect your union types in your `sdl` files and resolve *which* of your union's types is being returned. If the returned object does not match any of the valid types, the associated operation will produce a GraphQL error.
+:::note
+
+In order to use Union types web-side with your Apollo GraphQL client, you will need to [generate possible types from fragments and union types](#generate-possible-types).
+
+:::
+
+### useCache
+
+Apollo Client stores the results of your GraphQL queries in a local, normalized, in-memory cache. This enables the client to respond almost immediately to queries for already-cached data, without even sending a network request.
+
+useCache is a custom hook that returns the cache object and some useful methods to interact with the cache:
+
+* [evict](#evict)
+* [extract](#extract)
+* [identify](#identify)
+* [modify](#modify)
+* [resetStore](#resetStore)
+* [clearStore](#clearStore)
+
+```ts
+import { useCache } from '@redwoodjs/web/apollo'
+```
+
+#### cache
+
+Returns the normalized, in-memory cache.
+
+```ts
+import { useCache } from '@redwoodjs/web/apollo'
+
+const { cache } = useCache()
+```
+
+#### evict
+
+Either removes a normalized object from the cache or removes a specific field from a normalized object in the cache.
+
+```ts
+import { useCache } from '@redwoodjs/web/apollo'
+
+
+const Fruit = ({ id }: { id: FragmentIdentifier }) => {
+ const { evict } = useCache()
+ const { data: fruit, complete } = useRegisteredFragment(id)
+
+ evict(fruit)
+}
+```
+
+#### extract
+
+Returns a serialized representation of the cache's current contents
+
+```ts
+import { useCache } from '@redwoodjs/web/apollo'
+
+const Fruit = ({ id }: { id: FragmentIdentifier }) => {
+ const { extract } = useCache()
+
+ // Logs the cache's current contents
+ console.log(extract())
+
+```
+
+#### identify
+
+```ts
+import { useCache } from '@redwoodjs/web/apollo'
+
+const Fruit = ({ id }: { id: FragmentIdentifier }) => {
+ const { identify } = useCache()
+ const { data: fruit, complete } = useRegisteredFragment(id)
+
+ // Returns "Fruit:ownpc6co8a1w5bhfmavecko9"
+ console.log(identify(fruit))
+}
+```
+
+#### modify
+
+Modifies one or more field values of a cached object. Must provide a modifier function for each field to modify. A modifier function takes a cached field's current value and returns the value that should replace it.
+
+Returns true if the cache was modified successfully and false otherwise.
+
+```ts
+import { useCache } from '@redwoodjs/web/apollo'
+
+const Fruit = ({ id }: { id: FragmentIdentifier }) => {
+ const { modify } = useCache()
+ const { data: fruit, complete } = useRegisteredFragment(id)
+
+ // Modify the name of a given fruit entity to be uppercase
+
+
+
+ // ...
+}
+```
+
+#### clearStore
+
+To reset the cache without refetching active queries, use the clearStore method.
+
+
+```ts
+import { useCache } from '@redwoodjs/web/apollo'
+
+const Fruit = ({ id }: { id: FragmentIdentifier }) => {
+ const { clearStore } = useCache()
+
+ clearStore()
+}
+```
+
+#### resetStore
+
+Reset the cache entirely, such as when a user logs out.
+
+```ts
+import { useCache } from '@redwoodjs/web/apollo'
+
+const Fruit = ({ id }: { id: FragmentIdentifier }) => {
+ const { resetStore } = useCache()
+
+ resetStore()
+}
+```
+
## GraphQL Handler Setup
Redwood's `GraphQLHandlerOptions` allows you to configure your GraphQL handler schema, context, authentication, security and more.
@@ -999,7 +1430,7 @@ export const handler = createGraphQLHandler({
})
```
-> Note: Check-out the [in-depth look at Redwood Directives](directives.md) that explains how to generate directives so you may use them to validate access and transform the response.
+> Note: Check-out the [in-depth look at Redwood Directives](directives) that explains how to generate directives so you may use them to validate access and transform the response.
### Logging Setup
@@ -1014,9 +1445,9 @@ Logging is essential in production apps to be alerted about critical errors and
We want to make logging simple when using RedwoodJS and therefore have configured the api-side GraphQL handler to log common information about your queries and mutations. Log statements also be optionally enriched with [operation names](https://graphql.org/learn/queries/#operation-name), user agents, request ids, and performance timings to give you more visibility into your GraphQL api.
-By configuring the GraphQL handler to use your api side [RedwoodJS logger](logger.md), any errors and other log statements about the [GraphQL execution](https://graphql.org/learn/execution/) will be logged to the [destination](logger.md#destination-aka-where-to-log) you've set up: to standard output, file, or transport stream.
+By configuring the GraphQL handler to use your api side [RedwoodJS logger](logger), any errors and other log statements about the [GraphQL execution](https://graphql.org/learn/execution/) will be logged to the [destination](logger#destination-aka-where-to-log) you've set up: to standard output, file, or transport stream.
-You configure the logger using the `loggerConfig` that accepts a [`logger`](logger.md) and a set of [GraphQL Logger Options](#graphql-logger-options).
+You configure the logger using the `loggerConfig` that accepts a [`logger`](logger) and a set of [GraphQL Logger Options](#graphql-logger-options).
### Configure the GraphQL Logger
@@ -1147,9 +1578,9 @@ export const post = async ({ id }) => {
//...
```
-The GraphQL handler will then take care of logging your query and data -- as long as your logger is setup to log at the `info` [level](logger.md#log-level) and above.
+The GraphQL handler will then take care of logging your query and data -- as long as your logger is setup to log at the `info` [level](logger#log-level) and above.
-> You can also disable the statements in production by just logging at the `warn` [level](logger.md#log-level) or above
+> You can also disable the statements in production by just logging at the `warn` [level](logger#log-level) or above
This means that you can keep your services free of logger statements, but still see what's happening!
@@ -1184,7 +1615,7 @@ Stream to third-party log and application monitoring services vital to productio
Everyone has heard of reports that Company X logged emails, or passwords to files or systems that may not have been secured. While RedwoodJS logging won't necessarily prevent that, it does provide you with the mechanism to ensure that won't happen.
-To redact sensitive information, you can supply paths to keys that hold sensitive data using the RedwoodJS logger [redact option](logger.md#redaction).
+To redact sensitive information, you can supply paths to keys that hold sensitive data using the RedwoodJS logger [redact option](logger#redaction).
Because this logger is used with the GraphQL handler, it will respect any redaction paths setup.
@@ -1291,7 +1722,7 @@ By default, your GraphQL endpoint is open to the world.
That means anyone can request any query and invoke any Mutation.
Whatever types and fields are defined in your SDL is data that anyone can access.
-Redwood [encourages being secure by default](http://localhost:3000/docs/canary/directives#secure-by-default-with-built-in-directives) by defaulting all queries and mutations to have the `@requireAuth` directive when generating SDL or a service.
+Redwood [encourages being secure by default](directives) by defaulting all queries and mutations to have the `@requireAuth` directive when generating SDL or a service.
When your app builds and your server starts up, Redwood checks that **all** queries and mutations have `@requireAuth`, `@skipAuth` or a custom directive applied.
@@ -2047,7 +2478,7 @@ enum Color {
### SDL Comments
-When used with `--docs` option, [SDL generator](./cli-commands.md#generate-sdl) adds comments for:
+When used with `--docs` option, [SDL generator](cli-commands#generate-sdl) adds comments for:
* Directives
* Queries
diff --git a/packages/create-redwood-app/templates/js/graphql.config.js b/packages/create-redwood-app/templates/js/graphql.config.js
index 2da7862f6b57..e6c0ef53af71 100644
--- a/packages/create-redwood-app/templates/js/graphql.config.js
+++ b/packages/create-redwood-app/templates/js/graphql.config.js
@@ -2,4 +2,5 @@ const { getPaths } = require('@redwoodjs/internal')
module.exports = {
schema: getPaths().generated.schema,
+ documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}',
}
diff --git a/packages/create-redwood-app/templates/js/web/src/App.jsx b/packages/create-redwood-app/templates/js/web/src/App.jsx
index 97fb5e02520d..9216dd846148 100644
--- a/packages/create-redwood-app/templates/js/web/src/App.jsx
+++ b/packages/create-redwood-app/templates/js/web/src/App.jsx
@@ -1,6 +1,7 @@
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
+import possibleTypes from 'src/graphql/possibleTypes'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
@@ -9,7 +10,13 @@ import './index.css'
const App = () => (
-
+
diff --git a/packages/create-redwood-app/templates/js/web/src/graphql/possibleTypes.js b/packages/create-redwood-app/templates/js/web/src/graphql/possibleTypes.js
new file mode 100644
index 000000000000..751e2a3c1655
--- /dev/null
+++ b/packages/create-redwood-app/templates/js/web/src/graphql/possibleTypes.js
@@ -0,0 +1,6 @@
+
+const result = {
+ "possibleTypes": {}
+}
+
+export default result
diff --git a/packages/create-redwood-app/templates/ts/graphql.config.js b/packages/create-redwood-app/templates/ts/graphql.config.js
index 2da7862f6b57..e6c0ef53af71 100644
--- a/packages/create-redwood-app/templates/ts/graphql.config.js
+++ b/packages/create-redwood-app/templates/ts/graphql.config.js
@@ -2,4 +2,5 @@ const { getPaths } = require('@redwoodjs/internal')
module.exports = {
schema: getPaths().generated.schema,
+ documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}',
}
diff --git a/packages/create-redwood-app/templates/ts/web/src/App.tsx b/packages/create-redwood-app/templates/ts/web/src/App.tsx
index 97fb5e02520d..1232c7e72eb5 100644
--- a/packages/create-redwood-app/templates/ts/web/src/App.tsx
+++ b/packages/create-redwood-app/templates/ts/web/src/App.tsx
@@ -1,6 +1,7 @@
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
+import possibleTypes from 'src/graphql/possibleTypes'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
@@ -9,7 +10,13 @@ import './index.css'
const App = () => (
-
+
@@ -17,3 +24,4 @@ const App = () => (
)
export default App
+
diff --git a/packages/create-redwood-app/templates/ts/web/src/graphql/possibleTypes.ts b/packages/create-redwood-app/templates/ts/web/src/graphql/possibleTypes.ts
new file mode 100644
index 000000000000..3f72fb793443
--- /dev/null
+++ b/packages/create-redwood-app/templates/ts/web/src/graphql/possibleTypes.ts
@@ -0,0 +1,12 @@
+
+export interface PossibleTypesResultData {
+ possibleTypes: {
+ [key: string]: string[]
+ }
+}
+
+const result: PossibleTypesResultData = {
+ "possibleTypes": {}
+}
+
+export default result;
diff --git a/packages/create-redwood-app/tests/template.test.js b/packages/create-redwood-app/tests/template.test.js
index d48e05ccb451..809a9fcfd8aa 100644
--- a/packages/create-redwood-app/tests/template.test.js
+++ b/packages/create-redwood-app/tests/template.test.js
@@ -70,6 +70,8 @@ describe('template', () => {
"/web/src/components",
"/web/src/components/.keep",
"/web/src/entry.client.tsx",
+ "/web/src/graphql",
+ "/web/src/graphql/possibleTypes.ts",
"/web/src/index.css",
"/web/src/index.html",
"/web/src/layouts",
@@ -155,6 +157,8 @@ describe('JS template', () => {
"/web/src/components",
"/web/src/components/.keep",
"/web/src/entry.client.jsx",
+ "/web/src/graphql",
+ "/web/src/graphql/possibleTypes.js",
"/web/src/index.css",
"/web/src/index.html",
"/web/src/layouts",
diff --git a/packages/internal/package.json b/packages/internal/package.json
index 1c752b382775..d9487da6c6d3 100644
--- a/packages/internal/package.json
+++ b/packages/internal/package.json
@@ -36,6 +36,7 @@
"@graphql-codegen/add": "4.0.1",
"@graphql-codegen/cli": "3.3.1",
"@graphql-codegen/core": "3.1.0",
+ "@graphql-codegen/fragment-matcher": "5.0.0",
"@graphql-codegen/schema-ast": "3.0.1",
"@graphql-codegen/typescript": "3.0.4",
"@graphql-codegen/typescript-operations": "3.0.4",
diff --git a/packages/internal/src/__tests__/__snapshots__/possibleTypes.test.ts.snap b/packages/internal/src/__tests__/__snapshots__/possibleTypes.test.ts.snap
new file mode 100644
index 000000000000..61c3fb78b3d5
--- /dev/null
+++ b/packages/internal/src/__tests__/__snapshots__/possibleTypes.test.ts.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Generate gql possible types web from the GraphQL Schema when there are *no* union types 1`] = `
+"export interface PossibleTypesResultData {
+ possibleTypes: {
+ [key: string]: string[]
+ }
+}
+const result: PossibleTypesResultData = {
+ possibleTypes: {},
+}
+export default result
+"
+`;
+
+exports[`Generate gql possible types web from the GraphQL Schema when there are union types 1`] = `
+"export interface PossibleTypesResultData {
+ possibleTypes: {
+ [key: string]: string[]
+ }
+}
+const result: PossibleTypesResultData = {
+ possibleTypes: {
+ Groceries: ['Fruit', 'Vegetable'],
+ Grocery: ['Fruit', 'Vegetable'],
+ },
+}
+export default result
+"
+`;
diff --git a/packages/internal/src/__tests__/possibleTypes.test.ts b/packages/internal/src/__tests__/possibleTypes.test.ts
new file mode 100644
index 000000000000..88964af39eb0
--- /dev/null
+++ b/packages/internal/src/__tests__/possibleTypes.test.ts
@@ -0,0 +1,73 @@
+import fs from 'fs'
+import path from 'path'
+
+import { getPaths } from '@redwoodjs/project-config'
+
+import { generateGraphQLSchema } from '../generate/graphqlSchema'
+import { generatePossibleTypes } from '../generate/possibleTypes'
+
+afterEach(() => {
+ delete process.env.RWJS_CWD
+ jest.restoreAllMocks()
+})
+
+describe('Generate gql possible types web from the GraphQL Schema', () => {
+ test('when there are *no* union types', async () => {
+ const FIXTURE_PATH = path.resolve(
+ __dirname,
+ '../../../../__fixtures__/example-todo-main'
+ )
+
+ process.env.RWJS_CWD = FIXTURE_PATH
+
+ const s = await generateGraphQLSchema()
+
+ console.debug(s)
+
+ jest
+ .spyOn(fs, 'writeFileSync')
+ .mockImplementation(
+ (file: fs.PathOrFileDescriptor, data: string | ArrayBufferView) => {
+ expect(file).toMatch(
+ path.join(getPaths().web.graphql, 'possibleTypes.ts')
+ )
+ expect(data).toMatchSnapshot()
+ }
+ )
+
+ const { possibleTypesFiles } = await generatePossibleTypes()
+
+ expect(possibleTypesFiles).toHaveLength(1)
+ expect(possibleTypesFiles[0]).toMatch(
+ path.join(getPaths().web.graphql, 'possibleTypes.ts')
+ )
+ })
+
+ test('when there are union types ', async () => {
+ const FIXTURE_PATH = path.resolve(
+ __dirname,
+ '../../../../__fixtures__/fragment-test-project'
+ )
+
+ process.env.RWJS_CWD = FIXTURE_PATH
+ await generateGraphQLSchema()
+
+ jest
+ .spyOn(fs, 'writeFileSync')
+ .mockImplementation(
+ (file: fs.PathOrFileDescriptor, data: string | ArrayBufferView) => {
+ expect(file).toMatch(
+ path.join(getPaths().web.graphql, 'possibleTypes.ts')
+ )
+ expect(data).toMatchSnapshot()
+ }
+ )
+
+ const { possibleTypesFiles } = await generatePossibleTypes()
+
+ expect(possibleTypesFiles).toHaveLength(1)
+ expect(possibleTypesFiles[0]).toMatch(
+ path.join(getPaths().web.graphql, 'possibleTypes.ts')
+ )
+ })
+})
diff --git a/packages/internal/src/generate/generate.ts b/packages/internal/src/generate/generate.ts
index afa6ba94acbf..f24bca684de4 100644
--- a/packages/internal/src/generate/generate.ts
+++ b/packages/internal/src/generate/generate.ts
@@ -3,6 +3,7 @@
import { getPaths } from '@redwoodjs/project-config'
import { generateGraphQLSchema } from './graphqlSchema'
+import { generatePossibleTypes } from './possibleTypes'
import { generateTypeDefs } from './typeDefinitions'
export const generate = async () => {
@@ -10,6 +11,8 @@ export const generate = async () => {
await generateGraphQLSchema()
const { typeDefFiles, errors: generateTypeDefsErrors } =
await generateTypeDefs()
+ const { possibleTypesFiles, errors: generatePossibleTypesErrors } =
+ await generatePossibleTypes()
let files = []
@@ -17,11 +20,17 @@ export const generate = async () => {
files.push(schemaPath)
}
- files = [...files, ...typeDefFiles].filter((x) => typeof x === 'string')
+ files = [...files, ...typeDefFiles, ...possibleTypesFiles].filter(
+ (x) => typeof x === 'string'
+ )
return {
files,
- errors: [...generateGraphQLSchemaErrors, ...generateTypeDefsErrors],
+ errors: [
+ ...generateGraphQLSchemaErrors,
+ ...generateTypeDefsErrors,
+ ...generatePossibleTypesErrors,
+ ],
}
}
diff --git a/packages/internal/src/generate/graphqlCodeGen.ts b/packages/internal/src/generate/graphqlCodeGen.ts
index 49f50ad37aaa..a5e44b7d4fa9 100644
--- a/packages/internal/src/generate/graphqlCodeGen.ts
+++ b/packages/internal/src/generate/graphqlCodeGen.ts
@@ -203,7 +203,7 @@ async function runCodegenGraphQL(
return [filename]
}
-function getLoadDocumentsOptions(filename: string) {
+export function getLoadDocumentsOptions(filename: string) {
const loadTypedefsConfig: LoadTypedefsOptions<{ cwd: string }> = {
cwd: getPaths().base,
ignore: [path.join(process.cwd(), filename)],
diff --git a/packages/internal/src/generate/possibleTypes.ts b/packages/internal/src/generate/possibleTypes.ts
new file mode 100644
index 000000000000..f82756386c4e
--- /dev/null
+++ b/packages/internal/src/generate/possibleTypes.ts
@@ -0,0 +1,108 @@
+import fs from 'fs'
+import path from 'path'
+
+import * as fragmentMatcher from '@graphql-codegen/fragment-matcher'
+import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'
+import { loadDocuments, loadSchemaSync } from '@graphql-tools/load'
+import { format } from 'prettier'
+
+import { getPaths } from '@redwoodjs/project-config'
+
+import { getLoadDocumentsOptions } from './graphqlCodeGen'
+
+type PossibleTypesResult = {
+ possibleTypesFiles: string[]
+ errors: { message: string; error: unknown }[]
+}
+
+/**
+ * Generate possible types from fragments and union types
+ *
+ * In order to use fragments with unions and interfaces in Apollo Client,
+ * you need to tell the client how to discriminate between the different
+ * types that implement or belong to a supertype.
+ *
+ * You pass a possibleTypes option to the InMemoryCache constructor
+ * to specify these relationships in your schema.
+ *
+ * This object maps the name of an interface or union type (the supertype)
+ * to the types that implement or belong to it (the subtypes).
+ *
+ * For example:
+ *
+ * ```ts
+ * possibleTypes: {
+ * Character: ["Jedi", "Droid"],
+ * Test: ["PassingTest", "FailingTest", "SkippedTest"],
+ * Snake: ["Viper", "Python"],
+ * Groceries: ['Fruit', 'Vegetable'],
+ * },
+ * ```
+ *
+ * @see https://www.apollographql.com/docs/react/data/fragments/#using-fragments-with-unions-and-interfaces
+ **/
+export const generatePossibleTypes = async (): Promise => {
+ const filename = path.join(getPaths().web.graphql, 'possibleTypes.ts')
+ const options = getLoadDocumentsOptions(filename)
+ const documentsGlob = './web/src/**/!(*.d).{ts,tsx,js,jsx}'
+
+ let documents
+
+ try {
+ documents = await loadDocuments([documentsGlob], options)
+ } catch {
+ // No GraphQL documents present, no need to try to generate possibleTypes
+ return {
+ possibleTypesFiles: [],
+ errors: [],
+ }
+ }
+
+ const errors: { message: string; error: unknown }[] = []
+
+ try {
+ const files = []
+ const pluginConfig = {}
+ const info = {
+ outputFile: filename,
+ }
+ const schema = loadSchemaSync(getPaths().generated.schema, {
+ loaders: [new GraphQLFileLoader()],
+ sort: true,
+ })
+
+ const possibleTypes = await fragmentMatcher.plugin(
+ schema,
+ documents,
+ pluginConfig,
+ info
+ )
+
+ files.push(filename)
+
+ const output = format(possibleTypes.toString(), {
+ trailingComma: 'es5',
+ bracketSpacing: true,
+ tabWidth: 2,
+ semi: false,
+ singleQuote: true,
+ arrowParens: 'always',
+ parser: 'typescript',
+ })
+
+ fs.mkdirSync(path.dirname(filename), { recursive: true })
+ fs.writeFileSync(filename, output)
+
+ return { possibleTypesFiles: [filename], errors }
+ } catch (e) {
+ errors.push({
+ message: 'Error: Could not generate GraphQL possible types (web)',
+ error: e,
+ })
+
+ return {
+ possibleTypesFiles: [],
+ errors,
+ }
+ }
+}
diff --git a/packages/project-config/src/__tests__/paths.test.ts b/packages/project-config/src/__tests__/paths.test.ts
index 7d9f4a471ec6..7f5e0afc92bd 100644
--- a/packages/project-config/src/__tests__/paths.test.ts
+++ b/packages/project-config/src/__tests__/paths.test.ts
@@ -140,6 +140,7 @@ describe('paths', () => {
// Vite paths ~ not configured in empty-project
viteConfig: null,
entryClient: null,
+ graphql: path.join(FIXTURE_BASEDIR, 'web', 'src', 'graphql'),
},
}
@@ -377,6 +378,7 @@ describe('paths', () => {
),
dist: path.join(FIXTURE_BASEDIR, 'web', 'dist'),
types: path.join(FIXTURE_BASEDIR, 'web', 'types'),
+ graphql: path.join(FIXTURE_BASEDIR, 'web', 'src', 'graphql'),
// New Vite paths
viteConfig: path.join(FIXTURE_BASEDIR, 'web', 'vite.config.ts'),
entryClient: null, // doesnt exist in example-todo-main
@@ -664,6 +666,7 @@ describe('paths', () => {
entryClient: null,
dist: path.join(FIXTURE_BASEDIR, 'web', 'dist'),
types: path.join(FIXTURE_BASEDIR, 'web', 'types'),
+ graphql: path.join(FIXTURE_BASEDIR, 'web', 'src', 'graphql'),
},
}
@@ -906,6 +909,7 @@ describe('paths', () => {
),
dist: path.join(FIXTURE_BASEDIR, 'web', 'dist'),
types: path.join(FIXTURE_BASEDIR, 'web', 'types'),
+ graphql: path.join(FIXTURE_BASEDIR, 'web', 'src', 'graphql'),
// Vite paths
viteConfig: path.join(FIXTURE_BASEDIR, 'web', 'vite.config.ts'),
entryClient: path.join(FIXTURE_BASEDIR, 'web/src/entry.client.tsx'),
diff --git a/packages/project-config/src/paths.ts b/packages/project-config/src/paths.ts
index 7d607b0e94cb..cf3e1a528243 100644
--- a/packages/project-config/src/paths.ts
+++ b/packages/project-config/src/paths.ts
@@ -46,6 +46,7 @@ export interface WebPaths {
storybookManagerConfig: string
dist: string
types: string
+ graphql: string
}
export interface Paths {
@@ -102,6 +103,7 @@ const PATH_WEB_DIR_CONFIG = 'web/config'
const PATH_WEB_DIR_CONFIG_WEBPACK = 'web/config/webpack.config.js'
const PATH_WEB_DIR_CONFIG_VITE = 'web/vite.config' // .js,.ts
const PATH_WEB_DIR_ENTRY_CLIENT = 'web/src/entry.client' // .jsx,.tsx
+const PATH_WEB_DIR_GRAPHQL = 'web/src/graphql' // .js,.ts
const PATH_WEB_DIR_CONFIG_POSTCSS = 'web/config/postcss.config.js'
const PATH_WEB_DIR_CONFIG_STORYBOOK_CONFIG = 'web/config/storybook.config.js'
@@ -216,6 +218,7 @@ export const getPaths = (BASE_DIR: string = getBaseDir()): Paths => {
dist: path.join(BASE_DIR, PATH_WEB_DIR_DIST),
types: path.join(BASE_DIR, 'web/types'),
entryClient: resolveFile(path.join(BASE_DIR, PATH_WEB_DIR_ENTRY_CLIENT)), // new vite/stream entry point for client
+ graphql: path.join(BASE_DIR, PATH_WEB_DIR_GRAPHQL),
},
}
diff --git a/packages/web/src/apollo/fragmentRegistry.ts b/packages/web/src/apollo/fragmentRegistry.ts
new file mode 100644
index 000000000000..d8d1edac5e0d
--- /dev/null
+++ b/packages/web/src/apollo/fragmentRegistry.ts
@@ -0,0 +1,113 @@
+import * as apolloClient from '@apollo/client'
+import type { UseFragmentResult } from '@apollo/client'
+import type { FragmentRegistryAPI } from '@apollo/client/cache'
+import { createFragmentRegistry } from '@apollo/client/cache'
+import { getFragmentDefinitions } from '@apollo/client/utilities'
+import type { DocumentNode } from 'graphql'
+
+export type FragmentIdentifier = string | number
+
+export type CacheKey = {
+ __typename: string
+ id: FragmentIdentifier
+}
+
+export type RegisterFragmentResult = {
+ fragment: DocumentNode
+ typename: string
+ getCacheKey: (id: FragmentIdentifier) => CacheKey
+ useRegisteredFragment: (
+ id: FragmentIdentifier
+ ) => UseFragmentResult
+}
+
+/*
+ * Get the typename from a fragment.
+ */
+const getTypenameFromFragment = (fragment: DocumentNode): string => {
+ const [definition] = getFragmentDefinitions(fragment)
+ return definition.typeCondition.name.value
+}
+
+/**
+ *
+ * Relies on the useFragment hook which represents a lightweight
+ * live binding into the Apollo Client Cache.
+ *
+ * It enables Apollo Client to broadcast specific fragment results to
+ * individual components.
+ *
+ * This hook returns an always-up-to-date view of whatever data the
+ * cache currently contains for a given fragment.
+ *
+ * useFragment never triggers network requests of its own.
+ *
+ * @see https://www.apollographql.com/docs/react/api/react/hooks#usefragment
+ */
+const useRegisteredFragmentHook = (
+ fragment: DocumentNode,
+ id: string | number
+): UseFragmentResult => {
+ const from = { __typename: getTypenameFromFragment(fragment), id }
+
+ return apolloClient.useFragment({
+ fragment,
+ from,
+ })
+}
+
+/**
+ * Creates a fragment registry for Apollo Client's InMemoryCache so that they
+ * can be referred to by name in any query or InMemoryCache operation
+ * (such as cache.readFragment, cache.readQuery and cache.watch)
+ * without needing to interpolate their declaration.
+ *
+ * @see https://www.apollographql.com/docs/react/data/fragments/#registering-named-fragments-using-createfragmentregistry
+ **/
+export const fragmentRegistry: FragmentRegistryAPI = createFragmentRegistry()
+
+/**
+ * Registers a list of fragments with the fragment registry.
+ */
+export const registerFragments = (fragments: DocumentNode[]) => {
+ return fragments.map(registerFragment)
+}
+
+/**
+ * Registers a fragment with the fragment registry.
+ *
+ * It returns a set of utilities for working with the fragment, including:
+ * - the fragment itself
+ * - the typename of the fragment
+ * - a function to get the cache key for a given id
+ * - a hook to use the registered fragment in a component by id
+ * that returns cached data for the fragment
+ *
+ * Note: one does not need to use the hook, cacheKey to use the fragment in queries.
+ *
+ * @see https://www.apollographql.com/docs/react/data/fragments/#registering-named-fragments-using-createfragmentregistry
+ */
+export const registerFragment = (
+ fragment: DocumentNode
+): RegisterFragmentResult => {
+ fragmentRegistry.register(fragment)
+
+ const typename = getTypenameFromFragment(fragment)
+
+ const getCacheKey = (id: FragmentIdentifier): CacheKey => {
+ return { __typename: typename, id }
+ }
+
+ const useRegisteredFragment = (
+ id: FragmentIdentifier
+ ): UseFragmentResult => {
+ return useRegisteredFragmentHook(fragment, id)
+ }
+
+ return {
+ fragment,
+ typename,
+ getCacheKey,
+ useRegisteredFragment,
+ }
+}
diff --git a/packages/web/src/apollo/index.tsx b/packages/web/src/apollo/index.tsx
index 505e44bb0b73..25b7c0d574b0 100644
--- a/packages/web/src/apollo/index.tsx
+++ b/packages/web/src/apollo/index.tsx
@@ -7,7 +7,6 @@ import * as apolloClient from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { getMainDefinition } from '@apollo/client/utilities'
import { print } from 'graphql/language/printer'
-
// Note: Importing directly from `apollo/client` doesn't work properly in Storybook.
const {
ApolloProvider,
@@ -31,7 +30,23 @@ import {
} from '../components/FetchConfigProvider'
import { GraphQLHooksProvider } from '../components/GraphQLHooksProvider'
+import {
+ fragmentRegistry,
+ registerFragment,
+ registerFragments,
+} from './fragmentRegistry'
import { SSELink } from './sseLink'
+import { useCache } from './useCache'
+
+export type {
+ CacheKey,
+ FragmentIdentifier,
+ RegisterFragmentResult,
+} from './fragmentRegistry'
+
+export { useCache }
+
+export { fragmentRegistry, registerFragment, registerFragments, SSELink }
export type ApolloClientCacheConfig = apolloClient.InMemoryCacheConfig
@@ -282,11 +297,13 @@ class ErrorBoundary extends React.Component {
export const RedwoodApolloProvider: React.FunctionComponent<{
graphQLClientConfig?: GraphQLClientConfigProp
+ fragments?: apolloClient.DocumentNode[]
useAuth?: UseAuth
logLevel?: ReturnType
children: React.ReactNode
}> = ({
graphQLClientConfig,
+ fragments,
useAuth = useNoAuth,
logLevel = 'debug',
children,
@@ -295,9 +312,15 @@ export const RedwoodApolloProvider: React.FunctionComponent<{
// we have to instantiate `InMemoryCache` here, so that it doesn't get wiped.
const { cacheConfig, ...config } = graphQLClientConfig ?? {}
- const cache = new InMemoryCache(cacheConfig).restore(
- globalThis?.__REDWOOD__APOLLO_STATE ?? {}
- )
+ // Auto register fragments
+ if (fragments) {
+ fragmentRegistry.register(...fragments)
+ }
+
+ const cache = new InMemoryCache({
+ fragments: fragmentRegistry,
+ ...cacheConfig,
+ }).restore(globalThis?.__REDWOOD__APOLLO_STATE ?? {})
return (
diff --git a/packages/web/src/apollo/useCache.tsx b/packages/web/src/apollo/useCache.tsx
new file mode 100644
index 000000000000..98a0112cbbfe
--- /dev/null
+++ b/packages/web/src/apollo/useCache.tsx
@@ -0,0 +1,91 @@
+import type { ApolloCache, Reference, StoreObject } from '@apollo/client'
+import { useApolloClient } from '@apollo/client'
+import type { NormalizedCacheObject } from '@apollo/client/cache/inmemory/types'
+import type { ApolloQueryResult } from '@apollo/client/core'
+
+type useCacheType = {
+ cache: ApolloCache