restructered some of the internals and added basic js app hooks support
This commit is contained in:
parent
ff5508cb79
commit
3cf3e04866
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -33,6 +33,36 @@
|
||||||
|
|
||||||
- (@todo docs) Added `Dao.WithoutHooks()` helper to create a new `Dao` from the current one but without the create/update/delete hooks.
|
- (@todo docs) Added `Dao.WithoutHooks()` helper to create a new `Dao` from the current one but without the create/update/delete hooks.
|
||||||
|
|
||||||
|
- **!** Renamed `*Options` to `*Config` for consistency and replaced the unnecessary pointers with their value equivalent to keep the applied configuration defaults isolated within their function calls:
|
||||||
|
```go
|
||||||
|
old: pocketbase.NewWithConfig(config *pocketbase.Config)
|
||||||
|
new: pocketbase.NewWithConfig(config pocketbase.Config)
|
||||||
|
|
||||||
|
old: core.NewBaseApp(config *core.BaseAppConfig)
|
||||||
|
new: core.NewBaseApp(config core.BaseAppConfig)
|
||||||
|
|
||||||
|
old: apis.Serve(app core.App, options *apis.ServeOptions)
|
||||||
|
new: apis.Serve(app core.App, config apis.ServeConfig)
|
||||||
|
|
||||||
|
old: jsvm.MustRegisterMigrations(app core.App, options *jsvm.MigrationsOptions)
|
||||||
|
new: jsvm.MustRegisterMigrations(app core.App, config jsvm.MigrationsConfig)
|
||||||
|
|
||||||
|
old: ghupdate.MustRegister(app core.App, rootCmd *cobra.Command, options *ghupdate.Options)
|
||||||
|
new: ghupdate.MustRegister(app core.App, rootCmd *cobra.Command, config ghupdate.Config)
|
||||||
|
|
||||||
|
old: migratecmd.MustRegister(app core.App, rootCmd *cobra.Command, options *migratecmd.Options)
|
||||||
|
new: migratecmd.MustRegister(app core.App, rootCmd *cobra.Command, config migratecmd.Config)
|
||||||
|
```
|
||||||
|
|
||||||
|
- (@todo docs) Added new optional JavaScript app hooks binding via [goja](https://github.com/dop251/goja).
|
||||||
|
There are available by default with the prebuilt executable if you add a `*.pb.js` file in `pb_hooks` directory.
|
||||||
|
To enable them as part of a custom Go build:
|
||||||
|
```go
|
||||||
|
jsvm.MustRegisterHooks(app core.App, config jsvm.HooksConfig{})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Refactored `apis.ApiError` validation errors serialization to allow `map[string]error` and `map[string]any` when generating the public safe formatted `ApiError.Data`.
|
||||||
|
|
||||||
|
|
||||||
## v0.16.4
|
## v0.16.4
|
||||||
|
|
||||||
|
|
|
@ -66,43 +66,67 @@ func NewUnauthorizedError(message string, data any) *ApiError {
|
||||||
|
|
||||||
// NewApiError creates and returns new normalized `ApiError` instance.
|
// NewApiError creates and returns new normalized `ApiError` instance.
|
||||||
func NewApiError(status int, message string, data any) *ApiError {
|
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{
|
return &ApiError{
|
||||||
rawData: data,
|
rawData: data,
|
||||||
Data: formattedData,
|
Data: safeErrorsData(data),
|
||||||
Code: status,
|
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{}
|
result := map[string]any{}
|
||||||
|
|
||||||
// extract from each validation error its error code and message.
|
for name, err := range data {
|
||||||
for name, err := range validationErrors {
|
if isNestedError(err) {
|
||||||
// check for nested errors
|
result[name] = safeErrorsData(err)
|
||||||
if nestedErrs, ok := err.(validation.Errors); ok {
|
|
||||||
result[name] = resolveValidationErrors(nestedErrs)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
result[name] = resolveSafeErrorItem(err)
|
||||||
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()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
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
apis/base.go
10
apis/base.go
|
@ -11,6 +11,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
"github.com/labstack/echo/v5/middleware"
|
"github.com/labstack/echo/v5/middleware"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"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 {
|
e.ResetRouterCreator(func(ec *echo.Echo) echo.Router {
|
||||||
return echo.NewRouter(echo.RouterConfig{
|
return echo.NewRouter(echo.RouterConfig{
|
||||||
UnescapePathParamValues: true,
|
UnescapePathParamValues: true,
|
||||||
|
AllowOverwritingRoute: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -58,6 +60,14 @@ func InitApi(app core.App) (*echo.Echo, error) {
|
||||||
return
|
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
|
var apiErr *ApiError
|
||||||
|
|
||||||
switch v := err.(type) {
|
switch v := err.(type) {
|
||||||
|
|
|
@ -57,6 +57,8 @@ func RequestData(c echo.Context) *models.RequestData {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecordAuthResponse generates and writes a properly formatted record
|
||||||
|
// auth response into the specified request context.
|
||||||
func RecordAuthResponse(
|
func RecordAuthResponse(
|
||||||
app core.App,
|
app core.App,
|
||||||
c echo.Context,
|
c echo.Context,
|
||||||
|
|
|
@ -21,22 +21,25 @@ import (
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServeOptions defines an optional struct for apis.Serve().
|
// ServeConfig defines a configuration struct for apis.Serve().
|
||||||
type ServeOptions struct {
|
type ServeConfig struct {
|
||||||
|
// ShowStartBanner indicates whether to show or hide the server start console message.
|
||||||
ShowStartBanner bool
|
ShowStartBanner bool
|
||||||
|
|
||||||
|
// HttpAddr is the HTTP server address to bind (eg. `127.0.0.1:80`).
|
||||||
HttpAddr string
|
HttpAddr string
|
||||||
|
|
||||||
|
// HttpsAddr is the HTTPS server address to bind (eg. `127.0.0.1:443`).
|
||||||
HttpsAddr string
|
HttpsAddr string
|
||||||
AllowedOrigins []string // optional list of CORS origins (default to "*")
|
|
||||||
|
// AllowedOrigins is an optional list of CORS origins (default to "*").
|
||||||
|
AllowedOrigins []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve starts a new app web server.
|
// Serve starts a new app web server.
|
||||||
func Serve(app core.App, options *ServeOptions) error {
|
func Serve(app core.App, config ServeConfig) error {
|
||||||
if options == nil {
|
if len(config.AllowedOrigins) == 0 {
|
||||||
options = &ServeOptions{}
|
config.AllowedOrigins = []string{"*"}
|
||||||
}
|
|
||||||
|
|
||||||
if len(options.AllowedOrigins) == 0 {
|
|
||||||
options.AllowedOrigins = []string{"*"}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that the latest migrations are applied before starting the server
|
// 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
|
// configure cors
|
||||||
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||||
Skipper: middleware.DefaultSkipper,
|
Skipper: middleware.DefaultSkipper,
|
||||||
AllowOrigins: options.AllowedOrigins,
|
AllowOrigins: config.AllowedOrigins,
|
||||||
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
|
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// start http server
|
// start http server
|
||||||
// ---
|
// ---
|
||||||
mainAddr := options.HttpAddr
|
mainAddr := config.HttpAddr
|
||||||
if options.HttpsAddr != "" {
|
if config.HttpsAddr != "" {
|
||||||
mainAddr = options.HttpsAddr
|
mainAddr = config.HttpsAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
mainHost, _, _ := net.SplitHostPort(mainAddr)
|
mainHost, _, _ := net.SplitHostPort(mainAddr)
|
||||||
|
@ -80,7 +83,7 @@ func Serve(app core.App, options *ServeOptions) error {
|
||||||
HostPolicy: autocert.HostWhitelist(mainHost, "www."+mainHost),
|
HostPolicy: autocert.HostWhitelist(mainHost, "www."+mainHost),
|
||||||
}
|
}
|
||||||
|
|
||||||
serverConfig := &http.Server{
|
server := &http.Server{
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
GetCertificate: certManager.GetCertificate,
|
GetCertificate: certManager.GetCertificate,
|
||||||
NextProtos: []string{acme.ALPNProto},
|
NextProtos: []string{acme.ALPNProto},
|
||||||
|
@ -95,16 +98,16 @@ func Serve(app core.App, options *ServeOptions) error {
|
||||||
serveEvent := &core.ServeEvent{
|
serveEvent := &core.ServeEvent{
|
||||||
App: app,
|
App: app,
|
||||||
Router: router,
|
Router: router,
|
||||||
Server: serverConfig,
|
Server: server,
|
||||||
CertManager: certManager,
|
CertManager: certManager,
|
||||||
}
|
}
|
||||||
if err := app.OnBeforeServe().Trigger(serveEvent); err != nil {
|
if err := app.OnBeforeServe().Trigger(serveEvent); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.ShowStartBanner {
|
if config.ShowStartBanner {
|
||||||
schema := "http"
|
schema := "http"
|
||||||
if options.HttpsAddr != "" {
|
if config.HttpsAddr != "" {
|
||||||
schema = "https"
|
schema = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,34 +118,34 @@ func Serve(app core.App, options *ServeOptions) error {
|
||||||
bold.Printf(
|
bold.Printf(
|
||||||
"%s Server started at %s\n",
|
"%s Server started at %s\n",
|
||||||
strings.TrimSpace(date.String()),
|
strings.TrimSpace(date.String()),
|
||||||
color.CyanString("%s://%s", schema, serverConfig.Addr),
|
color.CyanString("%s://%s", schema, server.Addr),
|
||||||
)
|
)
|
||||||
|
|
||||||
regular := color.New()
|
regular := color.New()
|
||||||
regular.Printf(" ➜ REST API: %s\n", color.CyanString("%s://%s/api/", 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, serverConfig.Addr))
|
regular.Printf("└─ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, server.Addr))
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to gracefully shutdown the server on app termination
|
// try to gracefully shutdown the server on app termination
|
||||||
app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
serverConfig.Shutdown(ctx)
|
server.Shutdown(ctx)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// start HTTPS server
|
// 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 httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version
|
||||||
if options.HttpAddr != "" {
|
if config.HttpAddr != "" {
|
||||||
go http.ListenAndServe(options.HttpAddr, certManager.HTTPHandler(nil))
|
go http.ListenAndServe(config.HttpAddr, certManager.HTTPHandler(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverConfig.ListenAndServeTLS("", "")
|
return server.ListenAndServeTLS("", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// OR start HTTP server
|
// OR start HTTP server
|
||||||
return serverConfig.ListenAndServe()
|
return server.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
type migrationsConnection struct {
|
type migrationsConnection struct {
|
||||||
|
|
|
@ -20,7 +20,7 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command {
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "Starts the web server (default to 127.0.0.1:8090)",
|
Short: "Starts the web server (default to 127.0.0.1:8090)",
|
||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
err := apis.Serve(app, &apis.ServeOptions{
|
err := apis.Serve(app, apis.ServeConfig{
|
||||||
HttpAddr: httpAddr,
|
HttpAddr: httpAddr,
|
||||||
HttpsAddr: httpsAddr,
|
HttpsAddr: httpsAddr,
|
||||||
ShowStartBanner: showStartBanner,
|
ShowStartBanner: showStartBanner,
|
||||||
|
|
|
@ -180,7 +180,7 @@ type BaseAppConfig struct {
|
||||||
// configured with the provided arguments.
|
// configured with the provided arguments.
|
||||||
//
|
//
|
||||||
// To initialize the app, you need to call `app.Bootstrap()`.
|
// To initialize the app, you need to call `app.Bootstrap()`.
|
||||||
func NewBaseApp(config *BaseAppConfig) *BaseApp {
|
func NewBaseApp(config BaseAppConfig) *BaseApp {
|
||||||
app := &BaseApp{
|
app := &BaseApp{
|
||||||
dataDir: config.DataDir,
|
dataDir: config.DataDir,
|
||||||
isDebug: config.IsDebug,
|
isDebug: config.IsDebug,
|
||||||
|
|
|
@ -22,6 +22,14 @@ func main() {
|
||||||
// Optional plugin flags:
|
// Optional plugin flags:
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
var hooksDir string
|
||||||
|
app.RootCmd.PersistentFlags().StringVar(
|
||||||
|
&hooksDir,
|
||||||
|
"hooksDir",
|
||||||
|
"",
|
||||||
|
"the directory with the JS app hooks",
|
||||||
|
)
|
||||||
|
|
||||||
var migrationsDir string
|
var migrationsDir string
|
||||||
app.RootCmd.PersistentFlags().StringVar(
|
app.RootCmd.PersistentFlags().StringVar(
|
||||||
&migrationsDir,
|
&migrationsDir,
|
||||||
|
@ -68,20 +76,25 @@ func main() {
|
||||||
// Plugins and hooks:
|
// Plugins and hooks:
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// load js pb_hooks
|
||||||
|
jsvm.MustRegisterHooks(app, jsvm.HooksConfig{
|
||||||
|
Dir: hooksDir,
|
||||||
|
})
|
||||||
|
|
||||||
// load js pb_migrations
|
// load js pb_migrations
|
||||||
jsvm.MustRegisterMigrations(app, &jsvm.MigrationsOptions{
|
jsvm.MustRegisterMigrations(app, jsvm.MigrationsConfig{
|
||||||
Dir: migrationsDir,
|
Dir: migrationsDir,
|
||||||
})
|
})
|
||||||
|
|
||||||
// migrate command (with js templates)
|
// migrate command (with js templates)
|
||||||
migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{
|
migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
|
||||||
TemplateLang: migratecmd.TemplateLangJS,
|
TemplateLang: migratecmd.TemplateLangJS,
|
||||||
Automigrate: automigrate,
|
Automigrate: automigrate,
|
||||||
Dir: migrationsDir,
|
Dir: migrationsDir,
|
||||||
})
|
})
|
||||||
|
|
||||||
// GitHub selfupdate
|
// GitHub selfupdate
|
||||||
ghupdate.MustRegister(app, app.RootCmd, nil)
|
ghupdate.MustRegister(app, app.RootCmd, ghupdate.Config{})
|
||||||
|
|
||||||
app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
|
app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
|
||||||
app.Dao().ModelQueryTimeout = time.Duration(queryTimeout) * time.Second
|
app.Dao().ModelQueryTimeout = time.Duration(queryTimeout) * time.Second
|
||||||
|
@ -105,5 +118,6 @@ func defaultPublicDir() string {
|
||||||
// most likely ran with go run
|
// most likely ran with go run
|
||||||
return "./pb_public"
|
return "./pb_public"
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(os.Args[0], "../pb_public")
|
return filepath.Join(os.Args[0], "../pb_public")
|
||||||
}
|
}
|
||||||
|
|
17
go.mod
17
go.mod
|
@ -4,12 +4,13 @@ go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||||
github.com/aws/aws-sdk-go v1.44.274
|
github.com/aws/aws-sdk-go v1.44.278
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.0
|
github.com/domodwyer/mailyak/v3 v3.6.0
|
||||||
github.com/dop251/goja v0.0.0-20230427124612-428fc442ff5f
|
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20230322100729-2550c7b6c124
|
github.com/dop251/goja_nodejs v0.0.0-20230602164024-804a84515562
|
||||||
github.com/fatih/color v1.15.0
|
github.com/fatih/color v1.15.0
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2
|
github.com/gabriel-vasile/mimetype v1.4.2
|
||||||
github.com/ganigeorgiev/fexpr v0.3.0
|
github.com/ganigeorgiev/fexpr v0.3.0
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||||
|
@ -23,7 +24,7 @@ require (
|
||||||
golang.org/x/crypto v0.9.0
|
golang.org/x/crypto v0.9.0
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.10.0
|
||||||
golang.org/x/oauth2 v0.8.0
|
golang.org/x/oauth2 v0.8.0
|
||||||
modernc.org/sqlite v1.22.1
|
modernc.org/sqlite v1.23.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -54,7 +55,7 @@ require (
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20230510103437-eeec1cb781c3 // indirect
|
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/google/wire v0.5.0 // indirect
|
github.com/google/wire v0.5.0 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.10.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.10.0 // indirect
|
||||||
|
@ -83,11 +84,11 @@ require (
|
||||||
google.golang.org/grpc v1.55.0 // indirect
|
google.golang.org/grpc v1.55.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
lukechampine.com/uint128 v1.3.0 // indirect
|
lukechampine.com/uint128 v1.3.0 // indirect
|
||||||
modernc.org/cc/v3 v3.40.0 // indirect
|
modernc.org/cc/v3 v3.41.0 // indirect
|
||||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||||
modernc.org/libc v1.22.6 // indirect
|
modernc.org/libc v1.23.0 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/memory v1.6.0 // indirect
|
||||||
modernc.org/opt v0.1.3 // indirect
|
modernc.org/opt v0.1.3 // indirect
|
||||||
modernc.org/strutil v1.1.3 // indirect
|
modernc.org/strutil v1.1.3 // indirect
|
||||||
modernc.org/token v1.1.0 // indirect
|
modernc.org/token v1.1.0 // indirect
|
||||||
|
|
35
go.sum
35
go.sum
|
@ -545,8 +545,8 @@ github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4
|
||||||
github.com/aws/aws-sdk-go v1.44.156/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
github.com/aws/aws-sdk-go v1.44.156/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||||
github.com/aws/aws-sdk-go v1.44.187/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
github.com/aws/aws-sdk-go v1.44.187/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||||
github.com/aws/aws-sdk-go v1.44.200/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
github.com/aws/aws-sdk-go v1.44.200/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||||
github.com/aws/aws-sdk-go v1.44.274 h1:vfreSv19e/9Ka9YytOzgzJasrRZfX7dnttLlbh8NKeA=
|
github.com/aws/aws-sdk-go v1.44.278 h1:jJFDO/unYFI48WQk7UGSyO3rBA/gnmRpNYNuAw/fPgE=
|
||||||
github.com/aws/aws-sdk-go v1.44.274/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
github.com/aws/aws-sdk-go v1.44.278/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
|
github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
|
||||||
|
@ -851,13 +851,13 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.0 h1:MdKYNjL709iDKieDF9wGrH3PVhg9I4Tz3vmp7AcgInY=
|
github.com/domodwyer/mailyak/v3 v3.6.0 h1:MdKYNjL709iDKieDF9wGrH3PVhg9I4Tz3vmp7AcgInY=
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.0/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
github.com/domodwyer/mailyak/v3 v3.6.0/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
||||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||||
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs=
|
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||||
github.com/dop251/goja v0.0.0-20230427124612-428fc442ff5f h1:3Z9NjtffvA8Qoh8xjgUpPmyKawJw/mDRcJlR9oPCvqI=
|
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 h1:+3HCtB74++ClLy8GgjUQYeC8R4ILzVcIe8+5edAJJnE=
|
||||||
github.com/dop251/goja v0.0.0-20230427124612-428fc442ff5f/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20230322100729-2550c7b6c124 h1:QDuDMgEkC/lnmvk0d/fZfcUUml18uUbS9TY5QtbdFhs=
|
github.com/dop251/goja_nodejs v0.0.0-20230602164024-804a84515562 h1:0gomDSJiLLlpfKxQAHt5zj+9toIcyLMPgkI/Mgv7FAU=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20230322100729-2550c7b6c124/go.mod h1:0tlktQL7yHfYEtjcRGi/eiOkbDR5XF7gyFFvbC5//E0=
|
github.com/dop251/goja_nodejs v0.0.0-20230602164024-804a84515562/go.mod h1:X2TOTJ+Uamd454RFp7ig2tmP3hQg0Z2Qk8gbVQmU0mk=
|
||||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
@ -912,6 +912,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
|
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
|
@ -1161,8 +1162,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe
|
||||||
github.com/google/pprof v0.0.0-20220318212150-b2ab0324ddda/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
github.com/google/pprof v0.0.0-20220318212150-b2ab0324ddda/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||||
github.com/google/pprof v0.0.0-20230111200839-76d1ae5aea2b/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20230111200839-76d1ae5aea2b/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||||
github.com/google/pprof v0.0.0-20230510103437-eeec1cb781c3 h1:2XF1Vzq06X+inNqgJ9tRnGuw+ZVCB3FazXODD6JE1R8=
|
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
|
||||||
github.com/google/pprof v0.0.0-20230510103437-eeec1cb781c3/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
|
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
|
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
|
||||||
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||||
|
@ -2902,22 +2903,22 @@ k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt
|
||||||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||||
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
|
||||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
||||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||||
modernc.org/libc v1.22.6 h1:cbXU8R+A6aOjRuhsFh3nbDWXO/Hs4ClJRXYB11KmPDo=
|
modernc.org/libc v1.23.0 h1:8+5HcgFJUiuLxg1iDxT2YW1LU5/+5r8bcfF0eAQylr4=
|
||||||
modernc.org/libc v1.22.6/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
modernc.org/libc v1.23.0/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
|
||||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
|
||||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE=
|
modernc.org/sqlite v1.23.0 h1:MWTFBI5H1WLnXpNBh/BTruBVqzzoh28DA0iOnlkkRaM=
|
||||||
modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
modernc.org/sqlite v1.23.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
// Package ghupdate implements a new command to selfupdate the current
|
// Package ghupdate implements a new command to selfupdate the current
|
||||||
// PocketBase executable with the latest GitHub release.
|
// PocketBase executable with the latest GitHub release.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// ghupdate.MustRegister(app, app.RootCmd, ghupdate.Config{})
|
||||||
package ghupdate
|
package ghupdate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -27,10 +31,10 @@ type HttpClient interface {
|
||||||
Do(req *http.Request) (*http.Response, error)
|
Do(req *http.Request) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options defines optional struct to customize the default plugin behavior.
|
// Config defines the config options of the ghupdate plugin.
|
||||||
//
|
//
|
||||||
// NB! This plugin is considered experimental and its options may change in the future.
|
// NB! This plugin is considered experimental and its config options may change in the future.
|
||||||
type Options struct {
|
type Config struct {
|
||||||
// Owner specifies the account owner of the repository (default to "pocketbase").
|
// Owner specifies the account owner of the repository (default to "pocketbase").
|
||||||
Owner string
|
Owner string
|
||||||
|
|
||||||
|
@ -51,43 +55,38 @@ type Options struct {
|
||||||
|
|
||||||
// MustRegister registers the ghupdate plugin to the provided app instance
|
// MustRegister registers the ghupdate plugin to the provided app instance
|
||||||
// and panic if it fails.
|
// and panic if it fails.
|
||||||
func MustRegister(app core.App, rootCmd *cobra.Command, options *Options) {
|
func MustRegister(app core.App, rootCmd *cobra.Command, config Config) {
|
||||||
if err := Register(app, rootCmd, options); err != nil {
|
if err := Register(app, rootCmd, config); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register registers the ghupdate plugin to the provided app instance.
|
// Register registers the ghupdate plugin to the provided app instance.
|
||||||
func Register(app core.App, rootCmd *cobra.Command, options *Options) error {
|
func Register(app core.App, rootCmd *cobra.Command, config Config) error {
|
||||||
p := &plugin{
|
p := &plugin{
|
||||||
app: app,
|
app: app,
|
||||||
currentVersion: rootCmd.Version,
|
currentVersion: rootCmd.Version,
|
||||||
|
config: config,
|
||||||
}
|
}
|
||||||
|
|
||||||
if options != nil {
|
if p.config.Owner == "" {
|
||||||
p.options = options
|
p.config.Owner = "pocketbase"
|
||||||
} else {
|
|
||||||
p.options = &Options{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.options.Owner == "" {
|
if p.config.Repo == "" {
|
||||||
p.options.Owner = "pocketbase"
|
p.config.Repo = "pocketbase"
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.options.Repo == "" {
|
if p.config.ArchiveExecutable == "" {
|
||||||
p.options.Repo = "pocketbase"
|
p.config.ArchiveExecutable = "pocketbase"
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.options.ArchiveExecutable == "" {
|
if p.config.HttpClient == nil {
|
||||||
p.options.ArchiveExecutable = "pocketbase"
|
p.config.HttpClient = http.DefaultClient
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.options.HttpClient == nil {
|
if p.config.Context == nil {
|
||||||
p.options.HttpClient = http.DefaultClient
|
p.config.Context = context.Background()
|
||||||
}
|
|
||||||
|
|
||||||
if p.options.Context == nil {
|
|
||||||
p.options.Context = context.Background()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rootCmd.AddCommand(p.updateCmd())
|
rootCmd.AddCommand(p.updateCmd())
|
||||||
|
@ -98,7 +97,7 @@ func Register(app core.App, rootCmd *cobra.Command, options *Options) error {
|
||||||
type plugin struct {
|
type plugin struct {
|
||||||
app core.App
|
app core.App
|
||||||
currentVersion string
|
currentVersion string
|
||||||
options *Options
|
config Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *plugin) updateCmd() *cobra.Command {
|
func (p *plugin) updateCmd() *cobra.Command {
|
||||||
|
@ -130,10 +129,10 @@ func (p *plugin) update(withBackup bool) error {
|
||||||
color.Yellow("Fetching release information...")
|
color.Yellow("Fetching release information...")
|
||||||
|
|
||||||
latest, err := fetchLatestRelease(
|
latest, err := fetchLatestRelease(
|
||||||
p.options.Context,
|
p.config.Context,
|
||||||
p.options.HttpClient,
|
p.config.HttpClient,
|
||||||
p.options.Owner,
|
p.config.Owner,
|
||||||
p.options.Repo,
|
p.config.Repo,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -161,7 +160,7 @@ func (p *plugin) update(withBackup bool) error {
|
||||||
|
|
||||||
// download the release asset
|
// download the release asset
|
||||||
assetZip := filepath.Join(releaseDir, asset.Name)
|
assetZip := filepath.Join(releaseDir, asset.Name)
|
||||||
if err := downloadFile(p.options.Context, p.options.HttpClient, asset.DownloadUrl, assetZip); err != nil {
|
if err := downloadFile(p.config.Context, p.config.HttpClient, asset.DownloadUrl, assetZip); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +182,7 @@ func (p *plugin) update(withBackup bool) error {
|
||||||
renamedOldExec := oldExec + ".old"
|
renamedOldExec := oldExec + ".old"
|
||||||
defer os.Remove(renamedOldExec)
|
defer os.Remove(renamedOldExec)
|
||||||
|
|
||||||
newExec := filepath.Join(extractDir, p.options.ArchiveExecutable)
|
newExec := filepath.Join(extractDir, p.config.ArchiveExecutable)
|
||||||
if _, err := os.Stat(newExec); err != nil {
|
if _, err := os.Stat(newExec); err != nil {
|
||||||
// try again with an .exe extension
|
// try again with an .exe extension
|
||||||
newExec = newExec + ".exe"
|
newExec = newExec + ".exe"
|
||||||
|
@ -213,7 +212,7 @@ func (p *plugin) update(withBackup bool) error {
|
||||||
color.Yellow("Creating pb_data backup...")
|
color.Yellow("Creating pb_data backup...")
|
||||||
|
|
||||||
backupName := fmt.Sprintf("@update_%s.zip", latest.Tag)
|
backupName := fmt.Sprintf("@update_%s.zip", latest.Tag)
|
||||||
if err := p.app.CreateBackup(p.options.Context, backupName); err != nil {
|
if err := p.app.CreateBackup(p.config.Context, backupName); err != nil {
|
||||||
tryToRevertExecChanges()
|
tryToRevertExecChanges()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
package jsvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/dop251/goja_nodejs/console"
|
||||||
|
"github.com/dop251/goja_nodejs/eventloop"
|
||||||
|
"github.com/dop251/goja_nodejs/process"
|
||||||
|
"github.com/dop251/goja_nodejs/require"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HooksConfig defines the config options of the JS app hooks plugin.
|
||||||
|
type HooksConfig struct {
|
||||||
|
// Dir specifies the directory with the JS app hooks.
|
||||||
|
//
|
||||||
|
// If not set it fallbacks to a relative "pb_data/../pb_hooks" directory.
|
||||||
|
Dir string
|
||||||
|
|
||||||
|
// Watch enables auto app restarts when a JS app hook file changes.
|
||||||
|
//
|
||||||
|
// Note that currently the application cannot be automatically restarted on Windows
|
||||||
|
// because the restart process relies on execve.
|
||||||
|
Watch bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustRegisterHooks registers the JS hooks plugin to
|
||||||
|
// the provided app instance and panics if it fails.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// jsvm.MustRegisterHooks(app, jsvm.HooksConfig{})
|
||||||
|
func MustRegisterHooks(app core.App, config HooksConfig) {
|
||||||
|
if err := RegisterHooks(app, config); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHooks registers the JS hooks plugin to the provided app instance.
|
||||||
|
func RegisterHooks(app core.App, config HooksConfig) error {
|
||||||
|
p := &hooks{app: app, config: config}
|
||||||
|
|
||||||
|
if p.config.Dir == "" {
|
||||||
|
p.config.Dir = filepath.Join(app.DataDir(), "../pb_hooks")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch all js hooks sorted by their filename
|
||||||
|
files, err := filesContent(p.config.Dir, `^.*\.pb\.js$`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbx.HashExp{}.Build(app.DB(), nil)
|
||||||
|
|
||||||
|
registry := new(require.Registry) // this can be shared by multiple runtimes
|
||||||
|
|
||||||
|
loop := eventloop.NewEventLoop()
|
||||||
|
|
||||||
|
loop.Run(func(vm *goja.Runtime) {
|
||||||
|
registry.Enable(vm)
|
||||||
|
console.Enable(vm)
|
||||||
|
process.Enable(vm)
|
||||||
|
baseBinds(vm)
|
||||||
|
dbxBinds(vm)
|
||||||
|
filesystemBinds(vm)
|
||||||
|
tokensBinds(vm)
|
||||||
|
securityBinds(vm)
|
||||||
|
formsBinds(vm)
|
||||||
|
apisBinds(vm)
|
||||||
|
|
||||||
|
vm.Set("$app", app)
|
||||||
|
|
||||||
|
for file, content := range files {
|
||||||
|
_, err := vm.RunString(string(content))
|
||||||
|
if err != nil {
|
||||||
|
if p.config.Watch {
|
||||||
|
color.Red("Failed to execute %s: %v", file, err)
|
||||||
|
} else {
|
||||||
|
// return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loop.Start()
|
||||||
|
|
||||||
|
app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
||||||
|
loop.StopNoWait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if p.config.Watch {
|
||||||
|
return p.watchFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type hooks struct {
|
||||||
|
app core.App
|
||||||
|
config HooksConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hooks) watchFiles() error {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.app.OnTerminate().Add(func(e *core.TerminateEvent) error {
|
||||||
|
watcher.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
var debounceTimer *time.Timer
|
||||||
|
|
||||||
|
// start listening for events.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if debounceTimer != nil {
|
||||||
|
debounceTimer.Stop()
|
||||||
|
}
|
||||||
|
debounceTimer = time.AfterFunc(100*time.Millisecond, func() {
|
||||||
|
// app restart is currently not supported on Windows
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
color.Yellow("File %s changed, please restart the app", event.Name)
|
||||||
|
} else {
|
||||||
|
color.Yellow("File %s changed, restarting...", event.Name)
|
||||||
|
if err := h.app.Restart(); err != nil {
|
||||||
|
color.Red("Failed to restart the app:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
color.Red("Watch error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// add the directory to watch
|
||||||
|
err = watcher.Add(h.config.Dir)
|
||||||
|
if err != nil {
|
||||||
|
watcher.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package jsvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ goja.FieldNameMapper = (*FieldMapper)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// FieldMapper provides custom mapping between Go and JavaScript property names.
|
||||||
|
//
|
||||||
|
// It is similar to the builtin "uncapFieldNameMapper" but also converts
|
||||||
|
// all uppercase identifiers to their lowercase equivalent (eg. "GET" -> "get").
|
||||||
|
type FieldMapper struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldName implements the [FieldNameMapper.FieldName] interface method.
|
||||||
|
func (u FieldMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
|
||||||
|
return convertGoToJSName(f.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MethodName implements the [FieldNameMapper.MethodName] interface method.
|
||||||
|
func (u FieldMapper) MethodName(_ reflect.Type, m reflect.Method) string {
|
||||||
|
return convertGoToJSName(m.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertGoToJSName(name string) string {
|
||||||
|
allUppercase := true
|
||||||
|
for _, c := range name {
|
||||||
|
if c != '_' && !unicode.IsUpper(c) {
|
||||||
|
allUppercase = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eg. "JSON" -> "json"
|
||||||
|
if allUppercase {
|
||||||
|
return strings.ToLower(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eg. "GetField" -> "getField"
|
||||||
|
return strings.ToLower(name[0:1]) + name[1:]
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package jsvm_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/plugins/jsvm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFieldMapper(t *testing.T) {
|
||||||
|
mapper := jsvm.FieldMapper{}
|
||||||
|
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"test", "test"},
|
||||||
|
{"Test", "test"},
|
||||||
|
{"miXeD", "miXeD"},
|
||||||
|
{"MiXeD", "miXeD"},
|
||||||
|
{"ResolveRequestAsJSON", "resolveRequestAsJSON"},
|
||||||
|
{"Variable_with_underscore", "variable_with_underscore"},
|
||||||
|
{"ALLCAPS", "allcaps"},
|
||||||
|
{"NOTALLCAPs", "nOTALLCAPs"},
|
||||||
|
{"ALL_CAPS_WITH_UNDERSCORE", "all_caps_with_underscore"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, s := range scenarios {
|
||||||
|
field := reflect.StructField{Name: s.name}
|
||||||
|
if v := mapper.FieldName(nil, field); v != s.expected {
|
||||||
|
t.Fatalf("[%d] Expected FieldName %q, got %q", i, s.expected, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
method := reflect.Method{Name: s.name}
|
||||||
|
if v := mapper.MethodName(nil, method); v != s.expected {
|
||||||
|
t.Fatalf("[%d] Expected MethodName %q, got %q", i, s.expected, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,10 +2,9 @@ package jsvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
"github.com/dop251/goja_nodejs/console"
|
"github.com/dop251/goja_nodejs/console"
|
||||||
"github.com/dop251/goja_nodejs/process"
|
"github.com/dop251/goja_nodejs/process"
|
||||||
"github.com/dop251/goja_nodejs/require"
|
"github.com/dop251/goja_nodejs/require"
|
||||||
|
@ -14,52 +13,36 @@ import (
|
||||||
m "github.com/pocketbase/pocketbase/migrations"
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MigrationsOptions defines optional struct to customize the default migrations loader behavior.
|
// MigrationsConfig defines the config options of the JS migrations loader plugin.
|
||||||
type MigrationsOptions struct {
|
type MigrationsConfig struct {
|
||||||
// Dir specifies the directory with the JS migrations.
|
// Dir specifies the directory with the JS migrations.
|
||||||
//
|
//
|
||||||
// If not set it fallbacks to a relative "pb_data/../pb_migrations" directory.
|
// If not set it fallbacks to a relative "pb_data/../pb_migrations" directory.
|
||||||
Dir string
|
Dir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrations is the migrations loader plugin definition.
|
// MustRegisterMigrations registers the JS migrations loader plugin to
|
||||||
// Usually it is instantiated via RegisterMigrations or MustRegisterMigrations.
|
|
||||||
type migrations struct {
|
|
||||||
app core.App
|
|
||||||
options *MigrationsOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustRegisterMigrations registers the migrations loader plugin to
|
|
||||||
// the provided app instance and panics if it fails.
|
// the provided app instance and panics if it fails.
|
||||||
//
|
//
|
||||||
// Internally it calls RegisterMigrations(app, options).
|
// Example usage:
|
||||||
//
|
//
|
||||||
// If options is nil, by default the js files from pb_data/migrations are loaded.
|
// jsvm.MustRegisterMigrations(app, jsvm.MigrationsConfig{})
|
||||||
// Set custom options.Dir if you want to change it to some other directory.
|
func MustRegisterMigrations(app core.App, config MigrationsConfig) {
|
||||||
func MustRegisterMigrations(app core.App, options *MigrationsOptions) {
|
if err := RegisterMigrations(app, config); err != nil {
|
||||||
if err := RegisterMigrations(app, options); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterMigrations registers the plugin to the provided app instance.
|
// RegisterMigrations registers the JS migrations loader hooks plugin
|
||||||
//
|
// to the provided app instance.
|
||||||
// If options is nil, by default the js files from pb_data/migrations are loaded.
|
func RegisterMigrations(app core.App, config MigrationsConfig) error {
|
||||||
// Set custom options.Dir if you want to change it to some other directory.
|
l := &migrations{app: app, config: config}
|
||||||
func RegisterMigrations(app core.App, options *MigrationsOptions) error {
|
|
||||||
l := &migrations{app: app}
|
|
||||||
|
|
||||||
if options != nil {
|
if l.config.Dir == "" {
|
||||||
l.options = options
|
l.config.Dir = filepath.Join(app.DataDir(), "../pb_migrations")
|
||||||
} else {
|
|
||||||
l.options = &MigrationsOptions{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.options.Dir == "" {
|
files, err := filesContent(l.config.Dir, `^.*\.js$`)
|
||||||
l.options.Dir = filepath.Join(app.DataDir(), "../pb_migrations")
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := readDirFiles(l.options.Dir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -67,10 +50,13 @@ func RegisterMigrations(app core.App, options *MigrationsOptions) error {
|
||||||
registry := new(require.Registry) // this can be shared by multiple runtimes
|
registry := new(require.Registry) // this can be shared by multiple runtimes
|
||||||
|
|
||||||
for file, content := range files {
|
for file, content := range files {
|
||||||
vm := NewBaseVM()
|
vm := goja.New()
|
||||||
registry.Enable(vm)
|
registry.Enable(vm)
|
||||||
console.Enable(vm)
|
console.Enable(vm)
|
||||||
process.Enable(vm)
|
process.Enable(vm)
|
||||||
|
dbxBinds(vm)
|
||||||
|
tokensBinds(vm)
|
||||||
|
securityBinds(vm)
|
||||||
|
|
||||||
vm.Set("migrate", func(up, down func(db dbx.Builder) error) {
|
vm.Set("migrate", func(up, down func(db dbx.Builder) error) {
|
||||||
m.AppMigrations.Register(up, down, file)
|
m.AppMigrations.Register(up, down, file)
|
||||||
|
@ -85,30 +71,7 @@ func RegisterMigrations(app core.App, options *MigrationsOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readDirFiles returns a map with all directory files and their content.
|
type migrations struct {
|
||||||
//
|
app core.App
|
||||||
// If directory with dirPath is missing, it returns an empty map and no error.
|
config MigrationsConfig
|
||||||
func readDirFiles(dirPath string) (map[string][]byte, error) {
|
|
||||||
files, err := os.ReadDir(dirPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return map[string][]byte{}, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := map[string][]byte{}
|
|
||||||
|
|
||||||
for _, f := range files {
|
|
||||||
if f.IsDir() || !strings.HasSuffix(f.Name(), ".js") {
|
|
||||||
continue // not a .js file
|
|
||||||
}
|
|
||||||
raw, err := os.ReadFile(filepath.Join(dirPath, f.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result[f.Name()] = raw
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,36 +5,42 @@
|
||||||
//
|
//
|
||||||
// 1. JS Migrations loader:
|
// 1. JS Migrations loader:
|
||||||
//
|
//
|
||||||
// jsvm.MustRegisterMigrations(app, &jsvm.MigrationsOptions{
|
// jsvm.MustRegisterMigrations(app, jsvm.MigrationsConfig{
|
||||||
// Dir: "custom_js_migrations_dir_path", // default to "pb_data/../pb_migrations"
|
// Dir: "/custom/js/migrations/dir", // default to "pb_data/../pb_migrations"
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// 2. JS app hooks:
|
||||||
|
//
|
||||||
|
// jsvm.MustRegisterHooks(app, jsvm.HooksConfig{
|
||||||
|
// Dir: "/custom/js/hooks/dir", // default to "pb_data/../pb_hooks"
|
||||||
// })
|
// })
|
||||||
package jsvm
|
package jsvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"regexp"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/apis"
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
"github.com/pocketbase/pocketbase/daos"
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
"github.com/pocketbase/pocketbase/forms"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"github.com/pocketbase/pocketbase/models/schema"
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
|
"github.com/pocketbase/pocketbase/tokens"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewBaseVM() *goja.Runtime {
|
func baseBinds(vm *goja.Runtime) {
|
||||||
vm := goja.New()
|
|
||||||
vm.SetFieldNameMapper(FieldMapper{})
|
vm.SetFieldNameMapper(FieldMapper{})
|
||||||
|
|
||||||
baseBinds(vm)
|
|
||||||
dbxBinds(vm)
|
|
||||||
|
|
||||||
return vm
|
|
||||||
}
|
|
||||||
|
|
||||||
func baseBinds(vm *goja.Runtime) {
|
|
||||||
vm.Set("unmarshal", func(src map[string]any, dest any) (any, error) {
|
vm.Set("unmarshal", func(src map[string]any, dest any) (any, error) {
|
||||||
raw, err := json.Marshal(src)
|
raw, err := json.Marshal(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -72,31 +78,52 @@ func baseBinds(vm *goja.Runtime) {
|
||||||
|
|
||||||
vm.Set("Collection", func(call goja.ConstructorCall) *goja.Object {
|
vm.Set("Collection", func(call goja.ConstructorCall) *goja.Object {
|
||||||
instance := &models.Collection{}
|
instance := &models.Collection{}
|
||||||
return defaultConstructor(vm, call, instance)
|
return structConstructor(vm, call, instance)
|
||||||
})
|
})
|
||||||
|
|
||||||
vm.Set("Admin", func(call goja.ConstructorCall) *goja.Object {
|
vm.Set("Admin", func(call goja.ConstructorCall) *goja.Object {
|
||||||
instance := &models.Admin{}
|
instance := &models.Admin{}
|
||||||
return defaultConstructor(vm, call, instance)
|
return structConstructor(vm, call, instance)
|
||||||
})
|
})
|
||||||
|
|
||||||
vm.Set("Schema", func(call goja.ConstructorCall) *goja.Object {
|
vm.Set("Schema", func(call goja.ConstructorCall) *goja.Object {
|
||||||
instance := &schema.Schema{}
|
instance := &schema.Schema{}
|
||||||
return defaultConstructor(vm, call, instance)
|
return structConstructor(vm, call, instance)
|
||||||
})
|
})
|
||||||
|
|
||||||
vm.Set("SchemaField", func(call goja.ConstructorCall) *goja.Object {
|
vm.Set("SchemaField", func(call goja.ConstructorCall) *goja.Object {
|
||||||
instance := &schema.SchemaField{}
|
instance := &schema.SchemaField{}
|
||||||
return defaultConstructor(vm, call, instance)
|
return structConstructor(vm, call, instance)
|
||||||
|
})
|
||||||
|
|
||||||
|
vm.Set("Mail", func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
instance := &mailer.Message{}
|
||||||
|
return structConstructor(vm, call, instance)
|
||||||
|
})
|
||||||
|
|
||||||
|
vm.Set("ValidationError", func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
code, _ := call.Argument(0).Export().(string)
|
||||||
|
message, _ := call.Argument(1).Export().(string)
|
||||||
|
|
||||||
|
instance := validation.NewError(code, message)
|
||||||
|
instanceValue := vm.ToValue(instance).(*goja.Object)
|
||||||
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
|
|
||||||
|
return instanceValue
|
||||||
})
|
})
|
||||||
|
|
||||||
vm.Set("Dao", func(call goja.ConstructorCall) *goja.Object {
|
vm.Set("Dao", func(call goja.ConstructorCall) *goja.Object {
|
||||||
db, ok := call.Argument(0).Export().(dbx.Builder)
|
concurrentDB, _ := call.Argument(0).Export().(dbx.Builder)
|
||||||
if !ok || db == nil {
|
if concurrentDB == nil {
|
||||||
panic("missing required Dao(db) argument")
|
panic("missing required Dao(concurrentDB, [nonconcurrentDB]) argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
instance := daos.New(db)
|
nonConcurrentDB, _ := call.Argument(1).Export().(dbx.Builder)
|
||||||
|
if nonConcurrentDB == nil {
|
||||||
|
nonConcurrentDB = concurrentDB
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := daos.NewMultiDB(concurrentDB, nonConcurrentDB)
|
||||||
instanceValue := vm.ToValue(instance).(*goja.Object)
|
instanceValue := vm.ToValue(instance).(*goja.Object)
|
||||||
instanceValue.SetPrototype(call.This.Prototype())
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
|
|
||||||
|
@ -104,30 +131,13 @@ func baseBinds(vm *goja.Runtime) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultConstructor(vm *goja.Runtime, call goja.ConstructorCall, instance any) *goja.Object {
|
|
||||||
if data := call.Argument(0).Export(); data != nil {
|
|
||||||
if raw, err := json.Marshal(data); err == nil {
|
|
||||||
json.Unmarshal(raw, instance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceValue := vm.ToValue(instance).(*goja.Object)
|
|
||||||
instanceValue.SetPrototype(call.This.Prototype())
|
|
||||||
|
|
||||||
return instanceValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbxBinds(vm *goja.Runtime) {
|
func dbxBinds(vm *goja.Runtime) {
|
||||||
obj := vm.NewObject()
|
obj := vm.NewObject()
|
||||||
vm.Set("$dbx", obj)
|
vm.Set("$dbx", obj)
|
||||||
|
|
||||||
obj.Set("exp", dbx.NewExp)
|
obj.Set("exp", dbx.NewExp)
|
||||||
obj.Set("hashExp", func(data map[string]any) dbx.HashExp {
|
obj.Set("hashExp", func(data map[string]any) dbx.HashExp {
|
||||||
exp := dbx.HashExp{}
|
return dbx.HashExp(data)
|
||||||
for k, v := range data {
|
|
||||||
exp[k] = v
|
|
||||||
}
|
|
||||||
return exp
|
|
||||||
})
|
})
|
||||||
obj.Set("not", dbx.Not)
|
obj.Set("not", dbx.Not)
|
||||||
obj.Set("and", dbx.And)
|
obj.Set("and", dbx.And)
|
||||||
|
@ -144,8 +154,79 @@ func dbxBinds(vm *goja.Runtime) {
|
||||||
obj.Set("notBetween", dbx.NotBetween)
|
obj.Set("notBetween", dbx.NotBetween)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apisBind(vm *goja.Runtime) {
|
func tokensBinds(vm *goja.Runtime) {
|
||||||
obj := vm.NewObject()
|
obj := vm.NewObject()
|
||||||
|
vm.Set("$tokens", obj)
|
||||||
|
|
||||||
|
// admin
|
||||||
|
obj.Set("adminAuthToken", tokens.NewAdminAuthToken)
|
||||||
|
obj.Set("adminResetPasswordToken", tokens.NewAdminResetPasswordToken)
|
||||||
|
obj.Set("adminFileToken", tokens.NewAdminFileToken)
|
||||||
|
|
||||||
|
// record
|
||||||
|
obj.Set("recordAuthToken", tokens.NewRecordAuthToken)
|
||||||
|
obj.Set("recordVerifyToken", tokens.NewRecordVerifyToken)
|
||||||
|
obj.Set("recordResetPasswordToken", tokens.NewRecordResetPasswordToken)
|
||||||
|
obj.Set("recordChangeEmailToken", tokens.NewRecordChangeEmailToken)
|
||||||
|
obj.Set("recordFileToken", tokens.NewRecordFileToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func securityBinds(vm *goja.Runtime) {
|
||||||
|
obj := vm.NewObject()
|
||||||
|
vm.Set("$security", obj)
|
||||||
|
|
||||||
|
// random
|
||||||
|
obj.Set("randomString", security.RandomString)
|
||||||
|
obj.Set("randomStringWithAlphabet", security.RandomStringWithAlphabet)
|
||||||
|
obj.Set("pseudorandomString", security.PseudorandomString)
|
||||||
|
obj.Set("pseudorandomStringWithAlphabet", security.PseudorandomStringWithAlphabet)
|
||||||
|
|
||||||
|
// jwt
|
||||||
|
obj.Set("parseUnverifiedToken", security.ParseUnverifiedJWT)
|
||||||
|
obj.Set("parseToken", security.ParseJWT)
|
||||||
|
obj.Set("createToken", security.NewToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func filesystemBinds(vm *goja.Runtime) {
|
||||||
|
obj := vm.NewObject()
|
||||||
|
vm.Set("$filesystem", obj)
|
||||||
|
|
||||||
|
obj.Set("fileFromPath", filesystem.NewFileFromPath)
|
||||||
|
obj.Set("fileFromBytes", filesystem.NewFileFromBytes)
|
||||||
|
obj.Set("fileFromMultipart", filesystem.NewFileFromMultipart)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formsBinds(vm *goja.Runtime) {
|
||||||
|
registerFactoryAsConstructor(vm, "AdminLoginForm", forms.NewAdminLogin)
|
||||||
|
registerFactoryAsConstructor(vm, "AdminPasswordResetConfirmForm", forms.NewAdminPasswordResetConfirm)
|
||||||
|
registerFactoryAsConstructor(vm, "AdminPasswordResetRequestForm", forms.NewAdminPasswordResetRequest)
|
||||||
|
registerFactoryAsConstructor(vm, "AdminUpsertForm", forms.NewAdminUpsert)
|
||||||
|
registerFactoryAsConstructor(vm, "AppleClientSecretCreateForm", forms.NewAppleClientSecretCreate)
|
||||||
|
registerFactoryAsConstructor(vm, "CollectionUpsertForm", forms.NewCollectionUpsert)
|
||||||
|
registerFactoryAsConstructor(vm, "CollectionsImportForm", forms.NewCollectionsImport)
|
||||||
|
registerFactoryAsConstructor(vm, "RealtimeSubscribeForm", forms.NewRealtimeSubscribe)
|
||||||
|
registerFactoryAsConstructor(vm, "RecordEmailChangeConfirmForm", forms.NewRecordEmailChangeConfirm)
|
||||||
|
registerFactoryAsConstructor(vm, "RecordEmailChangeRequestForm", forms.NewRecordEmailChangeRequest)
|
||||||
|
registerFactoryAsConstructor(vm, "RecordOAuth2LoginForm", forms.NewRecordOAuth2Login)
|
||||||
|
registerFactoryAsConstructor(vm, "RecordPasswordLoginForm", forms.NewRecordPasswordLogin)
|
||||||
|
registerFactoryAsConstructor(vm, "RecordPasswordResetConfirmForm", forms.NewRecordPasswordResetConfirm)
|
||||||
|
registerFactoryAsConstructor(vm, "RecordPasswordResetRequestForm", forms.NewRecordPasswordResetRequest)
|
||||||
|
registerFactoryAsConstructor(vm, "RecordUpsertForm", forms.NewRecordUpsert)
|
||||||
|
registerFactoryAsConstructor(vm, "RecordVerificationConfirmForm", forms.NewRecordVerificationConfirm)
|
||||||
|
registerFactoryAsConstructor(vm, "RecordVerificationRequestForm", forms.NewRecordVerificationRequest)
|
||||||
|
registerFactoryAsConstructor(vm, "SettingsUpsertForm", forms.NewSettingsUpsert)
|
||||||
|
registerFactoryAsConstructor(vm, "TestEmailSendForm", forms.NewTestEmailSend)
|
||||||
|
registerFactoryAsConstructor(vm, "TestS3FilesystemForm", forms.NewTestS3Filesystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apisBinds(vm *goja.Runtime) {
|
||||||
|
obj := vm.NewObject()
|
||||||
|
|
||||||
|
vm.Set("Route", func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
instance := echo.Route{}
|
||||||
|
return structConstructor(vm, call, &instance)
|
||||||
|
})
|
||||||
|
|
||||||
vm.Set("$apis", obj)
|
vm.Set("$apis", obj)
|
||||||
|
|
||||||
// middlewares
|
// middlewares
|
||||||
|
@ -158,49 +239,107 @@ func apisBind(vm *goja.Runtime) {
|
||||||
obj.Set("requireAdminOrOwnerAuth", apis.RequireAdminOrOwnerAuth)
|
obj.Set("requireAdminOrOwnerAuth", apis.RequireAdminOrOwnerAuth)
|
||||||
obj.Set("activityLogger", apis.ActivityLogger)
|
obj.Set("activityLogger", apis.ActivityLogger)
|
||||||
|
|
||||||
|
// record helpers
|
||||||
|
obj.Set("requestData", apis.RequestData)
|
||||||
|
obj.Set("recordAuthResponse", apis.RecordAuthResponse)
|
||||||
|
obj.Set("enrichRecord", apis.EnrichRecord)
|
||||||
|
obj.Set("enrichRecords", apis.EnrichRecords)
|
||||||
|
|
||||||
// api errors
|
// api errors
|
||||||
|
vm.Set("ApiError", func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
status, _ := call.Argument(0).Export().(int64)
|
||||||
|
message, _ := call.Argument(1).Export().(string)
|
||||||
|
data := call.Argument(2).Export()
|
||||||
|
|
||||||
|
instance := apis.NewApiError(int(status), message, data)
|
||||||
|
instanceValue := vm.ToValue(instance).(*goja.Object)
|
||||||
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
|
|
||||||
|
return instanceValue
|
||||||
|
})
|
||||||
obj.Set("notFoundError", apis.NewNotFoundError)
|
obj.Set("notFoundError", apis.NewNotFoundError)
|
||||||
obj.Set("badRequestError", apis.NewBadRequestError)
|
obj.Set("badRequestError", apis.NewBadRequestError)
|
||||||
obj.Set("forbiddenError", apis.NewForbiddenError)
|
obj.Set("forbiddenError", apis.NewForbiddenError)
|
||||||
obj.Set("unauthorizedError", apis.NewUnauthorizedError)
|
obj.Set("unauthorizedError", apis.NewUnauthorizedError)
|
||||||
|
|
||||||
// record helpers
|
|
||||||
obj.Set("requestData", apis.RequestData)
|
|
||||||
obj.Set("enrichRecord", apis.EnrichRecord)
|
|
||||||
obj.Set("enrichRecords", apis.EnrichRecords)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FieldMapper provides custom mapping between Go and JavaScript property names.
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// registerFactoryAsConstructor registers the factory function as native JS constructor.
|
||||||
|
func registerFactoryAsConstructor(vm *goja.Runtime, constructorName string, factoryFunc any) {
|
||||||
|
vm.Set(constructorName, func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
f := reflect.ValueOf(factoryFunc)
|
||||||
|
|
||||||
|
args := []reflect.Value{}
|
||||||
|
|
||||||
|
for _, v := range call.Arguments {
|
||||||
|
args = append(args, reflect.ValueOf(v.Export()))
|
||||||
|
}
|
||||||
|
|
||||||
|
result := f.Call(args)
|
||||||
|
|
||||||
|
if len(result) != 1 {
|
||||||
|
panic("the factory function should return only 1 item")
|
||||||
|
}
|
||||||
|
|
||||||
|
value := vm.ToValue(result[0].Interface()).(*goja.Object)
|
||||||
|
value.SetPrototype(call.This.Prototype())
|
||||||
|
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// structConstructor wraps the provided struct with a native JS constructor.
|
||||||
|
func structConstructor(vm *goja.Runtime, call goja.ConstructorCall, instance any) *goja.Object {
|
||||||
|
if data := call.Argument(0).Export(); data != nil {
|
||||||
|
if raw, err := json.Marshal(data); err == nil {
|
||||||
|
json.Unmarshal(raw, instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceValue := vm.ToValue(instance).(*goja.Object)
|
||||||
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
|
|
||||||
|
return instanceValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// filesContent returns a map with all direct files within the specified dir and their content.
|
||||||
//
|
//
|
||||||
// It is similar to the builtin "uncapFieldNameMapper" but also converts
|
// If directory with dirPath is missing or no files matching the pattern were found,
|
||||||
// all uppercase identifiers to their lowercase equivalent (eg. "GET" -> "get").
|
// it returns an empty map and no error.
|
||||||
type FieldMapper struct {
|
//
|
||||||
}
|
// If pattern is empty string it matches all root files.
|
||||||
|
func filesContent(dirPath string, pattern string) (map[string][]byte, error) {
|
||||||
|
files, err := os.ReadDir(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return map[string][]byte{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// FieldName implements the [FieldNameMapper.FieldName] interface method.
|
var exp *regexp.Regexp
|
||||||
func (u FieldMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
|
if pattern != "" {
|
||||||
return convertGoToJSName(f.Name)
|
var err error
|
||||||
}
|
if exp, err = regexp.Compile(pattern); err != nil {
|
||||||
|
return nil, err
|
||||||
// MethodName implements the [FieldNameMapper.MethodName] interface method.
|
|
||||||
func (u FieldMapper) MethodName(_ reflect.Type, m reflect.Method) string {
|
|
||||||
return convertGoToJSName(m.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertGoToJSName(name string) string {
|
|
||||||
allUppercase := true
|
|
||||||
for _, c := range name {
|
|
||||||
if c != '_' && !unicode.IsUpper(c) {
|
|
||||||
allUppercase = false
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eg. "JSON" -> "json"
|
result := map[string][]byte{}
|
||||||
if allUppercase {
|
|
||||||
return strings.ToLower(name)
|
for _, f := range files {
|
||||||
|
if f.IsDir() || (exp != nil && !exp.MatchString(f.Name())) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// eg. "GetField" -> "getField"
|
raw, err := os.ReadFile(filepath.Join(dirPath, f.Name()))
|
||||||
return strings.ToLower(name[0:1]) + name[1:]
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result[f.Name()] = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,35 @@
|
||||||
package jsvm_test
|
package jsvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"encoding/json"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/daos"
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"github.com/pocketbase/pocketbase/models/schema"
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
"github.com/pocketbase/pocketbase/plugins/jsvm"
|
|
||||||
"github.com/pocketbase/pocketbase/tests"
|
"github.com/pocketbase/pocketbase/tests"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBaseVMUnmarshal(t *testing.T) {
|
// note: this test is useful as a reminder to update the tests in case
|
||||||
vm := jsvm.NewBaseVM()
|
// a new base binding is added.
|
||||||
|
func TestBaseBindsCount(t *testing.T) {
|
||||||
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
|
|
||||||
|
testBindsCount(vm, "this", 9, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseBindsUnmarshal(t *testing.T) {
|
||||||
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
|
|
||||||
v, err := vm.RunString(`unmarshal({ name: "test" }, new Collection())`)
|
v, err := vm.RunString(`unmarshal({ name: "test" }, new Collection())`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -29,7 +46,7 @@ func TestBaseVMUnmarshal(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseVMRecordBind(t *testing.T) {
|
func TestBaseBindsRecord(t *testing.T) {
|
||||||
app, _ := tests.NewTestApp()
|
app, _ := tests.NewTestApp()
|
||||||
defer app.Cleanup()
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
@ -38,7 +55,8 @@ func TestBaseVMRecordBind(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vm := jsvm.NewBaseVM()
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
vm.Set("collection", collection)
|
vm.Set("collection", collection)
|
||||||
|
|
||||||
// without record data
|
// without record data
|
||||||
|
@ -74,75 +92,9 @@ func TestBaseVMRecordBind(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo enable after https://github.com/dop251/goja/issues/426
|
func TestBaseBindsCollection(t *testing.T) {
|
||||||
// func TestBaseVMRecordGetAndSetBind(t *testing.T) {
|
vm := goja.New()
|
||||||
// app, _ := tests.NewTestApp()
|
baseBinds(vm)
|
||||||
// defer app.Cleanup()
|
|
||||||
|
|
||||||
// collection, err := app.Dao().FindCollectionByNameOrId("users")
|
|
||||||
// if err != nil {
|
|
||||||
// t.Fatal(err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// vm := jsvm.NewBaseVM()
|
|
||||||
// vm.Set("collection", collection)
|
|
||||||
// vm.Set("getRecord", func() *models.Record {
|
|
||||||
// return models.NewRecord(collection)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// _, runErr := vm.RunString(`
|
|
||||||
// const jsRecord = new Record(collection);
|
|
||||||
// jsRecord.email = "test@example.com"; // test js record setter
|
|
||||||
// const email = jsRecord.email; // test js record getter
|
|
||||||
|
|
||||||
// const goRecord = getRecord()
|
|
||||||
// goRecord.name = "test" // test go record setter
|
|
||||||
// const name = goRecord.name; // test go record getter
|
|
||||||
// `)
|
|
||||||
// if runErr != nil {
|
|
||||||
// t.Fatal(runErr)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// expectedEmail := "test@example.com"
|
|
||||||
// expectedName := "test"
|
|
||||||
|
|
||||||
// jsRecord, ok := vm.Get("jsRecord").Export().(*models.Record)
|
|
||||||
// if !ok {
|
|
||||||
// t.Fatalf("Failed to export jsRecord")
|
|
||||||
// }
|
|
||||||
// if v := jsRecord.Email(); v != expectedEmail {
|
|
||||||
// t.Fatalf("Expected the js created record to have email %q, got %q", expectedEmail, v)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// email := vm.Get("email").Export().(string)
|
|
||||||
// if email != expectedEmail {
|
|
||||||
// t.Fatalf("Expected exported email %q, got %q", expectedEmail, email)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// goRecord, ok := vm.Get("goRecord").Export().(*models.Record)
|
|
||||||
// if !ok {
|
|
||||||
// t.Fatalf("Failed to export goRecord")
|
|
||||||
// }
|
|
||||||
// if v := goRecord.GetString("name"); v != expectedName {
|
|
||||||
// t.Fatalf("Expected the go created record to have name %q, got %q", expectedName, v)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// name := vm.Get("name").Export().(string)
|
|
||||||
// if name != expectedName {
|
|
||||||
// t.Fatalf("Expected exported name %q, got %q", expectedName, name)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // ensure that the two record instances are not mixed
|
|
||||||
// if v := goRecord.Email(); v != "" {
|
|
||||||
// t.Fatalf("Expected the go created record to not have an email, got %q", v)
|
|
||||||
// }
|
|
||||||
// if v := jsRecord.GetString("name"); v != "" {
|
|
||||||
// t.Fatalf("Expected the js created record to not have a name, got %q", v)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
func TestBaseVMCollectionBind(t *testing.T) {
|
|
||||||
vm := jsvm.NewBaseVM()
|
|
||||||
|
|
||||||
v, err := vm.RunString(`new Collection({ name: "test", schema: [{name: "title", "type": "text"}] })`)
|
v, err := vm.RunString(`new Collection({ name: "test", schema: [{name: "title", "type": "text"}] })`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -164,7 +116,8 @@ func TestBaseVMCollectionBind(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseVMAdminBind(t *testing.T) {
|
func TestBaseVMAdminBind(t *testing.T) {
|
||||||
vm := jsvm.NewBaseVM()
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
|
|
||||||
v, err := vm.RunString(`new Admin({ email: "test@example.com" })`)
|
v, err := vm.RunString(`new Admin({ email: "test@example.com" })`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -177,8 +130,9 @@ func TestBaseVMAdminBind(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseVMSchemaBind(t *testing.T) {
|
func TestBaseBindsSchema(t *testing.T) {
|
||||||
vm := jsvm.NewBaseVM()
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
|
|
||||||
v, err := vm.RunString(`new Schema([{name: "title", "type": "text"}])`)
|
v, err := vm.RunString(`new Schema([{name: "title", "type": "text"}])`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -195,8 +149,9 @@ func TestBaseVMSchemaBind(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseVMSchemaFieldBind(t *testing.T) {
|
func TestBaseBindsSchemaField(t *testing.T) {
|
||||||
vm := jsvm.NewBaseVM()
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
|
|
||||||
v, err := vm.RunString(`new SchemaField({name: "title", "type": "text"})`)
|
v, err := vm.RunString(`new SchemaField({name: "title", "type": "text"})`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -213,56 +168,461 @@ func TestBaseVMSchemaFieldBind(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseVMDaoBind(t *testing.T) {
|
func TestBaseBindsMail(t *testing.T) {
|
||||||
app, _ := tests.NewTestApp()
|
vm := goja.New()
|
||||||
defer app.Cleanup()
|
baseBinds(vm)
|
||||||
|
|
||||||
vm := jsvm.NewBaseVM()
|
v, err := vm.RunString(`new Mail({
|
||||||
vm.Set("db", app.DB())
|
from: {name: "test_from", address: "test_from@example.com"},
|
||||||
|
to: [
|
||||||
v, err := vm.RunString(`new Dao(db)`)
|
{name: "test_to1", address: "test_to1@example.com"},
|
||||||
|
{name: "test_to2", address: "test_to2@example.com"},
|
||||||
|
],
|
||||||
|
bcc: [
|
||||||
|
{name: "test_bcc1", address: "test_bcc1@example.com"},
|
||||||
|
{name: "test_bcc2", address: "test_bcc2@example.com"},
|
||||||
|
],
|
||||||
|
cc: [
|
||||||
|
{name: "test_cc1", address: "test_cc1@example.com"},
|
||||||
|
{name: "test_cc2", address: "test_cc2@example.com"},
|
||||||
|
],
|
||||||
|
subject: "test_subject",
|
||||||
|
html: "test_html",
|
||||||
|
text: "test_text",
|
||||||
|
headers: {
|
||||||
|
header1: "a",
|
||||||
|
header2: "b",
|
||||||
|
}
|
||||||
|
})`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
d, ok := v.Export().(*daos.Dao)
|
m, ok := v.Export().(*mailer.Message)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Expected daos.Dao, got %v", d)
|
t.Fatalf("Expected mailer.Message, got %v", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.DB() != app.DB() {
|
raw, err := json.Marshal(m)
|
||||||
t.Fatalf("The db instances doesn't match")
|
|
||||||
|
expected := `{"from":{"Name":"test_from","Address":"test_from@example.com"},"to":[{"Name":"test_to1","Address":"test_to1@example.com"},{"Name":"test_to2","Address":"test_to2@example.com"}],"bcc":[{"Name":"test_bcc1","Address":"test_bcc1@example.com"},{"Name":"test_bcc2","Address":"test_bcc2@example.com"}],"cc":[{"Name":"test_cc1","Address":"test_cc1@example.com"},{"Name":"test_cc2","Address":"test_cc2@example.com"}],"subject":"test_subject","html":"test_html","text":"test_text","headers":{"header1":"a","header2":"b"},"attachments":null}`
|
||||||
|
|
||||||
|
if string(raw) != expected {
|
||||||
|
t.Fatalf("Expected \n%s, \ngot \n%s", expected, raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFieldMapper(t *testing.T) {
|
func TestBaseBindsValidationError(t *testing.T) {
|
||||||
mapper := jsvm.FieldMapper{}
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
|
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
name string
|
js string
|
||||||
expected string
|
expectCode string
|
||||||
|
expectMessage string
|
||||||
}{
|
}{
|
||||||
{"", ""},
|
{
|
||||||
{"test", "test"},
|
`new ValidationError()`,
|
||||||
{"Test", "test"},
|
"",
|
||||||
{"miXeD", "miXeD"},
|
"",
|
||||||
{"MiXeD", "miXeD"},
|
},
|
||||||
{"ResolveRequestAsJSON", "resolveRequestAsJSON"},
|
{
|
||||||
{"Variable_with_underscore", "variable_with_underscore"},
|
`new ValidationError("test_code")`,
|
||||||
{"ALLCAPS", "allcaps"},
|
"test_code",
|
||||||
{"NOTALLCAPs", "nOTALLCAPs"},
|
"",
|
||||||
{"ALL_CAPS_WITH_UNDERSCORE", "all_caps_with_underscore"},
|
},
|
||||||
|
{
|
||||||
|
`new ValidationError("test_code", "test_message")`,
|
||||||
|
"test_code",
|
||||||
|
"test_message",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, s := range scenarios {
|
for _, s := range scenarios {
|
||||||
field := reflect.StructField{Name: s.name}
|
v, err := vm.RunString(s.js)
|
||||||
if v := mapper.FieldName(nil, field); v != s.expected {
|
if err != nil {
|
||||||
t.Fatalf("[%d] Expected FieldName %q, got %q", i, s.expected, v)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
method := reflect.Method{Name: s.name}
|
m, ok := v.Export().(validation.Error)
|
||||||
if v := mapper.MethodName(nil, method); v != s.expected {
|
if !ok {
|
||||||
t.Fatalf("[%d] Expected MethodName %q, got %q", i, s.expected, v)
|
t.Fatalf("[%s] Expected validation.Error, got %v", s.js, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Code() != s.expectCode {
|
||||||
|
t.Fatalf("[%s] Expected code %q, got %q", s.js, s.expectCode, m.Code())
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Message() != s.expectMessage {
|
||||||
|
t.Fatalf("[%s] Expected message %q, got %q", s.js, s.expectMessage, m.Message())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBaseBindsDao(t *testing.T) {
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
|
vm.Set("db", app.Dao().ConcurrentDB())
|
||||||
|
vm.Set("db2", app.Dao().NonconcurrentDB())
|
||||||
|
|
||||||
|
scenarios := []struct {
|
||||||
|
js string
|
||||||
|
concurrentDB dbx.Builder
|
||||||
|
nonconcurrentDB dbx.Builder
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
js: "new Dao(db)",
|
||||||
|
concurrentDB: app.Dao().ConcurrentDB(),
|
||||||
|
nonconcurrentDB: app.Dao().ConcurrentDB(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
js: "new Dao(db, db2)",
|
||||||
|
concurrentDB: app.Dao().ConcurrentDB(),
|
||||||
|
nonconcurrentDB: app.Dao().NonconcurrentDB(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
v, err := vm.RunString(s.js)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d, ok := v.Export().(*daos.Dao)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("[%s] Expected daos.Dao, got %v", s.js, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.ConcurrentDB() != s.concurrentDB {
|
||||||
|
t.Fatalf("[%s] The ConcurrentDB instances doesn't match", s.js)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.NonconcurrentDB() != s.nonconcurrentDB {
|
||||||
|
t.Fatalf("[%s] The NonconcurrentDB instances doesn't match", s.js)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDbxBinds(t *testing.T) {
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
vm := goja.New()
|
||||||
|
vm.Set("db", app.Dao().DB())
|
||||||
|
baseBinds(vm)
|
||||||
|
dbxBinds(vm)
|
||||||
|
|
||||||
|
testBindsCount(vm, "$dbx", 15, t)
|
||||||
|
|
||||||
|
sceneraios := []struct {
|
||||||
|
js string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`$dbx.exp("a = 1").build(db, {})`,
|
||||||
|
"a = 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.hashExp({
|
||||||
|
"a": 1,
|
||||||
|
b: null,
|
||||||
|
c: [1, 2, 3],
|
||||||
|
}).build(db, {})`,
|
||||||
|
"`a`={:p0} AND `b` IS NULL AND `c` IN ({:p1}, {:p2}, {:p3})",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.not($dbx.exp("a = 1")).build(db, {})`,
|
||||||
|
"NOT (a = 1)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.and($dbx.exp("a = 1"), $dbx.exp("b = 2")).build(db, {})`,
|
||||||
|
"(a = 1) AND (b = 2)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.or($dbx.exp("a = 1"), $dbx.exp("b = 2")).build(db, {})`,
|
||||||
|
"(a = 1) OR (b = 2)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.in("a", 1, 2, 3).build(db, {})`,
|
||||||
|
"`a` IN ({:p0}, {:p1}, {:p2})",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.notIn("a", 1, 2, 3).build(db, {})`,
|
||||||
|
"`a` NOT IN ({:p0}, {:p1}, {:p2})",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.like("a", "test1", "test2").match(true, false).build(db, {})`,
|
||||||
|
"`a` LIKE {:p0} AND `a` LIKE {:p1}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.orLike("a", "test1", "test2").match(false, true).build(db, {})`,
|
||||||
|
"`a` LIKE {:p0} OR `a` LIKE {:p1}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.notLike("a", "test1", "test2").match(true, false).build(db, {})`,
|
||||||
|
"`a` NOT LIKE {:p0} AND `a` NOT LIKE {:p1}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.orNotLike("a", "test1", "test2").match(false, false).build(db, {})`,
|
||||||
|
"`a` NOT LIKE {:p0} OR `a` NOT LIKE {:p1}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.exists($dbx.exp("a = 1")).build(db, {})`,
|
||||||
|
"EXISTS (a = 1)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.notExists($dbx.exp("a = 1")).build(db, {})`,
|
||||||
|
"NOT EXISTS (a = 1)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.between("a", 1, 2).build(db, {})`,
|
||||||
|
"`a` BETWEEN {:p0} AND {:p1}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$dbx.notBetween("a", 1, 2).build(db, {})`,
|
||||||
|
"`a` NOT BETWEEN {:p0} AND {:p1}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range sceneraios {
|
||||||
|
result, err := vm.RunString(s.js)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := result.Export().(string)
|
||||||
|
|
||||||
|
if v != s.expected {
|
||||||
|
t.Fatalf("[%s] Expected \n%s, \ngot \n%s", s.js, s.expected, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokensBinds(t *testing.T) {
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
admin, err := app.Dao().FindAdminByEmail("test@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vm := goja.New()
|
||||||
|
vm.Set("$app", app)
|
||||||
|
vm.Set("admin", admin)
|
||||||
|
vm.Set("record", record)
|
||||||
|
baseBinds(vm)
|
||||||
|
tokensBinds(vm)
|
||||||
|
|
||||||
|
testBindsCount(vm, "$tokens", 8, t)
|
||||||
|
|
||||||
|
sceneraios := []struct {
|
||||||
|
js string
|
||||||
|
key string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`$tokens.adminAuthToken($app, admin)`,
|
||||||
|
admin.TokenKey + app.Settings().AdminAuthToken.Secret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$tokens.adminResetPasswordToken($app, admin)`,
|
||||||
|
admin.TokenKey + app.Settings().AdminPasswordResetToken.Secret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$tokens.adminFileToken($app, admin)`,
|
||||||
|
admin.TokenKey + app.Settings().AdminFileToken.Secret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$tokens.recordAuthToken($app, record)`,
|
||||||
|
record.TokenKey() + app.Settings().RecordAuthToken.Secret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$tokens.recordVerifyToken($app, record)`,
|
||||||
|
record.TokenKey() + app.Settings().RecordVerificationToken.Secret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$tokens.recordResetPasswordToken($app, record)`,
|
||||||
|
record.TokenKey() + app.Settings().RecordPasswordResetToken.Secret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$tokens.recordChangeEmailToken($app, record)`,
|
||||||
|
record.TokenKey() + app.Settings().RecordEmailChangeToken.Secret,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$tokens.recordFileToken($app, record)`,
|
||||||
|
record.TokenKey() + app.Settings().RecordFileToken.Secret,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range sceneraios {
|
||||||
|
result, err := vm.RunString(s.js)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := result.Export().(string)
|
||||||
|
|
||||||
|
if _, err := security.ParseJWT(v, s.key); err != nil {
|
||||||
|
t.Fatalf("[%s] Failed to parse JWT %v, got %v", s.js, v, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecurityRandomStringBinds(t *testing.T) {
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
|
securityBinds(vm)
|
||||||
|
|
||||||
|
testBindsCount(vm, "$security", 7, t)
|
||||||
|
|
||||||
|
sceneraios := []struct {
|
||||||
|
js string
|
||||||
|
length int
|
||||||
|
}{
|
||||||
|
{`$security.randomString(6)`, 6},
|
||||||
|
{`$security.randomStringWithAlphabet(7, "abc")`, 7},
|
||||||
|
{`$security.pseudorandomString(8)`, 8},
|
||||||
|
{`$security.pseudorandomStringWithAlphabet(9, "abc")`, 9},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range sceneraios {
|
||||||
|
result, err := vm.RunString(s.js)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, _ := result.Export().(string)
|
||||||
|
|
||||||
|
if len(v) != s.length {
|
||||||
|
t.Fatalf("[%s] Expected %d length string, \ngot \n%v", s.js, s.length, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecurityTokenBinds(t *testing.T) {
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
vm := goja.New()
|
||||||
|
baseBinds(vm)
|
||||||
|
securityBinds(vm)
|
||||||
|
|
||||||
|
testBindsCount(vm, "$security", 7, t)
|
||||||
|
|
||||||
|
sceneraios := []struct {
|
||||||
|
js string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`$security.parseUnverifiedToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.aXzC7q7z1lX_hxk5P0R368xEU7H1xRwnBQQcLAmG0EY")`,
|
||||||
|
`{"name":"John Doe","sub":"1234567890"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$security.parseToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.aXzC7q7z1lX_hxk5P0R368xEU7H1xRwnBQQcLAmG0EY", "test")`,
|
||||||
|
`{"name":"John Doe","sub":"1234567890"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$security.createToken({"exp": 123}, "test", 0)`, // overwrite the exp claim for static token
|
||||||
|
`"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyM30.7gbv7w672gApdBRASI6OniCtKwkKjhieSxsr6vxSrtw"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range sceneraios {
|
||||||
|
result, err := vm.RunString(s.js)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[%s] Failed to execute js script, got %v", s.js, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, _ := json.Marshal(result.Export())
|
||||||
|
|
||||||
|
if string(raw) != s.expected {
|
||||||
|
t.Fatalf("[%s] Expected \n%s, \ngot \n%s", s.js, s.expected, raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilesystemBinds(t *testing.T) {
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
vm := goja.New()
|
||||||
|
vm.Set("mh", &multipart.FileHeader{Filename: "test"})
|
||||||
|
vm.Set("testFile", filepath.Join(app.DataDir(), "data.db"))
|
||||||
|
baseBinds(vm)
|
||||||
|
filesystemBinds(vm)
|
||||||
|
|
||||||
|
testBindsCount(vm, "$filesystem", 3, t)
|
||||||
|
|
||||||
|
// fileFromPath
|
||||||
|
{
|
||||||
|
v, err := vm.RunString(`$filesystem.fileFromPath(testFile)`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, _ := v.Export().(*filesystem.File)
|
||||||
|
|
||||||
|
if file == nil || file.OriginalName != "data.db" {
|
||||||
|
t.Fatalf("[fileFromPath] Expected file with name %q, got %v", file.OriginalName, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileFromBytes
|
||||||
|
{
|
||||||
|
v, err := vm.RunString(`$filesystem.fileFromBytes([1, 2, 3], "test")`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, _ := v.Export().(*filesystem.File)
|
||||||
|
|
||||||
|
if file == nil || file.OriginalName != "test" {
|
||||||
|
t.Fatalf("[fileFromBytes] Expected file with name %q, got %v", file.OriginalName, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileFromMultipart
|
||||||
|
{
|
||||||
|
v, err := vm.RunString(`$filesystem.fileFromMultipart(mh)`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, _ := v.Export().(*filesystem.File)
|
||||||
|
|
||||||
|
if file == nil || file.OriginalName != "test" {
|
||||||
|
t.Fatalf("[fileFromMultipart] Expected file with name %q, got %v", file.OriginalName, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormsBinds(t *testing.T) {
|
||||||
|
vm := goja.New()
|
||||||
|
formsBinds(vm)
|
||||||
|
|
||||||
|
testBindsCount(vm, "this", 20, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBindsCount(vm *goja.Runtime, namespace string, count int, t *testing.T) {
|
||||||
|
v, err := vm.RunString(`Object.keys(` + namespace + `).length`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
total, _ := v.Export().(int64)
|
||||||
|
|
||||||
|
if int(total) != count {
|
||||||
|
t.Fatalf("Expected %d %s binds, got %d", count, namespace, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (p *plugin) afterCollectionChange() func(*core.ModelEvent) error {
|
||||||
|
|
||||||
var template string
|
var template string
|
||||||
var templateErr error
|
var templateErr error
|
||||||
if p.options.TemplateLang == TemplateLangJS {
|
if p.config.TemplateLang == TemplateLangJS {
|
||||||
template, templateErr = p.jsDiffTemplate(new, old)
|
template, templateErr = p.jsDiffTemplate(new, old)
|
||||||
} else {
|
} else {
|
||||||
template, templateErr = p.goDiffTemplate(new, old)
|
template, templateErr = p.goDiffTemplate(new, old)
|
||||||
|
@ -63,8 +63,8 @@ func (p *plugin) afterCollectionChange() func(*core.ModelEvent) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
appliedTime := time.Now().Unix()
|
appliedTime := time.Now().Unix()
|
||||||
name := fmt.Sprintf("%d_%s.%s", appliedTime, action, p.options.TemplateLang)
|
name := fmt.Sprintf("%d_%s.%s", appliedTime, action, p.config.TemplateLang)
|
||||||
filePath := filepath.Join(p.options.Dir, name)
|
filePath := filepath.Join(p.config.Dir, name)
|
||||||
|
|
||||||
return p.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
return p.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||||
// insert the migration entry
|
// insert the migration entry
|
||||||
|
@ -77,7 +77,7 @@ func (p *plugin) afterCollectionChange() func(*core.ModelEvent) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that the local migrations dir exist
|
// ensure that the local migrations dir exist
|
||||||
if err := os.MkdirAll(p.options.Dir, os.ModePerm); err != nil {
|
if err := os.MkdirAll(p.config.Dir, os.ModePerm); err != nil {
|
||||||
return fmt.Errorf("failed to create migration dir: %w", err)
|
return fmt.Errorf("failed to create migration dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ func (p *plugin) getCachedCollections() (map[string]*models.Collection, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *plugin) hasCustomMigrations() bool {
|
func (p *plugin) hasCustomMigrations() bool {
|
||||||
files, err := os.ReadDir(p.options.Dir)
|
files, err := os.ReadDir(p.config.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
//
|
//
|
||||||
// Example usage:
|
// Example usage:
|
||||||
//
|
//
|
||||||
// migratecmd.MustRegister(app, app.RootCmd, &migratecmd.Options{
|
// migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
|
||||||
// TemplateLang: migratecmd.TemplateLangJS, // default to migratecmd.TemplateLangGo
|
// TemplateLang: migratecmd.TemplateLangJS, // default to migratecmd.TemplateLangGo
|
||||||
// Automigrate: true,
|
// Automigrate: true,
|
||||||
// Dir: "migrations_dir_path", // optional template migrations path; default to "pb_migrations" (for JS) and "migrations" (for Go)
|
// Dir: "/custom/migrations/dir", // optional template migrations path; default to "pb_migrations" (for JS) and "migrations" (for Go)
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// Note: To allow running JS migrations you'll need to enable first
|
// Note: To allow running JS migrations you'll need to enable first
|
||||||
|
@ -32,8 +32,8 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options defines optional struct to customize the default plugin behavior.
|
// Config defines the config options of the migratecmd plugin.
|
||||||
type Options struct {
|
type Config struct {
|
||||||
// Dir specifies the directory with the user defined migrations.
|
// Dir specifies the directory with the user defined migrations.
|
||||||
//
|
//
|
||||||
// If not set it fallbacks to a relative "pb_data/../pb_migrations" (for js)
|
// If not set it fallbacks to a relative "pb_data/../pb_migrations" (for js)
|
||||||
|
@ -48,35 +48,31 @@ type Options struct {
|
||||||
TemplateLang string
|
TemplateLang string
|
||||||
}
|
}
|
||||||
|
|
||||||
type plugin struct {
|
// MustRegister registers the migratecmd plugin to the provided app instance
|
||||||
app core.App
|
// and panic if it fails.
|
||||||
options *Options
|
//
|
||||||
}
|
// Example usage:
|
||||||
|
//
|
||||||
func MustRegister(app core.App, rootCmd *cobra.Command, options *Options) {
|
// migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{})
|
||||||
if err := Register(app, rootCmd, options); err != nil {
|
func MustRegister(app core.App, rootCmd *cobra.Command, config Config) {
|
||||||
|
if err := Register(app, rootCmd, config); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Register(app core.App, rootCmd *cobra.Command, options *Options) error {
|
// Register registers the migratecmd plugin to the provided app instance.
|
||||||
p := &plugin{app: app}
|
func Register(app core.App, rootCmd *cobra.Command, config Config) error {
|
||||||
|
p := &plugin{app: app, config: config}
|
||||||
|
|
||||||
if options != nil {
|
if p.config.TemplateLang == "" {
|
||||||
p.options = options
|
p.config.TemplateLang = TemplateLangGo
|
||||||
} else {
|
|
||||||
p.options = &Options{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.options.TemplateLang == "" {
|
if p.config.Dir == "" {
|
||||||
p.options.TemplateLang = TemplateLangGo
|
if p.config.TemplateLang == TemplateLangJS {
|
||||||
}
|
p.config.Dir = filepath.Join(p.app.DataDir(), "../pb_migrations")
|
||||||
|
|
||||||
if p.options.Dir == "" {
|
|
||||||
if p.options.TemplateLang == TemplateLangJS {
|
|
||||||
p.options.Dir = filepath.Join(p.app.DataDir(), "../pb_migrations")
|
|
||||||
} else {
|
} else {
|
||||||
p.options.Dir = filepath.Join(p.app.DataDir(), "../migrations")
|
p.config.Dir = filepath.Join(p.app.DataDir(), "../migrations")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +82,7 @@ func Register(app core.App, rootCmd *cobra.Command, options *Options) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// watch for collection changes
|
// watch for collection changes
|
||||||
if p.options.Automigrate {
|
if p.config.Automigrate {
|
||||||
// refresh the cache right after app bootstap
|
// refresh the cache right after app bootstap
|
||||||
p.app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
|
p.app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error {
|
||||||
p.refreshCachedCollections()
|
p.refreshCachedCollections()
|
||||||
|
@ -129,6 +125,11 @@ func Register(app core.App, rootCmd *cobra.Command, options *Options) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type plugin struct {
|
||||||
|
app core.App
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
func (p *plugin) createCommand() *cobra.Command {
|
func (p *plugin) createCommand() *cobra.Command {
|
||||||
const cmdDesc = `Supported arguments are:
|
const cmdDesc = `Supported arguments are:
|
||||||
- up - runs all available migrations
|
- up - runs all available migrations
|
||||||
|
@ -185,9 +186,9 @@ func (p *plugin) migrateCreateHandler(template string, args []string, interactiv
|
||||||
}
|
}
|
||||||
|
|
||||||
name := args[0]
|
name := args[0]
|
||||||
dir := p.options.Dir
|
dir := p.config.Dir
|
||||||
|
|
||||||
filename := fmt.Sprintf("%d_%s.%s", time.Now().Unix(), inflector.Snakecase(name), p.options.TemplateLang)
|
filename := fmt.Sprintf("%d_%s.%s", time.Now().Unix(), inflector.Snakecase(name), p.config.TemplateLang)
|
||||||
|
|
||||||
resultFilePath := path.Join(dir, filename)
|
resultFilePath := path.Join(dir, filename)
|
||||||
|
|
||||||
|
@ -206,7 +207,7 @@ func (p *plugin) migrateCreateHandler(template string, args []string, interactiv
|
||||||
// get default create template
|
// get default create template
|
||||||
if template == "" {
|
if template == "" {
|
||||||
var templateErr error
|
var templateErr error
|
||||||
if p.options.TemplateLang == TemplateLangJS {
|
if p.config.TemplateLang == TemplateLangJS {
|
||||||
template, templateErr = p.jsBlankTemplate()
|
template, templateErr = p.jsBlankTemplate()
|
||||||
} else {
|
} else {
|
||||||
template, templateErr = p.goBlankTemplate()
|
template, templateErr = p.goBlankTemplate()
|
||||||
|
@ -244,7 +245,7 @@ func (p *plugin) migrateCollectionsHandler(args []string, interactive bool) (str
|
||||||
|
|
||||||
var template string
|
var template string
|
||||||
var templateErr error
|
var templateErr error
|
||||||
if p.options.TemplateLang == TemplateLangJS {
|
if p.config.TemplateLang == TemplateLangJS {
|
||||||
template, templateErr = p.jsSnapshotTemplate(collections)
|
template, templateErr = p.jsSnapshotTemplate(collections)
|
||||||
} else {
|
} else {
|
||||||
template, templateErr = p.goSnapshotTemplate(collections)
|
template, templateErr = p.goSnapshotTemplate(collections)
|
||||||
|
|
|
@ -343,7 +343,7 @@ func init() {
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
return fmt.Sprintf(template, filepath.Base(p.options.Dir)), nil
|
return fmt.Sprintf(template, filepath.Base(p.config.Dir)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *plugin) goSnapshotTemplate(collections []*models.Collection) (string, error) {
|
func (p *plugin) goSnapshotTemplate(collections []*models.Collection) (string, error) {
|
||||||
|
@ -380,7 +380,7 @@ func init() {
|
||||||
`
|
`
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
template,
|
template,
|
||||||
filepath.Base(p.options.Dir),
|
filepath.Base(p.config.Dir),
|
||||||
escapeBacktick(string(jsonData)),
|
escapeBacktick(string(jsonData)),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
@ -427,7 +427,7 @@ func init() {
|
||||||
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
template,
|
template,
|
||||||
filepath.Base(p.options.Dir),
|
filepath.Base(p.config.Dir),
|
||||||
escapeBacktick(string(jsonData)),
|
escapeBacktick(string(jsonData)),
|
||||||
collection.Id,
|
collection.Id,
|
||||||
), nil
|
), nil
|
||||||
|
@ -475,7 +475,7 @@ func init() {
|
||||||
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
template,
|
template,
|
||||||
filepath.Base(p.options.Dir),
|
filepath.Base(p.config.Dir),
|
||||||
collection.Id,
|
collection.Id,
|
||||||
escapeBacktick(string(jsonData)),
|
escapeBacktick(string(jsonData)),
|
||||||
), nil
|
), nil
|
||||||
|
@ -745,7 +745,7 @@ func init() {
|
||||||
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
template,
|
template,
|
||||||
filepath.Base(p.options.Dir),
|
filepath.Base(p.config.Dir),
|
||||||
imports,
|
imports,
|
||||||
old.Id, strings.TrimSpace(up),
|
old.Id, strings.TrimSpace(up),
|
||||||
new.Id, strings.TrimSpace(down),
|
new.Id, strings.TrimSpace(down),
|
||||||
|
|
|
@ -68,7 +68,7 @@ type Config struct {
|
||||||
func New() *PocketBase {
|
func New() *PocketBase {
|
||||||
_, isUsingGoRun := inspectRuntime()
|
_, isUsingGoRun := inspectRuntime()
|
||||||
|
|
||||||
return NewWithConfig(&Config{
|
return NewWithConfig(Config{
|
||||||
DefaultDebug: isUsingGoRun,
|
DefaultDebug: isUsingGoRun,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -80,11 +80,7 @@ func New() *PocketBase {
|
||||||
// Everything will be initialized when [Start()] is executed.
|
// Everything will be initialized when [Start()] is executed.
|
||||||
// If you want to initialize the application before calling [Start()],
|
// If you want to initialize the application before calling [Start()],
|
||||||
// then you'll have to manually call [Bootstrap()].
|
// then you'll have to manually call [Bootstrap()].
|
||||||
func NewWithConfig(config *Config) *PocketBase {
|
func NewWithConfig(config Config) *PocketBase {
|
||||||
if config == nil {
|
|
||||||
panic("missing config")
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize a default data directory based on the executable baseDir
|
// initialize a default data directory based on the executable baseDir
|
||||||
if config.DefaultDataDir == "" {
|
if config.DefaultDataDir == "" {
|
||||||
baseDir, _ := inspectRuntime()
|
baseDir, _ := inspectRuntime()
|
||||||
|
@ -112,10 +108,10 @@ func NewWithConfig(config *Config) *PocketBase {
|
||||||
|
|
||||||
// parse base flags
|
// parse base flags
|
||||||
// (errors are ignored, since the full flags parsing happens on Execute())
|
// (errors are ignored, since the full flags parsing happens on Execute())
|
||||||
pb.eagerParseFlags(config)
|
pb.eagerParseFlags(&config)
|
||||||
|
|
||||||
// initialize the app instance
|
// initialize the app instance
|
||||||
pb.appWrapper = &appWrapper{core.NewBaseApp(&core.BaseAppConfig{
|
pb.appWrapper = &appWrapper{core.NewBaseApp(core.BaseAppConfig{
|
||||||
DataDir: pb.dataDirFlag,
|
DataDir: pb.dataDirFlag,
|
||||||
EncryptionEnv: pb.encryptionEnvFlag,
|
EncryptionEnv: pb.encryptionEnvFlag,
|
||||||
IsDebug: pb.debugFlag,
|
IsDebug: pb.debugFlag,
|
||||||
|
|
|
@ -54,7 +54,7 @@ func TestNew(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewWithConfig(t *testing.T) {
|
func TestNewWithConfig(t *testing.T) {
|
||||||
app := NewWithConfig(&Config{
|
app := NewWithConfig(Config{
|
||||||
DefaultDebug: true,
|
DefaultDebug: true,
|
||||||
DefaultDataDir: "test_dir",
|
DefaultDataDir: "test_dir",
|
||||||
DefaultEncryptionEnv: "test_encryption_env",
|
DefaultEncryptionEnv: "test_encryption_env",
|
||||||
|
@ -108,7 +108,7 @@ func TestNewWithConfigAndFlags(t *testing.T) {
|
||||||
"--debug=false",
|
"--debug=false",
|
||||||
)
|
)
|
||||||
|
|
||||||
app := NewWithConfig(&Config{
|
app := NewWithConfig(Config{
|
||||||
DefaultDebug: true,
|
DefaultDebug: true,
|
||||||
DefaultDataDir: "test_dir",
|
DefaultDataDir: "test_dir",
|
||||||
DefaultEncryptionEnv: "test_encryption_env",
|
DefaultEncryptionEnv: "test_encryption_env",
|
||||||
|
@ -157,7 +157,7 @@ func TestSkipBootstrap(t *testing.T) {
|
||||||
defer os.RemoveAll(tempDir)
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
// already bootstrapped
|
// already bootstrapped
|
||||||
app0 := NewWithConfig(&Config{DefaultDataDir: tempDir})
|
app0 := NewWithConfig(Config{DefaultDataDir: tempDir})
|
||||||
app0.Bootstrap()
|
app0.Bootstrap()
|
||||||
if v := app0.skipBootstrap(); !v {
|
if v := app0.skipBootstrap(); !v {
|
||||||
t.Fatal("[bootstrapped] Expected true, got false")
|
t.Fatal("[bootstrapped] Expected true, got false")
|
||||||
|
@ -166,7 +166,7 @@ func TestSkipBootstrap(t *testing.T) {
|
||||||
// unknown command
|
// unknown command
|
||||||
os.Args = os.Args[:1]
|
os.Args = os.Args[:1]
|
||||||
os.Args = append(os.Args, "demo")
|
os.Args = append(os.Args, "demo")
|
||||||
app1 := NewWithConfig(&Config{DefaultDataDir: tempDir})
|
app1 := NewWithConfig(Config{DefaultDataDir: tempDir})
|
||||||
app1.RootCmd.AddCommand(&cobra.Command{Use: "test"})
|
app1.RootCmd.AddCommand(&cobra.Command{Use: "test"})
|
||||||
if v := app1.skipBootstrap(); !v {
|
if v := app1.skipBootstrap(); !v {
|
||||||
t.Fatal("[unknown] Expected true, got false")
|
t.Fatal("[unknown] Expected true, got false")
|
||||||
|
@ -185,7 +185,7 @@ func TestSkipBootstrap(t *testing.T) {
|
||||||
// base flag
|
// base flag
|
||||||
os.Args = os.Args[:1]
|
os.Args = os.Args[:1]
|
||||||
os.Args = append(os.Args, "--"+s.name)
|
os.Args = append(os.Args, "--"+s.name)
|
||||||
app1 := NewWithConfig(&Config{DefaultDataDir: tempDir})
|
app1 := NewWithConfig(Config{DefaultDataDir: tempDir})
|
||||||
if v := app1.skipBootstrap(); !v {
|
if v := app1.skipBootstrap(); !v {
|
||||||
t.Fatalf("[--%s] Expected true, got false", s.name)
|
t.Fatalf("[--%s] Expected true, got false", s.name)
|
||||||
}
|
}
|
||||||
|
@ -193,7 +193,7 @@ func TestSkipBootstrap(t *testing.T) {
|
||||||
// short flag
|
// short flag
|
||||||
os.Args = os.Args[:1]
|
os.Args = os.Args[:1]
|
||||||
os.Args = append(os.Args, "-"+s.short)
|
os.Args = append(os.Args, "-"+s.short)
|
||||||
app2 := NewWithConfig(&Config{DefaultDataDir: tempDir})
|
app2 := NewWithConfig(Config{DefaultDataDir: tempDir})
|
||||||
if v := app2.skipBootstrap(); !v {
|
if v := app2.skipBootstrap(); !v {
|
||||||
t.Fatalf("[-%s] Expected true, got false", s.short)
|
t.Fatalf("[-%s] Expected true, got false", s.short)
|
||||||
}
|
}
|
||||||
|
@ -205,7 +205,7 @@ func TestSkipBootstrap(t *testing.T) {
|
||||||
os.Args = os.Args[:1]
|
os.Args = os.Args[:1]
|
||||||
os.Args = append(os.Args, "custom")
|
os.Args = append(os.Args, "custom")
|
||||||
os.Args = append(os.Args, "--"+s.name)
|
os.Args = append(os.Args, "--"+s.name)
|
||||||
app3 := NewWithConfig(&Config{DefaultDataDir: tempDir})
|
app3 := NewWithConfig(Config{DefaultDataDir: tempDir})
|
||||||
app3.RootCmd.AddCommand(customCmd)
|
app3.RootCmd.AddCommand(customCmd)
|
||||||
if v := app3.skipBootstrap(); v {
|
if v := app3.skipBootstrap(); v {
|
||||||
t.Fatalf("[--%s custom] Expected false, got true", s.name)
|
t.Fatalf("[--%s custom] Expected false, got true", s.name)
|
||||||
|
@ -215,7 +215,7 @@ func TestSkipBootstrap(t *testing.T) {
|
||||||
os.Args = os.Args[:1]
|
os.Args = os.Args[:1]
|
||||||
os.Args = append(os.Args, "custom")
|
os.Args = append(os.Args, "custom")
|
||||||
os.Args = append(os.Args, "-"+s.short)
|
os.Args = append(os.Args, "-"+s.short)
|
||||||
app4 := NewWithConfig(&Config{DefaultDataDir: tempDir})
|
app4 := NewWithConfig(Config{DefaultDataDir: tempDir})
|
||||||
app4.RootCmd.AddCommand(customCmd)
|
app4.RootCmd.AddCommand(customCmd)
|
||||||
if v := app4.skipBootstrap(); v {
|
if v := app4.skipBootstrap(); v {
|
||||||
t.Fatalf("[-%s custom] Expected false, got true", s.short)
|
t.Fatalf("[-%s custom] Expected false, got true", s.short)
|
||||||
|
|
|
@ -94,7 +94,7 @@ func NewTestApp(optTestDataDir ...string) (*TestApp, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
app := core.NewBaseApp(&core.BaseAppConfig{
|
app := core.NewBaseApp(core.BaseAppConfig{
|
||||||
DataDir: tempDir,
|
DataDir: tempDir,
|
||||||
EncryptionEnv: "pb_test_env",
|
EncryptionEnv: "pb_test_env",
|
||||||
IsDebug: false,
|
IsDebug: false,
|
||||||
|
|
|
@ -7,15 +7,15 @@ import (
|
||||||
|
|
||||||
// Message defines a generic email message struct.
|
// Message defines a generic email message struct.
|
||||||
type Message struct {
|
type Message struct {
|
||||||
From mail.Address
|
From mail.Address `json:"from"`
|
||||||
To []mail.Address
|
To []mail.Address `json:"to"`
|
||||||
Bcc []mail.Address
|
Bcc []mail.Address `json:"bcc"`
|
||||||
Cc []mail.Address
|
Cc []mail.Address `json:"cc"`
|
||||||
Subject string
|
Subject string `json:"subject"`
|
||||||
HTML string
|
HTML string `json:"html"`
|
||||||
Text string
|
Text string `json:"text"`
|
||||||
Headers map[string]string
|
Headers map[string]string `json:"headers"`
|
||||||
Attachments map[string]io.Reader
|
Attachments map[string]io.Reader `json:"attachments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mailer defines a base mail client interface.
|
// Mailer defines a base mail client interface.
|
||||||
|
|
Loading…
Reference in New Issue