Closed dstpierre closed 1 year ago
The backend
can be imported and used like this:
package main
import (
import (
"fmt"
"log"
"time"
"github.com/staticbackendhq/core/backend"
"github.com/staticbackendhq/core/config"
"github.com/staticbackendhq/core/model"
)
)
func main() {
// we first need to create a Config structure with the desired option for
// which database engine we want, which file storage, cache, etc.
cfg := config.AppConfig{
AppEnv: "dev",
DatabaseURL: "host=127.0.0.1 user=postgres password=postgres dbname=postgres sslmode=disable",
DataStore: "pg",
LocalStorageURL: "http://localhost:8099",
}
// we create an instance of the Backend
bkn := backend.New(cfg)
// we create a Tenant for our application
// A Go program can have 1 or multiple Tenant
// Each Tenant has their own sets of tables
cus := model.Customer{
Email: "new@cust.com",
IsActive: true,
Created: time.Now(),
}
// We can use the Backend's DB (Persister interface)
// this DB is useful for built-in types and not for user generated
// data.
cus, err := bkn.DB.CreateCustomer(cus)
if err != nil {
log.Fatal(err)
}
// We create a database for this customer / Tenant
base := model.BaseConfig{
CustomerID: cus.ID,
Name: "random-name-here",
IsActive: true,
Created: time.Now(),
}
base, err = bkn.DB.CreateBase(base)
if err != nil {
log.Fatal(err)
}
// we can now create the first user for this database
// Notice that the User wants a base ID
usr := bkn.User(base.ID)
acctID, err := usr.CreateAccount("user1@mail.com")
if err != nil {
log.Fatal(err)
}
// we create the first user for this account
_, err = usr.CreateUserToken(acctID, "user1@mail.com", "test1234", 100)
if err != nil {
log.Fatal(err)
}
// this is how we authenticate a user and get their session token
token, err := usr.Authenticate("user1@mail.com", "test1234")
if err != nil {
log.Fatal(err)
}
// Now let's create data for this account
// I'm using a simple string here, but it's a generics type, you'd
// specify your type like Task or Product for instance.
// this db variable will have all the CRUD and Query functions typed
// with the proper Type T you specify.
db := backend.NewDatabase[string](token, base.ID)
s, err := db.GetByID("colhere", "id-here")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("no error", s)
}
}
This is an example of how one Go program could import the backend
package and use it to create data for a user account inside a Tenant.
Depending if the Go host application wants to support one or multi tenant, there has to be at least one Tenant / Database to start building on top of StaticBackend.
From here the backend
package exposes services like Cache
, Mailer
, FileStorage
, etc. For instance:
backend.Cache.Set("my-key", "my-value")
Here's the available services:
// Emailer sends email
Emailer email.Mailer
// Filestore store/load/delete file
Filestore storage.Storer
// Cache exposes the cache / pub-sub functionalities
Cache cache.Volatilizer
// Log exposes the configured logger
Log *logger.Logger
One Go program would build their web application normally, using the Authenticate
function to get a session token they'd need to use each time they want to CRUD Query user's data.
The baseID
can be pre-generated if the Go program will only have one Tenant. For multi-tenant application, the Go program could have a middleware that handle grabbing the baseID from a cookie for instance.
For the database there's two way to accomplish the same tasks. One is to use the DB
field of the Backend
instance:
bkn := backend.New(cfg)
bkn.DB.CreateDocument(...)
See the Persister
interface for all the available functions. This is useful for built-in StaticBackend types, like Customer,
BaseConfig
, Account
, Token
(User).
When one wants to create their own data, I'd say the generics way is more friendly as it will return strongly typed slices and structure vs. map[string]any
:
type Task struct {
ID string `json:"id"
AccountID string `json:"accountId"
Name string `json:"name"
Done bool `json:"done"
}
db := backend.NewDatabase[Task](token, base.ID)
task, err := db.GetByID("tasks", "task-id")
In the example above task
is properly typed as Task
. All functions of the db
=> Database[T]
are properly typed making using the extra line: db := backend.NewDatabase[T]()
worth it.
This means that in a HTTP handler that you'd need to grab two or more records from different collection/repository, you'd have multiple Database[T]
instance:
// a finctional blog show handler that grabs the Blog and all it's Comment
func show(w http.ResponseWriter, r *http.Request) {
// imagine we're getting token and the base ID from the request Context
// because they were filled from middlewares.
blogDB := backend.NewDatabase[Blog](token, base.ID)
cmtDB := backend.NewDatabase[Comment](token, base.ID)
// let's grab our blog entry
blog, err := blogDB.GetByID("blogs", r.URL.Query().Get("id"))
// let's grab all comments for this blog
filters, err := backend.BuildQueryFilters("blogId", "=", blog.ID)
// this come from github.com/staticbackendhq/core/model
lp := model.ListParams{Page: 1, Size: 50}
comments, err := cmtDB.Query("comments", filters, lp)
}
So far this is the public API I have in mind for the backend
package which removes the need to self-host the backend API for Go web applications.
This is an experimental branch to create an importable Go package removing the need to self-host a separated backend server API from the main application.
The main idea is to expose what can be exposed as-is and wraps into helper functions what needs to be hidden from an external Go package.
The package's name is
backend
the import path would begithub.com/staticbackendhq/core/backend
Once imported the Go program would gets all the functionalities of the
core
package but would not use HTTP requests but direct function call.This PR would close #57