Custom field not getting published in subscription apollo server - graph

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.

Related

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.

wait until first hook is complete before fetching data

I have this custom hook which fetches the query.me data from graphql. The console.log statement shows that this hook is running a number of times on page load, but only 1 of those console.logs() contains actual data.
import { useCustomQuery } from '../api-client';
export const useMe = () => {
const { data, isLoading, error } = useCustomQuery({
query: async (query) => {
return getFields(query.me, 'account_id', 'role', 'profile_id');
},
});
console.log(data ? data.account_id : 'empty');
return { isLoading, error, me: data };
};
I then have this other hook which is supposed to use the id's from the above hook to fetch more data from the server.
export const useActivityList = () => {
const { me, error } = useMe();
const criteria = { assignment: { uuid: { _eq: me.profile_id } } } as appointment_bool_exp;
const query = useQuery({
prepare({ prepass, query }) {
prepass(
query.appointment({ where: criteria }),
'scheduled_at',
'first_name',
'last_name',
);
},
suspense: true,
});
const activityList = query.appointment({ where: criteria });
return {
activityList,
isLoading: query.$state.isLoading,
};
};
The problem I am facing is that the second hook seems to call the first hook when me is still undefined, thus erroring out. How do I configure this, so that I only access the me when the values are populated?
I am bad with async stuff...
In the second hook do an early return if the required data is not available.
export const useActivityList = () => {
const { me, error } = useMe();
if (!me) {
return null;
// or another pattern that you may find useful is to set a flag to indicate that this query is idle e.g.
// idle = true;
}
const criteria = { assignment: { uuid: { _eq: me.profile_id } } } as appointment_bool_exp;
...

Strange issue with useQuery: Query arguments not being read

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"}}

Vuejs reset fields after POST success callback

I'm having some problems resetting a textarea field after a POST request.
This is my component code
<template lang="pug">
.col-sm-12
h2 Add new Task
hr
.form-group
textarea.form-control(v-model="task.taskContent")
.form-group
button.btn.btn-primary(#click="createNewTask") Add Task
</template>
<script>
export default {
data() {
return {
task: {
taskContent: ''
}
};
},
methods: {
createNewTask() {
if (this.task.taskContent.length > 0) {
// Sending data to the server
this.$http.post('https://vue-taskmanager.firebaseio.com/task.json', this.task)
.then(response => {
console.log(response);
// Adding the new task to the main template list
this.$emit('taskWasCreated', this.task);
// Resetting textarea content
this.task.taskContent = '';
}, error => {
console.log(error);
});
} else {
alert("Sorry you can't create an empty task");
}
}
}
}
</script>
This is the parent component
<template lang="pug">
.container
.row
app-newtask(#taskWasCreated="addTask")
app-taskswrapper(:tasks="tasksArr")
app-footer
</template>
<script>
import { EventBus } from './main.js';
import UserRegistration from './components/user/UserRegistration.vue';
import TasksWrapper from './components/TasksWrapper.vue';
import NewTask from './components/NewTask.vue';
import Footer from './components/Footer.vue';
export default {
data() {
return {
tasksArr: [
'Just something to see'
]
};
},
methods: {
addTask(task) {
this.tasksArr.push(task)
}
},
// Listening on Events from Task.vue
created() {
// Delete task from array
EventBus.$on('taskWasDeleted', (taskIndex) => {
this.tasksArr.splice(taskIndex, 1);
// Delete task from db
this.$http.delete('https://vue-taskmanager.firebaseio.com/task.json', this.task)
.then(response => {
console.log(response);
}, error => {
console.log(error);
});
});
// Fetch tasks from db
this.$http.get('https://vue-taskmanager.firebaseio.com/task.json')
.then(response => {
return response.json();
})
.then(task => {
const resultsArray = [];
for (let key in task) {
resultsArray.push(task[key]);
}
this.tasksArr = resultsArray;
});
},
components: {
'app-taskswrapper': TasksWrapper,
'app-newtask': NewTask,
'app-footer': Footer,
'app-userregistration': UserRegistration
}
}
</script>
As you can see inside the response callback function I reset the task.taskContent value but the problem is that the string is sent to the db without problems while is not updated in the root component where I have an array storing all these strings.
I was thinking about using a watcher but I don't know if it's a good solution, do you have any suggestions?
Link to the github repo https://github.com/Polenj86/vue-taskmanager
It's clear what is happening now that you've posted your parent component.
You are storing the task object in the parent's array. This is not going to be a copy of the task, it's going to be a reference of the same task that you are about to clear. So when you later set this.task.taskContent = '' you are changing the task in the parent array too.
Consider this:
var task_holder_array = []
var task = {name: "mark"}
task_holder_array.push(task)
console.log("array before: ", task_holder_array)
task.name = ""
console.log("array after: ", task_holder_array)
You need to somehow create a new task object to push into the parent's array. There are a lot of ways you could do this. For example:
this.$emit('taskWasCreated', {name: this.task.name});
Or you could just pass the task name string to the parent and let the parent create the object.

Resources