I'm trying to make a smart request in nuxt with nuxt-apollo-module in order to grab my routes for the nuxt-sitemaps-module (so I can create my sitemap with them).
I need to make this request from within nuxt.config.js file. I have tried this way with no luck (as app doesn't exist in this context). What would be the right way to do this?
Thanks in advance!
The relevant part of my nuxt.config.js
import gql from 'graphql-tag'
module.exports = {
modules: [
'#nuxtjs/apollo',
'#nuxtjs/sitemap'
],
apollo: {
clientConfigs: {
default: {
httpEndpoint: 'https://example.com/graphql'
}
}
},
sitemap: {
path: '/sitemap.xml',
hostname: 'https://example.com/',
generate: true,
cacheTime: 86400,
trailingSlash: true,
routes: async ({ app }) => {
const myRoutes = ['/one-random-path/']
let client = app.apolloProvider.defaultClient
let myProductsQuery = gql`query {
products {
slug
}
}`
let myBrandsQuery = gql`query {
brands {
slug
}
}`
const myProducts = await client.query({ query: myProductsQuery })
const myBrands = await client.query({ query: myBrandsQuery })
return [myRoutes, ...myProducts, ...myBrands]
}
}
}
I was able to generate it this way, but I'm sure there is a better way.
yarn add node-fetch apollo-boost
sitemap: {
routes: async () => {
const routes = []
const fetch = require("node-fetch")
const { gql } = require("apollo-boost")
const ApolloBoost = require("apollo-boost")
const ApolloClient = ApolloBoost.default
const client = new ApolloClient({
fetch: fetch,
uri: YOUR_API_ENDPOINT,
})
const fetchUsers = gql`
query {
users {
id
}
}
`
const users = await client
.query({
query: fetchUsers,
})
.then((res) => res.data.users)
users.forEach((user) => {
routes.push({
route: `/${user.id}`,
})
})
return routes
},
},
I gave up using Apollo. It was easier to use Axios. Moreover, no nneds to configure #nuxtjs/sitemap :
import axios from 'axios'
sitemap: {
hostname: URL_SITE,
gzip: true,
},
routes() {
return axios({
url: ENDPOINT,
method: 'post',
data: {
query: `
query GET_POSTS {
posts {
nodes {
slug
}
}
}
`,
},
}).then((result) => {
return result.data.data.posts.nodes.map((post) => {
return '/blog/' + post.slug
})
})
}
This was how I was able to do it with authentication. Got round to it by following the documentation here which had a Vue example: https://www.apollographql.com/docs/react/networking/authentication/
Might be a cleaner way to do it with apollo-boost if you can also use auth?
import fetch from 'node-fetch'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { ApolloLink, concat } from 'apollo-link'
import { InMemoryCache } from 'apollo-cache-inmemory'
import globalQuery from './apollo/queries/global.js'
const httpLink = new HttpLink({ uri: process.env.SCHEMA_URL, fetch })
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = process.env.STRAPI_API_TOKEN
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : 'Bearer',
},
})
return forward(operation)
})
export const apolloClient = new ApolloClient({
link: concat(authMiddleware, httpLink),
cache: new InMemoryCache(),
})
export default async () => {
let global = null
const globalResponse = await apolloClient.query({ query: globalQuery })
if (globalResponse?.data?.global?.data) {
global = globalResponse.data.global.data.attributes
}
console.log('globals:::', global)
}
Related
I have the same issue than this post: Why does vitest mock not catch my axios get-requests?
I would like to test my vuex store on vuejs and it works for getters etc but not for actions part with axios get request.
I don't know if it's a good practice to test vuex store than the component in Vue ?
But I guess I need to test both, right ?
a project https://stackblitz.com/edit/vitest-dev-vitest-nyks4u?file=test%2Ftag.spec.js
my js file to test tag.js
import axios from "axios";
const actions = {
async fetchTags({ commit }) {
try {
const response = await axios.get(
CONST_CONFIG.VUE_APP_URLAPI + "tag?OrderBy=id&Skip=0&Take=100"
);
commit("setTags", response.data);
} catch (error) {
console.log(error);
return;
}
},
};
export default {
state,
getters,
actions,
mutations,
};
then my test (tag.spec.js)
import { expect } from "chai";
import { vi } from "vitest";
import axios from "axios";
vi.mock("axios", () => {
return {
default: {
get: vi.fn(),
},
};
});
describe("tag", () => {
test("actions - fetchTags", async () => {
const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Andrew" },
];
axios.get.mockImplementation(() => Promise.resolve({ data: users }));
axios.get.mockResolvedValueOnce(users);
const commit = vi.fn();
await tag.actions.fetchTags({ commit });
expect(axios.get).toHaveBeenCalledTimes(1);
expect(commit).toHaveBeenCalledTimes(1);
});
});
It looks like some other peolpe have the same issues https://github.com/vitest-dev/vitest/issues/1274 but it's still not working.
I try with .ts too but I have exactly the same mistake:
FAIL tests/unit/store/apiObject/tag.spec.js > tag > actions - fetchTags
AssertionError: expected "spy" to be called 1 times
❯ tests/unit/store/apiObject/tag.spec.js:64:24
62| await tag.actions.fetchTags({ commit });
63|
64| expect(axios.get).toHaveBeenCalledTimes(1);
| ^
65| expect(commit).toHaveBeenCalledTimes(1);
66| });
Expected "1"
Received "0"
Thanks a lot for your help.
I finally found the mistake, it was on my vitest.config.ts file, I have to add my global config varaible for my api: import { config } from "#vue/test-utils";
import { defineConfig } from "vitest/config";
import { resolve } from "path";
var configApi = require("./public/config.js");
const { createVuePlugin } = require("vite-plugin-vue2");
const r = (p: string) => resolve(__dirname, p);
export default defineConfig({
test: {
globals: true,
environment: "jsdom",
},
define: {
CONST_CONFIG: configApi,
},
plugins: [createVuePlugin()],
resolve: {
alias: {
"#": r("."),
"~": r("."),
},
// alias: {
// "#": fileURLToPath(new URL("./src", import.meta.url)),
// },
},
});
While trying to fetch all images from the Galleries template I am receiving the following error/warn notification: "Property "token" was accessed during render but is not defined on instance". instead of displaying the images I receive the following: <img src="function link() { [native code] }"> What am I missing?
Galleries.vue
<template>
<div>
<img v-for="image in allImages" :src="image.link" :key="image.id" />
</div>
</template>
<script>
import { mapActions, mapGetters } from "vuex";
export default {
name: "Galleries",
computed: mapGetters(["allImages"]),
methods: mapActions(["fetchImages"]),
created() {
this.fetchImages();
}
};
</script>
imgur.js
import qs from 'qs';
import axios from 'axios';
const CLIENT_ID = 'e28971925a8d43c';
const ROOT_URL = 'https://api.imgur.com';
export default {
login() {
const querystring = {
client_id: CLIENT_ID,
response_type: 'token',
};
window.location = `${ROOT_URL}/oauth2/authorize?${qs.stringify(querystring)}`
},
fetchImages(token) {
return axios.get(`${ROOT_URL}/3/account/me/image/`, {
headers: {
Authorization: `Bearer ${token}`
}
});
},
uploadImages(images, token) {
const promises = Array.from(images).map(image => {
const formData = new FormData();
formData.append('image', image);
return axios.post(`${ROOT_URL}/3/image`, formData, {
headers: {
Authorization: `Bearer ${token}`
}
});
});
return Promise.all(promises);
}
};
images.js
import api from '../../api/imgur';
import { router } from '../../main';
const state = {
images: []
};
const getters = {
allImages: state => state.images
};
const mutations = {
setImages: (state, images) => {
state.images = images;
}
};
const actions = {
async fetchImages({ rootState, commit }) {
const { token } = rootState.auth;
const response = await api.fetchImages(token);
commit('setImages', response.data.data);
},
async uploadImages({ rootState }, images) {
// Get the access token
const { token } = rootState.auth;
// Call our API module to do the upload
await api.uploadImages(images, token);
// Redirect use to the gallery page
router.push('/galleries');
}
};
export default {
state,
getters,
mutations,
actions
}
auth.js
import api from '../../api/imgur';
import qs from 'qs';
import { router } from '../../main';
const state = {
token: window.localStorage.getItem('imgur_token')
};
const getters = {
isLoggedIn: state => !!state.token // turn a value into boolean
};
const actions = {
login: () => {
api.login();
},
finalizeLogin({ commit }, hash) {
const query = qs.parse(hash.replace('#', ''));
commit('setToken', query.access_token);
window.localStorage.setItem('imgur_token', query.access_token);
router.push('/');
},
logout: ({ commit }) => {
commit('setToken', null);
window.localStorage.removeItem('imgur_token');
router.push('/');
}
};
const mutations = {
setToken: (state, token) => {
state.token = token;
}
};
export default {
state,
getters,
actions,
mutations
};
// Galaries.vue
// You didn't pass token when calling function `fetchImages`.
created() {
this.fetchImages(); // missing token here
}
I recommend use token as environment variable for security reason. Never public your token. Should NOT pass it as an argument of a function. Store your token in a .env file. You can rewrite your fetchImages as below.
fetchImages(token) {
return axios.get(`${ROOT_URL}/3/account/me/image/`, {
headers: {
Authorization: `Bearer ${process.env.TOKEN}`
}
});
},
I am using Next.js (Frontend) and Nest.js (Backend) Frameworks and I am having issues with a HTTP Cookies. The following code works in development trough localhost, but not when publishing the code to vercel (Next.js) and Heroku (Nest.js)
Here is a snippet of my Nest.js Setup
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new LoggerService(),
cors: true,
});
app.use(cookieParser());
app.enableCors({
origin: ["<my-site>"],
credentials: true,
});
...
await app.listen(process.env.PORT || 5000);
}
bootstrap();
Here is my Route:
#Post("login/verify")
#HttpCode(201)
#ApiResponse({ description: "returns JWT for verified login attempt" })
async verify(
#Body() method: VerificationDto,
#Req() req: Request,
#Res({ passthrough: true }) res: Response
) {
const { accessToken } = await this.userService.verifyLogin(method);
const origin = req.get("origin");
this.loggerService.log(`origin: ${origin}`);
res
.set("Access-Control-Allow-Credentials", "true")
.set("Access-Control-Allow-Origin", origin)
.cookie("accessToken", accessToken, {
expires: new Date(new Date().getTime() + 60 * 1000 * 60 * 2),
sameSite: "none",
secure: true,
httpOnly: false,
});
return {
accessToken,
refreshToken,
};
}
Here is my useAuth component in Next.js
import React from "react";
import jwtDecode from "jwt-decode";
import nookies from "nookies";
import { redirect } from "../../services/redirect";
import { GetServerSideProps, NextPage } from "next";
export interface User {
accessToken: string;
email: string;
exp: number;
iat: number;
id: string;
name: string;
roles: Array<string>;
}
const AuthContext = React.createContext<User>(null as unknown as User);
const loginRoute = `/login`;
export const authenticate = (
getServerSidePropsInner: GetServerSideProps = async () => ({ props: {} })
) => {
const getServerSideProps: GetServerSideProps = async ctx => {
const { req, res } = ctx;
if (!req.headers.cookie) {
console.log("no cookie found");
redirect(ctx, loginRoute);
return { props: {} };
}
const { accessToken } = nookies.get(ctx);
console.log(accessToken);
let user = null;
try {
user = {
accessToken,
...(jwtDecode(accessToken as string) as object),
};
} catch (e) {
console.log(e);
redirect(ctx, loginRoute);
}
const result = await getServerSidePropsInner(ctx);
return {
...result,
props: {
user,
//#ts-ignore
...result.props,
},
};
};
return getServerSideProps;
};
export const withAuth = (C: NextPage) => {
const WithAuth = (props: any) => {
const { user, ...appProps } = props;
return (
<AuthContext.Provider value={user}>
<C {...appProps} />
</AuthContext.Provider>
);
};
WithAuth.displayName = `WithAuth(${C.displayName})`;
return WithAuth;
};
export default withAuth;
export const useAuth = (): User => React.useContext(AuthContext);
I am using axios with the following login request
verifyLogin: ({ code, email }: { code: string; email: string }) => {
return axios(`${apiURI}/user/login/verify`, {
method: `POST`,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
data: {
code: code,
email: email,
},
});
},
Setup works with localhost. It adds a http cookie which is read by the next.js application.
However, when deploying my Nest.js app to Heroku, the cookie does not seem to send to the next application. I have tried adding different values for the CookieOptions
sameSite: "none",
secure: true,
httpOnly: false,
However, this did not help either.
Any help is much appreciated as I am fighting with this for days.
I cannot correctly set my jwt token from my cookie to my Headers for an authenticaed gql request using apollo client.
I believe the problem is on my withApollo.js file, the one that wraps the App component on _app.js. The format of this file is based off of the wes bos advanced react nextjs graphql course. What happens is that nextauth saves the JWT as a cookie, and I can then grab the JWT from that cookie using a custom regex function. Then I try to set this token value to the authorization bearer header. The problem is that on the first load of a page with a gql query needing a jwt token, I get the error "Cannot read property 'cookie' of undefined". But, if I hit browser refresh, then suddenly it works and the token was successfully set to the header.
Some research led me to adding a setcontext link and so that's where I try to perform this operation. I tried to async await setting the token value but that doesn't seem to have helped. It just seems like the headers don't want to get set until on the refresh.
lib/withData.js
import { ApolloClient, ApolloLink, InMemoryCache } from '#apollo/client';
import { onError } from '#apollo/link-error';
import { getDataFromTree } from '#apollo/react-ssr';
import { createUploadLink } from 'apollo-upload-client';
import withApollo from 'next-with-apollo';
import { setContext } from 'apollo-link-context';
import { endpoint, prodEndpoint } from '../config';
import paginationField from './paginationField';
const getCookieValue = (name, cookie) =>
cookie.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`)?.pop() || '';
let token;
function createClient(props) {
const { initialState, headers, ctx } = props;
console.log({ headers });
// console.log({ ctx });
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError)
console.log(
`[Network error]: ${networkError}. Backend is unreachable. Is it running?`
);
}),
setContext(async (request, previousContext) => {
token = await getCookieValue('token', headers.cookie);
return {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
};
}),
createUploadLink({
uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
fetchOptions: {
credentials: 'include',
},
headers,
}),
]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
// TODO: We will add this together!
// allProducts: paginationField(),
},
},
},
}).restore(initialState || {}),
});
}
export default withApollo(createClient, { getDataFromTree });
page/_app.js
import { ApolloProvider } from '#apollo/client';
import NProgress from 'nprogress';
import Router from 'next/router';
import { Provider, getSession } from 'next-auth/client';
import { CookiesProvider } from 'react-cookie';
import nookies, { parseCookies } from 'nookies';
import Page from '../components/Page';
import '../components/styles/nprogress.css';
import withData from '../lib/withData';
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
function MyApp({ Component, pageProps, apollo, user }) {
return (
<Provider session={pageProps.session}>
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} {...user} />
</Page>
</ApolloProvider>
</Provider>
);
}
MyApp.getInitialProps = async function ({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
pageProps.query = ctx.query;
const user = {};
const { req } = ctx;
const session = await getSession({ req });
if (session) {
user.email = session.user.email;
user.id = session.user.id;
user.isUser = !!session;
// Set
nookies.set(ctx, 'token', session.accessToken, {
maxAge: 30 * 24 * 60 * 60,
path: '/',
});
}
return {
pageProps,
user: user || null,
};
};
export default withData(MyApp);
api/auth/[...nextAuth.js]
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from 'axios';
const providers = [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await axios
.post('http://localhost:1337/auth/local', {
identifier: credentials.username,
password: credentials.password,
})
.then((res) => {
res.data.user.token = res.data.jwt;
return res.data.user;
}) // define user as res.data.user (will be referenced in callbacks)
.catch((error) => {
console.log('An error occurred:', error);
});
if (user) {
return user;
}
return null;
},
}),
];
const callbacks = {
// Getting the JWT token from API response
async jwt(token, user, account, profile, isNewUser) {
// WRITE TO TOKEN (from above sources)
if (user) {
const provider = account.provider || user.provider || null;
let response;
let data;
switch (provider) {
case 'google':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/google/callback?access_token=${account?.accessToken}`
);
data = await response.json();
if (data) {
token.accessToken = data.jwt;
token.id = data.user._id;
} else {
console.log('ERROR No data');
}
break;
case 'local':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/local/callback?access_token=${account?.accessToken}`
);
data = await response.json();
token.accessToken = user.token;
token.id = user.id;
break;
default:
console.log(`ERROR: Provider value is ${provider}`);
break;
}
}
return token;
},
async session(session, token) {
// WRITE TO SESSION (from token)
// console.log(token);
session.accessToken = token.accessToken;
session.user.id = token.id;
return session;
},
redirect: async (url, baseUrl) => baseUrl,
};
const sessionPreferences = {
session: {
jwt: true,
},
};
const options = {
providers,
callbacks,
sessionPreferences,
};
export default (req, res) => NextAuth(req, res, options);
I am using GraphQl APIs in the react-native chat application. I want to get real-time updates when another user sends a message to me without refreshing the API. How can I do it using GraphQl API using GraphQl subscriptions or Websocket in react-native?
Should I use different URLs for subscription and normal API's?
Here is my config.js
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { HttpLink } from 'apollo-boost';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { AsyncStorage } from 'react-native';
// const httpLink = createHttpLink({
// uri: 'https://graphql.chat.dev.com/graphql',
// });
// const link = new HttpLink({
// uri: `https://graphql.chat.dev.com/graphql`,
// headers: {
// Authorization: AsyncStorage.getItem('#user_token');
// }
// });
const link = new WebSocketLink({
uri: `wss://graphql.chat.dev.com/graphql`,
options: {
reconnect: true,
connectionParams: {
headers: {
Authorization: AsyncStorage.getItem('#user_token');
}
}
}
})
const defaultOptions = {
query: {
fetchPolicy: "network-only",
errorPolicy: "all"
}
};
const client = new ApolloClient({
link: link,
cache: new InMemoryCache(),
defaultOptions
});
export default client;
I've not implemented Apollo with React Native but I did it with my React app. In my experience, you should use different URLs for subscription and normal APIs. Then, use import { split } from 'apollo-link' to split links, so you can send data to each link
depending on what kind of operation is being sent. You can read more about subscription in Apollo here.
This is my client.js file. Hopefully, it can help you.
import { ApolloClient } from 'apollo-client'
import { createUploadLink } from 'apollo-upload-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { setContext } from 'apollo-link-context'
import { split } from 'apollo-link'
import { WebSocketLink } from 'apollo-link-ws'
import { getMainDefinition } from 'apollo-utilities'
const getToken = () => localStorage.getItem('AUTH_TOKEN')
const APOLLO_SERVER ="APOLLO_SERVER url"
const APOLLO_SOCKET ="APOLLO_SOCKET url"
// Create an http link:
const httpLink = createUploadLink({
uri: APOLLO_SERVER,
credentials: 'same-origin',
})
const authLink = setContext((_, { headers }) => {
const token = getToken()
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
}
})
// Create a WebSocket link:
const wsLink = new WebSocketLink({
uri: APOLLO_SOCKET,
options: {
reconnect: true,
connectionParams: {
Authorization: getToken() ? `Bearer ${getToken()}` : '',
},
},
})
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
authLink.concat(httpLink)
)
const cache = new InMemoryCache()
const client = new ApolloClient({
cache,
link,
typeDefs,
resolvers,
})
This is my component where I integrate queries with subscriptions:
import React, { useEffect } from 'react'
import { useQuery } from '#apollo/react-hooks'
import gql from 'graphql-tag'
...
// query for querying message list
const GET_MESSAGE_LIST = gql`...`
// subscription for listening new message
const ON_MESSAGE_CREATED = gql`...`
const ChatView = props => {
const { data, loading, subscribeToMore } = useQuery(GET_MESSAGE_LIST, {
{
notifyOnNetworkStatusChange: true,
variables: {
query: {
limit: 10,
userId: props.userId,
},
},
}
})
useEffect(() => {
subscribeToMore({
document: ON_MESSAGE_CREATED,
variables: { filter: { userId: props.userId } },
shouldResubscribe: true,
updateQuery: (prev, { subscriptionData }) => {
let newMessage = subscriptionData.data.onZaloMessageCreated
return Object.assign({}, prev, {
messageList: {
...prev.messageList,
items:
prev.messageList.items.filter(
item => item.id === newMessage.id
).length === 0
? [newMessage, ...prev.messageList.items]
: prev.messageList.items,
},
})
},
})
}, [subscribeToMore])
return ...
}