Strange issue with useQuery: Query arguments not being read - next.js

I have a component that passes a string (userToFetch) it as a variable parameter in a parameterized query. The component looks like this:
// pages/index.jsx
import React from 'react';
import { useQuery } from '#apollo/react-hooks';
import gql from 'graphql-tag';
const GET_USERS = gql`
query users ($limit: Int!, $username: String!) {
users (limit: $limit, where: { username: $username }) {
username
firstName
}
}
`;
const Home = () => {
const userToFetch = 'jonsnow';
const {
loading,
error,
data,
} = useQuery(
GET_USERS,
{
variables: { limit: 2, username: userToFetch },
notifyOnNetworkStatusChange: true,
},
);
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {JSON.stringify(error)}</p>;
}
return (
<div>
<ul>
{data.users.map(user => {
return <li>{user.username} {user.firstName}</li>;
})}
</ul>
</div>
);
};
export default Home;
And this is how I have configured my Apollo client:
// /apollo-client.js
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import withApollo from 'next-with-apollo';
import { createHttpLink } from 'apollo-link-http';
import fetch from 'isomorphic-unfetch';
const GRAPHQL_URL = 'https://dev.schandillia.com/graphql';
const link = createHttpLink({
fetch, // Switches between unfetch & node-fetch for client & server.
uri: GRAPHQL_URL
});
// Export a HOC from next-with-apollo
// Docs: https://www.npmjs.com/package/next-with-apollo
export default withApollo(
// You can get headers and ctx (context) from the callback params
// e.g. ({ headers, ctx, initialState })
({ initialState, ctx }) => {
console.log('initialState', initialState);
console.log('ctx', ctx);
return new ApolloClient({
link: link,
cache: new InMemoryCache()
// rehydrate the cache using the initial data passed from the server:
.restore(initialState || {})
})
}
);
The database is a collection of following users:
"users": [
{
"username": "negger",
"firstName": "Arnold",
"lastName": "Schwarzenegger"
},
{
"username": "jonsnow",
"firstName": "Jon",
"lastName": "Snow"
},
{
"username": "tonystark",
"firstName": "Tony",
"lastName": "Stark"
}
]
}
Now, although this should work (it does when I run the query in my graphql playground at https://dev.schandillia.com/graphql), the code runs as if the where clause didn't exist! It just returns all results as if the query being run were:
users {
_id
username
firstName
}
In order to reproduce the issue, visit https://www.schandillia.com. The page ought to display a list with only one element consisting of a matching username-firstName value: jonsnow Jon but it returns two entries, negger Arnold and jonsnow Jon (respecing limit but completely ignoring where). Now, run the same query with jonsnow as a where parameter in https://dev.schandillia.com/graphql:
{
users(where: { username: "jonsnow" }) {
_id
username
firstName
}
}
And the results would be exactly as expected:
{
"data": {
"users": [
{
"_id": "5d9f261678a32159e61018fc",
"username": "jonsnow",
"firstName": "Jon",
}
]
}
}
What am I overlooking?
P.S.: The repo is up for reference at https://github.com/amitschandillia/proost/tree/master/apollo-nextjs.
UPDATE: In order to track down the root cause, I tried logging some values in apollo-client.js:
console.log('initialState', initialState);
Strangely, the output shows the right query, along with the variables being passed, but wrong results:
...
ROOT_QUERY.users({"limit":2,"where":{"username":"jonsnow"}}).0:
firstName: "Arnold"
username: "negger"
__typename: "UsersPermissionsUser"
...
UPDATE: Here's a screenshot of results in my Apollo Client Developer Tools:

The schema generated by Strapi gives the where attribute a Type JSON and hence you have to pass the entire where part in the query variable as JSON since the variables are not getting injected.
# Write your query or mutation here
query users($where: JSON) {
users(where: $where) {
username
firstName
}
}
And the variables would look like:
{"where": {"username": "jonsnow"}}

Related

Mocking session and userData with cypress

currently Iam trying to mock a session for website project with cypress, so the tests are not actually accessing database cause Iam using only 0Auth 3th party login (google and github). I already succesfuly mocked a session so main page shows as logged user. However when I click on my profile, it says: No profile (expected behavior when there is no UserInfo data).
This is my cypress code:
Command:
Cypress.Commands.add("login", () => {
cy.intercept("/api/auth/session", { fixture: "session.json" }).as("session");
cy.setCookie("next-auth.session-token", "testcookie");
Cypress.Cookies.preserveOnce("next-auth.session-token");
});
Fixture:
{
"user": {
"name": "test",
"email": "test#test.com",
"slug": "test0000",
"id": "strasnedlouhejstring",
"image": "https://avatars.githubusercontent.com/u/112759319?v=4"
},
"userInfo": {
"bio": "Lorem Testum",
"name": "test",
"email": "test#test.com",
"slug": "test0000",
"id": "strasnedlouhejstring",
"isActive": "true"
},
"expires": "3000-01-01T00:00:00.000Z",
"accessToken": "test"
}
First test (success):
describe("Cypress login", () => {
it("should provide a valid session", () => {
cy.login();
cy.visit("/");
cy.wait("#session");
cy.get("#dropdown > div")
.should("exist")
.then(() => {
cy.log("Cypress login successful");
});
});
});
And finally page rendering:
import ProfilePage from "#components/Layout/ProfilePage";
import { User } from "#prisma/client";
import { getUserInfo } from "#scripts/prisma/getUserInfo";
import { GetServerSideProps } from "next";
import { useSession } from "next-auth/react";
const UserProfile = ({ userInfo }: { userInfo: User | null }) => {
const { data: session } = useSession();
if (userInfo) return <ProfilePage userInfo={userInfo} session={session} />;
return <h1>No profile</h1>;
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const slug = context.query.userProfile?.toString();
if (slug) {
const user = await getUserInfo(slug);
if (user) {
return {
props: { userInfo: JSON.parse(JSON.stringify(user)) },
};
}
}
return {
props: { userInfo: null },
};
};
export default UserProfile;
As I said, the condition is always false so i get the message: No Profile.
I spent 2days figuring this out, however not successfuly.

stitchSchema returns null

I have stitched two schemas together and run on localhost to query it. But the query returns null for the data in the second schema and I am not sure why.
I have the following code to stitch to remote schemas together and run a localhost graphql server to serve it. It should add the linked data from the second schema under cmsMetaData in the main Product data. But cmsMetaData is null.
import { ApolloServer } from 'apollo-server-micro';
import { ApolloServerPluginInlineTraceDisabled, ApolloServerPluginLandingPageLocalDefault } from "apollo-server-core";
import { stitchSchemas } from '#graphql-tools/stitch';
import { delegateToSchema } from '#graphql-tools/delegate';
import { RenameTypes, RenameRootFields } from '#graphql-tools/wrap';
import createRemoteSchema from '../../utils/createRemoteExecutor';
// Configuration for Next.js API Routes
export const config = {
api: {
bodyParser: false,
},
};
// Export as a Next.js API Route
export default async (req, res) => {
// Setup subschema configurations
const productsSubschema = await createRemoteSchema({
url: 'https://schema1.com/graphql/'
});
const cmsSubschema = await createRemoteSchema({
url: 'https://schema2.com/graphql/',
transforms: [
new RenameRootFields(
(operationName, fieldName, fieldConfig) => `strapi_${fieldName}`,
),
new RenameTypes((name) => `Strapi_${name}`),
],
});
// Build the combined schema and set up the extended schema and resolver
const schema = stitchSchemas({
subschemas: [productsSubschema, cmsSubschema],
typeDefs: `
extend type Product {
cmsMetaData: Strapi_Product
}
`,
resolvers: {
Product: {
cmsMetaData: {
selectionSet: `{ id }`,
resolve(product, args, context, info) {
// Get the data for the extended type from the subschema for Strapi
return delegateToSchema({
schema: cmsSubschema,
operation: 'query',
fieldName: 'strapi_product',
args: { where: { SaleorID: product.id } },
context,
info,
});
},
},
},
},
});
// Set up the GraphQL server
const apolloServer = new ApolloServer({
schema,
plugins: [
ApolloServerPluginInlineTraceDisabled(),
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
],
});
await apolloServer.start();
const apolloServerHandler = apolloServer.createHandler({
path: '/api/graphql',
});
// Return the GraphQL endpoint
return apolloServerHandler(req, res);
};
utils/createRemoteExecutor.js is:
import { introspectSchema, wrapSchema } from '#graphql-tools/wrap';
import { print } from 'graphql';
// Builds a remote schema executor function,
// customize any way that you need (auth, headers, etc).
// Expects to recieve an object with "document" and "variable" params,
// and asynchronously returns a JSON response from the remote.
export default async function createRemoteSchema({ url, ...filters }) {
const executor = async ({ document, variables }) => {
const query = print(document);
const fetchResult = await fetch(url, {
method: 'POST',
headers: {
// We can also do Authentication here
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
});
return fetchResult.json();
};
return wrapSchema({
schema: await introspectSchema(executor),
executor,
...filters,
});
}
The query is:
products(first: 100, channel: "default-channel")
{
edges
{
node
{
id
name
cmsMetaData
{
Title
SaleorID
}
}
}
}
In my api.tsx, which I generate using codegen.yaml, Product contains cmsMetaData as follows, which is of type Strapi_Product:
export type Product = Node & ObjectWithMetadata & {
__typename?: 'Product';
...
cmsMetaData?: Maybe<Array<Maybe<Strapi_Product>>>;
...
}
Strapi_Product is as follows which contains Title, SaleorID etc.:
export type Strapi_Product = {
__typename?: 'Strapi_Product';
SaleorID?: Maybe<Scalars['String']>;
Title?: Maybe<Scalars['String']>;
createdAt?: Maybe<Scalars['Strapi_DateTime']>;
publishedAt?: Maybe<Scalars['Strapi_DateTime']>;
updatedAt?: Maybe<Scalars['Strapi_DateTime']>;
};
But the date in GraphQL shows null for cmsMetaData as null:
{
"data": {
"products": {
"edges": [
{
"node": {
"id": "UHJvZHVjdDoxMjc=",
"name": "52-00 Base Plate",
"cmsMetaData": null
}
},
{
"node": {
"id": "UHJvZHVjdDoxMjg=",
"name": "52-01HD Weigh Module",
"cmsMetaData": null
}
}
]
}
}
}
Your problem seems related to this github issue. Most of your code looks totally fine, so I guess it fetches the schema correctly. The cmsMetaData field is null because it did not find anything matching objects using the selection criteria. This unwanted behavior is in the transformation and/or the resolver.
A good starting point for debugging would be to remove the RenameRootFields mutation. Furthermore, this example looks like your use case, it is an excellent step-by-step guide.
I also rebuild your example from an example I found on the internet. You most probably have a typo in one of the field names, that caused a null value for me. Make sure A equals B in the code below. I am guessing the initial fieldName is "Product", so after the transformation, this does not equal "strapi_product" and returns a null value.
const cmsSubschema = await createRemoteSchema({
url: 'https://schema2.com/graphql/',
transforms: [
new RenameRootFields(
(_, fieldName) => `strapi_${fieldName}`,), // A
new RenameTypes((name) => `Strapi_${name}`),
],
});
// Build the combined schema and set up the extended schema and resolver
const schema = stitchSchemas({
subschemas: [productsSubschema, cmsSubschema],
typeDefs: `
extend type Product {
cmsMetaData: Strapi_Product
}
`,
resolvers: {
Product: {
cmsMetaData: {
selectionSet: `{ id }`,
resolve(product, args, context, info) {
// Get the data for the extended type from the subschema for Strapi
return delegateToSchema({
schema: cmsSubschema,
operation: 'query',
fieldName: 'strapi_product', // B
args: { where: { SaleorID: product.id } },
context,
info,
});
},
},
},
},
});

My website deployed on Vercel keeps giving me the "429: TOO_MANY_REQUESTS" errors. What could be going wrong, how can I debug this?

Occassionally (maybe about half the time) when I load a page on the website I'm working on, I'm getting an error that looks like this.
429: TOO_MANY_REQUESTS
Code: INTERNAL_FUNCTION_RATE_LIMIT
ID: lhr1::258d8-1638206479250-0a01c8648601
My website hasn't been launched yet, almost nobody visits it but me, so it can't be having too much traffic yet.
The page I'm loading has a getServerSideProps() function that does only one thing - uses prisma to fetch posts from my database, which are sent to my component to be rendered.
I can't imagine what could be causing too many requests.
My vercel usage stats look like this.
What am I doing wrong? What could be causing this? How can I debug this?
For reference, below is all my relevant code. Any chance you could take a look at it and let me know if you have any ideas on what could be happening?
index.tsx has getServerSideProps() function which calls a getPosts() function to fetch the posts.
import Layout from 'components/Layout/Layout'
import PostFeed from 'components/Posts/PostFeed'
import Subnav from 'components/Layout/Subnav'
import Pagination from 'components/Posts/Pagination'
import ProfileHeader from 'components/Users/ProfileHeader'
import TagHeader from 'components/Layout/TagHeader'
import HomeHeader from 'components/CTAs/HomeHeader'
import SubscribeBox from 'components/CTAs/SubscribeBox'
import AdBoxes from 'components/CTAs/AdBoxes'
export default function browse({ posts, postCount, username }) {
return (
<Layout subnav={<Subnav />}>
<PostFeed posts={posts} />
<Pagination postCount={postCount} />
<AdBoxes/>
<SubscribeBox />
<br />
</Layout>
)
}
import { getPosts } from 'prisma/api/posts/get-posts'
import config from 'config.json'
export async function getServerSideProps({ req, query }) {
const { username, sort, tag, search } = query
const { posts, postCount } = await getPosts({
published: true,
searchString: search,
username: username,
tagSlug: tag,
sort: sort,
skip: config.postsPerPage * (parseInt(query.page?.toString()) - 1 || 0),
take: config.postsPerPage,
})
return { props: { posts, postCount, username } }
}
get-posts.ts runs a prisma query and fetches the posts.
import prisma from 'prisma/prismaClient'
export async function getPosts({ username, published, tagSlug, searchString, sort, skip, take }) {
console.log(`Get posts. Sorting: ${sort}`)
// Filter posts by user (to show them on their profile)
let author
if (username) author = await prisma.user.findUnique({ where: { username } })
// Filter by tag
const tagFilter = tagSlug ? {
tags: { some: { slug: tagSlug } }
} : {}
// Search through posts
const search = searchString ? {
OR: [
{ title: { contains: searchString, mode: "insensitive", } },
{ body: { contains: searchString, mode: "insensitive", } },
{ tags: { some: { name: { contains: searchString, mode: "insensitive", } } } },
{ author: { username: { contains: searchString, mode: "insensitive", } } },
],
} : {}
let orderBy = [{ rank: 'desc' }]
if (sort === 'new') orderBy = [{ createdAt: 'desc' }]
if (sort === 'top') orderBy = [{ score: 'desc' }]
const allFilters = {
authorId: author?.id,
published: published,
...search,
...tagFilter,
}
const [posts, postCount] = await prisma.$transaction([
prisma.post.findMany({
where: allFilters,
orderBy: orderBy, //rank: 'desc' //score: 'desc'
take, skip,
include: {
tags: true,
author: {
select: {
username: true
}
},
upvoters: {
select: {
username: true
}
},
// Just for the comment counter
comments: {
select: {
id: true
}
}
}
}),
prisma.post.count({ where: allFilters })
])
return { posts, postCount }
}
the prismaClient which get-posts is using to connect to prisma
import { PrismaClient } from "#prisma/client";
// PrismaClient is attached to the `global` object in development to prevent
// exhausting your database connection limit.
//
// Learn more:
// https://pris.ly/d/help/next-js-best-practices
let prisma: PrismaClient
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!global.prisma) {
global.prisma = new PrismaClient()
}
prisma = global.prisma
}
export default prisma
Try going towards getInitialProps which will execute your function on the browser vs getServerSideProps which always goes to your function creating loops as customers refresh your page or traverse through the site.
As to why so many requests, i think as clients traverse your site, you are generating hits to your function in a loop.

NextJS rewrites with wildcard query string params

I am having a bit of an issue: I am trying to setup a rewrite in NextJS that will automatically send on the query string to the destination. However, the only examples I can find are with named params. In this case there could be any number of params, so I need a way of making a wildcard (I assume this will be possible?)
What I am looking to do is the following:
/results?param1=a&param2=b... => https://www.somedomain.com/results?param1=a&param2=b...
or
/results?house=red&car=blue&money=none => https://www.somedomain.com/results??house=red&car=blue&money=none
rewrites() {
return [
{
source:'/results?:params*',
destination:'https://www.somedomain.com/results?:params*'
},
Of course this doesn't work, so I looked into has but I cannot workout how to make it work without names params
{
source: '/some-page',
destination: '/somewhere-else',
has: [{ type: 'query', key: 'overrideMe' }],
},
You can use a standard page like this below. What I did is when the router is initialized in a useEffect hook it gets the query (object) and then use the objectToQueryString function to turn it back into a url encoded string, then used the router to redirect to the other page
// /pages/some-page.jsx
import { useRouter } from 'next/router'
import { useEffect } from 'react'
const objectToQueryString = (initialObj) => {
const reducer = (obj, parentPrefix = null) => (prev, key) => {
const val = obj[key];
key = encodeURIComponent(key);
const prefix = parentPrefix ? `${parentPrefix}[${key}]` : key;
if (val == null || typeof val === 'function') {
prev.push(`${prefix}=`);
return prev;
}
if (['number', 'boolean', 'string'].includes(typeof val)) {
prev.push(`${prefix}=${encodeURIComponent(val)}`);
return prev;
}
prev.push(Object.keys(val).reduce(reducer(val, prefix), []).join('&'));
return prev;
};
return Object.keys(initialObj).reduce(reducer(initialObj), []).join('&');
};
const Results = () => {
const router = useRouter()
useEffect(() => {
const query = router.query
router.replace(`https://www.somedomain.com/results?${objectToQueryString(query)}`)
}, [router])
return <></>
}
export default Results
I used the objectToQueryString based on Query-string encoding of a Javascript Object
To achieve this behavior, you need to capture the value of the query-parameter like this:
rewrites() {
return [
{
source: "/some-page",
has: [{ type: "query", key: "override_me", value: "(?<override>.*)" }],
destination: "/somewhere-else?override_me=:override",
},
];
}
By default, a rewrite will pass through any query params that the source URL may have.
In your case, you can simply have the following rewrite rule. All query params will be forwarded through the destination URL.
rewrites() {
return [
{
source: '/results',
destination: 'https://www.somedomain.com/results'
}
]
}

Custom field not getting published in subscription apollo server

I'm trying to publish the newly added post, but the fields author and voteCount which are custom fields and reference another type were not being publish so that I got undefined on those fields.
My schema:
type Post {
id: ID!
title: String!
content: String
voteCount: Int!
author: User!
votes: [Vote!]!
createdAt: Date!
updatedAt: Date!
}
type Subscription {
Post(filter: PostSubscriptionFilter): PostSubscriptionPayload
}
input PostSubscriptionFilter {
mutation_in: [_ModelMutationType!]
}
type PostSubscriptionPayload {
mutation: _ModelMutationType!
node: Post
}
enum _ModelMutationType {
CREATED
UPDATED
DELETED
}
Resolver
Mutation: {
addPost: async (
root,
{ title, content },
{ ValidationError, models: { Post }, user },
) => {
if (!user) {
throw new ValidationError('unauthorized');
}
const post = new Post({
title,
content,
author: user.id,
});
await post.save();
pubsub.publish('Post', { Post: { mutation: 'CREATED', node: post } });
return post;
},
},
Subscription: {
Post: {
subscribe: () => pubsub.asyncIterator('Post'),
},
},
Post: {
// eslint-disable-next-line no-underscore-dangle
id: root => root.id || root._id,
author: async ({ author }, data, { dataLoaders: { userLoader } }) => {
const postAuthor = await userLoader.load(author);
return postAuthor;
},
voteCount: async ({ _id }, data, { models: { Vote } }) => {
const voteCount = await Vote.find({ post: _id }).count();
return voteCount || 0;
},
votes: async ({ _id }, data, { models: { Vote } }) => {
const postVotes = await Vote.find({ post: _id });
return postVotes || [];
},
},
And the subscription in React client:
componentWillMount() {
this.subscribeToNewPosts();
}
subscribeToNewPosts() {
this.props.allPostsQuery.subscribeToMore({
document: gql`
subscription {
Post(filter: { mutation_in: [CREATED] }) {
node {
id
title
content
updatedAt
voteCount
}
}
}
`,
updateQuery: (previous, { subscriptionData }) => {
// const result = Object.assign({}, previous, {
// allPosts: [subscriptionData.data.Post.node, ...previous.allPosts],
// });
// return result;
console.log(subscriptionData);
return previous;
},
});
}
The field voteCount is undefined:
While using queries or mutations, it get published normally, what should I do? Thank you.
The error you're seeing doesn't necessarily mean that voteCount is null -- rather it means you're trying to destructure an undefined value instead of an object. The path tells you this error occurred while attempting to resolve voteCount. You utilize destructuring within your resolve function in two places -- once with the root object and again with context. There should be a root object with you to work with, so I imagine the issue is with context.
When you set up context for a typical GraphQL server, you do so by utilizing middleware (like graphqlExpress) to essentially inject it into the request you're making. When you use subscriptions, everything is done over websockets, so the middleware is never hit and your context is therefore null.
To get around this, I think you'll need to inject the same context into your subscriptions -- you can see an example of how to do that here.

Resources