backend

package
v1.5.1 Latest Latest
Warning

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

Go to latest
Published: Jun 20, 2023 License: MIT Imports: 32 Imported by: 0

Documentation

Overview

Package backend allows a Go program to import a standard Go package instead of self-hosting the backend API in a separate web server.

You need to call the Setup function to initialize all services passing a github.com/staticbackendhq/core/config.AppConfig. You may create environment variables and load the config directly by confing.Load function.

// You may load the configuration from environment variables
// config.Current = config.LoadConfig()

// this sample uses the in-memory database provider built-in
// you can use PostgreSQL or MongoDB
config.Current = config.AppConfig{
  AppEnv:      "dev",
  DataStore:   "mem",
  DatabaseURL: "mem",
  LocalStorageURL: "http://localhost:8099",
}
backend.Setup(config.Current)

The building blocks of StaticBackend are exported as variables and can be used directly accessing their interface's functions. For instance to use the github.com/staticbackendhq/core/cache.Volatilizer (cache and pub/sub) you'd use the Cache variable:

if err := backend.Cache.Set("key", "value"); err != nil {
  return err
}
val, err := backend.Cache.Get("key")

The available services are as follow:

You may see those services as raw building blocks that give you the most flexibility to build on top.

For easy of use, this package wraps important / commonly used functionalities into more developer friendly implementations.

For instance, the Membership function wants a github.com/staticbackendhq/core/model.DatabaseConfig and allows the caller to create account and user as well as reseting password etc.

usr := backend.Membership(base)
sessionToken, user, err := usr.CreateAccountAndUser("me@test.com", "passwd", 100)

To contrast, all of those can be done from your program by using the DB (github.com/staticbackendhq/core/database.Persister) data store, but for convenience this package offers easier / ready-made functions for common use-cases. Example for database CRUD and querying:

tasks := backend.Collection[Task](auth, base, "tasks")
newTask, err := tasks.Create(Task{Name: "testing"})

The Collection returns a strongly-typed structure where functions input/output are properly typed, it's a generic type.

StaticBackend makes your Go web application multi-tenant by default. For this reason you must supply a github.com/staticbackendhq/core/model.DatabaseConfig and (database) and sometimes a github.com/staticbackendhq/core/model.Auth (user performing the actions) to the different parts of the system so the data and security are applied to the right tenant, account and user.

You'd design your application around one or more tenants. Each tenant has their own database. It's fine to have one tenant/database. In that case you might create the tenant and its database and use the database ID in an environment variable. From a middleware you might load the database from this ID.

// if you'd want to use SB's middleware (it's not required)
// you use whatever you like for your web handlers and middleware.
// SB is a library not a framework.
func DetectTenant() middleware.Middleware {
  return func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      // check for presence of a public DB ID
      // this can come from cookie, URL query param
      key := r.Header.Get("DB-ID")
      // for multi-tenant, DB ID can be from an env var
      if len(key) == 0 {
        key = os.Getenv("SINGLE_TENANT_DBID")
      }
      var curDB model.DatabaseConfig
      if err := backend.Cache.GetTyped(key, &curDB); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
      }
      curDB, err := backend.DB.FindDatabase(key)
      // err != nil return HTTP 400 Bad request
      err = backend.Cache.SetTyped(key, curDB)
      // add the tenant's DB in context for the rest of
      // your pipeline to have the proper DB.
      ctx := r.Context()
      ctx = context.WithValue(ctx, ContextBase, curDB)
      next.ServeHTTP(w, r.WithContext(ctx)))
    })
  }
}

You'd create a similar middleware for adding the current user into the request context.

If you ever decide to switch to a multi-tenant design, you'd already be all set with this middleware, instead of getting the ID from the env variable, you'd define how the user should provide their database ID.

Example
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/staticbackendhq/core/backend"
	"github.com/staticbackendhq/core/config"
	"github.com/staticbackendhq/core/model"
)

type EntityDemo struct {
	ID        string `json:"id"`
	AccountID string `json:"accountId"`
	Name      string `json:"name"`
	Status    string `json:"status"`
}

func (x EntityDemo) String() string {
	return fmt.Sprintf("%s | %s", x.Name, x.Status)
}

func main() {
	// we initiate config.Current as type config.AppConfig
	// using the in-memory database engine.
	// You'd use PostgreSQL or Mongo in your real configuration

	// Also note that the config package has a LoadConfig() function that loads
	// config from environment variables i.e.:
	// config.Current = LoadConfig()
	config.Current = config.AppConfig{
		AppEnv:           "dev",
		Port:             "8099",
		DatabaseURL:      "mem",
		DataStore:        "mem",
		LocalStorageURL:  "http://localhost:8099",
		NoFullTextSearch: true,
	}

	// the Setup function will initialize all services based on config
	backend.Setup(config.Current)

	// StaticBackend is multi-tenant by default, so you'll minimaly need
	// at least one Tenant with their Database for your app
	//
	// In a real application you need to decide if your customers will
	// have their own Database (multi-tenant) or not.
	cus := model.Tenant{
		Email:    "new@tenant.com",
		IsActive: true,
		Created:  time.Now(),
	}
	cus, err := backend.DB.CreateTenant(cus)
	if err != nil {
		fmt.Println(err)
		return
	}

	base := model.DatabaseConfig{
		TenantID: cus.ID,
		Name:     "random-name-here",
		IsActive: true,
		Created:  time.Now(),
	}
	base, err = backend.DB.CreateDatabase(base)
	if err != nil {
		fmt.Println(err)
		return
	}

	// let's create a user in this new Database
	// You'll need to create an model.Account and model.User for each of
	// your users. They'll need a session token to authenticate.
	usr := backend.Membership(base)

	// Role 100 is for root user, root user is your app's super user.
	// As the builder of your application you have a special user which can
	// execute things on behalf of other users. This is very useful on
	// background tasks were your app does not have the user's session token.
	sessionToken, user, err := usr.CreateAccountAndUser("user1@mail.com", "passwd123456", 100)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(len(sessionToken) > 10)

	// In a real application, you'd store the session token for this user
	// inside local storage and/or a cookie etc. On each request you'd
	// request this session token and authenticate this user via a middleware.

	// we simulate having authenticated this user (from middleware normally)
	auth := model.Auth{
		AccountID: user.AccountID,
		UserID:    user.ID,
		Email:     user.Email,
		Role:      user.Role,
		Token:     user.Token,
	}

	// this is what you'd normally do in your web handlers to execute a request

	// we create a ready to use CRUD and Query collection that's typed with
	// our EntityDemo. In your application you'd get a Collection for your own
	// type, for instance: Product, Order, Customer, Blog, etc.
	//
	// Notice how we're passing the auth: current user and base: current database
	// so the operations are made from the proper user and in the proper DB/Tenant.
	entities := backend.Collection[EntityDemo](auth, base, "entities")

	// once we have this collection, we can perform database operations
	newEntity := EntityDemo{Name: "Go example code", Status: "new"}

	newEntity, err = entities.Create(newEntity)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(newEntity)

	// the Create function returned our EntityDemo with the ID and AccountID
	// filled, we can now update this record.
	newEntity.Status = "updated"

	newEntity, err = entities.Update(newEntity.ID, newEntity)
	if err != nil {
		fmt.Println(err)
		return
	}

	// let's fetch this entity via its ID to make sure our changes have
	// been persisted.
	check, err := entities.GetByID(newEntity.ID)
	if err != nil {
		fmt.Print(err)
		return
	}

	fmt.Println(check)
}
Output:

true
Go example code | new
Go example code | updated

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// Config reflect the configuration received on Setup
	Config config.AppConfig

	// DB initialized Persister data store
	DB database.Persister
	// Emailer initialized Mailer for sending emails
	Emailer email.Mailer
	// Filestore initialized Storer for raw save/delete blob file
	Filestore storage.Storer
	// Cache initialized Volatilizer for cache and pub/sub
	Cache  cache.Volatilizer
	Search *search.Search
	// Log initialized Logger for all logging
	Log *logger.Logger

	// Membership exposes Account and User functionalities like register, login, etc
	// account and user functionalities.
	Membership func(model.DatabaseConfig) User

	// Storage exposes file storage functionalities. It wraps the blob
	// storage as well as the database storage.
	Storage func(model.Auth, model.DatabaseConfig) FileStore

	// Scheduler to execute schedule jobs (only on PrimaryInstance)
	Scheduler *function.TaskScheduler
)

All StaticBackend services (need to call Setup before using them).

Functions

func BuildQueryFilters

func BuildQueryFilters(p ...any) (q [][]any, err error)

BuildQueryFilters helps building the proper slice of filters.

The arguments must be divided by 3 and has the following order:

field name | operator | value

backend.BuildQueryFilters("done", "=", false)

This would filter for the false value in the "done" field.

Supported operators: =, !=, >, <, >=, <=, in, !in

Example
package main

import (
	"fmt"

	"github.com/staticbackendhq/core/backend"
)

func main() {
	filters, err := backend.BuildQueryFilters(
		"done", "=", true,
		"effort", ">=", 15,
	)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(filters)

}
Output:

[[done = true] [effort >= 15]]

func GetJWT

func GetJWT(token string) ([]byte, error)

GetJWT returns a session token from a token

func Setup

func Setup(cfg config.AppConfig)

Setup initializes the core services based on the configuration received.

Types

type Database

type Database[T any] struct {
	// contains filtered or unexported fields
}

Database enables all CRUD and querying operations on a specific type

func Collection

func Collection[T any](auth model.Auth, base model.DatabaseConfig, col string) Database[T]

Collection returns a ready to use Database to perform DB operations on a specific type. You must pass auth which is the user performing the action and the tenant's database in which this action will be executed. The col is the name of the collection.

Collection name only accept alpha-numberic values and cannot start with a digit.

func (Database[T]) BulkCreate

func (d Database[T]) BulkCreate(entities []T) error

BulkCreate creates multiple records in the collection/repository

func (Database[T]) Create

func (d Database[T]) Create(data T) (inserted T, err error)

Create creates a record in the collection/repository

func (Database[T]) Delete

func (d Database[T]) Delete(id string) (int64, error)

Delete removes a record from a collection

func (Database[T]) GetByID

func (d Database[T]) GetByID(id string) (entity T, err error)

GetByID returns a specific record from a collection/repository

func (Database[T]) IncrementValue

func (d Database[T]) IncrementValue(id, field string, n int) error

IncrementValue increments or decrements a specifc field from a collection/repository

func (Database[T]) List

func (d Database[T]) List(lp model.ListParams) (res PagedResult[T], err error)

List returns records from a collection/repository using paging/sorting params

func (Database[T]) Query

func (d Database[T]) Query(filters [][]any, lp model.ListParams) (res PagedResult[T], err error)

Query returns records that match with the provided filters.

func (Database[T]) Update

func (d Database[T]) Update(id string, v any) (entity T, err error)

Update updates some fields of a record

func (Database[T]) UpdateMany

func (d Database[T]) UpdateMany(filters [][]any, v any) (int64, error)

UpdateMany updates multiple records matching filters

type FileStore

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

FileStore exposes file functions

func (FileStore) Delete

func (f FileStore) Delete(fileID string) error

Delete removes a file from storage and database

func (FileStore) Save

func (f FileStore) Save(filename, name string, file io.ReadSeeker, size int64) (sf SavedFile, err error)

Save saves a file content to the file storage (Storer interface) and to the database

type MagicLinkData

type MagicLinkData struct {
	FromEmail string `json:"fromEmail"`
	FromName  string `json:"fromName"`
	Email     string `json:"email"`
	Subject   string `json:"subject"`
	Body      string `json:"body"`
	MagicLink string `json:"link"`
}

MagicLinkData magic links for no-password sign-in

type PagedResult

type PagedResult[T any] struct {
	Page    int64
	Size    int64
	Total   int64
	Results []T
}

PageResult wraps a slice of type T with paging information

type SavedFile

type SavedFile struct {
	ID  string `json:"id"`
	URL string `json:"url"`
}

SavedFile when a file is saved it has an ID and an URL

type User

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

User handles everything related to accounts and users inside a database

func (User) Authenticate

func (u User) Authenticate(email, password string) (string, error)

Authenticate tries to authenticate an email/password and return a session token

func (User) CreateAccountAndUser

func (u User) CreateAccountAndUser(email, password string, role int) ([]byte, model.User, error)

CreateAccountAndUser creates an account with a user

func (User) CreateUser

func (u User) CreateUser(accountID, email, password string, role int) ([]byte, model.User, error)

CreateUser creates a user for an Account

func (User) GetAuthToken

func (u User) GetAuthToken(tok model.User) (jwtBytes []byte, err error)

GetAuthToken returns a session token for a user

func (User) Register

func (u User) Register(email, password string) (string, error)

Register creates a new account and user

func (User) ResetPassword

func (u User) ResetPassword(email, code, password string) error

ResetPassword resets the password of a matching email/code for a user

func (User) SetPasswordResetCode

func (u User) SetPasswordResetCode(email, code string) error

SetPasswordResetCode sets the password forget code for a user

func (User) SetUserRole

func (u User) SetUserRole(email string, role int) error

SetUserRole changes the role of a user

func (u User) SetupMagicLink(data MagicLinkData) error

SetupMagicLink initialize a magic link and send the email to the user

func (User) UserSetPassword

func (u User) UserSetPassword(email, oldpw, newpw string) error

UserSetPassword password changes initiated by the user

func (u User) ValidateMagicLink(email, code string) (string, error)

ValidateMagicLink validates a magic link code and returns a session token on success

Jump to

Keyboard shortcuts

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