Custom typing GlobalContext

Hey team,

I’m adding the request IP address in my redwood lambda gql api like so:

type ContextFunctionArgs = {
  context: { event: APIGatewayProxyEvent; ipAddress?: string }
}

const ipAddress = (args: ContextFunctionArgs) => {
  const ipAddress = args?.context?.event?.requestContext?.identity?.sourceIp
  if (!ipAddress) {
    throw new Error('Could not extract users IP address.')
  }
  return ipAddress
}
const setContext: ContextFunction = async (args: ContextFunctionArgs) => {
  args.context.ipAddress = ipAddress(args)
  return args.context
}

export const handler = createGraphQLHandler({
  authDecoder,
  getCurrentUser,
  loggerConfig: { logger, options: {} },
  directives,
  sdls,
  services,
  armorConfig: { maxDepth: { n: 10 } },
  onException: () => {
    // Disconnect from your database with an unhandled exception.
    db.$disconnect()
  },
  context: setContext,
  cors: {
    origin: corsOrigins,
    credentials: true,
  },
})

I’ve tried adding a custom type declaration like so:

declare module '@redwoodjs/graphql-server' {
  interface GlobalContext {
    ipAddress?: string
  }
}

But this confuses typescript and the GlobalContext now doesn’t have any types, including currentUser which is there by default.

Is there something that I’m missing or is this not supported by redwood atm?

Not sure if you found an answer. GlobalContext is a Redwood-defined type. When you use the declare module syntax to define an ambient module, you’re replacing what’s already defined. I think you could import the type and extend your custom type (with the ipAddress key) from it, something like:

import type { GlobalContext as RWGlobalContext } from "@redwoodjs/graphql-server"

declare module '@redwoodjs/graphql-server' {
  interface GlobalContext extends RWGlobalContext {
    ipAddress?: string
  }
}

I didn’t test it to see if it works.

I’m attempting to implement the solution recommend at Custom typing GlobalContext - #2 by WebstackBuilder to add to the GlobalContext type definition. It seems to work as intended but yarn rw type-check is throwing an error:

src/lib/context.ts:11:13 - error TS2310: Type 'GlobalContext' recursively references itself as a base type.
11   interface GlobalContext extends RWGlobalContext {
               ~~~~~~~~~~~~~
Found 1 error in src/lib/context.ts:11

The whole file in question is:

import type {
  GlobalContext as RWGlobalContext,
  RedwoodGraphQLContext,
} from '@redwoodjs/graphql-server'

import { getCurrentApiUser } from './auth'

type CurrentApiUser = Awaited<ReturnType<typeof getCurrentApiUser>>

declare module '@redwoodjs/graphql-server' {
  interface GlobalContext extends RWGlobalContext {
    currentApiUser?: CurrentApiUser
  }
}

export interface WishboneGraphQLContext extends RedwoodGraphQLContext {
  currentApiUser?: CurrentApiUser
}

Interestingly this gives me the correct type information in my IDE but I also get an error trying to access the added property when running yarn rw type-check. E.g. –

../api/src/lib/auth.ts:153:53 - error TS2339: Property 'station' does not exist on type 'unknown'.
153       args[stationIdArg] !== context.currentApiUser.station.id
                                                        ~~~~~~~
Found 1 error in ../api/src/lib/auth.ts:153

Has anyone successfully done this? Using this method or any other?

Trying another approach here and also hitting issue with type checking specifically.

For my use case I only really need the context I’m trying to add on the API side so I tried to make a global apiContext instead of using Redwood’s conext:

# api/types/global.d.ts
import { getCurrentApiUser } from 'src/lib/auth'

export type CurrentApiUser = Awaited<ReturnType<typeof getCurrentApiUser>>

type ApiContext = {
  currentUser: CurrentApiUser | null
}

declare global {
  const apiContext: ApiContext
}

export {}
# api/tsconfig.json
{
  "compilerOptions": {
    "noEmit": true,
    "allowJs": true,
    "esModuleInterop": true,
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "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",
    "./types"
  ]
}

With this setup I am able to access apiContext.currentUser as expected and my IDE has the correct typings:

image

But when I run yarn rw type-check I am again getting errors:

node ➜ /workspace $ yarn rw type-check
✔ Generating the Prisma client...
✔ Generating types
../api/src/lib/auth.ts:76:12 - error TS2552: Cannot find name 'apiContext'. Did you mean 'context'?

76   return !!apiContext.currentUser || false
              ~~~~~~~~~~

  ../node_modules/@redwoodjs/graphql-server/dist/global.api-auto-imports.d.ts:5:11
    5     const context: GlobalContext;
                ~~~~~~~
    'context' is declared here.

../api/src/lib/auth.ts:153:30 - error TS2552: Cannot find name 'apiContext'. Did you mean 'context'?

153       args[stationIdArg] !== apiContext.currentUser.station.id
                                 ~~~~~~~~~~

  ../node_modules/@redwoodjs/graphql-server/dist/global.api-auto-imports.d.ts:5:11
    5     const context: GlobalContext;
                ~~~~~~~
    'context' is declared here.


Found 2 errors in the same file, starting at: ../api/src/lib/auth.ts:76

This seems more like more of a general TS configuration/typing thing.

I also just noticed that the errors are coming from the web side type checking:

node ➜ /workspace/api $ yarn tsc --skipLibCheck    
node ➜ /workspace/api $ cd ../web
node ➜ /workspace/web $ yarn tsc --skipLibCheck 
../api/src/lib/auth.ts:76:12 - error TS2552: Cannot find name 'apiContext'. Did you mean 'context'?

76   return !!apiContext.currentUser || false
              ~~~~~~~~~~

  ../node_modules/@redwoodjs/graphql-server/dist/global.api-auto-imports.d.ts:5:11
    5     const context: GlobalContext;
                ~~~~~~~
    'context' is declared here.

../api/src/lib/auth.ts:153:30 - error TS2552: Cannot find name 'apiContext'. Did you mean 'context'?

153       args[stationIdArg] !== apiContext.currentUser.station.id
                                 ~~~~~~~~~~

  ../node_modules/@redwoodjs/graphql-server/dist/global.api-auto-imports.d.ts:5:11
    5     const context: GlobalContext;
                ~~~~~~~
    'context' is declared here.


Found 2 errors in the same file, starting at: ../api/src/lib/auth.ts:76

The web side config looks like this:

# web/tsconfig.json
{
  "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"
  ]
}

So the type-check issue goes away if I add ../api/types to include in web/tsconfig.json, which makes sense.

I also spun up a new project and verified that this configurations are basically stock (plus a few small modifications). So a few related questions:

  1. Does this fix make sense? I’m not familiar enough with TS config to understand how this change might impact anything else. After doing this type checks pass, the IDE is happy, tests pass (expect for requireAuth tests because they don’t account for my new global context, which makes sense), and the application still works as expected.
  2. Why does web/tsconfig.json include api side content in rootDirs and paths in the first place? I assume there is some reason for this.
  3. Why does the initial project api/tsconfig.json not have ./types in includes?

This still feels a little off. I think with the information I got from writing this up I can probably make this work by replacing the GlobalContext (thought extending still does not seem doable).