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

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 🙂.

Related

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(),
})

TypeError: Cannot perform 'get' on a proxy that has been revoked, redux-toolkit and nextJS

I'm get the error in tittle when a action is dispatched to redux in a next application and i can't find the solution: the first action is correctly dispatched but others raises the error: TypeError: Cannot perform 'get' on a proxy that has been revoked, redux-toolkit and nextJS, you can see the project in the follow link : https://github.com/cpereiramt/BACKBONE-TEST
Below I share the mainly snippets of code and the configuration in general :
configuring store:
import {
configureStore,
EnhancedStore,
getDefaultMiddleware
} from "#reduxjs/toolkit"
import { MakeStore } from "next-redux-wrapper"
import { Env } from "../constants"
import { rootReducer, RootState } from "./reducers"
import { createWrapper } from 'next-redux-wrapper';
/**
* #see https://redux-toolkit.js.org/usage/usage-with-typescript#correct-typings-for-the-dispatch-type
*/
const middlewares = [...getDefaultMiddleware<RootState>()]
const store = configureStore({
reducer: rootReducer,
middleware: middlewares,
devTools: Env.NODE_ENV === "development",
})
const makeStore: MakeStore = (_?: RootState): EnhancedStore => store
export const wrapper = createWrapper(makeStore);
combineReducers
import { combineReducers } from "redux"
import { contactReducer } from "./contact"
/**
* Combine reducers
* #see https://redux-toolkit.js.org/usage/usage-with-typescript
*/
export const rootReducer = combineReducers({
contacts: contactReducer,
})
export type RootState = ReturnType<typeof rootReducer>
actions
import { createAsyncThunk } from "#reduxjs/toolkit";
import { Contact } from "../../model";
import { FeatureKey } from "../featureKey";
/**
* Fetch all contact action
*/
export const fetchAllContactsAction = createAsyncThunk(
`${FeatureKey.CONTACT}/fetchAll`,
async (arg: { offset: number; limit: number }) => {
const { offset, limit } = arg
const url = `/api/contact?offset=${offset}&limit=${limit}`
const result: Contact[] = await fetch(url, {
method: "get",
}).then((response: Response) => response.json())
return { contacts: result }
}
)
/**
* Fetch contact action
*/
export const fetchContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/fetch`,
async (arg: { id: number }) => {
const url = `/api/contact/${arg}`
const result: Contact = await fetch(url, {
method: "get",
}).then((response: Response) => response.json())
return { contacts: result }
}
)
/**
* Add contact action
*/
export const addContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/add`,
async (arg: { contact: Contact }) => {
const url = `/api/contact`
const result: Contact = await fetch(url, {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg),
}).then((response: Response) => response.json())
return { contacts: result }
}
)
/**
* Edit contact action
*/
export const editContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/edit`,
(arg: { contact: Contact }) => {
const { contact } = arg
const url = `/api/contact/${arg.id}`
const result: Contact = fetch(url, {
method: "put",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(contact),
}).then((response: Response) => response.json())
return { contacts: result }
}
)
/**
* Delete contact action
*/
export const deleteContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/delete`,
async (arg: { id: number }) => {
const url = `/api/contact/${arg}`
await fetch(url, {
method: "delete",
})
}
)
reducers
import { ActionReducerMapBuilder, createReducer } from "#reduxjs/toolkit"
import {
addContactAction,
deleteContactAction,
editContactAction,
fetchAllContactsAction,
fetchContactAction
} from "./action"
import { adapter, ContactState, initialState } from "./state"
/**
* CONTACT reducer
*/
export const contactReducer = createReducer(
initialState,
(builder: ActionReducerMapBuilder<ContactState>) =>
builder
.addCase(fetchAllContactsAction.pending, (state) => {
return { ...state, isFetching: true }
})
.addCase(fetchAllContactsAction.fulfilled, (state, action) => {
const { contacts } = action.payload
return adapter.setAll({ ...state, isFetching: false }, contacts)
})
.addCase(fetchAllContactsAction.rejected, (state) => {
return { ...state, isFetching: false }
})
//-------------------------------------------------------------------------------
.addCase(fetchContactAction.pending, (state, action) => {
const { id } = action.meta.arg
return { ...state, isFetching: true, selectedId: id }
})
.addCase(fetchContactAction.fulfilled, (state, action) => {
const { contacts } = action.payload
return adapter.upsertOne({ ...state, isFetching: false }, contacts)
})
.addCase(fetchContactAction.rejected, (state) => {
return { ...state, isFetching: false }
})
//-------------------------------------------------------------------------------
.addCase(addContactAction.pending, (state, action) => {
const { contact } = action.meta.arg
return { ...state, isFetching: true, selectedId: contact?.id }
})
.addCase(addContactAction.fulfilled, (state, action) => {
const { contacts } = action.payload
return adapter.addOne({ ...state, isFetching: false }, contacts)
})
.addCase(addContactAction.rejected, (state) => {
return { ...state, isFetching: false }
})
//-------------------------------------------------------------------------------
.addCase(editContactAction.pending, (state, action) => {
const { contact } = action.meta.arg
return { ...state, isFetching: true, selectedId: contact?.id }
})
.addCase(editContactAction.fulfilled, (state, action) => {
const { contacts } = action.payload
return adapter.updateOne(
{ ...state, isFetching: false },
{
id: contacts.id,
changes: contacts,
}
)
})
.addCase(editContactAction.rejected, (state) => {
return { ...state, isFetching: false }
})
//-------------------------------------------------------------------------------
.addCase(deleteContactAction.pending, (state, action) => {
const { id } = action.meta.arg
return { ...state, isFetching: true, selectedId: id }
})
.addCase(deleteContactAction.fulfilled, (state, action) => {
const { id } = action.meta.arg
return adapter.removeOne({ ...state, isFetching: false }, id)
})
.addCase(deleteContactAction.rejected, (state) => {
return { ...state, isFetching: false }
})
)
selectors
import { createSelector } from "#reduxjs/toolkit"
import { RootState } from "../reducers"
import { adapter, ContactState } from "./state"
const { selectAll, selectEntities } = adapter.getSelectors()
const featureStateSelector = (state: RootState) => state.contacts
const entitiesSelector = createSelector(featureStateSelector, selectEntities)
/**
* isFetching selector
*/
export const isFetchingSelector = createSelector(
featureStateSelector,
(state: ContactState) => state?.isFetching
)
/**
* selectedId selector
*/
export const selectedIdSelector = createSelector(
featureStateSelector,
(state: ContactState) => state?.selectedId
)
/**
* all contact selector
*/
export const allContactSelector = createSelector(featureStateSelector, selectAll)
/**
* contact selector
*/
export const contactSelector = createSelector(
entitiesSelector,
selectedIdSelector,
(entities, id) => (id ? entities[id] || null : null)
)
states
import { createEntityAdapter, EntityState } from "#reduxjs/toolkit"
import { Contact } from "../../model"
export interface ContactState extends EntityState<Contact> {
isFetching: boolean
selectedId: number | null
}
export const adapter = createEntityAdapter<Contact>({
selectId: (contacts: Contact) => contacts.id,
})
export const initialState: ContactState = adapter.getInitialState({
isFetching: false,
selectedId: null,
})
And the _app file
import CssBaseline from "#material-ui/core/CssBaseline";
import { ThemeProvider } from "#material-ui/styles";
import { NextPageContext } from 'next';
import App from "next/app";
import React from "react";
import { MuiTheme } from "../components/MuiTheme";
import { Store } from '../redux/store';
import { wrapper } from "../store/configureStore";
import "../styles/main.css";
interface AppContext extends NextPageContext {
store: Store;
}
class MyApp extends App<AppContext> {
componentDidMount() {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector("#jss-server-side")
jssStyles?.parentNode?.removeChild(jssStyles)
}
render() {
const { Component, ...props } = this.props;
return (
<ThemeProvider theme={MuiTheme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Component {...props} />
</ThemeProvider>
)
}
}
export default wrapper.withRedux(MyApp);
And in index file the action fetchAllContacts() work without problems.
import React, { useEffect } from "react";
import ContactTable from "../components/Table/";
import { useContact } from "../hooks";
import { Contact } from '../model/Contact';
import appStyles from "./indexStyles";
type Props = {}
function Index(props: Props) {
const { fetchAllContacts } = useContact();
const [contacts, setContacts] = React.useState<Contact[]>([])
useEffect(() => {
const results = fetchAllContacts();
results.then(data => console.log(data));
results.then(data => setContacts(data.contacts));
}, [])
const classes = appStyles(props)
return (
<div className={classes.indexBackground}>
<div className={classes.indexTabletDiv}>
<ContactTable contacts={contacts} />
</div>
</div>
);
}
export default Index
But when I try to use the action addContact() in another component the error is raised
page add
import { Button, createStyles, InputLabel, makeStyles, TextField, Theme } from "#material-ui/core";
import router from "next/router";
import React from "react";
import { useContact } from "../../hooks";
const useStyles = makeStyles((_: Theme) =>
createStyles({
root: {},
})
)
type Props = { }
const AddContact = (props: Props) => {
const { addContact } = useContact();
const newContact = {
id:'455666gghghttttytyty',
firstName: 'clayton',
lastName: 'pereira',
email: 'cpereiramt#gmail.com',
phone: '5565992188269',
}
const handleCreateContact = () => {
addContact(newContact);
}
const { } = props
return (
<div style={{margin: '10px', display: 'flex', justifyContent: 'space-between', wrap: 'wrap', flexDirection:'column'}}>
<>
<InputLabel>Name </InputLabel><TextField />
<InputLabel>Last Name </InputLabel><TextField />
<InputLabel>Email </InputLabel><TextField />
<div>
<Button variant="outlined" color="primary" onClick={() => handleCreateContact(newContact)} >
Create Contact</Button>
<Button variant="outlined" color="primary" onClick={() => router.push('/')} >
Back</Button>
</div>
</>
</div>
)
}
export default AddContact;
I think I see the issue.
First, the actual error message is Immer telling you that something is trying to modify the Proxy-wrapped state in a reducer, but long after the reducer has actually finished running. Normally that's impossible, because reducers are synchronous. So, there has to be some kind of async behavior going on.
The case reducers themselves seem basically okay, and mostly look like this:
.addCase(fetchAllContactsAction.pending, (state) => {
return { ...state, isFetching: true }
})
I'll point out that Immer lets you write state.isFetching = true instead, so you don't have to do object spreads :) But this code should run fine, and it's synchronous. So, what's the problem?
You didn't actually describe which actions are causing errors, so I'm having to guess. But, I think it's in one of the async thunks, and specifically, here:
export const editContactAction = createAsyncThunk(
`${FeatureKey.CONTACT}/edit`,
(arg: { contact: Contact }) => {
const { contact } = arg
const url = `/api/contact/${arg.id}`
// PROBLEM 1
const result: Contact = fetch(url, {
method: "put",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(contact),
}).then((response: Response) => response.json())
// PROBLEM 2
return { contacts: result }
}
Notice the line const result: Contact = fetch(). This doesn't have any await in there So, this is going to end up returning a Promise and saving it as result, and then that Promise ends up being returned as the contacts field.
I think that the object with the promise is being put into Redux state, wrapped by an Immer proxy, and then modified sometime later, and that's what's causing the error. But I'm not 100% sure because I don't know which actions are actually involved.

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: {} });
};

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

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() },
}
}

How to Make http Request to My Project API from Firebase Cloud function?

Hello I am trying to make an API Post request using Firebase cloud function,Here are the code.
My effort is to get details from cloud and make an http request to my project's API. But i am getting an error of can not find module!!i have already put it.
so how to make an api call??
Here is my index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import {TenantServiceProxy, CreateTenantInput} from '../../src/app/cloud/cloud-service';
let _tenantService: TenantServiceProxy;
const tenant = new CreateTenantInput();
admin.initializeApp();
export const onOrganizationUpdate =
functions.firestore.document('organizations/{id}').onUpdate(change => {
const after = change.after.data()
const payload = {
data: {
OrganizationId: String(after.OrganizationId),
Name: String(after.Name),
IsDeleted: String(after.IsDeleted)
}
}
console.log("updated", payload);
https.get('https://reqres.in/api/users?page=2', (resp: any) => {
let data = '';
// A chunk of data has been recieved.
resp.on('data', (chunk: any) => {
data += chunk;
});
// The whole response has been received. Print out the result.
resp.on('end', () => {
console.log("-------------------->",JSON.parse(data));
});
}).on("error", (err: any) => {
console.log("Error: " + err.message);
});
return admin.messaging().sendToTopic("OrganizationId", payload)
})
export const onOrganizationCreate =
functions.firestore.document('organizations/{id}').onCreate(change=>{
const onCreationTime =change.data()
const payload={
data:{
organizationId:String(onCreationTime.organizationId),
name:String(onCreationTime.name),
isDeleted:String(onCreationTime.isDeleted)
},
}
console.log("created",payload);
tenant.pkOrganization = payload.data.organizationId;
tenant.name = payload.data.name;
tenant.isDeleted = Boolean(payload.data.isDeleted);
_tenantService.createTenant(tenant).subscribe(()=>{
console.log("created",payload);
});
return admin.messaging().sendToTopic("OrganizationId",payload)
})
Here is the cloud.service.module.TS
//cloud service module
import { AbpHttpInterceptor } from '#abp/abpHttpInterceptor';
import { HTTP_INTERCEPTORS } from '#angular/common/http';
import { NgModule } from '#angular/core';
import * as ApiServiceProxies from '../../app/cloud/cloud-service';
#NgModule({
providers: [
ApiServiceProxies.TenantServiceProxy,
{ provide: HTTP_INTERCEPTORS, useClass: AbpHttpInterceptor, multi: true }
]
})
export class CloudServiceModule { }
Here is My api call service
#Injectable()
export class TenantServiceProxy {
private http: HttpClient;
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(#Inject(HttpClient) http: HttpClient, #Optional() #Inject(API_BASE_URL) baseUrl?: string) {
this.http = http;
this.baseUrl = baseUrl ? baseUrl : '';
}
createTenant(input: CreateTenantInput | null | undefined): Observable<void> {
let url_ = this.baseUrl + '/api/services/app/Tenant/CreateTenant';
url_ = url_.replace(/[?&]$/, '');
const content_ = JSON.stringify(input);
const options_: any = {
body: content_,
observe: 'response',
responseType: 'blob',
headers: new HttpHeaders({
'Content-Type': 'application/json',
})
};
return this.http.request('post', url_, options_).pipe(_observableMergeMap((response_: any) => {
return this.processCreateTenant(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processCreateTenant(<any>response_);
} catch (e) {
return <Observable<void>><any>_observableThrow(e);
}
} else {
return <Observable<void>><any>_observableThrow(response_);
}
}));
}
protected processCreateTenant(response: HttpResponseBase): Observable<void> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
const _headers: any = {}; if (response.headers) { for (const key of response.headers.keys()) { _headers[key] = response.headers.get(key); } }
if (status === 200) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return _observableOf<void>(<any>null);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException('An unexpected server error occurred.', status, _responseText, _headers);
}));
}
return _observableOf<void>(<any>null);
}
}
I have defined the module in my services.

Resources