divan

package module
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2020 License: MIT Imports: 10 Imported by: 0

README

Divan

The ultimate Couchbase wrapper for easy server setup.

Prerequisites

Dependencies

Install Divan and Gin.

go get github.com/Alvarios/divan
go get -u github.com/gin-gonic/gin
Database

Have a running Couchbase instance on port 8091. If your instance runs with the default credentials listed below, you can jump to Setup:

USERNAME:   Administrator
PASSWORD:   password

This step is required if you run a couchbase instance with custom credentials.

Create a file in your project folder, preferably inside a secret/ sub-folder that is ignored by .gitignore, and pushed individually to your server in production.

project_root_folder/
    |_ secret/
        |_ couchbase.json
# .gitignore

secret/

Add the following content into it:

{
    "database": {
        "username": "your_instance_username",
        "password": "your_instance_password",
        "url":      "couchbase://127.0.0.1 or other if required"
    }
}

Setup

Basic setup
package myPackage

import (
    "github.com/Alvarios/divan"
    "github.com/gin-gonic/gin"
)

func main() {
    // Start gin router with default options.
    router:= gin.Default()
    
    // Initialise Divan.
    var instance divan.Divan

    // Fill Divan instance with default settings.
    instance.LoadDefaults()

    // Connect Divan to database.
    _ = instance.Connect()

    _ = router.Run()
}

Most of our examples don't handle errors for a simplicity purpose. However, you should be careful about that when writing your own code.

Setup with custom credentials

If you use custom credentials, add an ENV variable with a path to your credentials file. From your terminal:

export COUCHBASE_CREDENTIALS="project_root_folder/secret/couchbase.json"

Assuming that:

  1. You run all your commands from inside your Go project.
  2. Your project folder name is unique within the project ( no sub-folder share its name with the root folder )

You can start the path with no slash and the name of your project folder. Alternatively, if you saved your credentials outside the project folder, within a secure local folder, you can pass an absolute path to it by using a leading /.

Then write your main function like this:

package myPackage

import (
    "github.com/Alvarios/divan"
    "github.com/gin-gonic/gin"
    "os"
)

func main() {
    var instance divan.Divan

    // Name the ENV variable as you want, as long as you change it here too.
    _ = instance.LoadFrom(os.Getenv("COUCHBASE_CREDENTIALS"))
    
    _ = instance.Connect()
}
Fully custom setup

Finally, you can create your couchbase cluster on your own (especially if you want to use some of the advanced options provided by gocb.Connect).

package myPackage

import (
    "github.com/Alvarios/divan"
    "github.com/gin-gonic/gin"
)

func main() {
    // You don't need to call Connect() afterwards.
    instance:= divan.Divan{
        Cluster: ClusterInstance, // of type *gocb.Cluster
    }
}

Add routes

Once you set up your divan instance, you can easily add routes to interact with your buckets.

This example runs on a bucket named documents.

package myPackage

import (
    "github.com/Alvarios/divan"
    "github.com/Alvarios/divan/router"
    "github.com/Alvarios/divan/specs"
    "github.com/gin-gonic/gin"
    "os"
)

func main() {
    // Start gin router with defaults options.
    router:= gin.Default()
    
    // Initialise Divan.
    var instance divan.Divan

    // Name the ENV variable as you want, as long as you change it here too.
    _ = instance.LoadFrom(os.Getenv("COUCHBASE_CREDENTIALS"))

    // Connect Divan to database.
    _ = instance.Connect()

    // Add routes to gin engine.
    divanRouter.CRUD(
        router, 
        "/models/document",                  // Base route for all methods.
        instance.Link("documents"),          // Open bucket users.
        divanSpecs.CRUDDefault("documents"), // We'll configure it later.
    )

    _ = router.Run()
}

The CRUD automatic routage function takes 4 parameters:

Parameter Type Description
router *gin.Engine Required to add routes to gin router.
basePath string CRUD will append all methods to the basePath. Each method will be accessible from http://domain/basePath/methodPath.
handler *divan.Handler Handler will control our data flow. Below sections will provide more advanced ways to configure it.
specs *divanSpecs.CRUD Options for our router.

Doing this, you should have the 4 following routes available, given your server runs on localhost:8080:

POST    localhost:8080/models/document/create
GET     localhost:8080/models/document/read/:id
POST    localhost:8080/models/document/update/:id
DELETE  localhost:8080/models/document/delete/:id

create will need your document object as JSON in the body.

update takes a divanSpecs.Update object. Your request body should look something like this:

{
  "remove": ["description", "about.occupation"],
  "upsert": {
    "about": {
      "name": "John"
    } 
  },
  "append": {
    "about": {
      "hobbies": ["movies", "music"]
    }
  }
}

All of them are optional - an empty update will resolve normally.

remove represents a list of keys to remove from the model. It accepts nested dot syntax.

upsert is an object with the same structure as the whole model, but where only fields to update are declared.

append is a list of keys pointing to arrays in the model, with a list of values to add. It accepts nested dot syntax.

Advanced routage setup

The divanSpecs.CRUD structure looks like this:

type CRUD struct {
    Routes             []string                      `json:"routes"`
    IDGenerator        func(data interface{}) string `json:"id_generator"`
    GetDocumentFrom    string                        `json:"get_document_from"`
    GetUpdateSpecsFrom string                        `json:"get_update_specs_from"`
    GetIDFrom          string                        `json:"get_id_from"`
    ReportFailuresIn   string                        `json:"report_failures_in"`
    AbortOnError       bool                          `json:"abort_on_error"`
}
CRUD Routes

Tells which routes will actually be opened by the setup. Left it to nil to open all of the 4 CRUD routes:

// Equivalent to divanSpecs.CRUD{ Routes: nil }
config:= divanSpecs.CRUD {
    Routes: []string{
        divanSpecs.CRUD_CREATE,
        divanSpecs.CRUD_READ,
        divanSpecs.CRUD_UPDATE,
        divanSpecs.CRUD_DELETE,
    },
}

For example, the following main:

package myPackage

import (
    "github.com/Alvarios/divan"
    "github.com/Alvarios/divan/router"
    "github.com/Alvarios/divan/specs"
    "github.com/gin-gonic/gin"
)

func main() {
    config:= divanSpecs.CRUD {
        Routes: []string{
            divanSpecs.CRUD_READ,
        },
    }
    
    config.Default("document")

    // Add routes to gin engine.
    divanRouter.CRUD(
        router, 
        "/models/document",                  // Base route for all methods.
        instance.Link("documents"),          // Open bucket users.
        &config,
    )
}

Will only create this route:

GET     localhost:8080/models/document/read/:id
CRUD IDGenerator

Called on create, and is used to generate a unique ID for the new document. The interface in arguments correspond to the new document, in JSON format.

When working with Models, this function is overridden by the GetID() method of model.

CRUD Accessors
type CRUD struct {
    GetDocumentFrom    string           `json:"get_document_from"`
    GetUpdateSpecsFrom string           `json:"get_update_specs_from"`
    GetIDFrom          string           `json:"get_id_from"`
    ReportFailuresIn   string           `json:"report_failures_in"`
}

Accessors will be useful when you'll get to write your own custom middlewares. Gin middlewares communicate between them through a common *gin.Context object. Accessors will tell Divan middlewares where to seek for their data, and where to return it once done.

Each accessors name is explicit and should tell you what it is used for. Note this only serves for you to write custom middlewares, and those parameters should be left to default values when using automatic routage.

Writing Models

Models are the most important part of Divan. They are special interfaces with methods that tells Couchbase how we expect our data to behave.

A Model is basically a structure with few required methods. You are totally free for the structure part.

Sample Model
package models

import (
    "github.com/Alvarios/divan"
    "github.com/Alvarios/divan/defaults"
    "github.com/Alvarios/divan/methods/utils"
    "github.com/Alvarios/divan/specs"
)

type GuyCredentials struct {
    Username string `json:"username" required:"true"`
    Password string `json:"password" required:"true"`
}

type GuyAbout struct {
    Credentials  GuyCredentials `json:"credentials"`
    Achievements []string       `json:"achievements"`
}

type Guy struct {
    ID          string                 `json:"id" required:"true"`
    Age         int                    `json:"age"`
    Sex         string                 `json:"sex" restrict:"male female"`
    Description string                 `json:"description" required:"true" default:"hello\""`
    About       GuyAbout               `json:"about"`
}

func (g Guy) Integrity() error {
    return divanDefaults.Integrity(&g)
}

func (g Guy) UpdateIntegrity(us *divanSpecs.Update) error {
    return divanDefaults.UpdateIntegrity(g, us)
}

func (g Guy) AssignData(v interface{}) (divan.Model, error) {
    output:= Guy{}
    err:= divanUtils.Convert(&v, &output)
    
    return output, err
}

func (g Guy) GetID() string {
    return g.ID
}
Model default validators

With the above declaration, you can use the following custom tags.

Tag Works with Values Description
required all true or false This field is required and cannot be left blank.
ignore all any Do not check this field or any of its childrens.
default all any Assign default value if field is blank (on create only).
restrict []string or string string with keywords separated by spaces Restrict the field to a limited list of allowed values.

Then, using the above declarations, divan handlers will automatically parse every document and perform the required checks for you.

Model methods
Integrity() error

Called on creation, and runs data validation by the model. Integrity() should return nil if the data is ok to insert inside the database, and an explicit error otherwise.

The default validator will perform a validation based on tags. Other custom conditions can be added before.

For example, if we want to add some timestamps, such as creation_time an last_update, we'd also like last_update to always be, equal to or greater than creation_time. We can write our Integrity() method like this:

func (g Guy) Integrity() error {
    if g.CreationTime > g.LastUpdate {
        return fmt.Error("creation_time cannot be greater than last update")
    }

    return divanDefaults.Integrity(&g)
}
UpdateIntegrity(us *divanSpecs.Update) error

This function will be called on each update. It will read from divanSpecs.Update sent within request body to ensure the model is not broken.

Update methods are optimized so they only receive (and send) the keys that need to be updated within the document, meaning that this method theoretically doesn't have any access to the full document, and we recommend you to keep with that for optimization purposes.

Their is a workaround however: the update spec object holds an extra field referenced by the json key reference. You can pass some extra data from your client application (which may have access to a larger part of the document) and use it to perform some comparison, for example the last_update field from above example.

{
  "remove": [],
  "upsert": {},
  "append": {},
  "reference": "anything you want here: object, array, string, number"
}
func (g Guy) UpdateIntegrity(us *divanSpecs.Update) error {
    if us.Upsert != nil {
        creationTime, _:= us.Reference.(int)
        lastUpdate, ok:= divanUtils.Flatten(us.Upsert)["last_update"].(int)
    
        if ok && creationTime > lastUpdate {
            return fmt.Error("creation_time cannot be greater than last update")
        }
    }

    return divanDefaults.UpdateIntegrity(g, us)
}

Ultimately, you can perform a Read operation to fetch your original document, but take in account this will strongly affect your performances.

AssignData(v interface{}) (divan.Model, error)

Converts an interface to a valid instance of model using json.Unmarshal. Unless needed, it is recommended to go with the default declaration.

GetID() string

Called on creation, returns the ID that should be used for the document. Like Integrity() method, the model will be assigned with the values of a document, so you can access its keys to build the ID.

Using Models

Once your model is created, you can use it in the default router with the following declaration:

package myPackage

import (
    // Import your model too.
    "github.com/Alvarios/divan"
    "github.com/Alvarios/divan/router"
    "github.com/Alvarios/divan/specs"
    "github.com/gin-gonic/gin"
    "os"
)

func main() {
    // Start gin router with defaults options.
    router:= gin.Default()
    
    // Initialise Divan.
    var instance divan.Divan

    // Name the ENV variable as you want, as long as you change it here too.
    _ = instance.LoadFrom(os.Getenv("COUCHBASE_CREDENTIALS"))

    // Connect Divan to database.
    _ = instance.Connect()

    // Add routes to gin engine.
    divanRouter.CRUD(
        router, 
        "/models/guy",                               // Base route for all methods.
        instance.Link("guy").UseModel(models.Guy{}), // Open bucket users.
        divanSpecs.CRUDDefault("guy"),               // We'll configure it later.
    )

    _ = router.Run()
}

Writing your own middlewares

You can directly use divan CRUD middlewares in your own gin routers. All CRUD middlewares are methods of divan.Handler struct, which is returned by instance.Link().

// .UseModel is optional.
handler := instance.Link("guy").UseModel(models.Guy{})
CreateAPI
router.METHOD(
    // ...
    handler.CreateAPI(&divanSpecs.CRUD{
        GetDocumentFrom: "document_key",
        GetIDFrom: "id_key",
    }),
    // ...
)

This method requires the document was already extracted from request body, and saved to a *gin.Context key. This key is identified by GetDocumentFrom.

You can also set an ID within the context. If so, it will be used in place of the default ID, wether it comes from the specs IDGenerator() or the model method GetID().

You don't need to perform integrity test when you extract your data, as it will be performed anyway within the CreateAPI middleware.

ReadAPI
router.METHOD(
    // ...
    handler.ReadAPI(&divanSpecs.CRUD{
        GetDocumentFrom: "document_key",
        GetIDFrom: "id_key",
    }),
    // ...
)

This method only requires you to set the document ID within the gin Context. The other key is where Read operation result will be stored to be accessed by later middlewares.

UpdateAPI
router.METHOD(
    // ...
    handler.UpdateAPI(&divanSpecs.CRUD{
        GetUpdateSpecsFrom: "specs_key",
        GetIDFrom: "id_key",
    }),
    // ...
)

You need to extract document ID and update specs before using this middleware.

DeleteAPI
router.METHOD(
    // ...
    handler.DeleteAPI(&divanSpecs.CRUD{
        GetIDFrom: "id_key",
    }),
    // ...
)
Other methods

Divan provides you every middleware it uses, so you can write the minimal amount of code to match your needs.

All those methods are accessible from "github.com/Alvarios/divan/router/methods" as divanMethods.

ReadIDFromUrl
router.METHOD(
    // ...
    divanMethods.ReadIDFromUrl(&divanSpecs.CRUD{
        GetIDFrom: "id_key",
    }),
    // ...
)

Read the URL parameter called :id and save it to context.

RetrieveDocumentFromRequest
router.METHOD(
    // ...
    divanMethods.RetrieveDocumentFromRequest(&divanSpecs.CRUD{
        GetDocumentFrom: "document_key",
    }),
    // ...
)

Save the request body inside context.

RetrieveUpdateSpecsFromRequest
router.METHOD(
    // ...
    divanMethods.RetrieveUpdateSpecsFromRequest(&divanSpecs.CRUD{
        GetUpdateSpecsFrom: "specs_key",
    }),
    // ...
)
SendDocumentAsJSON
router.METHOD(
    // ...
    divanMethods.SendDocumentAsJSON(&divanSpecs.CRUD{
        GetDocumentFrom: "document_key",
    }),
)
SendDocumentIDAsJSON
router.METHOD(
    // ...
    divanMethods.SendDocumentIDAsJSON(&divanSpecs.CRUD{
        GetIDFrom: "id_key",
    }),
)

License

License MIT, licensed by Kushuh with Alvarios.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	Url      string `json:"url"`
	Username string `json:"username"`
	Password string `json:"password"`
}

type Divan

type Divan struct {
	Config  Config `json:"database"`
	Cluster *gocb.Cluster
}

func (*Divan) Connect

func (d *Divan) Connect() error

func (*Divan) Disconnect

func (d *Divan) Disconnect() error
func (d *Divan) Link(bucket string) *Handler

func (*Divan) LoadDefaults

func (d *Divan) LoadDefaults()

Initialize configuration with default parameters.

func (*Divan) LoadFrom

func (d *Divan) LoadFrom(filePath string) error

Load configuration from JSON file.

type Failure

type Failure struct {
	Method string
	Model  string
	Detail string
	Status int
}

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

func (*Handler) Create added in v1.1.0

func (h *Handler) Create(data interface{}, id string) (string, *Failure)

func (*Handler) CreateAPI

func (h *Handler) CreateAPI(config *divanSpecs.CRUD) func(c *gin.Context)

func (*Handler) Delete added in v1.1.0

func (h *Handler) Delete(id string) *Failure

func (*Handler) DeleteAPI

func (h *Handler) DeleteAPI(config *divanSpecs.CRUD) func(c *gin.Context)

func (*Handler) GenerateFailure added in v1.1.0

func (h *Handler) GenerateFailure(status int, message, method string) *Failure

func (*Handler) HandleError

func (h *Handler) HandleError(config *divanSpecs.CRUD, c *gin.Context, failure *Failure)

Read CRUD configuration to handle errors. CRUD specs have an optional .AbortOnError parameter, that is forced to true on automatic CRUD routage. If set to false, errors will not abort request but will be added in a special interface{} accessible from gin context in later middlewares.

func (*Handler) Model added in v1.1.1

func (h *Handler) Model() Model

func (*Handler) Open

func (h *Handler) Open(cluster *gocb.Cluster, bucket, collection string) *Handler

Connect the handler to a couchbase bucket. If collection string is empty, the handler will by default use the DefaultCollection of the bucket.

func (*Handler) Read added in v1.1.0

func (h *Handler) Read(id string) (interface{}, *Failure)

func (*Handler) ReadAPI

func (h *Handler) ReadAPI(config *divanSpecs.CRUD) func(c *gin.Context)

func (*Handler) Search added in v1.0.4

func (h *Handler) Search(query string) ([]interface{}, *Failure)

func (*Handler) SearchAPI added in v1.1.0

func (h *Handler) SearchAPI(config *divanSpecs.CRUD) func(c *gin.Context)

func (*Handler) Update added in v1.1.0

func (h *Handler) Update(specs divanSpecs.Update, id string) *Failure

func (*Handler) UpdateAPI

func (h *Handler) UpdateAPI(config *divanSpecs.CRUD) func(c *gin.Context)

func (*Handler) UseModel

func (h *Handler) UseModel(d Model) *Handler

Use a model to validate data with CRUD methods.

type Model

type Model interface {
	Integrity() error
	UpdateIntegrity(*divanSpecs.Update) error
	AssignData(v interface{}) (Model, error)
	GetID() string
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL