Skip to main content

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, 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.

// 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. 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.

// 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 "~/account/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.

// 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, 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. 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.).