Explorar el Código

docs: Improve organization

Michael Bromley hace 2 años
padre
commit
ff60108176
Se han modificado 31 ficheros con 966 adiciones y 1044 borrados
  1. 1 1
      docs/docs/guides/deployment/getting-data-into-production.md
  2. 0 0
      docs/docs/guides/developer-guide/db-subscribers/index.md
  3. 1 1
      docs/docs/guides/developer-guide/error-handling/index.mdx
  4. 0 0
      docs/docs/guides/developer-guide/logging/index.md
  5. 1 1
      docs/docs/guides/developer-guide/migrating-from-v1/breaking-api-changes.md
  6. 1 1
      docs/docs/guides/developer-guide/migrating-from-v1/database-migration.md
  7. 1 0
      docs/docs/guides/developer-guide/migrating-from-v1/index.md
  8. 1 1
      docs/docs/guides/developer-guide/migrating-from-v1/storefront-migration.md
  9. 636 2
      docs/docs/guides/developer-guide/plugins/index.mdx
  10. 0 0
      docs/docs/guides/developer-guide/stand-alone-scripts/index.md
  11. 0 0
      docs/docs/guides/developer-guide/testing/index.md
  12. 0 0
      docs/docs/guides/developer-guide/translations/index.md
  13. 0 0
      docs/docs/guides/developer-guide/updating/index.md
  14. 192 3
      docs/docs/guides/developer-guide/worker-job-queue/index.mdx
  15. 52 39
      docs/docs/guides/extending-the-admin-ui/introduction.md
  16. 16 2
      docs/docs/guides/getting-started/graphql-intro/index.mdx
  17. 1 1
      docs/docs/guides/how-to/extend-graphql-api/index.md
  18. 1 1
      docs/docs/guides/how-to/importing-data/index.md
  19. 0 195
      docs/docs/guides/how-to/job-queue/index.md
  20. 0 639
      docs/docs/guides/how-to/writing-a-vendure-plugin.md
  21. 0 25
      docs/docs/guides/plugins/index.md
  22. 0 17
      docs/docs/guides/plugins/plugin-architecture/index.md
  23. BIN
      docs/docs/guides/plugins/plugin-architecture/plugin_architecture.png
  24. 0 13
      docs/docs/guides/plugins/plugin-examples/index.md
  25. 0 28
      docs/docs/guides/plugins/plugin-examples/modifying-config.md
  26. 0 51
      docs/docs/guides/plugins/plugin-lifecycle/index.md
  27. 8 0
      docs/docs/guides/storefront/codegen/index.mdx
  28. 1 1
      docs/docs/guides/storefront/connect-api/index.mdx
  29. 1 1
      docs/docs/guides/storefront/product-detail/index.mdx
  30. 45 20
      docs/sidebars.js
  31. 7 1
      docs/src/css/custom.css

+ 1 - 1
docs/docs/guides/deployment/getting-data-into-production.md

@@ -47,4 +47,4 @@ Importing initial and catalog data can be handled by Vendure `populate()` helper
 
 ## Importing other data
 
-Any kinds of data not covered by the `populate()` function can be imported using a custom script, which can use any Vendure service or service defined by your custom plugins to populate data in any way you like. See the [Stand-alone scripts guide](/guides/advanced-topics/stand-alone-scripts).
+Any kinds of data not covered by the `populate()` function can be imported using a custom script, which can use any Vendure service or service defined by your custom plugins to populate data in any way you like. See the [Stand-alone scripts guide](/guides/developer-guide/stand-alone-scripts/).

+ 0 - 0
docs/docs/guides/advanced-topics/defining-db-subscribers.md → docs/docs/guides/developer-guide/db-subscribers/index.md


+ 1 - 1
docs/docs/guides/advanced-topics/error-handling.mdx → docs/docs/guides/developer-guide/error-handling/index.mdx

@@ -258,7 +258,7 @@ switch (result.applyCouponCode.__typename) {
 }
 ```
 
-If we combine this approach with [GraphQL code generation](/guides/advanced-topics/codegen), then TypeScript will even be able to
+If we combine this approach with [GraphQL code generation](/guides/storefront/codegen/), then TypeScript will even be able to
 help us ensure that we have handled all possible ErrorResults:
 
 ```ts

+ 0 - 0
docs/docs/guides/advanced-topics/logging.md → docs/docs/guides/developer-guide/logging/index.md


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

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

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

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

+ 1 - 0
docs/docs/guides/advanced-topics/migrating-from-v1/index.md → docs/docs/guides/developer-guide/migrating-from-v1/index.md

@@ -1,6 +1,7 @@
 ---
 title: "Migrating from v1"
 weight: 2
+sidebar_position: 1
 ---
 
 # Migrating from Vendure 1 to 2

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

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

+ 636 - 2
docs/docs/guides/developer-guide/plugins/index.mdx

@@ -3,6 +3,9 @@ title: 'Plugins'
 sidebar_position: 6
 ---
 
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
 The heart of Vendure is its plugin system. Plugins not only allow you to instantly add new functionality to your
 Vendure server via third-part npm packages, they are also the means by which you build out the custom business
 logic of your application.
@@ -131,6 +134,637 @@ export class MyPlugin implements NestModule {
 }
 ```
 
-## Complete example
+## Writing your first plugin
+
+In Vendure **plugins** are used to extend the core functionality of the server. Plugins can be pre-made functionality that you can install via npm, or they can be custom plugins that you write yourself.
+
+For any unit of functionality that you need to add to your project, you'll be writing creating a Vendure plugin. By convention, plugins are stored in the `plugins` directory of your project. However, this is not a requirement, and you are free to arrange your plugin files in any way you like.
+
+```txt
+├──src
+    ├── index.ts
+    ├── vendure-config.ts
+    ├── plugins
+        ├── reviews-plugin
+        ├── cms-plugin
+        ├── wishlist-plugin
+        ├── stock-sync-plugin
+```
+
+:::info
+For a complete working example of a Vendure plugin, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
+
+If you intend to write a shared plugin to be distributed as an npm package, see the [vendure plugin-template repo](https://github.com/vendure-ecommerce/plugin-template)
+:::
+
+In this guide, we will implement a simple but fully-functional **wishlist plugin** step-by-step. The goal of this plugin is to allow signed-in customers to add products to a wishlist, and to view and manage their wishlist.
+
+### Step 1: Create the plugin file
+
+We'll start by creating a new directory to house our plugin, add create the main plugin file:
+
+```txt
+├──src
+    ├── index.ts
+    ├── vendure-config.ts
+    ├── plugins
+        // highlight-next-line
+        ├── wishlist-plugin
+            // highlight-next-line
+            ├── wishlist.plugin.ts
+```
+
+```ts title="src/plugins/reviews-plugin/reviews.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+})
+export class WishlistPlugin {}
+```
+
+The `PluginCommonModule` will be required in all plugins that you create. It contains the common services that are exposed by Vendure Core, allowing you to inject them into your plugin's services and resolvers.
+
+### Step 2: Define an entity
+
+Next we will define a new database entity to store the wishlist items. Vendure uses [TypeORM](https://typeorm.io/) to manage the database schema, and an Entity corresponds to a database table.
+
+First let's create the file to house the entity:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    ├── entities
+        // highlight-next-line
+        ├── wishlist-item.entity.ts
+```
+
+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, EntityId } from '@vendure/core';
+import { Column, Entity, ManyToOne } from 'typeorm';
+
+@Entity()
+export class WishlistItem extends VendureEntity {
+    constructor(input?: DeepPartial<WishlistItem>) {
+        super(input);
+    }
+
+    @ManyToOne(type => ProductVariant)
+    productVariant: ProductVariant;
+
+    @EntityId()
+    productVariantId: ID;
+}
+```
+
+Let's break down what's happening here:
+
+-   The `WishlistItem` entity extends the [`VendureEntity` class](/reference/typescript-api/entities/vendure-entity/). This is a base class which provides the `id`, `createdAt` and `updatedAt` fields, and all custom entities should extend it.
+-   The `@Entity()` decorator marks this class as a TypeORM entity.
+-   The `@ManyToOne()` decorator defines a many-to-one relationship with the `ProductVariant` entity. This means that each `WishlistItem` will be associated with a single `ProductVariant`.
+-   The `productVariantId` column is not strictly necessary, but it allows us to always have access to the ID of the related `ProductVariant` without having to load the entire `ProductVariant` entity from the database.
+-   The `constructor()` is used to create a new instance of the entity. This is not strictly necessary, but it is a good practice to define a constructor which takes a `DeepPartial` of the entity as an argument. This allows us to create new instances of the entity using the `new` keyword, passing in a plain object with the desired properties.
+
+Next we need to register this entity with our plugin:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { WishlistItem } from './entities/wishlist-item.entity';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    entities: [WishlistItem],
+})
+export class WishlistPlugin {}
+```
+
+### Step 3: Add a custom field to the Customer entity
+
+We'll now define a new custom field on the Customer entity which will store a list of WishlistItems. This will allow us to easily query for all wishlist items associated with a particular customer.
+
+Custom fields are defined in the VendureConfig object, and in a plugin we use the `configuration` function to modify the config object:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { WishlistItem } from './entities/wishlist-item.entity';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    entities: [WishlistItem],
+    configuration: config => {
+        config.customFields.Customer.push({
+            name: 'wishlistItems',
+            type: 'relation',
+            list: true,
+            entity: WishlistItem,
+            internal: true,
+        });
+        return config;
+    },
+})
+export class WishlistPlugin {}
+```
+
+In this snippet we are pushing a new custom field definition onto the `Customer` entity's `customFields` array, and defining this new field as a list (array) of `WishlistItem` entities. Internally, this will tell TypeORM to update the database schema to store this new field. We set `internal: true` to indicate that this field should not be directly exposed to the GraphQL API as `Customer.customFields.wishlistItems`, but instead should be accessed via a custom resolver we will define later.
+
+In order to make use of this custom field in a type-safe way, we can tell TypeScript about this field in a new file:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    // highlight-next-line
+    ├── types.ts
+```
+
+```ts title="src/plugins/wishlist-plugin/types.ts"
+import { CustomCustomerFields } from '@vendure/core/dist/entity/custom-entity-fields';
+import { WishlistItem } from './entities/wishlist-item.entity';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+  interface CustomCustomerFields {
+    wishlistItems: WishlistItem[];
+  }
+}
+```
+
+We can then import this types file in our plugin's main file:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+// highlight-next-line
+import './types';
+```
+
+### Step 4: Create a service
+
+A "service" is a class which houses the bulk of the business logic of any plugin. A plugin can define multiple services if needed, but each service should be responsible for a single unit of functionality, such as dealing with a particular entity, or performing a particular task.
+
+Let's create a service to handle the wishlist functionality:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    ├── services
+        // highlight-next-line
+        ├── wishlist.service.ts
+```
+
+```ts title="src/plugins/wishlist-plugin/wishlist.service.ts"
+import { Injectable } from '@nestjs/common';
+import {
+    Customer,
+    ForbiddenError,
+    ID,
+    InternalServerError,
+    ProductVariantService,
+    RequestContext,
+    TransactionalConnection,
+    UserInputError,
+} from '@vendure/core';
+
+import { WishlistItem } from '../entities/wishlist-item.entity';
+
+@Injectable()
+export class WishlistService {
+    constructor(
+        private connection: TransactionalConnection,
+        private productVariantService: ProductVariantService,
+    ) {}
+
+    async getWishlistItems(ctx: RequestContext): Promise<WishlistItem[]> {
+        try {
+            const customer = await this.getCustomerWithWishlistItems(ctx);
+            return customer.customFields.wishlistItems;
+        } catch (err: any) {
+            return [];
+        }
+    }
+
+    /**
+     * Adds a new item to the active Customer's wishlist.
+     */
+    async addItem(ctx: RequestContext, variantId: ID): Promise<WishlistItem[]> {
+        const customer = await this.getCustomerWithWishlistItems(ctx);
+        const variant = this.productVariantService.findOne(ctx, variantId);
+        if (!variant) {
+            throw new UserInputError(`No ProductVariant with the id ${variantId} could be found`);
+        }
+        const existingItem = customer.customFields.wishlistItems.find(i => i.productVariantId === variantId);
+        if (existingItem) {
+            // Item already exists in wishlist, do not
+            // add it again
+            return customer.customFields.wishlistItems;
+        }
+        const wishlistItem = await this.connection
+            .getRepository(ctx, WishlistItem)
+            .save(new WishlistItem({ productVariantId: variantId }));
+        customer.customFields.wishlistItems.push(wishlistItem);
+        await this.connection.getRepository(ctx, Customer).save(customer, { reload: false });
+        return this.getWishlistItems(ctx);
+    }
+
+    /**
+     * Removes an item from the active Customer's wishlist.
+     */
+    async removeItem(ctx: RequestContext, itemId: ID): Promise<WishlistItem[]> {
+        const customer = await this.getCustomerWithWishlistItems(ctx);
+        const itemToRemove = customer.customFields.wishlistItems.find(i => i.id === itemId);
+        if (itemToRemove) {
+            await this.connection.getRepository(ctx, WishlistItem).remove(itemToRemove);
+            customer.customFields.wishlistItems = customer.customFields.wishlistItems.filter(
+                i => i.id !== itemId,
+            );
+        }
+        await this.connection.getRepository(ctx, Customer).save(customer);
+        return this.getWishlistItems(ctx);
+    }
+
+    /**
+     * Gets the active Customer from the context and loads the wishlist items.
+     */
+    private async getCustomerWithWishlistItems(ctx: RequestContext): Promise<Customer> {
+        if (!ctx.activeUserId) {
+            throw new ForbiddenError();
+        }
+        const customer = await this.connection.getRepository(ctx, Customer).findOne({
+            where: { user: { id: ctx.activeUserId } },
+            relations: {
+                customFields: {
+                    wishlistItems: {
+                        productVariant: true,
+                    },
+                },
+            },
+        });
+        if (!customer) {
+            throw new InternalServerError(`Customer was not found`);
+        }
+        return customer;
+    }
+}
+```
+
+Let's break down what's happening here:
+
+- The `WishlistService` class is decorated with the `@Injectable()` decorator. This is a standard NestJS decorator which tells the NestJS dependency injection (DI) system that this class can be injected into other classes. All your services should be decorated with this decorator.
+- The arguments passed to the constructor will be injected by the NestJS DI system. The `connection` argument is a [TransactionalConnection](/reference/typescript-api/data-access/transactional-connection/) instance, which is used to access and manipulate data in the database. The [`ProductVariantService`](/reference/typescript-api/services/product-variant-service/) argument is a built-in Vendure service which contains methods relating to ProductVariants.
+- The [`RequestContext`](/reference/typescript-api/request/request-context/) object is usually the first argument to any service method, and contains information and context about the current request as well as any open database transactions. It should always be passed to the methods of the `TransactionalConnection`.
+
+The service is then registered with the plugin metadata as a provider:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { WishlistService } from './services/wishlist.service';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    // highlight-next-line
+    providers: [WishlistService],
+    entities: [WishlistItem],
+    configuration: config => {
+        // ...
+    },
+})
+export class WishlistPlugin {}
+```
+
+### Step 5: Extend the GraphQL API
+
+This plugin will need to extend the Shop API, adding new mutations and queries to enable the customer to view and manage their wishlist.
+
+First we will create a new file to hold the GraphQL schema extensions:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    ├── api
+        // highlight-next-line
+        ├── api-extensions.ts
+```
+
+```ts title="src/plugins/wishlist-plugin/api/api-extensions.ts"
+import gql from 'graphql-tag';
+
+export const shopApiExtensions = gql`
+    type WishlistItem implements Node {
+        id: ID!
+        createdAt: DateTime!
+        updatedAt: DateTime!
+        productVariant: ProductVariant!
+        productVariantId: ID!
+    }
+
+    extend type Query {
+        activeCustomerWishlist: [WishlistItem!]!
+    }
+
+    extend type Mutation {
+        addToWishlist(productVariantId: ID!): [WishlistItem!]!
+        removeFromWishlist(itemId: ID!): [WishlistItem!]!
+    }
+`;
+```
+
+:::note
+
+The `graphql-tag` package is a dependency of the Vendure core package. Depending on the package manager you are using, you may need to install it separately with `yarn add graphql-tag` or `npm install graphql-tag`.
+
+:::
+
+The `api-extensions.ts` file is where we define the extensions we will be making to the Shop API GraphQL schema. We are defining a new `WishlistItem` type; a new query: `activeCustomerWishlist`; and two new mutations: `addToWishlist` and `removeFromWishlist`. This definition is written in [schema definition language](https://graphql.org/learn/schema/) (SDL), a convenient syntax for defining GraphQL schemas.
+
+Next we need to pass these extensions to our plugin's metadata:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { shopApiExtensions } from './api/api-extensions';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    shopApiExtensions: {
+        schema: shopApiExtensions,
+        resolvers: [],
+    },
+})
+export class WishlistPlugin {}
+```
+
+### Step 6: Create a resolver
+
+Now that we have defined the GraphQL schema extensions, we need to create a resolver to handle the new queries and mutations. A resolver in GraphQL is a function which actually implements the query or mutation defined in the schema. This is done by creating a new file in the `api` directory:
+
+```txt
+├── wishlist-plugin
+    ├── wishlist.plugin.ts
+    ├── api
+        ├── api-extensions.ts
+        // highlight-next-line
+        ├── wishlist.resolver.ts
+```
+
+```ts title="src/plugins/wishlist-plugin/api/wishlist.resolver.ts"
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import { Allow, Ctx, Permission, RequestContext, Transaction } from '@vendure/core';
+
+import { WishlistItem } from '../entities/wishlist-item.entity';
+import { WishlistService } from '../service/wishlist.service';
+
+@Resolver()
+export class WishlistShopResolver {
+    constructor(private wishlistService: WishlistService) {}
+
+    @Query()
+    @Allow(Permission.Owner)
+    activeCustomerWishlist(@Ctx() ctx: RequestContext) {
+        return this.wishlistService.getWishlistItems(ctx);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.Owner)
+    async addToWishlist(
+        @Ctx() ctx: RequestContext,
+        @Args() { productVariantId }: { productVariantId: string },
+    ) {
+        return this.wishlistService.addItem(ctx, productVariantId);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.Owner)
+    async removeFromWishlist(@Ctx() ctx: RequestContext, @Args() { itemId }: { itemId: string }) {
+        return this.wishlistService.removeItem(ctx, itemId);
+    }
+}
+```
+
+Resolvers are usually "thin" functions that delegate the actual work to a service. Vendure, like NestJS itself, makes heavy use of decorators at the API layer to define various aspects of the resolver. Let's break down what's happening here:
+
+- The `@Resolver()` decorator tells the NestJS DI system that this class is a resolver. Since a Resolver is part of the NestJS DI system, we can also inject dependencies into its constructor. In this case we are injecting the `WishlistService` which we created in the previous step.
+- The `@Mutation()` decorator tells Vendure that this is a mutation resolver. Similarly, `@Query()` decorator defines a query resolver. The name of the method is the name of the query or mutation in the schema.
+- The `@Transaction()` decorator tells Vendure that this resolver method should be wrapped in a database transaction. This is important because we are performing multiple database operations in this method, and we want them to be atomic.
+- The `@Allow()` decorator tells Vendure that this mutation is only allowed for users with the `Owner` permission. The `Owner` permission is a special permission which indicates that the active user should be the owner of this operation.
+- The `@Ctx()` decorator tells Vendure that this method requires access to the `RequestContext` object. Every resolver should have this as the first argument, as it is required throughout the Vendure request lifecycle.
+
+This resolver is then registered with the plugin metadata:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { shopApiExtensions } from './api/api-extensions';
+import { WishlistShopResolver } from './api/wishlist.resolver';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    shopApiExtensions: {
+        schema: shopApiExtensions,
+        // highlight-next-line
+        resolvers: [WishlistShopResolver],
+    },
+    configuration: config => {
+        // ...
+    },
+})
+export class WishlistPlugin {}
+```
+
+:::info
+
+More information about resolvers can be found in the [NestJS docs](https://docs.nestjs.com/graphql/resolvers).
+
+:::
+
+### Step 7: Specify compatibility
+
+Since Vendure v2.0.0, it is possible for a plugin to specify which versions of Vendure core it is compatible with. This is especially important if the plugin is intended to be made publicly available via npm or another package registry.
+
+The compatibility is specified via the `compatibility` property in the plugin metadata:
+
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
+@VendurePlugin({
+    // ...
+    // highlight-next-line
+    compatibility: '^2.0.0',
+})
+export class WishlistPlugin {}
+```
+
+The value of this property is a [semver range](https://docs.npmjs.com/about-semantic-versioning) which specifies the range of compatible versions. In this case, we are saying that this plugin is compatible with any version of Vendure core which is `>= 2.0.0 < 3.0.0`.
+
+### Step 8: Add the plugin to the VendureConfig
+
+The final step is to add the plugin to the `VendureConfig` object. This is done in the `vendure-config.ts` file:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { WishlistPlugin } from './plugins/wishlist-plugin/wishlist.plugin';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        // ...
+        // highlight-next-line
+        WishlistPlugin,
+    ],
+};
+```
+
+### Test the plugin
+
+Now that the plugin is installed, we can test it out. Since we have defined a custom field, we'll need to generate and run a migration to add the new column to the database:
+
+```bash
+npm run migration:generate wishlist-plugin
+```
+
+Then start the server:
+
+```bash
+npm run dev
+```
+
+Once the server is running, we should be able to log in as an existing Customer, and then add a product to the wishlist:
+
+<Tabs>
+<TabItem value="Login mutation" label="Login mutation" default>
+
+```graphql
+mutation Login {
+    login(username: "alec.breitenberg@gmail.com", password: "test") {
+        ... on CurrentUser {
+            id
+            identifier
+        }
+        ... on ErrorResult {
+            errorCode
+            message
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "login": {
+      "id": "9",
+      "identifier": "alec.breitenberg@gmail.com"
+    }
+  }
+}
+```
+
+  </TabItem>
+</Tabs>
+
+
+<Tabs>
+<TabItem value="AddToWishlist mutation" label="AddToWishlist mutation" default>
+
+```graphql
+mutation AddToWishlist {
+    addToWishlist(productVariantId: "7") {
+        id
+        productVariant {
+            id
+            name
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "addToWishlist": [
+      {
+        "id": "4",
+        "productVariant": {
+          "id": "7",
+          "name": "Wireless Optical Mouse"
+        }
+      }
+    ]
+  }
+}
+```
+
+  </TabItem>
+</Tabs>
+
+We can then query the wishlist items:
+
+
+<Tabs>
+<TabItem value="GetWishlist mutation" label="GetWishlist mutation" default>
+
+```graphql
+query GetWishlist {
+    activeCustomerWishlist {
+        id
+        productVariant {
+            id
+            name
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "activeCustomerWishlist": [
+      {
+        "id": "4",
+        "productVariant": {
+          "id": "7",
+          "name": "Wireless Optical Mouse"
+        }
+      }
+    ]
+  }
+}
+```
+
+  </TabItem>
+</Tabs>
+
+And finally, we can test removing an item from the wishlist:
+
+<Tabs>
+<TabItem value="RemoveFromWishlist mutation" label="RemoveFromWishlist mutation" default>
+
+```graphql
+mutation RemoveFromWishlist {
+    removeFromWishlist(itemId: "4") {
+        id
+        productVariant {
+            name
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "removeFromWishlist": []
+  }
+}
+```
+
+  </TabItem>
+</Tabs>
 
-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).

+ 0 - 0
docs/docs/guides/advanced-topics/stand-alone-scripts.md → docs/docs/guides/developer-guide/stand-alone-scripts/index.md


+ 0 - 0
docs/docs/guides/advanced-topics/testing.md → docs/docs/guides/developer-guide/testing/index.md


+ 0 - 0
docs/docs/guides/advanced-topics/translations.md → docs/docs/guides/developer-guide/translations/index.md


+ 0 - 0
docs/docs/guides/advanced-topics/updating-vendure.md → docs/docs/guides/developer-guide/updating/index.md


+ 192 - 3
docs/docs/guides/developer-guide/worker-job-queue/index.mdx

@@ -165,11 +165,200 @@ this will result in a constant 50 queries/second.
 In this case it is recommended to try the [BullMQJobQueuePlugin](/reference/core-plugins/job-queue-plugin/bull-mqjob-queue-plugin/),
 which uses an efficient push-based strategy built on Redis.
 
-### Using Job Queues in a plugin
+## Using Job Queues in a plugin
 
-If you create a [Vendure plugin](/guides/developer-guide/plugins/) which involves some long-running tasks, you can also make use of the job queue. See the [JobQueue plugin example](/TODO)
-for a detailed annotated example.
+If your plugin involves long-running tasks, you can also make use of the job queue.
 
 :::info
 A real example of this can be seen in the [EmailPlugin source](https://github.com/vendure-ecommerce/vendure/blob/master/packages/email-plugin/src/plugin.ts)
 :::
+
+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(/* ... */);
+```

+ 52 - 39
docs/docs/guides/extending-the-admin-ui/introduction.md

@@ -6,22 +6,22 @@ title: 'Introduction'
 
 When creating a plugin, you may wish to extend the Admin UI in order to expose a graphical interface to the plugin's functionality.
 
-This is possible by defining [AdminUiExtensions]({{< ref "admin-ui-extension" >}}).
+This is possible by defining [AdminUiExtensions](/reference/admin-ui-api/ui-devkit/admin-ui-extension/).
 
-{{< alert "primary" >}}
+:::info
 For a complete working example of a Vendure plugin that extends the Admin UI, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
-{{< /alert >}}
+:::
 
 ## How It Works
 
-A UI extension is an [Angular module](https://angular.io/guide/ngmodules) that gets compiled into the Admin UI application bundle by the [`compileUiExtensions`]({{< relref "compile-ui-extensions" >}}) function exported by the `@vendure/ui-devkit` package. Internally, the ui-devkit package makes use of the Angular CLI to compile an optimized set of JavaScript bundles containing your extensions.
+A UI extension is an [Angular module](https://angular.io/guide/ngmodules) that gets compiled into the Admin UI application bundle by the [`compileUiExtensions`](/reference/admin-ui-api/ui-devkit/compile-ui-extensions) function exported by the `@vendure/ui-devkit` package. Internally, the ui-devkit package makes use of the Angular CLI to compile an optimized set of JavaScript bundles containing your extensions.
 
 ## Use Your Favourite Framework
 
 The Vendure Admin UI is built with Angular, and writing UI extensions in Angular is seamless and powerful. But if you are not familiar with Angular, that's no problem! You can write UI extensions using **React**, **Vue**, or **any other** web technology of choice!
 
-* [UI extensions in Angular]({{< relref "using-angular" >}})
-* [UI extensions in other frameworks]({{< relref "using-other-frameworks" >}})
+* [UI extensions in Angular](/guides/extending-the-admin-ui/using-angular/)
+* [UI extensions in other frameworks](/guides/extending-the-admin-ui/using-other-frameworks/)
 
 ## Lazy vs Shared Modules
 
@@ -29,34 +29,40 @@ Angular uses the concept of modules ([NgModules](https://angular.io/guide/ngmodu
 
 When creating your UI extensions, you can set your module to be either `lazy` or `shared`. Shared modules are loaded _eagerly_, i.e. their code is bundled up with the main app and loaded as soon as the app loads.
 
-As a rule, modules defining new routes should be lazily loaded (so that the code is only loaded once that route is activated), and modules defining [new navigations items]({{< relref "modifying-navigation-items" >}}) and [custom form input]({{< relref "custom-form-inputs" >}}) should be set to `shared`.
+As a rule, modules defining new routes should be lazily loaded (so that the code is only loaded once that route is activated), and modules defining [new navigations items](/guides/extending-the-admin-ui/modifying-navigation-items/) and [custom form input](/guides/extending-the-admin-ui/custom-form-inputs/) should be set to `shared`.
 
 ## Dev mode
 
 When you are developing your Admin UI extension, you can set the `devMode` option to `true` which will compile the Admin UI app in development mode, and recompile and auto-refresh the browser on any changes to your extension source files.
 
-```ts
-// vendure-config.ts
-plugins: [
-  AdminUiPlugin.init({
-    port: 3002,
-    app: compileUiExtensions({
-      outputPath: path.join(__dirname, '../admin-ui'),
-      extensions: [{
-        // ...
-      }],
-      devMode: true,
-    }),
-  }),
-],
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
+import * as path from 'path';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        AdminUiPlugin.init({
+            port: 3002,
+            app: compileUiExtensions({
+                outputPath: path.join(__dirname, '../admin-ui'),
+                extensions: [{
+                    // ...
+                }],
+                devMode: true,
+            }),
+        }),
+    ],
+};
 ```
 
 ## Compiling as a deployment step
 
 Although the examples so far all use the `compileUiExtensions` function in conjunction with the AdminUiPlugin, it is also possible to use it on its own:
 
-```ts
-// compile-admin-ui.ts
+```ts title="src/compile-admin-ui.ts"
 import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
 import * as path from 'path';
 
@@ -77,19 +83,25 @@ yarn ts-node compile-admin-ui.ts
 
 Once complete, the production-ready app bundle will be output to `admin-ui/dist`. This method is suitable for a production setup, so that the Admin UI can be compiled ahead-of-time as part of your deployment process. This ensures that your Vendure server starts up as quickly as possible. In this case, you can pass the path of the compiled app to the AdminUiPlugin:
 
-```ts
-// project/vendure-config.ts
-plugins: [
-  AdminUiPlugin.init({
-    port: 3002,
-    app: {
-      path: 'admin-ui/dist'
-    }
-  }),
-],
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+import * as path from 'path';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        AdminUiPlugin.init({
+            port: 3002,
+            app: {
+                path: path.join(__dirname, '../admin-ui/dist'),
+            },
+        }),
+    ],
+};
 ```
 
-{{< alert warning >}}
+:::caution
 **Note:** the TypeScript source files of your UI extensions **must not** be compiled by your regular TypeScript build task. This is because they will instead be compiled by the Angular compiler when you run `compileUiExtensions()`. You can exclude them in your main `tsconfig.json` by adding a line to the "exclude" array:
 ```json
 {
@@ -98,8 +110,9 @@ plugins: [
   ]
 }
 ```
-{{< /alert >}}
-{{< alert warning >}}
+:::
+
+:::caution
 **Also note:** When you use compileUiExtensions to compile the Angular App, a new directory will be created to host the compiled Admin-UI app. The name and location of the app is specified by the "outputPath" which is set in the compileUiExtensions options object. Make sure to **exlcude** the admin-ui directory from typescript's transpilation since code there is already transpiled.
 ```json
 {
@@ -110,9 +123,9 @@ plugins: [
   ],
 }
 ```
-{{< /alert >}}
+:::
 
-{{< alert "primary" >}}
+:::info
 To compile the angular app ahead of time (for production) and copy the dist folder to Vendure's output dist folder, include the following commands in your packages.json scripts:
 ```json
 {
@@ -128,4 +141,4 @@ Make sure to install copyfiles before running the "copy" command:
 ```bash
 yarn install copyfiles
 ```
-{{< /alert >}}
+:::

+ 16 - 2
docs/docs/guides/getting-started/graphql-intro/index.mdx

@@ -51,7 +51,7 @@ Both GraphQL and REST are valid approaches to building an API. These are some of
 - **Many resources in a single request**: Very often, a single page in a web app will need to fetch data from multiple resources. For example, a product detail page might need to fetch the product, the product's variants, the product's collections, the product's reviews, and the product's images. With REST, this would require multiple requests. With GraphQL, you can fetch all of this data in a single request.
 - **Static typing**: GraphQL APIs are always defined by a statically typed schema. This means that you can be sure that the data you receive from the API will always be in the format you expect.
 - **Developer tooling**: The schema definition allows for powerful developer tooling. For example, the GraphQL Playground above with auto-complete and full documentation is generated automatically from the schema definition. You can also get auto-complete and type-checking directly in your IDE.
-- **Code generation**: TypeScript types can be generated automatically from the schema definition. This means that you can be sure that your frontend code is always in sync with the API. This end-to-end type safety is extremely valuable, especially when working on large projects or with teams. See the [GraphQL Code Generation guide](/guides/advanced-topics/codegen)
+- **Code generation**: TypeScript types can be generated automatically from the schema definition. This means that you can be sure that your frontend code is always in sync with the API. This end-to-end type safety is extremely valuable, especially when working on large projects or with teams. See the [GraphQL Code Generation guide](/guides/storefront/codegen/)
 - **Extensible**: Vendure is designed with extensibility in mind, and GraphQL is a perfect fit. You can extend the GraphQL API with your own custom queries, mutations, and types. You can also extend the built-in types with your own custom fields, or supply you own custom logic to resolve existing fields. See the [Extend the GraphQL API guide](/guides/how-to/extend-graphql-api/)
 
 ## GraphQL Terminology
@@ -399,7 +399,7 @@ type EmailAddressInUseError {
 ```
 
 :::info
-In Vendure, we use this pattern for almost all mutations. You can read more about it in the [Error Handling guide](/guides/advanced-topics/error-handling).
+In Vendure, we use this pattern for almost all mutations. You can read more about it in the [Error Handling guide](/guides/developer-guide/error-handling/).
 :::
 
 Now, when we perform this mutation, we need alter the way we select the fields in the response, since the response could be one of two types:
@@ -532,6 +532,20 @@ which fields of that object type we want to fetch. For example:
   }
   ```
 
+## IDE plugins
+
+Plugins are available for most popular IDEs & editors which provide auto-complete and type-checking for GraphQL operations
+as you write them. This is a huge productivity boost, and is **highly recommended**.
+
+- [GraphQL extension for VS Code](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql)
+- [GraphQL plugin for IntelliJ](https://plugins.jetbrains.com/plugin/8097-graphql) (including WebStorm)
+
+## Code generation
+
+Code generation means the automatic generation of TypeScript types based on your GraphQL schema and your GraphQL operations. This is a very powerful feature that allows you to write your code in a type-safe manner, without you needing to manually write any types for your API calls.
+
+For more information see the [GraphQL Code Generation guide](/guides/advanced-topics/codegen).
+
 ## Further reading
 
 This is just a very brief overview, intended to introduce you to the main concepts you'll need to build with Vendure.

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

@@ -384,7 +384,7 @@ export class FieldOverrideExampleResolver {
 
 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: 
+Union types are commonly returned from mutations in the Vendure APIs. For more detail on this see the section on [ErrorResults](/guides/developer-guide/error-handling#expected-errors-errorresults). For example: 
 
 ```graphql
 type MyCustomErrorResult implements ErrorResult {

+ 1 - 1
docs/docs/guides/how-to/importing-data/index.md

@@ -263,7 +263,7 @@ Running this script will populate the database with the test data like when you
 
 ### Custom populate scripts
 
-If you require more control over how your data is being imported - for example if you also need to import data into custom entities, or import customer or order information - you can create your own CLI script to do this: see [Stand-Alone CLI Scripts](/guides/advanced-topics/stand-alone-scripts).
+If you require more control over how your data is being imported - for example if you also need to import data into custom entities, or import customer or order information - you can create your own CLI script to do this: see [Stand-Alone CLI Scripts](/guides/developer-guide/stand-alone-scripts/).
 
 In addition to all the services available in the [Service Layer](/guides/developer-guide/the-service-layer/), the following specialized import services are available:
 

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

@@ -1,195 +0,0 @@
----
-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(/* ... */);
-```

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

@@ -1,639 +0,0 @@
----
-title: 'Writing your first plugin'
-sidebar_position: 4
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-In Vendure **plugins** are used to extend the core functionality of the server. Plugins can be pre-made functionality that you can install via npm, or they can be custom plugins that you write yourself.
-
-For any unit of functionality that you need to add to your project, you'll be writing creating a Vendure plugin. By convention, plugins are stored in the `plugins` directory of your project. However, this is not a requirement, and you are free to arrange your plugin files in any way you like.
-
-```txt
-├──src
-    ├── index.ts
-    ├── vendure-config.ts
-    ├── plugins
-        ├── reviews-plugin
-        ├── cms-plugin
-        ├── wishlist-plugin
-        ├── stock-sync-plugin
-```
-
-:::info
-For a complete working example of a Vendure plugin, see the [real-world-vendure Reviews plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/reviews)
-
-If you intend to write a shared plugin to be distributed as an npm package, see the [vendure plugin-template repo](https://github.com/vendure-ecommerce/plugin-template)
-:::
-
-In this guide, we will implement a simple but fully-functional **wishlist plugin** step-by-step. The goal of this plugin is to allow signed-in customers to add products to a wishlist, and to view and manage their wishlist.
-
-## Step 1: Create the plugin file
-
-We'll start by creating a new directory to house our plugin, add create the main plugin file:
-
-```txt
-├──src
-    ├── index.ts
-    ├── vendure-config.ts
-    ├── plugins
-        // highlight-next-line
-        ├── wishlist-plugin
-            // highlight-next-line
-            ├── wishlist.plugin.ts
-```
-
-```ts title="src/plugins/reviews-plugin/reviews.plugin.ts"
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-
-@VendurePlugin({
-    imports: [PluginCommonModule],
-})
-export class WishlistPlugin {}
-```
-
-The `PluginCommonModule` will be required in all plugins that you create. It contains the common services that are exposed by Vendure Core, allowing you to inject them into your plugin's services and resolvers.
-
-## Step 2: Define an entity
-
-Next we will define a new database entity to store the wishlist items. Vendure uses [TypeORM](https://typeorm.io/) to manage the database schema, and an Entity corresponds to a database table.
-
-First let's create the file to house the entity:
-
-```txt
-├── wishlist-plugin
-    ├── wishlist.plugin.ts
-    ├── entities
-        // highlight-next-line
-        ├── wishlist-item.entity.ts
-```
-
-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, EntityId } from '@vendure/core';
-import { Column, Entity, ManyToOne } from 'typeorm';
-
-@Entity()
-export class WishlistItem extends VendureEntity {
-    constructor(input?: DeepPartial<WishlistItem>) {
-        super(input);
-    }
-
-    @ManyToOne(type => ProductVariant)
-    productVariant: ProductVariant;
-
-    @EntityId()
-    productVariantId: ID;
-}
-```
-
-Let's break down what's happening here:
-
--   The `WishlistItem` entity extends the [`VendureEntity` class](/reference/typescript-api/entities/vendure-entity/). This is a base class which provides the `id`, `createdAt` and `updatedAt` fields, and all custom entities should extend it.
--   The `@Entity()` decorator marks this class as a TypeORM entity.
--   The `@ManyToOne()` decorator defines a many-to-one relationship with the `ProductVariant` entity. This means that each `WishlistItem` will be associated with a single `ProductVariant`.
--   The `productVariantId` column is not strictly necessary, but it allows us to always have access to the ID of the related `ProductVariant` without having to load the entire `ProductVariant` entity from the database.
--   The `constructor()` is used to create a new instance of the entity. This is not strictly necessary, but it is a good practice to define a constructor which takes a `DeepPartial` of the entity as an argument. This allows us to create new instances of the entity using the `new` keyword, passing in a plain object with the desired properties.
-
-Next we need to register this entity with our plugin:
-
-```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { WishlistItem } from './entities/wishlist-item.entity';
-
-@VendurePlugin({
-    imports: [PluginCommonModule],
-    entities: [WishlistItem],
-})
-export class WishlistPlugin {}
-```
-
-## Step 3: Add a custom field to the Customer entity
-
-We'll now define a new custom field on the Customer entity which will store a list of WishlistItems. This will allow us to easily query for all wishlist items associated with a particular customer.
-
-Custom fields are defined in the VendureConfig object, and in a plugin we use the `configuration` function to modify the config object:
-
-```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { WishlistItem } from './entities/wishlist-item.entity';
-
-@VendurePlugin({
-    imports: [PluginCommonModule],
-    entities: [WishlistItem],
-    configuration: config => {
-        config.customFields.Customer.push({
-            name: 'wishlistItems',
-            type: 'relation',
-            list: true,
-            entity: WishlistItem,
-            internal: true,
-        });
-        return config;
-    },
-})
-export class WishlistPlugin {}
-```
-
-In this snippet we are pushing a new custom field definition onto the `Customer` entity's `customFields` array, and defining this new field as a list (array) of `WishlistItem` entities. Internally, this will tell TypeORM to update the database schema to store this new field. We set `internal: true` to indicate that this field should not be directly exposed to the GraphQL API as `Customer.customFields.wishlistItems`, but instead should be accessed via a custom resolver we will define later.
-
-In order to make use of this custom field in a type-safe way, we can tell TypeScript about this field in a new file:
-
-```txt
-├── wishlist-plugin
-    ├── wishlist.plugin.ts
-    // highlight-next-line
-    ├── types.ts
-```
-
-```ts title="src/plugins/wishlist-plugin/types.ts"
-import { CustomCustomerFields } from '@vendure/core/dist/entity/custom-entity-fields';
-import { WishlistItem } from './entities/wishlist-item.entity';
-
-declare module '@vendure/core/dist/entity/custom-entity-fields' {
-  interface CustomCustomerFields {
-    wishlistItems: WishlistItem[];
-  }
-}
-```
-
-We can then import this types file in our plugin's main file:
-
-```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
-// highlight-next-line
-import './types';
-```
-
-## Step 4: Create a service
-
-A "service" is a class which houses the bulk of the business logic of any plugin. A plugin can define multiple services if needed, but each service should be responsible for a single unit of functionality, such as dealing with a particular entity, or performing a particular task.
-
-Let's create a service to handle the wishlist functionality:
-
-```txt
-├── wishlist-plugin
-    ├── wishlist.plugin.ts
-    ├── services
-        // highlight-next-line
-        ├── wishlist.service.ts
-```
-
-```ts title="src/plugins/wishlist-plugin/wishlist.service.ts"
-import { Injectable } from '@nestjs/common';
-import {
-    Customer,
-    ForbiddenError,
-    ID,
-    InternalServerError,
-    ProductVariantService,
-    RequestContext,
-    TransactionalConnection,
-    UserInputError,
-} from '@vendure/core';
-
-import { WishlistItem } from '../entities/wishlist-item.entity';
-
-@Injectable()
-export class WishlistService {
-    constructor(
-        private connection: TransactionalConnection,
-        private productVariantService: ProductVariantService,
-    ) {}
-
-    async getWishlistItems(ctx: RequestContext): Promise<WishlistItem[]> {
-        try {
-            const customer = await this.getCustomerWithWishlistItems(ctx);
-            return customer.customFields.wishlistItems;
-        } catch (err: any) {
-            return [];
-        }
-    }
-
-    /**
-     * Adds a new item to the active Customer's wishlist.
-     */
-    async addItem(ctx: RequestContext, variantId: ID): Promise<WishlistItem[]> {
-        const customer = await this.getCustomerWithWishlistItems(ctx);
-        const variant = this.productVariantService.findOne(ctx, variantId);
-        if (!variant) {
-            throw new UserInputError(`No ProductVariant with the id ${variantId} could be found`);
-        }
-        const existingItem = customer.customFields.wishlistItems.find(i => i.productVariantId === variantId);
-        if (existingItem) {
-            // Item already exists in wishlist, do not
-            // add it again
-            return customer.customFields.wishlistItems;
-        }
-        const wishlistItem = await this.connection
-            .getRepository(ctx, WishlistItem)
-            .save(new WishlistItem({ productVariantId: variantId }));
-        customer.customFields.wishlistItems.push(wishlistItem);
-        await this.connection.getRepository(ctx, Customer).save(customer, { reload: false });
-        return this.getWishlistItems(ctx);
-    }
-
-    /**
-     * Removes an item from the active Customer's wishlist.
-     */
-    async removeItem(ctx: RequestContext, itemId: ID): Promise<WishlistItem[]> {
-        const customer = await this.getCustomerWithWishlistItems(ctx);
-        const itemToRemove = customer.customFields.wishlistItems.find(i => i.id === itemId);
-        if (itemToRemove) {
-            await this.connection.getRepository(ctx, WishlistItem).remove(itemToRemove);
-            customer.customFields.wishlistItems = customer.customFields.wishlistItems.filter(
-                i => i.id !== itemId,
-            );
-        }
-        await this.connection.getRepository(ctx, Customer).save(customer);
-        return this.getWishlistItems(ctx);
-    }
-
-    /**
-     * Gets the active Customer from the context and loads the wishlist items.
-     */
-    private async getCustomerWithWishlistItems(ctx: RequestContext): Promise<Customer> {
-        if (!ctx.activeUserId) {
-            throw new ForbiddenError();
-        }
-        const customer = await this.connection.getRepository(ctx, Customer).findOne({
-            where: { user: { id: ctx.activeUserId } },
-            relations: {
-                customFields: {
-                    wishlistItems: {
-                        productVariant: true,
-                    },
-                },
-            },
-        });
-        if (!customer) {
-            throw new InternalServerError(`Customer was not found`);
-        }
-        return customer;
-    }
-}
-```
-
-Let's break down what's happening here:
-
-- The `WishlistService` class is decorated with the `@Injectable()` decorator. This is a standard NestJS decorator which tells the NestJS dependency injection (DI) system that this class can be injected into other classes. All your services should be decorated with this decorator.
-- The arguments passed to the constructor will be injected by the NestJS DI system. The `connection` argument is a [TransactionalConnection](/reference/typescript-api/data-access/transactional-connection/) instance, which is used to access and manipulate data in the database. The [`ProductVariantService`](/reference/typescript-api/services/product-variant-service/) argument is a built-in Vendure service which contains methods relating to ProductVariants.
-- The [`RequestContext`](/reference/typescript-api/request/request-context/) object is usually the first argument to any service method, and contains information and context about the current request as well as any open database transactions. It should always be passed to the methods of the `TransactionalConnection`.
-
-The service is then registered with the plugin metadata as a provider:
-
-```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { WishlistService } from './services/wishlist.service';
-
-@VendurePlugin({
-    imports: [PluginCommonModule],
-    // highlight-next-line
-    providers: [WishlistService],
-    entities: [WishlistItem],
-    configuration: config => {
-        // ...
-    },
-})
-export class WishlistPlugin {}
-```
-
-## Step 5: Extend the GraphQL API
-
-This plugin will need to extend the Shop API, adding new mutations and queries to enable the customer to view and manage their wishlist.
-
-First we will create a new file to hold the GraphQL schema extensions:
-
-```txt
-├── wishlist-plugin
-    ├── wishlist.plugin.ts
-    ├── api
-        // highlight-next-line
-        ├── api-extensions.ts
-```
-
-```ts title="src/plugins/wishlist-plugin/api/api-extensions.ts"
-import gql from 'graphql-tag';
-
-export const shopApiExtensions = gql`
-    type WishlistItem implements Node {
-        id: ID!
-        createdAt: DateTime!
-        updatedAt: DateTime!
-        productVariant: ProductVariant!
-        productVariantId: ID!
-    }
-
-    extend type Query {
-        activeCustomerWishlist: [WishlistItem!]!
-    }
-
-    extend type Mutation {
-        addToWishlist(productVariantId: ID!): [WishlistItem!]!
-        removeFromWishlist(itemId: ID!): [WishlistItem!]!
-    }
-`;
-```
-
-:::note
-
-The `graphql-tag` package is a dependency of the Vendure core package. Depending on the package manager you are using, you may need to install it separately with `yarn add graphql-tag` or `npm install graphql-tag`.
-
-:::
-
-The `api-extensions.ts` file is where we define the extensions we will be making to the Shop API GraphQL schema. We are defining a new `WishlistItem` type; a new query: `activeCustomerWishlist`; and two new mutations: `addToWishlist` and `removeFromWishlist`. This definition is written in [schema definition language](https://graphql.org/learn/schema/) (SDL), a convenient syntax for defining GraphQL schemas.
-
-Next we need to pass these extensions to our plugin's metadata:
-
-```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { shopApiExtensions } from './api/api-extensions';
-
-@VendurePlugin({
-    imports: [PluginCommonModule],
-    shopApiExtensions: {
-        schema: shopApiExtensions,
-        resolvers: [],
-    },
-})
-export class WishlistPlugin {}
-```
-
-## Step 6: Create a resolver
-
-Now that we have defined the GraphQL schema extensions, we need to create a resolver to handle the new queries and mutations. A resolver in GraphQL is a function which actually implements the query or mutation defined in the schema. This is done by creating a new file in the `api` directory:
-
-```txt
-├── wishlist-plugin
-    ├── wishlist.plugin.ts
-    ├── api
-        ├── api-extensions.ts
-        // highlight-next-line
-        ├── wishlist.resolver.ts
-```
-
-```ts title="src/plugins/wishlist-plugin/api/wishlist.resolver.ts"
-import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
-import { Allow, Ctx, Permission, RequestContext, Transaction } from '@vendure/core';
-
-import { WishlistItem } from '../entities/wishlist-item.entity';
-import { WishlistService } from '../service/wishlist.service';
-
-@Resolver()
-export class WishlistShopResolver {
-    constructor(private wishlistService: WishlistService) {}
-
-    @Query()
-    @Allow(Permission.Owner)
-    activeCustomerWishlist(@Ctx() ctx: RequestContext) {
-        return this.wishlistService.getWishlistItems(ctx);
-    }
-
-    @Mutation()
-    @Transaction()
-    @Allow(Permission.Owner)
-    async addToWishlist(
-        @Ctx() ctx: RequestContext,
-        @Args() { productVariantId }: { productVariantId: string },
-    ) {
-        return this.wishlistService.addItem(ctx, productVariantId);
-    }
-
-    @Mutation()
-    @Transaction()
-    @Allow(Permission.Owner)
-    async removeFromWishlist(@Ctx() ctx: RequestContext, @Args() { itemId }: { itemId: string }) {
-        return this.wishlistService.removeItem(ctx, itemId);
-    }
-}
-```
-
-Resolvers are usually "thin" functions that delegate the actual work to a service. Vendure, like NestJS itself, makes heavy use of decorators at the API layer to define various aspects of the resolver. Let's break down what's happening here:
-
-- The `@Resolver()` decorator tells the NestJS DI system that this class is a resolver. Since a Resolver is part of the NestJS DI system, we can also inject dependencies into its constructor. In this case we are injecting the `WishlistService` which we created in the previous step.
-- The `@Mutation()` decorator tells Vendure that this is a mutation resolver. Similarly, `@Query()` decorator defines a query resolver. The name of the method is the name of the query or mutation in the schema.
-- The `@Transaction()` decorator tells Vendure that this resolver method should be wrapped in a database transaction. This is important because we are performing multiple database operations in this method, and we want them to be atomic.
-- The `@Allow()` decorator tells Vendure that this mutation is only allowed for users with the `Owner` permission. The `Owner` permission is a special permission which indicates that the active user should be the owner of this operation. 
-- The `@Ctx()` decorator tells Vendure that this method requires access to the `RequestContext` object. Every resolver should have this as the first argument, as it is required throughout the Vendure request lifecycle.
-
-This resolver is then registered with the plugin metadata:
-
-```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
-import { shopApiExtensions } from './api/api-extensions';
-import { WishlistShopResolver } from './api/wishlist.resolver';
-
-@VendurePlugin({
-    imports: [PluginCommonModule],
-    shopApiExtensions: {
-        schema: shopApiExtensions,
-        // highlight-next-line
-        resolvers: [WishlistShopResolver],
-    },
-    configuration: config => {
-        // ...
-    },
-})
-export class WishlistPlugin {}
-```
-
-:::info
-
-More information about resolvers can be found in the [NestJS docs](https://docs.nestjs.com/graphql/resolvers).
-
-:::
-
-## Step 7: Specify compatibility
-
-Since Vendure v2.0.0, it is possible for a plugin to specify which versions of Vendure core it is compatible with. This is especially important if the plugin is intended to be made publicly available via npm or another package registry.
-
-The compatibility is specified via the `compatibility` property in the plugin metadata:
-
-```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
-@VendurePlugin({
-    // ...
-    // highlight-next-line
-    compatibility: '^2.0.0',
-})
-export class WishlistPlugin {}
-```
-
-The value of this property is a [semver range](https://docs.npmjs.com/about-semantic-versioning) which specifies the range of compatible versions. In this case, we are saying that this plugin is compatible with any version of Vendure core which is `>= 2.0.0 < 3.0.0`.
-
-## Step 8: Add the plugin to the VendureConfig
-
-The final step is to add the plugin to the `VendureConfig` object. This is done in the `vendure-config.ts` file:
-
-```ts title="src/vendure-config.ts"
-import { VendureConfig } from '@vendure/core';
-import { WishlistPlugin } from './plugins/wishlist-plugin/wishlist.plugin';
-
-export const config: VendureConfig = {
-    // ...
-    plugins: [
-        // ...
-        // highlight-next-line
-        WishlistPlugin,
-    ],
-};
-```
-
-## Test the plugin
-
-Now that the plugin is installed, we can test it out. Since we have defined a custom field, we'll need to generate and run a migration to add the new column to the database:
-
-```bash
-npm run migration:generate wishlist-plugin
-```
-
-Then start the server:
-
-```bash
-npm run dev
-```
-
-Once the server is running, we should be able to log in as an existing Customer, and then add a product to the wishlist:
-
-<Tabs>
-<TabItem value="Login mutation" label="Login mutation" default>
-
-```graphql
-mutation Login {
-    login(username: "alec.breitenberg@gmail.com", password: "test") {
-        ... on CurrentUser {
-            id
-            identifier
-        }
-        ... on ErrorResult {
-            errorCode
-            message
-        }
-    }
-}
-```
-
-</TabItem>
-<TabItem value="Response" label="Response">
-
-```json
-{
-  "data": {
-    "login": {
-      "id": "9",
-      "identifier": "alec.breitenberg@gmail.com"
-    }
-  }
-}
-```
-
-  </TabItem>
-</Tabs>
-
-
-<Tabs>
-<TabItem value="AddToWishlist mutation" label="AddToWishlist mutation" default>
-
-```graphql
-mutation AddToWishlist {
-    addToWishlist(productVariantId: "7") {
-        id
-        productVariant {
-            id
-            name
-        }
-    }
-}
-```
-
-</TabItem>
-<TabItem value="Response" label="Response">
-
-```json
-{
-  "data": {
-    "addToWishlist": [
-      {
-        "id": "4",
-        "productVariant": {
-          "id": "7",
-          "name": "Wireless Optical Mouse"
-        }
-      }
-    ]
-  }
-}
-```
-
-  </TabItem>
-</Tabs>
-
-We can then query the wishlist items:
-
-
-<Tabs>
-<TabItem value="GetWishlist mutation" label="GetWishlist mutation" default>
-
-```graphql
-query GetWishlist {
-    activeCustomerWishlist {
-        id
-        productVariant {
-            id
-            name
-        }
-    }
-}
-```
-
-</TabItem>
-<TabItem value="Response" label="Response">
-
-```json
-{
-  "data": {
-    "activeCustomerWishlist": [
-      {
-        "id": "4",
-        "productVariant": {
-          "id": "7",
-          "name": "Wireless Optical Mouse"
-        }
-      }
-    ]
-  }
-}
-```
-
-  </TabItem>
-</Tabs>
-
-And finally, we can test removing an item from the wishlist:
-
-<Tabs>
-<TabItem value="RemoveFromWishlist mutation" label="RemoveFromWishlist mutation" default>
-
-```graphql
-mutation RemoveFromWishlist {
-    removeFromWishlist(itemId: "4") {
-        id
-        productVariant {
-            name
-        }
-    }
-}
-```
-
-</TabItem>
-<TabItem value="Response" label="Response">
-
-```json
-{
-  "data": {
-    "removeFromWishlist": []
-  }
-}
-```
-
-  </TabItem>
-</Tabs>

+ 0 - 25
docs/docs/guides/plugins/index.md

@@ -1,25 +0,0 @@
----
-title: "Plugins"
-weight: 1
----
-
-# Vendure Plugins
-
-The heart of Vendure is its plugin system. Plugins not only allow you to instantly add new functionality to your Vendure server, they are also the means by which you build out the custom business logic of your application.
-
-Plugins in Vendure allow one to:
-
-* Modify the [VendureConfig]({{< ref "/reference/typescript-api/configuration" >}}#vendureconfig) object, such as defining [custom fields]({{< relref "customizing-models" >}}) on existing entities.
-* Extend the GraphQL APIs, including modifying existing types and adding completely new queries and mutations.
-* Define new database entities and interact directly with the database.
-* Interact with external systems that you need to integrate with.
-* Respond to [events]({{< relref "event-bus" >}}) such as new orders being placed.
-* Trigger background tasks to run on the worker process.
-* ... and more!
-
-In a typical Vendure application, custom logic and functionality is implemented as a set of plugins which are usually independent of one another. For example, there could be a plugin for each of the following: wishlists, product reviews, loyalty points, gift cards, etc. This allows for a clean separation of concerns and makes it easy to add or remove functionality as needed.
-
-## Core Plugins
-
-Vendure provides a set of **core plugins** covering common functionality such as assets handling, email sending, and search. For 
-documentation on these, see the [Core Plugins section]({{< relref "core-plugins" >}}).

+ 0 - 17
docs/docs/guides/plugins/plugin-architecture/index.md

@@ -1,17 +0,0 @@
----
-title: "Plugin Architecture"
-weight: 0
-showtoc: true
----
- 
-# Plugin Architecture
-
-![plugin_architecture.png](plugin_architecture.png)
-
-A plugin in Vendure is a specialized Nestjs Module that is decorated with the [`VendurePlugin` class decorator]({{< relref "vendure-plugin" >}}). This diagram illustrates how a plugin can integrate with and extend Vendure.
- 
-1. A Plugin may define logic to be run by the [Vendure Worker]({{< relref "/guides/developer-guide/vendure-worker" >}}). This is suitable for long-running or resource-intensive tasks.
-2. A Plugin can modify any aspect of server configuration via the [`configuration` metadata property]({{< relref "vendure-plugin-metadata" >}}#configuration).
-3. A Plugin can extend the GraphQL APIs via the [`shopApiExtensions` metadata property]({{< relref "vendure-plugin-metadata" >}}#shopapiextensions) and the [`adminApiExtensions` metadata property]({{< relref "vendure-plugin-metadata" >}}#adminapiextensions).
-4. A Plugin can interact with Vendure by importing the [`PluginCommonModule`]({{< relref "plugin-common-module" >}}), by which it may inject any of the core Vendure services (which are responsible for all interaction with the database as well as business logic). Additionally, a plugin may define new database entities via the [`entities` metadata property]({{< relref "vendure-plugin-metadata" >}}#entities) and otherwise define any other providers and controllers just like any [Nestjs module](https://docs.nestjs.com/modules).
-5. A Plugin can run arbitrary code, which allows it to make use of external services. For example, a plugin could interface with a cloud storage provider, a payment gateway, or a video encoding service.

BIN
docs/docs/guides/plugins/plugin-architecture/plugin_architecture.png


+ 0 - 13
docs/docs/guides/plugins/plugin-examples/index.md

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

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

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

+ 0 - 51
docs/docs/guides/plugins/plugin-lifecycle/index.md

@@ -1,51 +0,0 @@
----
-title: "Plugin Lifecycle"
-weight: 3
----
-
-# Plugin Lifecycle
-
-Since a VendurePlugin is built on top of the Nestjs module system, any plugin (as well as any providers it defines) can make use of any of the [Nestjs lifecycle hooks](https://docs.nestjs.com/fundamentals/lifecycle-events):
-
-* onModuleInit
-* onApplicationBootstrap
-* onModuleDestroy
-* beforeApplicationShutdown
-* onApplicationShutdown
-
-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]({{< relref "process-context" >}}) provider.
-
-## 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.
-
-## Example
-
-```ts
-import { MiddlewareConsumer, NestModule, OnApplicationBootstrap } from '@nestjs/common';
-import { EventBus, PluginCommonModule, VendurePlugin } from '@vendure/core';
-
-@VendurePlugin({
-    imports: [PluginCommonModule]
-})
-export class MyPlugin implements OnApplicationBootstrap, NestModule {
-
-  constructor(private eventBus: EventBus) {}
-
-  configure(consumer: MiddlewareConsumer) {
-    consumer
-      .apply(MyMiddleware)
-      .forRoutes('my-custom-route');
-  }
-
-  async onApplicationBootstrap() {
-    await myAsyncInitFunction();
-
-    this.eventBus
-      .ofType(OrderStateTransitionEvent)
-      .subscribe((event) => {
-        // do some action when this event fires
-      });
-  }
-}
-```

+ 8 - 0
docs/docs/guides/advanced-topics/codegen.mdx → docs/docs/guides/storefront/codegen/index.mdx

@@ -8,6 +8,10 @@ write any types for your API calls.
 
 To do this, we will use [Graphql Code Generator](https://the-guild.dev/graphql/codegen).
 
+:::note
+Code generation can be used when building a storefront _as well as_ when building backend plugins.
+:::
+
 ## Installation
 
 Follow the installation instructions in the [GraphQL Code Generator Quick Start](https://the-guild.dev/graphql/codegen/docs/getting-started/installation).
@@ -143,3 +147,7 @@ export default function App() {
 In the above example, the type information all works out of the box because the `graphql-request` library from v5.0.0
 has built-in support for the [`TypedDocumentNode`](https://github.com/dotansimha/graphql-typed-document-node) type,
 as do the latest versions of most of the popular GraphQL client libraries, such as Apollo Client & Urql.
+
+:::note
+In the documentation examples on other pages, we do not assume the use of code generation in order to keep the examples as simple as possible.
+:::

+ 1 - 1
docs/docs/guides/storefront/connect-api/index.mdx

@@ -160,7 +160,7 @@ POST http://localhost:3000/shop-api?languageCode=de
 If you are building your storefront with TypeScript, we highly recommend you set up code generation to ensure
 that the responses from your queries & mutation are always correctly typed according the fields you request.
 
-See the [GraphQL Code Generation guide](/guides/advanced-topics/codegen) for more information.
+See the [GraphQL Code Generation guide](/guides/storefront/codegen/) for more information.
 
 ## Examples
 

+ 1 - 1
docs/docs/guides/storefront/product-detail/index.mdx

@@ -280,7 +280,7 @@ There are some important things to note about this mutation:
 
 - Because the `addItemToOrder` mutation returns a union type, we need to use a [fragment](/TODO) to specify the fields we want to return.
 In this case we have defined a fragment called `UpdatedOrder` which contains the fields we are interested in.
-- If any [expected errors](/guides/advanced-topics/error-handling) occur, the mutation will return an `ErrorResult` object. We'll be able to
+- If any [expected errors](/guides/developer-guide/error-handling/) occur, the mutation will return an `ErrorResult` object. We'll be able to
 see the `errorCode` and `message` fields in the response, so that we can display a meaningful error message to the user.
 - In the special case of the `InsufficientStockError`, in addition to the `errorCode` and `message` fields, we also get the `quantityAvailable` field
 which tells us how many of the requested quantity are available (and have been added to the order). This is useful information to display to the user.

+ 45 - 20
docs/sidebars.js

@@ -40,33 +40,60 @@ const sidebars = {
                 icon: icon.bolt,
             },
         },
+        {
+            type: 'category',
+            label: 'Core Concepts',
+            items: [{ type: 'autogenerated', dirName: 'guides/core-concepts' }],
+            customProps: {
+                icon: icon.puzzle,
+            },
+        },
         {
             type: 'category',
             label: 'Developer Guide',
             items: [
+                {
+                    type: 'html',
+                    value: 'Overview',
+                    className: 'sidebar-section-header',
+                },
                 'guides/developer-guide/overview/index',
-                'guides/developer-guide/configuration/index',
                 'guides/developer-guide/the-api-layer/index',
                 'guides/developer-guide/the-service-layer/index',
+                {
+                    type: 'html',
+                    value: 'Fundamentals',
+                    className: 'sidebar-section-header',
+                },
+                'guides/developer-guide/configuration/index',
                 'guides/developer-guide/custom-fields/index',
+                'guides/developer-guide/error-handling/index',
                 'guides/developer-guide/events/index',
+                'guides/developer-guide/migrations/index',
+                'guides/developer-guide/plugins/index',
                 'guides/developer-guide/strategies-configurable-operations/index',
+                'guides/developer-guide/testing/index',
+                'guides/developer-guide/updating/index',
                 'guides/developer-guide/worker-job-queue/index',
-                'guides/developer-guide/plugins/index',
-                'guides/developer-guide/migrations/index',
+                {
+                    type: 'html',
+                    value: 'Advanced Topics',
+                    className: 'sidebar-section-header',
+                },
+                'guides/developer-guide/db-subscribers/index',
+                'guides/developer-guide/logging/index',
+                {
+                    type: 'category',
+                    label: 'Migrating from v1',
+                    items: [{ type: 'autogenerated', dirName: 'guides/developer-guide/migrating-from-v1' }],
+                },
+                'guides/developer-guide/stand-alone-scripts/index',
+                'guides/developer-guide/translations/index',
             ],
             customProps: {
                 icon: icon.angleBrackets,
             },
         },
-        {
-            type: 'category',
-            label: 'Core Concepts',
-            items: [{ type: 'autogenerated', dirName: 'guides/core-concepts' }],
-            customProps: {
-                icon: icon.puzzle,
-            },
-        },
         {
             type: 'category',
             label: 'How-to Guides',
@@ -75,14 +102,6 @@ const sidebars = {
                 icon: icon.book,
             },
         },
-        {
-            type: 'category',
-            label: 'Advanced Topics',
-            items: [{ type: 'autogenerated', dirName: 'guides/advanced-topics' }],
-            customProps: {
-                icon: icon.academicCap,
-            },
-        },
         {
             type: 'category',
             label: 'Extending the Admin UI',
@@ -114,6 +133,12 @@ const sidebars = {
             items: [
                 'guides/storefront/storefront-starters/index',
                 'guides/storefront/connect-api/index',
+                'guides/storefront/codegen/index',
+                {
+                    type: 'html',
+                    value: 'Storefront Tasks',
+                    className: 'sidebar-section-header',
+                },
                 'guides/storefront/navigation-menu/index',
                 'guides/storefront/listing-products/index',
                 'guides/storefront/product-detail/index',
@@ -137,7 +162,7 @@ const sidebars = {
                 'guides/deployment/deploying-admin-ui',
                 {
                     type: 'html',
-                    value: 'Guides',
+                    value: 'Deployment Guides',
                     className: 'sidebar-section-header',
                 },
                 'guides/deployment/deploy-to-google-cloud-run/index',

+ 7 - 1
docs/src/css/custom.css

@@ -87,9 +87,15 @@ html[data-theme='dark'] {
 
 .sidebar-section-header {
     text-transform: uppercase;
-    font-weight: bold;
+    /*font-weight: bold;*/
     font-size: 12px;
     opacity: 0.6;
     padding: var(--ifm-menu-link-padding-vertical)
          var(--ifm-menu-link-padding-horizontal);
 }
+
+.sidebar-section-divider {
+    border-bottom: 1px solid var(--ifm-font-color-base);
+    opacity: 0.2;
+    margin: 6px 12px;
+}