Browse Source

docs: More docs

Michael Bromley 2 years ago
parent
commit
5c48705dc3
29 changed files with 1547 additions and 868 deletions
  1. 41 31
      docs/docs/guides/advanced-topics/defining-db-subscribers.md
  2. 2 2
      docs/docs/guides/advanced-topics/error-handling.md
  3. 9 7
      docs/docs/guides/advanced-topics/logging.md
  4. 1 1
      docs/docs/guides/advanced-topics/migrating-from-v1/breaking-api-changes.md
  5. 1 1
      docs/docs/guides/advanced-topics/migrating-from-v1/database-migration.md
  6. 3 3
      docs/docs/guides/advanced-topics/migrating-from-v1/index.md
  7. 1 0
      docs/docs/guides/advanced-topics/migrating-from-v1/storefront-migration.md
  8. 80 59
      docs/docs/guides/advanced-topics/testing.md
  9. 2 2
      docs/docs/guides/advanced-topics/translations.md
  10. 5 2
      docs/docs/guides/advanced-topics/updating-vendure.md
  11. 291 0
      docs/docs/guides/developer-guide/events/index.mdx
  12. 1 1
      docs/docs/guides/developer-guide/migrations/index.md
  13. 28 0
      docs/docs/guides/developer-guide/plugins/index.mdx
  14. 3 58
      docs/docs/guides/developer-guide/the-api-layer/index.mdx
  15. 153 0
      docs/docs/guides/how-to/custom-permissions/index.md
  16. 120 0
      docs/docs/guides/how-to/database-entity/index.md
  17. 478 0
      docs/docs/guides/how-to/extend-graphql-api/index.md
  18. 195 0
      docs/docs/guides/how-to/job-queue/index.md
  19. 99 0
      docs/docs/guides/how-to/rest-endpoint/index.md
  20. 2 2
      docs/docs/guides/how-to/writing-a-vendure-plugin.md
  21. 0 60
      docs/docs/guides/plugins/plugin-examples/adding-rest-endpoint.md
  22. 0 75
      docs/docs/guides/plugins/plugin-examples/defining-custom-permissions.md
  23. 0 49
      docs/docs/guides/plugins/plugin-examples/defining-db-entity.md
  24. 0 336
      docs/docs/guides/plugins/plugin-examples/extending-graphql-api.md
  25. 0 163
      docs/docs/guides/plugins/plugin-examples/using-job-queue-service.md
  26. 3 16
      docs/docs/reference/typescript-api/money/index.md
  27. 27 0
      docs/docs/reference/typescript-api/money/money-decorator.md
  28. 1 0
      docs/sidebars.js
  29. 1 0
      packages/core/src/entity/money.decorator.ts

+ 41 - 31
docs/docs/guides/plugins/plugin-examples/defining-db-subscribers.md → docs/docs/guides/advanced-topics/defining-db-subscribers.md

@@ -1,19 +1,18 @@
 ---
-title: "Defining database subscribers"
-showtoc: true
+title: "Database subscribers"
 ---
 
 # Defining database subscribers
 
 TypeORM allows us to define [subscribers](https://typeorm.io/listeners-and-subscribers#what-is-a-subscriber). With a subscriber, we can listen to specific entity events and take actions based on inserts, updates, deletions and more.
 
-If you need lower-level access to database changes that you get with the [Vendure EventBus system]({{< relref "event-bus" >}}), TypeORM subscribers can be useful.
+If you need lower-level access to database changes that you get with the [Vendure EventBus system](/reference/typescript-api/events/event-bus/), TypeORM subscribers can be useful.
 
 ## Simple subscribers
 
 The simplest way to register a subscriber is to pass it to the `dbConnectionOptions.subscribers` array:
 
-```ts
+```ts title="src/plugins/my-plugin/product-subscriber.ts"
 import { Product, VendureConfig } from '@vendure/core';
 import { EntitySubscriberInterface, EventSubscriber, UpdateEvent } from 'typeorm';
 
@@ -27,6 +26,11 @@ export class ProductSubscriber implements EntitySubscriberInterface<Product> {
     console.log(`BEFORE PRODUCT UPDATED: `, event.entity);
   }
 }
+```
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { ProductSubscriber } from './plugins/my-plugin/product-subscriber';
 
 // ...
 export const config: VendureConfig = {
@@ -36,6 +40,7 @@ export const config: VendureConfig = {
   }
 }
 ```
+
 The limitation of this method is that the `ProductSubscriber` class cannot make use of dependency injection, since it is not known to the underlying NestJS application and is instead instantiated by TypeORM directly.
 
 If you need to make use of providers in your subscriber class, you'll need to use the following pattern.
@@ -44,7 +49,7 @@ If you need to make use of providers in your subscriber class, you'll need to us
 
 By defining the subscriber as an injectable provider, and passing it to a Vendure plugin, you can take advantage of Nest's dependency injection inside the subscriber methods.
 
-```ts
+```ts title="src/plugins/my-plugin/product-subscriber.ts"
 import {
   PluginCommonModule,
   Product,
@@ -59,39 +64,44 @@ import { MyService } from './services/my.service';
 @Injectable()
 @EventSubscriber()
 export class ProductSubscriber implements EntitySubscriberInterface<Product> {
-  constructor(private connection: TransactionalConnection,
-              private myService: MyService) {
-    // This is how we can dynamically register the subscriber
-    // with TypeORM
-    connection.rawConnection.subscribers.push(this);
-  }
-
-  listenTo() {
-    return Product;
-  }
-
-  async beforeUpdate(event: UpdateEvent<Product>) {
-    console.log(`BEFORE PRODUCT UPDATED: `, event.entity);
-    // Now we can make use of our injected provider
-    await this.myService.handleProductUpdate(event);
-  }
+    constructor(private connection: TransactionalConnection,
+                private myService: MyService) {
+        // This is how we can dynamically register the subscriber
+        // with TypeORM
+        connection.rawConnection.subscribers.push(this);
+    }
+
+    listenTo() {
+        return Product;
+    }
+
+    async beforeUpdate(event: UpdateEvent<Product>) {
+        console.log(`BEFORE PRODUCT UPDATED: `, event.entity);
+        // Now we can make use of our injected provider
+        await this.myService.handleProductUpdate(event);
+    }
 }
+```
 
+```ts title="src/plugins/my-plugin/my.plugin.ts"
 @VendurePlugin({
-  imports: [PluginCommonModule],
-  providers: [ProductSubscriber, MyService],
+    imports: [PluginCommonModule],
+    providers: [ProductSubscriber, MyService],
 })
-class MyPlugin {}
+class MyPlugin {
+}
+```
 
+```ts title="src/vendure-config.ts"
 // ...
 export const config: VendureConfig = {
-  dbConnectionOptions: {
-    // We no longer need to pass the subscriber here
-    // subscribers: [ProductSubscriber],
-  },
-  plugins: [
-    MyPlugin,  
-  ],
+    dbConnectionOptions: {
+        // We no longer need to pass the subscriber here
+        // subscribers: [ProductSubscriber],
+    },
+    plugins: [
+        MyPlugin,
+    ],
 }
 ```
 

+ 2 - 2
docs/docs/guides/advanced-topics/error-handling.md

@@ -29,7 +29,7 @@ if (!customer) {
 
 In the GraphQL APIs, these errors are returned in the standard `errors` array:
 
-```JSON
+```json
 {
   "errors": [
     {
@@ -125,4 +125,4 @@ mutation ApplyCoupon($code: String!) {
 
 This ensures that your client code is aware of and handles all the usual error cases.
 
-You can see all the ErrorResult types returned by the Shop API mutations in the [Shop API Mutations docs]({{< relref "/reference/graphql-api/shop/mutations" >}}). 
+You can see all the ErrorResult types returned by the Shop API mutations in the [Shop API Mutations docs](/reference/graphql-api/shop/mutations). 

+ 9 - 7
docs/docs/guides/advanced-topics/logging.md

@@ -7,9 +7,11 @@ showtoc: true
 
 Logging allows you to see what is happening inside the Vendure server. It is useful for debugging and for monitoring the health of the server in production.
 
-In Vendure, logging is configured using the `logger` property of the [VendureConfig]({{< relref "vendure-config" >}}) object. The logger must implement the [VendureLogger]({{< relref "vendure-logger" >}}) interface.
+In Vendure, logging is configured using the `logger` property of the [VendureConfig](/reference/typescript-api/configuration/vendure-config/#logger) object. The logger must implement the [VendureLogger](/reference/typescript-api/logger/vendure-logger) interface.
 
-See also: [Implementing a custom logger]({{< relref "logger" >}}#implementing-a-custom-logger)
+:::info
+To implement a custom logger, see the [Implementing a custom logger](/reference/typescript-api/logger/#implementing-a-custom-logger) guide.
+:::
 
 ## Log levels
 
@@ -26,9 +28,9 @@ Vendure uses 5 log levels, in order of increasing severity:
 
 ## DefaultLogger
 
-Vendure ships with a [DefaultLogger]({{< relref "default-logger" >}}) which logs to the console (process.stdout). It can be configured with the desired log level:
+Vendure ships with a [DefaultLogger](/reference/typescript-api/logger/default-logger) which logs to the console (process.stdout). It can be configured with the desired log level:
 
-```ts
+```ts title="src/vendure-config.ts"
 import { DefaultLogger, VendureConfig } from '@vendure/core';
 
 const config: VendureConfig = {
@@ -41,7 +43,7 @@ const config: VendureConfig = {
 
 To log database queries, set the `logging` property of the `dbConnectionOptions` as well as setting the logger to `Debug` level.
 
-```ts
+```ts title="src/vendure-config.ts"
 import { DefaultLogger, LogLevel, VendureConfig } from '@vendure/core';
 
 const config: VendureConfig = {
@@ -61,9 +63,9 @@ More information about the `logging` option can be found in the [TypeORM logging
 
 ## Logging in your own plugins
 
-When you extend Vendure by creating your own plugins, it's a good idea to log useful information about what your plugin is doing. To do this, you need to import the [Logger]({{< relref "logger" >}}) class from `@vendure/core` and use it in your plugin:
+When you extend Vendure by creating your own plugins, it's a good idea to log useful information about what your plugin is doing. To do this, you need to import the [Logger](/reference/typescript-api/logger/) class from `@vendure/core` and use it in your plugin:
 
-```ts
+```ts title="src/plugins/my-plugin/my.plugin.ts"
 import { Logger } from '@vendure/core';
 
 // It is customary to define a logger context for your plugin

+ 1 - 1
docs/docs/migrating-from-v1/breaking-api-changes.md → docs/docs/guides/advanced-topics/migrating-from-v1/breaking-api-changes.md

@@ -1,6 +1,6 @@
 ---
 title: "Breaking API Changes"
-weight: 1
+sidebar_position: 2
 ---
 
 # Breaking API Changes

+ 1 - 1
docs/docs/migrating-from-v1/database-migration.md → docs/docs/guides/advanced-topics/migrating-from-v1/database-migration.md

@@ -1,6 +1,6 @@
 ---
 title: "Database Migration"
-weight: 0
+sidebar_position: 1
 ---
 
 # v2 Database Migration

+ 3 - 3
docs/docs/migrating-from-v1/index.md → docs/docs/guides/advanced-topics/migrating-from-v1/index.md

@@ -35,6 +35,6 @@ Migration will consist of these main steps:
      }
    }
    ```
-2. **Migrate your database**. This is covered in detail in the [database migration section]({{< relref "database-migration" >}}).
-3. **Update your custom code** (configuration, plugins, admin ui extensions) to handle the breaking changes. Details of these changes are covered in the [breaking API changes section]({{< relref "breaking-api-changes" >}}).
-4. **Update your storefront** to handle some small breaking changes in the Shop GraphQL API. See the [storefront migration section]({{< relref "storefront-migration" >}}) for details.
+2. **Migrate your database**. This is covered in detail in the [database migration section](/guides/advanced-topics/migrating-from-v1/database-migration).
+3. **Update your custom code** (configuration, plugins, admin ui extensions) to handle the breaking changes. Details of these changes are covered in the [breaking API changes section](/guides/advanced-topics/migrating-from-v1/breaking-api-changes).
+4. **Update your storefront** to handle some small breaking changes in the Shop GraphQL API. See the [storefront migration section](/guides/advanced-topics/migrating-from-v1/storefront-migration) for details.

+ 1 - 0
docs/docs/migrating-from-v1/storefront-migration.md → docs/docs/guides/advanced-topics/migrating-from-v1/storefront-migration.md

@@ -1,5 +1,6 @@
 ---
 title: "Storefront Migration"
+sidebar_position: 3
 ---
 
 # Storefront migration

+ 80 - 59
docs/docs/guides/advanced-topics/testing.md

@@ -9,9 +9,11 @@ Vendure plugins allow you to extend all aspects of the standard Vendure server.
 
 The `@vendure/testing` package gives you some simple but powerful tooling for creating end-to-end tests for your custom Vendure code.
 
-{{< alert "primary" >}}
-  For a working example of a Vendure plugin with e2e testing, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
-{{< /alert >}}
+By "end-to-end" we mean we are testing the _entire server stack_ - from API, to services, to database - by making a real API request, and then making assertions about the response. This is a very effective way to ensure that _all_ parts of your plugin are working correctly together.
+
+:::info
+For a working example of a Vendure plugin with e2e testing, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
+:::
 
 ## Usage
 
@@ -34,25 +36,25 @@ import swc from 'unplugin-swc';
 import { defineConfig } from 'vitest/config';
 
 export default defineConfig({
-  test: {
-    include: '**/*.e2e-spec.ts',
-    typecheck: {
-      tsconfig: path.join(__dirname, 'tsconfig.e2e.json'),
-    },
-  },
-  plugins: [
-    // SWC required to support decorators used in test plugins
-    // See https://github.com/vitest-dev/vitest/issues/708#issuecomment-1118628479
-    // Vite plugin
-    swc.vite({
-      jsc: {
-        transform: {
-          // See https://github.com/vendure-ecommerce/vendure/issues/2099
-          useDefineForClassFields: false,
+    test: {
+        include: '**/*.e2e-spec.ts',
+        typecheck: {
+            tsconfig: path.join(__dirname, 'tsconfig.e2e.json'),
         },
-      },
-    }),
-  ],
+    },
+    plugins: [
+        // SWC required to support decorators used in test plugins
+        // See https://github.com/vitest-dev/vitest/issues/708#issuecomment-1118628479
+        // Vite plugin
+        swc.vite({
+            jsc: {
+                transform: {
+                    // See https://github.com/vendure-ecommerce/vendure/issues/2099
+                    useDefineForClassFields: false,
+                },
+            },
+        }),
+    ],
 });
 ```
 
@@ -79,14 +81,14 @@ and a `tsconfig.e2e.json` tsconfig file for the tests:
 
 ### Register database-specific initializers
 
-The `@vendure/testing` package uses "initializers" to create the test databases and populate them with initial data. We ship with initializers for `sqljs`, `postgres` and `mysql`. Custom initializers can be created to support running e2e tests against other databases supported by TypeORM. See the [`TestDbInitializer` docs]({{< relref "test-db-initializer" >}}) for more details.
+The `@vendure/testing` package uses "initializers" to create the test databases and populate them with initial data. We ship with initializers for `sqljs`, `postgres` and `mysql`. Custom initializers can be created to support running e2e tests against other databases supported by TypeORM. See the [`TestDbInitializer` docs](/reference/typescript-api/testing/test-db-initializer/) for more details.
 
-```ts
+```ts title="src/plugins/my-plugin/e2e/my-plugin.e2e-spec.ts"
 import {
-  MysqlInitializer,
-  PostgresInitializer,
-  SqljsInitializer,
-  registerInitializer,
+    MysqlInitializer,
+    PostgresInitializer,
+    SqljsInitializer,
+    registerInitializer,
 } from '@vendure/testing';
 
 const sqliteDataDir = path.join(__dirname, '__data__');
@@ -96,77 +98,78 @@ registerInitializer('postgres', new PostgresInitializer());
 registerInitializer('mysql', new MysqlInitializer());
 ```
 
-{{< alert "primary" >}}
+:::info
 Note re. the `sqliteDataDir`: The first time this test suite is run with the `SqljsInitializer`, the populated data will be saved into an SQLite file, stored in the directory specified by this constructor arg. On subsequent runs of the test suite, the data-population step will be skipped and the initial data directly loaded from the SQLite file. This method of caching significantly speeds up the e2e test runs. All the .sqlite files created in the `sqliteDataDir` can safely be deleted at any time.
-{{< /alert >}}
+:::
 
 ### Create a test environment
 
-The `@vendure/testing` package exports a [`createTestEnvironment` function]({{< relref "create-test-environment" >}}) which is used to set up a Vendure server and GraphQL clients to interact with both the Shop and Admin APIs:
+The `@vendure/testing` package exports a [`createTestEnvironment` function](/reference/typescript-api/testing/create-test-environment/) which is used to set up a Vendure server and GraphQL clients to interact with both the Shop and Admin APIs:
 
-```ts
+```ts title="src/plugins/my-plugin/e2e/my-plugin.e2e-spec.ts"
 import { createTestEnvironment, testConfig } from '@vendure/testing';
 import { describe } from 'vitest';
 import { MyPlugin } from '../my-plugin.ts';
 
 describe('my plugin', () => {
 
-  const { server, adminClient, shopClient } = createTestEnvironment({
-    ...testConfig,
-    plugins: [MyPlugin],
-  });
+    const {server, adminClient, shopClient} = createTestEnvironment({
+        ...testConfig,
+        plugins: [MyPlugin],
+    });
 
 });
 ```
 
-Notice that we pass a [`VendureConfig`]({{< relref "vendure-config" >}}) object into the `createTestEnvironment` function. The testing package provides a special [`testConfig`]({{< relref "test-config" >}}) which is pre-configured for e2e tests, but any aspect can be overridden for your tests. Here we are configuring the server to load the plugin under test, `MyPlugin`. 
+Notice that we pass a [`VendureConfig`](/reference/typescript-api/configuration/vendure-config/) object into the `createTestEnvironment` function. The testing package provides a special [`testConfig`](/reference/typescript-api/testing/test-config/) which is pre-configured for e2e tests, but any aspect can be overridden for your tests. Here we are configuring the server to load the plugin under test, `MyPlugin`. 
 
-{{< alert "warning" >}}
-**Note**: If you need to deeply merge in some custom configuration, use the [`mergeConfig` function]({{< relref "merge-config" >}}) which is provided by `@vendure/core`.
-{{< /alert >}}
+:::caution
+**Note**: If you need to deeply merge in some custom configuration, use the [`mergeConfig` function](/reference/typescript-api/configuration/merge-config/) which is provided by `@vendure/core`.
+:::
 
 ### Initialize the server
 
-The [`TestServer`]({{< relref "test-server" >}}) needs to be initialized before it can be used. The `TestServer.init()` method takes an options object which defines how to populate the server:
+The [`TestServer`](/reference/typescript-api/testing/test-server/) needs to be initialized before it can be used. The `TestServer.init()` method takes an options object which defines how to populate the server:
 
-```ts
+```ts title="src/plugins/my-plugin/e2e/my-plugin.e2e-spec.ts"
 import { beforeAll, afterAll } from 'vitest';
 import { myInitialData } from './fixtures/my-initial-data.ts';
 
 // ...
 
 beforeAll(async () => {
-  await server.init({
-    productsCsvPath: path.join(__dirname, 'fixtures/e2e-products.csv'),
-    initialData: myInitialData,
-    customerCount: 2,
-  });
-  await adminClient.asSuperAdmin();
+    await server.init({
+        productsCsvPath: path.join(__dirname, 'fixtures/e2e-products.csv'),
+        initialData: myInitialData,
+        customerCount: 2,
+    });
+    await adminClient.asSuperAdmin();
 }, 60000);
 
 afterAll(async () => {
-  await server.destroy();
+    await server.destroy();
 });
 ```
 
 An explanation of the options:
 
-* `productsCsvPath` This is a path to an optional CSV file containing product data. See [Product Import Format]({{< relref "importing-product-data" >}}#product-import-format). You can see [an example used in the Vendure e2e tests](https://github.com/vendure-ecommerce/vendure/blob/master/packages/core/e2e/fixtures/e2e-products-full.csv) to get an idea of how it works. To start with you can just copy this file directly and use it as-is.
-* `initialData` This is an object which defines how other non-product data (Collections, ShippingMethods, Countries etc.) is populated. See [Initial Data Format]({{< relref "importing-product-data" >}}#initial-data). You can [copy this example from the Vendure e2e tests](https://github.com/vendure-ecommerce/vendure/blob/master/e2e-common/e2e-initial-data.ts)
+* `productsCsvPath` This is a path to an optional CSV file containing product data. See [Product Import Format](/guides/how-to/importing-data/#product-import-format). You can see [an example used in the Vendure e2e tests](https://github.com/vendure-ecommerce/vendure/blob/master/packages/core/e2e/fixtures/e2e-products-full.csv) to get an idea of how it works. To start with you can just copy this file directly and use it as-is.
+* `initialData` This is an object which defines how other non-product data (Collections, ShippingMethods, Countries etc.) is populated. See [Initial Data Format](/guides/how-to/importing-data/#initial-data). You can [copy this example from the Vendure e2e tests](https://github.com/vendure-ecommerce/vendure/blob/master/e2e-common/e2e-initial-data.ts)
 * `customerCount` Specifies the number of fake Customers to create. Defaults to 10 if not specified.
 
 ### Write your tests
 
 Now we are all set up to create a test. Let's test one of the GraphQL queries used by our fictional plugin:
 
-```ts
+```ts title="src/plugins/my-plugin/e2e/my-plugin.e2e-spec.ts"
 import gql from 'graphql-tag';
-import { it, expect } from 'vitest';
+import { it, expect, beforeAll, afterAll } from 'vitest';
+import { myInitialData } from './fixtures/my-initial-data.ts';
 
 it('myNewQuery returns the expected result', async () => {
-  adminClient.asSuperAdmin(); // log in as the SuperAdmin user
+    adminClient.asSuperAdmin(); // log in as the SuperAdmin user
 
-  const query = gql`
+    const query = gql`
     query MyNewQuery($id: ID!) {
       myNewQuery(id: $id) {
         field1
@@ -174,9 +177,9 @@ it('myNewQuery returns the expected result', async () => {
       }
     }
   `;
-  const result = await adminClient.query(query, { id: 123 });
+    const result = await adminClient.query(query, {id: 123});
 
-  expect(result.myNewQuery).toEqual({ /* ... */ })
+    expect(result.myNewQuery).toEqual({ /* ... */})
 });
 ```
 
@@ -186,6 +189,24 @@ Running the test will then assert that your new query works as expected.
 
 All that's left is to run your tests to find out whether your code behaves as expected!
 
-{{< alert "warning" >}} 
-**Note:** When using **Vitest**, make sure you run with the [`--runInBand` option](https://jestjs.io/docs/cli#--runinband), which ensures that your tests run in series rather than in parallel.
-{{< /alert >}}
+:::caution
+**Note:** When using **Vitest** with multiple test suites (multiple `.e2e-spec.ts` files), it will attempt to run them in parallel. If all the test servers are running
+on the same port (the default in the `testConfig` is `3050`), then this will cause a port conflict. To avoid this, you can manually set a unique port for each test suite:
+
+```ts title="src/plugins/my-plugin/e2e/my-plugin.e2e-spec.ts"
+import { createTestEnvironment, testConfig } from '@vendure/testing';
+import { describe } from 'vitest';
+import { MyPlugin } from '../my-plugin.ts';
+
+describe('my plugin', () => {
+
+    const {server, adminClient, shopClient} = createTestEnvironment({
+        ...testConfig,
+        // highlight-next-line
+        port: 3051,
+        plugins: [MyPlugin],
+    });
+
+});
+```
+:::

+ 2 - 2
docs/docs/guides/advanced-topics/translations.md

@@ -3,9 +3,9 @@ title: "Translation"
 showtoc: true
 ---
 
-# Translation
+This section describes how to add new translations to the Vendure server.
 
-Using [`addTranslation`]({{< relref "i18n-service" >}}#addtranslation) inside the `onApplicationBootstrap` ([Nestjs lifecycle hooks](https://docs.nestjs.com/fundamentals/lifecycle-events)) of a Plugin is the easiest way to add new translations.
+Using [`addTranslation`](/reference/typescript-api/common/i18n-service/#addtranslation) inside the `onApplicationBootstrap` ([Nestjs lifecycle hooks](https://docs.nestjs.com/fundamentals/lifecycle-events)) of a Plugin is the easiest way to add new translations.
 While Vendure is only using `error`, `errorResult` and `message` resource keys you are free to use your own.
 
 ## Translatable Error

+ 5 - 2
docs/docs/guides/advanced-topics/updating-vendure.md

@@ -31,12 +31,15 @@ Then run `npm install` or `yarn install` depending on which package manager you
 
 ## Admin UI changes
 
-If you are using UI extensions to create your own custom Admin UI using the [`compileUiExtensions`]({{< relref "compile-ui-extensions" >}}) function, then you'll need to **delete and re-compile your admin-ui directory after upgrading** (this is the directory specified by the [`outputPath`]({{< relref "ui-extension-compiler-options" >}}#outputpath) property).
+If you are using UI extensions to create your own custom Admin UI using the [`compileUiExtensions`](/reference/admin-ui-api/ui-devkit/compile-ui-extensions/) function, then you'll need to **delete and re-compile your admin-ui directory after upgrading** (this is the directory specified by the [`outputPath`](/reference/admin-ui-api/ui-devkit/ui-extension-compiler-options#outputpath) property).
 
 ## Breaking changes
 
 Vendure follows the [SemVer convention](https://semver.org/) for version numbering. This means that breaking changes will only be introduced with changes to the major version (the first of the 3 digits in the version).
 
+However, we will occasionally upgrade underlying dependencies on minor versions, which may also require changes to your code in certain circumstances. If so, this will
+also be clearly noted in the changelog.
+
 ### What kinds of breaking changes can be expected?
 
 * Changes to the database schema
@@ -57,7 +60,7 @@ For any database schema changes, it is advised to:
 
 1. Read the changelog breaking changes entries to see what changes to expect
 2. **Important:** Make a backup of your database!
-3. Create a new database migration as described in the [Migrations guide]({{< relref "migrations" >}})
+3. Create a new database migration as described in the [Migrations guide](/guides/developer-guide/migrations/)
 4. Manually check the migration script. In some cases manual action is needed to customize the script in order to correctly migrate your existing data.
 5. Test the migration script against non-production data.
 6. Only when you have verified that the migration works as expected, run it against your production database.

+ 291 - 0
docs/docs/guides/developer-guide/events/index.mdx

@@ -0,0 +1,291 @@
+---
+title: "Events"
+---
+
+Vendure emits events which can be subscribed to by plugins. These events are published by the [EventBus](/reference/typescript-api/events/event-bus/) and
+likewise the `EventBus` is used to subscribe to events.
+
+An event exists for virtually all significant actions which occur in the system, such as:
+
+- When entities (e.g. `Product`, `Order`, `Customer`) are created, updated or deleted
+- When a user registers an account
+- When a user logs in or out
+- When the state of an `Order`, `Payment`, `Fulfillment` or `Refund` changes
+
+A full list of the available events follows.
+
+## Event types
+
+<div class="row">
+<div class="col col--6">
+
+ - [`AccountRegistrationEvent`](/reference/typescript-api/events/event-types#accountregistrationevent)
+ - [`AccountVerifiedEvent`](/reference/typescript-api/events/event-types#accountverifiedevent)
+ - [`AdministratorEvent`](/reference/typescript-api/events/event-types#administratorevent)
+ - [`AssetChannelEvent`](/reference/typescript-api/events/event-types#assetchannelevent)
+ - [`AssetEvent`](/reference/typescript-api/events/event-types#assetevent)
+ - [`AttemptedLoginEvent`](/reference/typescript-api/events/event-types#attemptedloginevent)
+ - [`ChangeChannelEvent`](/reference/typescript-api/events/event-types#changechannelevent)
+ - [`ChannelEvent`](/reference/typescript-api/events/event-types#channelevent)
+ - [`CollectionEvent`](/reference/typescript-api/events/event-types#collectionevent)
+ - [`CollectionModificationEvent`](/reference/typescript-api/events/event-types#collectionmodificationevent)
+ - [`CountryEvent`](/reference/typescript-api/events/event-types#countryevent)
+ - [`CouponCodeEvent`](/reference/typescript-api/events/event-types#couponcodeevent)
+ - [`CustomerAddressEvent`](/reference/typescript-api/events/event-types#customeraddressevent)
+ - [`CustomerEvent`](/reference/typescript-api/events/event-types#customerevent)
+ - [`CustomerGroupChangeEvent`](/reference/typescript-api/events/event-types#customergroupchangeevent)
+ - [`CustomerGroupEvent`](/reference/typescript-api/events/event-types#customergroupevent)
+ - [`FacetEvent`](/reference/typescript-api/events/event-types#facetevent)
+ - [`FacetValueEvent`](/reference/typescript-api/events/event-types#facetvalueevent)
+ - [`FulfillmentEvent`](/reference/typescript-api/events/event-types#fulfillmentevent)
+ - [`FulfillmentStateTransitionEvent`](/reference/typescript-api/events/event-types#fulfillmentstatetransitionevent)
+ - [`GlobalSettingsEvent`](/reference/typescript-api/events/event-types#globalsettingsevent)
+ - [`HistoryEntryEvent`](/reference/typescript-api/events/event-types#historyentryevent)
+ - [`IdentifierChangeEvent`](/reference/typescript-api/events/event-types#identifierchangeevent)
+ - [`IdentifierChangeRequestEvent`](/reference/typescript-api/events/event-types#identifierchangerequestevent)
+ - [`InitializerEvent`](/reference/typescript-api/events/event-types#initializerevent)
+ - [`LoginEvent`](/reference/typescript-api/events/event-types#loginevent)
+ - [`LogoutEvent`](/reference/typescript-api/events/event-types#logoutevent)
+ - [`OrderEvent`](/reference/typescript-api/events/event-types#orderevent)
+
+</div>
+<div class="col col--6">
+
+ - [`OrderLineEvent`](/reference/typescript-api/events/event-types#orderlineevent)
+ - [`OrderPlacedEvent`](/reference/typescript-api/events/event-types#orderplacedevent)
+ - [`OrderStateTransitionEvent`](/reference/typescript-api/events/event-types#orderstatetransitionevent)
+ - [`PasswordResetEvent`](/reference/typescript-api/events/event-types#passwordresetevent)
+ - [`PasswordResetVerifiedEvent`](/reference/typescript-api/events/event-types#passwordresetverifiedevent)
+ - [`PaymentMethodEvent`](/reference/typescript-api/events/event-types#paymentmethodevent)
+ - [`PaymentStateTransitionEvent`](/reference/typescript-api/events/event-types#paymentstatetransitionevent)
+ - [`ProductChannelEvent`](/reference/typescript-api/events/event-types#productchannelevent)
+ - [`ProductEvent`](/reference/typescript-api/events/event-types#productevent)
+ - [`ProductOptionEvent`](/reference/typescript-api/events/event-types#productoptionevent)
+ - [`ProductOptionGroupChangeEvent`](/reference/typescript-api/events/event-types#productoptiongroupchangeevent)
+ - [`ProductOptionGroupEvent`](/reference/typescript-api/events/event-types#productoptiongroupevent)
+ - [`ProductVariantChannelEvent`](/reference/typescript-api/events/event-types#productvariantchannelevent)
+ - [`ProductVariantEvent`](/reference/typescript-api/events/event-types#productvariantevent)
+ - [`PromotionEvent`](/reference/typescript-api/events/event-types#promotionevent)
+ - [`ProvinceEvent`](/reference/typescript-api/events/event-types#provinceevent)
+ - [`RefundStateTransitionEvent`](/reference/typescript-api/events/event-types#refundstatetransitionevent)
+ - [`RoleChangeEvent`](/reference/typescript-api/events/event-types#rolechangeevent)
+ - [`RoleEvent`](/reference/typescript-api/events/event-types#roleevent)
+ - [`SearchEvent`](/reference/typescript-api/events/event-types#searchevent)
+ - [`SellerEvent`](/reference/typescript-api/events/event-types#sellerevent)
+ - [`ShippingMethodEvent`](/reference/typescript-api/events/event-types#shippingmethodevent)
+ - [`StockMovementEvent`](/reference/typescript-api/events/event-types#stockmovementevent)
+ - [`TaxCategoryEvent`](/reference/typescript-api/events/event-types#taxcategoryevent)
+ - [`TaxRateEvent`](/reference/typescript-api/events/event-types#taxrateevent)
+ - [`TaxRateModificationEvent`](/reference/typescript-api/events/event-types#taxratemodificationevent)
+ - [`ZoneEvent`](/reference/typescript-api/events/event-types#zoneevent)
+ - [`ZoneMembersEvent`](/reference/typescript-api/events/event-types#zonemembersevent)
+
+</div>
+</div>
+
+## Subscribing to events
+
+To subscribe to an event, use the `EventBus`'s `.ofType()` method. It is typical to set up subscriptions in the `onModuleInit()` or `onApplicationBootstrap()`
+lifecycle hooks of a plugin or service (see [NestJS Lifecycle events](https://docs.nestjs.com/fundamentals/lifecycle-events).
+
+Here's an example where we subscribe to the `ProductEvent` and use it to trigger a rebuild of a static storefront:
+
+```ts title="src/plugins/storefront-build/storefront-build.plugin.ts"
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { EventBus, ProductEvent, PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+import { StorefrontBuildService } from './services/storefront-build.service';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+})
+export class StorefrontBuildPlugin implements OnModuleInit {
+    constructor(
+        // highlight-next-line
+        private eventBus: EventBus,
+        private storefrontBuildService: StorefrontBuildService) {}
+
+    onModuleInit() {
+        // highlight-start
+        this.eventBus
+            .ofType(ProductEvent)
+            .subscribe(event => {
+                this.storefrontBuildService.triggerBuild();
+            });
+        // highlight-end
+    }
+}
+```
+
+:::info
+The `EventBus.ofType()` and related `EventBus.filter()` methods return an RxJS `Observable`.
+This means that you can use any of the [RxJS operators](https://rxjs-dev.firebaseapp.com/guide/operators) to transform the stream of events.
+
+For example, to debounce the stream of events, you could do this:
+
+```ts
+// highlight-next-line
+import { debounceTime } from 'rxjs/operators';
+
+// ...
+
+this.eventBus
+    .ofType(ProductEvent)
+     // highlight-next-line
+    .pipe(debounceTime(1000))
+    .subscribe(event => {
+        this.storefrontBuildService.triggerBuild();
+    });
+```
+:::
+
+### Subscribing to multiple event types
+
+Using the `.ofType()` method allows us to subscribe to a single event type. If we want to subscribe to multiple event types, we can use the `.filter()` method instead:
+
+```ts title="src/plugins/my-plugin/my-plugin.plugin.ts"
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { EventBus, PluginCommonModule, VendurePlugin, ProductEvent, ProductVariantEvent } from '@vendure/core';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+})
+export class MyPluginPlugin implements OnModuleInit {
+    constructor(private eventBus: EventBus) {}
+
+    onModuleInit() {
+        this.eventBus
+            // highlight-start
+            .filter(event =>
+                event instanceof ProductEvent || event instanceof ProductVariantEvent)
+            // highlight-end
+            .subscribe(event => {
+                // the event will be a ProductEvent or ProductVariantEvent
+            });
+    }
+}
+```
+
+## Publishing events
+
+You can publish events using the `EventBus.publish()` method. This is useful if you want to trigger an event from within a plugin or service.
+
+For example, to publish a `ProductEvent`:
+
+```ts title="src/plugins/my-plugin/services/my-plugin.service.ts"
+import { Injectable } from '@nestjs/common';
+import { EventBus, ProductEvent, RequestContext, Product } from '@vendure/core';
+
+@Injectable()
+export class MyPluginService {
+    constructor(private eventBus: EventBus) {}
+
+    async doSomethingWithProduct(ctx: RequestContext, product: Product) {
+        // ... do something
+        // highlight-next-line
+        this.eventBus.publish(new ProductEvent(ctx, product, 'updated'));
+    }
+}
+```
+
+## Creating custom events
+
+You can create your own custom events by extending the [`VendureEvent`](/reference/typescript-api/events/vendure-event) class. For example, to create a custom event which is triggered when a customer submits a review, you could do this:
+
+```ts title="src/plugins/reviews/events/review-submitted.event.ts"
+import { ID, RequestContext, VendureEvent } from '@vendure/core';
+import { ProductReviewInput } from '../types';
+
+/**
+ * @description
+ * This event is fired whenever a ProductReview is submitted.
+ */
+export class ReviewSubmittedEvent extends VendureEvent {
+    constructor(
+        public ctx: RequestContext,
+        public input: ProductReviewInput,
+    ) {
+        super();
+    }
+}
+```
+
+The event would then be published from your plugin's `ProductReviewService`:
+
+```ts title="src/plugins/reviews/services/product-review.service.ts"
+import { Injectable } from '@nestjs/common';
+import { EventBus, ProductReviewService, RequestContext } from '@vendure/core';
+
+import { ReviewSubmittedEvent } from '../events/review-submitted.event';
+import { ProductReviewInput } from '../types';
+
+@Injectable()
+export class ProductReviewService {
+    constructor(private eventBus: EventBus, private productReviewService: ProductReviewService) {}
+
+    async submitReview(ctx: RequestContext, input: ProductReviewInput) {
+        // highlight-next-line
+        this.eventBus.publish(new ReviewSubmittedEvent(ctx, input));
+        // handle creation of the new review
+        // ...
+    }
+}
+```
+
+### Entity events
+
+There is a special event class [`VendureEntityEvent`](/reference/typescript-api/events/vendure-entity-event) for events relating to the creation, update or deletion of entities. Let's say you have a custom entity (see [defining a database entity](/guides/how-to/database-entity/)) `BlogPost` and you want to trigger an event whenever a new `BlogPost` is created, updated or deleted:
+
+```ts title="src/plugins/blog/events/blog-post-event.ts
+import { ID, RequestContext, VendureEntityEvent } from '@vendure/core';
+import { BlogPost } from '../entities/blog-post.entity';
+import { CreateBlogPostInput, UpdateBlogPostInput } from '../types';
+
+type BlogPostInputTypes = CreateBlogPostInput | UpdateBlogPostInput | ID | ID[];
+
+/**
+ * This event is fired whenever a BlogPost is added, updated
+ * or deleted.
+ */
+export class BlogPostEvent extends VendureEntityEvent<BlogPost[], BlogPostInputTypes> {
+    constructor(
+        ctx: RequestContext,
+        entity: BlogPost,
+        type: 'created' | 'updated' | 'deleted',
+        input?: BlogPostInputTypes,
+    ) {
+        super(entity, type, ctx, input);
+    }
+}
+```
+
+Using this event, you can subscribe to all `BlogPost` events, and for instance filter for only the `created` events:
+
+```ts title="src/plugins/blog/blog-plugin.ts"
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { EventBus, PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { filter } from 'rxjs/operators';
+
+import { BlogPostEvent } from './events/blog-post-event';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    // ...
+})
+export class BlogPlugin implements OnModuleInit {
+    constructor(private eventBus: EventBus) {}
+
+    onModuleInit() {
+        this.eventBus
+            // highlight-start
+            .ofType(BlogPostEvent).pipe(
+                filter(event => event.type === 'created'),
+            )
+            .subscribe(event => {
+                const blogPost = event.entity;
+                // do something with the newly created BlogPost
+            });
+            // highlight-end
+    }
+}
+```

+ 1 - 1
docs/docs/guides/developer-guide/migrations/index.md

@@ -8,7 +8,7 @@ import TabItem from '@theme/TabItem';
 Database migrations are needed whenever the database schema changes. This can be caused by:
 
 * changes to the [custom fields](/guides/developer-guide/custom-fields/) configuration.
-* new [database entities defined by plugins](/reference/typescript-api/plugin/vendure-plugin-metadata#entities)
+* new [database entities defined by plugins](/guides/how-to/database-entity/)
 * occasional changes to the core Vendure database schema when updating to newer versions
 
 ## Synchronize vs migrate

+ 28 - 0
docs/docs/guides/developer-guide/plugins/index.mdx

@@ -106,3 +106,31 @@ Note that lifecycle hooks are run in both the server and worker contexts.
 If you have code that should only run either in the server context or worker context,
 you can inject the [ProcessContext provider](/reference/typescript-api/common/process-context/).
 :::
+
+### Configure
+
+Another hook that is not strictly a lifecycle hook, but which can be useful to know is the [`configure` method](https://docs.nestjs.com/middleware#applying-middleware) which is
+used by NestJS to apply middleware. This method is called _only_ for the server and _not_ for the worker, since middleware relates
+to the network stack, and the worker has no network part.
+
+```ts
+import { MiddlewareConsumer, NestModule } from '@nestjs/common';
+import { EventBus, PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { MyMiddleware } from './api/my-middleware';
+
+@VendurePlugin({
+    imports: [PluginCommonModule]
+})
+export class MyPlugin implements NestModule {
+
+  configure(consumer: MiddlewareConsumer) {
+    consumer
+      .apply(MyMiddleware)
+      .forRoutes('my-custom-route');
+  }
+}
+```
+
+## Complete example
+
+For a complete example of writing a plugin from start to finish, read our [Writing your first plugin guide](/guides/how-to/writing-a-vendure-plugin).

+ 3 - 58
docs/docs/guides/developer-guide/the-api-layer/index.mdx

@@ -340,62 +340,7 @@ field in the GraphQL schema, or if the method name is different, a name can be p
 ## REST endpoints
 
 Although Vendure is primarily a GraphQL-based API, it is possible to add REST endpoints to the API. This is useful if
-you need to integrate with a third-party service which only supports REST, or if you need to provide a REST API for
+you need to integrate with a third-party service or client application which only supports REST, for example.
 
-In order to create REST endpoints, you need to create a plugin (see the [plugin developer guide](/guides/developer-guide/plugins/))
-which includes a [NestJS controller](https://docs.nestjs.com/controllers). Here's a simple example:
-
-```ts title="src/plugins/rest/api/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);
-    }
-}
-```
-
-The `@Controller()` decorator takes a path argument which is the base path for all the endpoints defined in the controller.
-In this case, the path is `products`, so the endpoint will be available at `/products`.
-
-The controller can be registered with a [plugin](/guides/developer-guide/plugins/):
-
-```ts title="src/plugins/rest/products.plugin.ts"
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { ProductsController } from './api/products.controller';
-
-@VendurePlugin({
-    imports: [PluginCommonModule],
-    // highlight-next-line
-    controllers: [ProductsController],
-})
-export class ProductsRestPlugin {}
-```
-
-And finally, the plugin is added to the `VendureConfig`:
-
-```ts title="src/vendure-config.ts"
-import { VendureConfig } from '@vendure/core';
-import { ProductsRestPlugin } from './plugins/rest/products.plugin';
-
-export const config: VendureConfig = {
-    // ...
-    plugins: [
-        // highlight-next-line
-        ProductsRestPlugin,
-    ],
-};
-```
-
-Now when the server is started, the REST endpoint will be available at `http://localhost:3000/products`.
-
-:::tip
-The following Vendure decorators can also be used with NestJS controllers: `@Allow()`, `@Transaction()`, `@Ctx()`.
-
-Additionally, NestJS supports a number of other REST decorators detailed in the [NestJS controllers guide](https://docs.nestjs.com/controllers#request-object)
-:::
+Creating a REST endpoint is covered in detail in the [Add a REST endpoint
+guide](/guides/how-to/rest-endpoint/).

+ 153 - 0
docs/docs/guides/how-to/custom-permissions/index.md

@@ -0,0 +1,153 @@
+---
+title: "Define custom permissions"
+showtoc: true
+---
+
+Vendure uses a fine-grained access control system based on roles & permissions. This is described in detail in the [Auth guide](/guides/core-concepts/auth/). The built-in [`Permission` enum](/reference/typescript-api/common/permission/) includes a range of permissions to control create, read, update, and delete access to the built-in entities.
+
+When building plugins, you may need to define new permissions to control access to new functionality. This guide explains how to do so.
+
+## Defining a single permission
+
+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 using the [`PermissionDefinition`](/reference/typescript-api/auth/permission-definition/) class:
+
+```ts title="src/plugins/inventory-sync/constants.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](/reference/typescript-api/request/allow-decorator/) to limit access to the mutation:
+
+```ts title="src/plugins/inventory-sync/api/inventory-sync.resolver.ts"
+import { Mutation, Resolver } from '@nestjs/graphql';
+import { Allow } from '@vendure/core';
+import { sync } from '../constants';
+
+@Resolver()
+export class InventorySyncResolver {
+
+    // highlight-next-line
+    @Allow(sync.Permission)
+    @Mutation()
+    syncInventory(/* ... */) {
+        // ...
+    }
+}
+```
+
+Finally, the `sync` PermissionDefinition must be passed into the VendureConfig so that Vendure knows about this new custom permission:
+
+```ts title="src/plugins/inventory-sync/inventory-sync.plugin.ts"
+import gql from 'graphql-tag';
+import { VendurePlugin } from '@vendure/core';
+
+import { InventorySyncResolver } from './api/inventory-sync.resolver'
+import { sync } from './constants';
+
+@VendurePlugin({
+    adminApiExtensions: {
+        schema: gql`
+            input InventoryDataInput {
+              # omitted for brevity
+            }
+        
+            extend type Mutation {
+              syncInventory(input: InventoryDataInput!): Boolean!
+            }
+        `,
+        resolvers: [InventorySyncResolver]
+    },
+    configuration: config => {
+        // highlight-next-line
+        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](/reference/typescript-api/auth/permission-definition/#crudpermissiondefinition) which simplifies the creation of the set of 4 CRUD permissions. 
+
+For example, let's imagine we are creating a plugin which adds a new entity called `ProductReview`. We can define the CRUD permissions like so:
+
+```ts title="src/plugins/product-review/constants.ts"
+import { CrudPermissionDefinition } from '@vendure/core';
+
+export const productReview = new CrudPermissionDefinition(ProductReview);
+```
+
+These permissions can then be used in our resolver:
+
+```ts title="src/plugins/product-review/api/product-review.resolver.ts"
+import { Mutation, Resolver } from '@nestjs/graphql';
+import { Allow, Transaction } from '@vendure/core';
+import { productReview } from '../constants';
+
+@Resolver()
+export class ProductReviewResolver {
+
+    // highlight-next-line
+    @Allow(productReview.Read)
+    @Query()
+    productReviews(/* ... */) {
+        // ...
+    }
+    
+    // highlight-next-line
+    @Allow(productReview.Create)
+    @Mutation()
+    @Transaction()
+    createProductReview(/* ... */) {
+        // ...
+    }
+    
+    // highlight-next-line
+    @Allow(productReview.Update)
+    @Mutation()
+    @Transaction()
+    updateProductReview(/* ... */) {
+        // ...
+    }
+    
+    // highlight-next-line
+    @Allow(productReview.Delete)
+    @Mutation()
+    @Transaction()
+    deleteProductReview(/* ... */) {
+        // ...
+    }
+}
+```
+
+Finally, the `productReview` CrudPermissionDefinition must be passed into the VendureConfig so that Vendure knows about this new custom permission:
+
+```ts title="src/plugins/product-review/product-review.plugin.ts"
+import gql from 'graphql-tag';
+import { VendurePlugin } from '@vendure/core';
+
+import { ProductReviewResolver } from './api/product-review.resolver'
+import { productReview } from './constants';
+
+@VendurePlugin({
+    adminApiExtensions: {
+        schema: gql`
+            # omitted for brevity
+        `,
+        resolvers: [ProductReviewResolver]
+    },
+    configuration: config => {
+        // highlight-next-line
+        config.authOptions.customPermissions.push(productReview);
+        return config;
+    },
+})
+export class ProductReviewPlugin {}
+```

+ 120 - 0
docs/docs/guides/how-to/database-entity/index.md

@@ -0,0 +1,120 @@
+---
+title: "Define a database entity"
+showtoc: true
+---
+
+Your plugin can define new database entities to model the data it needs to store. For instance, a product
+review plugin would need a way to store reviews. This would be done by defining a new database entity.
+
+This example shows how new [TypeORM database entities](https://typeorm.io/entities) can be defined by plugins.
+
+## Create the entity class
+
+```ts title="src/plugins/reviews/entities/product-review.entity.ts"
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { VendureEntity, Product, EntityId, ID } from '@vendure/core';
+import { Column, Entity, ManyToOne } from 'typeorm';
+
+@Entity()
+class ProductReview extends VendureEntity {
+    constructor(input?: DeepPartial<ProductReview>) {
+        super(input);
+    }
+
+    @ManyToOne(type => Product)
+    product: Product;
+    
+    @EntityId()
+    productId: ID;
+
+    @Column()
+    text: string;
+
+    @Column()
+    rating: number;
+}
+```
+
+:::note
+Any custom entities *must* extend the [`VendureEntity`](/reference/typescript-api/entities/vendure-entity/) class.
+:::
+
+In this example, we are making use of the following TypeORM decorators:
+
+* [`@Entity()`](https://typeorm.io/decorator-reference#entity) - defines the entity as a TypeORM entity. This is **required** for all entities. It tells TypeORM to create a new table in the database for this entity.
+* [`@Column()`](https://typeorm.io/decorator-reference#column) - defines a column in the database table. The data type of the column is inferred from the TypeScript type of the property, but can be overridden by passing an options object to the decorator. The `@Column()` also supports many other options for defining the column, such as `nullable`, `default`, `unique`, `primary`, `enum` etc.
+* [`@ManyToOne()`](https://typeorm.io/decorator-reference#manytoone) - defines a many-to-one relationship between this entity and another entity. In this case, many  `ProductReview` entities can be associated with a given `Product`. There are other types of relations that can be defined - see the [TypeORM relations docs](https://typeorm.io/relations).
+
+There is an additional Vendure-specific decorator:
+
+* [`@EntityId()`](/reference/typescript-api/entities/entity-id/) marks a property as the ID of another entity. In this case, the `productId` property is the ID of the `Product` entity. The reason that we have a special decorator for this is that Vendure supports both numeric and string IDs, and the `@EntityId()` decorator will automatically set the database column to be the correct type. This `productId` is not _necessary_, but it is a useful convention to allow access to the ID of the associated entity without having to perform a database join.
+
+## Register the entity
+
+The new entity is then passed to the `entities` array of the VendurePlugin metadata:
+
+```ts title="src/plugins/reviews/reviews-plugin.ts"
+import { VendurePlugin } from '@vendure/core';
+import { ProductReview } from './entities/product-review.entity';
+
+@VendurePlugin({
+    // highlight-next-line
+    entities: [ProductReview],
+})
+export class ReviewsPlugin {}
+```
+
+:::note
+Once you have added a new entity to your plugin, and the plugin has been added to your VendureConfig plugins array, you must create a [database migration](/guides/developer-guide/migrations/) to create the new table in the database.
+:::
+
+## Using the entity
+
+The new entity can now be used in your plugin code. For example, you might want to create a new product review when a customer submits a review via the storefront:
+
+```ts title="src/plugins/reviews/services/review.service.ts"
+import { Injectable } from '@nestjs/common';
+import { RequestContext, Product, TransactionalConnection } from '@vendure/core';
+
+import { ProductReview } from '../entities/product-review.entity';
+
+@Injectable()
+export class ReviewService {
+    constructor(private connection: TransactionalConnection) {}
+
+    async createReview(ctx: RequestContext, productId: string, rating: number, text: string) {
+        const product = await this.connection.getEntityOrThrow(ctx, Product, productId);
+        // highlight-start
+        const review = new ProductReview({
+            product,
+            rating,
+            text,
+        });
+        return this.connection.getRepository(ctx, ProductReview).save(review);
+        // highlight-end
+    }
+}
+```
+
+## Available entity decorators
+
+In addition to the decorators described above, there are many other decorators provided by TypeORM. Some commonly used ones are:
+
+- [`@OneToOne()`](https://typeorm.io/decorator-reference#onetoone)
+- [`@OneToMany()`](https://typeorm.io/decorator-reference#onetomany)
+- [`@ManyToMany()`](https://typeorm.io/decorator-reference#manytomany)
+- [`@Index()`](https://typeorm.io/decorator-reference#index)
+- [`@Unique()`](https://typeorm.io/decorator-reference#unique)
+
+There is also another Vendure-specific decorator for representing monetary values specifically:
+
+- [`@Money()`](/reference/typescript-api/money/money-decorator): This works together with the [`MoneyStrategy`](/reference/typescript-api/money/money-strategy) to allow configurable control over how monetary values are stored in the database.
+
+:::info
+The full list of TypeORM decorators can be found in the [TypeORM decorator reference](https://typeorm.io/decorator-reference)
+:::
+
+
+## Corresponding GraphQL type
+
+Once you have defined a new DB entity, it is likely that you want to expose it in your GraphQL API. Here's how to [define a new type in your GraphQL API](/guides/how-to/extend-graphql-api/#defining-a-new-type).

+ 478 - 0
docs/docs/guides/how-to/extend-graphql-api/index.md

@@ -0,0 +1,478 @@
+---
+title: "Extend the GraphQL API"
+showtoc: true
+---
+
+Extension to the GraphQL API consists of two parts:
+
+1. **Schema extensions**. These define new types, fields, queries and mutations.
+2. **Resolvers**. These provide the logic that backs up the schema extensions.
+
+The Shop API and Admin APIs can be extended independently:
+
+```ts title="src/plugins/top-products/top-products.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import gql from 'graphql-tag';
+import { TopSellersResolver } from './api/top-products.resolver';
+
+const schemaExtension = gql`
+  extend type Query {
+    topProducts: [Product!]!
+  }
+`
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    // highlight-start
+    // We pass our schema extension and any related resolvers
+    // to our plugin metadata  
+    shopApiExtensions: {
+        schema: schemaExtension,
+        resolvers: [TopProductsResolver],
+    },
+    // Likewise, if you want to extend the Admin API,
+    // you would use `adminApiExtensions` in exactly the
+    // same way.  
+    // adminApiExtensions: {
+    //     schema: someSchemaExtension
+    //     resolvers: [SomeResolver],
+    // },
+    // highlight-end
+})
+export class TopProductsPlugin {
+}
+```
+
+There are a number of ways the GraphQL APIs can be modified by a plugin.
+
+## Adding a new Query
+
+Let's take a simple example where we want to be able to display a banner in our storefront.
+
+First let's define a new query in the schema:
+
+```ts title="src/plugins/banner/api/api-extensions.ts"
+import gql from 'graphql-tag';
+
+export const shopApiExtensions = gql`
+  extend type Query {
+    // highlight-next-line
+    activeBanner(locationId: String!): String
+  }
+`;
+```
+
+This defines a new query called `activeBanner` which takes a `locationId` string argument and returns a string. 
+
+:::tip
+`!` = non-nullable
+
+In GraphQL, the `!` in `locationId: String!` indicates that the argument is required, and the lack of a `!` on the return type indicates that the return value can be `null`.
+:::
+
+We can now define the resolver for this query:
+
+```ts title="src/plugins/banner/api/banner-shop.resolver.ts"
+import { Args, Query, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext } from '@vendure/core';
+import { BannerService } from '../services/banner.service.ts';
+
+@Resolver()
+class BannerShopResolver {
+    constructor(private bannerService: BannerService) {}
+
+    // highlight-start
+    @Query()
+    activeBanner(@Ctx() ctx: RequestContext, @Args() args: { locationId: string; }) {
+        return this.bannerService.getBanner(ctx, args.locationId);
+    }
+    // highlight-end
+}
+```
+
+The `BannerService` would implement the actual logic for fetching the banner text from the database.
+
+Finally, we need to add the resolver to the plugin metadata:
+
+```ts title="src/plugins/banner/banner.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { BannerService } from './services/banner.service';
+import { BannerShopResolver } from './api/banner-shop.resolver';
+import { shopApiExtensions } from './api/api-extensions';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    // highlight-start
+    shopApiExtensions: {
+        schema: shopApiExtensions,
+        resolvers: [BannerShopResolver],
+    },
+    // highlight-end
+    providers: [BannerService],
+})
+export class BannerPlugin {}
+```
+
+## Adding a new Mutation
+
+Let's continue the `BannerPlugin` example and now add a mutation which allows the administrator to set the banner text.
+
+First we define the mutation in the schema:
+
+```ts title="src/plugins/banner/api/api-extensions.ts"
+import gql from 'graphql-tag';
+
+export const adminApiExtensions = gql`
+  extend type Mutation {
+    // highlight-next-line
+    setBannerText(locationId: String!, text: String!): String!
+  }
+`;
+```
+
+Here we are defining a new mutation called `setBannerText` which takes two arguments, `locationId` and `text`, both of which are required strings. The return type is a non-nullable string.
+
+Now let's define a resolver to handle that mutation:
+
+```ts title="src/plugins/banner/api/banner-admin.resolver.ts"
+import { Args, Mutation, Resolver } from '@nestjs/graphql';
+import { Allow, Ctx, RequestContext, Permission, Transaction } from '@vendure/core';
+import { BannerService } from '../services/banner.service.ts';
+
+@Resolver()
+class BannerAdminResolver {
+    constructor(private bannerService: BannerService) {}
+
+    // highlight-start
+    @Allow(Permission.UpdateSettings)
+    @Transaction()
+    @Mutation()
+    setBannerText(@Ctx() ctx: RequestContext, @Args() args: { locationId: string; text: string; }) {
+        return this.bannerService.setBannerText(ctx, args.locationId, args.text);
+    }
+    // highlight-end
+}
+```
+
+Note that we have used the `@Allow()` decorator to ensure that only users with the `UpdateSettings` permission can call this mutation. We have also wrapped the resolver in a transaction using `@Transaction()`, which is a good idea for any mutation which modifies the database.
+
+:::info
+For more information on the available decorators, see the [API Layer "decorators" guide](/guides/developer-guide/the-api-layer/#api-decorators).
+:::
+
+Finally, we add the resolver to the plugin metadata:
+
+```ts title="src/plugins/banner/banner.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { BannerService } from './services/banner.service';
+import { BannerShopResolver } from './api/banner-shop.resolver';
+import { BannerAdminResolver } from './api/banner-admin.resolver';
+import { shopApiExtensions, adminApiExtensions } from './api/api-extensions';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    shopApiExtensions: {
+        schema: shopApiExtensions,
+        resolvers: [BannerShopResolver],
+    },
+    // highlight-start
+    adminApiExtensions: {
+        schema: adminApiExtensions,
+        resolvers: [BannerAdminResolver],
+    },
+    // highlight-end
+    providers: [BannerService],
+})
+export class BannerPlugin {}
+```
+
+
+## Defining a new type
+
+If you have defined a new database entity, it is likely that you'll want to expose this entity in your GraphQL API. To do so, you'll need to define a corresponding GraphQL type.
+
+Using the `ProductReview` entity from the [Define a database entity guide](/guides/how-to/database-entity/), let's see how we can expose it as a new type in the API.
+
+As a reminder, here is the `ProductReview` entity:
+
+```ts title="src/plugins/reviews/product-review.entity.ts"
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { VendureEntity, Product, EntityId, ID } from '@vendure/core';
+import { Column, Entity, ManyToOne } from 'typeorm';
+
+@Entity()
+class ProductReview extends VendureEntity {
+    constructor(input?: DeepPartial<ProductReview>) {
+        super(input);
+    }
+
+    @ManyToOne(type => Product)
+    product: Product;
+    
+    @EntityId()
+    productId: ID;
+
+    @Column()
+    text: string;
+
+    @Column()
+    rating: number;
+}
+```
+Let's define a new GraphQL type which corresponds to this entity:
+
+```ts title="src/plugins/reviews/api/api-extensions.ts"
+import gql from 'graphql-tag';
+
+export const apiExtensions = gql`
+  // highlight-start
+  type ProductReview implements Node {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    product: Product!
+    productId: ID!
+    text: String!
+    rating: Float!
+  }
+  // highlight-end
+`;
+```
+
+:::info
+Assuming the entity is a standard `VendureEntity`, it is good practice to always include the `id`, `createdAt` and `updatedAt` fields in the GraphQL type.
+
+Additionally, we implement `Node` which is a built-in GraphQL interface.
+:::
+
+Now we can add this type to both the Admin and Shop APIs:
+
+```ts title="src/plugins/reviews/reviews.plugin.ts"
+import gql from 'graphql-tag';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { ReviewsResolver } from './api/reviews.resolver';
+import { apiExtensions } from './api/api-extensions';
+import { ProductReview } from './entities/product-review.entity';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    shopApiExtensions: {
+        // highlight-next-line
+        schema: apiExtensions,
+    },
+    entities: [ProductReview],
+})
+export class ReviewsPlugin {}
+```
+
+## Add fields to existing types
+
+Let's say you want to add a new field to the `ProductVariant` type to allow the storefront to display some indication of how long a particular product variant would take to deliver, based on data from some external service. 
+
+First we extend the `Product` GraphQL type:
+
+```ts title="src/plugins/delivery-time/api/api-extensions.ts"
+import gql from 'graphql-tag';
+
+export const shopApiExtensions = gql`
+  type DeliveryEstimate {
+    from: Int!
+    to: Int!
+  }
+
+  // highlight-start
+  extend type ProductVariant {
+    delivery: DeliveryEstimate!
+  }
+  // highlight-end
+}`;
+```
+
+This schema extension says that the `delivery` field will be added to the `ProductVariant` type, and that it will be of type `DeliveryEstimate!`, i.e. a non-nullable
+instance of the `DeliveryEstimate` type.
+
+Next we need to define an "entity resolver" for this field. Unlike the resolvers we have seen above, this resolver will be handling fields on the `ProductVariant` type _only_. This is done by scoping the resolver class that type by passing the type name to the `@Resolver()` decorator:
+
+```ts title="src/plugins/delivery-time/product-variant-entity.resolver.ts"
+import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext, ProductVariant } from '@vendure/core';
+import { DeliveryEstimateService } from '../services/delivery-estimate.service';
+
+// highlight-next-line
+@Resolver('ProductVariant')
+export class ProductVariantEntityResolver {
+    constructor(private deliveryEstimateService: DeliveryEstimateService) { }
+
+    // highlight-start
+    @ResolveField()
+    delivery(@Ctx() ctx: RequestContext, @Parent() variant: ProductVariant) {
+        return this.deliveryEstimateService.getEstimate(ctx, variant.id);
+    }
+    // highlight-end
+}
+```
+
+Finally we need to pass these schema extensions and the resolver to our plugin metadata:
+
+```ts title="src/plugins/delivery-time/delivery-time.plugin.ts"
+import gql from 'graphql-tag';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { ProductVariantEntityResolver } from './api/product-variant-entity.resolver';
+import { shopApiExtensions } from './api/api-extensions';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    shopApiExtensions: {
+        // highlight-start
+        schema: shopApiExtensions,
+        resolvers: [ProductVariantEntityResolver]
+        // highlight-end
+    }
+})
+export class DeliveryTimePlugin {}
+```
+
+## Override built-in resolvers
+
+It is also possible to override an existing built-in resolver function with one of your own. To do so, you need to define a resolver with the same name as the query or mutation you wish to override. When that query or mutation is then executed, your code, rather than the default Vendure resolver, will handle it.
+
+```ts
+import { Args, Query, Mutation, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext } from '@vendure/core'
+
+@Resolver()
+class OverrideExampleResolver {
+
+    @Query()
+    products(@Ctx() ctx: RequestContext, @Args() args: any) {
+        // when the `products` query is executed, this resolver function will
+        // now handle it.
+    }
+
+    @Transaction()
+    @Mutation()
+    addItemToOrder(@Ctx() ctx: RequestContext, @Args() args: any) {
+        // when the `addItemToOrder` mutation is executed, this resolver function will
+        // now handle it.
+    }
+
+}
+```
+
+The same can be done for resolving fields:
+
+```ts
+import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext, Product } from '@vendure/core';
+
+@Resolver('Product')
+export class FieldOverrideExampleResolver {
+
+    @ResolveField()
+    description(@Ctx() ctx: RequestContext, @Parent() product: Product) {
+        return this.wrapInFormatting(ctx, product.id);
+    }
+
+    private wrapInFormatting(ctx: RequestContext, id: ID): string {
+        // implementation omitted, but wraps the description
+        // text in some special formatting required by the storefront
+    }
+}
+```
+
+## Resolving union results
+
+When dealing with operations that return a GraphQL union type, there is an extra step needed.
+
+Union types are commonly returned from mutations in the Vendure APIs. For more detail on this see the section on [ErrorResults](/guides/advanced-topics/error-handling#expected-errors-errorresults). For example: 
+
+```graphql
+type MyCustomErrorResult implements ErrorResult {
+  errorCode: ErrorCode!
+  message: String!
+}
+
+union MyCustomMutationResult = Order | MyCustomErrorResult
+
+extend type Mutation {
+  myCustomMutation(orderId: ID!): MyCustomMutationResult!
+}
+```
+
+In this example, the resolver which handles the `myCustomMutation` operation will be returning either an `Order` object or a `MyCustomErrorResult` object. The problem here is that the GraphQL server has no way of knowing which one it is at run-time. Luckily Apollo Server (on which Vendure is built) has a means to solve this:
+
+> To fully resolve a union, Apollo Server needs to specify which of the union's types is being returned. To achieve this, you define a `__resolveType` function for the union in your resolver map.
+> 
+> The `__resolveType` function is responsible for determining an object's corresponding GraphQL type and returning the name of that type as a string.
+> 
+-- <cite>Source: [Apollo Server docs](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#resolving-a-union)</cite>
+
+In order to implement a `__resolveType` function as part of your plugin, you need to create a dedicated Resolver class with a single field resolver method which will look like this:
+
+```ts
+import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext, ProductVariant } from '@vendure/core';
+
+@Resolver('MyCustomMutationResult')
+export class MyCustomMutationResultResolver {
+  
+  @ResolveField()
+  __resolveType(value: any): string {
+    // If it has an "id" property we can assume it is an Order.  
+    return value.hasOwnProperty('id') ? 'Order' : 'MyCustomErrorResult';
+  }
+}
+```
+
+This resolver is then passed in to your plugin metadata like any other resolver:
+
+```ts
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  shopApiExtensions: {
+    schema: apiExtensions,
+    resolvers: [/* ... */, MyCustomMutationResultResolver]
+  }
+})
+export class MyPlugin {}
+```
+
+## Defining custom scalars
+
+By default, Vendure bundles `DateTime` and a `JSON` custom scalars (from the [graphql-scalars library](https://github.com/Urigo/graphql-scalars)). From v1.7.0, you can also define your own custom scalars for use in your schema extensions:
+
+```ts
+import { GraphQLScalarType} from 'graphql';
+import { GraphQLEmailAddress } from 'graphql-scalars';
+
+// Scalars can be custom-built as like this one,
+// or imported from a pre-made scalar library like
+// the GraphQLEmailAddress example.
+const FooScalar = new GraphQLScalarType({
+  name: 'Foo',
+  description: 'A test scalar',
+  serialize(value) {
+    // ...
+  },
+  parseValue(value) {
+    // ...
+  },
+});
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  shopApiExtensions: {
+    schema: gql`
+      scalar Foo
+      scalar EmailAddress
+    `,
+    scalars: { 
+      // The key must match the scalar name
+      // given in the schema  
+      Foo: FooScalar,
+      EmailAddress: GraphQLEmailAddress,
+    },
+  },
+})
+export class CustomScalarsPlugin {}
+```

+ 195 - 0
docs/docs/guides/how-to/job-queue/index.md

@@ -0,0 +1,195 @@
+---
+title: "Run a job on the worker"
+---
+
+If your plugin involves long-running tasks, you can take advantage of the [job queue system](/guides/developer-guide/worker-job-queue/). 
+
+Let's say you are building a plugin which allows a video URL to be specified, and then that video gets transcoded into a format suitable for streaming on the storefront. This is a long-running task which should not block the main thread, so we will use the job queue to run the task on the worker.
+
+First we'll add a new mutation to the Admin API schema:
+
+```ts title="src/plugins/product-video/api/api-extensions.ts"
+import gql from 'graphql-tag';
+
+export const adminApiExtensions = gql`
+  extend type Mutation {
+    addVideoToProduct(productId: ID! videoUrl: String!): Job!
+  }
+`;
+```
+
+The resolver looks like this:
+
+
+```ts title="src/plugins/product-video/api/product-video.resolver.ts"
+import { Args, Mutation, Resolver } from '@nestjs/graphql';
+import { Allow, Ctx, RequestContext, Permission, RequestContext } from '@vendure/core'
+import { ProductVideoService } from '../services/product-video.service';
+
+@Resolver()
+export class ProductVideoResolver {
+
+    constructor(private productVideoService: ProductVideoService) {}
+
+    @Mutation()
+    @Allow(Permission.UpdateProduct)
+    addVideoToProduct(@Ctx() ctx: RequestContext, @Args() args: { productId: ID; videoUrl: string; }) {
+        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`.
+
+## Creating a job queue
+
+The [`JobQueueService`](/reference/typescript-api/job-queue/job-queue-service/) creates and manages job queues. The queue is created when the
+application starts up (see [NestJS lifecycle events](https://docs.nestjs.com/fundamentals/lifecycle-events)), and then we can use the `add()` method to add jobs to the queue.
+
+```ts title="src/plugins/product-video/services/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';
+
+@Injectable()
+class ProductVideoService implements OnModuleInit {
+    
+    private jobQueue: JobQueue<{ productId: ID; videoUrl: string; }>;
+
+    constructor(private jobQueueService: JobQueueService,
+                private connection: TransactionalConnection) {
+    }
+
+    async onModuleInit() {
+        this.jobQueue = await this.jobQueueService.createQueue({
+            name: 'transcode-video',
+            process: async job => {
+                // Inside the `process` function 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.
+                const result = await transcode(job.data.videoUrl);
+                await this.connection.getRepository(Product).save({
+                    id: job.data.productId,
+                    customFields: {
+                        videoUrl: result.url,
+                    },
+                });
+                // The value returned from the `process` function is stored as the "result"
+                // field of the job (for those JobQueueStrategies that support recording of results).
+                //  
+                // Any error thrown from this function will cause the job to fail.  
+                return result;
+            },
+        });
+    }
+
+    transcodeForProduct(productId: ID, videoUrl: string) {
+        // Add a new job to the queue and immediately return the
+        // job itself.
+        return this.jobQueue.add({productId, videoUrl}, {retries: 2});
+    }
+}
+```
+
+Notice the generic type parameter of the `JobQueue`:
+
+```ts
+JobQueue<{ productId: ID; videoUrl: string; }>
+```
+
+This means that when we call `jobQueue.add()` we must pass in an object of this type. This data will then be available in the `process` function as the `job.data` property.
+
+:::note
+The data passed to `jobQueue.add()` must be JSON-serializable, because it gets serialized into a string when stored in the job queue. Therefore you should
+avoid passing in complex objects such as `Date` instances, `Buffer`s, etc.
+:::
+
+The `ProductVideoService` is in charge of setting up the JobQueue and adding jobs to that queue. Calling 
+
+```ts
+productVideoService.transcodeForProduct(id, url);
+```
+
+will add a transcoding job to the queue.
+
+:::tip
+Plugin code typically gets executed on both the server _and_ the worker. Therefore, you sometimes need to explicitly check
+what context you are in. This can be done with the [ProcessContext](/reference/typescript-api/common/process-context/) provider.
+:::
+
+
+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](/reference/typescript-api/plugin/plugin-common-module/) is imported as it exports the `JobQueueService`.
+
+```ts title="src/plugins/product-video/product-video.plugin.ts"
+import gql from 'graphql-tag';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { ProductVideoService } from './services/product-video.service';
+import { ProductVideoResolver } from './api/product-video.resolver';
+import { adminApiExtensions } from './api/api-extensions';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    providers: [ProductVideoService],
+    adminApiExtensions: {
+        schema: adminApiExtensions,
+        resolvers: [ProductVideoResolver]
+    },
+    configuration: config => {
+        config.customFields.Product.push({
+            name: 'videoUrl',
+            type: 'string',
+        });
+        return config;
+    }
+})
+export class ProductVideoPlugin {}
+```
+## Subscribing to job updates
+
+When creating a new job via `JobQueue.add()`, it is possible to subscribe to updates to that Job (progress and status changes). This allows you, for example, to create resolvers which are able to return the results of a given Job.
+
+In the video transcoding example above, we could modify the `transcodeForProduct()` call to look like this:
+
+```ts title="src/plugins/product-video/services/product-video.service.ts"
+import { Injectable, OnModuleInit } from '@nestjs/common';
+import { of } from 'rxjs';
+import { map, catchError } from 'rxjs/operators';
+import { ID, Product, TransactionalConnection } from '@vendure/core';
+
+@Injectable()
+class ProductVideoService implements OnModuleInit {
+    // ... omitted (see above)
+
+    transcodeForProduct(productId: ID, videoUrl: string) {
+        const job = await this.jobQueue.add({productId, videoUrl}, {retries: 2});
+
+        return job.updates().pipe(
+            map(update => {
+                // The returned Observable will emit a value for every update to the job
+                // such as when the `progress` or `status` value changes.
+                Logger.info(`Job ${update.id}: progress: ${update.progress}`);
+                if (update.state === JobState.COMPLETED) {
+                    Logger.info(`COMPLETED ${update.id}: ${update.result}`);
+                }
+                return update.result;
+            }),
+            catchError(err => of(err.message)),
+        );
+    }
+}
+```
+
+If you prefer to work with Promises rather than Rxjs Observables, you can also convert the updates to a promise:
+
+```ts
+const job = await this.jobQueue.add({ productId, videoUrl }, { retries: 2 });
+    
+return job.updates().toPromise()
+  .then(/* ... */)
+  .catch(/* ... */);
+```

+ 99 - 0
docs/docs/guides/how-to/rest-endpoint/index.md

@@ -0,0 +1,99 @@
+---
+title: "Add a REST endpoint"
+showtoc: true
+---
+
+REST-style endpoints can be defined as part of a [plugin](/guides/developer-guide/plugins/).
+
+:::info
+REST endpoints are implemented as NestJS Controllers. For comprehensive documentation, see the [NestJS controllers documentation](https://docs.nestjs.com/controllers).
+:::
+
+In this guide we will define a plugin adds a single REST endpoint at `http://localhost:3000/products` which returns a list of all products.
+
+## Create a controller
+
+First let's define the controller:
+
+```ts title="src/plugins/rest-plugin/api/products.controller.ts"
+// 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);
+    }
+}
+```
+
+The key points to note here are:
+
+- The `@Controller()` decorator defines the base path for all endpoints defined in this controller. In this case, all endpoints will be prefixed with `/products`.
+- The `@Get()` decorator defines a GET endpoint at the base path. The method name `findAll` is arbitrary.
+- The `@Ctx()` decorator injects the [RequestContext](/reference/typescript-api/request/request-context/) which is required for all service methods.
+
+## Register the controller with the plugin
+
+```ts title="src/plugins/rest-plugin/rest.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { ProductsController } from './api/products.controller';
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  controllers: [ProductsController],
+})
+export class RestPlugin {}
+```
+
+:::info
+**Note:** [The `PluginCommonModule`](/reference/typescript-api/plugin/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.
+:::
+
+The plugin can then be added to the `VendureConfig`:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { RestPlugin } from './plugins/rest-plugin/rest.plugin';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        // ...
+        // highlight-next-line
+        RestPlugin,
+    ],
+};
+```
+
+## Controlling access to REST endpoints
+
+You can use the [`@Allow()` decorator](/reference/typescript-api/request/allow-decorator/) to declare the permissions required to access a REST endpoint:
+
+```ts title="src/plugins/rest-plugin/api/products.controller.ts"
+import { Controller, Get } from '@nestjs/common';
+import { Allow, Permission, Ctx, ProductService, RequestContext } from '@vendure/core';
+
+@Controller('products')
+export class ProductsController {
+    constructor(private productService: ProductService) {}
+
+    // highlight-next-line
+    @Allow(Permission.ReadProduct)
+    @Get()
+    findAll(@Ctx() ctx: RequestContext) {
+        return this.productService.findAll(ctx);
+    }
+}
+```
+
+:::tip
+The following Vendure [API decorators](/guides/developer-guide/the-api-layer/#api-decorators) can also be used with NestJS controllers: `@Allow()`, `@Transaction()`, `@Ctx()`.
+
+Additionally, NestJS supports a number of other REST decorators detailed in the [NestJS controllers guide](https://docs.nestjs.com/controllers#request-object)
+:::

+ 2 - 2
docs/docs/guides/how-to/writing-a-vendure-plugin.md

@@ -72,7 +72,7 @@ First let's create the file to house the entity:
 By convention, we'll store the entity definitions in the `entities` directory of the plugin. Again, this is not a requirement, but it is a good way to keep your plugin organized.
 
 ```ts title="src/plugins/wishlist-plugin/entities/wishlist-item.entity.ts"
-import { DeepPartial, ID, ProductVariant, VendureEntity } from '@vendure/core';
+import { DeepPartial, ID, ProductVariant, VendureEntity, EntityId } from '@vendure/core';
 import { Column, Entity, ManyToOne } from 'typeorm';
 
 @Entity()
@@ -84,7 +84,7 @@ export class WishlistItem extends VendureEntity {
     @ManyToOne(type => ProductVariant)
     productVariant: ProductVariant;
 
-    @Column()
+    @EntityId()
     productVariantId: ID;
 }
 ```

+ 0 - 60
docs/docs/guides/plugins/plugin-examples/adding-rest-endpoint.md

@@ -1,60 +0,0 @@
----
-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).
-```ts
-// 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);
-  }
-}
-```
-```ts
-// 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.
-
-## Controlling access to REST endpoints
-
-You can use the [Allow decorator]({{< relref "allow-decorator" >}}) to declare the permissions required to access a REST endpoint:
-
-```ts {hl_lines=[8]}
-import { Controller, Get } from '@nestjs/common';
-import { Allow, Permission, Ctx, ProductService, RequestContext } from '@vendure/core'; 
-
-@Controller('products')
-export class ProductsController {
-  constructor(private productService: ProductService) {}
-    
-  @Allow(Permission.ReadProduct)  
-  @Get()
-  findAll(@Ctx() ctx: RequestContext) {
-    return this.productService.findAll(ctx);
-  }
-}
-```

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

@@ -1,75 +0,0 @@
----
-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:
-
-```ts
-// 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:
-
-```ts
-// 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:
-
-```ts {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.

+ 0 - 49
docs/docs/guides/plugins/plugin-examples/defining-db-entity.md

@@ -1,49 +0,0 @@
----
-title: "Defining a new database entity"
-showtoc: true
----
-
-# Defining a new database entity
-
-This example shows how new [TypeORM database entities](https://typeorm.io/entities) can be defined by plugins.
-
-```ts
-// 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:
-
-```ts {hl_lines=[6]}
-// reviews-plugin.ts
-import { VendurePlugin } from '@vendure/core';
-import { ProductReview } from './product-review.entity';
-
-@VendurePlugin({
-  entities: [ProductReview],
-})
-export class ReviewsPlugin {}
-```
-
-## Corresponding GraphQL type
-
-Once you have defined a new DB entity, it is likely that you want to expose it in your GraphQL API. Here's how to [define a new type in your GraphQL API]({{< relref "extending-graphql-api" >}}#defining-a-new-type).

+ 0 - 336
docs/docs/guides/plugins/plugin-examples/extending-graphql-api.md

@@ -1,336 +0,0 @@
----
-title: "Extending the GraphQL API"
-showtoc: true
----
-
-# Extending the GraphQL API
-
-Extension to the GraphQL API consists of two parts:
-
-1. **Schema extensions**. These define new types, fields, queries and mutations.
-2. **Resolvers**. These provide the logic that backs up the schema extensions.
-
-The Shop API and Admin APIs can be extended independently:
-
-```ts {hl_lines=["16-22"]}
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import gql from 'graphql-tag';
-import { TopSellersResolver } from './top-products.resolver';
-
-const schemaExtension = gql`
-  extend type Query {
-    topProducts: [Product!]!
-  }
-`
-
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  // We pass our schema extension and any related resolvers
-  // to our plugin metadata  
-  shopApiExtensions: {
-    schema: schemaExtension,
-    resolvers: [TopProductsResolver],
-  },
-  // Likewise, if you want to extend the Admin API,
-  // you would use `adminApiExtensions` in exactly the
-  // same way.  
-})
-export class TopProductsPlugin {}
-```
-
-There are a number of ways the GraphQL APIs can be modified by a plugin.
-
-## Adding a new Query or Mutation
-
-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.
- 
-```ts
-// 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 >}}
-
-```ts
-// top-sellers.service.ts
-import { Injectable } from '@nestjs/common';
-import { RequestContext } from '@vendure/core';
-
-@Injectable()
-class TopSellersService {
-    getTopSellers(ctx: RequestContext, from: Date, to: Date) { 
-        /* ... */
-    }
-}
-```
-
-The GraphQL schema is extended with the `topSellers` query (the query name should match the name of the corresponding resolver method):
-
-```ts
-// top-sellers.plugin.ts
-import gql from 'graphql-tag';
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { TopSellersService } from './top-sellers.service'
-import { TopSellersResolver } from './top-sellers.resolver'
-
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  providers: [TopSellersService],
-  adminApiExtensions: {
-    schema: gql`
-      extend type Query {
-        topSellers(from: DateTime! to: DateTime!): [Product!]!
-    }`,
-    resolvers: [TopSellersResolver]
-  }
-})
-export class TopSellersPlugin {}
-```
-
-New mutations are defined in the same way, except that the `@Mutation()` decorator is used in the resolver, and the schema Mutation type is extended:
-
-```graphql
-extend type Mutation { 
-    myCustomProductMutation(id: ID!): Product!
-}
-```
-
-## Defining a new type
-
-If you have [defined a new database entity]({{< relref "defining-db-entity" >}}), it is likely that you'll want to expose this entity in your GraphQL API. To do so, you'll need to define a corresponding GraphQL type:
-
-```ts
-import gql from 'graphql-tag';
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { ReviewsResolver } from './reviews.resolver';
-import { ProductReview } from './product-review.entity';
-
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  shopApiExtensions: {
-    schema: gql`
-      # This is where we define the GraphQL type
-      # which corresponds to the Review entity
-      type ProductReview implements Node {
-        id: ID!
-        createdAt: DateTime!
-        updatedAt: DateTime!
-        text: String!
-        rating: Float!
-      }
-      
-      extend type Query {
-        # Now we can use this ProductReview type in queries
-        # and mutations.
-        reviewsForProduct(productId: ID!): [ProductReview!]!
-      }
-    `,
-    resolvers: [ReviewsResolver]
-  },
-  entities: [ProductReview],  
-})
-export class ReviewsPlugin {}
-```
-
-## Add fields to existing types
-
-Let's say you want to add a new field, "availability" to the ProductVariant type, to allow the storefront to display some indication of whether a variant is available to purchase. First you define a resolver function:
-
-```ts
-import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
-import { Ctx, RequestContext, ProductVariant } from '@vendure/core';
-
-@Resolver('ProductVariant')
-export class ProductVariantEntityResolver {
-  
-  @ResolveField()
-  availability(@Ctx() ctx: RequestContext, @Parent() variant: ProductVariant) {
-    return this.getAvailbilityForVariant(ctx, variant.id);
-  }
-  
-  private getAvailbilityForVariant(ctx: RequestContext, id: ID): string {
-    // implementation omitted, but calculates the
-    // available salable stock and returns a string
-    // such as "in stock", "2 remaining" or "out of stock"
-  }
-}
-```
-
-Then in the plugin metadata, we extend the ProductVariant type and pass the resolver:
-
-```ts
-import gql from 'graphql-tag';
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { ProductVariantEntityResolver } from './product-variant-entity.resolver'
-
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  shopApiExtensions: {
-    schema: gql`
-      extend type ProductVariant {
-        availability: String!
-      }`,
-    resolvers: [ProductVariantEntityResolver]
-  }
-})
-export class AvailabilityPlugin {}
-```
-
-## Override built-in resolvers
-
-It is also possible to override an existing built-in resolver function with one of your own. To do so, you need to define a resolver with the same name as the query or mutation you wish to override. When that query or mutation is then executed, your code, rather than the default Vendure resolver, will handle it.
-
-```ts
-import { Args, Query, Mutation, Resolver } from '@nestjs/graphql';
-import { Ctx, RequestContext } from '@vendure/core'
-
-@Resolver()
-class OverrideExampleResolver {
-
-  @Query()
-  products(@Ctx() ctx: RequestContext, @Args() args: any) {
-    // when the `products` query is executed, this resolver function will
-    // now handle it.
-  }
-  
-  @Transaction()
-  @Mutation()
-  addItemToOrder(@Ctx() ctx: RequestContext, @Args() args: any) {
-    // when the `addItemToOrder` mutation is executed, this resolver function will
-    // now handle it.
-  }
-
-}
-```
-
-The same can be done for resolving fields:
-
-```ts
-import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
-import { Ctx, RequestContext, Product } from '@vendure/core';
-
-@Resolver('Product')
-export class FieldOverrideExampleResolver {
-  
-  @ResolveField()
-  description(@Ctx() ctx: RequestContext, @Parent() product: Product) {
-    return this.wrapInFormatting(ctx, product.id);
-  }
-  
-  private wrapInFormatting(ctx: RequestContext, id: ID): string {
-    // implementation omitted, but wraps the description
-    // text in some special formatting required by the storefront
-  }
-}
-```
-
-## Resolving union results
-
-When dealing with operations that return a GraphQL union type, there is an extra step needed.
-
-Union types are commonly returned from mutations in the Vendure APIs. For more detail on this see the section on [ErrorResults]({{< relref "error-handling" >}}#expected-errors-errorresults). For example: 
-
-```graphql
-type MyCustomErrorResult implements ErrorResult {
-  errorCode: ErrorCode!
-  message: String!
-}
-
-union MyCustomMutationResult = Order | MyCustomErrorResult
-
-extend type Mutation {
-  myCustomMutation(orderId: ID!): MyCustomMutationResult!
-}
-```
-
-In this example, the resolver which handles the `myCustomMutation` operation will be returning either an `Order` object or a `MyCustomErrorResult` object. The problem here is that the GraphQL server has no way of knowing which one it is at run-time. Luckily Apollo Server (on which Vendure is built) has a means to solve this:
-
-> To fully resolve a union, Apollo Server needs to specify which of the union's types is being returned. To achieve this, you define a `__resolveType` function for the union in your resolver map.
-> 
-> The `__resolveType` function is responsible for determining an object's corresponding GraphQL type and returning the name of that type as a string.
-> 
--- <cite>Source: [Apollo Server docs](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/#resolving-a-union)</cite>
-
-In order to implement a `__resolveType` function as part of your plugin, you need to create a dedicated Resolver class with a single field resolver method which will look like this:
-
-```ts
-import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
-import { Ctx, RequestContext, ProductVariant } from '@vendure/core';
-
-@Resolver('MyCustomMutationResult')
-export class MyCustomMutationResultResolver {
-  
-  @ResolveField()
-  __resolveType(value: any): string {
-    // If it has an "id" property we can assume it is an Order.  
-    return value.hasOwnProperty('id') ? 'Order' : 'MyCustomErrorResult';
-  }
-}
-```
-
-This resolver is then passed in to your plugin metadata like any other resolver:
-
-```ts
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  shopApiExtensions: {
-    schema: gql` ... `,
-    resolvers: [/* ... */, MyCustomMutationResultResolver]
-  }
-})
-export class MyPlugin {}
-```
-
-## Defining custom scalars
-
-By default, Vendure bundles `DateTime` and a `JSON` custom scalars (from the [graphql-scalars library](https://github.com/Urigo/graphql-scalars)). From v1.7.0, you can also define your own custom scalars for use in your schema extensions:
-
-```ts
-import { GraphQLScalarType} from 'graphql';
-import { GraphQLEmailAddress } from 'graphql-scalars';
-
-// Scalars can be custom-built as like this one,
-// or imported from a pre-made scalar library like
-// the GraphQLEmailAddress example.
-const FooScalar = new GraphQLScalarType({
-  name: 'Foo',
-  description: 'A test scalar',
-  serialize(value) {
-    // ...
-  },
-  parseValue(value) {
-    // ...
-  },
-});
-
-@VendurePlugin({
-  imports: [PluginCommonModule],
-  shopApiExtensions: {
-    schema: gql`
-      scalar Foo
-      scalar EmailAddress
-    `,
-    scalars: { 
-      // The key must match the scalar name
-      // given in the schema  
-      Foo: FooScalar,
-      EmailAddress: GraphQLEmailAddress,
-    },
-  },
-})
-export class CustomScalarsPlugin {}
-```

+ 0 - 163
docs/docs/guides/plugins/plugin-examples/using-job-queue-service.md

@@ -1,163 +0,0 @@
----
-title: "Using the Job Queue"
-weight: 4
-showtoc: true
----
-
-# Using the Job Queue
-
-If your plugin involves long-running tasks, you can take advantage of the [job queue system]({{< relref "/guides/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.
-
-```ts
-// 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: { productId: ID; videoUrl: string; }) {
-    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`:
-```ts
-// 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';
-
-@Injectable()
-class ProductVideoService implements OnModuleInit { 
-    
-  private jobQueue: JobQueue<{ productId: ID; videoUrl: string; }>;
-  
-  constructor(private jobQueueService: JobQueueService,
-              private connection: TransactionalConnection) {}
-
-  async onModuleInit() {
-    this.jobQueue = await this.jobQueueService.createQueue({
-      name: 'transcode-video',
-      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.
-        const result = await transcode(job.data.videoUrl);
-        await this.connection.getRepository(Product).save({
-          id: job.data.productId,
-          customFields: {
-            videoUrl: result.url,
-          },
-        });
-        // The value returned from the `process` function is stored as the "result"
-        // field of the job (for those JobQueueStrategies that support recording of results).
-        //  
-        // Any error thrown from this function will cause the job to fail.  
-        return result
-      },
-    });
-  }
-
-  transcodeForProduct(productId: ID, videoUrl: string) { 
-    // Add a new job to the queue and immediately return the
-    // job itself.
-    return this.jobQueue.add({ productId, videoUrl }, { retries: 2 });
-  }
-}
-```
-The `ProductVideoService` is in charge of setting up the JobQueue and adding jobs to that queue. Calling 
-
-```ts
-productVideoService.transcodeForProduct(id, url);
-```
-
-will add a transcoding job to the queue.
-
-{{< alert warning >}}
-**Note:** plugin code typically gets executed on both the server _and_ the worker. Therefore, you sometimes need to explicitly check
-what context you are in. This can be done with the [ProcessContext]({{< relref "process-context" >}}) provider.
-{{< /alert >}}
-
-```ts
-// 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.
-
-## Subscribing to job updates
-
-When creating a new job via `JobQueue.add()`, it is possible to subscribe to updates to that Job (progress and status changes). This allows you, for example, to create resolvers which are able to return the results of a given Job.
-
-In the video transcoding example above, we could modify the `transcodeForProduct()` call to look like this:
-
-```ts
-import { of } from 'rxjs';
-import { map, catchError } from 'rxjs/operators';
-
-@Injectable()
-class ProductVideoService implements OnModuleInit { 
-  // ... omitted
-    
-  transcodeForProduct(productId: ID, videoUrl: string) { 
-    const job = await this.jobQueue.add({ productId, videoUrl }, { retries: 2 });
-    
-    return job.updates().pipe(
-      map(update => {
-        // The returned Observable will emit a value for every update to the job
-        // such as when the `progress` or `status` value changes.
-        Logger.info(`Job ${update.id}: progress: ${update.progress}`);
-        if (update.state === JobState.COMPLETED) {
-          Logger.info(`COMPLETED ${update.id}: ${update.result}`);
-        }
-        return update.result;
-      }),
-      catchError(err => of(err.message)),
-    );
-  }
-}
-```
-
-If you prefer to work with Promises rather than Rxjs Observables, you can also convert the updates to a promise:
-
-```ts
-const job = await this.jobQueue.add({ productId, videoUrl }, { retries: 2 });
-    
-return job.updates().toPromise()
-  .then(/* ... */)
-  .catch(/* ... */);
-```

+ 3 - 16
docs/docs/reference/typescript-api/money/index.md

@@ -1,6 +1,6 @@
 ---
 title: "Money"
-isDefaultIndex: false
+isDefaultIndex: true
 generated: true
 ---
 <!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
@@ -9,19 +9,6 @@ import GenerationInfo from '@site/src/components/GenerationInfo';
 import MemberDescription from '@site/src/components/MemberDescription';
 
 
-## Money
-
-<GenerationInfo sourceFile="packages/core/src/entity/money.decorator.ts" sourceLine="29" packageName="@vendure/core" since="2.0.0" />
-
-Use this decorator for any entity field that is storing a monetary value.
-This allows the column type to be defined by the configured <a href='/reference/typescript-api/money/money-strategy#moneystrategy'>MoneyStrategy</a>.
-
-```ts title="Signature"
-function Money(options?: MoneyColumnOptions): void
-```
-Parameters
-
-### options
-
-<MemberInfo kind="parameter" type={`MoneyColumnOptions`} />
+import DocCardList from '@theme/DocCardList';
 
+<DocCardList />

+ 27 - 0
docs/docs/reference/typescript-api/money/money-decorator.md

@@ -0,0 +1,27 @@
+---
+title: "Money Decorator"
+isDefaultIndex: false
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+import MemberInfo from '@site/src/components/MemberInfo';
+import GenerationInfo from '@site/src/components/GenerationInfo';
+import MemberDescription from '@site/src/components/MemberDescription';
+
+
+## Money
+
+<GenerationInfo sourceFile="packages/core/src/entity/money.decorator.ts" sourceLine="26" packageName="@vendure/core" since="2.0.0" />
+
+Use this decorator for any entity field that is storing a monetary value.
+This allows the column type to be defined by the configured <a href='/reference/typescript-api/money/money-strategy#moneystrategy'>MoneyStrategy</a>.
+
+```ts title="Signature"
+function Money(options?: MoneyColumnOptions): void
+```
+Parameters
+
+### options
+
+<MemberInfo kind="parameter" type={`MoneyColumnOptions`} />
+

+ 1 - 0
docs/sidebars.js

@@ -45,6 +45,7 @@ const sidebars = {
                 'guides/developer-guide/the-api-layer/index',
                 'guides/developer-guide/the-service-layer/index',
                 'guides/developer-guide/custom-fields/index',
+                'guides/developer-guide/events/index',
                 'guides/developer-guide/strategies-configurable-operations/index',
                 'guides/developer-guide/worker-job-queue/index',
                 'guides/developer-guide/plugins/index',

+ 1 - 0
packages/core/src/entity/money.decorator.ts

@@ -20,6 +20,7 @@ const moneyColumnRegistry = new Map<any, MoneyColumnConfig[]>();
  * This allows the column type to be defined by the configured {@link MoneyStrategy}.
  *
  * @docsCategory money
+ * @docsPage Money Decorator
  * @since 2.0.0
  */
 export function Money(options?: MoneyColumnOptions) {