Ver Fonte

feat(core): Implement ActiveOrderStrategy

Relates to #1858
Michael Bromley há 3 anos atrás
pai
commit
e62009f6bb

+ 2 - 0
packages/core/src/api/config/configure-graphql-module.ts

@@ -22,6 +22,7 @@ import { AssetInterceptorPlugin } from '../middleware/asset-interceptor-plugin';
 import { IdCodecPlugin } from '../middleware/id-codec-plugin';
 import { TranslateErrorsPlugin } from '../middleware/translate-errors-plugin';
 
+import { generateActiveOrderTypes } from './generate-active-order-types';
 import { generateAuthenticationTypes } from './generate-auth-types';
 import { generateErrorCodeEnum } from './generate-error-code-enum';
 import { generateListOptions } from './generate-list-options';
@@ -158,6 +159,7 @@ async function createGraphQLOptions(
         }
         if (apiType === 'shop') {
             schema = addRegisterCustomerCustomFieldsInput(schema, customFields.Customer || []);
+            schema = generateActiveOrderTypes(schema, configService.orderOptions.activeOrderStrategy);
         }
         schema = generatePermissionEnum(schema, configService.authOptions.customPermissions);
 

+ 109 - 0
packages/core/src/api/config/generate-active-order-types.ts

@@ -0,0 +1,109 @@
+import { stitchSchemas, ValidationLevel } from '@graphql-tools/stitch';
+import { Mutation, Query } from '@vendure/common/lib/generated-shop-types';
+import {
+    buildASTSchema,
+    GraphQLInputFieldConfigMap,
+    GraphQLInputObjectType,
+    GraphQLSchema,
+    isInputObjectType,
+} from 'graphql';
+
+import { InternalServerError } from '../../common/error/errors';
+import { ActiveOrderStrategy, ACTIVE_ORDER_INPUT_FIELD_NAME } from '../../config/index';
+
+/**
+ * This function is responsible for constructing the `ActiveOrderInput` GraphQL input type.
+ * It does so based on the inputs defined by the configured ActiveOrderStrategy defineInputType
+ * methods, dynamically building a mapped input type of the format:
+ *
+ *```
+ * {
+ *     [strategy_name]: strategy_input_type
+ * }
+ * ```
+ */
+export function generateActiveOrderTypes(
+    schema: GraphQLSchema,
+    activeOrderStrategies: ActiveOrderStrategy | ActiveOrderStrategy[],
+): GraphQLSchema {
+    const fields: GraphQLInputFieldConfigMap = {};
+    const strategySchemas: GraphQLSchema[] = [];
+    const strategiesArray = Array.isArray(activeOrderStrategies)
+        ? activeOrderStrategies
+        : [activeOrderStrategies];
+    for (const strategy of strategiesArray) {
+        if (typeof strategy.defineInputType === 'function') {
+            const inputSchema = buildASTSchema(strategy.defineInputType());
+
+            const inputType = Object.values(inputSchema.getTypeMap()).find(
+                (type): type is GraphQLInputObjectType => isInputObjectType(type),
+            );
+            if (!inputType) {
+                throw new InternalServerError(
+                    `${strategy.constructor.name}.defineInputType() does not define a GraphQL Input type`,
+                );
+            }
+            fields[strategy.name] = { type: inputType };
+            strategySchemas.push(inputSchema);
+        }
+    }
+    if (Object.keys(fields).length === 0) {
+        return schema;
+    }
+    const activeOrderInput = new GraphQLInputObjectType({
+        name: 'ActiveOrderInput',
+        fields,
+    });
+
+    const activeOrderOperations: Array<{ name: keyof Query | keyof Mutation; isMutation: boolean }> = [
+        { name: 'activeOrder', isMutation: false },
+        { name: 'eligibleShippingMethods', isMutation: false },
+        { name: 'eligiblePaymentMethods', isMutation: false },
+        { name: 'nextOrderStates', isMutation: false },
+        { name: 'addItemToOrder', isMutation: true },
+        { name: 'adjustOrderLine', isMutation: true },
+        { name: 'removeOrderLine', isMutation: true },
+        { name: 'removeAllOrderLines', isMutation: true },
+        { name: 'applyCouponCode', isMutation: true },
+        { name: 'removeCouponCode', isMutation: true },
+        { name: 'addPaymentToOrder', isMutation: true },
+        { name: 'setCustomerForOrder', isMutation: true },
+        { name: 'setOrderShippingAddress', isMutation: true },
+        { name: 'setOrderBillingAddress', isMutation: true },
+        { name: 'setOrderShippingMethod', isMutation: true },
+        { name: 'setOrderCustomFields', isMutation: true },
+        { name: 'transitionOrderToState', isMutation: true },
+    ];
+
+    const queryType = schema.getQueryType();
+    const mutationType = schema.getMutationType();
+    const strategyNames = strategiesArray.map(s => s.name).join(', ');
+    const description = `Inputs for the configured ${
+        strategiesArray.length === 1 ? 'ActiveOrderStrategy' : 'ActiveOrderStrategies'
+    } ${strategyNames}`;
+    for (const operation of activeOrderOperations) {
+        const field = operation.isMutation
+            ? mutationType?.getFields()[operation.name]
+            : queryType?.getFields()[operation.name];
+        if (!field) {
+            throw new InternalServerError(
+                `Could not find a GraphQL type definition for the field ${operation.name}`,
+            );
+        }
+        field.args.push({
+            name: ACTIVE_ORDER_INPUT_FIELD_NAME,
+            type: activeOrderInput,
+            description,
+            defaultValue: null,
+            extensions: null,
+            astNode: null,
+            deprecationReason: null,
+        });
+    }
+
+    return stitchSchemas({
+        subschemas: [schema, ...strategySchemas],
+        types: [activeOrderInput],
+        typeMergingOptions: { validationSettings: { validationLevel: ValidationLevel.Off } },
+    });
+}

+ 105 - 33
packages/core/src/api/resolvers/shop/shop-order.resolver.ts

@@ -36,7 +36,7 @@ import {
 } from '../../../common/error/generated-graphql-shop-errors';
 import { Translated } from '../../../common/types/locale-types';
 import { idsAreEqual } from '../../../common/utils';
-import { ConfigService, LogLevel } from '../../../config';
+import { ACTIVE_ORDER_INPUT_FIELD_NAME, ConfigService, LogLevel } from '../../../config';
 import { Country } from '../../../entity';
 import { Order } from '../../../entity/order/order.entity';
 import { ActiveOrderService, CountryService } from '../../../service';
@@ -50,6 +50,8 @@ import { RelationPaths, Relations } from '../../decorators/relations.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 import { Transaction } from '../../decorators/transaction.decorator';
 
+type ActiveOrderArgs = { [ACTIVE_ORDER_INPUT_FIELD_NAME]?: any };
+
 @Resolver()
 export class ShopOrderResolver {
     constructor(
@@ -97,9 +99,13 @@ export class ShopOrderResolver {
     async activeOrder(
         @Ctx() ctx: RequestContext,
         @Relations(Order) relations: RelationPaths<Order>,
+        @Args() args: ActiveOrderArgs,
     ): Promise<Order | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            );
             if (sessionOrder) {
                 return this.orderService.findOne(ctx, sessionOrder.id);
             } else {
@@ -140,10 +146,13 @@ export class ShopOrderResolver {
     @Allow(Permission.Owner)
     async setOrderShippingAddress(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationSetOrderShippingAddressArgs,
+        @Args() args: MutationSetOrderShippingAddressArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<ActiveOrderResult, Order>> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            );
             if (sessionOrder) {
                 return this.orderService.setShippingAddress(ctx, sessionOrder.id, args.input);
             }
@@ -156,10 +165,13 @@ export class ShopOrderResolver {
     @Allow(Permission.Owner)
     async setOrderBillingAddress(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationSetOrderBillingAddressArgs,
+        @Args() args: MutationSetOrderBillingAddressArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<ActiveOrderResult, Order>> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            );
             if (sessionOrder) {
                 return this.orderService.setBillingAddress(ctx, sessionOrder.id, args.input);
             }
@@ -169,9 +181,15 @@ export class ShopOrderResolver {
 
     @Query()
     @Allow(Permission.Owner)
-    async eligibleShippingMethods(@Ctx() ctx: RequestContext): Promise<ShippingMethodQuote[]> {
+    async eligibleShippingMethods(
+        @Ctx() ctx: RequestContext,
+        @Args() args: ActiveOrderArgs,
+    ): Promise<ShippingMethodQuote[]> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            );
             if (sessionOrder) {
                 return this.orderService.getEligibleShippingMethods(ctx, sessionOrder.id);
             }
@@ -181,9 +199,15 @@ export class ShopOrderResolver {
 
     @Query()
     @Allow(Permission.Owner)
-    async eligiblePaymentMethods(@Ctx() ctx: RequestContext): Promise<PaymentMethodQuote[]> {
+    async eligiblePaymentMethods(
+        @Ctx() ctx: RequestContext,
+        @Args() args: ActiveOrderArgs,
+    ): Promise<PaymentMethodQuote[]> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            );
             if (sessionOrder) {
                 return this.orderService.getEligiblePaymentMethods(ctx, sessionOrder.id);
             }
@@ -196,10 +220,13 @@ export class ShopOrderResolver {
     @Allow(Permission.Owner)
     async setOrderShippingMethod(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationSetOrderShippingMethodArgs,
+        @Args() args: MutationSetOrderShippingMethodArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<SetOrderShippingMethodResult, Order>> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            );
             if (sessionOrder) {
                 return this.orderService.setShippingMethod(ctx, sessionOrder.id, args.shippingMethodId);
             }
@@ -212,10 +239,13 @@ export class ShopOrderResolver {
     @Allow(Permission.Owner)
     async setOrderCustomFields(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationSetOrderCustomFieldsArgs,
+        @Args() args: MutationSetOrderCustomFieldsArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<ActiveOrderResult, Order>> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            );
             if (sessionOrder) {
                 return this.orderService.updateCustomFields(ctx, sessionOrder.id, args.input.customFields);
             }
@@ -225,9 +255,16 @@ export class ShopOrderResolver {
 
     @Query()
     @Allow(Permission.Owner)
-    async nextOrderStates(@Ctx() ctx: RequestContext): Promise<ReadonlyArray<string>> {
+    async nextOrderStates(
+        @Ctx() ctx: RequestContext,
+        @Args() args: ActiveOrderArgs,
+    ): Promise<ReadonlyArray<string>> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx, true);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+                true,
+            );
             return this.orderService.getNextOrderStates(sessionOrder);
         }
         return [];
@@ -238,10 +275,14 @@ export class ShopOrderResolver {
     @Allow(Permission.Owner)
     async transitionOrderToState(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationTransitionOrderToStateArgs,
+        @Args() args: MutationTransitionOrderToStateArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<TransitionOrderToStateResult, Order> | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx, true);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+                true,
+            );
             return await this.orderService.transitionToState(ctx, sessionOrder.id, args.state as OrderState);
         }
     }
@@ -251,9 +292,13 @@ export class ShopOrderResolver {
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async addItemToOrder(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationAddItemToOrderArgs,
+        @Args() args: MutationAddItemToOrderArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
-        const order = await this.activeOrderService.getOrderFromContext(ctx, true);
+        const order = await this.activeOrderService.getActiveOrder(
+            ctx,
+            args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            true,
+        );
         return this.orderService.addItemToOrder(
             ctx,
             order.id,
@@ -268,12 +313,16 @@ export class ShopOrderResolver {
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async adjustOrderLine(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationAdjustOrderLineArgs,
+        @Args() args: MutationAdjustOrderLineArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         if (args.quantity === 0) {
             return this.removeOrderLine(ctx, { orderLineId: args.orderLineId });
         }
-        const order = await this.activeOrderService.getOrderFromContext(ctx, true);
+        const order = await this.activeOrderService.getActiveOrder(
+            ctx,
+            args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            true,
+        );
         return this.orderService.adjustOrderLine(
             ctx,
             order.id,
@@ -288,9 +337,13 @@ export class ShopOrderResolver {
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async removeOrderLine(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationRemoveOrderLineArgs,
+        @Args() args: MutationRemoveOrderLineArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<RemoveOrderItemsResult, Order>> {
-        const order = await this.activeOrderService.getOrderFromContext(ctx, true);
+        const order = await this.activeOrderService.getActiveOrder(
+            ctx,
+            args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            true,
+        );
         return this.orderService.removeItemFromOrder(ctx, order.id, args.orderLineId);
     }
 
@@ -299,8 +352,13 @@ export class ShopOrderResolver {
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async removeAllOrderLines(
         @Ctx() ctx: RequestContext,
+        @Args() args: ActiveOrderArgs,
     ): Promise<ErrorResultUnion<RemoveOrderItemsResult, Order>> {
-        const order = await this.activeOrderService.getOrderFromContext(ctx, true);
+        const order = await this.activeOrderService.getActiveOrder(
+            ctx,
+            args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            true,
+        );
         return this.orderService.removeAllItemsFromOrder(ctx, order.id);
     }
 
@@ -309,9 +367,13 @@ export class ShopOrderResolver {
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async applyCouponCode(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationApplyCouponCodeArgs,
+        @Args() args: MutationApplyCouponCodeArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<ApplyCouponCodeResult, Order>> {
-        const order = await this.activeOrderService.getOrderFromContext(ctx, true);
+        const order = await this.activeOrderService.getActiveOrder(
+            ctx,
+            args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            true,
+        );
         return this.orderService.applyCouponCode(ctx, order.id, args.couponCode);
     }
 
@@ -320,9 +382,13 @@ export class ShopOrderResolver {
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async removeCouponCode(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationApplyCouponCodeArgs,
+        @Args() args: MutationApplyCouponCodeArgs & ActiveOrderArgs,
     ): Promise<Order> {
-        const order = await this.activeOrderService.getOrderFromContext(ctx, true);
+        const order = await this.activeOrderService.getActiveOrder(
+            ctx,
+            args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            true,
+        );
         return this.orderService.removeCouponCode(ctx, order.id, args.couponCode);
     }
 
@@ -331,10 +397,13 @@ export class ShopOrderResolver {
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async addPaymentToOrder(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationAddPaymentToOrderArgs,
+        @Args() args: MutationAddPaymentToOrderArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<AddPaymentToOrderResult, Order>> {
         if (ctx.authorizedAsOwnerOnly) {
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            );
             if (sessionOrder) {
                 const order = await this.orderService.addPaymentToOrder(ctx, sessionOrder.id, args.input);
                 if (isGraphQlErrorResult(order)) {
@@ -357,13 +426,16 @@ export class ShopOrderResolver {
     @Allow(Permission.Owner)
     async setCustomerForOrder(
         @Ctx() ctx: RequestContext,
-        @Args() args: MutationSetCustomerForOrderArgs,
+        @Args() args: MutationSetCustomerForOrderArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<SetCustomerForOrderResult, Order>> {
         if (ctx.authorizedAsOwnerOnly) {
             if (ctx.activeUserId) {
                 return new AlreadyLoggedInError();
             }
-            const sessionOrder = await this.activeOrderService.getOrderFromContext(ctx);
+            const sessionOrder = await this.activeOrderService.getActiveOrder(
+                ctx,
+                args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            );
             if (sessionOrder) {
                 const customer = await this.customerService.createOrUpdate(ctx, args.input, true);
                 if (isGraphQlErrorResult(customer)) {

+ 2 - 0
packages/core/src/config/config.module.ts

@@ -86,6 +86,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             orderCodeStrategy,
             orderByCodeAccessStrategy,
             stockAllocationStrategy,
+            activeOrderStrategy,
         } = this.configService.orderOptions;
         const { customFulfillmentProcess } = this.configService.shippingOptions;
         const { customPaymentProcess } = this.configService.paymentOptions;
@@ -120,6 +121,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             stockDisplayStrategy,
             ...healthChecks,
             assetImportStrategy,
+            ...(Array.isArray(activeOrderStrategy) ? activeOrderStrategy : [activeOrderStrategy]),
         ];
     }
 

+ 2 - 0
packages/core/src/config/default-config.ts

@@ -22,6 +22,7 @@ import { DefaultStockDisplayStrategy } from './catalog/default-stock-display-str
 import { AutoIncrementIdStrategy } from './entity-id-strategy/auto-increment-id-strategy';
 import { manualFulfillmentHandler } from './fulfillment/manual-fulfillment-handler';
 import { DefaultLogger } from './logger/default-logger';
+import { DefaultActiveOrderStrategy } from './order/default-active-order-strategy';
 import { DefaultChangedPriceHandlingStrategy } from './order/default-changed-price-handling-strategy';
 import { DefaultOrderItemPriceCalculationStrategy } from './order/default-order-item-price-calculation-strategy';
 import { DefaultOrderPlacedStrategy } from './order/default-order-placed-strategy';
@@ -138,6 +139,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         orderByCodeAccessStrategy: new DefaultOrderByCodeAccessStrategy('2h'),
         changedPriceHandlingStrategy: new DefaultChangedPriceHandlingStrategy(),
         orderPlacedStrategy: new DefaultOrderPlacedStrategy(),
+        activeOrderStrategy: new DefaultActiveOrderStrategy(),
     },
     paymentOptions: {
         paymentMethodEligibilityCheckers: [],

+ 2 - 0
packages/core/src/config/index.ts

@@ -30,6 +30,8 @@ export * from './logger/default-logger';
 export * from './logger/noop-logger';
 export * from './logger/vendure-logger';
 export * from './merge-config';
+export * from './order/active-order-strategy';
+export * from './order/default-active-order-strategy';
 export * from './order/changed-price-handling-strategy';
 export * from './order/custom-order-process';
 export * from './order/default-changed-price-handling-strategy';

+ 84 - 0
packages/core/src/config/order/active-order-strategy.ts

@@ -0,0 +1,84 @@
+import { DocumentNode } from 'graphql';
+
+import { RequestContext } from '../../api/index';
+import { InjectableStrategy } from '../../common/index';
+import { Order } from '../../entity/index';
+
+export const ACTIVE_ORDER_INPUT_FIELD_NAME = 'activeOrderInput';
+
+/**
+ * @description
+ * This strategy is used to determine the active Order for all order-related operations in
+ * the Shop API. By default, all the Shop API operations that relate to the active Order (e.g.
+ * `activeOrder`, `addItemToOrder`, `applyCouponCode` etc.) will implicitly create a new Order
+ * and set it on the current Session, and then read the session to obtain the active Order.
+ * This behaviour is defined by the {@link DefaultActiveOrderStrategy}.
+ *
+ * @since 1.9.0
+ */
+export interface ActiveOrderStrategy extends InjectableStrategy {
+    /**
+     * @description
+     * The name of the strategy, e.g. "orderByToken", which will also be used as the
+     * field name in the ActiveOrderInput type.
+     */
+    readonly name: string;
+
+    /**
+     * @description
+     * Defines the type of the GraphQL Input object expected by the `authenticate`
+     * mutation. The final input object will be a map, with the key being the name
+     * of the strategy. The shape of the input object should match the generic `Data`
+     * type argument.
+     *
+     * @example
+     * For example, given the following:
+     *
+     * ```TypeScript
+     * defineInputType() {
+     *   return gql`
+     *      input OrderTokenInput {
+     *        token: String!
+     *      }
+     *   `;
+     * }
+     * ```
+     *
+     * assuming the strategy name is "my_auth", then the resulting call to `authenticate`
+     * would look like:
+     *
+     * ```GraphQL
+     * activeOrder(activeOrderInput: {
+     *   orderByToken: {
+     *     token: "foo"
+     *   }
+     * }) {
+     *   # ...
+     * }
+     * ```
+     *
+     * **Note:** if more than one graphql `input` type is being defined (as in a nested input type), then
+     * the _first_ input will be assumed to be the top-level input.
+     */
+    defineInputType?: () => DocumentNode;
+
+    /**
+     * @description
+     * Certain mutations such as `addItemToOrder` can automatically create a new Order if one does not exist.
+     * In these cases, this method will be called to create the new Order.
+     *
+     * If automatic creation of an Order does not make sense in your strategy, then leave this method
+     * undefined. You'll then need to take care of creating an order manually by defining a custom mutation.
+     */
+    createActiveOrder?: (ctx: RequestContext, inputs: any) => Promise<Order>;
+
+    /**
+     * @description
+     * This method is used to determine the active Order based on the current RequestContext in addition to any
+     * input values provided, as defined by the `defineInputType` method of this strategy.
+     *
+     * Note that this method is invoked frequently so you should aim to keep it efficient. The returned Order,
+     * for example, does not need to have its various relations joined.
+     */
+    determineActiveOrder(ctx: RequestContext, inputs: any): Promise<Order | undefined>;
+}

+ 65 - 0
packages/core/src/config/order/default-active-order-strategy.ts

@@ -0,0 +1,65 @@
+import { RequestContext } from '../../api/common/request-context';
+import { InternalServerError } from '../../common/error/errors';
+import { Injector } from '../../common/injector';
+import { TransactionalConnection } from '../../connection/transactional-connection';
+import { Order } from '../../entity/order/order.entity';
+// import { OrderService } from '../../service/services/order.service';
+// import { SessionService } from '../../service/services/session.service';
+
+import { ActiveOrderStrategy } from './active-order-strategy';
+
+/**
+ * @description
+ * The default {@link ActiveOrderStrategy}, which uses the current {@link Session} to determine
+ * the active Order, and requires no additional input in the Shop API since it is based on the
+ * session which is part of the RequestContext.
+ *
+ * @since 1.9.0
+ */
+export class DefaultActiveOrderStrategy implements ActiveOrderStrategy {
+    private connection: TransactionalConnection;
+    private orderService: import('../../service/services/order.service').OrderService;
+    private sessionService: import('../../service/services/session.service').SessionService;
+
+    name: 'default-active-order-strategy';
+
+    async init(injector: Injector) {
+        this.connection = injector.get(TransactionalConnection);
+        // Lazy import these dependencies to avoid a circular dependency issue in NestJS.
+        const { OrderService } = await import('../../service/services/order.service');
+        const { SessionService } = await import('../../service/services/session.service');
+        this.orderService = injector.get(OrderService);
+        this.sessionService = injector.get(SessionService);
+    }
+
+    createActiveOrder(ctx: RequestContext) {
+        return this.orderService.create(ctx, ctx.activeUserId);
+    }
+
+    async determineActiveOrder(ctx: RequestContext) {
+        if (!ctx.session) {
+            throw new InternalServerError(`error.no-active-session`);
+        }
+        let order = ctx.session.activeOrderId
+            ? await this.connection
+                  .getRepository(ctx, Order)
+                  .createQueryBuilder('order')
+                  .leftJoin('order.channels', 'channel')
+                  .where('order.id = :orderId', { orderId: ctx.session.activeOrderId })
+                  .andWhere('channel.id = :channelId', { channelId: ctx.channelId })
+                  .getOne()
+            : undefined;
+        if (order && order.active === false) {
+            // edge case where an inactive order may not have been
+            // removed from the session, i.e. the regular process was interrupted
+            await this.sessionService.unsetActiveOrder(ctx, ctx.session);
+            order = undefined;
+        }
+        if (!order) {
+            if (ctx.activeUserId) {
+                order = await this.orderService.getActiveOrderForUser(ctx, ctx.activeUserId);
+            }
+        }
+        return order || undefined;
+    }
+}

+ 13 - 0
packages/core/src/config/vendure-config.ts

@@ -26,6 +26,7 @@ import { CustomFulfillmentProcess } from './fulfillment/custom-fulfillment-proce
 import { FulfillmentHandler } from './fulfillment/fulfillment-handler';
 import { JobQueueStrategy } from './job-queue/job-queue-strategy';
 import { VendureLogger } from './logger/vendure-logger';
+import { ActiveOrderStrategy } from './order/active-order-strategy';
 import { ChangedPriceHandlingStrategy } from './order/changed-price-handling-strategy';
 import { CustomOrderProcess } from './order/custom-order-process';
 import { OrderByCodeAccessStrategy } from './order/order-by-code-access-strategy';
@@ -557,6 +558,18 @@ export interface OrderOptions {
      * @default DefaultOrderPlacedStrategy
      */
     orderPlacedStrategy?: OrderPlacedStrategy;
+    /**
+     * @description
+     * Defines the strategy used to determine the active Order when interacting with Shop API operations
+     * such as `activeOrder` and `addItemToOrder`. By default, the strategy uses the active Session.
+     *
+     * Note that if multiple strategies are defined, they will be checked in order and the first one that
+     * returns an Order will be used.
+     *
+     * @since 1.9.0
+     * @default DefaultActiveOrderStrategy
+     */
+    activeOrderStrategy?: ActiveOrderStrategy | ActiveOrderStrategy[];
 }
 
 /**

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -37,6 +37,7 @@
     "no-configurable-operation-def-with-code-found": "No { type } with the code '{ code }' could be found",
     "no-price-found-for-channel": "No price information was found for ProductVariant ID '{ variantId}' in the Channel '{ channel }'.",
     "no-search-plugin-configured": "No search plugin has been configured",
+    "order-could-not-be-determined-or-created": "No active Order could be determined nor created",
     "order-does-not-contain-line-with-id": "This order does not contain an OrderLine with the id { id }",
     "pending-identifier-missing": "Could not find the pending email address to update",
     "permission-invalid": "The permission \"{ permission }\" may not be assigned",

+ 61 - 2
packages/core/src/service/helpers/active-order/active-order.service.ts

@@ -1,8 +1,9 @@
 import { Injectable } from '@nestjs/common';
 
 import { RequestContext } from '../../../api/common/request-context';
-import { InternalServerError } from '../../../common/error/errors';
-import { TransactionalConnection } from '../../../connection/transactional-connection';
+import { InternalServerError, UserInputError } from '../../../common/index';
+import { ConfigService } from '../../../config/index';
+import { TransactionalConnection } from '../../../connection/index';
 import { Order } from '../../../entity/order/order.entity';
 import { OrderService } from '../../services/order.service';
 import { SessionService } from '../../services/session.service';
@@ -19,6 +20,7 @@ export class ActiveOrderService {
         private sessionService: SessionService,
         private orderService: OrderService,
         private connection: TransactionalConnection,
+        private configService: ConfigService,
     ) {}
 
     /**
@@ -28,6 +30,8 @@ export class ActiveOrderService {
      *
      * Intended to be used at the Resolver layer for those resolvers that depend upon an active Order
      * being present.
+     *
+     * @deprecated From v1.9.0, use the `getActiveOrder` method which uses any configured ActiveOrderStrategies
      */
     async getOrderFromContext(ctx: RequestContext): Promise<Order | undefined>;
     async getOrderFromContext(ctx: RequestContext, createIfNotExists: true): Promise<Order>;
@@ -65,4 +69,59 @@ export class ActiveOrderService {
         }
         return order || undefined;
     }
+
+    /**
+     * @description
+     * Retrieves the active Order based on the configured {@link ActiveOrderStrategy}.
+     *
+     * @since 1.9.0
+     */
+    async getActiveOrder(
+        ctx: RequestContext,
+        input: { [strategyName: string]: any } | undefined,
+    ): Promise<Order | undefined>;
+    async getActiveOrder(
+        ctx: RequestContext,
+        input: { [strategyName: string]: any } | undefined,
+        createIfNotExists: true,
+    ): Promise<Order>;
+    async getActiveOrder(
+        ctx: RequestContext,
+        input: { [strategyName: string]: any } | undefined,
+        createIfNotExists = false,
+    ): Promise<Order | undefined> {
+        let order: any;
+        if (!order) {
+            const { activeOrderStrategy } = this.configService.orderOptions;
+            const strategyArray = Array.isArray(activeOrderStrategy)
+                ? activeOrderStrategy
+                : [activeOrderStrategy];
+            for (const strategy of strategyArray) {
+                const strategyInput = input?.[strategy.name] ?? {};
+                order = await strategy.determineActiveOrder(ctx, strategyInput);
+                if (order) {
+                    break;
+                }
+                if (createIfNotExists && typeof strategy.createActiveOrder === 'function') {
+                    order = await strategy.createActiveOrder(ctx, input);
+                }
+                if (order) {
+                    break;
+                }
+            }
+
+            if (!order && createIfNotExists) {
+                // No order has been found, and none could be created, which indicates that
+                // none of the configured strategies have a `createActiveOrder` method defined.
+                // In this case, we should throw an error because it is assumed that such a configuration
+                // indicates that an external order creation mechanism should be defined.
+                throw new UserInputError('error.order-could-not-be-determined-or-created');
+            }
+
+            if (order && ctx.session) {
+                await this.sessionService.setActiveOrder(ctx, ctx.session, order);
+            }
+        }
+        return order || undefined;
+    }
 }

+ 76 - 0
packages/dev-server/test-plugins/custom-active-order-plugin.ts

@@ -0,0 +1,76 @@
+import {
+    ActiveOrderStrategy,
+    idsAreEqual,
+    Injector,
+    Order,
+    OrderService,
+    RequestContext,
+    TransactionalConnection,
+    VendurePlugin,
+} from '@vendure/core';
+import { CustomOrderFields } from '@vendure/core/dist/entity/custom-entity-fields';
+import gql from 'graphql-tag';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+    interface CustomOrderFields {
+        orderToken: string;
+    }
+}
+
+class TokenActiveOrderStrategy implements ActiveOrderStrategy {
+    readonly name = 'orderToken';
+
+    private connection: TransactionalConnection;
+    private orderService: OrderService;
+
+    init(injector: Injector) {
+        this.connection = injector.get(TransactionalConnection);
+        this.orderService = injector.get(OrderService);
+    }
+
+    defineInputType = () => gql`
+        input CustomActiveOrderInput {
+            orderToken: String
+        }
+    `;
+
+    async determineActiveOrder(ctx: RequestContext, input: { orderToken: string }) {
+        const qb = this.connection
+            .getRepository(ctx, Order)
+            .createQueryBuilder('order')
+            .leftJoinAndSelect('order.customer', 'customer')
+            .where('order.customFields.orderToken = :orderToken', { orderToken: input.orderToken });
+
+        const order = await qb.getOne();
+        if (!order) {
+            return;
+        }
+        return order;
+        // const orderUserId = order.customer && order.customer.user && order.customer.user.id;
+        // if (idsAreEqual(ctx.activeUserId, orderUserId)) {
+        //     return order;
+        // } else {
+        //     return;
+        // }
+    }
+
+    // async createActiveOrder(ctx: RequestContext) {
+    //     const order = await this.orderService.create(ctx, ctx.activeUserId);
+    //     order.customFields.orderToken = Math.random().toString(36).substr(5);
+    //     await this.connection.getRepository(ctx, Order).save(order);
+    //     return order;
+    // }
+}
+
+@VendurePlugin({
+    configuration: config => {
+        config.customFields.Order.push({
+            name: 'orderToken',
+            type: 'string',
+            internal: true,
+        });
+        config.orderOptions.activeOrderStrategy = new TokenActiveOrderStrategy();
+        return config;
+    },
+})
+export class CustomActiveOrderPlugin {}