request-context.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { LanguageCode } from '@vendure/common/lib/generated-types';
  2. import { ID, JsonCompatible } from '@vendure/common/lib/shared-types';
  3. import { isObject } from '@vendure/common/lib/shared-utils';
  4. import { Request } from 'express';
  5. import { TFunction } from 'i18next';
  6. import { CachedSession } from '../../config/session-cache/session-cache-strategy';
  7. import { Channel } from '../../entity/channel/channel.entity';
  8. import { ApiType } from './get-api-type';
  9. export type SerializedRequestContext = {
  10. _req?: any;
  11. _session: JsonCompatible<Required<CachedSession>>;
  12. _apiType: ApiType;
  13. _channel: JsonCompatible<Channel>;
  14. _languageCode: LanguageCode;
  15. _isAuthorized: boolean;
  16. _authorizedAsOwnerOnly: boolean;
  17. };
  18. /**
  19. * @description
  20. * The RequestContext holds information relevant to the current request, which may be
  21. * required at various points of the stack.
  22. *
  23. * It is a good practice to inject the RequestContext (using the {@link Ctx} decorator) into
  24. * _all_ resolvers & REST handlers, and then pass it through to the service layer.
  25. *
  26. * This allows the service layer to access information about the current user, the active language,
  27. * the active Channel, and so on. In addition, the {@link TransactionalConnection} relies on the
  28. * presence of the RequestContext object in order to correctly handle per-request database transactions.
  29. *
  30. * @example
  31. * ```TypeScript
  32. * \@Query()
  33. * myQuery(\@Ctx() ctx: RequestContext) {
  34. * return this.myService.getData(ctx);
  35. * }
  36. * ```
  37. * @docsCategory request
  38. */
  39. export class RequestContext {
  40. private readonly _languageCode: LanguageCode;
  41. private readonly _channel: Channel;
  42. private readonly _session?: CachedSession;
  43. private readonly _isAuthorized: boolean;
  44. private readonly _authorizedAsOwnerOnly: boolean;
  45. private readonly _translationFn: TFunction;
  46. private readonly _apiType: ApiType;
  47. private readonly _req?: Request;
  48. /**
  49. * @internal
  50. */
  51. constructor(options: {
  52. req?: Request;
  53. apiType: ApiType;
  54. channel: Channel;
  55. session?: CachedSession;
  56. languageCode?: LanguageCode;
  57. isAuthorized: boolean;
  58. authorizedAsOwnerOnly: boolean;
  59. translationFn?: TFunction;
  60. }) {
  61. const { req, apiType, channel, session, languageCode, translationFn } = options;
  62. this._req = req;
  63. this._apiType = apiType;
  64. this._channel = channel;
  65. this._session = session;
  66. this._languageCode = languageCode || (channel && channel.defaultLanguageCode);
  67. this._isAuthorized = options.isAuthorized;
  68. this._authorizedAsOwnerOnly = options.authorizedAsOwnerOnly;
  69. this._translationFn = translationFn || (((key: string) => key) as any);
  70. }
  71. /**
  72. * @description
  73. * Creates an "empty" RequestContext object. This is only intended to be used
  74. * when a service method must be called outside the normal request-response
  75. * cycle, e.g. when programmatically populating data.
  76. */
  77. static empty(): RequestContext {
  78. return new RequestContext({
  79. apiType: 'admin',
  80. authorizedAsOwnerOnly: false,
  81. channel: new Channel(),
  82. isAuthorized: true,
  83. });
  84. }
  85. /**
  86. * @description
  87. * Creates a new RequestContext object from a serialized object created by the
  88. * `serialize()` method.
  89. */
  90. static deserialize(ctxObject: SerializedRequestContext): RequestContext {
  91. return new RequestContext({
  92. req: ctxObject._req as any,
  93. apiType: ctxObject._apiType,
  94. channel: new Channel(ctxObject._channel),
  95. session: {
  96. ...ctxObject._session,
  97. expires: ctxObject._session?.expires && new Date(ctxObject._session.expires),
  98. },
  99. languageCode: ctxObject._languageCode,
  100. isAuthorized: ctxObject._isAuthorized,
  101. authorizedAsOwnerOnly: ctxObject._authorizedAsOwnerOnly,
  102. });
  103. }
  104. /**
  105. * @description
  106. * Serializes the RequestContext object into a JSON-compatible simple object.
  107. * This is useful when you need to send a RequestContext object to another
  108. * process, e.g. to pass it to the Job Queue via the {@link JobQueueService}.
  109. */
  110. serialize(): SerializedRequestContext {
  111. const serializableThis: any = Object.assign({}, this);
  112. if (this._req) {
  113. serializableThis._req = this.shallowCloneRequestObject(this._req);
  114. }
  115. return JSON.parse(JSON.stringify(serializableThis));
  116. }
  117. /**
  118. * @description
  119. * Creates a shallow copy of the RequestContext instance. This means that
  120. * mutations to the copy itself will not affect the original, but deep mutations
  121. * (e.g. copy.channel.code = 'new') *will* also affect the original.
  122. */
  123. copy(): RequestContext {
  124. return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
  125. }
  126. /**
  127. * @description
  128. * The raw Express request object.
  129. */
  130. get req(): Request | undefined {
  131. return this._req;
  132. }
  133. /**
  134. * @description
  135. * Signals which API this request was received by, e.g. `admin` or `shop`.
  136. */
  137. get apiType(): ApiType {
  138. return this._apiType;
  139. }
  140. /**
  141. * @description
  142. * The active {@link Channel} of this request.
  143. */
  144. get channel(): Channel {
  145. return this._channel;
  146. }
  147. get channelId(): ID {
  148. return this._channel.id;
  149. }
  150. get languageCode(): LanguageCode {
  151. return this._languageCode;
  152. }
  153. get session(): CachedSession | undefined {
  154. return this._session;
  155. }
  156. get activeUserId(): ID | undefined {
  157. return this.session?.user?.id;
  158. }
  159. /**
  160. * @description
  161. * True if the current session is authorized to access the current resolver method.
  162. */
  163. get isAuthorized(): boolean {
  164. return this._isAuthorized;
  165. }
  166. /**
  167. * @description
  168. * True if the current anonymous session is only authorized to operate on entities that
  169. * are owned by the current session.
  170. */
  171. get authorizedAsOwnerOnly(): boolean {
  172. return this._authorizedAsOwnerOnly;
  173. }
  174. /**
  175. * @description
  176. * Translate the given i18n key
  177. */
  178. translate(key: string, variables?: { [k: string]: any }): string {
  179. try {
  180. return this._translationFn(key, variables);
  181. } catch (e) {
  182. return `Translation format error: ${e.message}). Original key: ${key}`;
  183. }
  184. }
  185. /**
  186. * The Express "Request" object is huge and contains many circular
  187. * references. We will preserve just a subset of the whole, by preserving
  188. * only the serializable properties up to 2 levels deep.
  189. * @private
  190. */
  191. private shallowCloneRequestObject(req: Request) {
  192. function copySimpleFieldsToDepth(target: any, maxDepth: number, depth: number = 0) {
  193. const result: any = {};
  194. // tslint:disable-next-line:forin
  195. for (const key in target) {
  196. if (key === 'host' && depth === 0) {
  197. // avoid Express "deprecated: req.host" warning
  198. continue;
  199. }
  200. const val = (target as any)[key];
  201. if (!isObject(val) && typeof val !== 'function') {
  202. result[key] = val;
  203. } else if (depth < maxDepth) {
  204. depth++;
  205. result[key] = copySimpleFieldsToDepth(val, maxDepth, depth);
  206. depth--;
  207. }
  208. }
  209. return result;
  210. }
  211. return copySimpleFieldsToDepth(req, 1);
  212. }
  213. }