title: "Writing a Vendure Plugin"
A Vendure plugin is a class which implements the VendurePlugin interface. This interface allows the plugin to:
Let's learn about these capabilities by writing a plugin which defines a new database entity and GraphQL mutation.
This plugin will add a new mutation, addRandomCat, to the GraphQL API which allows us to conveniently link a random cat image from http://random.cat to any product in out catalog.
We need a place to store the url of the cat image, so we will add a custom field to the Product entity. This is done by modifying the VendureConfig object in the the plugin's configure method:
import { VendurePlugin } from '@vendure/core';
export class RandomCatPlugin implements VendurePlugin {
configure(config) {
config.customFields.Product.push({
type: 'string',
name: 'catImageUrl',
});
return config;
}
}
Now we will create a service which is responsible for making the HTTP call to the random.cat API and returning the URL of a random cat image:
import http from 'http';
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatFetcher {
/** Fetch a random cat image url from random.cat */
fetchCat(): Promise<string> {
return new Promise((resolve) => {
http.get('http://aws.random.cat/meow', (resp) => {
let data = '';
resp.on('data', chunk => data += chunk);
resp.on('end', () => resolve(JSON.parse(data).file));
});
});
}
}
{{% alert %}}
The @Injectable() decorator is part of the underlying Nest framework, and allows us to make use of Nest's powerful dependency injection features. In this case, we'll be able to inject the CatFetcher service into the resolver which we will soon create.
{{% /alert %}}
{{% alert "warning" %}}
To use decorators with TypeScript, you must set the "emitDecoratorMetadata" and "experimentalDecorators" compiler options to true in your tsconfig.json file.
{{% /alert %}}
Next we will define how the GraphQL API should be extended:
import gql from 'graphql-tag';
export class RandomCatPlugin implements VendurePlugin {
private schemaExtension = gql`
extend type Mutation {
addRandomCat(id: ID!): Product!
}
`;
configure(config) {
// as above
}
}
We will use this private schemaExtension variable in a later step.
Now that we've defined the new mutation, we'll need a resolver function to handle it. To do this, we'll create a new resolver class, following the Nest GraphQL resolver architecture. In short, this will be a class which has the @Resolver() decorator and features a method to handle our new mutation.
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { Ctx, Allow, ProductService, RequestContext } from '@vendure/core';
@Resolver()
export class RandomCatResolver {
constructor(private productService: ProductService, private catFetcher: CatFetcher) {}
@Mutation()
@Allow(Permission.UpdateCatalog)
async addRandomCat(@Ctx() ctx: RequestContext, @Args() args) {
const catImageUrl = await this.catFetcher.fetchCat();
return this.productService.update(ctx, {
id: args.id,
customFields: { catImageUrl },
});
}
}
Some explanations of this code are in order:
@Resolver() decorator tells Nest that this class contains GraphQL resolvers.CatFetcher class into the constructor of the resolver. We are also injecting an instance of the built-in ProductService class, which is responsible for operations on Products.@Mutation() decorator to mark this method as a resolver for a mutation with the corresponding name.@Allow() decorator enables us to define permissions restrictions on the mutation. Only those users whose permissions include UpdateCatalog may perform this operation. For a full list of available permissions, see the Permission enum.@Ctx() decorator injects the current RequestContext into the resolver. This provides information about the current request such as the current Session, User and Channel. It is required by most of the internal service methods.@Args() decorator injects the arguments passed to the mutation as an object.In order that out resolver is able to use Nest's dependency injection to inject and instance of CatFetcher, we must export it via the defineProviders method) in our plugin:
export class RandomCatPlugin implements VendurePlugin {
// ...
defineProviders() {
return [CatFetcher];
}
}
Now that we've defined the new mutation and we have a resolver capable of handling it, we just need to tell Vendure to extend the API. This is done with the extendAdminAPI method. If we wanted to extend the Shop API, we'd use the extendShopAPI method method instead.
export class RandomCatPlugin implements VendurePlugin {
// ...
extendAdminAPI() {
return {
schema: this.schemaExtension,
resolvers: [RandomCatResolver],
};
}
}
Finally we need to add an instance of our plugin to the config object with which we bootstrap out Vendure server:
import { bootstrap } from '@vendure/core';
bootstrap({
// .. config options
plugins: [
new RandomCatPlugin(),
],
});
Once we have started the Vendure server with the new config, we should be able to send the following GraphQL query to the Admin API:
mutation {
addRandomCat(id: "1") {
id
name
customFields {
catImageUrl
}
}
}
which should yield the following response:
{
"data": {
"addRandomCat": {
"id": "1",
"name": "Spiky Cactus",
"customFields": {
"catImageUrl": "https://purr.objects-us-east-1.dream.io/i/OoNx6.jpg"
}
}
}
}
import { Injectable } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import gql from 'graphql-tag';
import http from 'http';
import { Allow, Ctx, Permission, ProductService, RequestContext, VendureConfig, VendurePlugin } from '@vendure/core';
export class RandomCatPlugin implements VendurePlugin {
private schemaExtension = gql`
extend type Mutation {
addRandomCat(id: ID!): Product!
}
`;
configure(config: Required<VendureConfig>) {
config.customFields.Product.push({
type: 'string',
name: 'catImageUrl',
});
return config;
}
defineProviders() {
return [CatFetcher];
}
extendAdminAPI() {
return {
schema: this.schemaExtension,
resolvers: [RandomCatResolver],
};
}
}
@Injectable()
export class CatFetcher {
/** Fetch a random cat image url from random.cat */
fetchCat(): Promise<string> {
return new Promise((resolve) => {
http.get('http://aws.random.cat/meow', (resp) => {
let data = '';
resp.on('data', chunk => data += chunk);
resp.on('end', () => resolve(JSON.parse(data).file));
});
});
}
}
@Resolver()
export class RandomCatResolver {
constructor(private productService: ProductService, private catFetcher: CatFetcher) {}
@Mutation()
@Allow(Permission.UpdateCatalog)
async addRandomCat(@Ctx() ctx: RequestContext, @Args() args) {
const catImageUrl = await this.catFetcher.fetchCat();
return this.productService.update(ctx, {
id: args.id,
customFields: { catImageUrl },
});
}
}