3.3.5. Read-Only Module Link

In this chapter, you’ll learn what a read-only module link is and how to define one.

Consider a scenario where you need to access related records from another module, but don't want the overhead of managing or storing the links between them. This can include cases where you're working with external data models not stored in your Medusa database, such as third-party systems.

In those cases, instead of defining a Module Link whose linked records must be stored in a link table in the database, you can use a read-only module link. A read-only module link builds a virtual relation from one data model to another in a different module without creating a link table in the database. Instead, the linked record's ID is stored in the first data model's field.

For example, Medusa creates a read-only module link from the Cart data model of the Cart Module to the Customer data model of the Customer Module. This link allows you to access the details of the cart's customer without managing the link. Instead, the customer's ID is stored in the Cart data model.

Diagram illustrating the read-only module link from cart to customer


The defineLink function accepts an optional third-parameter object that can hold additional configurations for the module link.

NoteIf you're not familiar with the defineLink function, refer to the Module Links chapter for more information.

To make the module link read-only, pass the readOnly property as true. You must also set in the link configuration of the first data model a field property that specifies the data model's field where the linked record's ID is stored.

For example:

Code
1import BlogModule from "../modules/blog"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/utils"4
5export default defineLink(6  {7    linkable: BlogModule.linkable.post,8    field: "product_id",9  },10  ProductModule.linkable.product,11  {12    readOnly: true13  }14)

In this example, you define a read-only module link from the Blog Module's post data model to the Product Module's product data model. You do that by:

  • Passing an object as a first parameter that accepts the linkable configuration and the field where the linked record's ID is stored.
  • Setting the readOnly property to true in the third parameter.

Unlike the stored module link, Medusa will not create a table in the database for this link. Instead, Medusa uses the ID stored in the specified field of the first data model to retrieve the linked record.


Retrieve Read-Only Linked Record#

Query allows you to retrieve records linked through a read-only module link.

For example, assuming you have the module link created in the above section, you can retrieve a post and its linked product as follows:

Code
1const { result } = await query.graph({2  entity: "post",3  fields: ["id", "product.*"],4  filters: {5    id: "post_123"6  }7})

In the above example, you retrieve a post and its linked product. Medusa will use the ID of the product in the post's product_id field to determine which product should be retrieved.


A read-only module is uni-directional. So, you can only retrieve the linked record from the first data model. If you need to access the linked record from the second data model, you must define another read-only module link in the opposite direction.

In the blog -> product example, you can access a post's product, but you can't access a product's posts. You would have to define another read-only module link from product to blog to access a product's posts.


An inverse read-only module link is a read-only module link that allows you to access the linked record based on the ID stored in the second data model.

For example, consider you want to access a product's posts. You can define a read-only module link from the Product Module's product data model to the Blog Module's post data model:

Code
1import BlogModule from "../modules/blog"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/utils"4
5export default defineLink(6  {7    linkable: ProductModule.linkable.product,8    field: "id",9  },10  {11    ...BlogModule.linkable.post.id,12    primaryKey: "product_id",13  },14  {15    readOnly: true16  }17)

In the above example, you define a read-only module link from the Product Module's product data model to the Blog Module's post data model. This link allows you to access a product's posts.

Since you can't add a post_id field to the product data model, you must:

  1. Set the field property in the first data model's link configuration to the product's ID field.
  2. Spread the BlogModule.linkable.post.id object in the second parameter object and set the primaryKey property to the field in the post data model that holds the product's ID.

You can now retrieve a product and its linked posts:

Code
1const { result } = await query.graph({2  entity: "product",3  fields: ["id", "post.*"],4  filters: {5    id: "prod_123"6  }7})

One-to-One or One-to-Many?#

When you retrieve the linked record through a read-only module link, the retrieved data may be an object (one-to-one) or an array of objects (one-to-many) based on different criteria.

ScenarioRelation Type

The first data model's field is of type string.

One-to-one relation

The first data model's field is of type array of strings.

One-to-many relation

The read-only module link is inversed.

One-to-many relation if multiple records in the second data model have the same ID of the first data model. Otherwise, one-to-one relation.

One-to-One Relation#

Consider the first read-only module link you defined in this chapter:

Code
1import BlogModule from "../modules/blog"2import ProductModule from "@medusajs/medusa/product"3
4export default defineLink(5  {6    linkable: BlogModule.linkable.post,7    field: "product_id",8  },9  ProductModule.linkable.product,10  {11    readOnly: true12  }13)

Since the product_id field of a post stores the ID of a single product, the link is a one-to-one relation. When querying a post, you'll get a single product object:

Example Data
1[2  {3    "id": "post_123",4    "product_id": "prod_123",5    "product": {6      "id": "prod_123",7      // ...8    }9  }10]

One-to-Many Relation#

Consider the read-only module link from the post data model uses an array of product IDs:

Code
1import BlogModule from "../modules/blog"2import ProductModule from "@medusajs/medusa/product"3
4export default defineLink(5  {6    linkable: BlogModule.linkable.post,7    field: "product_ids",8  },9  ProductModule.linkable.product,10  {11    readOnly: true12  }13)

Where product_ids in the post data model is an array of strings. In this case, the link would be a one-to-many relation. So, an array of products would be returned when querying a post:

Example Data
1[2  {3    "id": "post_123",4    "product_ids": ["prod_123", "prod_124"],5    "product": [6      {7        "id": "prod_123",8        // ...9      },10      {11        "id": "prod_124",12        // ...13      }14    ]15  }16]

If you define an inversed read-only module link where the ID of the linked record is stored in the second data model, the link can be either one-to-one or one-to-many based on the number of records in the second data model that have the same ID of the first data model.

For example, consider the product -> post link you defined in an earlier section:

Code
1import BlogModule from "../modules/blog"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/utils"4
5export default defineLink(6  {7    linkable: ProductModule.linkable.product,8    field: "id",9  },10  {11    ...BlogModule.linkable.post.id,12    primaryKey: "product_id",13  },14  {15    readOnly: true16  }17)

In the above snippet, the ID of the product is stored in the post's product_id string field.

When you retrieve the post of a product, it may be a post object, or an array of post objects if multiple posts are linked to the product:

Example Data
1[2  {3    "id": "prod_123",4    "post": {5      "id": "post_123",6      "product_id": "prod_123"7      // ...8    }9  },10  {11    "id": "prod_321",12    "post": [13      {14        "id": "post_123",15        "product_id": "prod_321"16        // ...17      },18      {19        "id": "post_124",20        "product_id": "prod_321"21        // ...22      }23    ]24  }25]

If, however, you use an array field in post, the relation would always be one-to-many:

Example Data
1[2  {3    "id": "prod_123",4    "post": [5      {6        "id": "post_123",7        "product_id": "prod_123"8        // ...9      }10    ]11  }12]

Force One-to-Many Relation

Alternatively, you can force a one-to-many relation by setting isList to true in the first data model's link configuration. For example:

Code
1import BlogModule from "../modules/blog"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/utils"4
5export default defineLink(6  {7    linkable: ProductModule.linkable.product,8    field: "id",9    isList: true10  },11  {12    ...BlogModule.linkable.post.id,13    primaryKey: "product_id"14  },15  {16    readOnly: true17  }18)

In this case, the relation would always be one-to-many, even if only one post is linked to a product:

Example Data
1[2  {3    "id": "prod_123",4    "post": [5      {6        "id": "post_123",7        "product_id": "prod_123"8        // ...9      }10    ]11  }12]

Read-only module links are most useful when working with data models that aren't stored in your Medusa database. For example, data that is stored in a third-party system. In those cases, you can define a read-only module link between a data model in Medusa and the data model in the external system, facilitating the retrieval of the linked data.

To define the read-only module link to a virtual data model, you must:

  1. Create a list method in the custom module's service. This method retrieves the linked records filtered by the ID(s) of the first data model.
  2. Define the read-only module link from the first data model to the virtual data model.
  3. Use Query to retrieve the first data model and its linked records from the virtual data model.

For example, consider you have a third-party Content-Management System (CMS) that you're integrating with Medusa, and you want to retrieve the posts in the CMS associated with a product in Medusa.

To do that, first, create a CMS Module having the following service:

NoteRefer to the Modules chapter to learn how to create a module and its service.
src/modules/cms/service.ts
1type CmsModuleOptions = {2  apiKey: string3}4
5export default class CmsModuleService {6  private client7
8  constructor({}, options: CmsModuleOptions) {9    this.client = new Client(options)10  }11
12  async list(13    filter: {14      id: string | string[]15    }16  ) {17    return this.client.getPosts(filter)18    /**19     * Example of returned data:20     * 21     * [22     *   {23     *     "id": "post_123",24     *     "product_id": "prod_321"25     *   },26     *   {27     *     "id": "post_456",28     *     "product_id": "prod_654"29     *   }30     * ]31    */32  }33}

The above service initializes a client, assuming your CMS has an SDK that allows you to retrieve posts.

The service must have a list method to be part of the read-only module link. This method accepts the ID(s) of the products to retrieve their associated posts. The posts must include the product's ID in a field, such as product_id.

Next, define a read-only module link from the Product Module to the CMS Module:

src/links/product-cms.ts
1import { defineLink } from "@medusajs/framework/utils"2import ProductModule from "@medusajs/medusa/product"3import { CMS_MODULE } from "../modules/cms"4
5export default defineLink(6  {7    linkable: ProductModule.linkable.product,8    field: "id"9  },10  {11    linkable: {12      serviceName: CMS_MODULE,13      alias: "cms_post",14      primaryKey: "product_id",15    }16  },17  {18    readOnly: true,19  }20)

To define the read-only module link, you must pass to defineLink:

  1. The first parameter: an object with the linkable configuration of the data model in Medusa, and the fields that will be passed as a filter to the CMS service. For example, if you want to filter by product title instead, you can pass title instead of id.
  2. The second parameter: an object with the linkable configuration of the virtual data model in the CMS. This object must have the following properties:
    • serviceName: The name of the service, which is the CMS Module's name. Medusa uses this name to resolve the module's service from the Medusa container.
    • alias: The alias to use when querying the linked records. You'll see how that works in a bit.
    • primaryKey: The field in the CMS data model that holds the ID of a product.
  3. The third parameter: an object with the readOnly property set to true.

Now, you can use Query to retrieve a product and its linked post from the CMS:

Code
1const { data } = await query.graph({2  entity: "product",3  fields: ["id", "cms_post.*"],4})

In the above example, each product that has a CMS post with the product_id field set to the product's ID will be retrieved:

Example Data
1[2  {3    "id": "prod_123",4    "cms_post": {5      "id": "post_123",6      "product_id": "prod_123",7      // ...8    }9  }10]

If multiple posts have their product_id set to a product's ID, an array of posts is returned instead:

Example Data
1[2  {3    "id": "prod_123",4    "cms_post": [5      {6        "id": "post_123",7        "product_id": "prod_123",8        // ...9      },10      {11        "id": "post_124",12        "product_id": "prod_123",13        // ...14      }15    ]16  }17]
Recommended TutorialSanity Integration Tutorial.
Was this chapter helpful?
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break