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
Create a credential file (recommended for production servers)
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:
- You run all your commands from inside your Go project.
- 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.