Model views
When generated, models will produce a struct that contains all the fields of the table, and will also produce the same struct with all optional fields.
Overview
A view is a struct that defines which fields should be serialized and sent to the user, based on their visibility level. Each field in a view has a visible
tag that specifies the roles or contexts in which the field should be included.
Those tags are controlled by the comment in the SQL schema, for example:
CREATE TABLE users (
id SERIAL PRIMARY KEY COMMENT 'visible:"admin,user,other"',
...
);
This will generate the following view :
type userView struct {
ID *uint64 `json:"id,omitempty" visible:"admin,user,other"`
...
}
To convert a model to a view, you can use the model.View
functionn :
view := user.View()
Visibility Levels
admin
: Visible to administrators.user
: Visible to the user themselves.other
: Visible to other roles (e.g., public viewers).nobody
: Not visible to anyone.- Fields can also use
json:"-"
to ensure they are not serialized at all.
Creating a View
To create a new view, define a struct with the desired fields and visible
tags. Implement a function to map the model data to the view struct. For example:
func UserView(user *models.User) *userView {
return &userView{
ID: &user.ID,
Username: &user.Username,
PasswordHash: &user.PasswordHash,
Email: &user.Email,
EmailVerified: &user.EmailVerified,
EmailVerificationToken: &user.EmailVerificationToken,
IsAdmin: &user.IsAdmin,
IsHidden: &user.IsHidden,
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
DeletedAt: &user.DeletedAt,
}
}
Using Views in Handlers
To use a view in an API handler, call the appropriate view function and return it in the response. For example:
func profile(api *Api) func(ctx *atreugo.RequestCtx) error {
return func(ctx *atreugo.RequestCtx) error {
session := ctx.UserValue("session").(*Session)
userSession, err := models.UserSessions(models.UserSessionWhere.ID.EQ(session.UserSessionID)).OneG(ctx)
if err != nil {
...
}
user, err := userSession.User().OneG(ctx)
if err != nil {
...
}
return ctx.JSONResponse(NewResponse(200, views.Return(user, user, views.UserView(user))))
}
}
The params are explained by your IDE, the first one is the viewer, the second one the owner of the resource, and the third one is the model you want to send.
Explanation of views.Return
The views.Return
function is responsible for applying the visibility rules to the view. It takes the current user (to determine their role) and the data to be serialized.
Updating Views
To update an existing view:
- Modify the struct to add, remove, or update fields.
- Update the
visible
tags to reflect the new visibility rules. - Update the corresponding view function to map any new or removed fields.
Adding a New View
To add a new view:
- Define a new struct in the
views
package. - Add appropriate
json
andvisible
tags to each field. - Implement a function to map the model data to the view struct.
- Use the new view in your handlers as needed.
Example: Creating a productView
type productView struct {
ID *uint64 `json:"id,omitempty" visible:"admin,user,other"`
Name *string `json:"name,omitempty" visible:"admin,user,other"`
Description *string `json:"description,omitempty" visible:"admin,user"`
Price *float64 `json:"price,omitempty" visible:"admin,user"`
}
func ProductView(product *models.Product) *productView {
return &productView{
ID: &product.ID,
Name: &product.Name,
Description: &product.Description,
Price: &product.Price,
}
}
Best Practices
- Use
visible
tags carefully: Avoid exposing sensitive information (e.g., passwords, tokens) to unintended roles. - Keep view structs minimal: Only include fields that are necessary for the given context.
- Test visibility rules: Ensure fields are visible only to the intended roles.
Future Improvements
- Implement a utility function to automatically filter fields based on the
visible
tag, reducing boilerplate in handlers. - This one might be tricky to implement and the benefits might not be worth the complexity.