Skip to content

Latest commit

 

History

History
390 lines (280 loc) · 17.1 KB

LEGACY.md

File metadata and controls

390 lines (280 loc) · 17.1 KB

Legacy

Models

Go on Fire implements a small introspection library that is able to infer all necessary meta information about your models from the already available json and bson struct tags. Additionally, it introduces the coal struct tag that is used to declare to-one, to-many and has-many relationships.

Basics

The Base struct has to be embedded in every Go on Fire model as it holds the document ID and defines the models plural name and collection via the coal:"plural-name[:collection]" struct tag:

type Post struct {
    coal.Base `json:"-" bson:",inline" coal:"posts"`
    // ...
}
  • If the collection is not explicitly set the plural name is used instead.
  • The plural name of the model is also the type for to-one, to-many and has-many relationships.

Note: Ember Data requires you to use dashed names for multi-word model names like blog-posts.

All other fields of a struct are treated as attributes except for relationships (more on that later):

type Post struct {
    // ...
    Title    string `json:"title" bson:"title"`
    TextBody string `json:"text-body" bson:"text_body"`
    // ...
}
  • The bson struct tag is used to infer the database field or fallback to the lowercase version of the field name.
  • The json struct tag is used for marshaling and unmarshaling the models attributes from or to a JSON API resource object. Hidden fields can be marked with the tag json:"-".
  • Fields that may only be present while creating the resource (e.g. a plain password field) can be made optional and temporary using json:"password,omitempty" bson:"-".
  • The coal tag may be used on fields to tag them with custom and builtin tags.

Note: Ember Data requires you to use dashed names for multi-word attribute names like text-body.

Helpers

The ID method can be used to get the document ID:

post.ID()

The MustGet and MustSet methods can be used to get and set any field on the model:

title := post.MustGet("Title")
post.MustSet("Title", "New Title")
  • Both methods use the field name e.g. TextBody to find the value and panic if no matching field is found.
  • Calling MustSet with a different type than the field causes a panic.

Meta

All parsed information from the model struct and its tags is saved to the Meta struct that can be accessed using the Meta method:

post.Meta().Name
post.Meta().PluralName
post.Meta().Collection
post.Meta().Fields
post.Meta().OrderedFields
post.Meta().DatabaseFields
post.Meta().Attributes
post.Meta().Relationships
post.Meta().FlaggedFields
  • The Meta struct is read-only and should not be modified.

To-One Relationships

Fields of the type coal.ID can be marked as to-one relationships using the coal:"name:type" struct tag:

type Comment struct {
	// ...
	Post coal.ID `json:"-" bson:"post_id" coal:"post:posts"`
    // ...
}
  • Fields of the type *coal.ID are treated as optional relationships.

Note: To-one relationship fields should be excluded from the attributes object by using the json:"-" struct tag.

Note: Ember Data requires you to use dashed names for multi-word relationship names like last-posts.

To-Many Relationships

Fields of the type []coal.ID can be marked as to-many relationships using the coal:"name:type" struct tag:

type Selection struct {
    // ...
	Posts []coal.ID `json:"-" bson:"post_ids" coal:"posts:posts"`
	// ...
}

Note: To-many relationship fields should be excluded from the attributes object by using the json:"-" struct tag.

Note: Ember Data requires you to use dashed names for multi-word relationship names like favorited-posts.

Has-Many Relationships

Fields that have a coal.HasMany as their type define the inverse of a to-one relationship and require the coal:"name:type:inverse" struct tag:

type Post struct {
    // ...
	Comments coal.HasMany `json:"-" bson:"-" coal:"comments:comments:post"`
	// ...
}

Note: Ember Data requires you to use dashed names for multi-word relationship names like authored-posts.

Note: These fields should have the json:"-" bson"-" tag set, as they are only syntactic sugar and hold no other information.

Stores

Access to the database is managed using the Store struct:

store := coal.MustCreateStore("mongodb://localhost/my-app")

The C method can be used to easily get the collection for a model:

coll := store.C(&Post{})

The store does not provide other typical ORM methods that wrap the underlying driver, instead custom code should use the driver directly to get access to all offered features.

Advanced Features

The coal package offers the following advanced features:

  • Stream uses MongoDB change streams to provide an event source of created, updated and deleted models.
  • Reconcile uses streams to provide a simple API to synchronize a collection of models.
  • Catalog serves as a registry for models and indexes and allows the rendering of and ERD using graphviz.
  • Various helpers to DRY up the code.

Controllers

Go on Fire implements the JSON API specification and provides the management of the previously declared models via a set of controllers that are combined to a group which provides the necessary interconnection between resources.

Controllers are declared by creating a Controller and providing a reference to the model and store:

postsController := &fire.Controller{
    Model: &Post{},
    Store: store,
    // ...
}

Groups

Controller groups provide the necessary interconnection and integration between controllers as well as the main endpoint for incoming requests. A Group can be created by calling NewGroup while controllers are added using Add:

group := fire.NewGroup()
group.Add(postsController)
group.Add(commentsController)

The controller group can be served using the built-in http package:

http.Handle("/api/", group.Endpoint("/api/"))
http.ListenAndServe(":4000", nil)

The JSON API is now available at http://0.0.0.0:4000/api.

Filtering & Sorting

To enable the built-in support for filtering and sorting via the JSON API specification you need to specify the allowed fields for each feature:

postsController := &fire.Controller{
    // ...
    Filters: []string{"Title", "Published"},
    Sorters: []string{"Title"},
    // ...
}

Filters can be activated using the /posts?filter[published]=true query parameter while the sorting can be specified with the /posts?sort=created-at (ascending) or /posts?sort=-created-at (descending) query parameter.

Note: true and false are automatically converted to boolean values if the field has the bool type.

More information about filtering and sorting can be found in the JSON API Spec.

Sparse Fieldsets

Sparse Fieldsets are automatically supported on all responses and can be activated using the /posts?fields[posts]=bar query parameter.

More information about sparse fieldsets can be found in the JSON API Spec.

Callbacks

Controllers support the definition of multiple callbacks that are called while processing the requests:

postsController := &fire.Controller{
    // ...
    Authorizers: fire.L{},
    Validators: fire.L{},
    Decorators: fire.L{},
    Notifiers:  fire.L{},
    // ...
}

The Authorizers are run after inferring all available data from the request and are therefore perfectly suited to do a general user authentication. The Validators are only run before creating, updating or deleting a model and are ideal to protect resources from certain actions. The Decorators are run after the models or model have been loaded from the database or the model has been saved or updated. Finally, the Notifiers are run before the final response is written to the client. Errors returned by the callbacks are serialized to an JSON API compliant error object and yield a status code appropriate to the class of callback.

Go on Fire ships with several built-in callbacks that implement common concerns:

Custom callbacks can be created using the C helper:

fire.C("MyAuthorizer", fire.All(), func(ctx *fire.Context) error {
    // ...
}),
  • The first argument is the name of the callback (this is used to augment the tracing spans).
  • The second argument is the matcher that decides for which operations the callback is executed.
  • The third argument is the function of the callback that receives the current request context.

If returned errors from callbacks are marked as Safe or constructed using the E helper, the error message is serialized and returned in the JSON-API error response.

Custom Actions

Controllers allow the definition of custom CollectionActions and ResourceActions:

postsController := &fire.Controller{
    // ...
    CollectionActions: fire.M{
    	// POST /posts/clear
    	"clear": fire.A("Clear", []string{"POST"}, 0, func(ctx *Context) error {
            // ...
        }),
    },
    ResourceActions: fire.M{
    	// GET /posts/#/avatar
    	"avatar": fire.A("Avatar", []string{"GET"}, 0, func(ctx *Context) error {
            // ...
        }),  
    },
    // ...
}

Advanced Features

The fire package offers the following advanced features:

Authentication

The flame package implements the OAuth2 specification and provides the "Resource Owner Password", "Client Credentials" and "Implicit" grants. The issued access and refresh tokens are JWT tokens and are thus able to transport custom data.

Every authenticator needs a Policy that describes how the authentication is enforced. A basic policy can be created and extended using DefaultPolicy:

policy := flame.DefaultPolicy("a-very-long-secret")
policy.PasswordGrant = true

An Authenticator is created by specifying the policy and store:

authenticator := flame.NewAuthenticator(store, policy)

After that, it can be mounted and served using the built-in http package:

http.Handle("/auth/", authenticator.Endpoint("/auth/"))

A controller group or other endpoints can then be proctected by adding the Authorizer middleware:

endpoint := flame.Compose(
    authenticator.Authorizer("custom-scope", true, true),
    group.Endpoint("/api/"),
)

More information about OAuth2 flows can be found here.

Scope

The default grant strategy grants the requested scope if the client satisfies the scope. However, most applications want to grant the scope based on client types and owner roles. A custom grant strategy can be implemented by setting a different GrantStrategy.

The following example callback grants the default scope and additionally the admin scope if the user has the admin flag set:

policy.GrantStrategy = func(scope oauth2.Scope, client flame.Client, ro flame.ResourceOwner) (oauth2.Scope, error) {
    list := oauth2.Scope{"default"}
    
    if ro != nil && ro.(*User).Admin {
        list = append(list, "admin")
    }

    return list, nil
}

Callback

The authenticator Callback can be used to authorize access to JSON API resources by requiring a scope that must have been granted:

postsController := &fire.Controller{
    // ...
    Authorizers: []fire.Callback{
        flame.Callback(true, "admin"),
        // ...
    },
    // ...
}

Advanced Features

The flame package offers the following advanced features:

Authorization

The ash package implements a simple framework for declaratively define authorization of resources.

Authorization rules are defined using a Strategy that can be converted into a callback using the C helper:

postsController := &fire.Controller{
    // ...
    Authorizers: fire.L{
        ash.C(&ash.Strategy{
        	// ...
        	Read: ash.L{},
            Write: ash.L{},
            // ...
        }),
    },
    // ...
}