mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender.git
synced 2025-07-16 09:38:49 +02:00
feat:#10 added new minimal backend
This commit is contained in:
52
datamanager/backend/apis/base.go
Normal file
52
datamanager/backend/apis/base.go
Normal file
@ -0,0 +1,52 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"datamanager/backend/core"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/labstack/echo/v5/middleware"
|
||||
"github.com/pocketbase/pocketbase/tools/rest"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const fieldsQueryParam = "fields"
|
||||
|
||||
func InitApi(app core.App) (*echo.Echo, error) {
|
||||
e := echo.New()
|
||||
e.Debug = false
|
||||
e.Binder = &rest.MultiBinder{}
|
||||
e.JSONSerializer = &rest.Serializer{
|
||||
FieldsParam: fieldsQueryParam,
|
||||
}
|
||||
|
||||
// configure a custom router
|
||||
e.ResetRouterCreator(func(ec *echo.Echo) echo.Router {
|
||||
return echo.NewRouter(echo.RouterConfig{
|
||||
UnescapePathParamValues: true,
|
||||
AllowOverwritingRoute: true,
|
||||
})
|
||||
})
|
||||
|
||||
// default middlewares
|
||||
e.Pre(middleware.RemoveTrailingSlashWithConfig(middleware.RemoveTrailingSlashConfig{
|
||||
Skipper: func(c echo.Context) bool {
|
||||
// enable by default only for the API routes
|
||||
return !strings.HasPrefix(c.Request().URL.Path, "/api/")
|
||||
},
|
||||
}))
|
||||
e.Use(middleware.Recover())
|
||||
e.Use(middleware.Secure())
|
||||
|
||||
// custom error handler
|
||||
e.HTTPErrorHandler = func(c echo.Context, err error) {
|
||||
if err == nil {
|
||||
return // no error
|
||||
}
|
||||
|
||||
if c.Response().Committed {
|
||||
return // already committed
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
180
datamanager/backend/apis/serve.go
Normal file
180
datamanager/backend/apis/serve.go
Normal file
@ -0,0 +1,180 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"datamanager/backend/core"
|
||||
"github.com/fatih/color"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/labstack/echo/v5/middleware"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"golang.org/x/crypto/acme"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ServeConfig defines a configuration struct for apis.Serve().
|
||||
type ServeConfig struct {
|
||||
// ShowStartBanner indicates whether to show or hide the server start console message.
|
||||
ShowStartBanner bool
|
||||
|
||||
// HttpAddr is the TCP address to listen for the HTTP server (eg. `127.0.0.1:80`).
|
||||
HttpAddr string
|
||||
|
||||
// HttpsAddr is the TCP address to listen for the HTTPS server (eg. `127.0.0.1:443`).
|
||||
HttpsAddr string
|
||||
|
||||
// Optional domains list to use when issuing the TLS certificate.
|
||||
//
|
||||
// If not set, the host from the bound server address will be used.
|
||||
//
|
||||
// For convenience, for each "non-www" domain a "www" entry and
|
||||
// redirect will be automatically added.
|
||||
CertificateDomains []string
|
||||
|
||||
// AllowedOrigins is an optional list of CORS origins (default to "*").
|
||||
AllowedOrigins []string
|
||||
}
|
||||
|
||||
func Serve(app core.App, config ServeConfig) (*http.Server, error) {
|
||||
if len(config.AllowedOrigins) == 0 {
|
||||
config.AllowedOrigins = []string{"*"}
|
||||
}
|
||||
|
||||
router, err := InitApi(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// configure cors
|
||||
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
Skipper: middleware.DefaultSkipper,
|
||||
AllowOrigins: config.AllowedOrigins,
|
||||
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
|
||||
}))
|
||||
|
||||
// start http server
|
||||
// ---
|
||||
mainAddr := config.HttpAddr
|
||||
if config.HttpsAddr != "" {
|
||||
mainAddr = config.HttpsAddr
|
||||
}
|
||||
|
||||
var wwwRedirects []string
|
||||
|
||||
// extract the host names for the certificate host policy
|
||||
hostNames := config.CertificateDomains
|
||||
if len(hostNames) == 0 {
|
||||
host, _, _ := net.SplitHostPort(mainAddr)
|
||||
hostNames = append(hostNames, host)
|
||||
}
|
||||
for _, host := range hostNames {
|
||||
if strings.HasPrefix(host, "www.") {
|
||||
continue // explicitly set www host
|
||||
}
|
||||
|
||||
wwwHost := "www." + host
|
||||
if !list.ExistInSlice(wwwHost, hostNames) {
|
||||
hostNames = append(hostNames, wwwHost)
|
||||
wwwRedirects = append(wwwRedirects, wwwHost)
|
||||
}
|
||||
}
|
||||
|
||||
// implicit www->non-www redirect(s)
|
||||
if len(wwwRedirects) > 0 {
|
||||
router.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
host := c.Request().Host
|
||||
|
||||
if strings.HasPrefix(host, "www.") && list.ExistInSlice(host, wwwRedirects) {
|
||||
return c.Redirect(
|
||||
http.StatusTemporaryRedirect,
|
||||
c.Scheme()+"://"+host[4:]+c.Request().RequestURI,
|
||||
)
|
||||
}
|
||||
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// base request context used for cancelling long running requests
|
||||
// like the SSE connections
|
||||
baseCtx, cancelBaseCtx := context.WithCancel(context.Background())
|
||||
defer cancelBaseCtx()
|
||||
|
||||
server := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{acme.ALPNProto},
|
||||
},
|
||||
ReadTimeout: 10 * time.Minute,
|
||||
ReadHeaderTimeout: 30 * time.Second,
|
||||
// WriteTimeout: 60 * time.Second, // breaks sse!
|
||||
Handler: router,
|
||||
Addr: mainAddr,
|
||||
BaseContext: func(l net.Listener) context.Context {
|
||||
return baseCtx
|
||||
},
|
||||
}
|
||||
|
||||
if config.ShowStartBanner {
|
||||
schema := "http"
|
||||
addr := server.Addr
|
||||
|
||||
if config.HttpsAddr != "" {
|
||||
schema = "https"
|
||||
|
||||
if len(config.CertificateDomains) > 0 {
|
||||
addr = config.CertificateDomains[0]
|
||||
}
|
||||
}
|
||||
|
||||
date := new(strings.Builder)
|
||||
log.New(date, "", log.LstdFlags).Print()
|
||||
|
||||
bold := color.New(color.Bold).Add(color.FgGreen)
|
||||
_, _ = bold.Printf(
|
||||
"%s Server started at %s\n",
|
||||
strings.TrimSpace(date.String()),
|
||||
color.CyanString("%s://%s", schema, addr),
|
||||
)
|
||||
}
|
||||
|
||||
// WaitGroup to block until server.ShutDown() returns because Serve and similar methods exit immediately.
|
||||
// Note that the WaitGroup would not do anything if the app.OnTerminate() hook isn't triggered.
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// wait for the graceful shutdown to complete before exit
|
||||
defer wg.Wait()
|
||||
|
||||
// ---
|
||||
// @todo consider removing the server return value because it is
|
||||
// not really useful when combined with the blocking serve calls
|
||||
// ---
|
||||
|
||||
// start HTTPS server
|
||||
if config.HttpsAddr != "" {
|
||||
// if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version
|
||||
if config.HttpAddr != "" {
|
||||
go func() {
|
||||
err := http.ListenAndServe(config.HttpAddr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently)
|
||||
}))
|
||||
if err != nil {
|
||||
slog.Error("Failed to start HTTP server for redirect: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return server, server.ListenAndServeTLS("", "")
|
||||
}
|
||||
|
||||
// OR start HTTP server
|
||||
return server, server.ListenAndServe()
|
||||
}
|
188
datamanager/backend/backend.go
Normal file
188
datamanager/backend/backend.go
Normal file
@ -0,0 +1,188 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"datamanager/backend/cmd"
|
||||
"datamanager/backend/core"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var _ core.App = (*Backend)(nil)
|
||||
|
||||
// Version of Backend
|
||||
var Version = "(untracked)"
|
||||
|
||||
type appWrapper struct {
|
||||
core.App
|
||||
}
|
||||
|
||||
type Backend struct {
|
||||
*appWrapper
|
||||
|
||||
devFlag bool
|
||||
dataDirFlag string
|
||||
encryptionEnvFlag string
|
||||
hideStartBanner bool
|
||||
|
||||
// RootCmd is the main console command
|
||||
RootCmd *cobra.Command
|
||||
}
|
||||
|
||||
func New() *Backend {
|
||||
_, isUsingGoRun := inspectRuntime()
|
||||
|
||||
return NewWithConfig(Config{
|
||||
DefaultDev: isUsingGoRun,
|
||||
DBUsername: "neo4j",
|
||||
DBPassword: "htwkalender-db",
|
||||
})
|
||||
}
|
||||
|
||||
// inspectRuntime tries to find the base executable directory and how it was run.
|
||||
func inspectRuntime() (baseDir string, withGoRun bool) {
|
||||
if strings.HasPrefix(os.Args[0], os.TempDir()) {
|
||||
// probably ran with go run
|
||||
withGoRun = true
|
||||
baseDir, _ = os.Getwd()
|
||||
} else {
|
||||
// probably ran with go build
|
||||
withGoRun = false
|
||||
baseDir = filepath.Dir(os.Args[0])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// optional default values for the console flags
|
||||
DefaultDev bool
|
||||
DefaultDataDir string // if not set, it will fallback to "./pb_data"
|
||||
DBUsername string
|
||||
DBPassword string
|
||||
}
|
||||
|
||||
func NewWithConfig(config Config) *Backend {
|
||||
// initialize a default data directory based on the executable baseDir
|
||||
if config.DefaultDataDir == "" {
|
||||
baseDir, _ := inspectRuntime()
|
||||
config.DefaultDataDir = filepath.Join(baseDir, "data")
|
||||
}
|
||||
|
||||
backend := &Backend{
|
||||
RootCmd: &cobra.Command{
|
||||
Use: filepath.Base(os.Args[0]),
|
||||
Short: "Backend CLI",
|
||||
Version: Version,
|
||||
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||
UnknownFlags: true,
|
||||
},
|
||||
// no need to provide the default cobra completion command
|
||||
CompletionOptions: cobra.CompletionOptions{
|
||||
DisableDefaultCmd: true,
|
||||
},
|
||||
},
|
||||
devFlag: config.DefaultDev,
|
||||
dataDirFlag: config.DefaultDataDir,
|
||||
}
|
||||
|
||||
// replace with a colored stderr writer
|
||||
backend.RootCmd.SetErr(newErrWriter())
|
||||
|
||||
// parse base flags
|
||||
// (errors are ignored, since the full flags parsing happens on Execute())
|
||||
_ = backend.eagerParseFlags(&config)
|
||||
|
||||
// initialize the app instance
|
||||
backend.appWrapper = &appWrapper{core.NewBaseApp(core.BaseAppConfig{
|
||||
IsDev: backend.devFlag,
|
||||
DataDir: backend.dataDirFlag,
|
||||
DBUsername: config.DBUsername,
|
||||
DBPassword: config.DBPassword,
|
||||
})}
|
||||
|
||||
// hide the default help command (allow only `--help` flag)
|
||||
backend.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
|
||||
|
||||
return backend
|
||||
}
|
||||
|
||||
func newErrWriter() *coloredWriter {
|
||||
return &coloredWriter{
|
||||
w: os.Stderr,
|
||||
c: color.New(color.FgRed),
|
||||
}
|
||||
}
|
||||
|
||||
// coloredWriter is a small wrapper struct to construct a [color.Color] writter.
|
||||
type coloredWriter struct {
|
||||
w io.Writer
|
||||
c *color.Color
|
||||
}
|
||||
|
||||
// Write writes the p bytes using the colored writer.
|
||||
func (colored *coloredWriter) Write(p []byte) (n int, err error) {
|
||||
colored.c.SetWriter(colored.w)
|
||||
defer colored.c.UnsetWriter(colored.w)
|
||||
|
||||
return colored.c.Print(string(p))
|
||||
}
|
||||
|
||||
func (backend *Backend) eagerParseFlags(config *Config) error {
|
||||
backend.RootCmd.PersistentFlags().StringVar(
|
||||
&backend.dataDirFlag,
|
||||
"dir",
|
||||
config.DefaultDataDir,
|
||||
"the PocketBase data directory",
|
||||
)
|
||||
|
||||
backend.RootCmd.PersistentFlags().BoolVar(
|
||||
&backend.devFlag,
|
||||
"dev",
|
||||
config.DefaultDev,
|
||||
"enable dev mode, aka. printing logs and cypher/sql statements to the console",
|
||||
)
|
||||
|
||||
return backend.RootCmd.ParseFlags(os.Args[1:])
|
||||
}
|
||||
|
||||
func (backend *Backend) Start() error {
|
||||
// register system commands
|
||||
backend.RootCmd.AddCommand(cmd.NewServeCommand(backend, !backend.hideStartBanner))
|
||||
|
||||
return backend.Execute()
|
||||
}
|
||||
|
||||
func (backend *Backend) Execute() error {
|
||||
done := make(chan bool, 1)
|
||||
|
||||
// listen for interrupt signal to gracefully shutdown the application
|
||||
go func() {
|
||||
sigch := make(chan os.Signal, 1)
|
||||
signal.Notify(sigch, os.Interrupt, syscall.SIGTERM)
|
||||
<-sigch
|
||||
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// execute the root command
|
||||
go func() {
|
||||
// note: leave to the commands to decide whether to print their error
|
||||
_ = backend.RootCmd.Execute()
|
||||
|
||||
done <- true
|
||||
}()
|
||||
|
||||
<-done
|
||||
|
||||
// trigger cleanups
|
||||
return backend.OnTerminate().Trigger(&core.TerminateEvent{
|
||||
App: backend,
|
||||
}, func(e *core.TerminateEvent) error {
|
||||
return e.App.ResetBootstrapState()
|
||||
})
|
||||
}
|
76
datamanager/backend/cmd/serve.go
Normal file
76
datamanager/backend/cmd/serve.go
Normal file
@ -0,0 +1,76 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"datamanager/backend/apis"
|
||||
"datamanager/backend/core"
|
||||
"errors"
|
||||
"github.com/spf13/cobra"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NewServeCommand creates and returns new command responsible for
|
||||
// starting the default PocketBase web server.
|
||||
func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
|
||||
var allowedOrigins []string
|
||||
var httpAddr string
|
||||
var httpsAddr string
|
||||
|
||||
command := &cobra.Command{
|
||||
Use: "serve [domain(s)]",
|
||||
Args: cobra.ArbitraryArgs,
|
||||
Short: "Starts the web server (default to 127.0.0.1:8090 if no domain is specified)",
|
||||
SilenceUsage: true,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
// set default listener addresses if at least one domain is specified
|
||||
if len(args) > 0 {
|
||||
if httpAddr == "" {
|
||||
httpAddr = "0.0.0.0:80"
|
||||
}
|
||||
if httpsAddr == "" {
|
||||
httpsAddr = "0.0.0.0:443"
|
||||
}
|
||||
} else {
|
||||
if httpAddr == "" {
|
||||
httpAddr = "127.0.0.1:8090"
|
||||
}
|
||||
}
|
||||
|
||||
_, err := apis.Serve(app, apis.ServeConfig{
|
||||
HttpAddr: httpAddr,
|
||||
HttpsAddr: httpsAddr,
|
||||
ShowStartBanner: showStartBanner,
|
||||
AllowedOrigins: allowedOrigins,
|
||||
CertificateDomains: args,
|
||||
})
|
||||
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
command.PersistentFlags().StringSliceVar(
|
||||
&allowedOrigins,
|
||||
"origins",
|
||||
[]string{"*"},
|
||||
"CORS allowed domain origins list",
|
||||
)
|
||||
|
||||
command.PersistentFlags().StringVar(
|
||||
&httpAddr,
|
||||
"http",
|
||||
"",
|
||||
"TCP address to listen for the HTTP server\n(if domain args are specified - default to 0.0.0.0:80, otherwise - default to 127.0.0.1:8090)",
|
||||
)
|
||||
|
||||
command.PersistentFlags().StringVar(
|
||||
&httpsAddr,
|
||||
"https",
|
||||
"",
|
||||
"TCP address to listen for the HTTPS server\n(if domain args are specified - default to 0.0.0.0:443, otherwise - default to empty string, aka. no TLS)\nThe incoming HTTP traffic also will be auto redirected to the HTTPS version",
|
||||
)
|
||||
|
||||
return command
|
||||
}
|
55
datamanager/backend/core/app.go
Normal file
55
datamanager/backend/core/app.go
Normal file
@ -0,0 +1,55 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"datamanager/backend/dbx"
|
||||
"datamanager/backend/tools/hook"
|
||||
)
|
||||
|
||||
type App interface {
|
||||
// DB returns the default app database instance.
|
||||
DB() *dbx.DB
|
||||
OnTerminate() *hook.Hook[*TerminateEvent]
|
||||
ResetBootstrapState() error
|
||||
}
|
||||
|
||||
func NewBaseApp(config BaseAppConfig) App {
|
||||
|
||||
app := &BaseApp{
|
||||
isDev: config.IsDev,
|
||||
dataDir: config.DataDir,
|
||||
db: *dbx.NewDB("neo4j", "password"),
|
||||
onTerminate: &hook.Hook[*TerminateEvent]{},
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
type BaseApp struct {
|
||||
// configurable parameters
|
||||
isDev bool
|
||||
dataDir string
|
||||
db dbx.DB
|
||||
onTerminate *hook.Hook[*TerminateEvent]
|
||||
}
|
||||
|
||||
func (app *BaseApp) DB() *dbx.DB {
|
||||
db := app.db
|
||||
|
||||
return &db
|
||||
}
|
||||
|
||||
func (app *BaseApp) ResetBootstrapState() error {
|
||||
|
||||
if app.DB() != nil {
|
||||
if err := app.db.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// replace the db instance with nil
|
||||
app.db = dbx.DB{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BaseApp) OnTerminate() *hook.Hook[*TerminateEvent] {
|
||||
return app.onTerminate
|
||||
}
|
8
datamanager/backend/core/base.go
Normal file
8
datamanager/backend/core/base.go
Normal file
@ -0,0 +1,8 @@
|
||||
package core
|
||||
|
||||
type BaseAppConfig struct {
|
||||
IsDev bool
|
||||
DataDir string
|
||||
DBUsername string
|
||||
DBPassword string
|
||||
}
|
6
datamanager/backend/core/events.go
Normal file
6
datamanager/backend/core/events.go
Normal file
@ -0,0 +1,6 @@
|
||||
package core
|
||||
|
||||
type TerminateEvent struct {
|
||||
App App
|
||||
IsRestart bool
|
||||
}
|
90
datamanager/backend/dbx/db.go
Normal file
90
datamanager/backend/dbx/db.go
Normal file
@ -0,0 +1,90 @@
|
||||
package dbx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type (
|
||||
DB struct {
|
||||
driver neo4j.DriverWithContext
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// Errors represents a list of errors.
|
||||
Errors []error
|
||||
)
|
||||
|
||||
func NewDB(username string, password string) *DB {
|
||||
driver, err := neo4j.NewDriverWithContext("bolt://localhost:7687", neo4j.BasicAuth(username, password, ""))
|
||||
if err != nil {
|
||||
slog.Error("Failed to create a new driver: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &DB{
|
||||
driver: driver,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
// Context returns the context associated with the DB instance.
|
||||
// It returns nil if no context is associated.
|
||||
func (db *DB) Context() context.Context {
|
||||
return db.ctx
|
||||
}
|
||||
|
||||
// Close closes the database, releasing any open resources.
|
||||
func (db *DB) Close() error {
|
||||
if db.driver != nil {
|
||||
return db.driver.Close(db.ctx)
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Executor prepares, executes, or queries a SQL statement.
|
||||
type Executor interface {
|
||||
Query(query string, args ...interface{}) (*neo4j.ResultWithContext, error)
|
||||
// QueryContext queries a SQL statement with the given context
|
||||
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
||||
// Prepare creates a prepared statement
|
||||
Prepare(query string) (*sql.Stmt, error)
|
||||
}
|
||||
|
||||
// Query represents a neo4j query that can be executed.
|
||||
type Query struct {
|
||||
Executor Executor
|
||||
cypher, rawCypher string
|
||||
}
|
||||
|
||||
func helloWorld(ctx context.Context, uri, username, password string) (string, error) {
|
||||
driver, err := neo4j.NewDriverWithContext(uri, neo4j.BasicAuth(username, password, ""))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
|
||||
|
||||
greeting, err := session.ExecuteWrite(ctx, func(transaction neo4j.ManagedTransaction) (any, error) {
|
||||
result, err := transaction.Run(ctx,
|
||||
"CREATE (a:Greeting) SET a.message = $message RETURN a.message + ', from node ' + id(a)",
|
||||
map[string]any{"message": "hello, world"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.Next(ctx) {
|
||||
return result.Record().Values[0], nil
|
||||
}
|
||||
|
||||
return nil, result.Err()
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return greeting.(string), nil
|
||||
}
|
59
datamanager/backend/tools/hook/hook.go
Normal file
59
datamanager/backend/tools/hook/hook.go
Normal file
@ -0,0 +1,59 @@
|
||||
package hook
|
||||
|
||||
import (
|
||||
"datamanager/backend/tools/security"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Handler[T any] func(e T) error
|
||||
type handlerPair[T any] struct {
|
||||
id string
|
||||
handler Handler[T]
|
||||
}
|
||||
|
||||
type Hook[T any] struct {
|
||||
mux sync.RWMutex
|
||||
handlers []*handlerPair[T]
|
||||
}
|
||||
|
||||
var StopPropagation = errors.New("event hook propagation stopped")
|
||||
|
||||
func (h *Hook[T]) Trigger(data T, oneOffHandlers ...Handler[T]) error {
|
||||
h.mux.RLock()
|
||||
|
||||
handlers := make([]*handlerPair[T], 0, len(h.handlers)+len(oneOffHandlers))
|
||||
handlers = append(handlers, h.handlers...)
|
||||
|
||||
// append the one off handlers
|
||||
for i, oneOff := range oneOffHandlers {
|
||||
handlers = append(handlers, &handlerPair[T]{
|
||||
id: fmt.Sprintf("@%d", i),
|
||||
handler: oneOff,
|
||||
})
|
||||
}
|
||||
|
||||
// unlock is not deferred to avoid deadlocks in case Trigger
|
||||
// is called recursively by the handlers
|
||||
h.mux.RUnlock()
|
||||
|
||||
for _, item := range handlers {
|
||||
err := item.handler(data)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if errors.Is(err, StopPropagation) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateHookId() string {
|
||||
return security.PseudorandomString(8)
|
||||
}
|
11
datamanager/backend/tools/list/list.go
Normal file
11
datamanager/backend/tools/list/list.go
Normal file
@ -0,0 +1,11 @@
|
||||
package list
|
||||
|
||||
func ExistInSlice[T comparable](item T, list []T) bool {
|
||||
for _, v := range list {
|
||||
if v == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
18
datamanager/main.go
Normal file
18
datamanager/main.go
Normal file
@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"datamanager/backend"
|
||||
"datamanager/service"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
app := backend.New()
|
||||
|
||||
service.AddRoutes(app)
|
||||
|
||||
if err := app.Start(); err != nil {
|
||||
slog.Error("Failed to start app: %v", err)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user