Next.js graphql context is empty {} on SSR getServerSideProps - next.js

I have page with getServerSideProps:
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const apolloClient = initializeApollo()
await apolloClient.query({
query: UserQuery,
variables: { id: Number(ctx.query.id) },
})
return {
props: { initialApolloState: apolloClient.cache.extract() },
}
}
And resolver for User:
resolve: async (_parent, { where }, ctx, info) => {
const select = new PrismaSelect(info).value
console.log(ctx) // {} why is empty when i use getServerSideProps?
const res = await ctx.db.user.findOne({
where: { id: 1 },
...select,
})
return null
},
Same client side query works fine, but for some reason when i use getServerSideProps ctx is empty. How to fix this?
I can pass ctx to const apolloClient = initializeApollo(null, ctx), but it's will not run apolloServer ctx resolver function with db property and ctx.db will be undefined.
Apollo-server:
const apolloServer = new ApolloServer({
schema,
async context(ctx: Ctx): Promise<Ctx> {
ctx.db = prisma
return ctx
},
})
export const apolloServerHandler = apolloServer.createHandler({ path: '/api/graphql' })
Apollo-client:
import { useMemo } from 'react'
import { ApolloClient, InMemoryCache, NormalizedCacheObject } from '#apollo/client'
import { IncomingMessage, ServerResponse } from 'http'
type ResolverContext = {
req?: IncomingMessage
res?: ServerResponse
}
const typePolicies = {
Test: {
keyFields: ['id'],
},
User: {
keyFields: ['id'],
},
}
let apolloClient: ApolloClient<NormalizedCacheObject>
function createIsomorphLink(context: ResolverContext = {}): any {
if (typeof window === 'undefined') {
const { SchemaLink } = require('#apollo/client/link/schema')
const { schema } = require('backend/graphql/schema')
return new SchemaLink({ schema, context })
} else {
const { HttpLink } = require('#apollo/client/link/http')
return new HttpLink({
uri: '/api/graphql',
credentials: 'same-origin',
})
}
}
function createApolloClient(context?: ResolverContext): ApolloClient<NormalizedCacheObject> {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: createIsomorphLink(context),
cache: new InMemoryCache({ typePolicies }),
})
}
export function initializeApollo(
initialState: any = null,
// Pages with Next.js data fetching methods, like `getStaticProps`, can send
// a custom context which will be used by `SchemaLink` to server render pages
context?: ResolverContext
): ApolloClient<NormalizedCacheObject> {
const _apolloClient = apolloClient ?? createApolloClient(context)
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// get hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract()
// Restore the cache using the data passed from getStaticProps/getServerSideProps
// combined with the existing cached data
_apolloClient.cache.restore({ ...existingCache, ...initialState })
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
// eslint-disable-next-line
export function useApollo(initialState: any): ApolloClient<NormalizedCacheObject> {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
}

You need to aplly context resolver:
async contextResolver(ctx: Ctx): Promise<Ctx> {
ctx.db = prisma
return ctx
},
const apolloServer = new ApolloServer({
schema,
context: contextResolver,
})
Inside getServerSideProps:
export const getServerSideProps: GetServerSideProps = async (ctx) => {
await contextResolver(ctx)
const apolloClient = initializeApollo(null, ctx)
await apolloClient.query({
query: UserQuery,
variables: { id: Number(ctx.query.id) },
})
return {
props: { initialApolloState: apolloClient.cache.extract() },
}
}

Related

How to create a function that returns new session format with extra key value pair

I am using NextJS with NextAuth with google and email providers. Unfortunately, the session returns only few fields that does not include userId of the user from the database.
I created however a function that I intend to use with every getServerSideProps request. The function returns the following:
{
user: {
name: 'daniel sas',
email: 'emailofuser#gmail.com',
image: 'https://lh3.gooleusercontent.com/a/AEdFTp6r44ZwqcfJORNnuYtbVv_LYbab-wv5Uyxk=s96-c',
userId: 'clbcpc0hi0002sb1wsiea3q5d'
},
expires: '2022-12-17T20:18:52.580Z'
}
The problem is I am getting an error that does not allow me to pass the props in the page:
Error: Your `getServerSideProps` function did not return an object. Did you forget to add a `return`?
In the function I get the user by the email, and attach the userId.
import { getSession } from "next-auth/react";
import prisma from './prisma'
// This function get the email and returns a new session object that includes
// the userId
export const requireAuthentication = async context => {
const session = await getSession(context);
const errorOrUserNotFound = () => {
return {
redirect: {
destination: '/signup',
permanent: false
}
}
}
// If there is no user or there is an error ret to signup page
if (!session) {
errorOrUserNotFound();
}
// If the user is not found return same redirect to signup
else {
try {
const user = await prisma.user.findUnique({where: { email: session.user.email }});
if (!user) return errorOrUserNotFound();
// Must return a new session here that contains the userId...
else {
const newSession = {
user: {
...session.user,
userId: user.id
},
expires: session.expires
};
console.log(newSession);
return {
props: {
session: newSession
}
}
}
}
catch (error) {
if (error) {
console.log(error);
}
}
}
}
The react component looks like this. In the getServerSideProps i return the await function. The problem is that when I log the prop in the serverside, I get the following:
{
props: { session: { user: [Object], expires: '2022-12-17T20:18:52.580Z' } }
}
However, if i log the props in the clientside, I get an empty object...
//Clientside compoen
import { getSession } from "next-auth/react"
import { Fragment, useState } from "react";
import { requireAuthentication } from "../../lib/requireAuthentication";
import CreateListModal from "./CreateListModal";
const DashboardPage = props => {
const [loading, setloading] = useState(false);
console.log(props);
return (
<section className="border-4 border-orange-800 max-w-5xl mx-auto">
<CreateListModal userId={props.userId} loading={loading} setloading={setloading} />
</section>
)
}
export const getServerSideProps = async context => {
const session = await getSession(context);
const reqAuth = await requireAuthentication(context);
console.log(reqAuth);
return reqAuth
}
export default DashboardPage;

Graphql subscriptions not working on serverless environment

I'm using graphql yoga and apollo client in my next app. Graphql yoga suggests this example for apollo client. link It works fine on non-serverless environment but when I deploy on vercel, subscriptions not working. I have tried checking for if it's running on browser but didn't do much. Here's my current apollo client
import {
ApolloLink,
Observable,
ApolloClient,
InMemoryCache,
} from '#apollo/client/core'
import { split } from '#apollo/client/link/core'
import { HttpLink } from '#apollo/client/link/http'
import { print, getOperationAST } from 'graphql'
import { setContext } from '#apollo/client/link/context'
import { getCookie } from 'cookies-next'
const isBrowser = typeof window !== 'undefined'
const token = getCookie('token')
class SSELink extends ApolloLink {
constructor() {
super()
}
request(operation) {
const url = new URL(`${process.env.NEXT_PUBLIC_APP_BASE_URL}/api/graphql`)
url.searchParams.append('query', print(operation.query))
if (operation.operationName) {
url.searchParams.append(
'operationName',
JSON.stringify(operation.operationName)
)
}
if (operation.variables) {
url.searchParams.append('variables', JSON.stringify(operation.variables))
}
if (operation.extensions) {
url.searchParams.append(
'extensions',
JSON.stringify(operation.extensions)
)
}
return new Observable((sink) => {
const eventsource = new EventSource(url.toString(), this.options)
eventsource.onmessage = function (event) {
const data = JSON.parse(event.data)
sink.next(data)
if (eventsource.readyState === 2) {
sink.complete()
}
}
eventsource.onerror = function (error) {
sink.error(error)
}
return () => eventsource.close()
})
}
}
const uri = `${process.env.NEXT_PUBLIC_APP_BASE_URL}/api/graphql`
const sseLink = isBrowser ? new SSELink({ uri }) : null
const httpLink = new HttpLink({ uri })
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
...(isBrowser && token && { authorization: `Bearer ${token}` }),
},
}
})
const link = isBrowser
? split(
({ query, operationName }) => {
const definition = getOperationAST(query, operationName)
return (
definition?.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
sseLink,
httpLink
)
: authLink.concat(httpLink)
export default new ApolloClient({
ssrMode: !isBrowser,
link,
cache: new InMemoryCache(),
})

Set-Cookie header from NestJS server not accepted by fetchBaseQuery when using Next.js SSR

I'm trying to do authorization using JWT access&refresh tokens (Next.js SSR + Redux Toolkit RTK Query + NestJS). When I receive a response from the server on the client (for example using Postman) the cookies sent by the server are saved. But when I do it on SSR using RTK Query Set-Cookie from the server just doesn't do anything. Sorry if I misunderstood something, I'm new to this.
NSETJS auth.controller.ts:
import {
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Req,
Res,
UseGuards
} from '#nestjs/common';
import { Response } from 'express';
import { ApiTags } from '#nestjs/swagger';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { DiscordAuthGuard } from './guards/discord-auth.guard';
import JwtAuthGuard from './guards/jwt-auth.guard';
import { RequestWithUser } from './auth.interface';
import JwtRefreshGuard from './guards/jwt-auth-refresh.guard';
import { UserService } from '#/modules/user/user.service';
#Controller('auth')
#ApiTags('Auth routes')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService
) {}
#Get('login')
#HttpCode(HttpStatus.OK)
#UseGuards(DiscordAuthGuard)
login(#Req() _req: Request) {}
#Get('redirect')
#HttpCode(HttpStatus.OK)
#UseGuards(DiscordAuthGuard)
async redirect(#Req() req: RequestWithUser, #Res() res: Response) {
const { user } = req;
const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(
user.id
);
const refreshTokenCookie = this.authService.getCookieWithJwtRefreshToken(
user.id
);
await this.userService.setCurrentRefreshToken(
refreshTokenCookie.token,
user.id
);
req.res.setHeader('Set-Cookie', [
accessTokenCookie.cookie,
refreshTokenCookie.cookie
]);
return res.redirect('http://localhost:3000');
}
#Get('refresh')
#HttpCode(HttpStatus.OK)
#UseGuards(JwtRefreshGuard)
async refresh(#Req() req: RequestWithUser) {
const { user } = req;
const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(
user.id
);
req.res.setHeader('Set-Cookie', [accessTokenCookie.cookie]);
return user;
}
#Get('me')
#HttpCode(HttpStatus.OK)
#UseGuards(JwtAuthGuard)
me(#Req() req: RequestWithUser) {
const { user } = req;
return user;
}
#Post('logout')
#HttpCode(HttpStatus.OK)
#UseGuards(JwtAuthGuard)
async logout(#Req() req: RequestWithUser) {
const { user } = req;
await this.userService.removeRefreshToken(user.id);
req.res.setHeader('Set-Cookie', this.authService.getCookiesForLogOut());
}
}
NEXT.JS _app.tsx:
import '#/styles/globals.scss';
import { AppProps } from 'next/app';
import { wrapper } from '#/store';
import { me } from '#/store/auth/auth.api';
import { setCredentials } from '#/store/auth/auth.slice';
function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
App.getInitialProps = wrapper.getInitialAppProps(
(store) =>
async ({ ctx, Component }) => {
try {
const { data: user } = await store.dispatch(me.initiate());
if (user !== undefined) {
store.dispatch(
setCredentials({
user,
})
);
}
} catch (err) {
console.log(err);
}
return {
pageProps: {
...(Component.getInitialProps
? await Component.getInitialProps({ ...ctx, store })
: {}),
},
};
}
);
export default wrapper.withRedux(App);
NEXT.JS auth.api.ts:
import { parseCookies } from 'nookies';
import {
BaseQueryFn,
createApi,
fetchBaseQuery,
} from '#reduxjs/toolkit/query/react';
import { HYDRATE } from 'next-redux-wrapper';
import { Mutex } from 'async-mutex';
import { NextPageContext } from 'next/types';
import { IUser } from './auth.interface';
import { destroyCredentials } from './auth.slice';
const mutex = new Mutex();
const baseQuery = fetchBaseQuery({
baseUrl: 'http://localhost:7777/api/auth',
prepareHeaders: (headers, { extra }) => {
const ctx = extra as Pick<NextPageContext<any>, 'req'>;
const windowAvailable = () =>
!!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
if (windowAvailable()) {
console.log('running on browser, skipping header manipulation');
return headers;
}
const cookies = parseCookies(ctx);
// Build a cookie string from object
const cookieValue = Object.entries(cookies)
// .filter(([k]) => k === 'JSESSIONID') // only include relevant cookies
.map(([k, v]) => `${k}=${v}`) // rfc6265
.join('; ');
console.log('figured out cookie value: ' + cookieValue);
headers.set('Cookie', cookieValue);
return headers;
},
credentials: 'include',
});
const baseQueryWithReauth: BaseQueryFn = async (args, api, extraOptions) => {
await mutex.waitForUnlock();
console.log('🔥🔥🔥 sending request to server');
let result = await baseQuery(args, api, extraOptions);
if (result?.error?.status === 401) {
if (!mutex.isLocked()) {
console.log('🔥🔥🔥 401, sending refresh token');
const release = await mutex.acquire();
try {
const refreshResult = await baseQuery('refresh', api, extraOptions);
const setCookies =
refreshResult.meta?.response?.headers.get('set-cookie');
console.log('🔥🔥🔥 response set-cookies:', setCookies);
console.log(
'🔥🔥🔥 refresh response status:',
refreshResult.meta?.response?.status
);
if (refreshResult.data) {
const windowAvailable = () =>
!!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
if (!windowAvailable()) {
console.log('🔥🔥🔥 running on server');
}
result = await baseQuery(args, api, extraOptions);
console.log(
'🔥🔥🔥 request response status after /refresh',
result.meta?.response?.status
);
} else {
api.dispatch(destroyCredentials());
}
} finally {
release();
}
}
}
return result;
};
export const authApi = createApi({
reducerPath: 'api/auth',
baseQuery: baseQueryWithReauth,
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath];
}
},
tagTypes: ['Auth'],
endpoints: (build) => ({
me: build.query<IUser, void>({
query: () => ({
url: '/me',
method: 'GET',
}),
providesTags: ['Auth'],
}),
logout: build.mutation<void, void>({
query: () => ({
url: '/logout',
method: 'POST',
}),
invalidatesTags: ['Auth'],
}),
}),
});
// Export hooks for usage in functional components
export const {
useMeQuery,
useLogoutMutation,
util: { getRunningOperationPromises },
} = authApi;
// export endpoints for use in SSR
export const { me, logout } = authApi.endpoints;
NEXT.JS auth.slice.ts
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';
import { RootState } from '..';
import { IUser } from './auth.interface';
export interface IAuthState {
user: IUser | null;
}
const initialState: IAuthState = {
user: null,
};
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setCredentials: (
state,
action: PayloadAction<{
user: IUser | null;
}>
) => {
const { user } = action.payload;
state.user = user;
},
destroyCredentials: (state) => {
state.user = null;
},
},
extraReducers: {
[HYDRATE]: (state, action) => {
return {
...state,
...action.payload.auth,
};
},
},
});
export const { setCredentials, destroyCredentials } = authSlice.actions;
export const selectCurrentUser = (state: RootState) => state.auth.user;
export default authSlice.reducer;
NEXT.JS store/index.ts:
import {
configureStore,
ImmutableStateInvariantMiddlewareOptions,
SerializableStateInvariantMiddlewareOptions,
ThunkAction,
} from '#reduxjs/toolkit';
import { Action, combineReducers } from 'redux';
import { Context, createWrapper } from 'next-redux-wrapper';
import botApi from './bots/bot.api';
import authReducer from './auth/auth.slice';
import { authApi } from './auth/auth.api';
// ThunkOptions not exported in getDefaultMiddleware, so we have a copy here
interface MyThunkOptions<E> {
extraArgument: E;
}
// GetDefaultMiddlewareOptions in getDefaultMiddleware does not allow
// providing type for ThunkOptions, so here is our custom version
// https://redux-toolkit.js.org/api/getDefaultMiddleware#api-reference
interface MyDefaultMiddlewareOptions {
thunk?: boolean | MyThunkOptions<Context>;
immutableCheck?: boolean | ImmutableStateInvariantMiddlewareOptions;
serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions;
}
const rootReducer = combineReducers({
// Add the generated reducer as a specific top-level slice
[botApi.reducerPath]: botApi.reducer,
[authApi.reducerPath]: authApi.reducer,
auth: authReducer,
});
const makeStore = (wtf: any) => {
const ctx = wtf.ctx as Context;
return configureStore({
reducer: rootReducer,
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (gDM) =>
gDM<MyDefaultMiddlewareOptions>({
thunk: {
// https://github.com/reduxjs/redux-toolkit/issues/2228#issuecomment-1095409011
extraArgument: ctx,
},
}).concat(botApi.middleware, authApi.middleware),
devTools: process.env.NODE_ENV !== 'production',
});
};
export type RootState = ReturnType<AppStore['getState']>;
export type AppStore = ReturnType<typeof makeStore>;
export type AppDispatch = ReturnType<typeof makeStore>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action
>;
export const wrapper = createWrapper<AppStore>(makeStore);
I would be grateful to everyone for any help with a solution 🙂.

Solution to prefetch in a component on nextjs

I'm looking for a solution/module where I don't need to inject inital/fallback data for swr/react-query things from getServerSideProps. Like...
from
// fetcher.ts
export default fetcher = async (url: string) => {
return await fetch(url)
.then(res => res.json())
}
// getUserData.ts
export default function getUserData() {
return fetcher('/api')
}
// index.tsx
const Page = (props: {
// I know this typing doesn't work, only to deliver my intention
userData: Awaited<ReturnType<typeof getServerSideProps>>['props']
}) => {
const { data } = useSWR('/api', fetcher, {
fallbackData: props.userData,
})
// ...SSR with data...
}
export const getServerSideProps = async (ctx: ...) => {
const userData = await getUserData()
return {
props: {
userData,
},
}
}
to
// useUserData.ts
const fetcher = async (url: string) => {
return await fetch(url)
.then(res => res.json())
};
const url = '/api';
function useUserData() {
let fallbackData: Awaited<ReturnType<typeof fetcher>>;
if (typeof window === 'undefined') {
fallbackData = await fetcher(url);
}
const data = useSWR(
url,
fetcher,
{
fallbackData: fallbackData!,
}
);
return data;
}
// index.tsx
const Page = () => {
const data = useUserData()
// ...SSR with data...
}
My goal is making things related to userData modularized into a component.

How to extract query from `getServerSideProps` to separate helper file?

I've got several pages that I want to call a query inside of getServerSideProps to request the currentUser.
What I have currently is something like this:
import { NextPageContext } from 'next';
import { withAuth } from 'hoc/withAuth';
import { addApolloState, initializeApollo } from 'lib/apolloClient';
import { MeDocument } from 'generated/types';
import nookies from 'nookies';
import Profile from 'components/Profile';
const ProfilePage: React.FC = () => <Profile />;
export const getServerSideProps = async (
context: NextPageContext
): Promise<any> => {
// withAuth(context);
const client = initializeApollo();
const { my_token } = nookies.get(context);
await client.query({
query: MeDocument,
context: {
headers: {
authorization: my_token ? `Bearer ${my_token}` : '',
},
},
});
return addApolloState(client, { props: {} });
};
export default ProfilePage;
This works, and I can verify in my Apollo devtools that the cache is being updated with the User.
When I try to move the Apollo initialization and query in to a separate file, the cache is never updated for some reason.
Inside of a withAuth.tsx file, I had something like this:
import { NextPageContext } from 'next';
import { addApolloState, initializeApollo } from 'lib/apolloClient';
import { MeDocument } from 'generated/types';
import nookies from 'nookies';
export const withAuth = async (context: any,) => {
const client = initializeApollo();
const { gc_token } = nookies.get(context);
await client.query({
query: MeDocument,
context: {
headers: {
authorization: gc_token ? `Bearer ${gc_token}` : '',
},
},
});
return addApolloState(client, { props: {} });
};
With this, all I have to do is call withAuth() in the getServerSideProps. There are no errors, however the cache doesn't update.
How can I extract that code to a separate file correctly?
Thanks to #juliomalves in the comments, I simply forgot to return the withAuth function!
Here's how it looks now:
pages/index.tsx
export const getServerSideProps = async (
context: NextPageContext
): Promise<any> => await withAuth(context);
withAuth.tsx
/* eslint-disable #typescript-eslint/explicit-module-boundary-types */
import { getSession } from 'next-auth/client';
import redirectToLogin from 'helpers/redirectToLogin';
import { addApolloState, initializeApollo } from 'lib/apolloClient';
import { MeDocument } from 'generated/types';
import nookies from 'nookies';
export const withAuth = async (context: any) => {
const session = await getSession(context);
const isUser = !!session?.user;
// no authenticated session
if (!isUser) redirectToLogin();
const client = initializeApollo();
const { token } = nookies.get(context);
await client.query({
query: MeDocument,
context: {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
},
});
return addApolloState(client, { props: {} });
};

Resources