staticbackendhq / core

Backend server API handling user mgmt, database, storage and real-time component
https://staticbackend.com
MIT License
682 stars 67 forks source link

WIP: Create an importable Go package #58

Closed dstpierre closed 1 year ago

dstpierre commented 1 year ago

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 be github.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

dstpierre commented 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.