385 lines
11 KiB
Markdown
385 lines
11 KiB
Markdown
# Module Links
|
|
|
|
## Contents
|
|
- [When to Use Links](#when-to-use-links)
|
|
- [Implementing Module Links - Workflow Checklist](#implementing-module-links---workflow-checklist)
|
|
- [Step 1: Defining a Link](#step-1-defining-a-link)
|
|
- [Step 2: Link Configuration Options](#step-2-link-configuration-options)
|
|
- [List Links (One-to-Many)](#list-links-one-to-many)
|
|
- [Delete Cascades](#delete-cascades)
|
|
- [Step 3: Sync Links (Run Migrations)](#step-3-sync-links-run-migrations)
|
|
- [Step 4: Managing Links](#step-4-managing-links)
|
|
- [Step 5: Querying Linked Data](#step-5-querying-linked-data)
|
|
- [Advanced: Link with Custom Columns](#advanced-link-with-custom-columns)
|
|
|
|
Module links create associations between data models in different modules while maintaining module isolation. Use links to connect your custom models to Commerce Module models (products, customers, orders, etc.).
|
|
|
|
## When to Use Links
|
|
|
|
- **Extend commerce entities**: Add brands to products, wishlists to customers
|
|
- **Cross-module associations**: Connect custom modules to each other
|
|
- **Maintain isolation**: Keep modules independent and reusable
|
|
|
|
## Implementing Module Links - Workflow Checklist
|
|
|
|
**IMPORTANT FOR CLAUDE CODE**: When implementing module links, use the TodoWrite tool to track your progress through these steps. This ensures you don't miss any critical steps and provides visibility to the user.
|
|
|
|
Create these tasks in your todo list:
|
|
|
|
- Optional: Add linked ID in custom data model (if one-to-one or one-to-many)
|
|
- Define the link in src/links/
|
|
- Configure list or delete cascade options if needed
|
|
- **CRITICAL: Run migrations: npx medusa db:migrate** (Never skip this step!)
|
|
- Create links in code using link.create() or createRemoteLinkStep
|
|
- Query linked data using query.graph()
|
|
- **CRITICAL: Run build to validate implementation** (catches type errors and issues)
|
|
|
|
## Optional: Add Linked ID in Custom Data Model
|
|
|
|
Add the ID of a linked data model in the custom data model if the custom data model belongs to it or extends it. Otherwise, skip this step.
|
|
|
|
For example, add ID of customer and product to custom product review model:
|
|
|
|
```typescript
|
|
import { model } from "@medusajs/framework/utils"
|
|
|
|
const Review = model.define("review", {
|
|
// other properties...
|
|
// ID of linked customer
|
|
customer_id: model.text(),
|
|
// ID of linked product
|
|
product_id: model.text()
|
|
})
|
|
|
|
export default Review
|
|
```
|
|
|
|
## Step 1: Defining a Link
|
|
|
|
**⚠️ CRITICAL RULE: Create ONE link definition per file.** Do NOT export an array of links from a single file.
|
|
|
|
Create link files in `src/links/`:
|
|
|
|
```typescript
|
|
// ✅ CORRECT - src/links/product-brand.ts (one link per file)
|
|
import { defineLink } from "@medusajs/framework/utils"
|
|
import ProductModule from "@medusajs/medusa/product"
|
|
import BrandModule from "../modules/brand"
|
|
|
|
export default defineLink(
|
|
ProductModule.linkable.product,
|
|
BrandModule.linkable.brand
|
|
)
|
|
```
|
|
|
|
**If one model links to multiple others, create multiple files:**
|
|
|
|
```typescript
|
|
// ✅ CORRECT - src/links/review-product.ts
|
|
export default defineLink(
|
|
ReviewModule.linkable.review,
|
|
ProductModule.linkable.product
|
|
)
|
|
|
|
// ✅ CORRECT - src/links/review-customer.ts
|
|
export default defineLink(
|
|
ReviewModule.linkable.review,
|
|
CustomerModule.linkable.customer
|
|
)
|
|
|
|
// ❌ WRONG - Don't export array of links from one file
|
|
export default [
|
|
defineLink(ReviewModule.linkable.review, ProductModule.linkable.product),
|
|
defineLink(ReviewModule.linkable.review, CustomerModule.linkable.customer),
|
|
] // This doesn't work!
|
|
```
|
|
|
|
**IMPORTANT:** The `.linkable` property is **automatically added** to all modules by Medusa. You do NOT need to add `.linkable()` or any linkable definition to your data models. Simply use `ModuleName.linkable.modelName` when defining links.
|
|
|
|
For example, if you have a `Review` data model in a `ReviewModule`:
|
|
- ✅ CORRECT: `ReviewModule.linkable.review` (works automatically)
|
|
- ❌ WRONG: Adding `.linkable()` method to the Review model definition (not needed, causes errors)
|
|
|
|
**⚠️ NEXT STEP**: After defining a link, you MUST immediately proceed to Step 3 to run migrations (`npx medusa db:migrate`). Do not skip this step!
|
|
|
|
## Step 2: Link Configuration Options
|
|
|
|
### List Links (One-to-Many)
|
|
|
|
Allow multiple records to link to one record:
|
|
|
|
```typescript
|
|
// A brand can have many products
|
|
export default defineLink(
|
|
{
|
|
linkable: ProductModule.linkable.product,
|
|
isList: true,
|
|
},
|
|
BrandModule.linkable.brand
|
|
)
|
|
```
|
|
|
|
### Delete Cascades
|
|
|
|
Automatically delete links when a record is deleted:
|
|
|
|
```typescript
|
|
export default defineLink(ProductModule.linkable.product, {
|
|
linkable: BrandModule.linkable.brand,
|
|
deleteCascade: true,
|
|
})
|
|
```
|
|
|
|
## Step 3: Sync Links (Run Migrations)
|
|
|
|
**⚠️ CRITICAL - DO NOT SKIP**: After defining links, you MUST run migrations to sync the link to the database. Without this step, the link will not work and you will get runtime errors.
|
|
|
|
```bash
|
|
npx medusa db:migrate
|
|
```
|
|
|
|
**Why this matters:**
|
|
- Links create database tables that store the relationships between modules
|
|
- Without migrations, these tables don't exist and link operations will fail
|
|
- This step is REQUIRED before creating any links in code or querying linked data
|
|
|
|
**Common mistake:** Defining a link in `src/links/` and immediately trying to use it in a workflow or query without running migrations first. Always run migrations immediately after defining a link.
|
|
|
|
## Step 4: Managing Links
|
|
|
|
**⚠️ CRITICAL - Link Order (Direction):** When creating or dismissing links, the order of modules MUST match the order in `defineLink()`. Mismatched order causes runtime errors.
|
|
|
|
```typescript
|
|
// Example link definition: product FIRST, then brand
|
|
export default defineLink(
|
|
ProductModule.linkable.product,
|
|
BrandModule.linkable.brand
|
|
)
|
|
```
|
|
|
|
### In Workflow Composition Functions
|
|
|
|
To create a link between records in workflow composition functions, use the `createRemoteLinkStep`:
|
|
|
|
```typescript
|
|
import { Modules } from "@medusajs/framework/utils"
|
|
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
|
|
import {
|
|
createWorkflow,
|
|
transform,
|
|
} from "@medusajs/framework/workflows-sdk"
|
|
|
|
const BRAND_MODULE = "brand"
|
|
|
|
export const myWorkflow = createWorkflow(
|
|
"my-workflow",
|
|
function (input) {
|
|
// ...
|
|
// ✅ CORRECT - Order matches defineLink (product first, then brand)
|
|
const linkData = transform({ input }, ({ input }) => {
|
|
return [
|
|
{
|
|
[Modules.PRODUCT]: {
|
|
product_id: input.product_id,
|
|
},
|
|
[BRAND_MODULE]: {
|
|
brand_id: input.brand_id,
|
|
},
|
|
},
|
|
]
|
|
})
|
|
|
|
createRemoteLinkStep(linkData)
|
|
// ...
|
|
}
|
|
)
|
|
|
|
// ❌ WRONG - Order doesn't match defineLink
|
|
const linkData = transform({ input }, ({ input }) => {
|
|
return [
|
|
{
|
|
[BRAND_MODULE]: {
|
|
brand_id: input.brand_id,
|
|
},
|
|
[Modules.PRODUCT]: {
|
|
product_id: input.product_id,
|
|
},
|
|
},
|
|
]
|
|
}) // Runtime error: link direction mismatch!
|
|
```
|
|
|
|
To dismiss (remove) a link between records in workflow composition functions, use the `dismissRemoteLinkStep`:
|
|
|
|
```typescript
|
|
import { Modules } from "@medusajs/framework/utils"
|
|
import { dismissRemoteLinkStep } from "@medusajs/medusa/core-flows"
|
|
import {
|
|
createWorkflow,
|
|
transform,
|
|
} from "@medusajs/framework/workflows-sdk"
|
|
|
|
const BRAND_MODULE = "brand"
|
|
|
|
export const myWorkflow = createWorkflow(
|
|
"my-workflow",
|
|
function (input) {
|
|
// ...
|
|
// Order MUST match defineLink (product first, then brand)
|
|
const linkData = transform({ input }, ({ input }) => {
|
|
return [
|
|
{
|
|
[Modules.PRODUCT]: {
|
|
product_id: input.product_id,
|
|
},
|
|
[BRAND_MODULE]: {
|
|
brand_id: input.brand_id,
|
|
},
|
|
},
|
|
]
|
|
})
|
|
|
|
dismissRemoteLinkStep(linkData)
|
|
// ...
|
|
}
|
|
)
|
|
```
|
|
|
|
### Outside Workflows
|
|
|
|
Outside workflows or in workflow steps, use the `link` utility to create and manage links between records. **Order MUST match `defineLink()` here too:**
|
|
|
|
```typescript
|
|
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
|
|
|
// In an API route or workflow step
|
|
const link = container.resolve(ContainerRegistrationKeys.LINK)
|
|
|
|
const BRAND_MODULE = "brand"
|
|
|
|
// ✅ CORRECT - Create a link (order matches defineLink: product first, then brand)
|
|
await link.create({
|
|
[Modules.PRODUCT]: { product_id: "prod_123" },
|
|
[BRAND_MODULE]: { brand_id: "brand_456" },
|
|
})
|
|
|
|
// ✅ CORRECT - Dismiss (remove) a link (same order: product first, then brand)
|
|
await link.dismiss({
|
|
[Modules.PRODUCT]: { product_id: "prod_123" },
|
|
[BRAND_MODULE]: { brand_id: "brand_456" },
|
|
})
|
|
|
|
// ❌ WRONG - Order doesn't match defineLink
|
|
await link.create({
|
|
[BRAND_MODULE]: { brand_id: "brand_456" },
|
|
[Modules.PRODUCT]: { product_id: "prod_123" },
|
|
}) // Runtime error: link direction mismatch!
|
|
```
|
|
|
|
## Step 5: Querying Linked Data
|
|
|
|
### Using query.graph() - Retrieve Linked Data
|
|
|
|
Use `query.graph()` to fetch data across linked modules. **Note**: `query.graph()` can retrieve linked data but **cannot filter by properties of linked modules** (data models in separate modules).
|
|
|
|
```typescript
|
|
const query = container.resolve("query")
|
|
|
|
// ✅ Get products with their linked brands (no cross-module filtering)
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "brand.*"], // brand.* fetches linked brand data
|
|
filters: {
|
|
id: "prod_123", // ✅ Filter by product properties only
|
|
},
|
|
})
|
|
|
|
// ✅ Get brands with their linked products
|
|
const { data: brands } = await query.graph({
|
|
entity: "brand",
|
|
fields: ["id", "name", "products.*"],
|
|
})
|
|
|
|
// ❌ DOES NOT WORK: Cannot filter products by linked brand properties
|
|
const { data: products } = await query.graph({
|
|
entity: "product",
|
|
fields: ["id", "title", "brand.*"],
|
|
filters: {
|
|
brand: {
|
|
name: "Nike" // ❌ Fails: brand is in a different module
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
### Using query.index() - Filter Across Linked Modules
|
|
|
|
To filter by properties of linked modules (separate modules with module links), use `query.index()` from the Index Module:
|
|
|
|
```typescript
|
|
const query = container.resolve("query")
|
|
|
|
// ✅ Filter products by linked brand name using Index Module
|
|
const { data: products } = await query.index({
|
|
entity: "product",
|
|
fields: ["*", "brand.*"],
|
|
filters: {
|
|
brand: {
|
|
name: "Nike" // ✅ Works with Index Module!
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
**Key Distinction:**
|
|
- **Same module relations** (e.g., Product → ProductVariant): Use `query.graph()` - filtering works ✅
|
|
- **Different module links** (e.g., Product → Brand): Use `query.index()` for filtering ✅
|
|
|
|
**Index Module Requirements:**
|
|
1. Install `@medusajs/index` package
|
|
2. Add to `medusa-config.ts`
|
|
3. Enable `MEDUSA_FF_INDEX_ENGINE=true` in `.env`
|
|
4. Run `npx medusa db:migrate`
|
|
5. Mark properties as `filterable` in link definition:
|
|
|
|
```typescript
|
|
// src/links/product-brand.ts
|
|
defineLink(
|
|
{ linkable: ProductModule.linkable.product, isList: true },
|
|
{ linkable: BrandModule.linkable.brand, filterable: ["id", "name"] }
|
|
)
|
|
```
|
|
|
|
See the [Querying Data reference](querying-data.md#querying-linked-data) for complete details on both methods.
|
|
|
|
## Advanced: Link with Custom Columns
|
|
|
|
Add extra data to the link table:
|
|
|
|
```typescript
|
|
export default defineLink(
|
|
ProductModule.linkable.product,
|
|
BrandModule.linkable.brand,
|
|
{
|
|
database: {
|
|
extraColumns: {
|
|
featured: {
|
|
type: "boolean",
|
|
defaultValue: "false",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
```
|
|
|
|
Set custom column values when creating links:
|
|
|
|
```typescript
|
|
await link.create({
|
|
product: { product_id: "prod_123" },
|
|
brand: { brand_id: "brand_456" },
|
|
data: { featured: true },
|
|
})
|
|
```
|