Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve usability of internal Vendure services #1103

Closed
skid opened this issue Sep 20, 2021 · 4 comments
Closed

Improve usability of internal Vendure services #1103

skid opened this issue Sep 20, 2021 · 4 comments

Comments

@skid
Copy link
Contributor

skid commented Sep 20, 2021

It's kind of difficult to write services that operate on products / product variants or other internal entities. (A) You can't use the GraphQL API from within the same process (or it's not recommended). (B) The internal services are not documented and somewhat inconsistent and (C) it's really cumbersome to use TypeORM directly. I will give examples for B and C:

  • B. There is a method on the ProductVariantService called getProductForVariant - it does what it says, except it doesn't load the facetValues, AND, there is no way to tell it to do so. This is the case with many internal service methods.
  • C. I can do the same thing with TypeORM relatively easily, only I need to take care of translations, and all the other boilerplate for which the original services exist in the first place.

My biggest problem, as I said, is that I can't explicitly tell the services to eagerly fetch everything. One solution would be to implement a kind of "hydration" function that will eagerly load all relations for a given VendureEntity. For example:

import { magicHydrate } from '@vendure/core';

const product = await this.productService.findOne(ctx, someProductID)

// product.facetValues is empty

await magicHydrate(product, { relations: ['facetValues', 'facetValues.facet'] });

// product.facetValues is not empty now
@heiko-r
Copy link
Contributor

heiko-r commented Sep 20, 2021

This is maybe my most major grievance with Vendure, since I make heavy use of the core services in plugins. Instead of the magicHydrate function, ideally I would like to see a 'relations' option added to every service function that returns an entity, and to return only the absolute minimum when no relations are specified. That would require heavy refactoring though...

@michaelbromley
Copy link
Member

@heiko-r thanks for the feedback.

ideally I would like to see a 'relations' option added to every service function that returns an entity

This would have the advantage of doing all joins in a single query, rather than a second query for the magicHydrate.

Arguments against this approach:

  • would introduce a new argument to almost every service method
  • we might then run into the need to pass down "relations" objects from one service call to another.
  • for any method such as this one which uses the QueryBuilder API rather than the .find() API, implementing arbitrary deep joins might not be possible.
  • does not solve the case of EventBus subscriptions, so we would still need a "magicHydrate" for that case.

I think it is still worth thinking about a solution like this, but for now I would tend towards a "magicHydrate" type of approach which should be possible to implement in the next minor version with zero breaking changes.

@michaelbromley
Copy link
Member

This was a fun challenge to implement! Arrived at what I think is a nice API to work with - basically as covered above but with th following features:

  • Type-safe deep relation paths, so e.g. when hydrating a Product instance, the IDE & TypeScript will know that the string 'facetValues.facet' is valid, and can auto-suggest valid paths.
  • Automatic translation of translatable entities, no need to manuall call translateDeep()
  • Automatic application of ProductVariant prices for the current Channel.

More details here:

/**
* @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
* ```TypeScript
* const product = this.productVariantService.getProductForVariant(ctx, variantId);
* await this.entityHydrator.hydrate(ctx, product, { relations: ['facetValues.facet' ]});
*```
*
* 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.
*
* @since 1.3.0
*/
@Injectable()

@michaelbromley
Copy link
Member

Still to do for next minor: Include Services & Helpers in vendure.io docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants