restructered some of the internals and added basic js app hooks support
This commit is contained in:
+50
-26
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user