Link

In this chapter, you'll learn about the Link service, which you use to create, remove, and list the links between records of different modules.

Link is a service of the framework. It manages the records of the link tables that you define with DefineLink: it creates and removes links, lists them, and cascades the deletion of a record to the records linked to it.

It's registered in the container under the key common.ContainerRegistrationKeys.LINK, and its type is *modulessdk.Link, in the modules-sdk of the core package. You resolve it like any resource of the container. These are the imports of the examples of this chapter:

src/links/usage.go
import (
	"context"

	commonsteps "github.com/hitkey-io/medusago/packages/core/core-flows/src/common/steps"
	modulessdk "github.com/hitkey-io/medusago/packages/core/modules-sdk/src"
	"github.com/hitkey-io/medusago/packages/core/query/src/query"
	"github.com/hitkey-io/medusago/packages/core/utils/src/common"
	"github.com/hitkey-io/medusago/packages/core/utils/src/dal"
	utilsmodulessdk "github.com/hitkey-io/medusago/packages/core/utils/src/modules-sdk"
	"github.com/hitkey-io/medusago/packages/core/workflows-sdk/src/utils/composer"
)

The Link service is resolved with:

src/links/usage.go
// Resolver is what routes (req.Scope), workflow steps and scripts resolve
// from: the container.
type Resolver interface {
	Resolve(key string) any
}

func linkOf(c Resolver) *modulessdk.Link {
	return c.Resolve(common.ContainerRegistrationKeys.LINK).(*modulessdk.Link)
}

func queryOf(c Resolver) *query.Query {
	return c.Resolve(common.ContainerRegistrationKeys.QUERY).(*query.Query)
}

LinkDefinition is the description of a link: a *dal.Record whose keys are the two modules, in the order the link is defined, and whose values are records with the key of each side. The modulessdk.NewLinkDefinition function builds it:

src/links/usage.go
// postProductDefinition describes the link between a post and a product.
func postProductDefinition() modulessdk.LinkDefinition {
	return modulessdk.NewLinkDefinition(
		"blog", "post_id", "post_123",
		"product", "product_id", "prod_123",
	)
}

The modules are named by their keys, the same you use in the container. Constants such as utilsmodulessdk.PRODUCT and utilsmodulessdk.SALES_CHANNEL hold the keys of Commerce Modules. The keys of the linked records, post_id and product_id, are the linkable keys of the data models. A link definition that doesn't match a link of your project fails with an error that tells that the link wasn't found, and that the modules must be passed in the order they're defined.

In a workflow, use the steps of core-flows/src/common/steps instead of the Link service, so that a failed workflow reverts the changes. The steps are CreateRemoteLinkStep, DismissRemoteLinkStep, RemoveRemoteLinkStep, and UpdateRemoteLinksStep. They take a list of link definitions, and their compensation functions revert them:

src/workflows/link-post-to-product.go
// LinkPostInput is the input of LinkPostToProductWorkflow.
type LinkPostInput struct {
	PostID    string
	ProductID string
}

// LinkPostToProductWorkflow links a post to a product. If the workflow
// fails after the step that creates the link, the step's compensation
// dismisses the link again.
var LinkPostToProductWorkflow = composer.CreateWorkflow("link-post-to-product",
	func(e *composer.Execution, input LinkPostInput) ([]*dal.Record, error) {
		return commonsteps.CreateRemoteLinkStep.Run(e, []*dal.Record{
			modulessdk.NewLinkDefinition(
				"blog", "post_id", input.PostID,
				utilsmodulessdk.PRODUCT, "product_id", input.ProductID,
			),
		})
	})

To create a link between two records, call the Create method with their link definitions. It takes a list, so you can create many links with one call:

src/links/usage.go
// LinkPostToProduct links a post to a product.
func LinkPostToProduct(ctx context.Context, c Resolver, postID, productID string) error {
	_, err := linkOf(c).Create(ctx, []modulessdk.LinkDefinition{
		// the modules and their keys, in the order the link is defined
		modulessdk.NewLinkDefinition(
			"blog", "post_id", postID,
			utilsmodulessdk.PRODUCT, "product_id", productID,
		),
	})
	return err
}

Create returns the records of the link table that it created or restored.

When you create a link, Link enforces the cardinality of the link that you defined:

  • In a one-to-one link, creating a link fails if one of the two records is already linked to another record.
  • In a one-to-many link, creating a link fails if the record on the IsList side is already linked to another record of the other side.
  • In a many-to-many link, there's no such constraint.

A violation returns an INVALID_DATA error with the message Cannot create multiple links between 'blog' and 'product', which names the two modules. Creating the same link twice doesn't fail: the second call updates the existing link.


To remove a link between two records, call the Dismiss method with the same link definitions:

src/links/usage.go
// UnlinkPostFromProduct removes the link between a post and a product.
func UnlinkPostFromProduct(ctx context.Context, c Resolver, postID, productID string) error {
	_, err := linkOf(c).Dismiss(ctx, []modulessdk.LinkDefinition{
		modulessdk.NewLinkDefinition(
			"blog", "post_id", postID,
			utilsmodulessdk.PRODUCT, "product_id", productID,
		),
	})
	return err
}

Dismiss soft-deletes the links, by setting their deleted_at column, and returns them. It doesn't delete the records. If you create the same link again, the link is restored.


To list the links that exist between records, call the List method with the link definitions. Its last argument says whether to return the links as link definitions (true), or as the rows of the link table (false):

src/links/usage.go
// PostProductLinks lists the stored links of a post and a product.
func PostProductLinks(ctx context.Context, c Resolver, postID, productID string) ([]*dal.Record, error) {
	return linkOf(c).List(ctx, []modulessdk.LinkDefinition{
		modulessdk.NewLinkDefinition(
			"blog", "post_id", postID,
			utilsmodulessdk.PRODUCT, "product_id", productID,
		),
	}, false)
}

To retrieve linked records, not the links, use Query.


Cascade Delete Linked Records#

If you delete a record, the links that point to it remain, unless the deletion goes through Link. The Delete method soft-deletes the links of the records you pass, and the records linked with a delete cascade. It cascades through the links of the deleted records as well.

src/links/usage.go
// DeleteProductLinks soft-deletes the links of a product, and the records
// linked with a delete cascade.
func DeleteProductLinks(ctx context.Context, c Resolver, productID string) error {
	errs, _ := linkOf(c).Delete(ctx, modulessdk.DeleteEntityInput(utilsmodulessdk.PRODUCT, "product_id", productID))
	if len(errs) > 0 {
		return errs[0].Error
	}
	return nil
}

The method takes a record whose keys are modules and whose values are records of linkable keys and their IDs, which modulessdk.DeleteEntityInput builds. It returns the errors that happened, and a record of the keys that were deleted by module.

The cascade soft-deletes the records of the other module by calling the softDelete method that the service factory generates for its data model.


Restore Linked Records#

To restore the links that Delete soft-deleted, and the linked records, call the Restore method with the same input:

src/links/usage.go
// RestoreProductLinks restores the links soft-deleted with a product.
func RestoreProductLinks(ctx context.Context, c Resolver, productID string) error {
	errs, _ := linkOf(c).Restore(ctx, modulessdk.DeleteEntityInput(utilsmodulessdk.PRODUCT, "product_id", productID))
	if len(errs) > 0 {
		return errs[0].Error
	}
	return nil
}

A link may have custom columns that store data about it. To update them, call Create again with the same linked records and a data key with the new values. Learn more in the Custom Columns chapter.

Found a problem with this page?Report Issue
Edited Nov 14·Edit this page