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

chore: Abstract module service #6188

Merged
merged 59 commits into from
Feb 2, 2024
Merged

chore: Abstract module service #6188

merged 59 commits into from
Feb 2, 2024

Conversation

adrien2p
Copy link
Member

@adrien2p adrien2p commented Jan 23, 2024

What

  • Remove services that do not have any custom business and replace them with a simple interfaces
  • Abstract module service provide the following base implementation
    • retrieve
    • list
    • listAndCount
    • delete
    • softDelete
    • restore

The above methods are created for the main model and also for each other models for which a config is provided

all method such as list, listAndCount, delete, softDelete and restore are pluralized with the model it refers to

Migration

  • product
  • pricing
  • promotion
  • cart
  • auth
  • customer
  • payment
  • Sales channel
  • Workflow-*

Usage

Module

The module service can now extend the ModulesSdkUtils.abstractModuleServiceFactory which returns a class with the default implementation for each method and each model following the standard naming convention mentioned above.
This factory have 3 template arguments being the container, the main model DTO and an object representing the other model with a config object that contains at list the DTO and optionally a singular and plural property in case it needs to be set manually. It looks like the following:

export default class PricingModuleService</* ... */>
  extends ModulesSdkUtils.abstractModuleServiceFactory<
    InjectedDependencies,
    PricingTypes.PriceSetDTO,
    {
      Currency: { dto: PricingTypes.CurrencyDTO }
      MoneyAmount: { dto: PricingTypes.MoneyAmountDTO }
      PriceSetMoneyAmount: { dto: PricingTypes.PriceSetMoneyAmountDTO }
      PriceSetMoneyAmountRules: {
        dto: PricingTypes.PriceSetMoneyAmountRulesDTO
      }
      PriceRule: { dto: PricingTypes.PriceRuleDTO }
      RuleType: { dto: PricingTypes.RuleTypeDTO }
      PriceList: { dto: PricingTypes.PriceListDTO }
      PriceListRule: { dto: PricingTypes.PriceListRuleDTO }
    }
  >(PriceSet, generateMethodForModels, entityNameToLinkableKeysMap)
  implements PricingTypes.IPricingModuleService
{
// ...
}

In the above, the singular and plural can be inferred as there is no tricky naming. Also, the default implementation does not remove the fact that you need to provides all the overloads etc in your module service interface. The above will provide a default implementation following the interface AbstractModuleService which is also auto generated, hence you will have the following methods available:

for the main model

  • list
  • retrieve
  • listAndCount
  • delete
  • softDelete
  • restore

for the other models

  • listMyModels
  • retrieveMyModel
  • listAndCountMyModels
  • deleteMyModels
  • softDeleteMyModels
  • restoreMyModels

Internal module service

The internal module service can now extend ModulesSdkUtils.internalModuleServiceFactory which takes only one template argument which is the container type.
All internal services provides a default implementation for all retrieve, list, listAndCount, create, update, delete, softDelete, restore methods which follow the following interface ModulesSdkTypes.InternalModuleService:

export interface InternalModuleService<
  TEntity extends {},
  TContainer extends object = object
> {
  get __container__(): TContainer

  retrieve(
    idOrObject: string,
    config?: FindConfig<any>,
    sharedContext?: Context
  ): Promise<TEntity>
  retrieve(
    idOrObject: object,
    config?: FindConfig<any>,
    sharedContext?: Context
  ): Promise<TEntity>

  list(
    filters?: FilterQuery<any> | BaseFilterable<FilterQuery<any>>,
    config?: FindConfig<any>,
    sharedContext?: Context
  ): Promise<TEntity[]>

  listAndCount(
    filters?: FilterQuery<any> | BaseFilterable<FilterQuery<any>>,
    config?: FindConfig<any>,
    sharedContext?: Context
  ): Promise<[TEntity[], number]>

  create(data: any[], sharedContext?: Context): Promise<TEntity[]>
  create(data: any, sharedContext?: Context): Promise<TEntity>

  update(data: any[], sharedContext?: Context): Promise<TEntity[]>
  update(data: any, sharedContext?: Context): Promise<TEntity>
  update(
    selectorAndData: {
      selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
      data: any
    },
    sharedContext?: Context
  ): Promise<TEntity[]>
  update(
    selectorAndData: {
      selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
      data: any
    }[],
    sharedContext?: Context
  ): Promise<TEntity[]>

  delete(idOrSelector: string, sharedContext?: Context): Promise<void>
  delete(idOrSelector: string[], sharedContext?: Context): Promise<void>
  delete(idOrSelector: object, sharedContext?: Context): Promise<void>
  delete(idOrSelector: object[], sharedContext?: Context): Promise<void>
  delete(
    idOrSelector: {
      selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
    },
    sharedContext?: Context
  ): Promise<void>

  softDelete(
    idsOrFilter: string[] | InternalFilterQuery,
    sharedContext?: Context
  ): Promise<[TEntity[], Record<string, unknown[]>]>

  restore(
    idsOrFilter: string[] | InternalFilterQuery,
    sharedContext?: Context
  ): Promise<[TEntity[], Record<string, unknown[]>]>

  upsert(data: any[], sharedContext?: Context): Promise<TEntity[]>
  upsert(data: any, sharedContext?: Context): Promise<TEntity>
}

When a service is auto generated you can use that interface to type your class property representing the expected internal service.

Repositories

The repositories can now extend DALUtils.mikroOrmBaseRepositoryFactory which takes one template argument being the entity or the template entity and provides all the default implementation. If the repository is auto generated you can type it using the RepositoryService interface. Here is the new interface typings.

export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
  find(options?: FindOptions<T>, context?: Context): Promise<T[]>

  findAndCount(
    options?: FindOptions<T>,
    context?: Context
  ): Promise<[T[], number]>

  create(data: any[], context?: Context): Promise<T[]>

  // Becareful here, if you have a custom internal service, the update data should never be the entity otherwise
 // both entity and update will point to the same ref and create issues with mikro orm
  update(data: { entity; update }[], context?: Context): Promise<T[]>

  delete(
    idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
    context?: Context
  ): Promise<void>

  /**
   * Soft delete entities and cascade to related entities if configured.
   *
   * @param idsOrFilter
   * @param context
   *
   * @returns [T[], Record<string, string[]>] the second value being the map of the entity names and ids that were soft deleted
   */
  softDelete(
    idsOrFilter: string[] | InternalFilterQuery,
    context?: Context
  ): Promise<[T[], Record<string, unknown[]>]>

  restore(
    idsOrFilter: string[] | InternalFilterQuery,
    context?: Context
  ): Promise<[T[], Record<string, unknown[]>]>

  upsert(data: any[], context?: Context): Promise<T[]>
}

Copy link

changeset-bot bot commented Jan 23, 2024

⚠️ No Changeset found

Latest commit: 0821fd9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link

vercel bot commented Jan 23, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
medusa-dashboard ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 2, 2024 2:02pm
3 Ignored Deployments
Name Status Preview Comments Updated (UTC)
api-reference ⬜️ Ignored (Inspect) Feb 2, 2024 2:02pm
docs-ui ⬜️ Ignored (Inspect) Visit Preview Feb 2, 2024 2:02pm
medusa-docs ⬜️ Ignored (Inspect) Visit Preview Feb 2, 2024 2:02pm

@adrien2p
Copy link
Member Author

@srindom @olivermrbl could I have your opinion? Also, it doesn't seem a lot, but if I apply that to all module the footprint will be reduced by a lot. Also, it means that now all read and delete, soft, restore method are auto generated, meaning you can focus on create, update and other custom method which in most cases are the one having most of the business logic

cc @carlos-r-l-rodrigues

@srindom
Copy link
Collaborator

srindom commented Jan 23, 2024

This is amazing!

@olivermrbl
Copy link
Contributor

olivermrbl commented Jan 23, 2024

LFG!! 🚀 🚀

Really great

@adrien2p
Copy link
Member Author

adrien2p commented Jan 23, 2024

I ve reached one limitation of typescript microsoft/TypeScript#48125 (comment) if anybody want to pitch in 😅

@adrien2p adrien2p marked this pull request as ready for review February 2, 2024 10:41
@adrien2p adrien2p requested review from a team as code owners February 2, 2024 10:41
Copy link
Collaborator

@srindom srindom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Suggest that we merge and start using it ASAP, fixes and adjustments can follow in separate PRs.

@adrien2p
Copy link
Member Author

adrien2p commented Feb 2, 2024

@olivermrbl any reason this pr is mentioned in the dashboard pr?

@olivermrbl
Copy link
Contributor

No that was by mistake

Copy link
Contributor

@olivermrbl olivermrbl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 🔥 🔥 🔥

@adrien2p
Copy link
Member Author

adrien2p commented Feb 2, 2024

@olivermrbl @srindom ready to be merged as soon as the pipeline is green, please seb have a look to the changes I ve done on the customer module though. I had to change quite a few things, I ve put some todos here and there in different modules for naming and stuff, they are not specific but later on we can do a cleanup round

@@ -183,7 +183,7 @@ describe("Currency Service", () => {
error = e
}

expect(error.message).toEqual('"currencyCode" must be defined')
expect(error.message).toEqual("currency - code must be defined")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: How are these error messages generated?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can find them in the internal service factory. But before they where trying to provide the name of the expected argument except that it doesn't work anymore, so now it shows the primary key(s) missing

lowerCaseFirst(model.name) + upperCaseFirst(primaryKeys[0])
}"`
: `${lowerCaseFirst(model.name)} ${primaryKeys.join(", ")}`
? `"${lowerCaseFirst(model.name) + " - " + primaryKeys[0]}"`
Copy link
Contributor

@olivermrbl olivermrbl Feb 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought(non-blocking): Can we do better than this?

Ideally, we have something like:

currencyCode
priceListId
moneyAmountId

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it makes you think you need to provide { currencyCode } when we expect { code } ?

@kodiakhq kodiakhq bot merged commit a7be5d7 into develop Feb 2, 2024
17 checks passed
@kodiakhq kodiakhq bot deleted the chore/improve-modules-dev branch February 2, 2024 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants