|
|
@@ -5,57 +5,53 @@ weight: 0
|
|
|
|
|
|
# Writing a Vendure Plugin
|
|
|
|
|
|
-A Vendure plugin is a class which implements the [`VendurePlugin` interface]({{< relref "vendure-plugin" >}}). This interface allows the plugin to:
|
|
|
-
|
|
|
-1. Modify the [VendureConfig]({{< relref "vendure-config" >}}) object.
|
|
|
-2. Extend the GraphQL API, including modifying existing types and adding completely new queries and mutations.
|
|
|
-3. Define new database entities and interact directly with the database.
|
|
|
-4. Run code before the server bootstraps, such as starting webservers.
|
|
|
+This is a complete example of how to implement a simple plugin step-by-step.
|
|
|
|
|
|
## Example: RandomCatPlugin
|
|
|
|
|
|
-Let's learn about these capabilities by writing a plugin which defines a new database entity and GraphQL mutation.
|
|
|
+Let's learn about Vendure plugins 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](http://random.cat) to any product in out catalog.
|
|
|
+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](http://random.cat) to any product in our catalog.
|
|
|
|
|
|
### Step 1: Define a new custom field
|
|
|
|
|
|
-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]({{< relref "vendure-plugin" >}}#configure):
|
|
|
+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 via the plugin's [`configuration` metadata property]({{< relref "vendure-plugin-metadata" >}}#configuration):
|
|
|
|
|
|
-```ts
|
|
|
+```TypeScript
|
|
|
import { VendurePlugin } from '@vendure/core';
|
|
|
|
|
|
-export class RandomCatPlugin implements VendurePlugin {
|
|
|
- configure(config) {
|
|
|
- config.customFields.Product.push({
|
|
|
- type: 'string',
|
|
|
- name: 'catImageUrl',
|
|
|
- });
|
|
|
- return config;
|
|
|
- }
|
|
|
-}
|
|
|
+@VendurePlugin({
|
|
|
+ configuration: config => {
|
|
|
+ config.customFields.Product.push({
|
|
|
+ type: 'string',
|
|
|
+ name: 'catImageUrl',
|
|
|
+ });
|
|
|
+ return config;
|
|
|
+ }
|
|
|
+})
|
|
|
+export class RandomCatPlugin {}
|
|
|
```
|
|
|
|
|
|
### Step 2: Create a service to fetch the data
|
|
|
|
|
|
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:
|
|
|
|
|
|
-```ts
|
|
|
+```TypeScript
|
|
|
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));
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
+ /** 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));
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
```
|
|
|
|
|
|
@@ -72,30 +68,23 @@ To use decorators with TypeScript, you must set the "emitDecoratorMetadata" and
|
|
|
|
|
|
Next we will define how the GraphQL API should be extended:
|
|
|
|
|
|
-```ts
|
|
|
+```TypeScript
|
|
|
import gql from 'graphql-tag';
|
|
|
|
|
|
-export class RandomCatPlugin implements VendurePlugin {
|
|
|
-
|
|
|
- private schemaExtension = gql`
|
|
|
- extend type Mutation {
|
|
|
- addRandomCat(id: ID!): Product!
|
|
|
- }
|
|
|
- `;
|
|
|
-
|
|
|
- configure(config) {
|
|
|
- // as above
|
|
|
- }
|
|
|
-}
|
|
|
+const schemaExtension = gql`
|
|
|
+ extend type Mutation {
|
|
|
+ addRandomCat(id: ID!): Product!
|
|
|
+ }
|
|
|
+`;
|
|
|
```
|
|
|
|
|
|
-We will use this private `schemaExtension` variable in a later step.
|
|
|
+We will use this `schemaExtension` variable in a later step.
|
|
|
|
|
|
### Step 4: Create a resolver
|
|
|
|
|
|
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](https://docs.nestjs.com/graphql/resolvers-map). In short, this will be a class which has the `@Resolver()` decorator and features a method to handle our new mutation.
|
|
|
|
|
|
-```ts
|
|
|
+```TypeScript
|
|
|
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
|
|
import { Ctx, Allow, ProductService, RequestContext } from '@vendure/core';
|
|
|
import { Permission } from '@vendure/common/lib/generated-types';
|
|
|
@@ -103,17 +92,17 @@ import { Permission } from '@vendure/common/lib/generated-types';
|
|
|
@Resolver()
|
|
|
export class RandomCatResolver {
|
|
|
|
|
|
- constructor(private productService: ProductService, private catFetcher: CatFetcher) {}
|
|
|
+ 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 },
|
|
|
- });
|
|
|
- }
|
|
|
+ @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 },
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
```
|
|
|
|
|
|
@@ -126,55 +115,70 @@ Some explanations of this code are in order:
|
|
|
* The `@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.
|
|
|
* The `@Args()` decorator injects the arguments passed to the mutation as an object.
|
|
|
|
|
|
-### Step 5: Export any providers used in the resolver
|
|
|
+### Step 5: Import the PluginCommonModule
|
|
|
|
|
|
-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](({{< relref "vendure-plugin" >}}#defineproviders)) in our plugin:
|
|
|
+In the RandomCatResolver we make use of the ProductService. This is one of the built-in services which Vendure uses internally to interact with the database. It can be injected into our resolver only if we first import the [PluginCommonModule]({{< relref "plugin-common-module" >}}), which is a module which exports a number of providers which are usually needed by plugins.
|
|
|
|
|
|
-```ts
|
|
|
-export class RandomCatPlugin implements VendurePlugin {
|
|
|
+```TypeScript
|
|
|
+@VendurePlugin({
|
|
|
+ imports: [PluginCommonModule],
|
|
|
+ configuration: config => {
|
|
|
+ // omitted
|
|
|
+ }
|
|
|
+})
|
|
|
+export class RandomCatPlugin {}
|
|
|
+```
|
|
|
|
|
|
- // ...
|
|
|
+### Step 6: Declare any providers used in the resolver
|
|
|
|
|
|
- defineProviders() {
|
|
|
- return [CatFetcher];
|
|
|
- }
|
|
|
-}
|
|
|
-```
|
|
|
+In order that out resolver is able to use Nest's dependency injection to inject and instance of `CatFetcher`, we must add it to the `providers` array in our plugin:
|
|
|
|
|
|
-### Step 6: Extend the GraphQL API
|
|
|
+```TypeScript
|
|
|
+@VendurePlugin({
|
|
|
+ imports: [PluginCommonModule],
|
|
|
+ providers: [CatFetcher],
|
|
|
+ configuration: config => {
|
|
|
+ // omitted
|
|
|
+ }
|
|
|
+})
|
|
|
+export class RandomCatPlugin {}
|
|
|
+```
|
|
|
|
|
|
-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]({{< relref "vendure-plugin" >}}#extendadminapi). If we wanted to extend the Shop API, we'd use the [`extendShopAPI` method]({{< relref "vendure-plugin" >}}#extendshopapi) method instead.
|
|
|
+### Step 7: Extend the GraphQL API
|
|
|
|
|
|
-```ts
|
|
|
-export class RandomCatPlugin implements VendurePlugin {
|
|
|
+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 [`adminApiExtensions` metadata property]({{< relref "vendure-plugin-metadata" >}}#adminapiextensions). If we wanted to extend the Shop API, we'd use the [`adminApiExtensions` metadata property]({{< relref "vendure-plugin-metadata" >}}#shopapiextensions) instead.
|
|
|
|
|
|
- // ...
|
|
|
-
|
|
|
- extendAdminAPI() {
|
|
|
- return {
|
|
|
- schema: this.schemaExtension,
|
|
|
- resolvers: [RandomCatResolver],
|
|
|
- };
|
|
|
- }
|
|
|
-}
|
|
|
+```TypeScript
|
|
|
+@VendurePlugin({
|
|
|
+ imports: [PluginCommonModule],
|
|
|
+ providers: [CatFetcher],
|
|
|
+ adminApiExtensions: {
|
|
|
+ schema: schemaExtension,
|
|
|
+ resolvers: [RandomCatResolver],
|
|
|
+ },
|
|
|
+ configuration: config => {
|
|
|
+ // omitted
|
|
|
+ }
|
|
|
+})
|
|
|
+export class RandomCatPlugin {}
|
|
|
```
|
|
|
|
|
|
-### Step 7: Add the plugin to the Vendure config
|
|
|
+### Step 8: Add the plugin to the Vendure config
|
|
|
|
|
|
Finally we need to add an instance of our plugin to the config object with which we bootstrap out Vendure server:
|
|
|
|
|
|
-```ts
|
|
|
+```TypeScript
|
|
|
import { bootstrap } from '@vendure/core';
|
|
|
|
|
|
bootstrap({
|
|
|
// .. config options
|
|
|
plugins: [
|
|
|
- new RandomCatPlugin(),
|
|
|
+ RandomCatPlugin,
|
|
|
],
|
|
|
});
|
|
|
```
|
|
|
|
|
|
-### Step 8: Test the plugin
|
|
|
+### Step 9: Test the plugin
|
|
|
|
|
|
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:
|
|
|
|
|
|
@@ -192,7 +196,7 @@ mutation {
|
|
|
|
|
|
which should yield the following response:
|
|
|
|
|
|
-```JSON
|
|
|
+```JSON
|
|
|
{
|
|
|
"data": {
|
|
|
"addRandomCat": {
|
|
|
@@ -208,7 +212,7 @@ which should yield the following response:
|
|
|
|
|
|
### Full example plugin
|
|
|
|
|
|
-```ts
|
|
|
+```TypeScript
|
|
|
import { Injectable } from '@nestjs/common';
|
|
|
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
|
|
import gql from 'graphql-tag';
|
|
|
@@ -216,60 +220,55 @@ import http from 'http';
|
|
|
import { Allow, Ctx, ProductService, RequestContext, VendureConfig, VendurePlugin } from '@vendure/core';
|
|
|
import { Permission } from '@vendure/common/lib/generated-types';
|
|
|
|
|
|
-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];
|
|
|
+const schemaExtension = gql`
|
|
|
+ extend type Mutation {
|
|
|
+ addRandomCat(id: ID!): Product!
|
|
|
}
|
|
|
-
|
|
|
- extendAdminAPI() {
|
|
|
- return {
|
|
|
- schema: this.schemaExtension,
|
|
|
- resolvers: [RandomCatResolver],
|
|
|
- };
|
|
|
- }
|
|
|
-}
|
|
|
+`;
|
|
|
+
|
|
|
+@VendurePlugin({
|
|
|
+ imports: [PluginCommonModule],
|
|
|
+ providers: [CatFetcher],
|
|
|
+ adminApiExtensions: {
|
|
|
+ schema: schemaExtension,
|
|
|
+ resolvers: [RandomCatResolver],
|
|
|
+ },
|
|
|
+ configuration: config => {
|
|
|
+ config.customFields.Product.push({
|
|
|
+ type: 'string',
|
|
|
+ name: 'catImageUrl',
|
|
|
+ });
|
|
|
+ return config;
|
|
|
+ }
|
|
|
+})
|
|
|
+export class RandomCatPlugin {}
|
|
|
|
|
|
@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));
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
+ /** 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 },
|
|
|
- });
|
|
|
- }
|
|
|
+ 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 },
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
```
|