Skip to content

Commit

Permalink
fix(core): Resolve all LocaleString fields in GraphQL API
Browse files Browse the repository at this point in the history
Relates to #763
  • Loading branch information
michaelbromley committed Mar 16, 2021
1 parent 179679f commit 3ddadc0
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 7 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/api/api-internal-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ZoneResolver } from './resolvers/admin/zone.resolver';
import { AdministratorEntityResolver } from './resolvers/entity/administrator-entity.resolver';
import { AssetEntityResolver } from './resolvers/entity/asset-entity.resolver';
import { CollectionEntityResolver } from './resolvers/entity/collection-entity.resolver';
import { CountryEntityResolver } from './resolvers/entity/country-entity.resolver';
import {
CustomerAdminEntityResolver,
CustomerEntityResolver,
Expand Down Expand Up @@ -112,6 +113,7 @@ const shopResolvers = [

export const entityResolvers = [
CollectionEntityResolver,
CountryEntityResolver,
CustomerEntityResolver,
CustomerGroupEntityResolver,
FacetEntityResolver,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PaginatedList } from '@vendure/common/lib/shared-types';
import { ListQueryOptions } from '../../../common/types/common-types';
import { Translated } from '../../../common/types/locale-types';
import { Asset, Collection, Product, ProductVariant } from '../../../entity';
import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
import { AssetService } from '../../../service/services/asset.service';
import { CollectionService } from '../../../service/services/collection.service';
import { ProductVariantService } from '../../../service/services/product-variant.service';
Expand All @@ -19,8 +20,24 @@ export class CollectionEntityResolver {
private productVariantService: ProductVariantService,
private collectionService: CollectionService,
private assetService: AssetService,
private localeStringHydrator: LocaleStringHydrator,
) {}

@ResolveField()
name(@Ctx() ctx: RequestContext, @Parent() collection: Collection): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, collection, 'name');
}

@ResolveField()
slug(@Ctx() ctx: RequestContext, @Parent() collection: Collection): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, collection, 'slug');
}

@ResolveField()
description(@Ctx() ctx: RequestContext, @Parent() collection: Collection): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, collection, 'description');
}

@ResolveField()
async productVariants(
@Ctx() ctx: RequestContext,
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/api/resolvers/entity/country-entity.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';

import { Country } from '../../../entity/country/country.entity';
import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
import { RequestContext } from '../../common/request-context';
import { Ctx } from '../../decorators/request-context.decorator';

@Resolver('Country')
export class CountryEntityResolver {
constructor(private localeStringHydrator: LocaleStringHydrator) {}

@ResolveField()
name(@Ctx() ctx: RequestContext, @Parent() country: Country): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, country, 'name');
}
}
11 changes: 10 additions & 1 deletion packages/core/src/api/resolvers/entity/facet-entity.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql';

import { FacetValue } from '../../../entity/facet-value/facet-value.entity';
import { Facet } from '../../../entity/facet/facet.entity';
import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
import { FacetValueService } from '../../../service/services/facet-value.service';
import { RequestContext } from '../../common/request-context';
import { Ctx } from '../../decorators/request-context.decorator';

@Resolver('Facet')
export class FacetEntityResolver {
constructor(private facetValueService: FacetValueService) {}
constructor(
private facetValueService: FacetValueService,
private localeStringHydrator: LocaleStringHydrator,
) {}

@ResolveField()
name(@Ctx() ctx: RequestContext, @Parent() facetValue: FacetValue): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, facetValue, 'name');
}

@ResolveField()
async values(@Ctx() ctx: RequestContext, @Parent() facet: Facet): Promise<FacetValue[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql';

import { FacetValue } from '../../../entity/facet-value/facet-value.entity';
import { Facet } from '../../../entity/facet/facet.entity';
import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
import { FacetService } from '../../../service/services/facet.service';
import { RequestContext } from '../../common/request-context';
import { Ctx } from '../../decorators/request-context.decorator';

@Resolver('FacetValue')
export class FacetValueEntityResolver {
constructor(private facetService: FacetService) {}
constructor(private facetService: FacetService, private localeStringHydrator: LocaleStringHydrator) {}

@ResolveField()
name(@Ctx() ctx: RequestContext, @Parent() facetValue: FacetValue): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, facetValue, 'name');
}

@ResolveField()
async facet(@Ctx() ctx: RequestContext, @Parent() facetValue: FacetValue): Promise<Facet | undefined> {
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/api/resolvers/entity/product-entity.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FacetValue } from '../../../entity/facet-value/facet-value.entity';
import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
import { Product } from '../../../entity/product/product.entity';
import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
import { AssetService } from '../../../service/services/asset.service';
import { CollectionService } from '../../../service/services/collection.service';
import { ProductOptionGroupService } from '../../../service/services/product-option-group.service';
Expand All @@ -28,8 +29,24 @@ export class ProductEntityResolver {
private productOptionGroupService: ProductOptionGroupService,
private assetService: AssetService,
private productService: ProductService,
private localeStringHydrator: LocaleStringHydrator,
) {}

@ResolveField()
name(@Ctx() ctx: RequestContext, @Parent() product: Product): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, product, 'name');
}

@ResolveField()
slug(@Ctx() ctx: RequestContext, @Parent() product: Product): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, product, 'slug');
}

@ResolveField()
description(@Ctx() ctx: RequestContext, @Parent() product: Product): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, product, 'description');
}

@ResolveField()
async variants(
@Ctx() ctx: RequestContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@ import { Translated } from '../../../common/types/locale-types';
import { assertFound } from '../../../common/utils';
import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
import { ProductOption } from '../../../entity/product-option/product-option.entity';
import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
import { ProductOptionGroupService } from '../../../service/services/product-option-group.service';
import { RequestContext } from '../../common/request-context';
import { Allow } from '../../decorators/allow.decorator';
import { Ctx } from '../../decorators/request-context.decorator';

@Resolver('ProductOption')
export class ProductOptionEntityResolver {
constructor(private productOptionGroupService: ProductOptionGroupService) {}
constructor(
private productOptionGroupService: ProductOptionGroupService,
private localeStringHydrator: LocaleStringHydrator,
) {}

@ResolveField()
name(@Ctx() ctx: RequestContext, @Parent() productOption: ProductOption): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, productOption, 'name');
}

@ResolveField()
@Allow(Permission.ReadCatalog, Permission.Public)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@ import { Permission } from '@vendure/common/lib/generated-types';
import { Translated } from '../../../common/types/locale-types';
import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
import { ProductOption } from '../../../entity/product-option/product-option.entity';
import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
import { ProductOptionGroupService } from '../../../service/services/product-option-group.service';
import { RequestContext } from '../../common/request-context';
import { Allow } from '../../decorators/allow.decorator';
import { Ctx } from '../../decorators/request-context.decorator';

@Resolver('ProductOptionGroup')
export class ProductOptionGroupEntityResolver {
constructor(private productOptionGroupService: ProductOptionGroupService) {}
constructor(
private productOptionGroupService: ProductOptionGroupService,
private localeStringHydrator: LocaleStringHydrator,
) {}

@ResolveField()
name(@Ctx() ctx: RequestContext, @Parent() optionGroup: ProductOptionGroup): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, optionGroup, 'name');
}

@ResolveField()
@Allow(Permission.ReadCatalog, Permission.Public)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { idsAreEqual } from '../../../common/utils';
import { Asset, Channel, FacetValue, Product, ProductOption } from '../../../entity';
import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
import { StockMovement } from '../../../entity/stock-movement/stock-movement.entity';
import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
import { AssetService } from '../../../service/services/asset.service';
import { ProductVariantService } from '../../../service/services/product-variant.service';
import { StockMovementService } from '../../../service/services/stock-movement.service';
Expand All @@ -18,7 +19,16 @@ import { Ctx } from '../../decorators/request-context.decorator';

@Resolver('ProductVariant')
export class ProductVariantEntityResolver {
constructor(private productVariantService: ProductVariantService, private assetService: AssetService) {}
constructor(
private productVariantService: ProductVariantService,
private assetService: AssetService,
private localeStringHydrator: LocaleStringHydrator,
) {}

@ResolveField()
async name(@Ctx() ctx: RequestContext, @Parent() productVariant: ProductVariant): Promise<string> {
return this.localeStringHydrator.hydrateLocaleStringField(ctx, productVariant, 'name');
}

@ResolveField()
async product(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Injectable } from '@nestjs/common';

import { RequestContext } from '../../../api/common/request-context';
import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
import { Translatable, TranslatableKeys, Translated } from '../../../common/types/locale-types';
import { VendureEntity } from '../../../entity/base/base.entity';
import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
import { TransactionalConnection } from '../../transaction/transactional-connection';
import { translateDeep } from '../utils/translate-entity';

/**
* This helper class is to be used in GraphQL entity resolvers, to resolve fields which depend on being
* translated (i.e. the corresponding entity field is of type `LocaleString`).
*/
@Injectable()
export class LocaleStringHydrator {
constructor(
private connection: TransactionalConnection,
private requestCache: RequestContextCacheService,
) {}

async hydrateLocaleStringField<T extends VendureEntity & Translatable>(
ctx: RequestContext,
entity: T,
fieldName: TranslatableKeys<T>,
): Promise<string> {
if (entity[fieldName]) {
// Already hydrated, so return the value
return entity[fieldName] as any;
}
await this.hydrateLocaleStrings(ctx, entity);
return entity[fieldName] as any;
}

/**
* Takes a translatable entity and populates all the LocaleString fields
* by fetching the translations from the database (they will be eagerly loaded).
*
* This method includes a caching optimization to prevent multiple DB calls when many
* translatable fields are needed on the same entity in a resolver.
*/
private async hydrateLocaleStrings<T extends VendureEntity & Translatable>(
ctx: RequestContext,
entity: T,
): Promise<Translated<T>> {
const entityType = entity.constructor.name;
if (!entity.translations?.length) {
const cacheKey = `hydrate-${entityType}-${entity.id}`;
let dbCallPromise = this.requestCache.get<Promise<T | undefined>>(ctx, cacheKey);

if (!dbCallPromise) {
dbCallPromise = this.connection.getRepository<T>(ctx, entityType).findOne(entity.id);
this.requestCache.set(ctx, cacheKey, dbCallPromise);
}

await dbCallPromise.then(withTranslations => {
// tslint:disable-next-line:no-non-null-assertion
entity.translations = withTranslations!.translations;
});
}
if (entity.translations.length) {
const translated = translateDeep(entity, ctx.languageCode);
for (const localeStringProp of Object.keys(entity.translations[0])) {
if (localeStringProp === 'base' || localeStringProp === 'languageCode') {
continue;
}
if (localeStringProp === 'customFields') {
(entity as any)[localeStringProp] = Object.assign(
(entity as any)[localeStringProp],
(translated as any)[localeStringProp],
);
} else {
(entity as any)[localeStringProp] = (translated as any)[localeStringProp];
}
}
}
return entity as Translated<T>;
}
}
1 change: 1 addition & 0 deletions packages/core/src/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './helpers/utils/patch-entity';
export * from './helpers/utils/channel-aware-orm-utils';
export * from './helpers/utils/get-entity-or-throw';
export * from './helpers/list-query-builder/list-query-builder';
export * from './helpers/locale-string-hydrator/locale-string-hydrator';
export * from './helpers/external-authentication/external-authentication.service';
export * from './helpers/order-calculator/order-calculator';
export * from './helpers/order-merger/order-merger';
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/service/service.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CustomFieldRelationService } from './helpers/custom-field-relation/cust
import { ExternalAuthenticationService } from './helpers/external-authentication/external-authentication.service';
import { FulfillmentStateMachine } from './helpers/fulfillment-state-machine/fulfillment-state-machine';
import { ListQueryBuilder } from './helpers/list-query-builder/list-query-builder';
import { LocaleStringHydrator } from './helpers/locale-string-hydrator/locale-string-hydrator';
import { OrderCalculator } from './helpers/order-calculator/order-calculator';
import { OrderMerger } from './helpers/order-merger/order-merger';
import { OrderModifier } from './helpers/order-modifier/order-modifier';
Expand Down Expand Up @@ -113,10 +114,10 @@ const helpers = [
ExternalAuthenticationService,
TransactionalConnection,
CustomFieldRelationService,
LocaleStringHydrator,
];

let defaultTypeOrmModule: DynamicModule;
let workerTypeOrmModule: DynamicModule;

/**
* The ServiceCoreModule is imported internally by the ServiceModule. It is arranged in this way so that
Expand Down

0 comments on commit 3ddadc0

Please sign in to comment.