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 work with data that isn't stored in your database, such as the records of a third-party system.

Instead of defining a module link whose links are stored in a link table, 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. Instead, the ID of the linked record is stored in a property of the first data model.

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. It lets you access the customer of a cart without managing a link: the customer's ID is stored in the cart's customer_id property.


The DefineLink function takes an optional third parameter with options for the link. To make the link read-only, set its ReadOnly field to true. You must also set the Field of the first data model's side, which is the property that stores the linked record's ID. It needs the side to be a DefineLinkInput.

For example:

src/links/post-author.go
// DefinePostAuthor lets a post retrieve its author, the admin user whose ID
// is stored in post.author_id.
func DefinePostAuthor() error {
	post := blog.Module.Linkable["post"].ToJSON()
	user := utilsmodulessdk.LinkableOf(&modules.User)["user"].ToJSON()

	_, err := utilsmodulessdk.DefineLink(
		utilsmodulessdk.DefineLinkInput{Linkable: post, Field: "author_id"},
		user,
		utilsmodulessdk.DefineLinkOptions{ReadOnly: true},
	)
	return err
}

In this example, you define a read-only link from the post data model of the Blog Module to the user data model of the User Module. You do that by:

  • Passing a DefineLinkInput as the first side. It holds the linkable and the Field, author_id.
  • Setting ReadOnly to true in the options.

Unlike a stored link, MedusaGo doesn't create a table for this link. It uses the ID stored in the field of the first data model to retrieve the linked record.

Note: A read-only link doesn't support the Filterable option. DefineLink returns an error if you set it.

Retrieve Read-Only Linked Record#

Query retrieves the records of a read-only link. The property to retrieve is the alias of the second data model, user:

src/api/posts.go
// PostsWithAuthors retrieves posts with their authors, through the read-only
// link between post.author_id and the user module.
func PostsWithAuthors(ctx context.Context, c Resolver) ([]any, error) {
	res, err := queryOf(c).Graph(ctx, query.GraphInput{
		Entity: "post",
		Fields: []string{"id", "title", "user.email", "user.first_name"},
	}, joiner.Options{})
	if err != nil {
		return nil, err
	}
	return res.Data, nil
}

Query lists the posts, then retrieves the users whose IDs are in the author_id of the posts.


A read-only module link is unidirectional. You can only retrieve the linked record from the first data model. In the example, you retrieve the author of a post, but not the posts of a user.

To retrieve the posts of a user, define another read-only link in the opposite direction.


An inverse read-only module link retrieves linked records based on the ID stored in the second data model.

For example, to retrieve the posts of a user, define a read-only link from the User Module's user data model to the Blog Module's post data model:

src/links/author-posts.go
// DefineAuthorPosts is the inverse of DefinePostAuthor: a user retrieves the
// posts whose author_id is the user's ID. IsList makes it a list, as an author
// has many posts.
func DefineAuthorPosts() error {
	user := utilsmodulessdk.LinkableOf(&modules.User)["user"].ToJSON()
	post := blog.Module.Linkable["post"].ToJSON()
	// the field of the post that holds the user's ID
	post.PrimaryKey = "author_id"

	_, err := utilsmodulessdk.DefineLink(
		utilsmodulessdk.DefineLinkInput{Linkable: user, Field: "id", IsList: true},
		post,
		utilsmodulessdk.DefineLinkOptions{ReadOnly: true},
	)
	return err
}

Since you can't add a field to the user data model, you:

  1. Set the Field of the first side to the user's id.
  2. Take the linkable of the post data model and set its PrimaryKey to the field of the post that holds the user's ID, author_id.
  3. Set IsList on the first side, since a user can have many posts.

You can now retrieve a user and their posts:

src/api/users.go
// UsersWithPosts retrieves users with the posts they wrote, through the
// inverse read-only link.
func UsersWithPosts(ctx context.Context, c Resolver) ([]any, error) {
	res, err := queryOf(c).Graph(ctx, query.GraphInput{
		Entity: "user",
		Fields: []string{"id", "email", "posts.id", "posts.title"},
	}, joiner.Options{})
	if err != nil {
		return nil, err
	}
	return res.Data, nil
}

With IsList, the property is a list. Both posts and post retrieve it.


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

The records that a read-only link retrieves are one object or a list of objects, depending on the link's options:

  • By default, the first data model's field holds one ID, so the link is one-to-one: the property is an object.
  • When IsList is set, in the first side's DefineLinkInput or in the DefineLinkOptions, the property is a list.
Found a problem with this page?Report Issue
Edited Nov 14·Edit this page