Module Links

In this chapter, you'll learn what a module link is and how to define one.

A module is isolated: it can't have a foreign key to another module's table, or a relationship to its data models. But commerce features often need to relate data across modules. For example, a blog post is about a product, or a customer wrote a comment.

A module link forms an association between two data models of different modules, while the modules stay isolated. You define the link in code, and MedusaGo stores the links in a table of its own, in the database.

You can then:

Note: To relate data models of the same module, use relationships instead. They create foreign keys, so they're more efficient than links.

You define a module link with the DefineLink function of the modules-sdk of the utils package. You call it with the two sides of the link: the linkable data models of two modules.

A module returns the linkable data models in the Linkable field of its definition, a map whose keys are the names of the data models, with the first letter in lower case. For example, the Blog Module of the previous chapters has post, comment, and tag.

MedusaGo's modules have one too. LinkableOf returns it from the exports of a module in the modules package: utilsmodulessdk.LinkableOf(&modules.Product)["product"] is the Product data model of the Product Module, and ["productVariant"] is its ProductVariant data model.

Create the file src/links/post-product.go with a function that defines the link between a post and a product:

src/links/post-product.go
// DefinePostProduct links posts and products: a post features many products
// and a product appears in many posts.
func DefinePostProduct() error {
	post := blog.Module.Linkable["post"].ToJSON()
	product := utilsmodulessdk.LinkableOf(&modules.Product)["product"].ToJSON()

	_, err := utilsmodulessdk.DefineLink(
		utilsmodulessdk.DefineLinkInput{Linkable: post, IsList: true},
		utilsmodulessdk.DefineLinkInput{Linkable: product, IsList: true},
	)
	return err
}

The modules package, which holds the exports of MedusaGo's modules, is github.com/hitkey-io/medusago/packages/medusa/src/modules, and utilsmodulessdk is the modules-sdk package of utils. The DefineLink function takes the two sides, in the order they're defined. Each side is a linkable that you can wrap in a DefineLinkInput to set its options, such as IsList.

DefineLink registers the link in memory, so you call it before the application loads, in all commands. Call your link functions at the beginning of main, before cli.Run:

main.go
func main() {
	// Links must be defined before the application is loaded.
	if err := links.Define(); err != nil {
		log.Fatal(err)
	}

	os.Exit(cli.Run(os.Args[1:], cli.Options{
		Config: Config,
		Scripts: map[string]typescommon.ExecScript{
			"my-script": scripts.MyScript,
			"greet":     scripts.Greet,
			"seed":      scripts.Seed,
			"test-blog": scripts.TestBlog,
		},
	}))
}

The Define function of the example calls the link functions of the project:

src/links/links.go
// Define registers the links of the project. Call it before the
// application is loaded, for example at the start of main.
func Define() error {
	for _, define := range []func() error{
		DefinePostProduct,
		DefinePostSalesChannel,
		DefinePostAuthor,
	} {
		if err := define(); err != nil {
			return err
		}
	}
	return nil
}
Note: Module links are defined in Go code, so a project that uses medusa-config.json or medusa-config.yaml with the stock medusa binary can't define them. Use a Go project, as created by create-medusago-app.

A module link is stored in a link table in the database. The table is created when you sync the links. Run the following command:

Terminal
go run . db:migrate

This command runs the migrations of the modules, and syncs the links. You can also sync only the links:

Terminal
go run . db:sync-links

The sync:

  • Creates the link tables of the links that don't have one.
  • Reports, without changing them, the link tables that differ from their definition, for example when you add custom columns to a link whose table exists.
  • Skips the tables of the links that you removed, and logs a warning, unless you pass --execute-all to db:sync-links, or --execute-all-links to db:migrate. With the option, it drops the tables of the removed links, and the links in them.

The link table of the link above is named blog_post_product_product, after the two modules and their keys. It has these columns:

| Column | Description | | --- | --- | | id | The ID of the link. | | post_id | The ID of the post. | | product_id | The ID of the product. | | created_at, updated_at, deleted_at | The dates the link was created, updated, and soft-deleted. |

The primary key of the table is the pair of the two IDs, so a post and a product can be linked once. Link tables are tracked in the link_module_migrations table.


Use a module link when you want to relate a data model of your module to a data model of another module, and keep the modules isolated. Use it also when you want to extend a Commerce Module's data model with your own data without changing the module, such as a brand for products, or a post that features a product.

Don't use a link, but a relationship, for data models of the same module.


By default, a module link is one-to-one: a record of each side can be linked to one record of the other side. For example, the following link relates a post to one product variant, and a variant to one post:

src/links/post-variant.go
// DefinePostVariant links a post to one product variant, and a variant to one
// post.
func DefinePostVariant() error {
	post := blog.Module.Linkable["post"]
	variant := utilsmodulessdk.LinkableOf(&modules.Product)["productVariant"]

	_, err := utilsmodulessdk.DefineLink(post, variant)
	return err
}

To allow a record to be linked to many records of the other data model, set IsList to true on the side that can have many. For example, a customer can have many comments, but a comment is linked to one customer:

src/links/customer-comment.go
// DefineCustomerComment links a customer to many comments, while a comment
// belongs to at most one customer.
func DefineCustomerComment() error {
	customer := utilsmodulessdk.LinkableOf(&modules.Customer)["customer"]
	comment := blog.Module.Linkable["comment"].ToJSON()

	_, err := utilsmodulessdk.DefineLink(
		customer,
		utilsmodulessdk.DefineLinkInput{Linkable: comment, IsList: true},
	)
	return err
}

IsList is on the comment side: many comments can be linked to a customer.

To allow a record of each side to be linked to many records of the other side, set IsList to true on both. The link between posts and products does that: a post features many products, and a product appears in many posts.

When you retrieve the records with Query, a list side is retrieved as a list, with the plural of its name: products for a post, and posts for a product.


When a record is deleted, you may want to delete the records linked to it. Set DeleteCascade to true on the side whose records should be deleted with the other.

For example, to delete the posts of a sales channel when the sales channel is deleted:

src/links/sales-channel-post.go
// DefineSalesChannelPost links a sales channel to many posts, and deletes the
// posts of a sales channel when the channel is deleted.
func DefineSalesChannelPost() error {
	salesChannel := utilsmodulessdk.LinkableOf(&modules.SalesChannel)["salesChannel"]
	post := blog.Module.Linkable["post"].ToJSON()

	_, err := utilsmodulessdk.DefineLink(
		salesChannel,
		utilsmodulessdk.DefineLinkInput{Linkable: post, IsList: true, DeleteCascade: true},
	)
	return err
}

The cascade takes effect when the records are deleted through the Link service: you call its Delete method, or a workflow that does. The workflow that deletes sales channels removes their links with the RemoveRemoteLinkStep step, which calls it.


When you retrieve the linked records with Query, you use the alias of the linked data model: the name of its linkable key without _id, such as product or sales_channel. To use another property name, set the Alias of a side:

src/links/post-sales-channel.go
// DefinePostSalesChannelAlias links posts to sales channels and renames the
// property the linked posts are retrieved with.
func DefinePostSalesChannelAlias() error {
	post := blog.Module.Linkable["post"].ToJSON()
	salesChannel := utilsmodulessdk.LinkableOf(&modules.SalesChannel)["salesChannel"].ToJSON()

	_, err := utilsmodulessdk.DefineLink(
		utilsmodulessdk.DefineLinkInput{Linkable: post, Alias: "article", IsList: true},
		salesChannel,
	)
	return err
}

With this definition, a sales channel retrieves its posts with the articles property instead of posts, and the entry point of the link is article_sales_channel.


To remove a link, delete its definition from the project, and its call in Define. Then sync the links with the option that drops the tables of the removed links:

Terminal
go run . db:sync-links --execute-all
Warning: This drops the link table and the links in it. Without --execute-all, MedusaGo logs a warning and keeps the table.
Found a problem with this page?Report Issue
Edited Nov 14·Edit this page