Apollo GraphQL pass object - graphql

In GraphQL, how do I pass an object instead of a string?
Take this code from Apollo's website as an example, with my minor change:
import React, { useState } from 'react';
import { useLazyQuery } from '#apollo/client';
function DelayedQuery() {
const [dog, setDog] = useState(null);
const [getDog, { loading, data }] = useLazyQuery(GET_DOG_PHOTO);
if (loading) return <p>Loading ...</p>;
if (data && data.dog) {
setDog(data.dog);
}
const myObject = {
type: {
favors: [
tom: true,
bill: false
]
}
}
return (
<div>
{dog && <img src={dog.displayImage} />}
<button onClick={() => getDog({ variables: { theObject: myObject } })}>
Click me!
</button>
</div>
);
}
I believe React is trying to parse the object into a string, but (as the error message explains) JSON.stringify cannot serialize cyclic structures.
What do I do?

Related

RematchJS Refresh Issue

I am new at using RematchJS and have managed to display hardcoded Objects from an array. The dispatcher (addTopicAsync) however does not seems to update the Array, when I try to add a new Object.
The Array is briefly updated and the topic is flashes on the screen, but the Array is empty shortly afterwards.
My model code:
import { createModel } from "#rematch/core";
import { RootModel } from ".";
export interface Topic {
topic: String
}
export interface TopicsList {
list: Array<Topic>
}
const TOPICS_LIST_STATE = {
list: []
}
export const topics = createModel<RootModel>()({
state: TOPICS_LIST_STATE as TopicsList,
reducers: {
addTopic: (state, topic) => {
return { list: [...state.list, { topic }] }
},
clearTopics: () => {
return { list: [] }
}
},
effects: (dispatch) => ({
async addTopicAsync(topic: string) {
await dispatch.topics.addTopic(topic)
},
async clearTopicsAsync() {
await dispatch.topics.clearTopics()
}
})
});
My application code:
// eslint-disable-next-line #typescript-eslint/no-unused-vars
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState, Dispatch } from '#vanilla/data'
import { Topic } from 'libs/data/src/lib/models/topics';
export function App() {
const [topic, setTopic] = useState("aaa")
const topicsState = useSelector((state: RootState) => state.topics)
const dispatch = useDispatch<Dispatch>()
const topicChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault()
setTopic(event.target.value)
}
const updateTopicList = async () => {
await dispatch.topics.addTopicAsync(topic)
console.log('topicsState : ', topicsState.list)
}
return (
<>
<h3>Topics</h3>
<form>
<input type='text' value={topic} onChange={topicChange} />
<button onClick={() => { updateTopicList() }}> Add Topic</button>
</form>
<div className="container">
{topicsState.list.map((topicRecord: Topic, index: number) => (
<h5 key={index}>
{topicRecord.topic}
</h5>
))}
</div>
</>
)
}
export default (App)

How to make a graphql entry in a psql database

I am trying to figure out how to make a graphql entry in a psql database.
I am stuck and am not getting any feedback from console logs at any point in my attempt. I'm stuck for what to try next (or where to look for a tutorial showing how this step is supposed to work).
I have a table in my prisma schema called 'issue'. I am trying to create an 'issue' entry.
I have made a form with:
import * as React from "react"
import { Box, Center, Heading, Button, } from "#chakra-ui/react"
import { Select, OptionBase, GroupBase } from "chakra-react-select";
import { groupedIssueCategories } from "../components/issue/categories"
import { gql } from "#apollo/client"
import Head from 'next/head'
import { IssueInput, useAllIssuesQuery, useCreateIssueMutation } from "lib/graphql"
import * as c from "#chakra-ui/react"
import { Input } from "components/Input"
// import { Select } from "components/Select"
import { HomeLayout } from "components/HomeLayout"
import { Limiter } from "components/Limiter"
import { Form } from "components/Form"
import Yup from "lib/yup"
import { useForm } from "lib/hooks/useForm"
import { useMe } from "lib/hooks/useMe"
import { useToast } from "lib/hooks/useToast"
interface GroupedRiskOption extends OptionBase {
label: string
value: string
}
const _ = gql`
mutation CreateIssue($data: IssueInput!) {
createIssue(data: $data) {
id
title
issueCategory
description
userId
}
}
query AllIssues {
allIssues {
id
title
issueId
description
userId
}
}
`
export default function Issue() {
const toast = useToast()
const { me, loading: meLoading } = useMe()
const [createIssue] = useCreateIssueMutation()
const { data: issues, refetch } = useAllIssuesQuery()
const IssueSchema = Yup.object().shape({
title: Yup.string().required("Title is required"),
issueCategory: Yup.string().required("Category is required"),
description: Yup.string().required("Description is required"),
})
const form = useForm({ schema: IssueSchema })
const onSubmit = (data: IssueInput) => {
console.log(data)
return form.handler(() => createIssue({ variables: { data: { ...data, userId: me?.id || ""} } }), {
onSuccess: async () => {
toast({
title: "Issue created",
description: "Your issue has been created",
status: "success",
})
refetch()
form.reset()
},
})
}
if (meLoading)
return (
<c.Center>
<c.Spinner />
</c.Center>
)
if (!me) return null
return (
<Box>
<Head>
<title>Create Issue</title>
</Head>
<Limiter pt={20} minH="calc(100vh - 65px)">
<Center flexDir="column">
<Heading as="h1" size="3xl" fontWeight="extrabold" px="3rem" lineHeight="1.2" letterSpacing="tight" color="brand.orange">
Create Issue
</Heading>
<Form onSubmit={onSubmit} {...form}>
<c.Stack spacing={2}>
<c.Heading>Issues</c.Heading>
<Input autoFocus name="title" label="Title" placeholder="Eg: climate change" />
<Input name="description" label="Description" placeholder="Eg: Issues relating to climate change" />
<Select<GroupedRiskOption, true, GroupBase<GroupedRiskOption>>
// isMulti
name="issueCategory"
options={groupedIssueCategories}
placeholder="Select issue categories"
closeMenuOnSelect={false}
/>
<Button
color="brand.orange"
type="submit"
isFullWidth
isDisabled={form.formState.isSubmitting ||
!form.formState.isDirty}
isLoading={form.formState.isSubmitting}
>
Create Issue
</Button>
<c.List>
{/* {issues.allIssues.map((issue) => (
<c.ListItem key={issue.id}>
{issue.title}
{issue.issueCategory}
{issue.description}
</c.ListItem>
))} */}
</c.List>
</c.Stack>
</Form>
</Center>
</Limiter>
</Box>
)
}
Issue.getLayout = (page: React.ReactNode) => <HomeLayout>{page}</HomeLayout>
I have a create issue mutation in my lib/graphql:
export function useCreateIssueMutation(baseOptions?: Apollo.MutationHookOptions<CreateIssueMutation, CreateIssueMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateIssueMutation, CreateIssueMutationVariables>(CreateIssueDocument, options);
}
export type CreateIssueMutationHookResult = ReturnType<typeof useCreateIssueMutation>;
export type CreateIssueMutationResult = Apollo.MutationResult<CreateIssueMutation>;
export type CreateIssueMutationOptions = Apollo.BaseMutationOptions<CreateIssueMutation, CreateIssueMutationVariables>;
When I click submit, nothing happens in the console. I can't log the data, and I can't see any errors, either in the terminal or in the console.
Can anyone give me a steer on where to look for insights as to what is going wrong. There is no data in the database, the onSuccess step doesn't get performed.

Trying to test a component using the useQuery hook but onCompleted causing issues

I am trying to test the following component, but cannot get it to render in Jest once data is fetched due to an error on the query's onCompleted prop. I have another component that is essentially the same, but does not utilise onCompleted at all and presents no trouble to Jest.
Here is the component in question, with some code reduced for brevity purposes:
import { trackProductListView } from "src/shared/components/analytics";
import searchResourcesQuery from "./search-resources.graphql";
const SearchResources = ({ filter, orderBy, query }) => {
const { loading, error, data, fetchMore } = useQuery(searchResourcesQuery, {
variables: {
orderBy,
filter,
query
},
onCompleted: ({ searchResources: { results } }) =>
results && trackProductListView("Search", null, results, 1)
});
...
return (
<div>
{!results.length ? (
<EmptySearch variant="resources" />
) : (
<InfiniteResources
trackContext="Search"
hasMore={!!searchAfter}
loadMoreEntries={loadMoreEntries}
resources={results}
/>
)}
</div>
);
};
And here is the test, although I have removed the mock results (again, for brevity purposes):
import React from "react";
import { act, wait } from "#testing-library/react";
import "#testing-library/jest-dom/extend-expect";
import { MockedProvider } from "#apollo/react-testing";
import renderWithRouter from "src/shared/test-utils/renderWithRouter.js";
import SearchResources from "./search-resources";
import searchResourcesQuery from "./search-resources.graphql";
const mocks = [
{
request: {
query: searchResourcesQuery,
variables: { query: "test" }
},
result: {
data: {
searchResources: {
searchAfter: null,
results: [],
__typename: "ResourceList"
}
}
}
}
];
it("renders", async () => {
const { getByText } = renderWithRouter(
<MockedProvider addTypename={false} mocks={mocks}>
<SearchResources query="test" />
</MockedProvider>
);
await act(() => wait(() => getByText("Resource Partner Link test")));
expect(getByText("Resource Partner Link test")).toBeInTheDocument();
});
Running these tests results in:
TypeError: (0 , _analytics.trackProductListView) is not a function
Any help fixing this is most appreciated!
Well, turns out I could just mock the trackProductListView function

apollo-client: How to get inverse relation from cache?

I have a graphql query response of the shape
{
table {
id
legs {
id
}
}
This normalizes to table and leg entries in my InMemoryCache.
But then if my application retrieves a leg from cache and needs to know the corresponding table, how would I find this?
The two ideas I had are
adding a table prop to each leg when the query response comes in - not sure if/how that would work (I have multiple queries and mutations containing the graphql fragment with above shape)
having a suitable cache redirect, but I don't know how to do this without searching all tables for the leg.
Does apollo provide any features suitable to achieve this inverse lookup?
Update: to clarify, the leg has a table prop, but I since I already have the info in the client before resolving that prop, I'd like to resolve that prop client-side instead of server-side.
You should be adding a table prop to each leg. According to the graphql.org documentation you should be thinking in graphs:
With GraphQL, you model your business domain as a graph by defining a schema; within your schema, you define different types of nodes and how they connect/relate to one another.
In your model, tables and legs are nodes in your business model graph. When you add a table prop to each leg you are creating a new edge in this graph that your client-side code can traverse to get the relevant data.
Edit after clarification:
You can use writeFragment and to gain fine grained control of the Apollo cache. Once the cache filling query is done, compute the inverse relationship and write it to the cache like so:
fetchTables = async () => {
const client = this.props.client
const result = await client.query({
query: ALL_TABLES_QUERY,
variables: {}
})
// compute the reverse link
const tablesByLeg = {}
for (const table of result.data.table) {
for (const leg of table.legs) {
if (!tablesByLeg[leg.id]) {
tablesByLeg[leg.id] = {
leg: leg,
tables: []
}
}
tablesByLeg[leg.id].tables.push(table)
}
}
// write to the Apollo cache
for (const { leg, tables } of Object.values(tablesByLeg)) {
client.writeFragment({
id: dataIdFromObject(leg),
fragment: gql`
fragment reverseLink from Leg {
id
tables {
id
}
}
`,
data: {
...leg,
tables
}
})
}
// update component state
this.setState(state => ({
...state,
tables: Object.values(result)
}))
}
Demo
I put up a complete exemple here: https://codesandbox.io/s/6vx0m346z
I also put it below just for completeness sake.
index.js
import React from "react";
import ReactDOM from "react-dom";
import { ApolloProvider } from "react-apollo";
import { createClient } from "./client";
import { Films } from "./Films";
const client = createClient();
function App() {
return (
<ApolloProvider client={client}>
<Films />
</ApolloProvider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
client.js
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
export function dataIdFromObject(object) {
return object.id ? object.__typename + ":" + object.id : null;
}
export function createClient() {
return new ApolloClient({
connectToDevTools: true,
ssrMode: false,
link: new HttpLink({
uri: "https://prevostc-swapi-graphql.herokuapp.com"
}),
cache: new InMemoryCache({
dataIdFromObject,
cacheRedirects: {
Query: {
planet: (_, args, { getCacheKey }) =>
getCacheKey({ __typename: "Planet", id: args.id })
}
}
})
});
}
Films.js
import React from "react";
import gql from "graphql-tag";
import { withApollo } from "react-apollo";
import { dataIdFromObject } from "../src/client";
import { Planet } from "./Planet";
const ALL_FILMS_QUERY = gql`
query {
allFilms {
films {
id
title
planetConnection {
planets {
id
name
}
}
}
}
}
`;
const REVERSE_LINK_FRAGMENT = gql`
fragment reverseLink on Planet {
id
name
filmConnection {
films {
id
title
}
}
}
`;
class FilmsComponent extends React.Component {
constructor() {
super();
this.state = { films: [], selectedPlanetId: null };
}
componentDidMount() {
this.fetchFilms();
}
fetchFilms = async () => {
const result = await this.props.client.query({
query: ALL_FILMS_QUERY,
variables: {}
});
// compute the reverse link
const filmByPlanet = {};
for (const film of result.data.allFilms.films) {
for (const planet of film.planetConnection.planets) {
if (!filmByPlanet[planet.id]) {
filmByPlanet[planet.id] = {
planet: planet,
films: []
};
}
filmByPlanet[planet.id].films.push(film);
}
}
// write to the apollo cache
for (const { planet, films } of Object.values(filmByPlanet)) {
this.props.client.writeFragment({
id: dataIdFromObject(planet),
fragment: REVERSE_LINK_FRAGMENT,
data: {
...planet,
filmConnection: {
films,
__typename: "PlanetsFilmsConnection"
}
}
});
}
// update component state at last
this.setState(state => ({
...state,
films: Object.values(result.data.allFilms.films)
}));
};
render() {
return (
<div>
{this.state.selectedPlanetId && (
<div>
<h1>Planet query result</h1>
<Planet id={this.state.selectedPlanetId} />
</div>
)}
<h1>All films</h1>
{this.state.films.map(f => {
return (
<ul key={f.id}>
<li>id: {f.id}</li>
<li>
title: <strong>{f.title}</strong>
</li>
<li>__typename: {f.__typename}</li>
<li>
planets:
{f.planetConnection.planets.map(p => {
return (
<ul key={p.id}>
<li>id: {p.id}</li>
<li>
name: <strong>{p.name}</strong>
</li>
<li>__typename: {p.__typename}</li>
<li>
<button
onClick={() =>
this.setState(state => ({
...state,
selectedPlanetId: p.id
}))
}
>
select
</button>
</li>
<li> </li>
</ul>
);
})}
</li>
</ul>
);
})}
<h1>The current cache is:</h1>
<pre>{JSON.stringify(this.props.client.extract(), null, 2)}</pre>
</div>
);
}
}
export const Films = withApollo(FilmsComponent);
Planet.js
import React from "react";
import gql from "graphql-tag";
import { Query } from "react-apollo";
const PLANET_QUERY = gql`
query ($id: ID!) {
planet(id: $id) {
id
name
filmConnection {
films {
id
title
}
}
}
}
`;
export function Planet({ id }) {
return (
<Query query={PLANET_QUERY} variables={{ id }}>
{({ loading, error, data }) => {
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
const p = data.planet;
return (
<ul key={p.id}>
<li>id: {p.id}</li>
<li>
name: <strong>{p.name}</strong>
</li>
<li>__typename: {p.__typename}</li>
{p.filmConnection.films.map(f => {
return (
<ul key={f.id}>
<li>id: {f.id}</li>
<li>
title: <strong>{f.title}</strong>
</li>
<li>__typename: {f.__typename}</li>
<li> </li>
</ul>
);
})}
</ul>
);
}}
</Query>
);
}

relay refetch doesn't show the result

I'm trying to create a live search-result component(lazy load one). It works perfectly for the first time but refetch doesn't update the data. I see the request and respoonse in Network tab! so it does get the data, but it doesn't supply it to the component!
any idea why?
import React, { Component } from 'react';
import {
createRefetchContainer,
graphql,
} from 'react-relay';
import ProfileShow from './ProfileShow';
class ProfileList extends Component {
render() {
console.log("rendering....", this.props)
return (
<div className="row">
<input type="text" onClick={this._loadMe.bind(this)} />
{this.props.persons.map((person) => {
return (
<div className="col-md-3">
<ProfileShow person={person} />
</div>
);
})}
</div>
);
}
_loadMe(e) {
const refetchVariables = fragmentVariables => ({
queryStr: e.target.value,
});
this.props.relay.refetch(refetchVariables, null, (...data) => {
console.log(data)
});
}
}
const FragmentContainer = createRefetchContainer(
ProfileList,
{
persons: graphql.experimental`
fragment ProfileList_persons on Person #relay(plural: true) {
fullname
number
email
pic
}
`
},
graphql.experimental`
query ProfileListRefetchQuery($queryStr: String!) {
talentList(query: $queryStr) {
...ProfileList_persons
}
}
`,
);
export default FragmentContainer;

Resources