@vendure/core
Version:
A modern, headless ecommerce framework
317 lines • 14.9 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);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EntityHydrator = void 0;
const common_1 = require("@nestjs/common");
const unique_1 = require("@vendure/common/lib/unique");
const errors_1 = require("../../../common/error/errors");
const transactional_connection_1 = require("../../../connection/transactional-connection");
const product_variant_entity_1 = require("../../../entity/product-variant/product-variant.entity");
const product_price_applicator_1 = require("../product-price-applicator/product-price-applicator");
const translator_service_1 = require("../translator/translator.service");
const tree_relations_qb_joiner_1 = require("../utils/tree-relations-qb-joiner");
const merge_deep_1 = require("./merge-deep");
/**
* @description
* This is a helper class which is used to "hydrate" entity instances, which means to populate them
* with the specified relations. This is useful when writing plugin code which receives an entity,
* and you need to ensure that one or more relations are present.
*
* @example
* ```ts
* import { Injectable } from '\@nestjs/common';
* import { ID, RequestContext, EntityHydrator, ProductVariantService } from '\@vendure/core';
*
* \@Injectable()
* export class MyService {
*
* constructor(
* private entityHydrator: EntityHydrator, // [!code highlight]
* private productVariantService: ProductVariantService,
* ) {}
*
* myMethod(ctx: RequestContext, variantId: ID) {
* const product = await this.productVariantService
* .getProductForVariant(ctx, variantId);
*
* // at this stage, we don't know which of the Product relations
* // will be joined at runtime.
*
* await this.entityHydrator // [!code highlight]
* .hydrate(ctx, product, { relations: ['facetValues.facet' ]}); // [!code highlight]
*
* // You can be sure now that the `facetValues` & `facetValues.facet` relations are populated // [!code highlight]
* }
* }
*```
*
* In this above example, the `product` instance will now have the `facetValues` relation
* available, and those FacetValues will have their `facet` relations joined too.
*
* This `hydrate` method will _also_ automatically take care or translating any
* translatable entities (e.g. Product, Collection, Facet), and if the `applyProductVariantPrices`
* options is used (see {@link HydrateOptions}), any related ProductVariant will have the correct
* Channel-specific prices applied to them.
*
* Custom field relations may also be hydrated:
*
* @example
* ```ts
* const customer = await this.customerService
* .findOne(ctx, id);
*
* await this.entityHydrator
* .hydrate(ctx, customer, { relations: ['customFields.avatar' ]});
* ```
*
* @docsCategory data-access
* @since 1.3.0
*/
let EntityHydrator = class EntityHydrator {
constructor(connection, productPriceApplicator, translator) {
this.connection = connection;
this.productPriceApplicator = productPriceApplicator;
this.translator = translator;
}
/**
* @description
* Hydrates (joins) the specified relations to the target entity instance. This method
* mutates the `target` entity.
*
* @example
* ```ts
* await this.entityHydrator.hydrate(ctx, product, {
* relations: [
* 'variants.stockMovements'
* 'optionGroups.options',
* 'featuredAsset',
* ],
* applyProductVariantPrices: true,
* });
* ```
*
* @since 1.3.0
*/
async hydrate(ctx, target, options) {
if (options.relations) {
let missingRelations = this.getMissingRelations(target, options);
if (options.applyProductVariantPrices === true) {
const productVariantPriceRelations = this.getRequiredProductVariantRelations(target, missingRelations);
missingRelations = (0, unique_1.unique)([...missingRelations, ...productVariantPriceRelations]);
}
// Add .translations relations for translatable entities
// Note: For nested relations through arrays (like assets.asset), we rely on eager loading
// on the Asset.translations relation since explicitly loading deeply nested relations
// can cause issues with TypeORM's relation loading
const translationRelations = this.getTranslationRelationsForTranslatableEntities(target, missingRelations);
missingRelations = (0, unique_1.unique)([...missingRelations, ...translationRelations]);
if (missingRelations.length) {
const hydratedQb = this.connection
.getRepository(ctx, target.constructor)
.createQueryBuilder(target.constructor.name);
const joinedRelations = (0, tree_relations_qb_joiner_1.joinTreeRelationsDynamically)(hydratedQb, target.constructor, missingRelations);
hydratedQb.setFindOptions({
relationLoadStrategy: 'query',
where: { id: target.id },
relations: missingRelations.filter(relationPath => !joinedRelations.has(relationPath)),
});
const hydrated = await hydratedQb.getOne();
const propertiesToAdd = (0, unique_1.unique)(missingRelations.map(relation => relation.split('.')[0]));
for (const prop of propertiesToAdd) {
target[prop] = (0, merge_deep_1.mergeDeep)(target[prop], hydrated[prop]);
}
const relationsWithEntities = missingRelations.map(relation => ({
entity: this.getRelationEntityAtPath(target, relation.split('.')),
relation,
}));
if (options.applyProductVariantPrices === true) {
for (const relationWithEntities of relationsWithEntities) {
const entity = relationWithEntities.entity;
if (entity) {
if (Array.isArray(entity)) {
if (entity[0] instanceof product_variant_entity_1.ProductVariant) {
await Promise.all(entity.map((e) => this.productPriceApplicator.applyChannelPriceAndTax(e, ctx)));
}
}
else {
if (entity instanceof product_variant_entity_1.ProductVariant) {
await this.productPriceApplicator.applyChannelPriceAndTax(entity, ctx);
}
}
}
}
}
const translateDeepRelations = relationsWithEntities
.filter(item => this.isTranslatable(item.entity))
.map(item => item.relation.split('.'));
this.assignSettableProperties(target, this.translator.translate(target, ctx, translateDeepRelations));
}
}
return target;
}
assignSettableProperties(target, source) {
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(target))) {
if (typeof descriptor.get === 'function' && typeof descriptor.set !== 'function') {
// If the entity property has a getter only, we will skip it otherwise
// we will get an error of the form:
// `Cannot set property <name> of #<Entity> which has only a getter`
continue;
}
target[key] = source[key];
}
return target;
}
/**
* Compares the requested relations against the actual existing relations on the target entity,
* and returns an array of all missing relation paths that would need to be fetched.
*/
getMissingRelations(target, options) {
const missingRelations = [];
for (const relation of options.relations.slice().sort()) {
if (typeof relation === 'string') {
const parts = relation.split('.');
let entity = target;
const path = [];
for (const part of parts) {
path.push(part);
// null = the relation has been fetched but was null in the database.
// undefined = the relation has not been fetched.
if (entity && entity[part] === null) {
break;
}
if (entity && entity[part]) {
entity = Array.isArray(entity[part]) ? entity[part][0] : entity[part];
}
else {
const allParts = path.reduce((result, p, i) => {
if (i === 0) {
return [p];
}
else {
return [...result, [result[result.length - 1], p].join('.')];
}
}, []);
missingRelations.push(...allParts);
entity = undefined;
}
}
}
}
return (0, unique_1.unique)(missingRelations.filter(relation => !relation.endsWith('.customFields')));
}
getRequiredProductVariantRelations(target, missingRelations) {
const relationsToAdd = [];
for (const relation of missingRelations) {
const entityType = this.getRelationEntityTypeAtPath(target, relation);
if (entityType === product_variant_entity_1.ProductVariant) {
relationsToAdd.push([relation, 'taxCategory'].join('.'));
relationsToAdd.push([relation, 'productVariantPrices'].join('.'));
}
}
return relationsToAdd;
}
/**
* Returns an instance of the related entity at the given path. E.g. a path of `['variants', 'featuredAsset']`
* will return an Asset instance.
*/
getRelationEntityAtPath(entity, path) {
let isArrayResult = false;
const result = [];
function visit(parent, parts) {
if (parts.length === 0) {
return;
}
const part = parts.shift();
const target = parent[part];
if (Array.isArray(target)) {
isArrayResult = true;
if (parts.length === 0) {
result.push(...target);
}
else {
for (const item of target) {
visit(item, parts.slice());
}
}
}
else if (target == null) {
result.push(target);
}
else {
if (parts.length === 0) {
result.push(target);
}
else {
visit(target, parts.slice());
}
}
}
visit(entity, path.slice());
return isArrayResult ? result : result[0];
}
getRelationEntityTypeAtPath(entity, path) {
const { entityMetadatas } = this.connection.rawConnection;
const targetMetadata = entityMetadatas.find(m => m.target === entity.constructor);
if (!targetMetadata) {
throw new errors_1.InternalServerError(`Cannot find entity metadata for entity "${entity.constructor.name}"`);
}
let currentMetadata = targetMetadata;
for (const pathPart of path.split('.')) {
const relationMetadata = currentMetadata.findRelationWithPropertyPath(pathPart);
if (relationMetadata) {
currentMetadata = relationMetadata.inverseEntityMetadata;
}
else {
throw new errors_1.InternalServerError(`Cannot find relation metadata for entity "${currentMetadata.targetName}" at path "${pathPart}"`);
}
}
return currentMetadata.target;
}
/**
* Returns additional .translations relations for any translatable entities in the relations list.
* This ensures that translatable nested relations have their translations loaded.
*/
getTranslationRelationsForTranslatableEntities(target, missingRelations) {
const translationRelations = [];
for (const relation of missingRelations) {
try {
const entityType = this.getRelationEntityTypeAtPath(target, relation);
// Check if the entity type has a translations property in its metadata
const { entityMetadatas } = this.connection.rawConnection;
const entityMetadata = entityMetadatas.find(m => m.target === entityType);
if (entityMetadata) {
const translationsRelation = entityMetadata.findRelationWithPropertyPath('translations');
if (translationsRelation) {
translationRelations.push(`${relation}.translations`);
}
}
}
catch (_a) {
// If we can't find the entity type, skip this relation
}
}
return translationRelations;
}
isTranslatable(input) {
var _a, _b, _c;
return Array.isArray(input)
? ((_b = (_a = input[0]) === null || _a === void 0 ? void 0 : _a.hasOwnProperty('translations')) !== null && _b !== void 0 ? _b : false)
: ((_c = input === null || input === void 0 ? void 0 : input.hasOwnProperty('translations')) !== null && _c !== void 0 ? _c : false);
}
};
exports.EntityHydrator = EntityHydrator;
exports.EntityHydrator = EntityHydrator = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [transactional_connection_1.TransactionalConnection,
product_price_applicator_1.ProductPriceApplicator,
translator_service_1.TranslatorService])
], EntityHydrator);
//# sourceMappingURL=entity-hydrator.service.js.map