Skip to content

Commit

Permalink
docs: revise loaders documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
shahednasser committed Nov 22, 2024
1 parent 58f24a3 commit c34e8d9
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 27 deletions.
263 changes: 237 additions & 26 deletions www/apps/book/app/learn/basics/loaders/page.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Prerequisites } from "docs-ui"

export const metadata = {
title: `${pageNumber} Loaders`,
}
Expand All @@ -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.

<Note title="Why are modules isolated?">

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.
</Note>

---

## 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:
<Note title="Tip">

Learn how to create a module in [this chapter](../modules/page.mdx).

</Note>

```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.

<Note title="Tip">

Find the list of resources in the module's container in [this reference](!resources!/medusa-container-resources#module-container-resources).

</Note>

### 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...
Expand All @@ -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, youll 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

<Note title="Use loaders when" type="success">
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.

</Note>
<Prerequisites
items={[
{
text: "MongoDB database that you can connect to from a local machine.",
link: "https://www.mongodb.com"
},
{
text: "Install the MongoDB SDK in your Medusa application.",
link: "https://www.mongodb.com/docs/drivers/node/current/quick-start/download-and-install/#install-the-node.js-driver"
}
]}
/>

<Note title="Don't use loaders if" type="error">
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."]
]

</Note>
```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<ModuleOptions>) {
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.
2 changes: 1 addition & 1 deletion www/apps/book/generated/edit-dates.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit c34e8d9

Please sign in to comment.