-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
86b4806
commit 6af04a3
Showing
6 changed files
with
261 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
--- | ||
title: "Custom field resolvers" | ||
--- | ||
|
||
# Custom field resolvers | ||
|
||
## What's the difference with top-level resolvers? | ||
|
||
**Field resolvers do not really have an equivalent in a REST API, that's a benefit of GraphQL.** | ||
|
||
There is "graph" in "GraphQL": this means that GraphQl is very good at fetching data that is linked or related to a specific document. In a blog application, you might want to fetch an Article, all the related Comments. | ||
|
||
Field resolvers are function that can get those related data. For instance, they can get the list of Comments based on the id of your Article, or your Twitter description based on your Twitter handle, etc. | ||
|
||
## Write a custom field resolver | ||
|
||
### Method 1 - Assess if a relation is enough for you | ||
|
||
First, remember that you don't need a custom field resolvers just to get related data. For instance, if you need the list of comments for an article, based on the `commentIds` field of the Article object, you can define a `relation` in your Vulcan schema. | ||
|
||
If you are not in this scenario, you need a custom field resolver. | ||
|
||
```ts | ||
// Demo from Vulcan Next "sampleModel": | ||
// we can get the "User" from an userId very easily! | ||
userId: { | ||
type: String, | ||
optional: true, | ||
canRead: ["guests"], | ||
// This means you can resolve the "user" field when fetching for "samples" | ||
relation: { | ||
fieldName: "user", | ||
kind: "hasOne", | ||
//typeName: "VulcanUser" | ||
model: User, | ||
}, | ||
}, | ||
``` | ||
You can then write queries like this: | ||
```graphql | ||
query getSamplesWithUser { | ||
samples { | ||
# The id, stored in the database | ||
userId | ||
# This is available because you added a relation! | ||
user { | ||
_id | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Vulcan is in charge of resolving related data via its default "relation resolvers". It works for an unique id or an array of ids. | ||
|
||
|
||
### Method 2 - Almost the same as top-level resolvers... | ||
|
||
Field resolvers behave almost the same as [top-level resolvers](./customTopLevelResolvers.md) described earlier. So you can either create a fully custom resolver, or use a `mutator` if you must manipulate data. | ||
|
||
You can use the `resolveAs` field of a schema to create a custom field resolver for an existing Vulcan model. | ||
|
||
|
||
```ts | ||
// Example from Vulcan Express: | ||
// a fun resolver that get "itself" based on the document _id | ||
myselfVirtual: { | ||
type: String, | ||
canRead: ["guests"], | ||
canCreate: [], | ||
canUpdate: [], | ||
resolveAs: { | ||
fieldName: "myself", | ||
typeName: "Contributor", | ||
resolver: async (root /*: ContributorDocument*/, args, context) => { | ||
return await context.dataSources["Contributor"].findOneById(root._id); | ||
}, | ||
}, | ||
}, | ||
``` | ||
|
||
**/!\ You can only add custom field resolvers to server models! They should not exist client-side!** | ||
|
||
[Vulcan Next has a complete example of this setup.](https://github.dev/VulcanJS/vulcan-next/blob/main/src/models/sampleModel.server.ts) | ||
|
||
### Method 3 - ...but use DataSources if possible | ||
|
||
The main difference between field resolvers and top-level resolvers is that, if possible, you should use [DataSources](https://www.apollographql.com/docs/apollo-server/data/data-sources/) for field-resolvers. DataSources will reduce the number of calls to your database in many scenarios, this is what we call the "N+1" problem. | ||
|
||
As a default, Fire will generate [Mongo DataSources](https://github.com/GraphQLGuide/apollo-datasource-mongodb) for each model. | ||
|
||
**Be careful with ObjectId from Mongo ddocuments!** You should convert them to string ids before responding to a GraphQL request, otherwise you may end up with unexpected issues. Vulcan Fire default relation resolvers and Mongoose connector will handle the conversion for you. | ||
|
||
In your custom resolvers, you might need to use our `convertIdAndTransformToJSON` helper exported from `@vulcanjs/crud/server`, it works for a single document or an array of documents. |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
--- | ||
title: "Custom top-level resolvers" | ||
--- | ||
|
||
# Custom top-level resolvers | ||
|
||
## Why a custom resolver? | ||
|
||
**Top-level custom resolver in GraphQL ~ an API endpoint in REST.** | ||
|
||
A custom resolver is needed for everything that is not a normal CRUD operation handled by Vulcan Fire. | ||
|
||
For example, creating an Article on a blog is a normal CRUD operation, and Vulcan Fire handles that very well. | ||
|
||
However, triggering a spell-check algorithm server side is not a normal CRUD operation. For that you'll want a custom resolver. | ||
|
||
If you were using a traditionnal REST API, you would create an API endpoint: that's the same thing. | ||
|
||
## Field or top-level? | ||
|
||
**Top level resolvers:** when you want some very custom data that do not fit any of your model AND/OR when you want to write a "mutation" that create, update or delete some data or do some computations. | ||
|
||
- query resolvers are for getting data. Example: compute the nth decimal of Pi. | ||
- mutation resolvers are for modifying data or triggering side effects. Example: send emails to your users. | ||
|
||
**Field resolvers:** when you add a field to an existing model. For instance, you want to resolve the "Twitter profile picture" of an exsting user. [They are described in a separate section](./customFieldResolvers), and they are more specific to GraphQL. | ||
|
||
Let's first describe custom "top-level" resolvers. | ||
|
||
## Write custom top-level resolvers | ||
|
||
### Method 1 - Forget about Vulcan, just write your resolver | ||
|
||
You can still write an Apollo resolver as usual, as long as the name doesn't clash with an existing resolver generated by Fire (avoid `createBlogArticle` for instance). | ||
|
||
Forget everything about Fire, you have zero obligation to use any of the helpers we provide. | ||
|
||
```ts | ||
// This is a custom resolver: that's how you write them | ||
// without Vulcan! | ||
// Here we demo a "query" but it could be a "mutation" as well. | ||
const customResolvers = { | ||
Query: { | ||
// Demo with mongoose | ||
// Expected the database to be setup with the demo "restaurant" API from mongoose | ||
async restaurants() { | ||
try { | ||
const db = mongoose.connection; | ||
const restaurants = db.collection("restaurants"); | ||
// @ts-ignore | ||
const resultsCursor = (await restaurants.find(null, null)).limit(5); | ||
const results = await resultsCursor.toArray(); | ||
return results; | ||
} catch (err) { | ||
console.log("Could not fetch restaurants", err); | ||
throw err; | ||
} | ||
}, | ||
}, | ||
}; | ||
// Here is the corresponding "type definition" aka GraphQL schema: | ||
const customTypeDefs = gql` | ||
type Query { | ||
restaurants: [Restaurant] | ||
} | ||
type Restaurant { | ||
_id: ID! | ||
name: String | ||
} | ||
`; | ||
``` | ||
|
||
See [Vulcan Next GraphQL setup to discover how you can merge your custom resolver and your Vulcan generated API](https://github.dev/VulcanJS/vulcan-next/blob/main/src/pages/api/graphql.ts). Hopefully, everything is already setup for you. Just do your thing. | ||
|
||
|
||
### Method 2 - For mutation resolvers, use Fire "mutators" to manipulate data | ||
|
||
When writing a custom resolver, you will quickly understand why Vulcan Fire is _so cool_. This is especially true if you need to do a CRUD operation inside your custom resolver. For instance if your resolver must update or create some data from the database. | ||
|
||
You may need: | ||
|
||
- to check if the user is authorized to do the operation | ||
- to run some callbacks to update related data | ||
- to send a database request | ||
|
||
Hopefully, Fire exposes it's internal logic via the concept of "Mutators". | ||
|
||
A mutator is a function that includes all the heavy logic of Fire. Our GraphQL resolvers | ||
are actually just wrappers around mutator calls. | ||
|
||
```ts | ||
// This is how we seed data in Vulcan | ||
// You can use mutators in scripts or in custom mutation resolvers | ||
import { createMutator } from "@vulcanjs/crud/server"; | ||
import { User } from "~/models/user.server"; | ||
|
||
const admin = { | ||
email: process.env.ADMIN_EMAIL, | ||
password: process.env.ADMIN_INITIAL_PASSWORD, | ||
isAdmin: true, | ||
}; | ||
try { | ||
await createMutator({ | ||
model: User, | ||
data: admin, | ||
context, | ||
asAdmin: true, | ||
validate: false, | ||
}); | ||
} catch (error) { | ||
console.error("Could not seed admin user", error); | ||
} | ||
``` | ||
|
||
|
||
### Method 3 - For query resolvers, use Mongoose directly | ||
|
||
"Mutators" are for "mutating" data: creation, update and deletion. What if you just want to get some data? | ||
|
||
It's even simpler! | ||
|
||
You can use Mongoose or Mongo directly as you would do in any other application. | ||
|
||
```ts | ||
// This how we find the user during authentication | ||
// in Vulcan. | ||
export async function findUserByCredentials({ | ||
email, | ||
password, | ||
}: { | ||
email: string; | ||
password: string; | ||
}): Promise<UserTypeServer | null> { | ||
const user = await UserMongooseModel.findOne({ email }); | ||
if (!user) { | ||
return null; | ||
} | ||
const passwordsMatch = checkPasswordForUser(user, password); | ||
if (!passwordsMatch) { | ||
return null; | ||
} | ||
return user; | ||
} | ||
``` | ||
*Hey, you might have noticed that this function could even work outside of GraphQL. That's true!* | ||
|
||
You may however need to check permissions, for restricting fields or documents based on the "read" permissions. | ||
Check Vulcan Fire [defaultResolvers](https://github.dev/VulcanJS/vulcan-npm/blob/main/packages/graphql/server/resolvers/defaultQueryResolvers.ts), especially the "multi" function for more details. | ||
Feel free to copy-paste this code to build your own resolvers. | ||
|
||
|
||
[There is a pending issue for creating "queriers" equivalent to "mutators](https://github.com/VulcanJS/vulcan-npm/issues/82). Stay tuned! | ||
|
||
### /!\ Anti-pattern: do not use Vulcan connectors directly to communicate with the database | ||
|
||
Connectors are simplified functions that fits exactly the need of Fire CRUD operations. They allow us to support any database. | ||
|
||
If you come from Vulcan Meteor, you might have used them in your code. | ||
|
||
But they will feel limited if you use them directly. First because they can't support as many functionnalities as a normal database connector. Then because they won't run all the nice logic of Fire (no permission checks, no callbacks etc.) | ||
|
||
Instead, either use a `mutator`, or call your database as you would do usually (using `mongo` or `mongoose`, a raw SQL query, etc.). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters