UNPKG

@vendure/core

Version:

A modern, headless ecommerce framework

356 lines 13.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RequestContext = void 0; exports.internal_setRequestContext = internal_setRequestContext; exports.internal_getRequestContext = internal_getRequestContext; const shared_utils_1 = require("@vendure/common/lib/shared-utils"); const constants_1 = require("../../common/constants"); const utils_1 = require("../../common/utils"); const channel_entity_1 = require("../../entity/channel/channel.entity"); /** * @description * This function is used to set the {@link RequestContext} on the `req` object. This is the underlying * mechanism by which we are able to access the `RequestContext` from different places. * * For example, here is a diagram to show how, in an incoming API request, we are able to store * and retrieve the `RequestContext` in a resolver: * ``` * - query { product } * | * - AuthGuard.canActivate() * | | creates a `RequestContext`, stores it on `req` * | * - product() resolver * | @Ctx() decorator fetching `RequestContext` from `req` * ``` * * We named it this way to discourage usage outside the framework internals. */ function internal_setRequestContext(req, ctx, executionContext) { var _a; // If we have access to the `ExecutionContext`, it means we are able to bind // the `ctx` object to the specific "handler", i.e. the resolver function (for GraphQL) // or controller (for REST). let item; if (executionContext && typeof executionContext.getHandler === 'function') { // eslint-disable-next-line @typescript-eslint/ban-types const map = req[constants_1.REQUEST_CONTEXT_MAP_KEY] || new Map(); item = map.get(executionContext.getHandler()); const ctxHasTransaction = Object.getOwnPropertySymbols(ctx).includes(constants_1.TRANSACTION_MANAGER_KEY); if (item) { item.default = (_a = item.default) !== null && _a !== void 0 ? _a : ctx; if (ctxHasTransaction) { item.withTransactionManager = ctx; } } else { item = { default: ctx, withTransactionManager: ctxHasTransaction ? ctx : undefined, }; } map.set(executionContext.getHandler(), item); req[constants_1.REQUEST_CONTEXT_MAP_KEY] = map; } // We also bind to a shared key so that we can access the `ctx` object // later even if we don't have a reference to the `ExecutionContext` req[constants_1.REQUEST_CONTEXT_KEY] = item !== null && item !== void 0 ? item : { default: ctx, }; } /** * @description * Gets the {@link RequestContext} from the `req` object. See {@link internal_setRequestContext} * for more details on this mechanism. */ function internal_getRequestContext(req, executionContext) { var _a, _b; let item; if (executionContext && typeof executionContext.getHandler === 'function') { // eslint-disable-next-line @typescript-eslint/ban-types const map = req[constants_1.REQUEST_CONTEXT_MAP_KEY]; item = map === null || map === void 0 ? void 0 : map.get(executionContext.getHandler()); // If we have a ctx associated with the current handler (resolver function), we // return it. Otherwise, we fall back to the shared key which will be there. if (item) { return item.withTransactionManager || item.default; } } if (!item) { item = req[constants_1.REQUEST_CONTEXT_KEY]; } const transactionalCtx = (item === null || item === void 0 ? void 0 : item.withTransactionManager) && ((_b = (_a = item.withTransactionManager[constants_1.TRANSACTION_MANAGER_KEY]) === null || _a === void 0 ? void 0 : _a.queryRunner) === null || _b === void 0 ? void 0 : _b.isReleased) === false ? item.withTransactionManager : undefined; return transactionalCtx || item.default; } /** * @description * The RequestContext holds information relevant to the current request, which may be * required at various points of the stack. * * It is a good practice to inject the RequestContext (using the {@link Ctx} decorator) into * _all_ resolvers & REST handler, and then pass it through to the service layer. * * This allows the service layer to access information about the current user, the active language, * the active Channel, and so on. In addition, the {@link TransactionalConnection} relies on the * presence of the RequestContext object in order to correctly handle per-request database transactions. * * The RequestContext also provides mechanisms for managing the database replication mode via the * `setReplicationMode` method and the `replicationMode` getter. This allows for finer control * over whether database queries within the context should be executed against the master or a replica * database, which can be particularly useful in distributed database environments. * * @example * ```ts * \@Query() * myQuery(\@Ctx() ctx: RequestContext) { * return this.myService.getData(ctx); * } * ``` * * @example * ```ts * \@Query() * myMutation(\@Ctx() ctx: RequestContext) { * ctx.setReplicationMode('master'); * return this.myService.getData(ctx); * } * ``` * @docsCategory request */ class RequestContext { /** * @internal */ constructor(options) { const { req, apiType, channel, session, languageCode, currencyCode, translationFn } = options; this._req = req; this._apiType = apiType; this._channel = channel; this._session = session; this._languageCode = languageCode || (channel && channel.defaultLanguageCode); this._currencyCode = currencyCode || (channel && channel.defaultCurrencyCode); this._isAuthorized = options.isAuthorized; this._authorizedAsOwnerOnly = options.authorizedAsOwnerOnly; this._translationFn = translationFn || ((key) => key); } /** * @description * Creates an "empty" RequestContext object. This is only intended to be used * when a service method must be called outside the normal request-response * cycle, e.g. when programmatically populating data. Usually a better alternative * is to use the {@link RequestContextService} `create()` method, which allows more control * over the resulting RequestContext object. */ static empty() { return new RequestContext({ apiType: 'admin', authorizedAsOwnerOnly: false, channel: new channel_entity_1.Channel(), isAuthorized: true, }); } /** * @description * Creates a new RequestContext object from a serialized object created by the * `serialize()` method. */ static deserialize(ctxObject) { var _a; return new RequestContext({ req: ctxObject._req, apiType: ctxObject._apiType, channel: new channel_entity_1.Channel(ctxObject._channel), session: Object.assign(Object.assign({}, ctxObject._session), { expires: ((_a = ctxObject._session) === null || _a === void 0 ? void 0 : _a.expires) && new Date(ctxObject._session.expires) }), languageCode: ctxObject._languageCode, isAuthorized: ctxObject._isAuthorized, authorizedAsOwnerOnly: ctxObject._authorizedAsOwnerOnly, }); } /** * @description * Returns `true` if there is an active Session & User associated with this request, * and that User has the specified permissions on the active Channel. */ userHasPermissions(permissions) { var _a; const user = (_a = this.session) === null || _a === void 0 ? void 0 : _a.user; if (!user || !this.channelId) { return false; } const permissionsOnChannel = user.channelPermissions.find(c => (0, utils_1.idsAreEqual)(c.id, this.channelId)); if (permissionsOnChannel) { return this.arraysIntersect(permissionsOnChannel.permissions, permissions); } return false; } /** * @description * Serializes the RequestContext object into a JSON-compatible simple object. * This is useful when you need to send a RequestContext object to another * process, e.g. to pass it to the Job Queue via the {@link JobQueueService}. */ serialize() { const serializableThis = Object.assign({}, this); if (this._req) { serializableThis._req = this.shallowCloneRequestObject(this._req); } return JSON.parse(JSON.stringify(serializableThis)); } /** * @description * Creates a shallow copy of the RequestContext instance. This means that * mutations to the copy itself will not affect the original, but deep mutations * (e.g. copy.channel.code = 'new') *will* also affect the original. */ copy() { return Object.assign(Object.create(Object.getPrototypeOf(this)), this); } /** * @description * The raw Express request object. */ get req() { return this._req; } /** * @description * Signals which API this request was received by, e.g. `admin` or `shop`. */ get apiType() { return this._apiType; } /** * @description * The active {@link Channel} of this request. */ get channel() { return this._channel; } get channelId() { return this._channel.id; } get languageCode() { return this._languageCode; } get currencyCode() { return this._currencyCode; } get session() { return this._session; } get activeUserId() { var _a, _b; return (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.user) === null || _b === void 0 ? void 0 : _b.id; } /** * @description * True if the current session is authorized to access the current resolver method. * * @deprecated Use `userHasPermissions()` method instead. */ get isAuthorized() { return this._isAuthorized; } /** * @description * True if the current anonymous session is only authorized to operate on entities that * are owned by the current session. */ get authorizedAsOwnerOnly() { return this._authorizedAsOwnerOnly; } /** * @description * Translate the given i18n key */ translate(key, variables) { try { return this._translationFn(key, variables); } catch (e) { return `Translation format error: ${JSON.stringify(e.message)}). Original key: ${key}`; } } /** * Returns true if any element of arr1 appears in arr2. */ arraysIntersect(arr1, arr2) { return arr1.reduce((intersects, role) => { return intersects || arr2.includes(role); }, false); } /** * 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 */ shallowCloneRequestObject(req) { function copySimpleFieldsToDepth(target, maxDepth, depth = 0) { const result = {}; // eslint-disable-next-line guard-for-in for (const key in target) { if (key === 'host' && depth === 0) { // avoid Express "deprecated: req.host" warning continue; } let val; try { val = target[key]; } catch (e) { val = String(e); } if (Array.isArray(val)) { depth++; result[key] = val.map(v => { if (!(0, shared_utils_1.isObject)(v) && typeof val !== 'function') { return v; } else { return copySimpleFieldsToDepth(v, maxDepth, depth); } }); depth--; } else if (!(0, shared_utils_1.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); } /** * @description * Sets the replication mode for the current RequestContext. This mode determines whether the operations * within this context should interact with the master database or a replica. Use this method to explicitly * define the replication mode for the context. * * @param mode - The replication mode to be set (e.g., 'master' or 'replica'). */ setReplicationMode(mode) { this._replicationMode = mode; } /** * @description * Gets the current replication mode of the RequestContext. If no replication mode has been set, * it returns `undefined`. This property indicates whether the context is configured to interact with * the master database or a replica. * * @returns The current replication mode, or `undefined` if none is set. */ get replicationMode() { return this._replicationMode; } } exports.RequestContext = RequestContext; //# sourceMappingURL=request-context.js.map