Service Factory

In this chapter, you'll learn about the service factory, the generated methods it adds to your module's service, and how to use them.

What is the Service Factory?#

The service factory is the MedusaService type of the modules-sdk package of MedusaGo. When you define a module with Module, MedusaGo creates a MedusaService for the module's data models. It manages the records of each data model, so you don't write the code that creates, reads, updates, and deletes them.

Your module's service embeds the generated MedusaService, and you add the methods that hold your business logic. The service you define is what MedusaGo registers in the container.

The service factory gives you two ways to manage the records of a data model:

  • The generated methods, such as listPosts and createPosts, that you call by name with the Call method of the service.
  • The internal service of each data model, returned by the Internal method of the service, whose methods have typed arguments.

Both return the same data, as *dal.Record values. A dal.Record is an ordered map that keeps the order of its keys when it's encoded to JSON, like a JavaScript object.

Note: Medusa generates real methods on the service class. Go has no methods generated at run time, so MedusaGo dispatches the generated methods by name through Call. The names and the arguments of the generated methods are the same as in Medusa.

How to Extend the Service Factory?#

To use the service factory, embed *MedusaService in your service, and build the service in the Service function of the module's definition. The Service function receives the generated MedusaService:

src/modules/blog/index.go
// Module is the definition of the blog module.
var Module = utilsmodulessdk.Module(Name, utilsmodulessdk.ModuleDefinition{
	Models:     Models,
	Migrations: migrations.Migrations,
	Loaders:    []typesmodulessdk.Loader{SeedTags},
	Service: func(opts typesmodulessdk.ServiceOptions, base *utilsmodulessdk.MedusaService) (any, error) {
		options, err := ParseOptions(opts.Options)
		if err != nil {
			return nil, err
		}
		return &BlogModuleService{MedusaService: base, options: options}, nil
	},
})

The service embeds the generated service:

src/modules/blog/service.go
// BlogModuleService is the service of the blog module. Embedding
// *MedusaService gives it the generated methods (listPosts, createPosts, ...)
// and the internal service of each model.
type BlogModuleService struct {
	*utilsmodulessdk.MedusaService
	options Options
}

The Module function also builds the generated methods of the data models of the Models field. The module in the chapter has three: Post, Comment, and Tag.


Generated Methods#

The service factory generates the following methods for each data model of the module. The names use the data model's name, Post for the post model, and its plural, Posts.

| Method | Description | | --- | --- | | list<Plural>(filters, config) | Lists records. | | listAndCount<Plural>(filters, config) | Lists records and their total count. | | retrieve<Model>(id, config) | Retrieves a record by its primary key. It returns a NOT_FOUND error if there's none. | | create<Plural>(data) | Creates one or more records. | | update<Plural>(data) | Updates one or more records. | | delete<Plural>(ids) | Permanently deletes records. | | softDelete<Plural>(ids) | Soft-deletes records, by setting their deleted_at column. | | restore<Plural>(ids) | Restores soft-deleted records. |

If a data model is named BlogPost, the methods are listBlogPosts, retrieveBlogPost, and so on.

You call a generated method with the Call method of the service. It takes the context, the name of the method, and its arguments as a list:

src/modules/blog/usage.go
// callListPosts shows the shape of every call to a generated method: the name
// of the method, and its arguments as a list.
func callListPosts(ctx context.Context, s *BlogModuleService, filters, config *dal.Record) (any, error) {
	return s.Call(ctx, "listPosts", []any{filters, config})
}

Call returns the result as an any that you convert to the type that's described for each method below.

Note: 

The filters, configurations, and data are *dal.Record values, so the examples use the small rec function that builds one from a list of keys and values. You define it in your project:

src/modules/blog/helpers.go
import "github.com/hitkey-io/medusago/packages/core/utils/src/dal"

// rec builds a *dal.Record, an ordered map that encodes to JSON with its
// keys in insertion order, like a JavaScript object.
func rec(kv ...any) *dal.Record {
	r := dal.NewRecord()
	for i := 0; i+1 < len(kv); i += 2 {
		r.Set(kv[i].(string), kv[i+1])
	}
	return r
}

list Method#

list<Plural> takes the filters and the configuration of the query, and returns a []*dal.Record.

src/modules/blog/usage.go
// listPosts calls listPosts with filters and a configuration.
func listPosts(ctx context.Context, s *BlogModuleService) ([]*dal.Record, error) {
	out, err := s.Call(ctx, "listPosts",
		[]any{
			rec("status", "published"),
			rec(
				"select", []any{"id", "title"},
				"take", float64(10),
				"skip", float64(0),
				"order", rec("published_at", "DESC"),
			),
		})
	if err != nil {
		return nil, err
	}
	return out.([]*dal.Record), nil
}

The first argument holds the filters, which match the properties of the data model. The value of a filter can be a value to match, a list of values, or an object with an operator: $eq, $ne, $in, $nin, $gt, $gte, $lt, $lte, $like, $ilike, $exists, $not, $re, $contains, $contained, or $overlap. The $and, $or, and $not keys combine filters. A filter can also use a property of a relationship, such as rec("comments", rec("author_name", "Ada")). Learn more in the Query chapter.

If the data model has searchable properties, the q filter, such as rec("q", "medusa"), searches them.

The second argument is the configuration of the query. All its keys are optional:

| Key | Description | | --- | --- | | select | The properties to retrieve, as a list of names. By default, all the properties. | | relations | The relationships to load, as a list of names. | | take and skip | The number of records to retrieve, and the number to skip. | | order | An object whose keys are properties and values are "ASC" or "DESC". By default, the records are ordered by their primary key. | | withDeleted | Includes the soft-deleted records when true. |

listAndCount Method#

listAndCount<Plural> takes the same arguments as list<Plural>. It returns a []any of two elements: a []*dal.Record of the records, and an int, the number of records that match the filters, whatever take and skip are.

src/modules/blog/usage.go
// listAndCountPosts returns the posts and the total number of matches.
func listAndCountPosts(ctx context.Context, s *BlogModuleService) ([]*dal.Record, int, error) {
	out, err := s.Call(ctx, "listAndCountPosts", []any{
		rec("status", "published"),
		rec("take", float64(10)),
	})
	if err != nil {
		return nil, 0, err
	}
	result := out.([]any)
	return result[0].([]*dal.Record), result[1].(int), nil
}

retrieve Method#

retrieve<Model> takes the primary key of the record and a configuration with select and relations. It returns a *dal.Record. If no record has the key, it returns an error of the NOT_FOUND type with the message Post with id: post_123 was not found.

src/modules/blog/usage.go
// retrievePost calls retrievePost, which returns a NOT_FOUND error when the
// post does not exist.
func retrievePost(ctx context.Context, s *BlogModuleService, id string) (*dal.Record, error) {
	out, err := s.Call(ctx, "retrievePost", []any{
		id,
		rec("select", []any{"id", "title"}, "relations", []any{"comments"}),
	})
	if err != nil {
		return nil, err
	}
	return out.(*dal.Record), nil
}

create Method#

create<Plural> takes one record or a list of records. It returns a *dal.Record for one record, and a []*dal.Record for a list. Properties that you don't set take their default value. The ID is generated, with the prefix of the model's id property.

src/modules/blog/usage.go
// createPosts calls the generated createPosts method.
func createPosts(ctx context.Context, s *BlogModuleService) ([]*dal.Record, error) {
	out, err := s.Call(ctx, "createPosts", []any{
		[]any{
			rec("title", "Hello, world", "slug", "hello-world"),
			rec("title", "Release notes", "slug", "release-notes"),
		},
	})
	if err != nil {
		return nil, err
	}
	return out.([]*dal.Record), nil
}

You can create related records in the same call. For example, a comments property with a list of comments creates them with the post. Learn more in the Relationships chapter.

update Method#

update<Plural> takes the data to update, in one of three shapes:

  1. A record with the primary key and the properties to change. The method returns the updated *dal.Record.
  2. A list of such records. It returns a []*dal.Record.
  3. A record with a selector and a data key. The method sets the properties of data on every record that matches the selector, and returns a []*dal.Record.
src/modules/blog/usage.go
// updatePosts shows the three shapes of the update input.
func updatePosts(ctx context.Context, s *BlogModuleService) error {
	// one object with the primary key: returns the updated record
	if _, err := s.Call(ctx, "updatePosts", []any{
		rec("id", "post_123", "title", "New title"),
	}); err != nil {
		return err
	}

	// a list: returns the updated records
	if _, err := s.Call(ctx, "updatePosts", []any{
		[]any{
			rec("id", "post_123", "title", "New title"),
			rec("id", "post_456", "status", "archived"),
		},
	}); err != nil {
		return err
	}

	// a selector and the data to set on every match
	_, err := s.Call(ctx, "updatePosts", []any{
		rec(
			"selector", rec("status", "draft"),
			"data", rec("status", "archived"),
		),
	})
	return err
}

If a record of the first two shapes doesn't exist, the method returns a NOT_FOUND error. The metadata property of a record is merged with the value you pass: the keys that you set are added or replaced, and a key set to an empty string is removed.

delete, softDelete, and restore Methods#

delete<Plural> takes the primary keys of the records to delete, or a record with a selector. It deletes them permanently. The relationships that the data model cascades its deletion to are deleted with them.

softDelete<Plural> and restore<Plural> set and clear the deleted_at column of the records, and of the records of their cascading relationships. They return a *dal.Record that maps the linkable keys of the records that were affected to their IDs, such as { post_id: [...], comment_id: [...] }.

src/modules/blog/usage.go
// deletePosts removes posts, or soft-deletes and restores them. The relations
// listed in the cascades of the model follow.
func deletePosts(ctx context.Context, s *BlogModuleService) error {
	if _, err := s.Call(ctx, "deletePosts", []any{[]any{"post_123"}}); err != nil {
		return err
	}

	// the keys of the deleted records, by linkable key, for example
	// { post_id: [...], comment_id: [...] }
	if _, err := s.Call(ctx, "softDeletePosts", []any{[]any{"post_456"}}); err != nil {
		return err
	}
	_, err := s.Call(ctx, "restorePosts", []any{[]any{"post_456"}})
	return err
}

Module links use these methods to soft-delete and restore the linked records.


Internal Services#

Internal takes the name of a data model, as in its definition: Post. It returns an internal service, with typed methods for the data model:

| Method | Description | | --- | --- | | List(ctx, filters, config) | Lists records. | | ListAndCount(ctx, filters, config) | Lists records and counts them. | | Retrieve(ctx, id, config) | Retrieves a record by its primary key. | | Create(ctx, data) | Creates records. | | Update(ctx, input) | Updates records, with the same input shapes as update<Plural>. | | Upsert(ctx, data) | Creates the records, or updates those whose primary key exists. | | Delete(ctx, idsOrSelector) | Deletes records. | | SoftDelete(ctx, ids) and Restore(ctx, ids) | Soft-delete and restore records. | | ReplaceManyToMany(ctx, ownerID, relation, targetIDs) | Replaces the records related through a many-to-many relationship. |

The configuration is a FindConfig with Select, Relations, Take, Skip, Order, and WithDeleted fields:

src/modules/blog/usage.go
// recentPosts uses the internal service of the Post model, which takes typed
// configuration instead of a record.
func recentPosts(ctx context.Context, s *BlogModuleService) ([]*dal.Record, error) {
	take := 10
	return s.Internal("Post").List(ctx,
		rec("status", "published"),
		utilsmodulessdk.FindConfig{
			Select:    []string{"id", "title"},
			Relations: []string{"comments"},
			Take:      &take,
			Order:     []dal.OrderBy{{Field: "published_at", Direction: "DESC"}},
		},
	)
}

The generated methods call the internal services, so both behave the same way. Use the internal service in your own methods, where the typed configuration is more convenient. For example, the SetPostTags method of the Blog Module uses the Tag and Post internal services.


Events Emitted by the Service#

The service factory emits an event when its methods create, update, delete, or restore records, such as blog.post.created, blog.post.updated, blog.post.deleted, and blog.post.restored. The event's data holds the ID of the record. The events are emitted once the transaction that made the change commits. Learn more in the Events and Subscribers chapter.


Add Your Own Methods#

Add methods to your service to hold your business logic. They use the generated methods, the internal services, or database operations of your own. For example, the PublishPost method:

src/modules/blog/service.go
// PublishPost marks a draft post as published.
func (s *BlogModuleService) PublishPost(ctx context.Context, id string) (*dal.Record, error) {
	posts := s.Internal("Post")

	post, err := posts.Retrieve(ctx, id, utilsmodulessdk.FindConfig{Select: []string{"id", "status"}})
	if err != nil {
		return nil, err
	}
	if status, _ := post.Get("status"); status != "draft" {
		return nil, fmt.Errorf("post %s is not a draft", id)
	}

	updated, err := posts.Update(ctx, rec(
		"id", id,
		"status", "published",
		"published_at", dal.FormatJSDate(dal.Now()),
	))
	if err != nil {
		return nil, err
	}
	return updated.(*dal.Record), nil
}

It retrieves a post, checks that it's a draft, and updates its status and publish date. Learn more about the methods that run several operations in a transaction in the Database Operations chapter.

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