Skip to content

Commit

Permalink
improved doc for Vulcan Fire
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-burel committed May 16, 2022
1 parent 86b4806 commit 6af04a3
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 72 deletions.
94 changes: 94 additions & 0 deletions docusaurus/docs/vulcan-fire/customFieldResolvers.md
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
email
}
}
}
```

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.
70 changes: 0 additions & 70 deletions docusaurus/docs/vulcan-fire/customResolvers.md

This file was deleted.

162 changes: 162 additions & 0 deletions docusaurus/docs/vulcan-fire/customTopLevelResolvers.md
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.).
2 changes: 2 additions & 0 deletions docusaurus/docs/vulcan-fire/outsideGraphql.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ In an Express middleware:
You'll need to pass the `currentUser`. If there is no current user, for instance if you use a mutator
in a seed script, instead use the `asAdmin` option and the `validate` option.

- Use Mongoose to get data

You can optionnaly pass the GraphQL context to a mutator, though it should not be needed anymore:

- Optionnaly generate the `context`. This is the same context that is used in GraphQL resolvers. This concept is less common outside of the GraphQL ecosystem, but still perfectly relevant.
Expand Down
2 changes: 1 addition & 1 deletion docusaurus/docs/vulcan-next/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Many modern frameworks prefer _static_ generation for both, meaning they will li
- Static code generation is awesome when you need to customize the code later on. However,
it also mean that you need to manage a lot of additional files in your codebase.

- Dynamic generation won't generate any code. Customization is done either via the Vulcan Model object, or by [creating your custom resolvers as depicted in our GraphQL engine documentation](../vulcan-fire/customResolvers.md).
- Dynamic generation won't generate any code. Customization is done either via the Vulcan Model object, or by [creating your custom resolvers as depicted in our GraphQL engine documentation](../vulcan-fire/customTopLevelResolvers.md).

Actually, both patterns are perfectly compatible!

Expand Down
3 changes: 2 additions & 1 deletion docusaurus/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ const sidebars = {
link: { type: "doc", id: "vulcan-fire/index" },
items: [
"vulcan-fire/get-started",
"vulcan-fire/customResolvers",
"vulcan-fire/customTopLevelResolvers",
"vulcan-fire/customFieldResolvers",
"vulcan-fire/outsideGraphql",
],
},
Expand Down

0 comments on commit 6af04a3

Please sign in to comment.