Skip to content

Commit

Permalink
feat: client options, client context, server utils (#45)
Browse files Browse the repository at this point in the history
* implement graphqlMiddleware.clientOptions with client context

* use client context in server options

* fix generated types, create server utils

* move shared composable helpers to helpers folder

* fix tests

* clean up imports

* Make client context in server options partial

* add docs for server utils

* update vitepress
  • Loading branch information
dulnan authored Nov 15, 2024
1 parent befce3b commit 5fc2e59
Show file tree
Hide file tree
Showing 51 changed files with 13,610 additions and 8,461 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ Temporary Items
.apdisk

cypress/videos/
cypress/screenshots
28 changes: 25 additions & 3 deletions apollo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ import data from './data.json' assert { type: 'json' }
import type { Readable } from 'stream'
import { v4 as uuidv4 } from 'uuid'

function getLanguageFromPath(path = ''): string | undefined {
if (!path) {
return
}

const matches = /\/([^/]+)/.exec(path)
return matches?.[1]
}

const BASIC_LOGGING: any = {
requestDidStart(requestContext) {
console.log('request started')
Expand Down Expand Up @@ -95,9 +104,14 @@ const typeDefs = `#graphql
headerServer: String
}
type DataForLayer {
text: String
}
type DataForLayer {
text: String
}
type TestClientOptions {
language: String
languageFromPath: String
}
type Query {
users: [User!]!
Expand All @@ -108,6 +122,7 @@ text: String
getSubmissions: [FormSubmission]
getCurrentTime: String
dataForLayer: DataForLayer
testClientOptions(path: String!): TestClientOptions
}
type UploadedFile {
Expand Down Expand Up @@ -196,6 +211,13 @@ const resolvers = {
},
})
},

testClientOptions: (_parent: any, args: any, context: any) => {
return {
language: context.headers['x-nuxt-client-options-language'],
languageFromPath: getLanguageFromPath(args.path),
}
},
},
User: {
friends: () => {
Expand Down
2 changes: 1 addition & 1 deletion build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default defineBuildConfig({
'@polka/url',
'mrmime',
'#graphql-middleware/types',
'#build/nuxt-graphql-middleware',
'#nuxt-graphql-middleware/generated-types',
'#graphql-middleware-server-options-build',
],
})
19 changes: 19 additions & 0 deletions cypress/e2e/clientOptions.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
describe('The clientOptions', () => {
it('are working correctly with SSR', () => {
cy.visit('/de')
cy.get('#nuxt-language').first().should('have.text', 'de')
cy.get('#response-language').first().should('have.text', 'de')
})

it('are working correctly with SPA', () => {
cy.visit('/')
cy.get('#link-client-options').click()
cy.get('#nuxt-language').first().should('have.text', 'de')
cy.get('#response-language').first().should('have.text', 'de')

cy.get('#lang-switch-fr').click()

cy.get('#nuxt-language').first().should('have.text', 'fr')
cy.get('#response-language').first().should('have.text', 'fr')
})
})
14 changes: 14 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ export default defineConfig({
{ text: 'useGraphqlState', link: '/composables/useGraphqlState' },
],
},
{
text: 'Server Utils',
items: [
{ text: 'useGraphqlQuery', link: '/server-utils/useGraphqlQuery' },
{
text: 'useGraphqlMutation',
link: '/server-utils/useGraphqlMutation',
},
],
},
{
text: 'Configuration',
items: [
Expand All @@ -87,6 +97,10 @@ export default defineConfig({
text: 'Server Options',
link: '/configuration/server-options',
},
{
text: 'Client Options',
link: '/configuration/client-options',
},
{
text: 'Full Example',
link: '/configuration/full-example',
Expand Down
5 changes: 0 additions & 5 deletions docs/.vitepress/theme/components/HeroIllustration.vue

This file was deleted.

17 changes: 0 additions & 17 deletions docs/.vitepress/theme/custom.css

This file was deleted.

16 changes: 0 additions & 16 deletions docs/.vitepress/theme/index.ts

This file was deleted.

121 changes: 121 additions & 0 deletions docs/configuration/client-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Client Options

`nuxt-graphql-middleware` will look for a file called
`graphqlMiddleware.clientOptions.ts` in your app dir. This file can export so
called "client options" which are used when _making a request to the GraphQL
middleware_.

::: warning

Note that the client options are only used in a **Nuxt app context** - they are
not used when using `useGraphqlQuery` or other utils in a **Nitro context** such
as event handlers.

:::

## Defining Client Options

When using a composable such as `useGraphqlQuery`, behind the scenes it will use
`$fetch` to make a request to the GraphQL middleware server route. Sometimes
it's useful to pass some additional context with this request that can then be
used on the server.

Similar to [serverOptions](/configuration/server-options), you can create a file
called `graphqlMiddleware.clientOptions.ts` in your `app` directory (usually
`<rootDir>/app`).

::: code-group

```typescript [~/app/graphqlMiddleware.clientOptions.ts]
import { defineGraphqlClientOptions } from 'nuxt-graphql-middleware/dist/runtime/clientOptions'

export default defineGraphqlClientOptions({})
```

:::

## Defining Client Context

Implement the `buildClientContext()` method to return an object with string
values.

::: code-group

```typescript [~/app/graphqlMiddleware.clientOptions.ts]
import { defineGraphqlClientOptions } from 'nuxt-graphql-middleware/dist/runtime/clientOptions'

export default defineGraphqlClientOptions<{
language: string
country: string
}>({
buildClientContext() {
const language = useCurrentLanguage()
const country = useCurrentCountry()
return {
language: language.value,
country: country.value,
}
},
})
```

:::

::: info

By passing a generic in `defineGraphqlClientOptions` you can define the type of
your context object.

:::

Now everytime a request to the middleware is made with a composable such as
`useGraphqlQuery`, the composable will call the `buildClientContext` method to
get the current context. It then maps each property of the returned object to a
query parameter while prefixing the property to prevent collisions with
potential query parameters from GraphQL variables.

So for example, when making a GraphQL query like so:

```typescript
const data = await useGraphqlQuery('loadProduct', {
id: '123',
})
```

The composable will make a fetch request to this URL.

`/api/graphql_middleware/loadProduct?id=123&__gqlc_language=en&__gqlc_country=US`

Both the `language` and `country` properties we returned in the object in
`buildClientContext()` are appended as prefixed query parameters.

## Using Client Context

On the server you can then access this client context from within all
[serverOptions](/configuration/server-options) methods:

::: code-group

```typescript [~/server/graphqlMiddleware.serverOptions.ts]
import { defineGraphqlServerOptions } from 'nuxt-graphql-middleware/dist/runtime/serverOptions'

export default defineGraphqlServerOptions({
graphqlEndpoint(event, operation, operationName, context) {
// Use the language from the client context.
const language = context?.client?.language || 'en'
return `http://backend_server/${language}/graphql`
},

serverFetchOptions: function (event, _operation, operationName, context) {
// Pass the current country as a header when making a request to the
// GraphQL server.
return {
headers: {
'x-current-country': context.client?.country || 'US',
},
}
},
})
```

:::
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ titleTemplate:
hero:
name: Nuxt GraphQL Middleware
text: A GraphQL client for Nuxt
image:
src: /illustration.png
tagline:
Expose GraphQL queries and mutations as fully typed API endpoints. Hide your
GraphQL server from public access and prevent bundling large queries.
Expand Down
53 changes: 53 additions & 0 deletions docs/server-utils/useGraphqlMutation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# useGraphqlMutation()

::: warning

While this util has the same name as the composable it's a completely separate
method. In particular, it **does not use** any state set using the
`useGraphqlState` composable or the `graphqlMiddleware.clientOptions.ts` file.

:::

This util is auto-imported and available in a server (nitro) context. It's
function signature is identical to the
[useGraphqlMutation composable](/composables/useGraphqlMutation) composable
available in a Nuxt app context.

## Example

```typescript
import { getQuery } from 'h3'

export default defineEventHandler(async (event) => {
const id = getQuery(event).id
const data = await useGraphqlMutation('trackVisit', {
id,
})
return data.data.success
})
```

## Client Context

Since the client context returned in
[buildClientContext()](/configuration/client-options) is only available in a
Nuxt app context you can manually pass the context when making a mutation with
the server util:

```typescript
import { getQuery } from 'h3'

export default defineEventHandler(async (event) => {
const id = getQuery(event).id
const data = await useGraphqlMutation({
name: 'trackVisit',
variables: {
id,
},
clientContext: {
language: 'de',
},
})
return data.data.success
})
```
42 changes: 42 additions & 0 deletions docs/server-utils/useGraphqlQuery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# useGraphqlQuery()

::: warning

While this util has the same name as the composable it's a completely separate
method. In particular, it **does not use** any state set using the
`useGraphqlState` composable or the `graphqlMiddleware.clientOptions.ts` file.

:::

This util is auto-imported and available in a server (nitro) context. It's
function signature is identical to the
[useGraphqlQuery composable](/composables/useGraphqlQuery) composable available
in a Nuxt app context.

## Example

```typescript
export default defineEventHandler(async () => {
const data = await useGraphqlQuery('users')
return data.data.users.map((v) => v.email)
})
```

## Client Context

Since the client context returned in
[buildClientContext()](/configuration/client-options) is only available in a
Nuxt app context you can manually pass the context when making a query with the
server util:

```typescript
export default defineEventHandler(async () => {
const data = await useGraphqlQuery({
name: 'users',
clientContext: {
language: 'de',
},
})
return data.data.users.map((v) => v.email)
})
```
Loading

0 comments on commit 5fc2e59

Please sign in to comment.