Items Rest API & Web Page
In the HTTP Web Services I talked about the best practices on creating HTTP Web Services. My blog post was inspired by a number of articles about structuring GO code, written by a number of well-known individuals of GO community. I would recommend to read that first and then come back here.
This is a continuation of that blog post, as I’ll implement persistent storage using Firestore, instead of keeping all the data in Memory. Also I’ll create additional handlers to display the table of Items within webpage.
This is the first of the series of three blog posts, where I’m going to show some options to interact with the application API.
What is Cloud Firestore
Cloud Firestore is a flexible, scalable NoSQL database for mobile, web, and server development from Firebase and GCP. Firestore is a powerful database as it supports flexible/hierarchical data structures, it has expressive querying to retrieve individual, specific documents or to retrieve all the documents in a collection.
Cloud Firestore is a NoSQL, document-oriented database. Unlike a SQL database, there are no tables or rows. Instead, you store data in documents, which are organized into collections. Each document contains a set of key-value pairs. All documents must be stored in collections. Documents can contain subcollections and nested objects, both of which can include primitive fields like strings or complex objects like lists.
First let me remind you the model I’m using. If you compare with previous blog post, you’ll see that some fields were removed, other new were added.
/*
Good struct holds the data of an item type:
ID -- autogenarated
Name -- Goods name
Manufactured -- Goods manufactured date
ExpDate -- Goods validity
ExpOpen -- Goods validity if it is opened
*/
type Good struct {
Name string `json:"name" firestore:"Name"`
ExpDate Timestamp `json:"expdate" firestore:"ExpDate"`
ExpOpen int `json:"expopen" firestore:"ExpOpen"`
Comment string `json:"comment" firestore:"Comment"`
}
/*
Item struct holds the data of the instance of the goods:
ID -- autogenerated
Type -- the type of product
IsOpen -- True if the product is opened, False otherwise
Opened -- The date when it was opened
IsValid -- Is the item still in validity or has expired
*/
type Item struct {
ID string `json:"id" firestore:"Id"`
Created Timestamp
Good
TargetAge string `json:"targetage" firestore:"TargetAge"`
IsOpen bool `json:"isopen" firestore:"IsOpen"`
Opened Timestamp `json:"opened,omitempty" firestore:"Opened"`
IsValid bool `json:"isvalid" firestore:"IsValid"`
DaysValid int `json:"daysvalid" firestore:"DaysValid"`
}
At the Storage level, I moved from Memory database to Firestore, which is persistent database. As the script is modular and extensible the only thing I had to do is to implement the Storage Interface.
type Storage interface {
ListGoods() ([]model.Item, error)
AddGood(...model.Item) (string, error)
OpenState(string, bool) (string, error)
DelGood(string) (string, error)
}
Instantiate the client using NewFirestoreDb factory function.
//FirestoreDB store the client
type FirestoreDB struct {
dbClient *firestore.Client
}
//NewFirestoreDB instantiate the client
func NewFirestoreDB(client *firestore.Client) *FirestoreDB {
return &FirestoreDB{
dbClient: client,
}
}
I’m not going to list below all CRUD operations but you can find them here. I’m showing only the List method. The most important part is that q.Documents that returns an iterator over the query’s resulting documents.
The Next() method has the following signature:
func (it *DocumentIterator) Next() (*DocumentSnapshot, error)
Calling it returns the next result. Its second return value is iterator.Done if there are no more results. Once Next returns Done, all subsequent calls will return Done.
A slice of Items are returned by the function.
//ListGoods show all the items
func (db *FirestoreDB) ListGoods() ([]model.Item, error) {
var singleItem model.Item
var allItems []model.Item
var listItems []model.Item
ctx := context.Background()
itemsDoc := db.dbClient.Collection("meds")
q := itemsDoc.OrderBy("Id", firestore.Desc)
iter := q.Documents(ctx)
defer iter.Stop()
for {
doc, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, ErrIterate
}
if err := doc.DataTo(&singleItem); err != nil {
return nil, ErrExtractDataToStruct
}
allItems = append(allItems, singleItem)
}
for _, item := range allItems {
valid, days := checkValidity(item)
item.IsValid = valid
item.DaysValid = days
listItems = append(listItems, item)
}
return listItems, nil
}
HTML Template
I’m using HandlerFunc over handler. A type is not needed if you create the handler using HandlerFunc, since this gives you the option to use an anonymous function and cast it to the HTTP handler function. Also it allows you to define private variables and Structs which are used only within the handler when is called.
Another important aspect is that I’m using sync.once to parse the html files. Once is an object that will perform exactly one action. init.Do calls the anonymous function if and only if Do is being called for the first time for this instance of Once. The benefit is that the server app start fast and it loads the data only when the handler is called for the first time.
We instantiate the API.htmlFiles = []string{"../tmpl/index.html"} in the main file and add into the Handlers struct to be available for handlers.
func (h *Handlers) htmlHandleList() http.HandlerFunc {
var (
init sync.Once
tmpl *template.Template
err error
)
type ListItemPage struct {
PageTitle string
Items []model.Item
}
return func(w http.ResponseWriter, r *http.Request) {
init.Do(func() {
tmpl, err = template.ParseFiles(h.htmlFiles...)
})
retrieve, err := h.db.ListGoods()
if err != nil {
http.Error(w, "couldn't retrieve data", http.StatusInternalServerError)
}
data := ListItemPage{
PageTitle: "Item Database",
Items: retrieve,
}
tmpl.Execute(w, data)
}
}
In the htmlHandleAdd() method I create a local struct, which is recommended if the struct is used only within a function. The struct looks similar to Item struct, only that it has fewer elements, the elements that are required to be added by user when creating a new Item. The missing fields are created and copy within an Item struct to be inserted in database.
var respItem struct {
Name string `json:"name"`
ExpDate model.Timestamp `jsnon:"expdate"`
ExpOpen string `json:"expopen"`
Comment string `json:"comment"`
TargetAge string `json:"targetage"`
IsOpen string `json:"isopen"`
Opened model.Timestamp `json:"opened"`
}
The Web Page
I’m not a frontend engineer, therefore I created a simple html webpage, with a number of JavaScript elements. To simplify my work I’m using Bootstrap, which is the most popular HTML, CSS, and JS framework for developing responsive, mobile first projects on the web, according to the official website. I’m using Bootstrap version 3, now the recommended version is version 4.
The final product will look like this:
The stylesheet has to be added into the html <head> before all other stylesheet.
|
|
Many of Bootstrap components require the use of JavaScript to function. Specifically, they require jQuery, Popper.js, and some JavaScript plugins. Place the following scripts near the end of your pages, right before the closing </body> tag, to enable them.
|
|
I iterate over the Item list and display all in a table-striped model.
|
|
I’m using Bootstrap Modal plugin to add dialogs to the site, like: Add New Item, Details and Delete. Modals are built with HTML, CSS, and JavaScript. They’re positioned over everything else in the document and remove scroll from the body so that modal content scrolls instead. Below is the button which trigger the Details modal to show up. Most important to notice is data-target="#confirm-detail" which identify the modal element.
|
|
|
|
Call a modal with id confirm-detail within a JavaScript to populate the data.
|
|
Similar modals were created for the Form, to insert a new item and for the delete function.
Below is the submitForm function in JS, as the server expect a json object I had to datajson = JSON.stringify(jsonData) the Form data.
|
|
Conclusion
You can find the complete code on Github. I had to learn a little bit of JavaScript and Bootstrap framework to make it work, which was fun but not in my immediate interest. If you rather prefer a Mobile app, then checkout my next blog post in this series. The alternative is to use Voice Assistant, which is the third option.