Browse Source

feat(core): Make request pipeline compatible with REST requests

Michael Bromley 6 years ago
parent
commit
42aa5fbac5

+ 4 - 4
packages/core/src/api/common/get-api-type.ts

@@ -6,16 +6,16 @@ import { GraphQLResolveInfo } from 'graphql';
  *
  * @docsCategory request
  */
-export type ApiType = 'admin' | 'shop';
+export type ApiType = 'admin' | 'shop' | 'custom';
 
 /**
  * Inspects the GraphQL "info" resolver argument to determine which API
  * the request came through.
  */
-export function getApiType(info: GraphQLResolveInfo): ApiType {
-    const query = info.schema.getQueryType();
+export function getApiType(info?: GraphQLResolveInfo): ApiType {
+    const query = info && info.schema.getQueryType();
     if (query) {
         return !!query.getFields().administrators ? 'admin' : 'shop';
     }
-    return 'shop';
+    return 'custom';
 }

+ 32 - 0
packages/core/src/api/common/parse-context.ts

@@ -0,0 +1,32 @@
+import { ExecutionContext } from '@nestjs/common';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { Request, Response } from 'express';
+import { GraphQLResolveInfo } from 'graphql';
+
+/**
+ * Parses in the Nest ExecutionContext of the incoming request, accounting for both
+ * GraphQL & REST requests.
+ */
+export function parseContext(
+    context: ExecutionContext,
+): { req: Request; res: Response; isGraphQL: boolean; info?: GraphQLResolveInfo } {
+    const graphQlContext = GqlExecutionContext.create(context);
+    const restContext = GqlExecutionContext.create(context);
+    const info = graphQlContext.getInfo();
+    let req: Request;
+    let res: Response;
+    if (info) {
+        const ctx = graphQlContext.getContext();
+        req = ctx.req;
+        res = ctx.res;
+    } else {
+        req = context.switchToHttp().getRequest();
+        res = context.switchToHttp().getResponse();
+    }
+    return {
+        req,
+        res,
+        info,
+        isGraphQL: !!info,
+    };
+}

+ 1 - 1
packages/core/src/api/common/request-context.service.ts

@@ -28,7 +28,7 @@ export class RequestContextService {
      */
     async fromRequest(
         req: Request,
-        info: GraphQLResolveInfo,
+        info?: GraphQLResolveInfo,
         requiredPermissions?: Permission[],
         session?: Session,
     ): Promise<RequestContext> {

+ 8 - 2
packages/core/src/api/decorators/request-context.decorator.ts

@@ -6,6 +6,12 @@ import { REQUEST_CONTEXT_KEY } from '../common/request-context.service';
  * Resolver param decorator which extracts the RequestContext from the incoming
  * request object.
  */
-export const Ctx = createParamDecorator((data, [root, args, ctx]) => {
-    return ctx.req[REQUEST_CONTEXT_KEY];
+export const Ctx = createParamDecorator((data, arg) => {
+    if (Array.isArray(arg)) {
+        // GraphQL request
+        return arg[2].req[REQUEST_CONTEXT_KEY];
+    } else {
+        // REST request
+        return arg[REQUEST_CONTEXT_KEY];
+    }
 });

+ 13 - 9
packages/core/src/api/middleware/asset-interceptor.ts

@@ -1,5 +1,4 @@
 import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
-import { GqlExecutionContext } from '@nestjs/graphql';
 import { Type } from '@vendure/common/lib/shared-types';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
@@ -7,6 +6,7 @@ import { map } from 'rxjs/operators';
 import { AssetStorageStrategy } from '../../config/asset-storage-strategy/asset-storage-strategy';
 import { ConfigService } from '../../config/config.service';
 import { Asset } from '../../entity/asset/asset.entity';
+import { parseContext } from '../common/parse-context';
 
 /**
  * Transforms outputs so that any Asset instances are run through the {@link AssetStorageStrategy.toAbsoluteUrl}
@@ -30,20 +30,19 @@ export class AssetInterceptor implements NestInterceptor {
         if (toAbsoluteUrl === undefined) {
             return next.handle();
         }
-        const ctx = GqlExecutionContext.create(context).getContext();
-        const request = ctx.req;
+        const { req } = parseContext(context);
         return next.handle().pipe(
             map(data => {
                 if (data instanceof Asset) {
-                    data.preview = toAbsoluteUrl(request, data.preview);
-                    data.source = toAbsoluteUrl(request, data.source);
+                    data.preview = toAbsoluteUrl(req, data.preview);
+                    data.source = toAbsoluteUrl(req, data.source);
                 } else {
                     visitType(data, [Asset, 'productPreview', 'productVariantPreview'], asset => {
                         if (asset instanceof Asset) {
-                            asset.preview = toAbsoluteUrl(request, asset.preview);
-                            asset.source = toAbsoluteUrl(request, asset.source);
+                            asset.preview = toAbsoluteUrl(req, asset.preview);
+                            asset.source = toAbsoluteUrl(req, asset.source);
                         } else {
-                            asset = toAbsoluteUrl(request, asset);
+                            asset = toAbsoluteUrl(req, asset);
                         }
                         return asset;
                     });
@@ -58,7 +57,12 @@ export class AssetInterceptor implements NestInterceptor {
  * Traverses the object and when encountering a property with a value which
  * is an instance of class T, invokes the visitor function on that value.
  */
-function visitType<T>(obj: any, types: Array<Type<T> | string>, visit: (instance: T | string) => T | string, seen: Set<any> = new Set()) {
+function visitType<T>(
+    obj: any,
+    types: Array<Type<T> | string>,
+    visit: (instance: T | string) => T | string,
+    seen: Set<any> = new Set(),
+) {
     const keys = Object.keys(obj || {});
     for (const key of keys) {
         const value = obj[key];

+ 2 - 5
packages/core/src/api/middleware/auth-guard.ts

@@ -10,6 +10,7 @@ import { ConfigService } from '../../config/config.service';
 import { Session } from '../../entity/session/session.entity';
 import { AuthService } from '../../service/services/auth.service';
 import { extractAuthToken } from '../common/extract-auth-token';
+import { parseContext } from '../common/parse-context';
 import { REQUEST_CONTEXT_KEY, RequestContextService } from '../common/request-context.service';
 import { setAuthToken } from '../common/set-auth-token';
 import { PERMISSIONS_METADATA_KEY } from '../decorators/allow.decorator';
@@ -30,11 +31,7 @@ export class AuthGuard implements CanActivate {
     ) {}
 
     async canActivate(context: ExecutionContext): Promise<boolean> {
-        const graphQlContext = GqlExecutionContext.create(context);
-        const ctx = graphQlContext.getContext();
-        const info = graphQlContext.getInfo<GraphQLResolveInfo>();
-        const req: Request = ctx.req;
-        const res: Response = ctx.res;
+        const { req, res, info } = parseContext(context);
         const authDisabled = this.configService.authOptions.disableAuth;
         const permissions = this.reflector.get<Permission[]>(PERMISSIONS_METADATA_KEY, context.getHandler());
         const isPublic = !!permissions && permissions.includes(Permission.Public);

+ 12 - 8
packages/core/src/api/middleware/id-interceptor.ts

@@ -4,6 +4,7 @@ import { GqlExecutionContext } from '@nestjs/graphql';
 import { Observable } from 'rxjs';
 
 import { IdCodecService } from '../common/id-codec.service';
+import { parseContext } from '../common/parse-context';
 import { DECODE_METADATA_KEY } from '../decorators/decode.decorator';
 
 /**
@@ -18,14 +19,17 @@ export class IdInterceptor implements NestInterceptor {
     constructor(private idCodecService: IdCodecService, private readonly reflector: Reflector) {}
 
     intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
-        const args = GqlExecutionContext.create(context).getArgs();
-        const transformKeys = this.reflector.get<string[]>(DECODE_METADATA_KEY, context.getHandler());
-        const gqlRoot = context.getArgByIndex(0);
-        if (!gqlRoot) {
-            // Only need to decode ids if this is a root query/mutation.
-            // Internal (property-resolver) requests can then be assumed to
-            // be already decoded.
-            Object.assign(args, this.idCodecService.decode(args, transformKeys));
+        const { isGraphQL } = parseContext(context);
+        if (isGraphQL) {
+            const args = GqlExecutionContext.create(context).getArgs();
+            const transformKeys = this.reflector.get<string[]>(DECODE_METADATA_KEY, context.getHandler());
+            const gqlRoot = context.getArgByIndex(0);
+            if (!gqlRoot) {
+                // Only need to decode ids if this is a root query/mutation.
+                // Internal (property-resolver) requests can then be assumed to
+                // be already decoded.
+                Object.assign(args, this.idCodecService.decode(args, transformKeys));
+            }
         }
         return next.handle();
     }

+ 41 - 18
packages/core/src/api/middleware/validate-custom-fields-interceptor.ts

@@ -21,6 +21,7 @@ import {
     LocaleStringCustomFieldConfig,
     StringCustomFieldConfig,
 } from '../../config/custom-field/custom-field-types';
+import { parseContext } from '../common/parse-context';
 import { RequestContext } from '../common/request-context';
 import { REQUEST_CONTEXT_KEY } from '../common/request-context.service';
 import { validateCustomFieldValue } from '../common/validate-custom-field-value';
@@ -43,37 +44,52 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
     }
 
     intercept(context: ExecutionContext, next: CallHandler<any>) {
-        const gqlExecutionContext = GqlExecutionContext.create(context);
-        const { operation, schema } = gqlExecutionContext.getInfo<GraphQLResolveInfo>();
-        const variables = gqlExecutionContext.getArgs();
-        const ctx: RequestContext = gqlExecutionContext.getContext().req[REQUEST_CONTEXT_KEY];
+        const { isGraphQL } = parseContext(context);
+        if (isGraphQL) {
+            const gqlExecutionContext = GqlExecutionContext.create(context);
+            const { operation, schema } = gqlExecutionContext.getInfo<GraphQLResolveInfo>();
+            const variables = gqlExecutionContext.getArgs();
+            const ctx: RequestContext = gqlExecutionContext.getContext().req[REQUEST_CONTEXT_KEY];
 
-        if (operation.operation === 'mutation') {
-            const inputTypeNames = this.getArgumentMap(operation, schema);
-            Object.entries(inputTypeNames).forEach(([inputName, typeName]) => {
-                if (this.inputsWithCustomFields.has(typeName)) {
-                    if (variables[inputName]) {
-                        this.validateInput(typeName, ctx.languageCode, variables[inputName]);
+            if (operation.operation === 'mutation') {
+                const inputTypeNames = this.getArgumentMap(operation, schema);
+                Object.entries(inputTypeNames).forEach(([inputName, typeName]) => {
+                    if (this.inputsWithCustomFields.has(typeName)) {
+                        if (variables[inputName]) {
+                            this.validateInput(typeName, ctx.languageCode, variables[inputName]);
+                        }
                     }
-                }
-            });
+                });
+            }
         }
         return next.handle();
     }
 
-    private validateInput(typeName: string, languageCode: LanguageCode, variableValues?: { [key: string]: any }) {
+    private validateInput(
+        typeName: string,
+        languageCode: LanguageCode,
+        variableValues?: { [key: string]: any },
+    ) {
         if (variableValues) {
             const entityName = typeName.replace(/(Create|Update)(.+)Input/, '$2');
             const customFieldConfig = this.configService.customFields[entityName as keyof CustomFields];
             if (customFieldConfig) {
                 if (variableValues.customFields) {
-                    this.validateCustomFieldsObject(customFieldConfig, languageCode, variableValues.customFields);
+                    this.validateCustomFieldsObject(
+                        customFieldConfig,
+                        languageCode,
+                        variableValues.customFields,
+                    );
                 }
                 const translations = variableValues.translations;
                 if (Array.isArray(translations)) {
                     for (const translation of translations) {
                         if (translation.customFields) {
-                            this.validateCustomFieldsObject(customFieldConfig, languageCode, translation.customFields);
+                            this.validateCustomFieldsObject(
+                                customFieldConfig,
+                                languageCode,
+                                translation.customFields,
+                            );
                         }
                     }
                 }
@@ -81,7 +97,11 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
         }
     }
 
-    private validateCustomFieldsObject(customFieldConfig: CustomFieldConfig[], languageCode: LanguageCode, customFieldsObject: { [key: string]: any; }) {
+    private validateCustomFieldsObject(
+        customFieldConfig: CustomFieldConfig[],
+        languageCode: LanguageCode,
+        customFieldsObject: { [key: string]: any },
+    ) {
         for (const [key, value] of Object.entries(customFieldsObject)) {
             const config = customFieldConfig.find(c => c.name === key);
             if (config) {
@@ -90,12 +110,15 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
         }
     }
 
-    private getArgumentMap(operation: OperationDefinitionNode, schema: GraphQLSchema): { [inputName: string]: string; } {
+    private getArgumentMap(
+        operation: OperationDefinitionNode,
+        schema: GraphQLSchema,
+    ): { [inputName: string]: string } {
         const mutationType = schema.getMutationType();
         if (!mutationType) {
             return {};
         }
-        const map: { [inputName: string]: string; } = {};
+        const map: { [inputName: string]: string } = {};
 
         for (const selection of operation.selectionSet.selections) {
             if (selection.kind === 'Field') {

+ 2 - 16
packages/core/src/api/resolvers/entity/payment-entity.resolver.ts

@@ -1,31 +1,17 @@
 import { Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
 
-import { Translated } from '../../../common/types/locale-types';
-import { Collection } from '../../../entity/collection/collection.entity';
-import { OrderItem } from '../../../entity/order-item/order-item.entity';
 import { Payment } from '../../../entity/payment/payment.entity';
-import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
-import { Product } from '../../../entity/product/product.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
-import { CollectionService } from '../../../service/services/collection.service';
 import { OrderService } from '../../../service/services/order.service';
-import { ProductVariantService } from '../../../service/services/product-variant.service';
-import { ApiType } from '../../common/get-api-type';
 import { RequestContext } from '../../common/request-context';
-import { Api } from '../../decorators/api.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('Payment')
 export class PaymentEntityResolver {
-    constructor(
-        private orderService: OrderService,
-    ) {}
+    constructor(private orderService: OrderService) {}
 
     @ResolveProperty()
-    async refunds(
-        @Ctx() ctx: RequestContext,
-        @Parent() payment: Payment,
-    ): Promise<Refund[]> {
+    async refunds(@Ctx() ctx: RequestContext, @Parent() payment: Payment): Promise<Refund[]> {
         if (payment.refunds) {
             return payment.refunds;
         } else {

+ 2 - 15
packages/core/src/api/resolvers/entity/refund-entity.resolver.ts

@@ -1,30 +1,17 @@
 import { Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
 
-import { Translated } from '../../../common/types/locale-types';
-import { Collection } from '../../../entity/collection/collection.entity';
 import { OrderItem } from '../../../entity/order-item/order-item.entity';
-import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
-import { Product } from '../../../entity/product/product.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
-import { CollectionService } from '../../../service/services/collection.service';
 import { OrderService } from '../../../service/services/order.service';
-import { ProductVariantService } from '../../../service/services/product-variant.service';
-import { ApiType } from '../../common/get-api-type';
 import { RequestContext } from '../../common/request-context';
-import { Api } from '../../decorators/api.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('Refund')
 export class RefundEntityResolver {
-    constructor(
-        private orderService: OrderService,
-    ) {}
+    constructor(private orderService: OrderService) {}
 
     @ResolveProperty()
-    async orderItems(
-        @Ctx() ctx: RequestContext,
-        @Parent() refund: Refund,
-    ): Promise<OrderItem[]> {
+    async orderItems(@Ctx() ctx: RequestContext, @Parent() refund: Refund): Promise<OrderItem[]> {
         if (refund.orderItems) {
             return refund.orderItems;
         } else {