From 81dc1adfef51df2f30816bdae8153bd173dfe3f6 Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 9 Feb 2023 12:16:36 +0100 Subject: [PATCH] feat: initial commit --- .gitignore | 7 + Makefile | 84 + README.md | 5 + cmd/cli/command/app/package.go | 149 + cmd/cli/command/app/root.go | 16 + cmd/cli/command/app/run.go | 121 + cmd/cli/command/main.go | 48 + cmd/cli/main.go | 10 + doc/README.md | 15 + doc/apps/client-api/README.md | 64 + doc/apps/my-first-app.md | 103 + doc/apps/package-app.md | 3 + doc/apps/server-api/README.md | 28 + doc/apps/server-api/blob.md | 73 + doc/apps/server-api/console.md | 3 + doc/apps/server-api/context.md | 74 + doc/apps/server-api/net.md | 63 + doc/apps/server-api/rpc.md | 43 + doc/apps/server-api/store.md | 183 ++ go.mod | 57 + go.sum | 602 ++++ misc/client-sdk-testsuite/.gitignore | 2 + misc/client-sdk-testsuite/Makefile | 15 + misc/client-sdk-testsuite/README.md | 1 + misc/client-sdk-testsuite/package-lock.json | 2559 +++++++++++++++++ misc/client-sdk-testsuite/package.json | 16 + misc/client-sdk-testsuite/src/manifest.yml | 7 + .../src/public/index.html | 28 + .../src/public/test/client-sdk.js | 148 + misc/client-sdk-testsuite/src/server/main.js | 63 + modd.conf | 12 + package-lock.json | 235 ++ package.json | 15 + pkg/app/app.go | 16 + pkg/app/crypto.go | 25 + pkg/app/error.go | 5 + pkg/app/loader.go | 101 + pkg/app/promise_proxy.go | 41 + pkg/app/server.go | 182 ++ pkg/app/server_module.go | 17 + pkg/bundle/bundle.go | 11 + pkg/bundle/bundle_test.go | 70 + pkg/bundle/directory_bundle.go | 54 + pkg/bundle/error.go | 5 + pkg/bundle/filesystem.go | 101 + pkg/bundle/from_path.go | 60 + pkg/bundle/tar_bundle.go | 146 + pkg/bundle/testdata/bundle.tar.gz | Bin 0 -> 161 bytes pkg/bundle/testdata/bundle.zip | Bin 0 -> 527 bytes pkg/bundle/testdata/bundle/data/test/foo.txt | 1 + pkg/bundle/zip_bundle.go | 98 + pkg/bus/bus.go | 13 + pkg/bus/error.go | 9 + pkg/bus/memory/bus.go | 91 + pkg/bus/memory/bus_test.go | 29 + pkg/bus/memory/event_dispatcher.go | 117 + pkg/bus/memory/option.go | 19 + pkg/bus/memory/request_reply.go | 151 + pkg/bus/message.go | 33 + pkg/bus/testing/publish_subscribe.go | 96 + pkg/bus/testing/request_reply.go | 110 + pkg/http/blob.go | 281 ++ pkg/http/client.go | 22 + pkg/http/handler.go | 114 + pkg/http/options.go | 57 + pkg/http/sockjs.go | 233 ++ pkg/module/.gitignore | 1 + pkg/module/assert.go | 28 + pkg/module/authorization.go | 109 + pkg/module/authorization_test.go | 103 + pkg/module/blob.go | 282 ++ pkg/module/blob_message.go | 92 + pkg/module/console.go | 51 + pkg/module/context.go | 94 + pkg/module/error.go | 5 + pkg/module/lifecycle.go | 121 + pkg/module/message.go | 38 + pkg/module/net.go | 81 + pkg/module/rpc.go | 263 ++ pkg/module/store.go | 205 ++ pkg/module/store_test.go | 41 + pkg/module/testdata/store.js | 32 + pkg/module/user.go | 59 + pkg/module/user_test.go | 70 + pkg/sdk/client/src/client.ts | 255 ++ pkg/sdk/client/src/event-target.ts | 44 + pkg/sdk/client/src/index.ts | 3 + pkg/sdk/client/src/message.ts | 32 + pkg/sdk/client/src/rpc-error.ts | 11 + pkg/sdk/client/src/sock.ts | 3 + pkg/sdk/sdk.go | 6 + pkg/storage/blob_store.go | 66 + pkg/storage/document_store.go | 69 + pkg/storage/filter/and.go | 17 + pkg/storage/filter/eq.go | 17 + pkg/storage/filter/error.go | 13 + pkg/storage/filter/filter.go | 136 + pkg/storage/filter/gt.go | 17 + pkg/storage/filter/gte.go | 17 + pkg/storage/filter/in.go | 17 + pkg/storage/filter/like.go | 17 + pkg/storage/filter/lt.go | 17 + pkg/storage/filter/lte.go | 17 + pkg/storage/filter/neq.go | 17 + pkg/storage/filter/not.go | 17 + pkg/storage/filter/operator.go | 23 + pkg/storage/filter/or.go | 17 + pkg/storage/filter/sql/helper.go | 87 + pkg/storage/filter/sql/option.go | 78 + pkg/storage/filter/sql/sql.go | 159 + pkg/storage/filter/sql/sql_test.go | 84 + pkg/storage/filter/sql/transform.go | 45 + pkg/storage/query_option.go | 41 + pkg/storage/sqlite/blob_bucket.go | 424 +++ pkg/storage/sqlite/blob_info.go | 40 + pkg/storage/sqlite/blob_store.go | 136 + pkg/storage/sqlite/blob_store_test.go | 25 + pkg/storage/sqlite/document_store.go | 340 +++ pkg/storage/sqlite/document_store_test.go | 25 + pkg/storage/sqlite/json.go | 42 + pkg/storage/sqlite/sql.go | 99 + pkg/storage/sqlite/testdata/.gitignore | 1 + pkg/storage/testsuite/blob_store.go | 14 + pkg/storage/testsuite/blob_store_ops.go | 129 + pkg/storage/testsuite/document_store.go | 14 + pkg/storage/testsuite/document_store_query.go | 85 + 126 files changed, 11551 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/cli/command/app/package.go create mode 100644 cmd/cli/command/app/root.go create mode 100644 cmd/cli/command/app/run.go create mode 100644 cmd/cli/command/main.go create mode 100644 cmd/cli/main.go create mode 100644 doc/README.md create mode 100644 doc/apps/client-api/README.md create mode 100644 doc/apps/my-first-app.md create mode 100644 doc/apps/package-app.md create mode 100644 doc/apps/server-api/README.md create mode 100644 doc/apps/server-api/blob.md create mode 100644 doc/apps/server-api/console.md create mode 100644 doc/apps/server-api/context.md create mode 100644 doc/apps/server-api/net.md create mode 100644 doc/apps/server-api/rpc.md create mode 100644 doc/apps/server-api/store.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 misc/client-sdk-testsuite/.gitignore create mode 100644 misc/client-sdk-testsuite/Makefile create mode 100644 misc/client-sdk-testsuite/README.md create mode 100644 misc/client-sdk-testsuite/package-lock.json create mode 100644 misc/client-sdk-testsuite/package.json create mode 100644 misc/client-sdk-testsuite/src/manifest.yml create mode 100644 misc/client-sdk-testsuite/src/public/index.html create mode 100644 misc/client-sdk-testsuite/src/public/test/client-sdk.js create mode 100644 misc/client-sdk-testsuite/src/server/main.js create mode 100644 modd.conf create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pkg/app/app.go create mode 100644 pkg/app/crypto.go create mode 100644 pkg/app/error.go create mode 100644 pkg/app/loader.go create mode 100644 pkg/app/promise_proxy.go create mode 100644 pkg/app/server.go create mode 100644 pkg/app/server_module.go create mode 100644 pkg/bundle/bundle.go create mode 100644 pkg/bundle/bundle_test.go create mode 100644 pkg/bundle/directory_bundle.go create mode 100644 pkg/bundle/error.go create mode 100644 pkg/bundle/filesystem.go create mode 100644 pkg/bundle/from_path.go create mode 100644 pkg/bundle/tar_bundle.go create mode 100644 pkg/bundle/testdata/bundle.tar.gz create mode 100644 pkg/bundle/testdata/bundle.zip create mode 100644 pkg/bundle/testdata/bundle/data/test/foo.txt create mode 100644 pkg/bundle/zip_bundle.go create mode 100644 pkg/bus/bus.go create mode 100644 pkg/bus/error.go create mode 100644 pkg/bus/memory/bus.go create mode 100644 pkg/bus/memory/bus_test.go create mode 100644 pkg/bus/memory/event_dispatcher.go create mode 100644 pkg/bus/memory/option.go create mode 100644 pkg/bus/memory/request_reply.go create mode 100644 pkg/bus/message.go create mode 100644 pkg/bus/testing/publish_subscribe.go create mode 100644 pkg/bus/testing/request_reply.go create mode 100644 pkg/http/blob.go create mode 100644 pkg/http/client.go create mode 100644 pkg/http/handler.go create mode 100644 pkg/http/options.go create mode 100644 pkg/http/sockjs.go create mode 100644 pkg/module/.gitignore create mode 100644 pkg/module/assert.go create mode 100644 pkg/module/authorization.go create mode 100644 pkg/module/authorization_test.go create mode 100644 pkg/module/blob.go create mode 100644 pkg/module/blob_message.go create mode 100644 pkg/module/console.go create mode 100644 pkg/module/context.go create mode 100644 pkg/module/error.go create mode 100644 pkg/module/lifecycle.go create mode 100644 pkg/module/message.go create mode 100644 pkg/module/net.go create mode 100644 pkg/module/rpc.go create mode 100644 pkg/module/store.go create mode 100644 pkg/module/store_test.go create mode 100644 pkg/module/testdata/store.js create mode 100644 pkg/module/user.go create mode 100644 pkg/module/user_test.go create mode 100644 pkg/sdk/client/src/client.ts create mode 100644 pkg/sdk/client/src/event-target.ts create mode 100644 pkg/sdk/client/src/index.ts create mode 100644 pkg/sdk/client/src/message.ts create mode 100644 pkg/sdk/client/src/rpc-error.ts create mode 100644 pkg/sdk/client/src/sock.ts create mode 100644 pkg/sdk/sdk.go create mode 100644 pkg/storage/blob_store.go create mode 100644 pkg/storage/document_store.go create mode 100644 pkg/storage/filter/and.go create mode 100644 pkg/storage/filter/eq.go create mode 100644 pkg/storage/filter/error.go create mode 100644 pkg/storage/filter/filter.go create mode 100644 pkg/storage/filter/gt.go create mode 100644 pkg/storage/filter/gte.go create mode 100644 pkg/storage/filter/in.go create mode 100644 pkg/storage/filter/like.go create mode 100644 pkg/storage/filter/lt.go create mode 100644 pkg/storage/filter/lte.go create mode 100644 pkg/storage/filter/neq.go create mode 100644 pkg/storage/filter/not.go create mode 100644 pkg/storage/filter/operator.go create mode 100644 pkg/storage/filter/or.go create mode 100644 pkg/storage/filter/sql/helper.go create mode 100644 pkg/storage/filter/sql/option.go create mode 100644 pkg/storage/filter/sql/sql.go create mode 100644 pkg/storage/filter/sql/sql_test.go create mode 100644 pkg/storage/filter/sql/transform.go create mode 100644 pkg/storage/query_option.go create mode 100644 pkg/storage/sqlite/blob_bucket.go create mode 100644 pkg/storage/sqlite/blob_info.go create mode 100644 pkg/storage/sqlite/blob_store.go create mode 100644 pkg/storage/sqlite/blob_store_test.go create mode 100644 pkg/storage/sqlite/document_store.go create mode 100644 pkg/storage/sqlite/document_store_test.go create mode 100644 pkg/storage/sqlite/json.go create mode 100644 pkg/storage/sqlite/sql.go create mode 100644 pkg/storage/sqlite/testdata/.gitignore create mode 100644 pkg/storage/testsuite/blob_store.go create mode 100644 pkg/storage/testsuite/blob_store_ops.go create mode 100644 pkg/storage/testsuite/document_store.go create mode 100644 pkg/storage/testsuite/document_store_query.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..255d7fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/node_modules +/bin +/pkg/sdk/client/dist +/.env +/tools +*.sqlite +/.gitea-release \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c8e7906 --- /dev/null +++ b/Makefile @@ -0,0 +1,84 @@ +LINT_ARGS ?= --timeout 5m +GITCHLOG_ARGS ?= +SHELL := /bin/bash + +GOTEST_ARGS ?= -short + +ESBUILD_VERSION ?= v0.17.5 + +GIT_VERSION := $(shell git describe --always) + +build: build-edge-cli + +watch: + go run -mod=readonly github.com/cortesi/modd/cmd/modd@latest + +.PHONY: test +test: test-go + +test-go: + go test -v -count=1 $(GOTEST_ARGS) ./... + +lint: + golangci-lint run --enable-all $(LINT_ARGS) + +build-edge-cli: build-sdk + CGO_ENABLED=0 go build \ + -v \ + -o ./bin/cli \ + ./cmd/cli + +install-git-hooks: + git config core.hooksPath .githooks + + +tools/esbuild/bin/esbuild: + mkdir -p tools/esbuild/bin + curl -fsSL https://esbuild.github.io/dl/$(ESBUILD_VERSION) | sh + mv -f esbuild tools/esbuild/bin/esbuild + +build-sdk: pkg/sdk/client/dist/client.js + +.PHONY: pkg/sdk/client/dist/client.js +pkg/sdk/client/dist/client.js: tools/esbuild/bin/esbuild node_modules + mkdir -p pkg/sdk/client/dist + tools/esbuild/bin/esbuild \ + pkg/sdk/client/src/index.ts \ + --bundle \ + --sourcemap \ + --target=es2020 \ + --format=iife \ + --global-name=Edge \ + --define:global=window \ + --platform=browser \ + --footer:js="Edge=Edge.default;" \ + --outfile=pkg/sdk/client/dist/client.js + +node_modules: + npm ci + +gitea-release: tools/gitea-release/bin/gitea-release.sh build + mkdir -p .gitea-release + rm -rf .gitea-release/* + + cp bin/cli .gitea-release/edge_cli_amd64 + + # Create client-sdk-testsuite package + .gitea-release/edge_cli_amd64 app package -d ./misc/client-sdk-testsuite/dist -o .gitea-release + + GITEA_RELEASE_PROJECT="edge" \ + GITEA_RELEASE_ORG="arcad" \ + GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \ + GITEA_RELEASE_VERSION="$(GIT_VERSION)" \ + GITEA_RELEASE_NAME="$(GIT_VERSION)" \ + GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \ + GITEA_RELEASE_IS_DRAFT="false" \ + GITEA_RELEASE_IS_PRERELEASE="true" \ + GITEA_RELEASE_BODY="" \ + GITEA_RELEASE_ATTACHMENTS="$(shell find .gitea-release/* -type f)" \ + tools/gitea-release/bin/gitea-release.sh + +tools/gitea-release/bin/gitea-release.sh: + mkdir -p tools/gitea-release/bin + curl --output tools/gitea-release/bin/gitea-release.sh https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/gitea/gitea-release.sh + chmod +x tools/gitea-release/bin/gitea-release.sh \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f2c5af --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# `forge.cadoles/arcad/edge` + +## Documentation + +[Voir la documentation](./doc/README.md) \ No newline at end of file diff --git a/cmd/cli/command/app/package.go b/cmd/cli/command/app/package.go new file mode 100644 index 0000000..263d7e6 --- /dev/null +++ b/cmd/cli/command/app/package.go @@ -0,0 +1,149 @@ +package app + +import ( + "archive/zip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "forge.cadoles.com/arcad/edge/pkg/app" + "forge.cadoles.com/arcad/edge/pkg/bundle" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +func PackageCommand() *cli.Command { + return &cli.Command{ + Name: "package", + Usage: "Generate a new app package from given directory", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "directory", + Usage: "use source directory `DIR`", + Aliases: []string{"d"}, + Required: true, + }, + &cli.StringFlag{ + Name: "output-dir", + Aliases: []string{"o"}, + Usage: "use `DIR` as generated package destination", + }, + }, + Action: func(ctx *cli.Context) error { + appDir := ctx.String("directory") + outputDir := ctx.String("output-dir") + + if outputDir == "" { + workdir, err := os.Getwd() + if err != nil { + return errors.Wrap(err, "could not retrieve current working directory") + } + + outputDir = workdir + } + + bundle := bundle.NewDirectoryBundle(appDir) + + manifest, err := app.LoadAppManifest(bundle) + if err != nil { + return errors.Wrap(err, "could not load app manifest") + } + + if err := os.MkdirAll(outputDir, 0o755); err != nil { + return errors.Wrapf(err, "could not create directory ''%s'", outputDir) + } + + archiveName := fmt.Sprintf( + "%s_%s%s", + strings.ToLower(string(manifest.ID)), + string(manifest.Version), + ".zip", + ) + packagePath := filepath.Join(outputDir, archiveName) + + if err := zipDirectory(appDir, packagePath); err != nil { + return errors.Wrapf(err, "could not zip directory ''%s'", appDir) + } + + return nil + }, + } +} + +func zipDirectory(baseDir string, outputFile string) error { + outFile, err := os.Create(outputFile) + if err != nil { + return errors.WithStack(err) + } + + defer func() { + if err := outFile.Close(); err != nil { + panic(errors.WithStack(err)) + } + }() + + w := zip.NewWriter(outFile) + + if err := copyDir(w, baseDir+"/", ""); err != nil { + return errors.WithStack(err) + } + + if err := w.Close(); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func copyDir(writer *zip.Writer, baseDir string, zipBasePath string) error { + files, err := ioutil.ReadDir(baseDir) + if err != nil { + return errors.WithStack(err) + } + + for _, file := range files { + if !file.IsDir() { + srcPath := baseDir + file.Name() + zipPath := zipBasePath + file.Name() + + if err := copyFile(writer, srcPath, zipPath); err != nil { + return errors.WithStack(err) + } + } else if file.IsDir() { + newBase := baseDir + file.Name() + "/" + + if err := copyDir(writer, newBase, zipBasePath+file.Name()+"/"); err != nil { + return errors.WithStack(err) + } + } + } + + return nil +} + +func copyFile(writer *zip.Writer, srcPath string, zipPath string) error { + r, err := os.Open(srcPath) + if err != nil { + return errors.WithStack(err) + } + + defer func() { + if err := r.Close(); err != nil { + panic(errors.WithStack(err)) + } + }() + + f, err := writer.Create(zipPath) + if err != nil { + return errors.WithStack(err) + } + + if _, err = io.Copy(f, r); err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/cmd/cli/command/app/root.go b/cmd/cli/command/app/root.go new file mode 100644 index 0000000..cfd7309 --- /dev/null +++ b/cmd/cli/command/app/root.go @@ -0,0 +1,16 @@ +package app + +import ( + "github.com/urfave/cli/v2" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "app", + Usage: "App related commands", + Subcommands: []*cli.Command{ + RunCommand(), + PackageCommand(), + }, + } +} diff --git a/cmd/cli/command/app/run.go b/cmd/cli/command/app/run.go new file mode 100644 index 0000000..1296377 --- /dev/null +++ b/cmd/cli/command/app/run.go @@ -0,0 +1,121 @@ +package app + +import ( + "database/sql" + "net/http" + "path/filepath" + + "forge.cadoles.com/arcad/edge/pkg/bus/memory" + appHTTP "forge.cadoles.com/arcad/edge/pkg/http" + "forge.cadoles.com/arcad/edge/pkg/module" + "forge.cadoles.com/arcad/edge/pkg/storage/sqlite" + "gitlab.com/wpetit/goweb/logger" + + "forge.cadoles.com/arcad/edge/pkg/bundle" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" + + _ "modernc.org/sqlite" +) + +func RunCommand() *cli.Command { + return &cli.Command{ + Name: "run", + Usage: "Run the specified app bundle", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "path", + Usage: "use `PATH` as app bundle (zipped bundle or directory)", + Aliases: []string{"p"}, + Value: ".", + }, + &cli.StringFlag{ + Name: "address", + Usage: "use `ADDRESS` as http server listening address", + Aliases: []string{"a"}, + Value: ":8080", + }, + &cli.StringFlag{ + Name: "log-format", + Usage: "use `LOG-FORMAT` ('json' or 'human')", + Value: "human", + }, + &cli.IntFlag{ + Name: "log-level", + Usage: "use `LOG-LEVEL` (0: debug -> 5: fatal)", + Value: 0, + }, + &cli.StringFlag{ + Name: "storage-file", + Usage: "use `FILE` for SQLite storage database", + Value: "data.sqlite", + }, + }, + Action: func(ctx *cli.Context) error { + address := ctx.String("address") + path := ctx.String("path") + logFormat := ctx.String("log-format") + logLevel := ctx.Int("log-level") + storageFile := ctx.String("storage-file") + + logger.SetFormat(logger.Format(logFormat)) + logger.SetLevel(logger.Level(logLevel)) + + cmdCtx := ctx.Context + + absPath, err := filepath.Abs(path) + if err != nil { + return errors.Wrapf(err, "could not resolve path '%s'", path) + } + + logger.Info(cmdCtx, "opening app bundle", logger.F("path", absPath)) + + bundle, err := bundle.FromPath(path) + if err != nil { + return errors.Wrapf(err, "could not open path '%s' as an app bundle", path) + } + + mux := chi.NewMux() + + mux.Use(middleware.Logger) + + bus := memory.NewBus() + + db, err := sql.Open("sqlite", storageFile) + if err != nil { + return errors.Wrapf(err, "could not open database with path '%s'", storageFile) + } + + documentStore := sqlite.NewDocumentStoreWithDB(db) + blobStore := sqlite.NewBlobStoreWithDB(db) + + handler := appHTTP.NewHandler( + appHTTP.WithBus(bus), + appHTTP.WithServerModules( + module.ContextModuleFactory(), + module.ConsoleModuleFactory(), + module.LifecycleModuleFactory(bus), + module.NetModuleFactory(bus), + module.RPCModuleFactory(bus), + module.StoreModuleFactory(documentStore), + module.BlobModuleFactory(bus, blobStore), + ), + ) + if err := handler.Load(bundle); err != nil { + return errors.Wrap(err, "could not load app bundle") + } + + mux.Handle("/*", handler) + + logger.Info(cmdCtx, "listening", logger.F("address", address)) + + if err := http.ListenAndServe(address, mux); err != nil { + return errors.WithStack(err) + } + + return nil + }, + } +} diff --git a/cmd/cli/command/main.go b/cmd/cli/command/main.go new file mode 100644 index 0000000..d65bd3e --- /dev/null +++ b/cmd/cli/command/main.go @@ -0,0 +1,48 @@ +package command + +import ( + "context" + "fmt" + "os" + "sort" + + "github.com/urfave/cli/v2" +) + +func Main(commands ...*cli.Command) { + ctx := context.Background() + + app := &cli.App{ + Name: "edge-cli", + Usage: "Arcad edge cli", + Commands: commands, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + EnvVars: []string{"DEBUG"}, + Value: false, + }, + }, + } + + app.ExitErrHandler = func(ctx *cli.Context, err error) { + if err == nil { + return + } + + debug := ctx.Bool("debug") + + if !debug { + fmt.Printf("[ERROR] %v\n", err) + } else { + fmt.Printf("%+v", err) + } + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + if err := app.RunContext(ctx, os.Args); err != nil { + os.Exit(1) + } +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..34c47a0 --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "forge.cadoles.com/arcad/edge/cmd/cli/command" + "forge.cadoles.com/arcad/edge/cmd/cli/command/app" +) + +func main() { + command.Main(app.Root()) +} diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..86425e9 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,15 @@ +# Documentation + +## Edge Apps + +Une **Edge App** est une application capable de s'exécuter dans un environnement "Edge". + +### Référence + +- [API Client](./apps/client-api/README.md) +- [API Serveur](./apps/server-api/README.md) + +### Tutoriels + +- [Créer sa première application](./apps/my-first-app.md) +- [Empaqueter une application](./apps/package-app.md) \ No newline at end of file diff --git a/doc/apps/client-api/README.md b/doc/apps/client-api/README.md new file mode 100644 index 0000000..043e3fe --- /dev/null +++ b/doc/apps/client-api/README.md @@ -0,0 +1,64 @@ +# API Client + +## Méthodes + +### `Edge.connect(): Promise` + +> `TODO` + +### `Edge.disconnect(): void` + +> `TODO` + + +### `Edge.send(message: Object): void` + +> `TODO` + + +### `Edge.rpc(method: string, params: Object): Promise` + +> `TODO` +#### Exemple + +**Côté serveur** + +```js +function onInit() { + rpc.register(echo); +} + +function echo(ctx, params) { + return params; +} +``` + +**Côté client** + +```js +Edge.connect().then(() => { + Edge.rpc("echo", { hello: "world!" }) + .then(result => console.log(result)) + .catch(err => console.error(err)); +}); +``` + +### `Edge.upload(blob: Blob, metadata: Object): Promise` + +> `TODO` + +### `Edge.blobUrl(bucketName: string, blobId: string): string` + +> `TODO` + +## Événements + +### `"message"` + +> `TODO` + +#### Exemple + +```js +Edge.addEventListener("message", evt => console.log(evt.detail)); +``` \ No newline at end of file diff --git a/doc/apps/my-first-app.md b/doc/apps/my-first-app.md new file mode 100644 index 0000000..ca38371 --- /dev/null +++ b/doc/apps/my-first-app.md @@ -0,0 +1,103 @@ +# Créer ma première application + +## 1. Télécharger le CLI + +1. Se rendre à l'adresse https://forge.cadoles.com/arcad/edge/releases + +2. Télécharger la dernière version du binaire `cli` disponible dans la page. + +## 2. Créer l'arborescence de son application + +L'arborescence d'une "Edge App" doit correspondre à une structure prédéfinie. + +```bash +my-app + |-> manifest.yml # Le fichier "manifeste" décrivant votre application + |-> public # Répertoire contenant tous les fichiers accessibles publiquement + |-> server + |-> main.js # Le point d'entrée pour le code "serveur" de votre application +``` + +## 3. Compléter le fichier `manifest.yml` + +Ce fichier est le manifeste de votre application. Il permet au serveur d'identifier celle ci et de récupérer des informations la concernant. + +```yaml +--- +# L'identifiant de votre application. Il doit être globalement unique. +# Un identifiant du type nom de domaine inversé est en général conseillé (ex: tld.mycompany.myapp) +id: tld.mycompany.myapp + +# Le titre de votre application. +title: My App + +# Les mots-clés associés à votre applications. +tags: ["chat"] + +# La description de votre application. +# Vous pouvez utiliser la syntaxe Markdown pour la mettre en forme. +description: |> + A simple demo application +``` + +## 4. Créer la page d'accueil + +Créer le fichier `my-app/public/index.html`: + +```html + + + My App + + +

My App

+ + + + + +``` + +## 5. Créer le fichier `server/main.js` + +Ce fichier est nécessaire, même vide. + +```javascript +// La fonction "onInit()" (si déclarée) est automatiquement +// exécutée au démarrage du serveur de votre application. +function onInit() { + +} + +// La fonction "onClientMessage(ctx, message)" est automatiquement +// exécutée quand le serveur de votre application reçoit un +// message en provenance du client. +function onClientMessage(ctx, message) { + console.log(message); + + // On utilise le module "net" pour renvoyer un message au client + net.send(ctx, { "my": "message" }); +} +``` + +## 6. Exécuter votre application en local + +Utiliser le CLI téléchargé préalablement pour lancer votre nouvelle application localement. + +```bash +cli app run -p ./chemin/vers/app +``` + +La page d'accueil devrait être accessible à l'adresse http://localhost:8080. \ No newline at end of file diff --git a/doc/apps/package-app.md b/doc/apps/package-app.md new file mode 100644 index 0000000..771f3e1 --- /dev/null +++ b/doc/apps/package-app.md @@ -0,0 +1,3 @@ +# Empaqueter une application + +> `TODO` \ No newline at end of file diff --git a/doc/apps/server-api/README.md b/doc/apps/server-api/README.md new file mode 100644 index 0000000..524dcb2 --- /dev/null +++ b/doc/apps/server-api/README.md @@ -0,0 +1,28 @@ +# API Serveur + +## Fonctions de rappel + +### `onInit(): void` + +Cette méthode est automatiquement exécutée au démarrage du serveur de l'application. + +Comme son nom l'indique, elle permet d'exécuter des opérations d'initialisation de votre application. + +#### Exemple + +```js +function onInit() { + +} +``` + +## Modules + +Listes des modules disponibles côté serveur. + +- [`console`](./console.md) +- [`context`](./context.md) +- [`net`](./net.md) +- [`rpc`](./rpc.md) +- [`store`](./store.md) +- [`blob`](./blob.md) \ No newline at end of file diff --git a/doc/apps/server-api/blob.md b/doc/apps/server-api/blob.md new file mode 100644 index 0000000..341d5f8 --- /dev/null +++ b/doc/apps/server-api/blob.md @@ -0,0 +1,73 @@ +# Module `blob` + +Ce module permet de manipuler des fichiers ("blobs") au sein de votre application. + +## Fonctions de rappel + +Pour permettre aux utilisateurs de téléverser/télécharger des fichiers ("blobs") dans votre application, il vous faudra déclarer 2 fonctions dans votre fichier `server/main.js`. + +### `onBlobUpload(ctx: Context, blobId: string, blobInfo: BlobInfo, metadata: Metadata)` + +> `TODO` + +#### Usage + +```js +function onBlobUpload(ctx, blobId, blobInfo, metadata) { + // Autoriser le téléversement et indiquer que le fichier doit être stocké dans le bucket "my-bucket" + return { allow: true, bucket: "my-bucket" }; +} +``` + +### `onBlobDownload(ctx: Context, bucketName: string, blobId: string)` + +> `TODO` + +#### Usage + +```js +function onBlobDownload(ctx, bucketName, blobId) { + // Autoriser le téléchargement + return { allow: true }; +} +``` + +## Méthodes + +### `blob.listBuckets(ctx: Context): string[]` + +> `TODO` + +### `blob.writeBlob(ctx: Context, bucketName: string, blobId: string)` + +> `TODO` + +### `blob.readBlob(ctx: Context, bucketName: string, blobId: string)` + +> `TODO` + +### `blob.listBlobs(ctx: Context, bucketName: string): BlobInfo[]` + +> `TODO` + +### `blob.deleteBlob(ctx: Context, bucketName: string, blobId: string)` + +> `TODO` + +### `blob.deleteBucket(ctx: Context, bucketName: string)` + +> `TODO` + +### `blob.getBlobInfo(ctx: Context, bucketName: string, blobId: string): BlobInfo` + +> `TODO` + +## Objets + +### `Context` + +Voir la documentation de l'objet [`Context`](./context.md#Context). + +### `BlobInfo` + +### `Metadata` \ No newline at end of file diff --git a/doc/apps/server-api/console.md b/doc/apps/server-api/console.md new file mode 100644 index 0000000..6ff7c57 --- /dev/null +++ b/doc/apps/server-api/console.md @@ -0,0 +1,3 @@ +## Module `console` + +> `TODO` \ No newline at end of file diff --git a/doc/apps/server-api/context.md b/doc/apps/server-api/context.md new file mode 100644 index 0000000..dde6ad3 --- /dev/null +++ b/doc/apps/server-api/context.md @@ -0,0 +1,74 @@ +# Module `context` + +Ce module permet de manipuler les informations de contexte liées à la réception de messages ou à l'utilisation de certains autres modules. + +## Méthodes + +### `context.new(): Context` + +Renvoie un nouveau contexte vide. + +#### Arguments + +Aucun +#### Valeur de retour + +Un nouvel objet de contexte. + +#### Usage + +```js +var ctx = context.new(); +``` + +### `context.get(ctx: Context, key: string): any|null` + +Récupère la valeur associée à la clé `key` dans le contexte si celle ci existe. + +#### Arguments + +- `ctx` **Context** Contexte duquel extraire la valeur souhaitée +- `key` **string** Clé associé à la valeur à récupérer + +#### Valeur de retour + +Valeur associée à la clé ou `null`. + +#### Usage + +```js +function onClientMessage(ctx, message) { + var sessionId = context.get(ctx, "mykey"); + console.log(sessionId); +} +``` + +## Propriétés + +### `context.SESSION_ID` + +Clé permettant de récupérer la clé de session associé au client émetteur du message courant. + +#### Usage + +```js +function onClientMessage(ctx, message) { + var sessionId = context.get(ctx, context.SESSION_ID); + console.log(sessionId); +} +``` + +### `context.ORIGINAL_REQUEST` + +Clé permettant de récupérer la requête HTTP à l'origine de la connexion du client. + +_Cette propriété est utilisée par le module [`auth`](./auth.md) pour récupérer l'utilisateur associé au client._ + +#### Usage + +```js +function onClientMessage(ctx, message) { + var request = context.get(ctx, context.ORIGINAL_REQUEST); + console.log(request); +} +``` \ No newline at end of file diff --git a/doc/apps/server-api/net.md b/doc/apps/server-api/net.md new file mode 100644 index 0000000..41e6f1f --- /dev/null +++ b/doc/apps/server-api/net.md @@ -0,0 +1,63 @@ +# Module `net` + +Ce module permet d'envoyer des messages aux clients connectés au serveur. + +## Fonctions de rappel + +### `onClientMessage(ctx: Context, msg: Message)` + +Cette méthode est appelée pour chaque message reçu par le serveur depuis un client connecté. + +> `TODO` + +## Méthodes + +### `net.send(ctx: string|Context, data: Object): void` + +Envoie un message au client connecté au serveur. + +#### Arguments + +- `ctx` **string|Context** Identifiant de session du client ou contexte portant l'identifiant de session du client. Voir la documentation du module [`context`](./context.md). +- `data` **Object** Données à envoyer au client + +#### Valeur de retour + +Aucune + +#### Usage + +**Côté client** + +```js +// Les données envoyées par le serveur sont accessibles +// via la propriété evt.detail. +Edge.on('message', evt => console.log(evt.detail)); + +Edge.connect(); +``` + +**Côté serveur** + +```js +function onInit() { + var ctx = context.background(); + net.send(ctx, {"foo", "bar"}); +} +``` + +### `net.broadcast(data: Object): void` + +Envoie un message à l'ensemble des clients connectés au serveur. + +#### Arguments + +- `data` **object** Données à envoyer aux clients connectés + +#### Valeur de retour + +Aucune + +#### Usage + +Voir usage `net.send()`. \ No newline at end of file diff --git a/doc/apps/server-api/rpc.md b/doc/apps/server-api/rpc.md new file mode 100644 index 0000000..3ae975e --- /dev/null +++ b/doc/apps/server-api/rpc.md @@ -0,0 +1,43 @@ +# Module `rpc` + +Ce module permet de déclarer des méthodes côté serveur qui seront "invoquable" côté client via la méthode [`Edge.rpc(method: string, params: Object): Promise`](../client-api/README.md#edgerpcmethod-string-params-object-promise). + +## Méthodes + +### `rpc.register(name: string, cb?: Function): void` + +Marque une fonction comme étant appelable par le client. +#### Arguments + +- `name` **string** Le nom de la fonction telle qu'elle sera appelable par le client. Si `cb` n'est pas spécifié, la fonction portant le même nom est utilisée. +- `cb` **Function** Référence de la fonction à exécuter. + +#### Valeur de retour + +Aucune + +#### Usage + +```js +function onInit() { + rpc.register("echo", echo); +} + +function echo(ctx, params) { + return params; +} +``` + +**Côté client** + +```js +Edge.connect().then(() => { + Edge.rpc("echo", { hello: "world!" }) + .then(result => console.log(result)) + .catch(err => console.error(err)); +}); +``` + +### `rpc.unregister(name: string): void` + +> `TODO` \ No newline at end of file diff --git a/doc/apps/server-api/store.md b/doc/apps/server-api/store.md new file mode 100644 index 0000000..ddd2d0d --- /dev/null +++ b/doc/apps/server-api/store.md @@ -0,0 +1,183 @@ +# Module `store` + +Ce module permet de stocker et récupérer des données structurées ("documents") sur le serveur. + +## Méthodes + +### `store.upsert(ctx: Context, collection: string, doc: Object)` + +Enregistre un document dans une collection. + +Si le document a une propriété `_id` celle ci est utilisée comme identifiant. Dans le cas contraire elle sera autogénérée par le moteur de stockage. + +#### Arguments + +- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md) +- `collection` **string** Nom de la collection dans laquelle retrouver le document +- `doc` **Object** Le document à enregistrer + +#### Valeur de retour + +Le document dans sa forme après enregistrement. + +#### Usage + +```js +var ctx = context.new(); +var obj = store.upsert(ctx, "myCollection", {"foo": "bar"}); +``` + +### `store.get(ctx: Context, collection: string, docId: string)` + +Retourne le document associé à l'identifiant `docId` ou `null` si celui ci n'est pas trouvé. + +#### Arguments + +- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md) +- `collection` **string** Nom de la collection dans laquelle retrouver le document +- `docId` **string** Identifiant du document à récupérer + +#### Valeur de retour + +le document stocké si il existe, `null` sinon. + +#### Usage + +```js +function onInit() { + var ctx = context.new(); + var obj = store.get(ctx, "myCollection", "doc-id"); +} +``` + +### `store.delete(ctx: Context, collection: string, docId: string)` + +Supprime le document associé à l'identifiant dans la collection. + +#### Arguments + +- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md) +- `collection` **string** Nom de la collection dans laquelle retrouver le document +- `docId` **string** Identifiant de le document à supprime + +#### Valeur de retour + +Aucune + +#### Usage + +```js +var ctx = context.new(); +store.delete(ctx, "myCollection", "my-item-id"); +``` + +### `store.query(ctx: Context, collection: string, doc: Object, filter: Filter?, options: QueryOptions?)` + +Filtre la collection et récupère les documents associés à la requête. + +#### Arguments + +- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md) +- `collection` **string** Nom de la collection dans laquelle retrouver le document +- `filter` **Filter?** Filtre à appliquer à la collection, voir "Filtres". +- `options` **QueryOptions?** Options de filtrage, voir "Options". + + +## Propriétés + +### `store.DIRECTION_ASC` + +> `TODO` +### `store.DIRECTION_DESC` + +> `TODO` + +## Objets + +### `Filter` + +Un filtre se construit à partir d'une hiérarchie d'opérateurs sous la forme d'un document. + +On distingue deux types d'opérateurs: + +- Les opérateurs de combinaison logique; +- Les opérateurs de filtrage. + +Les opérateurs d'combinaison logique prennent la forme suivante: + +``` +{ + "": [ + , + , + etc + ] +} +``` + +**Exemple** + +```json +{ + "and": [ + { "eq": { "myAttr1": "foo" } }, + { "neq": { "myAttr2": "bar" } } + ] +} +``` + +Ce filtre serait traduit en syntaxe SQL en `myAttr1 = "foo" AND myAttr2 != "bar"`. + +Voici la liste des opérateurs de combinaison logique: + +- `and` - "ET" logique +- `or` - "OU" logique +- `not` - Négation logique + +Les opérateurs de filtrage prennent la forme suivantes: + +``` +{ + : { + "key1": "val1", + "key2": "val2", + "key3": "val3", + etc + } +} +``` + +**Exemple** + +```json +{ "gt": { "foo": "bar" } }, +``` + +Voici la liste des opérateurs de filtrage: + +- `eq` - Comparaison `==` +- `neq` - Comparaison `!=` +- `gt` - Comparaison `>` +- `gte` - Comparaison `>=` +- `lt` - Comparaison `<` +- `lte` - Comparaison `<=` +- `like` - Comparaison type `LIKE` MySQL/SQLite +- `in` - Comparaison type `IN` MySQL/SQLite + +### QueryOptions + +#### Usage + +```js +var ctx = context.new(); +var results = store.query(ctx, "myCollection", { + eq: { + "foo": "bar", + } +}, { + limit: 10, + offset: 5, + orderBy: "foo", + orderDirection: store.ASC, +}); +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8b32c0d --- /dev/null +++ b/go.mod @@ -0,0 +1,57 @@ +module forge.cadoles.com/arcad/edge + +go 1.19 + +require modernc.org/sqlite v1.20.4 + +require ( + cdr.dev/slog v1.4.0 // indirect + github.com/alecthomas/chroma v0.7.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32 // indirect + github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/fatih/color v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.1 // indirect + github.com/go-chi/chi/v5 v5.0.8 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/igm/sockjs-go/v3 v3.0.2 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect + github.com/orcaman/concurrent-map v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/afero v1.9.3 // indirect + github.com/urfave/cli/v2 v2.24.3 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9 // indirect + go.opencensus.io v0.22.5 // indirect + golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/net v0.4.0 // indirect + golang.org/x/sys v0.3.0 // indirect + golang.org/x/term v0.3.0 // indirect + golang.org/x/text v0.5.0 // indirect + golang.org/x/tools v0.1.12 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.22.2 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.4.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..601c998 --- /dev/null +++ b/go.sum @@ -0,0 +1,602 @@ +cdr.dev/slog v1.4.0 h1:tLXQJ/hZ5Q051h0MBHSd2Ha8xzdXj7CjtzmG/8dUvUk= +cdr.dev/slog v1.4.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= +github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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-20230203172422-5460598cfa32 h1:audXtK7nV3y4W9ckAxRBE+eQV5Bljf5Non4NTa9kLVE= +github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= +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-20230207183254-2229640ea097 h1:WsLyDk8yHsVT1puf/32883ZxEb6Pgqd19AlQH9mxVK0= +github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097/go.mod h1:0tlktQL7yHfYEtjcRGi/eiOkbDR5XF7gyFFvbC5//E0= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= +github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= +github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/igm/sockjs-go/v3 v3.0.2 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE= +github.com/igm/sockjs-go/v3 v3.0.2/go.mod h1:UqchsOjeagIBFHvd+RZpLaVRbCwGilEC08EDHsD1jYE= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= +github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0= +github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9 h1:6JlkcdjYVQglPWYuemK2MoZAtRE4vFx85zLXflGIyI8= +gitlab.com/wpetit/goweb v0.0.0-20230206085656-dec695f0e2e9/go.mod h1:3sus4zjoUv1GB7eDLL60QaPkUnXJCWBpjvbe0jWifeY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +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/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= +modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk= +modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE= +modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/misc/client-sdk-testsuite/.gitignore b/misc/client-sdk-testsuite/.gitignore new file mode 100644 index 0000000..b2d59d1 --- /dev/null +++ b/misc/client-sdk-testsuite/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/dist \ No newline at end of file diff --git a/misc/client-sdk-testsuite/Makefile b/misc/client-sdk-testsuite/Makefile new file mode 100644 index 0000000..5bd250c --- /dev/null +++ b/misc/client-sdk-testsuite/Makefile @@ -0,0 +1,15 @@ +dist: node_modules + mkdir -p dist + cp -rf src/* dist/ + mkdir -p dist/public/vendor + cp -f node_modules/mocha/mocha.js dist/public/vendor/mocha.js + cp -f node_modules/mocha/mocha.css dist/public/vendor/mocha.css + cp -f node_modules/chai/chai.js dist/public/vendor/chai.js + +node_modules: + npm ci + +clean: + rm -rf dist node_modules + +.PHONY: dist \ No newline at end of file diff --git a/misc/client-sdk-testsuite/README.md b/misc/client-sdk-testsuite/README.md new file mode 100644 index 0000000..e5e9579 --- /dev/null +++ b/misc/client-sdk-testsuite/README.md @@ -0,0 +1 @@ +# Client SDK Test suite \ No newline at end of file diff --git a/misc/client-sdk-testsuite/package-lock.json b/misc/client-sdk-testsuite/package-lock.json new file mode 100644 index 0000000..4d7ac7c --- /dev/null +++ b/misc/client-sdk-testsuite/package-lock.json @@ -0,0 +1,2559 @@ +{ + "name": "client-sdk-testsuite", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "client-sdk-testsuite", + "version": "0.0.0", + "license": "AGPL-3.0", + "dependencies": { + "chai": "^4.2.0", + "mocha": "^6.2.2" + } + }, + "node_modules/ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array.prototype.reduce": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", + "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "engines": { + "node": "*" + } + }, + "node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "node_modules/es-abstract": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", + "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.4", + "is-array-buffer": "^3.0.1", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract/node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flat": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", + "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", + "dependencies": { + "is-buffer": "~2.0.3" + }, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", + "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", + "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.3.tgz", + "integrity": "sha512-0R/3FvjIGH3eEuG17ccFPk117XL2rWxatr81a57D+r/x2uTYZRbdZ4oVidEUMh2W2TJDa7MdAb12Lm2/qrKajg==", + "dependencies": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.4", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dependencies": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dependencies": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", + "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "dependencies": { + "array.prototype.reduce": "^1.0.5", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "engines": { + "node": "*" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dependencies": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" + }, + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array.prototype.reduce": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", + "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==" + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "requires": { + "type-detect": "^4.0.0" + } + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "es-abstract": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", + "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.4", + "is-array-buffer": "^3.0.1", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + }, + "dependencies": { + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + } + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", + "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", + "requires": { + "is-buffer": "~2.0.3" + } + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==" + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "requires": { + "define-properties": "^1.1.3" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "internal-slot": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", + "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-array-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", + "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-typed-array": "^1.1.10" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "^2.0.1" + } + }, + "loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "requires": { + "get-func-name": "^2.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" + }, + "mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.3.tgz", + "integrity": "sha512-0R/3FvjIGH3eEuG17ccFPk117XL2rWxatr81a57D+r/x2uTYZRbdZ4oVidEUMh2W2TJDa7MdAb12Lm2/qrKajg==", + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.4", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", + "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "requires": { + "array.prototype.reduce": "^1.0.5", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" + }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + } + } + } +} diff --git a/misc/client-sdk-testsuite/package.json b/misc/client-sdk-testsuite/package.json new file mode 100644 index 0000000..ac9f418 --- /dev/null +++ b/misc/client-sdk-testsuite/package.json @@ -0,0 +1,16 @@ +{ + "name": "client-sdk-testsuite", + "version": "0.0.0", + "description": "Client SDK Test suite", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": {}, + "author": "", + "license": "AGPL-3.0", + "homepage": "", + "dependencies": { + "chai": "^4.2.0", + "mocha": "^6.2.2" + } +} diff --git a/misc/client-sdk-testsuite/src/manifest.yml b/misc/client-sdk-testsuite/src/manifest.yml new file mode 100644 index 0000000..168150b --- /dev/null +++ b/misc/client-sdk-testsuite/src/manifest.yml @@ -0,0 +1,7 @@ +--- +id: edge.sdk.client.test +title: SDK Test +version: 0.0.0 +description: | + Suite de tests pour le SDK client +tags: ["test"] \ No newline at end of file diff --git a/misc/client-sdk-testsuite/src/public/index.html b/misc/client-sdk-testsuite/src/public/index.html new file mode 100644 index 0000000..f4e87d4 --- /dev/null +++ b/misc/client-sdk-testsuite/src/public/index.html @@ -0,0 +1,28 @@ + + + + + Client SDK Test suite + + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/misc/client-sdk-testsuite/src/public/test/client-sdk.js b/misc/client-sdk-testsuite/src/public/test/client-sdk.js new file mode 100644 index 0000000..b8f8335 --- /dev/null +++ b/misc/client-sdk-testsuite/src/public/test/client-sdk.js @@ -0,0 +1,148 @@ +Edge.debug = true; + +describe('Edge', function() { + + describe('#connect()', function() { + after(() => { + Edge.disconnect(); + }); + + it('should open the connection', function() { + return Edge.connect() + .then(() => { + chai.assert.isNotNull(Edge._conn); + }); + }); + }); + + describe('#disconnect()', function() { + it('should close the connection', function() { + Edge.disconnect(); + chai.assert.isNull(Edge._conn); + }); + }); + + describe('#send()', function() { + this.timeout(5000); + + before(() => { + return Edge.connect(); + }); + + after(() => { + Edge.disconnect(); + }); + + it('should send a message to the server and echo back', function(done) { + const now = new Date(); + const handler = evt => { + chai.assert.equal(evt.detail.now, now.toJSON()); + Edge.removeEventListener('message', handler); + done(); + } + + // Server should echo back message + Edge.addEventListener('message', handler); + + // Send message to server + Edge.send({ now }); + }); + }); + +}); + +describe('Remote Procedure Call', function() { + + before(() => { + return Edge.connect(); + }); + + after(() => { + Edge.disconnect(); + }); + + it('should call the remote echo() method and resolve the returned value', function() { + const foo = "bar"; + + return Edge.rpc('echo', { foo }) + .then(result => { + chai.assert.equal(result.foo, foo); + }); + }); + + it('should call the remote throwErrorFromClient() method and reject with an error', function() { + return Edge.rpc('throwErrorFromClient') + .catch(err => { + // Assert that it's an "internal" error + // See https://www.jsonrpc.org/specification#error_object + chai.assert.equal(err.code, -32603); + }); + }); + + it('should call an unregistered method and reject with an error', function() { + return Edge.rpc('unregisteredMethod') + .catch(err => { + // Assert that it's an "method not found" error + // See https://www.jsonrpc.org/specification#error_object + chai.assert.equal(err.code, -32601); + }); + }); + + + it('should call the add() method repetitively and keep count of the sent values', function() { + this.timeout(10000); + + const values = []; + for(let i = 0; i <= 1000; i++) { + values.push((Math.random() * 1000 | 0)); + } + return Edge.rpc('reset') + .then(() => { + return Promise.all(values.map(v => Edge.rpc("add", {value: v}))); + }) + .then(() => Edge.rpc('total')) + .then(remoteTotal => { + const localTotal = values.reduce((t, v) => t+v); + console.log("Remote total:", remoteTotal, "Local total:", localTotal); + chai.assert.equal(remoteTotal, localTotal) + }) + }); + +}); + +describe('File Module', function() { + + before(() => { + return Edge.connect(); + }); + + after(() => { + Edge.disconnect(); + }); + + it('should upload then download a blob', function() { + const content = JSON.stringify({"date": new Date()}); + const blob = new Blob([content], {type: "application/json"}); + + return Edge.upload(blob) + .then(upload => upload.result()) + .then(result => { + + chai.assert.isNotEmpty(result.blobId); + chai.assert.isNotEmpty(result.bucket); + + const blobUrl = Edge.blobUrl(result.bucket, result.blobId); + chai.assert.isNotEmpty(blobUrl); + + return fetch(blobUrl) + .then(res => res.text()) + .then(blobContent => { + chai.assert.equal(content, blobContent); + }); + }) + .catch(err => { + chai.assert.fail(err); + }) + }); + +}); diff --git a/misc/client-sdk-testsuite/src/server/main.js b/misc/client-sdk-testsuite/src/server/main.js new file mode 100644 index 0000000..5e4e235 --- /dev/null +++ b/misc/client-sdk-testsuite/src/server/main.js @@ -0,0 +1,63 @@ + +// Called on server initialization +function onInit() { + console.log("server started"); + + // Register RPC exposed methods + rpc.register("echo", echo); + rpc.register("throwErrorFromClient", throwError); + + rpc.register("add", add); + rpc.register("reset", reset); + rpc.register("total", total); +} + +// Called for each client message +function onClientMessage(ctx, data) { + var sessionId = context.get(ctx, context.SESSION_ID); + console.log("onClientMessage", sessionId, data.now); + net.send(ctx, { now: data.now }); +} + +// Called for each blob upload request +function onBlobUpload(ctx, blobId, blobInfo, metadata) { + console.log("onBlobUpload", blobId, blobInfo, metadata); + + if (!blobInfo.contentType == "application/json") return { allow: false }; + if (!blobInfo.filename == "blob") return { allow: false }; + + return { allow: true, bucket: "test-bucket" }; +} + +// Called for each blob download request +function onBlobDownload(ctx, bucket, blobId) { + console.log("onBlobDownload", bucket, blobId); + return { allow: true }; +} + +// RPC methods + +function echo(ctx, params) { + console.log("echoing", params); + return params; +} + +function throwError(ctx, params) { + throw new Error("oh no !"); +} + +var count = 0; + +function add(ctx, params) { + console.log("add", params); + count += params.value; + return count; +} + +function reset(ctx, params) { + count = 0; +} + +function total(ctx, params) { + return count; +} \ No newline at end of file diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..9ceaadf --- /dev/null +++ b/modd.conf @@ -0,0 +1,12 @@ +**/*.go +pkg/app/sdk/client/src/**/*.js +pkg/app/sdk/client/src/**/*.ts +misc/client-sdk-testsuite/src/**/* +modd.conf +{ + prep: make build-sdk + prep: cd misc/client-sdk-testsuite && make dist + prep: make GOTEST_ARGS="-short" test + prep: make build + daemon: bin/cli app run -p misc/client-sdk-testsuite/dist --storage-file ./sdk-testsuite.sqlite +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7a1ff25 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,235 @@ +{ + "name": "edge", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "edge", + "version": "1.0.0", + "license": "AGPL-3.0", + "dependencies": { + "@types/sockjs-client": "^1.5.1", + "sockjs-client": "^1.6.1" + } + }, + "node_modules/@types/sockjs-client": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.1.tgz", + "integrity": "sha512-bmZM6A1GPdjF0bcuIUC+50hZEMGkzMsiG9by6X9U+7IZFOiPtz7MJ9h05FSpPVxlj4i+TzzoG3ESo1FJlbLb6A==" + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "dependencies": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://tidelift.com/funding/github/npm/sockjs-client" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + } + }, + "dependencies": { + "@types/sockjs-client": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.1.tgz", + "integrity": "sha512-bmZM6A1GPdjF0bcuIUC+50hZEMGkzMsiG9by6X9U+7IZFOiPtz7MJ9h05FSpPVxlj4i+TzzoG3ESo1FJlbLb6A==" + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==" + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "requires": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + } + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3845639 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "edge", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Cadoles ", + "license": "AGPL-3.0", + "dependencies": { + "@types/sockjs-client": "^1.5.1", + "sockjs-client": "^1.6.1" + } +} diff --git a/pkg/app/app.go b/pkg/app/app.go new file mode 100644 index 0000000..699a315 --- /dev/null +++ b/pkg/app/app.go @@ -0,0 +1,16 @@ +package app + +type ID string + +type Manifest struct { + ID ID `yaml:"id"` + Version string `yaml:"version"` + Title string `yaml:"title"` + Description string `yaml:"description"` + Tags []string `yaml:"tags"` +} + +type App struct { + ID ID + Manifest *Manifest +} diff --git a/pkg/app/crypto.go b/pkg/app/crypto.go new file mode 100644 index 0000000..5fb5e51 --- /dev/null +++ b/pkg/app/crypto.go @@ -0,0 +1,25 @@ +package app + +import ( + "crypto/rand" + "encoding/binary" + + "github.com/pkg/errors" +) + +type cryptoSource struct{} + +func (s cryptoSource) Seed(seed int64) {} + +func (s cryptoSource) Int63() int64 { + return int64(s.Uint64() & ^uint64(1<<63)) +} + +func (s cryptoSource) Uint64() (v uint64) { + err := binary.Read(rand.Reader, binary.BigEndian, &v) + if err != nil { + panic(errors.Wrap(err, "could not read number for random source")) + } + + return v +} diff --git a/pkg/app/error.go b/pkg/app/error.go new file mode 100644 index 0000000..8d04ac1 --- /dev/null +++ b/pkg/app/error.go @@ -0,0 +1,5 @@ +package app + +import "github.com/pkg/errors" + +var ErrUnknownBundleArchiveFormat = errors.New("unknown bundle archive format") diff --git a/pkg/app/loader.go b/pkg/app/loader.go new file mode 100644 index 0000000..45a8e1c --- /dev/null +++ b/pkg/app/loader.go @@ -0,0 +1,101 @@ +package app + +import ( + "context" + "path/filepath" + + "gitlab.com/wpetit/goweb/logger" + "gopkg.in/yaml.v2" + + "forge.cadoles.com/arcad/edge/pkg/bundle" + "github.com/pkg/errors" +) + +type FilesystemLoader struct { + searchPatterns []string +} + +type LoadedApp struct { + App *App + Bundle bundle.Bundle +} + +func (l *FilesystemLoader) Load(ctx context.Context) ([]*LoadedApp, error) { + apps := make([]*LoadedApp, 0) + + for _, seachPattern := range l.searchPatterns { + absSearchPattern, err := filepath.Abs(seachPattern) + if err != nil { + return nil, errors.Wrapf(err, "could not generate absolute path for '%s'", seachPattern) + } + + logger.Debug(ctx, "searching apps in filesystem", logger.F("searchPattern", absSearchPattern)) + + files, err := filepath.Glob(absSearchPattern) + if err != nil { + return nil, errors.Wrapf(err, "could not search files with pattern '%s'", absSearchPattern) + } + + for _, f := range files { + loopCtx := logger.With(ctx, logger.F("file", f)) + + logger.Debug(loopCtx, "found app bundle") + + b, err := bundle.FromPath(f) + if err != nil { + logger.Error(loopCtx, "could not load bundle", logger.E(errors.WithStack(err))) + + continue + } + + logger.Debug(loopCtx, "loading app manifest") + + appManifest, err := LoadAppManifest(b) + if err != nil { + logger.Error(loopCtx, "could not load app manifest", logger.E(errors.WithStack(err))) + + continue + } + + g := &App{ + ID: appManifest.ID, + Manifest: appManifest, + } + + apps = append(apps, &LoadedApp{ + App: g, + Bundle: b, + }) + } + } + + return apps, nil +} + +func NewFilesystemLoader(searchPatterns ...string) *FilesystemLoader { + return &FilesystemLoader{ + searchPatterns: searchPatterns, + } +} + +func LoadAppManifest(b bundle.Bundle) (*Manifest, error) { + reader, _, err := b.File("manifest.yml") + if err != nil { + return nil, errors.Wrap(err, "could not read manifest.yml") + } + + defer func() { + if err := reader.Close(); err != nil { + panic(errors.WithStack(err)) + } + }() + + manifest := &Manifest{} + + decoder := yaml.NewDecoder(reader) + if err := decoder.Decode(manifest); err != nil { + return nil, errors.Wrap(err, "could not decode manifest.yml") + } + + return manifest, nil +} diff --git a/pkg/app/promise_proxy.go b/pkg/app/promise_proxy.go new file mode 100644 index 0000000..b9a1b85 --- /dev/null +++ b/pkg/app/promise_proxy.go @@ -0,0 +1,41 @@ +package app + +import ( + "sync" + + "github.com/dop251/goja" +) + +type PromiseProxy struct { + *goja.Promise + wg sync.WaitGroup + resolve func(result interface{}) + reject func(reason interface{}) +} + +func (p *PromiseProxy) Resolve(result interface{}) { + defer p.wg.Done() + p.resolve(result) +} + +func (p *PromiseProxy) Reject(reason interface{}) { + defer p.wg.Done() + p.resolve(reason) +} + +func (p *PromiseProxy) Wait() { + p.wg.Wait() +} + +func NewPromiseProxy(promise *goja.Promise, resolve func(result interface{}), reject func(reason interface{})) *PromiseProxy { + proxy := &PromiseProxy{ + Promise: promise, + wg: sync.WaitGroup{}, + resolve: resolve, + reject: reject, + } + + proxy.wg.Add(1) + + return proxy +} diff --git a/pkg/app/server.go b/pkg/app/server.go new file mode 100644 index 0000000..558156c --- /dev/null +++ b/pkg/app/server.go @@ -0,0 +1,182 @@ +package app + +import ( + "math/rand" + "sync" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/eventloop" + "github.com/pkg/errors" +) + +var ErrFuncDoesNotExist = errors.New("function does not exist") + +type Server struct { + runtime *goja.Runtime + loop *eventloop.EventLoop + modules []ServerModule +} + +func (s *Server) Load(name string, src string) error { + _, err := s.runtime.RunScript(name, src) + if err != nil { + return errors.Wrap(err, "could not run js script") + } + + return nil +} + +func (s *Server) ExecFuncByName(funcName string, args ...interface{}) (goja.Value, error) { + callable, ok := goja.AssertFunction(s.runtime.Get(funcName)) + if !ok { + return nil, errors.WithStack(ErrFuncDoesNotExist) + } + + return s.Exec(callable, args...) +} + +func (s *Server) Exec(callable goja.Callable, args ...interface{}) (goja.Value, error) { + var ( + wg sync.WaitGroup + value goja.Value + err error + ) + + wg.Add(1) + + s.loop.RunOnLoop(func(vm *goja.Runtime) { + jsArgs := make([]goja.Value, 0, len(args)) + for _, a := range args { + jsArgs = append(jsArgs, vm.ToValue(a)) + } + + value, err = callable(nil, jsArgs...) + if err != nil { + err = errors.WithStack(err) + } + + wg.Done() + }) + + wg.Wait() + + return value, err +} + +func (s *Server) IsPromise(v goja.Value) (*goja.Promise, bool) { + promise, ok := v.Export().(*goja.Promise) + return promise, ok +} + +func (s *Server) WaitForPromise(promise *goja.Promise) goja.Value { + var ( + wg sync.WaitGroup + value goja.Value + ) + + wg.Add(1) + + // Wait for promise completion + go func() { + for { + var loopWait sync.WaitGroup + loopWait.Add(1) + + breakLoop := false + + s.loop.RunOnLoop(func(vm *goja.Runtime) { + defer loopWait.Done() + + if promise.State() == goja.PromiseStatePending { + return + } + + value = promise.Result() + + breakLoop = true + }) + + loopWait.Wait() + + if breakLoop { + wg.Done() + + return + } + } + }() + + wg.Wait() + + return value +} + +func (s *Server) NewPromise() *PromiseProxy { + promise, resolve, reject := s.runtime.NewPromise() + + return NewPromiseProxy(promise, resolve, reject) +} + +func (s *Server) ToValue(v interface{}) goja.Value { + return s.runtime.ToValue(v) +} + +func (s *Server) Start() error { + s.loop.Start() + + for _, mod := range s.modules { + initMod, ok := mod.(InitializableModule) + if !ok { + continue + } + + if err := initMod.OnInit(); err != nil { + return errors.WithStack(err) + } + } + + return nil +} + +func (s *Server) Stop() { + s.loop.Stop() +} + +func (s *Server) initModules(factories ...ServerModuleFactory) { + runtime := goja.New() + + runtime.SetFieldNameMapper(goja.UncapFieldNameMapper()) + runtime.SetRandSource(createRandomSource()) + + modules := make([]ServerModule, 0, len(factories)) + + for _, moduleFactory := range factories { + mod := moduleFactory(s) + export := runtime.NewObject() + mod.Export(export) + runtime.Set(mod.Name(), export) + + modules = append(modules, mod) + } + + s.runtime = runtime + s.modules = modules +} + +func NewServer(factories ...ServerModuleFactory) *Server { + server := &Server{ + loop: eventloop.NewEventLoop( + eventloop.EnableConsole(false), + ), + } + + server.initModules(factories...) + + return server +} + +func createRandomSource() goja.RandSource { + rnd := rand.New(&cryptoSource{}) + + return rnd.Float64 +} diff --git a/pkg/app/server_module.go b/pkg/app/server_module.go new file mode 100644 index 0000000..d331bf2 --- /dev/null +++ b/pkg/app/server_module.go @@ -0,0 +1,17 @@ +package app + +import ( + "github.com/dop251/goja" +) + +type ServerModuleFactory func(*Server) ServerModule + +type ServerModule interface { + Name() string + Export(*goja.Object) +} + +type InitializableModule interface { + ServerModule + OnInit() error +} diff --git a/pkg/bundle/bundle.go b/pkg/bundle/bundle.go new file mode 100644 index 0000000..ec85940 --- /dev/null +++ b/pkg/bundle/bundle.go @@ -0,0 +1,11 @@ +package bundle + +import ( + "io" + "os" +) + +type Bundle interface { + File(string) (io.ReadCloser, os.FileInfo, error) + Dir(string) ([]os.FileInfo, error) +} diff --git a/pkg/bundle/bundle_test.go b/pkg/bundle/bundle_test.go new file mode 100644 index 0000000..694e6b3 --- /dev/null +++ b/pkg/bundle/bundle_test.go @@ -0,0 +1,70 @@ +package bundle + +import ( + "fmt" + "testing" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +func TestBundle(t *testing.T) { + t.Parallel() + + logger.SetLevel(logger.LevelDebug) + + bundles := []Bundle{ + NewDirectoryBundle("testdata/bundle"), + NewTarBundle("testdata/bundle.tar.gz"), + NewZipBundle("testdata/bundle.zip"), + } + + for _, b := range bundles { + func(b Bundle) { + t.Run(fmt.Sprintf("'%T'", b), func(t *testing.T) { + t.Parallel() + + reader, info, err := b.File("data/test/foo.txt") + if err != nil { + t.Error(err) + } + + if reader == nil { + t.Fatal("File(data/test/foo.txt): reader should not be nil") + } + + defer func() { + if err := reader.Close(); err != nil { + t.Error(errors.WithStack(err)) + } + }() + + if info == nil { + t.Error("File(data/test/foo.txt): info should not be nil") + } + + files, err := b.Dir("data") + if err != nil { + t.Error(err) + } + + if e, g := 1, len(files); e != g { + t.Errorf("len(files): expected '%v', got '%v'", e, g) + } + + files, err = b.Dir("data/test") + if err != nil { + t.Error(err) + } + + if e, g := 1, len(files); e != g { + t.Fatalf("len(files): expected '%v', got '%v'", e, g) + } + + if e, g := "foo.txt", files[0].Name(); e != g { + t.Errorf("files[0].Name(): expected '%v', got '%v'", e, g) + } + }) + }(b) + } +} diff --git a/pkg/bundle/directory_bundle.go b/pkg/bundle/directory_bundle.go new file mode 100644 index 0000000..47d0312 --- /dev/null +++ b/pkg/bundle/directory_bundle.go @@ -0,0 +1,54 @@ +package bundle + +import ( + "context" + "io" + "io/ioutil" + "os" + "path" + + "gitlab.com/wpetit/goweb/logger" + + "github.com/pkg/errors" +) + +type DirectoryBundle struct { + baseDir string +} + +func (b *DirectoryBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) { + ctx := context.Background() + + fullPath := path.Join(b.baseDir, filename) + + logger.Debug(ctx, "accessing bundle file", logger.F("file", fullPath)) + + info, err := os.Stat(fullPath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil, err + } + + return nil, nil, errors.Wrapf(err, "stat '%s'", fullPath) + } + + reader, err := os.Open(fullPath) + if err != nil { + return nil, nil, errors.Wrapf(err, "open '%s'", fullPath) + } + + return reader, info, nil +} + +func (b *DirectoryBundle) Dir(dirname string) ([]os.FileInfo, error) { + fullPath := path.Join(b.baseDir, dirname) + ctx := context.Background() + logger.Debug(ctx, "accessing bundle directory", logger.F("file", fullPath)) + return ioutil.ReadDir(fullPath) +} + +func NewDirectoryBundle(baseDir string) *DirectoryBundle { + return &DirectoryBundle{ + baseDir: baseDir, + } +} diff --git a/pkg/bundle/error.go b/pkg/bundle/error.go new file mode 100644 index 0000000..8f148f9 --- /dev/null +++ b/pkg/bundle/error.go @@ -0,0 +1,5 @@ +package bundle + +import "errors" + +var ErrUnknownBundleArchiveExt = errors.New("unknown bundle archive extension") diff --git a/pkg/bundle/filesystem.go b/pkg/bundle/filesystem.go new file mode 100644 index 0000000..776996f --- /dev/null +++ b/pkg/bundle/filesystem.go @@ -0,0 +1,101 @@ +package bundle + +import ( + "bytes" + "context" + "io/ioutil" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type FileSystem struct { + prefix string + bundle Bundle +} + +func (fs *FileSystem) Open(name string) (http.File, error) { + if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) || + strings.Contains(name, "\x00") { + return nil, errors.New("http: invalid character in file path") + } + + p := path.Join(fs.prefix, strings.TrimPrefix(name, "/")) + + ctx := logger.With( + context.Background(), + logger.F("filename", name), + ) + + logger.Debug(ctx, "opening file") + + readCloser, fileInfo, err := fs.bundle.File(p) + if err != nil { + if os.IsNotExist(err) { + return nil, err + } + + logger.Error(ctx, "could not open bundle file", logger.E(err)) + + return nil, errors.Wrapf(err, "could not open bundle file '%s'", p) + } + defer readCloser.Close() + + file := &File{ + fi: fileInfo, + } + + if fileInfo.IsDir() { + files, err := fs.bundle.Dir(p) + if err != nil { + logger.Error(ctx, "could not read bundle directory", logger.E(err)) + + return nil, errors.Wrapf(err, "could not read bundle directory '%s'", p) + } + + file.files = files + } else { + data, err := ioutil.ReadAll(readCloser) + if err != nil { + logger.Error(ctx, "could not read bundle file", logger.E(err)) + + return nil, errors.Wrapf(err, "could not read bundle file '%s'", p) + } + + file.Reader = bytes.NewReader(data) + } + + return file, nil +} + +func NewFileSystem(prefix string, bundle Bundle) *FileSystem { + return &FileSystem{prefix, bundle} +} + +type File struct { + *bytes.Reader + fi os.FileInfo + files []os.FileInfo +} + +// A noop-closer. +func (f *File) Close() error { + return nil +} + +func (f *File) Readdir(count int) ([]os.FileInfo, error) { + if f.fi.IsDir() && f.files != nil { + return f.files, nil + } + + return nil, os.ErrNotExist +} + +func (f *File) Stat() (os.FileInfo, error) { + return f.fi, nil +} diff --git a/pkg/bundle/from_path.go b/pkg/bundle/from_path.go new file mode 100644 index 0000000..89448fd --- /dev/null +++ b/pkg/bundle/from_path.go @@ -0,0 +1,60 @@ +package bundle + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +type ArchiveExt string + +const ( + ExtZip ArchiveExt = "zip" + ExtTarGz ArchiveExt = "tar.gz" +) + +func FromPath(path string) (Bundle, error) { + stat, err := os.Stat(path) + if err != nil { + return nil, errors.Wrapf(err, "could not stat file '%s'", path) + } + + var b Bundle + + if stat.IsDir() { + b = NewDirectoryBundle(path) + } else { + b, err = matchArchivePattern(path) + if err != nil { + return nil, errors.WithStack(err) + } + } + + return b, nil +} + +func matchArchivePattern(archivePath string) (Bundle, error) { + base := filepath.Base(archivePath) + + matches, err := filepath.Match(fmt.Sprintf("*.%s", ExtTarGz), base) + if err != nil { + return nil, errors.Wrapf(err, "could not match file archive '%s'", archivePath) + } + + if matches { + return NewTarBundle(archivePath), nil + } + + matches, err = filepath.Match(fmt.Sprintf("*.%s", ExtZip), base) + if err != nil { + return nil, errors.Wrapf(err, "could not match file archive '%s'", archivePath) + } + + if matches { + return NewZipBundle(archivePath), nil + } + + return nil, errors.WithStack(ErrUnknownBundleArchiveExt) +} diff --git a/pkg/bundle/tar_bundle.go b/pkg/bundle/tar_bundle.go new file mode 100644 index 0000000..e8a7ae5 --- /dev/null +++ b/pkg/bundle/tar_bundle.go @@ -0,0 +1,146 @@ +package bundle + +import ( + "archive/tar" + "compress/gzip" + "context" + "io" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type TarBundle struct { + archivePath string +} + +func (b *TarBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) { + reader, archive, err := b.openArchive() + if err != nil { + return nil, nil, err + } + + ctx := logger.With( + context.Background(), + logger.F("filename", filename), + ) + + logger.Debug(ctx, "opening file") + + for { + header, err := reader.Next() + + if err == io.EOF { + break + } + + if err != nil { + return nil, nil, errors.Wrap(err, "could not get next tar file") + } + + p := strings.TrimPrefix(strings.TrimSuffix(header.Name, "/"), "./") + + logger.Debug(ctx, "reading archive file", logger.F("path", p)) + + if filename != p { + continue + } + + if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeDir { + continue + } + + rc := &archiveFile{reader, archive} + + return rc, header.FileInfo(), nil + } + + return nil, nil, os.ErrNotExist +} + +func (b *TarBundle) Dir(dirname string) ([]os.FileInfo, error) { + reader, archive, err := b.openArchive() + if err != nil { + return nil, err + } + defer archive.Close() + + files := make([]os.FileInfo, 0) + ctx := context.Background() + + for { + header, err := reader.Next() + + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return nil, errors.Wrap(err, "could not get next tar file") + } + + if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeDir { + continue + } + + if !strings.HasPrefix(header.Name, dirname) { + continue + } + + relPath, err := filepath.Rel(dirname, header.Name) + if err != nil { + return nil, errors.Wrap(err, "could not get relative path") + } + + logger.Debug( + ctx, "checking file prefix", + logger.F("dirname", dirname), + logger.F("filename", header.Name), + logger.F("relpath", relPath), + ) + + if relPath == filepath.Base(header.Name) { + files = append(files, header.FileInfo()) + } + } + + return files, nil +} + +func (b *TarBundle) openArchive() (*tar.Reader, *os.File, error) { + f, err := os.Open(b.archivePath) + if err != nil { + return nil, nil, errors.Wrapf(err, "could not open '%v'", b.archivePath) + } + + gzf, err := gzip.NewReader(f) + if err != nil { + return nil, nil, errors.Wrapf(err, "could not decompress '%v'", b.archivePath) + } + + tr := tar.NewReader(gzf) + + return tr, f, nil +} + +func NewTarBundle(archivePath string) *TarBundle { + return &TarBundle{ + archivePath: archivePath, + } +} + +type archiveFile struct { + reader io.Reader + closer io.Closer +} + +func (f *archiveFile) Read(p []byte) (n int, err error) { + return f.reader.Read(p) +} + +func (f *archiveFile) Close() error { + return f.closer.Close() +} diff --git a/pkg/bundle/testdata/bundle.tar.gz b/pkg/bundle/testdata/bundle.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..30227ea3856bbc2046d24d084bcc8527b8a26f3d GIT binary patch literal 161 zcmb2|=3oE==C>Cexegf!I9!xbRo~z_IrE%wCTsZAGME1RT^G0(H3_)<$v5sklXU3) z=4Wp0ktzylBgdV!pp$^SO0t>43}1LGcdBeU}j(d69I4| z{8Quok&WPj8Bvm2Tmms?`H5Yzc+KHp;9v+qG6!f2gCL4IY5DnjB^4!LXRX9-oVS+_ z6GMPEJIAV?|7EN|Q$QvKcr!AIFyr_K`bJ@k1!Hj03eKXh8am@a6rt31qiY? gKmh^+OB%aT%q22-0=!w-Kt?bFAr~V9!+WrK0A4J04*&oF literal 0 HcmV?d00001 diff --git a/pkg/bundle/testdata/bundle/data/test/foo.txt b/pkg/bundle/testdata/bundle/data/test/foo.txt new file mode 100644 index 0000000..ba0e162 --- /dev/null +++ b/pkg/bundle/testdata/bundle/data/test/foo.txt @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/pkg/bundle/zip_bundle.go b/pkg/bundle/zip_bundle.go new file mode 100644 index 0000000..76d8ea0 --- /dev/null +++ b/pkg/bundle/zip_bundle.go @@ -0,0 +1,98 @@ +package bundle + +import ( + "archive/zip" + "context" + "io" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type ZipBundle struct { + archivePath string +} + +func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) { + reader, err := b.openArchive() + if err != nil { + return nil, nil, err + } + + ctx := logger.With( + context.Background(), + logger.F("filename", filename), + ) + + logger.Debug(ctx, "opening file") + + f, err := reader.Open(filename) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + stat, err := f.Stat() + if err != nil { + return nil, nil, errors.WithStack(err) + } + + return f, stat, nil +} + +func (b *ZipBundle) Dir(dirname string) ([]os.FileInfo, error) { + reader, err := b.openArchive() + if err != nil { + return nil, err + } + + defer func() { + if err := reader.Close(); err != nil { + panic(errors.WithStack(err)) + } + }() + + files := make([]os.FileInfo, 0) + ctx := context.Background() + + for _, f := range reader.File { + if !strings.HasPrefix(f.Name, dirname) { + continue + } + + relPath, err := filepath.Rel(dirname, f.Name) + if err != nil { + return nil, errors.Wrap(err, "could not get relative path") + } + + logger.Debug( + ctx, "checking file prefix", + logger.F("dirname", dirname), + logger.F("filename", f.Name), + logger.F("relpath", relPath), + ) + + if relPath == filepath.Base(f.Name) { + files = append(files, f.FileInfo()) + } + } + + return files, nil +} + +func (b *ZipBundle) openArchive() (*zip.ReadCloser, error) { + zr, err := zip.OpenReader(b.archivePath) + if err != nil { + return nil, errors.Wrapf(err, "could not decompress '%v'", b.archivePath) + } + + return zr, nil +} + +func NewZipBundle(archivePath string) *ZipBundle { + return &ZipBundle{ + archivePath: archivePath, + } +} diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go new file mode 100644 index 0000000..a02d437 --- /dev/null +++ b/pkg/bus/bus.go @@ -0,0 +1,13 @@ +package bus + +import "context" + +type Bus interface { + Subscribe(ctx context.Context, ns MessageNamespace) (<-chan Message, error) + Unsubscribe(ctx context.Context, ns MessageNamespace, ch <-chan Message) + Publish(ctx context.Context, msg Message) error + Request(ctx context.Context, msg Message) (Message, error) + Reply(ctx context.Context, ns MessageNamespace, h RequestHandler) error +} + +type RequestHandler func(msg Message) (Message, error) diff --git a/pkg/bus/error.go b/pkg/bus/error.go new file mode 100644 index 0000000..ead8d31 --- /dev/null +++ b/pkg/bus/error.go @@ -0,0 +1,9 @@ +package bus + +import "github.com/pkg/errors" + +var ( + ErrPublishTimeout = errors.New("publish timeout") + ErrUnexpectedMessage = errors.New("unexpected message") + ErrNoResponse = errors.New("no response") +) diff --git a/pkg/bus/memory/bus.go b/pkg/bus/memory/bus.go new file mode 100644 index 0000000..9ac7f3a --- /dev/null +++ b/pkg/bus/memory/bus.go @@ -0,0 +1,91 @@ +package memory + +import ( + "context" + + "forge.cadoles.com/arcad/edge/pkg/bus" + cmap "github.com/orcaman/concurrent-map" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type Bus struct { + opt *Option + dispatchers cmap.ConcurrentMap + nextRequestID uint64 +} + +func (b *Bus) Subscribe(ctx context.Context, ns bus.MessageNamespace) (<-chan bus.Message, error) { + logger.Debug( + ctx, "subscribing to messages", + logger.F("messageNamespace", ns), + ) + + dispatchers := b.getDispatchers(ns) + d := newEventDispatcher(b.opt.BufferSize) + + go d.Run() + + dispatchers.Add(d) + + return d.Out(), nil +} + +func (b *Bus) Unsubscribe(ctx context.Context, ns bus.MessageNamespace, ch <-chan bus.Message) { + logger.Debug( + ctx, "unsubscribing from messages", + logger.F("messageNamespace", ns), + ) + + dispatchers := b.getDispatchers(ns) + dispatchers.RemoveByOutChannel(ch) +} + +func (b *Bus) Publish(ctx context.Context, msg bus.Message) error { + dispatchers := b.getDispatchers(msg.MessageNamespace()) + dispatchersList := dispatchers.List() + + logger.Debug( + ctx, "publishing message", + logger.F("dispatchers", len(dispatchersList)), + logger.F("messageNamespace", msg.MessageNamespace()), + ) + + for _, d := range dispatchersList { + if err := d.In(msg); err != nil { + return errors.WithStack(err) + } + } + + return nil +} + +func (b *Bus) getDispatchers(namespace bus.MessageNamespace) *eventDispatcherSet { + strNamespace := string(namespace) + + rawDispatchers, exists := b.dispatchers.Get(strNamespace) + dispatchers, ok := rawDispatchers.(*eventDispatcherSet) + + if !exists || !ok { + dispatchers = newEventDispatcherSet() + b.dispatchers.Set(strNamespace, dispatchers) + } + + return dispatchers +} + +func NewBus(funcs ...OptionFunc) *Bus { + opt := DefaultOption() + + for _, fn := range funcs { + fn(opt) + } + + return &Bus{ + opt: opt, + dispatchers: cmap.New(), + } +} + +// Check bus implementation. +var _ bus.Bus = NewBus() diff --git a/pkg/bus/memory/bus_test.go b/pkg/bus/memory/bus_test.go new file mode 100644 index 0000000..ac79f69 --- /dev/null +++ b/pkg/bus/memory/bus_test.go @@ -0,0 +1,29 @@ +package memory + +import ( + "testing" + + busTesting "forge.cadoles.com/arcad/edge/pkg/bus/testing" +) + +func TestMemoryBus(t *testing.T) { + if testing.Short() { + t.Skip("Test disabled when -short flag is set") + } + + t.Parallel() + + t.Run("PublishSubscribe", func(t *testing.T) { + t.Parallel() + + b := NewBus() + busTesting.TestPublishSubscribe(t, b) + }) + + t.Run("RequestReply", func(t *testing.T) { + t.Parallel() + + b := NewBus() + busTesting.TestRequestReply(t, b) + }) +} diff --git a/pkg/bus/memory/event_dispatcher.go b/pkg/bus/memory/event_dispatcher.go new file mode 100644 index 0000000..fd789f8 --- /dev/null +++ b/pkg/bus/memory/event_dispatcher.go @@ -0,0 +1,117 @@ +package memory + +import ( + "context" + "sync" + "time" + + "forge.cadoles.com/arcad/edge/pkg/bus" + "gitlab.com/wpetit/goweb/logger" +) + +type eventDispatcherSet struct { + mutex sync.Mutex + items map[*eventDispatcher]struct{} +} + +func (s *eventDispatcherSet) Add(d *eventDispatcher) { + s.mutex.Lock() + defer s.mutex.Unlock() + + s.items[d] = struct{}{} +} + +func (s *eventDispatcherSet) RemoveByOutChannel(out <-chan bus.Message) { + s.mutex.Lock() + defer s.mutex.Unlock() + + for d := range s.items { + if d.IsOut(out) { + d.Close() + delete(s.items, d) + } + } +} + +func (s *eventDispatcherSet) List() []*eventDispatcher { + s.mutex.Lock() + defer s.mutex.Unlock() + + dispatchers := make([]*eventDispatcher, 0, len(s.items)) + + for d := range s.items { + dispatchers = append(dispatchers, d) + } + + return dispatchers +} + +func newEventDispatcherSet() *eventDispatcherSet { + return &eventDispatcherSet{ + items: make(map[*eventDispatcher]struct{}), + } +} + +type eventDispatcher struct { + in chan bus.Message + out chan bus.Message + mutex sync.RWMutex + closed bool +} + +func (d *eventDispatcher) Close() { + d.mutex.Lock() + defer d.mutex.Unlock() + + d.closed = true + close(d.in) +} + +func (d *eventDispatcher) In(msg bus.Message) (err error) { + d.mutex.RLock() + defer d.mutex.RUnlock() + + if d.closed { + return + } + + d.in <- msg + + return nil +} + +func (d *eventDispatcher) Out() <-chan bus.Message { + return d.out +} + +func (d *eventDispatcher) IsOut(out <-chan bus.Message) bool { + return d.out == out +} + +func (d *eventDispatcher) Run() { + ctx := context.Background() + + for { + msg, ok := <-d.in + if !ok { + close(d.out) + + return + } + + timeout := time.After(2 * time.Second) + select { + case d.out <- msg: + case <-timeout: + logger.Error(ctx, "message out chan timed out", logger.F("message", msg)) + } + } +} + +func newEventDispatcher(bufferSize int64) *eventDispatcher { + return &eventDispatcher{ + in: make(chan bus.Message, bufferSize), + out: make(chan bus.Message, bufferSize), + closed: false, + } +} diff --git a/pkg/bus/memory/option.go b/pkg/bus/memory/option.go new file mode 100644 index 0000000..e9f3eaf --- /dev/null +++ b/pkg/bus/memory/option.go @@ -0,0 +1,19 @@ +package memory + +type Option struct { + BufferSize int64 +} + +type OptionFunc func(*Option) + +func DefaultOption() *Option { + return &Option{ + BufferSize: 16, // nolint: gomnd + } +} + +func WithBufferSize(size int64) OptionFunc { + return func(o *Option) { + o.BufferSize = size + } +} diff --git a/pkg/bus/memory/request_reply.go b/pkg/bus/memory/request_reply.go new file mode 100644 index 0000000..aaa9390 --- /dev/null +++ b/pkg/bus/memory/request_reply.go @@ -0,0 +1,151 @@ +package memory + +import ( + "context" + "strconv" + "sync/atomic" + + "forge.cadoles.com/arcad/edge/pkg/bus" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +const ( + MessageNamespaceRequest bus.MessageNamespace = "reqrep/request" + MessageNamespaceReply bus.MessageNamespace = "reqrep/reply" +) + +type RequestMessage struct { + RequestID uint64 + + Message bus.Message + + ns bus.MessageNamespace +} + +func (m *RequestMessage) MessageNamespace() bus.MessageNamespace { + return m.ns +} + +type ReplyMessage struct { + RequestID uint64 + Message bus.Message + Error error + + ns bus.MessageNamespace +} + +func (m *ReplyMessage) MessageNamespace() bus.MessageNamespace { + return m.ns +} + +func (b *Bus) Request(ctx context.Context, msg bus.Message) (bus.Message, error) { + requestID := atomic.AddUint64(&b.nextRequestID, 1) + + req := &RequestMessage{ + RequestID: requestID, + Message: msg, + ns: msg.MessageNamespace(), + } + + replyNamespace := createReplyNamespace(requestID) + + replies, err := b.Subscribe(ctx, replyNamespace) + if err != nil { + return nil, errors.WithStack(err) + } + + defer func() { + b.Unsubscribe(ctx, replyNamespace, replies) + }() + + logger.Debug(ctx, "publishing request", logger.F("request", req)) + + if err := b.Publish(ctx, req); err != nil { + return nil, errors.WithStack(err) + } + + for { + select { + case <-ctx.Done(): + return nil, errors.WithStack(ctx.Err()) + + case msg, ok := <-replies: + if !ok { + return nil, errors.WithStack(bus.ErrNoResponse) + } + + reply, ok := msg.(*ReplyMessage) + if !ok { + return nil, errors.WithStack(bus.ErrUnexpectedMessage) + } + + if reply.Error != nil { + return nil, errors.WithStack(err) + } + + return reply.Message, nil + } + } +} + +type RequestHandler func(evt bus.Message) (bus.Message, error) + +func (b *Bus) Reply(ctx context.Context, msgNamespace bus.MessageNamespace, h bus.RequestHandler) error { + requests, err := b.Subscribe(ctx, msgNamespace) + if err != nil { + return errors.WithStack(err) + } + + defer func() { + b.Unsubscribe(ctx, msgNamespace, requests) + }() + + for { + select { + case <-ctx.Done(): + return errors.WithStack(ctx.Err()) + + case msg, ok := <-requests: + if !ok { + return nil + } + + request, ok := msg.(*RequestMessage) + if !ok { + return errors.WithStack(bus.ErrUnexpectedMessage) + } + + logger.Debug(ctx, "handling request", logger.F("request", request)) + + msg, err := h(request.Message) + + reply := &ReplyMessage{ + RequestID: request.RequestID, + Message: nil, + Error: nil, + + ns: createReplyNamespace(request.RequestID), + } + + if err != nil { + reply.Error = errors.WithStack(err) + } else { + reply.Message = msg + } + + logger.Debug(ctx, "publishing reply", logger.F("reply", reply)) + + if err := b.Publish(ctx, reply); err != nil { + return errors.WithStack(err) + } + } + } +} + +func createReplyNamespace(requestID uint64) bus.MessageNamespace { + return bus.NewMessageNamespace( + MessageNamespaceReply, + bus.MessageNamespace(strconv.FormatUint(requestID, 10)), + ) +} diff --git a/pkg/bus/message.go b/pkg/bus/message.go new file mode 100644 index 0000000..3a470d1 --- /dev/null +++ b/pkg/bus/message.go @@ -0,0 +1,33 @@ +package bus + +import ( + "strings" + + "github.com/pkg/errors" +) + +type ( + MessageNamespace string +) + +type Message interface { + MessageNamespace() MessageNamespace +} + +func NewMessageNamespace(namespaces ...MessageNamespace) MessageNamespace { + var sb strings.Builder + + for i, ns := range namespaces { + if i != 0 { + if _, err := sb.WriteString(":"); err != nil { + panic(errors.Wrap(err, "could not build new message namespace")) + } + } + + if _, err := sb.WriteString(string(ns)); err != nil { + panic(errors.Wrap(err, "could not build new message namespace")) + } + } + + return MessageNamespace(sb.String()) +} diff --git a/pkg/bus/testing/publish_subscribe.go b/pkg/bus/testing/publish_subscribe.go new file mode 100644 index 0000000..6db69e3 --- /dev/null +++ b/pkg/bus/testing/publish_subscribe.go @@ -0,0 +1,96 @@ +package testing + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" + + "forge.cadoles.com/arcad/edge/pkg/bus" + "github.com/pkg/errors" +) + +const ( + testNamespace bus.MessageNamespace = "testNamespace" +) + +type testMessage struct{} + +func (e *testMessage) MessageNamespace() bus.MessageNamespace { + return testNamespace +} + +func TestPublishSubscribe(t *testing.T, b bus.Bus) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + t.Log("subscribe") + + messages, err := b.Subscribe(ctx, testNamespace) + if err != nil { + t.Fatal(errors.WithStack(err)) + } + + var wg sync.WaitGroup + + wg.Add(5) + + go func() { + // 5 events should be received + t.Log("publish 0") + + if err := b.Publish(ctx, &testMessage{}); err != nil { + t.Error(errors.WithStack(err)) + } + + t.Log("publish 1") + + if err := b.Publish(ctx, &testMessage{}); err != nil { + t.Error(errors.WithStack(err)) + } + + t.Log("publish 2") + + if err := b.Publish(ctx, &testMessage{}); err != nil { + t.Error(errors.WithStack(err)) + } + + t.Log("publish 3") + + if err := b.Publish(ctx, &testMessage{}); err != nil { + t.Error(errors.WithStack(err)) + } + + t.Log("publish 4") + + if err := b.Publish(ctx, &testMessage{}); err != nil { + t.Error(errors.WithStack(err)) + } + }() + + var count int32 = 0 + + go func() { + t.Log("range for events") + + for msg := range messages { + t.Logf("received msg %d", atomic.LoadInt32(&count)) + atomic.AddInt32(&count, 1) + + if e, g := testNamespace, msg.MessageNamespace(); e != g { + t.Errorf("evt.MessageNamespace(): expected '%v', got '%v'", e, g) + } + + wg.Done() + } + }() + + wg.Wait() + + b.Unsubscribe(ctx, testNamespace, messages) + + if e, g := int32(5), count; e != g { + t.Errorf("message received count: expected '%v', got '%v'", e, g) + } +} diff --git a/pkg/bus/testing/request_reply.go b/pkg/bus/testing/request_reply.go new file mode 100644 index 0000000..22ceddd --- /dev/null +++ b/pkg/bus/testing/request_reply.go @@ -0,0 +1,110 @@ +package testing + +import ( + "context" + "sync" + "testing" + "time" + + "forge.cadoles.com/arcad/edge/pkg/bus" + "github.com/pkg/errors" +) + +const ( + testTypeReqRes bus.MessageNamespace = "testNamspaceReqRes" +) + +type testReqResMessage struct { + i int +} + +func (m *testReqResMessage) MessageNamespace() bus.MessageNamespace { + return testNamespace +} + +func TestRequestReply(t *testing.T, b bus.Bus) { + expectedRoundTrips := 256 + timeout := time.Now().Add(time.Duration(expectedRoundTrips) * time.Second) + + var ( + initWaitGroup sync.WaitGroup + resWaitGroup sync.WaitGroup + ) + + initWaitGroup.Add(1) + + go func() { + repondCtx, cancelRespond := context.WithDeadline(context.Background(), timeout) + defer cancelRespond() + + initWaitGroup.Done() + + err := b.Reply(repondCtx, testNamespace, func(msg bus.Message) (bus.Message, error) { + defer resWaitGroup.Done() + + req, ok := msg.(*testReqResMessage) + if !ok { + return nil, errors.WithStack(bus.ErrUnexpectedMessage) + } + + result := &testReqResMessage{req.i} + + // Simulate random work + time.Sleep(time.Millisecond * 100) + + t.Logf("[RES] sending res #%d", req.i) + + return result, nil + }) + if err != nil { + t.Error(err) + } + }() + + initWaitGroup.Wait() + + var reqWaitGroup sync.WaitGroup + + for i := 0; i < expectedRoundTrips; i++ { + resWaitGroup.Add(1) + reqWaitGroup.Add(1) + + go func(i int) { + defer reqWaitGroup.Done() + + requestCtx, cancelRequest := context.WithDeadline(context.Background(), timeout) + defer cancelRequest() + + req := &testReqResMessage{i} + + t.Logf("[REQ] sending req #%d", i) + + result, err := b.Request(requestCtx, req) + if err != nil { + t.Error(err) + } + + t.Logf("[REQ] received req #%d reply", i) + + if result == nil { + t.Error("result should not be nil") + + return + } + + res, ok := result.(*testReqResMessage) + if !ok { + t.Error(errors.WithStack(bus.ErrUnexpectedMessage)) + + return + } + + if e, g := req.i, res.i; e != g { + t.Errorf("res.i: expected '%v', got '%v'", e, g) + } + }(i) + } + + reqWaitGroup.Wait() + resWaitGroup.Wait() +} diff --git a/pkg/http/blob.go b/pkg/http/blob.go new file mode 100644 index 0000000..ecf31dd --- /dev/null +++ b/pkg/http/blob.go @@ -0,0 +1,281 @@ +package http + +import ( + "encoding/json" + "io" + "io/fs" + "mime/multipart" + "net/http" + "os" + "time" + + "forge.cadoles.com/arcad/edge/pkg/bus" + "forge.cadoles.com/arcad/edge/pkg/module" + "forge.cadoles.com/arcad/edge/pkg/storage" + "github.com/go-chi/chi/v5" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +const ( + errorCodeForbidden = "forbidden" + errorCodeInternalError = "internal-error" + errorCodeBadRequest = "bad-request" + errorCodeNotFound = "not-found" +) + +type uploadResponse struct { + Bucket string `json:"bucket"` + BlobID storage.BlobID `json:"blobId"` +} + +func (h *Handler) handleAppUpload(w http.ResponseWriter, r *http.Request) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + ctx := r.Context() + + r.Body = http.MaxBytesReader(w, r.Body, h.uploadMaxFileSize) + + if err := r.ParseMultipartForm(h.uploadMaxFileSize); err != nil { + logger.Error(ctx, "could not parse multipart form", logger.E(errors.WithStack(err))) + jsonError(w, http.StatusBadRequest, errorCodeBadRequest) + + return + } + + _, fileHeader, err := r.FormFile("file") + if err != nil { + logger.Error(ctx, "could not read form file", logger.E(errors.WithStack(err))) + jsonError(w, http.StatusBadRequest, errorCodeBadRequest) + + return + } + + var metadata map[string]any + + rawMetadata := r.Form.Get("metadata") + if rawMetadata != "" { + if err := json.Unmarshal([]byte(rawMetadata), &metadata); err != nil { + logger.Error(ctx, "could not parse metadata", logger.E(errors.WithStack(err))) + jsonError(w, http.StatusBadRequest, errorCodeBadRequest) + + return + } + } + + ctx = module.WithContext(ctx, map[module.ContextKey]any{ + module.ContextKeyOriginRequest: r, + }) + + requestMsg := module.NewMessageUploadRequest(ctx, fileHeader, metadata) + + reply, err := h.bus.Request(ctx, requestMsg) + if err != nil { + logger.Error(ctx, "could not retrieve file", logger.E(errors.WithStack(err))) + jsonError(w, http.StatusInternalServerError, errorCodeInternalError) + + return + } + + logger.Debug(ctx, "upload reply", logger.F("reply", reply)) + + responseMsg, ok := reply.(*module.MessageUploadResponse) + if !ok { + logger.Error( + ctx, "unexpected upload response message", + logger.F("message", reply), + ) + jsonError(w, http.StatusInternalServerError, errorCodeInternalError) + + return + } + + if !responseMsg.Allow { + jsonError(w, http.StatusForbidden, errorCodeForbidden) + + return + } + + encoder := json.NewEncoder(w) + res := &uploadResponse{ + Bucket: responseMsg.Bucket, + BlobID: responseMsg.BlobID, + } + + if err := encoder.Encode(res); err != nil { + panic(errors.Wrap(err, "could not encode upload response")) + } +} + +func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + bucket := chi.URLParam(r, "bucket") + blobID := chi.URLParam(r, "blobID") + + ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket)) + ctx = module.WithContext(ctx, map[module.ContextKey]any{ + module.ContextKeyOriginRequest: r, + }) + + requestMsg := module.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID)) + + reply, err := h.bus.Request(ctx, requestMsg) + if err != nil { + logger.Error(ctx, "could not retrieve file", logger.E(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + replyMsg, ok := reply.(*module.MessageDownloadResponse) + if !ok { + logger.Error( + ctx, "unexpected download response message", + logger.E(errors.WithStack(bus.ErrUnexpectedMessage)), + logger.F("message", reply), + ) + jsonError(w, http.StatusInternalServerError, errorCodeInternalError) + + return + } + + if !replyMsg.Allow { + jsonError(w, http.StatusForbidden, errorCodeForbidden) + + return + } + + if replyMsg.Blob == nil { + jsonError(w, http.StatusNotFound, errorCodeNotFound) + + return + } + + defer func() { + if err := replyMsg.Blob.Close(); err != nil { + logger.Error(ctx, "could not close blob", logger.E(errors.WithStack(err))) + } + }() + + http.ServeContent(w, r, string(replyMsg.BlobInfo.ID()), replyMsg.BlobInfo.ModTime(), replyMsg.Blob) +} + +func serveFile(w http.ResponseWriter, r *http.Request, fs fs.FS, path string) { + ctx := logger.With(r.Context(), logger.F("path", path)) + + file, err := fs.Open(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + + return + } + + logger.Error(ctx, "error while opening fs file", logger.E(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + defer func() { + if err := file.Close(); err != nil { + logger.Error(ctx, "error while closing fs file", logger.E(errors.WithStack(err))) + } + }() + + info, err := file.Stat() + if err != nil { + logger.Error(ctx, "error while retrieving fs file stat", logger.E(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + reader, ok := file.(io.ReadSeeker) + if !ok { + return + } + + http.ServeContent(w, r, path, info.ModTime(), reader) +} + +type jsonErrorResponse struct { + Error jsonErr `json:"error"` +} + +type jsonErr struct { + Code string `json:"code"` +} + +func jsonError(w http.ResponseWriter, status int, code string) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(status) + + encoder := json.NewEncoder(w) + response := jsonErrorResponse{ + Error: jsonErr{ + Code: code, + }, + } + + if err := encoder.Encode(response); err != nil { + panic(errors.WithStack(err)) + } +} + +type uploadedFile struct { + multipart.File + header *multipart.FileHeader + modTime time.Time +} + +// Stat implements fs.File +func (f *uploadedFile) Stat() (fs.FileInfo, error) { + return &uploadedFileInfo{ + header: f.header, + modTime: f.modTime, + }, nil +} + +type uploadedFileInfo struct { + header *multipart.FileHeader + modTime time.Time +} + +// IsDir implements fs.FileInfo +func (i *uploadedFileInfo) IsDir() bool { + return false +} + +// ModTime implements fs.FileInfo +func (i *uploadedFileInfo) ModTime() time.Time { + return i.modTime +} + +// Mode implements fs.FileInfo +func (i *uploadedFileInfo) Mode() fs.FileMode { + return os.ModePerm +} + +// Name implements fs.FileInfo +func (i *uploadedFileInfo) Name() string { + return i.header.Filename +} + +// Size implements fs.FileInfo +func (i *uploadedFileInfo) Size() int64 { + return i.header.Size +} + +// Sys implements fs.FileInfo +func (i *uploadedFileInfo) Sys() any { + return nil +} + +var ( + _ fs.File = &uploadedFile{} + _ fs.FileInfo = &uploadedFileInfo{} +) diff --git a/pkg/http/client.go b/pkg/http/client.go new file mode 100644 index 0000000..96dd0b5 --- /dev/null +++ b/pkg/http/client.go @@ -0,0 +1,22 @@ +package http + +import ( + "net/http" + + "forge.cadoles.com/arcad/edge/pkg/sdk" +) + +func (h *Handler) handleSDKClient(w http.ResponseWriter, r *http.Request) { + serveFile(w, r, &sdk.FS, "client/dist/client.js") +} + +func (h *Handler) handleSDKClientMap(w http.ResponseWriter, r *http.Request) { + serveFile(w, r, &sdk.FS, "client/dist/client.js.map") +} + +func (h *Handler) handleAppFiles(w http.ResponseWriter, r *http.Request) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + h.public.ServeHTTP(w, r) +} diff --git a/pkg/http/handler.go b/pkg/http/handler.go new file mode 100644 index 0000000..0051d1f --- /dev/null +++ b/pkg/http/handler.go @@ -0,0 +1,114 @@ +package http + +import ( + "io/ioutil" + "net/http" + "sync" + + "forge.cadoles.com/arcad/edge/pkg/app" + "forge.cadoles.com/arcad/edge/pkg/bundle" + "forge.cadoles.com/arcad/edge/pkg/bus" + "github.com/go-chi/chi/v5" + "github.com/igm/sockjs-go/v3/sockjs" + "github.com/pkg/errors" +) + +const ( + sockJSPathPrefix = "/edge/sock" + serverMainScript = "server/main.js" +) + +type Handler struct { + bundle bundle.Bundle + public http.Handler + router chi.Router + + sockjs http.Handler + bus bus.Bus + sockjsOpts sockjs.Options + uploadMaxFileSize int64 + + server *app.Server + serverModuleFactories []app.ServerModuleFactory + + mutex sync.RWMutex +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.router.ServeHTTP(w, r) +} + +func (h *Handler) Load(bdle bundle.Bundle) error { + h.mutex.Lock() + defer h.mutex.Unlock() + + file, _, err := bdle.File(serverMainScript) + if err != nil { + return errors.Wrap(err, "could not open server main script") + } + + mainScript, err := ioutil.ReadAll(file) + if err != nil { + return errors.Wrap(err, "could not read server main script") + } + + server := app.NewServer(h.serverModuleFactories...) + + if err := server.Load(serverMainScript, string(mainScript)); err != nil { + return errors.WithStack(err) + } + + fs := bundle.NewFileSystem("public", bdle) + public := http.FileServer(fs) + sockjs := sockjs.NewHandler(sockJSPathPrefix, h.sockjsOpts, h.handleSockJSSession) + + if h.server != nil { + h.server.Stop() + } + + if err := server.Start(); err != nil { + return errors.WithStack(err) + } + + h.bundle = bdle + h.server = server + h.public = public + h.sockjs = sockjs + + return nil +} + +func NewHandler(funcs ...HandlerOptionFunc) *Handler { + opts := defaultHandlerOptions() + for _, fn := range funcs { + fn(opts) + } + + router := chi.NewRouter() + + handler := &Handler{ + uploadMaxFileSize: opts.UploadMaxFileSize, + sockjsOpts: opts.SockJS, + router: router, + serverModuleFactories: opts.ServerModuleFactories, + bus: opts.Bus, + } + + router.Route("/edge", func(r chi.Router) { + r.Route("/sdk", func(r chi.Router) { + r.Get("/client.js", handler.handleSDKClient) + r.Get("/client.js.map", handler.handleSDKClientMap) + }) + + r.Route("/api/v1", func(r chi.Router) { + r.Post("/upload", handler.handleAppUpload) + r.Get("/download/{bucket}/{blobID}", handler.handleAppDownload) + }) + + r.HandleFunc("/sock/*", handler.handleSockJS) + }) + + router.Get("/*", handler.handleAppFiles) + + return handler +} diff --git a/pkg/http/options.go b/pkg/http/options.go new file mode 100644 index 0000000..c136f5a --- /dev/null +++ b/pkg/http/options.go @@ -0,0 +1,57 @@ +package http + +import ( + "time" + + "forge.cadoles.com/arcad/edge/pkg/app" + "forge.cadoles.com/arcad/edge/pkg/bus" + "forge.cadoles.com/arcad/edge/pkg/bus/memory" + "github.com/igm/sockjs-go/v3/sockjs" +) + +type HandlerOptions struct { + Bus bus.Bus + SockJS sockjs.Options + ServerModuleFactories []app.ServerModuleFactory + UploadMaxFileSize int64 +} + +func defaultHandlerOptions() *HandlerOptions { + sockjsOptions := func() sockjs.Options { + return sockjs.DefaultOptions + }() + sockjsOptions.DisconnectDelay = 10 * time.Second + + return &HandlerOptions{ + Bus: memory.NewBus(), + SockJS: sockjsOptions, + ServerModuleFactories: make([]app.ServerModuleFactory, 0), + UploadMaxFileSize: 1024 * 10, // 10Mb + } +} + +type HandlerOptionFunc func(*HandlerOptions) + +func WithServerModules(factories ...app.ServerModuleFactory) HandlerOptionFunc { + return func(opts *HandlerOptions) { + opts.ServerModuleFactories = factories + } +} + +func WithSockJS(options sockjs.Options) HandlerOptionFunc { + return func(opts *HandlerOptions) { + opts.SockJS = options + } +} + +func WithBus(bus bus.Bus) HandlerOptionFunc { + return func(opts *HandlerOptions) { + opts.Bus = bus + } +} + +func WithUploadMaxFileSize(size int64) HandlerOptionFunc { + return func(opts *HandlerOptions) { + opts.UploadMaxFileSize = size + } +} diff --git a/pkg/http/sockjs.go b/pkg/http/sockjs.go new file mode 100644 index 0000000..3a29d35 --- /dev/null +++ b/pkg/http/sockjs.go @@ -0,0 +1,233 @@ +package http + +import ( + "context" + "encoding/json" + "net/http" + + "forge.cadoles.com/arcad/edge/pkg/module" + "github.com/igm/sockjs-go/v3/sockjs" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +const ( + statusChannelClosed = iota +) + +func (h *Handler) handleSockJS(w http.ResponseWriter, r *http.Request) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + h.sockjs.ServeHTTP(w, r) +} + +func (h *Handler) handleSockJSSession(sess sockjs.Session) { + ctx := logger.With(sess.Request().Context(), + logger.F("sessionID", sess.ID()), + ) + + logger.Debug(ctx, "new sockjs session") + + defer func() { + if sess.GetSessionState() == sockjs.SessionActive { + if err := sess.Close(statusChannelClosed, "channel closed"); err != nil { + logger.Error(ctx, "could not close sockjs session", logger.E(errors.WithStack(err))) + } + } + }() + + go h.handleServerMessages(ctx, sess) + h.handleClientMessages(ctx, sess) +} + +func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session) { + messages, err := h.bus.Subscribe(ctx, module.MessageNamespaceServer) + if err != nil { + panic(errors.WithStack(err)) + } + + defer func() { + // Close messages subscriber + h.bus.Unsubscribe(ctx, module.MessageNamespaceServer, messages) + + logger.Debug(ctx, "unsubscribed") + + if sess.GetSessionState() != sockjs.SessionActive { + return + } + + if err := sess.Close(statusChannelClosed, "channel closed"); err != nil { + logger.Error(ctx, "could not close sockjs session", logger.E(errors.WithStack(err))) + } + }() + + for { + select { + case <-ctx.Done(): + return + + case msg := <-messages: + serverMessage, ok := msg.(*module.ServerMessage) + if !ok { + logger.Error( + ctx, + "unexpected server message", + logger.F("message", msg), + ) + + continue + } + + sessionID := module.ContextValue[string](serverMessage.Context, module.ContextKeySessionID) + + isDest := sessionID == "" || sessionID == sess.ID() + if !isDest { + continue + } + + payload, err := json.Marshal(serverMessage.Data) + if err != nil { + logger.Error( + ctx, + "could not encode message", + logger.E(err), + ) + + continue + } + + message := NewWebsocketMessage( + WebsocketMessageTypeMessage, + json.RawMessage(payload), + ) + + data, err := json.Marshal(message) + if err != nil { + logger.Error( + ctx, + "could not encode message", + logger.E(err), + ) + + continue + } + + logger.Debug(ctx, "sending message") + + // Send message + if err := sess.Send(string(data)); err != nil { + logger.Error( + ctx, + "could not send message", + logger.E(err), + ) + } + } + } +} + +func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session) { + for { + select { + case <-ctx.Done(): + logger.Debug(ctx, "context done") + + return + + default: + logger.Debug(ctx, "waiting for websocket data") + + data, err := sess.RecvCtx(ctx) + if err != nil { + if errors.Is(err, sockjs.ErrSessionNotOpen) { + break + } + + logger.Error( + ctx, + "could not read message", + logger.E(errors.WithStack(err)), + ) + + break + } + + logger.Debug(ctx, "websocket data received", logger.F("data", data)) + + message := &WebsocketMessage{} + if err := json.Unmarshal([]byte(data), message); err != nil { + logger.Error( + ctx, + "could not decode message", + logger.E(errors.WithStack(err)), + ) + + break + } + + switch { + + case message.Type == WebsocketMessageTypeMessage: + var payload map[string]interface{} + if err := json.Unmarshal(message.Payload, &payload); err != nil { + logger.Error( + ctx, + "could not decode payload", + logger.E(errors.WithStack(err)), + ) + + return + } + + ctx := logger.With(ctx, logger.F("payload", payload)) + ctx = module.WithContext(ctx, map[module.ContextKey]any{ + module.ContextKeySessionID: sess.ID(), + module.ContextKeyOriginRequest: sess.Request(), + }) + + clientMessage := module.NewClientMessage(ctx, payload) + + logger.Debug(ctx, "publishing new client message", logger.F("message", clientMessage)) + + if err := h.bus.Publish(ctx, clientMessage); err != nil { + logger.Error(ctx, "could not publish message", + logger.E(errors.WithStack(err)), + logger.F("message", clientMessage), + ) + + return + } + + logger.Debug(ctx, "new client message published", logger.F("message", clientMessage)) + + default: + logger.Error( + ctx, + "unsupported message type", + logger.F("messageType", message.Type), + ) + } + } + } +} + +const ( + WebsocketMessageTypeMessage = "message" +) + +type WebsocketMessage struct { + Type string `json:"t"` + Payload json.RawMessage `json:"p"` +} + +type WebsocketMessagePayload struct { + Data map[string]interface{} `json:"d"` +} + +func NewWebsocketMessage(dataType string, payload json.RawMessage) *WebsocketMessage { + return &WebsocketMessage{ + Type: dataType, + Payload: payload, + } +} diff --git a/pkg/module/.gitignore b/pkg/module/.gitignore new file mode 100644 index 0000000..6a91a43 --- /dev/null +++ b/pkg/module/.gitignore @@ -0,0 +1 @@ +*.sqlite \ No newline at end of file diff --git a/pkg/module/assert.go b/pkg/module/assert.go new file mode 100644 index 0000000..26236fe --- /dev/null +++ b/pkg/module/assert.go @@ -0,0 +1,28 @@ +package module + +import ( + "context" + "fmt" + + "github.com/dop251/goja" +) + +func assertType[T any](v goja.Value, rt *goja.Runtime) T { + if c, ok := v.Export().(T); ok { + return c + } + + panic(rt.NewTypeError(fmt.Sprintf("expected value to be a '%T', got '%T'", new(T), v.Export()))) +} + +func assertContext(v goja.Value, r *goja.Runtime) context.Context { + return assertType[context.Context](v, r) +} + +func assertObject(v goja.Value, r *goja.Runtime) map[string]any { + return assertType[map[string]any](v, r) +} + +func assertString(v goja.Value, r *goja.Runtime) string { + return assertType[string](v, r) +} diff --git a/pkg/module/authorization.go b/pkg/module/authorization.go new file mode 100644 index 0000000..4bc2650 --- /dev/null +++ b/pkg/module/authorization.go @@ -0,0 +1,109 @@ +package module + +// import ( +// "context" +// "sync" + +// "forge.cadoles.com/arcad/edge/pkg/app" +// "forge.cadoles.com/arcad/edge/pkg/bus" +// "forge.cadoles.com/arcad/edge/pkg/repository" +// "github.com/dop251/goja" +// "github.com/pkg/errors" +// "gitlab.com/wpetit/goweb/logger" +// ) + +// type AuthorizationModule struct { +// appID app.ID +// bus bus.Bus +// backend *app.Server +// admins sync.Map +// } + +// func (m *AuthorizationModule) Name() string { +// return "authorization" +// } + +// func (m *AuthorizationModule) Export(export *goja.Object) { +// if err := export.Set("isAdmin", m.isAdmin); err != nil { +// panic(errors.Wrap(err, "could not set 'register' function")) +// } +// } + +// func (m *AuthorizationModule) isAdmin(call goja.FunctionCall) goja.Value { +// userID := call.Argument(0).String() +// if userID == "" { +// panic(errors.New("first argument must be a user id")) +// } + +// rawValue, exists := m.admins.Load(repository.UserID(userID)) +// if !exists { +// return m.backend.ToValue(false) +// } + +// isAdmin, ok := rawValue.(bool) +// if !ok { +// return m.backend.ToValue(false) +// } + +// return m.backend.ToValue(isAdmin) +// } + +// func (m *AuthorizationModule) handleEvents() { +// ctx := logger.With(context.Background(), logger.F("moduleAppID", m.appID)) + +// ns := AppMessageNamespace(m.appID) + +// userConnectedMessages, err := m.bus.Subscribe(ctx, ns, MessageTypeUserConnected) +// if err != nil { +// panic(errors.WithStack(err)) +// } + +// userDisconnectedMessages, err := m.bus.Subscribe(ctx, ns, MessageTypeUserDisconnected) +// if err != nil { +// panic(errors.WithStack(err)) +// } + +// defer func() { +// m.bus.Unsubscribe(ctx, ns, MessageTypeUserConnected, userConnectedMessages) +// m.bus.Unsubscribe(ctx, ns, MessageTypeUserDisconnected, userDisconnectedMessages) +// }() + +// for { +// select { +// case msg := <-userConnectedMessages: +// userConnectedMsg, ok := msg.(*MessageUserConnected) +// if !ok { +// continue +// } + +// logger.Debug(ctx, "user connected", logger.F("msg", userConnectedMsg)) + +// m.admins.Store(userConnectedMsg.UserID, userConnectedMsg.IsAdmin) + +// case msg := <-userDisconnectedMessages: +// userDisconnectedMsg, ok := msg.(*MessageUserDisconnected) +// if !ok { +// continue +// } + +// logger.Debug(ctx, "user disconnected", logger.F("msg", userDisconnectedMsg)) + +// m.admins.Delete(userDisconnectedMsg.UserID) +// } +// } +// } + +// func AuthorizationModuleFactory(b bus.Bus) app.ServerModuleFactory { +// return func(appID app.ID, backend *app.Server) app.ServerModule { +// mod := &AuthorizationModule{ +// appID: appID, +// bus: b, +// backend: backend, +// admins: sync.Map{}, +// } + +// go mod.handleEvents() + +// return mod +// } +// } diff --git a/pkg/module/authorization_test.go b/pkg/module/authorization_test.go new file mode 100644 index 0000000..bd3990b --- /dev/null +++ b/pkg/module/authorization_test.go @@ -0,0 +1,103 @@ +package module + +// import ( +// "context" +// "io/ioutil" +// "testing" +// "time" + +// "forge.cadoles.com/arcad/edge/pkg/app" +// "forge.cadoles.com/arcad/edge/pkg/bus/memory" +// ) + +// func TestAuthorizationModule(t *testing.T) { +// t.Parallel() + +// testAppID := app.ID("test-app") + +// b := memory.NewBus() + +// backend := app.NewServer(testAppID, +// ConsoleModuleFactory(), +// AuthorizationModuleFactory(b), +// ) + +// data, err := ioutil.ReadFile("testdata/authorization.js") +// if err != nil { +// t.Fatal(err) +// } + +// if err := backend.Load(string(data)); err != nil { +// t.Fatal(err) +// } + +// backend.Start() +// defer backend.Stop() + +// if err := backend.OnInit(); err != nil { +// t.Error(err) +// } + +// // Test non connected user + +// retValue, err := backend.ExecFuncByName("isAdmin", testUserID) +// if err != nil { +// t.Error(err) +// } + +// isAdmin := retValue.ToBoolean() + +// if e, g := false, isAdmin; e != g { +// t.Errorf("isAdmin: expected '%v', got '%v'", e, g) +// } + +// // Test user connection as normal user + +// ctx := context.Background() + +// b.Publish(ctx, NewMessageUserConnected(testAppID, testUserID, false)) +// time.Sleep(2 * time.Second) + +// retValue, err = backend.ExecFuncByName("isAdmin", testUserID) +// if err != nil { +// t.Error(err) +// } + +// isAdmin = retValue.ToBoolean() + +// if e, g := false, isAdmin; e != g { +// t.Errorf("isAdmin: expected '%v', got '%v'", e, g) +// } + +// // Test user connection as admin + +// b.Publish(ctx, NewMessageUserConnected(testAppID, testUserID, true)) +// time.Sleep(2 * time.Second) + +// retValue, err = backend.ExecFuncByName("isAdmin", testUserID) +// if err != nil { +// t.Error(err) +// } + +// isAdmin = retValue.ToBoolean() + +// if e, g := true, isAdmin; e != g { +// t.Errorf("isAdmin: expected '%v', got '%v'", e, g) +// } + +// // Test user disconnection + +// b.Publish(ctx, NewMessageUserDisconnected(testAppID, testUserID)) +// time.Sleep(2 * time.Second) + +// retValue, err = backend.ExecFuncByName("isAdmin", testUserID) +// if err != nil { +// t.Error(err) +// } + +// isAdmin = retValue.ToBoolean() + +// if e, g := false, isAdmin; e != g { +// t.Errorf("isAdmin: expected '%v', got '%v'", e, g) +// } +// } diff --git a/pkg/module/blob.go b/pkg/module/blob.go new file mode 100644 index 0000000..b52cc90 --- /dev/null +++ b/pkg/module/blob.go @@ -0,0 +1,282 @@ +package module + +import ( + "context" + "io" + "mime/multipart" + + "forge.cadoles.com/arcad/edge/pkg/app" + "forge.cadoles.com/arcad/edge/pkg/bus" + "forge.cadoles.com/arcad/edge/pkg/storage" + "github.com/dop251/goja" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +const ( + DefaultBlobBucket string = "default" +) + +type BlobModule struct { + server *app.Server + bus bus.Bus + store storage.BlobStore +} + +func (m *BlobModule) Name() string { + return "blob" +} + +func (m *BlobModule) Export(export *goja.Object) { +} + +func (m *BlobModule) handleMessages() { + ctx := context.Background() + + go func() { + err := m.bus.Reply(ctx, MessageNamespaceUploadRequest, func(msg bus.Message) (bus.Message, error) { + uploadRequest, ok := msg.(*MessageUploadRequest) + if !ok { + return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message upload request, got '%T'", msg) + } + + res, err := m.handleUploadRequest(uploadRequest) + if err != nil { + logger.Error(ctx, "could not handle upload request", logger.E(errors.WithStack(err))) + + return nil, errors.WithStack(err) + } + + logger.Debug(ctx, "upload request response", logger.F("response", res)) + + return res, nil + }) + if err != nil { + panic(errors.WithStack(err)) + } + }() + + err := m.bus.Reply(ctx, MessageNamespaceDownloadRequest, func(msg bus.Message) (bus.Message, error) { + downloadRequest, ok := msg.(*MessageDownloadRequest) + if !ok { + return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message download request, got '%T'", msg) + } + + res, err := m.handleDownloadRequest(downloadRequest) + if err != nil { + logger.Error(ctx, "could not handle download request", logger.E(errors.WithStack(err))) + + return nil, errors.WithStack(err) + } + + return res, nil + }) + if err != nil { + panic(errors.WithStack(err)) + } +} + +func (m *BlobModule) handleUploadRequest(req *MessageUploadRequest) (*MessageUploadResponse, error) { + blobID := storage.NewBlobID() + res := NewMessageUploadResponse(req.RequestID) + + ctx := logger.With(req.Context, logger.F("blobID", blobID)) + + blobInfo := map[string]interface{}{ + "size": req.FileHeader.Size, + "filename": req.FileHeader.Filename, + "contentType": req.FileHeader.Header.Get("Content-Type"), + } + + rawResult, err := m.server.ExecFuncByName("onBlobUpload", ctx, blobID, blobInfo, req.Metadata) + if err != nil { + if errors.Is(err, app.ErrFuncDoesNotExist) { + res.Allow = false + + return res, nil + } + + return nil, errors.WithStack(err) + } + + result, ok := rawResult.Export().(map[string]interface{}) + if !ok { + return nil, errors.Errorf( + "unexpected onBlobUpload result: expected 'map[string]interface{}', got '%T'", + rawResult.Export(), + ) + } + + var allow bool + + rawAllow, exists := result["allow"] + if !exists { + allow = false + } else { + allow, ok = rawAllow.(bool) + if !ok { + return nil, errors.Errorf("invalid 'allow' result property: got type '%T', expected type '%T'", rawAllow, false) + } + } + + res.Allow = allow + + if res.Allow { + bucket := DefaultBlobBucket + + rawBucket, exists := result["bucket"] + if exists { + bucket, ok = rawBucket.(string) + if !ok { + return nil, errors.Errorf("invalid 'bucket' result property: got type '%T', expected type '%T'", bucket, "") + } + } + + if err := m.saveBlob(ctx, bucket, blobID, *req.FileHeader); err != nil { + return nil, errors.WithStack(err) + } + + res.Bucket = bucket + res.BlobID = blobID + } + + return res, nil +} + +func (m *BlobModule) saveBlob(ctx context.Context, bucketName string, blobID storage.BlobID, fileHeader multipart.FileHeader) error { + file, err := fileHeader.Open() + if err != nil { + return errors.WithStack(err) + } + + defer func() { + if err := file.Close(); err != nil { + logger.Error(ctx, "could not close file", logger.E(errors.WithStack(err))) + } + }() + + bucket, err := m.store.OpenBucket(ctx, bucketName) + if err != nil { + return errors.WithStack(err) + } + + defer func() { + if err := bucket.Close(); err != nil { + logger.Error(ctx, "could not close bucket", logger.E(errors.WithStack(err))) + } + }() + + writer, err := bucket.NewWriter(ctx, blobID) + if err != nil { + return errors.WithStack(err) + } + + defer func() { + if err := file.Close(); err != nil { + logger.Error(ctx, "could not close file", logger.E(errors.WithStack(err))) + } + }() + + defer func() { + if err := writer.Close(); err != nil { + logger.Error(ctx, "could not close writer", logger.E(errors.WithStack(err))) + } + }() + + if _, err := io.Copy(writer, file); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (m *BlobModule) handleDownloadRequest(req *MessageDownloadRequest) (*MessageDownloadResponse, error) { + res := NewMessageDownloadResponse(req.RequestID) + + rawResult, err := m.server.ExecFuncByName("onBlobDownload", req.Context, req.Bucket, req.BlobID) + if err != nil { + if errors.Is(err, app.ErrFuncDoesNotExist) { + res.Allow = false + + return res, nil + } + + return nil, errors.WithStack(err) + } + + result, ok := rawResult.Export().(map[string]interface{}) + if !ok { + return nil, errors.Errorf( + "unexpected onBlobDownload result: expected 'map[string]interface{}', got '%T'", + rawResult.Export(), + ) + } + + var allow bool + + rawAllow, exists := result["allow"] + if !exists { + allow = false + } else { + allow, ok = rawAllow.(bool) + if !ok { + return nil, errors.Errorf("invalid 'allow' result property: got type '%T', expected type '%T'", rawAllow, false) + } + } + + res.Allow = allow + + reader, info, err := m.openBlob(req.Context, req.Bucket, req.BlobID) + if err != nil && !errors.Is(err, storage.ErrBlobNotFound) { + return nil, errors.WithStack(err) + } + + if reader != nil { + res.Blob = reader + } + + if info != nil { + res.BlobInfo = info + } + + return res, nil +} + +func (m *BlobModule) openBlob(ctx context.Context, bucketName string, blobID storage.BlobID) (io.ReadSeekCloser, storage.BlobInfo, error) { + bucket, err := m.store.OpenBucket(ctx, bucketName) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + defer func() { + if err := bucket.Close(); err != nil { + logger.Error(ctx, "could not close bucket", logger.E(errors.WithStack(err)), logger.F("bucket", bucket)) + } + }() + + info, err := bucket.Get(ctx, blobID) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + reader, err := bucket.NewReader(ctx, blobID) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + return reader, info, nil +} + +func BlobModuleFactory(bus bus.Bus, store storage.BlobStore) app.ServerModuleFactory { + return func(server *app.Server) app.ServerModule { + mod := &BlobModule{ + store: store, + bus: bus, + server: server, + } + + go mod.handleMessages() + + return mod + } +} diff --git a/pkg/module/blob_message.go b/pkg/module/blob_message.go new file mode 100644 index 0000000..d0db084 --- /dev/null +++ b/pkg/module/blob_message.go @@ -0,0 +1,92 @@ +package module + +import ( + "context" + "io" + "mime/multipart" + + "forge.cadoles.com/arcad/edge/pkg/bus" + "forge.cadoles.com/arcad/edge/pkg/storage" + "github.com/oklog/ulid/v2" +) + +const ( + MessageNamespaceUploadRequest bus.MessageNamespace = "uploadRequest" + MessageNamespaceUploadResponse bus.MessageNamespace = "uploadResponse" + MessageNamespaceDownloadRequest bus.MessageNamespace = "downloadRequest" + MessageNamespaceDownloadResponse bus.MessageNamespace = "downloadResponse" +) + +type MessageUploadRequest struct { + Context context.Context + RequestID string + FileHeader *multipart.FileHeader + Metadata map[string]interface{} +} + +func (m *MessageUploadRequest) MessageNamespace() bus.MessageNamespace { + return MessageNamespaceUploadRequest +} + +func NewMessageUploadRequest(ctx context.Context, fileHeader *multipart.FileHeader, metadata map[string]interface{}) *MessageUploadRequest { + return &MessageUploadRequest{ + Context: ctx, + RequestID: ulid.Make().String(), + FileHeader: fileHeader, + Metadata: metadata, + } +} + +type MessageUploadResponse struct { + RequestID string + BlobID storage.BlobID + Bucket string + Allow bool +} + +func (m *MessageUploadResponse) MessageNamespace() bus.MessageNamespace { + return MessageNamespaceDownloadResponse +} + +func NewMessageUploadResponse(requestID string) *MessageUploadResponse { + return &MessageUploadResponse{ + RequestID: requestID, + } +} + +type MessageDownloadRequest struct { + Context context.Context + RequestID string + Bucket string + BlobID storage.BlobID +} + +func (m *MessageDownloadRequest) MessageNamespace() bus.MessageNamespace { + return MessageNamespaceDownloadRequest +} + +func NewMessageDownloadRequest(ctx context.Context, bucket string, blobID storage.BlobID) *MessageDownloadRequest { + return &MessageDownloadRequest{ + Context: ctx, + RequestID: ulid.Make().String(), + Bucket: bucket, + BlobID: blobID, + } +} + +type MessageDownloadResponse struct { + RequestID string + Allow bool + BlobInfo storage.BlobInfo + Blob io.ReadSeekCloser +} + +func (m *MessageDownloadResponse) MessageNamespace() bus.MessageNamespace { + return MessageNamespaceDownloadResponse +} + +func NewMessageDownloadResponse(requestID string) *MessageDownloadResponse { + return &MessageDownloadResponse{ + RequestID: requestID, + } +} diff --git a/pkg/module/console.go b/pkg/module/console.go new file mode 100644 index 0000000..73c4b0b --- /dev/null +++ b/pkg/module/console.go @@ -0,0 +1,51 @@ +package module + +import ( + "context" + "fmt" + "strings" + + "gitlab.com/wpetit/goweb/logger" + + "forge.cadoles.com/arcad/edge/pkg/app" + "github.com/dop251/goja" + "github.com/pkg/errors" +) + +type ConsoleModule struct{} + +func (m *ConsoleModule) Name() string { + return "console" +} + +func (m *ConsoleModule) log(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + var sb strings.Builder + + fields := make([]logger.Field, 0) + + stack := rt.CaptureCallStack(0, nil) + if len(stack) > 1 { + fields = append(fields, logger.F("source", stack[1].Position().String())) + } + + for _, arg := range call.Arguments { + sb.WriteString(fmt.Sprintf("%+v", arg.Export())) + sb.WriteString(" ") + } + + logger.Debug(context.Background(), sb.String(), fields...) + + return nil +} + +func (m *ConsoleModule) Export(export *goja.Object) { + if err := export.Set("log", m.log); err != nil { + panic(errors.Wrap(err, "could not set 'log' function")) + } +} + +func ConsoleModuleFactory() app.ServerModuleFactory { + return func(server *app.Server) app.ServerModule { + return &ConsoleModule{} + } +} diff --git a/pkg/module/context.go b/pkg/module/context.go new file mode 100644 index 0000000..52ee3d9 --- /dev/null +++ b/pkg/module/context.go @@ -0,0 +1,94 @@ +package module + +import ( + "context" + + "forge.cadoles.com/arcad/edge/pkg/app" + "github.com/dop251/goja" + "github.com/pkg/errors" +) + +type ContextKey string + +const ( + ContextKeySessionID ContextKey = "sessionId" + ContextKeyOriginRequest ContextKey = "originRequest" +) + +type ContextModule struct{} + +func (m *ContextModule) Name() string { + return "context" +} + +func (m *ContextModule) new(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + return rt.ToValue(context.Background()) +} + +func (m *ContextModule) with(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + ctx := assertContext(call.Argument(0), rt) + rawValues := assertObject(call.Argument(1), rt) + + values := make(map[ContextKey]any) + for k, v := range rawValues { + values[ContextKey(k)] = v + } + + ctx = WithContext(ctx, values) + + return rt.ToValue(ctx) +} + +func (m *ContextModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + ctx := assertContext(call.Argument(0), rt) + rawKey := assertString(call.Argument(1), rt) + + value := ctx.Value(ContextKey(rawKey)) + + return rt.ToValue(value) +} + +func (m *ContextModule) Export(export *goja.Object) { + if err := export.Set("new", m.new); err != nil { + panic(errors.Wrap(err, "could not set 'new' function")) + } + + if err := export.Set("get", m.get); err != nil { + panic(errors.Wrap(err, "could not set 'get' function")) + } + + if err := export.Set("with", m.with); err != nil { + panic(errors.Wrap(err, "could not set 'with' function")) + } + + if err := export.Set("ORIGIN_REQUEST", string(ContextKeyOriginRequest)); err != nil { + panic(errors.Wrap(err, "could not set 'ORIGIN_REQUEST' property")) + } + + if err := export.Set("SESSION_ID", string(ContextKeySessionID)); err != nil { + panic(errors.Wrap(err, "could not set 'SESSION_ID' property")) + } +} + +func ContextModuleFactory() app.ServerModuleFactory { + return func(server *app.Server) app.ServerModule { + return &ContextModule{} + } +} + +func ContextValue[T any](ctx context.Context, key ContextKey) T { + value, ok := ctx.Value(key).(T) + if !ok { + return *new(T) + } + + return value +} + +func WithContext(ctx context.Context, values map[ContextKey]any) context.Context { + for k, v := range values { + ctx = context.WithValue(ctx, k, v) + } + + return ctx +} diff --git a/pkg/module/error.go b/pkg/module/error.go new file mode 100644 index 0000000..431c009 --- /dev/null +++ b/pkg/module/error.go @@ -0,0 +1,5 @@ +package module + +import "github.com/pkg/errors" + +var ErrUnexpectedArgumentsNumber = errors.New("unexpected number of arguments") diff --git a/pkg/module/lifecycle.go b/pkg/module/lifecycle.go new file mode 100644 index 0000000..0e9da6c --- /dev/null +++ b/pkg/module/lifecycle.go @@ -0,0 +1,121 @@ +package module + +import ( + "context" + + "forge.cadoles.com/arcad/edge/pkg/app" + "forge.cadoles.com/arcad/edge/pkg/bus" + "github.com/dop251/goja" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type LifecycleModule struct { + server *app.Server + bus bus.Bus +} + +func (m *LifecycleModule) Name() string { + return "lifecycle" +} + +func (m *LifecycleModule) Export(export *goja.Object) { +} + +func (m *LifecycleModule) OnInit() error { + if _, err := m.server.ExecFuncByName("onInit"); err != nil { + if errors.Is(err, app.ErrFuncDoesNotExist) { + logger.Warn(context.Background(), "could not find onInit() function", logger.E(errors.WithStack(err))) + + return nil + } + + return errors.WithStack(err) + } + + return nil +} + +func (m *LifecycleModule) handleMessages() { + ctx := context.Background() + + logger.Debug( + ctx, + "subscribing to bus messages", + ) + + clientMessages, err := m.bus.Subscribe(ctx, MessageNamespaceClient) + if err != nil { + panic(errors.WithStack(err)) + } + + defer func() { + logger.Debug( + ctx, + "unsubscribing from bus messages", + ) + + m.bus.Unsubscribe(ctx, MessageNamespaceClient, clientMessages) + }() + + for { + logger.Debug( + ctx, + "waiting for next message", + ) + select { + case <-ctx.Done(): + logger.Debug( + ctx, + "context done", + ) + + return + + case msg := <-clientMessages: + clientMessage, ok := msg.(*ClientMessage) + if !ok { + logger.Error( + ctx, + "unexpected message type", + logger.F("message", msg), + ) + + continue + } + + logger.Debug( + ctx, + "received client message", + logger.F("message", clientMessage), + ) + + if _, err := m.server.ExecFuncByName("onClientMessage", clientMessage.Context, clientMessage.Data); err != nil { + if errors.Is(err, app.ErrFuncDoesNotExist) { + continue + } + + logger.Error( + ctx, + "on client message error", + logger.E(err), + ) + } + } + } +} + +func LifecycleModuleFactory(bus bus.Bus) app.ServerModuleFactory { + return func(server *app.Server) app.ServerModule { + module := &LifecycleModule{ + server: server, + bus: bus, + } + + go module.handleMessages() + + return module + } +} + +var _ app.InitializableModule = &LifecycleModule{} diff --git a/pkg/module/message.go b/pkg/module/message.go new file mode 100644 index 0000000..728d03d --- /dev/null +++ b/pkg/module/message.go @@ -0,0 +1,38 @@ +package module + +import ( + "context" + + "forge.cadoles.com/arcad/edge/pkg/bus" +) + +const ( + MessageNamespaceClient bus.MessageNamespace = "client" + MessageNamespaceServer bus.MessageNamespace = "server" +) + +type ServerMessage struct { + Context context.Context + Data interface{} +} + +func (m *ServerMessage) MessageNamespace() bus.MessageNamespace { + return MessageNamespaceServer +} + +func NewServerMessage(ctx context.Context, data interface{}) *ServerMessage { + return &ServerMessage{ctx, data} +} + +type ClientMessage struct { + Context context.Context + Data map[string]interface{} +} + +func (m *ClientMessage) MessageNamespace() bus.MessageNamespace { + return MessageNamespaceClient +} + +func NewClientMessage(ctx context.Context, data map[string]interface{}) *ClientMessage { + return &ClientMessage{ctx, data} +} diff --git a/pkg/module/net.go b/pkg/module/net.go new file mode 100644 index 0000000..15510af --- /dev/null +++ b/pkg/module/net.go @@ -0,0 +1,81 @@ +package module + +import ( + "context" + + "forge.cadoles.com/arcad/edge/pkg/app" + "forge.cadoles.com/arcad/edge/pkg/bus" + "github.com/dop251/goja" + "github.com/pkg/errors" +) + +type NetModule struct { + server *app.Server + bus bus.Bus +} + +func (m *NetModule) Name() string { + return "net" +} + +func (m *NetModule) Export(export *goja.Object) { + if err := export.Set("broadcast", m.broadcast); err != nil { + panic(errors.Wrap(err, "could not set 'broadcast' function")) + } + + if err := export.Set("send", m.send); err != nil { + panic(errors.Wrap(err, "could not set 'send' function")) + } +} + +func (m *NetModule) broadcast(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 1 { + panic(m.server.ToValue("invalid number of argument")) + } + + data := call.Argument(0).Export() + + msg := NewServerMessage(nil, data) + if err := m.bus.Publish(context.Background(), msg); err != nil { + panic(errors.WithStack(err)) + } + + return nil +} + +func (m *NetModule) send(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + if len(call.Arguments) < 2 { + panic(m.server.ToValue("invalid number of argument")) + } + + var ctx context.Context + + firstArg := call.Argument(0) + + sessionID, ok := firstArg.Export().(string) + if ok { + ctx = WithContext(context.Background(), map[ContextKey]any{ + ContextKeySessionID: sessionID, + }) + } else { + ctx = assertContext(firstArg, rt) + } + + data := call.Argument(1).Export() + + msg := NewServerMessage(ctx, data) + if err := m.bus.Publish(ctx, msg); err != nil { + panic(errors.WithStack(err)) + } + + return nil +} + +func NetModuleFactory(bus bus.Bus) app.ServerModuleFactory { + return func(server *app.Server) app.ServerModule { + return &NetModule{ + server: server, + bus: bus, + } + } +} diff --git a/pkg/module/rpc.go b/pkg/module/rpc.go new file mode 100644 index 0000000..4033834 --- /dev/null +++ b/pkg/module/rpc.go @@ -0,0 +1,263 @@ +package module + +import ( + "context" + "fmt" + "sync" + + "forge.cadoles.com/arcad/edge/pkg/app" + "forge.cadoles.com/arcad/edge/pkg/bus" + "github.com/dop251/goja" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type RPCRequest struct { + Method string + Params interface{} + ID interface{} +} + +type RPCError struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +type RPCResponse struct { + Result interface{} + Error *RPCError + ID interface{} +} + +type RPCModule struct { + server *app.Server + bus bus.Bus + callbacks sync.Map +} + +func (m *RPCModule) Name() string { + return "rpc" +} + +func (m *RPCModule) Export(export *goja.Object) { + if err := export.Set("register", m.register); err != nil { + panic(errors.Wrap(err, "could not set 'register' function")) + } + + if err := export.Set("unregister", m.unregister); err != nil { + panic(errors.Wrap(err, "could not set 'unregister' function")) + } +} + +func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + fnName := assertString(call.Argument(0), rt) + + var ( + callable goja.Callable + ok bool + ) + + if len(call.Arguments) > 1 { + callable, ok = goja.AssertFunction(call.Argument(1)) + } else { + callable, ok = goja.AssertFunction(rt.Get(fnName)) + } + + if !ok { + panic(rt.NewTypeError("method should be a valid function")) + } + + ctx := context.Background() + + logger.Debug(ctx, "registering method", logger.F("method", fnName)) + + m.callbacks.Store(fnName, callable) + + return nil +} + +func (m *RPCModule) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + fnName := assertString(call.Argument(0), rt) + + m.callbacks.Delete(fnName) + + return nil +} + +func (m *RPCModule) handleMessages() { + ctx := context.Background() + + clientMessages, err := m.bus.Subscribe(ctx, MessageNamespaceClient) + if err != nil { + panic(errors.WithStack(err)) + } + + defer func() { + m.bus.Unsubscribe(ctx, MessageNamespaceClient, clientMessages) + }() + + sendRes := func(ctx context.Context, req *RPCRequest, result goja.Value) { + res := &RPCResponse{ + ID: req.ID, + Result: result.Export(), + } + + logger.Debug(ctx, "sending rpc response", logger.F("response", res)) + + if err := m.sendResponse(ctx, res); err != nil { + logger.Error( + ctx, "could not send response", + logger.E(errors.WithStack(err)), + logger.F("response", res), + logger.F("request", req), + ) + } + } + + for msg := range clientMessages { + clientMessage, ok := msg.(*ClientMessage) + if !ok { + logger.Warn(ctx, "unexpected bus message", logger.F("message", msg)) + + continue + } + + ok, req := m.isRPCRequest(clientMessage) + if !ok { + continue + } + + logger.Debug(ctx, "received rpc request", logger.F("request", req)) + + rawCallable, exists := m.callbacks.Load(req.Method) + if !exists { + logger.Debug(ctx, "method not found", logger.F("req", req)) + + if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil { + logger.Error( + ctx, "could not send method not found response", + logger.E(errors.WithStack(err)), + logger.F("request", req), + ) + } + + continue + } + + callable, ok := rawCallable.(goja.Callable) + if !ok { + logger.Debug(ctx, "invalid method", logger.F("req", req)) + + if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil { + logger.Error( + ctx, "could not send method not found response", + logger.E(errors.WithStack(err)), + logger.F("request", req), + ) + } + + continue + } + + result, err := m.server.Exec(callable, ctx, req.Params) + if err != nil { + if err := m.sendErrorResponse(clientMessage.Context, req, err); err != nil { + logger.Error( + ctx, "could not send error response", + logger.E(errors.WithStack(err)), + logger.F("originalError", err), + logger.F("request", req), + ) + } + + continue + } + + promise, ok := m.server.IsPromise(result) + if ok { + go func(ctx context.Context, req *RPCRequest, promise *goja.Promise) { + result := m.server.WaitForPromise(promise) + sendRes(ctx, req, result) + }(clientMessage.Context, req, promise) + } else { + sendRes(clientMessage.Context, req, result) + } + } +} + +func (m *RPCModule) sendErrorResponse(ctx context.Context, req *RPCRequest, err error) error { + return m.sendResponse(ctx, &RPCResponse{ + ID: req.ID, + Result: nil, + Error: &RPCError{ + Code: -32603, + Message: err.Error(), + }, + }) +} + +func (m *RPCModule) sendMethodNotFoundResponse(ctx context.Context, req *RPCRequest) error { + return m.sendResponse(ctx, &RPCResponse{ + ID: req.ID, + Result: nil, + Error: &RPCError{ + Code: -32601, + Message: fmt.Sprintf("method not found"), + }, + }) +} + +func (m *RPCModule) sendResponse(ctx context.Context, res *RPCResponse) error { + msg := NewServerMessage(ctx, map[string]interface{}{ + "jsonrpc": "2.0", + "id": res.ID, + "error": res.Error, + "result": res.Result, + }) + + if err := m.bus.Publish(ctx, msg); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (m *RPCModule) isRPCRequest(msg *ClientMessage) (bool, *RPCRequest) { + jsonRPC, exists := msg.Data["jsonrpc"] + if !exists || jsonRPC != "2.0" { + return false, nil + } + + rawMethod, exists := msg.Data["method"] + if !exists { + return false, nil + } + + method, ok := rawMethod.(string) + if !ok { + return false, nil + } + + id := msg.Data["id"] + params := msg.Data["params"] + + return true, &RPCRequest{ + ID: id, + Method: method, + Params: params, + } +} + +func RPCModuleFactory(bus bus.Bus) app.ServerModuleFactory { + return func(server *app.Server) app.ServerModule { + mod := &RPCModule{ + server: server, + bus: bus, + } + + go mod.handleMessages() + + return mod + } +} diff --git a/pkg/module/store.go b/pkg/module/store.go new file mode 100644 index 0000000..ad07753 --- /dev/null +++ b/pkg/module/store.go @@ -0,0 +1,205 @@ +package module + +import ( + "fmt" + + "forge.cadoles.com/arcad/edge/pkg/app" + "forge.cadoles.com/arcad/edge/pkg/storage" + "forge.cadoles.com/arcad/edge/pkg/storage/filter" + "github.com/dop251/goja" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +type StoreModule struct { + server *app.Server + store storage.DocumentStore +} + +func (m *StoreModule) Name() string { + return "store" +} + +func (m *StoreModule) Export(export *goja.Object) { + if err := export.Set("upsert", m.upsert); err != nil { + panic(errors.Wrap(err, "could not set 'upsert' function")) + } + + if err := export.Set("get", m.get); err != nil { + panic(errors.Wrap(err, "could not set 'get' function")) + } + + if err := export.Set("query", m.query); err != nil { + panic(errors.Wrap(err, "could not set 'query' function")) + } + + if err := export.Set("delete", m.delete); err != nil { + panic(errors.Wrap(err, "could not set 'delete' function")) + } + + if err := export.Set("DIRECTION_ASC", storage.OrderDirectionAsc); err != nil { + panic(errors.Wrap(err, "could not set 'DIRECTION_ASC' property")) + } + + if err := export.Set("DIRECTION_DESC", storage.OrderDirectionDesc); err != nil { + panic(errors.Wrap(err, "could not set 'DIRECTION_DESC' property")) + } +} + +func (m *StoreModule) upsert(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + ctx := assertContext(call.Argument(0), rt) + collection := m.assertCollection(call.Argument(1), rt) + document := m.assertDocument(call.Argument(2), rt) + + document, err := m.store.Upsert(ctx, collection, document) + if err != nil { + panic(errors.Wrapf(err, "error while upserting document in collection '%s'", collection)) + } + + return rt.ToValue(map[string]interface{}(document)) +} + +func (m *StoreModule) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + ctx := assertContext(call.Argument(0), rt) + collection := m.assertCollection(call.Argument(1), rt) + documentID := m.assertDocumentID(call.Argument(2), rt) + + document, err := m.store.Get(ctx, collection, documentID) + if err != nil { + if errors.Is(err, storage.ErrDocumentNotFound) { + return nil + } + + panic(errors.Wrapf(err, "error while getting document '%s' in collection '%s'", documentID, collection)) + } + + return rt.ToValue(map[string]interface{}(document)) +} + +type queryOptions struct { + Limit *int `mapstructure:"limit"` + Offset *int `mapstructure:"offset"` + OrderBy *string `mapstructure:"orderBy"` + OrderDirection *string `mapstructure:"orderDirection"` +} + +func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + ctx := assertContext(call.Argument(0), rt) + collection := m.assertCollection(call.Argument(1), rt) + filter := m.assertFilter(call.Argument(2), rt) + queryOptions := m.assertQueryOptions(call.Argument(3), rt) + + queryOptionsFuncs := make([]storage.QueryOptionFunc, 0) + + if queryOptions.Limit != nil { + queryOptionsFuncs = append(queryOptionsFuncs, storage.WithLimit(*queryOptions.Limit)) + } + + if queryOptions.OrderBy != nil { + queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderBy(*queryOptions.OrderBy)) + } + + if queryOptions.Offset != nil { + queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOffset(*queryOptions.Limit)) + } + + if queryOptions.OrderDirection != nil { + queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderDirection( + storage.OrderDirection(*queryOptions.OrderDirection), + )) + } + + documents, err := m.store.Query(ctx, collection, filter, queryOptionsFuncs...) + if err != nil { + panic(errors.Wrapf(err, "error while querying documents in collection '%s'", collection)) + } + + rawDocuments := make([]map[string]interface{}, len(documents)) + for idx, doc := range documents { + rawDocuments[idx] = map[string]interface{}(doc) + } + + return rt.ToValue(rawDocuments) +} + +func (m *StoreModule) delete(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + ctx := assertContext(call.Argument(0), rt) + collection := m.assertCollection(call.Argument(1), rt) + documentID := m.assertDocumentID(call.Argument(2), rt) + + if err := m.store.Delete(ctx, collection, documentID); err != nil { + panic(errors.Wrapf(err, "error while deleting document '%s' in collection '%s'", documentID, collection)) + } + + return nil +} + +func (m *StoreModule) assertCollection(value goja.Value, rt *goja.Runtime) string { + collection, ok := value.Export().(string) + if !ok { + panic(rt.NewTypeError(fmt.Sprintf("collection must be a string, got '%T'", value.Export()))) + } + + return collection +} + +func (m *StoreModule) assertFilter(value goja.Value, rt *goja.Runtime) *filter.Filter { + rawFilter, ok := value.Export().(map[string]interface{}) + if !ok { + panic(rt.NewTypeError(fmt.Sprintf("filter must be an object, got '%T'", value.Export()))) + } + + filter, err := filter.NewFrom(rawFilter) + if err != nil { + panic(errors.Wrap(err, "could not convert object to filter")) + } + + return filter +} + +func (m *StoreModule) assertDocumentID(value goja.Value, rt *goja.Runtime) storage.DocumentID { + documentID, ok := value.Export().(storage.DocumentID) + if !ok { + rawDocumentID, ok := value.Export().(string) + if !ok { + panic(rt.NewTypeError(fmt.Sprintf("document id must be a documentid or a string, got '%T'", value.Export()))) + } + + documentID = storage.DocumentID(rawDocumentID) + } + + return documentID +} + +func (m *StoreModule) assertQueryOptions(value goja.Value, rt *goja.Runtime) *queryOptions { + rawQueryOptions, ok := value.Export().(map[string]interface{}) + if !ok { + panic(rt.NewTypeError(fmt.Sprintf("query options must be an object, got '%T'", value.Export()))) + } + + queryOptions := &queryOptions{} + + if err := mapstructure.Decode(rawQueryOptions, queryOptions); err != nil { + panic(errors.Wrap(err, "could not convert object to query options")) + } + + return queryOptions +} + +func (m *StoreModule) assertDocument(value goja.Value, rt *goja.Runtime) storage.Document { + document, ok := value.Export().(map[string]interface{}) + if !ok { + panic(rt.NewTypeError("document must be an object")) + } + + return document +} + +func StoreModuleFactory(store storage.DocumentStore) app.ServerModuleFactory { + return func(server *app.Server) app.ServerModule { + return &StoreModule{ + server: server, + store: store, + } + } +} diff --git a/pkg/module/store_test.go b/pkg/module/store_test.go new file mode 100644 index 0000000..8cb8984 --- /dev/null +++ b/pkg/module/store_test.go @@ -0,0 +1,41 @@ +package module + +import ( + "io/ioutil" + "testing" + + "forge.cadoles.com/arcad/edge/pkg/app" + "forge.cadoles.com/arcad/edge/pkg/storage/sqlite" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +func TestStoreModule(t *testing.T) { + logger.SetLevel(logger.LevelDebug) + + store := sqlite.NewDocumentStore(":memory:") + server := app.NewServer( + ContextModuleFactory(), + ConsoleModuleFactory(), + StoreModuleFactory(store), + ) + + data, err := ioutil.ReadFile("testdata/store.js") + if err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + if err := server.Load("testdata/store.js", string(data)); err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + if err := server.Start(); err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + if _, err := server.ExecFuncByName("testStore"); err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + server.Stop() +} diff --git a/pkg/module/testdata/store.js b/pkg/module/testdata/store.js new file mode 100644 index 0000000..0670685 --- /dev/null +++ b/pkg/module/testdata/store.js @@ -0,0 +1,32 @@ +function testStore() { + var ctx = context.new() + + var obj = store.upsert(ctx, "test", {"foo": "bar"}); + var obj1 = store.get(ctx, "test", obj._id); + + console.log(obj, obj1); + + for (var key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + + if (obj[key].toString() !== obj1[key].toString()) { + throw new Error("obj['"+key+"'] !== obj1['"+key+"']"); + } + } + + var results = store.query(ctx, "test", { "eq": {"foo": "bar"} }, {"orderBy": "foo", "limit": 10, "skip": 0}); + + if (!results || results.length !== 1) { + throw new Error("results should contains 1 item"); + } + + store.delete(ctx, "test", obj._id); + + var obj2 = store.get(ctx, "test", obj._id); + + if (obj2 != null) { + throw new Error("obj2 should be null"); + } +} \ No newline at end of file diff --git a/pkg/module/user.go b/pkg/module/user.go new file mode 100644 index 0000000..f3aab3e --- /dev/null +++ b/pkg/module/user.go @@ -0,0 +1,59 @@ +package module + +// import ( +// "context" + +// "github.com/dop251/goja" +// "github.com/pkg/errors" +// "forge.cadoles.com/arcad/edge/pkg/app" +// "forge.cadoles.com/arcad/edge/pkg/repository" +// "gitlab.com/wpetit/goweb/logger" +// ) + +// type UserModule struct { +// appID app.ID +// repo repository.UserRepository +// backend *app.Server +// ctx context.Context +// } + +// func (m *UserModule) Name() string { +// return "user" +// } + +// func (m *UserModule) Export(export *goja.Object) { +// if err := export.Set("getUserById", m.getUserByID); err != nil { +// panic(errors.Wrap(err, "could not set 'getUserById' function")) +// } +// } + +// func (m *UserModule) getUserByID(call goja.FunctionCall) goja.Value { +// if len(call.Arguments) != 1 { +// panic(m.backend.ToValue("invalid number of arguments")) +// } + +// userID := repository.UserID(call.Arguments[0].String()) + +// user, err := m.repo.Get(userID) +// if err != nil { +// err = errors.Wrapf(err, "could not find user '%s'", userID) +// logger.Error(m.ctx, "could not find user", logger.E(err), logger.F("userID", userID)) +// panic(m.backend.ToValue(err)) +// } + +// return m.backend.ToValue(user) +// } + +// func UserModuleFactory(repo repository.UserRepository) app.ServerModuleFactory { +// return func(appID app.ID, backend *app.Server) app.ServerModule { +// return &UserModule{ +// appID: appID, +// repo: repo, +// backend: backend, +// ctx: logger.With( +// context.Background(), +// logger.F("appID", appID), +// ), +// } +// } +// } diff --git a/pkg/module/user_test.go b/pkg/module/user_test.go new file mode 100644 index 0000000..aa60974 --- /dev/null +++ b/pkg/module/user_test.go @@ -0,0 +1,70 @@ +package module + +// import ( +// "errors" +// "io/ioutil" +// "testing" + +// "gitlab.com/arcadbox/arcad/internal/app" +// "gitlab.com/arcadbox/arcad/internal/repository" +// ) + +// func TestUserModuleGetUserByID(t *testing.T) { +// repo := &fakeUserRepository{} + +// appID := app.ID("test") +// backend := app.NewServer(appID, +// ConsoleModuleFactory(), +// UserModuleFactory(repo), +// ) + +// data, err := ioutil.ReadFile("testdata/user_getbyid.js") +// if err != nil { +// t.Fatal(err) +// } + +// if err := backend.Load(string(data)); err != nil { +// t.Fatal(err) +// } + +// backend.Start() +// defer backend.Stop() + +// if err := backend.OnInit(); err != nil { +// t.Error(err) +// } +// } + +// type fakeUserRepository struct{} + +// func (r *fakeUserRepository) Create() (*repository.User, error) { +// return nil, errors.New("not implemented") +// } + +// func (r *fakeUserRepository) Save(user *repository.User) error { +// return errors.New("not implemented") +// } + +// func (r *fakeUserRepository) Get(userID repository.UserID) (*repository.User, error) { +// if userID == "0" { +// return &repository.User{}, nil +// } + +// return nil, errors.New("not implemented") +// } + +// func (r *fakeUserRepository) Delete(userID repository.UserID) error { +// return errors.New("not implemented") +// } + +// func (r *fakeUserRepository) Touch(userID repository.UserID, rawUserAgent string) error { +// return errors.New("not implemented") +// } + +// func (r *fakeUserRepository) List() ([]*repository.User, error) { +// return nil, errors.New("not implemented") +// } + +// func (r *fakeUserRepository) ListByID(userIDs ...repository.UserID) ([]*repository.User, error) { +// return nil, errors.New("not implemented") +// } diff --git a/pkg/sdk/client/src/client.ts b/pkg/sdk/client/src/client.ts new file mode 100644 index 0000000..01bd7a3 --- /dev/null +++ b/pkg/sdk/client/src/client.ts @@ -0,0 +1,255 @@ +import { EventTarget } from "./event-target"; +import { messageFrom,Message, TypeMessage } from "./message"; +import { RPCError } from "./rpc-error"; +import SockJS from 'sockjs-client'; + +const EventTypeMessage = "message"; + +export class Client extends EventTarget { + + _conn: any + _rpcID: number + _pendingRPC: {} + _queue: Message[] + _reconnectionDelay: number + _autoReconnect: boolean + debug: boolean + + constructor(autoReconnect = true) { + super(); + this._conn = null; + this._onConnectionClose = this._onConnectionClose.bind(this); + this._onConnectionMessage = this._onConnectionMessage.bind(this); + this._handleRPCResponse = this._handleRPCResponse.bind(this); + this._rpcID = 0; + this._pendingRPC = {}; + this._queue = []; + this._reconnectionDelay = 250; + this._autoReconnect = autoReconnect; + + this.debug = false; + + this.connect = this.connect.bind(this); + this.disconnect = this.disconnect.bind(this); + this.rpc = this.rpc.bind(this); + this.send = this.send.bind(this); + this.upload = this.upload.bind(this); + + this.addEventListener("message", this._handleRPCResponse); + } + + connect(token = "") { + return new Promise((resolve, reject) => { + const url = `//${document.location.host}/edge/sock?token=${token}`; + this._log("opening connection to", url); + const conn: any = new SockJS(url); + + const onOpen = () => { + this._log('client connected'); + resetHandlers(); + conn.onclose = this._onConnectionClose; + conn.onmessage = this._onConnectionMessage; + this._conn = conn; + this._sendQueued(); + setTimeout(() => { + this._dispatchConnect(); + }, 0); + return resolve(this); + }; + + const onError = (evt) => { + resetHandlers(); + this._scheduleReconnection(); + return reject(evt); + }; + + const resetHandlers = () => { + conn.removeEventListener('open', onOpen); + conn.removeEventListener('close', onError); + conn.removeEventListener('error', onError); + }; + + conn.addEventListener('open', onOpen); + conn.addEventListener('error', onError); + conn.addEventListener('close', onError); + }); + } + + disconnect() { + this._cleanupConnection(); + } + + _onConnectionMessage(evt) { + const rawMessage = JSON.parse(evt.data); + const message = messageFrom(rawMessage); + const event = new CustomEvent(message.getType(), { + cancelable: true, + detail: message.getPayload() + }); + this.dispatchEvent(event); + } + + _handleRPCResponse(evt) { + console.log(evt); + + const { jsonrpc, id, error, result } = evt.detail; + + if (jsonrpc !== '2.0' || id === undefined) return; + + // Prevent additional handlers to catch this event + evt.stopImmediatePropagation(); + + const pending = this._pendingRPC[id]; + if (!pending) return; + + delete this._pendingRPC[id]; + + if (error) { + pending.reject(new RPCError(error.code, error.message, error.data)); + return; + } + + pending.resolve(result); + } + + _onConnectionClose(evt) { + this._log('client disconnected'); + this._dispatchDisconnect(); + this._cleanupConnection(); + this._scheduleReconnection(); + } + + _dispatchDisconnect() { + const event = new CustomEvent('disconnect'); + this.dispatchEvent(event); + } + + _dispatchConnect() { + const event = new CustomEvent('connect'); + this.dispatchEvent(event); + } + + _scheduleReconnection() { + if (!this._autoReconnect) return; + + this._reconnectionDelay = this._reconnectionDelay * 2 + Math.random(); + this._log('client will try to reconnect in %dms', this._reconnectionDelay); + setTimeout(this.connect.bind(this), this._reconnectionDelay); + } + + _cleanupConnection() { + if (!this._conn) return; + this._conn.onopen = null; + this._conn.onerror = null; + this._conn.onclose = null; + this._conn.onmessage = null; + this._conn.close(); + this._conn = null; + } + + _send(message) { + if (!this._conn) return false; + this._log('sending message', message); + this._conn.send(JSON.stringify(message)); + return true; + } + + _sendQueued() { + this._log("sending queued messages", this._queue.length); + let msg = this._queue.shift(); + while (msg) { + const sent = this._send(msg); + if (!sent) return; + msg = this._queue.shift(); + } + } + + _log(...args) { + if (!this.debug) return; + console.log(...args); + } + + _sendOrQueue(msg) { + if (this.isConnected()) { + this._sendQueued(); + this._send(msg); + } else { + this._log('queuing message', msg); + this._queue.push(msg); + } + } + + send(data) { + const msg = new Message("message", data); + this._sendOrQueue(msg); + } + + rpc(method, params) { + return new Promise((resolve, reject) => { + const id = this._rpcID++; + const rpc = new Message(TypeMessage, { + jsonrpc: '2.0', + id, method, params + }); + this._sendOrQueue(rpc); + this._pendingRPC[id.toString()] = { resolve, reject }; + }); + } + + isConnected() { + return this._conn !== null; + } + + upload(blob: string|Blob, metadata: any) { + return new Promise((resolve, reject) => { + const formData = new FormData(); + formData.set("file", blob); + + if (metadata) { + try { + formData.set("metadata", JSON.stringify(metadata)); + } catch(err) { + return reject(err); + } + } + const xhr = new XMLHttpRequest(); + + const result = { + onProgress: null, + abort: () => xhr.abort(), + result: () => { + return new Promise((resolve, reject) => { + xhr.onload = () => { + let data; + try { + data = JSON.parse(xhr.responseText); + } catch(err) { + reject(err); + return; + } + resolve(data); + }; + xhr.onerror = reject; + xhr.onabort = reject; + }); + } + }; + + xhr.upload.onprogress = evt => { + if (typeof result.onProgress !== 'function') return; + (result as any).onProgress(evt.loaded, evt.total); + }; + xhr.onabort = reject; + xhr.onerror = reject; + + xhr.open('POST', `/edge/api/v1/upload`); + xhr.send(formData); + + resolve(result); + }); + } + + blobUrl(bucket: string, blobId: string) { + return `/edge/api/v1/download/${bucket}/${blobId}`; + } +} \ No newline at end of file diff --git a/pkg/sdk/client/src/event-target.ts b/pkg/sdk/client/src/event-target.ts new file mode 100644 index 0000000..c056441 --- /dev/null +++ b/pkg/sdk/client/src/event-target.ts @@ -0,0 +1,44 @@ +export class EventTarget { + listeners: { + [type: string]: Function[] + } + + constructor() { + this.listeners = {}; + } + + addEventListener(type: string, callback: Function) { + if (!(type in this.listeners)) { + this.listeners[type] = []; + } + this.listeners[type].push(callback); + }; + + removeEventListener(type: string, callback: Function) { + if (!(type in this.listeners)) { + return; + } + const stack = this.listeners[type]; + for (var i = 0, l = stack.length; i < l; i++) { + if (stack[i] === callback){ + stack.splice(i, 1); + return; + } + } + }; + + dispatchEvent(event: Event) { + if (!(event.type in this.listeners)) { + return true; + } + + const stack = this.listeners[event.type].slice(); + + for (let i = 0, l = stack.length; i < l; i++) { + stack[i].call(this, event); + if (event.cancelBubble) return; + } + return !event.defaultPrevented; + }; + +} \ No newline at end of file diff --git a/pkg/sdk/client/src/index.ts b/pkg/sdk/client/src/index.ts new file mode 100644 index 0000000..8b60d01 --- /dev/null +++ b/pkg/sdk/client/src/index.ts @@ -0,0 +1,3 @@ +import { Client } from './client.js'; + +export default new Client(); \ No newline at end of file diff --git a/pkg/sdk/client/src/message.ts b/pkg/sdk/client/src/message.ts new file mode 100644 index 0000000..4173746 --- /dev/null +++ b/pkg/sdk/client/src/message.ts @@ -0,0 +1,32 @@ + +export const TypeMessage = "message" + +export class Message { + _type: string + _payload: any + + constructor(type, payload) { + this._type = type; + this._payload = payload; + } + + getType() { + return this._type; + } + + getPayload() { + return this._payload; + } + + toJSON() { + return { + t: this._type, + p: this._payload + }; + } + +} + +export function messageFrom(raw) { + return new Message(raw.t, raw.p); +} diff --git a/pkg/sdk/client/src/rpc-error.ts b/pkg/sdk/client/src/rpc-error.ts new file mode 100644 index 0000000..97b7ffb --- /dev/null +++ b/pkg/sdk/client/src/rpc-error.ts @@ -0,0 +1,11 @@ +export class RPCError extends Error { + code: string + data: any + + constructor(code, message, data) { + super(message); + this.code = code; + this.data = data; + if((Error as any).captureStackTrace) (Error as any).captureStackTrace(this, RPCError); + } +} \ No newline at end of file diff --git a/pkg/sdk/client/src/sock.ts b/pkg/sdk/client/src/sock.ts new file mode 100644 index 0000000..8adc728 --- /dev/null +++ b/pkg/sdk/client/src/sock.ts @@ -0,0 +1,3 @@ +import SockJS from 'sockjs-client'; + +window.SockJS = SockJS; \ No newline at end of file diff --git a/pkg/sdk/sdk.go b/pkg/sdk/sdk.go new file mode 100644 index 0000000..51da151 --- /dev/null +++ b/pkg/sdk/sdk.go @@ -0,0 +1,6 @@ +package sdk + +import "embed" + +//go:embed client/dist/*.js client/dist/*.js.map +var FS embed.FS diff --git a/pkg/storage/blob_store.go b/pkg/storage/blob_store.go new file mode 100644 index 0000000..de95c22 --- /dev/null +++ b/pkg/storage/blob_store.go @@ -0,0 +1,66 @@ +package storage + +import ( + "context" + "errors" + "io" + "time" + + "github.com/oklog/ulid/v2" +) + +var ( + ErrBucketClosed = errors.New("bucket closed") + ErrBlobNotFound = errors.New("blob not found") +) + +type BlobID string + +func NewBlobID() BlobID { + return BlobID(ulid.Make().String()) +} + +type BlobStore interface { + OpenBucket(ctx context.Context, name string) (BlobBucket, error) + ListBuckets(ctx context.Context) ([]string, error) + DeleteBucket(ctx context.Context, name string) error +} + +type BlobBucket interface { + Name() string + Close() error + + Get(ctx context.Context, id BlobID) (BlobInfo, error) + Delete(ctx context.Context, id BlobID) error + NewReader(ctx context.Context, id BlobID) (io.ReadSeekCloser, error) + NewWriter(ctx context.Context, id BlobID) (io.WriteCloser, error) + List(ctx context.Context) ([]BlobInfo, error) + Size(ctx context.Context) (int64, error) +} + +type BlobInfo interface { + ID() BlobID + Bucket() string + ModTime() time.Time + Size() int64 + ContentType() string +} + +type BucketListOptions struct { + Limit *int + Offset *int +} + +type BucketListOptionsFunc func(o *BucketListOptions) + +func WithBucketListLimit(limit int) BucketListOptionsFunc { + return func(o *BucketListOptions) { + o.Limit = &limit + } +} + +func WithBucketListOffset(offset int) BucketListOptionsFunc { + return func(o *BucketListOptions) { + o.Offset = &offset + } +} diff --git a/pkg/storage/document_store.go b/pkg/storage/document_store.go new file mode 100644 index 0000000..8e231af --- /dev/null +++ b/pkg/storage/document_store.go @@ -0,0 +1,69 @@ +package storage + +import ( + "context" + "errors" + "time" + + "forge.cadoles.com/arcad/edge/pkg/storage/filter" + "github.com/oklog/ulid/v2" +) + +var ErrDocumentNotFound = errors.New("document not found") + +type DocumentID string + +const ( + DocumentAttrID = "_id" + DocumentAttrCreatedAt = "_createdAt" + DocumentAttrUpdatedAt = "_updatedAt" +) + +func NewDocumentID() DocumentID { + return DocumentID(ulid.Make().String()) +} + +type Document map[string]interface{} + +func (d Document) ID() (DocumentID, bool) { + rawID, exists := d[DocumentAttrID] + if !exists { + return "", false + } + + id, ok := rawID.(string) + if ok { + return "", false + } + + return DocumentID(id), true +} + +func (d Document) CreatedAt() (time.Time, bool) { + return d.timeAttr(DocumentAttrCreatedAt) +} + +func (d Document) UpdatedAt() (time.Time, bool) { + return d.timeAttr(DocumentAttrUpdatedAt) +} + +func (d Document) timeAttr(attr string) (time.Time, bool) { + rawTime, exists := d[attr] + if !exists { + return time.Time{}, false + } + + t, ok := rawTime.(time.Time) + if ok { + return time.Time{}, false + } + + return t, true +} + +type DocumentStore interface { + Get(ctx context.Context, collection string, id DocumentID) (Document, error) + Query(ctx context.Context, collection string, filter *filter.Filter, funcs ...QueryOptionFunc) ([]Document, error) + Upsert(ctx context.Context, collection string, document Document) (Document, error) + Delete(ctx context.Context, collection string, id DocumentID) error +} diff --git a/pkg/storage/filter/and.go b/pkg/storage/filter/and.go new file mode 100644 index 0000000..c504797 --- /dev/null +++ b/pkg/storage/filter/and.go @@ -0,0 +1,17 @@ +package filter + +type AndOperator struct { + children []Operator +} + +func (o *AndOperator) Token() Token { + return TokenAnd +} + +func (o *AndOperator) Children() []Operator { + return o.children +} + +func NewAndOperator(ops ...Operator) *AndOperator { + return &AndOperator{ops} +} diff --git a/pkg/storage/filter/eq.go b/pkg/storage/filter/eq.go new file mode 100644 index 0000000..c7d0163 --- /dev/null +++ b/pkg/storage/filter/eq.go @@ -0,0 +1,17 @@ +package filter + +type EqOperator struct { + fields map[string]interface{} +} + +func (o *EqOperator) Token() Token { + return TokenEq +} + +func (o *EqOperator) Fields() map[string]interface{} { + return o.fields +} + +func NewEqOperator(fields map[string]interface{}) *EqOperator { + return &EqOperator{fields} +} diff --git a/pkg/storage/filter/error.go b/pkg/storage/filter/error.go new file mode 100644 index 0000000..421fbb7 --- /dev/null +++ b/pkg/storage/filter/error.go @@ -0,0 +1,13 @@ +package filter + +import "errors" + +var ( + ErrInvalidFieldOperator = errors.New("invalid field operator") + ErrInvalidAggregationOperator = errors.New("invalid aggregation operator") + ErrInvalidFieldMap = errors.New("invalid field map") + ErrUnknownOperator = errors.New("unknown operator") + ErrUnexpectedOperator = errors.New("unexpected operator") + ErrUnsupportedOperator = errors.New("unsupported operator") + ErrInvalidRoot = errors.New("invalid root") +) diff --git a/pkg/storage/filter/filter.go b/pkg/storage/filter/filter.go new file mode 100644 index 0000000..9a6acf3 --- /dev/null +++ b/pkg/storage/filter/filter.go @@ -0,0 +1,136 @@ +package filter + +import ( + "github.com/pkg/errors" +) + +type Filter struct { + root Operator +} + +func (f *Filter) Root() Operator { + return f.root +} + +func New(root Operator) *Filter { + return &Filter{root} +} + +func NewFrom(raw map[string]interface{}) (*Filter, error) { + if len(raw) != 1 { + return nil, errors.WithStack(ErrInvalidRoot) + } + + op, err := toFieldOperator(raw) + if err != nil { + return nil, err + } + + return &Filter{op}, nil +} + +func toFieldOperator(v interface{}) (Operator, error) { + vv, ok := v.(map[string]interface{}) + if !ok { + return nil, errors.WithStack(ErrInvalidFieldOperator) + } + + ops := make([]Operator, 0) + + for rawToken, val := range vv { + var ( + op Operator + err error + ) + + token := Token(rawToken) + + switch { + case isAggregatorToken(token): + op, err = toAggregateOperator(token, val) + + case isFieldToken(token): + fields, ok := val.(map[string]interface{}) + if !ok { + return nil, errors.WithStack(ErrInvalidFieldMap) + } + + switch token { + case TokenEq: + op = NewEqOperator(fields) + case TokenNeq: + op = NewNeqOperator(fields) + case TokenGt: + op = NewGtOperator(fields) + case TokenGte: + op = NewGteOperator(fields) + case TokenLt: + op = NewLtOperator(fields) + case TokenLte: + op = NewLteOperator(fields) + case TokenIn: + op = NewInOperator(fields) + case TokenLike: + op = NewLikeOperator(fields) + default: + return nil, errors.Wrapf(ErrUnknownOperator, "unknown operator field '%s'", token) + } + + default: + return nil, errors.Wrapf(ErrUnknownOperator, "unknown operator field '%s'", token) + } + + if err != nil { + return nil, err + } + + ops = append(ops, op) + } + + and := NewAndOperator(ops...) + + return and, nil +} + +func toAggregateOperator(token Token, v interface{}) (Operator, error) { + vv, ok := v.([]interface{}) + if !ok { + return nil, errors.WithStack(ErrInvalidAggregationOperator) + } + + ops := make([]Operator, 0) + + for _, c := range vv { + op, err := toFieldOperator(c) + if err != nil { + return nil, err + } + + ops = append(ops, op) + } + + var aggregator Operator + + switch token { + case TokenAnd: + aggregator = NewAndOperator(ops...) + case TokenOr: + aggregator = NewOrOperator(ops...) + case TokenNot: + aggregator = NewNotOperator(ops...) + } + + return aggregator, nil +} + +func isAggregatorToken(token Token) bool { + return token == TokenAnd || token == TokenOr || token == TokenNot +} + +func isFieldToken(token Token) bool { + return token == TokenEq || + token == TokenGt || token == TokenGte || + token == TokenLt || token == TokenLte || + token == TokenNeq || token == TokenIn || + token == TokenLike +} diff --git a/pkg/storage/filter/gt.go b/pkg/storage/filter/gt.go new file mode 100644 index 0000000..32d1361 --- /dev/null +++ b/pkg/storage/filter/gt.go @@ -0,0 +1,17 @@ +package filter + +type GtOperator struct { + fields map[string]interface{} +} + +func (o *GtOperator) Token() Token { + return TokenGt +} + +func (o *GtOperator) Fields() map[string]interface{} { + return o.fields +} + +func NewGtOperator(fields OperatorFields) *GtOperator { + return &GtOperator{fields} +} diff --git a/pkg/storage/filter/gte.go b/pkg/storage/filter/gte.go new file mode 100644 index 0000000..de9a330 --- /dev/null +++ b/pkg/storage/filter/gte.go @@ -0,0 +1,17 @@ +package filter + +type GteOperator struct { + fields OperatorFields +} + +func (o *GteOperator) Token() Token { + return TokenGte +} + +func (o *GteOperator) Fields() map[string]interface{} { + return o.fields +} + +func NewGteOperator(fields OperatorFields) *GteOperator { + return &GteOperator{fields} +} diff --git a/pkg/storage/filter/in.go b/pkg/storage/filter/in.go new file mode 100644 index 0000000..9672785 --- /dev/null +++ b/pkg/storage/filter/in.go @@ -0,0 +1,17 @@ +package filter + +type InOperator struct { + fields map[string]interface{} +} + +func (o *InOperator) Token() Token { + return TokenIn +} + +func (o *InOperator) Fields() map[string]interface{} { + return o.fields +} + +func NewInOperator(fields OperatorFields) *InOperator { + return &InOperator{fields} +} diff --git a/pkg/storage/filter/like.go b/pkg/storage/filter/like.go new file mode 100644 index 0000000..2dd6cc0 --- /dev/null +++ b/pkg/storage/filter/like.go @@ -0,0 +1,17 @@ +package filter + +type LikeOperator struct { + fields map[string]interface{} +} + +func (o *LikeOperator) Token() Token { + return TokenLike +} + +func (o *LikeOperator) Fields() map[string]interface{} { + return o.fields +} + +func NewLikeOperator(fields OperatorFields) *LikeOperator { + return &LikeOperator{fields} +} diff --git a/pkg/storage/filter/lt.go b/pkg/storage/filter/lt.go new file mode 100644 index 0000000..a60fc60 --- /dev/null +++ b/pkg/storage/filter/lt.go @@ -0,0 +1,17 @@ +package filter + +type LtOperator struct { + fields map[string]interface{} +} + +func (o *LtOperator) Token() Token { + return TokenLt +} + +func (o *LtOperator) Fields() map[string]interface{} { + return o.fields +} + +func NewLtOperator(fields OperatorFields) *LtOperator { + return &LtOperator{fields} +} diff --git a/pkg/storage/filter/lte.go b/pkg/storage/filter/lte.go new file mode 100644 index 0000000..070635c --- /dev/null +++ b/pkg/storage/filter/lte.go @@ -0,0 +1,17 @@ +package filter + +type LteOperator struct { + fields map[string]interface{} +} + +func (o *LteOperator) Token() Token { + return TokenLte +} + +func (o *LteOperator) Fields() map[string]interface{} { + return o.fields +} + +func NewLteOperator(fields OperatorFields) *LteOperator { + return &LteOperator{fields} +} diff --git a/pkg/storage/filter/neq.go b/pkg/storage/filter/neq.go new file mode 100644 index 0000000..bdba587 --- /dev/null +++ b/pkg/storage/filter/neq.go @@ -0,0 +1,17 @@ +package filter + +type NeqOperator struct { + fields map[string]interface{} +} + +func (o *NeqOperator) Token() Token { + return TokenNeq +} + +func (o *NeqOperator) Fields() map[string]interface{} { + return o.fields +} + +func NewNeqOperator(fields map[string]interface{}) *NeqOperator { + return &NeqOperator{fields} +} diff --git a/pkg/storage/filter/not.go b/pkg/storage/filter/not.go new file mode 100644 index 0000000..1b60af9 --- /dev/null +++ b/pkg/storage/filter/not.go @@ -0,0 +1,17 @@ +package filter + +type NotOperator struct { + children []Operator +} + +func (o *NotOperator) Token() Token { + return TokenOr +} + +func (o *NotOperator) Children() []Operator { + return o.children +} + +func NewNotOperator(ops ...Operator) *NotOperator { + return &NotOperator{ops} +} diff --git a/pkg/storage/filter/operator.go b/pkg/storage/filter/operator.go new file mode 100644 index 0000000..e4781b9 --- /dev/null +++ b/pkg/storage/filter/operator.go @@ -0,0 +1,23 @@ +package filter + +type Token string + +const ( + TokenAnd Token = "and" + TokenOr Token = "or" + TokenNot Token = "not" + TokenEq Token = "eq" + TokenNeq Token = "neq" + TokenGt Token = "gt" + TokenGte Token = "gte" + TokenLt Token = "lt" + TokenLte Token = "lte" + TokenIn Token = "in" + TokenLike Token = "like" +) + +type OperatorFields map[string]interface{} + +type Operator interface { + Token() Token +} diff --git a/pkg/storage/filter/or.go b/pkg/storage/filter/or.go new file mode 100644 index 0000000..276f801 --- /dev/null +++ b/pkg/storage/filter/or.go @@ -0,0 +1,17 @@ +package filter + +type OrOperator struct { + children []Operator +} + +func (o *OrOperator) Token() Token { + return TokenOr +} + +func (o *OrOperator) Children() []Operator { + return o.children +} + +func NewOrOperator(ops ...Operator) *OrOperator { + return &OrOperator{ops} +} diff --git a/pkg/storage/filter/sql/helper.go b/pkg/storage/filter/sql/helper.go new file mode 100644 index 0000000..c1d2aac --- /dev/null +++ b/pkg/storage/filter/sql/helper.go @@ -0,0 +1,87 @@ +package sql + +import ( + "strings" + + "forge.cadoles.com/arcad/edge/pkg/storage/filter" + "github.com/pkg/errors" +) + +func aggregatorToSQL(operator string, opt *Option, children ...filter.Operator) (string, []interface{}, error) { + args := make([]interface{}, 0) + + if len(children) == 0 { + return "", args, nil + } + + var sb strings.Builder + + if _, err := sb.WriteString("("); err != nil { + return "", nil, errors.WithStack(err) + } + + for i, c := range children { + if i != 0 { + if _, err := sb.WriteString(" " + operator + " "); err != nil { + return "", nil, errors.WithStack(err) + } + } + + cSQL, cArgs, err := toSQL(c, opt) + if err != nil { + return "", nil, errors.WithStack(err) + } + + args = append(args, cArgs...) + + if _, err := sb.WriteString(cSQL); err != nil { + return "", nil, errors.WithStack(err) + } + } + + if _, err := sb.WriteString(")"); err != nil { + return "", nil, errors.WithStack(err) + } + + result := sb.String() + if result == "()" { + return "", args, nil + } + + return result, args, nil +} + +func fieldsToSQL(operator string, invert bool, fields map[string]interface{}, option *Option) (string, []interface{}, error) { + var sb strings.Builder + + args := make([]interface{}, 0) + + i := 0 + for k, v := range fields { + if i != 0 { + if _, err := sb.WriteString(" AND "); err != nil { + return "", nil, errors.WithStack(err) + } + } + + var ( + tr string + err error + ) + + tr, v, err = option.Transform(operator, invert, k, v, option) + if err != nil { + return "", nil, errors.WithStack(err) + } + + if _, err := sb.WriteString(tr); err != nil { + return "", nil, errors.WithStack(err) + } + + args = append(args, option.ValueTransform(v)) + + i++ + } + + return sb.String(), args, nil +} diff --git a/pkg/storage/filter/sql/option.go b/pkg/storage/filter/sql/option.go new file mode 100644 index 0000000..a115335 --- /dev/null +++ b/pkg/storage/filter/sql/option.go @@ -0,0 +1,78 @@ +package sql + +import ( + "strconv" +) + +type ( + PreparedParameterFunc func() string + KeyTransformFunc func(key string) string + ValueTransformFunc func(v interface{}) interface{} + TransformFunc func(operator string, invert bool, key string, value interface{}, option *Option) (string, interface{}, error) +) + +type Option struct { + PreparedParameter PreparedParameterFunc + KeyTransform KeyTransformFunc + ValueTransform ValueTransformFunc + Transform TransformFunc +} + +func DefaultOption() *Option { + opt := &Option{} + defaults := []OptionFunc{ + WithPreparedParameter("$", 1), + WithNoOpKeyTransform(), + WithNoOpValueTransform(), + WithDefaultTransform(), + } + + for _, fn := range defaults { + fn(opt) + } + + return opt +} + +type OptionFunc func(*Option) + +func WithPreparedParameter(prefix string, index int) OptionFunc { + return func(opt *Option) { + opt.PreparedParameter = func() string { + param := prefix + strconv.FormatInt(int64(index), 10) + index++ + + return param + } + } +} + +func WithKeyTransform(transform KeyTransformFunc) OptionFunc { + return func(opt *Option) { + opt.KeyTransform = transform + } +} + +func WithNoOpKeyTransform() OptionFunc { + return WithKeyTransform(func(key string) string { + return key + }) +} + +func WithValueTransform(transform ValueTransformFunc) OptionFunc { + return func(opt *Option) { + opt.ValueTransform = transform + } +} + +func WithDefaultTransform() OptionFunc { + return func(opt *Option) { + opt.Transform = DefaultTransform + } +} + +func WithNoOpValueTransform() OptionFunc { + return WithValueTransform(func(value interface{}) interface{} { + return value + }) +} diff --git a/pkg/storage/filter/sql/sql.go b/pkg/storage/filter/sql/sql.go new file mode 100644 index 0000000..34b3e8b --- /dev/null +++ b/pkg/storage/filter/sql/sql.go @@ -0,0 +1,159 @@ +package sql + +import ( + "forge.cadoles.com/arcad/edge/pkg/storage/filter" + "github.com/pkg/errors" +) + +type transformFunc func(op filter.Operator, option *Option) (string, []interface{}, error) + +var transforms map[filter.Token]transformFunc + +func init() { + // Initialise transforms map + transforms = map[filter.Token]transformFunc{ + filter.TokenAnd: transformAndOperator, + filter.TokenOr: transformOrOperator, + filter.TokenNot: transformNotOperator, + filter.TokenEq: transformEqOperator, + filter.TokenNeq: transformNeqOperator, + filter.TokenGt: transformGtOperator, + filter.TokenGte: transformGteOperator, + filter.TokenLte: transformLteOperator, + filter.TokenLt: transformLtOperator, + filter.TokenLike: transformLikeOperator, + filter.TokenIn: transformInOperator, + } +} + +func ToSQL(op filter.Operator, funcs ...OptionFunc) (string, []interface{}, error) { + opt := DefaultOption() + + for _, fn := range funcs { + fn(opt) + } + + return toSQL(op, opt) +} + +func toSQL(op filter.Operator, opt *Option) (string, []interface{}, error) { + if op == nil { + return "", nil, nil + } + + transform, exists := transforms[op.Token()] + if !exists { + return "", nil, errors.WithStack(filter.ErrUnsupportedOperator) + } + + sql, args, err := transform(op, opt) + if err != nil { + return "", nil, errors.WithStack(err) + } + + return sql, args, nil +} + +func transformAndOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + andOp, ok := op.(*filter.AndOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenAnd, op.Token()) + } + + return aggregatorToSQL("AND", option, andOp.Children()...) +} + +func transformOrOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + orOp, ok := op.(*filter.OrOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenOr, op.Token()) + } + + return aggregatorToSQL("OR", option, orOp.Children()...) +} + +func transformEqOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + eqOp, ok := op.(*filter.EqOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenEq, op.Token()) + } + + return fieldsToSQL("=", false, eqOp.Fields(), option) +} + +func transformNeqOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + eqOp, ok := op.(*filter.NeqOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenNeq, op.Token()) + } + + return fieldsToSQL("!=", false, eqOp.Fields(), option) +} + +func transformGtOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + gtOp, ok := op.(*filter.GtOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenGt, op.Token()) + } + + return fieldsToSQL(">", false, gtOp.Fields(), option) +} + +func transformGteOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + gteOp, ok := op.(*filter.GteOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenGte, op.Token()) + } + + return fieldsToSQL(">=", false, gteOp.Fields(), option) +} + +func transformLtOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + ltOp, ok := op.(*filter.LtOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLt, op.Token()) + } + + return fieldsToSQL("<", false, ltOp.Fields(), option) +} + +func transformLteOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + lteOp, ok := op.(*filter.LteOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLte, op.Token()) + } + + return fieldsToSQL("<=", false, lteOp.Fields(), option) +} + +func transformInOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + inOp, ok := op.(*filter.InOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenIn, op.Token()) + } + + return fieldsToSQL("IN", true, inOp.Fields(), option) +} + +func transformLikeOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + likeOp, ok := op.(*filter.LikeOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLike, op.Token()) + } + + return fieldsToSQL("LIKE", false, likeOp.Fields(), option) +} + +func transformNotOperator(op filter.Operator, option *Option) (string, []interface{}, error) { + notOp, ok := op.(*filter.NotOperator) + if !ok { + return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenNot, op.Token()) + } + + sql, args, err := aggregatorToSQL("AND", option, notOp.Children()...) + if err != nil { + return "", nil, errors.WithStack(err) + } + + return "NOT " + sql, args, nil +} diff --git a/pkg/storage/filter/sql/sql_test.go b/pkg/storage/filter/sql/sql_test.go new file mode 100644 index 0000000..24d9070 --- /dev/null +++ b/pkg/storage/filter/sql/sql_test.go @@ -0,0 +1,84 @@ +package sql + +import ( + "encoding/json" + "fmt" + "testing" + + "forge.cadoles.com/arcad/edge/pkg/storage/filter" +) + +type ( + op map[string]interface{} + aggr []interface{} +) + +type testCase struct { + RawFilter string + ExpectedSQL string + ExpectedArgs []interface{} +} + +var testCases = []testCase{ + { + RawFilter: ` + { + "or": [ + {"eq": {"foo": "bar"}}, + {"neq": {"hello": "world"}} + ] + } + `, + ExpectedSQL: "(((foo = $1) OR (hello != $2)))", + ExpectedArgs: []interface{}{"bar", "world"}, + }, +} + +func TestSQLFilter(t *testing.T) { + for i, tc := range testCases { + func(tc testCase) { + t.Run(fmt.Sprintf("Test case #%d", i), func(t *testing.T) { + raw := make(map[string]interface{}) + if err := json.Unmarshal([]byte(tc.RawFilter), &raw); err != nil { + t.Fatal(err) + } + + query, err := filter.NewFrom(raw) + if err != nil { + t.Fatal(err) + } + + sql, args, err := ToSQL(query.Root()) + if err != nil { + t.Error(err) + } + + if e, g := tc.ExpectedSQL, sql; e != g { + t.Errorf("sql: expected '%s', got '%s'", e, g) + } + + if args == nil { + t.Fatal("args should not be nil") + } + + for i, a := range args { + if i >= len(tc.ExpectedArgs) { + t.Errorf("args[%d]: expected nil, got '%v'", i, a) + + continue + } + + if e, g := tc.ExpectedArgs[i], a; e != g { + t.Errorf("args[%d]: expected '%v', got '%v'", i, e, g) + } + } + + for i, a := range tc.ExpectedArgs { + if i >= len(args) { + t.Errorf("args[%d]: expected '%v', got nil", i, a) + } + } + }) + }(tc) + } +} diff --git a/pkg/storage/filter/sql/transform.go b/pkg/storage/filter/sql/transform.go new file mode 100644 index 0000000..10d174d --- /dev/null +++ b/pkg/storage/filter/sql/transform.go @@ -0,0 +1,45 @@ +package sql + +import ( + "strings" + + "github.com/pkg/errors" +) + +func DefaultTransform(operator string, invert bool, key string, value interface{}, option *Option) (string, interface{}, error) { + var sb strings.Builder + + if invert { + if _, err := sb.WriteString(option.PreparedParameter()); err != nil { + return "", nil, errors.WithStack(err) + } + } else { + if _, err := sb.WriteString(option.KeyTransform(key)); err != nil { + return "", nil, errors.WithStack(err) + } + } + + if _, err := sb.WriteString(" "); err != nil { + return "", nil, errors.WithStack(err) + } + + if _, err := sb.WriteString(operator); err != nil { + return "", nil, errors.WithStack(err) + } + + if invert { + if _, err := sb.WriteString(" "); err != nil { + return "", nil, errors.WithStack(err) + } + + if _, err := sb.WriteString(key); err != nil { + return "", nil, errors.WithStack(err) + } + } else { + if _, err := sb.WriteString(" " + option.PreparedParameter()); err != nil { + return "", nil, errors.WithStack(err) + } + } + + return sb.String(), value, nil +} diff --git a/pkg/storage/query_option.go b/pkg/storage/query_option.go new file mode 100644 index 0000000..ea26469 --- /dev/null +++ b/pkg/storage/query_option.go @@ -0,0 +1,41 @@ +package storage + +type OrderDirection string + +const ( + OrderDirectionAsc OrderDirection = "ASC" + OrderDirectionDesc OrderDirection = "DESC" +) + +type QueryOption struct { + Limit *int + Offset *int + OrderBy *string + OrderDirection *OrderDirection +} + +type QueryOptionFunc func(o *QueryOption) + +func WithLimit(limit int) QueryOptionFunc { + return func(o *QueryOption) { + o.Limit = &limit + } +} + +func WithOffset(offset int) QueryOptionFunc { + return func(o *QueryOption) { + o.Offset = &offset + } +} + +func WithOrderBy(orderBy string) QueryOptionFunc { + return func(o *QueryOption) { + o.OrderBy = &orderBy + } +} + +func WithOrderDirection(direction OrderDirection) QueryOptionFunc { + return func(o *QueryOption) { + o.OrderDirection = &direction + } +} diff --git a/pkg/storage/sqlite/blob_bucket.go b/pkg/storage/sqlite/blob_bucket.go new file mode 100644 index 0000000..2bbbf7d --- /dev/null +++ b/pkg/storage/sqlite/blob_bucket.go @@ -0,0 +1,424 @@ +package sqlite + +import ( + "bytes" + "context" + "database/sql" + "io" + "sync" + "time" + + "forge.cadoles.com/arcad/edge/pkg/storage" + "github.com/gabriel-vasile/mimetype" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type BlobBucket struct { + name string + getDB getDBFunc + closed bool +} + +// Size implements storage.BlobBucket +func (b *BlobBucket) Size(ctx context.Context) (int64, error) { + var size int64 + + err := b.withTx(ctx, func(tx *sql.Tx) error { + query := `SELECT SUM(size) FROM blobs WHERE bucket = $1` + + row := tx.QueryRowContext(ctx, query, b.name) + + var nullSize sql.NullInt64 + + if err := row.Scan(&nullSize); err != nil { + return errors.WithStack(err) + } + + size = nullSize.Int64 + + return nil + }) + if err != nil { + return 0, errors.WithStack(err) + } + + return size, nil +} + +// Name implements storage.BlobBucket +func (b *BlobBucket) Name() string { + return b.name +} + +// Close implements storage.BlobBucket +func (b *BlobBucket) Close() error { + logger.Debug( + context.Background(), "closing bucket", + logger.F("alreadyClosed", b.closed), + logger.F("name", b.name), + ) + + b.closed = true + + return nil +} + +// Delete implements storage.BlobBucket +func (b *BlobBucket) Delete(ctx context.Context, id storage.BlobID) error { + err := b.withTx(ctx, func(tx *sql.Tx) error { + query := `DELETE FROM blobs WHERE bucket = $1 AND id = $2` + + if _, err := tx.ExecContext(ctx, query, b.name, id); err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +// Get implements storage.BlobBucket +func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobInfo, error) { + var blobInfo *BlobInfo + + err := b.withTx(ctx, func(tx *sql.Tx) error { + query := `SELECT content_type, mod_time, size FROM blobs WHERE bucket = $1 AND id = $2` + row := tx.QueryRowContext(ctx, query, b.name, id) + + var ( + contentType string + modTime time.Time + size int64 + ) + + if err := row.Scan(&contentType, &modTime, &size); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return errors.WithStack(storage.ErrBlobNotFound) + } + + return errors.WithStack(err) + } + + blobInfo = &BlobInfo{ + id: id, + bucket: b.name, + contentType: contentType, + modTime: modTime, + size: size, + } + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return blobInfo, nil +} + +// List implements storage.BlobBucket +func (b *BlobBucket) List(ctx context.Context) ([]storage.BlobInfo, error) { + var blobs []storage.BlobInfo + + err := b.withTx(ctx, func(tx *sql.Tx) error { + query := `SELECT id, content_type, mod_time, size FROM blobs WHERE bucket = $1` + + rows, err := tx.QueryContext(ctx, query, b.name) + if err != nil { + return errors.WithStack(err) + } + + blobs = make([]storage.BlobInfo, 0) + + for rows.Next() { + var ( + blobID string + contentType string + modTime time.Time + size int64 + ) + + if err := rows.Scan(&blobID, &contentType, &modTime, &size); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return errors.WithStack(storage.ErrBlobNotFound) + } + + return errors.WithStack(err) + } + + blobInfo := &BlobInfo{ + id: storage.BlobID(blobID), + bucket: b.name, + contentType: contentType, + modTime: modTime, + size: size, + } + + blobs = append(blobs, blobInfo) + } + + if err := rows.Err(); err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return blobs, nil +} + +// NewReader implements storage.BlobBucket +func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadSeekCloser, error) { + if b.closed { + return nil, errors.WithStack(storage.ErrBucketClosed) + } + + return &blobReaderCloser{ + id: id, + bucket: b.name, + getDB: b.getDB, + }, nil +} + +// NewWriter implements storage.BlobBucket +func (b *BlobBucket) NewWriter(ctx context.Context, id storage.BlobID) (io.WriteCloser, error) { + if b.closed { + return nil, errors.WithStack(storage.ErrBucketClosed) + } + + return &blobWriterCloser{ + id: id, + bucket: b.name, + getDB: b.getDB, + buf: bytes.Buffer{}, + }, nil +} + +func (b *BlobBucket) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error { + if b.closed { + return errors.WithStack(storage.ErrBucketClosed) + } + + db, err := b.getDB(ctx) + if err != nil { + return errors.WithStack(err) + } + + if err := withTx(ctx, db, fn); err != nil { + return errors.WithStack(err) + } + + return nil +} + +type blobWriterCloser struct { + id storage.BlobID + bucket string + getDB getDBFunc + buf bytes.Buffer + closed bool +} + +// Write implements io.WriteCloser +func (wbc *blobWriterCloser) Write(p []byte) (int, error) { + logger.Debug(context.Background(), "writing data to blob", logger.F("data", p)) + + n, err := wbc.buf.Write(p) + if err != nil { + return n, errors.WithStack(err) + } + + return n, nil +} + +// Close implements io.WriteCloser +func (wbc *blobWriterCloser) Close() error { + ctx := context.Background() + + logger.Debug( + ctx, "closing writer", + logger.F("alreadyClosed", wbc.closed), + logger.F("bucket", wbc.bucket), + logger.F("blobID", wbc.id), + ) + + if wbc.closed { + return nil + } + + err := wbc.withTx(ctx, func(tx *sql.Tx) error { + query := ` + INSERT INTO blobs (bucket, id, data, content_type, mod_time, size) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (id, bucket) DO UPDATE SET + data = $3, content_type = $4, mod_time = $5, size = $6 + ` + + data := wbc.buf.Bytes() + mime := mimetype.Detect(data) + modTime := time.Now().UTC() + + _, err := tx.Exec( + query, + wbc.bucket, + wbc.id, + data, + mime.String(), + modTime, + len(data), + ) + if err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return errors.WithStack(err) + } + + wbc.closed = true + + return nil +} + +func (wbc *blobWriterCloser) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error { + if wbc.closed { + return errors.WithStack(io.ErrClosedPipe) + } + + db, err := wbc.getDB(ctx) + if err != nil { + return errors.WithStack(err) + } + + if err := withTx(ctx, db, fn); err != nil { + return errors.WithStack(err) + } + + return nil +} + +type blobReaderCloser struct { + id storage.BlobID + bucket string + getDB getDBFunc + reader bytes.Reader + once sync.Once + closed bool +} + +// Read implements io.ReadSeekCloser +func (brc *blobReaderCloser) Read(p []byte) (int, error) { + var err error + + brc.once.Do(func() { + err = brc.loadBlob() + }) + + if err != nil { + return 0, errors.WithStack(err) + } + + n, err := brc.reader.Read(p) + if err != nil { + if errors.Is(err, io.EOF) { + return n, io.EOF + } + + return n, errors.WithStack(err) + } + + return n, nil +} + +// Seek implements io.ReadSeekCloser +func (brc *blobReaderCloser) Seek(offset int64, whence int) (int64, error) { + var err error + + brc.once.Do(func() { + err = brc.loadBlob() + }) + + if err != nil { + return 0, errors.WithStack(err) + } + + n, err := brc.reader.Seek(offset, whence) + if err != nil { + return n, errors.WithStack(err) + } + + return n, nil +} + +func (brc *blobReaderCloser) loadBlob() error { + ctx := context.Background() + logger.Debug(ctx, "loading blob", logger.F("alreadyClosed", brc.closed)) + + err := brc.withTx(ctx, func(tx *sql.Tx) error { + query := `SELECT data FROM blobs WHERE bucket = $1 AND id = $2` + row := tx.QueryRow(query, brc.bucket, brc.id) + + var data []byte + + if err := row.Scan(&data); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return errors.WithStack(storage.ErrBlobNotFound) + } + + return errors.WithStack(err) + } + + brc.reader = *bytes.NewReader(data) + + return nil + }) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +// Close implements io.ReadSeekCloser +func (brc *blobReaderCloser) Close() error { + logger.Debug( + context.Background(), "closing reader", + logger.F("alreadyClosed", brc.closed), + logger.F("bucket", brc.bucket), + logger.F("blobID", brc.id), + ) + + brc.closed = true + + return nil +} + +func (brc *blobReaderCloser) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error { + db, err := brc.getDB(ctx) + if err != nil { + return errors.WithStack(err) + } + + if err := withTx(ctx, db, fn); err != nil { + return errors.WithStack(err) + } + + return nil +} + +var ( + _ storage.BlobBucket = &BlobBucket{} + _ storage.BlobInfo = &BlobInfo{} + _ io.WriteCloser = &blobWriterCloser{} + _ io.ReadSeekCloser = &blobReaderCloser{} +) diff --git a/pkg/storage/sqlite/blob_info.go b/pkg/storage/sqlite/blob_info.go new file mode 100644 index 0000000..d9ff2e9 --- /dev/null +++ b/pkg/storage/sqlite/blob_info.go @@ -0,0 +1,40 @@ +package sqlite + +import ( + "time" + + "forge.cadoles.com/arcad/edge/pkg/storage" +) + +type BlobInfo struct { + id storage.BlobID + bucket string + contentType string + modTime time.Time + size int64 +} + +// Bucket implements storage.BlobInfo +func (i *BlobInfo) Bucket() string { + return i.bucket +} + +// ID implements storage.BlobInfo +func (i *BlobInfo) ID() storage.BlobID { + return i.id +} + +// ContentType implements storage.BlobInfo +func (i *BlobInfo) ContentType() string { + return i.contentType +} + +// ModTime implements storage.BlobInfo +func (i *BlobInfo) ModTime() time.Time { + return i.modTime +} + +// Size implements storage.BlobInfo +func (i *BlobInfo) Size() int64 { + return i.size +} diff --git a/pkg/storage/sqlite/blob_store.go b/pkg/storage/sqlite/blob_store.go new file mode 100644 index 0000000..65241e8 --- /dev/null +++ b/pkg/storage/sqlite/blob_store.go @@ -0,0 +1,136 @@ +package sqlite + +import ( + "context" + "database/sql" + + "forge.cadoles.com/arcad/edge/pkg/storage" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type BlobStore struct { + getDB getDBFunc +} + +// DeleteBucket implements storage.BlobStore +func (s *BlobStore) DeleteBucket(ctx context.Context, name string) error { + err := s.withTx(ctx, func(tx *sql.Tx) error { + query := `DELETE FROM blobs WHERE bucket = $1` + _, err := tx.ExecContext(ctx, query, name) + if err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +// ListBuckets implements storage.BlobStore +func (s *BlobStore) ListBuckets(ctx context.Context) ([]string, error) { + buckets := make([]string, 0) + + err := s.withTx(ctx, func(tx *sql.Tx) error { + query := `SELECT DISTINCT name FROM blobs` + rows, err := tx.QueryContext(ctx, query) + if err != nil { + return errors.WithStack(err) + } + + defer func() { + if err := rows.Close(); err != nil { + logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err))) + } + }() + + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return errors.WithStack(err) + } + + buckets = append(buckets, name) + } + + if err := rows.Err(); err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return buckets, nil +} + +// OpenBucket implements storage.BlobStore +func (s *BlobStore) OpenBucket(ctx context.Context, name string) (storage.BlobBucket, error) { + return &BlobBucket{ + name: name, + getDB: s.getDB, + }, nil +} + +func ensureBlobTables(ctx context.Context, db *sql.DB) error { + logger.Debug(ctx, "creating blobs table") + + err := withTx(ctx, db, func(tx *sql.Tx) error { + query := ` + CREATE TABLE IF NOT EXISTS blobs ( + id TEXT, + bucket TEXT, + data BLOB, + content_type TEXT NOT NULL, + mod_time TIMESTAMP NOT NULL, + size INTEGER, + PRIMARY KEY (id, bucket) + ); + ` + if _, err := tx.ExecContext(ctx, query); err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (s *BlobStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error { + var db *sql.DB + + db, err := s.getDB(ctx) + if err != nil { + return errors.WithStack(err) + } + + if err := withTx(ctx, db, fn); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func NewBlobStore(dsn string) *BlobStore { + getDB := newGetDBFunc(dsn, ensureBlobTables) + + return &BlobStore{getDB} +} + +func NewBlobStoreWithDB(db *sql.DB) *BlobStore { + getDB := newGetDBFuncFromDB(db, ensureBlobTables) + + return &BlobStore{getDB} +} + +var _ storage.BlobStore = &BlobStore{} diff --git a/pkg/storage/sqlite/blob_store_test.go b/pkg/storage/sqlite/blob_store_test.go new file mode 100644 index 0000000..cfa5fb7 --- /dev/null +++ b/pkg/storage/sqlite/blob_store_test.go @@ -0,0 +1,25 @@ +package sqlite + +import ( + "os" + "testing" + + "forge.cadoles.com/arcad/edge/pkg/storage/testsuite" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +func TestBlobStore(t *testing.T) { + t.Parallel() + logger.SetLevel(logger.LevelDebug) + + file := "./testdata/blobstore_test.sqlite" + + if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) { + t.Fatalf("%+v", errors.WithStack(err)) + } + + store := NewBlobStore(file) + + testsuite.TestBlobStore(t, store) +} diff --git a/pkg/storage/sqlite/document_store.go b/pkg/storage/sqlite/document_store.go new file mode 100644 index 0000000..f37afab --- /dev/null +++ b/pkg/storage/sqlite/document_store.go @@ -0,0 +1,340 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + "sync" + "time" + + "forge.cadoles.com/arcad/edge/pkg/storage" + "forge.cadoles.com/arcad/edge/pkg/storage/filter" + filterSQL "forge.cadoles.com/arcad/edge/pkg/storage/filter/sql" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" + + _ "modernc.org/sqlite" +) + +type DocumentStore struct { + db *sql.DB + path string + openOnce sync.Once + mutex sync.RWMutex +} + +// Delete implements storage.DocumentStore +func (s *DocumentStore) Delete(ctx context.Context, collection string, id storage.DocumentID) error { + err := s.withTx(ctx, func(tx *sql.Tx) error { + query := ` + DELETE FROM documents + WHERE collection = $1 AND id = $2 + ` + + _, err := tx.ExecContext(ctx, query, collection, string(id)) + if err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +// Get implements storage.DocumentStore +func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.DocumentID) (storage.Document, error) { + var document storage.Document + + err := s.withTx(ctx, func(tx *sql.Tx) error { + query := ` + SELECT id, data, created_at, updated_at + FROM documents + WHERE collection = $1 AND id = $2 + ` + + row := tx.QueryRowContext(ctx, query, collection, string(id)) + + var ( + createdAt time.Time + updatedAt time.Time + data JSONMap + ) + + err := row.Scan(&id, &data, &createdAt, &updatedAt) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return errors.WithStack(storage.ErrDocumentNotFound) + } + + return errors.WithStack(err) + } + + document = storage.Document(data) + + document[storage.DocumentAttrID] = id + document[storage.DocumentAttrCreatedAt] = createdAt + document[storage.DocumentAttrUpdatedAt] = updatedAt + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return document, nil +} + +// Query implements storage.DocumentStore +func (s *DocumentStore) Query(ctx context.Context, collection string, filter *filter.Filter, funcs ...storage.QueryOptionFunc) ([]storage.Document, error) { + var documents []storage.Document + + err := s.withTx(ctx, func(tx *sql.Tx) error { + criteria, args, err := filterSQL.ToSQL( + filter.Root(), + filterSQL.WithPreparedParameter("$", 2), + filterSQL.WithKeyTransform(func(key string) string { + return fmt.Sprintf("json_extract(data, '$.%s')", key) + }), + ) + if err != nil { + return errors.WithStack(err) + } + + query := ` + SELECT id, data, created_at, updated_at + FROM documents + WHERE collection = $1 AND (` + criteria + `) + ` + + args = append([]interface{}{collection}, args...) + + logger.Debug( + ctx, "executing query", + logger.F("query", query), + logger.F("args", args), + ) + + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + return errors.WithStack(err) + } + + defer rows.Close() + + documents = make([]storage.Document, 0) + + for rows.Next() { + var ( + id storage.DocumentID + createdAt time.Time + updatedAt time.Time + data JSONMap + ) + + if err := rows.Scan(&id, &data, &createdAt, &updatedAt); err != nil { + return errors.WithStack(err) + } + + document := storage.Document(data) + document[storage.DocumentAttrID] = id + document[storage.DocumentAttrCreatedAt] = createdAt + document[storage.DocumentAttrUpdatedAt] = updatedAt + + documents = append(documents, document) + } + + if err := rows.Err(); err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return documents, nil +} + +// Upsert implements storage.DocumentStore +func (s *DocumentStore) Upsert(ctx context.Context, collection string, document storage.Document) (storage.Document, error) { + var upsertedDocument storage.Document + + err := s.withTx(ctx, func(tx *sql.Tx) error { + query := ` + INSERT INTO documents (id, collection, data, created_at, updated_at) + VALUES($1, $2, $3, $4, $4) + ON CONFLICT (id, collection) DO UPDATE SET + data = $3, updated_at = $4 + RETURNING "id", "data", "created_at", "updated_at" + ` + + now := time.Now().UTC() + + id, exists := document.ID() + if !exists || id == "" { + id = storage.NewDocumentID() + } + + delete(document, storage.DocumentAttrID) + delete(document, storage.DocumentAttrCreatedAt) + delete(document, storage.DocumentAttrUpdatedAt) + + args := []any{id, collection, JSONMap(document), now, now} + + row := tx.QueryRowContext(ctx, query, args...) + + var ( + createdAt time.Time + updatedAt time.Time + data JSONMap + ) + + err := row.Scan(&id, &data, &createdAt, &updatedAt) + if err != nil { + return errors.WithStack(err) + } + + upsertedDocument = storage.Document(data) + + upsertedDocument[storage.DocumentAttrID] = id + upsertedDocument[storage.DocumentAttrCreatedAt] = createdAt + upsertedDocument[storage.DocumentAttrUpdatedAt] = updatedAt + + return nil + }) + if err != nil { + return nil, errors.WithStack(err) + } + + return upsertedDocument, nil +} + +func (s *DocumentStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) error { + var db *sql.DB + + db, err := s.getDatabase(ctx) + if err != nil { + return errors.WithStack(err) + } + + if err := withTx(ctx, db, fn); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (s *DocumentStore) getDatabase(ctx context.Context) (*sql.DB, error) { + s.mutex.RLock() + if s.db != nil { + defer s.mutex.RUnlock() + + var err error + + s.openOnce.Do(func() { + if err = s.ensureTables(ctx, s.db); err != nil { + err = errors.WithStack(err) + + return + } + }) + + if err != nil { + return nil, errors.WithStack(err) + } + + return s.db, nil + } + + s.mutex.RUnlock() + + var ( + db *sql.DB + err error + ) + + s.openOnce.Do(func() { + db, err = sql.Open("sqlite", s.path) + if err != nil { + err = errors.WithStack(err) + + return + } + + if err = s.ensureTables(ctx, db); err != nil { + err = errors.WithStack(err) + + return + } + }) + + if err != nil { + return nil, errors.WithStack(err) + } + + if db != nil { + s.mutex.Lock() + s.db = db + s.mutex.Unlock() + } + + s.mutex.RLock() + defer s.mutex.RUnlock() + + return s.db, nil +} + +func (s *DocumentStore) ensureTables(ctx context.Context, db *sql.DB) error { + err := withTx(ctx, db, func(tx *sql.Tx) error { + query := ` + CREATE TABLE IF NOT EXISTS documents ( + id TEXT PRIMARY KEY, + collection TEXT NOT NULL, + data TEXT, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + UNIQUE(id, collection) ON CONFLICT REPLACE + ); + ` + if _, err := tx.ExecContext(ctx, query); err != nil { + return errors.WithStack(err) + } + + query = ` + CREATE INDEX IF NOT EXISTS collection_idx ON documents (collection); + ` + if _, err := tx.ExecContext(ctx, query); err != nil { + return errors.WithStack(err) + } + + return nil + }) + if err != nil { + return errors.WithStack(err) + } + + return nil +} + +func NewDocumentStore(path string) *DocumentStore { + return &DocumentStore{ + db: nil, + path: path, + openOnce: sync.Once{}, + } +} + +func NewDocumentStoreWithDB(db *sql.DB) *DocumentStore { + return &DocumentStore{ + db: db, + path: "", + openOnce: sync.Once{}, + } +} + +var _ storage.DocumentStore = &DocumentStore{} diff --git a/pkg/storage/sqlite/document_store_test.go b/pkg/storage/sqlite/document_store_test.go new file mode 100644 index 0000000..157f918 --- /dev/null +++ b/pkg/storage/sqlite/document_store_test.go @@ -0,0 +1,25 @@ +package sqlite + +import ( + "os" + "testing" + + "forge.cadoles.com/arcad/edge/pkg/storage/testsuite" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +func TestDocumentStore(t *testing.T) { + // t.Parallel() + logger.SetLevel(logger.LevelDebug) + + file := "./testdata/documentstore_test.sqlite" + + if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) { + t.Fatalf("%+v", errors.WithStack(err)) + } + + store := NewDocumentStore(file) + + testsuite.TestDocumentStore(t, store) +} diff --git a/pkg/storage/sqlite/json.go b/pkg/storage/sqlite/json.go new file mode 100644 index 0000000..2bfc5a3 --- /dev/null +++ b/pkg/storage/sqlite/json.go @@ -0,0 +1,42 @@ +package sqlite + +import ( + "database/sql/driver" + "encoding/json" + + "github.com/pkg/errors" +) + +type JSONMap map[string]any + +func (j *JSONMap) Scan(value interface{}) error { + if value == nil { + return nil + } + + var data []byte + + switch typ := value.(type) { + case []byte: + data = typ + case string: + data = []byte(typ) + default: + return errors.Errorf("unexpected type '%T'", value) + } + + if err := json.Unmarshal(data, &j); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (j JSONMap) Value() (driver.Value, error) { + data, err := json.Marshal(j) + if err != nil { + return nil, errors.WithStack(err) + } + + return data, nil +} diff --git a/pkg/storage/sqlite/sql.go b/pkg/storage/sqlite/sql.go new file mode 100644 index 0000000..f8d1e96 --- /dev/null +++ b/pkg/storage/sqlite/sql.go @@ -0,0 +1,99 @@ +package sqlite + +import ( + "context" + "database/sql" + "sync" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +func withTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error { + var tx *sql.Tx + + tx, err := db.Begin() + if err != nil { + return errors.WithStack(err) + } + + defer func() { + if err := tx.Rollback(); err != nil { + if errors.Is(err, sql.ErrTxDone) { + return + } + + panic(errors.WithStack(err)) + } + }() + + if err = fn(tx); err != nil { + return errors.WithStack(err) + } + + if err = tx.Commit(); err != nil { + return errors.WithStack(err) + } + + return nil +} + +type getDBFunc func(ctx context.Context) (*sql.DB, error) + +func newGetDBFunc(dsn string, initFunc func(ctx context.Context, db *sql.DB) error) getDBFunc { + var ( + db *sql.DB + mutex sync.RWMutex + ) + + return func(ctx context.Context) (*sql.DB, error) { + mutex.RLock() + if db != nil { + defer mutex.RUnlock() + + return db, nil + } + + mutex.RUnlock() + + mutex.Lock() + defer mutex.Unlock() + + logger.Debug(ctx, "opening database", logger.F("dsn", dsn)) + + newDB, err := sql.Open("sqlite", dsn) + if err != nil { + return nil, errors.WithStack(err) + } + + logger.Debug(ctx, "initializing database") + + if err = initFunc(ctx, newDB); err != nil { + return nil, errors.WithStack(err) + } + + db = newDB + + return db, nil + } +} + +func newGetDBFuncFromDB(db *sql.DB, initFunc func(ctx context.Context, db *sql.DB) error) getDBFunc { + var err error + + initOnce := &sync.Once{} + + return func(ctx context.Context) (*sql.DB, error) { + initOnce.Do(func() { + logger.Debug(ctx, "initializing database") + + err = initFunc(ctx, db) + }) + + if err != nil { + return nil, errors.WithStack(err) + } + + return db, nil + } +} diff --git a/pkg/storage/sqlite/testdata/.gitignore b/pkg/storage/sqlite/testdata/.gitignore new file mode 100644 index 0000000..972b8db --- /dev/null +++ b/pkg/storage/sqlite/testdata/.gitignore @@ -0,0 +1 @@ +/*.sqlite \ No newline at end of file diff --git a/pkg/storage/testsuite/blob_store.go b/pkg/storage/testsuite/blob_store.go new file mode 100644 index 0000000..e2b4291 --- /dev/null +++ b/pkg/storage/testsuite/blob_store.go @@ -0,0 +1,14 @@ +package testsuite + +import ( + "testing" + + "forge.cadoles.com/arcad/edge/pkg/storage" +) + +func TestBlobStore(t *testing.T, store storage.BlobStore) { + t.Run("Ops", func(t *testing.T) { + // t.Parallel() + testBlobStoreOps(t, store) + }) +} diff --git a/pkg/storage/testsuite/blob_store_ops.go b/pkg/storage/testsuite/blob_store_ops.go new file mode 100644 index 0000000..6f97eb3 --- /dev/null +++ b/pkg/storage/testsuite/blob_store_ops.go @@ -0,0 +1,129 @@ +package testsuite + +import ( + "bytes" + "context" + "io" + "testing" + + "forge.cadoles.com/arcad/edge/pkg/storage" + "github.com/pkg/errors" +) + +type blobStoreTestCase struct { + Name string + Run func(ctx context.Context, store storage.BlobStore) error +} + +var blobStoreTestCases = []blobStoreTestCase{ + { + Name: "Open new bucket", + Run: func(ctx context.Context, store storage.BlobStore) error { + bucket, err := store.OpenBucket(ctx, "open-new-bucket") + if err != nil { + return errors.WithStack(err) + } + + if bucket == nil { + return errors.New("bucket should not be nil") + } + + size, err := bucket.Size(ctx) + if err != nil { + return errors.WithStack(err) + } + + if e, g := int64(0), size; e != g { + return errors.Errorf("bucket size: expected '%v', got '%v'", e, g) + } + + blobs, err := bucket.List(ctx) + if err != nil { + return errors.WithStack(err) + } + + if e, g := 0, len(blobs); e != g { + return errors.Errorf("len(blobs): expected '%v', got '%v'", e, g) + } + + if err := bucket.Close(); err != nil { + return errors.WithStack(err) + } + + return nil + }, + }, + { + Name: "Create blob", + Run: func(ctx context.Context, store storage.BlobStore) error { + bucket, err := store.OpenBucket(ctx, "create-blob") + if err != nil { + return errors.WithStack(err) + } + + blobID := storage.NewBlobID() + + writer, err := bucket.NewWriter(ctx, blobID) + if err != nil { + return errors.WithStack(err) + } + + data := []byte("foo") + + written, err := writer.Write(data) + if err != nil { + return errors.WithStack(err) + } + + if e, g := len(data), written; e != g { + return errors.Errorf("length of written data: expected '%v', got '%v'", e, g) + } + + if err := writer.Close(); err != nil { + panic(errors.WithStack(err)) + } + + reader, err := bucket.NewReader(ctx, blobID) + if err != nil { + return errors.WithStack(err) + } + + var buf bytes.Buffer + + written64, err := io.Copy(&buf, reader) + if err != nil { + return errors.WithStack(err) + } + + if e, g := int64(len(data)), written64; e != g { + return errors.Errorf("length of written data: expected '%v', got '%v'", e, g) + } + + if err := reader.Close(); err != nil { + panic(errors.WithStack(err)) + } + + if err := bucket.Close(); err != nil { + return errors.WithStack(err) + } + + return nil + }, + }, +} + +func testBlobStoreOps(t *testing.T, store storage.BlobStore) { + for _, tc := range blobStoreTestCases { + func(tc blobStoreTestCase) { + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + if err := tc.Run(ctx, store); err != nil { + t.Errorf("%+v", errors.WithStack(err)) + } + }) + }(tc) + } +} diff --git a/pkg/storage/testsuite/document_store.go b/pkg/storage/testsuite/document_store.go new file mode 100644 index 0000000..ed17b90 --- /dev/null +++ b/pkg/storage/testsuite/document_store.go @@ -0,0 +1,14 @@ +package testsuite + +import ( + "testing" + + "forge.cadoles.com/arcad/edge/pkg/storage" +) + +func TestDocumentStore(t *testing.T, store storage.DocumentStore) { + t.Run("Query", func(t *testing.T) { + // t.Parallel() + testDocumentStoreQuery(t, store) + }) +} diff --git a/pkg/storage/testsuite/document_store_query.go b/pkg/storage/testsuite/document_store_query.go new file mode 100644 index 0000000..c5ed901 --- /dev/null +++ b/pkg/storage/testsuite/document_store_query.go @@ -0,0 +1,85 @@ +package testsuite + +import ( + "context" + "testing" + + "forge.cadoles.com/arcad/edge/pkg/storage" + "forge.cadoles.com/arcad/edge/pkg/storage/filter" + "github.com/pkg/errors" +) + +type documentStoreQueryTestCase struct { + Name string + Before func(ctx context.Context, store storage.DocumentStore) error + Collection string + Filter *filter.Filter + QueryOptionsFuncs []storage.QueryOptionFunc + After func(t *testing.T, results []storage.Document, err error) +} + +var documentStoreQueryTestCases = []documentStoreQueryTestCase{ + { + Name: "Simple select", + Before: func(ctx context.Context, store storage.DocumentStore) error { + doc1 := storage.Document{ + "attr1": "Foo", + } + + if _, err := store.Upsert(ctx, "simple_select", doc1); err != nil { + return errors.WithStack(err) + } + + doc2 := storage.Document{ + "attr1": "Bar", + } + + if _, err := store.Upsert(ctx, "simple_select", doc2); err != nil { + return errors.WithStack(err) + } + + return nil + }, + Collection: "simple_select", + Filter: filter.New( + filter.NewEqOperator(map[string]interface{}{ + "attr1": "Foo", + }), + ), + After: func(t *testing.T, results []storage.Document, err error) { + if err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + + if e, g := 1, len(results); e != g { + t.Errorf("len(results): expected '%v', got '%v'", e, g) + } + + if e, g := "Foo", results[0]["attr1"]; e != g { + t.Errorf("results[0][\"Attr1\"]: expected '%v', got '%v'", e, g) + } + }, + }, +} + +func testDocumentStoreQuery(t *testing.T, store storage.DocumentStore) { + for _, tc := range documentStoreQueryTestCases { + func(tc documentStoreQueryTestCase) { + t.Run(tc.Name, func(t *testing.T) { + // t.Parallel() + + ctx := context.Background() + + if tc.Before != nil { + if err := tc.Before(ctx, store); err != nil { + t.Fatalf("%+v", errors.WithStack(err)) + } + } + + documents, err := store.Query(ctx, tc.Collection, tc.Filter, tc.QueryOptionsFuncs...) + + tc.After(t, documents, err) + }) + }(tc) + } +}