@vendure/core
Version:
A modern, headless ecommerce framework
364 lines • 17.6 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionalConnection = void 0;
const common_1 = require("@nestjs/common");
const typeorm_decorators_1 = require("@nestjs/typeorm/dist/common/typeorm.decorators");
const typeorm_1 = require("typeorm");
const request_context_1 = require("../api/common/request-context");
const constants_1 = require("../common/constants");
const errors_1 = require("../common/error/errors");
const config_service_1 = require("../config/config.service");
const tree_relations_qb_joiner_1 = require("../service/helpers/utils/tree-relations-qb-joiner");
const find_options_object_to_array_1 = require("./find-options-object-to-array");
const transaction_wrapper_1 = require("./transaction-wrapper");
/**
* Repository methods intercepted by the access control Proxy.
* Hoisted to module level to avoid re-creating the Set on every getRepository() call.
*/
const ACCESS_CONTROL_INTERCEPTED_METHODS = new Set([
'find',
'findOne',
'findOneOrFail',
'findAndCount',
'count',
]);
/**
* SelectQueryBuilder terminal methods that execute SQL. When a QueryBuilder
* is obtained via a Proxy-wrapped repository's `createQueryBuilder()`, these
* methods are intercepted to apply access control before execution.
*/
const QB_TERMINAL_METHODS = new Set([
'getMany',
'getOne',
'getOneOrFail',
'getManyAndCount',
'getCount',
'getExists',
'getRawMany',
'getRawOne',
'getRawAndEntities',
'stream',
]);
/**
* @description
* The TransactionalConnection is a wrapper around the TypeORM `Connection` object which works in conjunction
* with the {@link Transaction} decorator to implement per-request transactions. All services which access the
* database should use this class rather than the raw TypeORM connection, to ensure that db changes can be
* easily wrapped in transactions when required.
*
* The service layer does not need to know about the scope of a transaction, as this is covered at the
* API by the use of the `Transaction` decorator.
*
* @docsCategory data-access
*/
let TransactionalConnection = class TransactionalConnection {
constructor(dataSource, transactionWrapper, configService) {
this.dataSource = dataSource;
this.transactionWrapper = transactionWrapper;
this.configService = configService;
}
/**
* @description
* The plain TypeORM Connection object. Should be used carefully as any operations
* performed with this connection will not be performed within any outer
* transactions.
*/
get rawConnection() {
return this.dataSource;
}
/**
* @description
* Returns a TypeORM repository. Depending on the parameters passed, it will either be transaction-aware
* or not. If `RequestContext` is provided, the repository is bound to any ongoing transactions. The
* `options` parameter allows further customization, such as selecting the replication mode (e.g., 'master').
*
* @param ctxOrTarget - Either the RequestContext, which binds the repository to ongoing transactions, or the entity type/schema.
* @param maybeTarget - The entity type or schema for which the repository is returned (if `ctxOrTarget` is a RequestContext).
* @param options - Additional options for configuring the repository, such as the `replicationMode`.
*
* @returns A TypeORM repository for the specified entity type.
*/
getRepository(ctxOrTarget, maybeTarget, options) {
if (ctxOrTarget instanceof request_context_1.RequestContext) {
const transactionManager = this.getTransactionManager(ctxOrTarget);
let repo;
if (transactionManager) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
repo = transactionManager.getRepository(maybeTarget);
}
else if (ctxOrTarget.replicationMode === 'master' || (options === null || options === void 0 ? void 0 : options.replicationMode) === 'master') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
repo = this.dataSource.createQueryRunner('master').manager.getRepository(maybeTarget);
}
else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
repo = this.rawConnection.getRepository(maybeTarget);
}
const strategy = this.configService.authOptions.entityAccessControlStrategy;
if ((strategy === null || strategy === void 0 ? void 0 : strategy.applyAccessControl) && maybeTarget && typeof maybeTarget === 'function') {
return this.wrapWithAccessControl(repo, maybeTarget, ctxOrTarget, strategy);
}
return repo;
}
else {
if ((options === null || options === void 0 ? void 0 : options.replicationMode) === 'master') {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
return this.dataSource
.createQueryRunner(options.replicationMode)
.manager.getRepository(maybeTarget);
/* eslint-enable @typescript-eslint/no-non-null-assertion */
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.rawConnection.getRepository(ctxOrTarget !== null && ctxOrTarget !== void 0 ? ctxOrTarget : maybeTarget);
}
}
async withTransaction(ctxOrWork, maybeWork) {
let ctx;
let work;
if (ctxOrWork instanceof request_context_1.RequestContext) {
ctx = ctxOrWork;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
work = maybeWork;
}
else {
ctx = request_context_1.RequestContext.empty();
work = ctxOrWork;
}
return this.transactionWrapper.executeInTransaction(ctx, work, 'auto', undefined, this.rawConnection);
}
/**
* @description
* Manually start a transaction if one is not already in progress. This method should be used in
* conjunction with the `'manual'` mode of the {@link Transaction} decorator.
*/
async startTransaction(ctx, isolationLevel) {
var _a;
const transactionManager = this.getTransactionManager(ctx);
if (((_a = transactionManager === null || transactionManager === void 0 ? void 0 : transactionManager.queryRunner) === null || _a === void 0 ? void 0 : _a.isTransactionActive) === false) {
await transactionManager.queryRunner.startTransaction(isolationLevel);
}
}
/**
* @description
* Manually commits any open transaction. Should be very rarely needed, since the {@link Transaction} decorator
* and the internal TransactionInterceptor take care of this automatically. Use-cases include situations
* in which the worker thread needs to access changes made in the current transaction, or when using the
* Transaction decorator in manual mode.
*/
async commitOpenTransaction(ctx) {
var _a;
const transactionManager = this.getTransactionManager(ctx);
if ((_a = transactionManager === null || transactionManager === void 0 ? void 0 : transactionManager.queryRunner) === null || _a === void 0 ? void 0 : _a.isTransactionActive) {
await transactionManager.queryRunner.commitTransaction();
}
}
/**
* @description
* Manually rolls back any open transaction. Should be very rarely needed, since the {@link Transaction} decorator
* and the internal TransactionInterceptor take care of this automatically. Use-cases include when using the
* Transaction decorator in manual mode.
*/
async rollBackTransaction(ctx) {
var _a;
const transactionManager = this.getTransactionManager(ctx);
if ((_a = transactionManager === null || transactionManager === void 0 ? void 0 : transactionManager.queryRunner) === null || _a === void 0 ? void 0 : _a.isTransactionActive) {
await transactionManager.queryRunner.rollbackTransaction();
}
}
/**
* @description
* Finds an entity of the given type by ID, or throws an `EntityNotFoundError` if none
* is found.
*/
async getEntityOrThrow(ctx, entityType, id, options = {}) {
const { retries, retryDelay } = options;
if (retries == null || retries <= 0) {
return this.getEntityOrThrowInternal(ctx, entityType, id, options);
}
else {
let err;
const retriesInt = Math.ceil(retries);
const delay = Math.ceil(Math.max(retryDelay || 25, 1));
for (let attempt = 0; attempt < retriesInt; attempt++) {
try {
const result = await this.getEntityOrThrowInternal(ctx, entityType, id, options);
return result;
}
catch (e) {
err = e;
if (attempt < retriesInt - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw err;
}
}
async getEntityOrThrowInternal(ctx, entityType, id, options = {}) {
let entity;
if (options.channelId != null) {
const { channelId } = options, optionsWithoutChannelId = __rest(options, ["channelId"]);
entity = await this.findOneInChannel(ctx, entityType, id, options.channelId, optionsWithoutChannelId);
}
else {
const optionsWithId = Object.assign(Object.assign({}, options), { where: Object.assign(Object.assign({}, (options.where || {})), { id }) });
entity = await this.getRepository(ctx, entityType)
.findOne(optionsWithId)
.then(result => result !== null && result !== void 0 ? result : undefined);
}
if (!entity ||
(entity.hasOwnProperty('deletedAt') &&
entity.deletedAt !== null &&
options.includeSoftDeleted !== true)) {
throw new errors_1.EntityNotFoundError(entityType.name, id);
}
return entity;
}
/**
* @description
* Like the TypeOrm `Repository.findOne()` method, but limits the results to
* the given Channel.
*/
findOneInChannel(ctx, entity, id, channelId, options = {}) {
const qb = this.getRepository(ctx, entity).createQueryBuilder('entity');
if (options.relations) {
const joinedRelations = (0, tree_relations_qb_joiner_1.joinTreeRelationsDynamically)(qb, entity, options.relations);
// Remove any relations which are related to the 'collection' tree, as these are handled separately
// to avoid duplicate joins.
options.relations = (0, find_options_object_to_array_1.findOptionsObjectToArray)(options.relations).filter(relationPath => !joinedRelations.has(relationPath));
}
qb.setFindOptions(Object.assign({ relationLoadStrategy: 'query' }, options));
qb.leftJoin('entity.channels', '__channel')
.andWhere('entity.id = :id', { id })
.andWhere('__channel.id = :channelId', { channelId });
return qb.getOne().then(result => {
return result !== null && result !== void 0 ? result : undefined;
});
}
/**
* @description
* Like the TypeOrm `Repository.findByIds()` method, but limits the results to
* the given Channel.
*/
findByIdsInChannel(ctx, entity, ids, channelId, options) {
// the syntax described in https://github.com/typeorm/typeorm/issues/1239#issuecomment-366955628
// breaks if the array is empty
if (ids.length === 0) {
return Promise.resolve([]);
}
const qb = this.getRepository(ctx, entity).createQueryBuilder('entity');
if (Array.isArray(options.relations) && options.relations.length > 0) {
const joinedRelations = (0, tree_relations_qb_joiner_1.joinTreeRelationsDynamically)(qb, entity, options.relations);
// Remove any relations which are related to the 'collection' tree, as these are handled separately
// to avoid duplicate joins.
options.relations = options.relations.filter(relationPath => !joinedRelations.has(relationPath));
}
qb.setFindOptions(Object.assign({ relationLoadStrategy: 'query' }, options));
qb.leftJoin('entity.channels', 'channel')
.andWhere('entity.id IN (:...ids)', { ids })
.andWhere('channel.id = :channelId', { channelId });
return qb.getMany();
}
wrapWithAccessControl(repo, target, ctx, strategy) {
return new Proxy(repo, {
get(obj, prop, receiver) {
if (prop === 'createQueryBuilder') {
return (...args) => {
const qb = obj.createQueryBuilder(...args);
return wrapQueryBuilder(qb, target, ctx, strategy);
};
}
if (typeof prop === 'string' && ACCESS_CONTROL_INTERCEPTED_METHODS.has(prop)) {
return (options = {}) => {
var _a;
const alias = obj.metadata.name;
const qb = obj.createQueryBuilder(alias);
qb.setFindOptions(options);
(_a = strategy.applyAccessControl) === null || _a === void 0 ? void 0 : _a.call(strategy, qb, target, ctx);
switch (prop) {
case 'find':
return qb.getMany();
case 'findOne': {
if (!options.where) {
return Promise.resolve(null);
}
return qb.getOne();
}
case 'findOneOrFail': {
return qb.getOneOrFail();
}
case 'findAndCount':
return qb.getManyAndCount();
case 'count':
return qb.getCount();
default:
return qb.getMany();
}
};
}
return Reflect.get(obj, prop, receiver);
},
});
}
getTransactionManager(ctx) {
return ctx[constants_1.TRANSACTION_MANAGER_KEY];
}
};
exports.TransactionalConnection = TransactionalConnection;
exports.TransactionalConnection = TransactionalConnection = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, typeorm_decorators_1.InjectDataSource)()),
__metadata("design:paramtypes", [typeorm_1.DataSource,
transaction_wrapper_1.TransactionWrapper,
config_service_1.ConfigService])
], TransactionalConnection);
/**
* Wraps a SelectQueryBuilder so that `applyAccessControl` is called
* before any terminal method (getMany, getOne, etc.) executes.
*
* Uses an `applied` flag to ensure ACL is applied exactly once,
* even if terminal methods call each other internally (e.g.
* getOneOrFail → getOne → getRawAndEntities).
*/
function wrapQueryBuilder(qb, entityType, ctx, strategy) {
let applied = false;
return new Proxy(qb, {
get(obj, prop, receiver) {
if (typeof prop === 'string' && QB_TERMINAL_METHODS.has(prop)) {
return (...args) => {
var _a;
if (!applied) {
(_a = strategy.applyAccessControl) === null || _a === void 0 ? void 0 : _a.call(strategy, obj, entityType, ctx);
applied = true;
}
return obj[prop](...args);
};
}
return Reflect.get(obj, prop, receiver);
},
});
}
//# sourceMappingURL=transactional-connection.js.map