diff --git a/www/apps/book/app/learn/basics/loaders/page.mdx b/www/apps/book/app/learn/basics/loaders/page.mdx index a0f525e0d6ef8..fc378ac359fa5 100644 --- a/www/apps/book/app/learn/basics/loaders/page.mdx +++ b/www/apps/book/app/learn/basics/loaders/page.mdx @@ -1,3 +1,5 @@ +import { Prerequisites } from "docs-ui" + export const metadata = { title: `${pageNumber} Loaders`, } @@ -8,29 +10,66 @@ In this chapter, you’ll learn about loaders and how to use them. ## What is a Loader? -A loader is a function executed when the Medusa application starts. You define and export it in a module. +When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if you're integrating a non-supported database such as MongoDB, you want to establish the connection when the application starts and re-use it in your customizations. + +In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](../modules/page.mdx), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules. + +Loaders are useful to register custom resources, such as database connections, in the [module's container](../../advanced-development/modules/container/page.mdx), which is similar to the [Medusa container](../medusa-container/page.mdx) but includes only [resources available to the module](!resources!/medusa-container-resources#module-container-resources). Modules are isolated, so they can't access resources outside of them, such as a service in another module. + + + +Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](../../advanced-development/modules/isolation/page.mdx), and check out [this reference for the list of resources in the module's container](!resources!/medusa-container-resources#module-container-resources). -Loaders are useful to perform a task at the application start-up, such as to sync data between Medusa and a third-party service. + --- ## How to Create a Loader? -A loader is created in a TypeScript or JavaScript file under a module's `loaders` directory. +### 1. Implement Loader Function + +You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. + +For example, consider you have a `hello` module, you can create a loader at `src/modules/hello/loaders/hello-world.ts` with the following content: -For example, create the file `src/modules/hello/loaders/hello-world.ts` with the following content: + + +Learn how to create a module in [this chapter](../modules/page.mdx). + + ```ts title="src/modules/hello/loaders/hello-world.ts" -export default async function helloWorldLoader() { - console.log( - "[HELLO MODULE] Just started the Medusa application!" - ) +import { + LoaderOptions, +} from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + logger.info("[helloWorldLoader]: Hello, World!") } ``` -### Export Loader in Module Definition +The loader file exports an async function, which is the function executed when the application loads. + +The function receives an object parameter that has a `container` property, which is the module's container that you can use to resolve resources from. In this example, you resolve the Logger utility to log a message in the terminal. + + + +Find the list of resources in the module's container in [this reference](!resources!/medusa-container-resources#module-container-resources). + + + +### 2. Export Loader in Module Definition -Import the loader in `src/modules/hello/index.ts` and export it in the module's definition: +After implementing the loader, you must export it in the module's definition in the `index.ts` file at the root of the module's directory. Otherwise, the Medusa application will not run it. + +So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`: ```ts title="src/modules/hello/index.ts" // other imports... @@ -42,37 +81,209 @@ export default Module("hello", { }) ``` -The value of the `loaders` property is an array of loader functions. - ---- +The second parameter of the `Module` function accepts a `loaders` property whose value is an array of loader functions. The Medusa application will execute these functions when it starts. -## Test the Loader +### Test the Loader -Start the Medusa application: +Assuming your module is [added to Medusa's configuration](../modules/page.mdx#4-add-module-to-medusas-configurations), you can test the loader by starting the Medusa application: ```bash npm2yarn npm run dev ``` -Among the messages logged in the terminal, you’ll see the following message: +Then, you'll find the following message logged in the terminal: -```bash -[HELLO MODULE] Just started the Medusa application! +```plain +info: [HELLO MODULE] Just started the Medusa application! ``` +This indicates that the loader in the `hello` module ran and logged this message. + --- -## When to Use Loaders +## Example: Register Custom MongoDB Connection - +As mentioned in this chapter's introduction, loaders are most useful when you need to register a custom resource in the module's container to re-use it in other customizations in the module. -- You're performing an action at application start-up. -- You're establishing a one-time connection with an external system. +Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. - + - +To connect to the database, you create the following loader in your module: -You want to perform an action continuously or at a set time pattern in the application. Use scheduled jobs instead, which is explained in an upcoming chapter. +export const loaderHighlights = [ + ["6", "ModuleOptions", "Define a type for expected options."], + ["14", "ModuleOptions", "Pass the option type as a type argument to `LoaderOptions`."], + ["24", "clientDb", "Create a client instance that connects to the specified database."], + ["30", "register", "Register custom resource in the container."], + ["31", `"mongoClient"`, "The resource's key in the container."], + ["32", "asValue(clientDb)", "The resource to register."] +] - +```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} +import { LoaderOptions } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils"; +import { asValue } from "awilix"; +import { MongoClient } from "mongodb" + +type ModuleOptions = { + connection_url?: string + db_name?: string +} + +export default async function mongoConnectionLoader({ + container, + options +}: LoaderOptions) { + if (!options.connection_url) { + throw new Error(`[MONGO MDOULE]: connection_url option is required.`) + } + if (!options.db_name) { + throw new Error(`[MONGO MDOULE]: db_name option is required.`) + } + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + try { + const clientDb = ( + await (new MongoClient(options.connection_url)).connect() + ).db(options.db_name) + + logger.info("Connected to MongoDB") + + container.register( + "mongoClient", + asValue(clientDb) + ) + } catch (e) { + logger.error( + `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}` + ) + } +} +``` + +The loader function accepts in its object parameter an `options` property, which is the options passed to the module in Medusa's configurations. For example: + +export const optionHighlights = [ + ["6", "options", "The options to pass to the module."] +] + +```ts title="medusa-config.ts" highlights={optionHighlights} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/mongo", + options: { + connection_url: process.env.MONGO_CONNECTION_URL, + db_name: process.env.MONGO_DB_NAME + } + } + ] +}) +``` + +Passing options is useful when your module needs informations like connection URLs or API keys, as it ensures your module can be re-usable across applications. For the MongoDB Module, you expect two options: + +- `connection_url`: the URL to connect to the MongoDB database. +- `db_name`: The name of the database to connect to. + +In the loader, you check first that these options are set before proceeding. Then, you create an instance of the MongoDB client and connect to the database specified in the options. + +After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: + +1. The key to register the resource under, which in this case is `mongoClient`. You'll use this name later to resolve the client. +2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [awilix package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa. + +### Use Custom Registered Resource in Module's Service + +After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service. + +For example: + +export const serviceHighlights = [ + ["10", "mongoClient", "Resolve the MongoDB client from the container."], + ["11", "mongoClient_", "Set the MongoDB client as a class property."], + ["14", "createMovie", "Add a method that uses the MongoDB client to create a document."], + ["30", "deleteMovie", "Add a method that uses the MongoDB client to delete a document."] +] + +```ts title="src/modules/mongo/service.ts" +import type { Db } from "mongodb" + +type InjectedDependencies = { + mongoClient: Db +} + +export default class MongoModuleService { + private mongoClient_: Db + + constructor({ mongoClient }: InjectedDependencies) { + this.mongoClient_ = mongoClient + } + + async createMovie({ title }: { + title: string + }) { + const moviesCol = this.mongoClient_.collection("movie") + + const insertedMovie = await moviesCol.insertOne({ + title + }) + + const movie = await moviesCol.findOne({ + _id: insertedMovie.insertedId + }) + + return movie + } + + async deleteMovie(id: string) { + const moviesCol = this.mongoClient_.collection("movie") + + await moviesCol.deleteOne({ + _id: { + equals: id + } + }) + } +} +``` + +The service `MongoModuleService` resolves the `mongoClient` resource you registered in the loader and sets it as a class property. You then use it in the `createMovie` and `deleteMovie` methods, which create and delete a document in a `movie` collection in the MongoDB database, respectively. + +Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: + +```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} +import { Module } from "@medusajs/framework/utils" +import MongoModuleService from "./service" +import mongoConnectionLoader from "./loaders/connection" + +export const MONGO_MODULE = "mongo" + +export default Module(MONGO_MODULE, { + service: MongoModuleService, + loaders: [mongoConnectionLoader] +}) +``` + +### Test it Out + +You can test the connection out by starting the Medusa application. If it's successful, you'll see the following message logged in the terminal: + +```bash +info: Connected to MongoDB +``` + +You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 3f256c41663b4..77f807d7b6563 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -26,7 +26,7 @@ export const generatedEditDates = { "app/learn/basics/events-and-subscribers/page.mdx": "2024-09-30T08:43:53.131Z", "app/learn/advanced-development/modules/container/page.mdx": "2024-11-21T08:59:18.707Z", "app/learn/advanced-development/workflows/execute-another-workflow/page.mdx": "2024-09-30T08:43:53.129Z", - "app/learn/basics/loaders/page.mdx": "2024-09-03T08:00:45.993Z", + "app/learn/basics/loaders/page.mdx": "2024-11-22T10:51:32.931Z", "app/learn/advanced-development/admin/widgets/page.mdx": "2024-10-07T12:51:09.969Z", "app/learn/advanced-development/data-models/page.mdx": "2024-09-19T07:26:43.535Z", "app/learn/advanced-development/modules/remote-link/page.mdx": "2024-09-30T08:43:53.127Z",