restructered some of the internals and added basic js app hooks support

This commit is contained in:
Gani Georgiev
2023-06-08 17:59:08 +03:00
parent ff5508cb79
commit 3cf3e04866
24 changed files with 1218 additions and 422 deletions
+50 -26
View File
@@ -66,43 +66,67 @@ func NewUnauthorizedError(message string, data any) *ApiError {
// NewApiError creates and returns new normalized `ApiError` instance.
func NewApiError(status int, message string, data any) *ApiError {
message = inflector.Sentenize(message)
formattedData := map[string]any{}
if v, ok := data.(validation.Errors); ok {
formattedData = resolveValidationErrors(v)
}
return &ApiError{
rawData: data,
Data: formattedData,
Data: safeErrorsData(data),
Code: status,
Message: strings.TrimSpace(message),
Message: strings.TrimSpace(inflector.Sentenize(message)),
}
}
func resolveValidationErrors(validationErrors validation.Errors) map[string]any {
func safeErrorsData(data any) map[string]any {
switch v := data.(type) {
case validation.Errors:
return resolveSafeErrorsData[error](v)
case map[string]validation.Error:
return resolveSafeErrorsData[validation.Error](v)
case map[string]error:
return resolveSafeErrorsData[error](v)
case map[string]any:
return resolveSafeErrorsData[any](v)
default:
return map[string]any{} // not nil to ensure that is json serialized as object
}
}
func resolveSafeErrorsData[T any](data map[string]T) map[string]any {
result := map[string]any{}
// extract from each validation error its error code and message.
for name, err := range validationErrors {
// check for nested errors
if nestedErrs, ok := err.(validation.Errors); ok {
result[name] = resolveValidationErrors(nestedErrs)
for name, err := range data {
if isNestedError(err) {
result[name] = safeErrorsData(err)
continue
}
errCode := "validation_invalid_value" // default
if errObj, ok := err.(validation.ErrorObject); ok {
errCode = errObj.Code()
}
result[name] = map[string]string{
"code": errCode,
"message": inflector.Sentenize(err.Error()),
}
result[name] = resolveSafeErrorItem(err)
}
return result
}
func isNestedError(err any) bool {
switch err.(type) {
case validation.Errors, map[string]validation.Error, map[string]error, map[string]any:
return true
}
return false
}
// resolveSafeErrorItem extracts from each validation error its
// public safe error code and message.
func resolveSafeErrorItem(err any) map[string]string {
// default public safe error values
code := "validation_invalid_value"
msg := "Invalid value."
// only validation errors are public safe
if obj, ok := err.(validation.Error); ok {
code = obj.Code()
msg = inflector.Sentenize(obj.Error())
}
return map[string]string{
"code": code,
"message": msg,
}
}
+10
View File
@@ -11,6 +11,7 @@ import (
"path/filepath"
"strings"
"github.com/dop251/goja"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
"github.com/pocketbase/pocketbase/core"
@@ -34,6 +35,7 @@ func InitApi(app core.App) (*echo.Echo, error) {
e.ResetRouterCreator(func(ec *echo.Echo) echo.Router {
return echo.NewRouter(echo.RouterConfig{
UnescapePathParamValues: true,
AllowOverwritingRoute: true,
})
})
@@ -58,6 +60,14 @@ func InitApi(app core.App) (*echo.Echo, error) {
return
}
// manually extract the goja exception error value for
// consistency when throwing or returning errors
if jsException, ok := err.(*goja.Exception); ok {
if wrapped, ok := jsException.Value().Export().(error); ok {
err = wrapped
}
}
var apiErr *ApiError
switch v := err.(type) {
+2
View File
@@ -57,6 +57,8 @@ func RequestData(c echo.Context) *models.RequestData {
return result
}
// RecordAuthResponse generates and writes a properly formatted record
// auth response into the specified request context.
func RecordAuthResponse(
app core.App,
c echo.Context,
+32 -29
View File
@@ -21,22 +21,25 @@ import (
"golang.org/x/crypto/acme/autocert"
)
// ServeOptions defines an optional struct for apis.Serve().
type ServeOptions struct {
// 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 string
HttpsAddr string
AllowedOrigins []string // optional list of CORS origins (default to "*")
// HttpAddr is the HTTP server address to bind (eg. `127.0.0.1:80`).
HttpAddr string
// HttpsAddr is the HTTPS server address to bind (eg. `127.0.0.1:443`).
HttpsAddr string
// AllowedOrigins is an optional list of CORS origins (default to "*").
AllowedOrigins []string
}
// Serve starts a new app web server.
func Serve(app core.App, options *ServeOptions) error {
if options == nil {
options = &ServeOptions{}
}
if len(options.AllowedOrigins) == 0 {
options.AllowedOrigins = []string{"*"}
func Serve(app core.App, config ServeConfig) error {
if len(config.AllowedOrigins) == 0 {
config.AllowedOrigins = []string{"*"}
}
// ensure that the latest migrations are applied before starting the server
@@ -61,15 +64,15 @@ func Serve(app core.App, options *ServeOptions) error {
// configure cors
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
Skipper: middleware.DefaultSkipper,
AllowOrigins: options.AllowedOrigins,
AllowOrigins: config.AllowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}))
// start http server
// ---
mainAddr := options.HttpAddr
if options.HttpsAddr != "" {
mainAddr = options.HttpsAddr
mainAddr := config.HttpAddr
if config.HttpsAddr != "" {
mainAddr = config.HttpsAddr
}
mainHost, _, _ := net.SplitHostPort(mainAddr)
@@ -80,7 +83,7 @@ func Serve(app core.App, options *ServeOptions) error {
HostPolicy: autocert.HostWhitelist(mainHost, "www."+mainHost),
}
serverConfig := &http.Server{
server := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: certManager.GetCertificate,
NextProtos: []string{acme.ALPNProto},
@@ -95,16 +98,16 @@ func Serve(app core.App, options *ServeOptions) error {
serveEvent := &core.ServeEvent{
App: app,
Router: router,
Server: serverConfig,
Server: server,
CertManager: certManager,
}
if err := app.OnBeforeServe().Trigger(serveEvent); err != nil {
return err
}
if options.ShowStartBanner {
if config.ShowStartBanner {
schema := "http"
if options.HttpsAddr != "" {
if config.HttpsAddr != "" {
schema = "https"
}
@@ -115,34 +118,34 @@ func Serve(app core.App, options *ServeOptions) error {
bold.Printf(
"%s Server started at %s\n",
strings.TrimSpace(date.String()),
color.CyanString("%s://%s", schema, serverConfig.Addr),
color.CyanString("%s://%s", schema, server.Addr),
)
regular := color.New()
regular.Printf(" REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr))
regular.Printf(" Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr))
regular.Printf("├─ REST API: %s\n", color.CyanString("%s://%s/api/", schema, server.Addr))
regular.Printf("└─ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, server.Addr))
}
// try to gracefully shutdown the server on app termination
app.OnTerminate().Add(func(e *core.TerminateEvent) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
serverConfig.Shutdown(ctx)
server.Shutdown(ctx)
return nil
})
// start HTTPS server
if options.HttpsAddr != "" {
if config.HttpsAddr != "" {
// if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version
if options.HttpAddr != "" {
go http.ListenAndServe(options.HttpAddr, certManager.HTTPHandler(nil))
if config.HttpAddr != "" {
go http.ListenAndServe(config.HttpAddr, certManager.HTTPHandler(nil))
}
return serverConfig.ListenAndServeTLS("", "")
return server.ListenAndServeTLS("", "")
}
// OR start HTTP server
return serverConfig.ListenAndServe()
return server.ListenAndServe()
}
type migrationsConnection struct {