Browse Source

feat(docs): Add docs on custom permissions, split examples into pages

Relates to #450
Michael Bromley 5 years ago
parent
commit
c7320032f7

+ 0 - 351
docs/content/docs/plugins/plugin-examples.md

@@ -1,351 +0,0 @@
----
-title: "Plugin Examples"
-weight: 4
-showtoc: true
----
- 
-# Plugin Examples 
-
-Here are some simplified examples of plugins which serve to illustrate what can be done with Vendure plugins. *Note: implementation details are skipped in these examples for the sake of brevity. A complete example with explanation can be found in [Writing A Vendure Plugin]({{< relref "writing-a-vendure-plugin" >}}).*
-
-{{< alert "primary" >}}
-  For a complete working example of a Vendure plugin, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
-{{< /alert >}}
-
-## Modifying the VendureConfig
-
-This example shows how to modify the VendureConfig, in this case by adding a custom field to allow product ratings.
-```TypeScript
-// my-plugin.ts
-import { VendurePlugin } from '@vendure/core';
-
-@VendurePlugin({
-  configuration: config => {
-    config.customFields.Product.push({
-      name: 'rating',
-      type: 'float',
-      min: 0,
-      max: 5,
-    });
-    return config;
-  },
-})
-class ProductRatingPlugin {}
-```
-
-## Defining a new database entity
-
-This example shows how new TypeORM database entities can be defined by plugins.
-
-```TypeScript
-// product-review.entity.ts
-import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { VendureEntity } from '@vendure/core';
-import { Column, Entity } from 'typeorm';
-
-@Entity()
-class ProductReview extends VendureEntity {
-  constructor(input?: DeepPartial<ProductReview>) {
-    super(input);
-  }
-
-  @Column()
-  text: string;
-  
-  @Column()
-  rating: number;
-}
-```
-```TypeScript
-// reviews-plugin.ts
-import { VendurePlugin } from '@vendure/core';
-import { ProductReview } from './product-review.entity';
-
-@VendurePlugin({
-  entites: [ProductReview],
-})
-export class ReviewsPlugin {}
-```
-
-## Extending the GraphQL API
-
-This example adds a new query to the GraphQL Admin API. It also demonstrates how [Nest's dependency injection](https://docs.nestjs.com/providers) can be used to encapsulate and inject services within the plugin module.
- 
-```TypeScript
-// top-sellers.resolver.ts
-import { Args, Query, Resolver } from '@nestjs/graphql';
-import { Ctx, RequestContext } from '@vendure/core'
-
-@Resolver()
-class TopSellersResolver {
-
-  constructor(private topSellersService: TopSellersService) {}
-
-  @Query()
-  topSellers(@Ctx() ctx: RequestContext, @Args() args: any) {
-    return this.topSellersService.getTopSellers(ctx, args.from, args.to);
-  }
-
-}
-```
-{{< alert "primary" >}}
-  **Note:** The `@Ctx` decorator gives you access to [the `RequestContext`]({{< relref "request-context" >}}), which is an object containing useful information about the current request - active user, current channel etc.
-{{< /alert >}}
-```TypeScript
-// top-sellers.service.ts
-import { Injectable } from '@nestjs/common';
-
-@Injectable()
-class TopSellersService { 
-  getTopSellers() { /* ... */ }
-}
-```
-
-```TypeScript
-// top-sellers.plugin.ts
-import gql from 'graphql-tag';
-import { VendurePlugin } from '@vendure/core';
-import { TopSellersService } from './top-sellers.service'
-import { TopSellersResolver } from './top-sellers.resolver'
-
-@VendurePlugin({
-  providers: [TopSellersService],
-  adminApiExtensions: {
-    schema: gql`
-      extend type Query {
-        topSellers(from: DateTime! to: DateTime!): [Product!]!
-      }
-    `,
-    resolvers: [TopSellersResolver]
-  }
-})
-export class TopSellersPlugin {}
-```
-
-Using the `gql` tag, it is possible to:
-
-* add new queries `extend type Query { ... }`
-* add new mutations (`extend type Mutation { ... }`)
-* define brand new types `type MyNewType { ... }`
-* add new fields to built-in types (`extend type Product { newField: String }`)
-
-## Adding a REST endpoint
-
-This plugin adds a single REST endpoint at `http://localhost:3000/products` which returns a list of all Products. Find out more about [Nestjs REST Controllers](https://docs.nestjs.com/controllers).
-```TypeScript
-// products.controller.ts
-import { Controller, Get } from '@nestjs/common';
-import { Ctx, ProductService, RequestContext } from '@vendure/core'; 
-
-@Controller('products')
-export class ProductsController {
-    constructor(private productService: ProductService) {}
-
-    @Get()
-    findAll(@Ctx() ctx: RequestContext) {
-        return this.productService.findAll(ctx);
-    }
-}
-```
-```TypeScript
-// rest.plugin.ts
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { ProductsController } from './products.controller';
-
-@VendurePlugin({
-    imports: [PluginCommonModule],
-    controllers: [ProductsController],
-})
-export class RestPlugin {}
-```
-
-{{< alert "primary" >}}
-  **Note:** [The `PluginCommonModule`]({{< relref "plugin-common-module" >}}) should be imported to gain access to Vendure core providers - in this case it is required in order to be able to inject `ProductService` into our controller.
-{{< /alert >}}
-
-Side note: since this uses no Vendure-specific metadata, it could also be written using the Nestjs `@Module()` decorator rather than the `@VendurePlugin()` decorator.
-
-## Running processes on the Worker
-
-This example shows how to set up a microservice running on the Worker process, as well as subscribing to events via the [EventBus]({{< relref "event-bus" >}}).
-
-Also see the docs for [WorkerService]({{< relref "worker-service" >}}).
-
-```TypeScript
-// order-processing.controller.ts
-import { asyncObservable, ID, Order, TransactionalConnection } from '@vendure/core';
-import { Controller } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
-import { ProcessOrderMessage } from './process-order-message';
-
-@Controller()
-class OrderProcessingController {
-
-  constructor(private connection: TransactionalConnection) {}
-
-  @MessagePattern(ProcessOrderMessage.pattern)
-  async processOrder({ orderId }: ProcessOrderMessage['data']) {
-    const order = await this.connection.getRepository(Order).findOne(orderId);
-    // ...do some expensive / slow computation
-    return true;
-  }
-
-}
-```
-* This controller will be executed as a microservice in the [Vendure worker process]({{< relref "vendure-worker" >}}). This makes it suitable for long-running or resource-intensive tasks that you do not want to interfere with the main process which is handling GraphQL API requests.
-* Messages are sent to the worker using [WorkerMessages]({{< relref "worker-message" >}}), each of which has a unique pattern and can include a payload of data sent from the main process.
-* The return value of the method should correspond to the return type of the WorkerMessage (the second generic argument, `boolean` in the case of `ProcessOrderMessage` - see next snippet)
-
-```TypeScript
-// process-order-message.ts
-import { ID, WorkerMessage } from '@vendure/core';
-
-export class ProcessOrderMessage extends WorkerMessage<{ orderId: ID }, boolean> {
-  static readonly pattern = 'ProcessOrder';
-}
-```
-
-The `ProcessOrderMessage` is sent in response to certain events:
-
-```TypeScript
-import { OnVendureBootstrap, OrderStateTransitionEvent, PluginCommonModule, 
-  VendurePlugin, WorkerService, EventBus } from '@vendure/core';
-import { OrderProcessingController } from './process-order.controller';
-import { ProcessOrderMessage } from './process-order-message';
-
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  workers: [OrderProcessingController],
-})
-export class OrderAnalyticsPlugin implements OnVendureBootstrap {
-
-  constructor(
-    private workerService: WorkerService,
-    private eventBus: EventBus,
-  ) {}
-  
-  /**
-   * When the server bootstraps, set up a subscription for events 
-   * published whenever  an Order changes state. When an Order has 
-   * been fulfilled, we send a message to the controller running on
-   * the Worker process to let it process that order.
-   */
-  onVendureBootstrap() {
-    this.eventBus.ofType(OrderStateTransitionEvent).subscribe(event => {
-      if (event.toState === 'Delivered') {
-        this.workerService.send(new ProcessOrderMessage({ orderId: event.order.id })).subscribe();
-      }
-    });
-  }
-
-}
-```
-
-## Using the JobQueueService
-
-If your plugin involves long-running tasks, you can take advantage of the [job queue system]({{< relref "/docs/developer-guide/job-queue" >}}) that comes with Vendure. This example defines a mutation that can be used to transcode and link a video to a Product's customFields.
-
-```TypeScript
-// product-video.resolver.ts
-import { Args, Mutation, Resolver } from '@nestjs/graphql';
-import { Ctx, RequestContext } from '@vendure/core'
-import { ProductVideoService } from './product-video.service';
-
-@Resolver()
-class ProductVideoResolver {
-
-  constructor(private productVideoService: ProductVideoService) {}
-
-  @Mutation()
-  addVideoToProduct(@Args() args: any) {
-    return this.productVideoService.transcodeForProduct(
-      args.productId, 
-      args.videoUrl,
-    );
-  }
-
-}
-```
-The resolver just defines how to handle the new `addVideoToProduct` mutation, delegating the actual work to the `ProductVideoService`:
-```TypeScript
-// product-video.service.ts
-import { Injectable, OnModuleInit } from '@nestjs/common';
-import { JobQueue, JobQueueService, ID, Product, TransactionalConnection } from '@vendure/core';
-import { transcode } from 'third-party-video-sdk'; 
-
-let jobQueue: JobQueue<{ productId: ID; videoUrl: string; }>;
-
-@Injectable()
-class ProductVideoService implements OnModuleInit { 
-  
-  constructor(private jobQueueService: JobQueueService, 
-              private connection: TransactionalConnection) {}
-
-  onModuleInit() {
-    // This check ensures that only a single JobQueue is created, even if this
-    // service gets instantiated more than once.
-    if (!jobQueue) {
-      jobQueue = this.jobQueueService.createQueue({
-        name: 'transcode-video',
-        concurrency: 5,
-        process: async job => {
-          // Here we define how each job in the queue will be processed.
-          // In this case we call out to some imaginary 3rd-party video
-          // transcoding API, which performs the work and then
-          // returns a new URL of the transcoded video, which we can then
-          // associate with the Product via the customFields.
-          try {
-            const result = await transcode(job.data.videoId);
-            await this.connection.getRepository(Product).save({
-              id: job.data.productId,
-              customFields: {
-                videoUrl: result.url,
-              },
-            });
-            job.complete(result);
-          } catch (e) {
-            job.fail(e);
-          }
-        },
-      });
-    }
-  }
-
-  transcodeForProduct(productId: ID, videoUrl: string) { 
-    // Add a new job to the queue and immediately return the
-    // job itself.
-    return jobQueue.add({ productId, videoUrl });
-  }
-}
-```
-The `ProductVideoService` is in charge of setting up the JobQueue and adding jobs to that queue.
-
-```TypeScript
-// product-video.plugin.ts
-import gql from 'graphql-tag';
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { ProductVideoService } from './product-video.service'
-import { ProductVideoResolver } from './product-video.resolver'
-
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  providers: [ProductVideoService],
-  adminApiExtensions: {
-    schema: gql`
-      extend type Mutation {
-        addVideoToProduct(productId: ID! videoUrl: String!): Job!
-      }
-    `,
-    resolvers: [ProductVideoResolver]
-  },
-  configuration: config => {
-    config.customFields.Product.push({
-      name: 'videoUrl',
-      type: 'string',
-    });
-    return config;
-  }
-})
-export class ProductVideoPlugin {}
-```
-Finally, the `ProductVideoPlugin` brings it all together, extending the GraphQL API, defining the required CustomField to store the transcoded video URL, and registering our service and resolver. The [PluginCommonModule]({{< relref "plugin-common-module" >}}) is imported as it exports the JobQueueService.

+ 13 - 0
docs/content/docs/plugins/plugin-examples/_index.md

@@ -0,0 +1,13 @@
+---
+title: "Plugin Examples"
+weight: 4
+showtoc: true
+---
+
+# Plugin Examples 
+
+Here are some simplified examples of plugins which serve to illustrate what can be done with Vendure plugins. *Note: implementation details are skipped in these examples for the sake of brevity. A complete example with explanation can be found in [Writing A Vendure Plugin]({{< relref "writing-a-vendure-plugin" >}}).*
+
+{{< alert "primary" >}}
+  For a complete working example of a Vendure plugin, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
+{{< /alert >}}

+ 40 - 0
docs/content/docs/plugins/plugin-examples/adding-rest-endpoint.md

@@ -0,0 +1,40 @@
+---
+title: "Adding a REST endpoint"
+showtoc: true
+---
+
+# Adding a REST endpoint
+
+This plugin adds a single REST endpoint at `http://localhost:3000/products` which returns a list of all Products. Find out more about [Nestjs REST Controllers](https://docs.nestjs.com/controllers).
+```TypeScript
+// products.controller.ts
+import { Controller, Get } from '@nestjs/common';
+import { Ctx, ProductService, RequestContext } from '@vendure/core'; 
+
+@Controller('products')
+export class ProductsController {
+    constructor(private productService: ProductService) {}
+
+    @Get()
+    findAll(@Ctx() ctx: RequestContext) {
+        return this.productService.findAll(ctx);
+    }
+}
+```
+```TypeScript
+// rest.plugin.ts
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { ProductsController } from './products.controller';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    controllers: [ProductsController],
+})
+export class RestPlugin {}
+```
+
+{{< alert "primary" >}}
+  **Note:** [The `PluginCommonModule`]({{< relref "plugin-common-module" >}}) should be imported to gain access to Vendure core providers - in this case it is required in order to be able to inject `ProductService` into our controller.
+{{< /alert >}}
+
+Side note: since this uses no Vendure-specific metadata, it could also be written using the Nestjs `@Module()` decorator rather than the `@VendurePlugin()` decorator.

+ 75 - 0
docs/content/docs/plugins/plugin-examples/defining-custom-permissions.md

@@ -0,0 +1,75 @@
+---
+title: "Defining custom permissions"
+showtoc: true
+---
+
+# Defining custom permissions
+
+Your plugin may be defining new queries & mutations which require new permissions specific to those operations. This can be done by creating [PermissionDefinitions]({{< relref "permission-definition" >}}).
+
+For example, let's imagine you are creating a plugin which exposes a new mutation that can be used by remote services to sync your inventory. First of all we will define the new permission:
+
+```TypeScript
+// sync-permission.ts
+import { PermissionDefinition } from '@vendure/core';
+
+export const sync = new PermissionDefinition({
+  name: 'SyncInventory',
+  description: 'Allows syncing stock levels via Admin API'
+});
+```
+
+This permission can then be used in conjuction with the [@Allow() decorator]({{< relref "allow-decorator">}}) to limit access to the mutation:
+
+```TypeScript
+// inventory-sync.resolver.ts
+import { Allow } from '@vendure/core';
+import { Mutation, Resolver } from '@nestjs/graphql';
+import { sync } from './sync-permission';
+
+@Resolver()
+export class InventorySyncResolver {
+
+  @Allow(sync.Permission)
+  @Mutation()
+  syncInventory() {
+    // ...
+  }
+}
+```
+
+Finally, the `sync` PermissionDefinition must be passed into the VendureConfig so that Vendure knows about this new custom permission:
+
+```TypeScript {hl_lines=[21]}
+// inventory-sync.plugin.ts
+import gql from 'graphql-tag';
+import { VendurePlugin } from '@vendure/core';
+import { InventorySyncResolver } from './inventory-sync.resolver'
+import { sync } from './sync-permission';
+
+@VendurePlugin({
+  adminApiExtensions: {
+    schema: gql`
+      input InventoryDataInput {
+        # omitted for brevity
+      }
+
+      extend type Mutation {
+        syncInventory(input: InventoryDataInput!): Boolean!
+      }
+    `,
+    resolvers: [InventorySyncResolver]
+  },
+  configuration: config => {
+    config.authOptions.customPermissions.push(sync);
+    return config;
+  },
+})
+export class InventorySyncPlugin {}
+```
+
+On starting the Vendure server, this custom permission will now be visible in the Role detail view of the Admin UI, and can be assigned to Roles.
+
+## Custom CRUD permissions
+
+Quite often your plugin will define a new entity on which you must perform create, read, update and delete (CRUD) operations. In this case, you can use the [CrudPermissionDefinition]({{< relref "permission-definition" >}}#crudpermissiondefinition) which simplifies the creation of the set of 4 CRUD permissions. See the docs for an example of usage.

+ 45 - 0
docs/content/docs/plugins/plugin-examples/defining-db-entity.md

@@ -0,0 +1,45 @@
+---
+title: "Defining a new database entity"
+showtoc: true
+---
+
+# Defining a new database entity
+
+This example shows how new TypeORM database entities can be defined by plugins.
+
+```TypeScript
+// product-review.entity.ts
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { VendureEntity } from '@vendure/core';
+import { Column, Entity } from 'typeorm';
+
+@Entity()
+class ProductReview extends VendureEntity {
+  constructor(input?: DeepPartial<ProductReview>) {
+    super(input);
+  }
+
+  @Column()
+  text: string;
+  
+  @Column()
+  rating: number;
+}
+```
+
+{{< alert "primary" >}}
+  **Note** Any custom entities *must* extend the [`VendureEntity`]({{< relref "vendure-entity" >}}) class.
+{{< /alert >}}
+
+The new entity is then passed to the `entities` array of the VendurePlugin metadata:
+
+```TypeScript {hl_lines=[6]}
+// reviews-plugin.ts
+import { VendurePlugin } from '@vendure/core';
+import { ProductReview } from './product-review.entity';
+
+@VendurePlugin({
+  entites: [ProductReview],
+})
+export class ReviewsPlugin {}
+```

+ 66 - 0
docs/content/docs/plugins/plugin-examples/extending-graphql-api.md

@@ -0,0 +1,66 @@
+---
+title: "Extending the GraphQL API"
+showtoc: true
+---
+
+# Extending the GraphQL API
+
+This example adds a new query to the GraphQL Admin API. It also demonstrates how [Nest's dependency injection](https://docs.nestjs.com/providers) can be used to encapsulate and inject services within the plugin module.
+ 
+```TypeScript
+// top-sellers.resolver.ts
+import { Args, Query, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext } from '@vendure/core'
+
+@Resolver()
+class TopSellersResolver {
+
+  constructor(private topSellersService: TopSellersService) {}
+
+  @Query()
+  topSellers(@Ctx() ctx: RequestContext, @Args() args: any) {
+    return this.topSellersService.getTopSellers(ctx, args.from, args.to);
+  }
+
+}
+```
+{{< alert "primary" >}}
+  **Note:** The `@Ctx` decorator gives you access to [the `RequestContext`]({{< relref "request-context" >}}), which is an object containing useful information about the current request - active user, current channel etc.
+{{< /alert >}}
+```TypeScript
+// top-sellers.service.ts
+import { Injectable } from '@nestjs/common';
+
+@Injectable()
+class TopSellersService { 
+  getTopSellers() { /* ... */ }
+}
+```
+
+```TypeScript
+// top-sellers.plugin.ts
+import gql from 'graphql-tag';
+import { VendurePlugin } from '@vendure/core';
+import { TopSellersService } from './top-sellers.service'
+import { TopSellersResolver } from './top-sellers.resolver'
+
+@VendurePlugin({
+  providers: [TopSellersService],
+  adminApiExtensions: {
+    schema: gql`
+      extend type Query {
+        topSellers(from: DateTime! to: DateTime!): [Product!]!
+      }
+    `,
+    resolvers: [TopSellersResolver]
+  }
+})
+export class TopSellersPlugin {}
+```
+
+Using the `gql` tag, it is possible to:
+
+* add new queries `extend type Query { ... }`
+* add new mutations (`extend type Mutation { ... }`)
+* define brand new types `type MyNewType { ... }`
+* add new fields to built-in types (`extend type Product { newField: String }`)

+ 28 - 0
docs/content/docs/plugins/plugin-examples/modifying-config.md

@@ -0,0 +1,28 @@
+---
+title: "Modifying the VendureConfig"
+showtoc: true
+---
+
+# Modifying the VendureConfig
+
+The VendureConfig object defines all aspects of how a Vendure server works. A plugin may modify any part of it by defining a `configuration` function in the VendurePlugin metadata.
+
+This example shows how to modify the VendureConfig, in this case by adding a custom field to allow product ratings.
+
+```TypeScript
+// my-plugin.ts
+import { VendurePlugin } from '@vendure/core';
+
+@VendurePlugin({
+  configuration: config => {
+    config.customFields.Product.push({
+      name: 'rating',
+      type: 'float',
+      min: 0,
+      max: 5,
+    });
+    return config;
+  },
+})
+class ProductRatingPlugin {}
+```

+ 81 - 0
docs/content/docs/plugins/plugin-examples/running-on-worker-process.md

@@ -0,0 +1,81 @@
+---
+title: "Running processes on the Worker"
+weight: 4
+showtoc: true
+---
+
+# Running processes on the Worker
+
+This example shows how to set up a microservice running on the Worker process, as well as subscribing to events via the [EventBus]({{< relref "event-bus" >}}).
+
+Also see the docs for [WorkerService]({{< relref "worker-service" >}}).
+
+```TypeScript
+// order-processing.controller.ts
+import { asyncObservable, ID, Order, TransactionalConnection } from '@vendure/core';
+import { Controller } from '@nestjs/common';
+import { MessagePattern } from '@nestjs/microservices';
+import { ProcessOrderMessage } from './process-order-message';
+
+@Controller()
+class OrderProcessingController {
+
+  constructor(private connection: TransactionalConnection) {}
+
+  @MessagePattern(ProcessOrderMessage.pattern)
+  async processOrder({ orderId }: ProcessOrderMessage['data']) {
+    const order = await this.connection.getRepository(Order).findOne(orderId);
+    // ...do some expensive / slow computation
+    return true;
+  }
+
+}
+```
+* This controller will be executed as a microservice in the [Vendure worker process]({{< relref "vendure-worker" >}}). This makes it suitable for long-running or resource-intensive tasks that you do not want to interfere with the main process which is handling GraphQL API requests.
+* Messages are sent to the worker using [WorkerMessages]({{< relref "worker-message" >}}), each of which has a unique pattern and can include a payload of data sent from the main process.
+* The return value of the method should correspond to the return type of the WorkerMessage (the second generic argument, `boolean` in the case of `ProcessOrderMessage` - see next snippet)
+
+```TypeScript
+// process-order-message.ts
+import { ID, WorkerMessage } from '@vendure/core';
+
+export class ProcessOrderMessage extends WorkerMessage<{ orderId: ID }, boolean> {
+  static readonly pattern = 'ProcessOrder';
+}
+```
+
+The `ProcessOrderMessage` is sent in response to certain events:
+
+```TypeScript
+import { OnVendureBootstrap, OrderStateTransitionEvent, PluginCommonModule, 
+  VendurePlugin, WorkerService, EventBus } from '@vendure/core';
+import { OrderProcessingController } from './process-order.controller';
+import { ProcessOrderMessage } from './process-order-message';
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  workers: [OrderProcessingController],
+})
+export class OrderAnalyticsPlugin implements OnVendureBootstrap {
+
+  constructor(
+    private workerService: WorkerService,
+    private eventBus: EventBus,
+  ) {}
+  
+  /**
+   * When the server bootstraps, set up a subscription for events 
+   * published whenever  an Order changes state. When an Order has 
+   * been fulfilled, we send a message to the controller running on
+   * the Worker process to let it process that order.
+   */
+  onVendureBootstrap() {
+    this.eventBus.ofType(OrderStateTransitionEvent).subscribe(event => {
+      if (event.toState === 'Delivered') {
+        this.workerService.send(new ProcessOrderMessage({ orderId: event.order.id })).subscribe();
+      }
+    });
+  }
+
+}
+```

+ 114 - 0
docs/content/docs/plugins/plugin-examples/using-job-queue-service.md

@@ -0,0 +1,114 @@
+---
+title: "Using the JobQueueService"
+weight: 4
+showtoc: true
+---
+
+# Using the JobQueueService
+
+If your plugin involves long-running tasks, you can take advantage of the [job queue system]({{< relref "/docs/developer-guide/job-queue" >}}) that comes with Vendure. This example defines a mutation that can be used to transcode and link a video to a Product's customFields.
+
+```TypeScript
+// product-video.resolver.ts
+import { Args, Mutation, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext } from '@vendure/core'
+import { ProductVideoService } from './product-video.service';
+
+@Resolver()
+class ProductVideoResolver {
+
+  constructor(private productVideoService: ProductVideoService) {}
+
+  @Mutation()
+  addVideoToProduct(@Args() args: any) {
+    return this.productVideoService.transcodeForProduct(
+      args.productId, 
+      args.videoUrl,
+    );
+  }
+
+}
+```
+The resolver just defines how to handle the new `addVideoToProduct` mutation, delegating the actual work to the `ProductVideoService`:
+```TypeScript
+// product-video.service.ts
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { JobQueue, JobQueueService, ID, Product, TransactionalConnection } from '@vendure/core';
+import { transcode } from 'third-party-video-sdk'; 
+
+let jobQueue: JobQueue<{ productId: ID; videoUrl: string; }>;
+
+@Injectable()
+class ProductVideoService implements OnModuleInit { 
+  
+  constructor(private jobQueueService: JobQueueService, 
+              private connection: TransactionalConnection) {}
+
+  onModuleInit() {
+    // This check ensures that only a single JobQueue is created, even if this
+    // service gets instantiated more than once.
+    if (!jobQueue) {
+      jobQueue = this.jobQueueService.createQueue({
+        name: 'transcode-video',
+        concurrency: 5,
+        process: async job => {
+          // Here we define how each job in the queue will be processed.
+          // In this case we call out to some imaginary 3rd-party video
+          // transcoding API, which performs the work and then
+          // returns a new URL of the transcoded video, which we can then
+          // associate with the Product via the customFields.
+          try {
+            const result = await transcode(job.data.videoId);
+            await this.connection.getRepository(Product).save({
+              id: job.data.productId,
+              customFields: {
+                videoUrl: result.url,
+              },
+            });
+            job.complete(result);
+          } catch (e) {
+            job.fail(e);
+          }
+        },
+      });
+    }
+  }
+
+  transcodeForProduct(productId: ID, videoUrl: string) { 
+    // Add a new job to the queue and immediately return the
+    // job itself.
+    return jobQueue.add({ productId, videoUrl });
+  }
+}
+```
+The `ProductVideoService` is in charge of setting up the JobQueue and adding jobs to that queue.
+
+```TypeScript
+// product-video.plugin.ts
+import gql from 'graphql-tag';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { ProductVideoService } from './product-video.service'
+import { ProductVideoResolver } from './product-video.resolver'
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  providers: [ProductVideoService],
+  adminApiExtensions: {
+    schema: gql`
+      extend type Mutation {
+        addVideoToProduct(productId: ID! videoUrl: String!): Job!
+      }
+    `,
+    resolvers: [ProductVideoResolver]
+  },
+  configuration: config => {
+    config.customFields.Product.push({
+      name: 'videoUrl',
+      type: 'string',
+    });
+    return config;
+  }
+})
+export class ProductVideoPlugin {}
+```
+Finally, the `ProductVideoPlugin` brings it all together, extending the GraphQL API, defining the required CustomField to store the transcoded video URL, and registering our service and resolver. The [PluginCommonModule]({{< relref "plugin-common-module" >}}) is imported as it exports the JobQueueService.

+ 4 - 2
docs/content/docs/plugins/writing-a-vendure-plugin.md

@@ -92,7 +92,7 @@ Now that we've defined the new mutation, we'll need a resolver function to handl
 
 ```TypeScript
 import { Args, Mutation, Resolver } from '@nestjs/graphql';
-import { Ctx, Allow, ProductService, RequestContext } from '@vendure/core';
+import { Ctx, Allow, ProductService, RequestContext, Transaction } from '@vendure/core';
 import { Permission } from '@vendure/common/lib/generated-types';
 
 @Resolver()
@@ -100,6 +100,7 @@ export class RandomCatResolver {
 
   constructor(private productService: ProductService, private catFetcher: CatFetcher) {}
 
+  @Transaction()
   @Mutation()
   @Allow(Permission.UpdateCatalog)
   async addRandomCat(@Ctx() ctx: RequestContext, @Args() args) {
@@ -116,8 +117,9 @@ Some explanations of this code are in order:
 
 * The `@Resolver()` decorator tells Nest that this class contains GraphQL resolvers.
 * We are able to use Nest's dependency injection to inject an instance of our `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.
+* We use the `@Transaction()` decorator to ensure that all database operations in this resolver are run within a transaction. This ensure that if any part of it fails, all changes will be rolled back, keeping our data in a consistent state. For more on this, see the [Transaction Decorator docs]({{< relref "transaction-decorator" >}}).
 * We use the `@Mutation()` decorator to mark this method as a resolver for a mutation with the corresponding name.
-* The `@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]({{< relref "/docs/graphql-api/admin/enums" >}}#permission).
+* The `@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]({{< relref "/docs/graphql-api/admin/enums" >}}#permission). Plugins may also define custom permissions, see [Defining customer permissions]({{< relref "defining-custom-permissions" >}}).
 * 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.