diff --git a/www/apps/book/app/advanced-development/module-links/custom-columns/page.mdx b/www/apps/book/app/advanced-development/module-links/custom-columns/page.mdx new file mode 100644 index 0000000000000..802f20b3a380b --- /dev/null +++ b/www/apps/book/app/advanced-development/module-links/custom-columns/page.mdx @@ -0,0 +1,174 @@ +export const metadata = { + title: `${pageNumber} Add Columns to a Link`, +} + +# {metadata.title} + +In this chapter, you'll learn how to add custom columns to a link definition and manage them. + +## How to Add Custom Columns to a Link's Table? + +The `defineLink` function used to define a link accepts a third paramter, which is an object of options. + +To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: + +export const linkHighlights = [ + ["10", "extraColumns", "Custom columns to add to the created link's table."], + ["11", "metadata", "The column's name."], + ["12", "type", "The column's type."] +] + +```ts highlights={linkHighlights} +import HelloModule from "../modules/hello" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom, + { + database: { + extraColumns: { + metadata: { + type: "json", + } + } + } + } +) +``` + +This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`. + +### Database Options + +The `database` property defines configuration for the table created in the database. + +Its `extraColumns` property defines custom columns to create in the link's table. + +`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. + +### Column Configurations + +The column's configurations object accepts the following properties: + +- `type`: The column's type. Possible values are: + - `string` + - `text` + - `integer` + - `boolean` + - `date` + - `time` + - `datetime` + - `enum` + - `json` + - `array` + - `enumArray` + - `float` + - `double` + - `decimal` + - `bigint` + - `mediumint` + - `smallint` + - `tinyint` + - `blob` + - `uuid` + - `uint8array` +- `defaultValue`: The column's default value. +- `nullable`: Whether the column can have `null` values. + +--- + +## Set Custom Column when Creating Link + +The object you pass to the remote link's `create` method accepts a `data` property. Its value is an object whose keys are custom column names, and values are the value of the custom column for this link. + +For example: + + + +Learn more about the remote link, how to resolve it, and its methods in [this chapter](../remote-link/page.mdx). + + + +```ts +await remoteLink.create({ + [Modules.PRODUCT]: { + product_id: "123" + }, + HELLO_MODULE: { + my_custom_id: "321" + }, + data: { + metadata: { + test: true + } + } +}) +``` + +--- + +## Retrieve Custom Column with Link + +To retrieve linked records with their custom columns, use Query and pass the link definition as the `entity` property's value. + +For example: + + + +Learn more about Query and how to resolve use it [this chapter](../remote-link/page.mdx). + + + +export const retrieveHighlights = [ + ["1", "productHelloLink", "Import the exported link definition."], + ["6", "entity", "Pass the link definition to retrieve its data."], + ["7", `"metadata"`, "Retrieve the `metadata` column."], + ["7", `"product.*"`, "Retrieve the linked product's details."], + ["7", `"my_custom.*"`, "Retrieve the linked `myCustom` record's details."], +] + +```ts highlights={retrieveHighlights} +import productHelloLink from "../links/product-hello" + +// ... + +const { data } = await query.graph({ + entity: productHelloLink.entryPoint, + fields: ["metadata", "product.*", "my_custom.*"], + filters: { + product_id: "prod_123" + } +}) +``` + +This retrieves the product of id `prod_123` and its linked `my_custom` records. + +In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. + +--- + +## Update Custom Column's Value + +The remote link's `create` method updates a link's data if the link between the specified records already exists. + +So, to update the value of a custom column in a created link, use the `create` method again passing it a new value for the custom column. + +For example: + +```ts +await remoteLink.create({ + [Modules.PRODUCT]: { + product_id: "123" + }, + HELLO_MODULE: { + my_custom_id: "321" + }, + data: { + metadata: { + test: false + } + } +}) +``` diff --git a/www/apps/book/app/advanced-development/modules/module-link-directions/page.mdx b/www/apps/book/app/advanced-development/module-links/directions/page.mdx similarity index 93% rename from www/apps/book/app/advanced-development/modules/module-link-directions/page.mdx rename to www/apps/book/app/advanced-development/module-links/directions/page.mdx index 722abeda0ebe6..b6ba91df6c299 100644 --- a/www/apps/book/app/advanced-development/modules/module-link-directions/page.mdx +++ b/www/apps/book/app/advanced-development/module-links/directions/page.mdx @@ -4,7 +4,7 @@ export const metadata = { # {metadata.title} -In this chapter, you'll learn about difference in module link directions, and which to use based on your use case. +In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. ## Link Direction diff --git a/www/apps/book/app/advanced-development/modules/module-links/page.mdx b/www/apps/book/app/advanced-development/module-links/page.mdx similarity index 75% rename from www/apps/book/app/advanced-development/modules/module-links/page.mdx rename to www/apps/book/app/advanced-development/module-links/page.mdx index 8f9690bc1ce11..02d7e3fa865ae 100644 --- a/www/apps/book/app/advanced-development/modules/module-links/page.mdx +++ b/www/apps/book/app/advanced-development/module-links/page.mdx @@ -1,30 +1,16 @@ -import { BetaBadge } from "docs-ui" - export const metadata = { title: `${pageNumber} Module Link`, } -# {metadata.title} +# {metadata.title} In this chapter, you’ll learn what a module link is. ## What is a Module Link? -A module link forms an association between two data models of different modules, while maintaining module isolation. - -You can then retrieve data across the linked modules, and manage their linked records. - - - -You want to create a relation between data models from different modules. +Since modules are isolated, you can't access another module's data models to add a relation to it or extend it. - - - - -You want to create a relationship between data models in the same module. Use data model relationships instead. - - +Instead, you use a module link. A module link forms an association between two data models of different modules, while maintaining module isolation. --- @@ -58,12 +44,14 @@ In this example, you define a module link between the `hello` module's `MyCustom ### 2. Sync Links -Medusa stores links as pivot tables in the database. So, to reflect your link in the database, run the `db:sync-links` command: +After defining the link, run the `db:sync-links` command: ```bash npx medusa db:sync-links ``` +The Medusa application creates a new table for your link to store the IDs of linked records. + Use this command whenever you make changes to your links. For example, run this command if you remove your link definition file. @@ -74,6 +62,33 @@ You can also use the `db:migrate` command, which both runs the migrations and sy --- +## How Module Links Work? + +When you define a module link, the Medusa application creates a table in the database for that link. + +Then, when you create links between records of the data models, the IDs of these data models are stored as a new record in the link's table. + +![Diagram illustration for links](https://res.cloudinary.com/dza7lstvk/image/upload/v1726482168/Medusa%20Book/Custom_Link_Illustration_fsisfa.jpg) + +--- + +## When to Use Module Links + + + +- You want to create a relation between data models from different modules. +- You want to extend the data model of another module. + + + + + +You want to create a relationship between data models in the same module. Use data model relationships instead. + + + +--- + ## Define a List Link By default, the defined link establishes a one-to-one relation: a record of a data model is linked to one record of the other data model. @@ -105,23 +120,6 @@ In this example, a record of `product` can be linked to more than one record of --- -## Extend Data Models with Module Links - -Module links are most useful when you want to add properties to a data model of another module. - -For example, to add custom properties to the `Product` data model of the Product Module, you: - -1. Create a module. -2. Create in the module a data model that holds the custom properties you want to add to the `Product` data model. -2. Define a module link that links your module to the Product Module. - -Then, in the next chapters, you'll learn how to: - -- Link each product to a record of your data model. -- Retrieve your data model's properties when you retrieve products. - ---- - ## Set Delete Cascades on Link To enable delete cascade on a link so that when a record is deleted, its linked records are also deleted, pass the `deleteCascades` property in the object passed to `defineLink`. diff --git a/www/apps/book/app/advanced-development/modules/query/page.mdx b/www/apps/book/app/advanced-development/module-links/query/page.mdx similarity index 100% rename from www/apps/book/app/advanced-development/modules/query/page.mdx rename to www/apps/book/app/advanced-development/module-links/query/page.mdx diff --git a/www/apps/book/app/advanced-development/modules/remote-link/page.mdx b/www/apps/book/app/advanced-development/module-links/remote-link/page.mdx similarity index 100% rename from www/apps/book/app/advanced-development/modules/remote-link/page.mdx rename to www/apps/book/app/advanced-development/module-links/remote-link/page.mdx diff --git a/www/apps/book/app/advanced-development/modules/container/page.mdx b/www/apps/book/app/advanced-development/modules/container/page.mdx index 61c03c7b3f31c..e87f30f38501a 100644 --- a/www/apps/book/app/advanced-development/modules/container/page.mdx +++ b/www/apps/book/app/advanced-development/modules/container/page.mdx @@ -8,11 +8,13 @@ In this chapter, you'll learn about the module's container and how to resolve re ## Module's Container -Each module has a local container only used by the resources of that module. +Since modules are isolated, each module has a local container only used by the resources of that module. -So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, such as: +So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. -- `logger`: A utility to log message in the Medusa application's logs. +### List of Registered Resources + +Find a list of resources or dependencies registered in a module's container in [this Learning Resources reference](!resoures!/medusa-container-resources). --- diff --git a/www/apps/book/app/advanced-development/modules/db-operations/page.mdx b/www/apps/book/app/advanced-development/modules/db-operations/page.mdx new file mode 100644 index 0000000000000..acb89f4d1eac0 --- /dev/null +++ b/www/apps/book/app/advanced-development/modules/db-operations/page.mdx @@ -0,0 +1,464 @@ +import { CodeTabs, CodeTab } from "docs-ui" + +export const metadata = { + title: `${pageNumber} Perform Database Operations in a Service`, +} + +# {metadata.title} + +In this chapter, you'll learn how to perform database operations in a module's service. + + + +This chapter is intended for more advanced database use-cases where you need more control over queries and operations. For basic database operations, such as creating or retrieving data of a model, use the [Service Factory](../service-factory/page.mdx) instead. + + + +## Run Queries + +[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations. + +Medusa provides an `InjectManager` decorator imported from `@medusajs/utils` that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager). + +So, to run database queries in a service: + +1. Add the `InjectManager` decorator to the method. +2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator imported from `@medusajs/utils`. This context holds database-related context, including the manager injected by `InjectManager` + +For example, in your service, add the following methods: + +export const methodsHighlight = [ + ["4", "getCount", "Retrieves the number of records in `my_custom` using the `count` method."], + ["8", "getCountSql", "Retrieves the number of records in `my_custom` using the `execute` method."] +] + +```ts highlights={methodsHighlight} +// other imports... +import { + InjectManager, + MedusaContext +} from "@medusajs/framework/utils" + +class HelloModuleService { + // ... + + @InjectManager() + async getCount( + @MedusaContext() sharedContext?: Context + ): Promise { + return await sharedContext.manager.count("my_custom") + } + + @InjectManager() + async getCountSql( + @MedusaContext() sharedContext?: Context + ): Promise { + const data = await sharedContext.manager.execute( + "SELECT COUNT(*) as num FROM my_custom" + ) + + return parseInt(data[0].num) + } +} +``` + +You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator. + +The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/5.9/knex/class/EntityManager). + +You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count. + + + +Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods. + + + +--- + +## Execute Operations in Transactions + +To wrap database operations in a transaction, you create two methods: + +1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator imported from `@medusajs/utils`. +2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section. + +Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator imported from `@medusajs/utils`. It holds database-related contexts passed through the Medusa application. + +For example: + +export const opHighlights = [ + ["11", "InjectTransactionManager", "A decorator that injects the a transactional entity manager into the `sharedContext` parameter."], + ["17", "MedusaContext", "A decorator to use Medusa's shared context."], + ["20", "nativeUpdate", "Update a record."], + ["31", "execute", "Retrieve the updated record."], + ["38", "InjectManager", "A decorator that injects a forked entity manager into the context."], +] + +```ts highlights={opHighlights} +import { + InjectManager, + InjectTransactionManager, + MedusaContext +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + const transactionManager = sharedContext.transactionManager + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id + }, + { + name: input.name + } + ) + + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) + + return updatedRecord + } + + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } +} +``` + +The `HelloModuleService` has two methods: + +- A protected `update_` that performs the database operations inside a transaction. +- A public `update` that executes the transactional protected method. + +The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations. + + + +Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods. + + + +### Why Wrap a Transactional Method + +The variables in the transactional method (for example, `update_`) hold values that are uncomitted to the database. They're only committed once the method finishes execution. + +So, if in your method you perform database operations, then use their result to perform other actions, such as connect to a third-party service, you'll be working with uncommitted data. + +By placing only the database operations in a method that has the `InjectTransactionManager` and using it in a wrapper method, the wrapper method receives the committed result of the transactional method. + + + +This is also useful if you perform heavy data normalization outside of the database operations. In that case, you don't hold the transaction for a longer time than needed. + + + +For example, the `update` method could be changed to the following: + +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + const newData = await this.update_(input, sharedContext) + + await sendNewDataToSystem(newData) + + return newData + } +} +``` +In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method. + +### Using Methods in Transactional Methods + +If your transactional method uses other methods that accept a Medusa context, pass the shared context to those method. + +For example: + +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async anotherMethod( + @MedusaContext() sharedContext?: Context + ) { + // ... + } + + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + anotherMethod(sharedContext) + } +} +``` + +You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. + +The `anotherMethod` now runs in the same transaction as the `update_` method. + +--- + +## Configure Transactions + +To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` dependency registered in your module's container. + +The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. + +The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. + +For example, resolve the `baseRepository` in your service's constructor: + + + + +```ts highlights={[["14"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" +import { DAL } from "@medusajs/framework/types" + +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} + +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected baseRepository_: DAL.RepositoryService + + constructor({ baseRepository }: InjectedDependencies) { + super(...arguments) + this.baseRepository_ = baseRepository + } +} + +export default HelloModuleService +``` + + + +```ts highlights={[["10"]]} +import { DAL } from "@medusajs/framework/types" + +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} + +class HelloModuleService { + protected baseRepository_: DAL.RepositoryService + + constructor({ manager }: InjectedDependencies) { + this.baseRepository_ = baseRepository + } +} + +export default HelloModuleService +``` + + + + +Then, add the following method that uses it: + +export const repoHighlights = [ + ["20", "transaction", "Wrap the function parameter in a transaction."] +] + +```ts highlights={repoHighlights} +// ... +import { + InjectManager, + InjectTransactionManager, + MedusaContext +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id + }, + { + name: input.name + } + ) + + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) + + return updatedRecord + }, + { + transaction: sharedContext.transactionManager + } + ) + } + + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } +} +``` + +The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. + +The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. + +The `baseRepository_.transaction` method also receives as a second parameter an object of options. You must pass in it the `transaction` property and set its value to the `sharedContext.transactionManager` property so that the function wrapped in the transaction uses the injected transaction manager. + + + +Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods. + + + +### Transaction Options + +The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties: + +1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section. + +```ts highlights={[["16"]]} +// other imports... +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + transaction: sharedContext.transactionManager + } + ) + } +} +``` + +2. `isolationLevel`: Sets the transaction's [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Its values can be: + - `read committed` + - `read uncommitted` + - `snapshot` + - `repeatable read` + - `serializable` + +```ts highlights={[["19"]]} +// other imports... +import { IsolationLevel } from "@mikro-orm/core" + +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + isolationLevel: IsolationLevel.READ_COMMITTED + } + ) + } +} +``` + +3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions. + - If `transaction` is provided and this is disabled, the manager in `transaction` is re-used. + +```ts highlights={[["16"]]} +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + enableNestedTransactions: false + } + ) + } +} +``` diff --git a/www/apps/book/app/advanced-development/modules/isolation/page.mdx b/www/apps/book/app/advanced-development/modules/isolation/page.mdx index 634b10346b5ab..46cc9cfc9fb22 100644 --- a/www/apps/book/app/advanced-development/modules/isolation/page.mdx +++ b/www/apps/book/app/advanced-development/modules/isolation/page.mdx @@ -8,8 +8,8 @@ In this chapter, you'll learn how modules are isolated, and what that means for -- Modules can't access resources, such as services, from other modules. -- You can use Medusa's tools, as explained in the next chapters, to extend a modules' features or implement features across modules. +- Modules can't access resources, such as services or data models, from other modules. +- Use Medusa's linking concepts, as explained in the [Module Links chapters](../../module-links/page.mdx), to extend a module's data models and retrieve data across modules. @@ -21,12 +21,93 @@ For example, your custom module can't resolve the Product Module's main service --- -## How to Implement Custom Features Across Modules? +## Why are Modules Isolated -In your Medusa application, you want to implement features that span across modules, or you want to extend an existing module's features and customize them for your own use case. +Some of the module isolation's benefits include: -For example, you want to extend the Product Module to add new properties to the `Product` data model. +- Integrate your module into any Medusa application without side-effects to your setup. +- Replace existing modules with your custom implementation, if your use case is drastically different. +- Use modules in other environments, such as Edge functions and Next.js apps. -Medusa provides the tools to implement these use cases while maintaining isolation between modules. +--- + +## How to Extend Data Model of Another Module? + +To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](../../module-links/page.mdx). + +--- + +## How to Use Services of Other Modules? + +If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities. + +Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. + +### Example + +For example, consider you have two modules: + +1. A module that stores and manages brands in your application. +2. A module that integrates a third-party Content Management System (CMS). + +To sync brands from your application to the third-party system, create the following steps: + +export const stepsHighlights = [ + ["1", "retrieveBrandsStep", "A step that retrieves brands using a brand module."], + ["14", "createBrandsInCmsStep", "A step that creates brands using a CMS module."], + ["25", "", "Add a compensation function to the step if an error occurs."] +] + +```ts title="Example Steps" highlights={stepsHighlights} +const retrieveBrandsStep = createStep( + "retrieve-brands", + async (_, { container }) => { + const brandModuleService = container.resolve( + "brandModuleService" + ) + + const brands = await brandModuleService.listBrands() + + return new StepResponse(brands) + } +) + +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) + + const cmsBrands = await cmsModuleService.createBrands(brands) + + return new StepResponse(cmsBrands, cmsBrands) + }, + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) + + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) + } +) +``` + +The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. + +Then, create the following workflow that uses these steps: + +```ts title="Example Workflow" +export const syncBrandsWorkflow = createWorkflow( + "sync-brands", + () => { + const brands = retrieveBrandsStep() + + updateBrandsInCmsStep({ brands }) + } +) +``` -The next chapters explain these tools and how to use them in your custom development. +You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. diff --git a/www/apps/book/app/advanced-development/modules/multiple-services/page.mdx b/www/apps/book/app/advanced-development/modules/multiple-services/page.mdx new file mode 100644 index 0000000000000..ed4cf770f09b8 --- /dev/null +++ b/www/apps/book/app/advanced-development/modules/multiple-services/page.mdx @@ -0,0 +1,130 @@ +export const metadata = { + title: `${pageNumber} Multiple Services in a Module`, +} + +# {metadata.title} + +In this chapter, you'll learn how to use multiple services in a module. + +## Module's Main and Internal Services + +A module has one main service only, which is the service exported in the module's definition. + +However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. + +--- + +## How to Add an Internal Service + +### 1. Create Service + +To add an internal service, create it in the `services` directory of your module. + +For example, create the file `src/modules/hello/services/client.ts` with the following content: + +```ts title="src/modules/hello/services/client.ts" +export class ClientService { + async getMessage(): Promise { + return "Hello, World!" + } +} +``` + +### 2. Export Service in Index + +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. + +For example, create the file `src/modules/hello/services/index.ts` with the following content: + +```ts title="src/modules/hello/services/index.ts" +export * from "./client" +``` + +This exports the `ClientService`. + +### 3. Resolve Internal Service + +Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. + +For example, in your main service: + +```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} +// other imports... +import { ClientService } from "./services" + +type InjectedDependencies = { + clientService: ClientService +} + +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected clientService_: ClientService + + constructor({ clientService }: InjectedDependencies) { + super(...arguments) + this.clientService_ = clientService + } +} +``` + +You can now use your internal service in your main service. + +--- + +## Resolve Resources in Internal Service + +Resolve dependencies from your module's container in the constructor of your internal service. + +For example: + +```ts +import { Logger } from "@medusajs/framework/types" + +type InjectedDependencies = { + logger: Logger +} + +export class ClientService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + } +} +``` + +--- + +## Access Module Options + +Your internal service can't access the module's options. + +To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.js`. + +For example: + +```ts +import { ConfigModule } from "@medusajs/framework/types" +import { HELLO_MODULE } from ".." + +export type InjectedDependencies = { + configModule: ConfigModule +} + +export class ClientService { + protected options: Record + + constructor({ configModule }: InjectedDependencies) { + const moduleDef = configModule.modules[HELLO_MODULE] + + if (typeof moduleDef !== "boolean") { + this.options = moduleDef.options + } + } +} +``` + +The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. + +If its value is not a `boolean`, set the service's options to the module configuration's `options` property. diff --git a/www/apps/book/app/advanced-development/modules/options/page.mdx b/www/apps/book/app/advanced-development/modules/options/page.mdx index f76f39bd113d5..85131ff9b089a 100644 --- a/www/apps/book/app/advanced-development/modules/options/page.mdx +++ b/www/apps/book/app/advanced-development/modules/options/page.mdx @@ -1,5 +1,3 @@ -import { CodeTabs, CodeTab } from "docs-ui" - export const metadata = { title: `${pageNumber} Module Options`, } diff --git a/www/apps/book/app/advanced-development/modules/page.mdx b/www/apps/book/app/advanced-development/modules/page.mdx new file mode 100644 index 0000000000000..8811f4b9d4959 --- /dev/null +++ b/www/apps/book/app/advanced-development/modules/page.mdx @@ -0,0 +1,15 @@ +export const metadata = { + title: `${pageNumber} Modules Advanced Guides`, +} + +# {metadata.title} + +In the next chapters, you'll learn more about developing modules and related resources. + +By the end of this chapter, you'll know more about: + +1. A module's container and how a module is isolated. +2. Passing options to a module. +3. The service factory and the methods it generates. +4. Using a module's service to query and perform actions on the database. +5. Using multiple services in a module. diff --git a/www/apps/book/app/advanced-development/modules/service-constraints/page.mdx b/www/apps/book/app/advanced-development/modules/service-constraints/page.mdx index d4607c959b9f3..4bc4fb72f0217 100644 --- a/www/apps/book/app/advanced-development/modules/service-constraints/page.mdx +++ b/www/apps/book/app/advanced-development/modules/service-constraints/page.mdx @@ -8,7 +8,13 @@ This chapter lists constraints to keep in mind when creating a service. ## Use Async Methods -Medusa wraps adds wrappers around your service's methods and executes them as async methods. +Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronus, it always executes methods in the wrapper with the `await` keyword. + +For example, if you have a synchronous `getMessage` method, and you use it other resources like workflows, Medusa executes it as an async method: + +```ts +await helloModuleService.getMessage() +``` So, make sure your service's methods are always async to avoid unexpected errors or behavior. diff --git a/www/apps/book/app/advanced-development/modules/service-factory/page.mdx b/www/apps/book/app/advanced-development/modules/service-factory/page.mdx index b4d2dbfb833d8..ee2c1728509be 100644 --- a/www/apps/book/app/advanced-development/modules/service-factory/page.mdx +++ b/www/apps/book/app/advanced-development/modules/service-factory/page.mdx @@ -12,7 +12,7 @@ In this chapter, you’ll learn about what the service factory is and how to use Medusa provides a service factory that your module’s main service can extend. -The service factory generates data management methods for your data models, so you don't have to implement these methods manually. +The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. @@ -54,7 +54,7 @@ In the example above, since the `HelloModuleService` extends `MedusaService`, it ### Generated Methods -The service factory generates data-management methods for each of the data models provided in the first parameter. +The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. diff --git a/www/apps/book/app/basics/modules-and-services/page.mdx b/www/apps/book/app/basics/modules-and-services/page.mdx index 568a7cb833c3d..68cc092b2f033 100644 --- a/www/apps/book/app/basics/modules-and-services/page.mdx +++ b/www/apps/book/app/basics/modules-and-services/page.mdx @@ -10,20 +10,12 @@ In this chapter, you’ll learn about modules, their main service, and how to cr A module is a package of reusable commerce or architectural functionalities. It's integrated as a building block in your Medusa application, without implications on the existing setup. -You create a module to introduce custom features, extend existing ones, or integrate third-party services. +A module has a service, which is a class that can connect to the database or third-party systems to provide custom features. The service's methods are then used by other resources, such as API routes. --- ## How to Create a Module? - - -1. Create module's main service. -2. Create module definition. -3. Add module to Medusa's configurations. - - - Modules are created in a sub-directory of `src/modules`. For example, create the directory `src/modules/hello`. @@ -154,7 +146,8 @@ You’ll receive the following response: -- You're implementing a custom commerce feature. For example, you're implementing digital products. +- You're adding new tables to the database, as explained in later chapters. +- You're integrating a third-party system for commerce or architectural features. - You want to re-use your custom commerce functionalities across Medusa applications or use them in other environments, such as Edge functions and Next.js apps. diff --git a/www/apps/book/app/more-resources/examples/page.mdx b/www/apps/book/app/more-resources/examples/page.mdx index 84b211d801f54..fff457caba245 100644 --- a/www/apps/book/app/more-resources/examples/page.mdx +++ b/www/apps/book/app/more-resources/examples/page.mdx @@ -16,7 +16,7 @@ This chapter provides links to example sections on different Medusa topics. - [Retrieve logged-in customer's details](../../advanced-development/api-routes/protected-routes/page.mdx#retrieve-logged-in-customers-details) - [Retrieve logged-in user's details](../../advanced-development/api-routes/protected-routes/page.mdx#retrieve-logged-in-admin-users-details) - [Custom error handler](../../advanced-development/api-routes/errors/page.mdx#override-error-handler) -- [Using Query in an API route](../../advanced-development/modules/query/page.mdx#query-example) +- [Using Query in an API route](../../advanced-development/module-links/query/page.mdx#query-example) - [Upload files in a custom API route](!resources!/recipes/digital-products/examples/standard#step-7-upload-digital-product-media-api-route) - [Customize cart-completion API route](!resources!/recipes/digital-products/examples/standard#step-11-customize-cart-completion) diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index c9b65ef8251b8..97465a1e0cc6b 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -77,9 +77,16 @@ export const generatedEditDates = { "app/advanced-development/api-routes/validation/page.mdx": "2024-09-11T10:46:31.476Z", "app/advanced-development/api-routes/errors/page.mdx": "2024-09-30T08:43:53.121Z", "app/advanced-development/admin/constraints/page.mdx": "2024-09-10T11:39:51.165Z", - "app/advanced-development/modules/query/page.mdx": "2024-09-30T08:43:53.127Z", "app/debugging-and-testing/testing-tools/modules-tests/module-example/page.mdx": "2024-09-30T08:43:53.139Z", "app/debugging-and-testing/testing-tools/modules-tests/page.mdx": "2024-09-30T08:43:53.139Z", + "app/advanced-development/module-links/custom-columns/page.mdx": "2024-09-16T15:51:33.570Z", + "app/advanced-development/module-links/directions/page.mdx": "2024-09-16T15:37:51.441Z", + "app/advanced-development/module-links/page.mdx": "2024-09-16T15:36:48.190Z", + "app/advanced-development/module-links/query/page.mdx": "2024-09-16T12:42:27.579Z", + "app/advanced-development/module-links/remote-link/page.mdx": "2024-09-16T12:42:27.581Z", + "app/advanced-development/modules/db-operations/page.mdx": "2024-09-16T14:38:29.150Z", + "app/advanced-development/modules/multiple-services/page.mdx": "2024-09-16T14:41:32.975Z", + "app/advanced-development/modules/page.mdx": "2024-09-16T14:33:48.217Z", "app/debugging-and-testing/instrumentation/page.mdx": "2024-09-17T08:53:15.910Z", "app/advanced-development/api-routes/additional-data/page.mdx": "2024-09-30T08:43:53.120Z", "app/advanced-development/workflows/page.mdx": "2024-09-18T08:00:57.364Z", diff --git a/www/apps/book/next.config.mjs b/www/apps/book/next.config.mjs index 6e84fe1cf8a2b..4ecc7d776a983 100644 --- a/www/apps/book/next.config.mjs +++ b/www/apps/book/next.config.mjs @@ -131,7 +131,27 @@ const nextConfig = { return [ { source: "/advanced-development/modules/remote-query", - destination: "/advanced-development/modules/query", + destination: "/advanced-development/module-links/query", + permanent: true, + }, + { + source: "/advanced-development/modules/query", + destination: "/advanced-development/module-links/query", + permanent: true, + }, + { + source: "/advanced-development/modules/module-links", + destination: "/advanced-development/module-links", + permanent: true, + }, + { + source: "/advanced-development/modules/module-link-directions", + destination: "/advanced-development/module-links/directions", + permanent: true, + }, + { + source: "/advanced-development/modules/remote-link", + destination: "/advanced-development/module-links/remote-link", permanent: true, }, { diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index e6128aff1609b..ba11e80aca245 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -255,14 +255,25 @@ export const sidebar = numberSidebarItems( ], }, { - type: "sub-category", + type: "link", + path: "/advanced-development/modules", title: "Modules", children: [ + { + type: "link", + path: "/advanced-development/modules/isolation", + title: "Module Isolation", + }, { type: "link", path: "/advanced-development/modules/container", title: "Module's Container", }, + { + type: "link", + path: "/advanced-development/modules/options", + title: "Module Options", + }, { type: "link", path: "/advanced-development/modules/service-factory", @@ -275,33 +286,40 @@ export const sidebar = numberSidebarItems( }, { type: "link", - path: "/advanced-development/modules/isolation", - title: "Module Isolation", + path: "/advanced-development/modules/db-operations", + title: "Database Operations", }, { type: "link", - path: "/advanced-development/modules/module-links", - title: "Module Links", + path: "/advanced-development/modules/multiple-services", + title: "Multiple Services", }, + ], + }, + { + type: "link", + path: "/advanced-development/module-links", + title: "Module Links", + children: [ { type: "link", - path: "/advanced-development/modules/module-link-directions", + path: "/advanced-development/module-links/directions", title: "Module Link Direction", }, { type: "link", - path: "/advanced-development/modules/remote-link", + path: "/advanced-development/module-links/remote-link", title: "Remote Link", }, { type: "link", - path: "/advanced-development/modules/query", + path: "/advanced-development/module-links/query", title: "Query", }, { type: "link", - path: "/advanced-development/modules/options", - title: "Module Options", + path: "/advanced-development/module-links/custom-columns", + title: "Custom Columns", }, ], }, diff --git a/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx b/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx index aecc1c9f5a8af..84aa982c36044 100644 --- a/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx @@ -8,7 +8,7 @@ export const metadata = { # {metadata.title} -In this document, you'll learn how to retrieve product variant prices in the Medusa application using the [Query](!docs!/advanced-development/modules/query). +In this document, you'll learn how to retrieve product variant prices in the Medusa application using the [Query](!docs!/advanced-development/module-links/query). diff --git a/www/apps/resources/app/medusa-container-resources/page.mdx b/www/apps/resources/app/medusa-container-resources/page.mdx index 0b62afbe8ea07..e3f4161cf1a4a 100644 --- a/www/apps/resources/app/medusa-container-resources/page.mdx +++ b/www/apps/resources/app/medusa-container-resources/page.mdx @@ -1,16 +1,20 @@ import { Table } from "docs-ui" export const metadata = { - title: `Medusa Container Resources`, + title: `Medusa and Module Container Dependencies`, } # {metadata.title} -This documentation page includes the list of resources registered in the Medusa container of your Medusa application. +This documentation page includes the list of dependencies registered in the container of the Medusa application and a module. + +## Medusa Container Dependencies + +The following list of dependencies are resources that can be resolved by all resources (such as API route or workflow) except of a module's. -Use the `ContainerRegistrationKeys` enum imported from `@medusajs/framework/utils` to resolve these resources' names. +Use the `ContainerRegistrationKeys` enum imported from `@medusajs/framework/utils` where specified. @@ -121,8 +125,134 @@ Use the `ContainerRegistrationKeys` enum imported from `@medusajs/framework/util - - For custom modules, the registration name is the key of the module in the `modules` configuration. - - For Medusa's commerce modules, you can use the `Modules` enum imported from `@medusajs/framework/utils`. + - For custom modules, the registration name is the key of the module in the `modules` configuration in `medusa-config.js`. + - For Medusa's commerce modules, use the `Modules` enum imported from `@medusajs/framework/utils`. + + + + + + +--- + +## Module Container Dependencies + +The following resources are resources that can be resolved by a module's services and loaders. + + + +Use the `ContainerRegistrationKeys` enum imported from `@medusajs/framework/utils` where specified. + + + + + + + + + Resource + + + + + Description + + + + + + Registration Name + + + + + + + + + Logger + + + + + An instance of Medusa CLI’s logger. You can use it to log messages to the terminal. + + + + + `logger` or `ContainerRegistrationKeys.LOGGER` + + + + + + + + Entity Manager + + + + + An instance of [MikroORM's entity manager](https://mikro-orm.io/api/5.9/knex/class/EntityManager). + + + + + `manager` or `ContainerRegistrationKeys.MANAGER` + + + + + + + + Base Repository + + + + + An instance of the base repository, used to run transactions or perform other database operations. + + + + + `baseRepository` + + + + + + + + Configurations + + + + + The configurations exported from `medusa-config.js`. + + + + + `configModule` or `ContainerRegistrationKeys.CONFIG_MODULE` + + + + + + + + Modules' services + + + + + All services exported by the `services/index.ts` file of a module. + + + + + Each service is registered by its camel-case name. For example, if the service's class name is `ClientService`, its registration name is `clientService`. diff --git a/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx b/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx index ea4681b00c4b0..9bea15afd882e 100644 --- a/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx +++ b/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx @@ -170,7 +170,7 @@ class DigitalProductModuleService extends MedusaService({ export default DigitalProductModuleService ``` -The service extends the [service factory](https://docs.medusajs.com/v2/advanced-development/modules/service-factory), which provides basic data-management features. +The service extends the [service factory](!docs!/advanced-development/modules/service-factory), which provides basic data-management features. ### Create Module Definition @@ -255,7 +255,7 @@ This defines a link between `DigitalProductOrder` and the Order Module’s `Orde ### Further Read -- [How to Define Module Links](!docs!/advanced-development/modules/module-links) +- [How to Define Module Links](!docs!/advanced-development/module-links) --- @@ -362,7 +362,7 @@ Make sure to replace `{token}` with the JWT token you retrieved. ### Further Reads - [How to Create an API Route](!docs!/basics/api-routes) -- [Learn more about Query](!docs!/advanced-development/modules/query) +- [Learn more about Query](!docs!/advanced-development/module-links/query) --- @@ -611,7 +611,7 @@ You’ll test out the workflow in the next section. - [How to Create a Workflow](!docs!/basics/workflows) - [What is the Compensation Function](!docs!/advanced-development/workflows/compensation-function) -- [Learn more about the remote link function](!docs!/advanced-development/modules/remote-link) +- [Learn more about the remote link function](!docs!/advanced-development/module-links/remote-link) --- diff --git a/www/apps/resources/app/recipes/digital-products/page.mdx b/www/apps/resources/app/recipes/digital-products/page.mdx index a8889295586d2..98209ed713ec6 100644 --- a/www/apps/resources/app/recipes/digital-products/page.mdx +++ b/www/apps/resources/app/recipes/digital-products/page.mdx @@ -69,7 +69,7 @@ Then, you can link your custom data model to data models from other modules. For icon: AcademicCapSolid, }, { - href: "!docs!/advanced-development/modules/module-links", + href: "!docs!/advanced-development/module-links", title: "Module Links", text: "Learn how to link data models of different modules.", icon: AcademicCapSolid, @@ -129,13 +129,13 @@ Use the remote link to create a link between two records, and use Query to fetch -[Learn more about module links](!docs!/advanced-development/modules/module-links). +[Learn more about module links](!docs!/advanced-development/module-links). diff --git a/www/apps/resources/app/recipes/marketplace/page.mdx b/www/apps/resources/app/recipes/marketplace/page.mdx index fc71406ac5fe3..1378bf16ce55c 100644 --- a/www/apps/resources/app/recipes/marketplace/page.mdx +++ b/www/apps/resources/app/recipes/marketplace/page.mdx @@ -57,7 +57,7 @@ Since a vendor has products, orders, and other models based on your use case, de For example, if you defined a vendor data model in a marketplace module, you can define a module link between the vendor and the Product Module's product data model. -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/advanced-development/modules/query). +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/advanced-development/module-links/query). diff --git a/www/apps/resources/app/service-factory-reference/methods/listAndCount/page.mdx b/www/apps/resources/app/service-factory-reference/methods/listAndCount/page.mdx index 089f0fdb179ad..350f99c57672e 100644 --- a/www/apps/resources/app/service-factory-reference/methods/listAndCount/page.mdx +++ b/www/apps/resources/app/service-factory-reference/methods/listAndCount/page.mdx @@ -54,7 +54,7 @@ The method returns an array with two items: -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/advanced-development/modules/query). +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/advanced-development/module-links/query). diff --git a/www/apps/resources/app/service-factory-reference/methods/retrieve/page.mdx b/www/apps/resources/app/service-factory-reference/methods/retrieve/page.mdx index f813519919938..c4d81f61cc9fa 100644 --- a/www/apps/resources/app/service-factory-reference/methods/retrieve/page.mdx +++ b/www/apps/resources/app/service-factory-reference/methods/retrieve/page.mdx @@ -30,7 +30,7 @@ The method returns the record as an object. -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/advanced-development/modules/query). +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/advanced-development/module-links/query). diff --git a/www/apps/resources/generated/sidebar.mjs b/www/apps/resources/generated/sidebar.mjs index 3d3d10d732fea..bf4dffab9aee9 100644 --- a/www/apps/resources/generated/sidebar.mjs +++ b/www/apps/resources/generated/sidebar.mjs @@ -8745,7 +8745,7 @@ export const generatedSidebar = [ "isPathHref": true, "type": "link", "path": "/medusa-container-resources", - "title": "Medusa Container Resources", + "title": "Container Dependencies", "children": [] }, { diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index 3bac0ff6d4adc..83a07e9535560 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -2200,7 +2200,7 @@ export const sidebar = sidebarAttachHrefCommonOptions([ { type: "link", path: "/medusa-container-resources", - title: "Medusa Container Resources", + title: "Container Dependencies", }, { type: "link",