How can I add computed state to graph objects in React Apollo? - javascript

I really like the graphQL pattern of having components request their own data, but some data properties are expensive to compute and so I want to localize the logic (and code) to do so.
function CheaterList({ data: { PlayerList: players } }) {
return (
<ul>
{players && players.map(({ name, isCheater }) => (
<li key={name}>{name} seems to be a {isCheater ? 'cheater' : 'normal player'}</li>
))}
</ul>
);
}
export default graphql(gql`
query GetList {
PlayerList {
name,
isCheater
}
}
`)(CheaterList);
The schema looks like:
type Queries {
PlayerList: [Player]
}
type Player {
name: String,
kills: Integer,
deaths: Integer
}
And so I want to add the isCheater property to Player and have its code be:
function computeIsCheater(player: Player){
// This is a simplified version of what it actually is for the sake of the example
return player.deaths == 0 || (player.kills / player.deaths) > 20;
}
How would I do that?
Another way of phrasing this would be: how do I get the isCheater property to look as though it came from the backend? (However, if an optimistic update were applied the function should rerun on the new data)

Note: Local state management is now baked into apollo-client -- there's no need to add a separate link in order to make use of local resolvers and the #client directive. For an example of mixing local and remote fields, check out of the docs. Original answer follows.
As long as you're using Apollo 2.0, this should be possible by utilizing apollo-link-state as outlined in the docs.
Modify your client configuration to include apollo-link-state:
import { withClientState } from 'apollo-link-state';
const stateLink = withClientState({
cache, //same cache object you pass to the client constructor
resolvers: linkStateResolvers,
});
const client = new ApolloClient({
cache,
link: ApolloLink.from([stateLink, new HttpLink()]),
});
Define your client-only resolvers:
const linkStateResolvers = {
Player: {
isCheater: (player, args, ctx) => {
return player.deaths == 0 || (player.kills / player.deaths) > 20
}
}
}
Use the #client directive in your query
export default graphql(gql`
query GetList {
PlayerList {
name
kills
deaths
isCheater #client
}
}
`)(CheaterList);
However, it appears that combining local and remote fields in a single query is currently broken. There's an open issue here that you can track.

Related

How to properly use context in tRPC?

Let's say I have a very basic API with two sets of endpoints. One set queries and mutates properties about a User, which requires a username parameter, and one set queries and mutates properties about a Post, which requires a post ID. (Let's ignore authentication for simplicity.) I don't currently see a good way to implement this in a DRY way.
What makes the most sense to me is to have a separate Context for each set of routes, like this:
// post.ts
export async function createContext(
opts?: trpcExpress.CreateExpressContextOptions
) {
// pass through post id, throw if not present
}
type Context = trpc.inferAsyncReturnType<typeof createContext>;
const router = trpc
.router()
.query("get", {
resolve(req) {
// get post from database
return post;
},
});
// similar thing in user.ts
// server.ts
const trpcRouter = trpc
.router()
.merge("post.", postRouter)
.merge("user.", userRouter);
app.use(
"/trpc",
trpcExpress.createExpressMiddleware({
router: trpcRouter,
createContext,
})
);
This complains about context, and I can't find anything in the tRPC docs about passing a separate context to each router when merging. Middleware doesn't seem to solve the problem either - while I can fetch the post/user in a middleware and pass it on, I don't see any way to require a certain type of input in a middleware. I would have to throw { input: z.string() } or { input: z.number() } on every query/mutation, which of course isn't ideal.
The docs and examples seem pretty lacking for this (presumably common) use case, so what's the best way forward here?
This functionality has been added in (unreleased as of writing) v10. https://trpc.io/docs/v10/procedures#multiple-input-parsers
const roomProcedure = t.procedure.input(
z.object({
roomId: z.string(),
}),
);
const appRouter = t.router({
sendMessage: roomProcedure
.input(
z.object({
text: z.string(),
}),
)
.mutation(({ input }) => {
// input: { roomId: string; text: string }
}),
});

Custom useMutation hook with Apollo Client and React

When using Apollo client, I find it quite tedious to manually update the cache for every mutation that requires an immediate UI update. I therefore decided to try to make a custom hook which updates the cache automatically.
The hook works but it seems a little "hacky" and I'm worried it might mess with the normal functioning of the cache. So I just wanted to ask if this hook seems like it should work ok?
Here's the code (where mutationName is the actual graphql mutation name and fieldName is the original graphql query name corresponding to the mutation):
export const useMutationWithCacheUpdate = (
mutation,
mutationName,
fieldName
) => {
const [createMutation, { data, loading, error }] = useMutation(mutation, {
update(cache, { data }) {
data = data[mutationName];
cache.modify({
fields: {
[fieldName]: (existingItems = []) => {
const newItemRef = cache.writeFragment({
data: data,
fragment: gql`
fragment newItem on ${fieldName} {
id
type
}
`,
});
return [...existingItems, newItemRef];
},
},
});
},
});
return [createMutation, { data, loading, error }];
};

Feathers-mongoose : Get by custom attribute in feathers-mongoose

I have a very basic feathers service which stores data in mongoose using the feathers-mongoose package. The issue is with the get functionality. My model is as follows:
module.exports = function (app) {
const mongooseClient = app.get('mongooseClient');
const { Schema } = mongooseClient;
const messages = new Schema({
message: { type: String, required: true }
}, {
timestamps: true
});
return mongooseClient.model('messages', messages);
};
When the a user runs a GET command :
curl http://localhost:3030/messages/test
I have the following requirements
This essentially tries to convert test to ObjectID. What i would
like it to do is to run a query against the message attribute
{message : "test"} , i am not sure how i can achieve this. There is
not enough documentation for to understand to write or change this
in the hooks. Can some one please help
I want to return a custom error code (http) when a row is not found or does not match some of my criterias. How can i achive this?
Thanks
In a Feathers before hook you can set context.result in which case the original database call will be skipped. So the flow is
In a before get hook, try to find the message by name
If it exists set context.result to what was found
Otherwise do nothing which will return the original get by id
This is how it looks:
async context => {
const messages = context.service.find({
...context.params,
query: {
$limit: 1,
name: context.id
}
});
if (messages.total > 0) {
context.result = messages.data[0];
}
return context;
}
How to create custom errors and set the error code is documented in the Errors API.

Use existing variables when using refetchQueries in react-apollo

I am using postsConnection query for infinite scroll. It contains variables like after.
After doing an upvote mutation, I want to refetchQueries... like this 👇
const upvote = await client.mutate({
mutation: UPVOTE_MUTATION,
variables: {
postId: this.props.post.id
},
refetchQueries: [
{ query: POST_AUTHOR_QUERY }
]
})
Above code gives error because POST_AUTHOR_QUERY accepts few variables. Here's that query 👇
export const POST_AUTHOR_QUERY = gql`
query POST_AUTHOR_QUERY($authorUsername: String! $orderBy: PostOrderByInput $after: String){
postsAuthorConnection(authorUsername: $authorUsername orderBy: $orderBy after: $after) {
....
}
}
I do not want to add variables manually. Variables are already stored in the cache. How do I reuse them while using refetchQueries???
Here are a few resources I have read about this issue 👇
https://github.com/apollographql/react-apollo/issues/817
https://github.com/apollographql/apollo-client/issues/1900
As mentioned in the issue you linked, you should be able to do the following:
import { getOperationName } from 'apollo-link'
const upvote = await client.mutate({
// other options
refetchQueries={[getOperationName(POST_AUTHOR_QUERY)]}
})
From the docs:
Please note that if you call refetchQueries with an array of strings, then Apollo Client will look for any previously called queries that have the same names as the provided strings. It will then refetch those queries with their current variables.
getOperationName simply parses the document you pass it and extracts the operation name from it. You can, of course, provide the operation name yourself as a string instead, but this way avoids issues if the operation name changes in the future or you fat finger it.
If you don't want to pull in apollo-link, you can also get this via the base graphql package (note that I use optional chaining for convenience:
import { getOperationAST } from 'graphql';
const operationName = getOperationAST(POST_AUTHOR_QUERY)?.name?.value;
// Note that this could technically return `undefined`
const upvote = await client.mutate({
mutation: UPVOTE_MUTATION,
variables: {
postId: this.props.post.id
},
refetchQueries: [operationName]
})

How to keep data synchronized in ember using ember-apollo-client?

I have an app built using Ember and ember-apollo-client.
// templates/collaborators.hbs
// opens an ember-bootstrap modal
{{#bs-button type="success" onClick=(action (mut createCollaborator) true)}}Create collaborator{{/bs-button}}
// submit button in modal triggers "createCollaborator" in controller
{{#each model.collaborators as |collaborator|}}
{{collaborator.firstName}} {{collaborator.lastName}}
{{/each}}
// routes/collaborators.js
import Route from '#ember/routing/route';
import { RouteQueryManager } from 'ember-apollo-client';
import query from '../gql/collaborators/queries/listing';
export default Route.extend(RouteQueryManager, {
model() {
return this.get('apollo').watchQuery({ query });
}
});
// controllers/collaborator.js
export default Controller.extend({
apollo: service(),
actions: {
createCollaborator() {
let variables = {
firstName: this.firstName,
lastName: this.lastName,
hireDate: this.hireDate
}
return this.get('apollo').mutate({ mutation, variables }, 'createCollaborator')
.then(() => {
this.set('firstName', '');
this.set('lastName', '');
this.set('hireDate', '');
});
}
}
});
Currently, after creating a collaborator the data is stale and needs a browser refresh in order to update. I'd like the changes to be visible on the collaborators list right away.
From what I understood, in order to use GraphQL with Ember, I should use either Ember Data with ember-graphql-adapter OR just ember-apollo-client. I went on with apollo because of its better documentation.
I dont think I quite understood how to do that. Should I somehow use the store combined with watchQuery from apollo? Or is it something else?
LATER EDIT
Adi almost nailed it.
mutationResult actually needs to be the mutation itself.
second param in store.writeQuery should be either data: { cachedData } or data as below.
Leaving this here as it might help others.
return this.get('apollo').mutate({
mutation: createCollaborator,
variables,
update: (store, { data: { createCollaborator } }) => {
const data = store.readQuery({ query })
data.collaborators.push(createCollaborator);
store.writeQuery({ query, data });
}
}, createCollaborator');
You can use the apollo imperative store API similar to this:
return this.get('apollo').mutate(
{
mutation,
variables,
update: (store, { data: {mutationResult} }) => {
const cachedData = store.readyQuery({query: allCollaborators})
const newCollaborator = mutationResult; //this is the result of your mutation
store.writeQuery({query: allCollaborators, cachedData.push(newCollaborator)})
}
}, 'createCollaborator')

Categories