Browse Source

feat(core): Include express request object in RequestContext

Closes #581
Michael Bromley 5 years ago
parent
commit
c4352b2436

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

@@ -39,6 +39,7 @@ export class RequestContextService {
         const authorizedAsOwnerOnly = !isAuthorized && hasOwnerPermission;
         const translationFn = (req as any).t;
         return new RequestContext({
+            req,
             apiType,
             channel,
             languageCode,

+ 58 - 2
packages/core/src/api/common/request-context.ts

@@ -1,5 +1,7 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { ID, JsonCompatible } from '@vendure/common/lib/shared-types';
+import { isObject } from '@vendure/common/lib/shared-utils';
+import { Request } from 'express';
 import { TFunction } from 'i18next';
 
 import { CachedSession } from '../../config/session-cache/session-cache-strategy';
@@ -8,6 +10,7 @@ import { Channel } from '../../entity/channel/channel.entity';
 import { ApiType } from './get-api-type';
 
 export type SerializedRequestContext = {
+    _req?: any;
     _session: JsonCompatible<Required<CachedSession>>;
     _apiType: ApiType;
     _channel: JsonCompatible<Channel>;
@@ -45,11 +48,13 @@ export class RequestContext {
     private readonly _authorizedAsOwnerOnly: boolean;
     private readonly _translationFn: TFunction;
     private readonly _apiType: ApiType;
+    private readonly _req?: Request;
 
     /**
      * @internal
      */
     constructor(options: {
+        req?: Request;
         apiType: ApiType;
         channel: Channel;
         session?: CachedSession;
@@ -58,7 +63,8 @@ export class RequestContext {
         authorizedAsOwnerOnly: boolean;
         translationFn?: TFunction;
     }) {
-        const { apiType, channel, session, languageCode, translationFn } = options;
+        const { req, apiType, channel, session, languageCode, translationFn } = options;
+        this._req = req;
         this._apiType = apiType;
         this._channel = channel;
         this._session = session;
@@ -90,6 +96,7 @@ export class RequestContext {
      */
     static deserialize(ctxObject: SerializedRequestContext): RequestContext {
         return new RequestContext({
+            req: ctxObject._req as any,
             apiType: ctxObject._apiType,
             channel: new Channel(ctxObject._channel),
             session: {
@@ -109,7 +116,11 @@ export class RequestContext {
      * process, e.g. to pass it to the Worker process via the {@link WorkerService}.
      */
     serialize(): SerializedRequestContext {
-        return JSON.parse(JSON.stringify(this));
+        const serializableThis: any = Object.assign({}, this);
+        if (this._req) {
+            serializableThis._req = this.shallowCloneRequestObject(this._req);
+        }
+        return JSON.parse(JSON.stringify(serializableThis));
     }
 
     /**
@@ -122,10 +133,26 @@ export class RequestContext {
         return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
     }
 
+    /**
+     * @description
+     * The raw Express request object.
+     */
+    get req(): Request | undefined {
+        return this._req;
+    }
+
+    /**
+     * @description
+     * Signals which API this request was received by, e.g. `admin` or `shop`.
+     */
     get apiType(): ApiType {
         return this._apiType;
     }
 
+    /**
+     * @description
+     * The active {@link Channel} of this request.
+     */
     get channel(): Channel {
         return this._channel;
     }
@@ -174,4 +201,33 @@ export class RequestContext {
             return `Translation format error: ${e.message}). Original key: ${key}`;
         }
     }
+
+    /**
+     * The Express "Request" object is huge and contains many circular
+     * references. We will preserve just a subset of the whole, by preserving
+     * only the serializable properties up to 2 levels deep.
+     * @private
+     */
+    private shallowCloneRequestObject(req: Request) {
+        function copySimpleFieldsToDepth(target: any, maxDepth: number, depth: number = 0) {
+            const result: any = {};
+            // tslint:disable-next-line:forin
+            for (const key in target) {
+                if (key === 'host' && depth === 0) {
+                    // avoid Express "deprecated: req.host" warning
+                    continue;
+                }
+                const val = (target as any)[key];
+                if (!isObject(val) && typeof val !== 'function') {
+                    result[key] = val;
+                } else if (depth < maxDepth) {
+                    depth++;
+                    result[key] = copySimpleFieldsToDepth(val, maxDepth, depth);
+                    depth--;
+                }
+            }
+            return result;
+        }
+        return copySimpleFieldsToDepth(req, 1);
+    }
 }